Source code for mxcubeweb.core.server.openapidoc

import re

from flask import Blueprint, jsonify
from pydantic import (
    BaseModel,
)

DEFAULT_RESPONSES = {
    "200": {"description": "Success"},
    "400": {"description": "Invalid input data"},
    "404": {"description": "Invalid or non existing object"},
    "500": {"description": "Error calling method on adapter"},
}


[docs]def to_openapi_path(route: str) -> str: """Add the "{" "}" to the route arguments that OpenAPI requires. Args: route(str): The route Returns: returns: the openapi path, with the {} srounding the arguments """ _s = re.sub(r"<[^:]+:", "{", route) # Replace closing ">" with "}" return _s.replace(">", "}")
class OpenAPISpec: def __init__(self, name: str, url_prefix, api_version: str, api_title: str): self.bp = Blueprint(name, __name__, url_prefix=url_prefix) self._openapi_spec = { "openapi": "3.0.0", "info": {"title": api_title, "version": api_version}, "paths": {}, "components": {"schemas": {}}, } self.bp.add_url_rule( "/openapi.json", "openapi", self._serve_openapi, methods=["GET"] ) self.bp.add_url_rule("/docs", "redoc_ui", self._serve_redoc_ui, methods=["GET"]) self.bp.add_url_rule( "/docs_redoc", "redoc_ui", self._serve_redoc_ui, methods=["GET"] ) self.bp.add_url_rule( "/docs_swagger", "swagger_ui", self._serve_swagger_ui, methods=["GET"] ) self.bp.add_url_rule( "/docs_elements", "elements_ui", self._serve_elements_ui, methods=["GET"] ) def add_openapi_path( self, prefix: str, route: str, export: dict[str, str], http_method: str, view_func, ): """Adds API endpoints to OpenAPI spec.""" open_api_path = to_openapi_path(prefix + route) self._openapi_spec["paths"].setdefault(open_api_path, {})[ http_method.lower() ] = { "summary": f"{http_method} {export['attr']}", "description": str(view_func.__doc__), "tags": [prefix], "parameters": [ { "name": "object_id", "in": "path", "required": True, "schema": {"type": "string"}, } ], "requestBody": {}, "responses": dict(DEFAULT_RESPONSES), } def add_openapi_schema( self, prefix: str, route: str, http_method: str, model: type[BaseModel] ): """Adds Pydantic model definitions to OpenAPI schema.""" open_api_path = to_openapi_path(prefix + route) schema_name = model.__name__ if schema_name not in self._openapi_spec["components"]["schemas"]: self._openapi_spec["components"]["schemas"][schema_name] = model.schema() self._openapi_spec["paths"][open_api_path][http_method.lower()][ "requestBody" ].update( { "description": model.__name__, "required": True, "content": { "application/json": { "schema": {"$ref": f"#/components/schemas/{model.__name__}"} } }, } ) def add_openapi_response( self, prefix: str, route: str, http_method: str, model: type[BaseModel] ): """Adds response schema to OpenAPI documentation.""" open_api_path = to_openapi_path(prefix + route) schema_name = model.__name__ if isinstance(model, BaseModel) else None if schema_name: if schema_name not in self._openapi_spec["components"]["schemas"]: self._openapi_spec["components"]["schemas"][schema_name] = ( model.schema() ) self._openapi_spec["paths"][open_api_path][http_method.lower()][ "responses" ]["200"] = { "description": "Successful response", "content": { "application/json": { "schema": {"$ref": f"#/components/schemas/{model.__name__}"} } }, } def _serve_openapi(self): return jsonify(self._openapi_spec) def _serve_redoc_ui(self): """Serves the ReDoc UI for OpenAPI documentation.""" return """ <!DOCTYPE html> <html> <head> <title>Redoc</title> <!-- needed for adaptive design --> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet"> <!-- Redoc doesn't change outer page styles --> <style> body { margin: 0; padding: 0; } </style> </head> <body> <redoc spec-url='openapi.json'></redoc> <script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"> </script> </body> </html> """ # noqa: E501 def _serve_swagger_ui(self): """Serves the Swagger UI for OpenAPI documentation.""" return """ <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="description" content="SwaggerUI" /> <title>SwaggerUI</title> <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" /> </head> <body> <div id="swagger-ui"></div> <script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js" crossorigin></script> <script> window.onload = () => { window.ui = SwaggerUIBundle({ url: 'openapi.json', dom_id: '#swagger-ui', }); }; </script> </body> </html> """ # noqa: E501 def _serve_elements_ui(self): """Serves the Elements UI for OpenAPI documentation.""" return """ <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title>Elements in HTML</title> <script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script> <link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css"> </head> <body> <elements-api apiDescriptionUrl="openapi.json" router="hash" /> </body> </html> """ # noqa: E501