built out personal preferences for AI tools
This commit is contained in:
148
preferences/python-web.md
Normal file
148
preferences/python-web.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
- Use **pytest** with **pytest-asyncio** for async tests
|
||||
- Always provide a test for each router endpoint at minimum (happy path + 1 failure case)
|
||||
- Use a separate test database — never run tests against the real DB
|
||||
- Run with: `pytest -v`
|
||||
|
||||
## Comments
|
||||
|
||||
- Docstrings on all functions that aren't self-explanatory
|
||||
- Comment the **why** for non-obvious logic, not the what
|
||||
Reference in New Issue
Block a user