Skip to content

Commit

Permalink
feat: Added include_versions_route parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
alexschimpf authored Oct 3, 2023
1 parent 4550daa commit 42dcaf7
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 2 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ An APIRouter will be created for each version, with the URL prefix defined by th
- If True, docs page(s) will be created for each version
- <b>include_version_openapi_route</b>
- If True, an OpenAPI route will be created for each version
- <b>include_versions_route</b>
- If True, a "GET /versions" route will be added, which includes information about all API versions
- <b>sort_routes</b>
- If True, all routes will be naturally sorted by path within each version.
- If you have included the main docs page, the routes are sorted within each version, and versions are sorted from earliest to latest. If you have added a "latest" alias, its routes will be listed last.
Expand Down
1 change: 1 addition & 0 deletions examples/simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,5 +134,6 @@ def create_item_v2(item: ItemV2) -> ItemV2:
prefix_format='/v{major}',
semantic_version_format='{major}',
latest_prefix='/latest',
include_versions_route=True,
sort_routes=True
).versionize()
43 changes: 41 additions & 2 deletions fastapi_versionizer/versionizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from fastapi.openapi.docs import get_redoc_html
from fastapi.openapi.docs import get_swagger_ui_html
import fastapi.openapi.utils
from fastapi.responses import HTMLResponse
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.routing import APIRoute
from natsort import natsorted
from typing import Any, Callable, Dict, List, Tuple, TypeVar, Union
Expand Down Expand Up @@ -48,6 +48,7 @@ def __init__(
include_main_openapi_route: bool = True,
include_version_docs: bool = True,
include_version_openapi_route: bool = True,
include_versions_route: bool = False,
sort_routes: bool = False,
callback: Union[Callable[[APIRouter, Tuple[int, int], str], None], None] = None
):
Expand All @@ -72,6 +73,8 @@ def __init__(
If True, docs page(s) will be created for each version
:param include_version_openapi_route:
If True, an openapi route will be created for each version
:param include_versions_route:
If True, a "GET /versions" route will be added, which includes information about all API versions
:param sort_routes:
If True, all routes will be naturally sorted by path within each version.
If you have included the main docs page, the routes are sorted within each version, and versions
Expand All @@ -93,6 +96,7 @@ def __init__(
self._include_main_openapi_route = include_main_openapi_route
self._include_version_docs = include_version_docs
self._include_version_openapi_route = include_version_openapi_route
self._include_versions_route = include_versions_route
self._sort_routes = sort_routes
self._callback = callback

Expand All @@ -108,6 +112,7 @@ def versionize(self) -> Tuple[FastAPI, List[Tuple[int, int]]]:

version, routes_by_key = None, None
routes_by_version = self._get_routes_by_version()
versions = list(routes_by_version.keys())
for version, routes_by_key in routes_by_version.items():
major, minor = version
version_prefix = self._prefix_format.format(major=major, minor=minor)
Expand All @@ -130,10 +135,13 @@ def versionize(self) -> Tuple[FastAPI, List[Tuple[int, int]]]:
self._callback(latest_router, version, '/latest')
versioned_app.include_router(router=latest_router)

if self._include_versions_route:
self._add_versions_route(versioned_app=versioned_app, versions=versions)

if not self._include_main_docs or not self._include_main_openapi_route:
self._remove_docs_and_openapi(versioned_app=versioned_app)

return versioned_app, list(routes_by_version.keys())
return versioned_app, versions

def _build_version_router(
self,
Expand Down Expand Up @@ -231,6 +239,37 @@ async def get_redoc() -> HTMLResponse:
title=title
)

def _add_versions_route(self, versioned_app: FastAPI, versions: List[Tuple[int, int]]) -> None:
@versioned_app.get(
'/versions',
tags=['Versions'],
response_class=JSONResponse
)
def get_versions() -> Dict[str, Any]:
version_models: List[Dict[str, Any]] = []
for (major, minor) in versions:
version_prefix = self._prefix_format.format(major=major, minor=minor)
version_str = self._semantic_version_format.format(major=major, minor=minor)

version_model = {
'version': version_str,
}

if self._include_version_openapi_route:
version_model['openapi_url'] = f'{version_prefix}/openapi.json'

if self._include_version_docs and versioned_app.docs_url is not None:
version_model['swagger_url'] = f'{version_prefix}{versioned_app.docs_url}'

if self._include_version_docs and versioned_app.redoc_url is not None:
version_model['redoc_url'] = f'{version_prefix}{versioned_app.redoc_url}'

version_models.append(version_model)

return {
'versions': version_models
}

def _remove_docs_and_openapi(self, versioned_app: FastAPI) -> None:
paths_to_remove = []
if not self._include_main_docs:
Expand Down
44 changes: 44 additions & 0 deletions tests/test_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,28 @@ def test_simple_example(self) -> None:
self.assertEqual(404, test_client.get('/users/1').status_code)
self.assertEqual(404, test_client.get('/items/1').status_code)
self.assertEqual(404, test_client.get('/v2/items/1').status_code)
self.assertEqual(404, test_client.get('/v1/versions').status_code)
self.assertEqual(404, test_client.get('/v2/versions').status_code)
self.assertEqual(404, test_client.get('/latest/versions').status_code)

# versions route
self.assertDictEqual(
{
'versions': [
{
'version': '1',
'openapi_url': '/v1/openapi.json',
'swagger_url': '/v1/docs'
},
{
'version': '2',
'openapi_url': '/v2/openapi.json',
'swagger_url': '/v2/docs',
}
]
},
test_client.get('/versions').json()
)

# v1
self.assertDictEqual(
Expand Down Expand Up @@ -745,6 +767,28 @@ def test_simple_example(self) -> None:
}
}
}
},
'/versions': {
'get': {
'tags': [
'Versions'
],
'summary': 'Get Versions',
'operationId': 'get_versions_versions_get',
'responses': {
'200': {
'description': 'Successful Response',
'content': {
'application/json': {
'schema': {
'type': 'object',
'title': 'Response Get Versions Versions Get'
}
}
}
}
}
}
}
},
'components': {
Expand Down

0 comments on commit 42dcaf7

Please sign in to comment.