Source code for dmr.routing

from collections.abc import Callable, Coroutine, Sequence
from http import HTTPStatus
from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar, cast, overload

from django.http import HttpRequest, HttpResponse, HttpResponseBase
from django.urls import path as _django_path
from django.urls.resolvers import RoutePattern, URLPattern, URLResolver
from django.utils.encoding import force_str
from django.views import defaults
from typing_extensions import override

from dmr.errors import ErrorType, format_error
from dmr.exceptions import InternalServerError, NotAcceptableError
from dmr.openapi.collector import controller_mapping_collector
from dmr.openapi.objects import Components, OpenAPI, Paths

if TYPE_CHECKING:
    from django.utils.functional import (
        _StrOrPromise,  # pyright: ignore[reportPrivateUsage]
    )

    from dmr.internal.types import FormatError
    from dmr.openapi.core.context import OpenAPIContext
    from dmr.renderers import Renderer
    from dmr.serializer import BaseSerializer

_CapturedArgs: TypeAlias = tuple[Any, ...]
_CapturedKwargs: TypeAlias = dict[str, int | str]
_RouteMatch: TypeAlias = tuple[str, _CapturedArgs, _CapturedKwargs]
_AnyPattern: TypeAlias = URLPattern | URLResolver

_SerializerT = TypeVar('_SerializerT', bound='BaseSerializer')


