from __future__ import annotations
import abc
import dataclasses
from collections.abc import Awaitable, Callable, Mapping, Sequence, Set
from http import HTTPStatus
from typing import ( # noqa: WPS235
TYPE_CHECKING,
Any,
Final,
Generic,
Literal,
Never,
Self,
TypeAlias,
final,
overload,
)
from django.http import HttpRequest, HttpResponseBase
from typing_extensions import (
ParamSpec,
Protocol,
TypeVar,
deprecated,
)
from dmr.exceptions import EndpointMetadataError
from dmr.internal.types import EMPTY, StrOrPromise
if TYPE_CHECKING:
from dmr.controller import Controller
from dmr.cookies import CookieSpec, NewCookie
from dmr.endpoint import Endpoint
from dmr.errors import AsyncErrorHandler, SyncErrorHandler
from dmr.headers import HeaderSpec, NewHeader
from dmr.internal.context import SerializerContext as SerializerContext
from dmr.metadata import ResponseSpec
from dmr.openapi.objects import (
Callback,
ExternalDocumentation,
Link,
Reference,
SecurityRequirement,
Server,
)
from dmr.parsers import Parser
from dmr.renderers import Renderer
from dmr.security.base import AsyncAuth, SyncAuth
from dmr.serializer import BaseSerializer
from dmr.settings import HttpSpec
from dmr.throttling import AsyncThrottle, SyncThrottle
from dmr.validation import (
EndpointMetadataBuilder,
ModifyEndpointPayload,
ValidateEndpointPayload,
)
_ParamT = ParamSpec('_ParamT')
_ReturnT = TypeVar('_ReturnT')
_AnyResponseT = TypeVar(
'_AnyResponseT',
bound=HttpResponseBase | Awaitable[HttpResponseBase],
)
_SyncResponseT = TypeVar('_SyncResponseT', bound=HttpResponseBase)
_AsyncResponseT = TypeVar('_AsyncResponseT', bound=Awaitable[HttpResponseBase])
# Re-exported API:
[docs]
class ModifySyncCallable(Protocol):
"""
Type that represents ``@modify`` decorator for sync functions.
Features:
- Does not allow using ``HttpResponse`` as the return annotation
- Does not allow applying the decorator on async endpoints
"""
@overload
@deprecated(
# It is not actually deprecated, but impossible for the day one.
# But, this is the only way to trigger a typing error.
'Do not use `@modify` decorator with `HttpResponse` return type',
)
def __call__(self, func: Callable[_ParamT, _AnyResponseT], /) -> Never: ...
@overload
@deprecated(
# It is not actually deprecated, but impossible for the day one.
# But, this is the only way to trigger a typing error.
'Sync `error_handler`, `auth` or `throttling` require sync endpoint',
)
def __call__(
self,
func: Callable[_ParamT, Awaitable[_ReturnT]],
/,
) -> Never: ...
@overload
def __call__(
self,
func: Callable[_ParamT, _ReturnT],
/,
) -> Callable[_ParamT, _ReturnT]: ...
[docs]
class ModifyAsyncCallable(Protocol):
"""
Type that represents ``@modify`` decorator for async functions.
Features:
- Does not allow using ``HttpResponse`` as the return annotation
- Does not allow applying the decorator on sync endpoints
"""
@overload
@deprecated(
# It is not actually deprecated, but impossible for the day one.
# But, this is the only way to trigger a typing error.
'Do not use `@modify` decorator with `HttpResponse` return type',
)
def __call__(self, func: Callable[_ParamT, _AnyResponseT], /) -> Never: ...
@overload
def __call__( # type: ignore[overload-overlap]
self,
func: Callable[_ParamT, Awaitable[_ReturnT]],
/,
) -> Callable[_ParamT, _ReturnT]: ...
@overload
@deprecated(
# It is not actually deprecated, but impossible for the day one.
# But, this is the only way to trigger a typing error.
'Async `error_handler`, `auth` or `throttling` require async endpoint',
)
def __call__(
self,
func: Callable[_ParamT, _ReturnT],
/,
) -> Never: ...
[docs]
class ModifyAnyCallable(Protocol):
"""
Type that represents ``@modify`` decorator for any function.
Features:
- Does not allow using ``HttpResponse`` as the return annotation
- Does not allow specifying sync / async specific parts
"""
@overload
@deprecated(
# It is not actually deprecated, but impossible for the day one.
# But, this is the only way to trigger a typing error.
'Do not use `@modify` decorator with `HttpResponse` return type',
)
def __call__(self, func: Callable[_ParamT, _AnyResponseT], /) -> Never: ...
@overload
def __call__(
self,
func: Callable[_ParamT, _ReturnT],
/,
) -> Callable[_ParamT, _ReturnT]: ...
_ControllerT = TypeVar('_ControllerT', bound='Controller[BaseSerializer]')
_ModifyDecoratorT = TypeVar(
'_ModifyDecoratorT',
bound=ModifyAsyncCallable | ModifySyncCallable | ModifyAnyCallable,
)
# We can't split this line into multiline strings,
# because `pyrefly` does not support this pattern.
_CallableOrClassmethod: TypeAlias = (
'classmethod[_ControllerT, [], _ReturnT] | Callable[[type[Any]], _ReturnT]'
)
_BuiltExtrasT_co = TypeVar('_BuiltExtrasT_co', covariant=True, default=Any)
_ExtrasT = TypeVar('_ExtrasT', bound=Extras[Any] | EMPTY, default=EMPTY)
[docs]
@final
@dataclasses.dataclass(frozen=True, slots=True)
class ModifyEndpoint(Generic[_ExtrasT]):
"""
Decorator to modify endpoints that return raw model data.
Apply it to change some API parts:
.. code:: python
>>> from http import HTTPStatus
>>> from dmr import Controller, modify
>>> from dmr.plugins.pydantic import PydanticFastSerializer
>>> class TaskController(Controller[PydanticFastSerializer]):
... @modify(status_code=HTTPStatus.ACCEPTED)
... def post(self) -> list[int]:
... return [1, 2] # id of tasks you have started
Args:
status_code: Shows *status_code* in the documentation.
When *status_code* is passed, always use it by default.
When not provided, we use smart inference
based on the HTTP method name for default returned response.
headers: Shows *headers* in the documentation.
When *headers* are passed we will
add them for the default response.
cookies: Shows *cookies* in the documentation.
When *cookies* are passed we will add
them for the default response.
validate_responses: Do we have to run runtime validation
of responses for this endpoint? Customizable via global setting,
per controller, and per endpoint.
Here we only store the per endpoint information.
exclude_validate_responses: Set of status codes that we don't
validate, even when ``validate_responses`` is enabled.
Useful for errors like ``500`` that can be raised
from anywhere and that you might not want to describe.
Overrides controller and settings values.
Set it to ``None`` to validate all status codes back.
semantic_schema: Should we generate any
semantic schema for this endpoint?
semantic_responses: Should semantic responses be collected
from different providers for this endpoint.
exclude_semantic_responses: Set of semantic responses status codes
that user wants to disable.
Overrides controller and settings values.
Set it to ``None`` to enable all semantic responses back.
semantic_auth: Should semantic auth be collected
from different providers for this endpoint.
Overrides controller and settings values.
exclude_semantic_auth: Set of semantic security requirements names
that should not be collected.
Overrides controller and settings values.
extra_responses: Sequence of extra responses
that this endpoint can return.
Overrides controller and settings values.
Set it to ``None`` to only use the default response.
no_validate_http_spec: Set of http spec validation checks
that we disable for this endpoint.
Overrides controller and settings values.
Set it to ``None`` to enable all checks back.
error_handler: Callback function to be called
when this endpoint faces an exception.
parsers: Sequence of types to be used for this endpoint
to parse incoming request's body. All types must be subtypes
of :class:`~dmr.parsers.Parser`.
Overrides controller and settings values.
renderers: Sequence of types to be used for this endpoint
to render response's body. All types must be subtypes
of :class:`~dmr.renderers.Renderer`.
Overrides controller and settings values.
validate_negotiation: Should we validate that returned response's
``Content-Type`` header matches the one
that we inferred in the negotiation process?
auth: Sequence of auth instances to be used for this endpoint.
Sync endpoints must use instances
of :class:`dmr.security.SyncAuth`.
Async endpoints must use instances
of :class:`dmr.security.AsyncAuth`.
Overrides controller and settings values.
Set it to ``None`` to disable auth for this endpoint.
throttling: Sequence of throttle instances
to be used for this endpoint.
Sync endpoints must use instances
of :class:`dmr.throttling.SyncThrottle`.
Async endpoints must use instances
of :class:`dmr.throttling.AsyncThrottle`.
Overrides controller and settings values.
Set it to ``None`` to disable throttling of this endpoint.
summary: A short summary of what the operation does.
Defaults to the first paragraph of the endpoint's docstring.
Set it to ``None`` to have no summary at all.
description: A verbose explanation of the operation behavior.
Defaults to everything that goes after the first paragraph
of the endpoint's docstring.
Set it to ``None`` to have no description at all.
tags: A sequence of tags for API documentation control.
Used to group operations in OpenAPI documentation.
Overrides controller-level and router-level tags.
Set it to ``None`` to have no tags at all.
operation_id: Unique string used to identify the operation.
deprecated: Declares this operation to be deprecated.
Overrides controller-level and router-level values.
Set it to ``False`` to explicitly mark this operation
as not deprecated, even if its controller or router is.
security: A sequence of security requirement objects for this operation.
Overrides the controller value and ``security``
of :class:`dmr.openapi.OpenAPIConfig`.
Set it to ``None`` to disable ``security`` of this endpoint.
It is merged with the requirements generated from ``auth``,
but it never affects the runtime auth.
Useful to document external security mechanisms,
for example, the ones enforced by an HTTP proxy.
Used security schemes must be declared
as ``security_schemes`` in ``components``
of :class:`dmr.openapi.OpenAPIConfig`.
See :ref:`customizing_security_openapi`.
external_docs: Additional external documentation for this operation.
Overrides the controller value.
Set it to ``None`` to have no external docs at all.
callbacks: A map of possible out-of band callbacks related to the
parent operation. The key is a unique identifier
for the Callback Object. Each value in the map
is a Callback Object that describes
a request that may be initiated by the API provider and the
expected responses. Overrides the controller value.
Set it to ``None`` to have no callbacks at all.
servers: An alternative servers sequence to service this operation.
Overrides the controller value.
Set it to ``None`` to have no servers at all.
x_extensions: Specification extensions of this operation,
keys must start with ``x-``. Set only on this operation:
it is never inherited from or merged with
:attr:`~dmr.controller.Controller.x_extensions`,
which describes the path item, not its operations.
links: Possible links to other OpenAPI operations.
response_description: Description for the generated response object.
ignore_from_spec: If set to ``True``, this endpoint
would not be added to the final OpenAPI spec.
extras: Extra settings for custom controllers.
See :ref:`modify-and-validate-with-extras` to learn more.
Returns:
The same function with ``__dmr_payload__`` payload instance.
.. warning::
Do not disable ``validate_responses`` unless
this is performance critical for you!
.. versionchanged:: 0.15.0
``modify`` used to be a function, now it is an instance
with ``lazy`` method for lazy reusable endpoints.
.. versionchanged:: 0.16.0
Removed *validate_events* parameter, added *extras* parameter instead.
This class is now generic and public.
"""
extras_cls: type[_ExtrasT] | EMPTY = EMPTY
"""
Extras class that this decorator instance supports.
Pass it to create a typed decorator: ``ModifyEndpoint(MyExtras)``.
Controllers using such decorator must use the same extras class
in :attr:`~dmr.controller.Controller.extras`.
"""
@overload
def __call__( # pyright: ignore[reportOverlappingOverload]
self,
*,
error_handler: EMPTY = EMPTY,
status_code: HTTPStatus | EMPTY = EMPTY,
headers: Mapping[str, NewHeader | HeaderSpec] | EMPTY = EMPTY,
cookies: Mapping[str, NewCookie | CookieSpec] | EMPTY = EMPTY,
validate_responses: bool | EMPTY = EMPTY,
exclude_validate_responses: Set[HTTPStatus] | EMPTY | None = EMPTY,
semantic_schema: bool | EMPTY = EMPTY,
semantic_responses: bool | EMPTY = EMPTY,
exclude_semantic_responses: Set[HTTPStatus] | EMPTY | None = EMPTY,
semantic_auth: bool | EMPTY = EMPTY,
exclude_semantic_auth: Set[str] | EMPTY | None = EMPTY,
extra_responses: Sequence[ResponseSpec] | EMPTY | None = EMPTY,
no_validate_http_spec: Set[HttpSpec] | EMPTY | None = EMPTY,
parsers: Sequence[Parser] | EMPTY = EMPTY,
renderers: Sequence[Renderer] | EMPTY = EMPTY,
validate_negotiation: bool | EMPTY = EMPTY,
auth: Sequence[Never] | EMPTY | None = EMPTY,
throttling: Sequence[Never] | EMPTY | None = EMPTY,
summary: StrOrPromise | EMPTY | None = EMPTY,
description: StrOrPromise | EMPTY | None = EMPTY,
tags: Sequence[str] | EMPTY | None = EMPTY,
operation_id: str | EMPTY = EMPTY,
deprecated: bool | EMPTY = EMPTY,
security: Sequence[SecurityRequirement] | EMPTY | None = EMPTY,
external_docs: ExternalDocumentation | EMPTY | None = EMPTY,
callbacks: Mapping[str, Callback | Reference] | EMPTY | None = EMPTY,
servers: Sequence[Server] | EMPTY | None = EMPTY,
x_extensions: Mapping[str, Any] | EMPTY = EMPTY,
links: Mapping[str, Link | Reference] | EMPTY = EMPTY,
response_description: str | EMPTY = EMPTY,
ignore_from_spec: bool | EMPTY = EMPTY,
extras: _ExtrasT | EMPTY = EMPTY,
) -> ModifyAnyCallable: ...
@overload
def __call__(
self,
*,
error_handler: AsyncErrorHandler | EMPTY = EMPTY,
status_code: HTTPStatus | EMPTY = EMPTY,
headers: Mapping[str, NewHeader | HeaderSpec] | EMPTY = EMPTY,
cookies: Mapping[str, NewCookie | CookieSpec] | EMPTY = EMPTY,
validate_responses: bool | EMPTY = EMPTY,
exclude_validate_responses: Set[HTTPStatus] | EMPTY | None = EMPTY,
semantic_schema: bool | EMPTY = EMPTY,
semantic_responses: bool | EMPTY = EMPTY,
exclude_semantic_responses: Set[HTTPStatus] | EMPTY | None = EMPTY,
semantic_auth: bool | EMPTY = EMPTY,
exclude_semantic_auth: Set[str] | EMPTY | None = EMPTY,
extra_responses: Sequence[ResponseSpec] | EMPTY | None = EMPTY,
no_validate_http_spec: Set[HttpSpec] | EMPTY | None = EMPTY,
parsers: Sequence[Parser] | EMPTY = EMPTY,
renderers: Sequence[Renderer] | EMPTY = EMPTY,
validate_negotiation: bool | EMPTY = EMPTY,
auth: Sequence[AsyncAuth] | EMPTY | None = EMPTY,
throttling: Sequence[AsyncThrottle] | EMPTY | None = EMPTY,
summary: StrOrPromise | EMPTY | None = EMPTY,
description: StrOrPromise | EMPTY | None = EMPTY,
tags: Sequence[str] | EMPTY | None = EMPTY,
operation_id: str | EMPTY = EMPTY,
deprecated: bool | EMPTY = EMPTY,
security: Sequence[SecurityRequirement] | EMPTY | None = EMPTY,
external_docs: ExternalDocumentation | EMPTY | None = EMPTY,
callbacks: Mapping[str, Callback | Reference] | EMPTY | None = EMPTY,
servers: Sequence[Server] | EMPTY | None = EMPTY,
x_extensions: Mapping[str, Any] | EMPTY = EMPTY,
links: Mapping[str, Link | Reference] | EMPTY = EMPTY,
response_description: str | EMPTY = EMPTY,
ignore_from_spec: bool | EMPTY = EMPTY,
extras: _ExtrasT | EMPTY = EMPTY,
) -> ModifyAsyncCallable: ...
@overload
def __call__(
self,
*,
error_handler: SyncErrorHandler | EMPTY = EMPTY,
status_code: HTTPStatus | EMPTY = EMPTY,
headers: Mapping[str, NewHeader | HeaderSpec] | EMPTY = EMPTY,
cookies: Mapping[str, NewCookie | CookieSpec] | EMPTY = EMPTY,
validate_responses: bool | EMPTY = EMPTY,
exclude_validate_responses: Set[HTTPStatus] | EMPTY | None = EMPTY,
semantic_schema: bool | EMPTY = EMPTY,
semantic_responses: bool | EMPTY = EMPTY,
exclude_semantic_responses: Set[HTTPStatus] | EMPTY | None = EMPTY,
semantic_auth: bool | EMPTY = EMPTY,
exclude_semantic_auth: Set[str] | EMPTY | None = EMPTY,
extra_responses: Sequence[ResponseSpec] | EMPTY | None = EMPTY,
no_validate_http_spec: Set[HttpSpec] | EMPTY | None = EMPTY,
parsers: Sequence[Parser] | EMPTY = EMPTY,
renderers: Sequence[Renderer] | EMPTY = EMPTY,
validate_negotiation: bool | EMPTY = EMPTY,
auth: Sequence[SyncAuth] | EMPTY | None = EMPTY,
throttling: Sequence[SyncThrottle] | EMPTY | None = EMPTY,
summary: StrOrPromise | EMPTY | None = EMPTY,
description: StrOrPromise | EMPTY | None = EMPTY,
tags: Sequence[str] | EMPTY | None = EMPTY,
operation_id: str | EMPTY = EMPTY,
deprecated: bool | EMPTY = EMPTY,
security: Sequence[SecurityRequirement] | EMPTY | None = EMPTY,
external_docs: ExternalDocumentation | EMPTY | None = EMPTY,
callbacks: Mapping[str, Callback | Reference] | EMPTY | None = EMPTY,
servers: Sequence[Server] | EMPTY | None = EMPTY,
x_extensions: Mapping[str, Any] | EMPTY = EMPTY,
links: Mapping[str, Link | Reference] | EMPTY = EMPTY,
response_description: str | EMPTY = EMPTY,
ignore_from_spec: bool | EMPTY = EMPTY,
extras: _ExtrasT | EMPTY = EMPTY,
) -> ModifySyncCallable: ...
[docs]
def __call__( # noqa: WPS211
self,
*,
status_code: HTTPStatus | EMPTY = EMPTY,
headers: Mapping[str, NewHeader | HeaderSpec] | EMPTY = EMPTY,
cookies: Mapping[str, NewCookie | CookieSpec] | EMPTY = EMPTY,
validate_responses: bool | EMPTY = EMPTY,
exclude_validate_responses: Set[HTTPStatus] | EMPTY | None = EMPTY,
semantic_schema: bool | EMPTY = EMPTY,
semantic_responses: bool | EMPTY = EMPTY,
exclude_semantic_responses: Set[HTTPStatus] | EMPTY | None = EMPTY,
semantic_auth: bool | EMPTY = EMPTY,
exclude_semantic_auth: Set[str] | EMPTY | None = EMPTY,
extra_responses: Sequence[ResponseSpec] | EMPTY | None = EMPTY,
no_validate_http_spec: Set[HttpSpec] | EMPTY | None = EMPTY,
error_handler: SyncErrorHandler | AsyncErrorHandler | EMPTY = EMPTY,
parsers: Sequence[Parser] | EMPTY = EMPTY,
renderers: Sequence[Renderer] | EMPTY = EMPTY,
validate_negotiation: bool | EMPTY = EMPTY,
auth: Sequence[AsyncAuth] | Sequence[SyncAuth] | EMPTY | None = EMPTY,
throttling: (
Sequence[AsyncThrottle] | Sequence[SyncThrottle] | EMPTY | None
) = EMPTY,
summary: StrOrPromise | EMPTY | None = EMPTY,
description: StrOrPromise | EMPTY | None = EMPTY,
tags: Sequence[str] | EMPTY | None = EMPTY,
operation_id: str | EMPTY = EMPTY,
deprecated: bool | EMPTY = EMPTY,
security: Sequence[SecurityRequirement] | EMPTY | None = EMPTY,
external_docs: ExternalDocumentation | EMPTY | None = EMPTY,
callbacks: Mapping[str, Callback | Reference] | EMPTY | None = EMPTY,
servers: Sequence[Server] | EMPTY | None = EMPTY,
x_extensions: Mapping[str, Any] | EMPTY = EMPTY,
links: Mapping[str, Link | Reference] | EMPTY = EMPTY,
response_description: str | EMPTY = EMPTY,
ignore_from_spec: bool | EMPTY = EMPTY,
extras: _ExtrasT | EMPTY = EMPTY,
) -> ModifyAsyncCallable | ModifySyncCallable | ModifyAnyCallable:
"""Adds the payload to the endpoint function."""
from dmr.validation import ModifyEndpointPayload # noqa: PLC0415
return _add_payload( # type: ignore[return-value]
payload=ModifyEndpointPayload(
status_code=status_code,
headers=headers,
cookies=cookies,
responses=extra_responses,
validate_responses=validate_responses,
exclude_validate_responses=exclude_validate_responses,
semantic_schema=semantic_schema,
semantic_responses=semantic_responses,
exclude_semantic_responses=exclude_semantic_responses,
semantic_auth=semantic_auth,
exclude_semantic_auth=exclude_semantic_auth,
no_validate_http_spec=no_validate_http_spec,
error_handler=error_handler,
parsers=parsers,
renderers=renderers,
validate_negotiation=validate_negotiation,
security=security,
auth=auth,
throttling=throttling,
summary=summary,
description=description,
tags=tags,
operation_id=operation_id,
deprecated=deprecated,
external_docs=external_docs,
callbacks=callbacks,
servers=servers,
x_extensions=x_extensions,
links=links,
response_description=response_description,
ignore_from_spec=ignore_from_spec,
extras=extras,
extras_cls=self.extras_cls, # type: ignore[arg-type]
),
)
[docs]
def lazy(
self,
provider: _CallableOrClassmethod[_ControllerT, _ModifyDecoratorT],
) -> _ModifyDecoratorT:
"""
Create lazy endpoint for reusable controller definition.
This provides an agile way to customize all the response details,
including headers and cookies spec, response status code,
OpenAPI metadata and other things.
See :ref:`lazy-reusable-endpoints` for the full example.
.. versionadded:: 0.15.0
"""
return _lazy_payload(provider)
#: Default instance of :class:`ModifyEndpoint` for regular controllers.
modify: Final = ModifyEndpoint()
class _ValidateCallable(Protocol):
"""
Common base for all ``@validate`` decorator callables.
Serves as a nominal bound for ``@validate.lazy``: type checkers
must not structurally compare the sync / async / any protocols
against each other, since their generic ``__call__`` signatures
have incompatible type variable bounds.
"""
# This is a typing-hack to make `@modify` and `@validate` decorators
# incompatible, so `@validate.lazy(modify_spec)` and vice a versa
# can't be used in a real code.
__dmr_fake_marker__: Literal['validate']
[docs]
class ValidateSyncCallable(_ValidateCallable, Protocol):
"""
Type that represents ``@validate`` decorator for sync functions.
Features:
- Does not allow using anything other
than ``HttpResponse`` as the return annotation
- Does not allow applying the decorator on async endpoints
.. versionadded:: 0.15.0
"""
def __call__( # noqa: D102
self,
func: Callable[_ParamT, _SyncResponseT],
/,
) -> Callable[_ParamT, _SyncResponseT]: ...
[docs]
class ValidateAsyncCallable(_ValidateCallable, Protocol):
"""
Type that represents ``@validate`` decorator for async functions.
Features:
- Does not allow using anything other
than ``HttpResponse`` as the return annotation
- Does not allow applying the decorator on sync endpoints
.. versionadded:: 0.15.0
"""
def __call__( # noqa: D102
self,
func: Callable[_ParamT, _AsyncResponseT],
/,
) -> Callable[_ParamT, _AsyncResponseT]: ...
[docs]
class ValidateAnyCallable(_ValidateCallable, Protocol):
"""
Type that represents ``@modify`` decorator for any function.
Features:
- Does not allow using anything other
than ``HttpResponse`` as the return annotation
- Does not allow specifying sync / async specific parts
.. versionadded:: 0.15.0
"""
def __call__( # noqa: D102
self,
func: Callable[_ParamT, _AnyResponseT],
/,
) -> Callable[_ParamT, _AnyResponseT]: ...
_ValidateDecoratorT = TypeVar('_ValidateDecoratorT', bound=_ValidateCallable)
[docs]
@final
@dataclasses.dataclass(frozen=True, slots=True)
class ValidateEndpoint(Generic[_ExtrasT]):
"""
Decorator to validate responses from endpoints that return ``HttpResponse``.
Apply it to validate important API parts:
.. code:: python
>>> from http import HTTPStatus
>>> from django.http import HttpResponse
>>> from dmr import Controller, validate, ResponseSpec
>>> from dmr.plugins.pydantic import PydanticFastSerializer
>>> class TaskController(Controller[PydanticFastSerializer]):
... @validate(
... ResponseSpec(
... return_type=list[int],
... status_code=HTTPStatus.OK,
... ),
... )
... def post(self) -> HttpResponse:
... return HttpResponse(b'[1, 2]', status=HTTPStatus.OK)
Response validation can be disabled for extra speed
by sending *validate_responses* falsy parameter
or by setting this configuration in your ``settings.py`` file:
.. code-block:: python
:caption: settings.py
>>> DMR_SETTINGS = {'validate_responses': False}
Args:
response: The main response that this endpoint is allowed to return.
responses: A collection of other responses that are allowed
to be returned from this endpoint.
validate_responses: Do we have to run runtime validation
of responses for this endpoint? Customizable via global setting,
per controller, and per endpoint.
Here we only store the per endpoint information.
exclude_validate_responses: Set of status codes that we don't
validate, even when ``validate_responses`` is enabled.
Useful for errors like ``500`` that can be raised
from anywhere and that you might not want to describe.
Overrides controller and settings values.
Set it to ``None`` to validate all status codes back.
semantic_schema: Should we generate any
semantic schema for this endpoint?
semantic_responses: Should semantic responses be collected
from different providers for this endpoint.
Overrides controller and settings values.
exclude_semantic_responses: Set of semantic responses status codes
that user wants to disable.
Overrides controller and settings values.
Set it to ``None`` to enable all semantic responses back.
semantic_auth: Should semantic auth be collected
from different providers for this endpoint.
Overrides controller and settings values.
exclude_semantic_auth: Set of semantic security requirements names
that should not be collected.
Overrides controller and settings values.
no_validate_http_spec: Set of http spec validation checks
that we disable for this endpoint.
Overrides controller and settings values.
Set it to ``None`` to enable all checks back.
error_handler: Callback function to be called
when this endpoint faces an exception.
parsers: Sequence of types to be used for this endpoint
to parse incoming request's body. All types must be subtypes
of :class:`~dmr.parsers.Parser`.
Overrides controller and settings values.
renderers: Sequence of types to be used for this endpoint
to render response's body. All types must be subtypes
of :class:`~dmr.renderers.Renderer`.
Overrides controller and settings values.
validate_negotiation: Should we validate that returned response's
``Content-Type`` header matches the one
that we inferred in the negotiation process?
auth: Sequence of auth instances to be used for this endpoint.
Sync endpoints must use instances
of :class:`dmr.security.SyncAuth`.
Async endpoints must use instances
of :class:`dmr.security.AsyncAuth`.
Overrides controller and settings values.
Set it to ``None`` to disable auth for this endpoint.
throttling: Sequence of throttle instances to be used for this endpoint.
Sync endpoints must use instances
of :class:`dmr.throttling.SyncThrottle`.
Async endpoints must use instances
of :class:`dmr.throttling.AsyncThrottle`.
Overrides controller and settings values.
Set it to ``None`` to disable throttling of this endpoint.
summary: A short summary of what the operation does.
Defaults to the first paragraph of the endpoint's docstring.
Set it to ``None`` to have no summary at all.
description: A verbose explanation of the operation behavior.
Defaults to everything that goes after the first paragraph
of the endpoint's docstring.
Set it to ``None`` to have no description at all.
tags: A sequence of tags for API documentation control.
Used to group operations in OpenAPI documentation.
Overrides controller-level and router-level tags.
Set it to ``None`` to have no tags at all.
operation_id: Unique string used to identify the operation.
deprecated: Declares this operation to be deprecated.
Overrides controller-level and router-level values.
Set it to ``False`` to explicitly mark this operation
as not deprecated, even if its controller or router is.
security: A sequence of security requirement objects for this operation.
Overrides the controller value and ``security``
of :class:`dmr.openapi.OpenAPIConfig`.
Set it to ``None`` to disable ``security`` of this endpoint.
It is merged with the requirements generated from ``auth``,
but it never affects the runtime auth.
Useful to document external security mechanisms,
for example, the ones enforced by an HTTP proxy.
Used security schemes must be declared
as ``security_schemes`` in ``components``
of :class:`dmr.openapi.OpenAPIConfig`.
See :ref:`customizing_security_openapi`.
external_docs: Additional external documentation for this operation.
Overrides the controller value.
Set it to ``None`` to have no external docs at all.
callbacks: A map of possible out-of band callbacks related to the
parent operation. The key is a unique identifier for the Callback
Object. Each value in the map is a Callback Object that describes
a request that may be initiated by the API provider and the
expected responses. Overrides the controller value.
Set it to ``None`` to have no callbacks at all.
servers: An alternative servers sequence to service this operation.
Overrides the controller value.
Set it to ``None`` to have no servers at all.
x_extensions: Specification extensions of this operation,
keys must start with ``x-``. Set only on this operation:
it is never inherited from or merged with
:attr:`~dmr.controller.Controller.x_extensions`,
which describes the path item, not its operations.
ignore_from_spec: If set to ``True``, this endpoint
would not be added to the final OpenAPI spec.
extras: Extra settings for custom controllers.
See :ref:`modify-and-validate-with-extras` to learn more.
Returns:
The same function with ``__dmr_payload__`` payload instance.
.. warning::
Do not disable ``validate_responses`` unless
this is performance critical for you!
.. versionchanged:: 0.15.0
``validate`` used to be a function, now it is an instance
with ``lazy`` method for lazy reusable endpoints.
.. versionchanged:: 0.16.0
Removed *validate_events* parameter, added *extras* parameter instead.
This class is now generic and public.
"""
extras_cls: type[_ExtrasT] | EMPTY = EMPTY
"""
Extras class that this decorator instance supports.
Pass it to create a typed decorator: ``ValidateEndpoint(MyExtras)``.
Controllers using such decorator must use the same extras class
in :attr:`~dmr.controller.Controller.extras`.
"""
@overload
def __call__(
self,
response: ResponseSpec,
/,
*responses: ResponseSpec,
error_handler: EMPTY = EMPTY,
validate_responses: bool | EMPTY = EMPTY,
exclude_validate_responses: Set[HTTPStatus] | EMPTY | None = EMPTY,
semantic_schema: bool | EMPTY = EMPTY,
semantic_responses: bool | EMPTY = EMPTY,
exclude_semantic_responses: Set[HTTPStatus] | EMPTY | None = EMPTY,
semantic_auth: bool | EMPTY = EMPTY,
exclude_semantic_auth: Set[str] | EMPTY | None = EMPTY,
no_validate_http_spec: Set[HttpSpec] | EMPTY | None = EMPTY,
parsers: Sequence[Parser] | EMPTY = EMPTY,
renderers: Sequence[Renderer] | EMPTY = EMPTY,
validate_negotiation: bool | EMPTY = EMPTY,
auth: Sequence[Never] | EMPTY | None = EMPTY,
throttling: Sequence[Never] | EMPTY | None = EMPTY,
summary: StrOrPromise | EMPTY | None = EMPTY,
description: StrOrPromise | EMPTY | None = EMPTY,
tags: Sequence[str] | EMPTY | None = EMPTY,
operation_id: str | EMPTY = EMPTY,
deprecated: bool | EMPTY = EMPTY,
security: Sequence[SecurityRequirement] | EMPTY | None = EMPTY,
external_docs: ExternalDocumentation | EMPTY | None = EMPTY,
callbacks: Mapping[str, Callback | Reference] | EMPTY | None = EMPTY,
servers: Sequence[Server] | EMPTY | None = EMPTY,
x_extensions: Mapping[str, Any] | EMPTY = EMPTY,
ignore_from_spec: bool | EMPTY = EMPTY,
extras: _ExtrasT | EMPTY = EMPTY,
) -> ValidateAnyCallable: ...
@overload
def __call__( # pyright: ignore[reportOverlappingOverload]
self,
response: ResponseSpec,
/,
*responses: ResponseSpec,
error_handler: AsyncErrorHandler | EMPTY = EMPTY,
validate_responses: bool | EMPTY = EMPTY,
exclude_validate_responses: Set[HTTPStatus] | EMPTY | None = EMPTY,
semantic_schema: bool | EMPTY = EMPTY,
semantic_responses: bool | EMPTY = EMPTY,
exclude_semantic_responses: Set[HTTPStatus] | EMPTY | None = EMPTY,
semantic_auth: bool | EMPTY = EMPTY,
exclude_semantic_auth: Set[str] | EMPTY | None = EMPTY,
no_validate_http_spec: Set[HttpSpec] | EMPTY | None = EMPTY,
parsers: Sequence[Parser] | EMPTY = EMPTY,
renderers: Sequence[Renderer] | EMPTY = EMPTY,
validate_negotiation: bool | EMPTY = EMPTY,
auth: Sequence[AsyncAuth] | EMPTY | None = EMPTY,
throttling: Sequence[AsyncThrottle] | EMPTY | None = EMPTY,
summary: StrOrPromise | EMPTY | None = EMPTY,
description: StrOrPromise | EMPTY | None = EMPTY,
tags: Sequence[str] | EMPTY | None = EMPTY,
operation_id: str | EMPTY = EMPTY,
deprecated: bool | EMPTY = EMPTY,
security: Sequence[SecurityRequirement] | EMPTY | None = EMPTY,
external_docs: ExternalDocumentation | EMPTY | None = EMPTY,
callbacks: Mapping[str, Callback | Reference] | EMPTY | None = EMPTY,
servers: Sequence[Server] | EMPTY | None = EMPTY,
x_extensions: Mapping[str, Any] | EMPTY = EMPTY,
ignore_from_spec: bool | EMPTY = EMPTY,
extras: _ExtrasT | EMPTY = EMPTY,
) -> ValidateAsyncCallable: ...
@overload
def __call__(
self,
response: ResponseSpec,
/,
*responses: ResponseSpec,
error_handler: SyncErrorHandler | EMPTY = EMPTY,
validate_responses: bool | EMPTY = EMPTY,
exclude_validate_responses: Set[HTTPStatus] | EMPTY | None = EMPTY,
semantic_schema: bool | EMPTY = EMPTY,
semantic_responses: bool | EMPTY = EMPTY,
exclude_semantic_responses: Set[HTTPStatus] | EMPTY | None = EMPTY,
semantic_auth: bool | EMPTY = EMPTY,
exclude_semantic_auth: Set[str] | EMPTY | None = EMPTY,
no_validate_http_spec: Set[HttpSpec] | EMPTY | None = EMPTY,
parsers: Sequence[Parser] | EMPTY = EMPTY,
renderers: Sequence[Renderer] | EMPTY = EMPTY,
validate_negotiation: bool | EMPTY = EMPTY,
auth: Sequence[SyncAuth] | EMPTY | None = EMPTY,
throttling: Sequence[SyncThrottle] | EMPTY | None = EMPTY,
summary: StrOrPromise | EMPTY | None = EMPTY,
description: StrOrPromise | EMPTY | None = EMPTY,
tags: Sequence[str] | EMPTY | None = EMPTY,
operation_id: str | EMPTY = EMPTY,
deprecated: bool | EMPTY = EMPTY,
security: Sequence[SecurityRequirement] | EMPTY | None = EMPTY,
external_docs: ExternalDocumentation | EMPTY | None = EMPTY,
callbacks: Mapping[str, Callback | Reference] | EMPTY | None = EMPTY,
servers: Sequence[Server] | EMPTY | None = EMPTY,
x_extensions: Mapping[str, Any] | EMPTY = EMPTY,
ignore_from_spec: bool | EMPTY = EMPTY,
extras: _ExtrasT | EMPTY = EMPTY,
) -> ValidateSyncCallable: ...
[docs]
def __call__( # noqa: WPS211 # pyright: ignore[reportInconsistentOverload]
self,
response: ResponseSpec,
/,
*responses: ResponseSpec,
validate_responses: bool | EMPTY = EMPTY,
exclude_validate_responses: Set[HTTPStatus] | EMPTY | None = EMPTY,
semantic_schema: bool | EMPTY = EMPTY,
semantic_responses: bool | EMPTY = EMPTY,
exclude_semantic_responses: Set[HTTPStatus] | EMPTY | None = EMPTY,
semantic_auth: bool | EMPTY = EMPTY,
exclude_semantic_auth: Set[str] | EMPTY | None = EMPTY,
no_validate_http_spec: Set[HttpSpec] | EMPTY | None = EMPTY,
error_handler: SyncErrorHandler | AsyncErrorHandler | EMPTY = EMPTY,
parsers: Sequence[Parser] | EMPTY = EMPTY,
renderers: Sequence[Renderer] | EMPTY = EMPTY,
validate_negotiation: bool | EMPTY = EMPTY,
auth: Sequence[AsyncAuth] | Sequence[SyncAuth] | EMPTY | None = EMPTY,
throttling: (
Sequence[AsyncThrottle] | Sequence[SyncThrottle] | EMPTY | None
) = EMPTY,
summary: StrOrPromise | EMPTY | None = EMPTY,
description: StrOrPromise | EMPTY | None = EMPTY,
tags: Sequence[str] | EMPTY | None = EMPTY,
operation_id: str | EMPTY = EMPTY,
deprecated: bool | EMPTY = EMPTY,
security: Sequence[SecurityRequirement] | EMPTY | None = EMPTY,
external_docs: ExternalDocumentation | EMPTY | None = EMPTY,
callbacks: Mapping[str, Callback | Reference] | EMPTY | None = EMPTY,
servers: Sequence[Server] | EMPTY | None = EMPTY,
x_extensions: Mapping[str, Any] | EMPTY = EMPTY,
ignore_from_spec: bool | EMPTY = EMPTY,
extras: _ExtrasT | EMPTY = EMPTY,
) -> ValidateAnyCallable | ValidateAsyncCallable | ValidateSyncCallable:
"""Adds the payload to the endpoint function."""
from dmr.validation import ValidateEndpointPayload # noqa: PLC0415
return _add_payload( # type: ignore[return-value]
payload=ValidateEndpointPayload(
responses=[response, *responses],
validate_responses=validate_responses,
exclude_validate_responses=exclude_validate_responses,
semantic_schema=semantic_schema,
semantic_responses=semantic_responses,
exclude_semantic_responses=exclude_semantic_responses,
semantic_auth=semantic_auth,
exclude_semantic_auth=exclude_semantic_auth,
no_validate_http_spec=no_validate_http_spec,
error_handler=error_handler,
parsers=parsers,
renderers=renderers,
validate_negotiation=validate_negotiation,
security=security,
auth=auth,
throttling=throttling,
summary=summary,
description=description,
tags=tags,
operation_id=operation_id,
deprecated=deprecated,
external_docs=external_docs,
callbacks=callbacks,
servers=servers,
x_extensions=x_extensions,
ignore_from_spec=ignore_from_spec,
extras=extras,
extras_cls=self.extras_cls, # type: ignore[arg-type]
),
)
[docs]
def lazy(
self,
provider: _CallableOrClassmethod[_ControllerT, _ValidateDecoratorT],
) -> _ValidateDecoratorT:
"""
Create lazy endpoint for reusable controller definition.
This provides an agile way to customize all the response details,
including headers and cookies spec, response status code,
OpenAPI metadata and other things.
See :ref:`lazy-reusable-endpoints` for the full example.
.. versionadded:: 0.15.0
"""
return _lazy_payload(provider)
#: Default instance of :class:`ValidateEndpoint` for regular controllers.
validate: Final = ValidateEndpoint()
@overload
def request_endpoint(
request: HttpRequest,
*,
strict: Literal[True],
) -> Endpoint: ...
@overload
def request_endpoint(
request: HttpRequest,
*,
strict: bool = False,
) -> Endpoint | None: ...
[docs]
def request_endpoint(
request: HttpRequest,
*,
strict: bool = False,
) -> Endpoint | None:
"""
Return an instance of the ``Endpoint`` that was used for this request.
When *strict* is passed and *request* has no endpoint,
we raise :exc:`AttributeError`.
This can happen for ``405`` responses, for example.
They don't have endpoints. All others do.
.. versionadded:: 0.7.0
"""
endpoint = getattr(request, '__dmr_endpoint__', None)
if endpoint is None and strict:
raise AttributeError('__dmr_endpoint__')
return endpoint
# Internal API:
def _lazy_payload(
provider: _CallableOrClassmethod[_ControllerT, _ReturnT],
) -> _ReturnT:
def factory(controller_cls: type[_ControllerT]) -> Any:
if isinstance(provider, classmethod):
return getattr( # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
controller_cls,
provider.__name__,
provider, # pyright: ignore[reportUnknownArgumentType]
).__func__(controller_cls)
return provider(controller_cls)
return _add_payload(payload=factory) # type: ignore[return-value]
def _add_payload(
*,
payload: (
ModifyEndpointPayload
| ValidateEndpointPayload
| Callable[[type[_ControllerT]], Any]
),
) -> Callable[[Callable[_ParamT, _ReturnT]], Callable[_ParamT, _ReturnT]]:
# Add payload for future use in the Endpoint creation.
def decorator(
func: Callable[_ParamT, _ReturnT],
) -> Callable[_ParamT, _ReturnT]:
func.__dmr_payload__ = payload # type: ignore[attr-defined]
return func
return decorator