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