Main App¶
Cadwyn's standard usage involves a single customized FastAPI app: cadwyn.Cadwyn. It accepts the same arguments as fastapi.FastAPI plus several additional keyword-only arguments. The most important ones are:
- Required
versions: VersionBundledescribes all versions of your app - Optional
api_version_parameter_name: str = "x-api-version"is the parameter that Cadwyn uses for routing to different API versions of your app
After you have defined the main app, you can add versioned API routers to it using Cadwyn.generate_and_include_versioned_routers(*routers)
from cadwyn import Cadwyn, Version, VersionBundle, VersionedAPIRouter
my_version_bundle = VersionBundle(Version("2022-11-16"))
router = VersionedAPIRouter()
@router.get("/users/", tags=["users"])
async def read_users():
return [{"username": "Rick"}, {"username": "Morty"}]
@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
return {"username": username}
app = Cadwyn(versions=my_version_bundle)
app.generate_and_include_versioned_routers(router)
That's it. generate_and_include_versioned_routers() will generate all versions of your routers based on the versions argument.
Routing¶
Cadwyn is built on header-based routing. First, requests are routed to the appropriate API version based on the version header (x-api-version by default). Second, requests are routed by the appropriate url path and method. By default, Cadwyn uses ISO 8601 date-based versions (such as 2022-11-16), but it also supports arbitrary strings as versions. If a user provides a date that does not match any of the versions exactly, Cadwyn selects the closest earlier applicable version. For example, 2022-11-16 in a request can be matched by 2022-11-15 or 2000-01-01 but not by 2022-11-17.
However, header-based routing is the default way to use Cadwyn. Cadwyn can also take the version from a URL path parameter. If you want to use any other form of routing, you can use Cadwyn directly through cadwyn.generate_versioned_routers() or subclass cadwyn.Cadwyn with a different router and middleware. Remember to update the VersionBundle.api_version_var variable each time you route a request to a version. This variable allows Cadwyn to do side effects and data migrations.
VersionedAPIRouter¶
Cadwyn has its own API Router class: cadwyn.VersionedAPIRouter. You are free to use a regular fastapi.APIRouter but cadwyn.VersionedAPIRouter has a special decorator only_exists_in_older_versions(route) which allows you to define routes that have been previously deleted. First you define the route and then add this decorator to it.
Using fastapi-pagination¶
add_pagination(app) from fastapi-pagination does not work with Cadwyn's versioned routes: either the app fails to start with AttributeError: __params_type__, or paginated endpoints fail with UninitializedConfigurationError. Instead, call add_pagination() on your VersionedAPIRouter before you include it:
from fastapi_pagination import Page, add_pagination, paginate
router = VersionedAPIRouter()
@router.get("/users")
async def list_users() -> Page[User]:
return paginate(get_users())
add_pagination(router)
app.generate_and_include_versioned_routers(router)
Alternatively, pass the pagination params to paginate() explicitly:
from typing import Annotated
from fastapi import Depends
from fastapi_pagination import Page, Params, paginate
@router.get("/users")
async def list_users(params: Annotated[Params, Depends()]) -> Page[User]:
return paginate(get_users(), params)
Customizing OpenAPI¶
Pass a synchronous openapi_customizer callback to Cadwyn() to edit each version's OpenAPI document. Cadwyn() raises a TypeError if the callback is async. It receives the generated schema and the selected version as a string, or None for /openapi.json?version=unversioned. Cadwyn calls it after its own adjustments, including required version headers and servers for mounted apps or a proxy's root path, immediately before serialization.
For example, add a component used by a client SDK even though no endpoint references it, only in the version that needs it. Pass ref_template to model_json_schema() and move its $defs into components.schemas so that references to nested models resolve. The example raises an error instead of overwriting a different component with the same name:
from typing import Any, Union
from cadwyn import Cadwyn, Version, VersionBundle, VersionedAPIRouter
from pydantic import BaseModel
class TraceSpan(BaseModel):
span_id: str
class TaskMetadata(BaseModel):
trace_id: str
spans: list[TraceSpan]
def customize_openapi(
schema: dict[str, Any], version: Union[str, None]
) -> dict[str, Any]:
if version == "2024-01-01":
schemas = schema.setdefault("components", {}).setdefault("schemas", {})
metadata_schema = TaskMetadata.model_json_schema(
ref_template="#/components/schemas/{model}"
)
# Nested models such as TraceSpan become components of their own
new_schemas = metadata_schema.pop("$defs", {})
new_schemas["TaskMetadata"] = metadata_schema
for name, component in new_schemas.items():
# Never overwrite a component that endpoints already reference
if schemas.setdefault(name, component) != component:
raise ValueError(
f"OpenAPI already has a different {name!r} component"
)
return schema
app = Cadwyn(
versions=VersionBundle(Version("2024-01-01"), Version("2023-01-01")),
openapi_customizer=customize_openapi,
)
router = VersionedAPIRouter()
@router.get("/tasks")
def get_tasks() -> list[str]:
return ["example-task"]
app.generate_and_include_versioned_routers(router)
Return the edited dict or a replacement dict. Any other return value, including None, raises a TypeError naming the callback; exceptions raised by the callback propagate. Each request gets a freshly generated dict, and Cadwyn does not cache customized schemas. Omitting the callback or passing None preserves the existing output.
Custom docs static assets¶
By default, Swagger UI and ReDoc load JavaScript and CSS from CDNs. You can override these URLs to serve assets locally:
app = Cadwyn(
versions=my_version_bundle,
swagger_js_url="/static/swagger-ui-bundle.js",
swagger_css_url="/static/swagger-ui.css",
swagger_favicon_url="/static/favicon.png",
redoc_js_url="/static/redoc.standalone.js",
redoc_favicon_url="/static/favicon.png",
)