Source code for dmr.openapi.core.registry

import dataclasses
from typing import Any, ClassVar

from typing_extensions import TypeForm

from dmr.internal.types import EMPTY
from dmr.openapi.objects import Reference, Schema, SecurityScheme


[docs] class OperationIdRegistry: """Registry for OpenAPI operation IDs.""" __slots__ = ('_operation_ids',) def __init__(self) -> None: """Initialize an empty operation ID registry.""" self._operation_ids: set[str] = set()
[docs] def register(self, operation_id: str) -> None: """Register an operation ID in the registry.""" if operation_id in self._operation_ids: raise ValueError( f'Operation ID {operation_id!r} is already registered in the ' 'OpenAPI specification. Operation IDs must be unique across ' 'all endpoints to ensure proper API documentation. ' 'Please use a different operation ID for this endpoint.', ) self._operation_ids.add(operation_id)
[docs] class SchemaRegistry: """ Registry for ``Schemas``. .. versionchanged:: 0.16.0 Removed ``try_unregister``: components are now registered only when the final schema references them, see :meth:`dmr.openapi.generators.SchemaGenerator.register`. References to schemas are now :class:`~dmr.openapi.objects.Schema` objects with ``$ref`` set, not ``Reference`` objects, because OpenAPI 3.1 defines ``$ref`` as a JSON Schema keyword. """ __slots__ = ('_schemas',) schema_prefix: ClassVar[str] = '#/components/schemas/' def __init__(self) -> None: """Initialize empty schema and type registers.""" self._schemas: dict[str, tuple[Schema, int | None]] = {} @property def schemas(self) -> dict[str, Schema]: """Return schemas by name.""" return { schema_name: self._schemas[schema_name][0] for schema_name in sorted(self._schemas) }
[docs] def register( self, schema_name: str, schema: Schema, annotation: TypeForm[Any] | EMPTY = EMPTY, ) -> Schema: """Register Schema in registry, return a reference to it.""" existing_schema = self._schemas.get(schema_name) if existing_schema: _check_hashes( schema_name, annotation, existing_schema[1], ) return self._make_reference(schema_name) self._schemas[schema_name] = (schema, _safe_hash(annotation)) return self._make_reference(schema_name)
[docs] def get_reference( self, schema_name: str | None, annotation: TypeForm[Any] | EMPTY = EMPTY, ) -> Schema | None: """Get a reference to the registered schema, if it exists.""" if schema_name: existing_schema = self._schemas.get(schema_name) if existing_schema: _check_hashes( schema_name, annotation, existing_schema[1], ) return self._make_reference(schema_name) return None
[docs] def maybe_resolve_reference( self, reference: Schema, *, resolution_context: dict[str, Schema] | None = None, ) -> Schema: """ Resolve a schema with ``$ref`` and return the referenced schema back. Schemas without ``$ref`` are returned as is. *resolution_context* holds components that are not registered yet, they are checked before the registered ones. The keywords next to ``$ref``, like ``default``, only annotate that one usage: they are put on top of the component's own schema, but they never modify the component itself, #1491 .. versionchanged:: 0.16.0 Falls back to the registered schemas when *resolution_context* does not have the component. Accepts only ``Schema`` objects. Keywords next to ``$ref`` are kept in the result. """ if reference.ref is None: return reference schema_name = reference.ref.removeprefix(self.schema_prefix) if resolution_context and schema_name in resolution_context: target = resolution_context[schema_name] else: target = self._schemas[schema_name][0] return _overlay_ref_site(target, reference)
def _make_reference(self, name: str) -> Schema: return Schema(ref=f'{self.schema_prefix}{name}')
[docs] class SecuritySchemeRegistry: """ Registry for ``SecuritySchemes``. .. versionchanged:: 0.16.0 ``schemes`` is now a property that returns security schemes sorted by name. """ __slots__ = ('_schemes',) def __init__(self) -> None: """Initialize empty security schemes registry.""" self._schemes: dict[str, SecurityScheme | Reference] = {} @property def schemes(self) -> dict[str, SecurityScheme | Reference]: """Return security schemes by name.""" return { scheme_name: self._schemes[scheme_name] for scheme_name in sorted(self._schemes) }
[docs] def register( self, name: str, scheme: SecurityScheme | Reference, ) -> None: """Register security scheme in registry.""" existing = self._schemes.get(name) if existing is not None: if existing != scheme: raise ValueError( f'Security scheme {name!r} is already registered in the ' 'OpenAPI specification. Security scheme names must be ' 'unique. Re-registering the same name with a different ' 'scheme is not allowed.', ) return self._schemes[name] = scheme
def _overlay_ref_site(target: Schema, ref_site: Schema) -> Schema: """ Put the keywords next to ``$ref`` on top of the referenced schema. The component the ``$ref`` points to owns the actual shape, while the sibling keywords only annotate this one usage: set sibling values win, and everything else stays untouched. """ sibling_values = { schema_field.name: field_value for schema_field in dataclasses.fields(ref_site) if (field_value := _is_sibling_set(ref_site, schema_field)) is not EMPTY } if not sibling_values: return target return dataclasses.replace(target, **sibling_values) def _is_sibling_set( ref_site: Schema, schema_field: dataclasses.Field[Any], ) -> Any | EMPTY: """Check that a keyword next to ``$ref`` is really set on the schema.""" field_value = getattr(ref_site, schema_field.name) # Ignore fields with default values and `ref` itself: if ( schema_field.name == 'ref' or field_value == schema_field.default or ( schema_field.default_factory is not dataclasses.MISSING and field_value == schema_field.default_factory() ) ): return EMPTY return field_value def _check_hashes( schema_name: str, annotation: TypeForm[Any] | EMPTY, other_hash: int | None, ) -> None: if annotation is EMPTY: return ann_hash = _safe_hash(annotation) if ( ann_hash is not None and other_hash is not None and ann_hash != other_hash ): raise ValueError( f'Different schemas under a single name: {schema_name}', ) def _safe_hash(annotation: TypeForm[Any] | EMPTY) -> int | None: if annotation is EMPTY: return None try: return hash(annotation) except Exception: # pragma: no cover return None