1 line
63 KiB
JSON
1 line
63 KiB
JSON
{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"\ud83e\udde9 openapi-first \u2014 OpenAPI as the Single Source of Truth","text":"<p><code>openapi-first</code> is a small, strict library for OpenAPI-first FastAPI bootstrapping: the OpenAPI document is the application contract \u2014 not an afterthought generated from decorators)Skip. Routes, schemas, security, and even the HTTP client are all derived from one specification.</p> <p>Doc model: this wiki is written for humans \u2014 how-to guides, examples, and usage recipes. The authoritative API contracts live in the code (docstrings) and the machine-readable bundle under <code>docs/mcp/</code>.</p>"},{"location":"#key-features","title":"\ud83d\ude80 Key Features","text":"<ul> <li>\ud83d\udcdc One spec, two sides \u2014 the same OpenAPI document boots both a FastAPI server (<code>OpenAPIFirstApp</code>) and a strict HTTP client (<code>OpenAPIClient</code>)</li> <li>\ud83d\udd17 <code>operationId</code> binding \u2014 handler functions are bound to routes purely by <code>operationId</code>; no routing decorators</li> <li>\ud83e\uddf1 Fail-fast contracts \u2014 startup and client construction fail loudly when a handler, operation, or security scheme is missing</li> <li>\ud83d\udee1\ufe0f Spec-driven security \u2014 <code>securitySchemes</code> + per-operation <code>security</code> auto-injected as FastAPI dependencies (Bearer JWT introspection included)</li> <li>\ud83e\udde0 Contract-first codegen \u2014 <code>models</code> (Pydantic) and <code>routes</code> (resource stubs) generated from a spec</li> <li>\ud83e\udde9 Bundled templates \u2014 <code>health_app</code>, <code>crud_app</code>, <code>model_app</code>, <code>vet_app</code> scaffolds for the whole lifecycle, from <code>/health</code> to SSE + multi-resource CRUD</li> </ul>"},{"location":"#installation","title":"\ud83d\udce6 Installation","text":"<p>From your internal PyPI:</p> <pre><code>pip install --extra-index-url https://$PYPI_USERNAME:$PYPI_PASSWORD@pip.aetoskia.com/simple openapi-first\n</code></pre> <p>From local source:</p> <pre><code>pip install -e .\n</code></pre>"},{"location":"#documentation-structure","title":"\ud83d\udcc1 Documentation Structure","text":"Section Description Overview The mental model: spec as contract, <code>operationId</code> binding, fail-fast guarantees Components <code>app</code>, <code>binder</code>, <code>loader</code>, <code>client</code>, <code>errors</code>, <code>security</code>, <code>codegen</code>, <code>cli</code> Use cases Step-by-step recipes \u00b7 01 \u2013 Quickstart Scaffold your first OpenAPI-first service \u00b7 02 \u2013 Templates The bundled application templates and how to use them \u00b7 03 \u2013 Client setup Build an <code>operationId</code>-driven HTTP client \u00b7 04 \u2013 Codegen Generate Pydantic models and route stubs from a spec Design Architecture, responsibilities, and startup pipeline Security <code>securitySchemes</code>, env resolution, and JWT introspection Error Handling The error hierarchy and when each error is raised Testing Test strategy, quality gates, and coverage"},{"location":"#related-resources","title":"\ud83d\udd17 Related Resources","text":"<ul> <li>Source Code: Gitea Repository</li> <li>Internal PyPI: pip.aetoskia.com/simple/openapi-first</li> <li>Drone CI: Auto-builds and publishes tagged releases, gated on black / ruff / mypy / pytest.</li> </ul> <p>\u00a9 Aetoskia Internal \u2014 <code>openapi-first</code> 0.0.6</p>"},{"location":"01_overview/","title":"Overview \u2014 The OpenAPI-First Mental Model","text":"<p><code>openapi-first</code> 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.</p>"},{"location":"01_overview/#1-the-mental-model","title":"\ud83e\udde0 1. The Mental Model","text":""},{"location":"01_overview/#11-one-source-of-truth","title":"1.1 One source of truth","text":"<p>Your OpenAPI document (<code>openapi.yaml</code> or <code>openapi.json</code>) is the single authoritative contract:</p> <pre><code>paths:\n /health:\n get:\n operationId: get_health\n responses:\n \"200\":\n description: OK\n content:\n application/json:\n schema:\n type: object\n</code></pre> <p>Every route, method, parameter, schema, and security requirement lives here \u2014 and only here. Code never declares routes.</p>"},{"location":"01_overview/#12-operationid-is-the-binding-key","title":"1.2 <code>operationId</code> is the binding key","text":"<p>The only bridge between the spec and your Python code is the <code>operationId</code>. Each operation maps, by name, to exactly one plain callable:</p> <pre><code># routes.py\ndef get_health():\n \"\"\"Health check operation handler.\"\"\"\n return {\"status\": \"ok\"}\n</code></pre> <p><code>openapi_first</code> resolves <code>operationId: get_health</code> \u2192 <code>routes.get_health</code> and registers the route. No decorators, no <code>@app.get</code>, no routing metadata in code.</p> <p>Guarantees this binding provides:</p> <ul> <li>\ud83d\udeab No undocumented route can exist \u2014 every route must be in the spec</li> <li>\ud83d\udeab No spec operation can go unhandled \u2014 every <code>operationId</code> must resolve at startup</li> <li>\ud83d\udd12 No auth can be bypassed \u2014 security is injected from <code>securitySchemes</code> + per-operation <code>security</code>, spec-driven</li> <li>\ud83e\uddf1 No drift possible \u2014 the server and client are built from the same document</li> </ul>"},{"location":"01_overview/#13-fail-fast-by-design","title":"1.3 Fail-fast by design","text":"<p>Contract violations are detected at application startup (or client construction), never silently at request time:</p> What goes wrong When it fails Invalid / unloadable spec <code>load_openapi</code> at startup Spec fails OpenAPI 3.x validation <code>load_openapi</code> at startup <code>operationId</code> missing a handler <code>bind_routes</code> at startup Operation declared but no <code>operationId</code> <code>bind_routes</code> at startup Missing / duplicate <code>operationId</code> in client <code>OpenAPIClient(...)</code> construction"},{"location":"01_overview/#2-what-it-looks-like","title":"\u2699\ufe0f 2. What It Looks Like","text":""},{"location":"01_overview/#21-scaffold-an-application","title":"2.1 Scaffold an application","text":"<pre><code>openapi-first scaffold health_app my-health-service\nopenapi-first scaffold --list\n</code></pre> <p><code>scaffold</code> copies a bundled template (verbatim \u2014 no code generation, no mutation) into a directory of your choice.</p>"},{"location":"01_overview/#22-bootstrap-the-server","title":"2.2 Bootstrap the server","text":"<pre><code># main.py\nfrom openapi_first.app import OpenAPIFirstApp\nimport routes\n\napp = OpenAPIFirstApp(\n openapi_path=\"openapi.yaml\",\n routes_module=routes,\n title=\"My Service\",\n)\n</code></pre> <p>Run with your FastAPI-compatible server (ASGI):</p> <pre><code>uvicorn main:app --reload\n</code></pre> <p>FastAPI itself drives the server; every route, response model, and security dependency comes from the spec. <code>/openapi.json</code> and Swagger UI always reflect the spec, byte-for-byte.</p>"},{"location":"01_overview/#23-talk-to-it-with-the-client","title":"2.3 Talk to it with the client","text":"<pre><code># client-side\nfrom openapi_first.loader import load_openapi\nfrom openapi_first.client import OpenAPIClient\n\nspec = load_openapi(\"openapi.yaml\")\nclient = OpenAPIClient(spec)\n\nresponse = client.get_health() # operationId-driven call\nprint(response.status_code) # 200\nprint(response.json()) # {\"status\": \"ok\"}\n</code></pre> <p><code>OpenAPIClient</code> builds one callable per <code>operationId</code> from the same spec, so the client can never drift from the server.</p>"},{"location":"01_overview/#3-where-things-live","title":"\ud83e\udde9 3. Where Things Live","text":"Concern Module Load + validate spec <code>openapi_first.loader</code> Boot FastAPI app <code>openapi_first.app</code> Bind routes by opId <code>openapi_first.binder</code> HTTP client <code>openapi_first.client</code> Error hierarchy <code>openapi_first.errors</code> Security dependencies <code>openapi_first.security</code> Pydantic model codegen <code>openapi_first.codegen</code> (uses <code>datamodel_code_generator</code>) Route stub codegen <code>openapi_first.codegen_routes</code> Models / routes CLI <code>openapi_first.cli</code> Bundled templates <code>openapi_first.templates</code> <p>Jurisdictions:</p> <ul> <li><code>loader</code>, <code>app</code>, <code>binder</code>, <code>client</code>, <code>errors</code>, <code>security</code> are the library API surface \u2014 stable, tested, documented.</li> <li><code>templates</code> are copyable scaffolds \u2014 not part of the library API; excluded from lint/format/type gates, never imported at runtime.</li> </ul>"},{"location":"01_overview/#4-the-non-goals","title":"\ud83d\udcc4 4. The Non-Goals","text":"<p><code>openapi-first</code> deliberately does not:</p> <ul> <li>Parse decorators to generate an OpenAPI schema (that's default FastAPI behavior \u2014 the inverse)</li> <li>Generate models from code at runtime (only at build time via CLI, from spec \u2192 Pydantic)</li> <li>Validate request/response bodies against the spec at runtime (contract is enforced at startup / client construction; FastAPI + Pydantic handle runtime coercion)</li> <li>Invent routing from path conventions \u2014 <code>operationId</code> binding only</li> <li>Ship a production feature set in the bundled templates (they're demos/scaffolds: in-memory stores, no concurrency, no auth configured)</li> </ul>"},{"location":"01_overview/#5-path-forward","title":"\ud83e\udded 5. Path Forward","text":"<p>New here? Start with 01 \u2013 Quickstart. Want to copy a runnable app? Jump to 02 \u2013 Templates. Digging into internals? See Design.</p>"},{"location":"01_overview/#related","title":"Related","text":"<ul> <li>01 \u2013 Quickstart \u00b7 02 \u2013 Components \u00b7 01 \u2013 Overview</li> </ul>"},{"location":"02_components/","title":"Components \u2014 What Ships in the Box","text":"<p>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.</p>"},{"location":"02_components/#1-module-map","title":"\ud83d\uddc2\ufe0f 1. Module Map","text":"Module Responsibility Import <code>openapi_first.loader</code> Load/validate the spec, resolve <code>{ENV_VAR}</code> <code>load_openapi</code> <code>openapi_first.app</code> OpenAPI-first FastAPI application bootstrap <code>OpenAPIFirstApp</code> <code>openapi_first.binder</code> Spec \u2192 route binding via <code>operationId</code> <code>bind_routes</code> <code>openapi_first.client</code> <code>operationId</code>-driven HTTP client <code>OpenAPIClient</code> <code>openapi_first.errors</code> Explicit error hierarchy <code>OpenAPIFirstError</code>, <code>OpenAPIClientError</code>, <code>MissingOperationHandler</code> <code>openapi_first.security</code> Security-dependency construction from the spec <code>parse_security_schemes</code>, <code>make_security_dependencies</code> <code>openapi_first.codegen</code> Pydantic model generation (build-time) <code>generate_models</code> <code>openapi_first.codegen_routes</code> Route-handler stub generation (build-time) <code>generate_routes</code> <code>openapi_first.cli</code> <code>scaffold</code> / <code>models</code> / <code>routes</code> command surface CLI entry point <code>openapi_first.templates</code> Copyable application templates (NOT library API) <code>openapi-first scaffold</code>"},{"location":"02_components/#2-loader-load-validate","title":"\u2699\ufe0f 2. <code>loader</code> \u2014 Load & Validate","text":"<p><code>load_openapi(path: str | Path) -> dict[str, Any]</code> (in <code>openapi_first/loader.py</code>).</p> <p><code>loader.py</code> ensures a spec is real, readable, parseable, and schema-valid before anything else runs \u2014 a golden rule of fail-fast.</p> <pre><code>from openapi_first.loader import load_openapi\n\nspec = load_openapi(\"openapi.yaml\")\n</code></pre> <p>Behavior:</p> <ul> <li>Accepts <code>.json</code>, <code>.yaml</code>, <code>.yml</code> (parsed by extension).</li> <li>Runs strict OpenAPI 3.x validation (<code>openapi-spec-validator</code>) at load time.</li> <li>Raises <code>OpenAPISpecLoadError</code> on: missing file, unparseable content, or spec-validation failure.</li> <li>Does not modify, coerce, or \"fix\" the spec \u2014 it's a read-only gate.</li> </ul> <p>Env-var resolution lives at the security layer rather than the loader (see Security).</p>"},{"location":"02_components/#3-app-the-application-bootstrap","title":"\ud83e\uddec 3. <code>app</code> \u2014 The Application Bootstrap","text":"<p><code>OpenAPIFirstApp</code> (in <code>openapi_first/app.py</code>) is a FastAPI subclass that replaces manual route registration with OpenAPI-driven binding.</p> <pre><code>from openapi_first.app import OpenAPIFirstApp\nimport routes\n\napp = OpenAPIFirstApp(\n openapi_path=\"openapi.yaml\",\n routes_module=routes,\n title=\"My Service\",\n)\n</code></pre> <p>Startup pipeline (fail-fast, in this order):</p> <ol> <li>Load the spec (<code>.yaml</code>/<code>.json</code>).</li> <li>Validate it against OpenAPI 3.x schema.</li> <li>Parse <code>securitySchemes</code> and per-operation <code>security</code>.</li> <li>Build per-route security dependencies.</li> <li>Bind every path/method \u2192 handler by <code>operationId</code>; a missing handler, a missing <code>operationId</code> on a declared operation, or an unbound operation raises at startup.</li> </ol> <p>Guarantees:</p> <ul> <li>Every route has a spec declaration (no undocumented routes).</li> <li>Every spec operation has a handler (no unhandled operations).</li> <li>Auth enforcement is spec-driven, not hand-wired.</li> <li><code>/openapi.json</code> + Swagger UI always reflect the provided spec.</li> </ul> <p>Keyword arguments beyond <code>openapi_path</code> / <code>routes_module</code> pass straight through to <code>fastapi.FastAPI</code> (it's a subclass \u2014 <code>title</code>, <code>version</code>, middleware, lifespan, \u2026 all work).</p>"},{"location":"02_components/#4-binder-spec-route-binding","title":"\ud83d\udd17 4. <code>binder</code> \u2014 Spec \u2192 Route Binding","text":"<p><code>bind_routes(app, spec, routes_module, security_deps=None) -> None</code> (in <code>openapi_first/binder.py</code>).</p> <p>This is the heart of the OpenAPI-first guarantee. For each <code>path</code> + HTTP method in the spec:</p> <ol> <li>Reads the operation's <code>operationId</code>.</li> <li>Looks up <code>routes_module.<operationId></code> \u2014 a plain callable.</li> <li>Registers a FastAPI <code>APIRoute</code> bound to that handler, injecting <code>Depends(...)</code> for any matching security requirements.</li> </ol> <p>Failures are explicit and early:</p> Condition Raised No <code>operationId</code> on an operation <code>MissingOperationHandler</code> <code>operationId</code> has no handler function <code>MissingOperationHandler</code> A path/method declared but unbound <code>MissingOperationHandler</code> <p>Handlers stay framework-agnostic: they're plain functions <code>(payload, id, response)</code> named after <code>operationId</code>s \u2014 no decorators, no routing metadata.</p>"},{"location":"02_components/#5-client-the-other-side-of-the-contract","title":"\ud83d\udce1 5. <code>client</code> \u2014 The Other Side of the Contract","text":"<p><code>OpenAPIClient(spec, base_url=None, client=None)</code> (in <code>openapi_first/client.py</code>).</p> <p>The same spec that builds the server builds its client \u2014 one callable per <code>operationId</code>, keyed by name:</p> <pre><code>from openapi_first.loader import load_openapi\nfrom openapi_first.client import OpenAPIClient\n\nspec = load_openapi(\"openapi.yaml\")\nclient = OpenAPIClient(spec)\n\nresponse = client.get_health()\nresponse = client.get_user(path_params={\"user_id\": 1})\nresponse = client.create_user(body={\"name\": \"Ada\"})\n</code></pre> <p>How operations become methods:</p> <ul> <li>Each <code>operationId</code> \u2192 a dynamically-built callable on the client.</li> <li>Path parameters \u2192 <code>path_params={\"user_id\": 1}</code>.</li> <li>Request body \u2192 <code>body={...}</code> (JSON) or raw content for non-JSON media types.</li> <li>Query/headers \u2192 <code>query=</code> / <code>headers=</code>.</li> <li>Returns the raw <code>httpx.Response</code> (no implicit deserialization, no hidden schema inference).</li> </ul> <p>Client-construction guarantees (fail-fast, same philosophy as the app):</p> <ul> <li>Spec must declare at least one <code>servers</code> entry (<code>base_url</code> falls back to it).</li> <li>Spec must have <code>paths</code>.</li> <li>Every operation must have a unique <code>operationId</code>.</li> <li>Required params must match the spec at call time.</li> </ul> <p>Construction errors raise <code>OpenAPIClientError</code>; request-time errors surface as <code>httpx</code> exceptions with <code>operationId</code> context.</p>"},{"location":"02_components/#6-errors-explicit-error-hierarchy","title":"\ud83d\udca5 6. <code>errors</code> \u2014 Explicit Error Hierarchy","text":"<p>Everything raised by <code>openapi-first</code> derives from <code>OpenAPIFirstError</code> (in <code>openapi_first/errors.py</code>), so callers can handle first-party failures with a single <code>except</code>:</p> <ul> <li><code>OpenAPIFirstError</code> \u2014 base (comparable / aims to be picklable share).</li> <li><code>OpenAPIClientError</code> \u2014 client-side contract violations.</li> <li><code>MissingOperationHandler</code> \u2014 spec declares an operation whose handler is missing or unresolvable; carries <code>path</code>, <code>method</code>, and optional <code>operationId</code>.</li> </ul> <p>See Error Handling for the full table.</p>"},{"location":"02_components/#7-security-auth-from-the-spec","title":"\ud83d\udee1\ufe0f 7. <code>security</code> \u2014 Auth from the Spec","text":"<p><code>security.py</code> turns an OpenAPI <code>securitySchemes</code> section into FastAPI <code>Depends(...)</code> objects \u2014 no manual middleware.</p> <ul> <li><code>parse_security_schemes(spec) -> dict[str, dict]</code> \u2014 collects schemes, resolving <code>{ENV_VAR}</code> placeholders.</li> <li><code>make_security_dependencies(spec, security_schemes) -> dict[str, list[Depends]]</code> \u2014 builds <code>METHOD:/path</code> \u2192 dependency list from per-operation <code>security</code>, falling back to top-level <code>security</code>.</li> </ul> <p>Supported scheme types (extensible):</p> <ul> <li><code>type: http, scheme: bearer</code> \u2014 <code>OpenAPIFirstSecurityDependency(HTTPBearer)</code>. With <code>x-introspect-path</code>/<code>x-server-url</code>, it introspects the JWT against an auth service; optionally sets <code>request.state.user</code>. Without introspection, it validates token presence and stores it on <code>request.state.token</code>.</li> <li>API-key style schemes via the same resolution path.</li> </ul> <p>Key properties: resolution is per-operation, env placeholders resolve once at startup, and dependencies are injected by <code>binder</code> \u2014 your handlers never mention security.</p>"},{"location":"02_components/#8-codegen-cli-build-time-tooling","title":"\ud83c\udfed 8. <code>codegen</code> & <code>cli</code> \u2014 Build-Time Tooling","text":"<p>Generation is strictly build-time \u2014 spec \u2192 code, run once by a developer, committed:</p>"},{"location":"02_components/#openapi-first-models-spec-o-file","title":"<code>openapi-first models <spec> -o <file></code>","text":"<p>Generates Pydantic models from spec schemas (wraps <code>datamodel_code_generator</code>):</p> <pre><code># cli path\nopenapi-first models openapi.yaml -o app/models.py\n</code></pre>"},{"location":"02_components/#openapi-first-routes-spec-o-dir-use-models-models-module-models","title":"<code>openapi-first routes <spec> -o <dir> [--use-models] [--models-module models]</code>","text":"<p>Generates one <code>routes_<resource>.py</code> stub file per resource, with <code>NotImplementedError</code> handlers bound to <code>operationId</code>s (optionally importing your generated models):</p> <pre><code>openapi-first routes openapi.yaml -o app/routes --use-models\n</code></pre>"},{"location":"02_components/#openapi-first-scaffold-template-path","title":"<code>openapi-first scaffold <template> [path]</code>","text":"<p>Copies a bundled application template verbatim (see Templates).</p>"},{"location":"02_components/#9-templates-copyable-applications","title":"\ud83e\uddf1 9. <code>templates</code> \u2014 Copyable Applications","text":"<p>Four bundled, runnable applications live under <code>openapi_first/templates/</code>: <code>health_app</code>, <code>crud_app</code>, <code>model_app</code>, and <code>vet_app</code>. They are not part of the library API \u2014 no lint/format/type gates, never imported at runtime. They exist to be copied via:</p> <pre><code>openapi-first scaffold <template> [target-dir]\nopenapi-first scaffold --list\n</code></pre>"},{"location":"02_components/#related","title":"\ud83d\udd17 Related","text":"<ul> <li>01 \u2013 Overview \u00b7 02 \u2013 Templates \u00b7 03 \u2013 Security \u00b7 04 \u2013 Design</li> </ul>"},{"location":"04_design/","title":"Design \u2014 Guarantees, Startup Pipeline, and the Contract Model","text":"<p>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.</p>"},{"location":"04_design/#1-the-startup-pipeline","title":"\ud83c\udfd7\ufe0f 1. The Startup Pipeline","text":"<p>Everything happens once, eagerly, at construction time \u2014 for the app at <code>OpenAPIFirstApp(...)</code>, for the client at <code>OpenAPIClient(...)</code>. There is no lazy loading, no deferred binding, no \"it'll work on first request\".</p> <pre><code>openapi.yaml \u2500\u2500\u25ba loader.load_openapi\n \u251c\u2500 parse (json/yaml by extension)\n \u251c\u2500 validate (strict OpenAPI 3.x validator)\n \u2514\u2500\u25ba dict \u2500\u2500\u25b6 OpenAPISpecLoadError\n \u2502\nOpenAPIFirstApp(openapi_path=..., routes_module=routes)\n \u251c\u2500 1. load + validate spec (loader)\n \u251c\u2500 2. parse securitySchemes (security)\n \u251c\u2500 3. make_security_dependencies (security)\n \u251c\u2500 4. bind_routes: every operationId \u2500\u2500\u25ba routes.<operationId>\n \u2502 \u2514\u2500 missing op / missing handler \u2500\u2500\u25ba MissingOperationHandler\n \u2514\u2500\u25ba FastAPI app: routes registry + /openapi.json + Swagger UI\n</code></pre> <p>Client \u2014 same spec, same guarantees:</p> <pre><code>OpenAPIClient(spec)\n \u251c\u2500 require servers[]\n \u251c\u2500 require paths\n \u251c\u2500 one callable per operationId (client.<operationId>)\n \u2502 \u2514\u2500 missing / duplicate opId \u2500\u2500\u25ba OpenAPIClientError\n \u2514\u2500\u25ba ready\n</code></pre>"},{"location":"04_design/#2-guarantees-that-hold-by-construction","title":"\ud83d\udcdc 2. Guarantees That Hold by Construction","text":"<p>These are not conventions \u2014 they are enforced at startup or client construction:</p> <ol> <li>Every route is spec-declared. Routes are registered only from <code>paths</code>; there is no decorator-driven or implicit routing.</li> <li>Every operation is handled. Each <code>operationId</code> resolves to an exact handler name in <code>routes_module</code>; a missing one aborts bootstrap.</li> <li>Every operation has an <code>operationId</code>. An operation without one cannot be bound and fails fast.</li> <li>Client and server use the same spec. One document drives both sides, so they cannot drift.</li> <li>Auth is spec-driven. <code>security</code> dependencies come from <code>securitySchemes</code> + per-operation <code>security</code>; no manual middleware.</li> <li>Spec is valid before use. Invalid, malformed, or unloadable specs are rejected at load, not at first request.</li> </ol>"},{"location":"04_design/#the-startup-no-half-states-property","title":"The startup \"no half-states\" property","text":"<p>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 \u2014 the failure is loud, immediate, and tells you exactly what to fix.</p>"},{"location":"04_design/#3-component-responsibilities","title":"\ud83e\udde9 3. Component Responsibilities","text":"Module Owns Refuses to <code>loader</code> parse + validate the spec modify or \"fix\" the spec <code>app</code> assemble FastAPI from spec + handlers routing decisions, decorators <code>binder</code> <code>operationId</code> \u2192 handler mapping infer handlers from paths <code>client</code> build one callable per <code>operationId</code> guess URLs, deserialize implicitly <code>security</code> schemes \u2192 FastAPI <code>Depends</code> manual middleware <code>errors</code> the error hierarchy swallow failures <code>codegen</code> build-time model/routes generation runtime codegen <code>templates</code> copyable scaffolds production data stores <p>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.</p>"},{"location":"04_design/#4-design-trade-offs-accepted","title":"\u2696\ufe0f 4. Design Trade-offs (Accepted)","text":"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 <code>httpx.Response</code> from client No hidden deserialization/validation You read <code>.json()</code> 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"},{"location":"04_design/#related","title":"\ud83d\udd17 Related","text":"<ul> <li>01 \u2013 Overview \u00b7 02 \u2013 Components \u00b7 05 \u2013 Security \u00b7 06 \u2013 Error Handling \u00b7 07 \u2013 Testing</li> </ul>"},{"location":"05_security/","title":"Security \u2014 Auth Driven by the Spec, Not by Hand","text":"<p>The most distinctive thing about <code>openapi-first</code> security is that you never write middleware or <code>Depends(authenticate)</code> calls yourself. All authentication is declared in the OpenAPI document and enforced automatically.</p>"},{"location":"05_security/#1-where-security-lives","title":"\ud83d\uddfa\ufe0f 1. Where Security Lives","text":"<p>Two places in the spec, both respected:</p>"},{"location":"05_security/#11-componentssecurityschemes-the-inventory","title":"1.1 <code>components.securitySchemes</code> \u2014 the inventory","text":"<pre><code>components:\n securitySchemes:\n internalBearer:\n type: http\n scheme: bearer\n bearerFormat: JWT\n x-server-url: \"{AUTH_SERVER_URL}\"\n x-introspect-path: \"/introspect\"\n</code></pre> <p><code>x-server-url</code> and <code>x-introspect-path</code> are library extensions that point at a JWT introspection endpoint (see Introspection).</p>"},{"location":"05_security/#12-security-per-operation-or-global-requirements","title":"1.2 <code>security</code> \u2014 per-operation (or global) requirements","text":"<pre><code>security:\n - internalBearer: [] # applied to every operation by default\n\npaths:\n /pets:\n get:\n operationId: list_pets\n # inherits: security: [{internalBearer: []}]\n post:\n operationId: create_pet\n security: [] # override \u2014 public endpoint\n</code></pre> <p>OpenAPI dynamic-scoping rules apply: an operation-level <code>security</code> replaces the global list (it does not merge).</p>"},{"location":"05_security/#2-reading-the-spec","title":"\ud83d\udcd6 2. Reading the Spec","text":"<p><code>security.py</code> exposes two functions:</p> Function Returns Purpose <code>parse_security_schemes(spec)</code> <code>dict[str, dict]</code> Collect schemes, resolve <code>{ENV_VAR}</code> placeholders <code>make_security_dependencies(spec, schemes)</code> <code>dict[str, list[Depends]]</code> (keyed <code>METHOD:/path</code>) Effective per-operation security deps <p>Env placeholders of the form <code>{NAME}</code> are resolved once at startup from <code>os.environ</code>. This is how you avoid embedding credentials or auth-service URLs in the committed spec.</p>"},{"location":"05_security/#3-the-bearer-dependency","title":"\ud83d\udd10 3. The Bearer Dependency","text":"<p><code>make_security_dependencies</code> builds a FastAPI dependency for <code>type: http, scheme: bearer</code>.</p> <p>Two modes, decided by whether an introspection endpoint is configured:</p>"},{"location":"05_security/#31-with-introspection-x-introspect-path","title":"3.1 With introspection (<code>x-introspect-path</code>)","text":"<ul> <li>Reads <code>Authorization: Bearer <token></code></li> <li>POSTs <code>{\"token\": \"<token>\"}</code> to <code>{x-server-url}{x-introspect-path}</code> synchronously via the bundled httpx client</li> <li>Expects a response with <code>active: true</code></li> <li>On valid: <code>{\"user\": ...}</code> from the introspection body \u2192 <code>request.state.user</code></li> <li>On failure: <code>401</code> (missing/invalid token) or <code>503</code> (auth service unreachable)</li> </ul>"},{"location":"05_security/#32-without-introspection","title":"3.2 Without introspection","text":"<ul> <li>Validates only that a Bearer token is present</li> <li>Stores it on <code>request.state.token</code>; no remote call</li> </ul>"},{"location":"05_security/#4-wiring-it-together","title":"\ud83d\udd17 4. Wiring It Together","text":"<pre><code># server-side\nfrom openapi_first.loader import load_openapi\nfrom openapi_first.security import (\n parse_security_schemes,\n make_security_dependencies,\n)\nfrom openapi_first.app import OpenAPIFirstApp\nimport routes\n\nspec = load_openapi(\"openapi.yaml\")\nschemes = parse_security_schemes(spec)\nsecurity_deps = make_security_dependencies(spec, schemes)\n\napp = OpenAPIFirstApp(\n openapi_path=\"openapi.yaml\",\n routes_module=routes,\n)\n</code></pre> <p><code>OpenAPIFirstApp</code> already does this internally \u2014 the snippet above shows what it encapsulates (and what you use directly if you assemble the pieces by hand).</p>"},{"location":"05_security/#5-testing-security","title":"\ud83e\uddea 5. Testing Security","text":"<p>Because handlers are plain callables, security is the one place FastAPI's <code>TestClient</code> earns its keep:</p> <pre><code>from fastapi.testclient import TestClient\n\ndef test_unauthenticated_is_401(app, overrides):\n with TestClient(app) as client:\n r = client.get(\"/pets\")\n assert r.status_code == 401\n\ndef test_invalid_token_using_fake_introspector(app):\n # Point x-introspect-path at a stub uvicorn/TestServer returning active:false\n with TestClient(app) as client:\n r = client.get(\"/pets\", headers={\"Authorization\": \"Bearer nope\"})\n assert r.status_code == 401\n</code></pre> <p>See Testing for the full recipe, including how tests stub the introspection server.</p>"},{"location":"05_security/#7-common-patterns","title":"\ud83d\udee1\ufe0f 7. Common Patterns","text":"Pattern How Public endpoint <code>security: []</code> on the operation Whole-spec auth top-level <code>security:</code> (applies to all) Route-specific scheme replace <code>security</code> on that operation Env-driven auth URL <code>x-server-url: \"{AUTH_SERVER_URL}\"</code> Offline token carry scheme without <code>x-introspect-path</code> \u2192 <code>request.state.token</code> Auth on the client side pass the token via <code>client.<operationId>(headers={\"Authorization\": ...})</code>"},{"location":"05_security/#related","title":"Related","text":"<ul> <li>02 \u2013 Components \u00b7 04 \u2013 Design \u00b7 06 \u2013 Error Handling \u00b7 07 \u2013 Testing</li> </ul>"},{"location":"06_error_handling/","title":"Error Handling \u2014 Fail Loud, Fail Early","text":"<p><code>openapi-first</code> 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.</p>"},{"location":"06_error_handling/#1-the-hierarchy","title":"\ud83e\uddec 1. The Hierarchy","text":"<p>All errors derive from <code>OpenAPIFirstError</code> (in <code>openapi_first/errors.py</code>), so a single <code>except OpenAPIFirstError</code> catches every first-party failure:</p> <pre><code>OpenAPIFirstError\n\u251c\u2500\u2500 OpenAPISpecError # spec-level problems\n\u2502 \u2514\u2500\u2500 OpenAPISpecLoadError # load / parse / validation (loader)\n\u251c\u2500\u2500 OpenAPIClientError # client-side contract issues (client)\n\u2514\u2500\u2500 MissingOperationHandler # spec op with no handler (binder)\n\n# Security / loader layers raise through OpenAPISpecError subclasses too\n</code></pre> Exception Module Raised when <code>OpenAPISpecLoadError</code> <code>loader</code> Path missing, file unreadable, YAML/JSON invalid, or spec fails OpenAPI 3.x validation <code>OpenAPIClientError</code> <code>client</code> No <code>servers</code>, no <code>paths</code>, missing/duplicate <code>operationId</code>, missing required params at construction <code>MissingOperationHandler</code> <code>errors</code> An operation is declared whose <code>operationId</code> has no matching handler in <code>routes_module</code>"},{"location":"06_error_handling/#2-when-things-fail","title":"\u23f1\ufe0f 2. When Things Fail","text":"<p>The single most important rule: violations are eager, not lazy.</p>"},{"location":"06_error_handling/#21-at-application-startup","title":"2.1 At application startup","text":"<pre><code># openapi.yaml missing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba OpenAPISpecLoadError\n# operationId without a handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba MissingOperationHandler\n# operation with no operationId declared \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba MissingOperationHandler\n</code></pre> <p>Because these raise during <code>OpenAPIFirstApp(...)</code> construction, CI catches them the moment a spec and its routes drift \u2014 before a single request is served.</p>"},{"location":"06_error_handling/#22-at-client-construction","title":"2.2 At client construction","text":"<pre><code>OpenAPIClient(spec) # fails fast, same philosophy\n</code></pre> <ul> <li>Spec with no <code>servers</code> \u2192 <code>OpenAPIClientError</code></li> <li>Spec with no <code>paths</code> \u2192 <code>OpenAPIClientError</code></li> <li>Duplicate <code>operationId</code>s \u2192 <code>OpenAPIClientError</code> (client methods must be unambiguous)</li> <li>Operation missing <code>operationId</code> \u2192 <code>OpenAPIClientError</code></li> </ul>"},{"location":"06_error_handling/#23-at-call-time-client","title":"2.3 At call time (client)","text":"<p>Runtime transport errors surface as <code>httpx</code> exceptions (<code>httpx.RequestError</code> family), not swallowed or remapped. Missing required args fail before any HTTP request is made:</p> <pre><code>client.get_user(path_params={\"user_id\": ...}) # OK\nclient.get_user() # ValueError \u2014 user_id required\n</code></pre>"},{"location":"06_error_handling/#3-handling-in-your-app","title":"\ud83e\uddf0 3. Handling in Your App","text":""},{"location":"06_error_handling/#31-server-side","title":"3.1 Server-side","text":"<p>Handlers raise FastAPI <code>HTTPException</code> for expected operation-level failures (404/422), and the <code>OperationId</code>-binding errors only exist at startup:</p> <pre><code>from fastapi import HTTPException\n\ndef get_item(item_id: int):\n \"\"\"Retrieve an item by ID.\n\n Implements the OpenAPI operation ``get_item``.\n\n Args:\n item_id (int): Identifier of the item.\n\n Raises:\n HTTPException: If the item does not exist (404).\n \"\"\"\n try:\n return _get_item(item_id)\n except KeyError:\n raise HTTPException(status_code=404, detail=\"Item not found\")\n</code></pre>"},{"location":"06_error_handling/#32-client-side","title":"3.2 Client-side","text":"<pre><code>import httpx\n\ntry:\n response = client.get_item(path_params={\"item_id\": 1})\nexcept httpx.HTTPStatusError as exc:\n ... # 4xx/5xx from the server\n</code></pre> <p><code>httpx.HTTPStatusError</code> isn't raised by the library \u2014 it's the standard <code>httpx.raise_for_status()</code> you can opt into per call. The library never masks a response code.</p>"},{"location":"06_error_handling/#4-fail-fast-guarantees-recap","title":"\ud83d\udee1\ufe0f 4. Fail-Fast Guarantees Recap","text":"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 <p>None of these can silently degrade: a violation is an exception at construction, not a 500 at request time.</p>"},{"location":"06_error_handling/#related","title":"Related","text":"<ul> <li>01 \u2013 Overview \u00b7 05 \u2013 Security \u00b7 07 \u2013 Testing</li> </ul>"},{"location":"07_testing/","title":"Testing \u2014 Smoke-First, Contract-First","text":"<p>Everything <code>openapi-first</code> ships is tested against real specs, real handlers, and real HTTP through FastAPI's <code>TestClient</code> and the bundled templates. There are no fakes of the library itself.</p>"},{"location":"07_testing/#1-suite-overview","title":"\ud83e\uddea 1. Suite Overview","text":"<pre><code>tests/\n\u251c\u2500\u2500 conftest.py # fixtures: spec_file, routes_module, app, client\n\u251c\u2500\u2500 test_app.py # OpenAPIFirstApp: routes served, overrides, fail-fast\n\u251c\u2500\u2500 test_binder.py # bind_routes: opId resolution + missing-handler failures\n\u251c\u2500\u2500 test_loader.py # load_openapi: json/yaml, env resolution, validation\n\u2514\u2500\u2500 test_client.py # OpenAPIClient: opId\u2192callable, params, error cases\n</code></pre> <p>Run with:</p> <pre><code>pytest # 23 tests, zero mocks of the library\npytest -q\npytest tests/test_loader.py\n</code></pre>"},{"location":"07_testing/#2-the-fixture-pattern","title":"\ud83c\udfd7\ufe0f 2. The Fixture Pattern","text":"<pre><code># conftest.py (abridged)\n@pytest.fixture\ndef spec_file(tmp_path):\n path = tmp_path / \"openapi.json\"\n path.write_text(json.dumps(SPEC), encoding=\"utf-8\")\n return str(path)\n\n@pytest.fixture\ndef app(spec_file):\n return OpenAPIFirstApp(\n openapi_path=spec_file,\n routes_module=routes_module(),\n )\n\n@pytest.fixture\ndef client(spec_file):\n return OpenAPIClient(json.loads(Path(spec_file).read_text()))\n</code></pre>"},{"location":"07_testing/#3-whats-actually-asserted","title":"\ud83e\uddea 3. What's Actually Asserted","text":""},{"location":"07_testing/#31-app-level-smoke","title":"3.1 App-level smoke","text":"<pre><code>def test_app_routes_served(spec_file):\n app = OpenAPIFirstApp(openapi_path=spec_file, routes_module=routes)\n client = TestClient(app)\n assert client.get(\"/health\").json() == {\"status\": \"ok\"}\n</code></pre>"},{"location":"07_testing/#32-fail-fast-the-heart","title":"3.2 Fail-fast (the heart)","text":"<pre><code>def test_app_missing_handler_fails_at_startup(spec_file):\n with pytest.raises(MissingOperationHandler):\n OpenAPIFirstApp(openapi_path=spec_file, routes_module=empty_routes)\n</code></pre>"},{"location":"07_testing/#33-loader-validation","title":"3.3 Loader validation","text":"<pre><code>def test_loader_invalid_spec_fails():\n with pytest.raises(OpenAPISpecLoadError):\n load_openapi(\"broken.yaml\")\n</code></pre>"},{"location":"07_testing/#34-client-contract-drift","title":"3.4 Client contract drift","text":"<pre><code>def test_client_duplicate_operation_id_raises():\n with pytest.raises(OpenAPIClientError):\n OpenAPIClient(dup_spec)\n</code></pre>"},{"location":"07_testing/#4-testing-the-templates","title":"\ud83c\udfdb\ufe0f 4. Testing the Templates","text":"<p>Each bundled template ships its own test + in-memory store, so you get a runnable contract test the moment you scaffold:</p> <pre><code>openapi-first scaffold crud_app my-service\ncd my-service\npytest -q\n</code></pre> <p><code>test_crud_app.py</code> / <code>test_model_app.py</code> / <code>test_vet_app.py</code> exercise the full CRUD surface through <code>TestClient</code> \u2014 including <code>201</code>/<code>204</code> status codes, 404s, and (in <code>vet_app</code>) SSE streaming via <code>StreamingResponse</code>.</p>"},{"location":"07_testing/#5-quality-gates-ci","title":"\ud83d\udd79\ufe0f 5. Quality Gates (CI)","text":"<p>The same gates the library itself must pass are what keep the docs honest:</p> Gate Purpose <code>black --check</code> formatting parity <code>ruff check</code> lint hygiene <code>mypy</code> type safety (strict, <code>--disable-error-code</code> where intentional) <code>pytest</code> 23 tests, green coverage tracked via pytest-cov (HTML + XML + term) <p>Run the whole gate locally:</p> <pre><code>black --check openapi_first tests\nruff check openapi_first tests\nmypy openapi_first\npytest\n</code></pre>"},{"location":"07_testing/#6-testing-tips","title":"\ud83d\udca1 6. Testing Tips","text":"<ul> <li>Start from startup: assert <code>OpenAPIFirstApp(...)</code> raises for the broken contracts \u2014 those are your most valuable tests</li> <li>Client \u2194 server: smoke a client against the same spec the app was built from \u2014 one spec, two sides, zero drift</li> <li>Templates are scaffolds: their tests are copyable starting points, not canonical suites</li> <li>No mocking of the library: exercise loader \u2192 binder \u2192 app \u2192 client as a real pipeline</li> </ul>"},{"location":"07_testing/#related","title":"Related","text":"<ul> <li>01 \u2013 Overview \u00b7 02 \u2013 Components \u00b7 04 \u2013 Error Handling</li> </ul>"},{"location":"03_use_cases/01_quickstart/","title":"Use Case 1: Quickstart \u2014 Build Your First OpenAPI-First Service","text":"<p>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.</p>"},{"location":"03_use_cases/01_quickstart/#1-prerequisites","title":"\ud83d\udee0\ufe0f 1. Prerequisites","text":"<ul> <li>Python 3.10+</li> <li><code>openapi-first</code> installed (see Overview)</li> <li><code>pip install \"fastapi[standard]\"</code> (or <code>uvicorn</code>) to run the app</li> </ul>"},{"location":"03_use_cases/01_quickstart/#2-write-the-openapi-document","title":"\ud83d\udcc4 2. Write the OpenAPI document","text":"<p>OpenAPI comes first. Create <code>openapi.yaml</code>:</p> <pre><code>openapi: 3.0.3\ninfo:\n title: Greeting Service\n version: 1.0.0\nservers:\n - url: http://localhost:8000\npaths:\n /greet/{name}:\n get:\n operationId: get_greeting\n parameters:\n - name: name\n in: path\n required: true\n schema:\n type: string\n responses:\n \"200\":\n description: A greeting\n content:\n application/json:\n schema:\n type: object\n properties:\n greeting:\n type: string\n</code></pre> <p>Key points: every operation needs <code>operationId</code>, and every route must exist only here.</p>"},{"location":"03_use_cases/01_quickstart/#3-write-the-handlers","title":"\ud83e\uddd1\u200d\ud83d\udcbb 3. Write the handlers","text":"<p>Create <code>routes.py</code> \u2014 plain functions, no decorators, named exactly like the <code>operationId</code>s:</p> <pre><code># routes.py\ndef get_greeting(name: str) -> dict:\n \"\"\"Return a greeting for the given name.\"\"\"\n return {\"greeting\": f\"Hello, {name}!\"}\n</code></pre> <p>If a handler is missing at startup, the app refuses to boot (<code>MissingOperationHandler</code>) \u2014 the fail-fast guarantee catches contract drift immediately.</p>"},{"location":"03_use_cases/01_quickstart/#4-bootstrap-the-app","title":"\ud83d\ude80 4. Bootstrap the app","text":"<p>Create <code>main.py</code>:</p> <pre><code># main.py\nfrom openapi_first.app import OpenAPIFirstApp\nimport routes\n\napp = OpenAPIFirstApp(\n openapi_path=\"openapi.yaml\",\n routes_module=routes,\n title=\"Greeting Service\",\n)\n</code></pre> <p>Run it:</p> <pre><code>uvicorn main:app --reload\n</code></pre> <p>Visit <code>http://localhost:8000/docs</code> (Swagger UI) and <code>http://localhost:8000/openapi.json</code> \u2014 both are generated from your spec.</p>"},{"location":"03_use_cases/01_quickstart/#5-call-it-with-the-client","title":"\ud83d\udce1 5. Call it with the client","text":"<p>The same spec builds a strict client:</p> <pre><code># client.py\nfrom openapi_first.loader import load_openapi\nfrom openapi_first.client import OpenAPIClient\n\nspec = load_openapi(\"openapi.yaml\")\nclient = OpenAPIClient(spec)\n\nresponse = client.get_greeting(path_params={\"name\": \"Ada\"})\nprint(response.status_code) # 200\nprint(response.json()) # {\"greeting\": \"Hello, Ada!\"}\n</code></pre>"},{"location":"03_use_cases/01_quickstart/#6-next-steps","title":"\ud83d\udca1 6. Next Steps","text":"<ul> <li>Copy a fuller example: 02 \u2013 Templates</li> <li>Generate models/routes from a bigger spec: 04 \u2013 Codegen</li> <li>Drive everything from a client: 03 \u2013 Client</li> </ul>"},{"location":"03_use_cases/01_quickstart/#related","title":"Related","text":"<ul> <li>01 \u2013 Overview \u00b7 02 \u2013 Components \u00b7 01 \u2013 Quickstart</li> </ul>"},{"location":"03_use_cases/02_templates/","title":"Use Case 2: Templates \u2014 Copyable Reference Applications","text":"<p><code>openapi-first</code> ships four runnable, copyable applications under <code>openapi_first/templates/</code>. They are not part of the library API \u2014 they are bundled scaffold examples you copy into your own project and build on.</p>"},{"location":"03_use_cases/02_templates/#1-what-templates-are","title":"\ud83c\udfac 1. What Templates Are","text":"<p>A template is a complete, self-contained OpenAPI-first service:</p> <ul> <li>A bundled directory inside <code>openapi_first/templates/<name>/</code></li> <li>Copyable verbatim \u2014 no code generation, no mutation \u2014 via the CLI</li> <li>Each one demonstrates a specific set of OpenAPI-first behaviors and FastAPI features</li> </ul> <p>All templates share the same skeleton:</p> <pre><code><name>_app/\n\u251c\u2500\u2500 __init__.py # explains the template + how to scaffold it\n\u251c\u2500\u2500 openapi.yaml # the contract (source of truth)\n\u251c\u2500\u2500 main.py # assembles OpenAPIFirstApp from the spec\n\u251c\u2500\u2500 routes.py # operationId-bound handler functions\n\u2514\u2500\u2500 data.py # in-memory data store (demo only)\n</code></pre>"},{"location":"03_use_cases/02_templates/#2-the-four-templates","title":"\ud83d\udccb 2. The Four Templates","text":""},{"location":"03_use_cases/02_templates/#21-health_app-minimal-liveness-probe","title":"2.1 <code>health_app</code> \u2014 minimal liveness probe","text":"<pre><code>openapi-first scaffold health_app\n# or into a custom directory:\nopenapi-first scaffold health_app my-health-service\n</code></pre> File Purpose <code>openapi.yaml</code> <code>GET /health</code> \u2192 <code>operationId: get_health</code> <code>routes.py</code> <code>get_health()</code> returns <code>{\"status\": \"ok\"}</code> <code>main.py</code> <code>OpenAPIFirstApp(openapi_path=\"openapi.yaml\", routes_module=routes)</code> <p>Why it exists: the absolute minimal OpenAPI-first round trip \u2014 one operation, one handler, zero moving parts. The best starting point to internalize the mental model.</p> <p>Smoke test:</p> <pre><code>pip install -e .\nuvicorn main:app\ncurl http://localhost:8000/health\n# \u2192 {\"status\": \"ok\"}\n</code></pre>"},{"location":"03_use_cases/02_templates/#22-crud_app-dict-based-crud","title":"2.2 <code>crud_app</code> \u2014 dict-based CRUD","text":"<pre><code>openapi-first scaffold crud_app my-crud-service\n</code></pre> File Purpose <code>openapi.yaml</code> Full CRUD over <code>/items</code> (list/get/create/update/delete) <code>routes.py</code> Handlers bound via <code>operationId</code>s <code>list_items</code>, <code>get_item</code>, <code>create_item</code>, <code>update_item</code>, <code>delete_item</code> <code>data.py</code> In-memory dict store with auto-incrementing <code>id</code> <p>Behaviors you learn:</p> <ul> <li>Explicit status codes \u2014 <code>create_item</code>/<code>delete_item</code> take <code>response: Response</code> and set <code>201</code>/<code>204</code>; <code>get_item</code>/<code>update_item</code> raise <code>HTTPException(404)</code> on <code>KeyError</code></li> <li>Handlers as plain callables \u2014 no FastAPI decorators, routing comes solely from the spec</li> <li>Mock data store \u2014 <code>data.py</code> is a copyable in-memory store, explicitly not production-ready</li> </ul>"},{"location":"03_use_cases/02_templates/#23-model_app-pydantic-model-crud","title":"2.3 <code>model_app</code> \u2014 Pydantic model CRUD","text":"<pre><code>openapi-first scaffold model_app my-model-service\n</code></pre> File Purpose <code>openapi.yaml</code> Same CRUD surface, schemas reference models <code>models.py</code> Pydantic <code>Item</code>, <code>ItemCreate</code>, <code>ItemBase</code> (request/response models) <code>routes.py</code> Handlers type-annotated with the models; <code>create_item</code> sets <code>201</code> <code>data.py</code> In-memory store returning real model instances <p>Behaviors you learn:</p> <ul> <li>Pydantic request/response models \u2014 payloads validated and serialized via FastAPI</li> <li>Same handler contracts \u2014 identical <code>operationId</code> set as <code>crud_app</code>, so the two are interchangeable</li> <li>Models in the client too \u2014 the same spec drives <code>OpenAPIClient</code> body handling</li> </ul>"},{"location":"03_use_cases/02_templates/#24-vet_app-the-full-featured-demo","title":"2.4 <code>vet_app</code> \u2014 the full-featured demo","text":"<pre><code>openapi-first scaffold vet_app my-vet-clinic\n</code></pre> File Purpose <code>openapi.yaml</code> Five resources (parents, vets, treatments, pets, appointments) + SSE + upload + discriminated unions <code>models.py</code> Pydantic models incl. discriminated unions (<code>noteType</code> literal fields) <code>routes.py</code> ~20 handlers across all resources, incl. pagination, filtering, photo upload, SSE streaming <code>sse.py</code> Server-Sent Events helper (<code>StreamingResponse</code>, per-pet subscriber queues, background <code>asyncio</code> workers) <code>data.py</code> Larger in-memory store (parents \u2192 vets \u2192 treatments \u2192 pets \u2192 appointments) <code>main.py</code> App + CORS + lifespan example <p>Behaviors you learn \u2014 the advanced tier:</p> <ul> <li>Discriminated unions \u2014 <code>ProcedureNotes</code> uses <code>oneOf</code> + <code>discriminator.noteType</code> mapping; Pydantic models use <code>Literal[...]</code> discriminator fields</li> <li>SSE streaming \u2014 a <code>GET /pets/{id}/actions</code> operation streaming <code>text/event-stream</code> via <code>StreamingResponse</code> with background task workers</li> <li>File upload \u2014 <code>UploadFile</code> handler setting a multi-part body</li> <li>CORS + middleware \u2014 <code>add_middleware(CORSMiddleware, ...)</code> alongside the spec-driven setup</li> <li>Response injection \u2014 handlers set <code>201</code>/<code>204</code> explicitly via injected <code>Response</code></li> </ul>"},{"location":"03_use_cases/02_templates/#3-cli-reference","title":"\ud83d\ude80 3. CLI Reference","text":"<pre><code># List available templates\nopenapi-first scaffold --list\n\n# Copy a template into its default directory (template name, dashes)\nopenapi-first scaffold health_app\n\n# Copy into a custom target directory\nopenapi-first scaffold crud_app my-project/crud\n</code></pre> <p>Protip: <code>DEFAULT_TEMPLATE</code> is <code>health_app</code>, so <code>openapi-first scaffold</code> with no template name scaffolds the health app.</p>"},{"location":"03_use_cases/02_templates/#4-anatomy-of-a-scaffolded-service","title":"\ud83e\udde9 4. Anatomy of a Scaffolded Service","text":"<p>After <code>openapi-first scaffold health_app my-health-service</code>, your directory contains a drop-in FastAPI service:</p> <pre><code>my-health-service/\n\u251c\u2500\u2500 openapi.yaml # THE contract\n\u251c\u2500\u2500 main.py # `app = OpenAPIFirstApp(openapi_path=..., routes_module=routes)`\n\u2514\u2500\u2500 routes.py # `def get_health(): ...`\n</code></pre> <p>Run it:</p> <pre><code>cd my-health-service\npip install -e .\nuvicorn main:app --reload\n</code></pre> <p><code>/docs</code>, <code>/openapi.json</code>, and every declared route now exist \u2014 all derived from <code>openapi.yaml</code>.</p>"},{"location":"03_use_cases/02_templates/#5-production-disclaimer","title":"\u26a0\ufe0f 5. Production Disclaimer","text":"<p>Templates use in-memory, non-persistent, non-concurrency-safe data stores. They are learning scaffolds \u2014 not production references. Swap in a real data layer (SQL/REDIS/object store) the moment you go beyond a demo.</p> <p>See the <code>__init__.py</code> of each template for detailed client examples, CLI examples, and design notes.</p>"},{"location":"03_use_cases/02_templates/#related","title":"Related","text":"<ul> <li>01 \u2013 Quickstart \u00b7 04 \u2013 Codegen \u00b7 02 \u2013 Components</li> </ul>"},{"location":"03_use_cases/03_client/","title":"Use Case 3: The OperationId-Driven Client","text":"<p><code>OpenAPIClient</code> is the other side of the contract. It reads the same OpenAPI document the server runs on and exposes one callable per <code>operationId</code> \u2014 so \"client\" and \"server\" are two views of one truth.</p>"},{"location":"03_use_cases/03_client/#1-before-you-start","title":"\ud83d\udd0d 1. Before You Start","text":"<p>The client is <code>httpx</code>-based and returns raw <code>httpx.Response</code> objects \u2014 no magic deserialization, no hidden schema inference:</p> <ul> <li>No response Pydantic models</li> <li>No implicit URL construction (path params are explicit)</li> <li>No hand-written <code>requests.get(...)</code> scattered through your code</li> </ul>"},{"location":"03_use_cases/03_client/#2-constructing-the-client","title":"\ud83e\uddec 2. Constructing the Client","text":"<pre><code>from openapi_first.loader import load_openapi\nfrom openapi_first.client import OpenAPIClient\n\nspec = load_openapi(\"openapi.yaml\")\nclient = OpenAPIClient(spec)\n</code></pre> <p>The base URL comes from the spec's <code>servers</code> list (first entry) unless you pass <code>base_url</code> explicitly:</p> <pre><code>client = OpenAPIClient(spec, base_url=\"https://api.internal.myco/v1\")\n</code></pre> <p>You can also hand over a preconfigured <code>httpx.Client</code> (custom transport, TLS, retries):</p> <pre><code>import httpx\n\ntransport = httpx.HTTPTransport(retries=3)\nclient = OpenAPIClient(\n spec,\n client=httpx.Client(transport=transport),\n)\n</code></pre>"},{"location":"03_use_cases/03_client/#3-fail-fast-at-construction","title":"\ud83d\udca5 3. Fail-Fast at Construction","text":"<p><code>OpenAPIClient(...)</code> raises immediately if the contract is broken \u2014 you find out at startup, not on the first request:</p> Violation Error Spec has no <code>servers</code> entry <code>OpenAPIClientError</code> Spec has no <code>paths</code> <code>OpenAPIClientError</code> Operation missing <code>operationId</code> <code>OpenAPIClientError</code> Duplicate <code>operationId</code> <code>OpenAPIClientError</code> Operation references unknown parameters <code>OpenAPIClientError</code> <p>Missing required parameters fail at call time \u2014 pydoclint-grade strictness on the wire.</p>"},{"location":"03_use_cases/03_client/#4-calling-operations","title":"\ud83d\udcde 4. Calling Operations","text":"<p>Every <code>operationId</code> becomes a method. The call signature is uniform across the whole client:</p> <pre><code>response = client.<operationId>(\n *,\n path_params: dict | None = None,\n query: dict | None = None,\n headers: dict | None = None,\n body: Any | None = None,\n timeout: float | None = None,\n) -> httpx.Response\n</code></pre> <p>Concrete examples (from the <code>crud_app</code> / <code>health_app</code> templates):</p> <pre><code># No parameters \u2014 simplest\nresponse = client.get_health()\nassert response.status_code == 200\n\n# Path parameter\nresponse = client.get_item(path_params={\"item_id\": 3})\n\n# Query parameters\nresponse = client.list_items(query={\"limit\": 10, \"offset\": 20})\n\n# JSON request body\nresponse = client.create_item(body={\"name\": \"Orange\", \"price\": 0.8})\n\n# Custom headers / timeout\nresponse = client.get_user(\n path_params={\"user_id\": 1},\n headers={\"X-Internal-Key\": \"...\"},\n timeout=30,\n)\n</code></pre> <p>Returns: the raw <code>httpx.Response</code>, so <code>status_code</code>, <code>.json()</code>, <code>.headers</code> are all yours to inspect.</p>"},{"location":"03_use_cases/03_client/#5-how-parameters-are-bound","title":"\ud83e\udde0 5. How Parameters Are Bound","text":"<p>For each operation the client knows exactly where each parameter belongs:</p> OpenAPI location Client kwarg <code>in: path</code> <code>path_params[name]</code> <code>in: query</code> <code>query[name]</code> <code>in: header</code> <code>headers[name]</code> <code>requestBody</code> <code>body</code> <p>JSON media types are sent as <code>json=</code>; other media types as raw <code>content=</code>.</p>"},{"location":"03_use_cases/03_client/#6-server-client-one-spec","title":"\ud83d\udd04 6. Server \u2194 Client, One Spec","text":"<pre><code># One directory, two processes\nuvicorn main:app --port 8000 # server\npython -c \"import asyncio; from client_script import run; asyncio.run(run())\" # client\n</code></pre> <p>Or the same client against a remote environment:</p> <pre><code>client = OpenAPIClient(\n spec,\n base_url=\"https://staging.internal.myco\",\n)\n</code></pre> <p>The URL is the only thing that changes between environments \u2014 the contract never does.</p>"},{"location":"03_use_cases/03_client/#7-operationid-as-the-api","title":"\u270d\ufe0f 7. OperationId as the API","text":"<p>Because the client is operationId-driven:</p> <ul> <li>Adding an operation = adding a method (and vice versa)</li> <li>No operation can be \"forgotten\" by the client \u2014 it's constructed from the spec</li> <li><code>operationId</code> is the one name you remember; the HTTP verb/path is an implementation detail</li> </ul> <p>If an <code>operationId</code> you call doesn't exist, you get an <code>AttributeError</code> at construction-scan time \u2014 never a silent 404.</p>"},{"location":"03_use_cases/03_client/#related","title":"Related","text":"<ul> <li>01 \u2013 Overview \u00b7 01 \u2013 Quickstart \u00b7 02 \u2013 Templates</li> </ul>"},{"location":"03_use_cases/04_codegen/","title":"Codegen \u2014 Generate Models & Routes From Your Spec","text":"<p>TL;DR \u2014 point <code>openapi-first codegen</code> at your spec and get <code>models</code> + <code>routes</code> scaffolds you can bind with one line. The codegen output is deterministic, and it's a starting point \u2014 not a maintained artifact.</p>"},{"location":"03_use_cases/04_codegen/#1-why-celebrate-codegen","title":"\ud83e\udded 1. Why Celebrate Codegen?","text":"<p>Two pain points kill OpenAPI projects:</p> <ol> <li>The \"write the spec, then write the same thing as Pydantic models\" step \u2014 exactly where server/client param types drift (your route says <code>item_id: int</code>, your client sends a string\u2026).</li> <li>Writing the docs/spec catalog by hand, once you have more than a handful of operations.</li> </ol> <p>Codegen collapses both. One command, one source file, same generated shapes everywhere.</p>"},{"location":"03_use_cases/04_codegen/#2-model-generation","title":"\ud83d\ude80 2. Model Generation","text":"<pre><code>openapi-first codegen models --module my_project.models --input openapi.yaml\n</code></pre> <p>The generated model module mirrors the spec's <code>components.schemas</code> by name:</p> Spec Generated <code>components.schemas.User</code> <code>class User(BaseModel)</code> <code>components.schemas.Item</code> <code>class Item(BaseModel)</code> every <code>$ref</code> (schema) a <code>type: ClassVar</code> alias \u2713 <code>required + type</code> from schema Pydantic <code>Field(...)</code> / type hints \u2713 <p>Never hand-edit those files. If the spec changes, regenerate \u2014 just like you'd re-run <code>cargo build</code> after editing <code>Cargo.toml</code>.</p>"},{"location":"03_use_cases/04_codegen/#why-constructor-time-validation-still-applies","title":"Why constructor-time validation still applies","text":"<p>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 \u2013 Design and 06 \u2013 Error Handling.</p>"},{"location":"03_use_cases/04_codegen/#3-route-generation-verification","title":"\ud83d\udd01 3. Route Generation / Verification","text":"<pre><code># dry-run verification against your routes module\nopenapi-first codegen routes --module my_routes --input openapi.yaml --check\n\n# scaffold an operation skeleton (generates the handler with a TODO)\nopenapi-first codegen routes --module my_routes --input openapi.yaml\n</code></pre> <p>Why bother? Because <code>bind_routes</code> (in 02 \u2013 Components) needs an <code>operationId</code> \u2192 handler exactly matching the spec. Codegen guarantees you never type a handler name wrong \u2014 it wears the same <code>operationId</code> as the spec says.</p> <p>Note: <code>codegen</code> is build-time tooling. It does not run at runtime, and templates (the Bake-your-stuff section of 02 \u2013 Templates) make scaffolding a server out-of-the-box even simpler for greenfield projects.</p>"},{"location":"03_use_cases/04_codegen/#4-idempotent-output","title":"\ud83e\uddea 4. Idempotent Output","text":"<p>Generated output is deterministic w.r.t. the spec: - Same spec \u2192 byte-identical files (unless you hand-edit \u2014 you won't) - Ordering follows spec declaration order - No timestamps, no machine names, no hidden randomness</p> <p>That determinism is what lets you <code>git diff</code> after a spec change and see exactly what moved.</p>"},{"location":"03_use_cases/04_codegen/#related","title":"\ud83e\udde9 Related","text":"<ul> <li>01 \u2013 Overview \u00b7 02 \u2013 Templates \u00b7 03 \u2013 Client \u00b7 04 \u2013 Design \u00b7 07 \u2013 Testing</li> </ul>"}]} |