This commit is contained in:
John Lancaster
2026-06-18 22:06:40 -05:00
parent 6c5fda9c3a
commit e78383be1f
24 changed files with 0 additions and 0 deletions
@@ -0,0 +1,240 @@
---
name: fastapi-async-sqlalchemy-modernization
description: 'Create a step-by-step modernization plan for an existing FastAPI app using SQLAlchemy async patterns, context managers, and AsyncExitStack. Use when: planning migration from legacy DB setup, standardizing async engine/session lifecycles, defining transaction boundaries, and aligning with SQLAlchemy 2.x best practices.'
argument-hint: 'What is your current FastAPI + SQLAlchemy setup (sync/async driver, session pattern, lifespan usage, and deployment model)?'
---
# FastAPI Async SQLAlchemy Modernization Plan
Create an implementation-ready plan that brings an existing FastAPI application in line with modern async SQLAlchemy practices, with explicit resource lifecycles and deterministic cleanup using async context managers and AsyncExitStack.
Primary targets: PostgreSQL with asyncpg and SQLite with aiosqlite.
## When to Use
- Existing FastAPI app has ad hoc database setup or mixed sync/async access.
- Session management is inconsistent across routes/services.
- Lifespan startup and shutdown work is spread across globals and side effects.
- Team needs a migration plan first, not immediate large-scale rewrites.
## Outcome
Produce a practical modernization plan with:
- Current-state gap assessment.
- Target architecture for engine/session/transaction lifecycle.
- Branch-based migration path (low-risk staged rollout).
- Quality gates and completion checks.
- Risks, rollback strategy, and test plan.
## Top-Level Concepts
Use these concepts as the planning backbone:
1. Engine lifecycle and ownership:
One AsyncEngine per process for each DB URL, created once and disposed explicitly when the app lifecycle ends.
See: [references/engine.md](references/engine.md)
2. Session factory and scope:
Use async_sessionmaker for configuration; create one AsyncSession per request or unit-of-work, never shared across concurrent tasks.
See: [references/session.md](references/session.md)
3. Transaction boundaries:
Prefer context-managed begin blocks for write units and explicit read-only sessions for queries.
See: [references/transactions.md](references/transactions.md)
4. Lifespan composition:
Compose startup/shutdown resources with AsyncExitStack so cleanup is deterministic and ordered.
See: [references/engine.md](references/engine.md)
5. Dependency injection:
Provide sessions via FastAPI dependencies with async generators/context managers, not globals.
See: [references/session.md](references/session.md)
6. Implicit I/O control in ORM:
Avoid accidental lazy loads; use explicit eager-loading/refresh strategies for asyncio safety.
See: [references/implicit_io.md](references/implicit_io.md)
7. Observability and resilience:
Add pool/connection settings, logging, timeout, and health checks as first-class plan items.
See: [references/observability.md](references/observability.md)
### Concept Reference Map
| Concept | Reference |
|---|---|
| Engine lifecycle and ownership | [references/engine.md](references/engine.md) |
| Session factory and scope | [references/session.md](references/session.md) |
| Transaction boundaries | [references/transactions.md](references/transactions.md) |
| Lifespan composition | [references/engine.md](references/engine.md) |
| Dependency injection | [references/session.md](references/session.md) |
| Implicit I/O control in ORM | [references/implicit_io.md](references/implicit_io.md) |
| Observability and resilience | [references/observability.md](references/observability.md) |
## Decision Points
Use these branching decisions before proposing migration steps.
| Decision | Branch A | Branch B |
|---|---|---|
| DB driver | Already async driver (e.g. asyncpg, aiosqlite): modernize in place | Sync driver: plan driver migration first |
| ORM usage | Already ORM 2.x style (`select`, `session.execute`) | Legacy Query API: add compatibility stage and refactor incrementally |
| Session scope | Request-scoped already | Global/shared sessions found: prioritize session-scope fix first |
| Lifespan | Existing FastAPI lifespan hook | No lifespan hook: introduce lifespan before broader DB changes |
| Concurrency | Background jobs/tasks use DB | No background DB use |
| Transaction style | Explicit context-managed transactions | Implicit/autobegin side effects |
## Procedure
### Step 0: Audit Current State
Inventory the app and write a concise gap list.
- Engine creation location(s) and count.
- Driver URL(s) and async compatibility.
- Session creation patterns in routes/services/background tasks.
- Transaction handling style (explicit begin/commit/rollback vs implicit).
- Lifespan startup/shutdown and cleanup behavior.
- ORM loading patterns that may trigger implicit I/O.
Completion check: every DB touchpoint is mapped to its engine, session, and transaction source.
### Step 1: Define the Target Runtime Model
Define one canonical model to migrate toward.
- Create AsyncEngine once per process.
- Configure async_sessionmaker once.
- Use per-request AsyncSession dependency.
- Keep one AsyncSession per concurrent task.
- Use context-managed transactions for writes.
Completion check: architecture diagram can explain where engine/session are created, used, and closed.
### Step 2: Plan Engine Modernization
Plan engine creation and pool behavior.
- Use `create_async_engine()` with async dialect URL.
- Standardize pool settings and pre-ping strategy where relevant.
- Decide isolation level strategy at engine level (avoid ad hoc per-operation switching unless justified).
- Define explicit disposal policy for short-lived scopes and tests.
Completion check: engine configuration is centralized and no per-request engine creation remains.
### Step 3: Plan Session Lifecycle Modernization
Define session factory and request dependency pattern.
- Build `async_sessionmaker(engine, expire_on_commit=False)` unless a strict reason says otherwise.
- Provide session via dependency that yields exactly one AsyncSession.
- Explicitly prohibit sharing a single AsyncSession across concurrent tasks.
- Prefer direct dependency passing over async_scoped_session for new designs.
Completion check: all route/service entry points receive a session from one canonical dependency.
### Step 4: Plan Transaction Demarcation
Establish consistent write and read behavior.
- Writes: `async with session.begin(): ...` for atomic units.
- Reads: execute in managed session context with explicit loader options.
- Nested/SAVEPOINT use only where required; call out backend caveats.
- Define rollback behavior for service-layer exceptions.
Completion check: every mutating use case has a declared transaction boundary.
### Step 5: Compose Lifespan with AsyncExitStack
Use async context composition as the preferred orchestration pattern.
```python
from contextlib import AsyncExitStack, asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
async with AsyncExitStack() as stack:
# Compose resources in acquisition order; cleanup is automatic in reverse order.
engine = create_async_engine(settings.database_url)
stack.push_async_callback(engine.dispose)
session_factory = async_sessionmaker(engine, expire_on_commit=False)
app.state.session_factory = session_factory
# Add other async resources with stack.enter_async_context(...) as needed.
yield
```
Planning rules:
- Register every acquired resource with AsyncExitStack at acquisition time.
- Prefer `enter_async_context()` for resources that already expose async context managers.
- Prefer `push_async_callback()` for async cleanup callables.
- Keep resource ownership in lifespan, not in route handlers.
Completion check: startup/shutdown ordering is explicit and deterministic.
### Step 6: Prevent Implicit ORM I/O Under Asyncio (Advisory Mode)
Plan for explicit loading behavior, but treat this as progressive guidance rather than a hard gate.
- Recommend eager-loading strategies (for example selectin-style loading) where relationship access is required.
- For lazy/deferred attributes, define explicit awaitable or refresh paths on high-risk and high-traffic paths first.
- Document model-level defaults and known exceptions so teams can migrate incrementally.
Completion check: critical request paths have explicit loading plans; non-critical paths have tracked follow-up items.
### Step 7: Testing and Verification Plan
Create modernization quality gates.
- Unit tests for session dependency and transaction behavior.
- Integration tests for commit/rollback semantics.
- Concurrency tests confirming one-session-per-task behavior.
- Lifespan tests verifying cleanup calls and ordering.
- Health/readiness tests including DB connectivity checks.
Completion check: all quality gates pass under the target async configuration.
### Step 8: Rollout Strategy
Plan low-risk migration phases.
1. Introduce centralized engine/session factory and lifespan orchestration.
2. Migrate read paths to new session dependency.
3. Migrate write paths to explicit transaction blocks.
4. Remove legacy globals/helpers and dead code.
5. Enable stricter linting/review checks for forbidden patterns.
Completion check: no legacy session/engine creation path remains in production code.
## Quality Criteria
A plan is complete only when it includes:
- Clear current vs target architecture.
- Branch decisions with rationale.
- Explicit context-manager patterns for resource ownership.
- AsyncExitStack composition strategy.
- Transaction policy and exception behavior.
- Concrete tests and rollout checkpoints.
- A documented advisory backlog for non-critical implicit I/O improvements.
## Anti-Patterns to Flag
- Creating engines inside request handlers.
- Sharing one AsyncSession across concurrent tasks.
- Implicit commit/rollback behavior with unclear ownership.
- Global mutable session state.
- Lifespan cleanup that depends on implicit garbage collection.
## Output Contract
Return the plan as:
1. Current-state gap summary.
2. Target architecture summary.
3. Phased migration checklist with branch notes.
4. Risk register and rollback approach.
5. Verification matrix (tests + operational checks).
## References
- SQLAlchemy engine/connections: https://docs.sqlalchemy.org/en/21/core/connections.html
- SQLAlchemy asyncio extension: https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html
- Python async context managers and AsyncExitStack: https://docs.python.org/3/library/contextlib.html
@@ -0,0 +1,133 @@
# Async SQLAlchemy Engine
Source:
- https://docs.sqlalchemy.org/en/21/core/connections.html
- https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html
- https://docs.sqlalchemy.org/en/21/core/pooling.html#pooling-multiprocessing
- https://fastapi.tiangolo.com/advanced/events/
---
## Engine Ownership Model
Create one async engine per process per database URL and keep it for the app lifetime.
- SQLAlchemy guidance: the engine is intended as a long-lived, concurrent registry over pooled DB connections, not a per-request object.
- In FastAPI, app startup and shutdown ownership belongs in lifespan.
- Use `FastAPI(lifespan=...)` (not startup/shutdown events) for modern lifecycle wiring.
Practical rule:
- Exactly one `create_async_engine(...)` call in app bootstrap code.
- Zero `create_async_engine(...)` calls in request handlers.
---
## Canonical Lifespan Pattern (AsyncExitStack)
Use `@asynccontextmanager` + `AsyncExitStack` to make teardown deterministic and composable.
```python
from contextlib import AsyncExitStack, asynccontextmanager
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
@asynccontextmanager
async def lifespan(app: FastAPI):
async with AsyncExitStack() as stack:
engine: AsyncEngine = create_async_engine(
app.state.settings.database_url,
pool_pre_ping=True,
# Optional examples:
# echo=app.state.settings.sql_echo,
# pool_size=10,
# max_overflow=20,
)
app.state.engine = engine
# Ensure engine disposal always runs at shutdown.
stack.push_async_callback(engine.dispose)
yield
app = FastAPI(lifespan=lifespan)
```
Why this pattern:
- FastAPI executes code before `yield` at startup and after `yield` at shutdown.
- `AsyncExitStack` lets you register multiple async cleanups in one place while preserving order.
- Explicit disposal (directly awaited or via `AsyncExitStack` callback) avoids event-loop-closed warnings when objects fall out of scope.
---
## Driver URLs (Project Requirement: asyncpg + aiosqlite)
Use SQLAlchemy async driver URLs:
- PostgreSQL: `postgresql+asyncpg://user:pass@host:5432/dbname`
- SQLite: `sqlite+aiosqlite:///./app.db`
Notes:
- Do not mix sync drivers (for example `psycopg2`) with `create_async_engine()`.
- Keep URL construction centralized in settings/config, not in feature modules.
---
## Pooling Defaults and Tuning
Default behavior is usually correct first:
- Async engines use async-compatible pooling (`AsyncAdaptedQueuePool`) by default.
- Start with defaults, then tune from observed load (`pool_size`, `max_overflow`, `pool_timeout`, `pool_recycle`).
- Enable `pool_pre_ping=True` for safer stale-connection handling in long-running services.
When to switch pool strategy:
- `NullPool` if you explicitly need no pooling (special environments, some tests, or strict cross-loop constraints).
- Keep in mind this increases connect/disconnect churn.
---
## Disposal Semantics
`engine.dispose()` replaces/disposes the pool, but only checked-in connections are immediately closed.
Rules:
- Dispose when the app is shutting down.
- Dispose before reusing an engine across event loops.
- In forked child-process initialization, use `engine.dispose(close=False)` (sync API guidance) so child processes do not touch parent-held connections.
Avoid relying on garbage collection for engine cleanup in async code.
---
## Event Loop and Process Boundaries
Do not share pooled connections across boundaries:
- Multiple event loops: do not reuse the same pooled async engine across loops unless you intentionally disable pooling (`NullPool`) or dispose before handoff.
- Multiprocessing/fork: pooled connections must not be inherited for active use across process boundaries.
This prevents broken socket state and cross-process connection corruption.
---
## What Not to Do
- Create an engine inside every request dependency.
- Create/dispose engines inside repository methods.
- Keep engine creation as a hidden side effect of import-time module globals.
- Use deprecated FastAPI startup/shutdown events together with lifespan.
---
## Engine Design Checklist
- One engine per process per DB URL.
- Engine created in lifespan startup.
- Engine disposed in lifespan shutdown.
- Async driver URL matches backend (`asyncpg` or `aiosqlite`).
- Pooling strategy is explicit for non-default needs.
- No request-path engine creation.
@@ -0,0 +1,106 @@
# Preventing Implicit ORM I/O (Asyncio)
Source:
- https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html#preventing-implicit-io-when-using-asyncsession
- https://docs.sqlalchemy.org/en/21/orm/queryguide/relationships.html
Status: adopted
Decision level: advisory
Applies to: api-runtime, workers, tests
Last reviewed: 2026-06-17
---
## Purpose
Minimize unexpected database round-trips caused by attribute access in async ORM code.
In asyncio applications, hidden lazy loads are easy to miss and can produce runtime surprises. This guide defines explicit-loading defaults and progressive enforcement practices.
---
## Scope and Non-Goals
- In scope: relationship loading strategy, post-commit attribute access, explicit refresh/awaitable access patterns.
- Out of scope: full ORM performance tuning and domain-specific query architecture.
---
## Rules
- Prefer explicit eager loading for data required by endpoint/service outputs.
- Avoid relying on implicit lazy-load behavior in request critical paths.
- Keep `expire_on_commit=False` unless strict expiration behavior is intentionally required.
- Use explicit refresh or awaitable-attribute access when loading deferred state is necessary.
---
## Recommended Patterns
### Pattern A: Eager-load what you need
```python
from sqlalchemy import select
from sqlalchemy.orm import selectinload
stmt = select(User).options(selectinload(User.roles))
users = (await session.scalars(stmt)).all()
```
### Pattern B: Explicit refresh of named attributes
```python
user = await session.get(User, user_id)
await session.refresh(user, ["roles"])
```
### Pattern C: Awaitable attribute access where needed
```python
# Requires AsyncAttrs mixin on mapped base or class.
roles = await user.awaitable_attrs.roles
```
---
## Practical Enforcement Model
Use phased enforcement:
1. High-traffic and latency-sensitive routes: enforce explicit eager loading.
2. Background tasks and less critical paths: track and progressively tighten.
3. Add review checks to prevent newly introduced implicit-load hotspots.
This keeps modernization pragmatic while reducing hidden I/O over time.
---
## Anti-Patterns
- Returning ORM objects from handlers and triggering lazy loads during serialization.
- Assuming post-commit attribute access will always be loaded without explicit strategy.
- Relying on broad expiration + implicit reload behavior in async request flows.
- Enabling relationship patterns that hide SQL behavior in critical code paths.
---
## Operational Checks
- Endpoint query blocks define loader options for returned related data.
- Critical handlers do not depend on incidental lazy loads.
- Known exceptions are documented with rationale and follow-up items.
---
## Testing Checks
- Integration tests cover endpoints that return related objects.
- Tests verify expected data is present without hidden secondary query surprises.
- Regression tests exist for routes previously affected by implicit-load failures.
---
## Migration Notes
- Start advisory: target high-risk paths first.
- As coverage improves, elevate selected rules to mandatory in code review policy.
@@ -0,0 +1,32 @@
# FastAPI Async SQLAlchemy References Index
Purpose: concept registry for modernization guidance used by this skill.
---
## Concepts
| Concept | File | Status | Decision Level | Owner | Last Reviewed |
|---|---|---|---|---|---|
| Engine lifecycle and ownership | [engine.md](engine.md) | adopted | mandatory | platform/backend | 2026-06-17 |
| Session factory and scope | [session.md](session.md) | adopted | mandatory | platform/backend | 2026-06-17 |
| Transaction boundaries | [transactions.md](transactions.md) | adopted | mandatory | platform/backend | 2026-06-17 |
| Implicit ORM I/O under asyncio | [implicit_io.md](implicit_io.md) | adopted | advisory | platform/backend | 2026-06-17 |
| Observability and resilience | [observability.md](observability.md) | adopted | mandatory | platform/backend | 2026-06-17 |
---
## How to Use This Folder
- `SKILL.md` defines the planning workflow and migration procedure.
- Each concept doc defines policy-level guidance for one concern.
- Use the template in [template.md](template.md) for new concept docs.
- Keep references source-linked and implementation snippets minimal.
---
## Update Rules
- If a PR changes database lifecycle/session/ORM loading behavior, update the relevant concept file.
- Keep `Status`, `Decision Level`, and `Last Reviewed` current.
- Use `advisory` only when incremental rollout is intended; use `mandatory` for required runtime policy.
@@ -0,0 +1,113 @@
# DB Observability and Resilience
Source:
- https://docs.sqlalchemy.org/en/21/core/pooling.html
- https://docs.sqlalchemy.org/en/21/core/engines.html
- https://docs.sqlalchemy.org/en/21/core/events.html
- https://fastapi.tiangolo.com/advanced/events/
Status: adopted
Decision level: mandatory
Applies to: api-runtime, workers, tests
Last reviewed: 2026-06-17
---
## Purpose
Define baseline observability and resilience practices for DB connectivity in async FastAPI + SQLAlchemy apps.
Goals:
- detect and recover from stale/disconnected connections,
- expose useful diagnostics for pool/engine behavior,
- make readiness/liveness signals meaningful.
---
## Scope and Non-Goals
- In scope: pool health, connection liveness, SQL/pool logging hygiene, readiness checks, failure handling.
- Out of scope: full APM stack design and vendor-specific monitoring platform setup.
---
## Rules
- Enable connection liveness strategy (`pool_pre_ping=True`) for long-running services.
- Keep DB health checks out of liveness; include dependency checks in readiness.
- Centralize engine options and logging configuration.
- Avoid noisy SQL debug logging in production defaults.
- Treat disconnect handling as a first-class test scenario.
---
## Recommended Baseline
```python
engine = create_async_engine(
settings.database_url,
pool_pre_ping=True,
# Tune only from measured behavior:
# pool_size=10,
# max_overflow=20,
# pool_timeout=30,
# pool_recycle=1800,
)
```
Operational guidance:
- `pool_pre_ping=True` for stale-connection resilience.
- Introduce `pool_recycle` where backend/network idle timeout behavior warrants it.
- Use structured app logs with request correlation and error context.
---
## Health Endpoint Policy
- `/healthz`: process is alive; no DB call required.
- `/readyz`: application can currently serve traffic; include DB connectivity verification.
Readiness checks should be lightweight and bounded (timeouts), not heavy diagnostic queries.
---
## Failure Handling Guidance
- Handle transient disconnects with pool invalidation/reconnect semantics.
- Keep one failed request from cascading into broad app instability.
- Capture and log contextual DB errors with enough metadata for debugging.
---
## Anti-Patterns
- No readiness check for DB-dependent services.
- Permanent debug SQL echo in production.
- Per-handler ad hoc pool settings.
- Assuming disconnect events are too rare to test.
---
## Operational Checks
- Engine creation is centralized and configured once.
- Liveness/readiness behavior is documented and validated.
- Pool settings are explicit, versioned, and reviewed.
- DB-related errors produce actionable logs.
---
## Testing Checks
- Readiness endpoint test covers healthy and unhealthy DB states.
- Integration test simulates disconnect/reconnect behavior.
- Load/concurrency tests validate pool behavior under stress.
---
## Migration Notes
- Start with resilient defaults (`pool_pre_ping`) and simple health policy.
- Add deeper metrics/event hooks incrementally once baseline reliability is in place.
@@ -0,0 +1,140 @@
# Async SQLAlchemy Session Management
Source:
- https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html
- https://docs.sqlalchemy.org/en/21/orm/session_basics.html
- https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/
Status: adopted
Decision level: mandatory
Applies to: api-runtime, workers, tests
Last reviewed: 2026-06-17
---
## Purpose
Define one canonical session model for FastAPI + SQLAlchemy asyncio:
- configure one shared session factory,
- create one AsyncSession per request or per unit-of-work,
- never share one AsyncSession across concurrent tasks.
---
## Scope and Non-Goals
- In scope: session factory creation, FastAPI dependency wiring, request/task scoping, transaction demarcation.
- Out of scope: ORM model design, query optimization strategy, schema migration tooling.
---
## Rules
- Create `async_sessionmaker` once from app-owned AsyncEngine.
- Use a fresh AsyncSession for each request or explicit unit-of-work.
- Do not share AsyncSession across `asyncio.gather()` or parallel tasks.
- Prefer direct dependency injection over global scoped-session patterns in new code.
- Use explicit transaction boundaries (`async with session.begin():`) for writes.
---
## Canonical FastAPI Dependency Pattern
```python
from collections.abc import AsyncIterator
from fastapi import Depends, Request
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
def get_session_factory(request: Request) -> async_sessionmaker[AsyncSession]:
return request.app.state.session_factory
async def get_db_session(
session_factory: async_sessionmaker[AsyncSession] = Depends(get_session_factory),
) -> AsyncIterator[AsyncSession]:
async with session_factory() as session:
yield session
```
Route usage:
```python
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter()
@router.post("/items")
async def create_item(session: AsyncSession = Depends(get_db_session)) -> dict:
async with session.begin():
# write operations here
...
return {"status": "ok"}
```
---
## Configuration Guidance
Typical session factory setup:
```python
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
session_factory = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
```
Notes:
- `expire_on_commit=False` is commonly preferred in asyncio applications to reduce accidental post-commit reload behavior.
- `AsyncSession.refresh()` is preferred over broad expiration patterns when state refresh is needed.
---
## Concurrency Rules
- One session per concurrent task.
- If work fans out into parallel tasks, each task receives its own AsyncSession.
- Pass sessions explicitly to service functions; avoid mutable global session state.
---
## Anti-Patterns
- A singleton/global AsyncSession reused across requests.
- Sharing one AsyncSession across parallel tasks.
- Hidden session creation in lower repository helpers with no caller control.
- Mixing commit/rollback ownership across layers without a declared boundary.
---
## Operational Checks
- Exactly one `async_sessionmaker` is registered in app lifecycle.
- Request handlers receive sessions from one canonical dependency.
- No code path creates AsyncSession in module import side effects.
- Background jobs and API handlers each create task-local sessions.
---
## Testing Checks
- Dependency override exists for test session factory.
- Rollback behavior is verified for failed write units.
- Parallel-task tests verify no shared AsyncSession instances.
- Lifespan tests confirm session factory is initialized and teardown-safe.
---
## Migration Notes
- If current code uses global/shared sessions, fix scope first before refactoring query style.
- If legacy sync patterns are present, keep session boundary rules stable while migrating incrementally.
@@ -0,0 +1,63 @@
# <Concept Title>
Source:
- <primary source url>
- <secondary source url>
Status: draft|adopted|deprecated
Decision level: advisory|mandatory
Applies to: api-runtime|workers|tests
Last reviewed: YYYY-MM-DD
---
## Purpose
Describe what this concept governs and why it exists.
## Scope and Non-Goals
- In scope:
- Out of scope:
---
## Rules
- Rule 1
- Rule 2
---
## Recommended Pattern
```python
# minimal example
```
---
## Anti-Patterns
- Anti-pattern 1
- Anti-pattern 2
---
## Operational Checks
- Check 1
- Check 2
---
## Testing Checks
- Test 1
- Test 2
---
## Migration Notes
- Staged rollout notes and compatibility caveats.
@@ -0,0 +1,110 @@
# Async Transaction Boundaries
Source:
- https://docs.sqlalchemy.org/en/21/orm/session_transaction.html
- https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html
- https://docs.sqlalchemy.org/en/21/core/connections.html
Status: adopted
Decision level: mandatory
Applies to: api-runtime, workers, tests
Last reviewed: 2026-06-17
---
## Purpose
Define consistent transaction demarcation for async SQLAlchemy so write behavior is predictable, rollback semantics are clear, and concurrent request flows remain safe.
---
## Scope and Non-Goals
- In scope: transaction ownership, write/read policy, exception and rollback behavior, nested transaction guidance.
- Out of scope: business-domain validation rules and cross-service distributed transactions.
---
## Rules
- Every mutating use case must run inside an explicit transaction boundary.
- Prefer `async with session.begin():` for write units.
- Keep transaction ownership at service/use-case boundary, not deep in helper internals.
- Read paths should not auto-upgrade into hidden write behavior.
- On exception in a transaction block, rely on rollback semantics and propagate or map exceptions intentionally.
---
## Recommended Patterns
### Pattern A: Single write unit
```python
async def create_order(session: AsyncSession, payload: OrderIn) -> Order:
async with session.begin():
order = Order(...)
session.add(order)
# additional writes...
return order
```
### Pattern B: Explicit read flow
```python
async def get_order(session: AsyncSession, order_id: UUID) -> Order | None:
stmt = select(Order).where(Order.id == order_id)
return await session.scalar(stmt)
```
### Pattern C: Nested transaction (only when required)
```python
async with session.begin():
# outer transaction
async with session.begin_nested():
# savepoint-scoped operation
...
```
Use nested transactions only when partial failure semantics are explicitly required.
---
## Exception and Rollback Policy
- Write block fails: transaction context rolls back.
- Caller decides whether to translate exception (for example to domain/API errors).
- Do not swallow DB exceptions silently; map or re-raise intentionally.
---
## Anti-Patterns
- Multiple commits scattered across one logical use case.
- Helper functions that commit/rollback without caller awareness.
- Mixing implicit and explicit transaction styles in confusing ways.
- Using savepoints as a default pattern rather than a targeted tool.
---
## Operational Checks
- All mutating service functions declare one clear transaction boundary.
- No repository/helper performs hidden commit calls.
- Transaction style is consistent across handlers and workers.
---
## Testing Checks
- Success path test verifies expected durable writes.
- Failure path test verifies rollback behavior.
- Tests cover concurrency-sensitive write flows.
- Savepoint usage (if present) has dedicated behavior tests.
---
## Migration Notes
- First stabilize session scope, then normalize transaction ownership.
- Replace ad hoc commit patterns incrementally with bounded write units.
+425
View File
@@ -0,0 +1,425 @@
---
name: fastapi-uv-docker
description: 'Audit and migrate an existing Python project to best practices for a cloud-native ASGI FastAPI app managed with uv and run with uvicorn in Docker. Use when: conforming a project to production standards, setting up src layout, configuring pyproject.toml, writing multi-stage Dockerfiles, wiring lifespan and settings, adding health endpoints, enforcing non-root container user, migrating from requirements.txt to uv.'
argument-hint: 'What is the current state of the project (bare Python, requirements.txt, pip, etc.)?'
---
# FastAPI Project Best Practices
Bring an existing Python project into full conformance with cloud-native best practices: **FastAPI + uv + uvicorn + Docker**. This skill audits the current state, produces a gap list, and walks through each conformance area in priority order.
## When to Use
- Migrating an existing app from `pip`/`requirements.txt` to `uv`
- Conforming a FastAPI app to src-layout and app-factory structure
- Writing a production-grade Dockerfile with multi-stage builds
- Wiring Pydantic settings from environment variables
- Adding health endpoints and graceful lifespan shutdown
- Enforcing non-root container user and proper signal handling
- Setting up `docker-compose.yml` for local dev and CI
## Progressive Loading References
Load these references only when needed:
- FastAPI patterns and app structure: [./references/fastapi-best-practices.md](./references/fastapi-best-practices.md)
- uv project layout and dependency management: [./references/uv-project-layout.md](./references/uv-project-layout.md)
- uvicorn CLI settings reference: [./references/uvicorn-settings.md](./references/uvicorn-settings.md)
- Docker and cloud-native patterns: [./references/docker-cloud-native.md](./references/docker-cloud-native.md)
---
## Procedure
### Step 0: Audit the Project
Before making changes, map the current state across six areas. Produce a short gap list for each.
| Area | Check |
|------|-------|
| **Project manager** | Is `uv` used? Is `pyproject.toml` present? Is `uv.lock` committed? |
| **Package layout** | Is a `src/` layout used? Is the package installable? |
| **App structure** | Is `create_app()` factory used? Is lifespan wired? Are routers registered via `APIRouter`? |
| **Configuration** | Are settings loaded from env via Pydantic `BaseSettings`? Are secrets out of code? |
| **Container** | Is there a `Dockerfile`? Multi-stage? Non-root user? `.dockerignore` present? |
| **Cloud-native** | Is there a `/healthz` endpoint? Graceful shutdown? Structured logs? |
Load [./references/fastapi-best-practices.md](./references/fastapi-best-practices.md) for structure rules.
Load [./references/uv-project-layout.md](./references/uv-project-layout.md) for uv migration rules.
Load [./references/uvicorn-settings.md](./references/uvicorn-settings.md) for uvicorn CLI reference.
Completion check: You can name every gap before touching any file.
---
### Step 1: Migrate to uv and Establish pyproject.toml
**If the project uses `requirements.txt` / `setup.py` / `setup.cfg` / `pip`:**
1. Initialize uv if not present: `uv init` (or `uv init --lib` for importable package).
2. Import existing requirements: `uv add -r requirements.txt`.
3. Remove `requirements.txt`, `setup.py`, `setup.cfg`, and any `Pipfile`.
4. Ensure `.python-version` is committed with the target Python version.
5. Commit `uv.lock` — it is the source of truth for reproducible installs.
6. Add `.venv` to `.gitignore` and `.dockerignore`.
**Canonical `pyproject.toml` shape:**
```toml
[project]
name = "my-app"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"fastapi[standard]>=0.115",
"uvicorn[standard]>=0.34",
"pydantic-settings>=2.0",
]
[project.scripts]
my-app = "my_app.main:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.uv]
dev-dependencies = [
"pytest>=8",
"httpx>=0.27",
"pytest-asyncio>=0.24",
]
[tool.hatch.build.targets.wheel]
packages = ["src/my_app"]
```
**Commands:**
```bash
uv add fastapi[standard] uvicorn[standard] pydantic-settings
uv add --dev pytest httpx pytest-asyncio
uv sync # creates .venv and installs all deps
uv run pytest # run tests via uv
```
Completion check: `uv run python -c "import my_app"` succeeds.
---
### Step 2: Establish src Layout
Move the package under `src/` to prevent import confusion between installed and local code.
```
.
├── pyproject.toml
├── uv.lock
├── .python-version
├── .env.example
├── README.md
├── src/
│ └── my_app/
│ ├── __init__.py
│ ├── main.py # create_app() + entry point
│ ├── config.py # Pydantic BaseSettings
│ ├── lifespan.py # @asynccontextmanager lifespan
│ ├── api/
│ │ ├── __init__.py
│ │ ├── health.py # GET /healthz
│ │ └── v1/
│ │ └── __init__.py
│ └── services/
│ └── __init__.py
├── tests/
│ ├── conftest.py
│ └── test_health.py
├── Dockerfile
├── .dockerignore
└── docker-compose.yml
```
Completion check: `uv run python -m my_app` starts the server.
---
### Step 3: Wire the FastAPI App Factory
Load [./references/fastapi-best-practices.md](./references/fastapi-best-practices.md) for the full patterns. Key rules:
**`src/my_app/main.py`:**
```python
from contextlib import asynccontextmanager
from fastapi import FastAPI
from my_app.api.health import router as health_router
from my_app.config import Settings
def create_app(settings: Settings | None = None) -> FastAPI:
if settings is None:
settings = Settings()
@asynccontextmanager
async def lifespan(app: FastAPI):
# startup: open DB pools, load models, etc.
app.state.settings = settings
yield
# shutdown: close connections
app = FastAPI(
title="My App",
lifespan=lifespan,
docs_url="/docs" if settings.debug else None,
redoc_url=None,
)
app.include_router(health_router)
return app
app = create_app()
```
**`src/my_app/api/health.py`:**
```python
from fastapi import APIRouter
from fastapi.responses import JSONResponse
router = APIRouter(tags=["ops"])
@router.get("/healthz", include_in_schema=False)
async def health() -> JSONResponse:
return JSONResponse({"status": "ok"})
```
**`src/my_app/config.py`:**
```python
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
debug: bool = False
host: str = "0.0.0.0"
port: int = 8000
log_level: str = "info"
```
Completion check: `uv run uvicorn my_app.main:app --reload` starts with no import errors.
---
### Step 4: uvicorn Production Configuration
Load [./references/uvicorn-settings.md](./references/uvicorn-settings.md) for the full settings reference.
**Never** configure uvicorn inside application code. Pass all settings via CLI or environment variables (`UVICORN_*` prefix).
```bash
# Development — reload only; never in production
uv run uvicorn my_app.main:app \
--reload \
--host 127.0.0.1 \
--port 8000 \
--log-level debug
# Production — single process (orchestrator handles replication)
uv run uvicorn my_app.main:app \
--host 0.0.0.0 \
--port 8000 \
--workers 1 \
--loop auto \
--http auto \
--log-level info \
--proxy-headers \
--forwarded-allow-ips '*' \
--timeout-graceful-shutdown 30
```
**Key flags for production:**
| Flag | Value | Reason |
|------|-------|--------|
| `--host 0.0.0.0` | Required in containers | Bind to all interfaces, not just loopback |
| `--workers 1` | Kubernetes/Cloud Run | Orchestrator replicates containers |
| `--loop auto` | Default | Uses `uvloop` when available (install `uvicorn[standard]`) |
| `--http auto` | Default | Uses `httptools` when available |
| `--proxy-headers` | Behind any proxy | Trusts `X-Forwarded-For`, `X-Forwarded-Proto` |
| `--forwarded-allow-ips '*'` | Container/K8s | Trusts proxy headers from all IPs (safe when inside a trusted network) |
| `--timeout-graceful-shutdown 30` | Prod | Seconds to wait before force-closing requests on shutdown |
| `--no-access-log` | High-traffic prod | Disable per-request logs if using structured app-level logging |
**Environment variable equivalents** (useful in `docker-compose.yml` / K8s manifests):
```bash
UVICORN_HOST=0.0.0.0
UVICORN_PORT=8000
UVICORN_WORKERS=1
UVICORN_LOG_LEVEL=info
UVICORN_PROXY_HEADERS=true
UVICORN_FORWARDED_ALLOW_IPS=*
```
**`--reload` and `--workers` are mutually exclusive** — never combine them.
**When to use `--workers > 1`:** Only for Docker Compose on a single host where orchestrator-level replication is not available. For Kubernetes / Cloud Run / Fargate: always `--workers 1` and scale via replicas — this gives predictable per-container memory and cleaner crash isolation.
Completion check: `curl http://localhost:8000/healthz` returns `{"status":"ok"}`.
---
### Step 5: Write the Dockerfile
Load [./references/docker-cloud-native.md](./references/docker-cloud-native.md) for the full template and cloud-native rules. Key requirements:
- Multi-stage build: `builder` stage installs deps; `runtime` stage is slim.
- Pin uv version (copy from official image, not `latest`).
- Use `uv sync --locked --no-editable` to install into `.venv`.
- Set `ENV PATH="/app/.venv/bin:$PATH"` — do **not** use `uv run` in production `CMD`.
- Run as non-root user.
- Use `CMD` exec form, never shell form.
- Add `HEALTHCHECK`.
**Canonical Dockerfile:**
```dockerfile
# syntax=docker/dockerfile:1
# ── builder ──────────────────────────────────────────────────────────────────
FROM python:3.12-slim-bookworm AS builder
# Pin uv version for reproducibility
COPY --from=ghcr.io/astral-sh/uv:0.5.27 /uv /uvx /bin/
WORKDIR /app
# Install dependencies first (cache layer)
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --locked --no-install-project --no-editable
# Copy source and install project
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-editable
# ── runtime ───────────────────────────────────────────────────────────────────
FROM python:3.12-slim-bookworm AS runtime
# Non-root user
RUN groupadd --system --gid 1001 appgroup && \
useradd --system --uid 1001 --gid appgroup --no-log-init appuser
WORKDIR /app
# Copy only the virtual environment (not source code)
COPY --from=builder --chown=appuser:appgroup /app/.venv /app/.venv
# Copy application source
COPY --chown=appuser:appgroup src/ /app/src/
ENV PATH="/app/.venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/healthz')"
# Exec form — required for graceful shutdown / lifespan events
CMD ["uvicorn", "my_app.main:app", \
"--host", "0.0.0.0", \
"--port", "8000", \
"--workers", "1", \
"--proxy-headers"]
```
Completion check: `docker build -t my-app . && docker run --rm -p 8000:8000 my-app` serves `/healthz`.
---
### Step 6: Write .dockerignore and docker-compose.yml
**`.dockerignore`:**
```
.venv/
.git/
.gitignore
.env
*.pyc
__pycache__/
.pytest_cache/
dist/
*.egg-info/
README.md
```
**`docker-compose.yml` (local dev):**
```yaml
services:
app:
build: .
ports:
- "8000:8000"
environment:
- DEBUG=true
- LOG_LEVEL=debug
env_file:
- .env
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: ./pyproject.toml
- action: rebuild
path: ./uv.lock
```
Completion check: `docker compose up` starts the app; `docker compose watch` enables hot reload.
---
### Step 7: Cloud-Native Checklist
Run this final checklist before shipping:
- [ ] `GET /healthz` returns 200 with no auth required
- [ ] App reads all config from environment (`Settings` with no hardcoded values)
- [ ] `.env` is in `.gitignore` and `.dockerignore`; `.env.example` is committed
- [ ] `uv.lock` is committed
- [ ] `.python-version` is committed
- [ ] Dockerfile uses non-root user (`USER appuser`)
- [ ] `CMD` uses exec form (list, not string)
- [ ] `--proxy-headers` is set in the uvicorn `CMD` if behind a proxy
- [ ] `PYTHONUNBUFFERED=1` is set (logs flush immediately)
- [ ] `EXPOSE` declares the correct port
- [ ] `HEALTHCHECK` is defined
- [ ] Multi-stage build — final image contains no build tools or uv binary
- [ ] `.venv` is in `.dockerignore`
- [ ] No secrets hardcoded in `Dockerfile`, `pyproject.toml`, or source
- [ ] `uv sync --locked` in CI (fail if lock is stale)
- [ ] Tests pass via `uv run pytest`
---
## Anti-patterns to Fix
| Anti-pattern | Correct approach |
|---|---|
| `requirements.txt` | Use `uv` with `pyproject.toml` and `uv.lock` |
| `pip install` in Dockerfile | `uv sync --locked` |
| `ENV SECRET_KEY=abc123` in Dockerfile | Inject at runtime via env; never bake secrets |
| Shell form `CMD uvicorn ...` | Exec form `CMD ["uvicorn", ...]` |
| `FROM tiangolo/uvicorn-gunicorn-fastapi` | Build from scratch with `python:3.x-slim` |
| Multiple workers inside K8s container | `--workers 1`; scale via replicas |
| Running as root in container | `USER appuser` with explicit UID 1001 |
| Startup/shutdown in `@app.on_event` | Use `@asynccontextmanager` lifespan |
| Config loaded from `.env` directly in code | Pydantic `BaseSettings` with `env_file` |
@@ -0,0 +1,345 @@
# Docker and Cloud-Native Patterns
Source: https://docs.docker.com/build/building/best-practices/ | https://docs.astral.sh/uv/guides/integration/docker/ | https://fastapi.tiangolo.com/deployment/docker/ | https://uvicorn.dev/deployment/
---
## Canonical Multi-Stage Dockerfile (uv + FastAPI + uvicorn)
```dockerfile
# syntax=docker/dockerfile:1
# ── builder stage ─────────────────────────────────────────────────────────────
FROM python:3.12-slim-bookworm AS builder
# Pin uv version — never use :latest in production
COPY --from=ghcr.io/astral-sh/uv:0.5.27 /uv /uvx /bin/
WORKDIR /app
# Layer: install dependencies only (cached until pyproject.toml or uv.lock changes)
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --locked --no-install-project --no-dev --no-editable
# Layer: copy source and install project
COPY src/ /app/src/
COPY pyproject.toml uv.lock /app/
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev --no-editable
# ── runtime stage ─────────────────────────────────────────────────────────────
FROM python:3.12-slim-bookworm AS runtime
# Non-root user with explicit UID/GID
RUN groupadd --system --gid 1001 appgroup && \
useradd --system --uid 1001 --gid appgroup --no-log-init --home /app appuser
WORKDIR /app
# Copy only the venv (uv binary stays in builder)
COPY --from=builder --chown=appuser:appgroup /app/.venv /app/.venv
# Copy app source
COPY --from=builder --chown=appuser:appgroup /app/src /app/src
# Activate the venv via PATH — do not rely on `uv run` at runtime
ENV PATH="/app/.venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
UV_PYTHON_DOWNLOADS=0
USER appuser
EXPOSE 8000
# Liveness probe — no extra tools needed; uses stdlib urllib
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
CMD python -c \
"import urllib.request; urllib.request.urlopen('http://localhost:8000/healthz')" \
|| exit 1
# Exec form (NOT shell form) — required for SIGTERM → graceful shutdown
CMD ["uvicorn", "my_app.main:app", \
"--host", "0.0.0.0", \
"--port", "8000", \
"--workers", "1", \
"--proxy-headers", \
"--forwarded-allow-ips", "*"]
```
---
## .dockerignore
```
# Python artifacts
.venv/
__pycache__/
*.pyc
*.pyo
.pytest_cache/
.mypy_cache/
.ruff_cache/
dist/
*.egg-info/
# Environment and secrets
.env
.env.*
!.env.example
# Git
.git/
.gitignore
# Docs and local tooling
README.md
docs/
*.md
```
**Always add `.venv/` to `.dockerignore`** — it is platform-specific and will cause subtle failures if copied into the image.
---
## docker-compose.yml (Local Development)
```yaml
services:
app:
build:
context: .
target: runtime # build only up to the runtime stage
ports:
- "8000:8000"
environment:
DEBUG: "true"
LOG_LEVEL: "debug"
env_file:
- .env
restart: unless-stopped
# Docker Compose Watch — hot reload without rebuilding image
develop:
watch:
- action: sync
path: ./src
target: /app/src
ignore:
- .venv/
- __pycache__/
- action: rebuild
path: ./pyproject.toml
- action: rebuild
path: ./uv.lock
```
Run with:
```bash
docker compose up # start normally
docker compose watch # start with live sync
docker compose up --build # force rebuild
```
---
## Key Dockerfile Rules
### Multi-Stage Builds
- **Stage 1 (`builder`)**: Has uv, build tools, compiles `.pyc` files if needed.
- **Stage 2 (`runtime`)**: Minimal; contains only the venv and source. No uv, no build tools.
- Final image size: typically 150200 MB for a FastAPI app (vs 500+ MB with a single stage).
### Layer Caching Strategy
Copy files in order of change frequency (least → most):
```
1. uv.lock + pyproject.toml → install deps (cached for days)
2. Source code → install project (invalidated on every code change)
```
This means dependency installation is only re-run when `uv.lock` or `pyproject.toml` changes.
### CMD Exec Form (Critical)
```dockerfile
# ✅ Exec form — process receives SIGTERM directly → graceful shutdown
CMD ["uvicorn", "my_app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# ❌ Shell form — process is a child of /bin/sh → SIGTERM goes to shell, not uvicorn
CMD uvicorn my_app.main:app --host 0.0.0.0 --port 8000
```
Shell form breaks graceful shutdown and FastAPI lifespan shutdown events.
### Non-Root User
```dockerfile
RUN groupadd --system --gid 1001 appgroup && \
useradd --system --uid 1001 --gid appgroup --no-log-init --home /app appuser
USER appuser
```
- Use `--system` for service accounts (no shell, no home by default).
- Use `--no-log-init` to avoid `/var/log/faillog` disk exhaustion (Go runtime bug in older kernels).
- Use explicit UID/GID (1001) — deterministic, scanners can reason about it.
- Never run as UID 0 (root) in production.
### HEALTHCHECK
```dockerfile
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
CMD python -c \
"import urllib.request; urllib.request.urlopen('http://localhost:8000/healthz')" \
|| exit 1
```
- `--start-period`: grace period on startup before health checks begin.
- `--retries`: mark as unhealthy only after N consecutive failures.
- Uses stdlib `urllib` — no extra tools needed, not even `curl`.
---
## Cloud-Native Twelve-Factor Principles Applied
### Factor III — Config (Environment Variables)
- All config comes from environment variables, never from code.
- `pydantic-settings` reads from env automatically.
- Provide `.env.example` with documentation; never commit `.env`.
```bash
# Runtime injection
docker run -e DATABASE_URL="postgresql://..." -e SECRET_KEY="..." my-app
# or via env_file in compose / Kubernetes Secret
```
### Factor XI — Logs (Treat as Event Streams)
- Set `PYTHONUNBUFFERED=1` — logs are flushed immediately to stdout/stderr.
- Never write logs to files inside the container.
- Configure uvicorn to log JSON in production:
```python
# In create_app() lifespan or a logging setup module
import logging
import json
class JSONFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
return json.dumps({
"level": record.levelname,
"message": record.getMessage(),
"logger": record.name,
})
```
Or use `structlog` with the JSON renderer for production.
### Factor IX — Disposability (Fast Startup, Graceful Shutdown)
- FastAPI lifespan handles startup/shutdown.
- Uvicorn forwards `SIGTERM` to the Python process when using exec form `CMD`.
- The process should be fully ready to serve requests within 10 seconds.
- Kubernetes `terminationGracePeriodSeconds` should be >= your timeout.
---
## Kubernetes Readiness / Liveness Probes
Expose two endpoints:
```python
@router.get("/healthz", include_in_schema=False) # liveness
async def health():
return {"status": "ok"}
@router.get("/readyz", include_in_schema=False) # readiness
async def readiness(request: Request):
# Check DB, cache, etc.
return {"status": "ready"}
```
Kubernetes manifest snippet:
```yaml
livenessProbe:
httpGet:
path: /healthz
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /readyz
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
```
---
## Scaling: Workers vs. Replicas
| Deployment | Workers setting | Reasoning |
|---|---|---|
| Kubernetes / Cloud Run | `--workers 1` | Orchestrator handles replication; 1 process per container for predictable memory |
| Docker Compose (single host) | `--workers 4` | No external replication; use CPU cores |
| Local dev | `--reload` (single process) | Hot reload only works with single process |
**Never use `--reload` in production.**
---
## Base Image Selection
| Use case | Recommended base |
|---|---|
| Standard production | `python:3.12-slim-bookworm` |
| Smallest possible image | `python:3.12-alpine3.20` (musl; watch for C extension compat) |
| uv-managed Python | `ghcr.io/astral-sh/uv:python3.12-bookworm-slim` |
| Security-hardened | `cgr.dev/chainguard/python:latest` (distroless) |
**Do not use:**
- `python:latest` — unpinned, breaks reproducibility
- `tiangolo/uvicorn-gunicorn-fastapi` — deprecated by FastAPI team
- Full `python:3.x` (non-slim) — 300+ MB unnecessary overhead
---
## Build Optimizations
```bash
# Enable BuildKit (default in Docker >= 23)
export DOCKER_BUILDKIT=1
# Build with cache mount (fastest for repeated local builds)
docker build -t my-app .
# CI: force fresh base image + no layer cache
docker build --pull --no-cache -t my-app .
# Multi-platform build (for ARM deployment from x86 CI)
docker buildx build --platform linux/amd64,linux/arm64 -t my-app .
```
---
## Security Hardening Checklist
- [ ] Non-root user with explicit UID/GID
- [ ] Read-only filesystem where possible (`--read-only` docker run flag or `securityContext.readOnlyRootFilesystem: true` in K8s)
- [ ] No secrets in `ENV` Dockerfile instructions
- [ ] Minimal base image (slim/alpine)
- [ ] Multi-stage build (no build tools in runtime image)
- [ ] Pin base image and uv version (not `:latest`)
- [ ] `.dockerignore` excludes `.env`, `.git`, `.venv`
- [ ] `EXPOSE` only the port actually used
- [ ] Regular base image updates in CI (`docker build --pull`)
@@ -0,0 +1,238 @@
# FastAPI Best Practices
Source: https://fastapi.tiangolo.com/deployment/ | https://fastapi.tiangolo.com/advanced/events/
---
## App Factory Pattern
Always use `create_app()` — it makes the app testable (inject a test `Settings`) and avoids module-level side effects.
```python
# src/my_app/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from my_app.config import Settings
from my_app.api import health, v1
def create_app(settings: Settings | None = None) -> FastAPI:
if settings is None:
settings = Settings()
@asynccontextmanager
async def lifespan(app: FastAPI):
# --- startup ---
app.state.settings = settings
# Open DB pool, warm caches, etc.
yield
# --- shutdown ---
# Close DB pool, flush buffers, etc.
app = FastAPI(
title=settings.app_name,
version="1.0.0",
lifespan=lifespan,
docs_url="/docs" if settings.debug else None,
redoc_url=None,
)
app.include_router(health.router)
app.include_router(v1.router, prefix="/api/v1")
return app
# Module-level instance for uvicorn
app = create_app()
```
**Never use `@app.on_event("startup")` / `@app.on_event("shutdown")`** — these are deprecated. The `asynccontextmanager` lifespan is the canonical approach since FastAPI 0.95.
---
## Router Organization
```
src/my_app/api/
├── __init__.py
├── health.py # GET /healthz — no auth, no versioning
├── deps.py # Shared Depends() factories
└── v1/
├── __init__.py # APIRouter with prefix="/v1"
├── items.py
└── users.py
```
Each router file:
```python
from fastapi import APIRouter
router = APIRouter(prefix="/items", tags=["items"])
@router.get("/")
async def list_items() -> list[Item]:
...
```
Root registration:
```python
from fastapi import APIRouter
from my_app.api.v1 import items, users
router = APIRouter()
router.include_router(items.router)
router.include_router(users.router)
```
---
## Pydantic Settings (Configuration)
```python
# src/my_app/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
from functools import lru_cache
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
app_name: str = "My App"
debug: bool = False
host: str = "0.0.0.0"
port: int = 8000
log_level: str = "info"
# Add DB URL, secret keys, etc. here — never hardcode
# database_url: str # required — will raise if missing
@lru_cache
def get_settings() -> Settings:
return Settings()
```
Use `lru_cache` so the `.env` file is read once. In tests, override with:
```python
from my_app.config import get_settings
from my_app.main import create_app
app = create_app(settings=Settings(debug=True, database_url="sqlite://"))
```
**Never** import `settings` as a module-level singleton — it prevents test overrides.
---
## Health Endpoint
```python
# src/my_app/api/health.py
from fastapi import APIRouter
from fastapi.responses import JSONResponse
router = APIRouter(tags=["ops"])
@router.get("/healthz", include_in_schema=False)
async def health() -> JSONResponse:
"""Kubernetes/Docker liveness probe. No auth. No versioning."""
return JSONResponse({"status": "ok"})
@router.get("/readyz", include_in_schema=False)
async def readiness(request: Request) -> JSONResponse:
"""Readiness probe — check DB connectivity, etc."""
# Example: await request.app.state.db.execute("SELECT 1")
return JSONResponse({"status": "ready"})
```
Rules:
- No authentication required on `/healthz` and `/readyz`.
- `/healthz` — liveness: can the process respond?
- `/readyz` — readiness: are dependencies available?
- Keep them on the root path (not `/api/v1/healthz`).
---
## Dependency Injection
Use `Depends()` to share resources from app state:
```python
# src/my_app/api/deps.py
from fastapi import Depends, Request
from my_app.config import Settings
def get_settings(request: Request) -> Settings:
return request.app.state.settings
SettingsDep = Annotated[Settings, Depends(get_settings)]
```
In route handlers:
```python
@router.get("/config")
async def show_config(settings: SettingsDep) -> dict:
return {"debug": settings.debug}
```
---
## Error Handling
Register a global exception handler for unhandled errors:
```python
from fastapi import Request
from fastapi.responses import JSONResponse
@app.exception_handler(Exception)
async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse:
# Log the exception — never expose internal details to clients
logger.exception("Unhandled error", exc_info=exc)
return JSONResponse(status_code=500, content={"detail": "Internal server error"})
```
---
## CORS
Add CORS middleware only when needed (e.g., browser clients from a different origin):
```python
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins, # list from config, never ["*"] in prod
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
```
---
## Response Models
Always declare `response_model` or return type annotations — FastAPI uses them for OpenAPI docs and response validation:
```python
@router.post("/items/", response_model=ItemOut, status_code=201)
async def create_item(item: ItemIn) -> ItemOut:
...
```
Use separate `In` / `Out` models when the write shape differs from the read shape (e.g., password hashing, computed fields).
---
## Security Checklist
- Never expose `docs_url` in production (set `docs_url=None` when `not settings.debug`).
- Validate all user input with Pydantic models — never pass raw request data to DB queries.
- Use `SecretStr` for passwords and API keys in `Settings`.
- Apply authentication globally via middleware or `app.include_router(..., dependencies=[Depends(verify_token)])`.
- Add `X-Content-Type-Options: nosniff` and `X-Frame-Options: DENY` headers in production middleware.
@@ -0,0 +1,251 @@
# uv Project Layout and Dependency Management
Source: https://docs.astral.sh/uv/guides/projects/ | https://docs.astral.sh/uv/concepts/projects/layout/ | https://docs.astral.sh/uv/guides/integration/docker/
---
## Core Files
| File | Purpose | Commit? |
|------|---------|---------|
| `pyproject.toml` | Project metadata, deps, tool config | Yes |
| `uv.lock` | Exact resolved versions, cross-platform | **Yes** |
| `.python-version` | Default Python version for the project | Yes |
| `.venv/` | Local virtual environment | No (`.gitignore`) |
**`uv.lock` must be committed.** It is the source of truth for reproducible installs in CI and Docker. Never edit it by hand.
---
## Essential Commands
```bash
# Initialize a new project (app, not library)
uv init --app
# Initialize a library (installable package with src layout)
uv init --lib
# Add a runtime dependency
uv add fastapi[standard]
# Add multiple dependencies at once
uv add uvicorn[standard] pydantic-settings
# Add dev-only dependency
uv add --dev pytest httpx pytest-asyncio ruff mypy
# Remove a dependency
uv remove requests
# Upgrade a specific package (keeps rest of lockfile intact)
uv lock --upgrade-package fastapi
# Upgrade all packages
uv lock --upgrade
# Sync env to lockfile (install/remove as needed)
uv sync
# Sync without dev deps (e.g., in CI or Docker)
uv sync --no-dev
# Sync and assert lockfile is up-to-date (for CI / Docker)
uv sync --locked
# Run a command in the project environment
uv run pytest
uv run uvicorn my_app.main:app --reload
# Run a one-off command without installing anything permanently
uv run --with httpx python -c "import httpx; print(httpx.__version__)"
```
---
## pyproject.toml Reference
```toml
[project]
name = "my-app"
version = "0.1.0"
description = "Production FastAPI service"
requires-python = ">=3.12"
license = { text = "MIT" }
readme = "README.md"
dependencies = [
"fastapi[standard]>=0.115",
"uvicorn[standard]>=0.34",
"pydantic-settings>=2.0",
]
[project.scripts]
# Creates `my-app` CLI entry point when installed
my-app = "my_app.main:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/my_app"]
[tool.uv]
dev-dependencies = [
"pytest>=8",
"pytest-asyncio>=0.24",
"httpx>=0.27", # needed for FastAPI TestClient
"ruff>=0.6",
"mypy>=1.11",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
strict-markers = true
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "S"]
[tool.mypy]
strict = true
```
---
## src Layout with uv
Use `uv init --lib` or set up manually:
```
my-app/
├── pyproject.toml
├── uv.lock
├── .python-version # e.g., "3.12"
├── .env.example
├── README.md
├── src/
│ └── my_app/
│ └── __init__.py
└── tests/
└── conftest.py
```
The `src/` layout prevents the local directory from shadowing the installed package, which would otherwise cause silent test failures when testing the installed version.
**hatchling config for src layout:**
```toml
[tool.hatch.build.targets.wheel]
packages = ["src/my_app"]
```
---
## Migration from pip / requirements.txt
```bash
# 1. Initialize uv in an existing project
uv init --no-workspace # if already has pyproject.toml, skip this
# 2. Import from requirements.txt
uv add -r requirements.txt
# 3. Import dev requirements
uv add --dev -r requirements-dev.txt
# 4. Verify lockfile was created
cat uv.lock | head -20
# 5. Clean up old files
rm requirements.txt requirements-dev.txt setup.py setup.cfg Pipfile Pipfile.lock
# 6. Add .venv to .gitignore
echo ".venv/" >> .gitignore
```
---
## uv in Docker
The canonical pattern uses `--mount=type=cache` for fast rebuilds and `--no-install-project` for layer separation:
```dockerfile
# Copy uv binary (pin the version)
COPY --from=ghcr.io/astral-sh/uv:0.5.27 /uv /uvx /bin/
WORKDIR /app
# Layer 1: install dependencies (changes rarely → cached aggressively)
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --locked --no-install-project --no-editable
# Layer 2: copy source and install project (changes frequently)
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-editable
```
**Key flags:**
| Flag | Effect |
|------|--------|
| `--locked` | Fail if `uv.lock` is out of date with `pyproject.toml` |
| `--no-install-project` | Install deps but not the project itself (layer separation) |
| `--no-editable` | Install in non-editable mode (copy code into `.venv`, not symlink) |
| `--no-dev` | Skip dev dependencies (use in production images) |
| `--compile-bytecode` | Pre-compile `.pyc` files (faster startup, larger image) |
**After syncing, activate the venv via PATH (not `uv run`) in production:**
```dockerfile
ENV PATH="/app/.venv/bin:$PATH"
CMD ["uvicorn", "my_app.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
Using `CMD ["uv", "run", "uvicorn", ...]` in production is fine but adds a small overhead and requires uv to be present in the final image.
---
## CI/CD with uv
```yaml
# GitHub Actions example
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "0.5.27" # pin for reproducibility
- name: Sync dependencies
run: uv sync --locked
- name: Run tests
run: uv run pytest --tb=short
- name: Lint
run: uv run ruff check .
- name: Type check
run: uv run mypy src/
```
---
## .gitignore Entries for uv Projects
```
.venv/
__pycache__/
*.pyc
.env
dist/
*.egg-info/
.mypy_cache/
.ruff_cache/
.pytest_cache/
```
**Do NOT ignore `uv.lock`** — it must be committed.
@@ -0,0 +1,192 @@
# uvicorn Settings Reference
Source: https://uvicorn.dev/settings/ | https://uvicorn.dev/deployment/
---
## Configuration Methods
Three equivalent approaches (CLI takes precedence over env vars):
```bash
# 1. CLI flags
uvicorn main:app --host 0.0.0.0 --port 8000
# 2. UVICORN_* environment variables
export UVICORN_HOST=0.0.0.0
export UVICORN_PORT=8000
uvicorn main:app
# 3. Programmatic (dev/test only)
uvicorn.run("main:app", host="0.0.0.0", port=8000)
```
**Note:** `UVICORN_*` env vars cannot be used from within an `--env-file`. The `--env-file` flag is for the ASGI *application's* config, not uvicorn's own config.
---
## All Settings by Category
### Socket Binding
| Flag | Default | Notes |
|------|---------|-------|
| `--host <str>` | `127.0.0.1` | Use `0.0.0.0` in containers to bind all interfaces |
| `--port <int>` | `8000` | Use `0` to auto-pick an available port |
| `--uds <path>` | — | UNIX domain socket path (use behind Nginx) |
| `--fd <int>` | — | Inherit socket from file descriptor (use with Supervisor) |
### Production
| Flag | Default | Notes |
|------|---------|-------|
| `--workers <int>` | `1` (or `$WEB_CONCURRENCY`) | **Mutually exclusive with `--reload`** |
| `--env-file <path>` | — | Env file for the *application* (not uvicorn itself) |
| `--timeout-worker-healthcheck <int>` | `5` | Seconds before killing a stuck worker |
### Logging
| Flag | Default | Notes |
|------|---------|-------|
| `--log-level <str>` | `info` | `critical`, `error`, `warning`, `info`, `debug`, `trace` |
| `--log-config <path>` | — | `.json` or `.yaml` for `dictConfig()`; other formats use `fileConfig()` |
| `--no-access-log` | — | Disable access log without changing log level |
| `--use-colors / --no-use-colors` | auto | Force color on/off in log output |
### Implementation
| Flag | Default | Notes |
|------|---------|-------|
| `--loop <str>` | `auto` | `auto`, `asyncio`, `uvloop`. `uvloop` requires `uvicorn[standard]` |
| `--http <str>` | `auto` | `auto`, `h11`, `httptools`. `httptools` requires `uvicorn[standard]` |
| `--ws <str>` | `auto` | `auto`, `none`, `websockets`, `websockets-sansio`, `wsproto` |
| `--lifespan <str>` | `auto` | `auto`, `on`, `off` |
| `--ws-max-size <int>` | `16777216` | WebSocket max message size in bytes (16 MB) |
| `--ws-ping-interval <float>` | `20.0` | WebSocket ping interval in seconds |
| `--ws-ping-timeout <float>` | `20.0` | WebSocket ping timeout in seconds |
### HTTP / Proxy Headers
| Flag | Default | Notes |
|------|---------|-------|
| `--proxy-headers` | enabled | Trust `X-Forwarded-For`, `X-Forwarded-Proto` from trusted IPs |
| `--no-proxy-headers` | — | Disable proxy header trust entirely |
| `--forwarded-allow-ips <list>` | `127.0.0.1` | Comma-separated IPs/networks/literals to trust. Use `'*'` to trust all (safe in containers behind a trusted LB). **Security risk if exposed directly to internet.** |
| `--root-path <str>` | `""` | ASGI `root_path` for apps mounted below a URL prefix |
| `--server-header / --no-server-header` | enabled | Include/suppress `Server` response header |
| `--date-header / --no-date-header` | enabled | Include/suppress `Date` response header |
| `--header <name:value>` | — | Add custom default response headers (repeatable) |
### Resource Limits
| Flag | Default | Notes |
|------|---------|-------|
| `--limit-concurrency <int>` | — | Max concurrent connections/tasks; returns HTTP 503 above this |
| `--limit-max-requests <int>` | — | Restart worker after N requests (limits memory leak accumulation) |
| `--limit-max-requests-jitter <int>` | `0` | Random jitter added to `--limit-max-requests` to stagger worker restarts |
| `--backlog <int>` | `2048` | Max queued connections under high load |
### Timeouts
| Flag | Default | Notes |
|------|---------|-------|
| `--timeout-keep-alive <int>` | `5` | Close keep-alive connections after N seconds of inactivity |
| `--timeout-graceful-shutdown <int>` | — | Seconds to wait for in-flight requests to complete on SIGTERM before force-closing |
### Development
| Flag | Default | Notes |
|------|---------|-------|
| `--reload` | `False` | Auto-reload on file changes. **Never use in production.** Mutually exclusive with `--workers`. |
| `--reload-dir <path>` | `.` | Directory to watch for changes (repeatable) |
| `--reload-delay <float>` | `0.25` | Seconds between reload checks |
| `--reload-include <glob>` | `*.py` | Patterns to include in watch (requires `watchfiles`) |
| `--reload-exclude <glob>` | `.*, .py[cod], ...` | Patterns to exclude from watch (requires `watchfiles`) |
### Application
| Flag | Default | Notes |
|------|---------|-------|
| `--factory` | — | Treat `APP` as a `() -> ASGI app` callable (app factory pattern) |
| `--app-dir <path>` | `.` | Add to `PYTHONPATH` when resolving `APP` |
| `--reset-contextvars` | `False` | Run each request in a fresh `contextvars.Context` (asyncio only; workaround for a CPython context-leak bug) |
---
## Recommended Production CMD
```bash
uvicorn my_app.main:app \
--host 0.0.0.0 \
--port 8000 \
--workers 1 \
--loop auto \
--http auto \
--log-level info \
--proxy-headers \
--forwarded-allow-ips '*' \
--timeout-graceful-shutdown 30
```
In a Dockerfile (exec form):
```dockerfile
CMD ["uvicorn", "my_app.main:app",
"--host", "0.0.0.0",
"--port", "8000",
"--workers", "1",
"--proxy-headers",
"--forwarded-allow-ips", "*",
"--timeout-graceful-shutdown", "30"]
```
---
## Process Manager Options
### Built-in multi-worker (Docker Compose / single host)
```bash
uvicorn my_app.main:app --workers 4
```
The built-in manager spawns workers, monitors their health, and auto-restarts crashed workers. Signal support:
- `SIGHUP` — rolling graceful restart (deploy new code without dropping requests)
- `SIGTTIN` — add one worker
- `SIGTTOU` — remove one worker
### Behind Nginx (UNIX socket)
```bash
uvicorn my_app.main:app --uds /tmp/uvicorn.sock --proxy-headers
```
Nginx config headers to set:
```nginx
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
```
---
## `uvicorn[standard]` vs bare `uvicorn`
| Package | Extras included |
|---------|----------------|
| `uvicorn` | Pure Python h11 HTTP, asyncio event loop |
| `uvicorn[standard]` | `uvloop` (faster event loop), `httptools` (faster HTTP parser), `watchfiles` (better reload), `websockets`, `PyYAML` (for `--log-config`) |
Use `uvicorn[standard]` in all environments. The `[standard]` extras are also included when you install `fastapi[standard]`.
---
## Anti-patterns
| Anti-pattern | Fix |
|---|---|
| `--reload` in Dockerfile CMD | Remove it — `--reload` is dev-only |
| `--loop uvloop` explicitly | Use `--loop auto` — it selects uvloop automatically when available |
| `--http h11` explicitly in prod | Use `--http auto` — it selects httptools when available |
| `uvicorn.run()` at module level (no `if __name__ == '__main__':`) | Breaks multiprocessing workers; always guard it |
| Shell form `CMD uvicorn ...` | Exec form `CMD ["uvicorn", ...]` — required for SIGTERM to reach uvicorn |
@@ -0,0 +1,134 @@
---
name: nicegui-ui-customization
description: 'Design and implement production NiceGUI UIs with reusable components, Tailwind-first styling, event-driven interactions, and troubleshooting for uploads, state, and static assets. Use when building or refactoring NiceGUI pages and interaction flows.'
argument-hint: 'What UI outcome should this workflow produce?'
---
# NiceGUI UI Customization Workflow
Create, style, and ship production NiceGUI UI flows with a repeatable process. The workflow keeps structure in Python, favors Tailwind and Quasar APIs for styling, and uses event-driven interaction patterns over ad-hoc polling.
## When To Use
- Building a new NiceGUI page or dashboard
- Refactoring a page into reusable components
- Adding file upload, form submission, live status, or background-job UX
- Troubleshooting race conditions, stale assets, or inconsistent state updates
## Target Outcome
Deliver a responsive, accessible UI flow that:
- keeps clear boundaries between page adapters, reusable components, and services
- uses Tailwind-first styling with minimal custom CSS
- updates UI through events and bindings
- has validation, user feedback, and failure handling
- passes a production-readiness check at the end
## Progressive Loading References
Load these references only when needed:
- Architecture and styling rules: [./references/architecture-and-styling.md](./references/architecture-and-styling.md)
- Event and state interaction patterns: [./references/interaction-patterns.md](./references/interaction-patterns.md)
- Troubleshooting and release gates: [./references/troubleshooting-and-quality-gates.md](./references/troubleshooting-and-quality-gates.md)
## Procedure
### 1. Define the UI Slice
- Capture the user-visible outcome for this task in one sentence.
- Identify route-level page modules to touch.
- Identify service operations needed by the UI.
Completion check:
- You can name the target page, component candidates, and service calls before coding.
### 2. Choose Component Extraction Strategy
Decision point:
- If a layout pattern appears in 2 or more pages, extract it to `ui/components/`.
- If a pattern is page-specific, keep it in the page module.
Completion check:
- Reused UI patterns are encapsulated as callable components.
### 3. Build Responsive Layout First
- Use Tailwind utility classes for structure and spacing.
- Use responsive breakpoints (`sm:`, `md:`, `lg:`).
- Reserve `.style()` for dynamic values that cannot be expressed with classes.
Completion check:
- Layout works at mobile and desktop widths without custom CSS overrides.
### 4. Add Reactive State And Events
- Use bindable dataclasses for local page state.
- Prefer event handlers (`on_click`, `on_upload`, etc.) over periodic polling.
- Trigger explicit refreshes with `@ui.refreshable` where needed.
Decision point by interaction type:
- File upload: validate size/type, delegate storage to a service, notify success/failure.
- Form submit: bind inputs to dataclass fields, validate in service layer, clear state on success.
- Real-time status: use SSE or WebSocket for push updates.
- Long jobs: run in background task, update status endpoint or stream.
Completion check:
- Every user action has explicit positive and negative feedback via `ui.notify()`.
### 5. Apply Styling Strategy
Preferred order:
1. Tailwind utility classes
2. Quasar props
3. Reusable styled component functions
Only if absolutely necessary:
- Load minimal custom CSS once at startup in `bootstrap.py`.
- Keep custom CSS tokenized (variables) and documented.
Completion check:
- Styling is mostly class/props-driven and not dependent on scattered ad-hoc CSS.
### 6. Harden Against Common Failures
- Prevent duplicate submissions by disabling controls during in-flight operations.
- Avoid overlapping timers for the same state target.
- Serialize dependent updates (`await` service call before mutation/render).
- Verify static mount paths and cache behavior for changed assets.
Completion check:
- Race conditions and stale asset symptoms are addressed with explicit safeguards.
### 7. Final Production Readiness Review
Pass all checks:
- Structure: pages, components, services follow one-way dependency flow.
- Responsiveness: tested at small and large viewport widths.
- Accessibility: labels, button text, and action visibility are clear.
- Reliability: validation and exception paths produce user-facing notifications.
- Maintainability: repeated UI patterns are extracted; business logic stays in services.
If any check fails, return to the relevant step and iterate.
## Completion Contract
This workflow is complete when:
- the page flow meets the target outcome
- architecture boundaries are preserved
- chosen interaction pattern is implemented with explicit success and failure feedback
- troubleshooting checks pass
- production-readiness gate passes
@@ -0,0 +1,76 @@
# Architecture and Styling Reference
## Project Boundaries
Use this dependency direction:
- pages import components and services
- components contain presentation logic only
- services contain business logic and do not import UI
- static assets are mounted and loaded once at bootstrap
Suggested module split:
```text
src/app/
ui/pages/
ui/components/
ui/static/
services/
api/
bootstrap.py
```
## Component Extraction Rules
Extract to ui/components when a pattern appears in two or more pages.
Keep in-page if the layout is specific to a single route.
```python
def card_section(title: str, content: str) -> ui.card:
with ui.card().classes("w-full max-w-md") as card:
ui.label(title).classes("text-lg font-bold")
ui.label(content).classes("text-gray-600")
return card
```
## Tailwind-First Layout Pattern
Use Tailwind utility classes for structure and spacing.
Use breakpoint classes for responsive behavior.
Use .style() only for values that must be computed dynamically.
```python
with ui.column().classes("w-full"):
with ui.row().classes("w-full gap-4 flex-wrap sm:flex-nowrap"):
ui.card().classes("flex-1 min-w-64")
ui.card().classes("flex-1 min-w-64")
```
## Styling Decision Order
1. Tailwind utility classes
2. Quasar props
3. Reusable styled component functions
4. Minimal custom CSS loaded once at bootstrap (only when needed)
```python
from fastapi.staticfiles import StaticFiles
app.mount("/static", StaticFiles(directory="src/app/static"), name="static")
ui.add_css(open("src/app/static/css/base.css").read())
```
## Static Asset Rules
- Keep custom CSS small and tokenized with variables.
- Avoid per-page CSS injection.
- Verify static mount paths and reverse proxy rewrites.
## Links
- NiceGUI elements: https://nicegui.io/documentation/element
- NiceGUI binding: https://nicegui.io/documentation/section_binding_properties
- Tailwind: https://tailwindcss.com/docs/utility-first
- Quasar components: https://quasar.dev/vue-components
@@ -0,0 +1,109 @@
# Interaction Patterns Reference
## Reactive State
Use bindable dataclasses for local page state.
```python
from dataclasses import field
from nicegui import binding, ui
@binding.bindable_dataclass
class PageState:
selected_id: int | None = None
items: list = field(default_factory=list)
state = PageState()
ui.label().bind_text_from(state, "selected_id")
```
## File Upload Pattern
- Validate extension and size before storing.
- Delegate storage to a service method.
- Notify success and failure explicitly.
```python
async def handle_upload(e: ui.events.UploadEventArguments):
try:
if e.size > 10 * 1024 * 1024:
raise ValueError("File too large")
if not e.name.endswith(".pdf"):
raise ValueError("Only PDF allowed")
await file_service.store(e.content.read(), e.name)
ui.notify(f"Uploaded: {e.name}", type="positive")
except ValueError as err:
ui.notify(str(err), type="negative")
ui.upload(on_upload=handle_upload, auto_upload=True)
```
## Form Submission Pattern
- Bind UI inputs to dataclass fields.
- Perform validation in the service layer.
- Clear form state on success.
```python
@binding.bindable_dataclass
class FormData:
name: str = ""
email: str = ""
data = FormData()
ui.input("Name").bind_value(data, "name")
ui.input("Email").bind_value(data, "email")
async def on_submit():
try:
await user_service.create_user(name=data.name, email=data.email)
ui.notify("User created", type="positive")
data.name = data.email = ""
except ValueError as err:
ui.notify(str(err), type="negative")
ui.button("Submit").on_click(on_submit)
```
## Real-Time Updates Decision
Use SSE for one-way status streaming.
Use WebSocket for bidirectional messaging.
SSE endpoint example:
```python
@app.get("/events/status")
async def status_stream():
async def gen():
while True:
yield f"data: {await get_status()}\\n\\n"
await asyncio.sleep(1)
return StreamingResponse(gen(), media_type="text/event-stream")
```
## Background Work Pattern
- Start long jobs in FastAPI background tasks.
- Expose status via endpoint or streaming channel.
- Guard buttons against duplicate submissions during in-flight tasks.
## Explicit Refresh Pattern
Use @ui.refreshable and call refresh intentionally instead of polling unrelated state.
```python
@ui.refreshable
async def item_list():
items = await service.list()
for item in items:
ui.label(item.name)
ui.button("Refresh").on_click(lambda: item_list.refresh())
```
## Links
- NiceGUI action events: https://nicegui.io/documentation/section_action_events
- FastAPI SSE: https://fastapi.tiangolo.com/advanced/server-sent-events/
- FastAPI WebSockets: https://fastapi.tiangolo.com/advanced/websockets/
@@ -0,0 +1,39 @@
# Troubleshooting and Quality Gates
## Troubleshooting
### Upload Errors
- Validate extension and size before storage.
- Catch expected exceptions and return negative notifications.
- Log unexpected exceptions with request context.
### UI Race Conditions
- Disable triggering controls during async work.
- Remove duplicate timers and listeners targeting the same state.
- Ensure service call ordering is deterministic before render updates.
### Asset Caching
- Confirm static mount and proxy rewrite correctness.
- Add cache-busting query strings for changed assets.
- Avoid per-page CSS injection.
### Navigation and State Drift
- Avoid global mutable UI state.
- Keep state request-scoped or service-managed.
- Rehydrate page data during route load.
## Production Readiness Gate
Pass all checks before shipping:
- Structure: one-way dependencies between pages, components, and services.
- Responsiveness: UI validated at both small and large viewport widths.
- Accessibility: labels and actions are clear and readable.
- Reliability: validation and exception paths surface user feedback.
- Maintainability: repeated UI patterns are extracted; business logic remains in services.
If any check fails, return to the workflow step that owns that concern.
+196
View File
@@ -0,0 +1,196 @@
---
name: nicegui
description: 'Design and scaffold a production-ready NiceGUI + FastAPI application architecture. Use for multi-page app planning, package boundaries, optional DB/LangGraph/docs integration, and implementation checklists.'
argument-hint: 'What should this app include (pages, DB, AI, docs, constraints)?'
---
# NiceGUI
Design a production-minded NiceGUI + FastAPI architecture with clear boundaries, optional extensions, and a concrete implementation checklist.
## When to Use
- You need a reusable architecture plan before implementing a NiceGUI app.
- You want FastAPI app-factory structure and lifespan wiring.
- You need optional guidance for database, LangGraph workflows, or mounted static docs.
- You want output that is concise, structured, and implementation-ready.
## Inputs to Collect
Collect these inputs up front. If not provided, make safe defaults and state assumptions.
- Product scope and primary user journeys.
- Required pages and route map.
- Whether persistent data is required.
- Whether AI orchestration (multi-step, streaming, approvals) is required.
- Whether generated docs should be mounted in-app.
- Runtime/deployment constraints (single service vs split services, environment requirements).
## Outcome
Produce:
- A concise architecture explanation.
- How core services, UI pages, and UI components fit together.
- Explicit decision on DB ownership or involvement.
- Explicit decision on AI workflow (or no AI).
- A checklist implementation plan organized by package and domain.
## Procedure
1. Frame the baseline architecture.
2. Choose optional extensions (DB, AI, docs) using decision points below.
3. Map modules, dependencies, and key boundaries.
4. Define async behavior and UI responsiveness expectations.
5. Define key functions/classes and configuration surfaces.
6. Produce phased checklist with rollout or migration notes when relevant.
7. Run completion checks before returning.
### 1) Baseline architecture
Use a src-layout with FastAPI as the ASGI app and NiceGUI registered via composition.
- App factory pattern: `create_app()`.
- Lifespan for startup and shutdown resource management.
- `api/` for HTTP handlers, `services/` for business logic.
- `ui/pages/` for page modules, `ui/components/` for shared UI.
- Health endpoint on FastAPI side: `/healthz`.
Recommended base shape:
```text
.
├─ pyproject.toml
├─ .env.example
├─ README.md
├─ src/
│ └─ app/
│ ├─ __init__.py
│ ├─ main.py
│ ├─ bootstrap.py
│ ├─ config.py
│ ├─ logging.py
│ ├─ api/
│ │ ├─ __init__.py
│ │ └─ health.py
│ ├─ services/
│ │ ├─ __init__.py
│ │ └─ example_service.py
│ └─ ui/
│ ├─ __init__.py
│ ├─ components/
│ │ ├─ __init__.py
│ │ └─ nav.py
│ └─ pages/
│ ├─ __init__.py
│ ├─ home.py
│ ├─ dashboard.py
│ └─ about.py
└─ tests/
├─ test_health.py
└─ test_pages_registration.py
```
### 2) Decision points
#### Database needed?
- If no: keep `services/` pure and skip persistence layers.
- If yes: add `db/` package with engine/session/model/repository layering.
- Prefer one process-level engine and request-scoped sessions via `yield`.
- Prefer Alembic migrations for schema changes.
#### AI workflow needed?
- If no: keep `services/` focused on app logic only.
- If yes: add `ai/` package (state, nodes, graph, runtime, contracts).
- Keep graph internals out of `ui/pages/` and API handlers.
- Use stable thread/session IDs for resumable flows.
#### Mounted docs needed?
- If no: skip docs mounting.
- If yes: mount generated static site under configurable route (default `/docs`).
- Keep docs mounting in composition layer, not page modules.
### 3) Page and component registration
- Require at minimum page modules for `/`, `/dashboard`, `/about`.
- Prefer explicit registration pattern:
- `ui/pages/__init__.py` exports `register_pages()`.
- Each page module exports `register_page()`.
- Shared shell components (header/nav/drawer) live in `ui/components/`.
### 4) Dependency direction rules
Prefer:
- `main/bootstrap` -> `config/logging` + `api` + `ui/pages` + `services`
- `api` -> `services`
- `ui/pages` -> `ui/components` + `services`
- `services` -> helpers/clients (and `db/` when enabled)
Avoid reverse imports from services into API or UI modules.
### 5) Async and UI responsiveness rules
- Prefer `async def` for page handlers, service methods, and integrations when the call path includes I/O.
- Use non-blocking clients/libraries where possible so long-running I/O does not freeze UI updates.
- Do not run blocking calls (`time.sleep`, blocking HTTP/database clients) in UI event handlers.
- For heavy CPU work, offload to worker/background execution and keep the UI loop free.
- Show progress states for long actions (disable action button, show spinner/progress text, re-enable on completion).
- Stream or chunk incremental results to the UI when workflows are multi-step or long-running.
- Keep cancellation and timeout behavior explicit for user-triggered long tasks.
- Ensure exceptions from async tasks are surfaced with user-friendly feedback and logged for diagnostics.
### 6) Testing minimums
- Test FastAPI health route behavior.
- Test page registration wiring.
- If DB enabled: session lifecycle and rollback behavior tests.
- If AI enabled: graph happy path and interrupt/resume coverage.
- If docs enabled: mounted docs route returns index page.
- For async flows: test long-running actions preserve UI responsiveness (loading state, completion state, and error state).
### 7) Styling architecture
- Keep structure and layout in Python modules using NiceGUI class composition.
- Keep visual polish in shared CSS files, loaded once at startup.
- Prefer semantic reusable classes over ad hoc per-page styling.
## Completion Checks
- Uses app factory and FastAPI lifespan.
- Pages are modularized (not single-file UI).
- Health endpoint exists on FastAPI side.
- Dependency direction is clean and one-way.
- Async-first guidance is applied where I/O exists, with explicit non-blocking UX states.
- Optional DB/AI/docs decisions are explicit and reflected in structure.
- Output includes architecture summary and package-organized checklist.
## Output Contract
Return:
- Concise high-level architecture.
- How core services, pages, and shared components fit.
- DB involvement and ownership stance.
- AI workflow stance and runtime flow.
- Checklist plan by package and domain:
- key functions/classes
- settings/config surfaces
- rollout/migration notes (when relevant)
## Guardrails
- Do not collapse all pages into one file.
- Do not use globals or implicit global side effects.
- Do not block UI event handlers with synchronous I/O or long CPU tasks.
- Always define loading/progress/error states for long user-triggered actions.
- Keep code minimal but production-minded.
- Prefer clarity and maintainability over clever abstractions.
## References
- Architecture and integration details: [NiceGUI architecture reference](./references/architecture.md)
- Source documentation links: [NiceGUI source documentation](./references/source-documentation.md)
@@ -0,0 +1,92 @@
# NiceGUI Architecture Reference
This reference expands the workflow in the main skill file and is loaded only when needed.
## Baseline package boundaries
- `main.py`: process entrypoint only.
- `bootstrap.py`: app composition, router wiring, page registration, lifespan orchestration.
- `config.py`: typed settings and env parsing.
- `logging.py`: centralized logging setup.
- `api/`: HTTP transport layer; delegates to services.
- `services/`: business/use-case logic.
- `ui/pages/`: route-level NiceGUI pages.
- `ui/components/`: shared UI building blocks.
## Required baseline behavior
- FastAPI is the base ASGI app.
- NiceGUI pages are modular and registered from page modules.
- Minimum pages: `/`, `/dashboard`, `/about`.
- FastAPI health route: `/healthz`.
- Lifespan handles startup/shutdown resources.
- No global side effects at import time.
## Optional extension: Database
Use only if persistence is required.
Suggested additions:
```text
src/app/db/
├─ __init__.py
├─ base.py
├─ session.py
├─ models/
└─ repositories/
```
Guidelines:
- One engine and one sessionmaker per process.
- Request-scoped session dependency using `yield`.
- Explicit transaction boundaries in service/repository flows.
- Avoid shared sessions across concurrent tasks.
- Use Alembic as schema source of truth.
## Optional extension: LangGraph AI
Use only for multi-step AI orchestration or human-in-the-loop workflows.
Suggested additions:
```text
src/app/ai/
├─ state.py
├─ nodes/
├─ graphs/
├─ runtime.py
└─ contracts.py
```
Guidelines:
- Keep graph internals outside API/UI modules.
- Invoke graph through `services/ai_service.py`.
- Use stable thread/session IDs for resumable sessions.
- Keep interrupt payloads JSON-serializable.
## Optional extension: Mounted static docs
Use only when generated docs should be served in-app.
Suggested settings:
- `docs_enabled`
- `docs_mount_path`
- `docs_site_dir`
- `docs_require_build` (optional)
Guidelines:
- Mount docs in composition layer (`bootstrap.py`).
- Normalize mount path and avoid route conflicts.
- Warn on missing build artifacts unless strict mode is enabled.
## Suggested output quality criteria
- Clear architecture summary with assumptions.
- Explicit decisions for DB, AI, and docs.
- Package-scoped implementation checklist.
- Minimal test plan aligned to enabled features.
@@ -0,0 +1,35 @@
# Source Documentation
Use these links for framework-specific details.
## FastAPI
- FastAPI lifespan events: https://fastapi.tiangolo.com/advanced/events/
- FastAPI settings and environment variables: https://fastapi.tiangolo.com/advanced/settings/
- FastAPI dependencies with yield: https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/
- FastAPI SQL databases tutorial: https://fastapi.tiangolo.com/tutorial/sql-databases/
## SQLAlchemy and Alembic
- SQLAlchemy engine configuration and pooling: https://docs.sqlalchemy.org/en/20/core/engines.html
- SQLAlchemy session lifecycle basics: https://docs.sqlalchemy.org/en/20/orm/session_basics.html
- Alembic tutorial: https://alembic.sqlalchemy.org/en/latest/tutorial.html
## Pydantic
- Pydantic settings management: https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/
## NiceGUI
- NiceGUI pages/routing and FastAPI integration: https://www.nicegui.io/documentation/section_pages_routing
- NiceGUI security best practices: https://www.nicegui.io/documentation/section_security
## LangGraph
- LangGraph overview: https://docs.langchain.com/oss/python/langgraph/overview
- LangGraph quickstart: https://docs.langchain.com/oss/python/langgraph/quickstart
- LangGraph workflows and agents: https://docs.langchain.com/oss/python/langgraph/workflows-agents
- LangGraph persistence: https://docs.langchain.com/oss/python/langgraph/persistence
- LangGraph memory concepts: https://docs.langchain.com/oss/python/concepts/memory
- LangGraph streaming: https://docs.langchain.com/oss/python/langgraph/streaming
- LangGraph interrupts and human-in-the-loop: https://docs.langchain.com/oss/python/langgraph/interrupts
+136
View File
@@ -0,0 +1,136 @@
---
name: pytest-scaffolding
description: "Scaffold a maintainable, hierarchical pytest suite for core functionality first, then extend safely. Use when setting up tests, organizing fixtures by dependency, mirroring src structure in tests, or enforcing fast-by-default test runs."
argument-hint: "Target scope (for example: app/services/job, app/ai, or full repo)"
---
# Pytest Scaffolding
Create test scaffolding that is:
- Hierarchical: test layout roughly mirrors source layout.
- Fast by default: most tests run in under a second total for core units.
- Dependency-aware: slow/external dependencies are isolated behind markers and fixture scope.
- Extensible: minimal initial skeleton supports adding detailed tests later without refactors.
This repository currently uses:
- `uv run pytest` as the canonical test invocation.
- `pyproject.toml` pytest config under `[tool.pytest.ini_options]`.
- strict marker checking (`--strict-markers`).
Load [pytest references](./references/pytest-docs.md) when you need detailed rules.
## When To Use
- Bootstrapping tests for a new or existing Python repo.
- Reorganizing tests that have become flat, slow, or difficult to extend.
- Defining fixture boundaries before writing many assertions.
- Creating only the first-layer scaffold for core behavior (not exhaustive coverage yet).
## Inputs To Collect
1. Target test scope: full repo, package, or module.
2. Dependency profile: pure Python, DB, network/API, filesystem, UI/browser.
3. Runtime expectation: what must be instant vs allowed to be slower.
4. CI policy: which marker groups must block merges.
If these are missing, ask concise clarifying questions before editing.
## Workflow
1. Map source tree to test tree.
2. Classify tests by dependency cost.
3. Create minimal directories and placeholder test modules.
4. Create fixture layers (`tests/conftest.py` plus local `conftest.py` in subtrees only when needed).
5. Register markers and default selection behavior.
6. Run collection and fast path tests.
7. Report gaps and next extension points.
## Step 1: Map Source To Tests
Create a mirrored structure rooted at `tests/` that follows major source concepts.
Example mapping pattern:
- `src/app/services/job.py` -> `tests/app/services/test_job.py`
- `src/app/ai/graphs/transcription.py` -> `tests/app/ai/graphs/test_transcription.py`
- `src/app/api/routes.py` -> `tests/app/api/test_routes.py`
Rules:
- One initial test module per core source module.
- Prefer `test_<module>.py` naming.
- Keep directory mirrors shallow first; add deeper modules only where behavior is complex.
## Step 2: Classify By Dependency Cost
Assign each test module to one initial class:
- `unit`: no DB/network/filesystem side effects; instant execution.
- `integration`: touches DB, HTTP stack, workflow runtime, or external services.
- `smoke`: thin end-to-end confidence checks.
Decision logic:
- If logic can run with fakes/stubs, make it `unit`.
- If contract with framework/DB is essential, make it `integration`.
- If validating a user-critical path across layers, make it `smoke`.
## Step 3: Scaffold Minimal Test Modules
For each target module, scaffold:
- import section
- one happy-path test function
- one error/edge test function
- TODO comments indicating detail expansion points
Keep assertions minimal but behavior-focused. Avoid large fixtures in module files.
## Step 4: Fixture Layering Strategy
Use fixture scopes based on cost:
- `function` scope by default.
- broader scopes (`module`/`session`) only for expensive setup with clear teardown.
Layer fixtures by directory:
- `tests/conftest.py`: global, lightweight fixtures only (factories, deterministic defaults).
- subtree `conftest.py`: domain-specific fixtures (API client, DB session, AI runtime stubs).
Guidelines:
- Prefer yield fixtures for setup/teardown.
- Keep fixtures atomic (one state-changing responsibility per fixture).
- Avoid autouse except for truly universal behavior.
## Step 5: Marker Taxonomy And Config
Ensure marker names are explicit and registered in `pyproject.toml` because strict markers are enabled.
Recommended baseline markers:
- `unit`
- `integration`
- `smoke`
- `slow`
- `external` (requires network/service credentials)
Default run strategy:
- Fast local path: run only `unit` by default in day-to-day iteration.
- Full validation path: run all markers in CI or pre-release checks.
## Step 6: Execution And Verification
Run commands in this order:
1. `uv run pytest --collect-only -q`
2. `uv run pytest -m unit -q`
3. `uv run pytest -q` (if dependencies are available)
Optional targeted runs:
- by node id for one test
- by `-k` expression for focused iteration
## Step 7: Completion Checks
A scaffold pass is complete when all are true:
1. Every core source area has at least one corresponding test module.
2. Unit tests run quickly and deterministically.
3. Integration/external tests are isolated by marker and fixture boundaries.
4. No unregistered marker warnings/errors.
5. `tests/` structure is understandable without extra documentation.
6. A clear TODO path exists for deepening assertions later.
## Branching Scenarios
- If external APIs are required: provide stubs/mocks for unit tests; guard real calls behind `external` marker.
- If DB is required: build a dedicated integration fixture layer and keep unit tests DB-free.
- If tests become slow: split slow tests via marker and widen fixture scope only where safe.
- If naming conflicts appear: keep unique test module names or package test directories explicitly.
## Output Format
When applying this skill, provide:
1. Proposed test tree diff.
2. Marker and fixture plan.
3. Exact commands for fast path and full path.
4. Risks/open questions before writing detailed assertions.
@@ -0,0 +1,22 @@
# Pytest Documentation Notes
Primary references used:
- https://docs.pytest.org/en/stable/explanation/goodpractices.html
- https://docs.pytest.org/en/stable/how-to/fixtures.html
- https://docs.pytest.org/en/stable/example/markers.html
- https://docs.pytest.org/en/stable/reference/customize.html
- https://docs.pytest.org/en/stable/explanation/flaky.html
## Practical Guidance For This Skill
- Use src-aligned test layout and keep test discovery conventional.
- Keep fixtures small, composable, and explicit; use `yield` for teardown.
- Register custom markers and keep strict marker validation on.
- Separate quick unit runs from slower integration/external runs.
- Minimize flakiness by controlling shared state and avoiding hidden dependencies.
- Use `--collect-only` and marker-filtered runs to validate scaffold quality early.
## Commands Worth Remembering
- `uv run pytest --collect-only -q`
- `uv run pytest -m unit -q`
- `uv run pytest -m "not external" -q`
- `uv run pytest -q`
@@ -0,0 +1,97 @@
---
name: python-logging-dictconfig
description: 'Set up idiomatic Python logging with logging.config.dictConfig. Use when creating or refactoring logging setup, standardizing handlers/formatters, and enforcing centralized config.'
argument-hint: 'Target context (single script, package, FastAPI app, or CLI) and desired log destinations'
---
# Idiomatic Python Logging with dictConfig
Use this skill to produce a minimal, centralized logging setup using `logging.config.dictConfig`.
Load references only when needed:
- Python logging overview and hierarchy: [./references/python-logging-docs.md](./references/python-logging-docs.md)
## When to Use
- A project configures logging ad hoc with `basicConfig` across multiple modules.
- You need one canonical logging configuration for app startup.
- You need consistent formatting and levels across console/file handlers.
- You want library modules to use named loggers without configuring logging themselves.
## Inputs To Collect
1. Runtime type: script, library, web app, worker, CLI.
2. Destinations: stdout only, file only, or both.
3. Desired default level: `INFO`, `DEBUG`, etc.
4. Whether third-party loggers should be tuned (for example `uvicorn`, `sqlalchemy`).
If missing, assume:
- stdout handler
- human-readable formatter
- root level `INFO`
- `disable_existing_loggers: False`
## Procedure
1. Define a single `LOGGING` dictionary in one startup-oriented module (for example `logging_config.py`).
2. Include `version: 1` and set `disable_existing_loggers: False` unless there is a specific reason to silence existing loggers.
3. Define formatters first, then handlers, then logger routing (`root` and optional named `loggers`).
4. Use `logging.config.dictConfig(LOGGING)` exactly once during application startup.
5. In all modules, get loggers via `logger = logging.getLogger(__name__)` and never call `basicConfig`.
6. Keep libraries configuration-free: libraries should emit logs, applications decide routing.
7. Verify behavior with a quick smoke check at multiple levels (`DEBUG`, `INFO`, `WARNING`, `ERROR`).
## Minimal Baseline Template
```python
# logging_config.py
from logging.config import dictConfig
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "%(asctime)s %(levelname)s %(name)s: %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "standard",
"stream": "ext://sys.stdout",
}
},
"root": {
"level": "INFO",
"handlers": ["console"],
},
}
def configure_logging() -> None:
dictConfig(LOGGING)
```
```python
# app startup
from .logging_config import configure_logging
configure_logging()
```
```python
# any module
import logging
logger = logging.getLogger(__name__)
logger.info("module initialized")
```
## Completion Checks
1. `dictConfig` is called once at startup, not per module.
2. No `basicConfig` calls remain.
3. Modules use `getLogger(__name__)`.
4. Logs appear at expected level and destination.
5. Third-party logger noise is intentionally configured or left at defaults.
## Branching Guidance
- If structured logs are required: switch formatter output to JSON while keeping `dictConfig` topology unchanged.
- If both console and file output are needed: add a file handler and attach it to `root`.
- If a specific framework logger is too noisy: add a named logger override under `loggers`.
@@ -0,0 +1,18 @@
# Python Logging References
Use these official Python docs when applying this skill.
## Core Documentation
- Logging HOWTO: https://docs.python.org/3/howto/logging.html
- Logging Cookbook: https://docs.python.org/3/howto/logging-cookbook.html
- logging API reference: https://docs.python.org/3/library/logging.html
- logging.config reference: https://docs.python.org/3/library/logging.config.html
## dictConfig-Specific
- Dictionary schema details (`version`, formatters, handlers, loggers, root): https://docs.python.org/3/library/logging.config.html#logging-config-dictschema
- `logging.config.dictConfig` function: https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
## Practical Notes
- Prefer app-level centralized config with one startup call to `dictConfig`.
- In modules, use `logging.getLogger(__name__)`.
- Avoid calling `basicConfig` in libraries or scattered modules.