Skip to content

Commit cb8ba37

Browse files
author
Ilyas Gasanov
committed
[DOP-19924] Add custom openapi middleware
1 parent a21908a commit cb8ba37

File tree

12 files changed

+366
-2
lines changed

12 files changed

+366
-2
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,13 @@ repos:
4040
- id: chmod
4141
args: ['644']
4242
exclude_types: [shell]
43-
exclude: ^(.*__main__\.py)$
43+
exclude: ^(.*__main__\.py|syncmaster/backend/export_openapi_schema\.py)$
4444
- id: chmod
4545
args: ['755']
4646
types: [shell]
4747
- id: chmod
4848
args: ['755']
49-
files: ^(.*__main__\.py)$
49+
files: ^(.*__main__\.py|syncmaster/backend/export_openapi_schema\.py)$
5050
- id: insert-license
5151
files: .*\.py$
5252
exclude: ^(syncmaster/backend/dependencies/stub.py|docs/.*\.py|tests/.*\.py)$

docker/Dockerfile.backend

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,20 @@ ENTRYPOINT ["/app/entrypoint.sh"]
1919
FROM base AS prod
2020

2121
COPY ./syncmaster/ /app/syncmaster/
22+
# add this when logo will be ready
23+
# COPY ./docs/_static/*.svg ./syncmaster/backend/static/
24+
25+
# Swagger UI
26+
ADD https://cdn.jsdelivr.net/npm/swagger-ui-dist@latest/swagger-ui-bundle.js /app/syncmaster/backend/static/swagger/swagger-ui-bundle.js
27+
ADD https://cdn.jsdelivr.net/npm/swagger-ui-dist@latest/swagger-ui.css /app/syncmaster/backend/static/swagger/swagger-ui.css
28+
29+
# Redoc
30+
ADD https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js /app/syncmaster/backend/static/redoc/redoc.standalone.js
31+
32+
ENV SYNCMASTER__SERVER__OPENAPI__SWAGGER__JS_URL=/static/swagger/swagger-ui-bundle.js \
33+
SYNCMASTER__SERVER__OPENAPI__SWAGGER__CSS_URL=/static/swagger/swagger-ui.css \
34+
SYNCMASTER__SERVER__OPENAPI__REDOC__JS_URL=/static/redoc/redoc.standalone.js \
35+
SYNCMASTER__SERVER__STATIC_FILES__DIRECTORY=/app/syncmaster/backend/static
2236

2337

2438
FROM base as test

docs/backend/configuration/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Configuration
1414
cors
1515
debug
1616
monitoring
17+
static_files
18+
openapi
1719

1820
.. autopydantic_settings:: syncmaster.settings.Settings
1921
.. autopydantic_settings:: syncmaster.settings.server.ServerSettings
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.. _configuration-server-openapi:
2+
3+
OpenAPI settings
4+
================
5+
6+
These settings used to control exposing OpenAPI.json and SwaggerUI/ReDoc endpoints.
7+
8+
.. autopydantic_model:: syncmaster.settings.server.openapi.OpenAPISettings
9+
.. autopydantic_model:: syncmaster.settings.server.openapi.SwaggerSettings
10+
.. autopydantic_model:: syncmaster.settings.server.openapi.RedocSettings
11+
.. autopydantic_model:: syncmaster.settings.server.openapi.LogoSettings
12+
.. autopydantic_model:: syncmaster.settings.server.openapi.FaviconSettings
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.. _configuration-server-static-files:
2+
3+
Serving static files
4+
====================
5+
6+
These settings used to control serving static files by a server.
7+
8+
.. autopydantic_model:: syncmaster.settings.server.static_files.StaticFilesSettings

syncmaster/backend/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ def application_factory(settings: Settings) -> FastAPI:
2525
application = FastAPI(
2626
title="Syncmaster",
2727
debug=settings.server.debug,
28+
# will be set up by middlewares
29+
openapi_url=None,
30+
docs_url=None,
31+
redoc_url=None,
2832
)
2933
application.state.settings = settings
3034
application.include_router(api_router)

