Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ 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:
# CI runs tests in the worker container, so we need to turn off interaction with static files for it
- 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