Source code for betty.openapi

"""
Provide the OpenAPI specification.
"""

from pathlib import Path
from typing import Self, final

from betty import about, model
from betty.json.schema import FileBasedSchema
from betty.locale.localizer import DEFAULT_LOCALIZER
from betty.model import UserFacingEntity
from betty.project import Project, ProjectSchema
from betty.serde.dump import DumpMapping, Dump
from betty.string import kebab_case_to_lower_camel_case


[docs] class Specification: """ Build OpenAPI specifications. """
[docs] def __init__(self, project: Project): self._project = project
[docs] async def build(self) -> DumpMapping[Dump]: """ Build the OpenAPI specification. """ url_generator = await self._project.url_generator specification: DumpMapping[Dump] = { "openapi": "3.1.0", "servers": [ { "url": url_generator.generate("betty-static:///", absolute=True), } ], "info": { "title": "Betty", "version": about.version_label(), }, "paths": {}, "components": { "responses": { "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": await ProjectSchema.def_url( self._project, "errorResponse" ), }, }, }, }, "403": { "description": "Forbidden", "content": { "application/json": { "schema": { "$ref": await ProjectSchema.def_url( self._project, "errorResponse" ), }, }, }, }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": await ProjectSchema.def_url( self._project, "errorResponse" ), }, }, }, }, }, "parameters": { "id": { "name": "id", "in": "path", "required": True, "description": "The ID for the resource to retrieve.", "schema": { "type": "string", }, }, }, "schemas": { "betty": { "$ref": await ProjectSchema.url(self._project), }, }, }, } # Add entity operations. for entity_type in await model.ENTITY_TYPE_REPOSITORY.select(UserFacingEntity): await entity_type.linked_data_schema(self._project) if self._project.configuration.clean_urls: collection_path = f"/{entity_type.plugin_id()}/" single_path = f"/{entity_type.plugin_id()}/{{id}}/" else: collection_path = f"/{entity_type.plugin_id()}/index.json" single_path = f"/{entity_type.plugin_id()}/{{id}}/index.json" entity_type_label = entity_type.plugin_label().localize(DEFAULT_LOCALIZER) specification["paths"].update( # type: ignore[union-attr] { collection_path: { "get": { "summary": f"Retrieve the collection of {entity_type_label} entities.", "responses": { "200": { "description": f"The collection of {entity_type_label} entities.", "content": { "application/json": { "schema": { "$ref": await ProjectSchema.def_url( self._project, f"{kebab_case_to_lower_camel_case(entity_type.plugin_id())}EntityCollectionResponse", ), }, }, }, }, }, "tags": [entity_type_label], }, }, single_path: { "get": { "summary": f"Retrieve a single {entity_type_label} entity.", "responses": { "200": { "description": f"The {entity_type_label} entity.", "content": { "application/json": { "schema": { "$ref": await ProjectSchema.def_url( self._project, f"{kebab_case_to_lower_camel_case(entity_type.plugin_id())}Entity", ), }, }, }, }, }, "tags": [entity_type_label], }, }, } ) # Add default behavior to all requests. for path in specification["paths"]: # type: ignore[union-attr] specification["paths"][path]["get"]["responses"].update( # type: ignore[call-overload, index, union-attr] { "401": { "$ref": "#/components/responses/401", }, "403": { "$ref": "#/components/responses/403", }, "404": { "$ref": "#/components/responses/404", }, } ) return specification
[docs] @final class SpecificationSchema(FileBasedSchema): """ The OpenAPI Specification schema. """
[docs] @classmethod async def new(cls) -> Self: """ Create a new instance. """ return await cls.new_for( Path(__file__).parent / "json" / "schemas" / "openapi-specification.json", def_name="openApiSpecification", )