Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ repos:
- id: chmod
args: ['644']
exclude_types: [shell]
exclude: ^(.*__main__\.py)$
exclude: ^(.*__main__\.py|syncmaster/backend/export_openapi_schema\.py)$
- id: chmod
args: ['755']
types: [shell]
- id: chmod
args: ['755']
files: ^(.*__main__\.py)$
files: ^(.*__main__\.py|syncmaster/backend/export_openapi_schema\.py)$
- id: insert-license
files: .*\.py$
exclude: ^(syncmaster/backend/dependencies/stub.py|docs/.*\.py|tests/.*\.py)$
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ services:
command: --loglevel=info -Q test_queue
entrypoint: [python, -m, celery, -A, tests.test_integration.celery_test, worker, --max-tasks-per-child=1]
env_file: .env.docker
environment:
- SYNCMASTER__SERVER__STATIC_FILES__ENABLED=false
volumes:
- ./syncmaster:/app/syncmaster
- ./cached_jars:/root/.ivy2
Expand Down
14 changes: 14 additions & 0 deletions docker/Dockerfile.backend
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ ENTRYPOINT ["/app/entrypoint.sh"]
FROM base AS prod

COPY ./syncmaster/ /app/syncmaster/
# add this when logo will be ready
# COPY ./docs/_static/*.svg ./syncmaster/backend/static/

# Swagger UI
ADD https://cdn.jsdelivr.net/npm/swagger-ui-dist@latest/swagger-ui-bundle.js /app/syncmaster/backend/static/swagger/swagger-ui-bundle.js
ADD https://cdn.jsdelivr.net/npm/swagger-ui-dist@latest/swagger-ui.css /app/syncmaster/backend/static/swagger/swagger-ui.css

# Redoc
ADD https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js /app/syncmaster/backend/static/redoc/redoc.standalone.js

ENV SYNCMASTER__SERVER__OPENAPI__SWAGGER__JS_URL=/static/swagger/swagger-ui-bundle.js \
SYNCMASTER__SERVER__OPENAPI__SWAGGER__CSS_URL=/static/swagger/swagger-ui.css \
SYNCMASTER__SERVER__OPENAPI__REDOC__JS_URL=/static/redoc/redoc.standalone.js \
SYNCMASTER__SERVER__STATIC_FILES__DIRECTORY=/app/syncmaster/backend/static


FROM base as test
Expand Down
2 changes: 2 additions & 0 deletions docs/backend/configuration/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Configuration
cors
debug
monitoring
static_files
openapi

.. autopydantic_settings:: syncmaster.settings.Settings
.. autopydantic_settings:: syncmaster.settings.server.ServerSettings
12 changes: 12 additions & 0 deletions docs/backend/configuration/openapi.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.. _configuration-server-openapi:

OpenAPI settings
================

These settings used to control exposing OpenAPI.json and SwaggerUI/ReDoc endpoints.

.. autopydantic_model:: syncmaster.settings.server.openapi.OpenAPISettings
.. autopydantic_model:: syncmaster.settings.server.openapi.SwaggerSettings
.. autopydantic_model:: syncmaster.settings.server.openapi.RedocSettings
.. autopydantic_model:: syncmaster.settings.server.openapi.LogoSettings
.. autopydantic_model:: syncmaster.settings.server.openapi.FaviconSettings
8 changes: 8 additions & 0 deletions docs/backend/configuration/static_files.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.. _configuration-server-static-files:

Serving static files
====================

These settings used to control serving static files by a server.

.. autopydantic_model:: syncmaster.settings.server.static_files.StaticFilesSettings
4 changes: 4 additions & 0 deletions syncmaster/backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ def application_factory(settings: Settings) -> FastAPI:
application = FastAPI(
title="Syncmaster",
debug=settings.server.debug,
# will be set up by middlewares
openapi_url=None,
docs_url=None,
redoc_url=None,
)
application.state.settings = settings
application.include_router(api_router)
Expand Down
4 changes: 4 additions & 0 deletions syncmaster/backend/middlewares/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
from syncmaster.backend.middlewares.monitoring.metrics import (
apply_monitoring_metrics_middleware,
)
from syncmaster.backend.middlewares.openapi import apply_openapi_middleware
from syncmaster.backend.middlewares.request_id import apply_request_id_middleware
from syncmaster.backend.middlewares.static_files import apply_static_files
from syncmaster.settings import Settings


