16 Commits

39 changed files with 3307 additions and 578 deletions

96
README.md Normal file
View File

@@ -0,0 +1,96 @@
# openapi_first
# Summary
FastAPI OpenAPI First — strict OpenAPI-first application bootstrap for FastAPI.
FastAPI OpenAPI First is a **contract-first infrastructure library** that
enforces OpenAPI as the single source of truth for FastAPI services.
The library removes decorator-driven routing and replaces it with
deterministic, spec-driven application assembly. Every HTTP route,
method, and operation is defined in OpenAPI first and bound to Python
handlers explicitly via `operationId`.
---
# Installation
Install using pip:
```bash
pip install openapi-first
```
Or with Poetry:
```bash
poetry add openapi-first
```
---
# Quick Start
Minimal OpenAPI-first FastAPI application:
```python
from openapi_first import app
import my_service.routes as routes
api = app.OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=routes,
title="My Service",
version="1.0.0",
)
```
OperationId-driven HTTP client:
```python
from openapi_first.loader import load_openapi
from openapi_first.client import OpenAPIClient
spec = load_openapi("openapi.yaml")
client = OpenAPIClient(spec)
response = client.get_health()
```
---
# Architecture
The library is structured around four core responsibilities:
- `loader`: Load and validate OpenAPI 3.x specifications (JSON/YAML).
- `binder`: Bind OpenAPI operations to FastAPI routes via `operationId`.
- `app`: OpenAPI-first FastAPI application bootstrap.
- `client`: OpenAPI-first HTTP client driven by the same specification.
- `errors`: Explicit error hierarchy for contract violations.
---
# Public API
The supported public API consists of the following top-level modules:
- `openapi_first.app`
- `openapi_first.binder`
- `openapi_first.loader`
- `openapi_first.client`
- `openapi_first.errors`
---
# Design Guarantees
- OpenAPI is the single source of truth.
- No undocumented routes can exist.
- No OpenAPI operation can exist without a handler or client callable.
- All contract violations fail at application startup or client creation.
- No hidden FastAPI magic or implicit behavior.
- Deterministic, testable application assembly.
---

View File

