Skip to content

Commit

Permalink
Replace FastAPI favicon with /favicon.ico (#85)
Browse files Browse the repository at this point in the history
  • Loading branch information
caspervdw authored Jul 2, 2024
1 parent fc11e9e commit d4198f7
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 23 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

- Fixed datatype of ctx.path (it was starlette.URL, now it is pydantic_core.Url).

- Replaced FastAPI's favicon.ico with `/favicon.ico`.

- Deprecate `dramatiq` submodule.


Expand Down
42 changes: 41 additions & 1 deletion clean_python/fastapi/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,54 @@

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.yaml", include_in_schema=False)
@app.get(OPENAPI_URL.replace(".json", ".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,
)
50 changes: 28 additions & 22 deletions clean_python/fastapi/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,26 +35,15 @@
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

__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)


Expand Down Expand Up @@ -118,6 +107,7 @@ 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")(
Expand All @@ -127,16 +117,26 @@ def _create_root_app(
return app

def _create_versioned_app(
self, version: APIVersion, auth_scheme: OAuth2Schema | None, **fastapi_kwargs
self,
version: APIVersion,
auth_scheme: OAuth2Schema | None,
title: str,
description: str,
dependencies: list[Depends],
client_id: str | None,
) -> 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"],
),
**fastapi_kwargs,
redoc_url=None, # added manually later in add_redoc
docs_url=None, # added manually later in add_swagger_ui
)
for resource in resources:
app.include_router(
Expand All @@ -156,6 +156,8 @@ 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(
Expand All @@ -177,14 +179,18 @@ def create_app(
on_shutdown=on_shutdown,
access_logger_gateway=access_logger_gateway,
)
fastapi_kwargs = {
"title": title,
"description": description,
"dependencies": [Depends(set_request_context)],
"swagger_ui_init_oauth": get_swagger_ui_init_oauth(auth),
}
dependencies = [Depends(set_request_context)]
versioned_apps = {
v: self._create_versioned_app(v, auth_scheme=auth_scheme, **fastapi_kwargs)
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,
)
for v in self.versions
}
for v, versioned_app in versioned_apps.items():
Expand Down
24 changes: 24 additions & 0 deletions tests/fastapi/test_service_schema.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from html.parser import HTMLParser
from http import HTTPStatus

import pytest
Expand Down Expand Up @@ -150,3 +151,26 @@ 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"}

0 comments on commit d4198f7

Please sign in to comment.