From 64798c42b9b5da3c6cbbb4283aa24cb352e963e4 Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Wed, 22 Nov 2023 15:05:43 +0100 Subject: [PATCH 1/6] chore(event_handler): only apply serialization at the end --- aws_lambda_powertools/event_handler/api_gateway.py | 2 ++ tests/functional/event_handler/test_api_gateway.py | 12 ++++++------ tests/functional/event_handler/test_base_path.py | 12 ++++++------ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 05831a2eea5..01498c41526 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -790,6 +790,8 @@ def build(self, event: ResponseEventT, cors: Optional[CORSConfig] = None) -> Dic logger.debug("Encoding bytes response with base64") self.response.base64_encoded = True self.response.body = base64.b64encode(self.response.body).decode() + elif self.response.is_json(): + self.response.body = self.serializer(self.response.body) # We only apply the serializer when the content type is JSON and the # body is not a str, to avoid double encoding diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index d4c88b541aa..4fe56cf8756 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -368,7 +368,7 @@ def test_override_route_compress_parameter(): # AND the Response object with compress=False app = ApiGatewayResolver() mock_event = {"path": "/my/request", "httpMethod": "GET", "headers": {"Accept-Encoding": "deflate, gzip"}} - expected_value = '{"test": "value"}' + expected_value = {"test": "value"} @app.get("/my/request", compress=True) def with_compression() -> Response: @@ -382,7 +382,7 @@ def handler(event, context): # THEN the response is not compressed assert result["isBase64Encoded"] is False - assert result["body"] == expected_value + assert json.loads(result["body"]) == expected_value assert result["multiValueHeaders"].get("Content-Encoding") is None @@ -682,7 +682,7 @@ def another_one(): def test_no_content_response(): # GIVEN a response with no content-type or body response = Response(status_code=204, content_type=None, body=None, headers=None) - response_builder = ResponseBuilder(response) + response_builder = ResponseBuilder(response, serializer=json.dumps) # WHEN calling to_dict result = response_builder.build(APIGatewayProxyEvent(LOAD_GW_EVENT)) @@ -1528,7 +1528,7 @@ def get_lambda() -> Response: # THEN call the exception_handler assert result["statusCode"] == 500 assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] - assert result["body"] == "CUSTOM ERROR FORMAT" + assert result["body"] == '"CUSTOM ERROR FORMAT"' def test_exception_handler_not_found(): @@ -1824,11 +1824,11 @@ def test_route_match_prioritize_full_match(): @router.get("/my/{path}") def dynamic_handler() -> Response: - return Response(200, content_types.APPLICATION_JSON, json.dumps({"hello": "dynamic"})) + return Response(200, content_types.APPLICATION_JSON, {"hello": "dynamic"}) @router.get("/my/path") def static_handler() -> Response: - return Response(200, content_types.APPLICATION_JSON, json.dumps({"hello": "static"})) + return Response(200, content_types.APPLICATION_JSON, {"hello": "static"}) app.include_router(router) diff --git a/tests/functional/event_handler/test_base_path.py b/tests/functional/event_handler/test_base_path.py index 479a46bda55..adf3c5849df 100644 --- a/tests/functional/event_handler/test_base_path.py +++ b/tests/functional/event_handler/test_base_path.py @@ -21,7 +21,7 @@ def handle(): result = app(event, {}) assert result["statusCode"] == 200 - assert result["body"] == "" + assert result["body"] == '""' def test_base_path_api_gateway_http(): @@ -38,7 +38,7 @@ def handle(): result = app(event, {}) assert result["statusCode"] == 200 - assert result["body"] == "" + assert result["body"] == '""' def test_base_path_alb(): @@ -53,7 +53,7 @@ def handle(): result = app(event, {}) assert result["statusCode"] == 200 - assert result["body"] == "" + assert result["body"] == '""' def test_base_path_lambda_function_url(): @@ -70,7 +70,7 @@ def handle(): result = app(event, {}) assert result["statusCode"] == 200 - assert result["body"] == "" + assert result["body"] == '""' def test_vpc_lattice(): @@ -85,7 +85,7 @@ def handle(): result = app(event, {}) assert result["statusCode"] == 200 - assert result["body"] == "" + assert result["body"] == '""' def test_vpc_latticev2(): @@ -100,4 +100,4 @@ def handle(): result = app(event, {}) assert result["statusCode"] == 200 - assert result["body"] == "" + assert result["body"] == '""' From 8344760eb7ae15bbff128a00562c0217cb99f66b Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Wed, 22 Nov 2023 14:01:51 +0100 Subject: [PATCH 2/6] fix: avoid double encoding --- aws_lambda_powertools/event_handler/api_gateway.py | 5 ++++- tests/functional/event_handler/test_api_gateway.py | 2 +- tests/functional/event_handler/test_base_path.py | 12 ++++++------ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 01498c41526..5ec024b3c61 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -790,7 +790,10 @@ def build(self, event: ResponseEventT, cors: Optional[CORSConfig] = None) -> Dic logger.debug("Encoding bytes response with base64") self.response.base64_encoded = True self.response.body = base64.b64encode(self.response.body).decode() - elif self.response.is_json(): + + # We only apply the serializer when the content type is JSON and the + # body is not a str, to avoid double encoding + elif self.response.is_json() and not isinstance(self.response.body, str): self.response.body = self.serializer(self.response.body) # We only apply the serializer when the content type is JSON and the diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 4fe56cf8756..3cd5e9a65ff 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -1528,7 +1528,7 @@ def get_lambda() -> Response: # THEN call the exception_handler assert result["statusCode"] == 500 assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] - assert result["body"] == '"CUSTOM ERROR FORMAT"' + assert result["body"] == "CUSTOM ERROR FORMAT" def test_exception_handler_not_found(): diff --git a/tests/functional/event_handler/test_base_path.py b/tests/functional/event_handler/test_base_path.py index adf3c5849df..479a46bda55 100644 --- a/tests/functional/event_handler/test_base_path.py +++ b/tests/functional/event_handler/test_base_path.py @@ -21,7 +21,7 @@ def handle(): result = app(event, {}) assert result["statusCode"] == 200 - assert result["body"] == '""' + assert result["body"] == "" def test_base_path_api_gateway_http(): @@ -38,7 +38,7 @@ def handle(): result = app(event, {}) assert result["statusCode"] == 200 - assert result["body"] == '""' + assert result["body"] == "" def test_base_path_alb(): @@ -53,7 +53,7 @@ def handle(): result = app(event, {}) assert result["statusCode"] == 200 - assert result["body"] == '""' + assert result["body"] == "" def test_base_path_lambda_function_url(): @@ -70,7 +70,7 @@ def handle(): result = app(event, {}) assert result["statusCode"] == 200 - assert result["body"] == '""' + assert result["body"] == "" def test_vpc_lattice(): @@ -85,7 +85,7 @@ def handle(): result = app(event, {}) assert result["statusCode"] == 200 - assert result["body"] == '""' + assert result["body"] == "" def test_vpc_latticev2(): @@ -100,4 +100,4 @@ def handle(): result = app(event, {}) assert result["statusCode"] == 200 - assert result["body"] == '""' + assert result["body"] == "" From d9319e1db782bf30d891929a76b0d5b6567f7bb2 Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Wed, 22 Nov 2023 14:06:12 +0100 Subject: [PATCH 3/6] fix: rolled back test changes --- tests/functional/event_handler/test_api_gateway.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 3cd5e9a65ff..8e9bded9c7b 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -368,7 +368,7 @@ def test_override_route_compress_parameter(): # AND the Response object with compress=False app = ApiGatewayResolver() mock_event = {"path": "/my/request", "httpMethod": "GET", "headers": {"Accept-Encoding": "deflate, gzip"}} - expected_value = {"test": "value"} + expected_value = '{"test": "value"}' @app.get("/my/request", compress=True) def with_compression() -> Response: @@ -382,7 +382,7 @@ def handler(event, context): # THEN the response is not compressed assert result["isBase64Encoded"] is False - assert json.loads(result["body"]) == expected_value + assert result["body"] == expected_value assert result["multiValueHeaders"].get("Content-Encoding") is None @@ -1824,11 +1824,11 @@ def test_route_match_prioritize_full_match(): @router.get("/my/{path}") def dynamic_handler() -> Response: - return Response(200, content_types.APPLICATION_JSON, {"hello": "dynamic"}) + return Response(200, content_types.APPLICATION_JSON, json.dumps({"hello": "dynamic"})) @router.get("/my/path") def static_handler() -> Response: - return Response(200, content_types.APPLICATION_JSON, {"hello": "static"}) + return Response(200, content_types.APPLICATION_JSON, json.dumps({"hello": "static"})) app.include_router(router) From 8bdb410078c6a3007190782cf8cf614a2b5bb6b6 Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Wed, 22 Nov 2023 15:11:28 +0100 Subject: [PATCH 4/6] fix: remove code from bad rebase --- aws_lambda_powertools/event_handler/api_gateway.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 5ec024b3c61..05831a2eea5 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -796,11 +796,6 @@ def build(self, event: ResponseEventT, cors: Optional[CORSConfig] = None) -> Dic elif self.response.is_json() and not isinstance(self.response.body, str): self.response.body = self.serializer(self.response.body) - # We only apply the serializer when the content type is JSON and the - # body is not a str, to avoid double encoding - elif self.response.is_json() and not isinstance(self.response.body, str): - self.response.body = self.serializer(self.response.body) - return { "statusCode": self.response.status_code, "body": self.response.body, From add748ac9c78a293c11c80c177e174f54c31277f Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Wed, 22 Nov 2023 15:13:22 +0100 Subject: [PATCH 5/6] fix: remove unused code --- tests/functional/event_handler/test_api_gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 8e9bded9c7b..d4c88b541aa 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -682,7 +682,7 @@ def another_one(): def test_no_content_response(): # GIVEN a response with no content-type or body response = Response(status_code=204, content_type=None, body=None, headers=None) - response_builder = ResponseBuilder(response, serializer=json.dumps) + response_builder = ResponseBuilder(response) # WHEN calling to_dict result = response_builder.build(APIGatewayProxyEvent(LOAD_GW_EVENT)) From 84dccebb25ca68695305fa91d89593c2ea2f33c6 Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Wed, 22 Nov 2023 17:29:34 +0100 Subject: [PATCH 6/6] fix(event_handler): lazy load Pydantic to improve cold start --- aws_lambda_powertools/event_handler/openapi/compat.py | 2 +- aws_lambda_powertools/event_handler/openapi/models.py | 2 +- aws_lambda_powertools/event_handler/openapi/params.py | 3 ++- .../event_handler/openapi/pydantic_loader.py | 6 ++++++ aws_lambda_powertools/event_handler/openapi/types.py | 7 ------- tests/functional/event_handler/test_bedrock_agent.py | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 aws_lambda_powertools/event_handler/openapi/pydantic_loader.py diff --git a/aws_lambda_powertools/event_handler/openapi/compat.py b/aws_lambda_powertools/event_handler/openapi/compat.py index 54b78f7e5f6..bd102aa7b93 100644 --- a/aws_lambda_powertools/event_handler/openapi/compat.py +++ b/aws_lambda_powertools/event_handler/openapi/compat.py @@ -15,9 +15,9 @@ from pydantic import BaseModel, create_model from pydantic.fields import FieldInfo +from aws_lambda_powertools.event_handler.openapi.pydantic_loader import PYDANTIC_V2 from aws_lambda_powertools.event_handler.openapi.types import ( COMPONENT_REF_PREFIX, - PYDANTIC_V2, ModelNameMap, UnionType, ) diff --git a/aws_lambda_powertools/event_handler/openapi/models.py b/aws_lambda_powertools/event_handler/openapi/models.py index bbbc160f1e6..ab97b6dc2e7 100644 --- a/aws_lambda_powertools/event_handler/openapi/models.py +++ b/aws_lambda_powertools/event_handler/openapi/models.py @@ -4,7 +4,7 @@ from pydantic import AnyUrl, BaseModel, Field from aws_lambda_powertools.event_handler.openapi.compat import model_rebuild -from aws_lambda_powertools.event_handler.openapi.types import PYDANTIC_V2 +from aws_lambda_powertools.event_handler.openapi.pydantic_loader import PYDANTIC_V2 from aws_lambda_powertools.shared.types import Annotated, Literal """ diff --git a/aws_lambda_powertools/event_handler/openapi/params.py b/aws_lambda_powertools/event_handler/openapi/params.py index 28154466ff6..f267a4841f5 100644 --- a/aws_lambda_powertools/event_handler/openapi/params.py +++ b/aws_lambda_powertools/event_handler/openapi/params.py @@ -15,7 +15,8 @@ field_annotation_is_scalar, get_annotation_from_field_info, ) -from aws_lambda_powertools.event_handler.openapi.types import PYDANTIC_V2, CacheKey +from aws_lambda_powertools.event_handler.openapi.pydantic_loader import PYDANTIC_V2 +from aws_lambda_powertools.event_handler.openapi.types import CacheKey from aws_lambda_powertools.shared.types import Annotated, Literal, get_args, get_origin """ diff --git a/aws_lambda_powertools/event_handler/openapi/pydantic_loader.py b/aws_lambda_powertools/event_handler/openapi/pydantic_loader.py new file mode 100644 index 00000000000..12f06dad899 --- /dev/null +++ b/aws_lambda_powertools/event_handler/openapi/pydantic_loader.py @@ -0,0 +1,6 @@ +try: + from pydantic.version import VERSION as PYDANTIC_VERSION + + PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.") +except ImportError: + PYDANTIC_V2 = False diff --git a/aws_lambda_powertools/event_handler/openapi/types.py b/aws_lambda_powertools/event_handler/openapi/types.py index 9161d8dc170..0d166de1131 100644 --- a/aws_lambda_powertools/event_handler/openapi/types.py +++ b/aws_lambda_powertools/event_handler/openapi/types.py @@ -16,13 +16,6 @@ COMPONENT_REF_TEMPLATE = "#/components/schemas/{model}" METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"} -try: - from pydantic.version import VERSION as PYDANTIC_VERSION - - PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.") -except ImportError: - PYDANTIC_V2 = False - validation_error_definition = { "title": "ValidationError", diff --git a/tests/functional/event_handler/test_bedrock_agent.py b/tests/functional/event_handler/test_bedrock_agent.py index 223f9bdbef1..df9fb66afc8 100644 --- a/tests/functional/event_handler/test_bedrock_agent.py +++ b/tests/functional/event_handler/test_bedrock_agent.py @@ -2,7 +2,7 @@ from typing import Any, Dict from aws_lambda_powertools.event_handler import BedrockAgentResolver, Response, content_types -from aws_lambda_powertools.event_handler.openapi.types import PYDANTIC_V2 +from aws_lambda_powertools.event_handler.openapi.pydantic_loader import PYDANTIC_V2 from aws_lambda_powertools.utilities.data_classes import BedrockAgentEvent from tests.functional.utils import load_event