docs(templates): document CRUD and model CRUD apps and expose them in mkdocs
- Add comprehensive module and function docstrings to crud_app and model_app templates - Document OpenAPI-first guarantees, non-goals, and usage patterns in templates - Add template-level __init__.py files with CLI and client usage examples - Update mkdocs.yml to include CRUD and model-based CRUD template documentation - Ensure template documentation follows strict package-bound mkdocstrings rules
This commit is contained in:
3
docs/openapi_first/templates/crud_app/data.md
Normal file
3
docs/openapi_first/templates/crud_app/data.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Data
|
||||||
|
|
||||||
|
::: openapi_first.templates.crud_app.data
|
||||||
3
docs/openapi_first/templates/crud_app/index.md
Normal file
3
docs/openapi_first/templates/crud_app/index.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Crud App
|
||||||
|
|
||||||
|
::: openapi_first.templates.crud_app
|
||||||
3
docs/openapi_first/templates/crud_app/main.md
Normal file
3
docs/openapi_first/templates/crud_app/main.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Main
|
||||||
|
|
||||||
|
::: openapi_first.templates.crud_app.main
|
||||||
3
docs/openapi_first/templates/crud_app/routes.md
Normal file
3
docs/openapi_first/templates/crud_app/routes.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Routes
|
||||||
|
|
||||||
|
::: openapi_first.templates.crud_app.routes
|
||||||
3
docs/openapi_first/templates/crud_app/test_crud_app.md
Normal file
3
docs/openapi_first/templates/crud_app/test_crud_app.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Test Crud App
|
||||||
|
|
||||||
|
::: openapi_first.templates.crud_app.test_crud_app
|
||||||
3
docs/openapi_first/templates/model_app/data.md
Normal file
3
docs/openapi_first/templates/model_app/data.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Data
|
||||||
|
|
||||||
|
::: openapi_first.templates.model_app.data
|
||||||
3
docs/openapi_first/templates/model_app/index.md
Normal file
3
docs/openapi_first/templates/model_app/index.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Model App
|
||||||
|
|
||||||
|
::: openapi_first.templates.model_app
|
||||||
3
docs/openapi_first/templates/model_app/main.md
Normal file
3
docs/openapi_first/templates/model_app/main.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Main
|
||||||
|
|
||||||
|
::: openapi_first.templates.model_app.main
|
||||||
3
docs/openapi_first/templates/model_app/models.md
Normal file
3
docs/openapi_first/templates/model_app/models.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Models
|
||||||
|
|
||||||
|
::: openapi_first.templates.model_app.models
|
||||||
3
docs/openapi_first/templates/model_app/routes.md
Normal file
3
docs/openapi_first/templates/model_app/routes.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Routes
|
||||||
|
|
||||||
|
::: openapi_first.templates.model_app.routes
|
||||||
3
docs/openapi_first/templates/model_app/test_model_app.md
Normal file
3
docs/openapi_first/templates/model_app/test_model_app.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Test Model App
|
||||||
|
|
||||||
|
::: openapi_first.templates.model_app.test_model_app
|
||||||
@@ -49,10 +49,9 @@ nav:
|
|||||||
|
|
||||||
- Templates:
|
- Templates:
|
||||||
- Home: openapi_first/templates/index.md
|
- Home: openapi_first/templates/index.md
|
||||||
- Health App:
|
- Health App: openapi_first/templates/health_app/index.md
|
||||||
- Home: openapi_first/templates/health_app/index.md
|
- CRUD App: openapi_first/templates/crud_app/index.md
|
||||||
- App: openapi_first/templates/health_app/main.md
|
- Model App: openapi_first/templates/model_app/index.md
|
||||||
- Routes: openapi_first/templates/health_app/routes.md
|
|
||||||
|
|
||||||
- Errors:
|
- Errors:
|
||||||
- Error Hierarchy: openapi_first/errors.md
|
- Error Hierarchy: openapi_first/errors.md
|
||||||
|
|||||||
99
openapi_first/templates/crud_app/__init__.py
Normal file
99
openapi_first/templates/crud_app/__init__.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
"""
|
||||||
|
OpenAPI-first CRUD application template.
|
||||||
|
|
||||||
|
This package contains a complete, minimal example of an OpenAPI-first
|
||||||
|
CRUD service built using the ``openapi_first`` library.
|
||||||
|
|
||||||
|
The application is assembled exclusively from:
|
||||||
|
- an OpenAPI specification (``openapi.yaml``)
|
||||||
|
- a handler namespace implementing CRUD operations (``routes``)
|
||||||
|
- an in-memory mock data store (``data``)
|
||||||
|
|
||||||
|
All HTTP routes, methods, schemas, and operation bindings are defined
|
||||||
|
in the OpenAPI specification and enforced at application startup.
|
||||||
|
No decorator-driven routing or implicit framework behavior is used.
|
||||||
|
|
||||||
|
This template demonstrates:
|
||||||
|
- operationId-driven server-side route binding
|
||||||
|
- explicit HTTP status code control in handlers
|
||||||
|
- operationId-driven client usage against the same OpenAPI contract
|
||||||
|
- end-to-end validation using in-memory data and tests
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
Scaffolding via CLI
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
Create a new CRUD example service using the bundled template:
|
||||||
|
|
||||||
|
openapi-first crud_app
|
||||||
|
|
||||||
|
Create the service in a custom directory:
|
||||||
|
|
||||||
|
openapi-first crud_app my-crud-service
|
||||||
|
|
||||||
|
List all available application templates:
|
||||||
|
|
||||||
|
openapi-first --list
|
||||||
|
|
||||||
|
The CLI copies template files verbatim into the target directory.
|
||||||
|
No code is generated or modified beyond the copied scaffold.
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
Client Usage Example
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
The same OpenAPI specification used by the server can be used to
|
||||||
|
construct a strict, operationId-driven HTTP client.
|
||||||
|
|
||||||
|
Example client calls for CRUD operations:
|
||||||
|
|
||||||
|
from openapi_first.loader import load_openapi
|
||||||
|
from openapi_first.client import OpenAPIClient
|
||||||
|
|
||||||
|
spec = load_openapi("openapi.yaml")
|
||||||
|
client = OpenAPIClient(spec)
|
||||||
|
|
||||||
|
# List items
|
||||||
|
response = client.list_items()
|
||||||
|
|
||||||
|
# Get item by ID
|
||||||
|
response = client.get_item(
|
||||||
|
path_params={"item_id": 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create item
|
||||||
|
response = client.create_item(
|
||||||
|
body={"name": "Orange", "price": 0.8}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update item
|
||||||
|
response = client.update_item(
|
||||||
|
path_params={"item_id": 1},
|
||||||
|
body={"name": "Green Apple", "price": 0.6},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete item
|
||||||
|
response = client.delete_item(
|
||||||
|
path_params={"item_id": 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
Client guarantees:
|
||||||
|
- One callable per OpenAPI ``operationId``
|
||||||
|
- No hardcoded URLs or HTTP methods in user code
|
||||||
|
- Path and request parameters must match the OpenAPI specification
|
||||||
|
- Invalid or incomplete OpenAPI specs fail at client construction time
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
Non-Goals
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
This template is intentionally minimal and is NOT:
|
||||||
|
- production-ready
|
||||||
|
- persistent or concurrency-safe
|
||||||
|
- a reference architecture for data storage
|
||||||
|
|
||||||
|
It exists solely as a copyable example for learning, testing, and
|
||||||
|
bootstrapping OpenAPI-first services.
|
||||||
|
|
||||||
|
This package is not part of the ``openapi_first`` library API surface.
|
||||||
|
"""
|
||||||
@@ -3,27 +3,88 @@ In-memory mock data store for CRUD example.
|
|||||||
|
|
||||||
This module intentionally avoids persistence and concurrency guarantees.
|
This module intentionally avoids persistence and concurrency guarantees.
|
||||||
It is suitable for demos, tests, and scaffolding only.
|
It is suitable for demos, tests, and scaffolding only.
|
||||||
|
|
||||||
|
It intentionally avoids
|
||||||
|
- persistence
|
||||||
|
- concurrency guarantees
|
||||||
|
- validation
|
||||||
|
- error handling
|
||||||
|
|
||||||
|
The implementation is suitable for:
|
||||||
|
- demonstrations
|
||||||
|
- tests
|
||||||
|
- scaffolding and example services
|
||||||
|
|
||||||
|
It is explicitly NOT suitable for production use.
|
||||||
|
|
||||||
|
This module is not part of the ``openapi_first`` library API surface.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
|
# In-memory storage keyed by item ID.
|
||||||
_items: Dict[int, dict] = {
|
_items: Dict[int, dict] = {
|
||||||
1: {"id": 1, "name": "Apple", "price": 0.5},
|
1: {"id": 1, "name": "Apple", "price": 0.5},
|
||||||
2: {"id": 2, "name": "Banana", "price": 0.3},
|
2: {"id": 2, "name": "Banana", "price": 0.3},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Auto-incrementing ID counter.
|
||||||
_next_id = 3
|
_next_id = 3
|
||||||
|
|
||||||
|
|
||||||
def list_items():
|
def list_items():
|
||||||
|
"""
|
||||||
|
Return all items in the data store.
|
||||||
|
|
||||||
|
This function performs no filtering, pagination, or sorting.
|
||||||
|
The returned collection reflects the current in-memory state.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[dict]
|
||||||
|
A list of item representations.
|
||||||
|
"""
|
||||||
return list(_items.values())
|
return list(_items.values())
|
||||||
|
|
||||||
|
|
||||||
def get_item(item_id: int):
|
def get_item(item_id: int):
|
||||||
|
"""
|
||||||
|
Retrieve a single item by ID.
|
||||||
|
|
||||||
|
This function assumes the item exists and will raise ``KeyError``
|
||||||
|
if the ID is not present in the store.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
item_id : int
|
||||||
|
Identifier of the item to retrieve.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict
|
||||||
|
The stored item representation.
|
||||||
|
"""
|
||||||
return _items[item_id]
|
return _items[item_id]
|
||||||
|
|
||||||
|
|
||||||
def create_item(payload: dict):
|
def create_item(payload: dict):
|
||||||
|
"""
|
||||||
|
Create a new item in the data store.
|
||||||
|
|
||||||
|
A new integer ID is assigned automatically. No validation is
|
||||||
|
performed on the provided payload.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
payload : dict
|
||||||
|
Item attributes excluding the ``id`` field.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict
|
||||||
|
The newly created item, including its assigned ID.
|
||||||
|
"""
|
||||||
global _next_id
|
global _next_id
|
||||||
item = {"id": _next_id, **payload}
|
item = {"id": _next_id, **payload}
|
||||||
_items[_next_id] = item
|
_items[_next_id] = item
|
||||||
@@ -32,10 +93,40 @@ def create_item(payload: dict):
|
|||||||
|
|
||||||
|
|
||||||
def update_item(item_id: int, payload: dict):
|
def update_item(item_id: int, payload: dict):
|
||||||
|
"""
|
||||||
|
Replace an existing item in the data store.
|
||||||
|
|
||||||
|
This function overwrites the existing item entirely and does not
|
||||||
|
perform partial updates or validation. If the item does not exist,
|
||||||
|
it will be created implicitly.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
item_id : int
|
||||||
|
Identifier of the item to update.
|
||||||
|
payload : dict
|
||||||
|
Item attributes excluding the ``id`` field.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict
|
||||||
|
The updated item representation.
|
||||||
|
"""
|
||||||
item = {"id": item_id, **payload}
|
item = {"id": item_id, **payload}
|
||||||
_items[item_id] = item
|
_items[item_id] = item
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
def delete_item(item_id: int):
|
def delete_item(item_id: int):
|
||||||
|
"""
|
||||||
|
Remove an item from the data store.
|
||||||
|
|
||||||
|
This function assumes the item exists and will raise ``KeyError``
|
||||||
|
if the ID is not present.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
item_id : int
|
||||||
|
Identifier of the item to delete.
|
||||||
|
"""
|
||||||
del _items[item_id]
|
del _items[item_id]
|
||||||
|
|||||||
@@ -1,3 +1,30 @@
|
|||||||
|
"""
|
||||||
|
Application entry point for an OpenAPI-first CRUD example service.
|
||||||
|
|
||||||
|
This module constructs a FastAPI application exclusively from an
|
||||||
|
OpenAPI specification and a handler namespace, without using
|
||||||
|
decorator-driven routing.
|
||||||
|
|
||||||
|
All HTTP routes, methods, request/response schemas, and operation
|
||||||
|
bindings are defined in the OpenAPI document referenced by
|
||||||
|
``openapi_path``. Python callables defined in the ``routes`` module are
|
||||||
|
bound to OpenAPI operations strictly via ``operationId``.
|
||||||
|
|
||||||
|
This module contains no routing logic, persistence concerns, or
|
||||||
|
framework configuration beyond application assembly.
|
||||||
|
|
||||||
|
Design guarantees:
|
||||||
|
- OpenAPI is the single source of truth
|
||||||
|
- No undocumented routes can exist
|
||||||
|
- Every OpenAPI operationId must resolve to exactly one handler
|
||||||
|
- All contract violations fail at application startup
|
||||||
|
|
||||||
|
This file is intended to be used as the ASGI entry point.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
uvicorn main:app
|
||||||
|
"""
|
||||||
|
|
||||||
from openapi_first.app import OpenAPIFirstApp
|
from openapi_first.app import OpenAPIFirstApp
|
||||||
import routes
|
import routes
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,19 @@ CRUD route handlers bound via OpenAPI operationId.
|
|||||||
|
|
||||||
These handlers explicitly control HTTP status codes to ensure
|
These handlers explicitly control HTTP status codes to ensure
|
||||||
runtime behavior matches the OpenAPI contract.
|
runtime behavior matches the OpenAPI contract.
|
||||||
|
|
||||||
|
This module defines OpenAPI-bound operation handlers for a simple CRUD
|
||||||
|
service. Functions in this module are bound to HTTP routes exclusively
|
||||||
|
via OpenAPI ``operationId`` values.
|
||||||
|
|
||||||
|
Handlers explicitly control HTTP response status codes to ensure runtime
|
||||||
|
behavior matches the OpenAPI contract. Error conditions are translated
|
||||||
|
into explicit HTTP responses rather than relying on implicit framework
|
||||||
|
behavior.
|
||||||
|
|
||||||
|
No routing decorators or path definitions appear in this module. All
|
||||||
|
routing, HTTP methods, and schemas are defined in the OpenAPI
|
||||||
|
specification.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import Response, HTTPException
|
from fastapi import Response, HTTPException
|
||||||
@@ -17,10 +30,42 @@ from data import (
|
|||||||
|
|
||||||
|
|
||||||
def list_items():
|
def list_items():
|
||||||
|
"""
|
||||||
|
List all items.
|
||||||
|
|
||||||
|
Implements the OpenAPI operation identified by
|
||||||
|
``operationId: list_items``.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[dict]
|
||||||
|
A list of item representations.
|
||||||
|
"""
|
||||||
return _list_items()
|
return _list_items()
|
||||||
|
|
||||||
|
|
||||||
def get_item(item_id: int):
|
def get_item(item_id: int):
|
||||||
|
"""
|
||||||
|
Retrieve a single item by ID.
|
||||||
|
|
||||||
|
Implements the OpenAPI operation identified by
|
||||||
|
``operationId: get_item``.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
item_id : int
|
||||||
|
Identifier of the item to retrieve.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict
|
||||||
|
The requested item.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
HTTPException
|
||||||
|
404 if the item does not exist.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return _get_item(item_id)
|
return _get_item(item_id)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -28,12 +73,53 @@ def get_item(item_id: int):
|
|||||||
|
|
||||||
|
|
||||||
def create_item(payload: dict, response: Response):
|
def create_item(payload: dict, response: Response):
|
||||||
|
"""
|
||||||
|
Create a new item.
|
||||||
|
|
||||||
|
Implements the OpenAPI operation identified by
|
||||||
|
``operationId: create_item``.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
payload : dict
|
||||||
|
Item attributes excluding the ``id`` field.
|
||||||
|
response : fastapi.Response
|
||||||
|
Response object used to set the HTTP status code.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict
|
||||||
|
The newly created item.
|
||||||
|
"""
|
||||||
item = _create_item(payload)
|
item = _create_item(payload)
|
||||||
response.status_code = 201
|
response.status_code = 201
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
def update_item(item_id: int, payload: dict):
|
def update_item(item_id: int, payload: dict):
|
||||||
|
"""
|
||||||
|
Update an existing item.
|
||||||
|
|
||||||
|
Implements the OpenAPI operation identified by
|
||||||
|
``operationId: update_item``.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
item_id : int
|
||||||
|
Identifier of the item to update.
|
||||||
|
payload : dict
|
||||||
|
Item attributes excluding the ``id`` field.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict
|
||||||
|
The updated item.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
HTTPException
|
||||||
|
404 if the item does not exist.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return _update_item(item_id, payload)
|
return _update_item(item_id, payload)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -41,10 +127,32 @@ def update_item(item_id: int, payload: dict):
|
|||||||
|
|
||||||
|
|
||||||
def delete_item(item_id: int, response: Response):
|
def delete_item(item_id: int, response: Response):
|
||||||
|
"""
|
||||||
|
Delete an existing item.
|
||||||
|
|
||||||
|
Implements the OpenAPI operation identified by
|
||||||
|
``operationId: delete_item``.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
item_id : int
|
||||||
|
Identifier of the item to delete.
|
||||||
|
response : fastapi.Response
|
||||||
|
Response object used to set the HTTP status code.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
None
|
||||||
|
No content.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
HTTPException
|
||||||
|
404 if the item does not exist.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
_delete_item(item_id)
|
_delete_item(item_id)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise HTTPException(status_code=404, detail="Item not found")
|
raise HTTPException(status_code=404, detail="Item not found")
|
||||||
|
|
||||||
response.status_code = 204
|
response.status_code = 204
|
||||||
return None
|
|
||||||
|
|||||||
@@ -3,11 +3,20 @@ End-to-end tests for the OpenAPI-first CRUD example app.
|
|||||||
|
|
||||||
These tests validate that all CRUD operations behave correctly
|
These tests validate that all CRUD operations behave correctly
|
||||||
against the in-memory mock data store.
|
against the in-memory mock data store.
|
||||||
|
- OpenAPI specification loading
|
||||||
|
- OperationId-driven route binding on the server
|
||||||
|
- OperationId-driven client invocation
|
||||||
|
- Correct HTTP status codes and response payloads
|
||||||
|
|
||||||
|
The tests exercise all CRUD operations against an in-memory mock data
|
||||||
|
store and assume deterministic behavior within a single process.
|
||||||
|
|
||||||
The tests assume:
|
The tests assume:
|
||||||
- OpenAPI-first route binding
|
- OpenAPI-first route binding
|
||||||
- In-memory storage (no persistence guarantees)
|
- In-memory storage (no persistence guarantees)
|
||||||
- Deterministic behavior in a single process
|
- Deterministic behavior in a single process
|
||||||
|
- One-to-one correspondence between OpenAPI operationId values and
|
||||||
|
server/client callables
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|||||||
102
openapi_first/templates/model_app/__init__.py
Normal file
102
openapi_first/templates/model_app/__init__.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
"""
|
||||||
|
OpenAPI-first model-based CRUD application template.
|
||||||
|
|
||||||
|
This package contains a complete, minimal example of an OpenAPI-first
|
||||||
|
CRUD service that uses explicit Pydantic domain models for request and
|
||||||
|
response schemas.
|
||||||
|
|
||||||
|
The application is assembled exclusively from:
|
||||||
|
- an OpenAPI specification (``openapi.yaml``)
|
||||||
|
- a handler namespace implementing CRUD operations (``routes``)
|
||||||
|
- Pydantic domain models (``models``)
|
||||||
|
- an in-memory mock data store (``data``)
|
||||||
|
|
||||||
|
All HTTP routes, methods, schemas, and operation bindings are defined
|
||||||
|
in the OpenAPI specification and enforced at application startup.
|
||||||
|
No decorator-driven routing or implicit framework behavior is used.
|
||||||
|
|
||||||
|
This template demonstrates:
|
||||||
|
- operationId-driven server-side route binding
|
||||||
|
- explicit request and response modeling with Pydantic
|
||||||
|
- explicit HTTP status code control in handlers
|
||||||
|
- operationId-driven client usage against the same OpenAPI contract
|
||||||
|
- end-to-end validation using in-memory data and tests
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
Scaffolding via CLI
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
Create a new model-based CRUD example service using the bundled template:
|
||||||
|
|
||||||
|
openapi-first model_app
|
||||||
|
|
||||||
|
Create the service in a custom directory:
|
||||||
|
|
||||||
|
openapi-first model_app my-model-service
|
||||||
|
|
||||||
|
List all available application templates:
|
||||||
|
|
||||||
|
openapi-first --list
|
||||||
|
|
||||||
|
The CLI copies template files verbatim into the target directory.
|
||||||
|
No code is generated or modified beyond the copied scaffold.
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
Client Usage Example
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
The same OpenAPI specification used by the server can be used to
|
||||||
|
construct a strict, operationId-driven HTTP client.
|
||||||
|
|
||||||
|
Example client calls for model-based CRUD operations:
|
||||||
|
|
||||||
|
from openapi_first.loader import load_openapi
|
||||||
|
from openapi_first.client import OpenAPIClient
|
||||||
|
|
||||||
|
spec = load_openapi("openapi.yaml")
|
||||||
|
client = OpenAPIClient(spec)
|
||||||
|
|
||||||
|
# List items
|
||||||
|
response = client.list_items()
|
||||||
|
|
||||||
|
# Get item by ID
|
||||||
|
response = client.get_item(
|
||||||
|
path_params={"item_id": 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create item
|
||||||
|
response = client.create_item(
|
||||||
|
body={"name": "Orange", "price": 0.8}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update item
|
||||||
|
response = client.update_item(
|
||||||
|
path_params={"item_id": 1},
|
||||||
|
body={"name": "Green Apple", "price": 0.6},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete item
|
||||||
|
response = client.delete_item(
|
||||||
|
path_params={"item_id": 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
Client guarantees:
|
||||||
|
- One callable per OpenAPI ``operationId``
|
||||||
|
- No hardcoded URLs or HTTP methods in user code
|
||||||
|
- Request and response payloads conform to Pydantic models
|
||||||
|
- Invalid or incomplete OpenAPI specs fail at client construction time
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
Non-Goals
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
This template is intentionally minimal and is NOT:
|
||||||
|
- production-ready
|
||||||
|
- persistent or concurrency-safe
|
||||||
|
- a reference architecture for data storage
|
||||||
|
|
||||||
|
It exists solely as a copyable example for learning, testing, and
|
||||||
|
bootstrapping OpenAPI-first services.
|
||||||
|
|
||||||
|
This package is not part of the ``openapi_first`` library API surface.
|
||||||
|
"""
|
||||||
@@ -2,29 +2,86 @@
|
|||||||
In-memory data store using Pydantic models.
|
In-memory data store using Pydantic models.
|
||||||
|
|
||||||
This module is NOT thread-safe and is intended for demos and scaffolds only.
|
This module is NOT thread-safe and is intended for demos and scaffolds only.
|
||||||
|
This module provides a minimal, process-local data store for the
|
||||||
|
model-based CRUD example application. It stores and returns domain
|
||||||
|
objects defined using Pydantic models and is intended solely for
|
||||||
|
demonstration and scaffolding purposes.
|
||||||
|
|
||||||
|
The implementation intentionally avoids:
|
||||||
|
- persistence
|
||||||
|
- concurrency guarantees
|
||||||
|
- transactional semantics
|
||||||
|
- validation beyond what Pydantic provides
|
||||||
|
|
||||||
|
It is not part of the ``openapi_first`` library API surface.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from models import Item, ItemCreate
|
from models import Item, ItemCreate
|
||||||
|
|
||||||
|
|
||||||
|
# In-memory storage keyed by item ID.
|
||||||
_items: Dict[int, Item] = {
|
_items: Dict[int, Item] = {
|
||||||
1: Item(id=1, name="Apple", price=0.5),
|
1: Item(id=1, name="Apple", price=0.5),
|
||||||
2: Item(id=2, name="Banana", price=0.3),
|
2: Item(id=2, name="Banana", price=0.3),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Auto-incrementing identifier.
|
||||||
_next_id = 3
|
_next_id = 3
|
||||||
|
|
||||||
|
|
||||||
def list_items() -> list[Item]:
|
def list_items() -> list[Item]:
|
||||||
|
"""
|
||||||
|
Return all items in the data store.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[Item]
|
||||||
|
A list of item domain objects.
|
||||||
|
"""
|
||||||
return list(_items.values())
|
return list(_items.values())
|
||||||
|
|
||||||
|
|
||||||
def get_item(item_id: int) -> Item:
|
def get_item(item_id: int) -> Item:
|
||||||
|
"""
|
||||||
|
Retrieve a single item by ID.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
item_id : int
|
||||||
|
Identifier of the item to retrieve.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Item
|
||||||
|
The requested item.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
KeyError
|
||||||
|
If the item does not exist.
|
||||||
|
"""
|
||||||
return _items[item_id]
|
return _items[item_id]
|
||||||
|
|
||||||
|
|
||||||
def create_item(payload: ItemCreate) -> Item:
|
def create_item(payload: ItemCreate) -> Item:
|
||||||
|
"""
|
||||||
|
Create a new item in the data store.
|
||||||
|
|
||||||
|
A new identifier is assigned automatically. No additional validation
|
||||||
|
is performed beyond Pydantic model validation.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
payload : ItemCreate
|
||||||
|
Data required to create a new item.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Item
|
||||||
|
The newly created item.
|
||||||
|
"""
|
||||||
global _next_id
|
global _next_id
|
||||||
item = Item(id=_next_id, **payload.model_dump())
|
item = Item(id=_next_id, **payload.model_dump())
|
||||||
_items[_next_id] = item
|
_items[_next_id] = item
|
||||||
@@ -33,6 +90,29 @@ def create_item(payload: ItemCreate) -> Item:
|
|||||||
|
|
||||||
|
|
||||||
def update_item(item_id: int, payload: ItemCreate) -> Item:
|
def update_item(item_id: int, payload: ItemCreate) -> Item:
|
||||||
|
"""
|
||||||
|
Replace an existing item in the data store.
|
||||||
|
|
||||||
|
This function performs a full replacement of the stored item.
|
||||||
|
Partial updates are not supported.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
item_id : int
|
||||||
|
Identifier of the item to update.
|
||||||
|
payload : ItemCreate
|
||||||
|
New item data.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Item
|
||||||
|
The updated item.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
KeyError
|
||||||
|
If the item does not exist.
|
||||||
|
"""
|
||||||
if item_id not in _items:
|
if item_id not in _items:
|
||||||
raise KeyError(item_id)
|
raise KeyError(item_id)
|
||||||
item = Item(id=item_id, **payload.model_dump())
|
item = Item(id=item_id, **payload.model_dump())
|
||||||
@@ -41,4 +121,17 @@ def update_item(item_id: int, payload: ItemCreate) -> Item:
|
|||||||
|
|
||||||
|
|
||||||
def delete_item(item_id: int) -> None:
|
def delete_item(item_id: int) -> None:
|
||||||
|
"""
|
||||||
|
Remove an item from the data store.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
item_id : int
|
||||||
|
Identifier of the item to delete.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
KeyError
|
||||||
|
If the item does not exist.
|
||||||
|
"""
|
||||||
del _items[item_id]
|
del _items[item_id]
|
||||||
|
|||||||
@@ -1,3 +1,30 @@
|
|||||||
|
"""
|
||||||
|
Application entry point for an OpenAPI-first model-based CRUD example service.
|
||||||
|
|
||||||
|
This module constructs a FastAPI application exclusively from an
|
||||||
|
OpenAPI specification and a handler namespace, without using
|
||||||
|
decorator-driven routing.
|
||||||
|
|
||||||
|
All HTTP routes, methods, request/response schemas, and operation
|
||||||
|
bindings are defined in the OpenAPI document referenced by
|
||||||
|
``openapi_path``. Python callables defined in the ``routes`` module are
|
||||||
|
bound to OpenAPI operations strictly via ``operationId``.
|
||||||
|
|
||||||
|
This module contains no routing logic, persistence concerns, or
|
||||||
|
framework configuration beyond application assembly.
|
||||||
|
|
||||||
|
Design guarantees:
|
||||||
|
- OpenAPI is the single source of truth
|
||||||
|
- No undocumented routes can exist
|
||||||
|
- Every OpenAPI operationId must resolve to exactly one handler
|
||||||
|
- All contract violations fail at application startup
|
||||||
|
|
||||||
|
This file is intended to be used as the ASGI entry point.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
uvicorn main:app
|
||||||
|
"""
|
||||||
|
|
||||||
from openapi_first.app import OpenAPIFirstApp
|
from openapi_first.app import OpenAPIFirstApp
|
||||||
import routes
|
import routes
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,50 @@
|
|||||||
"""
|
"""
|
||||||
Pydantic domain models for the CRUD example.
|
Pydantic domain models for the CRUD example.
|
||||||
|
|
||||||
|
This module defines Pydantic models that represent the domain entities
|
||||||
|
used by the service. These models are referenced by the OpenAPI
|
||||||
|
specification for request and response schemas.
|
||||||
|
|
||||||
|
The models are declarative and framework-agnostic. They contain no
|
||||||
|
persistence logic, validation beyond type constraints, or business
|
||||||
|
behavior.
|
||||||
|
|
||||||
|
This module is not part of the ``openapi_first`` library API surface.
|
||||||
|
It exists solely to support the example application template.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class ItemBase(BaseModel):
|
class ItemBase(BaseModel):
|
||||||
|
"""
|
||||||
|
Base domain model for an item.
|
||||||
|
|
||||||
|
Defines fields common to all item representations.
|
||||||
|
"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
price: float
|
price: float
|
||||||
|
|
||||||
|
|
||||||
class ItemCreate(ItemBase):
|
class ItemCreate(ItemBase):
|
||||||
|
"""
|
||||||
|
Domain model for item creation requests.
|
||||||
|
|
||||||
|
This model is used for request bodies when creating new items.
|
||||||
|
It intentionally excludes the ``id`` field, which is assigned
|
||||||
|
by the service.
|
||||||
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Item(ItemBase):
|
class Item(ItemBase):
|
||||||
|
"""
|
||||||
|
Domain model for a persisted item.
|
||||||
|
|
||||||
|
This model represents the full item state returned in responses,
|
||||||
|
including the server-assigned identifier.
|
||||||
|
"""
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
"""
|
"""
|
||||||
CRUD route handlers bound via OpenAPI operationId.
|
CRUD route handlers bound via OpenAPI operationId.
|
||||||
|
|
||||||
|
This module defines OpenAPI-bound operation handlers for a model-based
|
||||||
|
CRUD service. Functions in this module are bound to HTTP routes
|
||||||
|
exclusively via OpenAPI ``operationId`` values.
|
||||||
|
|
||||||
|
Handlers explicitly control HTTP response status codes to ensure runtime
|
||||||
|
behavior matches the OpenAPI contract. Domain models defined using
|
||||||
|
Pydantic are used for request and response payloads.
|
||||||
|
|
||||||
|
No routing decorators, path definitions, or implicit framework behavior
|
||||||
|
appear in this module. All routing, HTTP methods, and schemas are defined
|
||||||
|
in the OpenAPI specification.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import Response, HTTPException
|
from fastapi import Response, HTTPException
|
||||||
@@ -15,10 +27,42 @@ from data import (
|
|||||||
|
|
||||||
|
|
||||||
def list_items():
|
def list_items():
|
||||||
|
"""
|
||||||
|
List all items.
|
||||||
|
|
||||||
|
Implements the OpenAPI operation identified by
|
||||||
|
``operationId: list_items``.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[Item]
|
||||||
|
A list of item domain objects.
|
||||||
|
"""
|
||||||
return _list_items()
|
return _list_items()
|
||||||
|
|
||||||
|
|
||||||
def get_item(item_id: int):
|
def get_item(item_id: int):
|
||||||
|
"""
|
||||||
|
Retrieve a single item by ID.
|
||||||
|
|
||||||
|
Implements the OpenAPI operation identified by
|
||||||
|
``operationId: get_item``.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
item_id : int
|
||||||
|
Identifier of the item to retrieve.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Item
|
||||||
|
The requested item.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
HTTPException
|
||||||
|
404 if the item does not exist.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return _get_item(item_id)
|
return _get_item(item_id)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -26,12 +70,53 @@ def get_item(item_id: int):
|
|||||||
|
|
||||||
|
|
||||||
def create_item(payload: ItemCreate, response: Response):
|
def create_item(payload: ItemCreate, response: Response):
|
||||||
|
"""
|
||||||
|
Create a new item.
|
||||||
|
|
||||||
|
Implements the OpenAPI operation identified by
|
||||||
|
``operationId: create_item``.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
payload : ItemCreate
|
||||||
|
Request body describing the item to create.
|
||||||
|
response : fastapi.Response
|
||||||
|
Response object used to set the HTTP status code.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Item
|
||||||
|
The newly created item.
|
||||||
|
"""
|
||||||
item = _create_item(payload)
|
item = _create_item(payload)
|
||||||
response.status_code = 201
|
response.status_code = 201
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
def update_item(item_id: int, payload: ItemCreate):
|
def update_item(item_id: int, payload: ItemCreate):
|
||||||
|
"""
|
||||||
|
Update an existing item.
|
||||||
|
|
||||||
|
Implements the OpenAPI operation identified by
|
||||||
|
``operationId: update_item``.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
item_id : int
|
||||||
|
Identifier of the item to update.
|
||||||
|
payload : ItemCreate
|
||||||
|
New item data.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Item
|
||||||
|
The updated item.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
HTTPException
|
||||||
|
404 if the item does not exist.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return _update_item(item_id, payload)
|
return _update_item(item_id, payload)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -39,6 +124,29 @@ def update_item(item_id: int, payload: ItemCreate):
|
|||||||
|
|
||||||
|
|
||||||
def delete_item(item_id: int, response: Response):
|
def delete_item(item_id: int, response: Response):
|
||||||
|
"""
|
||||||
|
Delete an existing item.
|
||||||
|
|
||||||
|
Implements the OpenAPI operation identified by
|
||||||
|
``operationId: delete_item``.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
item_id : int
|
||||||
|
Identifier of the item to delete.
|
||||||
|
response : fastapi.Response
|
||||||
|
Response object used to set the HTTP status code.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
None
|
||||||
|
No content.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
HTTPException
|
||||||
|
404 if the item does not exist.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
_delete_item(item_id)
|
_delete_item(item_id)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ End-to-end tests for the OpenAPI-first model CRUD example app.
|
|||||||
|
|
||||||
These tests validate that all CRUD operations behave correctly
|
These tests validate that all CRUD operations behave correctly
|
||||||
against the in-memory mock data store using Pydantic models.
|
against the in-memory mock data store using Pydantic models.
|
||||||
|
- OpenAPI specification loading
|
||||||
|
- OperationId-driven route binding on the server
|
||||||
|
- OperationId-driven client invocation
|
||||||
|
- Pydantic model-based request and response handling
|
||||||
|
|
||||||
|
All CRUD operations are exercised against an in-memory mock data store
|
||||||
|
backed by Pydantic domain models.
|
||||||
|
|
||||||
The tests assume:
|
The tests assume:
|
||||||
- OpenAPI-first route binding
|
- OpenAPI-first route binding
|
||||||
|
|||||||
Reference in New Issue
Block a user