[docs] class Router: """Collection of HTTP routes for REST framework.""" __slots__ = ('prefix', 'urls') def __init__(self, prefix: str, urls: Sequence[_AnyPattern]) -> None: """Just stores the passed routes.""" self.prefix = prefix self.urls = urls
[docs] def get_schema(self, context: 'OpenAPIContext') -> OpenAPI: """ Builds OpenAPI specification. This class orchestrates the process of generating a complete OpenAPI specification by collecting controllers from the router, generating path items for each controller, extracting shared components, and merging everything together with the configuration. """ paths_items: Paths = {} for path, pattern, controller in controller_mapping_collector( self.urls, base_path=self.prefix, ): paths_items[path] = controller.get_path_item(path, pattern, context) components = Components( schemas=context.registries.schema.schemas, security_schemes=context.registries.security_scheme.schemes, ) return context.config_merger(paths_items, components)
# We mimic django's name here:
[docs] def build_404_handler( # noqa: WPS114 prefix: str, /, *prefixes: str, serializer: type['BaseSerializer'], format_error: 'FormatError' = format_error, renderers: Sequence['Renderer'] | None = None, ) -> Callable[[HttpRequest, Exception], HttpResponse]: """ Create a 404 handler that returns an response with content negotiation. All prefixes are normalized to start with a leading slash. If the request path matches any of them, a 404 response is returned using the same serializer and renderers as your API. If the client's ``Accept`` does not match any renderer, the first configured renderer is used. For non-matching paths, Django's default ``page_not_found`` handler is used. Args: prefix: Path prefix (e.g. ``'api/'``) for which to return API 404. *prefixes: Additional path prefixes. format_error: Callable used to build the error body for the response. serializer: Serializer class used to serialize the error body. renderers: Optional sequence of renderers. If omitted, uses :attr:`~dmr.settings.Settings.renderers` from settings. See also: https://docs.djangoproject.com/en/stable/ref/views/#the-404-page-not-found-view """ from dmr.internal.negotiation import negotiate_renderer # noqa: PLC0415 from dmr.response import build_response # noqa: PLC0415 from dmr.settings import Settings, resolve_setting # noqa: PLC0415 combined = (prefix, *prefixes) all_prefixes = tuple(f'/{pref.strip("/")}' for pref in combined) renderers_list = ( resolve_setting(Settings.renderers) if renderers is None else renderers ) renderer_by_type = { renderer.content_type: renderer for renderer in renderers_list if not renderer.streaming } default_renderer = next(iter(renderer_by_type.values())) def factory( request: HttpRequest, exception: Exception, ) -> HttpResponse: if not request.path.startswith(all_prefixes): return defaults.page_not_found(request, exception) try: renderer = negotiate_renderer( request, renderer_by_type, default=default_renderer, ) except NotAcceptableError as exc: return build_response( serializer=serializer, raw_data=format_error(exc), status_code=exc.status_code, renderer=default_renderer, ) return build_response( serializer=serializer, raw_data=format_error( 'Page not found', error_type=ErrorType.not_found, ), status_code=HTTPStatus.NOT_FOUND, renderer=renderer, ) return factory
# We mimic django's name here:
[docs] def build_500_handler( # noqa: WPS114 prefix: str, /, *prefixes: str, serializer: type['BaseSerializer'], format_error: 'FormatError' = format_error, renderers: Sequence['Renderer'] | None = None, ) -> Callable[[HttpRequest], HttpResponse]: """ Create a 500 handler that returns an response with content negotiation. All prefixes are normalized to start with a leading slash. If the request path matches any of them, a 500 response is returned using the same serializer and renderers as your API. If the client's ``Accept`` does not match any renderer, the first configured renderer is used. For non-matching paths, Django's default ``server_error`` handler is used. Args: prefix: Path prefix (e.g. ``'api/'``) for which to return API 500. *prefixes: Additional path prefixes. format_error: Callable used to build the error body for the response. serializer: Serializer class used to serialize the error body. renderers: Optional sequence of renderers. If omitted, uses :attr:`~dmr.settings.Settings.renderers` from settings. See also: https://docs.djangoproject.com/en/stable/ref/views/#the-500-server-error-view """ from dmr.internal.negotiation import negotiate_renderer # noqa: PLC0415 from dmr.response import build_response # noqa: PLC0415 from dmr.settings import Settings, resolve_setting # noqa: PLC0415 combined = (prefix, *prefixes) all_prefixes = tuple(f'/{pref.strip("/")}' for pref in combined) renderers_list = ( resolve_setting(Settings.renderers) if renderers is None else renderers ) renderer_by_type = { renderer.content_type: renderer for renderer in renderers_list if not renderer.streaming } default_renderer = next(iter(renderer_by_type.values())) def factory(request: HttpRequest) -> HttpResponse: if not request.path.startswith(all_prefixes): return defaults.server_error(request) try: renderer = negotiate_renderer( request, renderer_by_type, default=default_renderer, ) except NotAcceptableError as exc: return build_response( serializer=serializer, raw_data=format_error(exc), status_code=exc.status_code, renderer=default_renderer, ) return build_response( serializer=serializer, raw_data=format_error( force_str(InternalServerError.default_message), error_type=ErrorType.internal_error, ), status_code=InternalServerError.status_code, renderer=renderer, ) return factory
class _PrefixRoutePattern(RoutePattern): def __init__( self, route: str, name: str | None = None, is_endpoint: bool = False, # noqa: FBT001, FBT002 ) -> None: idx = route.find('<') if idx == -1: self._prefix = route self._is_static = True else: self._is_static = False self._prefix = route[:idx] self._is_endpoint = is_endpoint super().__init__(route, name, is_endpoint) @override def match( self, path: str, ) -> _RouteMatch | None: if self._is_static: if self._is_endpoint and path == self._prefix: return '', (), {} if not self._is_endpoint and path.startswith(self._prefix): return path[len(self._prefix) :], (), {} elif path.startswith(self._prefix): return super().match(path) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] return None # NOTE: keep in sync with `django-stubs`! @overload def path( route: '_StrOrPromise', view: Callable[..., HttpResponseBase], kwargs: dict[str, Any] | None = None, name: str | None = None, ) -> URLPattern: ... @overload def path( route: '_StrOrPromise', view: Callable[..., Coroutine[Any, Any, HttpResponseBase]], kwargs: dict[str, Any] | None = None, name: str | None = None, ) -> URLPattern: ... @overload def path( route: '_StrOrPromise', view: tuple[Sequence[_AnyPattern], str | None, str | None], kwargs: dict[str, Any] | None = None, name: str | None = None, ) -> URLResolver: ... @overload def path( route: '_StrOrPromise', view: Sequence[URLResolver | str], kwargs: dict[str, Any] | None = None, name: str | None = None, ) -> URLResolver: ...
[docs] def path( route: '_StrOrPromise', view: ( Callable[..., HttpResponseBase] | Callable[..., Coroutine[Any, Any, HttpResponseBase]] | tuple[Sequence[_AnyPattern], str | None, str | None] | Sequence[URLResolver | str] ), kwargs: dict[str, Any] | None = None, name: str | None = None, ) -> _AnyPattern: """Creates URL pattern using prefix-based matching for faster routing.""" return cast( _AnyPattern, _django_path( # type: ignore[call-overload] route, view, kwargs, name, Pattern=_PrefixRoutePattern, ), )