@@ -2,7 +2,7 @@
"module": "openapi_first.app",
"content": {
"path": "openapi_first.app",
"docstring": "openapi_first.app\n=========================\n\nOpenAPI-first application bootstrap for FastAPI.\n\nThis module provides `OpenAPIFirstApp`, a thin but strict abstraction\nthat enforces OpenAPI as the single source of truth for a FastAPI service.\n\nCore principles\n---------------\n- The OpenAPI specification (JSON or YAML) defines the entire API surface.\n- Every operationId in the OpenAPI spec must have a corresponding\n Python handler function.\n- Handlers are plain Python callables (no FastAPI decorators).\n- FastAPI route registration is derived exclusively from the spec.\n- FastAPI's autogenerated OpenAPI schema is fully overridden.\n\nWhat this module does\n---------------------\n- Loads and validates an OpenAPI 3.x specification.\n- Dynamically binds HTTP routes to handler functions using operationId.\n- Registers routes with FastAPI at application startup.\n- Ensures runtime behavior matches the OpenAPI contract exactly.\n\nWhat this module does NOT do\n----------------------------\n- It does not generate OpenAPI specs.\n- It does not generate client code.\n- It does not introduce a new framework or lifecycle.\n- It does not alter FastAPI dependency injection semantics.\n\nIntended usage\n--------------\nThis module is intended for teams that want:\n\n- OpenAPI-first API development\n- Strong contract enforcement\n- Minimal FastAPI boilerplate\n- Predictable, CI-friendly failures for spec/implementation drift",
"docstring": "# Summary\n\nOpenAPI-first application bootstrap for FastAPI.\n\nThis module provides `OpenAPIFirstApp`, a thin but strict abstraction\nthat enforces OpenAPI as the single source of truth for a FastAPI service.\n\nNotes:\n **Core Principles:**\n\n - The OpenAPI specification (JSON or YAML) defines the entire API surface.\n - Every `operationId` in the OpenAPI spec must have a corresponding\n Python handler function.\n - Handlers are plain Python callables (no FastAPI decorators).\n - FastAPI route registration is derived exclusively from the spec.\n - FastAPI's autogenerated OpenAPI schema is fully overridden.\n\n **Responsibilities:**\n\n - Loads and validates an OpenAPI 3.x specification.\n - Dynamically binds HTTP routes to handler functions using `operationId`.\n - Registers routes with FastAPI at application startup.\n - Ensures runtime behavior matches the OpenAPI contract exactly.\n\n **Constraints:**\n\n - This module intentionally does NOT:\n - Generate OpenAPI specs.\n - Generate client code.\n - Introduce a new framework or lifecycle.\n - Alter FastAPI dependency injection semantics.",
"objects": {
"FastAPI": {
"name": "FastAPI",
@@ -16,21 +16,21 @@
"kind": "function",
"path": "openapi_first.app.load_openapi",
"signature": "<bound method Alias.signature of Alias('load_openapi', 'openapi_first.loader.load_openapi')>",
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nThe specification is parsed based on file extension and validated\nusing a strict OpenAPI schema validator. Any error results in an\nimmediate exception, preventing application startup.\n\nParameters\n----------\npath : str or pathlib.Path\n Filesystem path to an OpenAPI specification file.\n Supported extensions:\n - `.json`\n - `.yaml`\n - `.yml`\n\nReturns\n-------\ndict\n Parsed and validated OpenAPI specification.\n\nRaises\n------\nOpenAPISpecLoadError\n If the file does not exist, cannot be parsed, or fails\n OpenAPI schema validation."
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nArgs:\n path (str | Path):\n Filesystem path to an OpenAPI specification file. Supported\n extensions: `.json`, `.yaml`, `.yml`.\n\nReturns:\n dict[str, Any]:\n Parsed and validated OpenAPI specification.\n\nRaises:\n OpenAPISpecLoadError:\n If the file does not exist, cannot be parsed, or fails OpenAPI\n schema validation.\n\nNotes:\n **Guarantees:**\n\n - The specification is parsed based on file extension and validated\n using a strict OpenAPI schema validator.\n - Any error results in an immediate exception, preventing\n application startup."
},
"bind_routes": {
"name": "bind_routes",
"kind": "function",
"path": "openapi_first.app.bind_routes",
"signature": "<bound method Alias.signature of Alias('bind_routes', 'openapi_first.binder.bind_routes')>",
"docstring": "Bind OpenAPI operations to FastAPI routes.\n\nIterates through the OpenAPI specification paths and methods,\nresolves each operationId to a handler function, and registers\na corresponding APIRoute on the FastAPI application.\n\nParameters\n----------\napp : fastapi.FastAPI\n The FastAPI application instance to which routes will be added.\n\nspec : dict\n Parsed OpenAPI 3.x specification dictionary.\n\nroutes_module : module\n Python module containing handler functions. Each handler's\n name MUST exactly match an OpenAPI operationId.\n\nRaises\n------\nMissingOperationHandler\n If an operationId is missing from the spec or if no corresponding\n handler function exists in the routes module.\n\nBehavior guarantees\n-------------------\n- Route registration is deterministic and spec-driven.\n- No route decorators are required or supported.\n- Handler resolution errors surface at application startup."
"docstring": "Bind OpenAPI operations to FastAPI routes.\n\nArgs:\n app (fastapi.FastAPI):\n The FastAPI application instance to which routes will be added.\n spec (dict):\n Parsed OpenAPI 3.x specification dictionary.\n routes_module (module):\n Python module containing handler functions. Each handler's name MUST\n exactly match an OpenAPI `operationId`.\n\nRaises:\n MissingOperationHandler:\n If an `operationId` is missing from the spec or if no corresponding\n handler function exists in the routes module.\n\nNotes:\n **Responsibilities:**\n\n - Iterates through the OpenAPI specification paths and methods.\n - Resolves each `operationId` to a handler function, and registers\n a corresponding `APIRoute` on the FastAPI application.\n\n **Guarantees:**\n\n - Route registration is deterministic and spec-driven. No route\n decorators are required or supported. Handler resolution errors\n surface at application startup."
},
"OpenAPIFirstApp": {
"name": "OpenAPIFirstApp",
"kind": "class",
"path": "openapi_first.app.OpenAPIFirstApp",
"signature": "<bound method Class.signature of Class('OpenAPIFirstApp', 49, 120)>",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\n`OpenAPIFirstApp` subclasses FastAPI and replaces manual route\nregistration with OpenAPI-driven binding. All routes are derived\nfrom the provided OpenAPI specification, and each operationId is\nmapped to a Python function in the supplied routes module.\n\nParameters\n----------\nopenapi_path : str\n Filesystem path to the OpenAPI 3.x specification file.\n This specification is treated as the authoritative API contract.\n\nroutes_module : module\n Python module containing handler functions whose names correspond\n exactly to OpenAPI operationId values.\n\n**fastapi_kwargs\n Additional keyword arguments passed directly to `fastapi.FastAPI`\n (e.g., title, version, middleware, lifespan handlers).\n\nRaises\n------\nOpenAPIFirstError\n If the OpenAPI specification is invalid, or if any declared\n operationId does not have a corresponding handler function.\n\nBehavior guarantees\n-------------------\n- No route can exist without an OpenAPI declaration.\n- No OpenAPI operation can exist without a handler.\n- Swagger UI and `/openapi.json` always reflect the provided spec.\n- Handler functions remain framework-agnostic and testable.\n\nExample\n-------\n```python\nfrom openapi_first import OpenAPIFirstApp\nimport app.routes as routes\n\napp = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n)\n```",
"signature": "<bound method Class.signature of Class('OpenAPIFirstApp', 41, 115)>",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
"members": {
"openapi": {
"name": "openapi",

View File

@@ -2,7 +2,7 @@
"module": "openapi_first.binder",
"content": {
"path": "openapi_first.binder",
"docstring": "openapi_first.binder\n============================\n\nOpenAPI-driven route binding for FastAPI.\n\nThis module is responsible for translating an OpenAPI 3.x specification\ninto concrete FastAPI routes. It enforces a strict one-to-one mapping\nbetween OpenAPI operations and Python handler functions using operationId.\n\nCore responsibility\n-------------------\n- Read path + method definitions from an OpenAPI specification\n- Resolve each operationId to a Python callable\n- Register routes with FastAPI using APIRoute\n- Fail fast when contract violations are detected\n\nDesign constraints\n------------------\n- All routes MUST be declared in the OpenAPI specification.\n- All OpenAPI operations MUST define an operationId.\n- Every operationId MUST resolve to a handler function.\n- Handlers are plain Python callables (no decorators required).\n- No implicit route creation or inference is allowed.\n\nThis module intentionally does NOT:\n-------------------------------\n- Perform request or response validation\n- Generate Pydantic models\n- Modify FastAPI dependency injection\n- Interpret OpenAPI semantics beyond routing metadata\n\nThose concerns belong to other layers or tooling.",
"docstring": "# Summary\n\nOpenAPI-driven route binding for FastAPI.\n\nThis module is responsible for translating an OpenAPI 3.x specification\ninto concrete FastAPI routes. It enforces a strict one-to-one mapping\nbetween OpenAPI operations and Python handler functions using `operationId`.\n\nNotes:\n **Core Responsibility:**\n\n - Read path + method definitions from an OpenAPI specification.\n - Resolve each `operationId` to a Python callable.\n - Register routes with FastAPI using `APIRoute`.\n - Fail fast when contract violations are detected.\n\n **Design Constraints:**\n\n - All routes MUST be declared in the OpenAPI specification.\n - All OpenAPI operations MUST define an `operationId`.\n - Every `operationId` MUST resolve to a handler function.\n - Handlers are plain Python callables (no decorators required).\n - No implicit route creation or inference is allowed.\n\n **Constraints:**\n\n - This module intentionally does NOT:\n - Perform request or response validation.\n - Generate Pydantic models.\n - Modify FastAPI dependency injection.\n - Interpret OpenAPI semantics beyond routing metadata.",
"objects": {
"APIRoute": {
"name": "APIRoute",
@@ -16,14 +16,14 @@
"kind": "class",
"path": "openapi_first.binder.MissingOperationHandler",
"signature": "<bound method Alias.signature of Alias('MissingOperationHandler', 'openapi_first.errors.MissingOperationHandler')>",
"docstring": "Raised when an OpenAPI operation cannot be resolved to a handler.\n\nThis error occurs when:\n- An OpenAPI operation does not define an operationId, or\n- An operationId is defined but no matching function exists in the\n provided routes module.\n\nThis represents a violation of the OpenAPI-first contract and\nindicates that the specification and implementation are out of sync."
"docstring": "Raised when an OpenAPI operation cannot be resolved to a handler.\n\nNotes:\n **Scenarios:**\n\n - An OpenAPI operation does not define an `operationId`.\n - An `operationId` is defined but no matching function exists in\n the provided routes module.\n\n **Guarantees:**\n\n - This represents a violation of the OpenAPI-first contract and\n indicates that the specification and implementation are out of\n sync."
},
"bind_routes": {
"name": "bind_routes",
"kind": "function",
"path": "openapi_first.binder.bind_routes",
"signature": "<bound method Function.signature of Function('bind_routes', 41, 102)>",
"docstring": "Bind OpenAPI operations to FastAPI routes.\n\nIterates through the OpenAPI specification paths and methods,\nresolves each operationId to a handler function, and registers\na corresponding APIRoute on the FastAPI application.\n\nParameters\n----------\napp : fastapi.FastAPI\n The FastAPI application instance to which routes will be added.\n\nspec : dict\n Parsed OpenAPI 3.x specification dictionary.\n\nroutes_module : module\n Python module containing handler functions. Each handler's\n name MUST exactly match an OpenAPI operationId.\n\nRaises\n------\nMissingOperationHandler\n If an operationId is missing from the spec or if no corresponding\n handler function exists in the routes module.\n\nBehavior guarantees\n-------------------\n- Route registration is deterministic and spec-driven.\n- No route decorators are required or supported.\n- Handler resolution errors surface at application startup."
"signature": "<bound method Function.signature of Function('bind_routes', 40, 100)>",
"docstring": "Bind OpenAPI operations to FastAPI routes.\n\nArgs:\n app (fastapi.FastAPI):\n The FastAPI application instance to which routes will be added.\n spec (dict):\n Parsed OpenAPI 3.x specification dictionary.\n routes_module (module):\n Python module containing handler functions. Each handler's name MUST\n exactly match an OpenAPI `operationId`.\n\nRaises:\n MissingOperationHandler:\n If an `operationId` is missing from the spec or if no corresponding\n handler function exists in the routes module.\n\nNotes:\n **Responsibilities:**\n\n - Iterates through the OpenAPI specification paths and methods.\n - Resolves each `operationId` to a handler function, and registers\n a corresponding `APIRoute` on the FastAPI application.\n\n **Guarantees:**\n\n - Route registration is deterministic and spec-driven. No route\n decorators are required or supported. Handler resolution errors\n surface at application startup."
},
"Any": {
"name": "Any",

View File

@@ -2,7 +2,7 @@
"module": "openapi_first.cli",
"content": {
"path": "openapi_first.cli",
"docstring": "openapi_first.cli\n========================\n\nCommand-line interface for FastAPI OpenAPI-first scaffolding utilities.\n\nThis CLI bootstraps OpenAPI-first FastAPI applications from versioned,\nbundled templates packaged with the library.",
"docstring": "Command-line interface for FastAPI OpenAPI-first scaffolding utilities.\n\n---\n\n## Summary\n\nThis CLI bootstraps OpenAPI-first FastAPI applications from versioned,\nbundled templates packaged with the library.",
"objects": {
"argparse": {
"name": "argparse",
@@ -43,21 +43,21 @@
"name": "available_templates",
"kind": "function",
"path": "openapi_first.cli.available_templates",
"signature": "<bound method Function.signature of Function('available_templates', 20, 29)>",
"docstring": "Return a list of available application templates."
"signature": "<bound method Function.signature of Function('available_templates', 21, 34)>",
"docstring": "Return a list of available application templates.\n\nReturns:\n list[str]:\n Sorted list of template names found in the internal templates directory."
},
"copy_template": {
"name": "copy_template",
"kind": "function",
"path": "openapi_first.cli.copy_template",
"signature": "<bound method Function.signature of Function('copy_template', 32, 49)>",
"docstring": "Copy a bundled OpenAPI-first application template into a directory."
"signature": "<bound method Function.signature of Function('copy_template', 37, 64)>",
"docstring": "Copy a bundled OpenAPI-first application template into a directory.\n\nArgs:\n template (str):\n Name of the template to copy.\n target_dir (Path):\n Filesystem path where the template should be copied.\n\nRaises:\n FileNotFoundError:\n If the requested template does not exist."
},
"main": {
"name": "main",
"kind": "function",
"path": "openapi_first.cli.main",
"signature": "<bound method Function.signature of Function('main', 52, 88)>",
"signature": "<bound method Function.signature of Function('main', 67, 103)>",
"docstring": null
}
}

View File

@@ -2,7 +2,7 @@
"module": "openapi_first.client",
"content": {
"path": "openapi_first.client",
"docstring": "openapi_first.client\n====================\n\nOpenAPI-first HTTP client for contract-driven services.\n\nThis module provides `OpenAPIClient`, a thin, strict HTTP client that\nderives all callable operations directly from an OpenAPI 3.x specification.\n\nIt is the client counterpart to `OpenAPIFirstApp`.\n\nCore principles\n---------------\n- The OpenAPI specification is the single source of truth.\n- Each operationId becomes a callable Python method.\n- No implicit schema mutation or inference.\n- No code generation step.\n- Minimal abstraction over httpx.\n\nWhat this module does\n---------------------\n- Parses an OpenAPI 3.x specification.\n- Dynamically creates one callable per operationId.\n- Enforces presence of:\n - servers\n - paths\n - operationId\n- Formats path parameters safely.\n- Handles JSON request bodies explicitly.\n- Returns raw `httpx.Response` objects.\n\nWhat this module does NOT do\n----------------------------\n- It does not generate client code.\n- It does not validate request/response schemas.\n- It does not deserialize responses.\n- It does not retry requests.\n- It does not implement authentication helpers.\n- It does not assume non-2xx responses are failures.\n\nIntended usage\n--------------\nThis client is designed for:\n\n- Service-to-service communication\n- Integration testing\n- Contract-driven internal SDK usage\n- Systems that want OpenAPI-first symmetry with `OpenAPIFirstApp`\n\nDesign constraints\n------------------\n- Only the first server in the OpenAPI `servers` list is used if\n `base_url` is not explicitly provided.\n- Only explicitly declared request bodies are allowed.\n- `application/json` is handled natively; other media types are sent as raw content.\n- All responses are returned as-is.",
"docstring": "# Summary\n\nOpenAPI-first HTTP client for contract-driven services.\n\nThis module provides `OpenAPIClient`, a thin, strict HTTP client that\nderives all callable operations directly from an OpenAPI 3.x specification.\n\nIt is the client counterpart to `OpenAPIFirstApp`.\n\nNotes:\n **Core Principles:**\n\n - The OpenAPI specification is the single source of truth\n - Each operationId becomes a callable Python method\n - No implicit schema mutation or inference\n - No code generation step\n - Minimal abstraction over httpx\n\n **Responsibilities:**\n\n - Parses an OpenAPI 3.x specification\n - Dynamically creates one callable per operationId\n - Enforces presence of servers, paths, and operationId\n - Formats path parameters safely\n - Handles JSON request bodies explicitly\n - Returns raw `httpx.Response` objects\n\n **Constraints:**\n\n - This module intentionally does NOT: Generate client code, validate request/response schemas, deserialize responses, retry requests, implement authentication helpers, or assume non-2xx responses are failures.",
"objects": {
"Any": {
"name": "Any",
@@ -51,21 +51,21 @@
"kind": "class",
"path": "openapi_first.client.OpenAPIFirstError",
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstError', 'openapi_first.errors.OpenAPIFirstError')>",
"docstring": "Base exception for all OpenAPI-first enforcement errors.\n\nThis exception exists to allow callers, test suites, and CI pipelines\nto catch and distinguish OpenAPI contract violations from unrelated\nruntime errors.\n\nAll exceptions raised by the OpenAPI-first core should inherit from\nthis type."
"docstring": "Base exception for all OpenAPI-first enforcement errors.\n\nNotes:\n **Responsibilities:**\n\n - This exception exists to allow callers, test suites, and CI\n pipelines to catch and distinguish OpenAPI contract violations\n from unrelated runtime errors.\n - All exceptions raised by the OpenAPI-first core should inherit\n from this type."
},
"OpenAPIClientError": {
"name": "OpenAPIClientError",
"kind": "class",
"path": "openapi_first.client.OpenAPIClientError",
"signature": "<bound method Class.signature of Class('OpenAPIClientError', 67, 68)>",
"signature": "<bound method Class.signature of Class('OpenAPIClientError', 42, 45)>",
"docstring": "Raised when an OpenAPI client operation fails."
},
"OpenAPIClient": {
"name": "OpenAPIClient",
"kind": "class",
"path": "openapi_first.client.OpenAPIClient",
"signature": "<bound method Class.signature of Class('OpenAPIClient', 71, 291)>",
"docstring": "OpenAPI-first HTTP client (httpx-based).\n\nThis client derives all callable methods directly from an OpenAPI 3.x\nspecification. Each operationId becomes a method on the client\ninstance.\n\nDesign principles\n-----------------\n- One callable per operationId\n- Explicit parameters (path, query, headers, body)\n- No implicit schema inference or mutation\n- Returns raw httpx.Response objects\n- No response validation or deserialization\n\nParameters\n----------\nspec : dict\n Parsed OpenAPI 3.x specification.\nbase_url : str | None\n Base URL of the target service. If omitted, the first entry\n in the OpenAPI `servers` list is used.\nclient : httpx.Client | None\n Optional preconfigured httpx client instance.\n\nRaises\n------\nOpenAPIClientError\n If:\n - No servers are defined and base_url is not provided\n - OpenAPI spec has no paths\n - An operation is missing operationId\n - Duplicate operationIds are detected\n - Required path parameters are missing\n - Required request body is missing\n\n Example\n-------\n```python\nfrom openapi_first import loader, client\n\nspec = loader.load_openapi(\"openapi.yaml\")\n\napi = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n)\n\n# Call operationId: getUser\nresponse = api.getUser(\n path_params={\"user_id\": 123}\n)\n\nprint(response.status_code)\nprint(response.json())\n\n# Call operationId: createUser\nresponse = api.createUser(\n body={\"name\": \"Bob\"}\n)\n\nprint(response.status_code)\n```",
"signature": "<bound method Class.signature of Class('OpenAPIClient', 48, 257)>",
"docstring": "OpenAPI-first HTTP client (`httpx`-based).\n\nNotes:\n **Responsibilities:**\n\n - This client derives all callable methods directly from an\n OpenAPI 3.x specification. Each `operationId` becomes a method\n on the client instance.\n\n **Guarantees:**\n\n - One callable per `operationId`.\n - Explicit parameters (path, query, headers, body).\n - No implicit schema inference or mutation.\n - Returns raw `httpx.Response` objects.\n - No response validation or deserialization.\n\nExample:\n ```python\n from openapi_first import loader, client\n\n spec = loader.load_openapi(\"openapi.yaml\")\n\n api = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n )\n\n # Call operationId: getUser\n response = api.getUser(\n path_params={\"user_id\": 123}\n )\n\n print(response.status_code)\n print(response.json())\n ```",
"members": {
"spec": {
"name": "spec",
@@ -92,7 +92,7 @@
"name": "operations",
"kind": "function",
"path": "openapi_first.client.OpenAPIClient.operations",
"signature": "<bound method Function.signature of Function('operations', 160, 161)>",
"signature": "<bound method Function.signature of Function('operations', 126, 127)>",
"docstring": null
}
}

View File

@@ -2,21 +2,21 @@
"module": "openapi_first.errors",
"content": {
"path": "openapi_first.errors",
"docstring": "openapi_first.errors\n============================\n\nCustom exceptions for OpenAPI-first FastAPI applications.\n\nThis module defines a small hierarchy of explicit, intention-revealing\nexceptions used to signal contract violations between an OpenAPI\nspecification and its Python implementation.\n\nDesign principles\n-----------------\n- Errors represent *programmer mistakes*, not runtime conditions.\n- All errors are raised during application startup.\n- Messages are actionable and suitable for CI/CD output.\n- Exceptions are explicit rather than reused from generic built-ins.\n\nThese errors should normally cause immediate application failure.",
"docstring": "# Summary\n\nExceptions for OpenAPI-first FastAPI applications.\n\nThis module defines a small hierarchy of explicit, intention-revealing\nexceptions used to signal contract violations between an OpenAPI\nspecification and its Python implementation.\n\nNotes:\n **Design Principles:**\n\n - Errors represent programmer mistakes, not runtime conditions.\n - All errors are raised during application startup.\n - Messages are actionable and suitable for CI/CD output.\n - Exceptions are explicit rather than reused from generic built-ins.\n\n These errors should normally cause immediate application failure.",
"objects": {
"OpenAPIFirstError": {
"name": "OpenAPIFirstError",
"kind": "class",
"path": "openapi_first.errors.OpenAPIFirstError",
"signature": "<bound method Class.signature of Class('OpenAPIFirstError', 21, 32)>",
"docstring": "Base exception for all OpenAPI-first enforcement errors.\n\nThis exception exists to allow callers, test suites, and CI pipelines\nto catch and distinguish OpenAPI contract violations from unrelated\nruntime errors.\n\nAll exceptions raised by the OpenAPI-first core should inherit from\nthis type."
"signature": "<bound method Class.signature of Class('OpenAPIFirstError', 21, 34)>",
"docstring": "Base exception for all OpenAPI-first enforcement errors.\n\nNotes:\n **Responsibilities:**\n\n - This exception exists to allow callers, test suites, and CI\n pipelines to catch and distinguish OpenAPI contract violations\n from unrelated runtime errors.\n - All exceptions raised by the OpenAPI-first core should inherit\n from this type."
},
"MissingOperationHandler": {
"name": "MissingOperationHandler",
"kind": "class",
"path": "openapi_first.errors.MissingOperationHandler",
"signature": "<bound method Class.signature of Class('MissingOperationHandler', 35, 72)>",
"docstring": "Raised when an OpenAPI operation cannot be resolved to a handler.\n\nThis error occurs when:\n- An OpenAPI operation does not define an operationId, or\n- An operationId is defined but no matching function exists in the\n provided routes module.\n\nThis represents a violation of the OpenAPI-first contract and\nindicates that the specification and implementation are out of sync."
"signature": "<bound method Class.signature of Class('MissingOperationHandler', 37, 78)>",
"docstring": "Raised when an OpenAPI operation cannot be resolved to a handler.\n\nNotes:\n **Scenarios:**\n\n - An OpenAPI operation does not define an `operationId`.\n - An `operationId` is defined but no matching function exists in\n the provided routes module.\n\n **Guarantees:**\n\n - This represents a violation of the OpenAPI-first contract and\n indicates that the specification and implementation are out of\n sync."
},
"Optional": {
"name": "Optional",

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
"module": "openapi_first.loader",
"content": {
"path": "openapi_first.loader",
"docstring": "openapi_first.loaders\n=============================\n\nOpenAPI specification loading and validation utilities.\n\nThis module is responsible for loading an OpenAPI 3.x specification\nfrom disk and validating it before it is used by the application.\n\nIt enforces the principle that an invalid or malformed OpenAPI document\nmust never reach the routing or runtime layers.\n\nDesign principles\n-----------------\n- OpenAPI is treated as an authoritative contract.\n- Invalid specifications fail fast at application startup.\n- Supported formats are JSON and YAML.\n- Validation errors are surfaced clearly and early.\n\nThis module intentionally does NOT:\n-----------------------------------\n- Modify the OpenAPI document\n- Infer missing fields\n- Generate models or code\n- Perform request/response validation at runtime",
"docstring": "# Summary\n\nOpenAPI specification loading and validation utilities.\n\nThis module is responsible for loading an OpenAPI 3.x specification\nfrom disk and validating it before it is used by the application.\n\nIt enforces the principle that an invalid or malformed OpenAPI document\nmust never reach the routing or runtime layers.\n\nNotes:\n **Design Principles:**\n\n - OpenAPI is treated as an authoritative contract.\n - Invalid specifications fail fast at application startup.\n - Supported formats are JSON and YAML.\n - Validation errors are surfaced clearly and early.\n\n **Constraints:**\n\n - This module intentionally does NOT:\n - Modify the OpenAPI document.\n - Infer missing fields.\n - Generate models or code.\n - Perform request/response validation at runtime.",
"objects": {
"json": {
"name": "json",
@@ -44,21 +44,21 @@
"kind": "class",
"path": "openapi_first.loader.OpenAPIFirstError",
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstError', 'openapi_first.errors.OpenAPIFirstError')>",
"docstring": "Base exception for all OpenAPI-first enforcement errors.\n\nThis exception exists to allow callers, test suites, and CI pipelines\nto catch and distinguish OpenAPI contract violations from unrelated\nruntime errors.\n\nAll exceptions raised by the OpenAPI-first core should inherit from\nthis type."
"docstring": "Base exception for all OpenAPI-first enforcement errors.\n\nNotes:\n **Responsibilities:**\n\n - This exception exists to allow callers, test suites, and CI\n pipelines to catch and distinguish OpenAPI contract violations\n from unrelated runtime errors.\n - All exceptions raised by the OpenAPI-first core should inherit\n from this type."
},
"OpenAPISpecLoadError": {
"name": "OpenAPISpecLoadError",
"kind": "class",
"path": "openapi_first.loader.OpenAPISpecLoadError",
"signature": "<bound method Class.signature of Class('OpenAPISpecLoadError', 38, 45)>",
"docstring": "Raised when an OpenAPI specification cannot be loaded or validated.\n\nThis error indicates that the OpenAPI document is unreadable,\nmalformed, or violates the OpenAPI 3.x specification."
"signature": "<bound method Class.signature of Class('OpenAPISpecLoadError', 39, 49)>",
"docstring": "Raised when an OpenAPI specification cannot be loaded or validated.\n\nNotes:\n **Guarantees:**\n\n - This error indicates that the OpenAPI document is unreadable,\n malformed, or violates the OpenAPI 3.x specification."
},
"load_openapi": {
"name": "load_openapi",
"kind": "function",
"path": "openapi_first.loader.load_openapi",
"signature": "<bound method Function.signature of Function('load_openapi', 48, 107)>",
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nThe specification is parsed based on file extension and validated\nusing a strict OpenAPI schema validator. Any error results in an\nimmediate exception, preventing application startup.\n\nParameters\n----------\npath : str or pathlib.Path\n Filesystem path to an OpenAPI specification file.\n Supported extensions:\n - `.json`\n - `.yaml`\n - `.yml`\n\nReturns\n-------\ndict\n Parsed and validated OpenAPI specification.\n\nRaises\n------\nOpenAPISpecLoadError\n If the file does not exist, cannot be parsed, or fails\n OpenAPI schema validation."
"signature": "<bound method Function.signature of Function('load_openapi', 52, 109)>",
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nArgs:\n path (str | Path):\n Filesystem path to an OpenAPI specification file. Supported\n extensions: `.json`, `.yaml`, `.yml`.\n\nReturns:\n dict[str, Any]:\n Parsed and validated OpenAPI specification.\n\nRaises:\n OpenAPISpecLoadError:\n If the file does not exist, cannot be parsed, or fails OpenAPI\n schema validation.\n\nNotes:\n **Guarantees:**\n\n - The specification is parsed based on file extension and validated\n using a strict OpenAPI schema validator.\n - Any error results in an immediate exception, preventing\n application startup."
},
"Dict": {
"name": "Dict",

View File

@@ -67,7 +67,7 @@
"kind": "class",
"path": "openapi_first.templates.crud_app.main.OpenAPIFirstApp",
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstApp', 'openapi_first.app.OpenAPIFirstApp')>",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\n`OpenAPIFirstApp` subclasses FastAPI and replaces manual route\nregistration with OpenAPI-driven binding. All routes are derived\nfrom the provided OpenAPI specification, and each operationId is\nmapped to a Python function in the supplied routes module.\n\nParameters\n----------\nopenapi_path : str\n Filesystem path to the OpenAPI 3.x specification file.\n This specification is treated as the authoritative API contract.\n\nroutes_module : module\n Python module containing handler functions whose names correspond\n exactly to OpenAPI operationId values.\n\n**fastapi_kwargs\n Additional keyword arguments passed directly to `fastapi.FastAPI`\n (e.g., title, version, middleware, lifespan handlers).\n\nRaises\n------\nOpenAPIFirstError\n If the OpenAPI specification is invalid, or if any declared\n operationId does not have a corresponding handler function.\n\nBehavior guarantees\n-------------------\n- No route can exist without an OpenAPI declaration.\n- No OpenAPI operation can exist without a handler.\n- Swagger UI and `/openapi.json` always reflect the provided spec.\n- Handler functions remain framework-agnostic and testable.\n\nExample\n-------\n```python\nfrom openapi_first import OpenAPIFirstApp\nimport app.routes as routes\n\napp = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n)\n```",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
"members": {
"openapi": {
"name": "openapi",
@@ -178,14 +178,14 @@
"kind": "function",
"path": "openapi_first.templates.crud_app.test_crud_app.load_openapi",
"signature": "<bound method Alias.signature of Alias('load_openapi', 'openapi_first.loader.load_openapi')>",
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nThe specification is parsed based on file extension and validated\nusing a strict OpenAPI schema validator. Any error results in an\nimmediate exception, preventing application startup.\n\nParameters\n----------\npath : str or pathlib.Path\n Filesystem path to an OpenAPI specification file.\n Supported extensions:\n - `.json`\n - `.yaml`\n - `.yml`\n\nReturns\n-------\ndict\n Parsed and validated OpenAPI specification.\n\nRaises\n------\nOpenAPISpecLoadError\n If the file does not exist, cannot be parsed, or fails\n OpenAPI schema validation."
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nArgs:\n path (str | Path):\n Filesystem path to an OpenAPI specification file. Supported\n extensions: `.json`, `.yaml`, `.yml`.\n\nReturns:\n dict[str, Any]:\n Parsed and validated OpenAPI specification.\n\nRaises:\n OpenAPISpecLoadError:\n If the file does not exist, cannot be parsed, or fails OpenAPI\n schema validation.\n\nNotes:\n **Guarantees:**\n\n - The specification is parsed based on file extension and validated\n using a strict OpenAPI schema validator.\n - Any error results in an immediate exception, preventing\n application startup."
},
"OpenAPIClient": {
"name": "OpenAPIClient",
"kind": "class",
"path": "openapi_first.templates.crud_app.test_crud_app.OpenAPIClient",
"signature": "<bound method Alias.signature of Alias('OpenAPIClient', 'openapi_first.client.OpenAPIClient')>",
"docstring": "OpenAPI-first HTTP client (httpx-based).\n\nThis client derives all callable methods directly from an OpenAPI 3.x\nspecification. Each operationId becomes a method on the client\ninstance.\n\nDesign principles\n-----------------\n- One callable per operationId\n- Explicit parameters (path, query, headers, body)\n- No implicit schema inference or mutation\n- Returns raw httpx.Response objects\n- No response validation or deserialization\n\nParameters\n----------\nspec : dict\n Parsed OpenAPI 3.x specification.\nbase_url : str | None\n Base URL of the target service. If omitted, the first entry\n in the OpenAPI `servers` list is used.\nclient : httpx.Client | None\n Optional preconfigured httpx client instance.\n\nRaises\n------\nOpenAPIClientError\n If:\n - No servers are defined and base_url is not provided\n - OpenAPI spec has no paths\n - An operation is missing operationId\n - Duplicate operationIds are detected\n - Required path parameters are missing\n - Required request body is missing\n\n Example\n-------\n```python\nfrom openapi_first import loader, client\n\nspec = loader.load_openapi(\"openapi.yaml\")\n\napi = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n)\n\n# Call operationId: getUser\nresponse = api.getUser(\n path_params={\"user_id\": 123}\n)\n\nprint(response.status_code)\nprint(response.json())\n\n# Call operationId: createUser\nresponse = api.createUser(\n body={\"name\": \"Bob\"}\n)\n\nprint(response.status_code)\n```",
"docstring": "OpenAPI-first HTTP client (`httpx`-based).\n\nNotes:\n **Responsibilities:**\n\n - This client derives all callable methods directly from an\n OpenAPI 3.x specification. Each `operationId` becomes a method\n on the client instance.\n\n **Guarantees:**\n\n - One callable per `operationId`.\n - Explicit parameters (path, query, headers, body).\n - No implicit schema inference or mutation.\n - Returns raw `httpx.Response` objects.\n - No response validation or deserialization.\n\nExample:\n ```python\n from openapi_first import loader, client\n\n spec = loader.load_openapi(\"openapi.yaml\")\n\n api = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n )\n\n # Call operationId: getUser\n response = api.getUser(\n path_params={\"user_id\": 123}\n )\n\n print(response.status_code)\n print(response.json())\n ```",
"members": {
"spec": {
"name": "spec",

View File

@@ -9,7 +9,7 @@
"kind": "class",
"path": "openapi_first.templates.crud_app.main.OpenAPIFirstApp",
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstApp', 'openapi_first.app.OpenAPIFirstApp')>",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\n`OpenAPIFirstApp` subclasses FastAPI and replaces manual route\nregistration with OpenAPI-driven binding. All routes are derived\nfrom the provided OpenAPI specification, and each operationId is\nmapped to a Python function in the supplied routes module.\n\nParameters\n----------\nopenapi_path : str\n Filesystem path to the OpenAPI 3.x specification file.\n This specification is treated as the authoritative API contract.\n\nroutes_module : module\n Python module containing handler functions whose names correspond\n exactly to OpenAPI operationId values.\n\n**fastapi_kwargs\n Additional keyword arguments passed directly to `fastapi.FastAPI`\n (e.g., title, version, middleware, lifespan handlers).\n\nRaises\n------\nOpenAPIFirstError\n If the OpenAPI specification is invalid, or if any declared\n operationId does not have a corresponding handler function.\n\nBehavior guarantees\n-------------------\n- No route can exist without an OpenAPI declaration.\n- No OpenAPI operation can exist without a handler.\n- Swagger UI and `/openapi.json` always reflect the provided spec.\n- Handler functions remain framework-agnostic and testable.\n\nExample\n-------\n```python\nfrom openapi_first import OpenAPIFirstApp\nimport app.routes as routes\n\napp = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n)\n```",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
"members": {
"openapi": {
"name": "openapi",

View File

@@ -23,14 +23,14 @@
"kind": "function",
"path": "openapi_first.templates.crud_app.test_crud_app.load_openapi",
"signature": "<bound method Alias.signature of Alias('load_openapi', 'openapi_first.loader.load_openapi')>",
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nThe specification is parsed based on file extension and validated\nusing a strict OpenAPI schema validator. Any error results in an\nimmediate exception, preventing application startup.\n\nParameters\n----------\npath : str or pathlib.Path\n Filesystem path to an OpenAPI specification file.\n Supported extensions:\n - `.json`\n - `.yaml`\n - `.yml`\n\nReturns\n-------\ndict\n Parsed and validated OpenAPI specification.\n\nRaises\n------\nOpenAPISpecLoadError\n If the file does not exist, cannot be parsed, or fails\n OpenAPI schema validation."
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nArgs:\n path (str | Path):\n Filesystem path to an OpenAPI specification file. Supported\n extensions: `.json`, `.yaml`, `.yml`.\n\nReturns:\n dict[str, Any]:\n Parsed and validated OpenAPI specification.\n\nRaises:\n OpenAPISpecLoadError:\n If the file does not exist, cannot be parsed, or fails OpenAPI\n schema validation.\n\nNotes:\n **Guarantees:**\n\n - The specification is parsed based on file extension and validated\n using a strict OpenAPI schema validator.\n - Any error results in an immediate exception, preventing\n application startup."
},
"OpenAPIClient": {
"name": "OpenAPIClient",
"kind": "class",
"path": "openapi_first.templates.crud_app.test_crud_app.OpenAPIClient",
"signature": "<bound method Alias.signature of Alias('OpenAPIClient', 'openapi_first.client.OpenAPIClient')>",
"docstring": "OpenAPI-first HTTP client (httpx-based).\n\nThis client derives all callable methods directly from an OpenAPI 3.x\nspecification. Each operationId becomes a method on the client\ninstance.\n\nDesign principles\n-----------------\n- One callable per operationId\n- Explicit parameters (path, query, headers, body)\n- No implicit schema inference or mutation\n- Returns raw httpx.Response objects\n- No response validation or deserialization\n\nParameters\n----------\nspec : dict\n Parsed OpenAPI 3.x specification.\nbase_url : str | None\n Base URL of the target service. If omitted, the first entry\n in the OpenAPI `servers` list is used.\nclient : httpx.Client | None\n Optional preconfigured httpx client instance.\n\nRaises\n------\nOpenAPIClientError\n If:\n - No servers are defined and base_url is not provided\n - OpenAPI spec has no paths\n - An operation is missing operationId\n - Duplicate operationIds are detected\n - Required path parameters are missing\n - Required request body is missing\n\n Example\n-------\n```python\nfrom openapi_first import loader, client\n\nspec = loader.load_openapi(\"openapi.yaml\")\n\napi = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n)\n\n# Call operationId: getUser\nresponse = api.getUser(\n path_params={\"user_id\": 123}\n)\n\nprint(response.status_code)\nprint(response.json())\n\n# Call operationId: createUser\nresponse = api.createUser(\n body={\"name\": \"Bob\"}\n)\n\nprint(response.status_code)\n```",
"docstring": "OpenAPI-first HTTP client (`httpx`-based).\n\nNotes:\n **Responsibilities:**\n\n - This client derives all callable methods directly from an\n OpenAPI 3.x specification. Each `operationId` becomes a method\n on the client instance.\n\n **Guarantees:**\n\n - One callable per `operationId`.\n - Explicit parameters (path, query, headers, body).\n - No implicit schema inference or mutation.\n - Returns raw `httpx.Response` objects.\n - No response validation or deserialization.\n\nExample:\n ```python\n from openapi_first import loader, client\n\n spec = loader.load_openapi(\"openapi.yaml\")\n\n api = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n )\n\n # Call operationId: getUser\n response = api.getUser(\n path_params={\"user_id\": 123}\n )\n\n print(response.status_code)\n print(response.json())\n ```",
"members": {
"spec": {
"name": "spec",

View File

@@ -16,7 +16,7 @@
"kind": "class",
"path": "openapi_first.templates.health_app.main.OpenAPIFirstApp",
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstApp', 'openapi_first.app.OpenAPIFirstApp')>",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\n`OpenAPIFirstApp` subclasses FastAPI and replaces manual route\nregistration with OpenAPI-driven binding. All routes are derived\nfrom the provided OpenAPI specification, and each operationId is\nmapped to a Python function in the supplied routes module.\n\nParameters\n----------\nopenapi_path : str\n Filesystem path to the OpenAPI 3.x specification file.\n This specification is treated as the authoritative API contract.\n\nroutes_module : module\n Python module containing handler functions whose names correspond\n exactly to OpenAPI operationId values.\n\n**fastapi_kwargs\n Additional keyword arguments passed directly to `fastapi.FastAPI`\n (e.g., title, version, middleware, lifespan handlers).\n\nRaises\n------\nOpenAPIFirstError\n If the OpenAPI specification is invalid, or if any declared\n operationId does not have a corresponding handler function.\n\nBehavior guarantees\n-------------------\n- No route can exist without an OpenAPI declaration.\n- No OpenAPI operation can exist without a handler.\n- Swagger UI and `/openapi.json` always reflect the provided spec.\n- Handler functions remain framework-agnostic and testable.\n\nExample\n-------\n```python\nfrom openapi_first import OpenAPIFirstApp\nimport app.routes as routes\n\napp = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n)\n```",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
"members": {
"openapi": {
"name": "openapi",

View File

@@ -9,7 +9,7 @@
"kind": "class",
"path": "openapi_first.templates.health_app.main.OpenAPIFirstApp",
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstApp', 'openapi_first.app.OpenAPIFirstApp')>",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\n`OpenAPIFirstApp` subclasses FastAPI and replaces manual route\nregistration with OpenAPI-driven binding. All routes are derived\nfrom the provided OpenAPI specification, and each operationId is\nmapped to a Python function in the supplied routes module.\n\nParameters\n----------\nopenapi_path : str\n Filesystem path to the OpenAPI 3.x specification file.\n This specification is treated as the authoritative API contract.\n\nroutes_module : module\n Python module containing handler functions whose names correspond\n exactly to OpenAPI operationId values.\n\n**fastapi_kwargs\n Additional keyword arguments passed directly to `fastapi.FastAPI`\n (e.g., title, version, middleware, lifespan handlers).\n\nRaises\n------\nOpenAPIFirstError\n If the OpenAPI specification is invalid, or if any declared\n operationId does not have a corresponding handler function.\n\nBehavior guarantees\n-------------------\n- No route can exist without an OpenAPI declaration.\n- No OpenAPI operation can exist without a handler.\n- Swagger UI and `/openapi.json` always reflect the provided spec.\n- Handler functions remain framework-agnostic and testable.\n\nExample\n-------\n```python\nfrom openapi_first import OpenAPIFirstApp\nimport app.routes as routes\n\napp = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n)\n```",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
"members": {
"openapi": {
"name": "openapi",

View File

@@ -74,7 +74,7 @@
"kind": "class",
"path": "openapi_first.templates.crud_app.main.OpenAPIFirstApp",
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstApp', 'openapi_first.app.OpenAPIFirstApp')>",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\n`OpenAPIFirstApp` subclasses FastAPI and replaces manual route\nregistration with OpenAPI-driven binding. All routes are derived\nfrom the provided OpenAPI specification, and each operationId is\nmapped to a Python function in the supplied routes module.\n\nParameters\n----------\nopenapi_path : str\n Filesystem path to the OpenAPI 3.x specification file.\n This specification is treated as the authoritative API contract.\n\nroutes_module : module\n Python module containing handler functions whose names correspond\n exactly to OpenAPI operationId values.\n\n**fastapi_kwargs\n Additional keyword arguments passed directly to `fastapi.FastAPI`\n (e.g., title, version, middleware, lifespan handlers).\n\nRaises\n------\nOpenAPIFirstError\n If the OpenAPI specification is invalid, or if any declared\n operationId does not have a corresponding handler function.\n\nBehavior guarantees\n-------------------\n- No route can exist without an OpenAPI declaration.\n- No OpenAPI operation can exist without a handler.\n- Swagger UI and `/openapi.json` always reflect the provided spec.\n- Handler functions remain framework-agnostic and testable.\n\nExample\n-------\n```python\nfrom openapi_first import OpenAPIFirstApp\nimport app.routes as routes\n\napp = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n)\n```",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
"members": {
"openapi": {
"name": "openapi",
@@ -185,14 +185,14 @@
"kind": "function",
"path": "openapi_first.templates.crud_app.test_crud_app.load_openapi",
"signature": "<bound method Alias.signature of Alias('load_openapi', 'openapi_first.loader.load_openapi')>",
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nThe specification is parsed based on file extension and validated\nusing a strict OpenAPI schema validator. Any error results in an\nimmediate exception, preventing application startup.\n\nParameters\n----------\npath : str or pathlib.Path\n Filesystem path to an OpenAPI specification file.\n Supported extensions:\n - `.json`\n - `.yaml`\n - `.yml`\n\nReturns\n-------\ndict\n Parsed and validated OpenAPI specification.\n\nRaises\n------\nOpenAPISpecLoadError\n If the file does not exist, cannot be parsed, or fails\n OpenAPI schema validation."
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nArgs:\n path (str | Path):\n Filesystem path to an OpenAPI specification file. Supported\n extensions: `.json`, `.yaml`, `.yml`.\n\nReturns:\n dict[str, Any]:\n Parsed and validated OpenAPI specification.\n\nRaises:\n OpenAPISpecLoadError:\n If the file does not exist, cannot be parsed, or fails OpenAPI\n schema validation.\n\nNotes:\n **Guarantees:**\n\n - The specification is parsed based on file extension and validated\n using a strict OpenAPI schema validator.\n - Any error results in an immediate exception, preventing\n application startup."
},
"OpenAPIClient": {
"name": "OpenAPIClient",
"kind": "class",
"path": "openapi_first.templates.crud_app.test_crud_app.OpenAPIClient",
"signature": "<bound method Alias.signature of Alias('OpenAPIClient', 'openapi_first.client.OpenAPIClient')>",
"docstring": "OpenAPI-first HTTP client (httpx-based).\n\nThis client derives all callable methods directly from an OpenAPI 3.x\nspecification. Each operationId becomes a method on the client\ninstance.\n\nDesign principles\n-----------------\n- One callable per operationId\n- Explicit parameters (path, query, headers, body)\n- No implicit schema inference or mutation\n- Returns raw httpx.Response objects\n- No response validation or deserialization\n\nParameters\n----------\nspec : dict\n Parsed OpenAPI 3.x specification.\nbase_url : str | None\n Base URL of the target service. If omitted, the first entry\n in the OpenAPI `servers` list is used.\nclient : httpx.Client | None\n Optional preconfigured httpx client instance.\n\nRaises\n------\nOpenAPIClientError\n If:\n - No servers are defined and base_url is not provided\n - OpenAPI spec has no paths\n - An operation is missing operationId\n - Duplicate operationIds are detected\n - Required path parameters are missing\n - Required request body is missing\n\n Example\n-------\n```python\nfrom openapi_first import loader, client\n\nspec = loader.load_openapi(\"openapi.yaml\")\n\napi = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n)\n\n# Call operationId: getUser\nresponse = api.getUser(\n path_params={\"user_id\": 123}\n)\n\nprint(response.status_code)\nprint(response.json())\n\n# Call operationId: createUser\nresponse = api.createUser(\n body={\"name\": \"Bob\"}\n)\n\nprint(response.status_code)\n```",
"docstring": "OpenAPI-first HTTP client (`httpx`-based).\n\nNotes:\n **Responsibilities:**\n\n - This client derives all callable methods directly from an\n OpenAPI 3.x specification. Each `operationId` becomes a method\n on the client instance.\n\n **Guarantees:**\n\n - One callable per `operationId`.\n - Explicit parameters (path, query, headers, body).\n - No implicit schema inference or mutation.\n - Returns raw `httpx.Response` objects.\n - No response validation or deserialization.\n\nExample:\n ```python\n from openapi_first import loader, client\n\n spec = loader.load_openapi(\"openapi.yaml\")\n\n api = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n )\n\n # Call operationId: getUser\n response = api.getUser(\n path_params={\"user_id\": 123}\n )\n\n print(response.status_code)\n print(response.json())\n ```",
"members": {
"spec": {
"name": "spec",
@@ -296,7 +296,7 @@
"kind": "class",
"path": "openapi_first.templates.health_app.main.OpenAPIFirstApp",
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstApp', 'openapi_first.app.OpenAPIFirstApp')>",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\n`OpenAPIFirstApp` subclasses FastAPI and replaces manual route\nregistration with OpenAPI-driven binding. All routes are derived\nfrom the provided OpenAPI specification, and each operationId is\nmapped to a Python function in the supplied routes module.\n\nParameters\n----------\nopenapi_path : str\n Filesystem path to the OpenAPI 3.x specification file.\n This specification is treated as the authoritative API contract.\n\nroutes_module : module\n Python module containing handler functions whose names correspond\n exactly to OpenAPI operationId values.\n\n**fastapi_kwargs\n Additional keyword arguments passed directly to `fastapi.FastAPI`\n (e.g., title, version, middleware, lifespan handlers).\n\nRaises\n------\nOpenAPIFirstError\n If the OpenAPI specification is invalid, or if any declared\n operationId does not have a corresponding handler function.\n\nBehavior guarantees\n-------------------\n- No route can exist without an OpenAPI declaration.\n- No OpenAPI operation can exist without a handler.\n- Swagger UI and `/openapi.json` always reflect the provided spec.\n- Handler functions remain framework-agnostic and testable.\n\nExample\n-------\n```python\nfrom openapi_first import OpenAPIFirstApp\nimport app.routes as routes\n\napp = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n)\n```",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
"members": {
"openapi": {
"name": "openapi",
@@ -425,7 +425,7 @@
"kind": "class",
"path": "openapi_first.templates.model_app.main.OpenAPIFirstApp",
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstApp', 'openapi_first.app.OpenAPIFirstApp')>",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\n`OpenAPIFirstApp` subclasses FastAPI and replaces manual route\nregistration with OpenAPI-driven binding. All routes are derived\nfrom the provided OpenAPI specification, and each operationId is\nmapped to a Python function in the supplied routes module.\n\nParameters\n----------\nopenapi_path : str\n Filesystem path to the OpenAPI 3.x specification file.\n This specification is treated as the authoritative API contract.\n\nroutes_module : module\n Python module containing handler functions whose names correspond\n exactly to OpenAPI operationId values.\n\n**fastapi_kwargs\n Additional keyword arguments passed directly to `fastapi.FastAPI`\n (e.g., title, version, middleware, lifespan handlers).\n\nRaises\n------\nOpenAPIFirstError\n If the OpenAPI specification is invalid, or if any declared\n operationId does not have a corresponding handler function.\n\nBehavior guarantees\n-------------------\n- No route can exist without an OpenAPI declaration.\n- No OpenAPI operation can exist without a handler.\n- Swagger UI and `/openapi.json` always reflect the provided spec.\n- Handler functions remain framework-agnostic and testable.\n\nExample\n-------\n```python\nfrom openapi_first import OpenAPIFirstApp\nimport app.routes as routes\n\napp = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n)\n```",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
"members": {
"openapi": {
"name": "openapi",
@@ -605,14 +605,14 @@
"kind": "function",
"path": "openapi_first.templates.model_app.test_model_app.load_openapi",
"signature": "<bound method Alias.signature of Alias('load_openapi', 'openapi_first.loader.load_openapi')>",
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nThe specification is parsed based on file extension and validated\nusing a strict OpenAPI schema validator. Any error results in an\nimmediate exception, preventing application startup.\n\nParameters\n----------\npath : str or pathlib.Path\n Filesystem path to an OpenAPI specification file.\n Supported extensions:\n - `.json`\n - `.yaml`\n - `.yml`\n\nReturns\n-------\ndict\n Parsed and validated OpenAPI specification.\n\nRaises\n------\nOpenAPISpecLoadError\n If the file does not exist, cannot be parsed, or fails\n OpenAPI schema validation."
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nArgs:\n path (str | Path):\n Filesystem path to an OpenAPI specification file. Supported\n extensions: `.json`, `.yaml`, `.yml`.\n\nReturns:\n dict[str, Any]:\n Parsed and validated OpenAPI specification.\n\nRaises:\n OpenAPISpecLoadError:\n If the file does not exist, cannot be parsed, or fails OpenAPI\n schema validation.\n\nNotes:\n **Guarantees:**\n\n - The specification is parsed based on file extension and validated\n using a strict OpenAPI schema validator.\n - Any error results in an immediate exception, preventing\n application startup."
},
"OpenAPIClient": {
"name": "OpenAPIClient",
"kind": "class",
"path": "openapi_first.templates.model_app.test_model_app.OpenAPIClient",
"signature": "<bound method Alias.signature of Alias('OpenAPIClient', 'openapi_first.client.OpenAPIClient')>",
"docstring": "OpenAPI-first HTTP client (httpx-based).\n\nThis client derives all callable methods directly from an OpenAPI 3.x\nspecification. Each operationId becomes a method on the client\ninstance.\n\nDesign principles\n-----------------\n- One callable per operationId\n- Explicit parameters (path, query, headers, body)\n- No implicit schema inference or mutation\n- Returns raw httpx.Response objects\n- No response validation or deserialization\n\nParameters\n----------\nspec : dict\n Parsed OpenAPI 3.x specification.\nbase_url : str | None\n Base URL of the target service. If omitted, the first entry\n in the OpenAPI `servers` list is used.\nclient : httpx.Client | None\n Optional preconfigured httpx client instance.\n\nRaises\n------\nOpenAPIClientError\n If:\n - No servers are defined and base_url is not provided\n - OpenAPI spec has no paths\n - An operation is missing operationId\n - Duplicate operationIds are detected\n - Required path parameters are missing\n - Required request body is missing\n\n Example\n-------\n```python\nfrom openapi_first import loader, client\n\nspec = loader.load_openapi(\"openapi.yaml\")\n\napi = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n)\n\n# Call operationId: getUser\nresponse = api.getUser(\n path_params={\"user_id\": 123}\n)\n\nprint(response.status_code)\nprint(response.json())\n\n# Call operationId: createUser\nresponse = api.createUser(\n body={\"name\": \"Bob\"}\n)\n\nprint(response.status_code)\n```",
"docstring": "OpenAPI-first HTTP client (`httpx`-based).\n\nNotes:\n **Responsibilities:**\n\n - This client derives all callable methods directly from an\n OpenAPI 3.x specification. Each `operationId` becomes a method\n on the client instance.\n\n **Guarantees:**\n\n - One callable per `operationId`.\n - Explicit parameters (path, query, headers, body).\n - No implicit schema inference or mutation.\n - Returns raw `httpx.Response` objects.\n - No response validation or deserialization.\n\nExample:\n ```python\n from openapi_first import loader, client\n\n spec = loader.load_openapi(\"openapi.yaml\")\n\n api = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n )\n\n # Call operationId: getUser\n response = api.getUser(\n path_params={\"user_id\": 123}\n )\n\n print(response.status_code)\n print(response.json())\n ```",
"members": {
"spec": {
"name": "spec",

View File

@@ -81,7 +81,7 @@
"kind": "class",
"path": "openapi_first.templates.model_app.main.OpenAPIFirstApp",
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstApp', 'openapi_first.app.OpenAPIFirstApp')>",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\n`OpenAPIFirstApp` subclasses FastAPI and replaces manual route\nregistration with OpenAPI-driven binding. All routes are derived\nfrom the provided OpenAPI specification, and each operationId is\nmapped to a Python function in the supplied routes module.\n\nParameters\n----------\nopenapi_path : str\n Filesystem path to the OpenAPI 3.x specification file.\n This specification is treated as the authoritative API contract.\n\nroutes_module : module\n Python module containing handler functions whose names correspond\n exactly to OpenAPI operationId values.\n\n**fastapi_kwargs\n Additional keyword arguments passed directly to `fastapi.FastAPI`\n (e.g., title, version, middleware, lifespan handlers).\n\nRaises\n------\nOpenAPIFirstError\n If the OpenAPI specification is invalid, or if any declared\n operationId does not have a corresponding handler function.\n\nBehavior guarantees\n-------------------\n- No route can exist without an OpenAPI declaration.\n- No OpenAPI operation can exist without a handler.\n- Swagger UI and `/openapi.json` always reflect the provided spec.\n- Handler functions remain framework-agnostic and testable.\n\nExample\n-------\n```python\nfrom openapi_first import OpenAPIFirstApp\nimport app.routes as routes\n\napp = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n)\n```",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
"members": {
"openapi": {
"name": "openapi",
@@ -261,14 +261,14 @@
"kind": "function",
"path": "openapi_first.templates.model_app.test_model_app.load_openapi",
"signature": "<bound method Alias.signature of Alias('load_openapi', 'openapi_first.loader.load_openapi')>",
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nThe specification is parsed based on file extension and validated\nusing a strict OpenAPI schema validator. Any error results in an\nimmediate exception, preventing application startup.\n\nParameters\n----------\npath : str or pathlib.Path\n Filesystem path to an OpenAPI specification file.\n Supported extensions:\n - `.json`\n - `.yaml`\n - `.yml`\n\nReturns\n-------\ndict\n Parsed and validated OpenAPI specification.\n\nRaises\n------\nOpenAPISpecLoadError\n If the file does not exist, cannot be parsed, or fails\n OpenAPI schema validation."
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nArgs:\n path (str | Path):\n Filesystem path to an OpenAPI specification file. Supported\n extensions: `.json`, `.yaml`, `.yml`.\n\nReturns:\n dict[str, Any]:\n Parsed and validated OpenAPI specification.\n\nRaises:\n OpenAPISpecLoadError:\n If the file does not exist, cannot be parsed, or fails OpenAPI\n schema validation.\n\nNotes:\n **Guarantees:**\n\n - The specification is parsed based on file extension and validated\n using a strict OpenAPI schema validator.\n - Any error results in an immediate exception, preventing\n application startup."
},
"OpenAPIClient": {
"name": "OpenAPIClient",
"kind": "class",
"path": "openapi_first.templates.model_app.test_model_app.OpenAPIClient",
"signature": "<bound method Alias.signature of Alias('OpenAPIClient', 'openapi_first.client.OpenAPIClient')>",
"docstring": "OpenAPI-first HTTP client (httpx-based).\n\nThis client derives all callable methods directly from an OpenAPI 3.x\nspecification. Each operationId becomes a method on the client\ninstance.\n\nDesign principles\n-----------------\n- One callable per operationId\n- Explicit parameters (path, query, headers, body)\n- No implicit schema inference or mutation\n- Returns raw httpx.Response objects\n- No response validation or deserialization\n\nParameters\n----------\nspec : dict\n Parsed OpenAPI 3.x specification.\nbase_url : str | None\n Base URL of the target service. If omitted, the first entry\n in the OpenAPI `servers` list is used.\nclient : httpx.Client | None\n Optional preconfigured httpx client instance.\n\nRaises\n------\nOpenAPIClientError\n If:\n - No servers are defined and base_url is not provided\n - OpenAPI spec has no paths\n - An operation is missing operationId\n - Duplicate operationIds are detected\n - Required path parameters are missing\n - Required request body is missing\n\n Example\n-------\n```python\nfrom openapi_first import loader, client\n\nspec = loader.load_openapi(\"openapi.yaml\")\n\napi = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n)\n\n# Call operationId: getUser\nresponse = api.getUser(\n path_params={\"user_id\": 123}\n)\n\nprint(response.status_code)\nprint(response.json())\n\n# Call operationId: createUser\nresponse = api.createUser(\n body={\"name\": \"Bob\"}\n)\n\nprint(response.status_code)\n```",
"docstring": "OpenAPI-first HTTP client (`httpx`-based).\n\nNotes:\n **Responsibilities:**\n\n - This client derives all callable methods directly from an\n OpenAPI 3.x specification. Each `operationId` becomes a method\n on the client instance.\n\n **Guarantees:**\n\n - One callable per `operationId`.\n - Explicit parameters (path, query, headers, body).\n - No implicit schema inference or mutation.\n - Returns raw `httpx.Response` objects.\n - No response validation or deserialization.\n\nExample:\n ```python\n from openapi_first import loader, client\n\n spec = loader.load_openapi(\"openapi.yaml\")\n\n api = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n )\n\n # Call operationId: getUser\n response = api.getUser(\n path_params={\"user_id\": 123}\n )\n\n print(response.status_code)\n print(response.json())\n ```",
"members": {
"spec": {
"name": "spec",

View File

@@ -9,7 +9,7 @@
"kind": "class",
"path": "openapi_first.templates.model_app.main.OpenAPIFirstApp",
"signature": "<bound method Alias.signature of Alias('OpenAPIFirstApp', 'openapi_first.app.OpenAPIFirstApp')>",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\n`OpenAPIFirstApp` subclasses FastAPI and replaces manual route\nregistration with OpenAPI-driven binding. All routes are derived\nfrom the provided OpenAPI specification, and each operationId is\nmapped to a Python function in the supplied routes module.\n\nParameters\n----------\nopenapi_path : str\n Filesystem path to the OpenAPI 3.x specification file.\n This specification is treated as the authoritative API contract.\n\nroutes_module : module\n Python module containing handler functions whose names correspond\n exactly to OpenAPI operationId values.\n\n**fastapi_kwargs\n Additional keyword arguments passed directly to `fastapi.FastAPI`\n (e.g., title, version, middleware, lifespan handlers).\n\nRaises\n------\nOpenAPIFirstError\n If the OpenAPI specification is invalid, or if any declared\n operationId does not have a corresponding handler function.\n\nBehavior guarantees\n-------------------\n- No route can exist without an OpenAPI declaration.\n- No OpenAPI operation can exist without a handler.\n- Swagger UI and `/openapi.json` always reflect the provided spec.\n- Handler functions remain framework-agnostic and testable.\n\nExample\n-------\n```python\nfrom openapi_first import OpenAPIFirstApp\nimport app.routes as routes\n\napp = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n)\n```",
"docstring": "FastAPI application enforcing OpenAPI-first design.\n\nNotes:\n **Responsibilities:**\n\n - `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route\n registration with OpenAPI-driven binding.\n - All routes are derived from the provided OpenAPI specification,\n and each `operationId` is mapped to a Python function in the\n supplied routes module.\n\n **Guarantees:**\n\n - No route can exist without an OpenAPI declaration.\n - No OpenAPI operation can exist without a handler.\n - Swagger UI and `/openapi.json` always reflect the provided spec.\n - Handler functions remain framework-agnostic and testable.\n\nExample:\n ```python\n from openapi_first import OpenAPIFirstApp\n import app.routes as routes\n\n app = OpenAPIFirstApp(\n openapi_path=\"app/openapi.json\",\n routes_module=routes,\n title=\"Example Service\",\n )\n ```",
"members": {
"openapi": {
"name": "openapi",

View File

@@ -23,14 +23,14 @@
"kind": "function",
"path": "openapi_first.templates.model_app.test_model_app.load_openapi",
"signature": "<bound method Alias.signature of Alias('load_openapi', 'openapi_first.loader.load_openapi')>",
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nThe specification is parsed based on file extension and validated\nusing a strict OpenAPI schema validator. Any error results in an\nimmediate exception, preventing application startup.\n\nParameters\n----------\npath : str or pathlib.Path\n Filesystem path to an OpenAPI specification file.\n Supported extensions:\n - `.json`\n - `.yaml`\n - `.yml`\n\nReturns\n-------\ndict\n Parsed and validated OpenAPI specification.\n\nRaises\n------\nOpenAPISpecLoadError\n If the file does not exist, cannot be parsed, or fails\n OpenAPI schema validation."
"docstring": "Load and validate an OpenAPI 3.x specification from disk.\n\nArgs:\n path (str | Path):\n Filesystem path to an OpenAPI specification file. Supported\n extensions: `.json`, `.yaml`, `.yml`.\n\nReturns:\n dict[str, Any]:\n Parsed and validated OpenAPI specification.\n\nRaises:\n OpenAPISpecLoadError:\n If the file does not exist, cannot be parsed, or fails OpenAPI\n schema validation.\n\nNotes:\n **Guarantees:**\n\n - The specification is parsed based on file extension and validated\n using a strict OpenAPI schema validator.\n - Any error results in an immediate exception, preventing\n application startup."
},
"OpenAPIClient": {
"name": "OpenAPIClient",
"kind": "class",
"path": "openapi_first.templates.model_app.test_model_app.OpenAPIClient",
"signature": "<bound method Alias.signature of Alias('OpenAPIClient', 'openapi_first.client.OpenAPIClient')>",
"docstring": "OpenAPI-first HTTP client (httpx-based).\n\nThis client derives all callable methods directly from an OpenAPI 3.x\nspecification. Each operationId becomes a method on the client\ninstance.\n\nDesign principles\n-----------------\n- One callable per operationId\n- Explicit parameters (path, query, headers, body)\n- No implicit schema inference or mutation\n- Returns raw httpx.Response objects\n- No response validation or deserialization\n\nParameters\n----------\nspec : dict\n Parsed OpenAPI 3.x specification.\nbase_url : str | None\n Base URL of the target service. If omitted, the first entry\n in the OpenAPI `servers` list is used.\nclient : httpx.Client | None\n Optional preconfigured httpx client instance.\n\nRaises\n------\nOpenAPIClientError\n If:\n - No servers are defined and base_url is not provided\n - OpenAPI spec has no paths\n - An operation is missing operationId\n - Duplicate operationIds are detected\n - Required path parameters are missing\n - Required request body is missing\n\n Example\n-------\n```python\nfrom openapi_first import loader, client\n\nspec = loader.load_openapi(\"openapi.yaml\")\n\napi = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n)\n\n# Call operationId: getUser\nresponse = api.getUser(\n path_params={\"user_id\": 123}\n)\n\nprint(response.status_code)\nprint(response.json())\n\n# Call operationId: createUser\nresponse = api.createUser(\n body={\"name\": \"Bob\"}\n)\n\nprint(response.status_code)\n```",
"docstring": "OpenAPI-first HTTP client (`httpx`-based).\n\nNotes:\n **Responsibilities:**\n\n - This client derives all callable methods directly from an\n OpenAPI 3.x specification. Each `operationId` becomes a method\n on the client instance.\n\n **Guarantees:**\n\n - One callable per `operationId`.\n - Explicit parameters (path, query, headers, body).\n - No implicit schema inference or mutation.\n - Returns raw `httpx.Response` objects.\n - No response validation or deserialization.\n\nExample:\n ```python\n from openapi_first import loader, client\n\n spec = loader.load_openapi(\"openapi.yaml\")\n\n api = client.OpenAPIClient(\n spec=spec,\n base_url=\"http://localhost:8000\",\n )\n\n # Call operationId: getUser\n response = api.getUser(\n path_params={\"user_id\": 123}\n )\n\n print(response.status_code)\n print(response.json())\n ```",
"members": {
"spec": {
"name": "spec",

View File

@@ -8,12 +8,19 @@ theme:
text: Inter
code: JetBrains Mono
features:
- navigation.tabs
- navigation.sections
- navigation.expand
- navigation.top
- navigation.instant
- navigation.tracking
- navigation.indexes
- content.code.copy
- content.code.annotate
- content.tabs.link
- content.action.edit
- search.highlight
- search.share
- search.suggest
plugins:
- search
- mkdocstrings:
@@ -31,6 +38,30 @@ plugins:
annotations_path: brief
show_root_heading: true
group_by_category: true
show_category_heading: true
show_object_full_path: false
show_symbol_type_heading: true
markdown_extensions:
- pymdownx.superfences
- pymdownx.inlinehilite
- pymdownx.snippets
- admonition
- pymdownx.details
- pymdownx.superfences
- pymdownx.highlight:
linenums: true
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.tabbed:
alternate_style: true
- pymdownx.tasklist:
custom_checkbox: true
- tables
- footnotes
- pymdownx.caret
- pymdownx.tilde
- pymdownx.mark
site_name: openapi_first
nav:
- Home: index.md

View File

@@ -1,4 +1,6 @@
"""
# Summary
FastAPI OpenAPI First — strict OpenAPI-first application bootstrap for FastAPI.
FastAPI OpenAPI First is a **contract-first infrastructure library** that
@@ -7,225 +9,90 @@ enforces OpenAPI as the single source of truth for FastAPI services.
The library removes decorator-driven routing and replaces it with
deterministic, spec-driven application assembly. Every HTTP route,
method, and operation is defined in OpenAPI first and bound to Python
handlers explicitly via operationId.
handlers explicitly via `operationId`.
The package is intentionally minimal and layered. Each module has a
single responsibility and exposes explicit contracts rather than
convenience facades.
---
----------------------------------------------------------------------
Architecture Overview
----------------------------------------------------------------------
The library is structured around four core responsibilities:
- loader: load and validate OpenAPI 3.x specifications (JSON/YAML)
- binder: bind OpenAPI operations to FastAPI routes via operationId
- app: OpenAPI-first FastAPI application bootstrap
- client: OpenAPI-first HTTP client driven by the same specification
- errors: explicit error hierarchy for contract violations
The package root acts as a **namespace**, not a facade. Consumers are
expected to import functionality explicitly from the appropriate module.
----------------------------------------------------------------------
Installation
----------------------------------------------------------------------
# Installation
Install using pip:
pip install openapi-first
```bash
pip install openapi-first
```
Or with Poetry:
poetry add openapi-first
```bash
poetry add openapi-first
```
Runtime dependencies are intentionally minimal:
- fastapi (server-side)
- httpx (client-side)
- openapi-spec-validator
- pyyaml (optional, for YAML specs)
---
The ASGI server (e.g., uvicorn) is an application-level dependency and is
not bundled with this library.
----------------------------------------------------------------------
Command-Line Interface (Scaffolding, Templates)
----------------------------------------------------------------------
FastAPI OpenAPI First ships with a small CLI for bootstrapping
OpenAPI-first FastAPI applications from bundled templates.
List available application templates:
openapi-first --list
Create a new application using the default template:
openapi-first
Create a new application using a specific template:
openapi-first health_app
Create a new application in a custom directory:
openapi-first health_app my-service
The CLI copies template files verbatim into the target directory.
No code is generated or modified beyond the copied scaffold.
----------------------------------------------------------------------
Server-Side Usage (OpenAPI → FastAPI)
----------------------------------------------------------------------
# Quick Start
Minimal OpenAPI-first FastAPI application:
from openapi_first import app
import my_service.routes as routes
```python
from openapi_first import app
import my_service.routes as routes
api = app.OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=routes,
title="My Service",
version="1.0.0",
)
api = app.OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=routes,
title="My Service",
version="1.0.0",
)
```
# Run with:
# uvicorn my_service.main:api
OperationId-driven HTTP client:
Handler definitions (no decorators):
```python
from openapi_first.loader import load_openapi
from openapi_first.client import OpenAPIClient
def get_health():
return {"status": "ok"}
spec = load_openapi("openapi.yaml")
client = OpenAPIClient(spec)
OpenAPI snippet:
response = client.get_health()
```
paths:
/health:
get:
operationId: get_health
responses:
"200":
description: OK
---
The binder guarantees:
- Every OpenAPI operationId has exactly one handler
- No undocumented routes exist
- All mismatches fail at application startup
# Architecture
----------------------------------------------------------------------
Client-Side Usage (OpenAPI → HTTP Client)
----------------------------------------------------------------------
The library is structured around four core responsibilities:
The same OpenAPI specification can be used to construct a strict,
operationId-driven HTTP client.
- `loader`: Load and validate OpenAPI 3.x specifications (JSON/YAML).
- `binder`: Bind OpenAPI operations to FastAPI routes via `operationId`.
- `app`: OpenAPI-first FastAPI application bootstrap.
- `client`: OpenAPI-first HTTP client driven by the same specification.
- `errors`: Explicit error hierarchy for contract violations.
Client construction:
---
from openapi_first.loader import load_openapi
from openapi_first.client import OpenAPIClient
spec = load_openapi("openapi.yaml")
client = OpenAPIClient(spec)
Calling operations (operationId is the API):
response = client.get_health()
assert response.status_code == 200
assert response.json() == {"status": "ok"}
Path parameters must match the OpenAPI specification exactly:
response = client.get_item(
path_params={"item_id": 1}
)
Request bodies are passed explicitly:
response = client.create_item(
body={"name": "Orange", "price": 0.8}
)
Client guarantees:
- One callable per OpenAPI operationId
- No hardcoded URLs or HTTP methods in user code
- Path and body parameters must match the spec exactly
- Invalid or incomplete OpenAPI specs fail at client construction time
- No schema inference or mutation is performed
The client is transport-level only and returns `httpx.Response`
objects directly. Response interpretation and validation are left to
the consumer or higher-level layers.
----------------------------------------------------------------------
Extensibility Model
----------------------------------------------------------------------
FastAPI OpenAPI First is designed to be extended via **explicit contracts**:
- Users MAY extend OpenAPI loading behavior (e.g. multi-file specs)
by wrapping or replacing `loader.load_openapi`
- Users MAY extend route binding behavior by building on top of
`binder.bind_routes`
- Users MAY layer additional validation (e.g. signature checks)
without modifying core modules
Users SHOULD NOT rely on FastAPI decorators for routing when using this
library. Mixing decorator-driven routes with OpenAPI-first routing
defeats the contract guarantees and is explicitly unsupported.
----------------------------------------------------------------------
Public API Surface
----------------------------------------------------------------------
# Public API
The supported public API consists of the following top-level modules:
- openapi_first.app
- openapi_first.binder
- openapi_first.loader
- openapi_first.client
- openapi_first.errors
- `openapi_first.app`
- `openapi_first.binder`
- `openapi_first.loader`
- `openapi_first.client`
- `openapi_first.errors`
Classes and functions should be imported explicitly from these modules.
No individual symbols are re-exported at the package root.
---
----------------------------------------------------------------------
Design Guarantees
----------------------------------------------------------------------
# Design Guarantees
- OpenAPI is the single source of truth
- No undocumented routes can exist
- No OpenAPI operation can exist without a handler or client callable
- All contract violations fail at application startup or client creation
- No hidden FastAPI magic or implicit behavior
- Deterministic, testable application assembly
- CI-friendly failure modes
- OpenAPI is the single source of truth.
- No undocumented routes can exist.
- No OpenAPI operation can exist without a handler or client callable.
- All contract violations fail at application startup or client creation.
- No hidden FastAPI magic or implicit behavior.
- Deterministic, testable application assembly.
FastAPI OpenAPI First favors correctness, explicitness, and contract
enforcement over convenience shortcuts.
## Core Philosophy
`FastAPI OpenAPI First` operates on the **Contract-as-Code** principle:
1. **Spec-Driven Routing**: The OpenAPI document *is* the router. Code only exists to fulfill established contracts.
2. **Startup Fail-Fast**: Binding mismatches (missing handlers or extra operations) are detected during app initialization, not at runtime.
3. **Decoupled Symmetry**: The same specification drives both the FastAPI server and the `httpx`-based client, ensuring type-safe communication.
## Documentation Design
Follow these "AI-Native" docstring principles to maximize developer and agent productivity:
### For Humans
- **Logical Grouping**: Document the Loader, Binder, and Client as distinct infrastructure layers.
- **Spec Snippets**: Always include the corresponding OpenAPI YAML/JSON snippet alongside Python examples.
### For LLMs
- **Full Path Linking**: Refer to cross-module dependencies using their full dotted paths (e.g., `openapi_first.loader.load_openapi`).
- **Complete Stubs**: Maintain high-fidelity `.pyi` stubs for all public interfaces to provide an optimized machine-context.
- **Traceable Errors**: Use specific `: description` pairs in `Raises` blocks to allow agents to accurately map errors to spec violations.
---
"""
from . import app
@@ -233,6 +100,8 @@ from . import binder
from . import loader
from . import client
from . import errors
from . import codegen
from . import codegen_routes
__all__ = [
"app",
@@ -240,4 +109,6 @@ __all__ = [
"loader",
"client",
"errors",
"codegen",
"codegen_routes",
]

View File

@@ -3,5 +3,7 @@ from . import binder as binder
from . import loader as loader
from . import client as client
from . import errors as errors
from . import codegen as codegen
from . import codegen_routes as codegen_routes
__all__ = ["app", "binder", "loader", "client", "errors"]
__all__ = ["app", "binder", "loader", "client", "errors", "codegen", "codegen_routes"]

View File

@@ -1,43 +1,35 @@
"""
openapi_first.app
=========================
# Summary
OpenAPI-first application bootstrap for FastAPI.
This module provides `OpenAPIFirstApp`, a thin but strict abstraction
that enforces OpenAPI as the single source of truth for a FastAPI service.
Core principles
---------------
- The OpenAPI specification (JSON or YAML) defines the entire API surface.
- Every operationId in the OpenAPI spec must have a corresponding
Python handler function.
- Handlers are plain Python callables (no FastAPI decorators).
- FastAPI route registration is derived exclusively from the spec.
- FastAPI's autogenerated OpenAPI schema is fully overridden.
Notes:
**Core Principles:**
What this module does
---------------------
- Loads and validates an OpenAPI 3.x specification.
- Dynamically binds HTTP routes to handler functions using operationId.
- Registers routes with FastAPI at application startup.
- Ensures runtime behavior matches the OpenAPI contract exactly.
- The OpenAPI specification (JSON or YAML) defines the entire API surface.
- Every `operationId` in the OpenAPI spec must have a corresponding
Python handler function.
- Handlers are plain Python callables (no FastAPI decorators).
- FastAPI route registration is derived exclusively from the spec.
- FastAPI's autogenerated OpenAPI schema is fully overridden.
What this module does NOT do
----------------------------
- It does not generate OpenAPI specs.
- It does not generate client code.
- It does not introduce a new framework or lifecycle.
- It does not alter FastAPI dependency injection semantics.
**Responsibilities:**
Intended usage
--------------
This module is intended for teams that want:
- Loads and validates an OpenAPI 3.x specification.
- Dynamically binds HTTP routes to handler functions using `operationId`.
- Registers routes with FastAPI at application startup.
- Ensures runtime behavior matches the OpenAPI contract exactly.
- OpenAPI-first API development
- Strong contract enforcement
- Minimal FastAPI boilerplate
- Predictable, CI-friendly failures for spec/implementation drift
**Constraints:**
- This module intentionally does NOT:
- Generate OpenAPI specs.
- Generate client code.
- Introduce a new framework or lifecycle.
- Alter FastAPI dependency injection semantics.
"""
from fastapi import FastAPI
@@ -50,50 +42,33 @@ class OpenAPIFirstApp(FastAPI):
"""
FastAPI application enforcing OpenAPI-first design.
`OpenAPIFirstApp` subclasses FastAPI and replaces manual route
registration with OpenAPI-driven binding. All routes are derived
from the provided OpenAPI specification, and each operationId is
mapped to a Python function in the supplied routes module.
Notes:
**Responsibilities:**
Parameters
----------
openapi_path : str
Filesystem path to the OpenAPI 3.x specification file.
This specification is treated as the authoritative API contract.
- `OpenAPIFirstApp` subclasses `FastAPI` and replaces manual route
registration with OpenAPI-driven binding.
- All routes are derived from the provided OpenAPI specification,
and each `operationId` is mapped to a Python function in the
supplied routes module.
routes_module : module
Python module containing handler functions whose names correspond
exactly to OpenAPI operationId values.
**Guarantees:**
**fastapi_kwargs
Additional keyword arguments passed directly to `fastapi.FastAPI`
(e.g., title, version, middleware, lifespan handlers).
- No route can exist without an OpenAPI declaration.
- No OpenAPI operation can exist without a handler.
- Swagger UI and `/openapi.json` always reflect the provided spec.
- Handler functions remain framework-agnostic and testable.
Raises
------
OpenAPIFirstError
If the OpenAPI specification is invalid, or if any declared
operationId does not have a corresponding handler function.
Example:
```python
from openapi_first import OpenAPIFirstApp
import app.routes as routes
Behavior guarantees
-------------------
- No route can exist without an OpenAPI declaration.
- No OpenAPI operation can exist without a handler.
- Swagger UI and `/openapi.json` always reflect the provided spec.
- Handler functions remain framework-agnostic and testable.
Example
-------
```python
from openapi_first import OpenAPIFirstApp
import app.routes as routes
app = OpenAPIFirstApp(
openapi_path="app/openapi.json",
routes_module=routes,
title="Example Service",
)
```
app = OpenAPIFirstApp(
openapi_path="app/openapi.json",
routes_module=routes,
title="Example Service",
)
```
"""
def __init__(
@@ -103,6 +78,26 @@ class OpenAPIFirstApp(FastAPI):
routes_module,
**fastapi_kwargs,
):
"""
Initialize the application.
Args:
openapi_path (str):
Filesystem path to the OpenAPI 3.x specification file. This
specification is treated as the authoritative API contract.
routes_module (module):
Python module containing handler functions whose names correspond
exactly to OpenAPI `operationId` values.
**fastapi_kwargs (Any):
Additional keyword arguments passed directly to
`fastapi.FastAPI` (e.g., title, version, middleware, lifespan
handlers).
Raises:
OpenAPIFirstError:
If the OpenAPI specification is invalid, or if any declared
`operationId` does not have a corresponding handler function.
"""
# Initialize FastAPI normally
super().__init__(**fastapi_kwargs)

View File

@@ -1,36 +1,35 @@
"""
openapi_first.binder
============================
# Summary
OpenAPI-driven route binding for FastAPI.
This module is responsible for translating an OpenAPI 3.x specification
into concrete FastAPI routes. It enforces a strict one-to-one mapping
between OpenAPI operations and Python handler functions using operationId.
between OpenAPI operations and Python handler functions using `operationId`.
Core responsibility
-------------------
- Read path + method definitions from an OpenAPI specification
- Resolve each operationId to a Python callable
- Register routes with FastAPI using APIRoute
- Fail fast when contract violations are detected
Notes:
**Core Responsibility:**
Design constraints
------------------
- All routes MUST be declared in the OpenAPI specification.
- All OpenAPI operations MUST define an operationId.
- Every operationId MUST resolve to a handler function.
- Handlers are plain Python callables (no decorators required).
- No implicit route creation or inference is allowed.
- Read path + method definitions from an OpenAPI specification.
- Resolve each `operationId` to a Python callable.
- Register routes with FastAPI using `APIRoute`.
- Fail fast when contract violations are detected.
This module intentionally does NOT:
-------------------------------
- Perform request or response validation
- Generate Pydantic models
- Modify FastAPI dependency injection
- Interpret OpenAPI semantics beyond routing metadata
**Design Constraints:**
Those concerns belong to other layers or tooling.
- All routes MUST be declared in the OpenAPI specification.
- All OpenAPI operations MUST define an `operationId`.
- Every `operationId` MUST resolve to a handler function.
- Handlers are plain Python callables (no decorators required).
- No implicit route creation or inference is allowed.
**Constraints:**
- This module intentionally does NOT:
- Perform request or response validation.
- Generate Pydantic models.
- Modify FastAPI dependency injection.
- Interpret OpenAPI semantics beyond routing metadata.
"""
from fastapi.routing import APIRoute
@@ -42,33 +41,32 @@ def bind_routes(app, spec: dict, routes_module) -> None:
"""
Bind OpenAPI operations to FastAPI routes.
Iterates through the OpenAPI specification paths and methods,
resolves each operationId to a handler function, and registers
a corresponding APIRoute on the FastAPI application.
Args:
app (fastapi.FastAPI):
The FastAPI application instance to which routes will be added.
spec (dict):
Parsed OpenAPI 3.x specification dictionary.
routes_module (module):
Python module containing handler functions. Each handler's name MUST
exactly match an OpenAPI `operationId`.
Parameters
----------
app : fastapi.FastAPI
The FastAPI application instance to which routes will be added.
Raises:
MissingOperationHandler:
If an `operationId` is missing from the spec or if no corresponding
handler function exists in the routes module.
spec : dict
Parsed OpenAPI 3.x specification dictionary.
Notes:
**Responsibilities:**
routes_module : module
Python module containing handler functions. Each handler's
name MUST exactly match an OpenAPI operationId.
- Iterates through the OpenAPI specification paths and methods.
- Resolves each `operationId` to a handler function, and registers
a corresponding `APIRoute` on the FastAPI application.
Raises
------
MissingOperationHandler
If an operationId is missing from the spec or if no corresponding
handler function exists in the routes module.
**Guarantees:**
Behavior guarantees
-------------------
- Route registration is deterministic and spec-driven.
- No route decorators are required or supported.
- Handler resolution errors surface at application startup.
- Route registration is deterministic and spec-driven. No route
decorators are required or supported. Handler resolution errors
surface at application startup.
"""
paths = spec.get("paths", {})

View File

@@ -1,9 +1,10 @@
"""
openapi_first.cli
========================
Command-line interface for FastAPI OpenAPI-first scaffolding utilities.
---
## Summary
This CLI bootstraps OpenAPI-first FastAPI applications from versioned,
bundled templates packaged with the library.
"""
@@ -20,6 +21,10 @@ DEFAULT_TEMPLATE = "health_app"
def available_templates() -> list[str]:
"""
Return a list of available application templates.
Returns:
list[str]:
Sorted list of template names found in the internal templates directory.
"""
root = resources.files("openapi_first.templates")
return sorted(
@@ -32,6 +37,16 @@ def available_templates() -> list[str]:
def copy_template(template: str, target_dir: Path) -> None:
"""
Copy a bundled OpenAPI-first application template into a directory.
Args:
template (str):
Name of the template to copy.
target_dir (Path):
Filesystem path where the template should be copied.
Raises:
FileNotFoundError:
If the requested template does not exist.
"""
target_dir = target_dir.resolve()
target_dir.mkdir(parents=True, exist_ok=True)
@@ -51,38 +66,135 @@ def copy_template(template: str, target_dir: Path) -> None:
def main() -> None:
parser = argparse.ArgumentParser(
description="FastAPI OpenAPI-first scaffolding tools"
description="FastAPI OpenAPI-first developer tools"
)
parser.add_argument(
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
# Scaffold command
scaffold_parser = subparsers.add_parser(
"scaffold", help="Scaffold a new OpenAPI-first FastAPI application"
)
scaffold_parser.add_argument(
"template",
nargs="?",
default=DEFAULT_TEMPLATE,
help=f"Template name (default: {DEFAULT_TEMPLATE})",
)
parser.add_argument(
scaffold_parser.add_argument(
"path",
nargs="?",
default=None,
help="Target directory (defaults to template name)",
)
parser.add_argument(
scaffold_parser.add_argument(
"--list",
action="store_true",
help="List available templates and exit",
)
# Models command
models_parser = subparsers.add_parser(
"models", help="Generate Pydantic models from an OpenAPI specification"
)
models_parser.add_argument(
"spec",
help="Path to the OpenAPI specification file",
)
models_parser.add_argument(
"-o",
"--output",
required=True,
help="Path to the output Python file",
)
# Routes command
routes_parser = subparsers.add_parser(
"routes",
help="Generate route handler stubs from an OpenAPI specification",
)
routes_parser.add_argument(
"spec",
help="Path to the OpenAPI specification file",
)
routes_parser.add_argument(
"-o",
"--output-dir",
required=True,
help="Directory where route files will be created",
)
routes_parser.add_argument(
"--use-models",
action="store_true",
default=False,
help="Import Pydantic models for request bodies",
)
routes_parser.add_argument(
"--models-module",
default="models",
help="Python module path for models (default: models)",
)
args = parser.parse_args()
if args.list:
for name in available_templates():
print(name)
if args.command == "models":
from .codegen import generate_models
try:
generate_models(Path(args.spec), Path(args.output))
print(f"Models generated successfully at {args.output}")
except Exception as exc:
raise SystemExit(str(exc)) from exc
return
target = Path(args.path or args.template.replace("_", "-"))
if args.command == "routes":
from .codegen import generate_routes
try:
copy_template(args.template, target)
except Exception as exc:
raise SystemExit(str(exc)) from exc
try:
files = generate_routes(
spec_path=Path(args.spec),
output_dir=Path(args.output_dir),
use_models=args.use_models,
models_module=args.models_module,
)
print(f"Generated {len(files)} route file(s):")
for f in files:
print(f" {f}")
except Exception as exc:
raise SystemExit(str(exc)) from exc
return
print(f"Template '{args.template}' created at {target}")
# Default to scaffold if no command or scaffold command
if args.command == "scaffold" or args.command is None:
# Handle the case where someone uses the old CLI style (openapi-first template path)
# argparse with subparsers might not automatically handle this if positional args are passed
# but let's assume we want to encourage the new style.
# If no command was provided but positional args were, they might be for scaffolding
# This is a bit tricky with argparse subparsers.
# For simplicity, let's just support the new explicit commands.
if args.command is None and not any(vars(args).values()):
parser.print_help()
return
if getattr(args, "list", False):
for name in available_templates():
print(name)
return
template = getattr(args, "template", DEFAULT_TEMPLATE)
path_arg = getattr(args, "path", None)
target = Path(path_arg or template.replace("_", "-"))
try:
copy_template(template, target)
print(f"Template '{template}' created at {target}")
except Exception as exc:
raise SystemExit(str(exc)) from exc
return
parser.print_help()
if __name__ == "__main__":
main()

View File

@@ -1,6 +1,5 @@
"""
openapi_first.client
====================
# Summary
OpenAPI-first HTTP client for contract-driven services.
@@ -9,51 +8,27 @@ derives all callable operations directly from an OpenAPI 3.x specification.
It is the client counterpart to `OpenAPIFirstApp`.
Core principles
---------------
- The OpenAPI specification is the single source of truth.
- Each operationId becomes a callable Python method.
- No implicit schema mutation or inference.
- No code generation step.
- Minimal abstraction over httpx.
Notes:
**Core Principles:**
What this module does
---------------------
- Parses an OpenAPI 3.x specification.
- Dynamically creates one callable per operationId.
- Enforces presence of:
- servers
- paths
- operationId
- Formats path parameters safely.
- Handles JSON request bodies explicitly.
- Returns raw `httpx.Response` objects.
- The OpenAPI specification is the single source of truth
- Each operationId becomes a callable Python method
- No implicit schema mutation or inference
- No code generation step
- Minimal abstraction over httpx
What this module does NOT do
----------------------------
- It does not generate client code.
- It does not validate request/response schemas.
- It does not deserialize responses.
- It does not retry requests.
- It does not implement authentication helpers.
- It does not assume non-2xx responses are failures.
**Responsibilities:**
Intended usage
--------------
This client is designed for:
- Parses an OpenAPI 3.x specification
- Dynamically creates one callable per operationId
- Enforces presence of servers, paths, and operationId
- Formats path parameters safely
- Handles JSON request bodies explicitly
- Returns raw `httpx.Response` objects
- Service-to-service communication
- Integration testing
- Contract-driven internal SDK usage
- Systems that want OpenAPI-first symmetry with `OpenAPIFirstApp`
**Constraints:**
Design constraints
------------------
- Only the first server in the OpenAPI `servers` list is used if
`base_url` is not explicitly provided.
- Only explicitly declared request bodies are allowed.
- `application/json` is handled natively; other media types are sent as raw content.
- All responses are returned as-is.
- This module intentionally does NOT: Generate client code, validate request/response schemas, deserialize responses, retry requests, implement authentication helpers, or assume non-2xx responses are failures.
"""
from typing import Any, Callable, Dict, Optional
@@ -65,73 +40,49 @@ from .errors import OpenAPIFirstError
class OpenAPIClientError(OpenAPIFirstError):
"""Raised when an OpenAPI client operation fails."""
"""
Raised when an OpenAPI client operation fails.
"""
class OpenAPIClient:
"""
OpenAPI-first HTTP client (httpx-based).
OpenAPI-first HTTP client (`httpx`-based).
This client derives all callable methods directly from an OpenAPI 3.x
specification. Each operationId becomes a method on the client
instance.
Notes:
**Responsibilities:**
Design principles
-----------------
- One callable per operationId
- Explicit parameters (path, query, headers, body)
- No implicit schema inference or mutation
- Returns raw httpx.Response objects
- No response validation or deserialization
- This client derives all callable methods directly from an
OpenAPI 3.x specification. Each `operationId` becomes a method
on the client instance.
Parameters
----------
spec : dict
Parsed OpenAPI 3.x specification.
base_url : str | None
Base URL of the target service. If omitted, the first entry
in the OpenAPI `servers` list is used.
client : httpx.Client | None
Optional preconfigured httpx client instance.
**Guarantees:**
Raises
------
OpenAPIClientError
If:
- No servers are defined and base_url is not provided
- OpenAPI spec has no paths
- An operation is missing operationId
- Duplicate operationIds are detected
- Required path parameters are missing
- Required request body is missing
- One callable per `operationId`.
- Explicit parameters (path, query, headers, body).
- No implicit schema inference or mutation.
- Returns raw `httpx.Response` objects.
- No response validation or deserialization.
Example
-------
```python
from openapi_first import loader, client
Example:
```python
from openapi_first import loader, client
spec = loader.load_openapi("openapi.yaml")
spec = loader.load_openapi("openapi.yaml")
api = client.OpenAPIClient(
spec=spec,
base_url="http://localhost:8000",
)
api = client.OpenAPIClient(
spec=spec,
base_url="http://localhost:8000",
)
# Call operationId: getUser
response = api.getUser(
path_params={"user_id": 123}
)
# Call operationId: getUser
response = api.getUser(
path_params={"user_id": 123}
)
print(response.status_code)
print(response.json())
# Call operationId: createUser
response = api.createUser(
body={"name": "Bob"}
)
print(response.status_code)
```
print(response.status_code)
print(response.json())
```
"""
def __init__(
@@ -140,6 +91,21 @@ class OpenAPIClient:
base_url: Optional[str] = None,
client: Optional[httpx.Client] = None,
) -> None:
"""
Initialize the OpenAPI client.
Args:
spec (dict[str, Any]):
Parsed OpenAPI 3.x specification.
base_url (str, optional):
Base URL of the target service. If omitted, the first entry in the OpenAPI `servers` list is used.
client (httpx.Client, optional):
Optional preconfigured httpx client instance.
Raises:
OpenAPIClientError:
If no servers are defined, spec has no paths, operationIds are missing/duplicate, or required parameters are missing.
"""
self.spec = spec
self.base_url = base_url or self._resolve_base_url(spec)
self.client = client or httpx.Client(base_url=self.base_url)

53
openapi_first/codegen.py Normal file
View File

@@ -0,0 +1,53 @@
"""
# Summary
Core logic for generating Python source code from OpenAPI specifications.
This module provides reusable utilities for code generation, specifically
generating Pydantic models and route handler stubs from OpenAPI 3.x schema
definitions.
"""
from pathlib import Path
from datamodel_code_generator import (
InputFileType,
PythonVersion,
generate,
)
from .codegen_routes import generate_routes
def generate_models(
spec_path: Path,
output_path: Path,
pydantic_version: int = 2,
) -> None:
"""
Generate Pydantic models from an OpenAPI specification.
Args:
spec_path (Path):
Path to the OpenAPI specification file (YAML or JSON).
output_path (Path):
Path where the generated Python code should be written.
pydantic_version (int, optional):
The Pydantic version to target (1 or 2). Defaults to 2.
Notes:
**Reusability:**
This function is designed to be used by the CLI and can be
exposed as an MCP tool without modification.
"""
generate(
input_=spec_path,
input_file_type=InputFileType.OpenAPI,
output=output_path,
target_python_version=PythonVersion.PY_310,
use_schema_description=True,
use_standard_collections=True,
use_union_operator=True,
)
__all__ = ["generate_models", "generate_routes"]

View File

@@ -0,0 +1,312 @@
"""
Route handler code generation from OpenAPI specifications.
This module generates Python route handler stubs from an OpenAPI 3.x
specification. Each resource (derived from the first path segment)
gets its own file under the output directory. Every OpenAPI operation
must define an ``operationId``, which becomes the handler function name.
Notes:
**Design constraints:**
- ``operationId`` is required on every operation (matching
``binder.bind_routes``).
- Handlers are stubs raising ``NotImplementedError``.
- Sub-resources (e.g. ``/pets/{id}/photo``) are grouped with their
parent resource (``pets``).
- Parameter types and defaults are inferred from the spec.
- ``response: Response`` is injected for non-200 success codes.
"""
from pathlib import Path
from typing import Any
from .loader import load_openapi
def generate_routes(
spec_path: Path,
output_dir: Path,
*,
use_models: bool = False,
models_module: str = "models",
) -> list[Path]:
"""
Generate route handler stubs from an OpenAPI specification.
Creates one ``<resource>.py`` file per resource in *output_dir*.
Resources are derived from the first path segment (e.g. ``/pets``
and ``/pets/{id}`` both group under ``pets``).
Args:
spec_path:
Path to the OpenAPI specification file (YAML or JSON).
output_dir:
Directory where the generated route files are written.
Created automatically if it does not exist.
use_models:
If ``True``, import Pydantic models from *models_module*
for request-body schemas referenced via ``$ref``.
models_module:
Dotted Python module path from which to import models
(e.g. ``"models"``, ``"app.models"``).
Returns:
list[Path]:
Absolute paths of every generated route file.
Raises:
OpenAPISpecLoadError:
If the spec cannot be loaded or validated.
ValueError:
If any operation is missing ``operationId``.
"""
spec = load_openapi(spec_path)
output_dir = Path(output_dir).resolve()
output_dir.mkdir(parents=True, exist_ok=True)
# Group paths by resource (first non-param path segment)
resources: dict[str, list[tuple[str, str, dict]]] = {}
paths = spec.get("paths", {})
for path, methods in paths.items():
segments = [s for s in path.split("/") if s and not s.startswith("{")]
if not segments:
continue
resource = segments[0]
if resource not in resources:
resources[resource] = []
for http_method, operation in methods.items():
if http_method.startswith("x-"):
continue
resources[resource].append((path, http_method, operation))
generated_files: list[Path] = []
for resource in sorted(resources):
operations = resources[resource]
_validate_operations(resource, operations)
file_path = output_dir / f"{resource}.py"
content = _generate_resource_file(
resource=resource,
operations=operations,
spec_path=str(spec_path),
use_models=use_models,
models_module=models_module,
)
file_path.write_text(content, encoding="utf-8")
generated_files.append(file_path)
return generated_files
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
_TYPE_MAP: dict[str, str] = {
"integer": "int",
"number": "float",
"boolean": "bool",
"string": "str",
"array": "list",
"object": "dict",
}
def _validate_operations(resource: str, operations: list[tuple[str, str, dict]]) -> None:
"""Ensure every operation has an operationId."""
for path, http_method, operation in operations:
if not operation.get("operationId"):
raise ValueError(
f"Missing operationId for {http_method.upper()} {path} "
f"in resource '{resource}'. "
"All operations must have an explicit operationId."
)
def _resolve_type(schema: dict[str, Any]) -> str:
"""Map an OpenAPI schema type to a Python type annotation."""
openapi_type = schema.get("type", "")
return _TYPE_MAP.get(openapi_type, "str")
def _get_request_body_schema(operation: dict[str, Any]) -> str | None:
"""Extract the ``$ref`` schema name from a request body, if any."""
request_body = operation.get("requestBody")
if not request_body:
return None
content = request_body.get("content", {})
for media_info in content.values():
schema = media_info.get("schema", {})
ref = schema.get("$ref", "")
if ref:
return ref.rsplit("/", 1)[-1]
return None
def _get_success_status(operation: dict[str, Any]) -> str:
"""Return the primary success status code (200, 201, 204, or default)."""
responses = operation.get("responses", {})
for code in ("201", "200", "204"):
if code in responses:
return code
if "default" in responses:
return "default"
return "200"
def _needs_any(operations: list[tuple[str, str, dict]]) -> bool:
"""Check if any operation uses a type that requires `from typing import Any`."""
for _, _, op in operations:
for param in op.get("parameters", []):
schema = param.get("schema", {})
if schema.get("type", "") not in _TYPE_MAP:
return True
return False
# ---------------------------------------------------------------------------
# File / function generation
# ---------------------------------------------------------------------------
def _generate_resource_file(
resource: str,
operations: list[tuple[str, str, dict]],
spec_path: str,
use_models: bool,
models_module: str,
) -> str:
"""Assemble the full Python source for a single resource file."""
op_ids = [op.get("operationId", "") for _, _, op in operations]
lines: list[str] = [
f'"""Route handlers for the {resource} resource.',
"",
f"Generated from OpenAPI spec: {spec_path}",
f'Bind via operationIds: {", ".join(op_ids)}',
'"""',
"",
"from fastapi import Response, HTTPException",
]
# Conditional typing import
if _needs_any(operations):
lines.append("from typing import Any")
# Conditional model imports
if use_models:
schemas_needed: set[str] = set()
for _, _, op in operations:
schema = _get_request_body_schema(op)
if schema:
schemas_needed.add(schema)
if schemas_needed:
lines.append(f"from {models_module} import {', '.join(sorted(schemas_needed))}")
lines.append("")
for path, http_method, operation in operations:
lines.append("")
lines.extend(_generate_handler(path, http_method, operation, use_models))
lines.append("")
return "\n".join(lines)
def _generate_handler(
path: str,
http_method: str,
operation: dict[str, Any],
use_models: bool,
) -> list[str]:
"""Build the source lines for a single handler function."""
operation_id = operation.get("operationId", "")
summary = operation.get("summary", f"{http_method.upper()} {path}")
params: list[str] = []
path_params: list[str] = []
query_params: list[str] = []
for param in operation.get("parameters", []):
name: str = param.get("name", "")
param_in: str = param.get("in", "")
schema: dict[str, Any] = param.get("schema", {})
param_type: str = _resolve_type(schema)
required: bool = param.get("required", False)
description: str = schema.get("description", schema.get("x-description", ""))
default_raw = schema.get("default")
if param_in == "path":
path_params.append(f"{name}: {param_type}")
elif param_in == "query":
if default_raw is not None:
default_repr = repr(default_raw)
query_params.append(f"{name}: {param_type} = {default_repr}")
elif required:
query_params.append(f"{name}: {param_type}")
else:
query_params.append(f"{name}: {param_type} = None")
# header / cookie params could be extended here
params.extend(path_params)
params.extend(query_params)
# Request body
schema_name = _get_request_body_schema(operation)
if operation.get("requestBody"):
if use_models and schema_name:
params.append(f"payload: {schema_name}")
else:
params.append("payload: dict")
# Inject Response for non-200 success codes
success_status = _get_success_status(operation)
if success_status in ("201", "204"):
params.append("response: Response")
# Build function body
lines: list[str] = []
param_str = ", ".join(params)
lines.append(f"def {operation_id}({param_str}):")
lines.append(f' """{summary}')
# Document parameters
doc_params: list[tuple[str, str, str]] = []
for param in operation.get("parameters", []):
name: str = param.get("name", "")
param_in: str = param.get("in", "")
schema: dict[str, Any] = param.get("schema", {})
param_type: str = _resolve_type(schema)
description: str = param.get("description", schema.get("x-description", ""))
if param_in in ("path", "query"):
doc_params.append((name, param_type, description))
if doc_params:
lines.append("")
lines.append(" Parameters")
lines.append(" ----------")
for pname, ptype, desc in doc_params:
if desc:
lines.append(f" {pname} : {ptype}")
lines.append(f" {desc}")
else:
lines.append(f" {pname} : {ptype}")
if operation.get("requestBody"):
if use_models and schema_name:
lines.append("")
lines.append(f" payload : {schema_name}")
lines.append(" Request body.")
else:
lines.append("")
lines.append(" payload : dict")
lines.append(" Request body.")
lines.append(' """')
lines.append(" raise NotImplementedError")
return lines

View File

@@ -1,33 +1,35 @@
"""
openapi_first.errors
============================
# Summary
Custom exceptions for OpenAPI-first FastAPI applications.
Exceptions for OpenAPI-first FastAPI applications.
This module defines a small hierarchy of explicit, intention-revealing
exceptions used to signal contract violations between an OpenAPI
specification and its Python implementation.
Design principles
-----------------
- Errors represent *programmer mistakes*, not runtime conditions.
- All errors are raised during application startup.
- Messages are actionable and suitable for CI/CD output.
- Exceptions are explicit rather than reused from generic built-ins.
Notes:
**Design Principles:**
These errors should normally cause immediate application failure.
- Errors represent programmer mistakes, not runtime conditions.
- All errors are raised during application startup.
- Messages are actionable and suitable for CI/CD output.
- Exceptions are explicit rather than reused from generic built-ins.
These errors should normally cause immediate application failure.
"""
class OpenAPIFirstError(Exception):
"""
Base exception for all OpenAPI-first enforcement errors.
This exception exists to allow callers, test suites, and CI pipelines
to catch and distinguish OpenAPI contract violations from unrelated
runtime errors.
Notes:
**Responsibilities:**
All exceptions raised by the OpenAPI-first core should inherit from
this type.
- This exception exists to allow callers, test suites, and CI
pipelines to catch and distinguish OpenAPI contract violations
from unrelated runtime errors.
- All exceptions raised by the OpenAPI-first core should inherit
from this type.
"""
pass
@@ -36,27 +38,31 @@ class MissingOperationHandler(OpenAPIFirstError):
"""
Raised when an OpenAPI operation cannot be resolved to a handler.
This error occurs when:
- An OpenAPI operation does not define an operationId, or
- An operationId is defined but no matching function exists in the
provided routes module.
Notes:
**Scenarios:**
This represents a violation of the OpenAPI-first contract and
indicates that the specification and implementation are out of sync.
- An OpenAPI operation does not define an `operationId`.
- An `operationId` is defined but no matching function exists in
the provided routes module.
**Guarantees:**
- This represents a violation of the OpenAPI-first contract and
indicates that the specification and implementation are out of
sync.
"""
def __init__(self, *, path: str, method: str, operation_id: str | None = None):
"""
Parameters
----------
path : str
The HTTP path declared in the OpenAPI specification.
Initialize the error.
method : str
The HTTP method (as declared in the OpenAPI spec).
operation_id : str, optional
The operationId declared in the OpenAPI spec, if present.
Args:
path (str):
The HTTP path declared in the OpenAPI specification.
method (str):
The HTTP method (as declared in the OpenAPI spec).
operation_id (str, optional):
The operationId declared in the OpenAPI spec, if present.
"""
if operation_id:
message = (

View File

@@ -1,6 +1,5 @@
"""
openapi_first.loaders
=============================
# Summary
OpenAPI specification loading and validation utilities.
@@ -10,19 +9,21 @@ from disk and validating it before it is used by the application.
It enforces the principle that an invalid or malformed OpenAPI document
must never reach the routing or runtime layers.
Design principles
-----------------
- OpenAPI is treated as an authoritative contract.
- Invalid specifications fail fast at application startup.
- Supported formats are JSON and YAML.
- Validation errors are surfaced clearly and early.
Notes:
**Design Principles:**
This module intentionally does NOT:
-----------------------------------
- Modify the OpenAPI document
- Infer missing fields
- Generate models or code
- Perform request/response validation at runtime
- OpenAPI is treated as an authoritative contract.
- Invalid specifications fail fast at application startup.
- Supported formats are JSON and YAML.
- Validation errors are surfaced clearly and early.
**Constraints:**
- This module intentionally does NOT:
- Modify the OpenAPI document.
- Infer missing fields.
- Generate models or code.
- Perform request/response validation at runtime.
"""
import json
@@ -39,8 +40,11 @@ class OpenAPISpecLoadError(OpenAPIFirstError):
"""
Raised when an OpenAPI specification cannot be loaded or validated.
This error indicates that the OpenAPI document is unreadable,
malformed, or violates the OpenAPI 3.x specification.
Notes:
**Guarantees:**
- This error indicates that the OpenAPI document is unreadable,
malformed, or violates the OpenAPI 3.x specification.
"""
pass
@@ -49,29 +53,27 @@ def load_openapi(path: str | Path) -> dict[str, Any]:
"""
Load and validate an OpenAPI 3.x specification from disk.
The specification is parsed based on file extension and validated
using a strict OpenAPI schema validator. Any error results in an
immediate exception, preventing application startup.
Args:
path (str | Path):
Filesystem path to an OpenAPI specification file. Supported
extensions: `.json`, `.yaml`, `.yml`.
Parameters
----------
path : str or pathlib.Path
Filesystem path to an OpenAPI specification file.
Supported extensions:
- `.json`
- `.yaml`
- `.yml`
Returns:
dict[str, Any]:
Parsed and validated OpenAPI specification.
Returns
-------
dict
Parsed and validated OpenAPI specification.
Raises:
OpenAPISpecLoadError:
If the file does not exist, cannot be parsed, or fails OpenAPI
schema validation.
Raises
------
OpenAPISpecLoadError
If the file does not exist, cannot be parsed, or fails
OpenAPI schema validation.
Notes:
**Guarantees:**
- The specification is parsed based on file extension and validated
using a strict OpenAPI schema validator.
- Any error results in an immediate exception, preventing
application startup.
"""
spec_path = Path(path)

View File

@@ -70,7 +70,7 @@ def test_create_item():
}
response = client.create_item(
body=payload
body=payload,
)
assert response.status_code == 201
@@ -115,7 +115,7 @@ def test_update_item():
def test_delete_item():
"""Deleting an item should remove it from the store."""
response = client.delete_item(
path_params={"item_id": 2}
path_params={"item_id": 2},
)
assert response.status_code == 204

View File

@@ -69,7 +69,7 @@ def test_create_item():
}
response = client.create_item(
body=payload
body=payload,
)
assert response.status_code == 201
@@ -114,7 +114,7 @@ def test_update_item():
def test_delete_item():
"""Deleting an item should remove it from the store."""
response = client.delete_item(
path_params={"item_id": 2}
path_params={"item_id": 2},
)
assert response.status_code == 204

View File

@@ -0,0 +1,97 @@
"""
OpenAPI-first Veterinary Clinic application template.
This package contains a complete, runnable example of an OpenAPI-first
veterinary clinic management service. It demonstrates all ``x-`` extension
fields consumed by the ``react-openapi`` admin panel renderer.
The application manages five resources:
- **Parents** — pet owners with contact details
- **Vets** — veterinarians with specializations
- **Treatments** — medical procedure catalog
- **Pets** — animals with species, age, weight, and photos
- **Appointments** — scheduled visits linking pets, vets, and treatments
All HTTP routes, methods, schemas, and operation bindings are defined
in the OpenAPI specification (``openapi.yaml``). Every operation has an
explicit ``operationId`` that maps to a Python handler in ``routes.py``.
This file is a copyable template. It is not part of the ``openapi_first``
library API surface.
----------------------------------------------------------------------
OpenAPI x- extension fields demonstrated
----------------------------------------------------------------------
Schema-level extensions (mark a schema as a UI resource):
``x-resource`` (REQUIRED) Maps schema to URL path segment
``x-primary-key`` (REQUIRED) Primary key property name
``x-display-format`` (REQUIRED) Human-readable label template
``x-list-columns`` (REQUIRED) Columns for the datatable
Property-level extensions (control UI rendering):
``x-label`` (REQUIRED) Human-readable field label
``x-order`` (REQUIRED) Field ordering in forms/detail
``x-description`` (optional) Helper text below form fields
``x-hidden`` (optional) Visibility in form / list / detail
``x-filterable`` (optional) Allows column filtering
``x-sortable`` (optional) Allows column sorting
``x-fk`` (optional) Foreign key — renders as dropdown
``x-fk.resource`` (REQUIRED for FK) Target resource name
``x-fk.prefetch`` (optional) Preload all FK options on mount
``x-ui-type`` (optional) Custom UI type (e.g. image upload)
``x-upload-url`` (optional) Upload endpoint for binary fields
----------------------------------------------------------------------
Scaffolding via CLI
----------------------------------------------------------------------
Create a new vet clinic service using the bundled template:
openapi-first vet_app
Create the service in a custom directory:
openapi-first vet_app my-vet-clinic
----------------------------------------------------------------------
Client Usage Example
----------------------------------------------------------------------
from openapi_first.loader import load_openapi
from openapi_first.client import OpenAPIClient
spec = load_openapi("openapi.yaml")
client = OpenAPIClient(spec)
# List pets with pagination
response = client.list_pets(query_params={"limit": 10, "offset": 0})
# Create a pet with FK references
response = client.create_pet(
body={"name": "Fido", "species": "dog", "parents": [1, 2]}
)
# Upload a pet photo
response = client.upload_pet_photo(
path_params={"id": 1},
body={"file": open("photo.jpg", "rb")},
)
----------------------------------------------------------------------
Non-Goals
----------------------------------------------------------------------
This template is intentionally minimal and is NOT:
- production-ready
- persistent or concurrency-safe
- a reference architecture for data storage
It exists solely as a copyable example for learning, testing, and
bootstrapping OpenAPI-first services.
This package is not part of the ``openapi_first`` library API surface.
"""

View File

@@ -0,0 +1,356 @@
"""
In-memory data store for the Veterinary Clinic example.
This module is NOT thread-safe and is intended for demos and scaffolds only.
It provides minimal, process-local data stores for the five veterinary
clinic entities. Each store exposes standard CRUD operations backed by
a simple dictionary.
This module intentionally avoids:
- persistence
- concurrency guarantees
- transactional semantics
- validation beyond what Pydantic provides
This module is not part of the ``openapi_first`` library API surface.
"""
from datetime import date, datetime, timezone
from models import (
Parent, ParentCreate,
Vet, VetCreate,
Treatment, TreatmentCreate,
Procedure, ProcedureNotes,
Pet, PetCreate,
Appointment, AppointmentCreate,
)
def _now():
return datetime.now(timezone.utc).isoformat()
# ---------------------------------------------------------------------------
# Parents
# ---------------------------------------------------------------------------
_parents: dict[int, Parent] = {}
_parents_next_id = 1
def list_parents() -> list[Parent]:
return list(_parents.values())
def get_parent(parent_id: int) -> Parent:
return _parents[parent_id]
def create_parent(payload: ParentCreate) -> Parent:
global _parents_next_id
now = _now()
parent = Parent(
id=_parents_next_id,
name=payload.name,
email=payload.email,
phone=payload.phone,
metadata={"createdOn": now, "updatedOn": now} if payload.metadata else None,
)
_parents[_parents_next_id] = parent
_parents_next_id += 1
return parent
def update_parent(parent_id: int, payload: ParentCreate) -> Parent:
if parent_id not in _parents:
raise KeyError(parent_id)
now = _now()
current = _parents[parent_id]
updated = Parent(
id=parent_id,
name=payload.name,
email=payload.email,
phone=payload.phone if payload.phone is not None else current.phone,
metadata={"createdOn": current.metadata["createdOn"] if current.metadata else None, "updatedOn": now},
)
_parents[parent_id] = updated
return updated
def delete_parent(parent_id: int) -> None:
del _parents[parent_id]
# ---------------------------------------------------------------------------
# Vets
# ---------------------------------------------------------------------------
_vets: dict[int, Vet] = {}
_vets_next_id = 1
def list_vets() -> list[Vet]:
return list(_vets.values())
def get_vet(vet_id: int) -> Vet:
return _vets[vet_id]
def create_vet(payload: VetCreate) -> Vet:
global _vets_next_id
now = _now()
vet = Vet(
id=_vets_next_id,
name=payload.name,
specialty=payload.specialty,
email=payload.email,
phone=payload.phone,
metadata={"createdOn": now, "updatedOn": now} if payload.metadata else None,
)
_vets[_vets_next_id] = vet
_vets_next_id += 1
return vet
def update_vet(vet_id: int, payload: VetCreate) -> Vet:
if vet_id not in _vets:
raise KeyError(vet_id)
now = _now()
current = _vets[vet_id]
updated = Vet(
id=vet_id,
name=payload.name,
specialty=payload.specialty if payload.specialty is not None else current.specialty,
email=payload.email,
phone=payload.phone if payload.phone is not None else current.phone,
metadata={"createdOn": current.metadata["createdOn"] if current.metadata else None, "updatedOn": now},
)
_vets[vet_id] = updated
return updated
def delete_vet(vet_id: int) -> None:
del _vets[vet_id]
# ---------------------------------------------------------------------------
# Treatments
# ---------------------------------------------------------------------------
_treatments: dict[int, Treatment] = {}
_treatments_next_id = 1
def list_treatments() -> list[Treatment]:
return list(_treatments.values())
def get_treatment(treatment_id: int) -> Treatment:
return _treatments[treatment_id]
def create_treatment(payload: TreatmentCreate) -> Treatment:
global _treatments_next_id
now = _now()
treatment = Treatment(
id=_treatments_next_id,
label=payload.label,
description=payload.description,
metadata={"createdOn": now, "updatedOn": now} if payload.metadata else None,
)
_treatments[_treatments_next_id] = treatment
_treatments_next_id += 1
return treatment
def update_treatment(treatment_id: int, payload: TreatmentCreate) -> Treatment:
if treatment_id not in _treatments:
raise KeyError(treatment_id)
now = _now()
current = _treatments[treatment_id]
updated = Treatment(
id=treatment_id,
label=payload.label,
description=payload.description if payload.description is not None else current.description,
metadata={"createdOn": current.metadata["createdOn"] if current.metadata else None, "updatedOn": now},
)
_treatments[treatment_id] = updated
return updated
def delete_treatment(treatment_id: int) -> None:
del _treatments[treatment_id]
# ---------------------------------------------------------------------------
# Pets
# ---------------------------------------------------------------------------
_pets: dict[int, Pet] = {}
_pets_next_id = 1
def list_pets() -> list[Pet]:
return list(_pets.values())
def get_pet(pet_id: int) -> Pet:
return _pets[pet_id]
def create_pet(payload: PetCreate) -> Pet:
global _pets_next_id
now = _now()
parents = [_parents[pid] for pid in payload.parent_ids]
pet = Pet(
id=_pets_next_id,
name=payload.name,
species=payload.species,
age=payload.age,
weight=payload.weight,
birthDate=payload.birthDate,
photo=payload.photo,
parents=parents,
metadata={"createdOn": now, "updatedOn": now} if payload.metadata else None,
)
_pets[_pets_next_id] = pet
_pets_next_id += 1
return pet
def update_pet(pet_id: int, payload: PetCreate) -> Pet:
if pet_id not in _pets:
raise KeyError(pet_id)
now = _now()
parents = [_parents[pid] for pid in payload.parent_ids]
current = _pets[pet_id]
updated = Pet(
id=pet_id,
name=payload.name,
species=payload.species,
age=payload.age if payload.age is not None else current.age,
weight=payload.weight if payload.weight is not None else current.weight,
birthDate=payload.birthDate if payload.birthDate is not None else current.birthDate,
photo=payload.photo if payload.photo is not None else current.photo,
parents=parents,
metadata={"createdOn": current.metadata["createdOn"] if current.metadata else None, "updatedOn": now},
)
_pets[pet_id] = updated
return updated
def delete_pet(pet_id: int) -> None:
del _pets[pet_id]
# ---------------------------------------------------------------------------
# Appointments
# ---------------------------------------------------------------------------
_appointments: dict[int, Appointment] = {}
_appointments_next_id = 1
def list_appointments() -> list[Appointment]:
return list(_appointments.values())
def get_appointment(appointment_id: int) -> Appointment:
return _appointments[appointment_id]
def create_appointment(payload: AppointmentCreate) -> Appointment:
global _appointments_next_id
now = _now()
appointment = Appointment(
id=_appointments_next_id,
date=payload.date,
notes=payload.notes,
procedures=payload.procedures,
pet=_pets[payload.pet_id],
vet=_vets[payload.vet_id],
treatment=_treatments[payload.treatment_id],
metadata={"createdOn": now, "updatedOn": now} if payload.metadata else None,
)
_appointments[_appointments_next_id] = appointment
_appointments_next_id += 1
return appointment
def update_appointment(appointment_id: int, payload: AppointmentCreate) -> Appointment:
if appointment_id not in _appointments:
raise KeyError(appointment_id)
now = _now()
current = _appointments[appointment_id]
updated = Appointment(
id=appointment_id,
date=payload.date,
notes=payload.notes if payload.notes is not None else current.notes,
procedures=payload.procedures,
pet=_pets.get(payload.pet_id, current.pet),
vet=_vets.get(payload.vet_id, current.vet),
treatment=_treatments.get(payload.treatment_id, current.treatment),
metadata={"createdOn": current.metadata["createdOn"] if current.metadata else None, "updatedOn": now},
)
_appointments[appointment_id] = updated
return updated
def delete_appointment(appointment_id: int) -> None:
del _appointments[appointment_id]
# ---------------------------------------------------------------------------
# Seed data — populate stores so the UI isn't empty on startup
# ---------------------------------------------------------------------------
def _seed_data():
now = _now()
meta = {"createdOn": now, "updatedOn": now}
global _parents_next_id, _vets_next_id, _treatments_next_id
global _pets_next_id, _appointments_next_id
_parents[1] = Parent(id=1, name="Alice Johnson", email="alice@example.com", phone="555-0101", metadata=meta)
_parents[2] = Parent(id=2, name="Bob Smith", email="bob@example.com", phone="555-0102", metadata=meta)
_parents[3] = Parent(id=3, name="Carol Williams", email="carol@example.com", phone="555-0103", metadata=meta)
_parents[4] = Parent(id=4, name="Dave Brown", email="dave@example.com", phone="555-0104", metadata=meta)
_parents_next_id = 5
_vets[1] = Vet(id=1, name="Sarah Connor", specialty="Surgery", email="sarah@clinic.com", phone="555-0201", metadata=meta)
_vets[2] = Vet(id=2, name="James Wilson", specialty="Dentistry", email="james@clinic.com", phone="555-0202", metadata=meta)
_vets[3] = Vet(id=3, name="Emily Davis", specialty="General Practice", email="emily@clinic.com", phone="555-0203", metadata=meta)
_vets_next_id = 4
_treatments[1] = Treatment(id=1, label="Annual Checkup", description="Full physical examination", metadata=meta)
_treatments[2] = Treatment(id=2, label="Vaccination", description="Core vaccines for common diseases", metadata=meta)
_treatments[3] = Treatment(id=3, label="Dental Cleaning", description="Scaling, polishing, and oral exam", metadata=meta)
_treatments[4] = Treatment(id=4, label="Spay/Neuter", description="Surgical sterilization", metadata=meta)
_treatments[5] = Treatment(id=5, label="Blood Panel", description="Complete blood count and chemistry", metadata=meta)
_treatments_next_id = 6
_pets[1] = Pet(id=1, name="Max", species="dog", age=4, weight=25.5, birthDate=date(2022, 3, 15), parents=[_parents[1]], metadata=meta)
_pets[2] = Pet(id=2, name="Luna", species="cat", age=2, weight=4.2, birthDate=date(2024, 1, 10), parents=[_parents[1], _parents[2]], metadata=meta)
_pets[3] = Pet(id=3, name="Charlie", species="dog", age=7, weight=18.0, birthDate=date(2019, 8, 22), parents=[_parents[2]], metadata=meta)
_pets[4] = Pet(id=4, name="Bella", species="bird", age=1, weight=0.3, birthDate=date(2025, 5, 1), parents=[_parents[3]], metadata=meta)
_pets[5] = Pet(id=5, name="Rocky", species="dog", age=3, weight=30.0, birthDate=date(2023, 11, 5), parents=[_parents[4]], metadata=meta)
_pets_next_id = 6
_appointments[1] = Appointment(id=1, date=datetime(2026, 6, 18, 9, 0, tzinfo=timezone.utc), notes="Annual checkup",
procedures=[Procedure(name="Physical Exam", cost=50.0), Procedure(name="Heart Rate", notes=ProcedureNotes(summary="Normal rhythm"))],
pet=_pets[1], vet=_vets[1], treatment=_treatments[1], metadata=meta)
_appointments[2] = Appointment(id=2, date=datetime(2026, 6, 18, 10, 30, tzinfo=timezone.utc), notes="Dental cleaning",
procedures=[Procedure(name="Scaling", cost=80.0), Procedure(name="Polishing", cost=40.0, notes=ProcedureNotes(summary="High-speed polish"))],
pet=_pets[2], vet=_vets[2], treatment=_treatments[3], metadata=meta)
_appointments[3] = Appointment(id=3, date=datetime(2026, 6, 19, 11, 0, tzinfo=timezone.utc), notes="Vaccination booster",
procedures=[Procedure(name="DHPP Vaccine", cost=35.0), Procedure(name="Rabies Vaccine", cost=45.0)],
pet=_pets[3], vet=_vets[3], treatment=_treatments[2], metadata=meta)
_appointments[4] = Appointment(id=4, date=datetime(2026, 6, 20, 14, 0, tzinfo=timezone.utc), notes="Follow-up after surgery",
procedures=[Procedure(name="Pre-op Exam", cost=30.0), Procedure(name="Surgery", cost=200.0), Procedure(name="Post-op Care", cost=50.0)],
pet=_pets[5], vet=_vets[1], treatment=_treatments[4], metadata=meta)
_appointments_next_id = 5
_seed_data()

View File

@@ -0,0 +1,58 @@
"""
Application entry point for an OpenAPI-first Veterinary Clinic service.
This module constructs a FastAPI application exclusively from an
OpenAPI specification and a handler namespace, without using
decorator-driven routing.
All HTTP routes, methods, request/response schemas, and operation
bindings are defined in the OpenAPI document referenced by
``openapi_path``. Python callables defined in the ``routes`` module are
bound to OpenAPI operations strictly via ``operationId``.
This module contains no routing logic, persistence concerns, or
framework configuration beyond application assembly.
Design guarantees:
- OpenAPI is the single source of truth
- No undocumented routes can exist
- Every OpenAPI operationId must resolve to exactly one handler
- All contract violations fail at application startup
This file is intended to be used as the ASGI entry point.
Example:
uvicorn main:app
"""
from contextlib import asynccontextmanager
from starlette.middleware.cors import CORSMiddleware
from openapi_first.app import OpenAPIFirstApp
import routes
from sse import start_worker, stop_worker
@asynccontextmanager
async def lifespan(app):
start_worker()
try:
yield
finally:
stop_worker()
app = OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=routes,
title="Veterinary Clinic Service",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_methods=["*"],
allow_headers=["*"],
)

View File

@@ -0,0 +1,103 @@
from datetime import date, datetime
from pydantic import BaseModel
class Metadata(BaseModel):
createdOn: datetime | None = None
updatedOn: datetime | None = None
class ParentBase(BaseModel):
name: str
email: str
phone: str | None = None
metadata: Metadata | None = None
class ParentCreate(ParentBase):
pass
class Parent(ParentBase):
id: int
class VetBase(BaseModel):
name: str
specialty: str | None = None
email: str
phone: str | None = None
metadata: Metadata | None = None
class VetCreate(VetBase):
pass
class Vet(VetBase):
id: int
class ProcedureNotes(BaseModel):
summary: str | None = None
details: str | None = None
class Procedure(BaseModel):
name: str | None = None
description: str | None = None
cost: float | None = None
notes: ProcedureNotes | None = None
class TreatmentBase(BaseModel):
label: str
description: str | None = None
metadata: Metadata | None = None
class TreatmentCreate(TreatmentBase):
pass
class Treatment(TreatmentBase):
id: int
class PetBase(BaseModel):
name: str
species: str
age: int | None = None
weight: float | None = None
birthDate: date | None = None
photo: str | None = None
metadata: Metadata | None = None
class PetCreate(PetBase):
parent_ids: list[int] = []
class Pet(PetBase):
id: int
parents: list[Parent] = []
class AppointmentBase(BaseModel):
date: datetime
notes: str | None = None
procedures: list[Procedure] = []
metadata: Metadata | None = None
class AppointmentCreate(AppointmentBase):
pet_id: int
vet_id: int
treatment_id: int
class Appointment(AppointmentBase):
id: int
pet: Pet
vet: Vet
treatment: Treatment

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,383 @@
"""
Veterinary Clinic route handlers bound via OpenAPI operationId.
Handlers explicitly control HTTP response status codes to ensure runtime
behavior matches the OpenAPI contract. Domain models defined using
Pydantic are used for request and response payloads.
No routing decorators, path definitions, or implicit framework behavior
appear in this module. All routing, HTTP methods, and schemas are defined
in the OpenAPI specification.
"""
from fastapi import Response, HTTPException, UploadFile
from fastapi.responses import StreamingResponse
from sse import subscribe, unsubscribe
from models import (
ParentCreate,
VetCreate,
TreatmentCreate,
PetCreate,
AppointmentCreate,
)
from data import (
list_parents as _list_parents,
get_parent as _get_parent,
create_parent as _create_parent,
update_parent as _update_parent,
delete_parent as _delete_parent,
list_vets as _list_vets,
get_vet as _get_vet,
create_vet as _create_vet,
update_vet as _update_vet,
delete_vet as _delete_vet,
list_treatments as _list_treatments,
get_treatment as _get_treatment,
create_treatment as _create_treatment,
update_treatment as _update_treatment,
delete_treatment as _delete_treatment,
list_pets as _list_pets,
get_pet as _get_pet,
create_pet as _create_pet,
update_pet as _update_pet,
delete_pet as _delete_pet,
list_appointments as _list_appointments,
get_appointment as _get_appointment,
create_appointment as _create_appointment,
update_appointment as _update_appointment,
delete_appointment as _delete_appointment,
)
# ---------------------------------------------------------------------------
# Parents
# ---------------------------------------------------------------------------
def list_parents(limit: int = 20, offset: int = 0):
"""List parents (paginated).
Parameters
----------
limit : int
Maximum number of records to return.
offset : int
Number of records to skip.
Returns
-------
dict
Paginated response with ``total`` and ``items``.
"""
items = _list_parents()
return {"total": len(items), "items": items[offset:offset + limit] if limit else items[offset:]}
def create_parent(payload: ParentCreate, response: Response):
"""Create a parent.
Parameters
----------
payload : ParentCreate
Parent data excluding the ``id`` field.
Returns
-------
Parent
The newly created parent.
"""
parent = _create_parent(payload)
response.status_code = 201
return parent
def get_parent(id: int):
"""Retrieve a single parent by ID.
Parameters
----------
id : int
Identifier of the parent.
Returns
-------
Parent
The requested parent.
Raises
------
HTTPException
404 if the parent does not exist.
"""
try:
return _get_parent(id)
except KeyError:
raise HTTPException(status_code=404, detail="Parent not found")
def update_parent(id: int, payload: ParentCreate):
"""Update an existing parent.
Parameters
----------
id : int
Identifier of the parent.
payload : ParentCreate
Updated parent data.
Returns
-------
Parent
The updated parent.
Raises
------
HTTPException
404 if the parent does not exist.
"""
try:
return _update_parent(id, payload)
except KeyError:
raise HTTPException(status_code=404, detail="Parent not found")
def delete_parent(id: int, response: Response):
"""Delete an existing parent.
Parameters
----------
id : int
Identifier of the parent.
Raises
------
HTTPException
404 if the parent does not exist.
"""
try:
_delete_parent(id)
except KeyError:
raise HTTPException(status_code=404, detail="Parent not found")
response.status_code = 204
# ---------------------------------------------------------------------------
# Vets
# ---------------------------------------------------------------------------
def list_vets(limit: int = 20, offset: int = 0):
"""List vets (paginated)."""
items = _list_vets()
return {"total": len(items), "items": items[offset:offset + limit] if limit else items[offset:]}
def create_vet(payload: VetCreate, response: Response):
"""Create a vet."""
vet = _create_vet(payload)
response.status_code = 201
return vet
def get_vet(id: int):
"""Retrieve a single vet by ID."""
try:
return _get_vet(id)
except KeyError:
raise HTTPException(status_code=404, detail="Vet not found")
def update_vet(id: int, payload: VetCreate):
"""Update an existing vet."""
try:
return _update_vet(id, payload)
except KeyError:
raise HTTPException(status_code=404, detail="Vet not found")
def delete_vet(id: int, response: Response):
"""Delete an existing vet."""
try:
_delete_vet(id)
except KeyError:
raise HTTPException(status_code=404, detail="Vet not found")
response.status_code = 204
# ---------------------------------------------------------------------------
# Treatments
# ---------------------------------------------------------------------------
def list_treatments():
"""List treatments (catalogue).
Returns
-------
list[Treatment]
A list of treatment domain objects.
"""
return _list_treatments()
def create_treatment(payload: TreatmentCreate, response: Response):
"""Add a treatment (admin only)."""
treatment = _create_treatment(payload)
response.status_code = 201
return treatment
def get_treatment(id: int):
"""Retrieve a single treatment by ID."""
try:
return _get_treatment(id)
except KeyError:
raise HTTPException(status_code=404, detail="Treatment not found")
def update_treatment(id: int, payload: TreatmentCreate):
"""Update an existing treatment."""
try:
return _update_treatment(id, payload)
except KeyError:
raise HTTPException(status_code=404, detail="Treatment not found")
def delete_treatment(id: int, response: Response):
"""Delete an existing treatment."""
try:
_delete_treatment(id)
except KeyError:
raise HTTPException(status_code=404, detail="Treatment not found")
response.status_code = 204
# ---------------------------------------------------------------------------
# Pets
# ---------------------------------------------------------------------------
def list_pets(limit: int = 20, offset: int = 0):
"""List pets (paginated)."""
items = _list_pets()
return {"total": len(items), "items": items[offset:offset + limit] if limit else items[offset:]}
def create_pet(payload: PetCreate, response: Response):
"""Create a pet."""
pet = _create_pet(payload)
response.status_code = 201
return pet
def get_pet(id: int):
"""Retrieve a single pet by ID."""
try:
return _get_pet(id)
except KeyError:
raise HTTPException(status_code=404, detail="Pet not found")
def update_pet(id: int, payload: PetCreate):
"""Update an existing pet."""
try:
return _update_pet(id, payload)
except KeyError:
raise HTTPException(status_code=404, detail="Pet not found")
def delete_pet(id: int, response: Response):
"""Delete an existing pet."""
try:
_delete_pet(id)
except KeyError:
raise HTTPException(status_code=404, detail="Pet not found")
response.status_code = 204
def upload_pet_photo(id: int, file: UploadFile):
"""Upload a pet photo.
Parameters
----------
id : int
Identifier of the pet.
file : UploadFile
Image file to upload.
Returns
-------
dict
A confirmation with the pet ID.
"""
_ = file # In a real app, save to disk / object store
return {"id": id, "status": "photo_uploaded"}
# ---------------------------------------------------------------------------
# Appointments
# ---------------------------------------------------------------------------
def list_appointments(limit: int = 20, offset: int = 0, date: str = None, vet: int = None, pet: int = None):
"""List appointments (paginated, filterable)."""
items = _list_appointments()
# Basic in-memory filtering
if date:
items = [a for a in items if a.date.startswith(date)]
if vet is not None:
items = [a for a in items if a.vet.id == vet]
if pet is not None:
items = [a for a in items if a.pet.id == pet]
return {"total": len(items), "items": items[offset:offset + limit] if limit else items[offset:]}
def create_appointment(payload: AppointmentCreate, response: Response):
"""Create an appointment."""
appointment = _create_appointment(payload)
response.status_code = 201
return appointment
def get_appointment(id: int):
"""Retrieve a single appointment by ID."""
try:
return _get_appointment(id)
except KeyError:
raise HTTPException(status_code=404, detail="Appointment not found")
def update_appointment(id: int, payload: AppointmentCreate):
"""Update an existing appointment."""
try:
return _update_appointment(id, payload)
except KeyError:
raise HTTPException(status_code=404, detail="Appointment not found")
def delete_appointment(id: int, response: Response):
"""Delete an existing appointment."""
try:
_delete_appointment(id)
except KeyError:
raise HTTPException(status_code=404, detail="Appointment not found")
response.status_code = 204
async def stream_calls():
"""Stream random animal sounds via SSE."""
q = await subscribe()
async def event_generator():
try:
while True:
data = await q.get()
yield f"data: {data}\n\n"
finally:
unsubscribe(q)
return StreamingResponse(event_generator(), media_type="text/event-stream")

View File

@@ -0,0 +1,44 @@
"""
SSE broadcast for the animal-sounds worker.
Not part of the openapi_first library API surface.
"""
import asyncio
import random
import json
_sounds = ["woof", "meow", "coo"]
_subscribers: list[asyncio.Queue] = []
_worker_task: asyncio.Task | None = None
async def _sound_worker():
while True:
sound = random.choice(_sounds)
data = json.dumps({"sound": sound})
for q in _subscribers:
await q.put(data)
await asyncio.sleep(random.uniform(1, 5))
def start_worker():
global _worker_task
_worker_task = asyncio.create_task(_sound_worker())
def stop_worker():
if _worker_task is not None:
_worker_task.cancel()
async def subscribe() -> asyncio.Queue:
q: asyncio.Queue = asyncio.Queue()
_subscribers.append(q)
return q
def unsubscribe(q: asyncio.Queue):
if q in _subscribers:
_subscribers.remove(q)

View File

@@ -0,0 +1,151 @@
"""
End-to-end tests for the OpenAPI-first Veterinary Clinic example app.
These tests validate that all CRUD operations behave correctly
against the in-memory mock data store using Pydantic models.
"""
from fastapi.testclient import TestClient
from main import app
from openapi_first.loader import load_openapi
from openapi_first.client import OpenAPIClient
test_client = TestClient(app)
spec = load_openapi("openapi.yaml")
client = OpenAPIClient(
spec=spec,
base_url="http://testserver",
client=test_client,
)
def test_list_parents():
"""List parents returns paginated response."""
response = client.list_parents(query={"limit": 10, "offset": 0})
assert response.status_code == 200
data = response.json()
assert "total" in data
assert "items" in data
def test_create_parent():
"""Creating a parent returns 201 with the created entity."""
payload = {"name": "Alice", "email": "alice@example.com"}
response = client.create_parent(body=payload)
assert response.status_code == 201
parent = response.json()
assert parent["name"] == "Alice"
assert "id" in parent
def test_get_parent():
"""Get parent by ID returns the entity."""
parent = client.create_parent(body={"name": "Bob", "email": "bob@example.com"}).json()
response = client.get_parent(path_params={"id": parent["id"]})
assert response.status_code == 200
assert response.json()["name"] == "Bob"
def test_update_parent():
"""Update parent replaces its values."""
parent = client.create_parent(body={"name": "Carol", "email": "carol@example.com"}).json()
payload = {"name": "Carol Smith", "email": "carol.smith@example.com"}
response = client.update_parent(path_params={"id": parent["id"]}, body=payload)
assert response.status_code == 200
assert response.json()["name"] == "Carol Smith"
def test_delete_parent():
"""Delete parent returns 204 and removes the entity."""
parent = client.create_parent(body={"name": "Dave", "email": "dave@example.com"}).json()
response = client.delete_parent(path_params={"id": parent["id"]})
assert response.status_code == 204
def test_list_vets():
"""List vets returns paginated response."""
response = client.list_vets(query={"limit": 10, "offset": 0})
assert response.status_code == 200
data = response.json()
assert "total" in data
assert "items" in data
def test_create_vet():
"""Creating a vet returns 201."""
payload = {"name": "Dr. Smith", "specialty": "Surgery", "email": "smith@clinic.com"}
response = client.create_vet(body=payload)
assert response.status_code == 201
assert response.json()["name"] == "Dr. Smith"
def test_list_treatments():
"""List treatments returns an array."""
response = client.list_treatments()
assert response.status_code == 200
assert isinstance(response.json(), list)
def test_create_treatment():
"""Creating a treatment returns 201."""
payload = {"label": "Vaccination", "description": "Annual vaccination"}
response = client.create_treatment(body=payload)
assert response.status_code == 201
assert response.json()["label"] == "Vaccination"
def test_create_pet():
"""Creating a pet links FK references."""
parent = client.create_parent(body={"name": "Owner", "email": "owner@example.com"}).json()
payload = {"name": "Fido", "species": "dog", "parent_ids": [parent["id"]]}
response = client.create_pet(body=payload)
assert response.status_code == 201
assert response.json()["name"] == "Fido"
def test_upload_pet_photo():
"""Upload pet photo returns 200."""
pet = client.create_pet(body={"name": "PhotoPet", "species": "cat", "parent_ids": []}).json()
response = client.client.post(
f"http://testserver/pets/{pet['id']}",
files={"file": ("test.jpg", b"fake-image-data", "image/jpeg")},
)
assert response.status_code == 200
def test_list_appointments():
"""List appointments returns paginated response with filter params."""
response = client.list_appointments(query={"limit": 10, "offset": 0})
assert response.status_code == 200
data = response.json()
assert "total" in data
assert "items" in data
def test_full_appointment_lifecycle():
"""Create a parent, vet, treatment, pet, then an appointment."""
parent = client.create_parent(body={"name": "Eve", "email": "eve@example.com"}).json()
vet = client.create_vet(body={"name": "Dr. Jones", "specialty": "Dentistry", "email": "jones@clinic.com"}).json()
treatment = client.create_treatment(body={"label": "Cleaning", "description": "Teeth cleaning"}).json()
pet = client.create_pet(body={"name": "Max", "species": "dog", "parent_ids": [parent["id"]]}).json()
payload = {
"date": "2025-06-01T10:00:00",
"pet_id": pet["id"],
"vet_id": vet["id"],
"treatment_id": treatment["id"],
}
response = client.create_appointment(body=payload)
assert response.status_code == 201
appointment = response.json()
assert appointment["pet"]["id"] == pet["id"]
# Fetch it back
get_resp = client.get_appointment(path_params={"id": appointment["id"]})
assert get_resp.status_code == 200
# Delete it
del_resp = client.delete_appointment(path_params={"id": appointment["id"]})
assert del_resp.status_code == 204

View File

@@ -52,6 +52,9 @@ dependencies = [
# YAML support (optional but recommended)
"pyyaml>=6.0.1",
# Code generation
"datamodel-code-generator>=0.25.0",
]
[project.scripts]