docs: add OpenAPI-first wiki (overview, components, use cases for templates/client/codegen, design, security, error handling, testing) and refresh lib docs index

This commit is contained in:
2026-09-15 22:29:05 +05:30
parent adeb02e162
commit b559323dfe
26 changed files with 1590 additions and 86 deletions

View File

@@ -1,4 +1,3 @@
# openapi_first
::: openapi_first
- [Openapi First](openapi_first/)

74
docs/mkdocs.wiki.yml Normal file
View File

@@ -0,0 +1,74 @@
theme:
name: material
palette:
scheme: slate
primary: blue grey
accent: teal
font:
text: Roboto
code: JetBrains Mono
features:
- navigation.sections
- navigation.expand
- navigation.top
- navigation.instant
- navigation.tracking
- navigation.indexes
- content.code.copy
- content.code.annotate
- content.tabs.link
- content.action.edit
- search.highlight
- search.share
- search.suggest
- navigation.tabs
- toc.integrate
- header.autohide
- announce.dismiss
- footer.social
- content.code.select
- content.code.line_numbers
- content.tooltips
icon:
logo: material/api
repo: fontawesome/brands/github
markdown_extensions:
- pymdownx.superfences
- pymdownx.inlinehilite
- pymdownx.snippets
- admonition
- pymdownx.details
- pymdownx.highlight:
linenums: true
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.tabbed:
alternate_style: true
- pymdownx.tasklist:
custom_checkbox: true
- tables
- footnotes
- pymdownx.caret
- pymdownx.tilde
- pymdownx.mark
extra_css:
- https://unpkg.com/dracula-prism/dist/css/dracula-prism.css
plugins:
- search
site_name: openapi_first
docs_dir: wiki
site_dir: ../site/wiki
nav:
- Home: index.md
- Overview: 01_overview.md
- Components: 02_components.md
- Use Cases:
- Quickstart: 03_use_cases/01_quickstart.md
- Templates: 03_use_cases/02_templates.md
- Client: 03_use_cases/03_client.md
- Codegen: 03_use_cases/04_codegen.md
- Design: 04_design.md
- Security: 05_security.md
- Error Handling: 06_error_handling.md
- Testing: 07_testing.md

157
docs/wiki/01_overview.md Normal file
View File

