Last active
April 17, 2024 01:55
-
-
Save Kludex/1c515aa38d22ec28d514da6b6f36da9f to your computer and use it in GitHub Desktop.
Document each version on FastAPI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from fastapi import APIRouter, FastAPI | |
from utils import create_versioning_docs | |
app = FastAPI(docs_url=None, redoc_url=None) | |
v1_router = APIRouter(prefix="/v1") | |
v2_router = APIRouter(prefix="/v2") | |
create_versioning_docs(v1_router) | |
create_versioning_docs(v2_router) | |
@v1_router.get("/") | |
def get_hello_world(): | |
return {"message": "Hello World"} | |
@v2_router.get("/") | |
def get_another_world(): | |
return {"message": "Another World"} | |
app.include_router(v1_router) | |
app.include_router(v2_router) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fastapi==0.68.0 | |
uvicorn==0.14.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import copy | |
from collections import defaultdict | |
from typing import Any, DefaultDict, Dict, Optional | |
from fastapi import APIRouter, Request | |
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html | |
custom_openapi: DefaultDict[str, Optional[Dict[str, Any]]] = defaultdict(lambda: None) | |
def create_versioning_docs(router: APIRouter) -> None: | |
prefix = router.prefix | |
@router.get("/openapi.json", include_in_schema=False, name=f"{prefix}_openapi") | |
async def get_openapi_json(request: Request): | |
global custom_openapi | |
version = request.url.path.strip("/").split("/")[0] | |
if custom_openapi[version] is None: | |
custom_openapi[version] = copy.deepcopy(request.app.openapi()) | |
# Remove other version tags on openapi schema. | |
for path in custom_openapi[version]["paths"].copy(): | |
if not path.startswith(f"/{version}"): | |
del custom_openapi[version]["paths"][path] | |
return custom_openapi[version] | |
@router.get("/docs", include_in_schema=False, name=f"{prefix}_swagger") | |
async def get_swagger(request: Request): | |
return get_swagger_ui_html( | |
openapi_url=f"{prefix}/openapi.json", | |
title=request.app.title + " - Swagger UI", | |
) | |
@router.get("/redoc", include_in_schema=False, name=f"{prefix}_redoc") | |
async def redoc_html(request: Request): | |
return get_redoc_html( | |
openapi_url=f"{prefix}/openapi.json", title=request.app.title + " - ReDoc" | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@Kludex thanks so much for this; it's been awesomely helpful. I did discover one bug-lette in the
get_openapi_json
function where if you have two versions running concurrently, hitting the first versioned docs endpoint in your browser effectively nukes the docs for the second versioned docs endpoint.Changing
utils.py:19
tocustom_openapi[version] = copy.deepcopy(request.app.openapi())
resolves this.See https://gist.github.com/vicchi/81213006e2fd7049da9e3aa10728af06 for a working version, including a (slightly hacky) way of overriding the OpenAPI
info
to have version specific overrides in the generated docs.