initial commit

This commit is contained in:
2025-09-11 18:24:36 +00:00
commit ba58bb60ec
16 changed files with 335 additions and 0 deletions

View 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
View 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
View 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
View 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
View 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
View 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
View 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}

View File

@@ -0,0 +1,2 @@
-r requirements.txt
pytest

59
backend/main.py Normal file
View 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
View 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
View File

@@ -0,0 +1,5 @@
fastapi
uvicorn
httpx
pyyaml
pydantic-settings

10
backend/ruff.toml Normal file
View 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
View 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()

View File

View 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."}

View 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"