From 8db4fb37919230647820b65a29614cf97814b3c4 Mon Sep 17 00:00:00 2001 From: Tiago Silva Date: Tue, 4 Jun 2024 18:31:04 +0100 Subject: [PATCH] Refactor internal of routing (#335) --- esmerald/routing/base.py | 708 +++++++++++++++++++++++++---------- esmerald/routing/gateways.py | 8 +- esmerald/routing/router.py | 8 +- 3 files changed, 516 insertions(+), 208 deletions(-) diff --git a/esmerald/routing/base.py b/esmerald/routing/base.py index 66af717a..af835b42 100644 --- a/esmerald/routing/base.py +++ b/esmerald/routing/base.py @@ -11,7 +11,6 @@ Callable, Dict, List, - Optional, Set, Type, TypeVar, @@ -27,9 +26,7 @@ from lilya.types import Receive, Scope, Send from typing_extensions import TypedDict -from esmerald.backgound import BackgroundTask, BackgroundTasks from esmerald.datastructures import ResponseContainer -from esmerald.enums import MediaType from esmerald.exceptions import ImproperlyConfigured from esmerald.injector import Inject from esmerald.permissions.utils import continue_or_raise_permission_exception @@ -56,13 +53,7 @@ from esmerald.permissions import BasePermission from esmerald.permissions.types import Permission from esmerald.routing.router import HTTPHandler - from esmerald.types import ( - APIGateHandler, - AsyncAnyCallable, - Dependencies, - ResponseCookies, - ResponseHeaders, - ) + from esmerald.types import APIGateHandler, Dependencies, ResponseCookies, ResponseHeaders from esmerald.typing import AnyCallable param_type_map = { @@ -81,7 +72,7 @@ CONV2TYPE = {conv: typ for typ, conv in TRANSFORMER_TYPES.items()} -T = TypeVar("T", bound="BaseHandlerMixin") +T = TypeVar("T", bound="Dispatcher") class PathParameterSchema(TypedDict): @@ -156,14 +147,75 @@ class BaseResponseHandler: In charge of handling the responses of the handlers. """ - def response_container_handler( + @staticmethod + async def _get_response_data( + route: "HTTPHandler", parameter_model: "TransformerModel", request: Request + ) -> Any: + """ + Determine required kwargs for the given handler, assign to the object dictionary, and get the response data. + + Args: + route (HTTPHandler): The route handler for the request. + parameter_model (TransformerModel): The parameter model for handling request parameters. + request (Request): The incoming request. + + Returns: + Any: The response data generated by processing the request. + """ + signature_model = get_signature(route) + is_data_or_payload: str = None + + if parameter_model.has_kwargs: + kwargs = parameter_model.to_kwargs(connection=request, handler=route) + + is_data_or_payload = DATA if kwargs.get(DATA) else PAYLOAD + request_data = kwargs.get(DATA) or kwargs.get(PAYLOAD) + + if request_data: + kwargs[is_data_or_payload] = await request_data + + for dependency in parameter_model.dependencies: + kwargs[dependency.key] = await parameter_model.get_dependencies( + dependency=dependency, connection=request, **kwargs + ) + + parsed_kwargs = signature_model.parse_values_for_connection( + connection=request, **kwargs + ) + else: + parsed_kwargs = {} + + if isinstance(route.parent, View): + fn = partial( + cast("AnyCallable", route.fn), + route.parent, + **parsed_kwargs, + ) + else: + fn = partial(cast("AnyCallable", route.fn), **parsed_kwargs) + + if is_async_callable(fn): + return await fn() + return fn() + + def _get_response_container_handler( self, cookies: "ResponseCookies", headers: Dict[str, Any], media_type: str, - status_code: int, - ) -> "AsyncAnyCallable": - """Creates a handler for ResponseContainer Types""" + ) -> Callable[[ResponseContainer, Type["Esmerald"], Dict[str, Any]], LilyaResponse]: + """ + Creates a handler for ResponseContainer types. + + Args: + cookies (ResponseCookies): The response cookies. + headers (Dict[str, Any]): The response headers. + media_type (str): The media type. + + Returns: + Callable[[ResponseContainer, Type["Esmerald"], Dict[str, Any]], LilyaResponse]: The response container handler function. + + """ async def response_content( data: ResponseContainer, app: Type["Esmerald"], **kwargs: Dict[str, Any] @@ -173,78 +225,101 @@ async def response_content( response: Response = data.to_response( app=app, headers=_headers, - status_code=status_code, + status_code=self.status_code, media_type=media_type, ) for cookie in _cookies: response.set_cookie(**cookie) return response - return response_content + return cast( + Callable[[ResponseContainer, Type["Esmerald"], Dict[str, Any]], LilyaResponse], + response_content, + ) + + def _get_json_response_handler( + self, cookies: "ResponseCookies", headers: Dict[str, Any] + ) -> Callable[[Response, Dict[str, Any]], LilyaResponse]: + """ + Creates a handler function for JSON responses. + + Args: + cookies (ResponseCookies): The response cookies. + headers (Dict[str, Any]): The response headers. + + Returns: + Callable[[Response, Dict[str, Any]], LilyaResponse]: The JSON response handler function. + """ - def response_handler( - self, - cookies: "ResponseCookies", - headers: Optional["ResponseHeaders"] = None, - status_code: Optional[int] = None, - media_type: Optional[str] = MediaType.TEXT, - ) -> "AsyncAnyCallable": async def response_content(data: Response, **kwargs: Dict[str, Any]) -> LilyaResponse: - _cookies = self.get_cookies(data.cookies, cookies) + _cookies = self.get_cookies(cookies, []) _headers = { **self.get_headers(headers), **data.headers, **self.allow_header, } - for cookie in _cookies: - data.set_cookie(**cookie) - - if status_code: - data.status_code = status_code - - if media_type: - data.media_type = media_type + data.set_cookie(**cookie) # pragma: no cover for header, value in _headers.items(): data.headers[header] = value + + if self.status_code: + data.status_code = self.status_code return data - return response_content + return cast(Callable[[Response, Dict[str, Any]], LilyaResponse], response_content) - def json_response_handler( - self, - status_code: Optional[int] = None, - cookies: Optional["ResponseCookies"] = None, - headers: Optional["ResponseHeaders"] = None, - ) -> "AsyncAnyCallable": - """Creates a handler function for Esmerald JSON responses""" + def _get_response_handler( + self, cookies: "ResponseCookies", headers: Dict[str, Any], media_type: str + ) -> Callable[[Response, Dict[str, Any]], LilyaResponse]: + """ + Creates a handler function for Response types. + + Args: + cookies (ResponseCookies): The response cookies. + headers (Dict[str, Any]): The response headers. + media_type (str): The media type. + + Returns: + Callable[[Response, Dict[str, Any]], LilyaResponse]: The response handler function. + """ async def response_content(data: Response, **kwargs: Dict[str, Any]) -> LilyaResponse: - _cookies = self.get_cookies(cookies, []) + _cookies = self.get_cookies(data.cookies, cookies) _headers = { **self.get_headers(headers), **data.headers, **self.allow_header, } for cookie in _cookies: - data.set_cookie(**cookie) # pragma: no cover + data.set_cookie(**cookie) + + if self.status_code: + data.status_code = self.status_code + + if media_type: + data.media_type = media_type for header, value in _headers.items(): data.headers[header] = value - - if status_code: - data.status_code = status_code return data - return response_content + return cast(Callable[[Response, Dict[str, Any]], LilyaResponse], response_content) - def lilya_response_handler( - self, - cookies: "ResponseCookies", - headers: Optional["ResponseHeaders"] = None, - ) -> "AsyncAnyCallable": - """Creates an handler for Lilya Responses.""" + def _get_lilya_response_handler( + self, cookies: "ResponseCookies", headers: Dict[str, Any] + ) -> Callable[[LilyaResponse, Dict[str, Any]], LilyaResponse]: + """ + Creates a handler function for Lilya Responses. + + Args: + cookies (ResponseCookies): The response cookies. + headers (Dict[str, Any]): The response headers. + + Returns: + Callable[[LilyaResponse, Dict[str, Any]], LilyaResponse]: The Lilya response handler function. + """ async def response_content(data: LilyaResponse, **kwargs: Dict[str, Any]) -> LilyaResponse: _cookies = self.get_cookies(cookies, []) @@ -260,39 +335,94 @@ async def response_content(data: LilyaResponse, **kwargs: Dict[str, Any]) -> Lil data.headers[header] = value return data - return response_content + return cast(Callable[[LilyaResponse, Dict[str, Any]], LilyaResponse], response_content) - def _handler( + def _get_default_handler( self, - background: Optional[Union["BackgroundTask", "BackgroundTasks"]], cookies: "ResponseCookies", headers: Dict[str, Any], media_type: str, response_class: Any, - status_code: int, - ) -> "AsyncAnyCallable": - async def response_content(data: Any, **kwargs: Dict[str, Any]) -> LilyaResponse: + ) -> Callable[[Any, Dict[str, Any]], LilyaResponse]: + """ + Creates a default handler function. + + Args: + cookies (ResponseCookies): The response cookies. + headers (Dict[str, Any]): The response headers. + media_type (str): The media type. + response_class (Any): The response class. + Returns: + Callable[[Any, Dict[str, Any]], LilyaResponse]: The default handler function. + """ + + async def response_content(data: Any, **kwargs: Dict[str, Any]) -> LilyaResponse: data = await self.get_response_data(data=data) _cookies = self.get_cookies(cookies, []) if isinstance(data, JSONResponse): response = data - response.status_code = status_code - response.background = background + response.status_code = self.status_code + response.background = self.background else: response = response_class( - background=background, + background=self.background, content=data, headers=headers, media_type=media_type, - status_code=status_code, + status_code=self.status_code, ) for cookie in _cookies: response.set_cookie(**cookie) # pragma: no cover return response - return response_content + return cast(Callable[[Response, Dict[str, Any]], LilyaResponse], response_content) + + +class BaseDispatcher(BaseResponseHandler): + """ + The base class for dispatching requests to route handlers. + + This class provides methods for getting a response for a request and calling the handler function. + """ + + def get_response_for_handler(self) -> Callable[[Any], Awaitable[LilyaResponse]]: + """ + Checks and validates the type of return response and maps to the corresponding + handler with the given parameters. + + Returns: + Callable[[Any], Awaitable[LilyaResponse]]: The response handler function. + """ + if self._response_handler is not Void: + return cast("Callable[[Any], Awaitable[LilyaResponse]]", self._response_handler) + + media_type = ( + self.media_type.value if isinstance(self.media_type, Enum) else self.media_type + ) + + response_class = self.get_response_class() + headers = self.get_response_headers() + cookies = self.get_response_cookies() + + if is_class_and_subclass(self.handler_signature.return_annotation, ResponseContainer): + handler = self._get_response_container_handler(cookies, headers, media_type) + elif is_class_and_subclass(self.handler_signature.return_annotation, JSONResponse): + handler = self._get_json_response_handler(cookies, headers) # type: ignore[assignment] + elif is_class_and_subclass(self.handler_signature.return_annotation, Response): + handler = self._get_response_handler(cookies, headers, media_type) # type: ignore[assignment] + elif is_class_and_subclass(self.handler_signature.return_annotation, LilyaResponse): + handler = self._get_lilya_response_handler(cookies, headers) # type: ignore[assignment] + else: + handler = self._get_default_handler(cookies, headers, media_type, response_class) # type: ignore[assignment] + + self._response_handler = handler + + return cast( + Callable[[Any], Awaitable[LilyaResponse]], + self._response_handler, + ) async def get_response_for_request( self, @@ -351,122 +481,33 @@ async def call_handler_function( data=response_data, ) - @staticmethod - async def _get_response_data( - route: "HTTPHandler", parameter_model: "TransformerModel", request: Request - ) -> Any: - """ - Determine required kwargs for the given handler, assign to the object dictionary, and get the response data. - Args: - route (HTTPHandler): The route handler for the request. - parameter_model (TransformerModel): The parameter model for handling request parameters. - request (Request): The incoming request. +class Dispatcher(BaseSignature, BaseDispatcher, OpenAPIDefinitionMixin): + """ + The Dispatcher class is responsible for handling interceptors and executing them before reaching any of the handlers. + """ - Returns: - Any: The response data generated by processing the request. + @property + def handler_signature(self) -> Signature: """ - signature_model = get_signature(route) - is_data_or_payload: str = None - - if parameter_model.has_kwargs: - kwargs = parameter_model.to_kwargs(connection=request, handler=route) - - is_data_or_payload = DATA if kwargs.get(DATA) else PAYLOAD - request_data = kwargs.get(DATA) or kwargs.get(PAYLOAD) - - if request_data: - kwargs[is_data_or_payload] = await request_data - - for dependency in parameter_model.dependencies: - kwargs[dependency.key] = await parameter_model.get_dependencies( - dependency=dependency, connection=request, **kwargs - ) + Returns the Signature of the handler function. - parsed_kwargs = signature_model.parse_values_for_connection( - connection=request, **kwargs - ) - else: - parsed_kwargs = {} + This property returns the Signature object representing the signature of the handler function. + The Signature object provides information about the parameters, return type, and annotations of the handler function. - if isinstance(route.parent, View): - fn = partial( - cast("AnyCallable", route.fn), - route.parent, - **parsed_kwargs, - ) - else: - fn = partial(cast("AnyCallable", route.fn), **parsed_kwargs) + Returns: + - Signature: The Signature object representing the signature of the handler function. - if is_async_callable(fn): - return await fn() - return fn() + Example: + >>> handler = Dispatcher() + >>> signature = handler.handler_signature + >>> print(signature) - def get_response_handler(self) -> Callable[[Any], Awaitable[LilyaResponse]]: - """ - Checks and validates the type of return response and maps to the corresponding - handler with the given parameters. + Note: + - The Signature object is created using the `from_callable` method of the `Signature` class. + - The `from_callable` method takes a callable object (in this case, the handler function) as input and returns a Signature object. + - The Signature object can be used to inspect the parameters and return type of the handler function. """ - if self._response_handler is Void: - media_type = ( - self.media_type.value if isinstance(self.media_type, Enum) else self.media_type - ) - - response_class = self.get_response_class() - headers = self.get_response_headers() - cookies = self.get_response_cookies() - - if is_class_and_subclass(self.handler_signature.return_annotation, ResponseContainer): - handler = self.response_container_handler( - cookies=cookies, - media_type=self.media_type, - status_code=self.status_code, - headers=headers, - ) - elif is_class_and_subclass( - self.handler_signature.return_annotation, - JSONResponse, - ): - handler = self.json_response_handler( - status_code=self.status_code, cookies=cookies, headers=headers - ) - elif is_class_and_subclass(self.handler_signature.return_annotation, Response): - handler = self.response_handler( - cookies=cookies, - status_code=self.status_code, - media_type=self.media_type, - headers=headers, - ) - elif is_class_and_subclass(self.handler_signature.return_annotation, LilyaResponse): - handler = self.lilya_response_handler( - cookies=cookies, - headers=headers, - ) - else: - handler = self._handler( - background=self.background, - cookies=cookies, - headers=headers, - media_type=media_type, - response_class=response_class, - status_code=self.status_code, - ) - self._response_handler = handler - - return cast( - "Callable[[Any], Awaitable[LilyaResponse]]", - self._response_handler, - ) - - -class BaseHandlerMixin(BaseSignature, BaseResponseHandler, OpenAPIDefinitionMixin): - """ - Base of HTTPHandler and WebSocketHandler. - """ - - @property - def handler_signature(self) -> Signature: - """The Signature of 'self.fn'.""" return Signature.from_callable(cast("AnyCallable", self.fn)) @property @@ -474,7 +515,22 @@ def path_parameters(self) -> Set[str]: """ Gets the path parameters in a set format. - Example: {'name', 'id'} + This property returns a set of path parameters used in the URL pattern of the handler. + Each path parameter represents a dynamic value that is extracted from the URL during routing. + + Returns: + - Set[str]: A set of path parameters. + + Example: + >>> handler = Dispatcher() + >>> parameters = handler.path_parameters + >>> print(parameters) + {'name', 'id'} + + Note: + - The path parameters are extracted from the URL pattern defined in the handler's route. + - The path parameters are represented as strings. + - If no path parameters are defined in the URL pattern, an empty set will be returned. """ parameters = set() for param_name, _ in self.param_convertors.items(): @@ -486,6 +542,32 @@ def stringify_parameters(self) -> List[str]: # pragma: no cover """ Gets the param:type in string like list. Used for the directive `esmerald show_urls`. + + This property returns a list of strings representing the parameter name and type in the format "param:type". + It is used specifically for the `esmerald show_urls` directive. + + The method first parses the path of the dispatcher object using the `parse_path` method. + It then filters out any path components that are not dictionaries, leaving only the parameter components. + + Next, it iterates over each parameter component and creates a string in the format "param:type". + The parameter name is obtained from the 'name' key of the component dictionary, + and the parameter type is obtained from the 'type' key of the component dictionary. + + Finally, the method returns the list of stringified parameters. + + Returns: + - List[str]: A list of strings representing the parameter name and type in the format "param:type". + + Example: + >>> dispatcher = Dispatcher() + >>> parameters = dispatcher.stringify_parameters() + >>> print(parameters) + ['param1:int', 'param2:str', 'param3:bool'] + + Note: + - The parameter type is obtained using the `__name__` attribute of the type object. + - The parameter components are obtained by parsing the path of the dispatcher object. + - If there are no parameter components in the path, an empty list will be returned. """ path_components = self.parse_path(self.path) parameters = [component for component in path_components if isinstance(component, dict)] @@ -500,19 +582,31 @@ def parent_levels(self) -> List[Any]: """ Returns the handler from the app down to the route handler. - Who is the parent of a given layer/level. + This property returns a list of all the parent levels of the current handler. + Each parent level represents a higher level in the routing hierarchy. Example: - - app = Esmerald(routes=[ - Include(path='/api/v1', routes=[ - Gateway(path='/home', handler=home) - ]) + Consider the following routing hierarchy: + app = Esmerald(routes=[ + Include(path='/api/v1', routes=[ + Gateway(path='/home', handler=home) ]) + ]) + + In this example, the parent of the Gateway handler is the Include handler. + The parent of the Include handler is the Esmerald router. + The parent of the Esmerald router is the Esmerald app itself. - 1. Parent of Gateway is the Include. - 2. Parent of the Include is the Esmerald router. - 3. Parent of the Esmerald router is the Esmerald app itself. + The `parent_levels` property uses a while loop to traverse the parent hierarchy. + It starts with the current handler and iteratively adds each parent level to a list. + Finally, it reverses the list to maintain the correct order of parent levels. + + Returns: + - List[Any]: A list of parent levels, starting from the current handler and going up to the app level. + + Note: + - The parent levels are determined based on the `parent` attribute of each handler. + - If there are no parent levels (i.e., the current handler is the top-level handler), an empty list will be returned. """ levels = [] current: Any = self @@ -523,14 +617,47 @@ def parent_levels(self) -> List[Any]: @property def dependency_names(self) -> Set[str]: - """A unique set of all dependency names provided in the handlers parent - levels.""" + """ + Returns a unique set of all dependency names provided in the handlers parent levels. + + This property retrieves the dependencies from each parent level of the handler and collects all the dependency names in a set. + It ensures that the set only contains unique dependency names. + + Returns: + - Set[str]: A set of unique dependency names. + + Example: + >>> handler = Dispatcher() + >>> dependency_names = handler.dependency_names + >>> print(dependency_names) + + Note: + - If no dependencies are defined in any of the parent levels, an empty set will be returned. + - The dependencies are collected from all parent levels, ensuring that there are no duplicate dependency names in the final set. + """ level_dependencies = (level.dependencies or {} for level in self.parent_levels) return {name for level in level_dependencies for name in level.keys()} def get_permissions(self) -> List["AsyncCallable"]: """ - Returns all the permissions in the handler scope from the ownsership layers. + Returns all the permissions in the handler scope from the ownership layers. + + This method retrieves all the permissions associated with the handler by iterating over each parent level. + It collects the permissions defined in each level and stores them in a list. + + Returns: + - List[AsyncCallable]: A list of permissions associated with the handler. + + Example: + >>> handler = Dispatcher() + >>> permissions = handler.get_permissions() + >>> print(permissions) + + Note: + - If no permissions are defined in any of the parent levels, an empty list will be returned. + - Each permission is represented by an instance of the AsyncCallable class. + - The AsyncCallable class represents an asynchronous callable object. + - The permissions are collected from all parent levels, ensuring that there are no duplicate permissions in the final list. """ if self._permissions is Void: self._permissions: Union[List["Permission"], "VoidType"] = [] @@ -545,6 +672,25 @@ def get_permissions(self) -> List["AsyncCallable"]: def get_dependencies(self) -> "Dependencies": """ Returns all dependencies of the handler function's starting from the parent levels. + + This method retrieves all the dependencies of the handler function by iterating over each parent level. + It collects the dependencies defined in each level and stores them in a dictionary. + + Returns: + - Dependencies: A dictionary containing all the dependencies of the handler function. + + Raises: + - RuntimeError: If `get_dependencies` is called before a signature model has been generated. + + Example: + >>> handler = Dispatcher() + >>> dependencies = handler.get_dependencies() + >>> print(dependencies) + + Note: + - If no dependencies are defined in any of the parent levels, an empty dictionary will be returned. + - Each dependency is represented by a key-value pair in the dictionary, where the key is the dependency name and the value is the dependency object. + - The dependencies are collected from all parent levels, ensuring that there are no duplicate dependencies in the final dictionary. """ if not self.signature_model: raise RuntimeError( @@ -566,8 +712,30 @@ def get_dependencies(self) -> "Dependencies": @staticmethod def is_unique_dependency(dependencies: "Dependencies", key: str, injector: Inject) -> None: """ - Validates that a given inject has not been already defined under a - different key in any of the levels. + Validates that a given inject has not been already defined under a different key in any of the levels. + + This method takes in a dictionary of dependencies, a key, and an injector. It checks if the injector is already defined in the dependencies dictionary under a different key. + + Parameters: + - dependencies (Dependencies): A dictionary of dependencies. + - key (str): The key to check for uniqueness. + - injector (Inject): The injector to check. + + Raises: + - ImproperlyConfigured: If the injector is already defined under a different key in the dependencies dictionary. + + Example: + >>> dependencies = {"db": injector1, "logger": injector2} + >>> key = "db" + >>> injector = injector3 + >>> is_unique_dependency(dependencies, key, injector) + + This method iterates over each key-value pair in the dependencies dictionary. If the value matches the given injector, it raises an ImproperlyConfigured exception with a detailed error message. + + Note: + - The dependencies dictionary is expected to have string keys and values of type Inject. + - The key parameter should be a string representing the key to check for uniqueness. + - The injector parameter should be an instance of the Inject class. """ for dependency_key, value in dependencies.items(): if injector == value: @@ -581,6 +749,37 @@ def get_cookies( ) -> List[Dict[str, Any]]: # pragma: no cover """ Returns a unique list of cookies. + + This method takes two sets of cookies, `local_cookies` and `other_cookies`, + and returns a list of dictionaries representing the normalized cookies. + + Parameters: + - local_cookies (ResponseCookies): The set of local cookies. + - other_cookies (ResponseCookies): The set of other cookies. + + Returns: + - List[Dict[str, Any]]: A list of dictionaries representing the normalized cookies. + + The method first creates a filtered list of cookies by combining the `local_cookies` + and `other_cookies` sets. It ensures that only unique cookies are included in the list. + + Then, it normalizes each cookie by converting it into a dictionary representation, + excluding the 'description' attribute. The normalized cookies are stored in a list. + + Finally, the method returns the list of normalized cookies. + + Note: + - The 'description' attribute is excluded from the normalized cookies. + + Example usage: + ``` + local_cookies = [...] + other_cookies = [...] + normalized_cookies = get_cookies(local_cookies, other_cookies) + print(normalized_cookies) + ``` + + This will output the list of normalized cookies. """ filtered_cookies = [*local_cookies] for cookie in other_cookies: @@ -595,13 +794,48 @@ def get_cookies( def get_headers(self, headers: "ResponseHeaders") -> Dict[str, Any]: """ - Returns a dict of response headers. + Returns a dictionary of response headers. + + Parameters: + - headers (ResponseHeaders): The response headers object. + + Returns: + - dict[str, Any]: A dictionary containing the response headers. + + Example: + >>> headers = {"Content-Type": "application/json", "Cache-Control": "no-cache"} + >>> response_headers = get_headers(headers) + >>> print(response_headers) + {'Content-Type': 'application/json', 'Cache-Control': 'no-cache'} + + This method takes a `ResponseHeaders` object and converts it into a dictionary + of response headers. Each key-value pair in the `ResponseHeaders` object is + added to the dictionary. + + Note: + - The `ResponseHeaders` object is expected to have string keys and values. + - If the `ResponseHeaders` object is empty, an empty dictionary will be returned. """ return {k: v.value for k, v in headers.items()} async def get_response_data(self, data: Any) -> Any: # pragma: no cover """ - Retrives the response data for sync and async. + Retrieves the response data for synchronous and asynchronous operations. + + This method takes in a `data` parameter, which can be either a regular value or an awaitable object. + If `data` is an awaitable object, it will be awaited to retrieve the actual response data. + If `data` is a regular value, it will be returned as is. + + Parameters: + - data (Any): The response data, which can be either a regular value or an awaitable object. + + Returns: + - Any: The actual response data. + + Example usage: + ``` + response_data = await get_response_data(some_data) + ``` """ if isawaitable(data): data = await data @@ -609,11 +843,20 @@ async def get_response_data(self, data: Any) -> Any: # pragma: no cover async def allow_connection(self, connection: "Connection") -> None: # pragma: no cover """ - Validates the connection. + Validates the connection and handles permissions for each view. - Handles with the permissions for each view (get, put, post, delete, patch, route...) after the request. + This method is responsible for validating the connection and handling the permissions for each view (e.g., get, put, post, delete, patch, route) after the request. - Raises a PermissionDenied exception if not allowed.. + If the connection is not allowed, it raises a PermissionDenied exception. + + Parameters: + - connection: The connection object representing the request. + + Returns: + None + + Raises: + - PermissionDenied: If the connection is not allowed. """ for permission in self.get_permissions(): awaitable: "BasePermission" = cast("BasePermission", await permission()) @@ -623,7 +866,24 @@ async def allow_connection(self, connection: "Connection") -> None: # pragma: n def get_security_schemes(self) -> List["SecurityScheme"]: """ - Returns all security schemes from every level. + Returns a list of all security schemes associated with the handler. + + This method iterates over each parent level of the handler and collects the security schemes defined in each level. + The collected security schemes are stored in a list and returned. + + Returns: + - List[SecurityScheme]: A list of security schemes associated with the handler. + + Example: + >>> handler = Dispatcher() + >>> security_schemes = handler.get_security_schemes() + >>> print(security_schemes) + [SecurityScheme(name='BearerAuth', type='http', scheme='bearer', bearer_format='JWT'), SecurityScheme(name='ApiKeyAuth', type='apiKey', in_='header', name='X-API-Key')] + + Note: + - If no security schemes are defined in any of the parent levels, an empty list will be returned. + - Each security scheme is represented by an instance of the SecurityScheme class. + - The SecurityScheme class has attributes such as name, type, scheme, bearer_format, in_, and name, which provide information about the security scheme. """ security_schemes: List["SecurityScheme"] = [] for layer in self.parent_levels: @@ -632,8 +892,24 @@ def get_security_schemes(self) -> List["SecurityScheme"]: def get_handler_tags(self) -> List[str]: """ - Returns all the tags associated with the handler - by checking the parents as well. + Returns all the tags associated with the handler by checking the parents as well. + + This method retrieves all the tags associated with the handler by iterating over each parent level. + It collects the tags defined in each level and stores them in a list. + + Returns: + - List[str]: A list of tags associated with the handler. + + Example: + >>> handler = Dispatcher() + >>> tags = handler.get_handler_tags() + >>> print(tags) + ['api', 'user'] + + Note: + - If no tags are defined in any of the parent levels, an empty list will be returned. + - Each tag is represented as a string. + - The tags are collected from all parent levels, ensuring that there are no duplicate tags in the final list. """ tags: List[str] = [] for layer in self.parent_levels: @@ -646,11 +922,24 @@ def get_handler_tags(self) -> List[str]: return tags_clean if tags_clean else None - -class BaseInterceptorMixin(BaseHandlerMixin): # pragma: no cover def get_interceptors(self) -> List["AsyncCallable"]: """ - Returns all the interceptors in the handler scope from the ownsership layers. + Returns a list of all the interceptors in the handler scope from the ownership layers. + If the interceptors have not been initialized, it initializes them by collecting interceptors from each parent level. + + Returns: + - List[AsyncCallable]: A list of all the interceptors in the handler scope. + + Example: + >>> handler = Dispatcher() + >>> interceptors = handler.get_interceptors() + >>> print(interceptors) + [, ] + + Note: + - If no interceptors are defined in any of the parent levels, an empty list will be returned. + - Each interceptor is represented by an instance of the AsyncCallable class. + - The AsyncCallable class provides a way to call the interceptor asynchronously. """ if self._interceptors is Void: self._interceptors: Union[List["Interceptor"], "VoidType"] = [] @@ -664,8 +953,27 @@ def get_interceptors(self) -> List["AsyncCallable"]: async def intercept(self, scope: "Scope", receive: "Receive", send: "Send") -> None: """ - Checks for every interceptor on each level and runs them all before reaching any - of the handlers. + Executes all the interceptors in the handler scope before reaching any of the handlers. + + This method iterates over each interceptor in the handler scope and calls the `intercept` method on each of them. + The `intercept` method is responsible for executing the logic of the interceptor. + + Parameters: + - scope (Scope): The scope object representing the current request. + - receive (Receive): The receive channel for receiving messages from the client. + - send (Send): The send channel for sending messages to the client. + + Returns: + None + + Example: + >>> handler = Dispatcher() + >>> await handler.intercept(scope, receive, send) + + Note: + - The `intercept` method is an asynchronous method, hence it needs to be awaited. + - The `intercept` method does not return any value. + - The `intercept` method is responsible for executing the interceptors in the handler scope. """ for interceptor in self.get_interceptors(): awaitable: "EsmeraldInterceptor" = await interceptor() diff --git a/esmerald/routing/gateways.py b/esmerald/routing/gateways.py index 54ff32e7..dc24a8fc 100644 --- a/esmerald/routing/gateways.py +++ b/esmerald/routing/gateways.py @@ -9,7 +9,7 @@ from typing_extensions import Annotated, Doc from esmerald.routing.apis.base import View -from esmerald.routing.base import BaseInterceptorMixin +from esmerald.routing.base import Dispatcher from esmerald.typing import Void, VoidType from esmerald.utils.helpers import clean_string @@ -75,7 +75,7 @@ def generate_operation_id( return operation_id -class Gateway(LilyaPath, BaseInterceptorMixin, BaseMiddleware, GatewayUtil): +class Gateway(LilyaPath, Dispatcher, BaseMiddleware, GatewayUtil): """ `Gateway` object class used by Esmerald routes. @@ -331,7 +331,7 @@ async def handle_dispatch(self, scope: "Scope", receive: "Receive", send: "Send" await self.app(scope, receive, send) -class WebSocketGateway(LilyaWebSocketPath, BaseInterceptorMixin, BaseMiddleware): +class WebSocketGateway(LilyaWebSocketPath, Dispatcher, BaseMiddleware): """ `WebSocketGateway` object class used by Esmerald routes. @@ -529,7 +529,7 @@ async def handle_dispatch(self, scope: "Scope", receive: "Receive", send: "Send" await self.app(scope, receive, send) -class WebhookGateway(LilyaPath, BaseInterceptorMixin, GatewayUtil): +class WebhookGateway(LilyaPath, Dispatcher, GatewayUtil): """ `WebhookGateway` object class used by Esmerald routes. diff --git a/esmerald/routing/router.py b/esmerald/routing/router.py index 8cae9466..826d032c 100644 --- a/esmerald/routing/router.py +++ b/esmerald/routing/router.py @@ -60,7 +60,7 @@ from esmerald.responses import Response from esmerald.routing._internal import OpenAPIFieldInfoMixin from esmerald.routing.apis.base import View -from esmerald.routing.base import BaseInterceptorMixin +from esmerald.routing.base import Dispatcher from esmerald.routing.events import handle_lifespan_events from esmerald.routing.gateways import Gateway, WebhookGateway, WebSocketGateway from esmerald.transformers.model import TransformerModel @@ -997,7 +997,7 @@ async def websocket_route(socket: WebSocket) -> None: self.routes.append(websocket_gateway) -class HTTPHandler(BaseInterceptorMixin, OpenAPIFieldInfoMixin, LilyaPath): +class HTTPHandler(Dispatcher, OpenAPIFieldInfoMixin, LilyaPath): __slots__ = ( "path", "_interceptors", @@ -1319,7 +1319,7 @@ def validate_handler(self) -> None: self.validate_reserved_kwargs() async def to_response(self, app: "Esmerald", data: Any) -> LilyaResponse: - response_handler = self.get_response_handler() + response_handler = self.get_response_for_handler() return await response_handler(app=app, data=data) # type: ignore[call-arg] @@ -1416,7 +1416,7 @@ def __init__( self.path = path -class WebSocketHandler(BaseInterceptorMixin, LilyaWebSocketPath): +class WebSocketHandler(Dispatcher, LilyaWebSocketPath): """ Websocket handler object representation. """