syncmaster/backend/middlewares/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
from syncmaster.backend.middlewares.monitoring.metrics import (
99
apply_monitoring_metrics_middleware,
1010
)
11+
from syncmaster.backend.middlewares.openapi import apply_openapi_middleware
1112
from syncmaster.backend.middlewares.request_id import apply_request_id_middleware
13+
from syncmaster.backend.middlewares.static_files import apply_static_files
1214
from syncmaster.settings import Settings
1315

1416

@@ -24,5 +26,7 @@ def apply_middlewares(
2426
apply_cors_middleware(application, settings.server.cors)
2527
apply_monitoring_metrics_middleware(application, settings.server.monitoring)
2628
apply_request_id_middleware(application, settings.server.request_id)
29+
apply_openapi_middleware(application, settings.server.openapi)
30+
apply_static_files(application, settings.server.static_files)
2731

2832
return application
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# SPDX-FileCopyrightText: 2023-2024 MTS PJSC
2+
# SPDX-License-Identifier: Apache-2.0
3+
from functools import partial
4+
5+
from fastapi import FastAPI
6+
from fastapi.openapi.docs import (
7+
get_redoc_html,
8+
get_swagger_ui_html,
9+
get_swagger_ui_oauth2_redirect_html,
10+
)
11+
from fastapi.openapi.utils import get_openapi
12+
from starlette.requests import Request
13+
from starlette.responses import JSONResponse
14+
15+
from syncmaster.settings.server.openapi import OpenAPISettings
16+
17+
18+
async def custom_openapi(request: Request) -> JSONResponse:
19+
app: FastAPI = request.app
20+
root_path = request.scope.get("root_path", "").rstrip("/")
21+
server_urls = set(filter(None, (server_data.get("url") for server_data in app.servers)))
22+
23+
if root_path not in server_urls:
24+
if root_path and app.root_path_in_servers:
25+
app.servers.insert(0, {"url": root_path})
26+
server_urls.add(root_path)
27+
28+
return JSONResponse(app.openapi())
29+
30+
31+
def custom_openapi_schema(app: FastAPI, settings: OpenAPISettings) -> dict:
32+
if app.openapi_schema:
33+
return app.openapi_schema
34+
35+
openapi_schema = get_openapi(
36+
title=app.title,
37+
version=app.version,
38+
openapi_version=app.openapi_version,
39+
summary=app.summary,
40+
description=app.description,
41+
terms_of_service=app.terms_of_service,
42+
contact=app.contact,
43+
license_info=app.license_info,
44+
routes=app.routes,
45+
webhooks=app.webhooks.routes,
46+
tags=app.openapi_tags,
47+
servers=app.servers,
48+
separate_input_output_schemas=app.separate_input_output_schemas,
49+
)
50+
# https://redocly.com/docs/api-reference-docs/specification-extensions/x-logo/
51+
openapi_schema["info"]["x-logo"] = {
52+
"url": str(settings.logo.url),
53+
"altText": str(settings.logo.alt_text),
54+
"backgroundColor": f"#{settings.logo.background_color}", # noqa: WPS237
55+
"href": str(settings.logo.href),
56+
}
57+
app.openapi_schema = openapi_schema
58+
return app.openapi_schema
59+
60+
61+
async def custom_swagger_ui_html(request: Request):
62+
app: FastAPI = request.app
63+
settings: OpenAPISettings = app.state.settings.server.openapi
64+
root_path = request.scope.get("root_path", "").rstrip("/")
65+
openapi_url = root_path + request.app.openapi_url # type: ignore[arg-type]
66+
return get_swagger_ui_html(
67+
openapi_url=openapi_url,
68+
title=f"{app.title} - Swagger UI",
69+
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
70+
swagger_js_url=str(settings.swagger.js_url),
71+
swagger_css_url=str(settings.swagger.css_url),
72+
swagger_favicon_url=str(settings.favicon.url),
73+
)
74+
75+
76+
async def custom_swagger_ui_redirect(request: Request):
77+
return get_swagger_ui_oauth2_redirect_html()
78+
79+
80+
async def custom_redoc_html(request: Request):
81+
app: FastAPI = request.app
82+
settings: OpenAPISettings = app.state.settings.server.openapi
83+
root_path = request.scope.get("root_path", "").rstrip("/")
84+
openapi_url = root_path + request.app.openapi_url # type: ignore[arg-type]
85+
return get_redoc_html(
86+
openapi_url=openapi_url,
87+
title=f"{app.title} - ReDoc",
88+
redoc_js_url=settings.redoc.js_url,
89+
redoc_favicon_url=settings.favicon.url,
90+
with_google_fonts=False,
91+
)
92+
93+
94+
def apply_openapi_middleware(app: FastAPI, settings: OpenAPISettings) -> FastAPI:
95+
"""Add OpenAPI middleware to the application."""
96+
# https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/#include-the-custom-docs
97+
if settings.enabled:
98+
app.openapi_url = "/openapi.json"
99+
app.add_route(app.openapi_url, custom_openapi, include_in_schema=False)
100+
101+
if settings.swagger.enabled:
102+
app.docs_url = "/docs"
103+
app.swagger_ui_oauth2_redirect_url = "/docs/oauth2-redirect"
104+
app.add_route(app.docs_url, custom_swagger_ui_html, include_in_schema=False)
105+
app.add_route(app.swagger_ui_oauth2_redirect_url, custom_swagger_ui_redirect, include_in_schema=False)
106+
107+
if settings.redoc.enabled:
108+
app.redoc_url = "/redoc"
109+
app.add_route(app.redoc_url, custom_redoc_html, include_in_schema=False)
110+
111+
app.openapi = partial(custom_openapi_schema, app=app, settings=settings) # type: ignore[method-assign]
112+
return app
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# SPDX-FileCopyrightText: 2023-2024 MTS PJSC
2+
# SPDX-License-Identifier: Apache-2.0
3+
from fastapi import FastAPI
4+
from fastapi.staticfiles import StaticFiles
5+
6+
from syncmaster.settings.server.static_files import StaticFilesSettings
7+
8+
9+
def apply_static_files(app: FastAPI, settings: StaticFilesSettings) -> FastAPI:
10+
"""Add static files serving middleware to the application."""
11+
if not settings.enabled:
12+
return app
13+
14+
# https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/#serve-the-static-files
15+
app.mount("/static", StaticFiles(directory=settings.directory), name="static")
16+
return app

syncmaster/settings/server/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
from syncmaster.settings.log import LoggingSettings
99
from syncmaster.settings.server.cors import CORSSettings
1010
from syncmaster.settings.server.monitoring import MonitoringSettings
11+
from syncmaster.settings.server.openapi import OpenAPISettings
1112
from syncmaster.settings.server.request_id import RequestIDSettings
13+
from syncmaster.settings.server.static_files import StaticFilesSettings
1214

1315

1416
class ServerSettings(BaseModel):
@@ -47,3 +49,11 @@ class ServerSettings(BaseModel):
4749
default_factory=MonitoringSettings,
4850
description=":ref:`Monitoring settings <backend-configuration-monitoring>`",
4951
)
52+
openapi: OpenAPISettings = Field(
53+
default_factory=OpenAPISettings,
54+
description=":ref:`OpenAPI.json settings <backend-configuration-openapi>`",
55+
)
56+
static_files: StaticFilesSettings = Field(
57+
default_factory=StaticFilesSettings,
58+
description=":ref:`Static files settings <configuration-server-static-files>`",
59+
)

0 commit comments

Comments
 (0)