@eltonssouza/development-utility-kit 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/README.md +24 -0
- package/.claude/agents/analyst.md +198 -0
- package/.claude/agents/backend-developer.md +126 -0
- package/.claude/agents/brain-keeper.md +229 -0
- package/.claude/agents/code-reviewer.md +181 -0
- package/.claude/agents/database-engineer.md +94 -0
- package/.claude/agents/devops-engineer.md +141 -0
- package/.claude/agents/frontend-developer.md +97 -0
- package/.claude/agents/gate-keeper.md +118 -0
- package/.claude/agents/migrator.md +291 -0
- package/.claude/agents/mobile-developer.md +80 -0
- package/.claude/agents/n8n-specialist.md +94 -0
- package/.claude/agents/product-owner.md +115 -0
- package/.claude/agents/qa-engineer.md +232 -0
- package/.claude/agents/release-engineer.md +204 -0
- package/.claude/agents/scaffold.md +87 -0
- package/.claude/agents/security-engineer.md +199 -0
- package/.claude/agents/sprint-runner.md +46 -0
- package/.claude/agents/stack-resolver.md +104 -0
- package/.claude/agents/tech-lead.md +182 -0
- package/.claude/agents/update-template.md +54 -0
- package/.claude/agents/ux-designer.md +118 -0
- package/.claude/hooks/flow-guard.js +261 -0
- package/.claude/hooks/flow-state.js +197 -0
- package/.claude/local/CLAUDE.md +71 -0
- package/.claude/settings.json +55 -0
- package/.claude/skills/README.md +331 -0
- package/.claude/skills/active-project/SKILL.md +131 -0
- package/.claude/skills/api-integration-test/SKILL.md +84 -0
- package/.claude/skills/auto-test-guard/SKILL.md +239 -0
- package/.claude/skills/auto-test-guard/resources/backend-tests.md +20 -0
- package/.claude/skills/auto-test-guard/resources/e2e-tests.md +24 -0
- package/.claude/skills/auto-test-guard/resources/execution-report.md +49 -0
- package/.claude/skills/auto-test-guard/resources/frontend-tests.md +18 -0
- package/.claude/skills/auto-test-guard/resources/initial-setup.md +108 -0
- package/.claude/skills/auto-test-guard/resources/run-suite.md +48 -0
- package/.claude/skills/auto-test-guard/resources/senior-gate.md +19 -0
- package/.claude/skills/brain-keeper/SKILL.md +62 -0
- package/.claude/skills/brain-keeper/obsidian/app.json +9 -0
- package/.claude/skills/brain-keeper/obsidian/appearance.json +4 -0
- package/.claude/skills/brain-keeper/obsidian/core-plugins.json +20 -0
- package/.claude/skills/brain-keeper/obsidian/daily-notes.json +5 -0
- package/.claude/skills/brain-keeper/obsidian/graph.json +32 -0
- package/.claude/skills/brain-keeper/obsidian/snippets/folder-colors.css +90 -0
- package/.claude/skills/brain-keeper/obsidian/templates.json +5 -0
- package/.claude/skills/brain-keeper/templates/README.md +51 -0
- package/.claude/skills/brain-keeper/templates/adr.md +40 -0
- package/.claude/skills/brain-keeper/templates/bug.md +35 -0
- package/.claude/skills/brain-keeper/templates/daily.md +38 -0
- package/.claude/skills/brain-keeper/templates/feature.md +62 -0
- package/.claude/skills/brain-keeper/templates/meeting.md +34 -0
- package/.claude/skills/brain-keeper/templates/tech-debt.md +21 -0
- package/.claude/skills/caveman/SKILL.md +189 -0
- package/.claude/skills/create-stack-pack/SKILL.md +281 -0
- package/.claude/skills/grill-me/SKILL.md +80 -0
- package/.claude/skills/pair-debug/SKILL.md +288 -0
- package/.claude/skills/prd-ready-check/SKILL.md +86 -0
- package/.claude/skills/project-manager/SKILL.md +334 -0
- package/.claude/skills/quality-standards/SKILL.md +203 -0
- package/.claude/skills/quick-feature/SKILL.md +266 -0
- package/.claude/skills/run-sprint/SKILL.md +41 -0
- package/.claude/skills/scaffold/SKILL.md +60 -0
- package/.claude/skills/stack-discovery/SKILL.md +161 -0
- package/.claude/skills/test-coverage-auditor/SKILL.md +87 -0
- package/.claude/skills/to-issues/SKILL.md +163 -0
- package/.claude/skills/to-prd/SKILL.md +130 -0
- package/.claude/skills/update-template/SKILL.md +256 -0
- package/.claude/stacks/CODEOWNERS +30 -0
- package/.claude/stacks/README.md +97 -0
- package/.claude/stacks/_template.md +116 -0
- package/.claude/stacks/dotnet/aspire-9.md +528 -0
- package/.claude/stacks/go/gin-1.10.md +570 -0
- package/.claude/stacks/java/spring-boot-3.md +376 -0
- package/.claude/stacks/java/spring-boot-4.md +438 -0
- package/.claude/stacks/node/express-5.md +538 -0
- package/.claude/stacks/python/django-5.md +483 -0
- package/.claude/stacks/python/fastapi-0.115.md +522 -0
- package/.claude/stacks/typescript/angular-18.md +420 -0
- package/.claude/stacks/typescript/angular-19.md +397 -0
- package/.claude/stacks/typescript/angular-21.md +494 -0
- package/CLAUDE.md +472 -0
- package/README.md +412 -0
- package/bin/cli.js +848 -0
- package/bin/lib/adr.js +146 -0
- package/bin/lib/backup.js +62 -0
- package/bin/lib/detect-stack.js +476 -0
- package/bin/lib/doctor.js +527 -0
- package/bin/lib/help.js +328 -0
- package/bin/lib/identity.js +108 -0
- package/bin/lib/lint-allowlist.json +15 -0
- package/bin/lib/lint.js +798 -0
- package/bin/lib/local-dir.js +68 -0
- package/bin/lib/manifest.js +236 -0
- package/bin/lib/sync-all.js +394 -0
- package/bin/lib/version-check.js +398 -0
- package/dashboard/db.js +321 -0
- package/dashboard/package.json +22 -0
- package/dashboard/public/app.js +853 -0
- package/dashboard/public/content/docs/agents-reference.en.md +911 -0
- package/dashboard/public/content/docs/architecture-overview.en.md +252 -0
- package/dashboard/public/content/docs/autonomy-matrix.en.md +186 -0
- package/dashboard/public/content/docs/cli-reference.en.md +538 -0
- package/dashboard/public/content/docs/git-flow.en.md +525 -0
- package/dashboard/public/content/docs/honcho-memory.en.md +394 -0
- package/dashboard/public/content/docs/hooks-reference.en.md +404 -0
- package/dashboard/public/content/docs/pipeline.en.md +414 -0
- package/dashboard/public/content/docs/plugins.en.md +289 -0
- package/dashboard/public/content/docs/quality-gate.en.md +315 -0
- package/dashboard/public/content/docs/skills-reference.en.md +484 -0
- package/dashboard/public/content/docs/stack-rules.en.md +362 -0
- package/dashboard/public/content/docs/troubleshooting.en.md +565 -0
- package/dashboard/public/content/manifest.json +114 -0
- package/dashboard/public/content/manual/backend.en.md +1053 -0
- package/dashboard/public/content/manual/existing-project.en.md +848 -0
- package/dashboard/public/content/manual/frontend.en.md +1008 -0
- package/dashboard/public/content/manual/fullstack.en.md +1459 -0
- package/dashboard/public/content/manual/mobile.en.md +837 -0
- package/dashboard/public/content/manual/quickstart.en.md +169 -0
- package/dashboard/public/index.html +217 -0
- package/dashboard/public/style.css +857 -0
- package/dashboard/public/vendor/marked.min.js +69 -0
- package/dashboard/rtk.js +143 -0
- package/dashboard/server-app.js +421 -0
- package/dashboard/server.js +104 -0
- package/dashboard/test/sprint1.test.js +406 -0
- package/dashboard/test/sprint2.test.js +571 -0
- package/dashboard/test/sprint3.test.js +560 -0
- package/package.json +33 -0
- package/scripts/hooks/subagent-telemetry.sh +14 -0
- package/scripts/hooks/telemetry-writer.js +250 -0
- package/scripts/latest-versions.json +56 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
---
|
|
2
|
+
stack: python/fastapi-0.115
|
|
3
|
+
versions_covered: "0.115.x — 0.118.x"
|
|
4
|
+
last_validated: 2026-05-28
|
|
5
|
+
validated_against: "reference pack — Python 3.13 + FastAPI 0.115 + Pydantic 2.9 + SQLAlchemy 2.0"
|
|
6
|
+
status: active
|
|
7
|
+
pack_owner: "@elton"
|
|
8
|
+
security_review: 2026-05-28
|
|
9
|
+
next_review_due: 2027-05-28
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Python 3.13 + FastAPI 0.115+
|
|
13
|
+
|
|
14
|
+
Canonical knowledge pack for FastAPI projects on Python 3.12+/3.13 + FastAPI 0.115+ (current line) + Pydantic v2 + SQLAlchemy 2.0. FastAPI's strengths show in async-first API services, MLOps endpoints, microservices, and projects where the admin/ORM/forms of Django would be dead weight. For projects that want Django's batteries-included experience (admin, forms, auth, ORM, migrations), use `python/django-5.md` instead.
|
|
15
|
+
|
|
16
|
+
## 1. When to use this pack
|
|
17
|
+
|
|
18
|
+
- Project declares `Primary stack: Python 3.12+ + FastAPI 0.115+` in `## Project Identity`.
|
|
19
|
+
- `pyproject.toml` declares `fastapi>=0.115,<0.120` and `python_requires = ">=3.12"`.
|
|
20
|
+
- Service is API-only (REST or gRPC), no admin UI, no server-rendered HTML at scale.
|
|
21
|
+
- Heavy async I/O — multiple external HTTP calls, websockets, server-sent events, streaming.
|
|
22
|
+
- MLOps / model-serving — Pydantic validation + automatic OpenAPI is FastAPI's home turf.
|
|
23
|
+
- For full-stack monolith with admin + forms + ORM: use Django (`python/django-5.md`).
|
|
24
|
+
- For batch processing without HTTP layer: use a plain Python project with `celery` or `prefect`, not FastAPI.
|
|
25
|
+
|
|
26
|
+
## 2. Stack baseline (what this pack assumes)
|
|
27
|
+
|
|
28
|
+
| Component | Version range | Notes |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| Python | 3.12 (min) / 3.13 (recommended) | 3.13 free-threaded build works with FastAPI; type hints are mandatory for the framework to function |
|
|
31
|
+
| FastAPI | 0.115.x — 0.118.x | API stable; Annotated dependencies are the canonical form (0.100+) |
|
|
32
|
+
| Pydantic | 2.9.x+ | Pydantic v1 unsupported as of FastAPI 0.100; do not mix |
|
|
33
|
+
| Pydantic Settings | 2.5.x+ | Config via env vars, validated at startup |
|
|
34
|
+
| SQLAlchemy | 2.0.x | New 2.0-style API (`select(...).where(...)`), `Mapped[...]` type annotations |
|
|
35
|
+
| Async DB driver | `asyncpg` (Postgres), `aiomysql` (MySQL), `aiosqlite` (tests only) | NEVER mix sync `psycopg2` with async engine; pick one stack |
|
|
36
|
+
| Migrations | Alembic 1.13+ | autogenerate is a draft, always review; never manual `Base.metadata.create_all()` in prod |
|
|
37
|
+
| Build / packaging | `uv` (recommended) or `pip` + `pyproject.toml` | `uv pip sync requirements.lock` for deterministic installs |
|
|
38
|
+
| ASGI server | `uvicorn[standard]` workers + `gunicorn` master | Or `granian` for lower latency in production |
|
|
39
|
+
| Tests | pytest 8.x + `pytest-asyncio` + `httpx` AsyncClient + Testcontainers Postgres | NEVER SQLite if prod is Postgres |
|
|
40
|
+
| Mutation | mutmut 3.x | Target ≥70% on `domain/` + `application/` |
|
|
41
|
+
| Coverage | coverage.py 7.x with `branch = True` | Target ≥85% lines, ≥80% branches |
|
|
42
|
+
| Static analysis | `ruff` + `mypy --strict` | Pydantic models get static checks via mypy plugin |
|
|
43
|
+
| Security scan | `pip-audit` + `bandit` | 0 CVE with CVSS ≥7.0 |
|
|
44
|
+
| Observability | OpenTelemetry SDK + `opentelemetry-instrumentation-fastapi` | W3C Trace Context; auto-instruments routes + DB + HTTP client |
|
|
45
|
+
| Background jobs | Celery 5.x + Redis OR `arq` (async-native) | `arq` integrates with FastAPI lifespan cleanly |
|
|
46
|
+
| OpenAPI docs | Auto-generated at `/docs` (Swagger UI) and `/redoc` (Redoc) | Disable in prod via `FastAPI(docs_url=None, redoc_url=None)` |
|
|
47
|
+
|
|
48
|
+
## 3. Project structure (DDD-flavored FastAPI)
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
service/
|
|
52
|
+
├── pyproject.toml
|
|
53
|
+
├── uv.lock
|
|
54
|
+
├── alembic.ini
|
|
55
|
+
├── alembic/
|
|
56
|
+
│ ├── env.py
|
|
57
|
+
│ └── versions/
|
|
58
|
+
├── env.example
|
|
59
|
+
├── src/
|
|
60
|
+
│ ├── domain/ # pure Python, no FastAPI/SQLAlchemy imports
|
|
61
|
+
│ │ ├── products/
|
|
62
|
+
│ │ │ ├── entities.py
|
|
63
|
+
│ │ │ ├── repository.py # Protocol/ABC port
|
|
64
|
+
│ │ │ └── services.py
|
|
65
|
+
│ │ └── shared/
|
|
66
|
+
│ ├── application/ # use cases (1 callable each)
|
|
67
|
+
│ │ ├── products/
|
|
68
|
+
│ │ │ ├── create_product.py
|
|
69
|
+
│ │ │ ├── list_products.py
|
|
70
|
+
│ │ │ └── dto.py
|
|
71
|
+
│ │ └── shared/
|
|
72
|
+
│ ├── infrastructure/ # SQLAlchemy + external clients
|
|
73
|
+
│ │ ├── db/
|
|
74
|
+
│ │ │ ├── engine.py # async engine + sessionmaker
|
|
75
|
+
│ │ │ └── base.py # DeclarativeBase
|
|
76
|
+
│ │ ├── products/
|
|
77
|
+
│ │ │ ├── models.py # ORM (SQLAlchemy 2.0 Mapped)
|
|
78
|
+
│ │ │ └── repository_impl.py
|
|
79
|
+
│ │ └── http/
|
|
80
|
+
│ │ └── upstream_client.py # httpx.AsyncClient wrapper
|
|
81
|
+
│ ├── api/ # FastAPI routes + schemas
|
|
82
|
+
│ │ ├── products/
|
|
83
|
+
│ │ │ ├── router.py
|
|
84
|
+
│ │ │ ├── schemas.py # Pydantic req/res models
|
|
85
|
+
│ │ │ └── deps.py # Depends() factories
|
|
86
|
+
│ │ └── shared/
|
|
87
|
+
│ │ ├── exception_handlers.py # RFC 9457 ProblemDetail
|
|
88
|
+
│ │ └── middleware.py
|
|
89
|
+
│ ├── config/
|
|
90
|
+
│ │ └── settings.py # pydantic-settings BaseSettings
|
|
91
|
+
│ └── main.py # FastAPI() instance + lifespan
|
|
92
|
+
└── tests/
|
|
93
|
+
├── unit/ # domain + application
|
|
94
|
+
├── integration/ # API + DB with Testcontainers
|
|
95
|
+
└── e2e/ # full HTTP through TestClient
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Rule**: `domain/` and `application/` contain **zero FastAPI and zero SQLAlchemy imports**. They are pure Python testable without any HTTP server or DB. `infrastructure/` imports SQLAlchemy. `api/` imports FastAPI. The 4 layers do not skip — `api/` never imports `infrastructure/` directly; it goes through `application/`.
|
|
99
|
+
|
|
100
|
+
## 4. Code patterns
|
|
101
|
+
|
|
102
|
+
### Pydantic schemas (request/response separated)
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
# src/api/products/schemas.py
|
|
106
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
107
|
+
from decimal import Decimal
|
|
108
|
+
from uuid import UUID
|
|
109
|
+
from datetime import datetime
|
|
110
|
+
|
|
111
|
+
class CreateProductRequest(BaseModel):
|
|
112
|
+
model_config = ConfigDict(extra="forbid") # reject unknown fields
|
|
113
|
+
name: str = Field(min_length=1, max_length=120)
|
|
114
|
+
price: Decimal = Field(gt=Decimal("0"))
|
|
115
|
+
stock: int = Field(ge=0)
|
|
116
|
+
|
|
117
|
+
class ProductResponse(BaseModel):
|
|
118
|
+
model_config = ConfigDict(from_attributes=True)
|
|
119
|
+
id: UUID
|
|
120
|
+
name: str
|
|
121
|
+
price: Decimal
|
|
122
|
+
stock: int
|
|
123
|
+
created_at: datetime
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Rule**: `extra="forbid"` on every request model — silent acceptance of unknown fields is a footgun. Response models declared explicitly (never just return ORM object dict).
|
|
127
|
+
|
|
128
|
+
### Route (Annotated dependencies, FastAPI 0.115 canonical)
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
# src/api/products/router.py
|
|
132
|
+
from typing import Annotated
|
|
133
|
+
from fastapi import APIRouter, Depends, status
|
|
134
|
+
from src.api.products.schemas import CreateProductRequest, ProductResponse
|
|
135
|
+
from src.application.products.create_product import CreateProductUseCase
|
|
136
|
+
from src.api.products.deps import get_create_product_use_case
|
|
137
|
+
|
|
138
|
+
router = APIRouter(prefix="/api/v1/products", tags=["products"])
|
|
139
|
+
|
|
140
|
+
@router.post("", status_code=status.HTTP_201_CREATED, response_model=ProductResponse)
|
|
141
|
+
async def create_product(
|
|
142
|
+
req: CreateProductRequest,
|
|
143
|
+
use_case: Annotated[CreateProductUseCase, Depends(get_create_product_use_case)],
|
|
144
|
+
) -> ProductResponse:
|
|
145
|
+
product = await use_case.execute(name=req.name, price=req.price, stock=req.stock)
|
|
146
|
+
return ProductResponse.model_validate(product)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Rule**: `Annotated[T, Depends(...)]` is the canonical form (FastAPI 0.95+). Routes are thin — parse, call use case, validate response. Business logic stays in `application/`.
|
|
150
|
+
|
|
151
|
+
### Domain entity (no SQLAlchemy / no FastAPI)
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
# src/domain/products/entities.py
|
|
155
|
+
from dataclasses import dataclass
|
|
156
|
+
from decimal import Decimal
|
|
157
|
+
from uuid import UUID
|
|
158
|
+
from datetime import datetime
|
|
159
|
+
|
|
160
|
+
@dataclass(frozen=True)
|
|
161
|
+
class Product:
|
|
162
|
+
id: UUID
|
|
163
|
+
name: str
|
|
164
|
+
price: Decimal
|
|
165
|
+
stock: int
|
|
166
|
+
created_at: datetime
|
|
167
|
+
|
|
168
|
+
def reserve(self, qty: int) -> "Product":
|
|
169
|
+
if qty <= 0:
|
|
170
|
+
raise ValueError("qty must be positive")
|
|
171
|
+
if qty > self.stock:
|
|
172
|
+
raise ValueError("insufficient stock")
|
|
173
|
+
return Product(self.id, self.name, self.price, self.stock - qty, self.created_at)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Domain port + infrastructure adapter (SQLAlchemy 2.0)
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
# src/domain/products/repository.py
|
|
180
|
+
from typing import Protocol, Optional
|
|
181
|
+
from uuid import UUID
|
|
182
|
+
from .entities import Product
|
|
183
|
+
|
|
184
|
+
class ProductRepository(Protocol):
|
|
185
|
+
async def get(self, id: UUID) -> Optional[Product]: ...
|
|
186
|
+
async def save(self, product: Product) -> None: ...
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
# src/infrastructure/products/models.py
|
|
191
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
192
|
+
from sqlalchemy import String, Numeric, DateTime, func
|
|
193
|
+
from datetime import datetime
|
|
194
|
+
from decimal import Decimal
|
|
195
|
+
from uuid import UUID, uuid4
|
|
196
|
+
import sqlalchemy as sa
|
|
197
|
+
from src.infrastructure.db.base import Base
|
|
198
|
+
|
|
199
|
+
class ProductORM(Base):
|
|
200
|
+
__tablename__ = "products"
|
|
201
|
+
id: Mapped[UUID] = mapped_column(sa.Uuid, primary_key=True, default=uuid4)
|
|
202
|
+
name: Mapped[str] = mapped_column(String(120), index=True)
|
|
203
|
+
price: Mapped[Decimal] = mapped_column(Numeric(12, 2))
|
|
204
|
+
stock: Mapped[int] = mapped_column()
|
|
205
|
+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
# src/infrastructure/products/repository_impl.py
|
|
210
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
211
|
+
from sqlalchemy import select
|
|
212
|
+
from uuid import UUID
|
|
213
|
+
from typing import Optional
|
|
214
|
+
from src.domain.products.entities import Product
|
|
215
|
+
from src.domain.products.repository import ProductRepository
|
|
216
|
+
from src.infrastructure.products.models import ProductORM
|
|
217
|
+
|
|
218
|
+
class SqlAlchemyProductRepository(ProductRepository):
|
|
219
|
+
def __init__(self, session: AsyncSession):
|
|
220
|
+
self._session = session
|
|
221
|
+
|
|
222
|
+
async def get(self, id: UUID) -> Optional[Product]:
|
|
223
|
+
row = await self._session.scalar(select(ProductORM).where(ProductORM.id == id))
|
|
224
|
+
return self._to_domain(row) if row else None
|
|
225
|
+
|
|
226
|
+
async def save(self, product: Product) -> None:
|
|
227
|
+
row = ProductORM(
|
|
228
|
+
id=product.id, name=product.name,
|
|
229
|
+
price=product.price, stock=product.stock,
|
|
230
|
+
)
|
|
231
|
+
await self._session.merge(row)
|
|
232
|
+
await self._session.commit()
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
def _to_domain(row: ProductORM) -> Product:
|
|
236
|
+
return Product(
|
|
237
|
+
id=row.id, name=row.name, price=row.price,
|
|
238
|
+
stock=row.stock, created_at=row.created_at,
|
|
239
|
+
)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Rule**: SQLAlchemy 2.0 style — `select(Model).where(...)` not legacy `Model.query`. `Mapped[T]` annotations for static type-check. Repository implements the domain `Protocol`.
|
|
243
|
+
|
|
244
|
+
### Lifespan + dependency injection
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
# src/main.py
|
|
248
|
+
from contextlib import asynccontextmanager
|
|
249
|
+
from fastapi import FastAPI
|
|
250
|
+
from src.infrastructure.db.engine import engine, AsyncSessionLocal
|
|
251
|
+
from src.api.products.router import router as products_router
|
|
252
|
+
|
|
253
|
+
@asynccontextmanager
|
|
254
|
+
async def lifespan(app: FastAPI):
|
|
255
|
+
# Startup: warm-up, health checks against DB, etc.
|
|
256
|
+
yield
|
|
257
|
+
# Shutdown: close engine cleanly
|
|
258
|
+
await engine.dispose()
|
|
259
|
+
|
|
260
|
+
app = FastAPI(
|
|
261
|
+
title="Products Service",
|
|
262
|
+
lifespan=lifespan,
|
|
263
|
+
docs_url=None, # disable in prod via settings
|
|
264
|
+
redoc_url=None,
|
|
265
|
+
)
|
|
266
|
+
app.include_router(products_router)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Rule**: `lifespan` is the canonical startup/shutdown hook (FastAPI 0.93+). Never use deprecated `@app.on_event("startup")` — it does not support async DI correctly.
|
|
270
|
+
|
|
271
|
+
## 5. Testing
|
|
272
|
+
|
|
273
|
+
### Unit (pytest, no FastAPI/DB)
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
# tests/unit/domain/products/test_product.py
|
|
277
|
+
import pytest
|
|
278
|
+
from decimal import Decimal
|
|
279
|
+
from uuid import uuid4
|
|
280
|
+
from datetime import datetime, timezone
|
|
281
|
+
from src.domain.products.entities import Product
|
|
282
|
+
|
|
283
|
+
def test_reserve_decreases_stock():
|
|
284
|
+
p = Product(uuid4(), "x", Decimal("9.90"), 10, datetime.now(timezone.utc))
|
|
285
|
+
p2 = p.reserve(3)
|
|
286
|
+
assert p2.stock == 7
|
|
287
|
+
|
|
288
|
+
def test_reserve_refuses_excess():
|
|
289
|
+
p = Product(uuid4(), "x", Decimal("9.90"), 10, datetime.now(timezone.utc))
|
|
290
|
+
with pytest.raises(ValueError):
|
|
291
|
+
p.reserve(11)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Integration (httpx AsyncClient + Testcontainers Postgres)
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
# conftest.py
|
|
298
|
+
import pytest
|
|
299
|
+
import pytest_asyncio
|
|
300
|
+
from testcontainers.postgres import PostgresContainer
|
|
301
|
+
from httpx import ASGITransport, AsyncClient
|
|
302
|
+
from src.main import app
|
|
303
|
+
|
|
304
|
+
@pytest.fixture(scope="session")
|
|
305
|
+
def postgres():
|
|
306
|
+
with PostgresContainer("postgres:16-alpine") as pg:
|
|
307
|
+
yield pg
|
|
308
|
+
|
|
309
|
+
@pytest_asyncio.fixture
|
|
310
|
+
async def client():
|
|
311
|
+
transport = ASGITransport(app=app)
|
|
312
|
+
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
|
313
|
+
yield ac
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
# tests/integration/api/products/test_create.py
|
|
318
|
+
import pytest
|
|
319
|
+
|
|
320
|
+
@pytest.mark.asyncio
|
|
321
|
+
async def test_create_product_returns_201(client):
|
|
322
|
+
r = await client.post("/api/v1/products", json={
|
|
323
|
+
"name": "widget", "price": "9.90", "stock": 100,
|
|
324
|
+
})
|
|
325
|
+
assert r.status_code == 201
|
|
326
|
+
body = r.json()
|
|
327
|
+
assert body["name"] == "widget"
|
|
328
|
+
assert body["stock"] == 100
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Rule**: TestClient from FastAPI is sync — use `httpx.AsyncClient` with `ASGITransport` for async tests. `pytest-asyncio` strict mode.
|
|
332
|
+
|
|
333
|
+
### Mutation (mutmut)
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
mutmut run --paths-to-mutate=src/domain,src/application
|
|
337
|
+
mutmut results
|
|
338
|
+
# Target: killed/total >= 70% on domain + application
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## 6. Build & run commands
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
# Setup
|
|
345
|
+
uv venv
|
|
346
|
+
uv pip sync requirements.lock # or: uv pip install -e ".[dev]"
|
|
347
|
+
|
|
348
|
+
# Migrations
|
|
349
|
+
alembic upgrade head
|
|
350
|
+
|
|
351
|
+
# Run dev
|
|
352
|
+
uvicorn src.main:app --reload --host 0.0.0.0 --port 8000
|
|
353
|
+
|
|
354
|
+
# Run prod (multiple workers)
|
|
355
|
+
gunicorn src.main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
|
|
356
|
+
|
|
357
|
+
# OR with granian for lower latency
|
|
358
|
+
granian --interface asgi src.main:app --workers 4 --port 8000
|
|
359
|
+
|
|
360
|
+
# Lint + format
|
|
361
|
+
ruff check --fix .
|
|
362
|
+
ruff format .
|
|
363
|
+
|
|
364
|
+
# Type check
|
|
365
|
+
mypy --strict src/
|
|
366
|
+
|
|
367
|
+
# Tests
|
|
368
|
+
pytest # all
|
|
369
|
+
pytest tests/unit/ # fast loop
|
|
370
|
+
pytest --cov=src --cov-branch --cov-fail-under=85
|
|
371
|
+
pytest -m "not slow" # skip Testcontainers
|
|
372
|
+
|
|
373
|
+
# Mutation
|
|
374
|
+
mutmut run --paths-to-mutate=src/domain,src/application
|
|
375
|
+
|
|
376
|
+
# Security scan
|
|
377
|
+
pip-audit
|
|
378
|
+
bandit -ll -r src/
|
|
379
|
+
|
|
380
|
+
# Migrations (always review autogenerate)
|
|
381
|
+
alembic revision --autogenerate -m "add products table"
|
|
382
|
+
alembic upgrade head
|
|
383
|
+
alembic downgrade -1
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## 7. Security (per ADR-007 + ADR-027 — MANDATORY section)
|
|
387
|
+
|
|
388
|
+
### 7.1 Authentication & Authorization
|
|
389
|
+
|
|
390
|
+
- **JWT (stateless)**: `python-jose[cryptography]` + `passlib[bcrypt]`. RS256 (asymmetric) recommended for multi-service setups; HS256 acceptable for single-service.
|
|
391
|
+
- **OAuth2 + OpenID Connect**: `authlib` integrates with Auth0/Keycloak/Cognito.
|
|
392
|
+
- **API keys for internal services**: `secrets.token_urlsafe(32)` + hashed storage. Never plaintext.
|
|
393
|
+
- **Password hashing**: bcrypt cost 12 (passlib default), or Argon2 via `argon2-cffi` (preferred for new projects).
|
|
394
|
+
- **Authorization**: dependency-based. `Depends(get_current_user)` → `Depends(require_role("admin"))` chain. Object-level checks inside the use case.
|
|
395
|
+
|
|
396
|
+
### 7.2 CORS
|
|
397
|
+
|
|
398
|
+
```python
|
|
399
|
+
# src/main.py
|
|
400
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
401
|
+
|
|
402
|
+
app.add_middleware(
|
|
403
|
+
CORSMiddleware,
|
|
404
|
+
allow_origins=["https://app.example.com"], # NEVER ["*"] in prod
|
|
405
|
+
allow_credentials=True,
|
|
406
|
+
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
407
|
+
allow_headers=["Authorization", "Content-Type"],
|
|
408
|
+
)
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### 7.3 Validation & input sanitization
|
|
412
|
+
|
|
413
|
+
- **SQL injection**: SQLAlchemy 2.0 parameterizes by default. NEVER `text(f"SELECT * FROM users WHERE id = {user_input}")` — use `text("... :id").bindparams(id=user_input)`.
|
|
414
|
+
- **NoSQL injection**: if using MongoDB via `motor`, use the typed query builder, not dict concatenation.
|
|
415
|
+
- **Pydantic strictness**: `extra="forbid"` on all request models. Type validation is the first line of defense.
|
|
416
|
+
- **File uploads**: `UploadFile.content_type` is client-supplied — always re-validate via `python-magic` server-side. Never trust the extension.
|
|
417
|
+
|
|
418
|
+
### 7.4 Secrets management
|
|
419
|
+
|
|
420
|
+
```python
|
|
421
|
+
# src/config/settings.py
|
|
422
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
423
|
+
|
|
424
|
+
class Settings(BaseSettings):
|
|
425
|
+
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
|
|
426
|
+
database_url: str
|
|
427
|
+
jwt_secret_key: str
|
|
428
|
+
sentry_dsn: str | None = None
|
|
429
|
+
|
|
430
|
+
settings = Settings()
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
- `.env` gitignored; `env.example` committed.
|
|
434
|
+
- Cloud-native: prefer Secret Manager (AWS / GCP) over `.env` in prod. `pydantic-settings` can read from there.
|
|
435
|
+
|
|
436
|
+
### 7.5 Rate limiting
|
|
437
|
+
|
|
438
|
+
```python
|
|
439
|
+
# pip install slowapi
|
|
440
|
+
from slowapi import Limiter, _rate_limit_exceeded_handler
|
|
441
|
+
from slowapi.errors import RateLimitExceeded
|
|
442
|
+
from slowapi.util import get_remote_address
|
|
443
|
+
|
|
444
|
+
limiter = Limiter(key_func=get_remote_address)
|
|
445
|
+
app.state.limiter = limiter
|
|
446
|
+
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
447
|
+
|
|
448
|
+
@router.post("/login")
|
|
449
|
+
@limiter.limit("5/minute")
|
|
450
|
+
async def login(request: Request, ...): ...
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
- `5/minute` on `/login`, `/signup`, `/password-reset`.
|
|
454
|
+
- `100/minute` on regular authenticated GETs.
|
|
455
|
+
- Behind reverse proxy: use `X-Forwarded-For` (validate the proxy is trusted!).
|
|
456
|
+
|
|
457
|
+
### 7.6 OWASP Top 10 mapping
|
|
458
|
+
|
|
459
|
+
| OWASP | Mitigation in FastAPI 0.115 |
|
|
460
|
+
|---|---|
|
|
461
|
+
| A01 Broken Access Control | `Depends(require_role(...))` per route; object-level check inside use case; default-deny pattern |
|
|
462
|
+
| A02 Cryptographic Failures | Argon2/bcrypt password hashing; TLS terminated at proxy with HSTS; JWT RS256 with kid rotation |
|
|
463
|
+
| A03 Injection | SQLAlchemy parameterization; Pydantic `extra="forbid"`; never `text()` with f-strings |
|
|
464
|
+
| A04 Insecure Design | DDD layering enforced (`domain/` no FastAPI); use case = single async callable; ADRs document deviations |
|
|
465
|
+
| A05 Security Misconfiguration | `docs_url=None, redoc_url=None` in prod; `DEBUG=False` via env split; explicit CORS allowlist |
|
|
466
|
+
| A06 Vulnerable Components | `pip-audit` in CI; `renovate` for PR bumps; `bandit` for code patterns |
|
|
467
|
+
| A07 Auth Failures | slowapi rate limit on auth endpoints; account lock via Redis counter; MFA via `pyotp` |
|
|
468
|
+
| A08 Data Integrity | `uv pip install --require-hashes`; SBOM via `cyclonedx-py`; signed releases |
|
|
469
|
+
| A09 Logging Failures | Structured JSON via `structlog`; correlation ID via `asgi-correlation-id`; **NEVER** log request body raw |
|
|
470
|
+
| A10 SSRF | Allowlist outbound HTTP via centralized `httpx.AsyncClient` with explicit allowed hosts; reject `localhost`, `169.254.169.254`, private CIDRs by default |
|
|
471
|
+
|
|
472
|
+
### 7.7 LGPD / GDPR / compliance specifics
|
|
473
|
+
|
|
474
|
+
- **PII fields tagged**: convention `pii=True` in column comments + custom mypy check.
|
|
475
|
+
- **Soft delete**: `is_deleted` + `deleted_at` columns; filter via SQLAlchemy event listener that injects `WHERE is_deleted = false`.
|
|
476
|
+
- **Data subject access (Article 15)**: `GET /api/v1/me/export` returns all user data as ZIP/JSON.
|
|
477
|
+
- **Erasure (Article 17)**: `DELETE /api/v1/me` redacts PII columns while preserving FKs (orders kept, customer = null).
|
|
478
|
+
- **Encryption at rest**: PostgreSQL TDE (cloud-managed) or column-level via `sqlalchemy-utils` `EncryptedType`.
|
|
479
|
+
|
|
480
|
+
## 8. Anti-patterns (block in code-review)
|
|
481
|
+
|
|
482
|
+
| ❌ Bad | ✅ Good | Why |
|
|
483
|
+
|---|---|---|
|
|
484
|
+
| `def` async route that does sync I/O (e.g., `requests.get`) | `async def` + `httpx.AsyncClient` OR `def` sync route with sync I/O | Sync I/O inside `async def` blocks the event loop and kills throughput |
|
|
485
|
+
| `extra="allow"` on request `BaseModel` (default `extra="ignore"`) | `extra="forbid"` explicitly | Silent acceptance of typos / unintended fields = footgun |
|
|
486
|
+
| Returning ORM object directly from route | `response_model=Schema` + `Schema.model_validate(obj)` | API contract leaks data model otherwise |
|
|
487
|
+
| `Base.metadata.create_all()` in `lifespan` startup | Alembic migrations, applied separately | Auto-create skips review and migration history |
|
|
488
|
+
| `Session.commit()` inside a use case AND inside repository | Commit at exactly one layer (typically the use case via UnitOfWork) | Double commit / nested transactions = subtle bugs |
|
|
489
|
+
| SQLite for tests when prod is Postgres | Testcontainers Postgres 16 | Subtle SQL differences cause prod-only bugs |
|
|
490
|
+
| `engine = create_engine(url, echo=True)` in prod | `echo=False` in prod; `echo=True` only for local debugging | Logs every query; PII leakage; performance hit |
|
|
491
|
+
| Catching `Exception:` in routes | Custom exception handlers via `app.add_exception_handler` | Hides bugs and breaks 500 telemetry |
|
|
492
|
+
| Storing JWT secret in code | `pydantic_settings` from env, validated at startup | Secret in code = secret in git history |
|
|
493
|
+
| Disabling `docs_url`/`redoc_url` via global if/else | Settings flag + `docs_url=settings.openapi_url` pattern | Centralized config wins |
|
|
494
|
+
| Returning `dict` directly without a Pydantic response model | `response_model=...` declared explicitly | Type safety + OpenAPI schema correctness |
|
|
495
|
+
|
|
496
|
+
## 9. Migration hints — FastAPI 0.95 → 0.115
|
|
497
|
+
|
|
498
|
+
Breaking changes worth flagging when `migrator` agent runs FastAPI 0.95+ → 0.115:
|
|
499
|
+
|
|
500
|
+
- **Pydantic v1 unsupported** (FastAPI 0.100+). All `BaseModel`/`Field`/`validator` imports must use Pydantic v2. `@validator` → `@field_validator`. `Config` class → `model_config = ConfigDict(...)`.
|
|
501
|
+
- **`Annotated` dependencies are the canonical form**. `param: SomeType = Depends(...)` still works but is being phased out. Migrate to `param: Annotated[SomeType, Depends(...)]`.
|
|
502
|
+
- **`lifespan` replaces `on_event`** (FastAPI 0.93+). `@app.on_event("startup")` is deprecated; use `lifespan` context manager.
|
|
503
|
+
- **`response_model_exclude_unset`** behavior changed slightly when combined with `response_model_by_alias`. Test both options if you depend on aliasing.
|
|
504
|
+
- **WebSocket** API gained stricter typing. If you use `WebSocket.receive_json()` patterns from <0.100, double-check.
|
|
505
|
+
- **OpenAPI 3.1**: FastAPI 0.99+ emits OpenAPI 3.1 by default. Older clients may need explicit `openapi_version="3.0.0"` or use the `openapi_extra` shim.
|
|
506
|
+
|
|
507
|
+
Hand off to `migrator` with: current FastAPI version, current Pydantic version, list of routes using `Depends()` without `Annotated`, list of `@app.on_event` handlers.
|
|
508
|
+
|
|
509
|
+
## 10. References
|
|
510
|
+
|
|
511
|
+
- [FastAPI 0.115 docs](https://fastapi.tiangolo.com/)
|
|
512
|
+
- [Pydantic v2 migration guide](https://docs.pydantic.dev/latest/migration/)
|
|
513
|
+
- [SQLAlchemy 2.0 docs](https://docs.sqlalchemy.org/en/20/)
|
|
514
|
+
- [Alembic docs](https://alembic.sqlalchemy.org/en/latest/)
|
|
515
|
+
- [pytest-asyncio docs](https://pytest-asyncio.readthedocs.io/)
|
|
516
|
+
- [slowapi (rate limiting)](https://github.com/laurentS/slowapi)
|
|
517
|
+
- [asgi-correlation-id](https://github.com/snok/asgi-correlation-id)
|
|
518
|
+
- [OWASP REST Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html)
|
|
519
|
+
- ADR-007 (Senior+ gate thresholds — coverage ≥85%, mutation ≥70%)
|
|
520
|
+
- ADR-026 (Generic agents + stack packs architecture)
|
|
521
|
+
- ADR-027 (Pack governance — frontmatter + security mandatory + CODEOWNERS + annual review)
|
|
522
|
+
- ADR-029 (Canonical pack format — this document follows it)
|