Expand All @@ -24,5 +26,7 @@ def apply_middlewares(
apply_cors_middleware(application, settings.server.cors)
apply_monitoring_metrics_middleware(application, settings.server.monitoring)
apply_request_id_middleware(application, settings.server.request_id)
apply_openapi_middleware(application, settings.server.openapi)
apply_static_files(application, settings.server.static_files)

return application
112 changes: 112 additions & 0 deletions syncmaster/backend/middlewares/openapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# SPDX-FileCopyrightText: 2023-2024 MTS PJSC
# SPDX-License-Identifier: Apache-2.0
from functools import partial

from fastapi import FastAPI
from fastapi.openapi.docs import (
get_redoc_html,
get_swagger_ui_html,
get_swagger_ui_oauth2_redirect_html,
)
from fastapi.openapi.utils import get_openapi
from starlette.requests import Request
from starlette.responses import JSONResponse

from syncmaster.settings.server.openapi import OpenAPISettings


async def custom_openapi(request: Request) -> JSONResponse:
app: FastAPI = request.app
root_path = request.scope.get("root_path", "").rstrip("/")
server_urls = set(filter(None, (server_data.get("url") for server_data in app.servers)))

if root_path not in server_urls:
if root_path and app.root_path_in_servers:
app.servers.insert(0, {"url": root_path})
server_urls.add(root_path)

return JSONResponse(app.openapi())


def custom_openapi_schema(app: FastAPI, settings: OpenAPISettings) -> dict:
if app.openapi_schema:
return app.openapi_schema

openapi_schema = get_openapi(
title=app.title,
version=app.version,
openapi_version=app.openapi_version,
summary=app.summary,
description=app.description,
terms_of_service=app.terms_of_service,
contact=app.contact,
license_info=app.license_info,
routes=app.routes,
webhooks=app.webhooks.routes,
tags=app.openapi_tags,
servers=app.servers,
separate_input_output_schemas=app.separate_input_output_schemas,
)
# https://redocly.com/docs/api-reference-docs/specification-extensions/x-logo/
openapi_schema["info"]["x-logo"] = {
"url": str(settings.logo.url),
"altText": str(settings.logo.alt_text),
"backgroundColor": f"#{settings.logo.background_color}", # noqa: WPS237
"href": str(settings.logo.href),
}
app.openapi_schema = openapi_schema
return app.openapi_schema


async def custom_swagger_ui_html(request: Request):
app: FastAPI = request.app
settings: OpenAPISettings = app.state.settings.server.openapi
root_path = request.scope.get("root_path", "").rstrip("/")
openapi_url = root_path + request.app.openapi_url # type: ignore[arg-type]
return get_swagger_ui_html(
openapi_url=openapi_url,
title=f"{app.title} - Swagger UI",
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
swagger_js_url=str(settings.swagger.js_url),
swagger_css_url=str(settings.swagger.css_url),
swagger_favicon_url=str(settings.favicon.url),
)


async def custom_swagger_ui_redirect(request: Request):
return get_swagger_ui_oauth2_redirect_html()


async def custom_redoc_html(request: Request):
app: FastAPI = request.app
settings: OpenAPISettings = app.state.settings.server.openapi
root_path = request.scope.get("root_path", "").rstrip("/")
openapi_url = root_path + request.app.openapi_url # type: ignore[arg-type]
return get_redoc_html(
openapi_url=openapi_url,
title=f"{app.title} - ReDoc",
redoc_js_url=settings.redoc.js_url,
redoc_favicon_url=settings.favicon.url,
with_google_fonts=False,
)


def apply_openapi_middleware(app: FastAPI, settings: OpenAPISettings) -> FastAPI:
"""Add OpenAPI middleware to the application."""
# https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/#include-the-custom-docs
if settings.enabled:
app.openapi_url = "/openapi.json"
app.add_route(app.openapi_url, custom_openapi, include_in_schema=False)

if settings.swagger.enabled:
app.docs_url = "/docs"
app.swagger_ui_oauth2_redirect_url = "/docs/oauth2-redirect"
app.add_route(app.docs_url, custom_swagger_ui_html, include_in_schema=False)
app.add_route(app.swagger_ui_oauth2_redirect_url, custom_swagger_ui_redirect, include_in_schema=False)

if settings.redoc.enabled:
app.redoc_url = "/redoc"
app.add_route(app.redoc_url, custom_redoc_html, include_in_schema=False)

app.openapi = partial(custom_openapi_schema, app=app, settings=settings) # type: ignore[method-assign]
return app
16 changes: 16 additions & 0 deletions syncmaster/backend/middlewares/static_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# SPDX-FileCopyrightText: 2023-2024 MTS PJSC
# SPDX-License-Identifier: Apache-2.0
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

from syncmaster.settings.server.static_files import StaticFilesSettings


def apply_static_files(app: FastAPI, settings: StaticFilesSettings) -> FastAPI:
"""Add static files serving middleware to the application."""
if not settings.enabled:
return app

# https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/#serve-the-static-files
app.mount("/static", StaticFiles(directory=settings.directory), name="static")
return app
10 changes: 10 additions & 0 deletions syncmaster/settings/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
from syncmaster.settings.log import LoggingSettings
from syncmaster.settings.server.cors import CORSSettings
from syncmaster.settings.server.monitoring import MonitoringSettings
from syncmaster.settings.server.openapi import OpenAPISettings
from syncmaster.settings.server.request_id import RequestIDSettings
from syncmaster.settings.server.static_files import StaticFilesSettings


class ServerSettings(BaseModel):
Expand Down Expand Up @@ -47,3 +49,11 @@ class ServerSettings(BaseModel):
default_factory=MonitoringSettings,
description=":ref:`Monitoring settings <backend-configuration-monitoring>`",
)
openapi: OpenAPISettings = Field(
default_factory=OpenAPISettings,
description=":ref:`OpenAPI.json settings <backend-configuration-openapi>`",
)
static_files: StaticFilesSettings = Field(
default_factory=StaticFilesSettings,
description=":ref:`Static files settings <configuration-server-static-files>`",
)
Loading