google styled doc
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
FastAPI OpenAPI First — strict OpenAPI-first application bootstrap for FastAPI.
|
FastAPI OpenAPI First — strict OpenAPI-first application bootstrap for FastAPI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
FastAPI OpenAPI First is a **contract-first infrastructure library** that
|
FastAPI OpenAPI First is a **contract-first infrastructure library** that
|
||||||
enforces OpenAPI as the single source of truth for FastAPI services.
|
enforces OpenAPI as the single source of truth for FastAPI services.
|
||||||
|
|
||||||
@@ -9,28 +13,9 @@ deterministic, spec-driven application assembly. Every HTTP route,
|
|||||||
method, and operation is defined in OpenAPI first and bound to Python
|
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.
|
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
## Installation
|
||||||
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
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
|
|
||||||
Install using pip:
|
Install using pip:
|
||||||
|
|
||||||
@@ -40,44 +25,9 @@ Or with Poetry:
|
|||||||
|
|
||||||
poetry add openapi-first
|
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
|
## Quick start
|
||||||
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)
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
|
|
||||||
Minimal OpenAPI-first FastAPI application:
|
Minimal OpenAPI-first FastAPI application:
|
||||||
|
|
||||||
@@ -91,109 +41,43 @@ Minimal OpenAPI-first FastAPI application:
|
|||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Run with:
|
OperationId-driven HTTP client:
|
||||||
# uvicorn my_service.main:api
|
|
||||||
|
|
||||||
Handler definitions (no decorators):
|
|
||||||
|
|
||||||
def get_health():
|
|
||||||
return {"status": "ok"}
|
|
||||||
|
|
||||||
OpenAPI snippet:
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
Client-Side Usage (OpenAPI → HTTP Client)
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
|
|
||||||
The same OpenAPI specification can be used to construct a strict,
|
|
||||||
operationId-driven HTTP client.
|
|
||||||
|
|
||||||
Client construction:
|
|
||||||
|
|
||||||
from openapi_first.loader import load_openapi
|
from openapi_first.loader import load_openapi
|
||||||
from openapi_first.client import OpenAPIClient
|
from openapi_first.client import OpenAPIClient
|
||||||
|
|
||||||
spec = load_openapi("openapi.yaml")
|
spec = load_openapi("openapi.yaml")
|
||||||
|
|
||||||
client = OpenAPIClient(spec)
|
client = OpenAPIClient(spec)
|
||||||
|
|
||||||
Calling operations (operationId is the API):
|
|
||||||
|
|
||||||
response = client.get_health()
|
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(
|
## Architecture
|
||||||
path_params={"item_id": 1}
|
|
||||||
)
|
|
||||||
|
|
||||||
Request bodies are passed explicitly:
|
The library is structured around four core responsibilities:
|
||||||
|
|
||||||
response = client.create_item(
|
- **loader**: Load and validate OpenAPI 3.x specifications (JSON/YAML)
|
||||||
body={"name": "Orange", "price": 0.8}
|
- **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 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`
|
## Public API
|
||||||
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
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
|
|
||||||
The supported public API consists of the following top-level modules:
|
The supported public API consists of the following top-level modules:
|
||||||
|
|
||||||
- openapi_first.app
|
- `openapi_first.app`
|
||||||
- openapi_first.binder
|
- `openapi_first.binder`
|
||||||
- openapi_first.loader
|
- `openapi_first.loader`
|
||||||
- openapi_first.client
|
- `openapi_first.client`
|
||||||
- openapi_first.errors
|
- `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
|
- OpenAPI is the single source of truth
|
||||||
- No undocumented routes can exist
|
- No undocumented routes can exist
|
||||||
@@ -201,31 +85,8 @@ Design Guarantees
|
|||||||
- All contract violations fail at application startup or client creation
|
- All contract violations fail at application startup or client creation
|
||||||
- No hidden FastAPI magic or implicit behavior
|
- No hidden FastAPI magic or implicit behavior
|
||||||
- Deterministic, testable application assembly
|
- Deterministic, testable application assembly
|
||||||
- CI-friendly failure modes
|
|
||||||
|
|
||||||
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
|
from . import app
|
||||||
|
|||||||
@@ -1,43 +1,32 @@
|
|||||||
"""
|
"""
|
||||||
openapi_first.app
|
|
||||||
=========================
|
|
||||||
|
|
||||||
OpenAPI-first application bootstrap for FastAPI.
|
OpenAPI-first application bootstrap for FastAPI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
This module provides `OpenAPIFirstApp`, a thin but strict abstraction
|
This module provides `OpenAPIFirstApp`, a thin but strict abstraction
|
||||||
that enforces OpenAPI as the single source of truth for a FastAPI service.
|
that enforces OpenAPI as the single source of truth for a FastAPI service.
|
||||||
|
|
||||||
Core principles
|
Notes:
|
||||||
---------------
|
**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.
|
|
||||||
|
|
||||||
What this module does
|
- The OpenAPI specification (JSON or YAML) defines the entire API surface
|
||||||
---------------------
|
- Every operationId in the OpenAPI spec must have a corresponding Python handler function
|
||||||
- Loads and validates an OpenAPI 3.x specification.
|
- Handlers are plain Python callables (no FastAPI decorators)
|
||||||
- Dynamically binds HTTP routes to handler functions using operationId.
|
- FastAPI route registration is derived exclusively from the spec
|
||||||
- Registers routes with FastAPI at application startup.
|
- FastAPI's autogenerated OpenAPI schema is fully overridden
|
||||||
- Ensures runtime behavior matches the OpenAPI contract exactly.
|
|
||||||
|
|
||||||
What this module does NOT do
|
**Responsibilities:**
|
||||||
----------------------------
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
Intended usage
|
- Loads and validates an OpenAPI 3.x specification
|
||||||
--------------
|
- Dynamically binds HTTP routes to handler functions using operationId
|
||||||
This module is intended for teams that want:
|
- Registers routes with FastAPI at application startup
|
||||||
|
- Ensures runtime behavior matches the OpenAPI contract exactly
|
||||||
|
|
||||||
- OpenAPI-first API development
|
**Constraints:**
|
||||||
- Strong contract enforcement
|
|
||||||
- Minimal FastAPI boilerplate
|
- This module intentionally does NOT: Generate OpenAPI specs, generate client code, introduce a new framework or lifecycle, or alter FastAPI dependency injection semantics.
|
||||||
- Predictable, CI-friendly failures for spec/implementation drift
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
@@ -50,40 +39,20 @@ class OpenAPIFirstApp(FastAPI):
|
|||||||
"""
|
"""
|
||||||
FastAPI application enforcing OpenAPI-first design.
|
FastAPI application enforcing OpenAPI-first design.
|
||||||
|
|
||||||
`OpenAPIFirstApp` subclasses FastAPI and replaces manual route
|
Notes:
|
||||||
registration with OpenAPI-driven binding. All routes are derived
|
**Responsibilities:**
|
||||||
from the provided OpenAPI specification, and each operationId is
|
|
||||||
mapped to a Python function in the supplied routes module.
|
|
||||||
|
|
||||||
Parameters
|
- `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
|
||||||
openapi_path : str
|
|
||||||
Filesystem path to the OpenAPI 3.x specification file.
|
|
||||||
This specification is treated as the authoritative API contract.
|
|
||||||
|
|
||||||
routes_module : module
|
**Guarantees:**
|
||||||
Python module containing handler functions whose names correspond
|
|
||||||
exactly to OpenAPI operationId values.
|
|
||||||
|
|
||||||
**fastapi_kwargs
|
- No route can exist without an OpenAPI declaration
|
||||||
Additional keyword arguments passed directly to `fastapi.FastAPI`
|
- No OpenAPI operation can exist without a handler
|
||||||
(e.g., title, version, middleware, lifespan handlers).
|
- Swagger UI and `/openapi.json` always reflect the provided spec
|
||||||
|
- Handler functions remain framework-agnostic and testable
|
||||||
|
|
||||||
Raises
|
Example:
|
||||||
------
|
|
||||||
OpenAPIFirstError
|
|
||||||
If the OpenAPI specification is invalid, or if any declared
|
|
||||||
operationId does not have a corresponding handler function.
|
|
||||||
|
|
||||||
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
|
```python
|
||||||
from openapi_first import OpenAPIFirstApp
|
from openapi_first import OpenAPIFirstApp
|
||||||
import app.routes as routes
|
import app.routes as routes
|
||||||
@@ -103,6 +72,21 @@ class OpenAPIFirstApp(FastAPI):
|
|||||||
routes_module,
|
routes_module,
|
||||||
**fastapi_kwargs,
|
**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
|
# Initialize FastAPI normally
|
||||||
super().__init__(**fastapi_kwargs)
|
super().__init__(**fastapi_kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,33 @@
|
|||||||
"""
|
"""
|
||||||
openapi_first.binder
|
|
||||||
============================
|
|
||||||
|
|
||||||
OpenAPI-driven route binding for FastAPI.
|
OpenAPI-driven route binding for FastAPI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
This module is responsible for translating an OpenAPI 3.x specification
|
This module is responsible for translating an OpenAPI 3.x specification
|
||||||
into concrete FastAPI routes. It enforces a strict one-to-one mapping
|
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
|
Notes:
|
||||||
-------------------
|
**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
|
|
||||||
|
|
||||||
Design constraints
|
- Read path + method definitions from an OpenAPI specification
|
||||||
------------------
|
- Resolve each operationId to a Python callable
|
||||||
- All routes MUST be declared in the OpenAPI specification.
|
- Register routes with FastAPI using APIRoute
|
||||||
- All OpenAPI operations MUST define an operationId.
|
- Fail fast when contract violations are detected
|
||||||
- Every operationId MUST resolve to a handler function.
|
|
||||||
- Handlers are plain Python callables (no decorators required).
|
|
||||||
- No implicit route creation or inference is allowed.
|
|
||||||
|
|
||||||
This module intentionally does NOT:
|
**Design Constraints:**
|
||||||
-------------------------------
|
|
||||||
- Perform request or response validation
|
|
||||||
- Generate Pydantic models
|
|
||||||
- Modify FastAPI dependency injection
|
|
||||||
- Interpret OpenAPI semantics beyond routing metadata
|
|
||||||
|
|
||||||
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, or interpret OpenAPI semantics beyond routing metadata.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi.routing import APIRoute
|
from fastapi.routing import APIRoute
|
||||||
@@ -42,33 +39,27 @@ def bind_routes(app, spec: dict, routes_module) -> None:
|
|||||||
"""
|
"""
|
||||||
Bind OpenAPI operations to FastAPI routes.
|
Bind OpenAPI operations to FastAPI routes.
|
||||||
|
|
||||||
Iterates through the OpenAPI specification paths and methods,
|
Args:
|
||||||
resolves each operationId to a handler function, and registers
|
app (fastapi.FastAPI):
|
||||||
a corresponding APIRoute on the FastAPI application.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
app : fastapi.FastAPI
|
|
||||||
The FastAPI application instance to which routes will be added.
|
The FastAPI application instance to which routes will be added.
|
||||||
|
spec (dict):
|
||||||
spec : dict
|
|
||||||
Parsed OpenAPI 3.x specification dictionary.
|
Parsed OpenAPI 3.x specification dictionary.
|
||||||
|
routes_module (module):
|
||||||
|
Python module containing handler functions. Each handler's name MUST exactly match an OpenAPI operationId.
|
||||||
|
|
||||||
routes_module : module
|
Raises:
|
||||||
Python module containing handler functions. Each handler's
|
MissingOperationHandler:
|
||||||
name MUST exactly match an OpenAPI operationId.
|
If an operationId is missing from the spec or if no corresponding handler function exists in the routes module.
|
||||||
|
|
||||||
Raises
|
Notes:
|
||||||
------
|
**Responsibilities:**
|
||||||
MissingOperationHandler
|
|
||||||
If an operationId is missing from the spec or if no corresponding
|
|
||||||
handler function exists in the routes module.
|
|
||||||
|
|
||||||
Behavior guarantees
|
- Iterates through the OpenAPI specification paths and methods
|
||||||
-------------------
|
- Resolves each operationId to a handler function, and registers a corresponding APIRoute on the FastAPI application
|
||||||
- Route registration is deterministic and spec-driven.
|
|
||||||
- No route decorators are required or supported.
|
**Guarantees:**
|
||||||
- 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", {})
|
paths = spec.get("paths", {})
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
openapi_first.cli
|
|
||||||
========================
|
|
||||||
|
|
||||||
Command-line interface for FastAPI OpenAPI-first scaffolding utilities.
|
Command-line interface for FastAPI OpenAPI-first scaffolding utilities.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
This CLI bootstraps OpenAPI-first FastAPI applications from versioned,
|
This CLI bootstraps OpenAPI-first FastAPI applications from versioned,
|
||||||
bundled templates packaged with the library.
|
bundled templates packaged with the library.
|
||||||
"""
|
"""
|
||||||
@@ -20,6 +21,10 @@ DEFAULT_TEMPLATE = "health_app"
|
|||||||
def available_templates() -> list[str]:
|
def available_templates() -> list[str]:
|
||||||
"""
|
"""
|
||||||
Return a list of available application templates.
|
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")
|
root = resources.files("openapi_first.templates")
|
||||||
return sorted(
|
return sorted(
|
||||||
@@ -32,6 +37,16 @@ def available_templates() -> list[str]:
|
|||||||
def copy_template(template: str, target_dir: Path) -> None:
|
def copy_template(template: str, target_dir: Path) -> None:
|
||||||
"""
|
"""
|
||||||
Copy a bundled OpenAPI-first application template into a directory.
|
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 = target_dir.resolve()
|
||||||
target_dir.mkdir(parents=True, exist_ok=True)
|
target_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|||||||
@@ -1,59 +1,36 @@
|
|||||||
"""
|
"""
|
||||||
openapi_first.client
|
|
||||||
====================
|
|
||||||
|
|
||||||
OpenAPI-first HTTP client for contract-driven services.
|
OpenAPI-first HTTP client for contract-driven services.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
This module provides `OpenAPIClient`, a thin, strict HTTP client that
|
This module provides `OpenAPIClient`, a thin, strict HTTP client that
|
||||||
derives all callable operations directly from an OpenAPI 3.x specification.
|
derives all callable operations directly from an OpenAPI 3.x specification.
|
||||||
|
|
||||||
It is the client counterpart to `OpenAPIFirstApp`.
|
It is the client counterpart to `OpenAPIFirstApp`.
|
||||||
|
|
||||||
Core principles
|
Notes:
|
||||||
---------------
|
**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.
|
|
||||||
|
|
||||||
What this module does
|
- The OpenAPI specification is the single source of truth
|
||||||
---------------------
|
- Each operationId becomes a callable Python method
|
||||||
- Parses an OpenAPI 3.x specification.
|
- No implicit schema mutation or inference
|
||||||
- Dynamically creates one callable per operationId.
|
- No code generation step
|
||||||
- Enforces presence of:
|
- Minimal abstraction over httpx
|
||||||
- servers
|
|
||||||
- paths
|
|
||||||
- operationId
|
|
||||||
- Formats path parameters safely.
|
|
||||||
- Handles JSON request bodies explicitly.
|
|
||||||
- Returns raw `httpx.Response` objects.
|
|
||||||
|
|
||||||
What this module does NOT do
|
**Responsibilities:**
|
||||||
----------------------------
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
Intended usage
|
- Parses an OpenAPI 3.x specification
|
||||||
--------------
|
- Dynamically creates one callable per operationId
|
||||||
This client is designed for:
|
- 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
|
**Constraints:**
|
||||||
- Integration testing
|
|
||||||
- Contract-driven internal SDK usage
|
|
||||||
- Systems that want OpenAPI-first symmetry with `OpenAPIFirstApp`
|
|
||||||
|
|
||||||
Design constraints
|
- 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.
|
||||||
------------------
|
|
||||||
- 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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Callable, Dict, Optional
|
from typing import Any, Callable, Dict, Optional
|
||||||
@@ -65,48 +42,29 @@ from .errors import OpenAPIFirstError
|
|||||||
|
|
||||||
|
|
||||||
class OpenAPIClientError(OpenAPIFirstError):
|
class OpenAPIClientError(OpenAPIFirstError):
|
||||||
"""Raised when an OpenAPI client operation fails."""
|
"""
|
||||||
|
Raised when an OpenAPI client operation fails.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class OpenAPIClient:
|
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
|
Notes:
|
||||||
specification. Each operationId becomes a method on the client
|
**Responsibilities:**
|
||||||
instance.
|
|
||||||
|
- This client derives all callable methods directly from an OpenAPI 3.x specification. Each operationId becomes a method on the client instance.
|
||||||
|
|
||||||
|
**Guarantees:**
|
||||||
|
|
||||||
Design principles
|
|
||||||
-----------------
|
|
||||||
- One callable per operationId
|
- One callable per operationId
|
||||||
- Explicit parameters (path, query, headers, body)
|
- Explicit parameters (path, query, headers, body)
|
||||||
- No implicit schema inference or mutation
|
- No implicit schema inference or mutation
|
||||||
- Returns raw httpx.Response objects
|
- Returns raw `httpx.Response` objects
|
||||||
- No response validation or deserialization
|
- No response validation or deserialization
|
||||||
|
|
||||||
Parameters
|
Example:
|
||||||
----------
|
|
||||||
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.
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------
|
|
||||||
```python
|
```python
|
||||||
from openapi_first import loader, client
|
from openapi_first import loader, client
|
||||||
|
|
||||||
@@ -124,13 +82,6 @@ class OpenAPIClient:
|
|||||||
|
|
||||||
print(response.status_code)
|
print(response.status_code)
|
||||||
print(response.json())
|
print(response.json())
|
||||||
|
|
||||||
# Call operationId: createUser
|
|
||||||
response = api.createUser(
|
|
||||||
body={"name": "Bob"}
|
|
||||||
)
|
|
||||||
|
|
||||||
print(response.status_code)
|
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -140,6 +91,21 @@ class OpenAPIClient:
|
|||||||
base_url: Optional[str] = None,
|
base_url: Optional[str] = None,
|
||||||
client: Optional[httpx.Client] = None,
|
client: Optional[httpx.Client] = None,
|
||||||
) -> 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.spec = spec
|
||||||
self.base_url = base_url or self._resolve_base_url(spec)
|
self.base_url = base_url or self._resolve_base_url(spec)
|
||||||
self.client = client or httpx.Client(base_url=self.base_url)
|
self.client = client or httpx.Client(base_url=self.base_url)
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
"""
|
"""
|
||||||
openapi_first.errors
|
Exceptions for OpenAPI-first FastAPI applications.
|
||||||
============================
|
|
||||||
|
|
||||||
Custom exceptions for OpenAPI-first FastAPI applications.
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
This module defines a small hierarchy of explicit, intention-revealing
|
This module defines a small hierarchy of explicit, intention-revealing
|
||||||
exceptions used to signal contract violations between an OpenAPI
|
exceptions used to signal contract violations between an OpenAPI
|
||||||
specification and its Python implementation.
|
specification and its Python implementation.
|
||||||
|
|
||||||
Design principles
|
Notes:
|
||||||
-----------------
|
**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.
|
|
||||||
|
|
||||||
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):
|
class OpenAPIFirstError(Exception):
|
||||||
"""
|
"""
|
||||||
Base exception for all OpenAPI-first enforcement errors.
|
Base exception for all OpenAPI-first enforcement errors.
|
||||||
|
|
||||||
This exception exists to allow callers, test suites, and CI pipelines
|
Notes:
|
||||||
to catch and distinguish OpenAPI contract violations from unrelated
|
**Responsibilities:**
|
||||||
runtime errors.
|
|
||||||
|
|
||||||
All exceptions raised by the OpenAPI-first core should inherit from
|
- This exception exists to allow callers, test suites, and CI pipelines to catch and distinguish OpenAPI contract violations from unrelated runtime errors
|
||||||
this type.
|
- All exceptions raised by the OpenAPI-first core should inherit from this type
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -36,26 +37,27 @@ class MissingOperationHandler(OpenAPIFirstError):
|
|||||||
"""
|
"""
|
||||||
Raised when an OpenAPI operation cannot be resolved to a handler.
|
Raised when an OpenAPI operation cannot be resolved to a handler.
|
||||||
|
|
||||||
This error occurs when:
|
Notes:
|
||||||
- An OpenAPI operation does not define an operationId, or
|
**Scenarios:**
|
||||||
- An operationId is defined but no matching function exists in the
|
|
||||||
provided routes module.
|
|
||||||
|
|
||||||
This represents a violation of the OpenAPI-first contract and
|
- An OpenAPI operation does not define an operationId
|
||||||
indicates that the specification and implementation are out of sync.
|
- 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):
|
def __init__(self, *, path: str, method: str, operation_id: str | None = None):
|
||||||
"""
|
"""
|
||||||
Parameters
|
Initialize the error.
|
||||||
----------
|
|
||||||
path : str
|
Args:
|
||||||
|
path (str):
|
||||||
The HTTP path declared in the OpenAPI specification.
|
The HTTP path declared in the OpenAPI specification.
|
||||||
|
method (str):
|
||||||
method : str
|
|
||||||
The HTTP method (as declared in the OpenAPI spec).
|
The HTTP method (as declared in the OpenAPI spec).
|
||||||
|
operation_id (str, optional):
|
||||||
operation_id : str, optional
|
|
||||||
The operationId declared in the OpenAPI spec, if present.
|
The operationId declared in the OpenAPI spec, if present.
|
||||||
"""
|
"""
|
||||||
if operation_id:
|
if operation_id:
|
||||||
|
|||||||
@@ -1,28 +1,27 @@
|
|||||||
"""
|
"""
|
||||||
openapi_first.loaders
|
|
||||||
=============================
|
|
||||||
|
|
||||||
OpenAPI specification loading and validation utilities.
|
OpenAPI specification loading and validation utilities.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
This module is responsible for loading an OpenAPI 3.x specification
|
This module is responsible for loading an OpenAPI 3.x specification
|
||||||
from disk and validating it before it is used by the application.
|
from disk and validating it before it is used by the application.
|
||||||
|
|
||||||
It enforces the principle that an invalid or malformed OpenAPI document
|
It enforces the principle that an invalid or malformed OpenAPI document
|
||||||
must never reach the routing or runtime layers.
|
must never reach the routing or runtime layers.
|
||||||
|
|
||||||
Design principles
|
Notes:
|
||||||
-----------------
|
**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.
|
|
||||||
|
|
||||||
This module intentionally does NOT:
|
- OpenAPI is treated as an authoritative contract
|
||||||
-----------------------------------
|
- Invalid specifications fail fast at application startup
|
||||||
- Modify the OpenAPI document
|
- Supported formats are JSON and YAML
|
||||||
- Infer missing fields
|
- Validation errors are surfaced clearly and early
|
||||||
- Generate models or code
|
|
||||||
- Perform request/response validation at runtime
|
**Constraints:**
|
||||||
|
|
||||||
|
- This module intentionally does NOT: Modify the OpenAPI document, infer missing fields, generate models or code, or perform request/response validation at runtime.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -39,8 +38,10 @@ class OpenAPISpecLoadError(OpenAPIFirstError):
|
|||||||
"""
|
"""
|
||||||
Raised when an OpenAPI specification cannot be loaded or validated.
|
Raised when an OpenAPI specification cannot be loaded or validated.
|
||||||
|
|
||||||
This error indicates that the OpenAPI document is unreadable,
|
Notes:
|
||||||
malformed, or violates the OpenAPI 3.x specification.
|
**Guarantees:**
|
||||||
|
|
||||||
|
- This error indicates that the OpenAPI document is unreadable, malformed, or violates the OpenAPI 3.x specification
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -49,29 +50,23 @@ def load_openapi(path: str | Path) -> dict[str, Any]:
|
|||||||
"""
|
"""
|
||||||
Load and validate an OpenAPI 3.x specification from disk.
|
Load and validate an OpenAPI 3.x specification from disk.
|
||||||
|
|
||||||
The specification is parsed based on file extension and validated
|
Args:
|
||||||
using a strict OpenAPI schema validator. Any error results in an
|
path (str | Path):
|
||||||
immediate exception, preventing application startup.
|
Filesystem path to an OpenAPI specification file. Supported extensions: .json, .yaml, .yml.
|
||||||
|
|
||||||
Parameters
|
Returns:
|
||||||
----------
|
dict[str, Any]:
|
||||||
path : str or pathlib.Path
|
|
||||||
Filesystem path to an OpenAPI specification file.
|
|
||||||
Supported extensions:
|
|
||||||
- `.json`
|
|
||||||
- `.yaml`
|
|
||||||
- `.yml`
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
dict
|
|
||||||
Parsed and validated OpenAPI specification.
|
Parsed and validated OpenAPI specification.
|
||||||
|
|
||||||
Raises
|
Raises:
|
||||||
------
|
OpenAPISpecLoadError:
|
||||||
OpenAPISpecLoadError
|
If the file does not exist, cannot be parsed, or fails OpenAPI schema validation.
|
||||||
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)
|
spec_path = Path(path)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user