diff options
author | cyfraeviolae <cyfraeviolae> | 2024-04-03 03:10:44 -0400 |
---|---|---|
committer | cyfraeviolae <cyfraeviolae> | 2024-04-03 03:10:44 -0400 |
commit | 6d7ba58f880be618ade07f8ea080fe8c4bf8a896 (patch) | |
tree | b1c931051ffcebd2bd9d61d98d6233ffa289bbce /venv/lib/python3.11/site-packages/litestar/handlers | |
parent | 4f884c9abc32990b4061a1bb6997b4b37e58ea0b (diff) |
venv
Diffstat (limited to 'venv/lib/python3.11/site-packages/litestar/handlers')
22 files changed, 3305 insertions, 0 deletions
diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/__init__.py b/venv/lib/python3.11/site-packages/litestar/handlers/__init__.py new file mode 100644 index 0000000..822fe7e --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/__init__.py @@ -0,0 +1,29 @@ +from .asgi_handlers import ASGIRouteHandler, asgi +from .base import BaseRouteHandler +from .http_handlers import HTTPRouteHandler, delete, get, head, patch, post, put, route +from .websocket_handlers import ( + WebsocketListener, + WebsocketListenerRouteHandler, + WebsocketRouteHandler, + websocket, + websocket_listener, +) + +__all__ = ( + "ASGIRouteHandler", + "BaseRouteHandler", + "HTTPRouteHandler", + "WebsocketListener", + "WebsocketRouteHandler", + "WebsocketListenerRouteHandler", + "asgi", + "delete", + "get", + "head", + "patch", + "post", + "put", + "route", + "websocket", + "websocket_listener", +) diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/handlers/__pycache__/__init__.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..6976d76 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/__pycache__/__init__.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/__pycache__/asgi_handlers.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/handlers/__pycache__/asgi_handlers.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..85de8f8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/__pycache__/asgi_handlers.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/__pycache__/base.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/handlers/__pycache__/base.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..2331fc7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/__pycache__/base.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/asgi_handlers.py b/venv/lib/python3.11/site-packages/litestar/handlers/asgi_handlers.py new file mode 100644 index 0000000..91f3517 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/asgi_handlers.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Mapping, Sequence + +from litestar.exceptions import ImproperlyConfiguredException +from litestar.handlers.base import BaseRouteHandler +from litestar.types.builtin_types import NoneType +from litestar.utils.predicates import is_async_callable + +__all__ = ("ASGIRouteHandler", "asgi") + + +if TYPE_CHECKING: + from litestar.types import ( + ExceptionHandlersMap, + Guard, + MaybePartial, # noqa: F401 + ) + + +class ASGIRouteHandler(BaseRouteHandler): + """ASGI Route Handler decorator. + + Use this decorator to decorate ASGI applications. + """ + + __slots__ = ("is_mount", "is_static") + + def __init__( + self, + path: str | Sequence[str] | None = None, + *, + exception_handlers: ExceptionHandlersMap | None = None, + guards: Sequence[Guard] | None = None, + name: str | None = None, + opt: Mapping[str, Any] | None = None, + is_mount: bool = False, + is_static: bool = False, + signature_namespace: Mapping[str, Any] | None = None, + **kwargs: Any, + ) -> None: + """Initialize ``ASGIRouteHandler``. + + Args: + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + name: A string identifying the route handler. + opt: A string key mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or + wherever you have access to :class:`Request <.connection.Request>` or + :class:`ASGI Scope <.types.Scope>`. + path: A path fragment for the route handler function or a list of path fragments. If not given defaults to + ``/`` + is_mount: A boolean dictating whether the handler's paths should be regarded as mount paths. Mount path + accept any arbitrary paths that begin with the defined prefixed path. For example, a mount with the path + ``/some-path/`` will accept requests for ``/some-path/`` and any sub path under this, e.g. + ``/some-path/sub-path/`` etc. + is_static: A boolean dictating whether the handler's paths should be regarded as static paths. Static paths + are used to deliver static files. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + **kwargs: Any additional kwarg - will be set in the opt dictionary. + """ + self.is_mount = is_mount or is_static + self.is_static = is_static + super().__init__( + path, + exception_handlers=exception_handlers, + guards=guards, + name=name, + opt=opt, + signature_namespace=signature_namespace, + **kwargs, + ) + + def _validate_handler_function(self) -> None: + """Validate the route handler function once it's set by inspecting its return annotations.""" + super()._validate_handler_function() + + if not self.parsed_fn_signature.return_type.is_subclass_of(NoneType): + raise ImproperlyConfiguredException("ASGI handler functions should return 'None'") + + if any(key not in self.parsed_fn_signature.parameters for key in ("scope", "send", "receive")): + raise ImproperlyConfiguredException( + "ASGI handler functions should define 'scope', 'send' and 'receive' arguments" + ) + if not is_async_callable(self.fn): + raise ImproperlyConfiguredException("Functions decorated with 'asgi' must be async functions") + + +asgi = ASGIRouteHandler diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/base.py b/venv/lib/python3.11/site-packages/litestar/handlers/base.py new file mode 100644 index 0000000..9dbb70e --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/base.py @@ -0,0 +1,577 @@ +from __future__ import annotations + +from copy import copy +from functools import partial +from typing import TYPE_CHECKING, Any, Callable, Mapping, Sequence, cast + +from litestar._signature import SignatureModel +from litestar.config.app import ExperimentalFeatures +from litestar.di import Provide +from litestar.dto import DTOData +from litestar.exceptions import ImproperlyConfiguredException +from litestar.plugins import DIPlugin, PluginRegistry +from litestar.serialization import default_deserializer, default_serializer +from litestar.types import ( + Dependencies, + Empty, + ExceptionHandlersMap, + Guard, + Middleware, + TypeDecodersSequence, + TypeEncodersMap, +) +from litestar.typing import FieldDefinition +from litestar.utils import ensure_async_callable, get_name, normalize_path +from litestar.utils.helpers import unwrap_partial +from litestar.utils.signature import ParsedSignature, add_types_to_signature_namespace + +if TYPE_CHECKING: + from typing_extensions import Self + + from litestar.app import Litestar + from litestar.connection import ASGIConnection + from litestar.controller import Controller + from litestar.dto import AbstractDTO + from litestar.dto._backend import DTOBackend + from litestar.params import ParameterKwarg + from litestar.router import Router + from litestar.types import AnyCallable, AsyncAnyCallable, ExceptionHandler + from litestar.types.empty import EmptyType + +__all__ = ("BaseRouteHandler",) + + +class BaseRouteHandler: + """Base route handler. + + Serves as a subclass for all route handlers + """ + + __slots__ = ( + "_fn", + "_parsed_data_field", + "_parsed_fn_signature", + "_parsed_return_field", + "_resolved_data_dto", + "_resolved_dependencies", + "_resolved_guards", + "_resolved_layered_parameters", + "_resolved_return_dto", + "_resolved_signature_namespace", + "_resolved_type_decoders", + "_resolved_type_encoders", + "_signature_model", + "dependencies", + "dto", + "exception_handlers", + "guards", + "middleware", + "name", + "opt", + "owner", + "paths", + "return_dto", + "signature_namespace", + "type_decoders", + "type_encoders", + ) + + def __init__( + self, + path: str | Sequence[str] | None = None, + *, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + exception_handlers: ExceptionHandlersMap | None = None, + guards: Sequence[Guard] | None = None, + middleware: Sequence[Middleware] | None = None, + name: str | None = None, + opt: Mapping[str, Any] | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + signature_namespace: Mapping[str, Any] | None = None, + signature_types: Sequence[Any] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + **kwargs: Any, + ) -> None: + """Initialize ``HTTPRouteHandler``. + + Args: + path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults + to ``/`` + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and + validation of request data. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + name: A string identifying the route handler. + opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or + wherever you have access to :class:`Request <.connection.Request>` or + :class:`ASGI Scope <.types.Scope>`. + return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing + outbound response data. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature + modelling. + signature_types: A sequence of types for use in forward reference resolution during signature modeling. + These types will be added to the signature namespace using their ``__name__`` attribute. + type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec hook for deserialization. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + **kwargs: Any additional kwarg - will be set in the opt dictionary. + """ + self._parsed_fn_signature: ParsedSignature | EmptyType = Empty + self._parsed_return_field: FieldDefinition | EmptyType = Empty + self._parsed_data_field: FieldDefinition | None | EmptyType = Empty + self._resolved_data_dto: type[AbstractDTO] | None | EmptyType = Empty + self._resolved_dependencies: dict[str, Provide] | EmptyType = Empty + self._resolved_guards: list[Guard] | EmptyType = Empty + self._resolved_layered_parameters: dict[str, FieldDefinition] | EmptyType = Empty + self._resolved_return_dto: type[AbstractDTO] | None | EmptyType = Empty + self._resolved_signature_namespace: dict[str, Any] | EmptyType = Empty + self._resolved_type_decoders: TypeDecodersSequence | EmptyType = Empty + self._resolved_type_encoders: TypeEncodersMap | EmptyType = Empty + self._signature_model: type[SignatureModel] | EmptyType = Empty + + self.dependencies = dependencies + self.dto = dto + self.exception_handlers = exception_handlers + self.guards = guards + self.middleware = middleware + self.name = name + self.opt = dict(opt or {}) + self.opt.update(**kwargs) + self.owner: Controller | Router | None = None + self.return_dto = return_dto + self.signature_namespace = add_types_to_signature_namespace( + signature_types or [], dict(signature_namespace or {}) + ) + self.type_decoders = type_decoders + self.type_encoders = type_encoders + + self.paths = ( + {normalize_path(p) for p in path} if path and isinstance(path, list) else {normalize_path(path or "/")} # type: ignore[arg-type] + ) + + def __call__(self, fn: AsyncAnyCallable) -> Self: + """Replace a function with itself.""" + self._fn = fn + return self + + @property + def handler_id(self) -> str: + """A unique identifier used for generation of DTOs.""" + return f"{self!s}::{sum(id(layer) for layer in self.ownership_layers)}" + + @property + def default_deserializer(self) -> Callable[[Any, Any], Any]: + """Get a default deserializer for the route handler. + + Returns: + A default deserializer for the route handler. + + """ + return partial(default_deserializer, type_decoders=self.resolve_type_decoders()) + + @property + def default_serializer(self) -> Callable[[Any], Any]: + """Get a default serializer for the route handler. + + Returns: + A default serializer for the route handler. + + """ + return partial(default_serializer, type_encoders=self.resolve_type_encoders()) + + @property + def signature_model(self) -> type[SignatureModel]: + """Get the signature model for the route handler. + + Returns: + A signature model for the route handler. + + """ + if self._signature_model is Empty: + self._signature_model = SignatureModel.create( + dependency_name_set=self.dependency_name_set, + fn=cast("AnyCallable", self.fn), + parsed_signature=self.parsed_fn_signature, + data_dto=self.resolve_data_dto(), + type_decoders=self.resolve_type_decoders(), + ) + return self._signature_model + + @property + def fn(self) -> AsyncAnyCallable: + """Get the handler function. + + Raises: + ImproperlyConfiguredException: if handler fn is not set. + + Returns: + Handler function + """ + if not hasattr(self, "_fn"): + raise ImproperlyConfiguredException("No callable has been registered for this handler") + return self._fn + + @property + def parsed_fn_signature(self) -> ParsedSignature: + """Return the parsed signature of the handler function. + + This method is memoized so the computation occurs only once. + + Returns: + A ParsedSignature instance + """ + if self._parsed_fn_signature is Empty: + self._parsed_fn_signature = ParsedSignature.from_fn( + unwrap_partial(self.fn), self.resolve_signature_namespace() + ) + + return self._parsed_fn_signature + + @property + def parsed_return_field(self) -> FieldDefinition: + if self._parsed_return_field is Empty: + self._parsed_return_field = self.parsed_fn_signature.return_type + return self._parsed_return_field + + @property + def parsed_data_field(self) -> FieldDefinition | None: + if self._parsed_data_field is Empty: + self._parsed_data_field = self.parsed_fn_signature.parameters.get("data") + return self._parsed_data_field + + @property + def handler_name(self) -> str: + """Get the name of the handler function. + + Raises: + ImproperlyConfiguredException: if handler fn is not set. + + Returns: + Name of the handler function + """ + return get_name(unwrap_partial(self.fn)) + + @property + def dependency_name_set(self) -> set[str]: + """Set of all dependency names provided in the handler's ownership layers.""" + layered_dependencies = (layer.dependencies or {} for layer in self.ownership_layers) + return {name for layer in layered_dependencies for name in layer} # pyright: ignore + + @property + def ownership_layers(self) -> list[Self | Controller | Router]: + """Return the handler layers from the app down to the route handler. + + ``app -> ... -> route handler`` + """ + layers = [] + + cur: Any = self + while cur: + layers.append(cur) + cur = cur.owner + + return list(reversed(layers)) + + @property + def app(self) -> Litestar: + return cast("Litestar", self.ownership_layers[0]) + + def resolve_type_encoders(self) -> TypeEncodersMap: + """Return a merged type_encoders mapping. + + This method is memoized so the computation occurs only once. + + Returns: + A dict of type encoders + """ + if self._resolved_type_encoders is Empty: + self._resolved_type_encoders = {} + + for layer in self.ownership_layers: + if type_encoders := getattr(layer, "type_encoders", None): + self._resolved_type_encoders.update(type_encoders) + return cast("TypeEncodersMap", self._resolved_type_encoders) + + def resolve_type_decoders(self) -> TypeDecodersSequence: + """Return a merged type_encoders mapping. + + This method is memoized so the computation occurs only once. + + Returns: + A dict of type encoders + """ + if self._resolved_type_decoders is Empty: + self._resolved_type_decoders = [] + + for layer in self.ownership_layers: + if type_decoders := getattr(layer, "type_decoders", None): + self._resolved_type_decoders.extend(list(type_decoders)) + return cast("TypeDecodersSequence", self._resolved_type_decoders) + + def resolve_layered_parameters(self) -> dict[str, FieldDefinition]: + """Return all parameters declared above the handler.""" + if self._resolved_layered_parameters is Empty: + parameter_kwargs: dict[str, ParameterKwarg] = {} + + for layer in self.ownership_layers: + parameter_kwargs.update(getattr(layer, "parameters", {}) or {}) + + self._resolved_layered_parameters = { + key: FieldDefinition.from_kwarg(name=key, annotation=parameter.annotation, kwarg_definition=parameter) + for key, parameter in parameter_kwargs.items() + } + + return self._resolved_layered_parameters + + def resolve_guards(self) -> list[Guard]: + """Return all guards in the handlers scope, starting from highest to current layer.""" + if self._resolved_guards is Empty: + self._resolved_guards = [] + + for layer in self.ownership_layers: + self._resolved_guards.extend(layer.guards or []) # pyright: ignore + + self._resolved_guards = cast( + "list[Guard]", [ensure_async_callable(guard) for guard in self._resolved_guards] + ) + + return self._resolved_guards + + def _get_plugin_registry(self) -> PluginRegistry | None: + from litestar.app import Litestar + + root_owner = self.ownership_layers[0] + if isinstance(root_owner, Litestar): + return root_owner.plugins + return None + + def resolve_dependencies(self) -> dict[str, Provide]: + """Return all dependencies correlating to handler function's kwargs that exist in the handler's scope.""" + plugin_registry = self._get_plugin_registry() + if self._resolved_dependencies is Empty: + self._resolved_dependencies = {} + for layer in self.ownership_layers: + for key, provider in (layer.dependencies or {}).items(): + self._resolved_dependencies[key] = self._resolve_dependency( + key=key, provider=provider, plugin_registry=plugin_registry + ) + + return self._resolved_dependencies + + def _resolve_dependency( + self, key: str, provider: Provide | AnyCallable, plugin_registry: PluginRegistry | None + ) -> Provide: + if not isinstance(provider, Provide): + provider = Provide(provider) + + if self._resolved_dependencies is not Empty: # pragma: no cover + self._validate_dependency_is_unique(dependencies=self._resolved_dependencies, key=key, provider=provider) + + if not getattr(provider, "parsed_fn_signature", None): + dependency = unwrap_partial(provider.dependency) + plugin: DIPlugin | None = None + if plugin_registry: + plugin = next( + (p for p in plugin_registry.di if isinstance(p, DIPlugin) and p.has_typed_init(dependency)), + None, + ) + if plugin: + signature, init_type_hints = plugin.get_typed_init(dependency) + provider.parsed_fn_signature = ParsedSignature.from_signature(signature, init_type_hints) + else: + provider.parsed_fn_signature = ParsedSignature.from_fn(dependency, self.resolve_signature_namespace()) + + if not getattr(provider, "signature_model", None): + provider.signature_model = SignatureModel.create( + dependency_name_set=self.dependency_name_set, + fn=provider.dependency, + parsed_signature=provider.parsed_fn_signature, + data_dto=self.resolve_data_dto(), + type_decoders=self.resolve_type_decoders(), + ) + return provider + + def resolve_middleware(self) -> list[Middleware]: + """Build the middleware stack for the RouteHandler and return it. + + The middlewares are added from top to bottom (``app -> router -> controller -> route handler``) and then + reversed. + """ + resolved_middleware: list[Middleware] = [] + for layer in self.ownership_layers: + resolved_middleware.extend(layer.middleware or []) # pyright: ignore + return list(reversed(resolved_middleware)) + + def resolve_exception_handlers(self) -> ExceptionHandlersMap: + """Resolve the exception_handlers by starting from the route handler and moving up. + + This method is memoized so the computation occurs only once. + """ + resolved_exception_handlers: dict[int | type[Exception], ExceptionHandler] = {} + for layer in self.ownership_layers: + resolved_exception_handlers.update(layer.exception_handlers or {}) # pyright: ignore + return resolved_exception_handlers + + def resolve_opts(self) -> None: + """Build the route handler opt dictionary by going from top to bottom. + + When merging keys from multiple layers, if the same key is defined by multiple layers, the value from the + layer closest to the response handler will take precedence. + """ + + opt: dict[str, Any] = {} + for layer in self.ownership_layers: + opt.update(layer.opt or {}) # pyright: ignore + + self.opt = opt + + def resolve_signature_namespace(self) -> dict[str, Any]: + """Build the route handler signature namespace dictionary by going from top to bottom. + + When merging keys from multiple layers, if the same key is defined by multiple layers, the value from the + layer closest to the response handler will take precedence. + """ + if self._resolved_layered_parameters is Empty: + ns: dict[str, Any] = {} + for layer in self.ownership_layers: + ns.update(layer.signature_namespace) + + self._resolved_signature_namespace = ns + return cast("dict[str, Any]", self._resolved_signature_namespace) + + def _get_dto_backend_cls(self) -> type[DTOBackend] | None: + if ExperimentalFeatures.DTO_CODEGEN in self.app.experimental_features: + from litestar.dto._codegen_backend import DTOCodegenBackend + + return DTOCodegenBackend + return None + + def resolve_data_dto(self) -> type[AbstractDTO] | None: + """Resolve the data_dto by starting from the route handler and moving up. + If a handler is found it is returned, otherwise None is set. + This method is memoized so the computation occurs only once. + + Returns: + An optional :class:`DTO type <.dto.base_dto.AbstractDTO>` + """ + if self._resolved_data_dto is Empty: + if data_dtos := cast( + "list[type[AbstractDTO] | None]", + [layer.dto for layer in self.ownership_layers if layer.dto is not Empty], + ): + data_dto: type[AbstractDTO] | None = data_dtos[-1] + elif self.parsed_data_field and ( + plugins_for_data_type := [ + plugin + for plugin in self.app.plugins.serialization + if self.parsed_data_field.match_predicate_recursively(plugin.supports_type) + ] + ): + data_dto = plugins_for_data_type[0].create_dto_for_type(self.parsed_data_field) + else: + data_dto = None + + if self.parsed_data_field and data_dto: + data_dto.create_for_field_definition( + field_definition=self.parsed_data_field, + handler_id=self.handler_id, + backend_cls=self._get_dto_backend_cls(), + ) + + self._resolved_data_dto = data_dto + + return self._resolved_data_dto + + def resolve_return_dto(self) -> type[AbstractDTO] | None: + """Resolve the return_dto by starting from the route handler and moving up. + If a handler is found it is returned, otherwise None is set. + This method is memoized so the computation occurs only once. + + Returns: + An optional :class:`DTO type <.dto.base_dto.AbstractDTO>` + """ + if self._resolved_return_dto is Empty: + if return_dtos := cast( + "list[type[AbstractDTO] | None]", + [layer.return_dto for layer in self.ownership_layers if layer.return_dto is not Empty], + ): + return_dto: type[AbstractDTO] | None = return_dtos[-1] + elif plugins_for_return_type := [ + plugin + for plugin in self.app.plugins.serialization + if self.parsed_return_field.match_predicate_recursively(plugin.supports_type) + ]: + return_dto = plugins_for_return_type[0].create_dto_for_type(self.parsed_return_field) + else: + return_dto = self.resolve_data_dto() + + if return_dto and return_dto.is_supported_model_type_field(self.parsed_return_field): + return_dto.create_for_field_definition( + field_definition=self.parsed_return_field, + handler_id=self.handler_id, + backend_cls=self._get_dto_backend_cls(), + ) + self._resolved_return_dto = return_dto + else: + self._resolved_return_dto = None + + return self._resolved_return_dto + + async def authorize_connection(self, connection: ASGIConnection) -> None: + """Ensure the connection is authorized by running all the route guards in scope.""" + for guard in self.resolve_guards(): + await guard(connection, copy(self)) # type: ignore[misc] + + @staticmethod + def _validate_dependency_is_unique(dependencies: dict[str, Provide], key: str, provider: Provide) -> None: + """Validate that a given provider has not been already defined under a different key.""" + for dependency_key, value in dependencies.items(): + if provider == value: + raise ImproperlyConfiguredException( + f"Provider for key {key} is already defined under the different key {dependency_key}. " + f"If you wish to override a provider, it must have the same key." + ) + + def on_registration(self, app: Litestar) -> None: + """Called once per handler when the app object is instantiated. + + Args: + app: The :class:`Litestar<.app.Litestar>` app object. + + Returns: + None + """ + self._validate_handler_function() + self.resolve_dependencies() + self.resolve_guards() + self.resolve_middleware() + self.resolve_opts() + self.resolve_data_dto() + self.resolve_return_dto() + + def _validate_handler_function(self) -> None: + """Validate the route handler function once set by inspecting its return annotations.""" + if ( + self.parsed_data_field is not None + and self.parsed_data_field.is_subclass_of(DTOData) + and not self.resolve_data_dto() + ): + raise ImproperlyConfiguredException( + f"Handler function {self.handler_name} has a data parameter that is a subclass of DTOData but no " + "DTO has been registered for it." + ) + + def __str__(self) -> str: + """Return a unique identifier for the route handler. + + Returns: + A string + """ + target: type[AsyncAnyCallable] | AsyncAnyCallable # pyright: ignore + target = unwrap_partial(self.fn) + if not hasattr(target, "__qualname__"): + target = type(target) + return f"{target.__module__}.{target.__qualname__}" diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/__init__.py b/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/__init__.py new file mode 100644 index 0000000..844f046 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/__init__.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from .base import HTTPRouteHandler, route +from .decorators import delete, get, head, patch, post, put + +__all__ = ( + "HTTPRouteHandler", + "delete", + "get", + "head", + "patch", + "post", + "put", + "route", +) diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/__pycache__/__init__.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..848ac8f --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/__pycache__/__init__.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/__pycache__/_utils.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/__pycache__/_utils.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..22a7f67 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/__pycache__/_utils.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/__pycache__/base.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/__pycache__/base.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..eb39166 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/__pycache__/base.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/__pycache__/decorators.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/__pycache__/decorators.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..0acd7c8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/__pycache__/decorators.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/_utils.py b/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/_utils.py new file mode 100644 index 0000000..ec95145 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/_utils.py @@ -0,0 +1,221 @@ +from __future__ import annotations + +from functools import lru_cache +from inspect import isawaitable +from typing import TYPE_CHECKING, Any, Sequence, cast + +from litestar.enums import HttpMethod +from litestar.exceptions import ValidationException +from litestar.response import Response +from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT +from litestar.types.builtin_types import NoneType + +if TYPE_CHECKING: + from litestar.app import Litestar + from litestar.background_tasks import BackgroundTask, BackgroundTasks + from litestar.connection import Request + from litestar.datastructures import Cookie, ResponseHeader + from litestar.types import AfterRequestHookHandler, ASGIApp, AsyncAnyCallable, Method, TypeEncodersMap + from litestar.typing import FieldDefinition + +__all__ = ( + "create_data_handler", + "create_generic_asgi_response_handler", + "create_response_handler", + "get_default_status_code", + "is_empty_response_annotation", + "normalize_headers", + "normalize_http_method", +) + + +def create_data_handler( + after_request: AfterRequestHookHandler | None, + background: BackgroundTask | BackgroundTasks | None, + cookies: frozenset[Cookie], + headers: frozenset[ResponseHeader], + media_type: str, + response_class: type[Response], + status_code: int, + type_encoders: TypeEncodersMap | None, +) -> AsyncAnyCallable: + """Create a handler function for arbitrary data. + + Args: + after_request: An after request handler. + background: A background task or background tasks. + cookies: A set of pre-defined cookies. + headers: A set of response headers. + media_type: The response media type. + response_class: The response class to use. + status_code: The response status code. + type_encoders: A mapping of types to encoder functions. + + Returns: + A handler function. + + """ + + async def handler( + data: Any, + request: Request[Any, Any, Any], + app: Litestar, + **kwargs: Any, + ) -> ASGIApp: + if isawaitable(data): + data = await data + + response = response_class( + background=background, + content=data, + media_type=media_type, + status_code=status_code, + type_encoders=type_encoders, + ) + + if after_request: + response = await after_request(response) # type: ignore[arg-type,misc] + + return response.to_asgi_response(app=None, request=request, headers=normalize_headers(headers), cookies=cookies) # pyright: ignore + + return handler + + +def create_generic_asgi_response_handler(after_request: AfterRequestHookHandler | None) -> AsyncAnyCallable: + """Create a handler function for Responses. + + Args: + after_request: An after request handler. + + Returns: + A handler function. + """ + + async def handler(data: ASGIApp, **kwargs: Any) -> ASGIApp: + return await after_request(data) if after_request else data # type: ignore[arg-type, misc, no-any-return] + + return handler + + +@lru_cache(1024) +def normalize_headers(headers: frozenset[ResponseHeader]) -> dict[str, str]: + """Given a dictionary of ResponseHeader, filter them and return a dictionary of values. + + Args: + headers: A dictionary of :class:`ResponseHeader <litestar.datastructures.ResponseHeader>` values + + Returns: + A string keyed dictionary of normalized values + """ + return { + header.name: cast("str", header.value) # we know value to be a string at this point because we validate it + # that it's not None when initializing a header with documentation_only=True + for header in headers + if not header.documentation_only + } + + +def create_response_handler( + after_request: AfterRequestHookHandler | None, + background: BackgroundTask | BackgroundTasks | None, + cookies: frozenset[Cookie], + headers: frozenset[ResponseHeader], + media_type: str, + status_code: int, + type_encoders: TypeEncodersMap | None, +) -> AsyncAnyCallable: + """Create a handler function for Litestar Responses. + + Args: + after_request: An after request handler. + background: A background task or background tasks. + cookies: A set of pre-defined cookies. + headers: A set of response headers. + media_type: The response media type. + status_code: The response status code. + type_encoders: A mapping of types to encoder functions. + + Returns: + A handler function. + """ + + normalized_headers = normalize_headers(headers) + cookie_list = list(cookies) + + async def handler( + data: Response, + app: Litestar, + request: Request, + **kwargs: Any, # kwargs is for return dto + ) -> ASGIApp: + response = await after_request(data) if after_request else data # type:ignore[arg-type,misc] + return response.to_asgi_response( # type: ignore[no-any-return] + app=None, + background=background, + cookies=cookie_list, + headers=normalized_headers, + media_type=media_type, + request=request, + status_code=status_code, + type_encoders=type_encoders, + ) + + return handler + + +def normalize_http_method(http_methods: HttpMethod | Method | Sequence[HttpMethod | Method]) -> set[Method]: + """Normalize HTTP method(s) into a set of upper-case method names. + + Args: + http_methods: A value for http method. + + Returns: + A normalized set of http methods. + """ + output: set[str] = set() + + if isinstance(http_methods, str): + http_methods = [http_methods] # pyright: ignore + + for method in http_methods: + method_name = method.value.upper() if isinstance(method, HttpMethod) else method.upper() + if method_name not in HTTP_METHOD_NAMES: + raise ValidationException(f"Invalid HTTP method: {method_name}") + output.add(method_name) + + return cast("set[Method]", output) + + +def get_default_status_code(http_methods: set[Method]) -> int: + """Return the default status code for a given set of HTTP methods. + + Args: + http_methods: A set of method strings + + Returns: + A status code + """ + if HttpMethod.POST in http_methods: + return HTTP_201_CREATED + if HttpMethod.DELETE in http_methods: + return HTTP_204_NO_CONTENT + return HTTP_200_OK + + +def is_empty_response_annotation(return_annotation: FieldDefinition) -> bool: + """Return whether the return annotation is an empty response. + + Args: + return_annotation: A return annotation. + + Returns: + Whether the return annotation is an empty response. + """ + return ( + return_annotation.is_subclass_of(NoneType) + or return_annotation.is_subclass_of(Response) + and return_annotation.has_inner_subclass_of(NoneType) + ) + + +HTTP_METHOD_NAMES = {m.value for m in HttpMethod} diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/base.py b/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/base.py new file mode 100644 index 0000000..757253e --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/base.py @@ -0,0 +1,591 @@ +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING, AnyStr, Mapping, Sequence, TypedDict, cast + +from litestar._layers.utils import narrow_response_cookies, narrow_response_headers +from litestar.connection import Request +from litestar.datastructures.cookie import Cookie +from litestar.datastructures.response_header import ResponseHeader +from litestar.enums import HttpMethod, MediaType +from litestar.exceptions import ( + HTTPException, + ImproperlyConfiguredException, +) +from litestar.handlers.base import BaseRouteHandler +from litestar.handlers.http_handlers._utils import ( + create_data_handler, + create_generic_asgi_response_handler, + create_response_handler, + get_default_status_code, + is_empty_response_annotation, + normalize_http_method, +) +from litestar.openapi.spec import Operation +from litestar.response import Response +from litestar.status_codes import HTTP_204_NO_CONTENT, HTTP_304_NOT_MODIFIED +from litestar.types import ( + AfterRequestHookHandler, + AfterResponseHookHandler, + AnyCallable, + ASGIApp, + BeforeRequestHookHandler, + CacheKeyBuilder, + Dependencies, + Empty, + EmptyType, + ExceptionHandlersMap, + Guard, + Method, + Middleware, + ResponseCookies, + ResponseHeaders, + TypeEncodersMap, +) +from litestar.utils import ensure_async_callable +from litestar.utils.predicates import is_async_callable +from litestar.utils.warnings import warn_implicit_sync_to_thread, warn_sync_to_thread_with_async_callable + +if TYPE_CHECKING: + from typing import Any, Awaitable, Callable + + from litestar.app import Litestar + from litestar.background_tasks import BackgroundTask, BackgroundTasks + from litestar.config.response_cache import CACHE_FOREVER + from litestar.datastructures import CacheControlHeader, ETag + from litestar.dto import AbstractDTO + from litestar.openapi.datastructures import ResponseSpec + from litestar.openapi.spec import SecurityRequirement + from litestar.types.callable_types import AsyncAnyCallable, OperationIDCreator + from litestar.types.composite_types import TypeDecodersSequence + +__all__ = ("HTTPRouteHandler", "route") + + +class ResponseHandlerMap(TypedDict): + default_handler: Callable[[Any], Awaitable[ASGIApp]] | EmptyType + response_type_handler: Callable[[Any], Awaitable[ASGIApp]] | EmptyType + + +class HTTPRouteHandler(BaseRouteHandler): + """HTTP Route Decorator. + + Use this decorator to decorate an HTTP handler with multiple methods. + """ + + __slots__ = ( + "_resolved_after_response", + "_resolved_before_request", + "_response_handler_mapping", + "_resolved_include_in_schema", + "_resolved_tags", + "_resolved_security", + "after_request", + "after_response", + "background", + "before_request", + "cache", + "cache_control", + "cache_key_builder", + "content_encoding", + "content_media_type", + "deprecated", + "description", + "etag", + "has_sync_callable", + "http_methods", + "include_in_schema", + "media_type", + "operation_class", + "operation_id", + "raises", + "request_class", + "response_class", + "response_cookies", + "response_description", + "response_headers", + "responses", + "security", + "status_code", + "summary", + "sync_to_thread", + "tags", + "template_name", + ) + + has_sync_callable: bool + + def __init__( + self, + path: str | Sequence[str] | None = None, + *, + after_request: AfterRequestHookHandler | None = None, + after_response: AfterResponseHookHandler | None = None, + background: BackgroundTask | BackgroundTasks | None = None, + before_request: BeforeRequestHookHandler | None = None, + cache: bool | int | type[CACHE_FOREVER] = False, + cache_control: CacheControlHeader | None = None, + cache_key_builder: CacheKeyBuilder | None = None, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + etag: ETag | None = None, + exception_handlers: ExceptionHandlersMap | None = None, + guards: Sequence[Guard] | None = None, + http_method: HttpMethod | Method | Sequence[HttpMethod | Method], + media_type: MediaType | str | None = None, + middleware: Sequence[Middleware] | None = None, + name: str | None = None, + opt: Mapping[str, Any] | None = None, + request_class: type[Request] | None = None, + response_class: type[Response] | None = None, + response_cookies: ResponseCookies | None = None, + response_headers: ResponseHeaders | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + status_code: int | None = None, + sync_to_thread: bool | None = None, + # OpenAPI related attributes + content_encoding: str | None = None, + content_media_type: str | None = None, + deprecated: bool = False, + description: str | None = None, + include_in_schema: bool | EmptyType = Empty, + operation_class: type[Operation] = Operation, + operation_id: str | OperationIDCreator | None = None, + raises: Sequence[type[HTTPException]] | None = None, + response_description: str | None = None, + responses: Mapping[int, ResponseSpec] | None = None, + signature_namespace: Mapping[str, Any] | None = None, + security: Sequence[SecurityRequirement] | None = None, + summary: str | None = None, + tags: Sequence[str] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + **kwargs: Any, + ) -> None: + """Initialize ``HTTPRouteHandler``. + + Args: + path: A path fragment for the route handler function or a sequence of path fragments. + If not given defaults to ``/`` + after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed + to any route handler. If this function returns a value, the request will not reach the route handler, + and instead this value will be used. + after_response: A sync or async function called after the response has been awaited. It receives the + :class:`Request <.connection.Request>` object and should not return any values. + background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or + :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. + Defaults to ``None``. + before_request: A sync or async function called immediately before calling the route handler. Receives + the :class:`Request <.connection.Request>` instance and any non-``None`` return value is used for the + response, bypassing the route handler. + cache: Enables response caching if configured on the application level. Valid values are ``True`` or a + number of seconds (e.g. ``120``) to cache the response. + cache_control: A ``cache-control`` header of type + :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. + cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization + of the cache key if caching is configured on the application level. + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and + validation of request data. + etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + http_method: An :class:`http method string <.types.Method>`, a member of the enum + :class:`HttpMethod <.enums.HttpMethod>` or a list of these that correlates to the methods the route + handler function should handle. + media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a valid IANA + Media-Type. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + name: A string identifying the route handler. + opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or + wherever you have access to :class:`Request <.connection.Request>` or + :class:`ASGI Scope <.types.Scope>`. + request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's + default request. + response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's + default response. + response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. + response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` + instances. + responses: A mapping of additional status codes and a description of their expected content. + This information will be included in the OpenAPI schema + return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing + outbound response data. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. + status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and + ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. + sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the + main event loop. This has an effect only for sync handler functions. See using sync handler functions. + content_encoding: A string describing the encoding of the content, e.g. ``"base64"``. + content_media_type: A string designating the media-type of the content, e.g. ``"image/png"``. + deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. + description: Text used for the route's schema description section. + include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. + operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. + operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. + raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. + This list should describe all exceptions raised within the route handler's function/method. The Litestar + ValidationException will be added automatically for the schema if any validation is involved. + response_description: Text used for the route's response schema description section. + security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. + summary: Text used for the route's schema summary section. + tags: A sequence of string tags that will be appended to the OpenAPI schema. + type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec hook for deserialization. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + **kwargs: Any additional kwarg - will be set in the opt dictionary. + """ + if not http_method: + raise ImproperlyConfiguredException("An http_method kwarg is required") + + self.http_methods = normalize_http_method(http_methods=http_method) + self.status_code = status_code or get_default_status_code(http_methods=self.http_methods) + + super().__init__( + path=path, + dependencies=dependencies, + dto=dto, + exception_handlers=exception_handlers, + guards=guards, + middleware=middleware, + name=name, + opt=opt, + return_dto=return_dto, + signature_namespace=signature_namespace, + type_decoders=type_decoders, + type_encoders=type_encoders, + **kwargs, + ) + + self.after_request = ensure_async_callable(after_request) if after_request else None # pyright: ignore + self.after_response = ensure_async_callable(after_response) if after_response else None + self.background = background + self.before_request = ensure_async_callable(before_request) if before_request else None + self.cache = cache + self.cache_control = cache_control + self.cache_key_builder = cache_key_builder + self.etag = etag + self.media_type: MediaType | str = media_type or "" + self.request_class = request_class + self.response_class = response_class + self.response_cookies: Sequence[Cookie] | None = narrow_response_cookies(response_cookies) + self.response_headers: Sequence[ResponseHeader] | None = narrow_response_headers(response_headers) + + self.sync_to_thread = sync_to_thread + # OpenAPI related attributes + self.content_encoding = content_encoding + self.content_media_type = content_media_type + self.deprecated = deprecated + self.description = description + self.include_in_schema = include_in_schema + self.operation_class = operation_class + self.operation_id = operation_id + self.raises = raises + self.response_description = response_description + self.summary = summary + self.tags = tags + self.security = security + self.responses = responses + # memoized attributes, defaulted to Empty + self._resolved_after_response: AsyncAnyCallable | None | EmptyType = Empty + self._resolved_before_request: AsyncAnyCallable | None | EmptyType = Empty + self._response_handler_mapping: ResponseHandlerMap = {"default_handler": Empty, "response_type_handler": Empty} + self._resolved_include_in_schema: bool | EmptyType = Empty + self._resolved_security: list[SecurityRequirement] | EmptyType = Empty + self._resolved_tags: list[str] | EmptyType = Empty + + def __call__(self, fn: AnyCallable) -> HTTPRouteHandler: + """Replace a function with itself.""" + if not is_async_callable(fn): + if self.sync_to_thread is None: + warn_implicit_sync_to_thread(fn, stacklevel=3) + elif self.sync_to_thread is not None: + warn_sync_to_thread_with_async_callable(fn, stacklevel=3) + + super().__call__(fn) + return self + + def resolve_request_class(self) -> type[Request]: + """Return the closest custom Request class in the owner graph or the default Request class. + + This method is memoized so the computation occurs only once. + + Returns: + The default :class:`Request <.connection.Request>` class for the route handler. + """ + return next( + (layer.request_class for layer in reversed(self.ownership_layers) if layer.request_class is not None), + Request, + ) + + def resolve_response_class(self) -> type[Response]: + """Return the closest custom Response class in the owner graph or the default Response class. + + This method is memoized so the computation occurs only once. + + Returns: + The default :class:`Response <.response.Response>` class for the route handler. + """ + return next( + (layer.response_class for layer in reversed(self.ownership_layers) if layer.response_class is not None), + Response, + ) + + def resolve_response_headers(self) -> frozenset[ResponseHeader]: + """Return all header parameters in the scope of the handler function. + + Returns: + A dictionary mapping keys to :class:`ResponseHeader <.datastructures.ResponseHeader>` instances. + """ + resolved_response_headers: dict[str, ResponseHeader] = {} + + for layer in self.ownership_layers: + if layer_response_headers := layer.response_headers: + if isinstance(layer_response_headers, Mapping): + # this can't happen unless you manually set response_headers on an instance, which would result in a + # type-checking error on everything but the controller. We cover this case nevertheless + resolved_response_headers.update( + {name: ResponseHeader(name=name, value=value) for name, value in layer_response_headers.items()} + ) + else: + resolved_response_headers.update({h.name: h for h in layer_response_headers}) + for extra_header in ("cache_control", "etag"): + if header_model := getattr(layer, extra_header, None): + resolved_response_headers[header_model.HEADER_NAME] = ResponseHeader( + name=header_model.HEADER_NAME, + value=header_model.to_header(), + documentation_only=header_model.documentation_only, + ) + + return frozenset(resolved_response_headers.values()) + + def resolve_response_cookies(self) -> frozenset[Cookie]: + """Return a list of Cookie instances. Filters the list to ensure each cookie key is unique. + + Returns: + A list of :class:`Cookie <.datastructures.Cookie>` instances. + """ + response_cookies: set[Cookie] = set() + for layer in reversed(self.ownership_layers): + if layer_response_cookies := layer.response_cookies: + if isinstance(layer_response_cookies, Mapping): + # this can't happen unless you manually set response_cookies on an instance, which would result in a + # type-checking error on everything but the controller. We cover this case nevertheless + response_cookies.update( + {Cookie(key=key, value=value) for key, value in layer_response_cookies.items()} + ) + else: + response_cookies.update(cast("set[Cookie]", layer_response_cookies)) + return frozenset(response_cookies) + + def resolve_before_request(self) -> AsyncAnyCallable | None: + """Resolve the before_handler handler by starting from the route handler and moving up. + + If a handler is found it is returned, otherwise None is set. + This method is memoized so the computation occurs only once. + + Returns: + An optional :class:`before request lifecycle hook handler <.types.BeforeRequestHookHandler>` + """ + if self._resolved_before_request is Empty: + before_request_handlers = [layer.before_request for layer in self.ownership_layers if layer.before_request] + self._resolved_before_request = before_request_handlers[-1] if before_request_handlers else None + return cast("AsyncAnyCallable | None", self._resolved_before_request) + + def resolve_after_response(self) -> AsyncAnyCallable | None: + """Resolve the after_response handler by starting from the route handler and moving up. + + If a handler is found it is returned, otherwise None is set. + This method is memoized so the computation occurs only once. + + Returns: + An optional :class:`after response lifecycle hook handler <.types.AfterResponseHookHandler>` + """ + if self._resolved_after_response is Empty: + after_response_handlers: list[AsyncAnyCallable] = [ + layer.after_response # type: ignore[misc] + for layer in self.ownership_layers + if layer.after_response + ] + self._resolved_after_response = after_response_handlers[-1] if after_response_handlers else None + + return cast("AsyncAnyCallable | None", self._resolved_after_response) + + def resolve_include_in_schema(self) -> bool: + """Resolve the 'include_in_schema' property by starting from the route handler and moving up. + + If 'include_in_schema' is found in any of the ownership layers, the last value found is returned. + If not found in any layer, the default value ``True`` is returned. + + Returns: + bool: The resolved 'include_in_schema' property. + """ + if self._resolved_include_in_schema is Empty: + include_in_schemas = [ + i.include_in_schema for i in self.ownership_layers if isinstance(i.include_in_schema, bool) + ] + self._resolved_include_in_schema = include_in_schemas[-1] if include_in_schemas else True + + return self._resolved_include_in_schema + + def resolve_security(self) -> list[SecurityRequirement]: + """Resolve the security property by starting from the route handler and moving up. + + Security requirements are additive, so the security requirements of the route handler are the sum of all + security requirements of the ownership layers. + + Returns: + list[SecurityRequirement]: The resolved security property. + """ + if self._resolved_security is Empty: + self._resolved_security = [] + for layer in self.ownership_layers: + if isinstance(layer.security, Sequence): + self._resolved_security.extend(layer.security) + + return self._resolved_security + + def resolve_tags(self) -> list[str]: + """Resolve the tags property by starting from the route handler and moving up. + + Tags are additive, so the tags of the route handler are the sum of all tags of the ownership layers. + + Returns: + list[str]: A sorted list of unique tags. + """ + if self._resolved_tags is Empty: + tag_set = set() + for layer in self.ownership_layers: + for tag in layer.tags or []: + tag_set.add(tag) + self._resolved_tags = sorted(tag_set) + + return self._resolved_tags + + def get_response_handler(self, is_response_type_data: bool = False) -> Callable[[Any], Awaitable[ASGIApp]]: + """Resolve the response_handler function for the route handler. + + This method is memoized so the computation occurs only once. + + Args: + is_response_type_data: Whether to return a handler for 'Response' instances. + + Returns: + Async Callable to handle an HTTP Request + """ + if self._response_handler_mapping["default_handler"] is Empty: + after_request_handlers: list[AsyncAnyCallable] = [ + layer.after_request # type: ignore[misc] + for layer in self.ownership_layers + if layer.after_request + ] + after_request = cast( + "AfterRequestHookHandler | None", + after_request_handlers[-1] if after_request_handlers else None, + ) + + media_type = self.media_type.value if isinstance(self.media_type, Enum) else self.media_type + response_class = self.resolve_response_class() + headers = self.resolve_response_headers() + cookies = self.resolve_response_cookies() + type_encoders = self.resolve_type_encoders() + + return_type = self.parsed_fn_signature.return_type + return_annotation = return_type.annotation + + self._response_handler_mapping["response_type_handler"] = response_type_handler = create_response_handler( + after_request=after_request, + background=self.background, + cookies=cookies, + headers=headers, + media_type=media_type, + status_code=self.status_code, + type_encoders=type_encoders, + ) + + if return_type.is_subclass_of(Response): + self._response_handler_mapping["default_handler"] = response_type_handler + elif is_async_callable(return_annotation) or return_annotation is ASGIApp: + self._response_handler_mapping["default_handler"] = create_generic_asgi_response_handler( + after_request=after_request + ) + else: + self._response_handler_mapping["default_handler"] = create_data_handler( + after_request=after_request, + background=self.background, + cookies=cookies, + headers=headers, + media_type=media_type, + response_class=response_class, + status_code=self.status_code, + type_encoders=type_encoders, + ) + + return cast( + "Callable[[Any], Awaitable[ASGIApp]]", + self._response_handler_mapping["response_type_handler"] + if is_response_type_data + else self._response_handler_mapping["default_handler"], + ) + + async def to_response(self, app: Litestar, data: Any, request: Request) -> ASGIApp: + """Return a :class:`Response <.response.Response>` from the handler by resolving and calling it. + + Args: + app: The :class:`Litestar <litestar.app.Litestar>` app instance + data: Either an instance of a :class:`Response <.response.Response>`, + a Response instance or an arbitrary value. + request: A :class:`Request <.connection.Request>` instance + + Returns: + A Response instance + """ + if return_dto_type := self.resolve_return_dto(): + data = return_dto_type(request).data_to_encodable_type(data) + + response_handler = self.get_response_handler(is_response_type_data=isinstance(data, Response)) + return await response_handler(app=app, data=data, request=request) # type: ignore[call-arg] + + def on_registration(self, app: Litestar) -> None: + super().on_registration(app) + self.resolve_after_response() + self.resolve_include_in_schema() + self.has_sync_callable = not is_async_callable(self.fn) + + if self.has_sync_callable and self.sync_to_thread: + self._fn = ensure_async_callable(self.fn) + self.has_sync_callable = False + + def _validate_handler_function(self) -> None: + """Validate the route handler function once it is set by inspecting its return annotations.""" + super()._validate_handler_function() + + return_type = self.parsed_fn_signature.return_type + + if return_type.annotation is Empty: + raise ImproperlyConfiguredException( + "A return value of a route handler function should be type annotated. " + "If your function doesn't return a value, annotate it as returning 'None'." + ) + + if ( + self.status_code < 200 or self.status_code in {HTTP_204_NO_CONTENT, HTTP_304_NOT_MODIFIED} + ) and not is_empty_response_annotation(return_type): + raise ImproperlyConfiguredException( + "A status code 204, 304 or in the range below 200 does not support a response body. " + "If the function should return a value, change the route handler status code to an appropriate value.", + ) + + if not self.media_type: + if return_type.is_subclass_of((str, bytes)) or return_type.annotation is AnyStr: + self.media_type = MediaType.TEXT + elif not return_type.is_subclass_of(Response): + self.media_type = MediaType.JSON + + if "socket" in self.parsed_fn_signature.parameters: + raise ImproperlyConfiguredException("The 'socket' kwarg is not supported with http handlers") + + if "data" in self.parsed_fn_signature.parameters and "GET" in self.http_methods: + raise ImproperlyConfiguredException("'data' kwarg is unsupported for 'GET' request handlers") + + +route = HTTPRouteHandler diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/decorators.py b/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/decorators.py new file mode 100644 index 0000000..1ae72e5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/http_handlers/decorators.py @@ -0,0 +1,1096 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from litestar.enums import HttpMethod, MediaType +from litestar.exceptions import HTTPException, ImproperlyConfiguredException +from litestar.openapi.spec import Operation +from litestar.response.file import ASGIFileResponse, File +from litestar.types import Empty, TypeDecodersSequence +from litestar.types.builtin_types import NoneType +from litestar.utils import is_class_and_subclass + +from .base import HTTPRouteHandler + +if TYPE_CHECKING: + from typing import Any, Mapping, Sequence + + from litestar.background_tasks import BackgroundTask, BackgroundTasks + from litestar.config.response_cache import CACHE_FOREVER + from litestar.connection import Request + from litestar.datastructures import CacheControlHeader, ETag + from litestar.dto import AbstractDTO + from litestar.openapi.datastructures import ResponseSpec + from litestar.openapi.spec import SecurityRequirement + from litestar.response import Response + from litestar.types import ( + AfterRequestHookHandler, + AfterResponseHookHandler, + BeforeRequestHookHandler, + CacheKeyBuilder, + Dependencies, + EmptyType, + ExceptionHandlersMap, + Guard, + Middleware, + ResponseCookies, + ResponseHeaders, + TypeEncodersMap, + ) + from litestar.types.callable_types import OperationIDCreator + + +__all__ = ("get", "head", "post", "put", "patch", "delete") + +MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP = "semantic route handlers cannot define http_method" + + +class delete(HTTPRouteHandler): + """DELETE Route Decorator. + + Use this decorator to decorate an HTTP handler for DELETE requests. + """ + + def __init__( + self, + path: str | None | Sequence[str] = None, + *, + after_request: AfterRequestHookHandler | None = None, + after_response: AfterResponseHookHandler | None = None, + background: BackgroundTask | BackgroundTasks | None = None, + before_request: BeforeRequestHookHandler | None = None, + cache: bool | int | type[CACHE_FOREVER] = False, + cache_control: CacheControlHeader | None = None, + cache_key_builder: CacheKeyBuilder | None = None, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + etag: ETag | None = None, + exception_handlers: ExceptionHandlersMap | None = None, + guards: Sequence[Guard] | None = None, + media_type: MediaType | str | None = None, + middleware: Sequence[Middleware] | None = None, + name: str | None = None, + opt: Mapping[str, Any] | None = None, + request_class: type[Request] | None = None, + response_class: type[Response] | None = None, + response_cookies: ResponseCookies | None = None, + response_headers: ResponseHeaders | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + signature_namespace: Mapping[str, Any] | None = None, + status_code: int | None = None, + sync_to_thread: bool | None = None, + # OpenAPI related attributes + content_encoding: str | None = None, + content_media_type: str | None = None, + deprecated: bool = False, + description: str | None = None, + include_in_schema: bool | EmptyType = Empty, + operation_class: type[Operation] = Operation, + operation_id: str | OperationIDCreator | None = None, + raises: Sequence[type[HTTPException]] | None = None, + response_description: str | None = None, + responses: Mapping[int, ResponseSpec] | None = None, + security: Sequence[SecurityRequirement] | None = None, + summary: str | None = None, + tags: Sequence[str] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + **kwargs: Any, + ) -> None: + """Initialize ``delete`` + + Args: + path: A path fragment for the route handler function or a sequence of path fragments. + If not given defaults to ``/`` + after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed + to any route handler. If this function returns a value, the request will not reach the route handler, + and instead this value will be used. + after_response: A sync or async function called after the response has been awaited. It receives the + :class:`Request <.connection.Request>` object and should not return any values. + background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or + :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. + Defaults to ``None``. + before_request: A sync or async function called immediately before calling the route handler. Receives + the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, + bypassing the route handler. + cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number + of seconds (e.g. ``120``) to cache the response. + cache_control: A ``cache-control`` header of type + :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. + cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization + of the cache key if caching is configured on the application level. + dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and + validation of request data. + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + http_method: An :class:`http method string <.types.Method>`, a member of the enum + :class:`HttpMethod <litestar.enums.HttpMethod>` or a list of these that correlates to the methods the + route handler function should handle. + media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a + valid IANA Media-Type. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + name: A string identifying the route handler. + opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or + wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. + request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's + default request. + response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's + default response. + response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. + response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` + instances. + responses: A mapping of additional status codes and a description of their expected content. + This information will be included in the OpenAPI schema + return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing + outbound response data. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. + status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` + and ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. + sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the + main event loop. This has an effect only for sync handler functions. See using sync handler functions. + content_encoding: A string describing the encoding of the content, e.g. ``base64``. + content_media_type: A string designating the media-type of the content, e.g. ``image/png``. + deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. + description: Text used for the route's schema description section. + include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. + operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. + operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. + raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. + This list should describe all exceptions raised within the route handler's function/method. The Litestar + ValidationException will be added automatically for the schema if any validation is involved. + response_description: Text used for the route's response schema description section. + security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. + summary: Text used for the route's schema summary section. + tags: A sequence of string tags that will be appended to the OpenAPI schema. + type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec + hook for deserialization. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + **kwargs: Any additional kwarg - will be set in the opt dictionary. + """ + if "http_method" in kwargs: + raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP) + super().__init__( + after_request=after_request, + after_response=after_response, + background=background, + before_request=before_request, + cache=cache, + cache_control=cache_control, + cache_key_builder=cache_key_builder, + content_encoding=content_encoding, + content_media_type=content_media_type, + dependencies=dependencies, + deprecated=deprecated, + description=description, + dto=dto, + etag=etag, + exception_handlers=exception_handlers, + guards=guards, + http_method=HttpMethod.DELETE, + include_in_schema=include_in_schema, + media_type=media_type, + middleware=middleware, + name=name, + operation_class=operation_class, + operation_id=operation_id, + opt=opt, + path=path, + raises=raises, + request_class=request_class, + response_class=response_class, + response_cookies=response_cookies, + response_description=response_description, + response_headers=response_headers, + responses=responses, + return_dto=return_dto, + security=security, + signature_namespace=signature_namespace, + status_code=status_code, + summary=summary, + sync_to_thread=sync_to_thread, + tags=tags, + type_decoders=type_decoders, + type_encoders=type_encoders, + **kwargs, + ) + + +class get(HTTPRouteHandler): + """GET Route Decorator. + + Use this decorator to decorate an HTTP handler for GET requests. + """ + + def __init__( + self, + path: str | None | Sequence[str] = None, + *, + after_request: AfterRequestHookHandler | None = None, + after_response: AfterResponseHookHandler | None = None, + background: BackgroundTask | BackgroundTasks | None = None, + before_request: BeforeRequestHookHandler | None = None, + cache: bool | int | type[CACHE_FOREVER] = False, + cache_control: CacheControlHeader | None = None, + cache_key_builder: CacheKeyBuilder | None = None, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + etag: ETag | None = None, + exception_handlers: ExceptionHandlersMap | None = None, + guards: Sequence[Guard] | None = None, + media_type: MediaType | str | None = None, + middleware: Sequence[Middleware] | None = None, + name: str | None = None, + opt: Mapping[str, Any] | None = None, + request_class: type[Request] | None = None, + response_class: type[Response] | None = None, + response_cookies: ResponseCookies | None = None, + response_headers: ResponseHeaders | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + signature_namespace: Mapping[str, Any] | None = None, + status_code: int | None = None, + sync_to_thread: bool | None = None, + # OpenAPI related attributes + content_encoding: str | None = None, + content_media_type: str | None = None, + deprecated: bool = False, + description: str | None = None, + include_in_schema: bool | EmptyType = Empty, + operation_class: type[Operation] = Operation, + operation_id: str | OperationIDCreator | None = None, + raises: Sequence[type[HTTPException]] | None = None, + response_description: str | None = None, + responses: Mapping[int, ResponseSpec] | None = None, + security: Sequence[SecurityRequirement] | None = None, + summary: str | None = None, + tags: Sequence[str] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + **kwargs: Any, + ) -> None: + """Initialize ``get``. + + Args: + path: A path fragment for the route handler function or a sequence of path fragments. + If not given defaults to ``/`` + after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed + to any route handler. If this function returns a value, the request will not reach the route handler, + and instead this value will be used. + after_response: A sync or async function called after the response has been awaited. It receives the + :class:`Request <.connection.Request>` object and should not return any values. + background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or + :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. + Defaults to ``None``. + before_request: A sync or async function called immediately before calling the route handler. Receives + the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, + bypassing the route handler. + cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number + of seconds (e.g. ``120``) to cache the response. + cache_control: A ``cache-control`` header of type + :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. + cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization + of the cache key if caching is configured on the application level. + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and + validation of request data. + etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + http_method: An :class:`http method string <.types.Method>`, a member of the enum + :class:`HttpMethod <litestar.enums.HttpMethod>` or a list of these that correlates to the methods the + route handler function should handle. + media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a + valid IANA Media-Type. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + name: A string identifying the route handler. + opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or + wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. + request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's + default request. + response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's + default response. + response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. + response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` + instances. + responses: A mapping of additional status codes and a description of their expected content. + This information will be included in the OpenAPI schema + return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing + outbound response data. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. + status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and + ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. + sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the + main event loop. This has an effect only for sync handler functions. See using sync handler functions. + content_encoding: A string describing the encoding of the content, e.g. ``base64``. + content_media_type: A string designating the media-type of the content, e.g. ``image/png``. + deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. + description: Text used for the route's schema description section. + include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. + operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. + operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. + raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. + This list should describe all exceptions raised within the route handler's function/method. The Litestar + ValidationException will be added automatically for the schema if any validation is involved. + response_description: Text used for the route's response schema description section. + security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. + summary: Text used for the route's schema summary section. + tags: A sequence of string tags that will be appended to the OpenAPI schema. + type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec + hook for deserialization. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + **kwargs: Any additional kwarg - will be set in the opt dictionary. + """ + if "http_method" in kwargs: + raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP) + + super().__init__( + after_request=after_request, + after_response=after_response, + background=background, + before_request=before_request, + cache=cache, + cache_control=cache_control, + cache_key_builder=cache_key_builder, + content_encoding=content_encoding, + content_media_type=content_media_type, + dependencies=dependencies, + deprecated=deprecated, + description=description, + dto=dto, + etag=etag, + exception_handlers=exception_handlers, + guards=guards, + http_method=HttpMethod.GET, + include_in_schema=include_in_schema, + media_type=media_type, + middleware=middleware, + name=name, + operation_class=operation_class, + operation_id=operation_id, + opt=opt, + path=path, + raises=raises, + request_class=request_class, + response_class=response_class, + response_cookies=response_cookies, + response_description=response_description, + response_headers=response_headers, + responses=responses, + return_dto=return_dto, + security=security, + signature_namespace=signature_namespace, + status_code=status_code, + summary=summary, + sync_to_thread=sync_to_thread, + tags=tags, + type_decoders=type_decoders, + type_encoders=type_encoders, + **kwargs, + ) + + +class head(HTTPRouteHandler): + """HEAD Route Decorator. + + Use this decorator to decorate an HTTP handler for HEAD requests. + """ + + def __init__( + self, + path: str | None | Sequence[str] = None, + *, + after_request: AfterRequestHookHandler | None = None, + after_response: AfterResponseHookHandler | None = None, + background: BackgroundTask | BackgroundTasks | None = None, + before_request: BeforeRequestHookHandler | None = None, + cache: bool | int | type[CACHE_FOREVER] = False, + cache_control: CacheControlHeader | None = None, + cache_key_builder: CacheKeyBuilder | None = None, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + etag: ETag | None = None, + exception_handlers: ExceptionHandlersMap | None = None, + guards: Sequence[Guard] | None = None, + media_type: MediaType | str | None = None, + middleware: Sequence[Middleware] | None = None, + name: str | None = None, + opt: Mapping[str, Any] | None = None, + request_class: type[Request] | None = None, + response_class: type[Response] | None = None, + response_cookies: ResponseCookies | None = None, + response_headers: ResponseHeaders | None = None, + signature_namespace: Mapping[str, Any] | None = None, + status_code: int | None = None, + sync_to_thread: bool | None = None, + # OpenAPI related attributes + content_encoding: str | None = None, + content_media_type: str | None = None, + deprecated: bool = False, + description: str | None = None, + include_in_schema: bool | EmptyType = Empty, + operation_class: type[Operation] = Operation, + operation_id: str | OperationIDCreator | None = None, + raises: Sequence[type[HTTPException]] | None = None, + response_description: str | None = None, + responses: Mapping[int, ResponseSpec] | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + security: Sequence[SecurityRequirement] | None = None, + summary: str | None = None, + tags: Sequence[str] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + **kwargs: Any, + ) -> None: + """Initialize ``head``. + + Notes: + - A response to a head request cannot include a body. + See: [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD). + + Args: + path: A path fragment for the route handler function or a sequence of path fragments. + If not given defaults to ``/`` + after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed + to any route handler. If this function returns a value, the request will not reach the route handler, + and instead this value will be used. + after_response: A sync or async function called after the response has been awaited. It receives the + :class:`Request <.connection.Request>` object and should not return any values. + background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or + :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. + Defaults to ``None``. + before_request: A sync or async function called immediately before calling the route handler. Receives + the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, + bypassing the route handler. + cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number + of seconds (e.g. ``120``) to cache the response. + cache_control: A ``cache-control`` header of type + :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. + cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization + of the cache key if caching is configured on the application level. + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and + validation of request data. + etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + http_method: An :class:`http method string <.types.Method>`, a member of the enum + :class:`HttpMethod <litestar.enums.HttpMethod>` or a list of these that correlates to the methods the + route handler function should handle. + media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a + valid IANA Media-Type. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + name: A string identifying the route handler. + opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or + wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. + request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's + default request. + response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's + default response. + response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. + response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` + instances. + responses: A mapping of additional status codes and a description of their expected content. + This information will be included in the OpenAPI schema + return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing + outbound response data. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. + status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and + ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. + sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the + main event loop. This has an effect only for sync handler functions. See using sync handler functions. + content_encoding: A string describing the encoding of the content, e.g. ``base64``. + content_media_type: A string designating the media-type of the content, e.g. ``image/png``. + deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. + description: Text used for the route's schema description section. + include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. + operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. + operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. + raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. + This list should describe all exceptions raised within the route handler's function/method. The Litestar + ValidationException will be added automatically for the schema if any validation is involved. + response_description: Text used for the route's response schema description section. + security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. + summary: Text used for the route's schema summary section. + tags: A sequence of string tags that will be appended to the OpenAPI schema. + type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec + hook for deserialization. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + **kwargs: Any additional kwarg - will be set in the opt dictionary. + """ + if "http_method" in kwargs: + raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP) + + super().__init__( + after_request=after_request, + after_response=after_response, + background=background, + before_request=before_request, + cache=cache, + cache_control=cache_control, + cache_key_builder=cache_key_builder, + content_encoding=content_encoding, + content_media_type=content_media_type, + dependencies=dependencies, + deprecated=deprecated, + description=description, + dto=dto, + etag=etag, + exception_handlers=exception_handlers, + guards=guards, + http_method=HttpMethod.HEAD, + include_in_schema=include_in_schema, + media_type=media_type, + middleware=middleware, + name=name, + operation_class=operation_class, + operation_id=operation_id, + opt=opt, + path=path, + raises=raises, + request_class=request_class, + response_class=response_class, + response_cookies=response_cookies, + response_description=response_description, + response_headers=response_headers, + responses=responses, + return_dto=return_dto, + security=security, + signature_namespace=signature_namespace, + status_code=status_code, + summary=summary, + sync_to_thread=sync_to_thread, + tags=tags, + type_decoders=type_decoders, + type_encoders=type_encoders, + **kwargs, + ) + + def _validate_handler_function(self) -> None: + """Validate the route handler function once it is set by inspecting its return annotations.""" + super()._validate_handler_function() + + # we allow here File and File because these have special setting for head responses + return_annotation = self.parsed_fn_signature.return_type.annotation + if not ( + return_annotation in {NoneType, None} + or is_class_and_subclass(return_annotation, File) + or is_class_and_subclass(return_annotation, ASGIFileResponse) + ): + raise ImproperlyConfiguredException("A response to a head request should not have a body") + + +class patch(HTTPRouteHandler): + """PATCH Route Decorator. + + Use this decorator to decorate an HTTP handler for PATCH requests. + """ + + def __init__( + self, + path: str | None | Sequence[str] = None, + *, + after_request: AfterRequestHookHandler | None = None, + after_response: AfterResponseHookHandler | None = None, + background: BackgroundTask | BackgroundTasks | None = None, + before_request: BeforeRequestHookHandler | None = None, + cache: bool | int | type[CACHE_FOREVER] = False, + cache_control: CacheControlHeader | None = None, + cache_key_builder: CacheKeyBuilder | None = None, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + etag: ETag | None = None, + exception_handlers: ExceptionHandlersMap | None = None, + guards: Sequence[Guard] | None = None, + media_type: MediaType | str | None = None, + middleware: Sequence[Middleware] | None = None, + name: str | None = None, + opt: Mapping[str, Any] | None = None, + request_class: type[Request] | None = None, + response_class: type[Response] | None = None, + response_cookies: ResponseCookies | None = None, + response_headers: ResponseHeaders | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + signature_namespace: Mapping[str, Any] | None = None, + status_code: int | None = None, + sync_to_thread: bool | None = None, + # OpenAPI related attributes + content_encoding: str | None = None, + content_media_type: str | None = None, + deprecated: bool = False, + description: str | None = None, + include_in_schema: bool | EmptyType = Empty, + operation_class: type[Operation] = Operation, + operation_id: str | OperationIDCreator | None = None, + raises: Sequence[type[HTTPException]] | None = None, + response_description: str | None = None, + responses: Mapping[int, ResponseSpec] | None = None, + security: Sequence[SecurityRequirement] | None = None, + summary: str | None = None, + tags: Sequence[str] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + **kwargs: Any, + ) -> None: + """Initialize ``patch``. + + Args: + path: A path fragment for the route handler function or a sequence of path fragments. + If not given defaults to ``/`` + after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed + to any route handler. If this function returns a value, the request will not reach the route handler, + and instead this value will be used. + after_response: A sync or async function called after the response has been awaited. It receives the + :class:`Request <.connection.Request>` object and should not return any values. + background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or + :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. + Defaults to ``None``. + before_request: A sync or async function called immediately before calling the route handler. Receives + the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, + bypassing the route handler. + cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number + of seconds (e.g. ``120``) to cache the response. + cache_control: A ``cache-control`` header of type + :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. + cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization + of the cache key if caching is configured on the application level. + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and + validation of request data. + etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + http_method: An :class:`http method string <.types.Method>`, a member of the enum + :class:`HttpMethod <litestar.enums.HttpMethod>` or a list of these that correlates to the methods the + route handler function should handle. + media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a + valid IANA Media-Type. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + name: A string identifying the route handler. + opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or + wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. + request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's + default request. + response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's + default response. + response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. + response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` + instances. + responses: A mapping of additional status codes and a description of their expected content. + This information will be included in the OpenAPI schema + return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing + outbound response data. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. + status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and + ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. + sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the + main event loop. This has an effect only for sync handler functions. See using sync handler functions. + content_encoding: A string describing the encoding of the content, e.g. ``base64``. + content_media_type: A string designating the media-type of the content, e.g. ``image/png``. + deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. + description: Text used for the route's schema description section. + include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. + operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. + operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. + raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. + This list should describe all exceptions raised within the route handler's function/method. The Litestar + ValidationException will be added automatically for the schema if any validation is involved. + response_description: Text used for the route's response schema description section. + security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. + summary: Text used for the route's schema summary section. + tags: A sequence of string tags that will be appended to the OpenAPI schema. + type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec + hook for deserialization. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + **kwargs: Any additional kwarg - will be set in the opt dictionary. + """ + if "http_method" in kwargs: + raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP) + super().__init__( + after_request=after_request, + after_response=after_response, + background=background, + before_request=before_request, + cache=cache, + cache_control=cache_control, + cache_key_builder=cache_key_builder, + content_encoding=content_encoding, + content_media_type=content_media_type, + dependencies=dependencies, + deprecated=deprecated, + description=description, + dto=dto, + etag=etag, + exception_handlers=exception_handlers, + guards=guards, + http_method=HttpMethod.PATCH, + include_in_schema=include_in_schema, + media_type=media_type, + middleware=middleware, + name=name, + operation_class=operation_class, + operation_id=operation_id, + opt=opt, + path=path, + raises=raises, + request_class=request_class, + response_class=response_class, + response_cookies=response_cookies, + response_description=response_description, + response_headers=response_headers, + responses=responses, + return_dto=return_dto, + security=security, + signature_namespace=signature_namespace, + status_code=status_code, + summary=summary, + sync_to_thread=sync_to_thread, + tags=tags, + type_decoders=type_decoders, + type_encoders=type_encoders, + **kwargs, + ) + + +class post(HTTPRouteHandler): + """POST Route Decorator. + + Use this decorator to decorate an HTTP handler for POST requests. + """ + + def __init__( + self, + path: str | None | Sequence[str] = None, + *, + after_request: AfterRequestHookHandler | None = None, + after_response: AfterResponseHookHandler | None = None, + background: BackgroundTask | BackgroundTasks | None = None, + before_request: BeforeRequestHookHandler | None = None, + cache: bool | int | type[CACHE_FOREVER] = False, + cache_control: CacheControlHeader | None = None, + cache_key_builder: CacheKeyBuilder | None = None, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + etag: ETag | None = None, + exception_handlers: ExceptionHandlersMap | None = None, + guards: Sequence[Guard] | None = None, + media_type: MediaType | str | None = None, + middleware: Sequence[Middleware] | None = None, + name: str | None = None, + opt: Mapping[str, Any] | None = None, + request_class: type[Request] | None = None, + response_class: type[Response] | None = None, + response_cookies: ResponseCookies | None = None, + response_headers: ResponseHeaders | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + signature_namespace: Mapping[str, Any] | None = None, + status_code: int | None = None, + sync_to_thread: bool | None = None, + # OpenAPI related attributes + content_encoding: str | None = None, + content_media_type: str | None = None, + deprecated: bool = False, + description: str | None = None, + include_in_schema: bool | EmptyType = Empty, + operation_class: type[Operation] = Operation, + operation_id: str | OperationIDCreator | None = None, + raises: Sequence[type[HTTPException]] | None = None, + response_description: str | None = None, + responses: Mapping[int, ResponseSpec] | None = None, + security: Sequence[SecurityRequirement] | None = None, + summary: str | None = None, + tags: Sequence[str] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + **kwargs: Any, + ) -> None: + """Initialize ``post`` + + Args: + path: A path fragment for the route handler function or a sequence of path fragments. + If not given defaults to ``/`` + after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed + to any route handler. If this function returns a value, the request will not reach the route handler, + and instead this value will be used. + after_response: A sync or async function called after the response has been awaited. It receives the + :class:`Request <.connection.Request>` object and should not return any values. + background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or + :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. + Defaults to ``None``. + before_request: A sync or async function called immediately before calling the route handler. Receives + the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, + bypassing the route handler. + cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number + of seconds (e.g. ``120``) to cache the response. + cache_control: A ``cache-control`` header of type + :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. + cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization + of the cache key if caching is configured on the application level. + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and + validation of request data. + etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + http_method: An :class:`http method string <.types.Method>`, a member of the enum + :class:`HttpMethod <litestar.enums.HttpMethod>` or a list of these that correlates to the methods the + route handler function should handle. + media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a + valid IANA Media-Type. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + name: A string identifying the route handler. + opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or + wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. + request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's + default request. + response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's + default response. + response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. + response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` + instances. + responses: A mapping of additional status codes and a description of their expected content. + This information will be included in the OpenAPI schema + return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing + outbound response data. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. + status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and + ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. + sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the + main event loop. This has an effect only for sync handler functions. See using sync handler functions. + content_encoding: A string describing the encoding of the content, e.g. ``base64``. + content_media_type: A string designating the media-type of the content, e.g. ``image/png``. + deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. + description: Text used for the route's schema description section. + include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. + operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. + operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. + raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. + This list should describe all exceptions raised within the route handler's function/method. The Litestar + ValidationException will be added automatically for the schema if any validation is involved. + response_description: Text used for the route's response schema description section. + security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. + summary: Text used for the route's schema summary section. + tags: A sequence of string tags that will be appended to the OpenAPI schema. + type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec + hook for deserialization. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + **kwargs: Any additional kwarg - will be set in the opt dictionary. + """ + if "http_method" in kwargs: + raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP) + super().__init__( + after_request=after_request, + after_response=after_response, + background=background, + before_request=before_request, + cache=cache, + cache_control=cache_control, + cache_key_builder=cache_key_builder, + content_encoding=content_encoding, + content_media_type=content_media_type, + dependencies=dependencies, + deprecated=deprecated, + description=description, + dto=dto, + exception_handlers=exception_handlers, + etag=etag, + guards=guards, + http_method=HttpMethod.POST, + include_in_schema=include_in_schema, + media_type=media_type, + middleware=middleware, + name=name, + operation_class=operation_class, + operation_id=operation_id, + opt=opt, + path=path, + raises=raises, + request_class=request_class, + response_class=response_class, + response_cookies=response_cookies, + response_description=response_description, + response_headers=response_headers, + responses=responses, + return_dto=return_dto, + signature_namespace=signature_namespace, + security=security, + status_code=status_code, + summary=summary, + sync_to_thread=sync_to_thread, + tags=tags, + type_decoders=type_decoders, + type_encoders=type_encoders, + **kwargs, + ) + + +class put(HTTPRouteHandler): + """PUT Route Decorator. + + Use this decorator to decorate an HTTP handler for PUT requests. + """ + + def __init__( + self, + path: str | None | Sequence[str] = None, + *, + after_request: AfterRequestHookHandler | None = None, + after_response: AfterResponseHookHandler | None = None, + background: BackgroundTask | BackgroundTasks | None = None, + before_request: BeforeRequestHookHandler | None = None, + cache: bool | int | type[CACHE_FOREVER] = False, + cache_control: CacheControlHeader | None = None, + cache_key_builder: CacheKeyBuilder | None = None, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + etag: ETag | None = None, + exception_handlers: ExceptionHandlersMap | None = None, + guards: Sequence[Guard] | None = None, + media_type: MediaType | str | None = None, + middleware: Sequence[Middleware] | None = None, + name: str | None = None, + opt: Mapping[str, Any] | None = None, + request_class: type[Request] | None = None, + response_class: type[Response] | None = None, + response_cookies: ResponseCookies | None = None, + response_headers: ResponseHeaders | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + signature_namespace: Mapping[str, Any] | None = None, + status_code: int | None = None, + sync_to_thread: bool | None = None, + # OpenAPI related attributes + content_encoding: str | None = None, + content_media_type: str | None = None, + deprecated: bool = False, + description: str | None = None, + include_in_schema: bool | EmptyType = Empty, + operation_class: type[Operation] = Operation, + operation_id: str | OperationIDCreator | None = None, + raises: Sequence[type[HTTPException]] | None = None, + response_description: str | None = None, + responses: Mapping[int, ResponseSpec] | None = None, + security: Sequence[SecurityRequirement] | None = None, + summary: str | None = None, + tags: Sequence[str] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + **kwargs: Any, + ) -> None: + """Initialize ``put`` + + Args: + path: A path fragment for the route handler function or a sequence of path fragments. + If not given defaults to ``/`` + after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed + to any route handler. If this function returns a value, the request will not reach the route handler, + and instead this value will be used. + after_response: A sync or async function called after the response has been awaited. It receives the + :class:`Request <.connection.Request>` object and should not return any values. + background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or + :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. + Defaults to ``None``. + before_request: A sync or async function called immediately before calling the route handler. Receives + the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, + bypassing the route handler. + cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number + of seconds (e.g. ``120``) to cache the response. + cache_control: A ``cache-control`` header of type + :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. + cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization + of the cache key if caching is configured on the application level. + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and + validation of request data. + etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + http_method: An :class:`http method string <.types.Method>`, a member of the enum + :class:`HttpMethod <litestar.enums.HttpMethod>` or a list of these that correlates to the methods the + route handler function should handle. + media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a + valid IANA Media-Type. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + name: A string identifying the route handler. + opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or + wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. + request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's + default request. + response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's + default response. + response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. + response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` + instances. + responses: A mapping of additional status codes and a description of their expected content. + This information will be included in the OpenAPI schema + return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing + outbound response data. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. + status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and + ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. + sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the + main event loop. This has an effect only for sync handler functions. See using sync handler functions. + content_encoding: A string describing the encoding of the content, e.g. ``base64``. + content_media_type: A string designating the media-type of the content, e.g. ``image/png``. + deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. + description: Text used for the route's schema description section. + include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. + operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. + operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. + raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. + This list should describe all exceptions raised within the route handler's function/method. The Litestar + ValidationException will be added automatically for the schema if any validation is involved. + response_description: Text used for the route's response schema description section. + security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. + summary: Text used for the route's schema summary section. + tags: A sequence of string tags that will be appended to the OpenAPI schema. + type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec + hook for deserialization. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + **kwargs: Any additional kwarg - will be set in the opt dictionary. + """ + if "http_method" in kwargs: + raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP) + super().__init__( + after_request=after_request, + after_response=after_response, + background=background, + before_request=before_request, + cache=cache, + cache_control=cache_control, + cache_key_builder=cache_key_builder, + content_encoding=content_encoding, + content_media_type=content_media_type, + dependencies=dependencies, + deprecated=deprecated, + description=description, + dto=dto, + exception_handlers=exception_handlers, + etag=etag, + guards=guards, + http_method=HttpMethod.PUT, + include_in_schema=include_in_schema, + media_type=media_type, + middleware=middleware, + name=name, + operation_class=operation_class, + operation_id=operation_id, + opt=opt, + path=path, + raises=raises, + request_class=request_class, + response_class=response_class, + response_cookies=response_cookies, + response_description=response_description, + response_headers=response_headers, + responses=responses, + return_dto=return_dto, + security=security, + signature_namespace=signature_namespace, + status_code=status_code, + summary=summary, + sync_to_thread=sync_to_thread, + tags=tags, + type_decoders=type_decoders, + type_encoders=type_encoders, + **kwargs, + ) diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/__init__.py b/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/__init__.py new file mode 100644 index 0000000..5b24734 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/__init__.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from litestar.handlers.websocket_handlers.listener import ( + WebsocketListener, + WebsocketListenerRouteHandler, + websocket_listener, +) +from litestar.handlers.websocket_handlers.route_handler import WebsocketRouteHandler, websocket + +__all__ = ( + "WebsocketListener", + "WebsocketListenerRouteHandler", + "WebsocketRouteHandler", + "websocket", + "websocket_listener", +) diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/__pycache__/__init__.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..f6d1115 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/__pycache__/__init__.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/__pycache__/_utils.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/__pycache__/_utils.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..c5ae4c8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/__pycache__/_utils.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/__pycache__/listener.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/__pycache__/listener.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..38b8219 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/__pycache__/listener.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/__pycache__/route_handler.cpython-311.pyc b/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/__pycache__/route_handler.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..0e92ccd --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/__pycache__/route_handler.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/_utils.py b/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/_utils.py new file mode 100644 index 0000000..bcd90ac --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/_utils.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +from functools import wraps +from inspect import Parameter, Signature +from typing import TYPE_CHECKING, Any, Callable, Coroutine, Dict + +from msgspec.json import Encoder as JsonEncoder + +from litestar.di import Provide +from litestar.serialization import decode_json +from litestar.types.builtin_types import NoneType +from litestar.utils import ensure_async_callable +from litestar.utils.helpers import unwrap_partial + +if TYPE_CHECKING: + from litestar import WebSocket + from litestar.handlers.websocket_handlers.listener import WebsocketListenerRouteHandler + from litestar.types import AnyCallable + from litestar.utils.signature import ParsedSignature + + +def create_handle_receive(listener: WebsocketListenerRouteHandler) -> Callable[[WebSocket], Coroutine[Any, None, None]]: + if data_dto := listener.resolve_data_dto(): + + async def handle_receive(socket: WebSocket) -> Any: + received_data = await socket.receive_data(mode=listener._receive_mode) + return data_dto(socket).decode_bytes( + received_data.encode("utf-8") if isinstance(received_data, str) else received_data + ) + + elif listener.parsed_data_field and listener.parsed_data_field.annotation is str: + + async def handle_receive(socket: WebSocket) -> Any: + received_data = await socket.receive_data(mode=listener._receive_mode) + return received_data.decode("utf-8") if isinstance(received_data, bytes) else received_data + + elif listener.parsed_data_field and listener.parsed_data_field.annotation is bytes: + + async def handle_receive(socket: WebSocket) -> Any: + received_data = await socket.receive_data(mode=listener._receive_mode) + return received_data.encode("utf-8") if isinstance(received_data, str) else received_data + + else: + + async def handle_receive(socket: WebSocket) -> Any: + received_data = await socket.receive_data(mode=listener._receive_mode) + return decode_json(value=received_data, type_decoders=socket.route_handler.resolve_type_decoders()) + + return handle_receive + + +def create_handle_send( + listener: WebsocketListenerRouteHandler, +) -> Callable[[WebSocket, Any], Coroutine[None, None, None]]: + json_encoder = JsonEncoder(enc_hook=listener.default_serializer) + + if return_dto := listener.resolve_return_dto(): + + async def handle_send(socket: WebSocket, data: Any) -> None: + encoded_data = return_dto(socket).data_to_encodable_type(data) + data = json_encoder.encode(encoded_data) + await socket.send_data(data=data, mode=listener._send_mode) + + elif listener.parsed_return_field.is_subclass_of((str, bytes)) or ( + listener.parsed_return_field.is_optional and listener.parsed_return_field.has_inner_subclass_of((str, bytes)) + ): + + async def handle_send(socket: WebSocket, data: Any) -> None: + await socket.send_data(data=data, mode=listener._send_mode) + + else: + + async def handle_send(socket: WebSocket, data: Any) -> None: + data = json_encoder.encode(data) + await socket.send_data(data=data, mode=listener._send_mode) + + return handle_send + + +class ListenerHandler: + __slots__ = ("_can_send_data", "_fn", "_listener", "_pass_socket") + + def __init__( + self, + listener: WebsocketListenerRouteHandler, + fn: AnyCallable, + parsed_signature: ParsedSignature, + namespace: dict[str, Any], + ) -> None: + self._can_send_data = not parsed_signature.return_type.is_subclass_of(NoneType) + self._fn = ensure_async_callable(fn) + self._listener = listener + self._pass_socket = "socket" in parsed_signature.parameters + + async def __call__( + self, + *args: Any, + socket: WebSocket, + connection_lifespan_dependencies: Dict[str, Any], # noqa: UP006 + **kwargs: Any, + ) -> None: + lifespan_mananger = self._listener._connection_lifespan or self._listener.default_connection_lifespan + handle_send = self._listener.resolve_send_handler() if self._can_send_data else None + handle_receive = self._listener.resolve_receive_handler() + + if self._pass_socket: + kwargs["socket"] = socket + + async with lifespan_mananger(**connection_lifespan_dependencies): + while True: + received_data = await handle_receive(socket) + data = await self._fn(*args, data=received_data, **kwargs) + if handle_send: + await handle_send(socket, data) + + +def create_handler_signature(callback_signature: Signature) -> Signature: + """Creates a :class:`Signature` for the handler function for signature modelling. + + This is required for two reasons: + + 1. the :class:`.handlers.WebsocketHandler` signature model cannot contain the ``data`` parameter, which is + required for :class:`.handlers.websocket_listener` handlers. + 2. the :class;`.handlers.WebsocketHandler` signature model must include the ``socket`` parameter, which is + optional for :class:`.handlers.websocket_listener` handlers. + + Args: + callback_signature: The :class:`Signature` of the listener callback. + + Returns: + The :class:`Signature` for the listener callback as required for signature modelling. + """ + new_params = [p for p in callback_signature.parameters.values() if p.name != "data"] + if "socket" not in callback_signature.parameters: + new_params.append(Parameter(name="socket", kind=Parameter.KEYWORD_ONLY, annotation="WebSocket")) + + new_params.append( + Parameter(name="connection_lifespan_dependencies", kind=Parameter.KEYWORD_ONLY, annotation="Dict[str, Any]") + ) + + return callback_signature.replace(parameters=new_params) + + +def create_stub_dependency(src: AnyCallable) -> Provide: + """Create a stub dependency, accepting any kwargs defined in ``src``, and + wrap it in ``Provide`` + """ + src = unwrap_partial(src) + + @wraps(src) + async def stub(**kwargs: Any) -> Dict[str, Any]: # noqa: UP006 + return kwargs + + return Provide(stub) diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/listener.py b/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/listener.py new file mode 100644 index 0000000..86fefc9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/listener.py @@ -0,0 +1,417 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from contextlib import AbstractAsyncContextManager, asynccontextmanager +from typing import ( + TYPE_CHECKING, + Any, + AsyncGenerator, + Callable, + Dict, + Mapping, + Optional, + cast, + overload, +) + +from litestar._signature import SignatureModel +from litestar.connection import WebSocket +from litestar.exceptions import ImproperlyConfiguredException, WebSocketDisconnect +from litestar.types import ( + AnyCallable, + Dependencies, + Empty, + EmptyType, + ExceptionHandler, + Guard, + Middleware, + TypeEncodersMap, +) +from litestar.utils import ensure_async_callable +from litestar.utils.signature import ParsedSignature, get_fn_type_hints + +from ._utils import ( + ListenerHandler, + create_handle_receive, + create_handle_send, + create_handler_signature, + create_stub_dependency, +) +from .route_handler import WebsocketRouteHandler + +if TYPE_CHECKING: + from typing import Coroutine + + from typing_extensions import Self + + from litestar import Router + from litestar.dto import AbstractDTO + from litestar.types.asgi_types import WebSocketMode + from litestar.types.composite_types import TypeDecodersSequence + +__all__ = ("WebsocketListener", "WebsocketListenerRouteHandler", "websocket_listener") + + +class WebsocketListenerRouteHandler(WebsocketRouteHandler): + """A websocket listener that automatically accepts a connection, handles disconnects, + invokes a callback function every time new data is received and sends any data + returned + """ + + __slots__ = { + "connection_accept_handler": "Callback to accept a WebSocket connection. By default, calls WebSocket.accept", + "on_accept": "Callback invoked after a WebSocket connection has been accepted", + "on_disconnect": "Callback invoked after a WebSocket connection has been closed", + "weboscket_class": "WebSocket class", + "_connection_lifespan": None, + "_handle_receive": None, + "_handle_send": None, + "_receive_mode": None, + "_send_mode": None, + } + + @overload + def __init__( + self, + path: str | list[str] | None = None, + *, + connection_lifespan: Callable[..., AbstractAsyncContextManager[Any]] | None = None, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None, + guards: list[Guard] | None = None, + middleware: list[Middleware] | None = None, + receive_mode: WebSocketMode = "text", + send_mode: WebSocketMode = "text", + name: str | None = None, + opt: dict[str, Any] | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + signature_namespace: Mapping[str, Any] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + websocket_class: type[WebSocket] | None = None, + **kwargs: Any, + ) -> None: ... + + @overload + def __init__( + self, + path: str | list[str] | None = None, + *, + connection_accept_handler: Callable[[WebSocket], Coroutine[Any, Any, None]] = WebSocket.accept, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None, + guards: list[Guard] | None = None, + middleware: list[Middleware] | None = None, + receive_mode: WebSocketMode = "text", + send_mode: WebSocketMode = "text", + name: str | None = None, + on_accept: AnyCallable | None = None, + on_disconnect: AnyCallable | None = None, + opt: dict[str, Any] | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + signature_namespace: Mapping[str, Any] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + websocket_class: type[WebSocket] | None = None, + **kwargs: Any, + ) -> None: ... + + def __init__( + self, + path: str | list[str] | None = None, + *, + connection_accept_handler: Callable[[WebSocket], Coroutine[Any, Any, None]] = WebSocket.accept, + connection_lifespan: Callable[..., AbstractAsyncContextManager[Any]] | None = None, + dependencies: Dependencies | None = None, + dto: type[AbstractDTO] | None | EmptyType = Empty, + exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None, + guards: list[Guard] | None = None, + middleware: list[Middleware] | None = None, + receive_mode: WebSocketMode = "text", + send_mode: WebSocketMode = "text", + name: str | None = None, + on_accept: AnyCallable | None = None, + on_disconnect: AnyCallable | None = None, + opt: dict[str, Any] | None = None, + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + signature_namespace: Mapping[str, Any] | None = None, + type_decoders: TypeDecodersSequence | None = None, + type_encoders: TypeEncodersMap | None = None, + websocket_class: type[WebSocket] | None = None, + **kwargs: Any, + ) -> None: + """Initialize ``WebsocketRouteHandler`` + + Args: + path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults + to ``/`` + connection_accept_handler: A callable that accepts a :class:`WebSocket <.connection.WebSocket>` instance + and returns a coroutine that when awaited, will accept the connection. Defaults to ``WebSocket.accept``. + connection_lifespan: An asynchronous context manager, handling the lifespan of the connection. By default, + it calls the ``connection_accept_handler``, ``on_connect`` and ``on_disconnect``. Can request any + dependencies, for example the :class:`WebSocket <.connection.WebSocket>` connection + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and + validation of request data. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + receive_mode: Websocket mode to receive data in, either `text` or `binary`. + send_mode: Websocket mode to receive data in, either `text` or `binary`. + name: A string identifying the route handler. + on_accept: Callback invoked after a connection has been accepted. Can request any dependencies, for example + the :class:`WebSocket <.connection.WebSocket>` connection + on_disconnect: Callback invoked after a connection has been closed. Can request any dependencies, for + example the :class:`WebSocket <.connection.WebSocket>` connection + opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or + wherever you have access to :class:`Request <.connection.Request>` or + :class:`ASGI Scope <.types.Scope>`. + return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing + outbound response data. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature + modelling. + type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec + hook for deserialization. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + **kwargs: Any additional kwarg - will be set in the opt dictionary. + websocket_class: A custom subclass of :class:`WebSocket <.connection.WebSocket>` to be used as route handler's + default websocket class. + """ + if connection_lifespan and any([on_accept, on_disconnect, connection_accept_handler is not WebSocket.accept]): + raise ImproperlyConfiguredException( + "connection_lifespan can not be used with connection hooks " + "(on_accept, on_disconnect, connection_accept_handler)", + ) + + self._receive_mode: WebSocketMode = receive_mode + self._send_mode: WebSocketMode = send_mode + self._connection_lifespan = connection_lifespan + self._send_handler: Callable[[WebSocket, Any], Coroutine[None, None, None]] | EmptyType = Empty + self._receive_handler: Callable[[WebSocket], Any] | EmptyType = Empty + + self.connection_accept_handler = connection_accept_handler + self.on_accept = ensure_async_callable(on_accept) if on_accept else None + self.on_disconnect = ensure_async_callable(on_disconnect) if on_disconnect else None + self.type_decoders = type_decoders + self.type_encoders = type_encoders + self.websocket_class = websocket_class + + listener_dependencies = dict(dependencies or {}) + + listener_dependencies["connection_lifespan_dependencies"] = create_stub_dependency( + connection_lifespan or self.default_connection_lifespan + ) + + if self.on_accept: + listener_dependencies["on_accept_dependencies"] = create_stub_dependency(self.on_accept) + + if self.on_disconnect: + listener_dependencies["on_disconnect_dependencies"] = create_stub_dependency(self.on_disconnect) + + super().__init__( + path=path, + dependencies=listener_dependencies, + exception_handlers=exception_handlers, + guards=guards, + middleware=middleware, + name=name, + opt=opt, + signature_namespace=signature_namespace, + dto=dto, + return_dto=return_dto, + type_decoders=type_decoders, + type_encoders=type_encoders, + websocket_class=websocket_class, + **kwargs, + ) + + def __call__(self, fn: AnyCallable) -> Self: + parsed_signature = ParsedSignature.from_fn(fn, self.resolve_signature_namespace()) + + if "data" not in parsed_signature.parameters: + raise ImproperlyConfiguredException("Websocket listeners must accept a 'data' parameter") + + for param in ("request", "body"): + if param in parsed_signature.parameters: + raise ImproperlyConfiguredException(f"The {param} kwarg is not supported with websocket listeners") + + # we are manipulating the signature of the decorated function below, so we must store the original values for + # use elsewhere. + self._parsed_return_field = parsed_signature.return_type + self._parsed_data_field = parsed_signature.parameters.get("data") + self._parsed_fn_signature = ParsedSignature.from_signature( + create_handler_signature(parsed_signature.original_signature), + fn_type_hints={ + **get_fn_type_hints(fn, namespace=self.resolve_signature_namespace()), + **get_fn_type_hints(ListenerHandler.__call__, namespace=self.resolve_signature_namespace()), + }, + ) + + return super().__call__( + ListenerHandler( + listener=self, fn=fn, parsed_signature=parsed_signature, namespace=self.resolve_signature_namespace() + ) + ) + + def _validate_handler_function(self) -> None: + """Validate the route handler function once it's set by inspecting its return annotations.""" + # validation occurs in the call method + + @property + def signature_model(self) -> type[SignatureModel]: + """Get the signature model for the route handler. + + Returns: + A signature model for the route handler. + + """ + if self._signature_model is Empty: + self._signature_model = SignatureModel.create( + dependency_name_set=self.dependency_name_set, + fn=cast("AnyCallable", self.fn), + parsed_signature=self.parsed_fn_signature, + type_decoders=self.resolve_type_decoders(), + ) + return self._signature_model + + @asynccontextmanager + async def default_connection_lifespan( + self, + socket: WebSocket, + on_accept_dependencies: Optional[Dict[str, Any]] = None, # noqa: UP006, UP007 + on_disconnect_dependencies: Optional[Dict[str, Any]] = None, # noqa: UP006, UP007 + ) -> AsyncGenerator[None, None]: + """Handle the connection lifespan of a :class:`WebSocket <.connection.WebSocket>`. + + Args: + socket: The :class:`WebSocket <.connection.WebSocket>` connection + on_accept_dependencies: Dependencies requested by the :attr:`on_accept` hook + on_disconnect_dependencies: Dependencies requested by the :attr:`on_disconnect` hook + + By, default this will + + - Call :attr:`connection_accept_handler` to accept a connection + - Call :attr:`on_accept` if defined after a connection has been accepted + - Call :attr:`on_disconnect` upon leaving the context + """ + await self.connection_accept_handler(socket) + + if self.on_accept: + await self.on_accept(**(on_accept_dependencies or {})) + + try: + yield + except WebSocketDisconnect: + pass + finally: + if self.on_disconnect: + await self.on_disconnect(**(on_disconnect_dependencies or {})) + + def resolve_receive_handler(self) -> Callable[[WebSocket], Any]: + if self._receive_handler is Empty: + self._receive_handler = create_handle_receive(self) + return self._receive_handler + + def resolve_send_handler(self) -> Callable[[WebSocket, Any], Coroutine[None, None, None]]: + if self._send_handler is Empty: + self._send_handler = create_handle_send(self) + return self._send_handler + + +websocket_listener = WebsocketListenerRouteHandler + + +class WebsocketListener(ABC): + path: str | list[str] | None = None + """A path fragment for the route handler function or a sequence of path fragments. If not given defaults to ``/``""" + dependencies: Dependencies | None = None + """A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances.""" + dto: type[AbstractDTO] | None | EmptyType = Empty + """:class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and validation of request data""" + exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None + """A mapping of status codes and/or exception types to handler functions.""" + guards: list[Guard] | None = None + """A sequence of :class:`Guard <.types.Guard>` callables.""" + middleware: list[Middleware] | None = None + """A sequence of :class:`Middleware <.types.Middleware>`.""" + on_accept: AnyCallable | None = None + """Called after a :class:`WebSocket <.connection.WebSocket>` connection has been accepted. Can receive any dependencies""" + on_disconnect: AnyCallable | None = None + """Called after a :class:`WebSocket <.connection.WebSocket>` connection has been disconnected. Can receive any dependencies""" + receive_mode: WebSocketMode = "text" + """:class:`WebSocket <.connection.WebSocket>` mode to receive data in, either ``text`` or ``binary``.""" + send_mode: WebSocketMode = "text" + """Websocket mode to send data in, either `text` or `binary`.""" + name: str | None = None + """A string identifying the route handler.""" + opt: dict[str, Any] | None = None + """ + A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or wherever you + have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. + """ + return_dto: type[AbstractDTO] | None | EmptyType = Empty + """:class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data.""" + signature_namespace: Mapping[str, Any] | None = None + """ + A mapping of names to types for use in forward reference resolution during signature modelling. + """ + type_decoders: TypeDecodersSequence | None = None + """ + type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec + hook for deserialization. + """ + type_encoders: TypeEncodersMap | None = None + """ + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + """ + websocket_class: type[WebSocket] | None = None + """ + websocket_class: A custom subclass of :class:`WebSocket <.connection.WebSocket>` to be used as route handler's + default websocket class. + """ + + def __init__(self, owner: Router) -> None: + """Initialize a WebsocketListener instance. + + Args: + owner: The :class:`Router <.router.Router>` instance that owns this listener. + """ + self._owner = owner + + def to_handler(self) -> WebsocketListenerRouteHandler: + handler = WebsocketListenerRouteHandler( + dependencies=self.dependencies, + dto=self.dto, + exception_handlers=self.exception_handlers, + guards=self.guards, + middleware=self.middleware, + send_mode=self.send_mode, + receive_mode=self.receive_mode, + name=self.name, + on_accept=self.on_accept, + on_disconnect=self.on_disconnect, + opt=self.opt, + path=self.path, + return_dto=self.return_dto, + signature_namespace=self.signature_namespace, + type_decoders=self.type_decoders, + type_encoders=self.type_encoders, + websocket_class=self.websocket_class, + )(self.on_receive) + handler.owner = self._owner + return handler + + @abstractmethod + def on_receive(self, *args: Any, **kwargs: Any) -> Any: + """Called after data has been received from the WebSocket. + + This should take a ``data`` argument, receiving the processed WebSocket data, + and can additionally include handler dependencies such as ``state``, or other + regular dependencies. + + Data returned from this function will be serialized and sent via the socket + according to handler configuration. + """ + raise NotImplementedError diff --git a/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/route_handler.py b/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/route_handler.py new file mode 100644 index 0000000..edb49c3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/handlers/websocket_handlers/route_handler.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Mapping + +from litestar.connection import WebSocket +from litestar.exceptions import ImproperlyConfiguredException +from litestar.handlers import BaseRouteHandler +from litestar.types.builtin_types import NoneType +from litestar.utils.predicates import is_async_callable + +if TYPE_CHECKING: + from litestar.types import Dependencies, ExceptionHandler, Guard, Middleware + + +class WebsocketRouteHandler(BaseRouteHandler): + """Websocket route handler decorator. + + Use this decorator to decorate websocket handler functions. + """ + + def __init__( + self, + path: str | list[str] | None = None, + *, + dependencies: Dependencies | None = None, + exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None, + guards: list[Guard] | None = None, + middleware: list[Middleware] | None = None, + name: str | None = None, + opt: dict[str, Any] | None = None, + signature_namespace: Mapping[str, Any] | None = None, + websocket_class: type[WebSocket] | None = None, + **kwargs: Any, + ) -> None: + """Initialize ``WebsocketRouteHandler`` + + Args: + path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults + to ``/`` + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + name: A string identifying the route handler. + opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or + wherever you have access to :class:`Request <.connection.Request>` or + :class:`ASGI Scope <.types.Scope>`. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + **kwargs: Any additional kwarg - will be set in the opt dictionary. + websocket_class: A custom subclass of :class:`WebSocket <.connection.WebSocket>` to be used as route handler's + default websocket class. + """ + self.websocket_class = websocket_class + + super().__init__( + path=path, + dependencies=dependencies, + exception_handlers=exception_handlers, + guards=guards, + middleware=middleware, + name=name, + opt=opt, + signature_namespace=signature_namespace, + **kwargs, + ) + + def resolve_websocket_class(self) -> type[WebSocket]: + """Return the closest custom WebSocket class in the owner graph or the default Websocket class. + + This method is memoized so the computation occurs only once. + + Returns: + The default :class:`WebSocket <.connection.WebSocket>` class for the route handler. + """ + return next( + (layer.websocket_class for layer in reversed(self.ownership_layers) if layer.websocket_class is not None), + WebSocket, + ) + + def _validate_handler_function(self) -> None: + """Validate the route handler function once it's set by inspecting its return annotations.""" + super()._validate_handler_function() + + if not self.parsed_fn_signature.return_type.is_subclass_of(NoneType): + raise ImproperlyConfiguredException("Websocket handler functions should return 'None'") + + if "socket" not in self.parsed_fn_signature.parameters: + raise ImproperlyConfiguredException("Websocket handlers must set a 'socket' kwarg") + + for param in ("request", "body", "data"): + if param in self.parsed_fn_signature.parameters: + raise ImproperlyConfiguredException(f"The {param} kwarg is not supported with websocket handlers") + + if not is_async_callable(self.fn): + raise ImproperlyConfiguredException("Functions decorated with 'websocket' must be async functions") + + +websocket = WebsocketRouteHandler |