google styled doc
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
"""
|
||||
FastAPI OpenAPI First — strict OpenAPI-first application bootstrap for FastAPI.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
FastAPI OpenAPI First is a **contract-first infrastructure library** that
|
||||
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
|
||||
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:
|
||||
|
||||
@@ -40,44 +25,9 @@ Or with Poetry:
|
||||
|
||||
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:
|
||||
|
||||
@@ -91,109 +41,43 @@ Minimal OpenAPI-first FastAPI application:
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
# Run with:
|
||||
# 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:
|
||||
OperationId-driven HTTP client:
|
||||
|
||||
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}
|
||||
)
|
||||
## Architecture
|
||||
|
||||
Request bodies are passed explicitly:
|
||||
The library is structured around four core responsibilities:
|
||||
|
||||
response = client.create_item(
|
||||
body={"name": "Orange", "price": 0.8}
|
||||
)
|
||||
- **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 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
|
||||
@@ -201,31 +85,8 @@ Design Guarantees
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
@@ -1,43 +1,32 @@
|
||||
"""
|
||||
openapi_first.app
|
||||
=========================
|
||||
|
||||
OpenAPI-first application bootstrap for FastAPI.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
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, or alter FastAPI dependency injection semantics.
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI
|
||||
@@ -50,40 +39,20 @@ 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.
|
||||
|
||||
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
|
||||
-------
|
||||
Example:
|
||||
```python
|
||||
from openapi_first import OpenAPIFirstApp
|
||||
import app.routes as routes
|
||||
@@ -103,6 +72,21 @@ 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)
|
||||
|
||||
|
||||
@@ -1,36 +1,33 @@
|
||||
"""
|
||||
openapi_first.binder
|
||||
============================
|
||||
|
||||
OpenAPI-driven route binding for FastAPI.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
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.
|
||||
|
||||
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, or interpret OpenAPI semantics beyond routing metadata.
|
||||
"""
|
||||
|
||||
from fastapi.routing import APIRoute
|
||||
@@ -42,33 +39,27 @@ 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.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
app : fastapi.FastAPI
|
||||
Args:
|
||||
app (fastapi.FastAPI):
|
||||
The FastAPI application instance to which routes will be added.
|
||||
|
||||
spec : dict
|
||||
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.
|
||||
|
||||
routes_module : module
|
||||
Python module containing handler functions. Each handler's
|
||||
name MUST exactly match an OpenAPI operationId.
|
||||
Raises:
|
||||
MissingOperationHandler:
|
||||
If an operationId is missing from the spec or if no corresponding handler function exists in the routes module.
|
||||
|
||||
Raises
|
||||
------
|
||||
MissingOperationHandler
|
||||
If an operationId is missing from the spec or if no corresponding
|
||||
handler function exists in the routes module.
|
||||
Notes:
|
||||
**Responsibilities:**
|
||||
|
||||
Behavior guarantees
|
||||
-------------------
|
||||
- Route registration is deterministic and spec-driven.
|
||||
- No route decorators are required or supported.
|
||||
- Handler resolution errors surface at application startup.
|
||||
- Iterates through the OpenAPI specification paths and methods
|
||||
- Resolves each operationId to a handler function, and registers a corresponding APIRoute on the FastAPI application
|
||||
|
||||
**Guarantees:**
|
||||
|
||||
- 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", {})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,59 +1,36 @@
|
||||
"""
|
||||
openapi_first.client
|
||||
====================
|
||||
|
||||
OpenAPI-first HTTP client for contract-driven services.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
This module provides `OpenAPIClient`, a thin, strict HTTP client that
|
||||
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,48 +42,29 @@ 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).
|
||||
|
||||
This client derives all callable methods directly from an OpenAPI 3.x
|
||||
specification. Each operationId becomes a method on the client
|
||||
instance.
|
||||
Notes:
|
||||
**Responsibilities:**
|
||||
|
||||
- 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
|
||||
- Explicit parameters (path, query, headers, body)
|
||||
- No implicit schema inference or mutation
|
||||
- Returns raw httpx.Response objects
|
||||
- Returns raw `httpx.Response` objects
|
||||
- No response validation or deserialization
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
-------
|
||||
Example:
|
||||
```python
|
||||
from openapi_first import loader, client
|
||||
|
||||
@@ -124,13 +82,6 @@ class OpenAPIClient:
|
||||
|
||||
print(response.status_code)
|
||||
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,
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
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,26 +37,27 @@ 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
|
||||
Initialize the error.
|
||||
|
||||
Args:
|
||||
path (str):
|
||||
The HTTP path declared in the OpenAPI specification.
|
||||
|
||||
method : str
|
||||
method (str):
|
||||
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.
|
||||
"""
|
||||
if operation_id:
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
"""
|
||||
openapi_first.loaders
|
||||
=============================
|
||||
|
||||
OpenAPI specification loading and validation utilities.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
This module is responsible for loading an OpenAPI 3.x specification
|
||||
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, or perform request/response validation at runtime.
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -39,8 +38,10 @@ 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 +50,23 @@ 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
|
||||
Returns:
|
||||
dict[str, Any]:
|
||||
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user