diff --git a/doc-forge/wiki/05_development/05_mkdocs_configs/index.html b/doc-forge/wiki/05_development/05_mkdocs_configs/index.html index 24dca41..bd7afe6 100644 --- a/doc-forge/wiki/05_development/05_mkdocs_configs/index.html +++ b/doc-forge/wiki/05_development/05_mkdocs_configs/index.html @@ -675,6 +675,15 @@ + + +
Commit docs/mkdocs.{kind}.yml so served docs are reproducible from source.
Kinds are separate MkDocs sites served side by side under /<repo>/<kind>/,
+so a wiki page may link to the library reference with a source-relative path
+such as ../lib/13_library_registry/. MkDocs only rewrites links that point
+inside docs_dir; anything reaching a sibling kind is emitted verbatim and,
+under use_directory_urls, resolves into <kind>/<kind>/ and breaks.
The docforge-linkfix plugin (part of doc-forge, enabled in the wiki
+template) fixes this at render time: it rewrites ../lib/, ../api/, and
+../wiki/ links to climb to the repository root, so they resolve correctly
+however deeply the page is nested.
docforge is an internal documentation tool that generates reference documentation from Google-Styled Doc-Forge Convention (GSDFC) docstrings and assembles it into per-kind MkDocs sites, alongside hand-written wiki pages and OpenAPI-based API docs.
Doc model: this wiki is written for humans \u2014 how\u2011to guides, examples, and testing recipes. The authoritative API contracts live in the code (docstrings) and the machine\u2011readable bundle under docs/mcp/.
site/{kind}# Internal PyPI (TBD)\npip install docforge\n"},{"location":"#documentation-structure","title":"\ud83d\udcc1 Documentation Structure","text":"Section Description Overview What docforge is and how it fits the pipeline Components Loaders, models, nav, renderers, CLI, servers Conventions GSDFC, .pyi stubs, and template conventions Iterative Workflow How a docs build runs end to end Environment Environment setup Quality Gates Test layout and quality checks GSDFC Guide How to write GSDFC-conformant docstrings MCP Guide Building, serving, and consuming the MCP bundle MkDocs Configs MkDocs config generation and templates"},{"location":"#related-resources","title":"\ud83d\udd17 Related Resources","text":"doc-forge repositorydoc-forge build --mcp --module docforge into docs/mcp/docs/wiki/; use the prompt in the wiki-related resource below # AGENTS.md \u2014 Wiki Authoring Guide (Aetoskia)\n\nThis file instructs AI agents how to create, extend, and maintain the wiki\n(`docs/wiki/**`) for any Aetoskia repository.\n\n> **Workflow:** this prompt covers **phase 2 only**. Phase 1 \u2014 writing GSDFC docstrings\n> (plus `.pyi` stubs and `py.typed`) \u2014 is done by the agent using the doc-forge MCP\n> bundle: `doc://modules/docforge` carries the full GSDFC spec. Write the wiki only\n> after docstrings exist and `doc-forge build --mcp --module <pkg>` produced the\n> repo's own bundle.\n\n---\n\n## 1. Doc model \u2014 know the two sources of truth\n\nEvery repo keeps two distinct kinds of documentation that must **not** bleed\ninto each other:\n\n| Kind | Source | Where | Audience |\n|-------------------|-----------------------|------------------------------------------|------------------------------------|\n| **Wiki** | Hand-written markdown | `docs/wiki/**` | Humans (how-to, examples, testing) |\n| **API reference** | Docstrings (GSDFC) | Generated \u2192 `docs/lib/**`, `docs/mcp/**` | Consumers (exact contracts) |\n\nRules:\n\n- The wiki is written **for humans** \u2014 how-to guides, recipes, and testing\n patterns. It is never generated.\n- API contracts (signatures, parameter types, exact behavior) live in the\n docstrings and the machine-readable bundle under `docs/mcp/`. **Never**\n duplicate full API documentation in the wiki \u2014 link to the lib site instead.\n- If a page starts to drift into contract territory, cut it down to usage and\n point at `docs/lib/`.\n- This doc model is stated on every index page as a blockquote; keep it there:\n > **Doc model:** this wiki is written for humans \u2014 how\u2011to guides, examples,\n > and testing recipes. The authoritative API contracts live in the code\n > (docstrings) and the machine\u2011readable bundle under `docs/mcp/`.\n\n---\n\n## 2. Directory layout & file naming\n\n```\ndocs/wiki/\n\u251c\u2500\u2500 index.md # site Home\n\u251c\u2500\u2500 01_overview.md\n\u251c\u2500\u2500 02_components.md\n\u251c\u2500\u2500 03_use_cases/ # nested directory \u2192 nav group\n\u2502 \u251c\u2500\u2500 01_basic_crud.md\n\u2502 \u251c\u2500\u2500 02_custom_repo.md\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 04_best_practices.md\n\u251c\u2500\u2500 06_error_handling.md\n\u2514\u2500\u2500 07_testing_example.md\n```\n\n- Location: `docs/wiki/` (hand-written only).\n- File names: `NN_snake_case_name.md` with a **zero-padded two-digit** numeric\n prefix. e.g. `01_overview.md`, `03_use_cases/07_caching.md`.\n- Top-level pages use `NN_name.md` directly. High-volume sections get their own\n directory, e.g. `03_use_cases/NN_name.md` (nav group \"Use Cases\").\n- **No frontmatter.** Every file opens directly with an H1 `# Title`.\n- Navigation labels are short human titles set explicitly in\n `docs/mkdocs.wiki.yml` (title case) \u2014 often **shorter** than the page H1.\n Example: nav label `Basic CRUD` vs page H1 `Use Case 1: Basic FastAPI CRUD API`.\n Never use the raw filename slug as the label.\n- Renumbering: prefixes order pages. When inserting a page, pick the next\n number in the relevant group (subgroup numbers stay grouped).\n\n---\n\n## 3. Navigation \u2014 `docs/mkdocs.wiki.yml` is the source of truth\n\nEvery wiki page **must** be wired into the nav defined in\n`docs/mkdocs.wiki.yml`. If a page is not in the nav, it is orphaned.\n\n- The `nav:` block mirrors the directory tree; directories become nested\n groups.\n- When a section has many pages, group them **by theme** into nested\n subgroups. mongo-ops groups its use cases into:\n `Getting Started`, `Data & Queries`, `Caching`, `Population`,\n `Advanced Usage`.\n- Keep `index.md`'s **Documentation Structure** table in sync with the nav \u2014\n it is the human-facing version of the same TOC. Every nav entry needs a row\n (or nested bullet group) there.\n- Nav label = human title (title case), not the filename slug.\n\n---\n\n## 4. Page anatomy\n\n### 4.1 `index.md` (Home)\n\n```\n# \ud83e\udde9 <package> \u2014 <slogan>\n<one-paragraph pitch>\n\n> **Doc model:** \u2026 (see \u00a71)\n\n---\n\n## \ud83d\ude80 Key Features (bullet list, emoji per feature)\n## \ud83d\udce6 Installation (code blocks: internal PyPI, extras, local source)\n## \ud83d\udcc1 Documentation Structure (table = TOC, mirrors nav exactly)\n## \ud83d\udd17 Related Resources (bullets: source repo, internal PyPI, CI)\n---\n\u00a9 Aetoskia Internal \u2014 `<package>` <version>\n```\n\n### 4.2 Overview page (`01_*`)\n\n- Opens with `# Library Overview`, one sentence about what the library does,\n then a line stating the page builds the mental model.\n- Sections: `## \ud83c\udfd7\ufe0f Architecture` (ASCII diagram + numbered layer list),\n `## \ud83d\udd04 Lifecycle Rules` (table: step / call / why),\n `## \ud83e\udded Which \u2026 Should You Use?` (decision table).\n- Ends with `## \u27a1\ufe0f Read Next` (cross-links to the next pages to read).\n\n### 4.3 Components page (`02_*`)\n\n- Validated reference for the public API surface \u2014 describes what each\n component is for and how the pieces wire together. Keep it usage-level;\n exact signatures belong in `docs/lib/`.\n- Anatomy: numbered sections, one per component (`## 1. MongoConnectionManager`,\n `## 2. BaseDocument`, \u2026), with `### 8.1`-style sub-numbering for related\n types under a component (e.g. the cache layer's backend / stats / config /\n in-memory / redis implementations). Ends with `## Related`.\n\n### 4.4 Use-case page (the most common)\n\nStandard anatomy, in this order:\n\n```\n# Use Case N: <Human Title>\n\n**Scenario:** <one sentence: the problem this recipe solves>\n\n---\n\n## \ud83d\udce6 What's New? (table: Component | Description \u2014 new API used here)\n## \ud83d\ude80 Example (languaged code block, complete runnable-looking snippet)\n## \ud83d\udca1 Tips (bullets: gotchas, ordering, common mistakes)\n## Related (\u00a75)\n```\n\n### 4.5 Deep-dive / worked-example page\n\nFor intricate flows (population shapes, cache round-trips), use numbered\ndepth-first sections and comparison tables:\n\n```\n## 1. <Write path> (### 1a., ### 1b., \u2026 per sub-case)\n## 2. <Read path>\n## 3. <Round trip / tricky bit>\n## 4. When to use which (comparison table)\n## Related\n```\n\n### 4.6 Best Practices page (`04_*`)\n\n- Flat themed `##` sections (e.g. Layering, Lifecycle, Data & Performance,\n Transactions & Errors, Testing), each a short list of numbered conventions.\n- Pure prose \u2014 no large code blocks.\n\n### 4.7 Error Handling (`06_*`) / Testing pages\n\n- **Error Handling:** an exceptions table (`Exception | Source | Meaning / fix`),\n a `> ` note on which failures are by design, a `## \ud83d\ude80 FastAPI Mapping Example`\n code block, then `## \ud83d\udca1 Tips` bullets, then `## Related`.\n- **Testing example (`07_*`):** `## \ud83d\ude80 Mock-Based Quickstart` (focused code\n block), `## \ud83d\udca1 Notes` tip list, `## Related`.\n\nCommon to all pages: no frontmatter, `---` between every major section, and a\n`## Related` (or variant) footer as the **last** section.\n\n---\n\n## 5. The `## Related` footer\n\nEvery page ends with a cross-link footer. Rules:\n\n- Always the **last** section, always preceded by a `---`.\n- Standard heading: `## Related` (no emoji).\n- One bullet line; links separated by ` \u00b7 ` (middle dot plus spaces).\n- Link label format: `[NN \u2013 Title](relative_path)` \u2014 en-dash, page number, title.\n- Paths are **relative**: same directory \u2192 `02_custom_repo.md`; parent \u2192\n `../01_overview.md`; child \u2192 `03_use_cases/01_basic_crud.md`.\n- Link order follows nav order (each `Related` points to related next/sibling\n pages, plus the overview/components anchor).\n\nExamples (from mongo-ops):\n\n```\n## Related\n- [02 \u2013 Custom repository](02_custom_repo.md) \u00b7 [04 \u2013 Pagination](04_pagination.md) \u00b7 [Overview](../01_overview.md)\n```\n\nVariants:\n\n- `## \u27a1\ufe0f Read Next` \u2014 used on the overview page (linear reading order).\n- `## \ud83d\udd17 Related Resources` \u2014 only on `index.md`, one bullet per external resource.\n\n---\n\n## 6. Tone & formatting rules\n\n- **Written for humans**: short sentences, concrete examples, plain words.\n- Emoji-prefixed H2 headings for recognizable section types\n (`\ud83d\udce6 What's New?`, `\ud83d\ude80 Example`, `\ud83d\udca1 Tips`, `\ud83d\ude80 Key Features`).\n- `---` horizontal rule between every major section.\n- Code blocks always declare a language (```python, ```bash, etc.).\n- Use tables for comparisons and structured decisions; bullets for lists;\n ASCII diagrams for architecture.\n- Bold key terms inline. Use `inline code` for symbols/APIs.\n- Keep pages focused: one recipe/purpose per page. If a page balloons past\n ~10 KB, split it (add a numbered deep-dive page instead).\n- Match tone of existing pages in the repo before writing new ones.\n- Ground every `## \ud83d\ude80 Example`, `\ud83d\udca1 Tips`, and `\ud83d\udce6 What's New?` row in the\n library's actual source, tests, and its `docs/mcp` bundle \u2014 never invent\n APIs or behavior. Signatures and exceptions come from the code; gotchas\n come from the tests.\n\n---\n\n## 7. Build & verify workflow\n\nAfter creating or editing wiki pages:\n\n```bash\n# 1. Build the wiki (from the repo root)\ndoc-forge build --wiki # wiki only\ndoc-forge build --wiki --mkdocs --module <pkg> # wiki + lib together\n\n# 2. Preview locally\ndoc-forge serve --wiki\n\n# 3. Collect into the hub (run from the docs/ service repo)\npython collect.py # copies site, regenerates index + nginx.conf\npython collect.py --dry-run # preview without writing\n```\n\nVerification checklist:\n\n- `doc-forge build --wiki` completes with no warnings about missing files.\n- Every new page is present in `docs/mkdocs.wiki.yml` **and** in the index\n Documentation Structure table.\n- Every page ends with a `## Related` block and no dead links (paths resolve).\n- After `collect.py`: the hub card links to both `/wiki/` and `/lib/` and the\n pages render (spot-check with the served site).\n- If code/docstrings changed (no change when wiki-only), run the repo gates:\n `black`, `ruff`, `mypy`, `pytest`.\n\n---\n\n## 8. Before/after checklist\n\n**Before creating a page:**\n\n- [ ] Confirm it is genuinely wiki material (how-to) and not API contract.\n- [ ] Find the right filename (`NN_snake_case.md`) and directory for its theme.\n- [ ] Outline the sections per \u00a74 anatomy that fits the content.\n\n**After creating/editing a page:**\n\n- [ ] Page wired into `docs/mkdocs.wiki.yml` nav (themed subgroup if needed).\n- [ ] `index.md` Documentation Structure table updated.\n- [ ] `## Related` footer present, relative links, nav order.\n- [ ] `---` separators consistent; no frontmatter; emoji headings where apt.\n- [ ] Build passes; collect run; hub shows `/wiki/` + `/lib/` links.\n- [ ] No orphaned files, no dead links.\n\u00a9 Aetoskia Internal \u2014 docforge 1.0
docforge turns GSDFC-compliant Python docstrings into maintainable reference documentation. It never edits source docstrings; it reads them, renders them, and assembles documentation sites from all available material.
lib GSDFC docstrings docs/lib/** rendered markdown api OpenAPI JSON spec docs/api/** rendered markdown wiki Hand-written markdown docs/wiki/** (unchanged) mcp GSDFC docstrings + renderers docs/mcp/** structured JSON"},{"location":"01_overview/#architecture","title":"\ud83c\udfd7\ufe0f Architecture","text":"Each enabled site kind produces its own MkDocs configuration and build:
--mkdocs (lib) writes docs/mkdocs.lib.yml and builds site/lib/.--api writes docs/mkdocs.api.yml and builds site/api/.--wiki writes docs/mkdocs.wiki.yml and builds site/wiki/.Every site is self-contained (own theme assets and search index) with navigation derived or scoped to that kind. Hand-written wiki content is never overwritten or regenerated \u2014 only its navigation is derived automatically.
"},{"location":"01_overview/#lifecycle-rules","title":"\ud83d\udd04 Lifecycle Rules","text":"docs/mcp/** is a self-contained, machine-readable reference generated from docforge's own GSDFC docstrings. doc-forge serve --mcp --module docforge serves it over MCP. See the MCP Guide.
docforge is split into four horizontal layers. Everything flows top to bottom through the CLI.
The docforge/loaders package wraps griffe to extract modules, functions, classes, and Google-style docstring sections. Loaded data is normalized into the object model under docforge/models (Project, Module, DocObject).
docforge/nav parses docforge.nav.yml specs (NavSpec, Resolver, and the MkDocs nav emitter) and, since the wiki kind, derives wiki navigation from the file structure via build_wiki_nav.
docforge/renderers turn model data into artifacts:
MkDocsRenderer \u2192 docs/lib/** reference markdownMCPRenderer \u2192 docs/mcp/** structured documentationdocforge/servers serves generated artifacts over live protocols:
MCPServer \u2192 serves a pre-generated docs/mcp/** bundle through MCP resources (docs://index, docs://nav, docs://modules/{module})docforge/cli wires it all together:
commands.py \u2014 the build command and its --mkdocs / --api / --wiki / --mcp modesmkdocs_utils.py \u2014 per-kind config generation (docs/mkdocs.{lib,api,wiki}.yml) with navigation re-rooted to each kind's docs_dirapi_utils.py \u2014 OpenAPI loading and API docs generationAll documented source uses the Google-Styled Doc-Forge Convention (GSDFC): Args:, Returns:, Raises:, and Attributes: sections with properly typed signatures.
The authoritative GSDFC specification lives in the docforge/__init__.py package docstring, which is exported verbatim into docs/mcp/modules/docforge.json, so the MCP bundle is self-documenting.
Key rules:
name (Type):) that match the signature types.# Summary, # Examples, and # Notes sections at module level.Args:, Returns:, Raises:, Yields:, Notes:, and Example: sections at function and method level..pyi stub alongside the .py implementation.py.typed marker so type checkers consume the authored signatures instead of Any..pyi stubs and py.typed","text":"Every module ships a matching .pyi stub kept in sync with the .py implementation. When signatures change, update both files. Packages expose a py.typed marker so consumers and type checkers use the authored signatures.
docs/wiki/** and is never generated.01_overview.md, 02_components.md.05_development/01_environment.md \u2192 group Development.index.md is the site Home at the root, and a section root inside a directory.MkDocs config fragments live in docforge/templates. The mkdocs.wiki.yml fragment carries only the search plugin, since wiki pages contain no mkdocstrings directives.
A docs build runs through the CLI in a single pass.
"},{"location":"04_iterative_workflow/#build-commands","title":"\ud83d\ude80 Build commands","text":"# Library reference only\ndoc-forge build --mkdocs --module docforge\n\n# Wiki + library (each an independent MkDocs build)\ndoc-forge build --wiki --mkdocs --module docforge\n\n# Wiki only \u2014 no module required\ndoc-forge build --wiki --site-name docforge\n\n# MCP structured bundle\ndoc-forge build --mcp --module docforge\n"},{"location":"04_iterative_workflow/#what-a-build-does","title":"\ud83d\udd04 What a build does","text":"--mkdocs, --api, --wiki, --mcp).docs/lib/** with MkDocsRenderer.docs/api/** when --api is given.docs/wiki/**.docs/mkdocs.{lib,api,wiki}.yml), re-rooting navigation paths to each kind's docs_dir.mkdocs build once per config, emitting self-contained sites site/lib/, site/api/, and site/wiki/.doc-forge build --wiki --mkdocs --module docforge\ndoc-forge serve --wiki # serves site preview from docs/mkdocs.wiki.yml\ndoc-forge serve --lib\ndoc-forge serve --api\n# or target any config directly:\ndoc-forge serve --mkdocs --mkdocs-yml docs/mkdocs.wiki.yml\n"},{"location":"04_iterative_workflow/#serve-the-mcp-bundle","title":"\ud83d\udce6 Serve the MCP bundle","text":"doc-forge build --mcp --module docforge\ndoc-forge serve --mcp --module docforge\n"},{"location":"04_iterative_workflow/#related","title":"Related","text":"python -m venv .venv\n.venv\\Scripts\\activate\npip install -e .\n Requires Python 3.10+ (per pyproject.toml).
Development extras include:
pytest, pytest-cov \u2014 test suite and coverageruff \u2014 lint and format checkingblack \u2014 auto-formattingmypy \u2014 strict typing checkspydoclint \u2014 docstring \u2194 signature consistency checksRun all checks before pushing:
"},{"location":"05_development/02_quality_gates/#commands","title":"\ud83d\ude80 Commands","text":".venv\\Scripts\\python.exe -m pytest\n.venv\\Scripts\\python.exe -m ruff check docforge tests\n.venv\\Scripts\\python.exe -m black --check docforge tests\n.venv\\Scripts\\python.exe -m mypy docforge\n.venv\\Scripts\\pydoclint.exe docforge\n"},{"location":"05_development/02_quality_gates/#test-layout","title":"\ud83d\udcc1 Test layout","text":"Path Covers tests/nav/ Nav spec, resolver, wiki nav tests/cli/ Build command flows and modes tests/renderers/ MkDocs / MCP rendering CLI tests use the cli_runner fixture with mock_mkdocs_build and mock_mkdocs_load_config so they exercise the full flow without invoking a real MkDocs build.
This page is the practical companion to the authoritative GSDFC specification in the docforge/__init__.py package docstring. It describes how to write docstrings that render correctly in MkDocs and stay machine-parseable by doc-forge, MkDocs, and MCP clients.
doc-forge compiles docstrings but never rewrites them.Modules use Markdown headings and --- separators.
Recommended sections:
# Summary \u2014 what the subsystem does# Examples \u2014 a representative usage snippet# Notes \u2014 guarantees, lifecycle, and thread-safety notesRecommended sections, in order:
Attributes: \u2014 instance attributes with name (Type): entriesNotes: \u2014 grouped subsections such as Guarantees, LifecycleExample: \u2014 indented python code blockRecommended section order:
Args:Returns:Raises:Yields:Notes:Example:Formatting rules:
Args: entries are name (Type): followed by an indented description.Returns: entries are Type: followed by an indented description.Raises: entries are ExceptionType: followed by an indented condition.Yields: replaces Returns: for generators.python blocks are allowed inside Example: sections, indented four spaces.Properties document their return values with a Returns: section and, when meaningful, an Example:.
def process(foo: Foo, multiplier: int) -> int:\n \"\"\"Process a Foo instance.\n\n Args:\n foo (Foo):\n Foo instance to process.\n\n multiplier (int):\n Value used to scale foo.\n\n Returns:\n int:\n Processed result.\n\n Raises:\n ValueError:\n If multiplier is negative.\n\n Example:\n Process foo:\n\n ```python\n foo = Foo(\"example\", value=10)\n result = process(foo, multiplier=2)\n print(result)\n ```\n \"\"\"\n"},{"location":"05_development/03_gsdfc_guide/#keeping-stubs-in-sync","title":"\ud83d\udd04 Keeping stubs in sync","text":"Every .py module ships a matching .pyi stub. When a signature or a public symbol changes, update both files. Signature annotations in the stub must match the implementation.
pydoclint (Google style) runs in CI and verifies that Args:/Returns: sections match function signatures, including types.
allow-init-docstring = true \u2014 __init__ docstrings are allowed.skip-checking-raises = true \u2014 Raises: sections are descriptive and are not required to map to literal raise statements.check-class-attributes = false \u2014 Attributes: sections document instance attributes without class-level annotations.Doc-forge can export a machine-readable MCP bundle from a project's GSDFC docstrings and serve that bundle over the Model Context Protocol (MCP). The bundle is self-contained: readers can derive the full API reference \u2014 signatures, docstrings, and structure \u2014 for the documented project.
"},{"location":"05_development/04_mcp_guide/#building-the-bundle","title":"\ud83d\ude80 Building the bundle","text":"doc-forge build --mcp --module docforge\n This writes structured JSON into docs/mcp/:
index.json \u2014 project metadata and module countnav.json \u2014 module list with docs://modules/{module} resource URIsmodules/{dotted.path}.json \u2014 per-module serialized documentationdoc-forge serve --mcp --module docforge\n The MCPServer (in docforge/servers/mcp_server.py) exposes:
docs://index Project metadata docs://nav Navigation structure docs://modules/{module} Individual module documentation The server also registers a single diagnostic tool, ping, and is read-only. Missing resources are returned as structured error dictionaries rather than exceptions. The default transport is streamable-http; stdio and sse are supported via MCPServer.run.
Each module resource contains:
path \u2014 dotted module pathdocstring \u2014 the raw GSDFC module docstringobjects \u2014 a recursive mapping of public symbols with name, kind, path, signature, and docstring, plus nested membersBecause the bundle is generated from docstrings, the quality of the bundle equals the quality of the project's GSDFC docstrings. For docforge itself, the GSDFC specification is carried in modules/docforge.json via the docforge/__init__.py package docstring, making the bundle self-documenting.
Each documentation kind (lib, api, wiki) is built by its own MkDocs site. A repo that documents kind {kind} carries a config at docs/mkdocs.{kind}.yml that sets docs_dir to the kind's source directory and site_dir to ../site/{kind}.
docs/mkdocs.{kind}.yml is a repo-owned file, not a build byproduct:
doc-forge build --{kind} materializes a copy from the packaged templates (see below) and then builds site/{kind}.site_dir are respected as-is.--refresh \u2014 force-regenerates the file from the templates, replacing any local changes.Because a present file is never rewritten, rerunning a build is a no-op for versioned configs: the working tree stays clean.
"},{"location":"05_development/05_mkdocs_configs/#templates","title":"\ud83e\udde9 Templates","text":"The built-in defaults live in docforge/templates/:
mkdocs.common.yml Shared theme + markdown extensions mkdocs.lib.yml mkdocstrings handler for library reference mkdocs.api.yml Swagger/OpenAPI plugins and API-side features mkdocs.wiki.yml Wiki look: horizontal navigation.tabs header, blue grey/teal palette, Roboto A per-kind config is generated by deep-merging the common template with the kind fragment, then filling in generation-time values (site_name, docs_dir, site_dir, nav, API site_description/site_author, and the theme icon from docforge.nav.yml). Pass --template <path> to replace the built-in templates entirely.
site/{kind} and is served under /<repo>/{kind}/.mkdocs.yml (for example mongo-ops, whose wiki uses docs_dir: docs/wiki). doc-forge never reads or writes root mkdocs.yml; the docs service maps the kind to the root site/ dir in this case.Commit docs/mkdocs.{kind}.yml so served docs are reproducible from source.
docforge is an internal documentation tool that generates reference documentation from Google-Styled Doc-Forge Convention (GSDFC) docstrings and assembles it into per-kind MkDocs sites, alongside hand-written wiki pages and OpenAPI-based API docs.
Doc model: this wiki is written for humans \u2014 how\u2011to guides, examples, and testing recipes. The authoritative API contracts live in the code (docstrings) and the machine\u2011readable bundle under docs/mcp/.
site/{kind}# Internal PyPI (TBD)\npip install docforge\n"},{"location":"#documentation-structure","title":"\ud83d\udcc1 Documentation Structure","text":"Section Description Overview What docforge is and how it fits the pipeline Components Loaders, models, nav, renderers, CLI, servers Conventions GSDFC, .pyi stubs, and template conventions Iterative Workflow How a docs build runs end to end Environment Environment setup Quality Gates Test layout and quality checks GSDFC Guide How to write GSDFC-conformant docstrings MCP Guide Building, serving, and consuming the MCP bundle MkDocs Configs MkDocs config generation and templates"},{"location":"#related-resources","title":"\ud83d\udd17 Related Resources","text":"doc-forge repositorydoc-forge build --mcp --module docforge into docs/mcp/docs/wiki/; use the prompt in the wiki-related resource below # AGENTS.md \u2014 Wiki Authoring Guide (Aetoskia)\n\nThis file instructs AI agents how to create, extend, and maintain the wiki\n(`docs/wiki/**`) for any Aetoskia repository.\n\n> **Workflow:** this prompt covers **phase 2 only**. Phase 1 \u2014 writing GSDFC docstrings\n> (plus `.pyi` stubs and `py.typed`) \u2014 is done by the agent using the doc-forge MCP\n> bundle: `doc://modules/docforge` carries the full GSDFC spec. Write the wiki only\n> after docstrings exist and `doc-forge build --mcp --module <pkg>` produced the\n> repo's own bundle.\n\n---\n\n## 1. Doc model \u2014 know the two sources of truth\n\nEvery repo keeps two distinct kinds of documentation that must **not** bleed\ninto each other:\n\n| Kind | Source | Where | Audience |\n|-------------------|-----------------------|------------------------------------------|------------------------------------|\n| **Wiki** | Hand-written markdown | `docs/wiki/**` | Humans (how-to, examples, testing) |\n| **API reference** | Docstrings (GSDFC) | Generated \u2192 `docs/lib/**`, `docs/mcp/**` | Consumers (exact contracts) |\n\nRules:\n\n- The wiki is written **for humans** \u2014 how-to guides, recipes, and testing\n patterns. It is never generated.\n- API contracts (signatures, parameter types, exact behavior) live in the\n docstrings and the machine-readable bundle under `docs/mcp/`. **Never**\n duplicate full API documentation in the wiki \u2014 link to the lib site instead.\n- If a page starts to drift into contract territory, cut it down to usage and\n point at `docs/lib/`.\n- This doc model is stated on every index page as a blockquote; keep it there:\n > **Doc model:** this wiki is written for humans \u2014 how\u2011to guides, examples,\n > and testing recipes. The authoritative API contracts live in the code\n > (docstrings) and the machine\u2011readable bundle under `docs/mcp/`.\n\n---\n\n## 2. Directory layout & file naming\n\n```\ndocs/wiki/\n\u251c\u2500\u2500 index.md # site Home\n\u251c\u2500\u2500 01_overview.md\n\u251c\u2500\u2500 02_components.md\n\u251c\u2500\u2500 03_use_cases/ # nested directory \u2192 nav group\n\u2502 \u251c\u2500\u2500 01_basic_crud.md\n\u2502 \u251c\u2500\u2500 02_custom_repo.md\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 04_best_practices.md\n\u251c\u2500\u2500 06_error_handling.md\n\u2514\u2500\u2500 07_testing_example.md\n```\n\n- Location: `docs/wiki/` (hand-written only).\n- File names: `NN_snake_case_name.md` with a **zero-padded two-digit** numeric\n prefix. e.g. `01_overview.md`, `03_use_cases/07_caching.md`.\n- Top-level pages use `NN_name.md` directly. High-volume sections get their own\n directory, e.g. `03_use_cases/NN_name.md` (nav group \"Use Cases\").\n- **No frontmatter.** Every file opens directly with an H1 `# Title`.\n- Navigation labels are short human titles set explicitly in\n `docs/mkdocs.wiki.yml` (title case) \u2014 often **shorter** than the page H1.\n Example: nav label `Basic CRUD` vs page H1 `Use Case 1: Basic FastAPI CRUD API`.\n Never use the raw filename slug as the label.\n- Renumbering: prefixes order pages. When inserting a page, pick the next\n number in the relevant group (subgroup numbers stay grouped).\n\n---\n\n## 3. Navigation \u2014 `docs/mkdocs.wiki.yml` is the source of truth\n\nEvery wiki page **must** be wired into the nav defined in\n`docs/mkdocs.wiki.yml`. If a page is not in the nav, it is orphaned.\n\n- The `nav:` block mirrors the directory tree; directories become nested\n groups.\n- When a section has many pages, group them **by theme** into nested\n subgroups. mongo-ops groups its use cases into:\n `Getting Started`, `Data & Queries`, `Caching`, `Population`,\n `Advanced Usage`.\n- Keep `index.md`'s **Documentation Structure** table in sync with the nav \u2014\n it is the human-facing version of the same TOC. Every nav entry needs a row\n (or nested bullet group) there.\n- Nav label = human title (title case), not the filename slug.\n\n---\n\n## 4. Page anatomy\n\n### 4.1 `index.md` (Home)\n\n```\n# \ud83e\udde9 <package> \u2014 <slogan>\n<one-paragraph pitch>\n\n> **Doc model:** \u2026 (see \u00a71)\n\n---\n\n## \ud83d\ude80 Key Features (bullet list, emoji per feature)\n## \ud83d\udce6 Installation (code blocks: internal PyPI, extras, local source)\n## \ud83d\udcc1 Documentation Structure (table = TOC, mirrors nav exactly)\n## \ud83d\udd17 Related Resources (bullets: source repo, internal PyPI, CI)\n---\n\u00a9 Aetoskia Internal \u2014 `<package>` <version>\n```\n\n### 4.2 Overview page (`01_*`)\n\n- Opens with `# Library Overview`, one sentence about what the library does,\n then a line stating the page builds the mental model.\n- Sections: `## \ud83c\udfd7\ufe0f Architecture` (ASCII diagram + numbered layer list),\n `## \ud83d\udd04 Lifecycle Rules` (table: step / call / why),\n `## \ud83e\udded Which \u2026 Should You Use?` (decision table).\n- Ends with `## \u27a1\ufe0f Read Next` (cross-links to the next pages to read).\n\n### 4.3 Components page (`02_*`)\n\n- Validated reference for the public API surface \u2014 describes what each\n component is for and how the pieces wire together. Keep it usage-level;\n exact signatures belong in `docs/lib/`.\n- Anatomy: numbered sections, one per component (`## 1. MongoConnectionManager`,\n `## 2. BaseDocument`, \u2026), with `### 8.1`-style sub-numbering for related\n types under a component (e.g. the cache layer's backend / stats / config /\n in-memory / redis implementations). Ends with `## Related`.\n\n### 4.4 Use-case page (the most common)\n\nStandard anatomy, in this order:\n\n```\n# Use Case N: <Human Title>\n\n**Scenario:** <one sentence: the problem this recipe solves>\n\n---\n\n## \ud83d\udce6 What's New? (table: Component | Description \u2014 new API used here)\n## \ud83d\ude80 Example (languaged code block, complete runnable-looking snippet)\n## \ud83d\udca1 Tips (bullets: gotchas, ordering, common mistakes)\n## Related (\u00a75)\n```\n\n### 4.5 Deep-dive / worked-example page\n\nFor intricate flows (population shapes, cache round-trips), use numbered\ndepth-first sections and comparison tables:\n\n```\n## 1. <Write path> (### 1a., ### 1b., \u2026 per sub-case)\n## 2. <Read path>\n## 3. <Round trip / tricky bit>\n## 4. When to use which (comparison table)\n## Related\n```\n\n### 4.6 Best Practices page (`04_*`)\n\n- Flat themed `##` sections (e.g. Layering, Lifecycle, Data & Performance,\n Transactions & Errors, Testing), each a short list of numbered conventions.\n- Pure prose \u2014 no large code blocks.\n\n### 4.7 Error Handling (`06_*`) / Testing pages\n\n- **Error Handling:** an exceptions table (`Exception | Source | Meaning / fix`),\n a `> ` note on which failures are by design, a `## \ud83d\ude80 FastAPI Mapping Example`\n code block, then `## \ud83d\udca1 Tips` bullets, then `## Related`.\n- **Testing example (`07_*`):** `## \ud83d\ude80 Mock-Based Quickstart` (focused code\n block), `## \ud83d\udca1 Notes` tip list, `## Related`.\n\nCommon to all pages: no frontmatter, `---` between every major section, and a\n`## Related` (or variant) footer as the **last** section.\n\n---\n\n## 5. The `## Related` footer\n\nEvery page ends with a cross-link footer. Rules:\n\n- Always the **last** section, always preceded by a `---`.\n- Standard heading: `## Related` (no emoji).\n- One bullet line; links separated by ` \u00b7 ` (middle dot plus spaces).\n- Link label format: `[NN \u2013 Title](relative_path)` \u2014 en-dash, page number, title.\n- Paths are **relative**: same directory \u2192 `02_custom_repo.md`; parent \u2192\n `../01_overview.md`; child \u2192 `03_use_cases/01_basic_crud.md`.\n- Link order follows nav order (each `Related` points to related next/sibling\n pages, plus the overview/components anchor).\n\nExamples (from mongo-ops):\n\n```\n## Related\n- [02 \u2013 Custom repository](02_custom_repo.md) \u00b7 [04 \u2013 Pagination](04_pagination.md) \u00b7 [Overview](../01_overview.md)\n```\n\nVariants:\n\n- `## \u27a1\ufe0f Read Next` \u2014 used on the overview page (linear reading order).\n- `## \ud83d\udd17 Related Resources` \u2014 only on `index.md`, one bullet per external resource.\n\n---\n\n## 6. Tone & formatting rules\n\n- **Written for humans**: short sentences, concrete examples, plain words.\n- Emoji-prefixed H2 headings for recognizable section types\n (`\ud83d\udce6 What's New?`, `\ud83d\ude80 Example`, `\ud83d\udca1 Tips`, `\ud83d\ude80 Key Features`).\n- `---` horizontal rule between every major section.\n- Code blocks always declare a language (```python, ```bash, etc.).\n- Use tables for comparisons and structured decisions; bullets for lists;\n ASCII diagrams for architecture.\n- Bold key terms inline. Use `inline code` for symbols/APIs.\n- Keep pages focused: one recipe/purpose per page. If a page balloons past\n ~10 KB, split it (add a numbered deep-dive page instead).\n- Match tone of existing pages in the repo before writing new ones.\n- Ground every `## \ud83d\ude80 Example`, `\ud83d\udca1 Tips`, and `\ud83d\udce6 What's New?` row in the\n library's actual source, tests, and its `docs/mcp` bundle \u2014 never invent\n APIs or behavior. Signatures and exceptions come from the code; gotchas\n come from the tests.\n\n---\n\n## 7. Build & verify workflow\n\nAfter creating or editing wiki pages:\n\n```bash\n# 1. Build the wiki (from the repo root)\ndoc-forge build --wiki # wiki only\ndoc-forge build --wiki --mkdocs --module <pkg> # wiki + lib together\n\n# 2. Preview locally\ndoc-forge serve --wiki\n\n# 3. Collect into the hub (run from the docs/ service repo)\npython collect.py # copies site, regenerates index + nginx.conf\npython collect.py --dry-run # preview without writing\n```\n\nVerification checklist:\n\n- `doc-forge build --wiki` completes with no warnings about missing files.\n- Every new page is present in `docs/mkdocs.wiki.yml` **and** in the index\n Documentation Structure table.\n- Every page ends with a `## Related` block and no dead links (paths resolve).\n- After `collect.py`: the hub card links to both `/wiki/` and `/lib/` and the\n pages render (spot-check with the served site).\n- If code/docstrings changed (no change when wiki-only), run the repo gates:\n `black`, `ruff`, `mypy`, `pytest`.\n\n---\n\n## 8. Before/after checklist\n\n**Before creating a page:**\n\n- [ ] Confirm it is genuinely wiki material (how-to) and not API contract.\n- [ ] Find the right filename (`NN_snake_case.md`) and directory for its theme.\n- [ ] Outline the sections per \u00a74 anatomy that fits the content.\n\n**After creating/editing a page:**\n\n- [ ] Page wired into `docs/mkdocs.wiki.yml` nav (themed subgroup if needed).\n- [ ] `index.md` Documentation Structure table updated.\n- [ ] `## Related` footer present, relative links, nav order.\n- [ ] `---` separators consistent; no frontmatter; emoji headings where apt.\n- [ ] Build passes; collect run; hub shows `/wiki/` + `/lib/` links.\n- [ ] No orphaned files, no dead links.\n\u00a9 Aetoskia Internal \u2014 docforge 1.0
docforge turns GSDFC-compliant Python docstrings into maintainable reference documentation. It never edits source docstrings; it reads them, renders them, and assembles documentation sites from all available material.
lib GSDFC docstrings docs/lib/** rendered markdown api OpenAPI JSON spec docs/api/** rendered markdown wiki Hand-written markdown docs/wiki/** (unchanged) mcp GSDFC docstrings + renderers docs/mcp/** structured JSON"},{"location":"01_overview/#architecture","title":"\ud83c\udfd7\ufe0f Architecture","text":"Each enabled site kind produces its own MkDocs configuration and build:
--mkdocs (lib) writes docs/mkdocs.lib.yml and builds site/lib/.--api writes docs/mkdocs.api.yml and builds site/api/.--wiki writes docs/mkdocs.wiki.yml and builds site/wiki/.Every site is self-contained (own theme assets and search index) with navigation derived or scoped to that kind. Hand-written wiki content is never overwritten or regenerated \u2014 only its navigation is derived automatically.
"},{"location":"01_overview/#lifecycle-rules","title":"\ud83d\udd04 Lifecycle Rules","text":"docs/mcp/** is a self-contained, machine-readable reference generated from docforge's own GSDFC docstrings. doc-forge serve --mcp --module docforge serves it over MCP. See the MCP Guide.
docforge is split into four horizontal layers. Everything flows top to bottom through the CLI.
The docforge/loaders package wraps griffe to extract modules, functions, classes, and Google-style docstring sections. Loaded data is normalized into the object model under docforge/models (Project, Module, DocObject).
docforge/nav parses docforge.nav.yml specs (NavSpec, Resolver, and the MkDocs nav emitter) and, since the wiki kind, derives wiki navigation from the file structure via build_wiki_nav.
docforge/renderers turn model data into artifacts:
MkDocsRenderer \u2192 docs/lib/** reference markdownMCPRenderer \u2192 docs/mcp/** structured documentationdocforge/servers serves generated artifacts over live protocols:
MCPServer \u2192 serves a pre-generated docs/mcp/** bundle through MCP resources (docs://index, docs://nav, docs://modules/{module})docforge/cli wires it all together:
commands.py \u2014 the build command and its --mkdocs / --api / --wiki / --mcp modesmkdocs_utils.py \u2014 per-kind config generation (docs/mkdocs.{lib,api,wiki}.yml) with navigation re-rooted to each kind's docs_dirapi_utils.py \u2014 OpenAPI loading and API docs generationAll documented source uses the Google-Styled Doc-Forge Convention (GSDFC): Args:, Returns:, Raises:, and Attributes: sections with properly typed signatures.
The authoritative GSDFC specification lives in the docforge/__init__.py package docstring, which is exported verbatim into docs/mcp/modules/docforge.json, so the MCP bundle is self-documenting.
Key rules:
name (Type):) that match the signature types.# Summary, # Examples, and # Notes sections at module level.Args:, Returns:, Raises:, Yields:, Notes:, and Example: sections at function and method level..pyi stub alongside the .py implementation.py.typed marker so type checkers consume the authored signatures instead of Any..pyi stubs and py.typed","text":"Every module ships a matching .pyi stub kept in sync with the .py implementation. When signatures change, update both files. Packages expose a py.typed marker so consumers and type checkers use the authored signatures.
docs/wiki/** and is never generated.01_overview.md, 02_components.md.05_development/01_environment.md \u2192 group Development.index.md is the site Home at the root, and a section root inside a directory.MkDocs config fragments live in docforge/templates. The mkdocs.wiki.yml fragment carries only the search plugin, since wiki pages contain no mkdocstrings directives.
A docs build runs through the CLI in a single pass.
"},{"location":"04_iterative_workflow/#build-commands","title":"\ud83d\ude80 Build commands","text":"# Library reference only\ndoc-forge build --mkdocs --module docforge\n\n# Wiki + library (each an independent MkDocs build)\ndoc-forge build --wiki --mkdocs --module docforge\n\n# Wiki only \u2014 no module required\ndoc-forge build --wiki --site-name docforge\n\n# MCP structured bundle\ndoc-forge build --mcp --module docforge\n"},{"location":"04_iterative_workflow/#what-a-build-does","title":"\ud83d\udd04 What a build does","text":"--mkdocs, --api, --wiki, --mcp).docs/lib/** with MkDocsRenderer.docs/api/** when --api is given.docs/wiki/**.docs/mkdocs.{lib,api,wiki}.yml), re-rooting navigation paths to each kind's docs_dir.mkdocs build once per config, emitting self-contained sites site/lib/, site/api/, and site/wiki/.doc-forge build --wiki --mkdocs --module docforge\ndoc-forge serve --wiki # serves site preview from docs/mkdocs.wiki.yml\ndoc-forge serve --lib\ndoc-forge serve --api\n# or target any config directly:\ndoc-forge serve --mkdocs --mkdocs-yml docs/mkdocs.wiki.yml\n"},{"location":"04_iterative_workflow/#serve-the-mcp-bundle","title":"\ud83d\udce6 Serve the MCP bundle","text":"doc-forge build --mcp --module docforge\ndoc-forge serve --mcp --module docforge\n"},{"location":"04_iterative_workflow/#related","title":"Related","text":"python -m venv .venv\n.venv\\Scripts\\activate\npip install -e .\n Requires Python 3.10+ (per pyproject.toml).
Development extras include:
pytest, pytest-cov \u2014 test suite and coverageruff \u2014 lint and format checkingblack \u2014 auto-formattingmypy \u2014 strict typing checkspydoclint \u2014 docstring \u2194 signature consistency checksRun all checks before pushing:
"},{"location":"05_development/02_quality_gates/#commands","title":"\ud83d\ude80 Commands","text":".venv\\Scripts\\python.exe -m pytest\n.venv\\Scripts\\python.exe -m ruff check docforge tests\n.venv\\Scripts\\python.exe -m black --check docforge tests\n.venv\\Scripts\\python.exe -m mypy docforge\n.venv\\Scripts\\pydoclint.exe docforge\n"},{"location":"05_development/02_quality_gates/#test-layout","title":"\ud83d\udcc1 Test layout","text":"Path Covers tests/nav/ Nav spec, resolver, wiki nav tests/cli/ Build command flows and modes tests/renderers/ MkDocs / MCP rendering CLI tests use the cli_runner fixture with mock_mkdocs_build and mock_mkdocs_load_config so they exercise the full flow without invoking a real MkDocs build.
This page is the practical companion to the authoritative GSDFC specification in the docforge/__init__.py package docstring. It describes how to write docstrings that render correctly in MkDocs and stay machine-parseable by doc-forge, MkDocs, and MCP clients.
doc-forge compiles docstrings but never rewrites them.Modules use Markdown headings and --- separators.
Recommended sections:
# Summary \u2014 what the subsystem does# Examples \u2014 a representative usage snippet# Notes \u2014 guarantees, lifecycle, and thread-safety notesRecommended sections, in order:
Attributes: \u2014 instance attributes with name (Type): entriesNotes: \u2014 grouped subsections such as Guarantees, LifecycleExample: \u2014 indented python code blockRecommended section order:
Args:Returns:Raises:Yields:Notes:Example:Formatting rules:
Args: entries are name (Type): followed by an indented description.Returns: entries are Type: followed by an indented description.Raises: entries are ExceptionType: followed by an indented condition.Yields: replaces Returns: for generators.python blocks are allowed inside Example: sections, indented four spaces.Properties document their return values with a Returns: section and, when meaningful, an Example:.
def process(foo: Foo, multiplier: int) -> int:\n \"\"\"Process a Foo instance.\n\n Args:\n foo (Foo):\n Foo instance to process.\n\n multiplier (int):\n Value used to scale foo.\n\n Returns:\n int:\n Processed result.\n\n Raises:\n ValueError:\n If multiplier is negative.\n\n Example:\n Process foo:\n\n ```python\n foo = Foo(\"example\", value=10)\n result = process(foo, multiplier=2)\n print(result)\n ```\n \"\"\"\n"},{"location":"05_development/03_gsdfc_guide/#keeping-stubs-in-sync","title":"\ud83d\udd04 Keeping stubs in sync","text":"Every .py module ships a matching .pyi stub. When a signature or a public symbol changes, update both files. Signature annotations in the stub must match the implementation.
pydoclint (Google style) runs in CI and verifies that Args:/Returns: sections match function signatures, including types.
allow-init-docstring = true \u2014 __init__ docstrings are allowed.skip-checking-raises = true \u2014 Raises: sections are descriptive and are not required to map to literal raise statements.check-class-attributes = false \u2014 Attributes: sections document instance attributes without class-level annotations.Doc-forge can export a machine-readable MCP bundle from a project's GSDFC docstrings and serve that bundle over the Model Context Protocol (MCP). The bundle is self-contained: readers can derive the full API reference \u2014 signatures, docstrings, and structure \u2014 for the documented project.
"},{"location":"05_development/04_mcp_guide/#building-the-bundle","title":"\ud83d\ude80 Building the bundle","text":"doc-forge build --mcp --module docforge\n This writes structured JSON into docs/mcp/:
index.json \u2014 project metadata and module countnav.json \u2014 module list with docs://modules/{module} resource URIsmodules/{dotted.path}.json \u2014 per-module serialized documentationdoc-forge serve --mcp --module docforge\n The MCPServer (in docforge/servers/mcp_server.py) exposes:
docs://index Project metadata docs://nav Navigation structure docs://modules/{module} Individual module documentation The server also registers a single diagnostic tool, ping, and is read-only. Missing resources are returned as structured error dictionaries rather than exceptions. The default transport is streamable-http; stdio and sse are supported via MCPServer.run.
Each module resource contains:
path \u2014 dotted module pathdocstring \u2014 the raw GSDFC module docstringobjects \u2014 a recursive mapping of public symbols with name, kind, path, signature, and docstring, plus nested membersBecause the bundle is generated from docstrings, the quality of the bundle equals the quality of the project's GSDFC docstrings. For docforge itself, the GSDFC specification is carried in modules/docforge.json via the docforge/__init__.py package docstring, making the bundle self-documenting.
Each documentation kind (lib, api, wiki) is built by its own MkDocs site. A repo that documents kind {kind} carries a config at docs/mkdocs.{kind}.yml that sets docs_dir to the kind's source directory and site_dir to ../site/{kind}.
docs/mkdocs.{kind}.yml is a repo-owned file, not a build byproduct:
doc-forge build --{kind} materializes a copy from the packaged templates (see below) and then builds site/{kind}.site_dir are respected as-is.--refresh \u2014 force-regenerates the file from the templates, replacing any local changes.Because a present file is never rewritten, rerunning a build is a no-op for versioned configs: the working tree stays clean.
"},{"location":"05_development/05_mkdocs_configs/#templates","title":"\ud83e\udde9 Templates","text":"The built-in defaults live in docforge/templates/:
mkdocs.common.yml Shared theme + markdown extensions mkdocs.lib.yml mkdocstrings handler for library reference mkdocs.api.yml Swagger/OpenAPI plugins and API-side features mkdocs.wiki.yml Wiki look: horizontal navigation.tabs header, blue grey/teal palette, Roboto A per-kind config is generated by deep-merging the common template with the kind fragment, then filling in generation-time values (site_name, docs_dir, site_dir, nav, API site_description/site_author, and the theme icon from docforge.nav.yml). Pass --template <path> to replace the built-in templates entirely.
site/{kind} and is served under /<repo>/{kind}/.mkdocs.yml (for example mongo-ops, whose wiki uses docs_dir: docs/wiki). doc-forge never reads or writes root mkdocs.yml; the docs service maps the kind to the root site/ dir in this case.Commit docs/mkdocs.{kind}.yml so served docs are reproducible from source.
Kinds are separate MkDocs sites served side by side under /<repo>/<kind>/, so a wiki page may link to the library reference with a source-relative path such as ../lib/13_library_registry/. MkDocs only rewrites links that point inside docs_dir; anything reaching a sibling kind is emitted verbatim and, under use_directory_urls, resolves into <kind>/<kind>/ and breaks.
The docforge-linkfix plugin (part of doc-forge, enabled in the wiki template) fixes this at render time: it rewrites ../lib/, ../api/, and ../wiki/ links to climb to the repository root, so they resolve correctly however deeply the page is nested.
--package<resource>/routes.py packages (with __init__.py) instead of flat <resource>.py--template-styleasync def handlers with a JSON body parameter named body (the standards-template convention) plus a service-resolution hint, instead of sync def handlers taking payloadWhy bother? Because bind_routes (in 02 – Components) needs an operationId → handler exactly matching the spec. Codegen guarantees you never type a handler name wrong — it wears the same operationId the spec declares.
multipart/form-data and application/x-www-form-urlencoded bodies expand to one parameter per schema property: UploadFile = File(...) for format: binary, Form(...) otherwise — not an opaque payload: dict. Fields are required only when listed in the schema's required.The stubs raise NotImplementedError; fill each body in (typically by resolving a service from request.app.state).
Both shapes bind equally — the router resolves the handler by operationId and introspects the FastAPI signature, so it never inspects the request body:
def create_item(payload: ItemInput, response: Response): — the body parameter is payload.--template-style: async def create_item(request: Request, body: ItemInput, response: Response): — matches the standards template (async def, body parameter, a request.app.state.db_resources["<resource>_svc"] hint).Use --template-style when generating against the standards template so the scaffold matches the hand-written handlers you will keep.
diff --git a/openapi-first/wiki/search/search_index.json b/openapi-first/wiki/search/search_index.json index 87f9436..e6197cf 100644 --- a/openapi-first/wiki/search/search_index.json +++ b/openapi-first/wiki/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"\ud83e\udde9 openapi-first \u2014 OpenAPI as the Single Source of Truth","text":"Note:
models/routesare build-time tooling. They do not run at runtime, and templates (the Bake-your-stuff section of 02 – Templates) scaffold a working server out-of-the-box for greenfield projects.
openapi-first 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.
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 docs/mcp/.
OpenAPIFirstApp) and a strict HTTP client (OpenAPIClient)operationId binding \u2014 handler functions are bound to routes purely by operationId; no routing decoratorssecuritySchemes + per-operation security auto-injected as FastAPI dependencies (Bearer JWT introspection included)models (Pydantic) and routes (resource stubs) generated from a spechealth_app, crud_app, model_app, vet_app scaffolds for the whole lifecycle, from /health to SSE + multi-resource CRUDFrom your internal PyPI:
pip install --extra-index-url https://$PYPI_USERNAME:$PYPI_PASSWORD@pip.aetoskia.com/simple openapi-first\n From local source:
pip install -e .\n"},{"location":"#documentation-structure","title":"\ud83d\udcc1 Documentation Structure","text":"Section Description Overview The mental model: spec as contract, operationId binding, fail-fast guarantees Components app, binder, loader, client, errors, security, codegen, cli 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 operationId-driven HTTP client \u00b7 04 \u2013 Codegen Generate Pydantic models and route stubs from a spec Design Architecture, responsibilities, and startup pipeline Security securitySchemes, 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":"\u00a9 Aetoskia Internal \u2014 openapi-first 0.0.6
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.
Your OpenAPI document (openapi.yaml or openapi.json) is the single authoritative contract:
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 Every route, method, parameter, schema, and security requirement lives here \u2014 and only here. Code never declares routes.
"},{"location":"01_overview/#12-operationid-is-the-binding-key","title":"1.2operationId is the binding key","text":"The only bridge between the spec and your Python code is the operationId. Each operation maps, by name, to exactly one plain callable:
# routes.py\ndef get_health():\n \"\"\"Health check operation handler.\"\"\"\n return {\"status\": \"ok\"}\n openapi_first resolves operationId: get_health \u2192 routes.get_health and registers the route. No decorators, no @app.get, no routing metadata in code.
Guarantees this binding provides:
operationId must resolve at startupsecuritySchemes + per-operation security, spec-drivenContract violations are detected at application startup (or client construction), never silently at request time:
What goes wrong When it fails Invalid / unloadable specload_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"},{"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":"openapi-first scaffold health_app my-health-service\nopenapi-first scaffold --list\n scaffold copies a bundled template (verbatim \u2014 no code generation, no mutation) into a directory of your choice.
# 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 Run with your FastAPI-compatible server (ASGI):
uvicorn main:app --reload\n 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.
# 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 OpenAPIClient builds one callable per operationId from the same spec, so the client can never drift from the server.
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 \u2014 stable, tested, documented.templates are copyable scaffolds \u2014 not part of the library API; excluded from lint/format/type gates, never imported at runtime.openapi-first deliberately does not:
operationId binding onlyNew here? Start with 01 \u2013 Quickstart. Want to copy a runnable app? Jump to 02 \u2013 Templates. Digging into internals? See Design.
"},{"location":"01_overview/#related","title":"Related","text":"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.
"},{"location":"02_components/#1-module-map","title":"\ud83d\uddc2\ufe0f 1. Module Map","text":"Module Responsibility Importopenapi_first.loader Load/validate the spec, resolve {ENV_VAR} load_openapi openapi_first.app OpenAPI-first FastAPI application bootstrap OpenAPIFirstApp openapi_first.binder Spec \u2192 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"},{"location":"02_components/#2-loader-load-validate","title":"\u2699\ufe0f 2. loader \u2014 Load & Validate","text":"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 \u2014 a golden rule of fail-fast.
from openapi_first.loader import load_openapi\n\nspec = load_openapi(\"openapi.yaml\")\n Behavior:
.json, .yaml, .yml (parsed by extension).openapi-spec-validator) at load time.OpenAPISpecLoadError on: missing file, unparseable content, or spec-validation failure.Env-var resolution lives at the security layer rather than the loader (see Security).
"},{"location":"02_components/#3-app-the-application-bootstrap","title":"\ud83e\uddec 3.app \u2014 The Application Bootstrap","text":"OpenAPIFirstApp (in openapi_first/app.py) is a FastAPI subclass that replaces manual route registration with OpenAPI-driven binding.
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 Startup pipeline (fail-fast, in this order):
.yaml/.json).securitySchemes and per-operation security.operationId; a missing handler, a missing operationId on a declared operation, or an unbound operation raises at startup.Guarantees:
/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 \u2014 title, version, middleware, lifespan, \u2026 all work).
binder \u2014 Spec \u2192 Route Binding","text":"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:
operationId.routes_module.<operationId> \u2014 a plain callable.APIRoute bound to that handler, injecting Depends(...) for any matching security requirements.Failures are explicit and early:
Condition Raised NooperationId 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 operationIds \u2014 no decorators, no routing metadata.
client \u2014 The Other Side of the Contract","text":"OpenAPIClient(spec, base_url=None, client=None) (in openapi_first/client.py).
The same spec that builds the server builds its client \u2014 one callable per operationId, keyed by name:
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 How operations become methods:
operationId \u2192 a dynamically-built callable on the client.path_params={\"user_id\": 1}.body={...} (JSON) or raw content for non-JSON media types.query= / headers=.httpx.Response (no implicit deserialization, no hidden schema inference).Client-construction guarantees (fail-fast, same philosophy as the app):
servers entry (base_url falls back to it).paths.operationId.Construction errors raise OpenAPIClientError; request-time errors surface as httpx exceptions with operationId context.
errors \u2014 Explicit Error Hierarchy","text":"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 \u2014 base (comparable / aims to be picklable share).OpenAPIClientError \u2014 client-side contract violations.MissingOperationHandler \u2014 spec declares an operation whose handler is missing or unresolvable; carries path, method, and optional operationId.See Error Handling for the full table.
"},{"location":"02_components/#7-security-auth-from-the-spec","title":"\ud83d\udee1\ufe0f 7.security \u2014 Auth from the Spec","text":"security.py turns an OpenAPI securitySchemes section into FastAPI Depends(...) objects \u2014 no manual middleware.
parse_security_schemes(spec) -> dict[str, dict] \u2014 collects schemes, resolving {ENV_VAR} placeholders.make_security_dependencies(spec, security_schemes) -> dict[str, list[Depends]] \u2014 builds METHOD:/path \u2192 dependency list from per-operation security, falling back to top-level security.Supported scheme types (extensible):
type: http, scheme: bearer \u2014 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.Key properties: resolution is per-operation, env placeholders resolve once at startup, and dependencies are injected by binder \u2014 your handlers never mention security.
codegen & cli \u2014 Build-Time Tooling","text":"Generation is strictly build-time \u2014 spec \u2192 code, run once by a developer, committed:
"},{"location":"02_components/#openapi-first-models-spec-o-file","title":"openapi-first models <spec> -o <file>","text":"Generates Pydantic models from spec schemas (wraps datamodel_code_generator):
# cli path\nopenapi-first models openapi.yaml -o app/models.py\n"},{"location":"02_components/#openapi-first-routes-spec-o-dir-use-models-models-module-models","title":"openapi-first routes <spec> -o <dir> [--use-models] [--models-module models]","text":"Generates one routes_<resource>.py stub file per resource, with NotImplementedError handlers bound to operationIds (optionally importing your generated models):
openapi-first routes openapi.yaml -o app/routes --use-models\n"},{"location":"02_components/#openapi-first-scaffold-template-path","title":"openapi-first scaffold <template> [path]","text":"Copies a bundled application template verbatim (see Templates).
"},{"location":"02_components/#9-templates-copyable-applications","title":"\ud83e\uddf1 9.templates \u2014 Copyable Applications","text":"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 \u2014 no lint/format/type gates, never imported at runtime. They exist to be copied via:
openapi-first scaffold <template> [target-dir]\nopenapi-first scaffold --list\n"},{"location":"02_components/#related","title":"\ud83d\udd17 Related","text":"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.
"},{"location":"04_design/#1-the-startup-pipeline","title":"\ud83c\udfd7\ufe0f 1. The Startup Pipeline","text":"Everything happens once, eagerly, at construction time \u2014 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\".
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 Client \u2014 same spec, same guarantees:
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"},{"location":"04_design/#2-guarantees-that-hold-by-construction","title":"\ud83d\udcdc 2. Guarantees That Hold by Construction","text":"These are not conventions \u2014 they are enforced at startup or client construction:
paths; there is no decorator-driven or implicit routing.operationId resolves to an exact handler name in routes_module; a missing one aborts bootstrap.operationId. An operation without one cannot be bound and fails fast.security dependencies come from securitySchemes + per-operation security; no manual middleware.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.
"},{"location":"04_design/#3-component-responsibilities","title":"\ud83e\udde9 3. Component Responsibilities","text":"Module Owns Refuses toloader parse + validate the spec modify or \"fix\" the spec app assemble FastAPI from spec + handlers routing decisions, decorators binder operationId \u2192 handler mapping infer handlers from paths client build one callable per operationId guess URLs, deserialize implicitly security schemes \u2192 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.
"},{"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 Rawhttpx.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"},{"location":"04_design/#related","title":"\ud83d\udd17 Related","text":"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.
Two places in the spec, both respected:
"},{"location":"05_security/#11-componentssecurityschemes-the-inventory","title":"1.1components.securitySchemes \u2014 the inventory","text":"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 x-server-url and x-introspect-path are library extensions that point at a JWT introspection endpoint (see Introspection).
security \u2014 per-operation (or global) requirements","text":"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 OpenAPI dynamic-scoping rules apply: an operation-level security replaces the global list (it does not merge).
security.py exposes two functions:
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.
make_security_dependencies builds a FastAPI dependency for type: http, scheme: bearer.
Two modes, decided by whether an introspection endpoint is configured:
"},{"location":"05_security/#31-with-introspection-x-introspect-path","title":"3.1 With introspection (x-introspect-path)","text":"Authorization: Bearer <token>{\"token\": \"<token>\"} to {x-server-url}{x-introspect-path} synchronously via the bundled httpx clientactive: true{\"user\": ...} from the introspection body \u2192 request.state.user401 (missing/invalid token) or 503 (auth service unreachable)request.state.token; no remote call# 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 OpenAPIFirstApp already does this internally \u2014 the snippet above shows what it encapsulates (and what you use directly if you assemble the pieces by hand).
Because handlers are plain callables, security is the one place FastAPI's TestClient earns its keep:
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 See Testing for the full recipe, including how tests stub the introspection server.
"},{"location":"05_security/#7-common-patterns","title":"\ud83d\udee1\ufe0f 7. Common Patterns","text":"Pattern How Public endpointsecurity: [] 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 \u2192 request.state.token Auth on the client side pass the token via client.<operationId>(headers={\"Authorization\": ...})"},{"location":"05_security/#related","title":"Related","text":"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.
All errors derive from OpenAPIFirstError (in openapi_first/errors.py), so a single except OpenAPIFirstError catches every first-party failure:
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 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"},{"location":"06_error_handling/#2-when-things-fail","title":"\u23f1\ufe0f 2. When Things Fail","text":"The single most important rule: violations are eager, not lazy.
"},{"location":"06_error_handling/#21-at-application-startup","title":"2.1 At application startup","text":"# 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 Because these raise during OpenAPIFirstApp(...) construction, CI catches them the moment a spec and its routes drift \u2014 before a single request is served.
OpenAPIClient(spec) # fails fast, same philosophy\n servers \u2192 OpenAPIClientErrorpaths \u2192 OpenAPIClientErroroperationIds \u2192 OpenAPIClientError (client methods must be unambiguous)operationId \u2192 OpenAPIClientErrorRuntime transport errors surface as httpx exceptions (httpx.RequestError family), not swallowed or remapped. Missing required args fail before any HTTP request is made:
client.get_user(path_params={\"user_id\": ...}) # OK\nclient.get_user() # ValueError \u2014 user_id required\n"},{"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":"Handlers raise FastAPI HTTPException for expected operation-level failures (404/422), and the OperationId-binding errors only exist at startup:
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"},{"location":"06_error_handling/#32-client-side","title":"3.2 Client-side","text":"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 httpx.HTTPStatusError isn't raised by the library \u2014 it's the standard httpx.raise_for_status() you can opt into per call. The library never masks a response code.
None of these can silently degrade: a violation is an exception at construction, not a 500 at request time.
"},{"location":"06_error_handling/#related","title":"Related","text":"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.
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 Run with:
pytest # 23 tests, zero mocks of the library\npytest -q\npytest tests/test_loader.py\n"},{"location":"07_testing/#2-the-fixture-pattern","title":"\ud83c\udfd7\ufe0f 2. The Fixture Pattern","text":"# 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"},{"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":"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"},{"location":"07_testing/#32-fail-fast-the-heart","title":"3.2 Fail-fast (the heart)","text":"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"},{"location":"07_testing/#33-loader-validation","title":"3.3 Loader validation","text":"def test_loader_invalid_spec_fails():\n with pytest.raises(OpenAPISpecLoadError):\n load_openapi(\"broken.yaml\")\n"},{"location":"07_testing/#34-client-contract-drift","title":"3.4 Client contract drift","text":"def test_client_duplicate_operation_id_raises():\n with pytest.raises(OpenAPIClientError):\n OpenAPIClient(dup_spec)\n"},{"location":"07_testing/#4-testing-the-templates","title":"\ud83c\udfdb\ufe0f 4. Testing the Templates","text":"Each bundled template ships its own test + in-memory store, so you get a runnable contract test the moment you scaffold:
openapi-first scaffold crud_app my-service\ncd my-service\npytest -q\n test_crud_app.py / test_model_app.py / test_vet_app.py exercise the full CRUD surface through TestClient \u2014 including 201/204 status codes, 404s, and (in vet_app) SSE streaming via StreamingResponse.
The same gates the library itself must pass are what keep the docs honest:
Gate Purposeblack --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:
black --check openapi_first tests\nruff check openapi_first tests\nmypy openapi_first\npytest\n"},{"location":"07_testing/#6-testing-tips","title":"\ud83d\udca1 6. Testing Tips","text":"OpenAPIFirstApp(...) raises for the broken contracts \u2014 those are your most valuable testsThis 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.
"},{"location":"03_use_cases/01_quickstart/#1-prerequisites","title":"\ud83d\udee0\ufe0f 1. Prerequisites","text":"openapi-first installed (see Overview)pip install \"fastapi[standard]\" (or uvicorn) to run the appOpenAPI comes first. Create openapi.yaml:
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 Key points: every operation needs operationId, and every route must exist only here.
Create routes.py \u2014 plain functions, no decorators, named exactly like the operationIds:
# routes.py\ndef get_greeting(name: str) -> dict:\n \"\"\"Return a greeting for the given name.\"\"\"\n return {\"greeting\": f\"Hello, {name}!\"}\n If a handler is missing at startup, the app refuses to boot (MissingOperationHandler) \u2014 the fail-fast guarantee catches contract drift immediately.
Create main.py:
# 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 Run it:
uvicorn main:app --reload\n Visit http://localhost:8000/docs (Swagger UI) and http://localhost:8000/openapi.json \u2014 both are generated from your spec.
The same spec builds a strict client:
# 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"},{"location":"03_use_cases/01_quickstart/#6-next-steps","title":"\ud83d\udca1 6. Next Steps","text":"openapi-first ships four runnable, copyable applications under openapi_first/templates/. They are not part of the library API \u2014 they are bundled scaffold examples you copy into your own project and build on.
A template is a complete, self-contained OpenAPI-first service:
openapi_first/templates/<name>/All templates share the same skeleton:
<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"},{"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 health_app \u2014 minimal liveness probe","text":"openapi-first scaffold health_app\n# or into a custom directory:\nopenapi-first scaffold health_app my-health-service\n File Purpose openapi.yaml GET /health \u2192 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 \u2014 one operation, one handler, zero moving parts. The best starting point to internalize the mental model.
Smoke test:
pip install -e .\nuvicorn main:app\ncurl http://localhost:8000/health\n# \u2192 {\"status\": \"ok\"}\n"},{"location":"03_use_cases/02_templates/#22-crud_app-dict-based-crud","title":"2.2 crud_app \u2014 dict-based CRUD","text":"openapi-first scaffold crud_app my-crud-service\n File Purpose openapi.yaml Full CRUD over /items (list/get/create/update/delete) routes.py Handlers bound via operationIds list_items, get_item, create_item, update_item, delete_item data.py In-memory dict store with auto-incrementing id Behaviors you learn:
create_item/delete_item take response: Response and set 201/204; get_item/update_item raise HTTPException(404) on KeyErrordata.py is a copyable in-memory store, explicitly not production-readymodel_app \u2014 Pydantic model CRUD","text":"openapi-first scaffold model_app my-model-service\n 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:
operationId set as crud_app, so the two are interchangeableOpenAPIClient body handlingvet_app \u2014 the full-featured demo","text":"openapi-first scaffold vet_app my-vet-clinic\n 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 \u2192 vets \u2192 treatments \u2192 pets \u2192 appointments) main.py App + CORS + lifespan example Behaviors you learn \u2014 the advanced tier:
ProcedureNotes uses oneOf + discriminator.noteType mapping; Pydantic models use Literal[...] discriminator fieldsGET /pets/{id}/actions operation streaming text/event-stream via StreamingResponse with background task workersUploadFile handler setting a multi-part bodyadd_middleware(CORSMiddleware, ...) alongside the spec-driven setup201/204 explicitly via injected Response# 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 Protip: DEFAULT_TEMPLATE is health_app, so openapi-first scaffold with no template name scaffolds the health app.
After openapi-first scaffold health_app my-health-service, your directory contains a drop-in FastAPI service:
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 Run it:
cd my-health-service\npip install -e .\nuvicorn main:app --reload\n /docs, /openapi.json, and every declared route now exist \u2014 all derived from openapi.yaml.
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.
See the __init__.py of each template for detailed client examples, CLI examples, and design notes.
OpenAPIClient is the other side of the contract. It reads the same OpenAPI document the server runs on and exposes one callable per operationId \u2014 so \"client\" and \"server\" are two views of one truth.
The client is httpx-based and returns raw httpx.Response objects \u2014 no magic deserialization, no hidden schema inference:
requests.get(...) scattered through your codefrom openapi_first.loader import load_openapi\nfrom openapi_first.client import OpenAPIClient\n\nspec = load_openapi(\"openapi.yaml\")\nclient = OpenAPIClient(spec)\n The base URL comes from the spec's servers list (first entry) unless you pass base_url explicitly:
client = OpenAPIClient(spec, base_url=\"https://api.internal.myco/v1\")\n You can also hand over a preconfigured httpx.Client (custom transport, TLS, retries):
import httpx\n\ntransport = httpx.HTTPTransport(retries=3)\nclient = OpenAPIClient(\n spec,\n client=httpx.Client(transport=transport),\n)\n"},{"location":"03_use_cases/03_client/#3-fail-fast-at-construction","title":"\ud83d\udca5 3. Fail-Fast at Construction","text":"OpenAPIClient(...) raises immediately if the contract is broken \u2014 you find out at startup, not on the first request:
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 \u2014 pydoclint-grade strictness on the wire.
"},{"location":"03_use_cases/03_client/#4-calling-operations","title":"\ud83d\udcde 4. Calling Operations","text":"Every operationId becomes a method. The call signature is uniform across the whole client:
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 Concrete examples (from the crud_app / health_app templates):
# 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 Returns: the raw httpx.Response, so status_code, .json(), .headers are all yours to inspect.
For each operation the client knows exactly where each parameter belongs:
OpenAPI location Client kwargin: 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=.
# One directory, two processes\nuvicorn main:app --port 8000 # server\npython -c \"import asyncio; from client_script import run; asyncio.run(run())\" # client\n Or the same client against a remote environment:
client = OpenAPIClient(\n spec,\n base_url=\"https://staging.internal.myco\",\n)\n The URL is the only thing that changes between environments \u2014 the contract never does.
"},{"location":"03_use_cases/03_client/#7-operationid-as-the-api","title":"\u270d\ufe0f 7. OperationId as the API","text":"Because the client is operationId-driven:
operationId is the one name you remember; the HTTP verb/path is an implementation detailIf an operationId you call doesn't exist, you get an AttributeError at construction-scan time \u2014 never a silent 404.
TL;DR \u2014 point openapi-first models / openapi-first routes at your spec and get Pydantic models plus operation-matching route stubs. The output is deterministic and is a starting point \u2014 not a maintained artifact.
Two pain points kill OpenAPI projects:
item_id: int, your client sends a string\u2026).bind_routes requires each operationId to resolve to a function of the exact same name.Codegen collapses both. One command, one source file, the same shapes everywhere.
"},{"location":"03_use_cases/04_codegen/#2-model-generation","title":"\ud83d\ude80 2. Model Generation","text":"openapi-first models openapi.yaml -o app/_gen/models.py\n The generated module mirrors the spec's components.schemas by name:
components.schemas.User class User(BaseModel) components.schemas.Item class Item(BaseModel) required + type from schema Pydantic Field(...) / type hints \u2713 Never hand-edit those files. If the spec changes, regenerate \u2014 just like re-running a compiler after editing source.
"},{"location":"03_use_cases/04_codegen/#why-constructor-time-validation-still-applies","title":"Why constructor-time validation still applies","text":"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.
Wire model vs. domain model. Generated models are the wire contract. Keep them separate from your persisted/domain model (see the standards CRUD Template): give the wire schema an id: str, but model the Mongo document so the id is optional.
openapi-first routes openapi.yaml -o app/interfaces \\\n --use-models --models-module app._gen.models \\\n --inject-request --package\n One file per resource (derived from the first path segment). Flags:
Flag Effect--use-models Import request-body $ref models from --models-module --models-module MOD Dotted module for those imports (default models) --inject-request Prepend request: Request to every handler (DI convention) --package Emit <resource>/routes.py packages (with __init__.py) instead of flat <resource>.py Why bother? Because bind_routes (in 02 \u2013 Components) needs an operationId \u2192 handler exactly matching the spec. Codegen guarantees you never type a handler name wrong \u2014 it wears the same operationId the spec declares.
/fetch-requests yields a sanitized module name fetch_requests.py (a raw hyphen could never be imported).multipart/form-data and application/x-www-form-urlencoded bodies expand to one parameter per schema property: UploadFile = File(...) for format: binary, Form(...) otherwise \u2014 not an opaque payload: dict. Fields are required only when listed in the schema's required.The stubs raise NotImplementedError; fill each body in (typically by resolving a service from request.app.state).
Note: models / routes are build-time tooling. They do not run at runtime, and templates (the Bake-your-stuff section of 02 \u2013 Templates) scaffold a working server out-of-the-box for greenfield projects.
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
That determinism is what lets you git diff after a spec change and see exactly what moved.
openapi-first 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.
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 docs/mcp/.
OpenAPIFirstApp) and a strict HTTP client (OpenAPIClient)operationId binding \u2014 handler functions are bound to routes purely by operationId; no routing decoratorssecuritySchemes + per-operation security auto-injected as FastAPI dependencies (Bearer JWT introspection included)models (Pydantic) and routes (resource stubs) generated from a spechealth_app, crud_app, model_app, vet_app scaffolds for the whole lifecycle, from /health to SSE + multi-resource CRUDFrom your internal PyPI:
pip install --extra-index-url https://$PYPI_USERNAME:$PYPI_PASSWORD@pip.aetoskia.com/simple openapi-first\n From local source:
pip install -e .\n"},{"location":"#documentation-structure","title":"\ud83d\udcc1 Documentation Structure","text":"Section Description Overview The mental model: spec as contract, operationId binding, fail-fast guarantees Components app, binder, loader, client, errors, security, codegen, cli 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 operationId-driven HTTP client \u00b7 04 \u2013 Codegen Generate Pydantic models and route stubs from a spec Design Architecture, responsibilities, and startup pipeline Security securitySchemes, 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":"\u00a9 Aetoskia Internal \u2014 openapi-first 0.0.6
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.
Your OpenAPI document (openapi.yaml or openapi.json) is the single authoritative contract:
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 Every route, method, parameter, schema, and security requirement lives here \u2014 and only here. Code never declares routes.
"},{"location":"01_overview/#12-operationid-is-the-binding-key","title":"1.2operationId is the binding key","text":"The only bridge between the spec and your Python code is the operationId. Each operation maps, by name, to exactly one plain callable:
# routes.py\ndef get_health():\n \"\"\"Health check operation handler.\"\"\"\n return {\"status\": \"ok\"}\n openapi_first resolves operationId: get_health \u2192 routes.get_health and registers the route. No decorators, no @app.get, no routing metadata in code.
Guarantees this binding provides:
operationId must resolve at startupsecuritySchemes + per-operation security, spec-drivenContract violations are detected at application startup (or client construction), never silently at request time:
What goes wrong When it fails Invalid / unloadable specload_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"},{"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":"openapi-first scaffold health_app my-health-service\nopenapi-first scaffold --list\n scaffold copies a bundled template (verbatim \u2014 no code generation, no mutation) into a directory of your choice.
# 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 Run with your FastAPI-compatible server (ASGI):
uvicorn main:app --reload\n 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.
# 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 OpenAPIClient builds one callable per operationId from the same spec, so the client can never drift from the server.
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 \u2014 stable, tested, documented.templates are copyable scaffolds \u2014 not part of the library API; excluded from lint/format/type gates, never imported at runtime.openapi-first deliberately does not:
operationId binding onlyNew here? Start with 01 \u2013 Quickstart. Want to copy a runnable app? Jump to 02 \u2013 Templates. Digging into internals? See Design.
"},{"location":"01_overview/#related","title":"Related","text":"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.
"},{"location":"02_components/#1-module-map","title":"\ud83d\uddc2\ufe0f 1. Module Map","text":"Module Responsibility Importopenapi_first.loader Load/validate the spec, resolve {ENV_VAR} load_openapi openapi_first.app OpenAPI-first FastAPI application bootstrap OpenAPIFirstApp openapi_first.binder Spec \u2192 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"},{"location":"02_components/#2-loader-load-validate","title":"\u2699\ufe0f 2. loader \u2014 Load & Validate","text":"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 \u2014 a golden rule of fail-fast.
from openapi_first.loader import load_openapi\n\nspec = load_openapi(\"openapi.yaml\")\n Behavior:
.json, .yaml, .yml (parsed by extension).openapi-spec-validator) at load time.OpenAPISpecLoadError on: missing file, unparseable content, or spec-validation failure.Env-var resolution lives at the security layer rather than the loader (see Security).
"},{"location":"02_components/#3-app-the-application-bootstrap","title":"\ud83e\uddec 3.app \u2014 The Application Bootstrap","text":"OpenAPIFirstApp (in openapi_first/app.py) is a FastAPI subclass that replaces manual route registration with OpenAPI-driven binding.
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 Startup pipeline (fail-fast, in this order):
.yaml/.json).securitySchemes and per-operation security.operationId; a missing handler, a missing operationId on a declared operation, or an unbound operation raises at startup.Guarantees:
/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 \u2014 title, version, middleware, lifespan, \u2026 all work).
binder \u2014 Spec \u2192 Route Binding","text":"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:
operationId.routes_module.<operationId> \u2014 a plain callable.APIRoute bound to that handler, injecting Depends(...) for any matching security requirements.Failures are explicit and early:
Condition Raised NooperationId 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 operationIds \u2014 no decorators, no routing metadata.
client \u2014 The Other Side of the Contract","text":"OpenAPIClient(spec, base_url=None, client=None) (in openapi_first/client.py).
The same spec that builds the server builds its client \u2014 one callable per operationId, keyed by name:
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 How operations become methods:
operationId \u2192 a dynamically-built callable on the client.path_params={\"user_id\": 1}.body={...} (JSON) or raw content for non-JSON media types.query= / headers=.httpx.Response (no implicit deserialization, no hidden schema inference).Client-construction guarantees (fail-fast, same philosophy as the app):
servers entry (base_url falls back to it).paths.operationId.Construction errors raise OpenAPIClientError; request-time errors surface as httpx exceptions with operationId context.
errors \u2014 Explicit Error Hierarchy","text":"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 \u2014 base (comparable / aims to be picklable share).OpenAPIClientError \u2014 client-side contract violations.MissingOperationHandler \u2014 spec declares an operation whose handler is missing or unresolvable; carries path, method, and optional operationId.See Error Handling for the full table.
"},{"location":"02_components/#7-security-auth-from-the-spec","title":"\ud83d\udee1\ufe0f 7.security \u2014 Auth from the Spec","text":"security.py turns an OpenAPI securitySchemes section into FastAPI Depends(...) objects \u2014 no manual middleware.
parse_security_schemes(spec) -> dict[str, dict] \u2014 collects schemes, resolving {ENV_VAR} placeholders.make_security_dependencies(spec, security_schemes) -> dict[str, list[Depends]] \u2014 builds METHOD:/path \u2192 dependency list from per-operation security, falling back to top-level security.Supported scheme types (extensible):
type: http, scheme: bearer \u2014 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.Key properties: resolution is per-operation, env placeholders resolve once at startup, and dependencies are injected by binder \u2014 your handlers never mention security.
codegen & cli \u2014 Build-Time Tooling","text":"Generation is strictly build-time \u2014 spec \u2192 code, run once by a developer, committed:
"},{"location":"02_components/#openapi-first-models-spec-o-file","title":"openapi-first models <spec> -o <file>","text":"Generates Pydantic models from spec schemas (wraps datamodel_code_generator):
# cli path\nopenapi-first models openapi.yaml -o app/models.py\n"},{"location":"02_components/#openapi-first-routes-spec-o-dir-use-models-models-module-models","title":"openapi-first routes <spec> -o <dir> [--use-models] [--models-module models]","text":"Generates one routes_<resource>.py stub file per resource, with NotImplementedError handlers bound to operationIds (optionally importing your generated models):
openapi-first routes openapi.yaml -o app/routes --use-models\n"},{"location":"02_components/#openapi-first-scaffold-template-path","title":"openapi-first scaffold <template> [path]","text":"Copies a bundled application template verbatim (see Templates).
"},{"location":"02_components/#9-templates-copyable-applications","title":"\ud83e\uddf1 9.templates \u2014 Copyable Applications","text":"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 \u2014 no lint/format/type gates, never imported at runtime. They exist to be copied via:
openapi-first scaffold <template> [target-dir]\nopenapi-first scaffold --list\n"},{"location":"02_components/#related","title":"\ud83d\udd17 Related","text":"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.
"},{"location":"04_design/#1-the-startup-pipeline","title":"\ud83c\udfd7\ufe0f 1. The Startup Pipeline","text":"Everything happens once, eagerly, at construction time \u2014 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\".
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 Client \u2014 same spec, same guarantees:
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"},{"location":"04_design/#2-guarantees-that-hold-by-construction","title":"\ud83d\udcdc 2. Guarantees That Hold by Construction","text":"These are not conventions \u2014 they are enforced at startup or client construction:
paths; there is no decorator-driven or implicit routing.operationId resolves to an exact handler name in routes_module; a missing one aborts bootstrap.operationId. An operation without one cannot be bound and fails fast.security dependencies come from securitySchemes + per-operation security; no manual middleware.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.
"},{"location":"04_design/#3-component-responsibilities","title":"\ud83e\udde9 3. Component Responsibilities","text":"Module Owns Refuses toloader parse + validate the spec modify or \"fix\" the spec app assemble FastAPI from spec + handlers routing decisions, decorators binder operationId \u2192 handler mapping infer handlers from paths client build one callable per operationId guess URLs, deserialize implicitly security schemes \u2192 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.
"},{"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 Rawhttpx.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"},{"location":"04_design/#related","title":"\ud83d\udd17 Related","text":"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.
Two places in the spec, both respected:
"},{"location":"05_security/#11-componentssecurityschemes-the-inventory","title":"1.1components.securitySchemes \u2014 the inventory","text":"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 x-server-url and x-introspect-path are library extensions that point at a JWT introspection endpoint (see Introspection).
security \u2014 per-operation (or global) requirements","text":"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 OpenAPI dynamic-scoping rules apply: an operation-level security replaces the global list (it does not merge).
security.py exposes two functions:
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.
make_security_dependencies builds a FastAPI dependency for type: http, scheme: bearer.
Two modes, decided by whether an introspection endpoint is configured:
"},{"location":"05_security/#31-with-introspection-x-introspect-path","title":"3.1 With introspection (x-introspect-path)","text":"Authorization: Bearer <token>{\"token\": \"<token>\"} to {x-server-url}{x-introspect-path} synchronously via the bundled httpx clientactive: true{\"user\": ...} from the introspection body \u2192 request.state.user401 (missing/invalid token) or 503 (auth service unreachable)request.state.token; no remote call# 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 OpenAPIFirstApp already does this internally \u2014 the snippet above shows what it encapsulates (and what you use directly if you assemble the pieces by hand).
Because handlers are plain callables, security is the one place FastAPI's TestClient earns its keep:
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 See Testing for the full recipe, including how tests stub the introspection server.
"},{"location":"05_security/#7-common-patterns","title":"\ud83d\udee1\ufe0f 7. Common Patterns","text":"Pattern How Public endpointsecurity: [] 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 \u2192 request.state.token Auth on the client side pass the token via client.<operationId>(headers={\"Authorization\": ...})"},{"location":"05_security/#related","title":"Related","text":"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.
All errors derive from OpenAPIFirstError (in openapi_first/errors.py), so a single except OpenAPIFirstError catches every first-party failure:
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 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"},{"location":"06_error_handling/#2-when-things-fail","title":"\u23f1\ufe0f 2. When Things Fail","text":"The single most important rule: violations are eager, not lazy.
"},{"location":"06_error_handling/#21-at-application-startup","title":"2.1 At application startup","text":"# 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 Because these raise during OpenAPIFirstApp(...) construction, CI catches them the moment a spec and its routes drift \u2014 before a single request is served.
OpenAPIClient(spec) # fails fast, same philosophy\n servers \u2192 OpenAPIClientErrorpaths \u2192 OpenAPIClientErroroperationIds \u2192 OpenAPIClientError (client methods must be unambiguous)operationId \u2192 OpenAPIClientErrorRuntime transport errors surface as httpx exceptions (httpx.RequestError family), not swallowed or remapped. Missing required args fail before any HTTP request is made:
client.get_user(path_params={\"user_id\": ...}) # OK\nclient.get_user() # ValueError \u2014 user_id required\n"},{"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":"Handlers raise FastAPI HTTPException for expected operation-level failures (404/422), and the OperationId-binding errors only exist at startup:
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"},{"location":"06_error_handling/#32-client-side","title":"3.2 Client-side","text":"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 httpx.HTTPStatusError isn't raised by the library \u2014 it's the standard httpx.raise_for_status() you can opt into per call. The library never masks a response code.
None of these can silently degrade: a violation is an exception at construction, not a 500 at request time.
"},{"location":"06_error_handling/#related","title":"Related","text":"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.
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 Run with:
pytest # 23 tests, zero mocks of the library\npytest -q\npytest tests/test_loader.py\n"},{"location":"07_testing/#2-the-fixture-pattern","title":"\ud83c\udfd7\ufe0f 2. The Fixture Pattern","text":"# 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"},{"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":"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"},{"location":"07_testing/#32-fail-fast-the-heart","title":"3.2 Fail-fast (the heart)","text":"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"},{"location":"07_testing/#33-loader-validation","title":"3.3 Loader validation","text":"def test_loader_invalid_spec_fails():\n with pytest.raises(OpenAPISpecLoadError):\n load_openapi(\"broken.yaml\")\n"},{"location":"07_testing/#34-client-contract-drift","title":"3.4 Client contract drift","text":"def test_client_duplicate_operation_id_raises():\n with pytest.raises(OpenAPIClientError):\n OpenAPIClient(dup_spec)\n"},{"location":"07_testing/#4-testing-the-templates","title":"\ud83c\udfdb\ufe0f 4. Testing the Templates","text":"Each bundled template ships its own test + in-memory store, so you get a runnable contract test the moment you scaffold:
openapi-first scaffold crud_app my-service\ncd my-service\npytest -q\n test_crud_app.py / test_model_app.py / test_vet_app.py exercise the full CRUD surface through TestClient \u2014 including 201/204 status codes, 404s, and (in vet_app) SSE streaming via StreamingResponse.
The same gates the library itself must pass are what keep the docs honest:
Gate Purposeblack --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:
black --check openapi_first tests\nruff check openapi_first tests\nmypy openapi_first\npytest\n"},{"location":"07_testing/#6-testing-tips","title":"\ud83d\udca1 6. Testing Tips","text":"OpenAPIFirstApp(...) raises for the broken contracts \u2014 those are your most valuable testsThis 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.
"},{"location":"03_use_cases/01_quickstart/#1-prerequisites","title":"\ud83d\udee0\ufe0f 1. Prerequisites","text":"openapi-first installed (see Overview)pip install \"fastapi[standard]\" (or uvicorn) to run the appOpenAPI comes first. Create openapi.yaml:
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 Key points: every operation needs operationId, and every route must exist only here.
Create routes.py \u2014 plain functions, no decorators, named exactly like the operationIds:
# routes.py\ndef get_greeting(name: str) -> dict:\n \"\"\"Return a greeting for the given name.\"\"\"\n return {\"greeting\": f\"Hello, {name}!\"}\n If a handler is missing at startup, the app refuses to boot (MissingOperationHandler) \u2014 the fail-fast guarantee catches contract drift immediately.
Create main.py:
# 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 Run it:
uvicorn main:app --reload\n Visit http://localhost:8000/docs (Swagger UI) and http://localhost:8000/openapi.json \u2014 both are generated from your spec.
The same spec builds a strict client:
# 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"},{"location":"03_use_cases/01_quickstart/#6-next-steps","title":"\ud83d\udca1 6. Next Steps","text":"openapi-first ships four runnable, copyable applications under openapi_first/templates/. They are not part of the library API \u2014 they are bundled scaffold examples you copy into your own project and build on.
A template is a complete, self-contained OpenAPI-first service:
openapi_first/templates/<name>/All templates share the same skeleton:
<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"},{"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 health_app \u2014 minimal liveness probe","text":"openapi-first scaffold health_app\n# or into a custom directory:\nopenapi-first scaffold health_app my-health-service\n File Purpose openapi.yaml GET /health \u2192 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 \u2014 one operation, one handler, zero moving parts. The best starting point to internalize the mental model.
Smoke test:
pip install -e .\nuvicorn main:app\ncurl http://localhost:8000/health\n# \u2192 {\"status\": \"ok\"}\n"},{"location":"03_use_cases/02_templates/#22-crud_app-dict-based-crud","title":"2.2 crud_app \u2014 dict-based CRUD","text":"openapi-first scaffold crud_app my-crud-service\n File Purpose openapi.yaml Full CRUD over /items (list/get/create/update/delete) routes.py Handlers bound via operationIds list_items, get_item, create_item, update_item, delete_item data.py In-memory dict store with auto-incrementing id Behaviors you learn:
create_item/delete_item take response: Response and set 201/204; get_item/update_item raise HTTPException(404) on KeyErrordata.py is a copyable in-memory store, explicitly not production-readymodel_app \u2014 Pydantic model CRUD","text":"openapi-first scaffold model_app my-model-service\n 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:
operationId set as crud_app, so the two are interchangeableOpenAPIClient body handlingvet_app \u2014 the full-featured demo","text":"openapi-first scaffold vet_app my-vet-clinic\n 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 \u2192 vets \u2192 treatments \u2192 pets \u2192 appointments) main.py App + CORS + lifespan example Behaviors you learn \u2014 the advanced tier:
ProcedureNotes uses oneOf + discriminator.noteType mapping; Pydantic models use Literal[...] discriminator fieldsGET /pets/{id}/actions operation streaming text/event-stream via StreamingResponse with background task workersUploadFile handler setting a multi-part bodyadd_middleware(CORSMiddleware, ...) alongside the spec-driven setup201/204 explicitly via injected Response# 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 Protip: DEFAULT_TEMPLATE is health_app, so openapi-first scaffold with no template name scaffolds the health app.
After openapi-first scaffold health_app my-health-service, your directory contains a drop-in FastAPI service:
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 Run it:
cd my-health-service\npip install -e .\nuvicorn main:app --reload\n /docs, /openapi.json, and every declared route now exist \u2014 all derived from openapi.yaml.
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.
See the __init__.py of each template for detailed client examples, CLI examples, and design notes.
OpenAPIClient is the other side of the contract. It reads the same OpenAPI document the server runs on and exposes one callable per operationId \u2014 so \"client\" and \"server\" are two views of one truth.
The client is httpx-based and returns raw httpx.Response objects \u2014 no magic deserialization, no hidden schema inference:
requests.get(...) scattered through your codefrom openapi_first.loader import load_openapi\nfrom openapi_first.client import OpenAPIClient\n\nspec = load_openapi(\"openapi.yaml\")\nclient = OpenAPIClient(spec)\n The base URL comes from the spec's servers list (first entry) unless you pass base_url explicitly:
client = OpenAPIClient(spec, base_url=\"https://api.internal.myco/v1\")\n You can also hand over a preconfigured httpx.Client (custom transport, TLS, retries):
import httpx\n\ntransport = httpx.HTTPTransport(retries=3)\nclient = OpenAPIClient(\n spec,\n client=httpx.Client(transport=transport),\n)\n"},{"location":"03_use_cases/03_client/#3-fail-fast-at-construction","title":"\ud83d\udca5 3. Fail-Fast at Construction","text":"OpenAPIClient(...) raises immediately if the contract is broken \u2014 you find out at startup, not on the first request:
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 \u2014 pydoclint-grade strictness on the wire.
"},{"location":"03_use_cases/03_client/#4-calling-operations","title":"\ud83d\udcde 4. Calling Operations","text":"Every operationId becomes a method. The call signature is uniform across the whole client:
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 Concrete examples (from the crud_app / health_app templates):
# 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 Returns: the raw httpx.Response, so status_code, .json(), .headers are all yours to inspect.
For each operation the client knows exactly where each parameter belongs:
OpenAPI location Client kwargin: 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=.
# One directory, two processes\nuvicorn main:app --port 8000 # server\npython -c \"import asyncio; from client_script import run; asyncio.run(run())\" # client\n Or the same client against a remote environment:
client = OpenAPIClient(\n spec,\n base_url=\"https://staging.internal.myco\",\n)\n The URL is the only thing that changes between environments \u2014 the contract never does.
"},{"location":"03_use_cases/03_client/#7-operationid-as-the-api","title":"\u270d\ufe0f 7. OperationId as the API","text":"Because the client is operationId-driven:
operationId is the one name you remember; the HTTP verb/path is an implementation detailIf an operationId you call doesn't exist, you get an AttributeError at construction-scan time \u2014 never a silent 404.
TL;DR \u2014 point openapi-first models / openapi-first routes at your spec and get Pydantic models plus operation-matching route stubs. The output is deterministic and is a starting point \u2014 not a maintained artifact.
Two pain points kill OpenAPI projects:
item_id: int, your client sends a string\u2026).bind_routes requires each operationId to resolve to a function of the exact same name.Codegen collapses both. One command, one source file, the same shapes everywhere.
"},{"location":"03_use_cases/04_codegen/#2-model-generation","title":"\ud83d\ude80 2. Model Generation","text":"openapi-first models openapi.yaml -o app/_gen/models.py\n The generated module mirrors the spec's components.schemas by name:
components.schemas.User class User(BaseModel) components.schemas.Item class Item(BaseModel) required + type from schema Pydantic Field(...) / type hints \u2713 Never hand-edit those files. If the spec changes, regenerate \u2014 just like re-running a compiler after editing source.
"},{"location":"03_use_cases/04_codegen/#why-constructor-time-validation-still-applies","title":"Why constructor-time validation still applies","text":"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.
Wire model vs. domain model. Generated models are the wire contract. Keep them separate from your persisted/domain model (see the standards CRUD Template): give the wire schema an id: str, but model the Mongo document so the id is optional.
openapi-first routes openapi.yaml -o app/interfaces \\\n --use-models --models-module app._gen.models \\\n --inject-request --package\n One file per resource (derived from the first path segment). Flags:
Flag Effect--use-models Import request-body $ref models from --models-module --models-module MOD Dotted module for those imports (default models) --inject-request Prepend request: Request to every handler (DI convention) --package Emit <resource>/routes.py packages (with __init__.py) instead of flat <resource>.py --template-style Emit async def handlers with a JSON body parameter named body (the standards-template convention) plus a service-resolution hint, instead of sync def handlers taking payload Why bother? Because bind_routes (in 02 \u2013 Components) needs an operationId \u2192 handler exactly matching the spec. Codegen guarantees you never type a handler name wrong \u2014 it wears the same operationId the spec declares.
/fetch-requests yields a sanitized module name fetch_requests.py (a raw hyphen could never be imported).multipart/form-data and application/x-www-form-urlencoded bodies expand to one parameter per schema property: UploadFile = File(...) for format: binary, Form(...) otherwise \u2014 not an opaque payload: dict. Fields are required only when listed in the schema's required.The stubs raise NotImplementedError; fill each body in (typically by resolving a service from request.app.state).
Both shapes bind equally \u2014 the router resolves the handler by operationId and introspects the FastAPI signature, so it never inspects the request body:
def create_item(payload: ItemInput, response: Response): \u2014 the body parameter is payload.--template-style: async def create_item(request: Request, body: ItemInput, response: Response): \u2014 matches the standards template (async def, body parameter, a request.app.state.db_resources[\"<resource>_svc\"] hint).Use --template-style when generating against the standards template so the scaffold matches the hand-written handlers you will keep.
Note: models / routes are build-time tooling. They do not run at runtime, and templates (the Bake-your-stuff section of 02 \u2013 Templates) scaffold a working server out-of-the-box for greenfield projects.
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
That determinism is what lets you git diff after a spec change and see exactly what moved.