Squash-merge lib-standardisation: adopt repo standards across packaging, tooling, docs, and CI. - standardize pyproject.toml (canonical packaging, lint config, extras, GSDFC pydoclint) - add py.typed marker, modernize type hints and Google-style docstrings - nest generated docs under docs/lib and docs/mcp; add hand-written wiki - adopt standard Type A PyPI CI pipeline (tag==version guard + quality gate) - promote generated CHANGELOG.md; bump version to 0.0.7
96 lines
5.0 KiB
Markdown
96 lines
5.0 KiB
Markdown
# Codegen — Generate Models & Routes From Your Spec
|
||
|
||
> **TL;DR** — point `openapi-first models` / `openapi-first routes` at your spec and get Pydantic models plus operation-matching route stubs. The output is deterministic and is a *starting point* — not a maintained artifact.
|
||
|
||
---
|
||
|
||
## 🧭 1. Why Celebrate Codegen?
|
||
|
||
Two pain points kill OpenAPI projects:
|
||
|
||
1. **The "write the spec, then write the same thing as Pydantic models" step** — exactly where server/client param types drift (your route says `item_id: int`, your client sends a string…).
|
||
2. **Typing handler names by hand** when `bind_routes` requires each `operationId` to resolve to a function of the exact same name.
|
||
|
||
Codegen collapses both. One command, one source file, the same shapes everywhere.
|
||
|
||
---
|
||
|
||
## 🚀 2. Model Generation
|
||
|
||
```bash
|
||
openapi-first models openapi.yaml -o app/_gen/models.py
|
||
```
|
||
|
||
The generated module mirrors the spec's `components.schemas` **by name**:
|
||
|
||
| Spec | Generated |
|
||
|------|-----------|
|
||
| `components.schemas.User` | `class User(BaseModel)` |
|
||
| `components.schemas.Item` | `class Item(BaseModel)` |
|
||
| `required + type` from schema | Pydantic `Field(...)` / type hints ✓ |
|
||
|
||
**Never hand-edit those files.** If the spec changes, regenerate — just like re-running a compiler after editing source.
|
||
|
||
### Why constructor-time validation still applies
|
||
|
||
Codegen doesn't change the design: the generated models are plain Pydantic, and the **client/server still validate against the spec** at startup. Codegen is a *convenience accelerator* on top of the fail-fast guarantees in [04 – Design](../04_design.md) and [06 – Error Handling](../06_error_handling.md).
|
||
|
||
> **Wire model vs. domain model.** Generated models are the **wire contract**. Keep them separate from your persisted/domain model (see the standards *CRUD Template*): give the wire schema an `id: str`, but model the Mongo document so the id is optional.
|
||
|
||
---
|
||
|
||
## 🔁 3. Route Generation
|
||
|
||
```bash
|
||
openapi-first routes openapi.yaml -o app/interfaces \
|
||
--use-models --models-module app._gen.models \
|
||
--inject-request --package
|
||
```
|
||
|
||
One file per resource (derived from the first path segment). Flags:
|
||
|
||
| Flag | Effect |
|
||
|------|--------|
|
||
| `--use-models` | Import request-body `$ref` models from `--models-module` |
|
||
| `--models-module MOD` | Dotted module for those imports (default `models`) |
|
||
| `--inject-request` | Prepend `request: Request` to every handler (DI convention) |
|
||
| `--package` | Emit `<resource>/routes.py` packages (with `__init__.py`) instead of flat `<resource>.py` |
|
||
| `--template-style` | Emit `async def` handlers with a JSON body parameter named `body` (the standards-template convention) plus a service-resolution hint, instead of sync `def` handlers taking `payload` |
|
||
|
||
Why bother? Because `bind_routes` (in [02 – Components](../02_components.md)) needs an `operationId` → handler **exactly** matching the spec. Codegen guarantees you never type a handler name wrong — it wears the same `operationId` the spec declares.
|
||
|
||
### Resource names and extra media types
|
||
|
||
- A path such as `/fetch-requests` yields a **sanitized** module name `fetch_requests.py` (a raw hyphen could never be imported).
|
||
- `multipart/form-data` and `application/x-www-form-urlencoded` bodies expand to one parameter per schema property: `UploadFile = File(...)` for `format: binary`, `Form(...)` otherwise — not an opaque `payload: dict`. Fields are required only when listed in the schema's `required`.
|
||
|
||
The stubs `raise NotImplementedError`; fill each body in (typically by resolving a service from `request.app.state`).
|
||
|
||
### Handler shapes
|
||
|
||
Both shapes bind equally — the router resolves the handler by `operationId` and introspects the FastAPI signature, so it never inspects the request body:
|
||
|
||
- **Default (sync stub):** `def create_item(payload: ItemInput, response: Response):` — the body parameter is `payload`.
|
||
- **`--template-style`:** `async def create_item(request: Request, body: ItemInput, response: Response):` — matches the standards template (`async def`, `body` parameter, a `request.app.state.db_resources["<resource>_svc"]` hint).
|
||
|
||
Use `--template-style` when generating against the standards template so the scaffold matches the hand-written handlers you will keep.
|
||
|
||
> **Note:** `models` / `routes` are build-time tooling. They do **not** run at runtime, and templates (the Bake-your-stuff section of [02 – Templates](02_templates.md)) scaffold a working server out-of-the-box for greenfield projects.
|
||
|
||
---
|
||
|
||
## 🧪 4. Idempotent Output
|
||
|
||
Generated output is deterministic w.r.t. the spec:
|
||
- Same spec → byte-identical files (unless you hand-edit — you won't)
|
||
- Ordering follows spec declaration order
|
||
- No timestamps, no machine names, no hidden randomness
|
||
|
||
That determinism is what lets you `git diff` after a spec change and see **exactly** what moved.
|
||
|
||
---
|
||
|
||
## 🧩 Related
|
||
|
||
- [01 – Overview](../01_overview.md) · [02 – Templates](02_templates.md) · [03 – Client](03_client.md) · [04 – Design](../04_design.md) · [07 – Testing](../07_testing.md)
|