From 903c4e80c8738e447e0d1e007bab293ac23df09f Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 28 Dec 2024 00:08:49 +0100 Subject: [PATCH 1/3] fix handling cookies is None in get_cookies Changes: - fix handling cookies is None in get_cookies - Optimize get_cookies - fix wrong example - remove left-over special handling of an empty string, lilya json encoding can do that now --- esmerald/responses/base.py | 9 ++-- esmerald/responses/template.py | 2 +- esmerald/routing/base.py | 79 +++++++++++++++++++--------------- 3 files changed, 48 insertions(+), 42 deletions(-) diff --git a/esmerald/responses/base.py b/esmerald/responses/base.py index 432df843..2bacd09c 100644 --- a/esmerald/responses/base.py +++ b/esmerald/responses/base.py @@ -141,7 +141,7 @@ def __init__( ) ] - Response(response_cookies=response_cookies) + Response(cookies=response_cookies) ``` """ ), @@ -163,12 +163,13 @@ def __init__( encoders=[PydanticEncoder, MsgSpecEncoder] ] - Response(response_cookies=response_cookies) + Response(cookies=response_cookies) ``` """ ), ] = None, ) -> None: + self.cookies = cookies or [] super().__init__( content=content, status_code=status_code, @@ -177,7 +178,6 @@ def __init__( background=cast("BackgroundTask", background), encoders=encoders, ) - self.cookies = cookies or [] def make_response(self, content: Any) -> bytes | memoryview | str: if ( @@ -204,9 +204,6 @@ def make_response(self, content: Any) -> bytes | memoryview | str: try: # switch to a special mode for MediaType.JSON (default handlers) if self.media_type == MediaType.JSON: - # "" should serialize to json - if content == "": - return b'""' # keep it a serialized json object transform_kwargs.setdefault("post_transform_fn", None) # otherwise use default logic of lilya striping '"' diff --git a/esmerald/responses/template.py b/esmerald/responses/template.py index e07f672c..931355ab 100644 --- a/esmerald/responses/template.py +++ b/esmerald/responses/template.py @@ -28,7 +28,7 @@ def __init__( if media_type == MediaType.JSON: # we assume this is the default suffixes = PurePath(template_name).suffixes for suffix in suffixes: - _type = guess_type("name" + suffix)[0] + _type = guess_type(f"name{suffix}")[0] if _type: media_type = _type break diff --git a/esmerald/routing/base.py b/esmerald/routing/base.py index 12879790..80af538e 100644 --- a/esmerald/routing/base.py +++ b/esmerald/routing/base.py @@ -1,8 +1,11 @@ +from __future__ import annotations + from datetime import date, datetime, time, timedelta from decimal import Decimal from enum import Enum from functools import partial from inspect import Signature, isawaitable +from itertools import chain from pathlib import Path from typing import ( TYPE_CHECKING, @@ -54,7 +57,13 @@ from esmerald.permissions import BasePermission from esmerald.permissions.types import Permission from esmerald.routing.router import HTTPHandler - from esmerald.types import APIGateHandler, Dependencies, ResponseCookies, ResponseHeaders + from esmerald.types import ( + APIGateHandler, + Cookie, + Dependencies, + ResponseCookies, + ResponseHeaders, + ) from esmerald.typing import AnyCallable param_type_map = { @@ -74,6 +83,7 @@ T = TypeVar("T", bound="Dispatcher") +_empty: tuple[Any, ...] = () class PathParameterSchema(TypedDict): @@ -131,7 +141,7 @@ def create_signature_model(self, is_websocket: bool = False) -> None: else: self.websocket_parameter_model = transformer_model - def create_handler_transformer_model(self) -> "TransformerModel": + def create_handler_transformer_model(self) -> TransformerModel: """Method to create a TransformerModel for a given handler.""" dependencies = self.get_dependencies() signature_model = get_signature(self) @@ -262,10 +272,10 @@ def _get_default_status_code(self, data: Response) -> int: def _get_response_container_handler( self, - cookies: "ResponseCookies", + cookies: ResponseCookies, headers: Dict[str, Any], media_type: str, - ) -> Callable[[ResponseContainer, Type["Esmerald"], Dict[str, Any]], LilyaResponse]: + ) -> Callable[[ResponseContainer, Type[Esmerald], Dict[str, Any]], LilyaResponse]: """ Creates a handler for ResponseContainer types. @@ -300,7 +310,7 @@ async def response_content( ) def _get_json_response_handler( - self, cookies: "ResponseCookies", headers: Dict[str, Any] + self, cookies: ResponseCookies, headers: Dict[str, Any] ) -> Callable[[Response, Dict[str, Any]], LilyaResponse]: """ Creates a handler function for JSON responses. @@ -314,7 +324,7 @@ def _get_json_response_handler( """ async def response_content(data: Response, **kwargs: Dict[str, Any]) -> LilyaResponse: - _cookies = self.get_cookies(cookies, []) + _cookies = self.get_cookies(cookies) _headers = { **self.get_headers(headers), **data.headers, @@ -334,7 +344,7 @@ async def response_content(data: Response, **kwargs: Dict[str, Any]) -> LilyaRes return cast(Callable[[Response, Dict[str, Any]], LilyaResponse], response_content) def _get_response_handler( - self, cookies: "ResponseCookies", headers: Dict[str, Any], media_type: str + self, cookies: ResponseCookies, headers: Dict[str, Any], media_type: str ) -> Callable[[Response, Dict[str, Any]], LilyaResponse]: """ Creates a handler function for Response types. @@ -372,7 +382,7 @@ async def response_content(data: Response, **kwargs: Dict[str, Any]) -> LilyaRes return cast(Callable[[Response, Dict[str, Any]], LilyaResponse], response_content) def _get_lilya_response_handler( - self, cookies: "ResponseCookies", headers: Dict[str, Any] + self, cookies: ResponseCookies, headers: Dict[str, Any] ) -> Callable[[LilyaResponse, Dict[str, Any]], LilyaResponse]: """ Creates a handler function for Lilya Responses. @@ -386,7 +396,7 @@ def _get_lilya_response_handler( """ async def response_content(data: LilyaResponse, **kwargs: Dict[str, Any]) -> LilyaResponse: - _cookies = self.get_cookies(cookies, []) + _cookies = self.get_cookies(cookies) _headers = { **self.get_headers(headers), **data.headers, @@ -404,7 +414,7 @@ async def response_content(data: LilyaResponse, **kwargs: Dict[str, Any]) -> Lil def _get_default_handler( self, - cookies: "ResponseCookies", + cookies: ResponseCookies, headers: Dict[str, Any], media_type: str, response_class: Any, @@ -424,7 +434,7 @@ def _get_default_handler( async def response_content(data: Any, **kwargs: Dict[str, Any]) -> LilyaResponse: data = await self.get_response_data(data=data) - _cookies = self.get_cookies(cookies, []) + _cookies = self.get_cookies(cookies) if isinstance(data, JSONResponse): response = data response.status_code = self.status_code @@ -677,7 +687,7 @@ def dependency_names(self) -> Set[str]: 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"]: + def get_permissions(self) -> List[AsyncCallable]: """ Returns all the permissions in the handler scope from the ownership layers. @@ -699,7 +709,7 @@ def get_permissions(self) -> List["AsyncCallable"]: - 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"] = [] + self._permissions: Union[List[Permission], VoidType] = [] for layer in self.parent_levels: self._permissions.extend(layer.permissions or []) self._permissions = cast( @@ -708,7 +718,7 @@ def get_permissions(self) -> List["AsyncCallable"]: ) return cast("List[AsyncCallable]", self._permissions) - def get_dependencies(self) -> "Dependencies": + def get_dependencies(self) -> Dependencies: """ Returns all dependencies of the handler function's starting from the parent levels. @@ -737,7 +747,7 @@ def get_dependencies(self) -> "Dependencies": ) if not self._dependencies or self._dependencies is Void: - self._dependencies: "Dependencies" = {} + self._dependencies: Dependencies = {} for level in self.parent_levels: for key, value in (level.dependencies or {}).items(): self.is_unique_dependency( @@ -749,7 +759,7 @@ def get_dependencies(self) -> "Dependencies": return self._dependencies @staticmethod - def is_unique_dependency(dependencies: "Dependencies", key: str, injector: Inject) -> None: + 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. @@ -784,7 +794,9 @@ def is_unique_dependency(dependencies: "Dependencies", key: str, injector: Injec ) def get_cookies( - self, local_cookies: "ResponseCookies", other_cookies: "ResponseCookies" + self, + local_cookies: ResponseCookies | None, + other_cookies: ResponseCookies | None = None, ) -> List[Dict[str, Any]]: # pragma: no cover """ Returns a unique list of cookies. @@ -820,18 +832,15 @@ def get_cookies( This will output the list of normalized cookies. """ - filtered_cookies = [*local_cookies] - for cookie in other_cookies: - if not any(cookie.key == c.key for c in filtered_cookies): - filtered_cookies.append(cookie) - normalized_cookies: List[Dict[str, Any]] = [] - for cookie in filtered_cookies: - normalized_cookies.append( - cookie.model_dump(exclude_none=True, exclude={"description"}) - ) - return normalized_cookies + filtered_cookies: dict[str, Cookie] = {} + for cookie in chain(local_cookies or _empty, other_cookies or _empty): + filtered_cookies.setdefault(cookie.key, cookie) + return [ + cookie.model_dump(exclude_none=True, exclude={"description"}) + for cookie in filtered_cookies.values() + ] - def get_headers(self, headers: "ResponseHeaders") -> Dict[str, Any]: + def get_headers(self, headers: ResponseHeaders) -> Dict[str, Any]: """ Returns a dictionary of response headers. @@ -898,12 +907,12 @@ async def allow_connection(self, connection: "Connection") -> None: # pragma: n - PermissionDenied: If the connection is not allowed. """ for permission in self.get_permissions(): - awaitable: "BasePermission" = cast("BasePermission", await permission()) - request: "Request" = cast("Request", connection) + awaitable: BasePermission = cast("BasePermission", await permission()) + request: Request = cast("Request", connection) handler = cast("APIGateHandler", self) await continue_or_raise_permission_exception(request, handler, awaitable) - def get_security_schemes(self) -> List["SecurityScheme"]: + def get_security_schemes(self) -> List[SecurityScheme]: """ Returns a list of all security schemes associated with the handler. @@ -924,7 +933,7 @@ def get_security_schemes(self) -> List["SecurityScheme"]: - 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"] = [] + security_schemes: List[SecurityScheme] = [] for layer in self.parent_levels: security_schemes.extend(layer.security or []) return security_schemes @@ -961,7 +970,7 @@ def get_handler_tags(self) -> List[str]: return tags_clean if tags_clean else None - def get_interceptors(self) -> List["AsyncCallable"]: + def get_interceptors(self) -> List[AsyncCallable]: """ 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. @@ -981,7 +990,7 @@ def get_interceptors(self) -> List["AsyncCallable"]: - The AsyncCallable class provides a way to call the interceptor asynchronously. """ if self._interceptors is Void: - self._interceptors: Union[List["Interceptor"], "VoidType"] = [] + self._interceptors: Union[List[Interceptor], VoidType] = [] for layer in self.parent_levels: self._interceptors.extend(layer.interceptors or []) self._interceptors = cast( @@ -1015,5 +1024,5 @@ async def intercept(self, scope: "Scope", receive: "Receive", send: "Send") -> N - The `intercept` method is responsible for executing the interceptors in the handler scope. """ for interceptor in self.get_interceptors(): - awaitable: "EsmeraldInterceptor" = await interceptor() + awaitable: EsmeraldInterceptor = await interceptor() await awaitable.intercept(scope, receive, send) From afe9d95dcc9d12fd58adbe0bda7eacb005ca3cd4 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 28 Dec 2024 00:21:17 +0100 Subject: [PATCH 2/3] Changes: - fix release notes - add release note for cookie issue --- docs/en/docs/release-notes.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 68acf6f3..5911e371 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -5,12 +5,6 @@ hide: # Release Notes -## 3.6.3 - -### Fixed - -- SessionConfig has a unneccessarily heavily restricted secret_key parameter. - ## 3.6.2 ### Added @@ -27,6 +21,8 @@ hide: ### Fixed - `bytes` won't be encoded as json when returned from a handler. This would unexpectly lead to a base64 encoding. +- SessionConfig has a unneccessarily heavily restricted secret_key parameter. +- Gracefully handle situations where cookies are None in `get_cookies`. ## 3.6.1 From 59be6899c9fd4f36225fcd444ee047e6a52f8b91 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 28 Dec 2024 00:26:21 +0100 Subject: [PATCH 3/3] bump version --- esmerald/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esmerald/__init__.py b/esmerald/__init__.py index aa20ad5b..f952b1fa 100644 --- a/esmerald/__init__.py +++ b/esmerald/__init__.py @@ -1,4 +1,4 @@ -__version__ = "3.6.1" +__version__ = "3.6.2" from lilya import status