docs: add OpenAPI-first wiki (overview, components, use cases for templates/client/codegen, design, security, error handling, testing) and refresh lib docs index
This commit is contained in:
@@ -70,6 +70,23 @@ class OpenAPIFirstApp(FastAPI):
|
||||
"""
|
||||
FastAPI application enforcing OpenAPI-first design.
|
||||
|
||||
Args:
|
||||
openapi_path (str):
|
||||
Filesystem path to the OpenAPI 3.x specification file. This
|
||||
specification is treated as the authoritative API contract.
|
||||
routes_module (ModuleType):
|
||||
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.
|
||||
|
||||
Notes:
|
||||
**Responsibilities:**
|
||||
|
||||
@@ -92,7 +109,7 @@ class OpenAPIFirstApp(FastAPI):
|
||||
|
||||
Example:
|
||||
```python
|
||||
from openapi_first import OpenAPIFirstApp
|
||||
from openapi_first.app import OpenAPIFirstApp
|
||||
import app.routes as routes
|
||||
|
||||
app = OpenAPIFirstApp(
|
||||
@@ -110,26 +127,6 @@ class OpenAPIFirstApp(FastAPI):
|
||||
routes_module: ModuleType,
|
||||
**fastapi_kwargs: Any,
|
||||
):
|
||||
"""
|
||||
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 (ModuleType):
|
||||
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)
|
||||
|
||||
|
||||
@@ -50,6 +50,20 @@ class OpenAPIClient:
|
||||
"""
|
||||
OpenAPI-first HTTP client (`httpx`-based).
|
||||
|
||||
Args:
|
||||
spec (dict[str, Any]):
|
||||
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, spec has no paths, operationIds are
|
||||
missing/duplicate, or required parameters are missing.
|
||||
|
||||
Notes:
|
||||
**Responsibilities:**
|
||||
|
||||
@@ -92,21 +106,6 @@ class OpenAPIClient:
|
||||
base_url: str | None = None,
|
||||
client: httpx.Client | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the OpenAPI client.
|
||||
|
||||
Args:
|
||||
spec (dict[str, Any]):
|
||||
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, 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)
|
||||
|
||||
@@ -40,6 +40,14 @@ class MissingOperationHandler(OpenAPIFirstError):
|
||||
"""
|
||||
Raised when an OpenAPI operation cannot be resolved to a handler.
|
||||
|
||||
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 | None):
|
||||
The operationId declared in the OpenAPI spec, if present.
|
||||
|
||||
Notes:
|
||||
**Scenarios:**
|
||||
|
||||
@@ -55,17 +63,6 @@ class MissingOperationHandler(OpenAPIFirstError):
|
||||
"""
|
||||
|
||||
def __init__(self, *, path: str, method: str, operation_id: str | None = None):
|
||||
"""
|
||||
Initialize the error.
|
||||
|
||||
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 | None):
|
||||
The operationId declared in the OpenAPI spec, if present.
|
||||
"""
|
||||
if operation_id:
|
||||
message = (
|
||||
f"Missing handler for operationId '{operation_id}' "
|
||||
|
||||
@@ -25,15 +25,15 @@ Scaffolding via CLI
|
||||
|
||||
Create a new CRUD example service using the bundled template:
|
||||
|
||||
openapi-first crud_app
|
||||
openapi-first scaffold crud_app
|
||||
|
||||
Create the service in a custom directory:
|
||||
|
||||
openapi-first crud_app my-crud-service
|
||||
openapi-first scaffold crud_app my-crud-service
|
||||
|
||||
List all available application templates:
|
||||
|
||||
openapi-first --list
|
||||
openapi-first scaffold --list
|
||||
|
||||
The CLI copies template files verbatim into the target directory.
|
||||
No code is generated or modified beyond the copied scaffold.
|
||||
|
||||
@@ -33,7 +33,7 @@ _items: Dict[int, dict] = {
|
||||
_next_id = 3
|
||||
|
||||
|
||||
def list_items():
|
||||
def list_items() -> list[dict]:
|
||||
"""
|
||||
Return all items in the data store.
|
||||
|
||||
@@ -48,7 +48,7 @@ def list_items():
|
||||
return list(_items.values())
|
||||
|
||||
|
||||
def get_item(item_id: int):
|
||||
def get_item(item_id: int) -> dict:
|
||||
"""
|
||||
Retrieve a single item by ID.
|
||||
|
||||
@@ -68,7 +68,7 @@ def get_item(item_id: int):
|
||||
return _items[item_id]
|
||||
|
||||
|
||||
def create_item(payload: dict):
|
||||
def create_item(payload: dict) -> dict:
|
||||
"""
|
||||
Create a new item in the data store.
|
||||
|
||||
@@ -92,7 +92,7 @@ def create_item(payload: dict):
|
||||
return item
|
||||
|
||||
|
||||
def update_item(item_id: int, payload: dict):
|
||||
def update_item(item_id: int, payload: dict) -> dict:
|
||||
"""
|
||||
Replace an existing item in the data store.
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ from data import (
|
||||
)
|
||||
|
||||
|
||||
def list_items():
|
||||
def list_items() -> list[dict]:
|
||||
"""
|
||||
List all items.
|
||||
|
||||
@@ -44,7 +44,7 @@ def list_items():
|
||||
return _list_items()
|
||||
|
||||
|
||||
def get_item(item_id: int):
|
||||
def get_item(item_id: int) -> dict:
|
||||
"""
|
||||
Retrieve a single item by ID.
|
||||
|
||||
@@ -72,7 +72,7 @@ def get_item(item_id: int):
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
|
||||
|
||||
def create_item(payload: dict, response: Response):
|
||||
def create_item(payload: dict, response: Response) -> dict:
|
||||
"""
|
||||
Create a new item.
|
||||
|
||||
@@ -83,7 +83,7 @@ def create_item(payload: dict, response: Response):
|
||||
----------
|
||||
payload : dict
|
||||
Item attributes excluding the ``id`` field.
|
||||
response : fastapi.Response
|
||||
response : Response
|
||||
Response object used to set the HTTP status code.
|
||||
|
||||
Returns
|
||||
@@ -96,7 +96,7 @@ def create_item(payload: dict, response: Response):
|
||||
return item
|
||||
|
||||
|
||||
def update_item(item_id: int, payload: dict):
|
||||
def update_item(item_id: int, payload: dict) -> dict:
|
||||
"""
|
||||
Update an existing item.
|
||||
|
||||
@@ -126,7 +126,7 @@ def update_item(item_id: int, payload: dict):
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
|
||||
|
||||
def delete_item(item_id: int, response: Response):
|
||||
def delete_item(item_id: int, response: Response) -> None:
|
||||
"""
|
||||
Delete an existing item.
|
||||
|
||||
@@ -137,7 +137,7 @@ def delete_item(item_id: int, response: Response):
|
||||
----------
|
||||
item_id : int
|
||||
Identifier of the item to delete.
|
||||
response : fastapi.Response
|
||||
response : Response
|
||||
Response object used to set the HTTP status code.
|
||||
|
||||
Returns
|
||||
|
||||
@@ -24,15 +24,15 @@ Scaffolding via CLI
|
||||
Create a new OpenAPI-first health check service using the bundled
|
||||
template:
|
||||
|
||||
openapi-first health_app
|
||||
openapi-first scaffold health_app
|
||||
|
||||
Create the service in a custom directory:
|
||||
|
||||
openapi-first health_app my-health-service
|
||||
openapi-first scaffold health_app my-health-service
|
||||
|
||||
List all available application templates:
|
||||
|
||||
openapi-first --list
|
||||
openapi-first scaffold --list
|
||||
|
||||
The CLI copies template files verbatim into the target directory.
|
||||
No code is generated or modified beyond the copied scaffold.
|
||||
|
||||
@@ -13,7 +13,7 @@ This module serves solely as an operationId namespace.
|
||||
"""
|
||||
|
||||
|
||||
def get_health():
|
||||
def get_health() -> dict:
|
||||
"""
|
||||
Health check operation handler.
|
||||
|
||||
|
||||
@@ -28,15 +28,15 @@ Scaffolding via CLI
|
||||
|
||||
Create a new model-based CRUD example service using the bundled template:
|
||||
|
||||
openapi-first model_app
|
||||
openapi-first scaffold model_app
|
||||
|
||||
Create the service in a custom directory:
|
||||
|
||||
openapi-first model_app my-model-service
|
||||
openapi-first scaffold model_app my-model-service
|
||||
|
||||
List all available application templates:
|
||||
|
||||
openapi-first --list
|
||||
openapi-first scaffold --list
|
||||
|
||||
The CLI copies template files verbatim into the target directory.
|
||||
No code is generated or modified beyond the copied scaffold.
|
||||
|
||||
@@ -62,6 +62,8 @@ def get_item(item_id: int) -> Item:
|
||||
KeyError
|
||||
If the item does not exist.
|
||||
"""
|
||||
if item_id not in _items:
|
||||
raise KeyError(item_id)
|
||||
return _items[item_id]
|
||||
|
||||
|
||||
@@ -134,4 +136,6 @@ def delete_item(item_id: int) -> None:
|
||||
KeyError
|
||||
If the item does not exist.
|
||||
"""
|
||||
if item_id not in _items:
|
||||
raise KeyError(item_id)
|
||||
del _items[item_id]
|
||||
|
||||
@@ -26,7 +26,7 @@ from data import (
|
||||
)
|
||||
|
||||
|
||||
def list_items():
|
||||
def list_items() -> list[Item]:
|
||||
"""
|
||||
List all items.
|
||||
|
||||
@@ -41,7 +41,7 @@ def list_items():
|
||||
return _list_items()
|
||||
|
||||
|
||||
def get_item(item_id: int):
|
||||
def get_item(item_id: int) -> Item:
|
||||
"""
|
||||
Retrieve a single item by ID.
|
||||
|
||||
@@ -69,7 +69,7 @@ def get_item(item_id: int):
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
|
||||
|
||||
def create_item(payload: ItemCreate, response: Response):
|
||||
def create_item(payload: ItemCreate, response: Response) -> Item:
|
||||
"""
|
||||
Create a new item.
|
||||
|
||||
@@ -80,7 +80,7 @@ def create_item(payload: ItemCreate, response: Response):
|
||||
----------
|
||||
payload : ItemCreate
|
||||
Request body describing the item to create.
|
||||
response : fastapi.Response
|
||||
response : Response
|
||||
Response object used to set the HTTP status code.
|
||||
|
||||
Returns
|
||||
@@ -93,7 +93,7 @@ def create_item(payload: ItemCreate, response: Response):
|
||||
return item
|
||||
|
||||
|
||||
def update_item(item_id: int, payload: ItemCreate):
|
||||
def update_item(item_id: int, payload: ItemCreate) -> Item:
|
||||
"""
|
||||
Update an existing item.
|
||||
|
||||
@@ -123,7 +123,7 @@ def update_item(item_id: int, payload: ItemCreate):
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
|
||||
|
||||
def delete_item(item_id: int, response: Response):
|
||||
def delete_item(item_id: int, response: Response) -> None:
|
||||
"""
|
||||
Delete an existing item.
|
||||
|
||||
@@ -134,7 +134,7 @@ def delete_item(item_id: int, response: Response):
|
||||
----------
|
||||
item_id : int
|
||||
Identifier of the item to delete.
|
||||
response : fastapi.Response
|
||||
response : Response
|
||||
Response object used to set the HTTP status code.
|
||||
|
||||
Returns
|
||||
|
||||
@@ -50,11 +50,11 @@ Scaffolding via CLI
|
||||
|
||||
Create a new vet clinic service using the bundled template:
|
||||
|
||||
openapi-first vet_app
|
||||
openapi-first scaffold vet_app
|
||||
|
||||
Create the service in a custom directory:
|
||||
|
||||
openapi-first vet_app my-vet-clinic
|
||||
openapi-first scaffold vet_app my-vet-clinic
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Client Usage Example
|
||||
|
||||
@@ -43,7 +43,6 @@ from data import (
|
||||
create_pet as _create_pet,
|
||||
update_pet as _update_pet,
|
||||
delete_pet as _delete_pet,
|
||||
get_pet as _get_pet,
|
||||
list_appointments as _list_appointments,
|
||||
get_appointment as _get_appointment,
|
||||
create_appointment as _create_appointment,
|
||||
@@ -57,7 +56,7 @@ from data import (
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def list_parents(limit: int = 20, offset: int = 0):
|
||||
def list_parents(limit: int = 20, offset: int = 0) -> dict:
|
||||
"""List parents (paginated).
|
||||
|
||||
Parameters
|
||||
@@ -76,13 +75,15 @@ def list_parents(limit: int = 20, offset: int = 0):
|
||||
return {"total": len(items), "items": items[offset:offset + limit] if limit else items[offset:]}
|
||||
|
||||
|
||||
def create_parent(payload: ParentCreate, response: Response):
|
||||
def create_parent(payload: ParentCreate, response: Response) -> Parent:
|
||||
"""Create a parent.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
payload : ParentCreate
|
||||
Parent data excluding the ``id`` field.
|
||||
response : Response
|
||||
Response object used to set the HTTP status code.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -94,7 +95,7 @@ def create_parent(payload: ParentCreate, response: Response):
|
||||
return parent
|
||||
|
||||
|
||||
def get_parent(id: int):
|
||||
def get_parent(id: int) -> Parent:
|
||||
"""Retrieve a single parent by ID.
|
||||
|
||||
Parameters
|
||||
@@ -118,7 +119,7 @@ def get_parent(id: int):
|
||||
raise HTTPException(status_code=404, detail="Parent not found")
|
||||
|
||||
|
||||
def update_parent(id: int, payload: ParentCreate):
|
||||
def update_parent(id: int, payload: ParentCreate) -> Parent:
|
||||
"""Update an existing parent.
|
||||
|
||||
Parameters
|
||||
@@ -144,13 +145,15 @@ def update_parent(id: int, payload: ParentCreate):
|
||||
raise HTTPException(status_code=404, detail="Parent not found")
|
||||
|
||||
|
||||
def delete_parent(id: int, response: Response):
|
||||
def delete_parent(id: int, response: Response) -> None:
|
||||
"""Delete an existing parent.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id : int
|
||||
Identifier of the parent.
|
||||
response : Response
|
||||
Response object used to set the HTTP status code.
|
||||
|
||||
Raises
|
||||
------
|
||||
@@ -212,7 +215,7 @@ def delete_vet(id: int, response: Response):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def list_treatments():
|
||||
def list_treatments() -> list[Treatment]:
|
||||
"""List treatments (catalogue).
|
||||
|
||||
Returns
|
||||
@@ -298,7 +301,7 @@ def delete_pet(id: int, response: Response):
|
||||
response.status_code = 204
|
||||
|
||||
|
||||
def upload_pet_photo(id: int, file: UploadFile):
|
||||
def upload_pet_photo(id: int, file: UploadFile) -> dict:
|
||||
"""Upload a pet photo.
|
||||
|
||||
Parameters
|
||||
|
||||
Reference in New Issue
Block a user