diff --git a/CHANGES.md b/CHANGES.md index c636506..d3a1494 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,7 +3,8 @@ ## 0.16.1 (unreleased) ---------------------- -- Nothing changed yet. +- Reverted "FastAPI's favicon.ico with `/favicon.ico`" because of + login issues in Swagger. ## 0.16.0 (2024-07-02) diff --git a/clean_python/fastapi/schema.py b/clean_python/fastapi/schema.py index 2f1df07..94cd013 100644 --- a/clean_python/fastapi/schema.py +++ b/clean_python/fastapi/schema.py @@ -3,54 +3,14 @@ import yaml from fastapi import FastAPI -from fastapi import Request from fastapi import Response -from fastapi.openapi.docs import get_redoc_html -from fastapi.openapi.docs import get_swagger_ui_html -from fastapi.responses import HTMLResponse - -OPENAPI_URL = "/openapi.json" -FAVICON_URL = "/favicon.ico" def add_cached_openapi_yaml(app: FastAPI) -> None: - @app.get(OPENAPI_URL.replace(".json", ".yaml"), include_in_schema=False) + @app.get("/openapi.yaml", include_in_schema=False) @lru_cache def openapi_yaml() -> Response: openapi_json = app.openapi() yaml_s = StringIO() yaml.dump(openapi_json, yaml_s) return Response(yaml_s.getvalue(), media_type="text/yaml") - - -def get_openapi_url(request: Request) -> str: - root_path = request.scope.get("root_path", "").rstrip("/") - return root_path + OPENAPI_URL - - -def add_swagger_ui(app: FastAPI, title: str, client_id: str | None) -> None: - # Code below is copied from fastapi.applications to modify the favicon - @app.get("/docs", include_in_schema=False) - async def swagger_ui_html(request: Request) -> HTMLResponse: - return get_swagger_ui_html( - openapi_url=get_openapi_url(request), - title=f"{title} - Swagger UI", - swagger_favicon_url=FAVICON_URL, - init_oauth={ - "clientId": client_id, - "usePkceWithAuthorizationCodeGrant": True, - } - if client_id - else None, - ) - - -def add_redoc(app: FastAPI, title: str) -> None: - # Code below is copied from fastapi.applications to modify the favicon - @app.get("/redoc", include_in_schema=False) - async def redoc_html(request: Request) -> HTMLResponse: - return get_redoc_html( - openapi_url=get_openapi_url(request), - title=f"{title} - ReDoc", - redoc_favicon_url=FAVICON_URL, - ) diff --git a/clean_python/fastapi/service.py b/clean_python/fastapi/service.py index 95a665d..56d3d84 100644 --- a/clean_python/fastapi/service.py +++ b/clean_python/fastapi/service.py @@ -35,8 +35,6 @@ from .resource import clean_resources from .resource import Resource from .schema import add_cached_openapi_yaml -from .schema import add_redoc -from .schema import add_swagger_ui from .security import AuthSettings from .security import OAuth2Schema from .security import set_auth_scheme @@ -44,6 +42,19 @@ __all__ = ["Service"] +def get_swagger_ui_init_oauth( + auth: AuthSettings | None = None, +) -> dict[str, Any] | None: + return ( + None + if auth is None or not auth.oauth2.login_enabled() + else { + "clientId": auth.oauth2.client_id, + "usePkceWithAuthorizationCodeGrant": True, + } + ) + + AnyHttpUrlTA = TypeAdapter(AnyHttpUrl) @@ -107,7 +118,6 @@ def _create_root_app( for x in self.versions ], root_path_in_servers=False, - openapi_url=None, # disables redoc and docs as well ) if access_logger_gateway is not None: app.middleware("http")( @@ -117,26 +127,16 @@ def _create_root_app( return app def _create_versioned_app( - self, - version: APIVersion, - auth_scheme: OAuth2Schema | None, - title: str, - description: str, - dependencies: list[Depends], - client_id: str | None, + self, version: APIVersion, auth_scheme: OAuth2Schema | None, **fastapi_kwargs ) -> FastAPI: resources = [x for x in self.resources if x.version == version] app = FastAPI( - title=title, - description=description, - dependencies=dependencies, version=version.prefix, tags=sorted( [x.get_openapi_tag().model_dump() for x in resources], key=lambda x: x["name"], ), - redoc_url=None, # added manually later in add_redoc - docs_url=None, # added manually later in add_swagger_ui + **fastapi_kwargs, ) for resource in resources: app.include_router( @@ -156,8 +156,6 @@ def _create_versioned_app( app.add_exception_handler(PermissionDenied, permission_denied_handler) app.add_exception_handler(Unauthorized, unauthorized_handler) add_cached_openapi_yaml(app) - add_swagger_ui(app, title=title, client_id=client_id) - add_redoc(app, title=title) return app def create_app( @@ -179,18 +177,14 @@ def create_app( on_shutdown=on_shutdown, access_logger_gateway=access_logger_gateway, ) - dependencies = [Depends(set_request_context)] + fastapi_kwargs = { + "title": title, + "description": description, + "dependencies": [Depends(set_request_context)], + "swagger_ui_init_oauth": get_swagger_ui_init_oauth(auth), + } versioned_apps = { - v: self._create_versioned_app( - v, - auth_scheme=auth_scheme, - title=title, - description=description, - dependencies=dependencies, - client_id=auth.oauth2.client_id - if auth and auth.oauth2.login_enabled() - else None, - ) + v: self._create_versioned_app(v, auth_scheme=auth_scheme, **fastapi_kwargs) for v in self.versions } for v, versioned_app in versioned_apps.items(): diff --git a/tests/fastapi/test_service_schema.py b/tests/fastapi/test_service_schema.py index 9c0500d..3a5d2de 100644 --- a/tests/fastapi/test_service_schema.py +++ b/tests/fastapi/test_service_schema.py @@ -1,4 +1,3 @@ -from html.parser import HTMLParser from http import HTTPStatus import pytest @@ -151,26 +150,3 @@ def test_schema_yaml(client: TestClient, expected_schema: Json): actual = yaml.safe_load(response.content.decode("utf-8")) assert actual == expected_schema - - -@pytest.mark.parametrize("path", ["/v1/docs", "/v1/redoc"]) -def test_favicon(client: TestClient, path: str): - response = client.get(path) - assert response.status_code == HTTPStatus.OK - - # parse favicon from html - found = set() - - class FaviconParser(HTMLParser): - def handle_starttag(self, tag, attrs): - if tag == "link": - attr_dict = dict(attrs) - if ( - attr_dict.get("rel") in {"icon", "shortcut icon"} - and "href" in attr_dict - ): - found.add(attr_dict["href"]) - - FaviconParser().feed(response.text) - - assert found == {"/favicon.ico"}