Introduce an OpenAPI-first HTTP client driven by the same specification
used for server-side route binding, and refactor documentation and templates to treat the client as a first-class contract consumer. Key changes: - Add OpenAPI-first client module based on httpx - Document client usage alongside server-side binder usage - Update mkdocs navigation to include client documentation - Refactor CRUD and model app templates to call APIs via operationId instead of hardcoded paths - Align package documentation and public API surface with client support - Clarify server/client dependency split (fastapi vs httpx) This establishes strict symmetry between OpenAPI-driven server binding and OpenAPI-driven client invocation, reinforcing OpenAPI as the single source of truth on both sides of the contract.
This commit is contained in:
@@ -17,11 +17,12 @@ convenience facades.
|
||||
Architecture Overview
|
||||
----------------------------------------------------------------------
|
||||
|
||||
The library is structured around three core responsibilities:
|
||||
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
|
||||
@@ -40,7 +41,8 @@ Or with Poetry:
|
||||
poetry add openapi-first
|
||||
|
||||
Runtime dependencies are intentionally minimal:
|
||||
- fastapi
|
||||
- fastapi (server-side)
|
||||
- httpx (client-side)
|
||||
- openapi-spec-validator
|
||||
- pyyaml (optional, for YAML specs)
|
||||
|
||||
@@ -48,7 +50,7 @@ The ASGI server (e.g., uvicorn) is an application-level dependency and is
|
||||
not bundled with this library.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Basic Usage
|
||||
Server-Side Usage (OpenAPI → FastAPI)
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Minimal OpenAPI-first FastAPI application:
|
||||
@@ -57,7 +59,7 @@ Minimal OpenAPI-first FastAPI application:
|
||||
import my_service.routes as routes
|
||||
|
||||
api = app.OpenAPIFirstApp(
|
||||
openapi_path="openapi.json",
|
||||
openapi_path="openapi.yaml",
|
||||
routes_module=routes,
|
||||
title="My Service",
|
||||
version="1.0.0",
|
||||
@@ -81,6 +83,56 @@ OpenAPI snippet:
|
||||
"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.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
|
||||
----------------------------------------------------------------------
|
||||
@@ -107,6 +159,7 @@ 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
|
||||
|
||||
Classes and functions should be imported explicitly from these modules.
|
||||
@@ -118,8 +171,8 @@ Design Guarantees
|
||||
|
||||
- OpenAPI is the single source of truth
|
||||
- No undocumented routes can exist
|
||||
- No OpenAPI operation can exist without a handler
|
||||
- All contract violations fail at application startup
|
||||
- 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
|
||||
@@ -131,11 +184,13 @@ enforcement over convenience shortcuts.
|
||||
from . import app
|
||||
from . import binder
|
||||
from . import loader
|
||||
from . import client
|
||||
from . import errors
|
||||
|
||||
__all__ = [
|
||||
"app",
|
||||
"binder",
|
||||
"loader",
|
||||
"client",
|
||||
"errors",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user