initial commit
This commit is contained in:
29
backend/.devcontainer/devcontainer.json
Normal file
29
backend/.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,29 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/python
|
||||
{
|
||||
"name": "Python 3",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye",
|
||||
"features": {
|
||||
"ghcr.io/itsmechlark/features/redis-server:1": {}
|
||||
},
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "pip3 install --user -r dev-requirements.txt",
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"charliermarsh.ruff",
|
||||
"ms-python.debugpy"
|
||||
]
|
||||
}
|
||||
}
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
13
backend/.gitignore
vendored
Normal file
13
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
||||
.pytest_cache/
|
||||
13
backend/.vscode/extensions.json
vendored
Normal file
13
backend/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
|
||||
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
|
||||
// List of extensions which should be recommended for users of this workspace.
|
||||
"recommendations": [
|
||||
"ms-python.vscode-pylance",
|
||||
"ms-python.python",
|
||||
"ms-python.debugpy",
|
||||
"charliermarsh.ruff",
|
||||
],
|
||||
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
21
backend/.vscode/launch.json
vendored
Normal file
21
backend/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python Debugger: FastAPI",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "uvicorn",
|
||||
"args": [
|
||||
"main:app",
|
||||
"--reload",
|
||||
"--port=5000"
|
||||
],
|
||||
"jinja": true,
|
||||
"autoStartBrowser": true
|
||||
}
|
||||
]
|
||||
}
|
||||
15
backend/.vscode/settings.json
vendored
Normal file
15
backend/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"[python]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit",
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
},
|
||||
"python.testing.pytestArgs": [
|
||||
"tests"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true
|
||||
}
|
||||
33
backend/README.md
Normal file
33
backend/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# FastAPI Template
|
||||
|
||||
This sample repo contains the recommended structure for a Python FastAPI project. In this sample, we use `fastapi` to build a web application and the `pytest` to run tests.
|
||||
|
||||
For a more in-depth tutorial, see our [Fast API tutorial](https://code.visualstudio.com/docs/python/tutorial-fastapi).
|
||||
|
||||
The code in this repo aims to follow Python style guidelines as outlined in [PEP 8](https://peps.python.org/pep-0008/).
|
||||
|
||||
## Set up instructions
|
||||
|
||||
This sample makes use of Dev Containers, in order to leverage this setup, make sure you have [Docker installed](https://www.docker.com/products/docker-desktop).
|
||||
|
||||
To successfully run this example, we recommend the following VS Code extensions:
|
||||
|
||||
- [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
||||
- [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python)
|
||||
- [Python Debugger](https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy)
|
||||
- [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance)
|
||||
|
||||
In addition to these extension there a few settings that are also useful to enable. You can enable to following settings by opening the Settings editor (`Ctrl+,`) and searching for the following settings:
|
||||
|
||||
- Python > Analysis > **Type Checking Mode** : `basic`
|
||||
- Python > Analysis > Inlay Hints: **Function Return Types** : `enable`
|
||||
- Python > Analysis > Inlay Hints: **Variable Types** : `enable`
|
||||
|
||||
## Running the sample
|
||||
- Open the template folder in VS Code (**File** > **Open Folder...**)
|
||||
- Open the Command Palette in VS Code (**View > Command Palette...**) and run the **Dev Container: Reopen in Container** command.
|
||||
- Run the app using the Run and Debug view or by pressing `F5`
|
||||
- `Ctrl + click` on the URL that shows up on the terminal to open the running application
|
||||
- Test the API functionality by navigating to `/docs` URL to view the Swagger UI
|
||||
- Configure your Python test in the Test Panel or by triggering the **Python: Configure Tests** command from the Command Palette
|
||||
- Run tests in the Test Panel or by clicking the Play Button next to the individual tests in the `test_main.py` file
|
||||
33
backend/app.py
Normal file
33
backend/app.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
from models import MsgPayload
|
||||
|
||||
app = FastAPI()
|
||||
messages_list: dict[int, MsgPayload] = {}
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def root() -> dict[str, str]:
|
||||
return {"message": "Hello"}
|
||||
|
||||
|
||||
# About page route
|
||||
@app.get("/about")
|
||||
def about() -> dict[str, str]:
|
||||
return {"message": "This is the about page."}
|
||||
|
||||
|
||||
# Route to add a message
|
||||
@app.post("/messages/{msg_name}/")
|
||||
def add_msg(msg_name: str) -> dict[str, MsgPayload]:
|
||||
# Generate an ID for the item based on the highest ID in the messages_list
|
||||
msg_id = max(messages_list.keys()) + 1 if messages_list else 0
|
||||
messages_list[msg_id] = MsgPayload(msg_id=msg_id, msg_name=msg_name)
|
||||
|
||||
return {"message": messages_list[msg_id]}
|
||||
|
||||
|
||||
# Route to list all messages
|
||||
@app.get("/messages")
|
||||
def message_items() -> dict[str, dict[int, MsgPayload]]:
|
||||
return {"messages:": messages_list}
|
||||
2
backend/dev-requirements.txt
Normal file
2
backend/dev-requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
-r requirements.txt
|
||||
pytest
|
||||
59
backend/main.py
Normal file
59
backend/main.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import uvicorn
|
||||
|
||||
from settings import Settings, load_settings
|
||||
|
||||
|
||||
def merge_settings(base: Settings, overrides: dict[str, Any]) -> Settings:
|
||||
base_dict = base.model_dump() if hasattr(base, "model_dump") else {k: v for k, v in vars(base).items() if not k.startswith("_")}
|
||||
clean_overrides = {k: v for k, v in overrides.items() if v is not None}
|
||||
return Settings(**{**base_dict, **clean_overrides})
|
||||
|
||||
|
||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description="Start FastAPI app with configurable settings")
|
||||
p.add_argument("--config", "-c", help="Path to YAML config file (overrides env/.env)", default=None)
|
||||
p.add_argument("--host", help="Host to bind", default=None)
|
||||
p.add_argument("--port", type=int, help="Port to bind", default=None)
|
||||
p.add_argument("--workers", type=int, help="Number of workers (uvicorn)", default=None)
|
||||
p.add_argument("--log-level", help="Log level for uvicorn", default=None)
|
||||
p.add_argument("--reload", action="store_true", help="Enable reload (development)")
|
||||
return p.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> None:
|
||||
args = parse_args(argv)
|
||||
|
||||
if args.config:
|
||||
os.environ["CONFIG_FILE"] = args.config
|
||||
|
||||
base = load_settings()
|
||||
overrides: dict[str, Any] = {
|
||||
"host": args.host,
|
||||
"port": args.port,
|
||||
"workers": args.workers,
|
||||
"log_level": args.log_level,
|
||||
}
|
||||
final_settings = merge_settings(base, overrides)
|
||||
|
||||
uvicorn_kwargs = {
|
||||
"app": "app:app",
|
||||
"host": final_settings.host,
|
||||
"port": int(final_settings.port),
|
||||
"log_level": final_settings.log_level,
|
||||
"workers": int(final_settings.workers) if final_settings.workers and final_settings.workers > 0 else None,
|
||||
"reload": args.reload,
|
||||
}
|
||||
uvicorn_kwargs = {k: v for k, v in uvicorn_kwargs.items() if v is not None}
|
||||
|
||||
print("Starting app with settings:", final_settings.model_dump() if hasattr(final_settings, "model_dump") else vars(final_settings))
|
||||
uvicorn.run(**uvicorn_kwargs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
7
backend/models.py
Normal file
7
backend/models.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class MsgPayload(BaseModel):
|
||||
msg_id: Optional[int]
|
||||
msg_name: str
|
||||
5
backend/requirements.txt
Normal file
5
backend/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
httpx
|
||||
pyyaml
|
||||
pydantic-settings
|
||||
10
backend/ruff.toml
Normal file
10
backend/ruff.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
target-version = "py39"
|
||||
line-length = 144
|
||||
|
||||
[lint]
|
||||
select = ["ALL"]
|
||||
fixable = ["UP034", "I001"]
|
||||
ignore = ["T201", "D", "ANN101", "TD002", "TD003"]
|
||||
|
||||
[lint.extend-per-file-ignores]
|
||||
"test*.py" = ["S101"]
|
||||
28
backend/settings.py
Normal file
28
backend/settings.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from pydantic import ConfigDict
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
host: str = "0.0.0.0" # noqa: S104
|
||||
port: int = 8000
|
||||
workers: int = 1
|
||||
log_level: str = "info"
|
||||
|
||||
model_config = ConfigDict(env_file=".env", env_file_encoding="utf-8")
|
||||
|
||||
|
||||
def load_settings() -> Settings:
|
||||
cfg_path = os.getenv("CONFIG_FILE")
|
||||
if cfg_path and Path(cfg_path).exists():
|
||||
with Path(cfg_path).open(encoding="utf-8") as f:
|
||||
data: dict[str, Any] = yaml.safe_load(f) or {}
|
||||
return Settings(**data)
|
||||
return Settings()
|
||||
|
||||
|
||||
settings = load_settings()
|
||||
0
backend/tests/__init__.py
Normal file
0
backend/tests/__init__.py
Normal file
22
backend/tests/test_main.py
Normal file
22
backend/tests/test_main.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app import app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
with TestClient(app) as client:
|
||||
yield client
|
||||
|
||||
|
||||
def test_home_route(client):
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Hello"}
|
||||
|
||||
|
||||
def test_about_route(client):
|
||||
response = client.get("/about")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "This is the about page."}
|
||||
45
backend/tests/test_settings.py
Normal file
45
backend/tests/test_settings.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from settings import load_settings
|
||||
|
||||
|
||||
def test_default_settings(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.delenv("CONFIG_FILE", raising=False)
|
||||
for k in ("HOST", "PORT", "WORKERS", "LOG_LEVEL"):
|
||||
monkeypatch.delenv(k, raising=False)
|
||||
|
||||
s = load_settings()
|
||||
assert s.host == "0.0.0.0" # noqa: S104
|
||||
assert s.port == 8000 # noqa: PLR2004
|
||||
assert s.workers == 1
|
||||
assert s.log_level == "info"
|
||||
|
||||
|
||||
def test_env_overrides(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("HOST", "127.0.0.1")
|
||||
monkeypatch.setenv("PORT", "9000")
|
||||
monkeypatch.setenv("WORKERS", "3")
|
||||
monkeypatch.setenv("LOG_LEVEL", "debug")
|
||||
monkeypatch.delenv("CONFIG_FILE", raising=False)
|
||||
|
||||
s = load_settings()
|
||||
assert s.host == "127.0.0.1"
|
||||
assert s.port == 9000 # noqa: PLR2004
|
||||
assert s.workers == 3 # noqa: PLR2004
|
||||
assert s.log_level == "debug"
|
||||
|
||||
|
||||
def test_yaml_config_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
cfg = tmp_path / "config.yaml"
|
||||
cfg.write_text("host: 10.0.0.5\nport: 8088\nworkers: 5\nlog_level: debug\n")
|
||||
monkeypatch.setenv("CONFIG_FILE", str(cfg))
|
||||
for k in ("HOST", "PORT", "WORKERS", "LOG_LEVEL"):
|
||||
monkeypatch.delenv(k, raising=False)
|
||||
|
||||
s = load_settings()
|
||||
assert s.host == "10.0.0.5"
|
||||
assert s.port == 8088 # noqa: PLR2004
|
||||
assert s.workers == 5 # noqa: PLR2004
|
||||
assert s.log_level == "debug"
|
||||
Reference in New Issue
Block a user