move
This commit is contained in:
@@ -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.
|
||||
@@ -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 150–200 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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user