@@ -0,0 +1,157 @@
# Overview — The OpenAPI-First Mental Model
`openapi-first` inverts the usual FastAPI workflow. Instead of decorating routes in code and letting FastAPI *invent* an OpenAPI document for you, you write **one OpenAPI document first** and let the library assemble both the application **and** the client from it. The spec is the contract; code is the implementation.
---
## 🧠 1. The Mental Model
### 1.1 One source of truth
Your OpenAPI document (`openapi.yaml` or `openapi.json`) is the **single authoritative contract**:
```yaml
paths:
/health:
get:
operationId: get_health
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
```
Every route, method, parameter, schema, and security requirement lives **here** — and only here. Code never declares routes.
### 1.2 `operationId` is the binding key
The only bridge between the spec and your Python code is the `operationId`. Each operation maps, **by name**, to exactly one plain callable:
```python
# routes.py
def get_health():
"""Health check operation handler."""
return {"status": "ok"}
```
`openapi_first` resolves `operationId: get_health``routes.get_health` and registers the route. **No decorators, no `@app.get`, no routing metadata in code.**
Guarantees this binding provides:
- 🚫 No undocumented route can exist — every route *must* be in the spec
- 🚫 No spec operation can go unhandled — every `operationId` *must* resolve at startup
- 🔒 No auth can be bypassed — security is injected from `securitySchemes` + per-operation `security`, spec-driven
- 🧱 No drift possible — the server and client are built from the *same* document
### 1.3 Fail-fast by design
Contract violations are detected **at application startup** (or client construction), never silently at request time:
| What goes wrong | When it fails |
|------------------------------------------|----------------------|
| Invalid / unloadable spec | `load_openapi` at startup |
| Spec fails OpenAPI 3.x validation | `load_openapi` at startup |
| `operationId` missing a handler | `bind_routes` at startup |
| Operation declared but no `operationId` | `bind_routes` at startup |
| Missing / duplicate `operationId` in client | `OpenAPIClient(...)` construction |
---
## ⚙️ 2. What It Looks Like
### 2.1 Scaffold an application
```bash
openapi-first scaffold health_app my-health-service
openapi-first scaffold --list
```
`scaffold` copies a bundled template (verbatim — no code generation, no mutation) into a directory of your choice.
### 2.2 Bootstrap the server
```python
# main.py
from openapi_first.app import OpenAPIFirstApp
import routes
app = OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=routes,
title="My Service",
)
```
Run with your FastAPI-compatible server (ASGI):
```bash
uvicorn main:app --reload
```
FastAPI itself drives the server; every route, response model, and security dependency comes from the spec. `/openapi.json` and Swagger UI always reflect the spec, byte-for-byte.
### 2.3 Talk to it with the client
```python
# client-side
from openapi_first.loader import load_openapi
from openapi_first.client import OpenAPIClient
spec = load_openapi("openapi.yaml")
client = OpenAPIClient(spec)
response = client.get_health() # operationId-driven call
print(response.status_code) # 200
print(response.json()) # {"status": "ok"}
```
`OpenAPIClient` builds one callable per `operationId` from the *same* spec, so the client can never drift from the server.
---
## 🧩 3. Where Things Live
| Concern | Module |
|------------------------|---------------------|
| Load + validate spec | `openapi_first.loader` |
| Boot FastAPI app | `openapi_first.app` |
| Bind routes by opId | `openapi_first.binder` |
| HTTP client | `openapi_first.client` |
| Error hierarchy | `openapi_first.errors` |
| Security dependencies | `openapi_first.security` |
| Pydantic model codegen | `openapi_first.codegen` (uses `datamodel_code_generator`) |
| Route stub codegen | `openapi_first.codegen_routes` |
| Models / routes CLI | `openapi_first.cli` |
| Bundled templates | `openapi_first.templates` |
Jurisdictions:
- `loader`, `app`, `binder`, `client`, `errors`, `security` are the **library API surface** — stable, tested, documented.
- `templates` are **copyable scaffolds** — not part of the library API; excluded from lint/format/type gates, never imported at runtime.
---
## 📄 4. The Non-Goals
`openapi-first` deliberately does **not**:
- Parse decorators to generate an OpenAPI schema (that's default FastAPI behavior — the inverse)
- Generate models from code at runtime (only at **build time** via CLI, from spec → Pydantic)
- Validate request/response bodies against the spec at runtime (contract is enforced at startup / client construction; FastAPI + Pydantic handle runtime coercion)
- Invent routing from path conventions — `operationId` binding only
- Ship a production feature set in the bundled templates (they're demos/scaffolds: in-memory stores, no concurrency, no auth configured)
---
## 🧭 5. Path Forward
New here? Start with [01 Quickstart](03_use_cases/01_quickstart.md). Want to copy a runnable app? Jump to [02 Templates](03_use_cases/02_templates.md). Digging into internals? See [Design](04_design.md).
---
## Related
- [01 Quickstart](03_use_cases/01_quickstart.md) · [02 Components](02_components.md) · [01 Overview](01_overview.md)

208
docs/wiki/02_components.md Normal file
View File

@@ -0,0 +1,208 @@
# Components — What Ships in the Box
The library is deliberately small. Everything you need to run an OpenAPI-first service and talk to it with a strict client fits in a handful of modules.
---
## 🗂️ 1. Module Map
| Module | Responsibility | Import |
|---------------------------------|---------------------------------------------------|--------|
| `openapi_first.loader` | Load/validate the spec, resolve `{ENV_VAR}` | `load_openapi` |
| `openapi_first.app` | OpenAPI-first FastAPI application bootstrap | `OpenAPIFirstApp` |
| `openapi_first.binder` | Spec → route binding via `operationId` | `bind_routes` |
| `openapi_first.client` | `operationId`-driven HTTP client | `OpenAPIClient` |
| `openapi_first.errors` | Explicit error hierarchy | `OpenAPIFirstError`, `OpenAPIClientError`, `MissingOperationHandler` |
| `openapi_first.security` | Security-dependency construction from the spec | `parse_security_schemes`, `make_security_dependencies` |
| `openapi_first.codegen` | Pydantic model generation (build-time) | `generate_models` |
| `openapi_first.codegen_routes` | Route-handler stub generation (build-time) | `generate_routes` |
| `openapi_first.cli` | `scaffold` / `models` / `routes` command surface | CLI entry point |
| `openapi_first.templates` | Copyable application templates (NOT library API) | `openapi-first scaffold` |
---
## ⚙️ 2. `loader` — Load & Validate
`load_openapi(path: str | Path) -> dict[str, Any]` (in `openapi_first/loader.py`).
`loader.py` ensures a spec is real, readable, parseable, and schema-valid *before* anything else runs — a golden rule of fail-fast.
```python
from openapi_first.loader import load_openapi
spec = load_openapi("openapi.yaml")
```
Behavior:
- Accepts `.json`, `.yaml`, `.yml` (parsed by extension).
- Runs strict OpenAPI 3.x validation (`openapi-spec-validator`) at load time.
- Raises `OpenAPISpecLoadError` on: missing file, unparseable content, or spec-validation failure.
- Does **not** modify, coerce, or "fix" the spec — it's a read-only gate.
**Env-var resolution** lives at the *security* layer rather than the loader (see [Security](05_security.md)).
---
## 🧬 3. `app` — The Application Bootstrap
`OpenAPIFirstApp` (in `openapi_first/app.py`) is a FastAPI subclass that replaces manual route registration with OpenAPI-driven binding.
```python
from openapi_first.app import OpenAPIFirstApp
import routes
app = OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=routes,
title="My Service",
)
```
**Startup pipeline (fail-fast, in this order):**
1. **Load** the spec (`.yaml`/`.json`).
2. **Validate** it against OpenAPI 3.x schema.
3. **Parse** `securitySchemes` and per-operation `security`.
4. **Build** per-route security dependencies.
5. **Bind** every path/method → handler by `operationId`; a missing handler, a missing `operationId` on a declared operation, or an unbound operation raises at startup.
**Guarantees:**
- Every route has a spec declaration (no undocumented routes).
- Every spec operation has a handler (no unhandled operations).
- Auth enforcement is spec-driven, not hand-wired.
- `/openapi.json` + Swagger UI always reflect the provided spec.
Keyword arguments beyond `openapi_path` / `routes_module` pass straight through to `fastapi.FastAPI` (it's a subclass — `title`, `version`, middleware, lifespan, … all work).
---
## 🔗 4. `binder` — Spec → Route Binding
`bind_routes(app, spec, routes_module, security_deps=None) -> None` (in `openapi_first/binder.py`).
This is the heart of the OpenAPI-first guarantee. For each `path` + HTTP method in the spec:
1. Reads the operation's `operationId`.
2. Looks up `routes_module.<operationId>` — a **plain callable**.
3. Registers a FastAPI `APIRoute` bound to that handler, injecting `Depends(...)` for any matching security requirements.
Failures are explicit and early:
| Condition | Raised |
|---------------------------------------|---------------------------------------|
| No `operationId` on an operation | `MissingOperationHandler` |
| `operationId` has no handler function | `MissingOperationHandler` |
| A path/method declared but unbound | `MissingOperationHandler` |
Handlers stay framework-agnostic: they're plain functions `(payload, id, response)` named after `operationId`s — no decorators, no routing metadata.
---
## 📡 5. `client` — The Other Side of the Contract
`OpenAPIClient(spec, base_url=None, client=None)` (in `openapi_first/client.py`).
The same spec that builds the server builds its client — one callable per `operationId`, keyed by name:
```python
from openapi_first.loader import load_openapi
from openapi_first.client import OpenAPIClient
spec = load_openapi("openapi.yaml")
client = OpenAPIClient(spec)
response = client.get_health()
response = client.get_user(path_params={"user_id": 1})
response = client.create_user(body={"name": "Ada"})
```
How operations become methods:
- Each `operationId` → a dynamically-built callable on the client.
- **Path parameters** → `path_params={"user_id": 1}`.
- **Request body** → `body={...}` (JSON) or raw content for non-JSON media types.
- **Query/headers** → `query=` / `headers=`.
- Returns the raw `httpx.Response` (no implicit deserialization, no hidden schema inference).
Client-construction guarantees (fail-fast, same philosophy as the app):
- Spec must declare at least one `servers` entry (`base_url` falls back to it).
- Spec must have `paths`.
- Every operation must have a **unique** `operationId`.
- Required params must match the spec at call time.
Construction errors raise `OpenAPIClientError`; request-time errors surface as `httpx` exceptions with `operationId` context.
---
## 💥 6. `errors` — Explicit Error Hierarchy
Everything raised by `openapi-first` derives from `OpenAPIFirstError` (in `openapi_first/errors.py`), so callers can handle first-party failures with a single `except`:
- `OpenAPIFirstError` — base (comparable / aims to be picklable share).
- `OpenAPIClientError` — client-side contract violations.
- `MissingOperationHandler` — spec declares an operation whose handler is missing or unresolvable; carries `path`, `method`, and optional `operationId`.
See [Error Handling](06_error_handling.md) for the full table.
---
## 🛡️ 7. `security` — Auth from the Spec
`security.py` turns an OpenAPI `securitySchemes` section into FastAPI `Depends(...)` objects — **no manual middleware**.
- `parse_security_schemes(spec) -> dict[str, dict]` — collects schemes, resolving `{ENV_VAR}` placeholders.
- `make_security_dependencies(spec, security_schemes) -> dict[str, list[Depends]]` — builds `METHOD:/path` → dependency list from per-operation `security`, falling back to top-level `security`.
Supported scheme types (extensible):
- `type: http, scheme: bearer``OpenAPIFirstSecurityDependency(HTTPBearer)`. With `x-introspect-path`/`x-server-url`, it introspects the JWT against an auth service; optionally sets `request.state.user`. Without introspection, it validates token presence and stores it on `request.state.token`.
- API-key style schemes via the same resolution path.
Key properties: resolution is **per-operation**, env placeholders resolve once at startup, and dependencies are injected by `binder` — your handlers never mention security.
---
## 🏭 8. `codegen` & `cli` — Build-Time Tooling
Generation is strictly **build-time** — spec → code, run once by a developer, committed:
### `openapi-first models <spec> -o <file>`
Generates Pydantic models from spec schemas (wraps `datamodel_code_generator`):
```python
# cli path
openapi-first models openapi.yaml -o app/models.py
```
### `openapi-first routes <spec> -o <dir> [--use-models] [--models-module models]`
Generates one `routes_<resource>.py` stub file per resource, with `NotImplementedError` handlers bound to `operationId`s (optionally importing your generated models):
```python
openapi-first routes openapi.yaml -o app/routes --use-models
```
### `openapi-first scaffold <template> [path]`
Copies a bundled application template verbatim (see [Templates](03_use_cases/02_templates.md)).
---
## 🧱 9. `templates` — Copyable Applications
Four bundled, runnable applications live under `openapi_first/templates/`: `health_app`, `crud_app`, `model_app`, and `vet_app`. They are **not** part of the library API — no lint/format/type gates, never imported at runtime. They exist to be copied via:
```bash
openapi-first scaffold <template> [target-dir]
openapi-first scaffold --list
```
---
## 🔗 Related
- [01 Overview](01_overview.md) · [02 Templates](03_use_cases/02_templates.md) · [03 Security](05_security.md) · [04 Design](04_design.md)

View File

@@ -0,0 +1,122 @@
# Use Case 1: Quickstart — Build Your First OpenAPI-First Service
This guide walks you from an empty directory to a running, contract-driven service in a few minutes, then talks to it with the generated client.
---
## 🛠️ 1. Prerequisites
- Python 3.10+
- `openapi-first` installed (see [Overview](../01_overview.md#installation))
- `pip install "fastapi[standard]"` (or `uvicorn`) to run the app
---
## 📄 2. Write the OpenAPI document
OpenAPI comes first. Create `openapi.yaml`:
```yaml
openapi: 3.0.3
info:
title: Greeting Service
version: 1.0.0
servers:
- url: http://localhost:8000
paths:
/greet/{name}:
get:
operationId: get_greeting
parameters:
- name: name
in: path
required: true
schema:
type: string
responses:
"200":
description: A greeting
content:
application/json:
schema:
type: object
properties:
greeting:
type: string
```
Key points: every operation needs `operationId`, and every route must exist *only* here.
---
## 🧑‍💻 3. Write the handlers
Create `routes.py` — plain functions, no decorators, named exactly like the `operationId`s:
```python
# routes.py
def get_greeting(name: str) -> dict:
"""Return a greeting for the given name."""
return {"greeting": f"Hello, {name}!"}
```
If a handler is missing at startup, the app **refuses to boot** (`MissingOperationHandler`) — the fail-fast guarantee catches contract drift immediately.
---
## 🚀 4. Bootstrap the app
Create `main.py`:
```python
# main.py
from openapi_first.app import OpenAPIFirstApp
import routes
app = OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=routes,
title="Greeting Service",
)
```
Run it:
```bash
uvicorn main:app --reload
```
Visit `http://localhost:8000/docs` (Swagger UI) and `http://localhost:8000/openapi.json` — both are generated from *your* spec.
---
## 📡 5. Call it with the client
The same spec builds a strict client:
```python
# client.py
from openapi_first.loader import load_openapi
from openapi_first.client import OpenAPIClient
spec = load_openapi("openapi.yaml")
client = OpenAPIClient(spec)
response = client.get_greeting(path_params={"name": "Ada"})
print(response.status_code) # 200
print(response.json()) # {"greeting": "Hello, Ada!"}
```
---
## 💡 6. Next Steps
- Copy a fuller example: [02 Templates](02_templates.md)
- Generate models/routes from a bigger spec: [04 Codegen](04_codegen.md)
- Drive everything from a client: [03 Client](03_client.md)
---
## Related
- [01 Overview](../01_overview.md) · [02 Components](../02_components.md) · [01 Quickstart](01_quickstart.md)

View File

@@ -0,0 +1,167 @@
# Use Case 2: Templates — Copyable Reference Applications
`openapi-first` ships four runnable, copyable applications under `openapi_first/templates/`. They are **not part of the library API** — they are bundled scaffold examples you copy into your own project and build on.
---
## 🎬 1. What Templates Are
A template is a complete, self-contained OpenAPI-first service:
- A bundled directory inside `openapi_first/templates/<name>/`
- Copyable **verbatim** — no code generation, no mutation — via the CLI
- Each one demonstrates a specific set of OpenAPI-first behaviors and FastAPI features
All templates share the same skeleton:
```
<name>_app/
├── __init__.py # explains the template + how to scaffold it
├── openapi.yaml # the contract (source of truth)
├── main.py # assembles OpenAPIFirstApp from the spec
├── routes.py # operationId-bound handler functions
└── data.py # in-memory data store (demo only)
```
---
## 📋 2. The Four Templates
### 2.1 `health_app` — minimal liveness probe
```bash
openapi-first scaffold health_app
# or into a custom directory:
openapi-first scaffold health_app my-health-service
```
| File | Purpose |
|------|---------|
| `openapi.yaml` | `GET /health``operationId: get_health` |
| `routes.py` | `get_health()` returns `{"status": "ok"}` |
| `main.py` | `OpenAPIFirstApp(openapi_path="openapi.yaml", routes_module=routes)` |
**Why it exists:** the absolute minimal OpenAPI-first round trip — one operation, one handler, zero moving parts. The best starting point to internalize the mental model.
**Smoke test:**
```bash
pip install -e .
uvicorn main:app
curl http://localhost:8000/health
# → {"status": "ok"}
```
### 2.2 `crud_app` — dict-based CRUD
```bash
openapi-first scaffold crud_app my-crud-service
```
| File | Purpose |
|------|---------|
| `openapi.yaml` | Full CRUD over `/items` (list/get/create/update/delete) |
| `routes.py` | Handlers bound via `operationId`s `list_items`, `get_item`, `create_item`, `update_item`, `delete_item` |
| `data.py` | In-memory dict store with auto-incrementing `id` |
Behaviors you learn:
- **Explicit status codes** — `create_item`/`delete_item` take `response: Response` and set `201`/`204`; `get_item`/`update_item` raise `HTTPException(404)` on `KeyError`
- **Handlers as plain callables** — no FastAPI decorators, routing comes solely from the spec
- **Mock data store** — `data.py` is a copyable in-memory store, explicitly **not** production-ready
### 2.3 `model_app` — Pydantic model CRUD
```bash
openapi-first scaffold model_app my-model-service
```
| File | Purpose |
|------|---------|
| `openapi.yaml` | Same CRUD surface, schemas reference models |
| `models.py` | Pydantic `Item`, `ItemCreate`, `ItemBase` (request/response models) |
| `routes.py` | Handlers type-annotated with the models; `create_item` sets `201` |
| `data.py` | In-memory store returning real model instances |
Behaviors you learn:
- **Pydantic request/response models** — payloads validated and serialized via FastAPI
- **Same handler contracts** — identical `operationId` set as `crud_app`, so the two are interchangeable
- **Models in the client too** — the same spec drives `OpenAPIClient` body handling
### 2.4 `vet_app` — the full-featured demo
```bash
openapi-first scaffold vet_app my-vet-clinic
```
| File | Purpose |
|------|---------|
| `openapi.yaml` | Five resources (parents, vets, treatments, pets, appointments) + SSE + upload + discriminated unions |
| `models.py` | Pydantic models incl. **discriminated unions** (`noteType` literal fields) |
| `routes.py` | ~20 handlers across all resources, incl. pagination, filtering, photo upload, SSE streaming |
| `sse.py` | Server-Sent Events helper (`StreamingResponse`, per-pet subscriber queues, background `asyncio` workers) |
| `data.py` | Larger in-memory store (parents → vets → treatments → pets → appointments) |
| `main.py` | App + CORS + lifespan example |
Behaviors you learn — the *advanced* tier:
- **Discriminated unions** — `ProcedureNotes` uses `oneOf` + `discriminator.noteType` mapping; Pydantic models use `Literal[...]` discriminator fields
- **SSE streaming** — a `GET /pets/{id}/actions` operation streaming `text/event-stream` via `StreamingResponse` with background task workers
- **File upload** — `UploadFile` handler setting a multi-part body
- **CORS + middleware** — `add_middleware(CORSMiddleware, ...)` alongside the spec-driven setup
- **Response injection** — handlers set `201`/`204` explicitly via injected `Response`
---
## 🚀 3. CLI Reference
```bash
# List available templates
openapi-first scaffold --list
# Copy a template into its default directory (template name, dashes)
openapi-first scaffold health_app
# Copy into a custom target directory
openapi-first scaffold crud_app my-project/crud
```
> **Protip:** `DEFAULT_TEMPLATE` is `health_app`, so `openapi-first scaffold` with no template name scaffolds the health app.
---
## 🧩 4. Anatomy of a Scaffolded Service
After `openapi-first scaffold health_app my-health-service`, your directory contains a drop-in FastAPI service:
```
my-health-service/
├── openapi.yaml # THE contract
├── main.py # `app = OpenAPIFirstApp(openapi_path=..., routes_module=routes)`
└── routes.py # `def get_health(): ...`
```
Run it:
```bash
cd my-health-service
pip install -e .
uvicorn main:app --reload
```
`/docs`, `/openapi.json`, and every declared route now exist — all derived from `openapi.yaml`.
---
## ⚠️ 5. Production Disclaimer
Templates use **in-memory, non-persistent, non-concurrency-safe** data stores. They are learning scaffolds — **not** production references. Swap in a real data layer (SQL/REDIS/object store) the moment you go beyond a demo.
See the `__init__.py` of each template for detailed client examples, CLI examples, and design notes.
---
## Related
- [01 Quickstart](01_quickstart.md) · [04 Codegen](04_codegen.md) · [02 Components](../02_components.md)

View File

@@ -0,0 +1,156 @@
# Use Case 3: The OperationId-Driven Client
`OpenAPIClient` is the other side of the contract. It reads the *same* OpenAPI document the server runs on and exposes one callable per `operationId` — so "client" and "server" are two views of one truth.
---
## 🔍 1. Before You Start
The client is `httpx`-based and returns raw `httpx.Response` objects — no magic deserialization, no hidden schema inference:
- **No** response Pydantic models
- **No** implicit URL construction (path params are explicit)
- **No** hand-written `requests.get(...)` scattered through your code
---
## 🧬 2. Constructing the Client
```python
from openapi_first.loader import load_openapi
from openapi_first.client import OpenAPIClient
spec = load_openapi("openapi.yaml")
client = OpenAPIClient(spec)
```
The base URL comes from the spec's `servers` list (first entry) unless you pass `base_url` explicitly:
```python
client = OpenAPIClient(spec, base_url="https://api.internal.myco/v1")
```
You can also hand over a preconfigured `httpx.Client` (custom transport, TLS, retries):
```python
import httpx
transport = httpx.HTTPTransport(retries=3)
client = OpenAPIClient(
spec,
client=httpx.Client(transport=transport),
)
```
---
## 💥 3. Fail-Fast at Construction
`OpenAPIClient(...)` **raises immediately** if the contract is broken — you find out at startup, not on the first request:
| Violation | Error |
|----------------------------------------|-----------------------------------|
| Spec has no `servers` entry | `OpenAPIClientError` |
| Spec has no `paths` | `OpenAPIClientError` |
| Operation missing `operationId` | `OpenAPIClientError` |
| Duplicate `operationId` | `OpenAPIClientError` |
| Operation references unknown parameters| `OpenAPIClientError` |
Missing *required parameters* fail at **call time** — pydoclint-grade strictness on the wire.
---
## 📞 4. Calling Operations
Every `operationId` becomes a method. The call signature is uniform across the whole client:
```python
response = client.<operationId>(
*,
path_params: dict | None = None,
query: dict | None = None,
headers: dict | None = None,
body: Any | None = None,
timeout: float | None = None,
) -> httpx.Response
```
Concrete examples (from the `crud_app` / `health_app` templates):
```python
# No parameters — simplest
response = client.get_health()
assert response.status_code == 200
# Path parameter
response = client.get_item(path_params={"item_id": 3})
# Query parameters
response = client.list_items(query={"limit": 10, "offset": 20})
# JSON request body
response = client.create_item(body={"name": "Orange", "price": 0.8})
# Custom headers / timeout
response = client.get_user(
path_params={"user_id": 1},
headers={"X-Internal-Key": "..."},
timeout=30,
)
```
**Returns:** the raw `httpx.Response`, so `status_code`, `.json()`, `.headers` are all yours to inspect.
---
## 🧠 5. How Parameters Are Bound
For each operation the client knows exactly *where* each parameter belongs:
| OpenAPI location | Client kwarg |
|------------------|-------------------|
| `in: path` | `path_params[name]` |
| `in: query` | `query[name]` |
| `in: header` | `headers[name]` |
| `requestBody` | `body` |
JSON media types are sent as `json=`; other media types as raw `content=`.
---
## 🔄 6. Server ↔ Client, One Spec
```bash
# One directory, two processes
uvicorn main:app --port 8000 # server
python -c "import asyncio; from client_script import run; asyncio.run(run())" # client
```
Or the same client against a remote environment:
```python
client = OpenAPIClient(
spec,
base_url="https://staging.internal.myco",
)
```
The URL is the *only* thing that changes between environments — the contract never does.
---
## ✍️ 7. OperationId as the API
Because the client is operationId-driven:
- Adding an operation = adding a method (and vice versa)
- No operation can be "forgotten" by the client — it's constructed from the spec
- `operationId` is the one name you remember; the HTTP verb/path is an implementation detail
If an `operationId` you call doesn't exist, you get an `AttributeError` at construction-scan time — never a silent 404.
---
## Related
- [01 Overview](../01_overview.md) · [01 Quickstart](01_quickstart.md) · [02 Templates](02_templates.md)

View File

@@ -0,0 +1,70 @@
# Codegen — Generate Models & Routes From Your Spec
> **TL;DR** — point `openapi-first codegen` at your spec and get `models` + `routes` scaffolds you can bind with one line. The codegen output is deterministic, and it's 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. **Writing the docs/spec catalog by hand**, once you have more than a handful of operations.
Codegen collapses both. One command, one source file, same generated shapes everywhere.
---
## 🚀 2. Model Generation
```bash
openapi-first codegen models --module my_project.models --input openapi.yaml
```
The generated model module mirrors the spec's `components.schemas` **by name**:
| Spec | Generated |
|------|-----------|
| `components.schemas.User` | `class User(BaseModel)` |
| `components.schemas.Item` | `class Item(BaseModel)` |
| every `$ref` (schema) | a `type: ClassVar` alias ✓ |
| `required + type` from schema | Pydantic `Field(...)` / type hints ✓ |
**Never hand-edit those files.** If the spec changes, regenerate — just like you'd re-run `cargo build` after editing `Cargo.toml`.
### 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).
---
## 🔁 3. Route Generation / Verification
```bash
# dry-run verification against your routes module
openapi-first codegen routes --module my_routes --input openapi.yaml --check
# scaffold an operation skeleton (generates the handler with a TODO)
openapi-first codegen routes --module my_routes --input openapi.yaml
```
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` as the spec says.
> **Note:** `codegen` is build-time tooling. It does **not** run at runtime, and templates (the Bake-your-stuff section of [02 Templates](02_templates.md)) make scaffolding a server out-of-the-box even simpler 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)

87
docs/wiki/04_design.md Normal file
View File

@@ -0,0 +1,87 @@
# Design — Guarantees, Startup Pipeline, and the Contract Model
This page is the architecture reference: the fail-fast startup surface, the guarantees that hold by construction, and the trade-offs baked into every decision.
---
## 🏗️ 1. The Startup Pipeline
Everything happens **once, eagerly, at construction time** — for the app at `OpenAPIFirstApp(...)`, for the client at `OpenAPIClient(...)`. There is no lazy loading, no deferred binding, no "it'll work on first request".
```text
openapi.yaml ──► loader.load_openapi
├─ parse (json/yaml by extension)
├─ validate (strict OpenAPI 3.x validator)
└─► dict ──▶ OpenAPISpecLoadError
OpenAPIFirstApp(openapi_path=..., routes_module=routes)
├─ 1. load + validate spec (loader)
├─ 2. parse securitySchemes (security)
├─ 3. make_security_dependencies (security)
├─ 4. bind_routes: every operationId ──► routes.<operationId>
│ └─ missing op / missing handler ──► MissingOperationHandler
└─► FastAPI app: routes registry + /openapi.json + Swagger UI
```
Client — same spec, same guarantees:
```text
OpenAPIClient(spec)
├─ require servers[]
├─ require paths
├─ one callable per operationId (client.<operationId>)
│ └─ missing / duplicate opId ──► OpenAPIClientError
└─► ready
```
---
## 📜 2. Guarantees That Hold by Construction
These are not conventions — they are enforced at startup or client construction:
1. **Every route is spec-declared.** Routes are registered *only* from `paths`; there is no decorator-driven or implicit routing.
2. **Every operation is handled.** Each `operationId` resolves to an exact handler name in `routes_module`; a missing one aborts bootstrap.
3. **Every operation has an `operationId`.** An operation without one cannot be bound and fails fast.
4. **Client and server use the same spec.** One document drives both sides, so they cannot drift.
5. **Auth is spec-driven.** `security` dependencies come from `securitySchemes` + per-operation `security`; no manual middleware.
6. **Spec is valid before use.** Invalid, malformed, or unloadable specs are rejected at load, not at first request.
### The startup "no half-states" property
Because binding, validation, and security resolution all run at construction, a booted app is a **provably-complete app**. If the contract is violated in any way, the process refuses to start — the failure is loud, immediate, and tells you exactly what to fix.
---
## 🧩 3. Component Responsibilities
| Module | Owns | Refuses to |
|--------|------|------------|
| `loader` | parse + validate the spec | modify or "fix" the spec |
| `app` | assemble FastAPI from spec + handlers | routing decisions, decorators |
| `binder` | `operationId` → handler mapping | infer handlers from paths |
| `client` | build one callable per `operationId` | guess URLs, deserialize implicitly |
| `security` | schemes → FastAPI `Depends` | manual middleware |
| `errors` | the error hierarchy | swallow failures |
| `codegen` | build-time model/routes generation | runtime codegen |
| `templates` | copyable scaffolds | production data stores |
**Rule of thumb for contributors:** keep modules coercive (they raise if the contract is wrong) and narrow (one responsibility each). Pydoclint + the test suite keep that contract honest.
---
## ⚖️ 4. Design Trade-offs (Accepted)
| Decision | Chosen because | What you give up |
|----------|----------------|------------------|
| Handlers are plain callables | Framework-agnostic, testable, grep-able | No decorator sugar |
| Fail-fast at startup | Drift caught in CI, not at 3am | Slightly heavier boot |
| Raw `httpx.Response` from client | No hidden deserialization/validation | You read `.json()` yourself |
| Build-time codegen | One-way generation, no runtime generator dep | Spec must already be spec-valid |
| Templates copy verbatim | Scaffold, not magic | Templates never updated in place |
---
## 🔗 Related
- [01 Overview](01_overview.md) · [02 Components](02_components.md) · [05 Security](05_security.md) · [06 Error Handling](06_error_handling.md) · [07 Testing](07_testing.md)

144
docs/wiki/05_security.md Normal file
View File

@@ -0,0 +1,144 @@
# Security — Auth Driven by the Spec, Not by Hand
The most distinctive thing about `openapi-first` security is that you **never write middleware or `Depends(authenticate)` calls yourself**. All authentication is *declared* in the OpenAPI document and *enforced* automatically.
---
## 🗺️ 1. Where Security Lives
Two places in the spec, both respected:
### 1.1 `components.securitySchemes` — the inventory
```yaml
components:
securitySchemes:
internalBearer:
type: http
scheme: bearer
bearerFormat: JWT
x-server-url: "{AUTH_SERVER_URL}"
x-introspect-path: "/introspect"
```
`x-server-url` and `x-introspect-path` are library extensions that point at a JWT introspection endpoint (see [Introspection](#-5-introspection)).
### 1.2 `security` — per-operation (or global) requirements
```yaml
security:
- internalBearer: [] # applied to every operation by default
paths:
/pets:
get:
operationId: list_pets
# inherits: security: [{internalBearer: []}]
post:
operationId: create_pet
security: [] # override — public endpoint
```
OpenAPI dynamic-scoping rules apply: an operation-level `security` **replaces** the global list (it does not merge).
---
## 📖 2. Reading the Spec
`security.py` exposes two functions:
| Function | Returns | Purpose |
|----------|---------|---------|
| `parse_security_schemes(spec)` | `dict[str, dict]` | Collect schemes, resolve `{ENV_VAR}` placeholders |
| `make_security_dependencies(spec, schemes)` | `dict[str, list[Depends]]` (keyed `METHOD:/path`) | Effective per-operation security deps |
Env placeholders of the form `{NAME}` are resolved **once at startup** from `os.environ`. This is how you avoid embedding credentials or auth-service URLs in the committed spec.
---
## 🔐 3. The Bearer Dependency
`make_security_dependencies` builds a FastAPI dependency for `type: http, scheme: bearer`.
Two modes, decided by whether an introspection endpoint is configured:
### 3.1 With introspection (`x-introspect-path`)
- Reads `Authorization: Bearer <token>`
- POSTs `{"token": "<token>"}` to `{x-server-url}{x-introspect-path}` synchronously via the bundled httpx client
- Expects a response with `active: true`
- On valid: `{"user": ...}` from the introspection body → `request.state.user`
- On failure: `401` (missing/invalid token) or `503` (auth service unreachable)
### 3.2 Without introspection
- Validates only that a Bearer token is present
- Stores it on `request.state.token`; no remote call
---
## 🔗 4. Wiring It Together
```python
# server-side
from openapi_first.loader import load_openapi
from openapi_first.security import (
parse_security_schemes,
make_security_dependencies,
)
from openapi_first.app import OpenAPIFirstApp
import routes
spec = load_openapi("openapi.yaml")
schemes = parse_security_schemes(spec)
security_deps = make_security_dependencies(spec, schemes)
app = OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=routes,
)
```
`OpenAPIFirstApp` already does this internally — the snippet above shows what it encapsulates (and what you use directly if you assemble the pieces by hand).
---
## 🧪 5. Testing Security
Because handlers are plain callables, security is the *one* place FastAPI's `TestClient` earns its keep:
```python
from fastapi.testclient import TestClient
def test_unauthenticated_is_401(app, overrides):
with TestClient(app) as client:
r = client.get("/pets")
assert r.status_code == 401
def test_invalid_token_using_fake_introspector(app):
# Point x-introspect-path at a stub uvicorn/TestServer returning active:false
with TestClient(app) as client:
r = client.get("/pets", headers={"Authorization": "Bearer nope"})
assert r.status_code == 401
```
See [Testing](07_testing.md) for the full recipe, including how tests stub the introspection server.
---
## 🛡️ 7. Common Patterns
| Pattern | How |
|---------|-----|
| Public endpoint | `security: []` on the operation |
| Whole-spec auth | top-level `security:` (applies to all) |
| Route-specific scheme | replace `security` on that operation |
| Env-driven auth URL | `x-server-url: "{AUTH_SERVER_URL}"` |
| Offline token carry | scheme without `x-introspect-path``request.state.token` |
| Auth on the client side | pass the token via `client.<operationId>(headers={"Authorization": ...})` |
---
## Related
- [02 Components](02_components.md) · [04 Design](04_design.md) · [06 Error Handling](06_error_handling.md) · [07 Testing](07_testing.md)

View File

@@ -0,0 +1,121 @@
# Error Handling — Fail Loud, Fail Early
`openapi-first` treats errors as **first-class contract documents**: every failure mode is a named exception with a stable import pathhare, and every one surfaces as early as possible.
---
## 🧬 1. The Hierarchy
All errors derive from `OpenAPIFirstError` (in `openapi_first/errors.py`), so a single `except OpenAPIFirstError` catches every first-party failure:
```text
OpenAPIFirstError
├── OpenAPISpecError # spec-level problems
│ └── OpenAPISpecLoadError # load / parse / validation (loader)
├── OpenAPIClientError # client-side contract issues (client)
└── MissingOperationHandler # spec op with no handler (binder)
# Security / loader layers raise through OpenAPISpecError subclasses too
```
| Exception | Module | Raised when |
|----------------------------------|-----------|-------------|
| `OpenAPISpecLoadError` | `loader` | Path missing, file unreadable, YAML/JSON invalid, or spec fails OpenAPI 3.x validation |
| `OpenAPIClientError` | `client` | No `servers`, no `paths`, missing/duplicate `operationId`, missing required params at construction |
| `MissingOperationHandler` | `errors` | An operation is declared whose `operationId` has no matching handler in `routes_module` |
---
## ⏱️ 2. When Things Fail
The single most important rule: **violations are eager, not lazy.**
### 2.1 At application startup
```python
# openapi.yaml missing ────────────────────────────► OpenAPISpecLoadError
# operationId without a handler ───────────────────► MissingOperationHandler
# operation with no operationId declared ──────────► MissingOperationHandler
```
Because these raise during `OpenAPIFirstApp(...)` construction, CI catches them the moment a spec and its routes drift — before a single request is served.
### 2.2 At client construction
```python
OpenAPIClient(spec) # fails fast, same philosophy
```
- Spec with no `servers``OpenAPIClientError`
- Spec with no `paths``OpenAPIClientError`
- Duplicate `operationId`s → `OpenAPIClientError` (client methods must be unambiguous)
- Operation missing `operationId``OpenAPIClientError`
### 2.3 At call time (client)
Runtime transport errors surface as `httpx` exceptions (`httpx.RequestError` family), not swallowed or remapped. Missing required args fail before any HTTP request is made:
```python
client.get_user(path_params={"user_id": ...}) # OK
client.get_user() # ValueError — user_id required
```
---
## 🧰 3. Handling in Your App
### 3.1 Server-side
Handlers raise FastAPI `HTTPException` for expected operation-level failures (404/422), and the `OperationId`-binding errors only exist at startup:
```python
from fastapi import HTTPException
def get_item(item_id: int):
"""Retrieve an item by ID.
Implements the OpenAPI operation ``get_item``.
Args:
item_id (int): Identifier of the item.
Raises:
HTTPException: If the item does not exist (404).
"""
try:
return _get_item(item_id)
except KeyError:
raise HTTPException(status_code=404, detail="Item not found")
```
### 3.2 Client-side
```python
import httpx
try:
response = client.get_item(path_params={"item_id": 1})
except httpx.HTTPStatusError as exc:
... # 4xx/5xx from the server
```
`httpx.HTTPStatusError` isn't raised by the library — it's the standard `httpx.raise_for_status()` you can opt into per call. The library never masks a response code.
---
## 🛡️ 4. Fail-Fast Guarantees Recap
| Layer | You write | The library guarantees |
|-------|-----------|------------------------|
| Loader | a spec path | unreadable/invalid specs never reach your app |
| Binder | handler functions | every operation must resolve, or the app won't start |
| Client | a spec | every operationId becomes a callable; missing ones fail at construction |
| Runtime | handler code | FastAPI + Pydantic handle coercion; contract checks already happened at startup |
None of these can silently degrade: a violation is an **exception at construction**, not a 500 at request time.
---
## Related
- [01 Overview](01_overview.md) · [05 Security](05_security.md) · [07 Testing](07_testing.md)

137
docs/wiki/07_testing.md Normal file
View File

@@ -0,0 +1,137 @@
# Testing — Smoke-First, Contract-First
Everything `openapi-first` ships is tested against **real specs, real handlers, and real HTTP** through FastAPI's `TestClient` and the bundled templates. There are no fakes of the library itself.
---
## 🧪 1. Suite Overview
```
tests/
├── conftest.py # fixtures: spec_file, routes_module, app, client
├── test_app.py # OpenAPIFirstApp: routes served, overrides, fail-fast
├── test_binder.py # bind_routes: opId resolution + missing-handler failures
├── test_loader.py # load_openapi: json/yaml, env resolution, validation
└── test_client.py # OpenAPIClient: opId→callable, params, error cases
```
Run with:
```bash
pytest # 23 tests, zero mocks of the library
pytest -q
pytest tests/test_loader.py
```
---
## 🏗️ 2. The Fixture Pattern
```python
# conftest.py (abridged)
@pytest.fixture
def spec_file(tmp_path):
path = tmp_path / "openapi.json"
path.write_text(json.dumps(SPEC), encoding="utf-8")
return str(path)
@pytest.fixture
def app(spec_file):
return OpenAPIFirstApp(
openapi_path=spec_file,
routes_module=routes_module(),
)
@pytest.fixture
def client(spec_file):
return OpenAPIClient(json.loads(Path(spec_file).read_text()))
```
---
## 🧪 3. What's Actually Asserted
### 3.1 App-level smoke
```python
def test_app_routes_served(spec_file):
app = OpenAPIFirstApp(openapi_path=spec_file, routes_module=routes)
client = TestClient(app)
assert client.get("/health").json() == {"status": "ok"}
```
### 3.2 Fail-fast (the heart)
```python
def test_app_missing_handler_fails_at_startup(spec_file):
with pytest.raises(MissingOperationHandler):
OpenAPIFirstApp(openapi_path=spec_file, routes_module=empty_routes)
```
### 3.3 Loader validation
```python
def test_loader_invalid_spec_fails():
with pytest.raises(OpenAPISpecLoadError):
load_openapi("broken.yaml")
```
### 3.4 Client contract drift
```python
def test_client_duplicate_operation_id_raises():
with pytest.raises(OpenAPIClientError):
OpenAPIClient(dup_spec)
```
---
## 🏛️ 4. Testing the Templates
Each bundled template ships its own test + in-memory store, so you get a runnable contract test the moment you scaffold:
```bash
openapi-first scaffold crud_app my-service
cd my-service
pytest -q
```
`test_crud_app.py` / `test_model_app.py` / `test_vet_app.py` exercise the full CRUD surface through `TestClient` — including `201`/`204` status codes, 404s, and (in `vet_app`) SSE streaming via `StreamingResponse`.
---
## 🕹️ 5. Quality Gates (CI)
The same gates the library itself must pass are what keep the docs honest:
| Gate | Purpose |
|------|---------|
| `black --check` | formatting parity |
| `ruff check` | lint hygiene |
| `mypy` | type safety (strict, `--disable-error-code` where intentional) |
| `pytest` | 23 tests, green |
| coverage | tracked via pytest-cov (HTML + XML + term) |
Run the whole gate locally:
```bash
black --check openapi_first tests
ruff check openapi_first tests
mypy openapi_first
pytest
```
---
## 💡 6. Testing Tips
- **Start from startup:** assert `OpenAPIFirstApp(...)` *raises* for the broken contracts — those are your most valuable tests
- **Client ↔ server:** smoke a client against the same spec the app was built from — one spec, two sides, zero drift
- **Templates are scaffolds:** their tests are copyable starting points, not canonical suites
- **No mocking of the library:** exercise loader → binder → app → client as a real pipeline
---
## Related
- [01 Overview](01_overview.md) · [02 Components](02_components.md) · [04 Error Handling](06_error_handling.md)

62
docs/wiki/index.md Normal file
View File

@@ -0,0 +1,62 @@
# 🧩 openapi-first — OpenAPI as the Single Source of Truth
`openapi-first` is a small, strict library for **OpenAPI-first FastAPI bootstrapping**: the OpenAPI document *is* the application contract — not an afterthought generated from decorators)Skip. Routes, schemas, security, and even the HTTP client are all derived from one specification.
> **Doc model:** this wiki is written for humans — how-to guides, examples, and usage recipes. The authoritative API contracts live in the code (docstrings) and the machine-readable bundle under `docs/mcp/`.
---
## 🚀 Key Features
* 📜 **One spec, two sides** — the same OpenAPI document boots both a FastAPI server (`OpenAPIFirstApp`) and a strict HTTP client (`OpenAPIClient`)
* 🔗 **`operationId` binding** — handler functions are bound to routes purely by `operationId`; no routing decorators
* 🧱 **Fail-fast contracts** — startup and client construction fail loudly when a handler, operation, or security scheme is missing
* 🛡️ **Spec-driven security**`securitySchemes` + per-operation `security` auto-injected as FastAPI dependencies (Bearer JWT introspection included)
* 🧠 **Contract-first codegen**`models` (Pydantic) and `routes` (resource stubs) generated from a spec
* 🧩 **Bundled templates**`health_app`, `crud_app`, `model_app`, `vet_app` scaffolds for the whole lifecycle, from `/health` to SSE + multi-resource CRUD
---
## 📦 Installation
From your internal PyPI:
```bash
pip install --extra-index-url https://$PYPI_USERNAME:$PYPI_PASSWORD@pip.aetoskia.com/simple openapi-first
```
From local source:
```bash
pip install -e .
```
---
## 📁 Documentation Structure
| Section | Description |
|---------|-------------|
| [Overview](01_overview.md) | The mental model: spec as contract, `operationId` binding, fail-fast guarantees |
| [Components](02_components.md) | `app`, `binder`, `loader`, `client`, `errors`, `security`, `codegen`, `cli` |
| **Use cases** | Step-by-step recipes |
| · [01 Quickstart](03_use_cases/01_quickstart.md) | Scaffold your first OpenAPI-first service |
| · [02 Templates](03_use_cases/02_templates.md) | The bundled application templates and how to use them |
| · [03 Client setup](03_use_cases/03_client.md) | Build an `operationId`-driven HTTP client |
| · [04 Codegen](03_use_cases/04_codegen.md) | Generate Pydantic models and route stubs from a spec |
| [Design](04_design.md) | Architecture, responsibilities, and startup pipeline |
| [Security](05_security.md) | `securitySchemes`, env resolution, and JWT introspection |
| [Error Handling](06_error_handling.md) | The error hierarchy and when each error is raised |
| [Testing](07_testing.md) | Test strategy, quality gates, and coverage |
---
## 🔗 Related Resources
* **Source Code:** [Gitea Repository](https://git.aetoskia.com/aetos/openapi-first)
* **Internal PyPI:** [pip.aetoskia.com/simple/openapi-first](https://pip.aetoskia.com/simple/openapi-first)
* **Drone CI:** Auto-builds and publishes tagged releases, gated on black / ruff / mypy / pytest.
---
© Aetoskia Internal — `openapi-first` 0.0.6