Skip to content

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: VersionBundle describes 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",
)