# Python Web Stack Standards ## Stack - **API:** FastAPI (async) - **ORM:** async SQLAlchemy 2.0 with ORM models (not raw queries) - **Database:** MySQL 8 - **Runtime:** Docker Compose - **Reverse proxy:** Nginx - **Container management:** Portainer ## Project Layout ``` project-name/ ├── docker-compose.yml ├── .env.example # Template — always include this, never commit .env ├── .gitignore ├── README.md ├── nginx/ │ └── default.conf └── backend/ ├── Dockerfile ├── requirements.txt └── app/ ├── main.py # App entry point, router registration, startup events ├── config.py # Settings loaded from environment via pydantic-settings ├── database.py # Async SQLAlchemy engine and session factory ├── dependencies.py # Shared FastAPI dependencies (auth, db session, etc.) ├── models/ # SQLAlchemy ORM table definitions ├── schemas/ # Pydantic request/response models ├── routers/ # API endpoint handlers, one file per resource └── utils/ # Shared helpers ``` ## Configuration & Secrets - All secrets and environment-specific values go in `.env` — never hardcode them - Always provide `.env.example` with placeholder values and comments explaining each variable - Load settings via `pydantic-settings`: ```python # app/config.py from pydantic_settings import BaseSettings class Settings(BaseSettings): db_host: str db_port: int = 3306 db_name: str db_user: str db_password: str secret_key: str class Config: env_file = ".env" settings = Settings() ``` ## Database (SQLAlchemy 2.0 Async) - Use async engine and session factory - Define models with `DeclarativeBase` - One model per file in `models/` ```python # app/database.py from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession from sqlalchemy.orm import DeclarativeBase engine = create_async_engine(DATABASE_URL, echo=False) AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False) class Base(DeclarativeBase): pass ``` - Inject DB sessions via FastAPI dependencies (not global sessions) - Use `async with session.begin()` for transactions ## Models - One file per table in `models/` - Use typed columns with SQLAlchemy 2.0 `Mapped` syntax: ```python from sqlalchemy.orm import Mapped, mapped_column from app.database import Base class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(primary_key=True) username: Mapped[str] = mapped_column(unique=True) email: Mapped[str] ``` ## Schemas (Pydantic) - Separate schemas for Create, Update, and Response — don't reuse the same schema for all three - Response schemas should never expose password hashes or internal fields ```python class UserCreate(BaseModel): ... class UserResponse(BaseModel): model_config = ConfigDict(from_attributes=True) ``` ## Routers - One router file per resource (e.g., `routers/users.py`, `routers/auth.py`) - Register all routers in `main.py` with a consistent prefix and tags ## Docker Compose - Use named volumes for persistent data (database, uploads) - Always define a `healthcheck` for the database service - Use `depends_on` with `condition: service_healthy` for the backend - Keep the Nginx config in a `nginx/` folder and mount it as a volume ```yaml services: db: image: mysql:8 healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s timeout: 5s retries: 5 backend: build: ./backend depends_on: db: condition: service_healthy ``` ## Ntfy Notifications - All web apps must integrate Ntfy push notifications for admin-relevant events - Use a self-hosted Ntfy instance; URL and topic stored in `.env` — never hardcoded - Send notifications via HTTP POST using `httpx` (async) — do not add a separate Ntfy library ```python # app/utils/notify.py import httpx from app.config import settings async def notify(title: str, message: str, priority: str = "default", tags: list[str] | None = None): headers = { "Title": title, "Priority": priority, } if tags: headers["Tags"] = ",".join(tags) async with httpx.AsyncClient() as client: await client.post( f"{settings.ntfy_url}/{settings.ntfy_topic}", content=message, headers=headers, ) ``` **Required `.env` variables:** ``` NTFY_URL=https://ntfy.example.com NTFY_TOPIC=your-topic ``` **Standard events to notify on** (adapt to what the app actually does): | Event | Priority | Tags | |---|---|---| | Successful admin login | high | `warning` | | Failed admin login (threshold reached) | urgent | `rotating_light` | | New user registration | default | `busts_in_silhouette` | | User account deletion | default | `wastebasket` | | Role/permission escalation | high | `shield` | | Password reset requested | default | `key` | | Rate limit triggered | default | `no_entry` | | API key created or revoked | high | `key` | | Service startup / crash recovery | default | `white_check_mark` / `x` | | High error rate (5xx spike) | high | `rotating_light` | | Large data export initiated | default | `outbox_tray` | **Rules:** - Add Ntfy to every new web app from the start — it is checked during go-live review - Only notify on events that are genuinely useful to an admin — don't spam low-value noise - Failed notifications must not crash the app — wrap calls in `try/except` and log the error --- ## Testing - Do **not** create test files — I'll run the code directly to verify it works - If asked to write tests, use **pytest** with **pytest-asyncio**; use a separate test DB, never the real one ## Comments - Docstrings on all functions that aren't self-explanatory - Comment the **why** for non-obvious logic, not the what