From 0c1f2f0707d97ae22ecf8a2fd62343c5d1df1602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Fri, 5 Aug 2022 10:59:59 +0200 Subject: [PATCH 01/35] feat(event_handler): add support for setting cookies --- .../event_handler/api_gateway.py | 23 ++++++++++- docs/core/event_handler/api_gateway.md | 2 +- .../src/fine_grained_responses.py | 1 + .../src/fine_grained_responses_output.json | 3 +- .../event_handler/test_api_gateway.py | 39 +++++++++++++++++++ .../event_handler/test_lambda_function_url.py | 20 ++++++++++ 6 files changed, 84 insertions(+), 4 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 903fc7e828f..f39a2fe4dae 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -146,6 +146,7 @@ def __init__( content_type: Optional[str], body: Union[str, bytes, None], headers: Optional[Dict] = None, + cookies: Optional[List[str]] = None, ): """ @@ -159,12 +160,15 @@ def __init__( body: Union[str, bytes, None] Optionally set the response body. Note: bytes body will be automatically base64 encoded headers: dict - Optionally set specific http headers. Setting "Content-Type" hear would override the `content_type` value. + Optionally set specific http headers. Setting "Content-Type" here would override the `content_type` value. + cookies: list[str] + Optionally set cookies. """ self.status_code = status_code self.body = body self.base64_encoded = False self.headers: Dict = headers or {} + self.cookies = cookies or [] if content_type: self.headers.setdefault("Content-Type", content_type) @@ -218,6 +222,19 @@ def _route(self, event: BaseProxyEvent, cors: Optional[CORSConfig]): if self.route.compress and "gzip" in (event.get_header_value("accept-encoding", "") or ""): self._compress() + def _format_cookies(self, event: BaseProxyEvent, payload: Dict[str, Any]) -> Dict[str, Any]: + if self.response.cookies: + if isinstance(event, APIGatewayProxyEventV2) or isinstance(event, LambdaFunctionUrlEvent): + payload["cookies"] = self.response.cookies + + if isinstance(event, APIGatewayProxyEvent) or isinstance(event, ALBEvent): + if len(self.response.cookies) == 1: + payload["headers"]["Set-Cookie"] = self.response.cookies[0] + else: + payload["multiValueHeaders"] = {"Set-Cookie": self.response.cookies} + + return payload + def build(self, event: BaseProxyEvent, cors: Optional[CORSConfig] = None) -> Dict[str, Any]: """Build the full response dict to be returned by the lambda""" self._route(event, cors) @@ -226,12 +243,14 @@ def build(self, event: BaseProxyEvent, 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() - return { + + payload = { "statusCode": self.response.status_code, "headers": self.response.headers, "body": self.response.body, "isBase64Encoded": self.response.base64_encoded, } + return self._format_cookies(event, payload) class BaseRouter(ABC): diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index f4f45a051f8..5e9ce37a472 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -312,7 +312,7 @@ For convenience, these are the default values when using `CORSConfig` to enable ### Fine grained responses -You can use the `Response` class to have full control over the response, for example you might want to add additional headers or set a custom Content-type. +You can use the `Response` class to have full control over the response, for example you might want to add additional headers, cookies, or set a custom Content-type. === "fine_grained_responses.py" diff --git a/examples/event_handler_rest/src/fine_grained_responses.py b/examples/event_handler_rest/src/fine_grained_responses.py index 3e477160307..e930aa32713 100644 --- a/examples/event_handler_rest/src/fine_grained_responses.py +++ b/examples/event_handler_rest/src/fine_grained_responses.py @@ -26,6 +26,7 @@ def get_todos(): content_type=content_types.APPLICATION_JSON, body=todos.json()[:10], headers=custom_headers, + cookies=["=; Secure; Expires="], ) diff --git a/examples/event_handler_rest/src/fine_grained_responses_output.json b/examples/event_handler_rest/src/fine_grained_responses_output.json index c3d58098e80..38d642145fa 100644 --- a/examples/event_handler_rest/src/fine_grained_responses_output.json +++ b/examples/event_handler_rest/src/fine_grained_responses_output.json @@ -2,7 +2,8 @@ "statusCode": 200, "headers": { "Content-Type": "application/json", - "X-Transaction-Id": "3490eea9-791b-47a0-91a4-326317db61a9" + "X-Transaction-Id": "3490eea9-791b-47a0-91a4-326317db61a9", + "Set-Cookie": "=; Secure; Expires=", }, "body": "{\"todos\":[{\"userId\":1,\"id\":1,\"title\":\"delectus aut autem\",\"completed\":false},{\"userId\":1,\"id\":2,\"title\":\"quis ut nam facilis et officia qui\",\"completed\":false},{\"userId\":1,\"id\":3,\"title\":\"fugiat veniam minus\",\"completed\":false},{\"userId\":1,\"id\":4,\"title\":\"et porro tempora\",\"completed\":true},{\"userId\":1,\"id\":5,\"title\":\"laboriosam mollitia et enim quasi adipisci quia provident illum\",\"completed\":false},{\"userId\":1,\"id\":6,\"title\":\"qui ullam ratione quibusdam voluptatem quia omnis\",\"completed\":false},{\"userId\":1,\"id\":7,\"title\":\"illo expedita consequatur quia in\",\"completed\":false},{\"userId\":1,\"id\":8,\"title\":\"quo adipisci enim quam ut ab\",\"completed\":true},{\"userId\":1,\"id\":9,\"title\":\"molestiae perspiciatis ipsa\",\"completed\":false},{\"userId\":1,\"id\":10,\"title\":\"illo est ratione doloremque quia maiores aut\",\"completed\":true}]}", "isBase64Encoded": false diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 4b1d7c1ee32..44ef3bd1182 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -93,6 +93,25 @@ def get_lambda() -> Response: # AND set the current_event type as APIGatewayProxyEvent assert result["statusCode"] == 200 assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + assert "multiValueHeaders" not in result["headers"] + + +def test_api_gateway_v1_cookies(): + # GIVEN a Http API V1 proxy type event + app = APIGatewayRestResolver() + + @app.get("/my/path") + def get_lambda() -> Response: + assert isinstance(app.current_event, APIGatewayProxyEvent) + return Response(200, content_types.TEXT_PLAIN, "Hello world", cookies=["CookieMonster"]) + + # WHEN calling the event handler + result = app(LOAD_GW_EVENT, {}) + + # THEN process event correctly + # AND set the current_event type as APIGatewayProxyEvent + assert result["statusCode"] == 200 + assert result["headers"]["multiValueHeaders"]["Set-Cookie"] == ["CookieMonster"] def test_api_gateway(): @@ -132,9 +151,29 @@ def my_path() -> Response: # AND set the current_event type as APIGatewayProxyEventV2 assert result["statusCode"] == 200 assert result["headers"]["Content-Type"] == content_types.TEXT_PLAIN + assert "Cookies" not in result["headers"] assert result["body"] == "tom" +def test_api_gateway_v2_cookies(): + # GIVEN a Http API V2 proxy type event + app = APIGatewayHttpResolver() + + @app.post("/my/path") + def my_path() -> Response: + assert isinstance(app.current_event, APIGatewayProxyEventV2) + return Response(200, content_types.TEXT_PLAIN, "Hello world", cookies=["CookieMonster"]) + + # WHEN calling the event handler + result = app(load_event("apiGatewayProxyV2Event.json"), {}) + + # THEN process event correctly + # AND set the current_event type as APIGatewayProxyEventV2 + assert result["statusCode"] == 200 + assert result["headers"]["Content-Type"] == content_types.TEXT_PLAIN + assert result["headers"]["Cookies"] == ["CookieMonster"] + + def test_include_rule_matching(): # GIVEN app = ApiGatewayResolver() diff --git a/tests/functional/event_handler/test_lambda_function_url.py b/tests/functional/event_handler/test_lambda_function_url.py index dc00c535580..c6160f0e071 100644 --- a/tests/functional/event_handler/test_lambda_function_url.py +++ b/tests/functional/event_handler/test_lambda_function_url.py @@ -21,9 +21,29 @@ def foo(): # AND set the current_event type as LambdaFunctionUrlEvent assert result["statusCode"] == 200 assert result["headers"]["Content-Type"] == content_types.TEXT_HTML + assert "Cookies" not in result["headers"] assert result["body"] == "foo" +def test_lambda_function_url_event_with_cookies(): + # GIVEN a Lambda Function Url type event + app = LambdaFunctionUrlResolver() + + @app.post("/my/path") + def foo(): + assert isinstance(app.current_event, LambdaFunctionUrlEvent) + assert app.lambda_context == {} + return Response(200, content_types.TEXT_PLAIN, "foo", cookies=["CookieMonster"]) + + # WHEN calling the event handler + result = app(load_event("lambdaFunctionUrlEvent.json"), {}) + + # THEN process event correctly + # AND set the current_event type as LambdaFunctionUrlEvent + assert result["statusCode"] == 200 + assert result["headers"]["Cookies"] == ["CookieMonster"] + + def test_lambda_function_url_no_matches(): # GIVEN a Lambda Function Url type event app = LambdaFunctionUrlResolver() From 0d9dae3ae0a49fd9b3e0af1b1730fd0ed6e48f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Mon, 15 Aug 2022 21:04:04 +0200 Subject: [PATCH 02/35] feat(event_handler): format headers and cookies according to the request type --- .../event_handler/api_gateway.py | 26 ++-- .../event_handler/headers_serializer.py | 115 ++++++++++++++++++ 2 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 aws_lambda_powertools/event_handler/headers_serializer.py diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index f39a2fe4dae..bc2d83ab738 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -14,6 +14,7 @@ from aws_lambda_powertools.event_handler import content_types from aws_lambda_powertools.event_handler.exceptions import NotFoundError, ServiceError +from aws_lambda_powertools.event_handler.headers_serializer import HeadersSerializer from aws_lambda_powertools.shared import constants from aws_lambda_powertools.shared.functions import resolve_truthy_env_var_choice from aws_lambda_powertools.shared.json_encoder import Encoder @@ -145,7 +146,7 @@ def __init__( status_code: int, content_type: Optional[str], body: Union[str, bytes, None], - headers: Optional[Dict] = None, + headers: Optional[Dict[str, str]] = None, cookies: Optional[List[str]] = None, ): """ @@ -159,7 +160,7 @@ def __init__( provided http headers body: Union[str, bytes, None] Optionally set the response body. Note: bytes body will be automatically base64 encoded - headers: dict + headers: dict[str, str] Optionally set specific http headers. Setting "Content-Type" here would override the `content_type` value. cookies: list[str] Optionally set cookies. @@ -167,7 +168,7 @@ def __init__( self.status_code = status_code self.body = body self.base64_encoded = False - self.headers: Dict = headers or {} + self.headers: Dict[str, str] = headers or {} self.cookies = cookies or [] if content_type: self.headers.setdefault("Content-Type", content_type) @@ -222,19 +223,6 @@ def _route(self, event: BaseProxyEvent, cors: Optional[CORSConfig]): if self.route.compress and "gzip" in (event.get_header_value("accept-encoding", "") or ""): self._compress() - def _format_cookies(self, event: BaseProxyEvent, payload: Dict[str, Any]) -> Dict[str, Any]: - if self.response.cookies: - if isinstance(event, APIGatewayProxyEventV2) or isinstance(event, LambdaFunctionUrlEvent): - payload["cookies"] = self.response.cookies - - if isinstance(event, APIGatewayProxyEvent) or isinstance(event, ALBEvent): - if len(self.response.cookies) == 1: - payload["headers"]["Set-Cookie"] = self.response.cookies[0] - else: - payload["multiValueHeaders"] = {"Set-Cookie": self.response.cookies} - - return payload - def build(self, event: BaseProxyEvent, cors: Optional[CORSConfig] = None) -> Dict[str, Any]: """Build the full response dict to be returned by the lambda""" self._route(event, cors) @@ -246,11 +234,13 @@ def build(self, event: BaseProxyEvent, cors: Optional[CORSConfig] = None) -> Dic payload = { "statusCode": self.response.status_code, - "headers": self.response.headers, "body": self.response.body, "isBase64Encoded": self.response.base64_encoded, } - return self._format_cookies(event, payload) + payload.update( + HeadersSerializer(event=event, cookies=self.response.cookies, headers=self.response.headers).serialize() + ) + return payload class BaseRouter(ABC): diff --git a/aws_lambda_powertools/event_handler/headers_serializer.py b/aws_lambda_powertools/event_handler/headers_serializer.py new file mode 100644 index 00000000000..3806970ab5b --- /dev/null +++ b/aws_lambda_powertools/event_handler/headers_serializer.py @@ -0,0 +1,115 @@ +import warnings +from typing import Any, Dict, List + +from aws_lambda_powertools.utilities.data_classes import ( + ALBEvent, + APIGatewayProxyEvent, + APIGatewayProxyEventV2, + LambdaFunctionUrlEvent, +) +from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent + + +class HeadersSerializer: + """ + Helper class to correctly serialize headers and cookies on the response payload. + """ + + def __init__(self, event: BaseProxyEvent, cookies: List[str], headers: Dict[str, str]): + """ + + Parameters + ---------- + event: BaseProxyEvent + The request event, used to derive the response format + cookies: List[str] + A list of cookies to set in the response + headers: Dict[str, str] + A dictionary of headers to set in the response + """ + self.event = event + self.cookies = cookies + self.headers = headers + + def serialize(self) -> Dict[str, Any]: + """ + Serializes headers and cookies according to the request type. + Returns a dict that can be merged with the response payload. + """ + payload: Dict[str, Any] = {} + + if isinstance(self.event, APIGatewayProxyEventV2) or isinstance(self.event, LambdaFunctionUrlEvent): + """ + When using HTTP APIs or LambdaFunctionURLs, everything is taken care automatically. + One can directly assign a list of cookies and a dict of headers to the response payload, and the + runtime will automatically serialize them correctly on the output. + + https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.proxy-format + https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.response + """ + payload["cookies"] = self.cookies + payload["headers"] = self.headers + + if isinstance(self.event, APIGatewayProxyEvent): + """ + When using REST APIs, headers can be encoded using the "multiValueHeaders" key on the response. + This will cover both single and multi-value headers. + + https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format + """ + + payload["multiValueHeaders"] = self._build_multivalue_headers() + + if isinstance(self.event, ALBEvent): + """ + The ALB integration can work with multiValueHeaders disabled (default) or enabled. + We can detect if the feature is enabled by looking for the presence of `multiValueHeaders` in the request. + If the feature is disabled, and we try to set multiple headers with the same key, or more than one cookie, + print a warning. + + + https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#respond-to-load-balancer + https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers-response + """ + if self.event.multi_value_headers is not None: + payload["multiValueHeaders"] = self._build_multivalue_headers() + else: + payload.setdefault("headers", {}) + + if self.cookies: + if len(self.cookies) > 1: + warnings.warn( + "Can't encode more than one cookie in the response. " + "Did you enable multiValueHeaders on the ALB Target Group?" + ) + + # We can only send one cookie, send the last one + payload["headers"]["Set-Cookie"] = self.cookies[-1] + + for key, value in self.headers.items(): + values = value.split(",") + if len(values) > 1: + warnings.warn( + "Can't encode more than on header with the same key in the response. " + "Did you enable multiValueHeaders on the ALB Target Group?" + ) + + # We can only send on header for this key, send the last value + payload["headers"][key] = values[-1].strip() + + return payload + + def _build_multivalue_headers(self) -> Dict[str, List[str]]: + """Formats headers and cookies according to the `multiValueHeader` format""" + multi_value_headers: Dict[str, List[str]] = {} + + for key, value in self.headers.items(): + values = value.split(",") + multi_value_headers[key] = [value.strip() for value in values] + + if self.cookies: + multi_value_headers.setdefault("Set-Cookie", []) + for cookie in self.cookies: + multi_value_headers["Set-Cookie"].append(cookie) + + return multi_value_headers From e28c8ca77d7b24ddd5e9dc31044b312ce60493ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Mon, 15 Aug 2022 21:04:42 +0200 Subject: [PATCH 03/35] chore(tests): move load_event helper to global utils --- tests/functional/data_classes/test_amazon_mq.py | 2 +- tests/functional/data_classes/test_lambda_function_url.py | 2 +- tests/functional/event_handler/test_api_gateway.py | 2 +- tests/functional/event_handler/test_appsync.py | 2 +- tests/functional/event_handler/test_lambda_function_url.py | 2 +- tests/functional/idempotency/conftest.py | 3 ++- tests/functional/idempotency/test_idempotency.py | 3 ++- tests/functional/parser/test_alb.py | 2 +- tests/functional/parser/test_apigw.py | 2 +- tests/functional/parser/test_apigwv2.py | 2 +- tests/functional/parser/test_cloudwatch.py | 2 +- tests/functional/parser/test_dynamodb.py | 2 +- tests/functional/parser/test_eventbridge.py | 2 +- tests/functional/parser/test_kinesis.py | 2 +- tests/functional/parser/test_s3 object_event.py | 2 +- tests/functional/parser/test_s3.py | 2 +- tests/functional/parser/test_ses.py | 2 +- tests/functional/parser/test_sns.py | 2 +- tests/functional/parser/test_sqs.py | 2 +- tests/functional/test_data_classes.py | 2 +- tests/functional/utils.py | 7 ------- 21 files changed, 22 insertions(+), 27 deletions(-) diff --git a/tests/functional/data_classes/test_amazon_mq.py b/tests/functional/data_classes/test_amazon_mq.py index a88a962c17b..9e21aa10ebf 100644 --- a/tests/functional/data_classes/test_amazon_mq.py +++ b/tests/functional/data_classes/test_amazon_mq.py @@ -2,7 +2,7 @@ from aws_lambda_powertools.utilities.data_classes.active_mq_event import ActiveMQEvent, ActiveMQMessage from aws_lambda_powertools.utilities.data_classes.rabbit_mq_event import BasicProperties, RabbitMessage, RabbitMQEvent -from tests.functional.utils import load_event +from tests.utils import load_event def test_active_mq_event(): diff --git a/tests/functional/data_classes/test_lambda_function_url.py b/tests/functional/data_classes/test_lambda_function_url.py index c27920c3392..ffc215a99ff 100644 --- a/tests/functional/data_classes/test_lambda_function_url.py +++ b/tests/functional/data_classes/test_lambda_function_url.py @@ -1,5 +1,5 @@ from aws_lambda_powertools.utilities.data_classes import LambdaFunctionUrlEvent -from tests.functional.utils import load_event +from tests.utils import load_event def test_lambda_function_url_event(): diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 44ef3bd1182..d88bc7083dc 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -37,7 +37,7 @@ APIGatewayProxyEventV2, event_source, ) -from tests.functional.utils import load_event +from tests.utils import load_event @pytest.fixture diff --git a/tests/functional/event_handler/test_appsync.py b/tests/functional/event_handler/test_appsync.py index 79173e55825..329ac9ee985 100644 --- a/tests/functional/event_handler/test_appsync.py +++ b/tests/functional/event_handler/test_appsync.py @@ -7,7 +7,7 @@ from aws_lambda_powertools.event_handler.appsync import Router from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.functional.utils import load_event +from tests.utils import load_event def test_direct_resolver(): diff --git a/tests/functional/event_handler/test_lambda_function_url.py b/tests/functional/event_handler/test_lambda_function_url.py index c6160f0e071..22247e32ed7 100644 --- a/tests/functional/event_handler/test_lambda_function_url.py +++ b/tests/functional/event_handler/test_lambda_function_url.py @@ -1,6 +1,6 @@ from aws_lambda_powertools.event_handler import LambdaFunctionUrlResolver, Response, content_types from aws_lambda_powertools.utilities.data_classes import LambdaFunctionUrlEvent -from tests.functional.utils import load_event +from tests.utils import load_event def test_lambda_function_url_event(): diff --git a/tests/functional/idempotency/conftest.py b/tests/functional/idempotency/conftest.py index b5cf79727b1..328900cda07 100644 --- a/tests/functional/idempotency/conftest.py +++ b/tests/functional/idempotency/conftest.py @@ -14,7 +14,8 @@ from aws_lambda_powertools.utilities.jmespath_utils import extract_data_from_envelope from aws_lambda_powertools.utilities.validation import envelopes from tests.functional.idempotency.utils import hash_idempotency_key -from tests.functional.utils import json_serialize, load_event +from tests.functional.utils import json_serialize +from tests.utils import load_event TABLE_NAME = "TEST_TABLE" diff --git a/tests/functional/idempotency/test_idempotency.py b/tests/functional/idempotency/test_idempotency.py index 97a9166efa0..3fd8ee619fc 100644 --- a/tests/functional/idempotency/test_idempotency.py +++ b/tests/functional/idempotency/test_idempotency.py @@ -29,7 +29,8 @@ build_idempotency_update_item_stub, hash_idempotency_key, ) -from tests.functional.utils import json_serialize, load_event +from tests.functional.utils import json_serialize +from tests.utils import load_event TABLE_NAME = "TEST_TABLE" diff --git a/tests/functional/parser/test_alb.py b/tests/functional/parser/test_alb.py index d48e39f1bab..c03066c140f 100644 --- a/tests/functional/parser/test_alb.py +++ b/tests/functional/parser/test_alb.py @@ -3,7 +3,7 @@ from aws_lambda_powertools.utilities.parser import ValidationError, event_parser from aws_lambda_powertools.utilities.parser.models import AlbModel from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=AlbModel) diff --git a/tests/functional/parser/test_apigw.py b/tests/functional/parser/test_apigw.py index 35b2fdb1926..8e08869d9f3 100644 --- a/tests/functional/parser/test_apigw.py +++ b/tests/functional/parser/test_apigw.py @@ -5,7 +5,7 @@ from aws_lambda_powertools.utilities.parser.models import APIGatewayProxyEventModel from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyApiGatewayBusiness -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=MyApiGatewayBusiness, envelope=envelopes.ApiGatewayEnvelope) diff --git a/tests/functional/parser/test_apigwv2.py b/tests/functional/parser/test_apigwv2.py index d3510b185dd..676fe4bcf5d 100644 --- a/tests/functional/parser/test_apigwv2.py +++ b/tests/functional/parser/test_apigwv2.py @@ -6,7 +6,7 @@ ) from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyApiGatewayBusiness -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=MyApiGatewayBusiness, envelope=envelopes.ApiGatewayV2Envelope) diff --git a/tests/functional/parser/test_cloudwatch.py b/tests/functional/parser/test_cloudwatch.py index 7290d0bffcb..8ea2441100f 100644 --- a/tests/functional/parser/test_cloudwatch.py +++ b/tests/functional/parser/test_cloudwatch.py @@ -9,7 +9,7 @@ from aws_lambda_powertools.utilities.parser.models import CloudWatchLogsLogEvent, CloudWatchLogsModel from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyCloudWatchBusiness -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=MyCloudWatchBusiness, envelope=envelopes.CloudWatchLogsEnvelope) diff --git a/tests/functional/parser/test_dynamodb.py b/tests/functional/parser/test_dynamodb.py index 9917fac234b..c3dfee65db3 100644 --- a/tests/functional/parser/test_dynamodb.py +++ b/tests/functional/parser/test_dynamodb.py @@ -5,7 +5,7 @@ from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, event_parser from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyAdvancedDynamoBusiness, MyDynamoBusiness -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=MyDynamoBusiness, envelope=envelopes.DynamoDBStreamEnvelope) diff --git a/tests/functional/parser/test_eventbridge.py b/tests/functional/parser/test_eventbridge.py index 6242403ab35..93188a5c59f 100644 --- a/tests/functional/parser/test_eventbridge.py +++ b/tests/functional/parser/test_eventbridge.py @@ -5,7 +5,7 @@ from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, event_parser from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyAdvancedEventbridgeBusiness, MyEventbridgeBusiness -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=MyEventbridgeBusiness, envelope=envelopes.EventBridgeEnvelope) diff --git a/tests/functional/parser/test_kinesis.py b/tests/functional/parser/test_kinesis.py index 552cb6cef68..a9aa59dd9a6 100644 --- a/tests/functional/parser/test_kinesis.py +++ b/tests/functional/parser/test_kinesis.py @@ -6,7 +6,7 @@ from aws_lambda_powertools.utilities.parser.models import KinesisDataStreamModel, KinesisDataStreamRecordPayload from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyKinesisBusiness -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=MyKinesisBusiness, envelope=envelopes.KinesisDataStreamEnvelope) diff --git a/tests/functional/parser/test_s3 object_event.py b/tests/functional/parser/test_s3 object_event.py index 90c2555360d..03ecf227139 100644 --- a/tests/functional/parser/test_s3 object_event.py +++ b/tests/functional/parser/test_s3 object_event.py @@ -1,7 +1,7 @@ from aws_lambda_powertools.utilities.parser import event_parser from aws_lambda_powertools.utilities.parser.models import S3ObjectLambdaEvent from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=S3ObjectLambdaEvent) diff --git a/tests/functional/parser/test_s3.py b/tests/functional/parser/test_s3.py index 71a5dc6afe3..77e0adb7b4a 100644 --- a/tests/functional/parser/test_s3.py +++ b/tests/functional/parser/test_s3.py @@ -1,7 +1,7 @@ from aws_lambda_powertools.utilities.parser import event_parser, parse from aws_lambda_powertools.utilities.parser.models import S3Model, S3RecordModel from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=S3Model) diff --git a/tests/functional/parser/test_ses.py b/tests/functional/parser/test_ses.py index d434e2350f8..09f6ff6b76d 100644 --- a/tests/functional/parser/test_ses.py +++ b/tests/functional/parser/test_ses.py @@ -1,7 +1,7 @@ from aws_lambda_powertools.utilities.parser import event_parser from aws_lambda_powertools.utilities.parser.models import SesModel, SesRecordModel from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=SesModel) diff --git a/tests/functional/parser/test_sns.py b/tests/functional/parser/test_sns.py index b0d9ff69a9b..992871126bc 100644 --- a/tests/functional/parser/test_sns.py +++ b/tests/functional/parser/test_sns.py @@ -6,8 +6,8 @@ from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, event_parser from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyAdvancedSnsBusiness, MySnsBusiness -from tests.functional.utils import load_event from tests.functional.validator.conftest import sns_event # noqa: F401 +from tests.utils import load_event @event_parser(model=MySnsBusiness, envelope=envelopes.SnsEnvelope) diff --git a/tests/functional/parser/test_sqs.py b/tests/functional/parser/test_sqs.py index 7ca883616f2..93b2fca5d6c 100644 --- a/tests/functional/parser/test_sqs.py +++ b/tests/functional/parser/test_sqs.py @@ -5,8 +5,8 @@ from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, event_parser from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyAdvancedSqsBusiness, MySqsBusiness -from tests.functional.utils import load_event from tests.functional.validator.conftest import sqs_event # noqa: F401 +from tests.utils import load_event @event_parser(model=MySqsBusiness, envelope=envelopes.SqsEnvelope) diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py index 8a87075d16c..fe543db34cb 100644 --- a/tests/functional/test_data_classes.py +++ b/tests/functional/test_data_classes.py @@ -76,7 +76,7 @@ ) from aws_lambda_powertools.utilities.data_classes.event_source import event_source from aws_lambda_powertools.utilities.data_classes.s3_object_event import S3ObjectLambdaEvent -from tests.functional.utils import load_event +from tests.utils import load_event def test_dict_wrapper_equals(): diff --git a/tests/functional/utils.py b/tests/functional/utils.py index 6b73053e0d0..c07f4fe32cc 100644 --- a/tests/functional/utils.py +++ b/tests/functional/utils.py @@ -1,16 +1,9 @@ import base64 import json -from pathlib import Path -from typing import Any from aws_lambda_powertools.shared.json_encoder import Encoder -def load_event(file_name: str) -> Any: - path = Path(str(Path(__file__).parent.parent) + "/events/" + file_name) - return json.loads(path.read_text()) - - def str_to_b64(data: str) -> str: return base64.b64encode(data.encode()).decode("utf-8") From 38f1d925d7bd9579ad5c37f659a15dec1911720d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Mon, 15 Aug 2022 21:06:32 +0200 Subject: [PATCH 04/35] chore(tests): add tests for header serializer --- tests/events/albMultiValueHeadersEvent.json | 35 ++++++ tests/unit/event_handler/__init__.py | 0 .../event_handler/test_headers_serializer.py | 106 ++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 tests/events/albMultiValueHeadersEvent.json create mode 100644 tests/unit/event_handler/__init__.py create mode 100644 tests/unit/event_handler/test_headers_serializer.py diff --git a/tests/events/albMultiValueHeadersEvent.json b/tests/events/albMultiValueHeadersEvent.json new file mode 100644 index 00000000000..6b34709605c --- /dev/null +++ b/tests/events/albMultiValueHeadersEvent.json @@ -0,0 +1,35 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:eu-central-1:1234567890:targetgroup/alb-c-Targe-11GDXTPQ7663S/804a67588bfdc10f" + } + }, + "httpMethod": "GET", + "path": "/todos", + "multiValueQueryStringParameters": {}, + "multiValueHeaders": { + "accept": [ + "*/*" + ], + "host": [ + "alb-c-LoadB-14POFKYCLBNSF-1815800096.eu-central-1.elb.amazonaws.com" + ], + "user-agent": [ + "curl/7.79.1" + ], + "x-amzn-trace-id": [ + "Root=1-62fa9327-21cdd4da4c6db451490a5fb7" + ], + "x-forwarded-for": [ + "123.123.123.123" + ], + "x-forwarded-port": [ + "80" + ], + "x-forwarded-proto": [ + "http" + ] + }, + "body": "", + "isBase64Encoded": false +} diff --git a/tests/unit/event_handler/__init__.py b/tests/unit/event_handler/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit/event_handler/test_headers_serializer.py b/tests/unit/event_handler/test_headers_serializer.py new file mode 100644 index 00000000000..592ee7f4c4a --- /dev/null +++ b/tests/unit/event_handler/test_headers_serializer.py @@ -0,0 +1,106 @@ +import warnings + +from aws_lambda_powertools.event_handler.headers_serializer import HeadersSerializer +from aws_lambda_powertools.utilities.data_classes import ( + ALBEvent, + APIGatewayProxyEvent, + APIGatewayProxyEventV2, + LambdaFunctionUrlEvent, +) +from tests.utils import load_event + + +def test_headers_serializer_apigatewayv2(): + event = APIGatewayProxyEventV2(load_event("apigatewayproxyv2event.json")) + + builder = HeadersSerializer(event=event, cookies=[], headers={}) + assert builder.serialize() == {"cookies": [], "headers": {}} + + builder = HeadersSerializer(event=event, cookies=[], headers={"Content-Type": "text/html"}) + assert builder.serialize() == {"cookies": [], "headers": {"Content-Type": "text/html"}} + + builder = HeadersSerializer(event=event, cookies=["UUID=12345"], headers={}) + assert builder.serialize() == {"cookies": ["UUID=12345"], "headers": {}} + + builder = HeadersSerializer(event=event, cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": "bar,zbr"}) + assert builder.serialize() == {"cookies": ["UUID=12345", "SSID=0xdeadbeef"], "headers": {"Foo": "bar,zbr"}} + + +def test_headers_serializer_lambdafunctionurl(): + event = LambdaFunctionUrlEvent(load_event("lambdaFunctionUrlEvent.json")) + + builder = HeadersSerializer(event=event, cookies=[], headers={}) + assert builder.serialize() == {"cookies": [], "headers": {}} + + builder = HeadersSerializer(event=event, cookies=[], headers={"Content-Type": "text/html"}) + assert builder.serialize() == {"cookies": [], "headers": {"Content-Type": "text/html"}} + + builder = HeadersSerializer(event=event, cookies=["UUID=12345"], headers={}) + assert builder.serialize() == {"cookies": ["UUID=12345"], "headers": {}} + + builder = HeadersSerializer(event=event, cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": "bar,zbr"}) + assert builder.serialize() == {"cookies": ["UUID=12345", "SSID=0xdeadbeef"], "headers": {"Foo": "bar,zbr"}} + + +def test_headers_serializer_apigateway(): + event = APIGatewayProxyEvent(load_event("apiGatewayProxyEvent.json")) + + builder = HeadersSerializer(event=event, cookies=[], headers={}) + assert builder.serialize() == {"multiValueHeaders": {}} + + builder = HeadersSerializer(event=event, cookies=[], headers={"Content-Type": "text/html"}) + assert builder.serialize() == {"multiValueHeaders": {"Content-Type": ["text/html"]}} + + builder = HeadersSerializer(event=event, cookies=["UUID=12345"], headers={}) + assert builder.serialize() == {"multiValueHeaders": {"Set-Cookie": ["UUID=12345"]}} + + builder = HeadersSerializer(event=event, cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": "bar,zbr"}) + assert builder.serialize() == { + "multiValueHeaders": {"Set-Cookie": ["UUID=12345", "SSID=0xdeadbeef"], "Foo": ["bar", "zbr"]} + } + + +def test_headers_serializer_alb_without_multi_headers_noop(): + event = ALBEvent(load_event("albEvent.json")) + + builder = HeadersSerializer(event=event, cookies=[], headers={}) + assert builder.serialize() == {"headers": {}} + + builder = HeadersSerializer(event=event, cookies=[], headers={"Content-Type": "text/html"}) + assert builder.serialize() == {"headers": {"Content-Type": "text/html"}} + + builder = HeadersSerializer(event=event, cookies=["UUID=12345"], headers={}) + assert builder.serialize() == {"headers": {"Set-Cookie": "UUID=12345"}} + + builder = HeadersSerializer(event=event, cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": "bar,zbr"}) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("default") + assert builder.serialize() == {"headers": {"Set-Cookie": "SSID=0xdeadbeef", "Foo": "zbr"}} + + assert len(w) == 2 + assert str(w[-2].message) == ( + "Can't encode more than one cookie in the response. " + "Did you enable multiValueHeaders on the ALB Target Group?" + ) + assert str(w[-1].message) == ( + "Can't encode more than on header with the same key in the response. " + "Did you enable multiValueHeaders on the ALB Target Group?" + ) + + +def test_headers_serializer_alb_with_multi_headers_noop(): + event = ALBEvent(load_event("albMultiValueHeadersEvent.json")) + + builder = HeadersSerializer(event=event, cookies=[], headers={}) + assert builder.serialize() == {"multiValueHeaders": {}} + + builder = HeadersSerializer(event=event, cookies=[], headers={"Content-Type": "text/html"}) + assert builder.serialize() == {"multiValueHeaders": {"Content-Type": ["text/html"]}} + + builder = HeadersSerializer(event=event, cookies=["UUID=12345"], headers={}) + assert builder.serialize() == {"multiValueHeaders": {"Set-Cookie": ["UUID=12345"]}} + + builder = HeadersSerializer(event=event, cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": "bar,zbr"}) + assert builder.serialize() == { + "multiValueHeaders": {"Set-Cookie": ["UUID=12345", "SSID=0xdeadbeef"], "Foo": ["bar", "zbr"]} + } From 8178362956e7a08d87b598ccafba9b7103cff742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Mon, 15 Aug 2022 21:42:27 +0200 Subject: [PATCH 05/35] chore(tests): fix all tests --- .../event_handler/test_api_gateway.py | 131 +++++++++--------- .../event_handler/test_lambda_function_url.py | 4 +- 2 files changed, 67 insertions(+), 68 deletions(-) diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index d88bc7083dc..49d95e063ee 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -92,8 +92,7 @@ def get_lambda() -> Response: # THEN process event correctly # AND set the current_event type as APIGatewayProxyEvent assert result["statusCode"] == 200 - assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON - assert "multiValueHeaders" not in result["headers"] + assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] def test_api_gateway_v1_cookies(): @@ -111,7 +110,7 @@ def get_lambda() -> Response: # THEN process event correctly # AND set the current_event type as APIGatewayProxyEvent assert result["statusCode"] == 200 - assert result["headers"]["multiValueHeaders"]["Set-Cookie"] == ["CookieMonster"] + assert result["multiValueHeaders"]["Set-Cookie"] == ["CookieMonster"] def test_api_gateway(): @@ -129,7 +128,7 @@ def get_lambda() -> Response: # THEN process event correctly # AND set the current_event type as APIGatewayProxyEvent assert result["statusCode"] == 200 - assert result["headers"]["Content-Type"] == content_types.TEXT_HTML + assert result["multiValueHeaders"]["Content-Type"] == [content_types.TEXT_HTML] assert result["body"] == "foo" @@ -171,7 +170,7 @@ def my_path() -> Response: # AND set the current_event type as APIGatewayProxyEventV2 assert result["statusCode"] == 200 assert result["headers"]["Content-Type"] == content_types.TEXT_PLAIN - assert result["headers"]["Cookies"] == ["CookieMonster"] + assert result["cookies"] == ["CookieMonster"] def test_include_rule_matching(): @@ -188,7 +187,7 @@ def get_lambda(my_id: str, name: str) -> Response: # THEN assert result["statusCode"] == 200 - assert result["headers"]["Content-Type"] == content_types.TEXT_HTML + assert result["multiValueHeaders"]["Content-Type"] == [content_types.TEXT_HTML] assert result["body"] == "path" @@ -239,7 +238,7 @@ def handler(event, context): result = handler(LOAD_GW_EVENT, None) assert result["statusCode"] == 404 # AND cors headers are not returned - assert "Access-Control-Allow-Origin" not in result["headers"] + assert "Access-Control-Allow-Origin" not in result["multiValueHeaders"] def test_cors(): @@ -262,17 +261,17 @@ def handler(event, context): result = handler(LOAD_GW_EVENT, None) # THEN the headers should include cors headers - assert "headers" in result - headers = result["headers"] - assert headers["Content-Type"] == content_types.TEXT_HTML - assert headers["Access-Control-Allow-Origin"] == "*" + assert "multiValueHeaders" in result + headers = result["multiValueHeaders"] + assert headers["Content-Type"] == [content_types.TEXT_HTML] + assert headers["Access-Control-Allow-Origin"] == ["*"] assert "Access-Control-Allow-Credentials" not in headers - assert headers["Access-Control-Allow-Headers"] == ",".join(sorted(CORSConfig._REQUIRED_HEADERS)) + assert headers["Access-Control-Allow-Headers"] == sorted(CORSConfig._REQUIRED_HEADERS) # THEN for routes without cors flag return no cors headers mock_event = {"path": "/my/request", "httpMethod": "GET"} result = handler(mock_event, None) - assert "Access-Control-Allow-Origin" not in result["headers"] + assert "Access-Control-Allow-Origin" not in result["multiValueHeaders"] def test_cors_preflight_body_is_empty_not_null(): @@ -311,8 +310,8 @@ def handler(event, context): assert isinstance(body, str) decompress = zlib.decompress(base64.b64decode(body), wbits=zlib.MAX_WBITS | 16).decode("UTF-8") assert decompress == expected_value - headers = result["headers"] - assert headers["Content-Encoding"] == "gzip" + headers = result["multiValueHeaders"] + assert headers["Content-Encoding"] == ["gzip"] def test_base64_encode(): @@ -331,8 +330,8 @@ def read_image() -> Response: assert result["isBase64Encoded"] is True body = result["body"] assert isinstance(body, str) - headers = result["headers"] - assert headers["Content-Encoding"] == "gzip" + headers = result["multiValueHeaders"] + assert headers["Content-Encoding"] == ["gzip"] def test_compress_no_accept_encoding(): @@ -387,9 +386,9 @@ def handler(event, context): result = handler({"path": "/success", "httpMethod": "GET"}, None) # THEN return the set Cache-Control - headers = result["headers"] - assert headers["Content-Type"] == content_types.TEXT_HTML - assert headers["Cache-Control"] == "max-age=600" + headers = result["multiValueHeaders"] + assert headers["Content-Type"] == [content_types.TEXT_HTML] + assert headers["Cache-Control"] == ["max-age=600"] def test_cache_control_non_200(): @@ -408,9 +407,9 @@ def handler(event, context): result = handler({"path": "/fails", "httpMethod": "DELETE"}, None) # THEN return a Cache-Control of "no-cache" - headers = result["headers"] - assert headers["Content-Type"] == content_types.TEXT_HTML - assert headers["Cache-Control"] == "no-cache" + headers = result["multiValueHeaders"] + assert headers["Content-Type"] == [content_types.TEXT_HTML] + assert headers["Cache-Control"] == ["no-cache"] def test_rest_api(): @@ -427,7 +426,7 @@ def rest_func() -> Dict: # THEN automatically process this as a json rest api response assert result["statusCode"] == 200 - assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] expected_str = json.dumps(expected_dict, separators=(",", ":"), indent=None, cls=Encoder) assert result["body"] == expected_str @@ -450,8 +449,8 @@ def rest_func() -> Response: # THEN the result can include some additional field control like overriding http headers assert result["statusCode"] == 404 - assert result["headers"]["Content-Type"] == "header-content-type-wins" - assert result["headers"]["custom"] == "value" + assert result["multiValueHeaders"]["Content-Type"] == ["header-content-type-wins"] + assert result["multiValueHeaders"]["custom"] == ["value"] assert result["body"] == "Not found" @@ -480,16 +479,16 @@ def another_one(): result = app(event, None) # THEN routes by default return the custom cors headers - assert "headers" in result - headers = result["headers"] - assert headers["Content-Type"] == content_types.APPLICATION_JSON - assert headers["Access-Control-Allow-Origin"] == cors_config.allow_origin - expected_allows_headers = ",".join(sorted(set(allow_header + cors_config._REQUIRED_HEADERS))) + assert "multiValueHeaders" in result + headers = result["multiValueHeaders"] + assert headers["Content-Type"] == [content_types.APPLICATION_JSON] + assert headers["Access-Control-Allow-Origin"] == [cors_config.allow_origin] + expected_allows_headers = sorted(set(allow_header + cors_config._REQUIRED_HEADERS)) assert headers["Access-Control-Allow-Headers"] == expected_allows_headers - assert headers["Access-Control-Expose-Headers"] == ",".join(cors_config.expose_headers) - assert headers["Access-Control-Max-Age"] == str(cors_config.max_age) + assert headers["Access-Control-Expose-Headers"] == cors_config.expose_headers + assert headers["Access-Control-Max-Age"] == [str(cors_config.max_age)] assert "Access-Control-Allow-Credentials" in headers - assert headers["Access-Control-Allow-Credentials"] == "true" + assert headers["Access-Control-Allow-Credentials"] == ["true"] # AND custom cors was set on the app assert isinstance(app._cors, CORSConfig) @@ -498,7 +497,7 @@ def another_one(): # AND routes without cors don't include "Access-Control" headers event = {"path": "/another-one", "httpMethod": "GET"} result = app(event, None) - headers = result["headers"] + headers = result["multiValueHeaders"] assert "Access-Control-Allow-Origin" not in headers @@ -513,7 +512,7 @@ def test_no_content_response(): # THEN return an None body and no Content-Type header assert result["statusCode"] == response.status_code assert result["body"] is None - headers = result["headers"] + headers = result["multiValueHeaders"] assert "Content-Type" not in headers @@ -528,7 +527,7 @@ def test_no_matches_with_cors(): # THEN return a 404 # AND cors headers are returned assert result["statusCode"] == 404 - assert "Access-Control-Allow-Origin" in result["headers"] + assert "Access-Control-Allow-Origin" in result["multiValueHeaders"] assert "Not found" in result["body"] @@ -556,10 +555,10 @@ def post_no_cors(): # AND include Access-Control-Allow-Methods of the cors methods used assert result["statusCode"] == 204 assert result["body"] == "" - headers = result["headers"] + headers = result["multiValueHeaders"] assert "Content-Type" not in headers - assert "Access-Control-Allow-Origin" in result["headers"] - assert headers["Access-Control-Allow-Methods"] == "DELETE,GET,OPTIONS" + assert "Access-Control-Allow-Origin" in result["multiValueHeaders"] + assert headers["Access-Control-Allow-Methods"] == ["DELETE", "GET", "OPTIONS"] def test_custom_preflight_response(): @@ -587,10 +586,10 @@ def custom_method(): # THEN return the custom preflight response assert result["statusCode"] == 200 assert result["body"] == "Foo" - headers = result["headers"] - assert headers["Content-Type"] == content_types.TEXT_HTML - assert "Access-Control-Allow-Origin" in result["headers"] - assert headers["Access-Control-Allow-Methods"] == "CUSTOM" + headers = result["multiValueHeaders"] + assert headers["Content-Type"] == [content_types.TEXT_HTML] + assert "Access-Control-Allow-Origin" in result["multiValueHeaders"] + assert headers["Access-Control-Allow-Methods"] == ["CUSTOM"] def test_service_error_responses(json_dump): @@ -608,7 +607,7 @@ def bad_request_error(): # THEN return the bad request error response # AND status code equals 400 assert result["statusCode"] == 400 - assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] expected = {"statusCode": 400, "message": "Missing required parameter"} assert result["body"] == json_dump(expected) @@ -623,7 +622,7 @@ def unauthorized_error(): # THEN return the unauthorized error response # AND status code equals 401 assert result["statusCode"] == 401 - assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] expected = {"statusCode": 401, "message": "Unauthorized"} assert result["body"] == json_dump(expected) @@ -638,7 +637,7 @@ def not_found_error(): # THEN return the not found error response # AND status code equals 404 assert result["statusCode"] == 404 - assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] expected = {"statusCode": 404, "message": "Not found"} assert result["body"] == json_dump(expected) @@ -653,7 +652,7 @@ def internal_server_error(): # THEN return the internal server error response # AND status code equals 500 assert result["statusCode"] == 500 - assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] expected = {"statusCode": 500, "message": "Internal server error"} assert result["body"] == json_dump(expected) @@ -668,8 +667,8 @@ def service_error(): # THEN return the service error response # AND status code equals 502 assert result["statusCode"] == 502 - assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON - assert "Access-Control-Allow-Origin" in result["headers"] + assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] + assert "Access-Control-Allow-Origin" in result["multiValueHeaders"] expected = {"statusCode": 502, "message": "Something went wrong!"} assert result["body"] == json_dump(expected) @@ -692,8 +691,8 @@ def raises_error(): # AND include the exception traceback in the response assert result["statusCode"] == 500 assert "Traceback (most recent call last)" in result["body"] - headers = result["headers"] - assert headers["Content-Type"] == content_types.TEXT_PLAIN + headers = result["multiValueHeaders"] + assert headers["Content-Type"] == [content_types.TEXT_PLAIN] def test_debug_unhandled_exceptions_debug_off(): @@ -980,7 +979,7 @@ def base(): # THEN process event correctly assert result["statusCode"] == 200 - assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] def test_api_gateway_app_router(): @@ -998,7 +997,7 @@ def foo(): # THEN process event correctly assert result["statusCode"] == 200 - assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] def test_api_gateway_app_router_with_params(): @@ -1024,7 +1023,7 @@ def foo(account_id): # THEN process event correctly assert result["statusCode"] == 200 - assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] def test_api_gateway_app_router_with_prefix(): @@ -1043,7 +1042,7 @@ def foo(): # THEN process event correctly assert result["statusCode"] == 200 - assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] def test_api_gateway_app_router_with_prefix_equals_path(): @@ -1063,7 +1062,7 @@ def foo(): # THEN process event correctly assert result["statusCode"] == 200 - assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] def test_api_gateway_app_router_with_different_methods(): @@ -1113,7 +1112,7 @@ def patch_func(): result = app(LOAD_GW_EVENT, None) assert result["statusCode"] == 404 # AND cors headers are not returned - assert "Access-Control-Allow-Origin" not in result["headers"] + assert "Access-Control-Allow-Origin" not in result["multiValueHeaders"] def test_duplicate_routes(): @@ -1172,11 +1171,11 @@ def foo(account_id): # THEN events are processed correctly assert get_result["statusCode"] == 200 - assert get_result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + assert get_result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] assert post_result["statusCode"] == 200 - assert post_result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + assert post_result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] assert put_result["statusCode"] == 404 - assert put_result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + assert put_result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] def test_api_gateway_app_router_access_to_resolver(): @@ -1195,7 +1194,7 @@ def foo(): result = app(LOAD_GW_EVENT, {}) assert result["statusCode"] == 200 - assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] def test_exception_handler(): @@ -1221,7 +1220,7 @@ def get_lambda() -> Response: # THEN call the exception_handler assert result["statusCode"] == 418 - assert result["headers"]["Content-Type"] == content_types.TEXT_HTML + assert result["multiValueHeaders"]["Content-Type"] == [content_types.TEXT_HTML] assert result["body"] == "Foo!" @@ -1248,7 +1247,7 @@ def get_lambda() -> Response: # THEN call the exception_handler assert result["statusCode"] == 500 - assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] assert result["body"] == "CUSTOM ERROR FORMAT" @@ -1267,7 +1266,7 @@ def handle_not_found(exc: NotFoundError) -> Response: # THEN call the exception_handler assert result["statusCode"] == 404 - assert result["headers"]["Content-Type"] == content_types.TEXT_PLAIN + assert result["multiValueHeaders"]["Content-Type"] == [content_types.TEXT_PLAIN] assert result["body"] == "I am a teapot!" @@ -1305,7 +1304,7 @@ def get_lambda() -> Response: # THEN call the exception_handler assert result["statusCode"] == 400 - assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] expected = {"statusCode": 400, "message": "Bad request"} assert result["body"] == json_dump(expected) diff --git a/tests/functional/event_handler/test_lambda_function_url.py b/tests/functional/event_handler/test_lambda_function_url.py index 22247e32ed7..282f49bb2b4 100644 --- a/tests/functional/event_handler/test_lambda_function_url.py +++ b/tests/functional/event_handler/test_lambda_function_url.py @@ -29,7 +29,7 @@ def test_lambda_function_url_event_with_cookies(): # GIVEN a Lambda Function Url type event app = LambdaFunctionUrlResolver() - @app.post("/my/path") + @app.get("/") def foo(): assert isinstance(app.current_event, LambdaFunctionUrlEvent) assert app.lambda_context == {} @@ -41,7 +41,7 @@ def foo(): # THEN process event correctly # AND set the current_event type as LambdaFunctionUrlEvent assert result["statusCode"] == 200 - assert result["headers"]["Cookies"] == ["CookieMonster"] + assert result["cookies"] == ["CookieMonster"] def test_lambda_function_url_no_matches(): From d7dea934411382e79f5c974d35a77762dc938d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Mon, 15 Aug 2022 21:46:25 +0200 Subject: [PATCH 06/35] chore(tests): typo --- tests/unit/event_handler/test_headers_serializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/event_handler/test_headers_serializer.py b/tests/unit/event_handler/test_headers_serializer.py index 592ee7f4c4a..17d42b400e6 100644 --- a/tests/unit/event_handler/test_headers_serializer.py +++ b/tests/unit/event_handler/test_headers_serializer.py @@ -11,7 +11,7 @@ def test_headers_serializer_apigatewayv2(): - event = APIGatewayProxyEventV2(load_event("apigatewayproxyv2event.json")) + event = APIGatewayProxyEventV2(load_event("apiGatewayProxyV2Event.json")) builder = HeadersSerializer(event=event, cookies=[], headers={}) assert builder.serialize() == {"cookies": [], "headers": {}} From 9089df7524ab36a02c6975c1d028121347e4408b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Tue, 16 Aug 2022 11:43:54 +0200 Subject: [PATCH 07/35] chore(event_handler): move header serializer logic --- .../event_handler/api_gateway.py | 5 +- .../event_handler/headers_serializer.py | 160 ++++++++---------- .../utilities/data_classes/alb_event.py | 13 ++ .../data_classes/api_gateway_proxy_event.py | 11 ++ .../utilities/data_classes/common.py | 5 + 5 files changed, 100 insertions(+), 94 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index bc2d83ab738..70671c017a7 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -14,7 +14,6 @@ from aws_lambda_powertools.event_handler import content_types from aws_lambda_powertools.event_handler.exceptions import NotFoundError, ServiceError -from aws_lambda_powertools.event_handler.headers_serializer import HeadersSerializer from aws_lambda_powertools.shared import constants from aws_lambda_powertools.shared.functions import resolve_truthy_env_var_choice from aws_lambda_powertools.shared.json_encoder import Encoder @@ -236,10 +235,8 @@ def build(self, event: BaseProxyEvent, cors: Optional[CORSConfig] = None) -> Dic "statusCode": self.response.status_code, "body": self.response.body, "isBase64Encoded": self.response.base64_encoded, + **event.header_serializer().serialize(headers=self.response.headers, cookies=self.response.cookies), } - payload.update( - HeadersSerializer(event=event, cookies=self.response.cookies, headers=self.response.headers).serialize() - ) return payload diff --git a/aws_lambda_powertools/event_handler/headers_serializer.py b/aws_lambda_powertools/event_handler/headers_serializer.py index 3806970ab5b..c2902545986 100644 --- a/aws_lambda_powertools/event_handler/headers_serializer.py +++ b/aws_lambda_powertools/event_handler/headers_serializer.py @@ -1,115 +1,95 @@ import warnings +from abc import ABC from typing import Any, Dict, List -from aws_lambda_powertools.utilities.data_classes import ( - ALBEvent, - APIGatewayProxyEvent, - APIGatewayProxyEventV2, - LambdaFunctionUrlEvent, -) -from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent - -class HeadersSerializer: +class BaseHeadersSerializer(ABC): """ Helper class to correctly serialize headers and cookies on the response payload. """ - def __init__(self, event: BaseProxyEvent, cookies: List[str], headers: Dict[str, str]): + def serialize(self, headers: Dict[str, str], cookies: List[str]) -> Dict[str, Any]: """ + Serializes headers and cookies according to the request type. + Returns a dict that can be merged with the response payload. Parameters ---------- - event: BaseProxyEvent - The request event, used to derive the response format - cookies: List[str] - A list of cookies to set in the response headers: Dict[str, str] A dictionary of headers to set in the response + cookies: List[str] + A list of cookies to set in the response """ - self.event = event - self.cookies = cookies - self.headers = headers + raise NotImplementedError() - def serialize(self) -> Dict[str, Any]: + +class HttpApiSerializer(BaseHeadersSerializer): + def serialize(self, headers: Dict[str, str], cookies: List[str]) -> Dict[str, Any]: """ - Serializes headers and cookies according to the request type. - Returns a dict that can be merged with the response payload. + When using HTTP APIs or LambdaFunctionURLs, everything is taken care automatically for us. + We can directly assign a list of cookies and a dict of headers to the response payload, and the + runtime will automatically serialize them correctly on the output. + + https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.proxy-format + https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.response + """ + return {"headers": headers, "cookies": cookies} + + +class MultiValueHeadersSerializer(BaseHeadersSerializer): + def serialize(self, headers: Dict[str, str], cookies: List[str]) -> Dict[str, Any]: + """ + When using REST APIs, headers can be encoded using the `multiValueHeaders` key on the response. + This is also the case when using an ALB integration with the `multiValueHeaders` option enabled. + The solution covers headers with just one key or multiple keys. + + https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format + https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers-response """ - payload: Dict[str, Any] = {} - - if isinstance(self.event, APIGatewayProxyEventV2) or isinstance(self.event, LambdaFunctionUrlEvent): - """ - When using HTTP APIs or LambdaFunctionURLs, everything is taken care automatically. - One can directly assign a list of cookies and a dict of headers to the response payload, and the - runtime will automatically serialize them correctly on the output. - - https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.proxy-format - https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.response - """ - payload["cookies"] = self.cookies - payload["headers"] = self.headers - - if isinstance(self.event, APIGatewayProxyEvent): - """ - When using REST APIs, headers can be encoded using the "multiValueHeaders" key on the response. - This will cover both single and multi-value headers. - - https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format - """ - - payload["multiValueHeaders"] = self._build_multivalue_headers() - - if isinstance(self.event, ALBEvent): - """ - The ALB integration can work with multiValueHeaders disabled (default) or enabled. - We can detect if the feature is enabled by looking for the presence of `multiValueHeaders` in the request. - If the feature is disabled, and we try to set multiple headers with the same key, or more than one cookie, - print a warning. - - - https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#respond-to-load-balancer - https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers-response - """ - if self.event.multi_value_headers is not None: - payload["multiValueHeaders"] = self._build_multivalue_headers() - else: - payload.setdefault("headers", {}) - - if self.cookies: - if len(self.cookies) > 1: - warnings.warn( - "Can't encode more than one cookie in the response. " - "Did you enable multiValueHeaders on the ALB Target Group?" - ) - - # We can only send one cookie, send the last one - payload["headers"]["Set-Cookie"] = self.cookies[-1] - - for key, value in self.headers.items(): - values = value.split(",") - if len(values) > 1: - warnings.warn( - "Can't encode more than on header with the same key in the response. " - "Did you enable multiValueHeaders on the ALB Target Group?" - ) - - # We can only send on header for this key, send the last value - payload["headers"][key] = values[-1].strip() + payload: Dict[str, List[str]] = {} + + for key, value in headers.items(): + values = value.split(",") + payload[key] = [value.strip() for value in values] + + if cookies: + payload.setdefault("Set-Cookie", []) + for cookie in cookies: + payload["Set-Cookie"].append(cookie) return payload - def _build_multivalue_headers(self) -> Dict[str, List[str]]: - """Formats headers and cookies according to the `multiValueHeader` format""" - multi_value_headers: Dict[str, List[str]] = {} - for key, value in self.headers.items(): +class SingleValueHeadersSerializer(BaseHeadersSerializer): + def serialize(self, headers: Dict[str, str], cookies: List[str]) -> Dict[str, Any]: + """ + The ALB integration has `multiValueHeaders` disabled by default. + If we try to set multiple headers with the same key, or more than one cookie, print a warning. + + https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#respond-to-load-balancer + """ + payload: Dict[str, Dict[str, str]] = {} + payload.setdefault("headers", {}) + + if cookies: + if len(cookies) > 1: + warnings.warn( + "Can't encode more than one cookie in the response. " + "Did you enable multiValueHeaders on the ALB Target Group?" + ) + + # We can only send one cookie, send the last one + payload["headers"]["Set-Cookie"] = cookies[-1] + + for key, value in headers.items(): values = value.split(",") - multi_value_headers[key] = [value.strip() for value in values] + if len(values) > 1: + warnings.warn( + "Can't encode more than on header with the same key in the response. " + "Did you enable multiValueHeaders on the ALB Target Group?" + ) - if self.cookies: - multi_value_headers.setdefault("Set-Cookie", []) - for cookie in self.cookies: - multi_value_headers["Set-Cookie"].append(cookie) + # We can only send on header for this key, send the last value + payload["headers"][key] = values[-1].strip() - return multi_value_headers + return payload diff --git a/aws_lambda_powertools/utilities/data_classes/alb_event.py b/aws_lambda_powertools/utilities/data_classes/alb_event.py index 159779c86a7..952f929e373 100644 --- a/aws_lambda_powertools/utilities/data_classes/alb_event.py +++ b/aws_lambda_powertools/utilities/data_classes/alb_event.py @@ -1,5 +1,10 @@ from typing import Dict, List, Optional +from aws_lambda_powertools.event_handler.headers_serializer import ( + BaseHeadersSerializer, + MultiValueHeadersSerializer, + SingleValueHeadersSerializer, +) from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent, DictWrapper @@ -30,3 +35,11 @@ def multi_value_query_string_parameters(self) -> Optional[Dict[str, List[str]]]: @property def multi_value_headers(self) -> Optional[Dict[str, List[str]]]: return self.get("multiValueHeaders") + + def header_serializer(self) -> BaseHeadersSerializer: + # When using the ALB integration, the `multiValueHeaders` feature can be disabled (default) or enabled. + # We can determine if the feature is enabled by looking if the event has a `multiValueHeaders` key. + if self.multi_value_headers: + return MultiValueHeadersSerializer() + else: + return SingleValueHeadersSerializer() diff --git a/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py b/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py index be374aba398..33d01223d7c 100644 --- a/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py +++ b/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py @@ -1,5 +1,10 @@ from typing import Any, Dict, List, Optional +from aws_lambda_powertools.event_handler.headers_serializer import ( + BaseHeadersSerializer, + HttpApiSerializer, + MultiValueHeadersSerializer, +) from aws_lambda_powertools.utilities.data_classes.common import ( BaseProxyEvent, BaseRequestContext, @@ -106,6 +111,9 @@ def path_parameters(self) -> Optional[Dict[str, str]]: def stage_variables(self) -> Optional[Dict[str, str]]: return self.get("stageVariables") + def header_serializer(self) -> BaseHeadersSerializer: + return MultiValueHeadersSerializer() + class RequestContextV2AuthorizerIam(DictWrapper): @property @@ -250,3 +258,6 @@ def path(self) -> str: def http_method(self) -> str: """The HTTP method used. Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT.""" return self.request_context.http.method + + def header_serializer(self): + return HttpApiSerializer() diff --git a/aws_lambda_powertools/utilities/data_classes/common.py b/aws_lambda_powertools/utilities/data_classes/common.py index 2109ee3dd3e..79afe2a3a1d 100644 --- a/aws_lambda_powertools/utilities/data_classes/common.py +++ b/aws_lambda_powertools/utilities/data_classes/common.py @@ -2,6 +2,8 @@ import json from typing import Any, Dict, Optional +from aws_lambda_powertools.event_handler.headers_serializer import BaseHeadersSerializer + class DictWrapper: """Provides a single read only access to a wrapper dict""" @@ -127,6 +129,9 @@ def get_header_value( """ return get_header_value(self.headers, name, default_value, case_sensitive) + def header_serializer(self) -> BaseHeadersSerializer: + raise NotImplementedError() + class RequestContextClientCert(DictWrapper): @property From c7edb6d24eaac61ce0d7b3f1442c83f3b080787c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Tue, 16 Aug 2022 11:44:40 +0200 Subject: [PATCH 08/35] Revert "chore(tests): move load_event helper to global utils" This reverts commit cb1797f0e602f1a5ba0a49ebfbb6e303a07270c2. --- tests/functional/data_classes/test_amazon_mq.py | 2 +- tests/functional/data_classes/test_lambda_function_url.py | 2 +- tests/functional/event_handler/test_api_gateway.py | 2 +- tests/functional/event_handler/test_appsync.py | 2 +- tests/functional/event_handler/test_lambda_function_url.py | 2 +- tests/functional/idempotency/conftest.py | 3 +-- tests/functional/idempotency/test_idempotency.py | 3 +-- tests/functional/parser/test_alb.py | 2 +- tests/functional/parser/test_apigw.py | 2 +- tests/functional/parser/test_apigwv2.py | 2 +- tests/functional/parser/test_cloudwatch.py | 2 +- tests/functional/parser/test_dynamodb.py | 2 +- tests/functional/parser/test_eventbridge.py | 2 +- tests/functional/parser/test_kinesis.py | 2 +- tests/functional/parser/test_s3 object_event.py | 2 +- tests/functional/parser/test_s3.py | 2 +- tests/functional/parser/test_ses.py | 2 +- tests/functional/parser/test_sns.py | 2 +- tests/functional/parser/test_sqs.py | 2 +- tests/functional/test_data_classes.py | 2 +- tests/functional/utils.py | 7 +++++++ 21 files changed, 27 insertions(+), 22 deletions(-) diff --git a/tests/functional/data_classes/test_amazon_mq.py b/tests/functional/data_classes/test_amazon_mq.py index 9e21aa10ebf..a88a962c17b 100644 --- a/tests/functional/data_classes/test_amazon_mq.py +++ b/tests/functional/data_classes/test_amazon_mq.py @@ -2,7 +2,7 @@ from aws_lambda_powertools.utilities.data_classes.active_mq_event import ActiveMQEvent, ActiveMQMessage from aws_lambda_powertools.utilities.data_classes.rabbit_mq_event import BasicProperties, RabbitMessage, RabbitMQEvent -from tests.utils import load_event +from tests.functional.utils import load_event def test_active_mq_event(): diff --git a/tests/functional/data_classes/test_lambda_function_url.py b/tests/functional/data_classes/test_lambda_function_url.py index ffc215a99ff..c27920c3392 100644 --- a/tests/functional/data_classes/test_lambda_function_url.py +++ b/tests/functional/data_classes/test_lambda_function_url.py @@ -1,5 +1,5 @@ from aws_lambda_powertools.utilities.data_classes import LambdaFunctionUrlEvent -from tests.utils import load_event +from tests.functional.utils import load_event def test_lambda_function_url_event(): diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 49d95e063ee..40590e4f235 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -37,7 +37,7 @@ APIGatewayProxyEventV2, event_source, ) -from tests.utils import load_event +from tests.functional.utils import load_event @pytest.fixture diff --git a/tests/functional/event_handler/test_appsync.py b/tests/functional/event_handler/test_appsync.py index 329ac9ee985..79173e55825 100644 --- a/tests/functional/event_handler/test_appsync.py +++ b/tests/functional/event_handler/test_appsync.py @@ -7,7 +7,7 @@ from aws_lambda_powertools.event_handler.appsync import Router from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.utils import load_event +from tests.functional.utils import load_event def test_direct_resolver(): diff --git a/tests/functional/event_handler/test_lambda_function_url.py b/tests/functional/event_handler/test_lambda_function_url.py index 282f49bb2b4..c24c9f0e0c8 100644 --- a/tests/functional/event_handler/test_lambda_function_url.py +++ b/tests/functional/event_handler/test_lambda_function_url.py @@ -1,6 +1,6 @@ from aws_lambda_powertools.event_handler import LambdaFunctionUrlResolver, Response, content_types from aws_lambda_powertools.utilities.data_classes import LambdaFunctionUrlEvent -from tests.utils import load_event +from tests.functional.utils import load_event def test_lambda_function_url_event(): diff --git a/tests/functional/idempotency/conftest.py b/tests/functional/idempotency/conftest.py index 328900cda07..b5cf79727b1 100644 --- a/tests/functional/idempotency/conftest.py +++ b/tests/functional/idempotency/conftest.py @@ -14,8 +14,7 @@ from aws_lambda_powertools.utilities.jmespath_utils import extract_data_from_envelope from aws_lambda_powertools.utilities.validation import envelopes from tests.functional.idempotency.utils import hash_idempotency_key -from tests.functional.utils import json_serialize -from tests.utils import load_event +from tests.functional.utils import json_serialize, load_event TABLE_NAME = "TEST_TABLE" diff --git a/tests/functional/idempotency/test_idempotency.py b/tests/functional/idempotency/test_idempotency.py index 3fd8ee619fc..97a9166efa0 100644 --- a/tests/functional/idempotency/test_idempotency.py +++ b/tests/functional/idempotency/test_idempotency.py @@ -29,8 +29,7 @@ build_idempotency_update_item_stub, hash_idempotency_key, ) -from tests.functional.utils import json_serialize -from tests.utils import load_event +from tests.functional.utils import json_serialize, load_event TABLE_NAME = "TEST_TABLE" diff --git a/tests/functional/parser/test_alb.py b/tests/functional/parser/test_alb.py index c03066c140f..d48e39f1bab 100644 --- a/tests/functional/parser/test_alb.py +++ b/tests/functional/parser/test_alb.py @@ -3,7 +3,7 @@ from aws_lambda_powertools.utilities.parser import ValidationError, event_parser from aws_lambda_powertools.utilities.parser.models import AlbModel from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=AlbModel) diff --git a/tests/functional/parser/test_apigw.py b/tests/functional/parser/test_apigw.py index 8e08869d9f3..35b2fdb1926 100644 --- a/tests/functional/parser/test_apigw.py +++ b/tests/functional/parser/test_apigw.py @@ -5,7 +5,7 @@ from aws_lambda_powertools.utilities.parser.models import APIGatewayProxyEventModel from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyApiGatewayBusiness -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=MyApiGatewayBusiness, envelope=envelopes.ApiGatewayEnvelope) diff --git a/tests/functional/parser/test_apigwv2.py b/tests/functional/parser/test_apigwv2.py index 676fe4bcf5d..d3510b185dd 100644 --- a/tests/functional/parser/test_apigwv2.py +++ b/tests/functional/parser/test_apigwv2.py @@ -6,7 +6,7 @@ ) from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyApiGatewayBusiness -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=MyApiGatewayBusiness, envelope=envelopes.ApiGatewayV2Envelope) diff --git a/tests/functional/parser/test_cloudwatch.py b/tests/functional/parser/test_cloudwatch.py index 8ea2441100f..7290d0bffcb 100644 --- a/tests/functional/parser/test_cloudwatch.py +++ b/tests/functional/parser/test_cloudwatch.py @@ -9,7 +9,7 @@ from aws_lambda_powertools.utilities.parser.models import CloudWatchLogsLogEvent, CloudWatchLogsModel from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyCloudWatchBusiness -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=MyCloudWatchBusiness, envelope=envelopes.CloudWatchLogsEnvelope) diff --git a/tests/functional/parser/test_dynamodb.py b/tests/functional/parser/test_dynamodb.py index c3dfee65db3..9917fac234b 100644 --- a/tests/functional/parser/test_dynamodb.py +++ b/tests/functional/parser/test_dynamodb.py @@ -5,7 +5,7 @@ from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, event_parser from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyAdvancedDynamoBusiness, MyDynamoBusiness -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=MyDynamoBusiness, envelope=envelopes.DynamoDBStreamEnvelope) diff --git a/tests/functional/parser/test_eventbridge.py b/tests/functional/parser/test_eventbridge.py index 93188a5c59f..6242403ab35 100644 --- a/tests/functional/parser/test_eventbridge.py +++ b/tests/functional/parser/test_eventbridge.py @@ -5,7 +5,7 @@ from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, event_parser from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyAdvancedEventbridgeBusiness, MyEventbridgeBusiness -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=MyEventbridgeBusiness, envelope=envelopes.EventBridgeEnvelope) diff --git a/tests/functional/parser/test_kinesis.py b/tests/functional/parser/test_kinesis.py index a9aa59dd9a6..552cb6cef68 100644 --- a/tests/functional/parser/test_kinesis.py +++ b/tests/functional/parser/test_kinesis.py @@ -6,7 +6,7 @@ from aws_lambda_powertools.utilities.parser.models import KinesisDataStreamModel, KinesisDataStreamRecordPayload from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyKinesisBusiness -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=MyKinesisBusiness, envelope=envelopes.KinesisDataStreamEnvelope) diff --git a/tests/functional/parser/test_s3 object_event.py b/tests/functional/parser/test_s3 object_event.py index 03ecf227139..90c2555360d 100644 --- a/tests/functional/parser/test_s3 object_event.py +++ b/tests/functional/parser/test_s3 object_event.py @@ -1,7 +1,7 @@ from aws_lambda_powertools.utilities.parser import event_parser from aws_lambda_powertools.utilities.parser.models import S3ObjectLambdaEvent from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=S3ObjectLambdaEvent) diff --git a/tests/functional/parser/test_s3.py b/tests/functional/parser/test_s3.py index 77e0adb7b4a..71a5dc6afe3 100644 --- a/tests/functional/parser/test_s3.py +++ b/tests/functional/parser/test_s3.py @@ -1,7 +1,7 @@ from aws_lambda_powertools.utilities.parser import event_parser, parse from aws_lambda_powertools.utilities.parser.models import S3Model, S3RecordModel from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=S3Model) diff --git a/tests/functional/parser/test_ses.py b/tests/functional/parser/test_ses.py index 09f6ff6b76d..d434e2350f8 100644 --- a/tests/functional/parser/test_ses.py +++ b/tests/functional/parser/test_ses.py @@ -1,7 +1,7 @@ from aws_lambda_powertools.utilities.parser import event_parser from aws_lambda_powertools.utilities.parser.models import SesModel, SesRecordModel from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=SesModel) diff --git a/tests/functional/parser/test_sns.py b/tests/functional/parser/test_sns.py index 992871126bc..b0d9ff69a9b 100644 --- a/tests/functional/parser/test_sns.py +++ b/tests/functional/parser/test_sns.py @@ -6,8 +6,8 @@ from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, event_parser from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyAdvancedSnsBusiness, MySnsBusiness +from tests.functional.utils import load_event from tests.functional.validator.conftest import sns_event # noqa: F401 -from tests.utils import load_event @event_parser(model=MySnsBusiness, envelope=envelopes.SnsEnvelope) diff --git a/tests/functional/parser/test_sqs.py b/tests/functional/parser/test_sqs.py index 93b2fca5d6c..7ca883616f2 100644 --- a/tests/functional/parser/test_sqs.py +++ b/tests/functional/parser/test_sqs.py @@ -5,8 +5,8 @@ from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, event_parser from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyAdvancedSqsBusiness, MySqsBusiness +from tests.functional.utils import load_event from tests.functional.validator.conftest import sqs_event # noqa: F401 -from tests.utils import load_event @event_parser(model=MySqsBusiness, envelope=envelopes.SqsEnvelope) diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py index fe543db34cb..8a87075d16c 100644 --- a/tests/functional/test_data_classes.py +++ b/tests/functional/test_data_classes.py @@ -76,7 +76,7 @@ ) from aws_lambda_powertools.utilities.data_classes.event_source import event_source from aws_lambda_powertools.utilities.data_classes.s3_object_event import S3ObjectLambdaEvent -from tests.utils import load_event +from tests.functional.utils import load_event def test_dict_wrapper_equals(): diff --git a/tests/functional/utils.py b/tests/functional/utils.py index c07f4fe32cc..6b73053e0d0 100644 --- a/tests/functional/utils.py +++ b/tests/functional/utils.py @@ -1,9 +1,16 @@ import base64 import json +from pathlib import Path +from typing import Any from aws_lambda_powertools.shared.json_encoder import Encoder +def load_event(file_name: str) -> Any: + path = Path(str(Path(__file__).parent.parent) + "/events/" + file_name) + return json.loads(path.read_text()) + + def str_to_b64(data: str) -> str: return base64.b64encode(data.encode()).decode("utf-8") From 8ddce93ed8b550c21104dd2baaf534512320b91d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Tue, 16 Aug 2022 12:26:08 +0200 Subject: [PATCH 09/35] fix(event_handler): simplified tests --- .../headers_serializer.py | 2 +- .../utilities/data_classes/alb_event.py | 2 +- .../data_classes/api_gateway_proxy_event.py | 2 +- .../utilities/data_classes/common.py | 2 +- .../event_handler/test_headers_serializer.py | 68 +++++++++++ tests/unit/event_handler/__init__.py | 0 .../event_handler/test_headers_serializer.py | 106 ------------------ 7 files changed, 72 insertions(+), 110 deletions(-) rename aws_lambda_powertools/{event_handler => shared}/headers_serializer.py (98%) create mode 100644 tests/functional/event_handler/test_headers_serializer.py delete mode 100644 tests/unit/event_handler/__init__.py delete mode 100644 tests/unit/event_handler/test_headers_serializer.py diff --git a/aws_lambda_powertools/event_handler/headers_serializer.py b/aws_lambda_powertools/shared/headers_serializer.py similarity index 98% rename from aws_lambda_powertools/event_handler/headers_serializer.py rename to aws_lambda_powertools/shared/headers_serializer.py index c2902545986..cdee2d532ce 100644 --- a/aws_lambda_powertools/event_handler/headers_serializer.py +++ b/aws_lambda_powertools/shared/headers_serializer.py @@ -57,7 +57,7 @@ def serialize(self, headers: Dict[str, str], cookies: List[str]) -> Dict[str, An for cookie in cookies: payload["Set-Cookie"].append(cookie) - return payload + return {"multiValueHeaders": payload} class SingleValueHeadersSerializer(BaseHeadersSerializer): diff --git a/aws_lambda_powertools/utilities/data_classes/alb_event.py b/aws_lambda_powertools/utilities/data_classes/alb_event.py index 952f929e373..7f0dd8779a6 100644 --- a/aws_lambda_powertools/utilities/data_classes/alb_event.py +++ b/aws_lambda_powertools/utilities/data_classes/alb_event.py @@ -1,6 +1,6 @@ from typing import Dict, List, Optional -from aws_lambda_powertools.event_handler.headers_serializer import ( +from aws_lambda_powertools.shared.headers_serializer import ( BaseHeadersSerializer, MultiValueHeadersSerializer, SingleValueHeadersSerializer, diff --git a/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py b/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py index 33d01223d7c..133a27c5e02 100644 --- a/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py +++ b/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py @@ -1,6 +1,6 @@ from typing import Any, Dict, List, Optional -from aws_lambda_powertools.event_handler.headers_serializer import ( +from aws_lambda_powertools.shared.headers_serializer import ( BaseHeadersSerializer, HttpApiSerializer, MultiValueHeadersSerializer, diff --git a/aws_lambda_powertools/utilities/data_classes/common.py b/aws_lambda_powertools/utilities/data_classes/common.py index 79afe2a3a1d..ffd608f3015 100644 --- a/aws_lambda_powertools/utilities/data_classes/common.py +++ b/aws_lambda_powertools/utilities/data_classes/common.py @@ -2,7 +2,7 @@ import json from typing import Any, Dict, Optional -from aws_lambda_powertools.event_handler.headers_serializer import BaseHeadersSerializer +from aws_lambda_powertools.shared.headers_serializer import BaseHeadersSerializer class DictWrapper: diff --git a/tests/functional/event_handler/test_headers_serializer.py b/tests/functional/event_handler/test_headers_serializer.py new file mode 100644 index 00000000000..9d228635562 --- /dev/null +++ b/tests/functional/event_handler/test_headers_serializer.py @@ -0,0 +1,68 @@ +import warnings + +from aws_lambda_powertools.shared.headers_serializer import ( + HttpApiSerializer, + MultiValueHeadersSerializer, + SingleValueHeadersSerializer, +) + + +def test_headers_serializer_http_api(): + serializer = HttpApiSerializer() + + payload = serializer.serialize(cookies=[], headers={}) + assert payload == {"cookies": [], "headers": {}} + + payload = serializer.serialize(cookies=[], headers={"Content-Type": "text/html"}) + assert payload == {"cookies": [], "headers": {"Content-Type": "text/html"}} + + payload = serializer.serialize(cookies=["UUID=12345"], headers={}) + assert payload == {"cookies": ["UUID=12345"], "headers": {}} + + payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": "bar,zbr"}) + assert payload == {"cookies": ["UUID=12345", "SSID=0xdeadbeef"], "headers": {"Foo": "bar,zbr"}} + + +def test_headers_serializer_multi_value_headers(): + serializer = MultiValueHeadersSerializer() + + payload = serializer.serialize(cookies=[], headers={}) + assert payload == {"multiValueHeaders": {}} + + payload = serializer.serialize(cookies=[], headers={"Content-Type": "text/html"}) + assert payload == {"multiValueHeaders": {"Content-Type": ["text/html"]}} + + payload = serializer.serialize(cookies=["UUID=12345"], headers={}) + assert payload == {"multiValueHeaders": {"Set-Cookie": ["UUID=12345"]}} + + payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": "bar,zbr"}) + assert payload == {"multiValueHeaders": {"Set-Cookie": ["UUID=12345", "SSID=0xdeadbeef"], "Foo": ["bar", "zbr"]}} + + +def test_headers_serializer_single_value_headers(): + serializer = SingleValueHeadersSerializer() + + payload = serializer.serialize(cookies=[], headers={}) + assert payload == {"headers": {}} + + payload = serializer.serialize(cookies=[], headers={"Content-Type": "text/html"}) + assert payload == {"headers": {"Content-Type": "text/html"}} + + payload = serializer.serialize(cookies=["UUID=12345"], headers={}) + assert payload == {"headers": {"Set-Cookie": "UUID=12345"}} + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("default") + + payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": "bar,zbr"}) + assert payload == {"headers": {"Set-Cookie": "SSID=0xdeadbeef", "Foo": "zbr"}} + + assert len(w) == 2 + assert str(w[-2].message) == ( + "Can't encode more than one cookie in the response. " + "Did you enable multiValueHeaders on the ALB Target Group?" + ) + assert str(w[-1].message) == ( + "Can't encode more than on header with the same key in the response. " + "Did you enable multiValueHeaders on the ALB Target Group?" + ) diff --git a/tests/unit/event_handler/__init__.py b/tests/unit/event_handler/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/unit/event_handler/test_headers_serializer.py b/tests/unit/event_handler/test_headers_serializer.py deleted file mode 100644 index 17d42b400e6..00000000000 --- a/tests/unit/event_handler/test_headers_serializer.py +++ /dev/null @@ -1,106 +0,0 @@ -import warnings - -from aws_lambda_powertools.event_handler.headers_serializer import HeadersSerializer -from aws_lambda_powertools.utilities.data_classes import ( - ALBEvent, - APIGatewayProxyEvent, - APIGatewayProxyEventV2, - LambdaFunctionUrlEvent, -) -from tests.utils import load_event - - -def test_headers_serializer_apigatewayv2(): - event = APIGatewayProxyEventV2(load_event("apiGatewayProxyV2Event.json")) - - builder = HeadersSerializer(event=event, cookies=[], headers={}) - assert builder.serialize() == {"cookies": [], "headers": {}} - - builder = HeadersSerializer(event=event, cookies=[], headers={"Content-Type": "text/html"}) - assert builder.serialize() == {"cookies": [], "headers": {"Content-Type": "text/html"}} - - builder = HeadersSerializer(event=event, cookies=["UUID=12345"], headers={}) - assert builder.serialize() == {"cookies": ["UUID=12345"], "headers": {}} - - builder = HeadersSerializer(event=event, cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": "bar,zbr"}) - assert builder.serialize() == {"cookies": ["UUID=12345", "SSID=0xdeadbeef"], "headers": {"Foo": "bar,zbr"}} - - -def test_headers_serializer_lambdafunctionurl(): - event = LambdaFunctionUrlEvent(load_event("lambdaFunctionUrlEvent.json")) - - builder = HeadersSerializer(event=event, cookies=[], headers={}) - assert builder.serialize() == {"cookies": [], "headers": {}} - - builder = HeadersSerializer(event=event, cookies=[], headers={"Content-Type": "text/html"}) - assert builder.serialize() == {"cookies": [], "headers": {"Content-Type": "text/html"}} - - builder = HeadersSerializer(event=event, cookies=["UUID=12345"], headers={}) - assert builder.serialize() == {"cookies": ["UUID=12345"], "headers": {}} - - builder = HeadersSerializer(event=event, cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": "bar,zbr"}) - assert builder.serialize() == {"cookies": ["UUID=12345", "SSID=0xdeadbeef"], "headers": {"Foo": "bar,zbr"}} - - -def test_headers_serializer_apigateway(): - event = APIGatewayProxyEvent(load_event("apiGatewayProxyEvent.json")) - - builder = HeadersSerializer(event=event, cookies=[], headers={}) - assert builder.serialize() == {"multiValueHeaders": {}} - - builder = HeadersSerializer(event=event, cookies=[], headers={"Content-Type": "text/html"}) - assert builder.serialize() == {"multiValueHeaders": {"Content-Type": ["text/html"]}} - - builder = HeadersSerializer(event=event, cookies=["UUID=12345"], headers={}) - assert builder.serialize() == {"multiValueHeaders": {"Set-Cookie": ["UUID=12345"]}} - - builder = HeadersSerializer(event=event, cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": "bar,zbr"}) - assert builder.serialize() == { - "multiValueHeaders": {"Set-Cookie": ["UUID=12345", "SSID=0xdeadbeef"], "Foo": ["bar", "zbr"]} - } - - -def test_headers_serializer_alb_without_multi_headers_noop(): - event = ALBEvent(load_event("albEvent.json")) - - builder = HeadersSerializer(event=event, cookies=[], headers={}) - assert builder.serialize() == {"headers": {}} - - builder = HeadersSerializer(event=event, cookies=[], headers={"Content-Type": "text/html"}) - assert builder.serialize() == {"headers": {"Content-Type": "text/html"}} - - builder = HeadersSerializer(event=event, cookies=["UUID=12345"], headers={}) - assert builder.serialize() == {"headers": {"Set-Cookie": "UUID=12345"}} - - builder = HeadersSerializer(event=event, cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": "bar,zbr"}) - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("default") - assert builder.serialize() == {"headers": {"Set-Cookie": "SSID=0xdeadbeef", "Foo": "zbr"}} - - assert len(w) == 2 - assert str(w[-2].message) == ( - "Can't encode more than one cookie in the response. " - "Did you enable multiValueHeaders on the ALB Target Group?" - ) - assert str(w[-1].message) == ( - "Can't encode more than on header with the same key in the response. " - "Did you enable multiValueHeaders on the ALB Target Group?" - ) - - -def test_headers_serializer_alb_with_multi_headers_noop(): - event = ALBEvent(load_event("albMultiValueHeadersEvent.json")) - - builder = HeadersSerializer(event=event, cookies=[], headers={}) - assert builder.serialize() == {"multiValueHeaders": {}} - - builder = HeadersSerializer(event=event, cookies=[], headers={"Content-Type": "text/html"}) - assert builder.serialize() == {"multiValueHeaders": {"Content-Type": ["text/html"]}} - - builder = HeadersSerializer(event=event, cookies=["UUID=12345"], headers={}) - assert builder.serialize() == {"multiValueHeaders": {"Set-Cookie": ["UUID=12345"]}} - - builder = HeadersSerializer(event=event, cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": "bar,zbr"}) - assert builder.serialize() == { - "multiValueHeaders": {"Set-Cookie": ["UUID=12345", "SSID=0xdeadbeef"], "Foo": ["bar", "zbr"]} - } From 66f1c411fe72bedae4076a1b60b101317ac29938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Tue, 16 Aug 2022 16:10:16 +0200 Subject: [PATCH 10/35] docs(event_handler): update example --- docs/core/event_handler/api_gateway.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 5e9ce37a472..1d1b26a202c 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -314,9 +314,16 @@ For convenience, these are the default values when using `CORSConfig` to enable You can use the `Response` class to have full control over the response, for example you might want to add additional headers, cookies, or set a custom Content-type. +???+ info + Powertools serializes the headers and cookies according to the type of input event. + For some integrations this requires headers and cookies to be encoded as `multiValueHeaders`. + +???+ warning + If you use the ALB integration, make sure you [enable the multi value headers feature](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers) in order to fully support this feature. + === "fine_grained_responses.py" - ```python hl_lines="7 24-28" + ```python hl_lines="7 24-29" --8<-- "examples/event_handler_rest/src/fine_grained_responses.py" ``` From a240f02af7e76a1ad098e962094b8e6b1ad36366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Wed, 17 Aug 2022 15:38:21 +0200 Subject: [PATCH 11/35] fix(event_handler): don't be smart about multiple headers --- aws_lambda_powertools/shared/headers_serializer.py | 13 ++----------- tests/functional/event_handler/test_api_gateway.py | 8 ++++---- .../event_handler/test_headers_serializer.py | 12 ++++-------- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/aws_lambda_powertools/shared/headers_serializer.py b/aws_lambda_powertools/shared/headers_serializer.py index cdee2d532ce..f0db3968c38 100644 --- a/aws_lambda_powertools/shared/headers_serializer.py +++ b/aws_lambda_powertools/shared/headers_serializer.py @@ -49,8 +49,7 @@ def serialize(self, headers: Dict[str, str], cookies: List[str]) -> Dict[str, An payload: Dict[str, List[str]] = {} for key, value in headers.items(): - values = value.split(",") - payload[key] = [value.strip() for value in values] + payload[key] = [value] if cookies: payload.setdefault("Set-Cookie", []) @@ -82,14 +81,6 @@ def serialize(self, headers: Dict[str, str], cookies: List[str]) -> Dict[str, An payload["headers"]["Set-Cookie"] = cookies[-1] for key, value in headers.items(): - values = value.split(",") - if len(values) > 1: - warnings.warn( - "Can't encode more than on header with the same key in the response. " - "Did you enable multiValueHeaders on the ALB Target Group?" - ) - - # We can only send on header for this key, send the last value - payload["headers"][key] = values[-1].strip() + payload["headers"][key] = value return payload diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 40590e4f235..fce5e7fa047 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -266,7 +266,7 @@ def handler(event, context): assert headers["Content-Type"] == [content_types.TEXT_HTML] assert headers["Access-Control-Allow-Origin"] == ["*"] assert "Access-Control-Allow-Credentials" not in headers - assert headers["Access-Control-Allow-Headers"] == sorted(CORSConfig._REQUIRED_HEADERS) + assert headers["Access-Control-Allow-Headers"] == [",".join(sorted(CORSConfig._REQUIRED_HEADERS))] # THEN for routes without cors flag return no cors headers mock_event = {"path": "/my/request", "httpMethod": "GET"} @@ -483,9 +483,9 @@ def another_one(): headers = result["multiValueHeaders"] assert headers["Content-Type"] == [content_types.APPLICATION_JSON] assert headers["Access-Control-Allow-Origin"] == [cors_config.allow_origin] - expected_allows_headers = sorted(set(allow_header + cors_config._REQUIRED_HEADERS)) + expected_allows_headers = [",".join(sorted(set(allow_header + cors_config._REQUIRED_HEADERS)))] assert headers["Access-Control-Allow-Headers"] == expected_allows_headers - assert headers["Access-Control-Expose-Headers"] == cors_config.expose_headers + assert headers["Access-Control-Expose-Headers"] == [",".join(cors_config.expose_headers)] assert headers["Access-Control-Max-Age"] == [str(cors_config.max_age)] assert "Access-Control-Allow-Credentials" in headers assert headers["Access-Control-Allow-Credentials"] == ["true"] @@ -558,7 +558,7 @@ def post_no_cors(): headers = result["multiValueHeaders"] assert "Content-Type" not in headers assert "Access-Control-Allow-Origin" in result["multiValueHeaders"] - assert headers["Access-Control-Allow-Methods"] == ["DELETE", "GET", "OPTIONS"] + assert headers["Access-Control-Allow-Methods"] == ["DELETE,GET,OPTIONS"] def test_custom_preflight_response(): diff --git a/tests/functional/event_handler/test_headers_serializer.py b/tests/functional/event_handler/test_headers_serializer.py index 9d228635562..794376059d3 100644 --- a/tests/functional/event_handler/test_headers_serializer.py +++ b/tests/functional/event_handler/test_headers_serializer.py @@ -36,7 +36,7 @@ def test_headers_serializer_multi_value_headers(): assert payload == {"multiValueHeaders": {"Set-Cookie": ["UUID=12345"]}} payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": "bar,zbr"}) - assert payload == {"multiValueHeaders": {"Set-Cookie": ["UUID=12345", "SSID=0xdeadbeef"], "Foo": ["bar", "zbr"]}} + assert payload == {"multiValueHeaders": {"Set-Cookie": ["UUID=12345", "SSID=0xdeadbeef"], "Foo": ["bar,zbr"]}} def test_headers_serializer_single_value_headers(): @@ -55,14 +55,10 @@ def test_headers_serializer_single_value_headers(): warnings.simplefilter("default") payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": "bar,zbr"}) - assert payload == {"headers": {"Set-Cookie": "SSID=0xdeadbeef", "Foo": "zbr"}} + assert payload == {"headers": {"Set-Cookie": "SSID=0xdeadbeef", "Foo": "bar,zbr"}} - assert len(w) == 2 - assert str(w[-2].message) == ( - "Can't encode more than one cookie in the response. " - "Did you enable multiValueHeaders on the ALB Target Group?" - ) + assert len(w) == 1 assert str(w[-1].message) == ( - "Can't encode more than on header with the same key in the response. " + "Can't encode more than one cookie in the response. " "Did you enable multiValueHeaders on the ALB Target Group?" ) From 5263a2d1c1926596e36c32cbc2ad6d5d40117001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Wed, 17 Aug 2022 15:40:42 +0200 Subject: [PATCH 12/35] fix(docs): simplified wording --- docs/core/event_handler/api_gateway.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 1d1b26a202c..cad60cfadb8 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -316,7 +316,7 @@ You can use the `Response` class to have full control over the response, for exa ???+ info Powertools serializes the headers and cookies according to the type of input event. - For some integrations this requires headers and cookies to be encoded as `multiValueHeaders`. + Some integrations require headers and cookies to be encoded as `multiValueHeaders`. ???+ warning If you use the ALB integration, make sure you [enable the multi value headers feature](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers) in order to fully support this feature. From 7ad8b88bde7ca43a640340a9953b0e88cbf62d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Thu, 18 Aug 2022 23:29:11 +0200 Subject: [PATCH 13/35] feat(event_handler): add support for multiple headers with same key --- .../event_handler/api_gateway.py | 33 ++++++++-------- .../shared/headers_serializer.py | 38 +++++++++++++------ .../src/fine_grained_responses.py | 2 +- .../event_handler/test_api_gateway.py | 4 +- .../event_handler/test_headers_serializer.py | 12 +++--- 5 files changed, 53 insertions(+), 36 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 70671c017a7..d89a13602ce 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -7,6 +7,7 @@ import warnings import zlib from abc import ABC, abstractmethod +from collections import defaultdict from enum import Enum from functools import partial from http import HTTPStatus @@ -122,18 +123,18 @@ def __init__( self.max_age = max_age self.allow_credentials = allow_credentials - def to_dict(self) -> Dict[str, str]: + def to_dict(self) -> Dict[str, List[str]]: """Builds the configured Access-Control http headers""" - headers = { - "Access-Control-Allow-Origin": self.allow_origin, - "Access-Control-Allow-Headers": ",".join(sorted(self.allow_headers)), - } + headers: Dict[str, List[str]] = defaultdict(list) + headers["Access-Control-Allow-Origin"].append(self.allow_origin) + headers["Access-Control-Allow-Headers"].append(",".join(sorted(self.allow_headers))) + if self.expose_headers: - headers["Access-Control-Expose-Headers"] = ",".join(self.expose_headers) + headers["Access-Control-Expose-Headers"].append(",".join(self.expose_headers)) if self.max_age is not None: - headers["Access-Control-Max-Age"] = str(self.max_age) + headers["Access-Control-Max-Age"].append(str(self.max_age)) if self.allow_credentials is True: - headers["Access-Control-Allow-Credentials"] = "true" + headers["Access-Control-Allow-Credentials"].append("true") return headers @@ -145,7 +146,7 @@ def __init__( status_code: int, content_type: Optional[str], body: Union[str, bytes, None], - headers: Optional[Dict[str, str]] = None, + headers: Optional[Dict[str, List[str]]] = None, cookies: Optional[List[str]] = None, ): """ @@ -159,7 +160,7 @@ def __init__( provided http headers body: Union[str, bytes, None] Optionally set the response body. Note: bytes body will be automatically base64 encoded - headers: dict[str, str] + headers: dict[str, List[str]] Optionally set specific http headers. Setting "Content-Type" here would override the `content_type` value. cookies: list[str] Optionally set cookies. @@ -167,10 +168,10 @@ def __init__( self.status_code = status_code self.body = body self.base64_encoded = False - self.headers: Dict[str, str] = headers or {} + self.headers: Dict[str, List[str]] = defaultdict(list, **headers) if headers else defaultdict(list) self.cookies = cookies or [] if content_type: - self.headers.setdefault("Content-Type", content_type) + self.headers.setdefault("Content-Type", [content_type]) class Route: @@ -200,11 +201,11 @@ def _add_cors(self, cors: CORSConfig): def _add_cache_control(self, cache_control: str): """Set the specified cache control headers for 200 http responses. For non-200 `no-cache` is used.""" - self.response.headers["Cache-Control"] = cache_control if self.response.status_code == 200 else "no-cache" + self.response.headers["Cache-Control"].append(cache_control if self.response.status_code == 200 else "no-cache") def _compress(self): """Compress the response body, but only if `Accept-Encoding` headers includes gzip.""" - self.response.headers["Content-Encoding"] = "gzip" + self.response.headers["Content-Encoding"].append("gzip") if isinstance(self.response.body, str): logger.debug("Converting string response to bytes before compressing it") self.response.body = bytes(self.response.body, "utf-8") @@ -602,14 +603,14 @@ def _path_starts_with(path: str, prefix: str): def _not_found(self, method: str) -> ResponseBuilder: """Called when no matching route was found and includes support for the cors preflight response""" - headers = {} + headers: Dict[str, List[str]] = defaultdict(list) if self._cors: logger.debug("CORS is enabled, updating headers.") headers.update(self._cors.to_dict()) if method == "OPTIONS": logger.debug("Pre-flight request detected. Returning CORS with null response") - headers["Access-Control-Allow-Methods"] = ",".join(sorted(self._cors_methods)) + headers["Access-Control-Allow-Methods"].append(",".join(sorted(self._cors_methods))) return ResponseBuilder(Response(status_code=204, content_type=None, headers=headers, body="")) handler = self._lookup_exception_handler(NotFoundError) diff --git a/aws_lambda_powertools/shared/headers_serializer.py b/aws_lambda_powertools/shared/headers_serializer.py index f0db3968c38..abf5955339c 100644 --- a/aws_lambda_powertools/shared/headers_serializer.py +++ b/aws_lambda_powertools/shared/headers_serializer.py @@ -1,5 +1,6 @@ import warnings from abc import ABC +from collections import defaultdict from typing import Any, Dict, List @@ -8,14 +9,14 @@ class BaseHeadersSerializer(ABC): Helper class to correctly serialize headers and cookies on the response payload. """ - def serialize(self, headers: Dict[str, str], cookies: List[str]) -> Dict[str, Any]: + def serialize(self, headers: Dict[str, List[str]], cookies: List[str]) -> Dict[str, Any]: """ Serializes headers and cookies according to the request type. Returns a dict that can be merged with the response payload. Parameters ---------- - headers: Dict[str, str] + headers: Dict[str, List[str]] A dictionary of headers to set in the response cookies: List[str] A list of cookies to set in the response @@ -24,7 +25,7 @@ def serialize(self, headers: Dict[str, str], cookies: List[str]) -> Dict[str, An class HttpApiSerializer(BaseHeadersSerializer): - def serialize(self, headers: Dict[str, str], cookies: List[str]) -> Dict[str, Any]: + def serialize(self, headers: Dict[str, List[str]], cookies: List[str]) -> Dict[str, Any]: """ When using HTTP APIs or LambdaFunctionURLs, everything is taken care automatically for us. We can directly assign a list of cookies and a dict of headers to the response payload, and the @@ -33,11 +34,18 @@ def serialize(self, headers: Dict[str, str], cookies: List[str]) -> Dict[str, An https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.proxy-format https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.response """ - return {"headers": headers, "cookies": cookies} + + # Format 2.0 doesn't have multiValueHeaders or multiValueQueryStringParameters fields. + # Duplicate headers are combined with commas and included in the headers field. + combined_headers: Dict[str, str] = {} + for key, values in headers.items(): + combined_headers[key] = ",".join(values) + + return {"headers": combined_headers, "cookies": cookies} class MultiValueHeadersSerializer(BaseHeadersSerializer): - def serialize(self, headers: Dict[str, str], cookies: List[str]) -> Dict[str, Any]: + def serialize(self, headers: Dict[str, List[str]], cookies: List[str]) -> Dict[str, Any]: """ When using REST APIs, headers can be encoded using the `multiValueHeaders` key on the response. This is also the case when using an ALB integration with the `multiValueHeaders` option enabled. @@ -46,10 +54,11 @@ def serialize(self, headers: Dict[str, str], cookies: List[str]) -> Dict[str, An https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers-response """ - payload: Dict[str, List[str]] = {} + payload: Dict[str, List[str]] = defaultdict(list) - for key, value in headers.items(): - payload[key] = [value] + for key, values in headers.items(): + for value in values: + payload[key].append(value) if cookies: payload.setdefault("Set-Cookie", []) @@ -60,7 +69,7 @@ def serialize(self, headers: Dict[str, str], cookies: List[str]) -> Dict[str, An class SingleValueHeadersSerializer(BaseHeadersSerializer): - def serialize(self, headers: Dict[str, str], cookies: List[str]) -> Dict[str, Any]: + def serialize(self, headers: Dict[str, List[str]], cookies: List[str]) -> Dict[str, Any]: """ The ALB integration has `multiValueHeaders` disabled by default. If we try to set multiple headers with the same key, or more than one cookie, print a warning. @@ -80,7 +89,14 @@ def serialize(self, headers: Dict[str, str], cookies: List[str]) -> Dict[str, An # We can only send one cookie, send the last one payload["headers"]["Set-Cookie"] = cookies[-1] - for key, value in headers.items(): - payload["headers"][key] = value + for key, values in headers.items(): + if len(values) > 1: + warnings.warn( + "Can't encode more than one header value for the same key in the response. " + "Did you enable multiValueHeaders on the ALB Target Group?" + ) + + # We can only set one header per key, send the last one + payload["headers"][key] = values[-1] return payload diff --git a/examples/event_handler_rest/src/fine_grained_responses.py b/examples/event_handler_rest/src/fine_grained_responses.py index e930aa32713..4892de9c798 100644 --- a/examples/event_handler_rest/src/fine_grained_responses.py +++ b/examples/event_handler_rest/src/fine_grained_responses.py @@ -19,7 +19,7 @@ def get_todos(): todos: requests.Response = requests.get("https://jsonplaceholder.typicode.com/todos") todos.raise_for_status() - custom_headers = {"X-Transaction-Id": f"{uuid4()}"} + custom_headers = {"X-Transaction-Id": [f"{uuid4()}"]} return Response( status_code=HTTPStatus.OK.value, # 200 diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index fce5e7fa047..74e8ae82daa 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -441,7 +441,7 @@ def rest_func() -> Response: status_code=404, content_type="used-if-not-set-in-header", body="Not found", - headers={"Content-Type": "header-content-type-wins", "custom": "value"}, + headers={"Content-Type": ["header-content-type-wins"], "custom": ["value"]}, ) # WHEN calling the event handler @@ -573,7 +573,7 @@ def custom_preflight(): status_code=200, content_type=content_types.TEXT_HTML, body="Foo", - headers={"Access-Control-Allow-Methods": "CUSTOM"}, + headers={"Access-Control-Allow-Methods": ["CUSTOM"]}, ) @app.route(method="CUSTOM", rule="/some-call", cors=True) diff --git a/tests/functional/event_handler/test_headers_serializer.py b/tests/functional/event_handler/test_headers_serializer.py index 794376059d3..228d227f61d 100644 --- a/tests/functional/event_handler/test_headers_serializer.py +++ b/tests/functional/event_handler/test_headers_serializer.py @@ -13,13 +13,13 @@ def test_headers_serializer_http_api(): payload = serializer.serialize(cookies=[], headers={}) assert payload == {"cookies": [], "headers": {}} - payload = serializer.serialize(cookies=[], headers={"Content-Type": "text/html"}) + payload = serializer.serialize(cookies=[], headers={"Content-Type": ["text/html"]}) assert payload == {"cookies": [], "headers": {"Content-Type": "text/html"}} payload = serializer.serialize(cookies=["UUID=12345"], headers={}) assert payload == {"cookies": ["UUID=12345"], "headers": {}} - payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": "bar,zbr"}) + payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": ["bar,zbr"]}) assert payload == {"cookies": ["UUID=12345", "SSID=0xdeadbeef"], "headers": {"Foo": "bar,zbr"}} @@ -29,13 +29,13 @@ def test_headers_serializer_multi_value_headers(): payload = serializer.serialize(cookies=[], headers={}) assert payload == {"multiValueHeaders": {}} - payload = serializer.serialize(cookies=[], headers={"Content-Type": "text/html"}) + payload = serializer.serialize(cookies=[], headers={"Content-Type": ["text/html"]}) assert payload == {"multiValueHeaders": {"Content-Type": ["text/html"]}} payload = serializer.serialize(cookies=["UUID=12345"], headers={}) assert payload == {"multiValueHeaders": {"Set-Cookie": ["UUID=12345"]}} - payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": "bar,zbr"}) + payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": ["bar,zbr"]}) assert payload == {"multiValueHeaders": {"Set-Cookie": ["UUID=12345", "SSID=0xdeadbeef"], "Foo": ["bar,zbr"]}} @@ -45,7 +45,7 @@ def test_headers_serializer_single_value_headers(): payload = serializer.serialize(cookies=[], headers={}) assert payload == {"headers": {}} - payload = serializer.serialize(cookies=[], headers={"Content-Type": "text/html"}) + payload = serializer.serialize(cookies=[], headers={"Content-Type": ["text/html"]}) assert payload == {"headers": {"Content-Type": "text/html"}} payload = serializer.serialize(cookies=["UUID=12345"], headers={}) @@ -54,7 +54,7 @@ def test_headers_serializer_single_value_headers(): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("default") - payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": "bar,zbr"}) + payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": ["bar,zbr"]}) assert payload == {"headers": {"Set-Cookie": "SSID=0xdeadbeef", "Foo": "bar,zbr"}} assert len(w) == 1 From a705d26e71f7f7431b195cb0520d9c849c44aace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Thu, 18 Aug 2022 23:31:55 +0200 Subject: [PATCH 14/35] chore(tests): fix headers test --- .../event_handler/test_headers_serializer.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/functional/event_handler/test_headers_serializer.py b/tests/functional/event_handler/test_headers_serializer.py index 228d227f61d..25f84f8cdea 100644 --- a/tests/functional/event_handler/test_headers_serializer.py +++ b/tests/functional/event_handler/test_headers_serializer.py @@ -19,7 +19,7 @@ def test_headers_serializer_http_api(): payload = serializer.serialize(cookies=["UUID=12345"], headers={}) assert payload == {"cookies": ["UUID=12345"], "headers": {}} - payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": ["bar,zbr"]}) + payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": ["bar", "zbr"]}) assert payload == {"cookies": ["UUID=12345", "SSID=0xdeadbeef"], "headers": {"Foo": "bar,zbr"}} @@ -35,8 +35,8 @@ def test_headers_serializer_multi_value_headers(): payload = serializer.serialize(cookies=["UUID=12345"], headers={}) assert payload == {"multiValueHeaders": {"Set-Cookie": ["UUID=12345"]}} - payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": ["bar,zbr"]}) - assert payload == {"multiValueHeaders": {"Set-Cookie": ["UUID=12345", "SSID=0xdeadbeef"], "Foo": ["bar,zbr"]}} + payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": ["bar", "zbr"]}) + assert payload == {"multiValueHeaders": {"Set-Cookie": ["UUID=12345", "SSID=0xdeadbeef"], "Foo": ["bar", "zbr"]}} def test_headers_serializer_single_value_headers(): @@ -54,11 +54,15 @@ def test_headers_serializer_single_value_headers(): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("default") - payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": ["bar,zbr"]}) - assert payload == {"headers": {"Set-Cookie": "SSID=0xdeadbeef", "Foo": "bar,zbr"}} + payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": ["bar", "zbr"]}) + assert payload == {"headers": {"Set-Cookie": "SSID=0xdeadbeef", "Foo": "zbr"}} - assert len(w) == 1 - assert str(w[-1].message) == ( + assert len(w) == 2 + assert str(w[-2].message) == ( "Can't encode more than one cookie in the response. " "Did you enable multiValueHeaders on the ALB Target Group?" ) + assert str(w[-1].message) == ( + "Can't encode more than one header value for the same key in the response. " + "Did you enable multiValueHeaders on the ALB Target Group?" + ) From 8bb265861c1892ade1a39d0aac8af6847cb2e67a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Thu, 18 Aug 2022 23:33:19 +0200 Subject: [PATCH 15/35] chore(tests): move headers test to the correct place --- tests/functional/{event_handler => }/test_headers_serializer.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/functional/{event_handler => }/test_headers_serializer.py (100%) diff --git a/tests/functional/event_handler/test_headers_serializer.py b/tests/functional/test_headers_serializer.py similarity index 100% rename from tests/functional/event_handler/test_headers_serializer.py rename to tests/functional/test_headers_serializer.py From e702ab826f75ba45999d24c2efe5879778677aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Fri, 19 Aug 2022 13:30:05 +0200 Subject: [PATCH 16/35] tests(event_handler): add first e2e test --- tests/e2e/event_handler/__init__.py | 0 tests/e2e/event_handler/conftest.py | 27 ++++++++++++++++++ .../e2e/event_handler/handlers/alb_handler.py | 18 ++++++++++++ tests/e2e/event_handler/infrastructure.py | 25 +++++++++++++++++ .../event_handler/test_header_serializer.py | 28 +++++++++++++++++++ tests/e2e/utils/data_fetcher/__init__.py | 2 +- tests/e2e/utils/data_fetcher/common.py | 6 ++++ tests/e2e/utils/infrastructure.py | 13 ++++++++- 8 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/event_handler/__init__.py create mode 100644 tests/e2e/event_handler/conftest.py create mode 100644 tests/e2e/event_handler/handlers/alb_handler.py create mode 100644 tests/e2e/event_handler/infrastructure.py create mode 100644 tests/e2e/event_handler/test_header_serializer.py diff --git a/tests/e2e/event_handler/__init__.py b/tests/e2e/event_handler/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/e2e/event_handler/conftest.py b/tests/e2e/event_handler/conftest.py new file mode 100644 index 00000000000..32ea6820bb6 --- /dev/null +++ b/tests/e2e/event_handler/conftest.py @@ -0,0 +1,27 @@ +import pytest + +from tests.e2e.event_handler.infrastructure import EventHandlerStack +from tests.e2e.utils.infrastructure import deploy_once + + +@pytest.fixture(autouse=True, scope="module") +def infrastructure(request: pytest.FixtureRequest, tmp_path_factory: pytest.TempPathFactory, worker_id: str): + """Setup and teardown logic for E2E test infrastructure + + Parameters + ---------- + request : pytest.FixtureRequest + pytest request fixture to introspect absolute path to test being executed + tmp_path_factory : pytest.TempPathFactory + pytest temporary path factory to discover shared tmp when multiple CPU processes are spun up + worker_id : str + pytest-xdist worker identification to detect whether parallelization is enabled + + Yields + ------ + Dict[str, str] + CloudFormation Outputs from deployed infrastructure + """ + yield from deploy_once( + stack=EventHandlerStack, request=request, tmp_path_factory=tmp_path_factory, worker_id=worker_id + ) diff --git a/tests/e2e/event_handler/handlers/alb_handler.py b/tests/e2e/event_handler/handlers/alb_handler.py new file mode 100644 index 00000000000..8043ab85e7b --- /dev/null +++ b/tests/e2e/event_handler/handlers/alb_handler.py @@ -0,0 +1,18 @@ +from aws_lambda_powertools.event_handler import ALBResolver, Response, content_types + +app = ALBResolver() + + +@app.route("/todos", method=["GET"]) +def hello(): + return Response( + status_code=200, + content_type=content_types.TEXT_PLAIN, + body="Hello world", + cookies=["CookieMonster; Secure", "MonsterCookie"], + headers={"Foo": ["bar", "zbr"]}, + ) + + +def lambda_handler(event, context): + return app.resolve(event, context) diff --git a/tests/e2e/event_handler/infrastructure.py b/tests/e2e/event_handler/infrastructure.py new file mode 100644 index 00000000000..b43925c3b56 --- /dev/null +++ b/tests/e2e/event_handler/infrastructure.py @@ -0,0 +1,25 @@ +from pathlib import Path + +from aws_cdk import CfnOutput +from aws_cdk import aws_ec2 as ec2 +from aws_cdk import aws_elasticloadbalancingv2 as elbv2 +from aws_cdk import aws_elasticloadbalancingv2_targets as targets + +from tests.e2e.utils.infrastructure import BaseInfrastructureV2 + + +class EventHandlerStack(BaseInfrastructureV2): + def __init__(self, handlers_dir: Path, feature_name: str = "event-handlers") -> None: + super().__init__(feature_name, handlers_dir) + + def create_resources(self): + functions = self.create_lambda_functions() + api_gateway_http_handler_function = functions["AlbHandler"] + + vpc = ec2.Vpc(self.stack, "EventHandlerVPC", max_azs=2) + + alb = elbv2.ApplicationLoadBalancer(self.stack, "ALB", vpc=vpc, internet_facing=True) + listener = alb.add_listener("HTTPListener", port=80) + listener.add_targets("HTTPListenerTarget", targets=[targets.LambdaTarget(api_gateway_http_handler_function)]) + + CfnOutput(self.stack, "ALBDnsName", value=alb.load_balancer_dns_name) diff --git a/tests/e2e/event_handler/test_header_serializer.py b/tests/e2e/event_handler/test_header_serializer.py new file mode 100644 index 00000000000..aa4026b2632 --- /dev/null +++ b/tests/e2e/event_handler/test_header_serializer.py @@ -0,0 +1,28 @@ +import pytest +from requests import Request + +from tests.e2e.utils import data_fetcher + + +@pytest.fixture +def alb_endpoint(infrastructure: dict) -> str: + dns_name = infrastructure.get("ALBDnsName") + return f"http://{dns_name}" + + +def test_alb_headers_serializer(alb_endpoint): + # GIVEN + url = f"{alb_endpoint}/todos" + + # WHEN + response = data_fetcher.get_http_response(Request(method="GET", url=url)) + + # THEN + assert response.status_code == 200 + assert response.content == "Hello world" + assert response.headers["content-type"] == "text/plain" + + assert response.headers["Foo"] == "zbr" + + assert "MonsterCookie" in response.cookies + assert "CookieMonster; Secure" not in response.cookies diff --git a/tests/e2e/utils/data_fetcher/__init__.py b/tests/e2e/utils/data_fetcher/__init__.py index 43024f9946f..be6909537e5 100644 --- a/tests/e2e/utils/data_fetcher/__init__.py +++ b/tests/e2e/utils/data_fetcher/__init__.py @@ -1,4 +1,4 @@ -from tests.e2e.utils.data_fetcher.common import get_lambda_response +from tests.e2e.utils.data_fetcher.common import get_http_response, get_lambda_response from tests.e2e.utils.data_fetcher.logs import get_logs from tests.e2e.utils.data_fetcher.metrics import get_metrics from tests.e2e.utils.data_fetcher.traces import get_traces diff --git a/tests/e2e/utils/data_fetcher/common.py b/tests/e2e/utils/data_fetcher/common.py index 2de8838dc74..d38f185544a 100644 --- a/tests/e2e/utils/data_fetcher/common.py +++ b/tests/e2e/utils/data_fetcher/common.py @@ -2,6 +2,7 @@ from typing import Optional, Tuple import boto3 +import requests as requests from mypy_boto3_lambda import LambdaClient from mypy_boto3_lambda.type_defs import InvocationResponseTypeDef @@ -13,3 +14,8 @@ def get_lambda_response( payload = payload or "" execution_time = datetime.utcnow() return client.invoke(FunctionName=lambda_arn, InvocationType="RequestResponse", Payload=payload), execution_time + + +def get_http_response(request: requests.Request) -> requests.Response: + session = requests.Session() + return session.send(request.prepare()) diff --git a/tests/e2e/utils/infrastructure.py b/tests/e2e/utils/infrastructure.py index 7f232bb063f..610f9115525 100644 --- a/tests/e2e/utils/infrastructure.py +++ b/tests/e2e/utils/infrastructure.py @@ -57,7 +57,7 @@ def __init__(self, feature_name: str, handlers_dir: Path, layer_arn: str = "") - self.account_id = self.session.client("sts").get_caller_identity()["Account"] self.region = self.session.region_name - def create_lambda_functions(self, function_props: Optional[Dict] = None): + def create_lambda_functions(self, function_props: Optional[Dict] = None) -> Dict[str, Function]: """Create Lambda functions available under handlers_dir It creates CloudFormation Outputs for every function found in PascalCase. For example, @@ -69,6 +69,11 @@ def create_lambda_functions(self, function_props: Optional[Dict] = None): function_props: Optional[Dict] Dictionary representing CDK Lambda FunctionProps to override defaults + Returns + ------- + output: Dict[str, Function] + A dict with PascalCased function names and the corresponding CDK Function object + Examples -------- @@ -97,6 +102,8 @@ def create_lambda_functions(self, function_props: Optional[Dict] = None): layer = LayerVersion.from_layer_version_arn(self.stack, "layer-arn", layer_version_arn=self.layer_arn) function_settings_override = function_props or {} + output: Dict[str, Function] = {} + for fn in handlers: fn_name = fn.stem fn_name_pascal_case = fn_name.title().replace("_", "") # basic_handler -> BasicHandler @@ -124,6 +131,10 @@ def create_lambda_functions(self, function_props: Optional[Dict] = None): # CFN Outputs only support hyphen hence pascal case self.add_cfn_output(name=fn_name_pascal_case, value=function.function_name, arn=function.function_arn) + output[fn_name_pascal_case] = function + + return output + def deploy(self) -> Dict[str, str]: """Creates CloudFormation Stack and return stack outputs as dict From 7e169a9c5bf988a4bc0ac76023312b6a1bf73665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Mon, 22 Aug 2022 14:44:55 +0200 Subject: [PATCH 17/35] chore(docs): initial upgrade guide for v2 --- docs/upgrade.md | 62 +++++++++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 63 insertions(+) create mode 100644 docs/upgrade.md diff --git a/docs/upgrade.md b/docs/upgrade.md new file mode 100644 index 00000000000..e65c1bbfe91 --- /dev/null +++ b/docs/upgrade.md @@ -0,0 +1,62 @@ +--- +title: Upgrade guide +description: asdfasdf +--- + + + +## Migrate to v2 from v1 + +The transition from Powertools for Python v1 to v2 is as painless as possible, as we strove for minimal breaking changes. +The API for event handler's Response has minor changes, but we kept the breaking changes to a bare minimum. We've also added some new features to some components. + +???+ important + Powertools for Python v2 drops suport for Python 3.6, following the Python 3.6 End-Of-Life (EOL) reached on December 23, 2021. + +### Initial Steps + +Before starting, it is highly suggested to make a copy of your current working project or create a new branch with git. + +1. **Upgrade** Python to at least v3.7 + +2. **Ensure** you have the latest `aws-lambda-powertools` + + ```bash + pip install aws-lambda-powertools -U + ``` + +3. **Check** the following sections to see if any of your code is affected + +## Event Handler Response (headers and cookies) + +The `Response` class of the event handler utility was changed slightly: + +1. The `headers` parameter now has a type signature of `Dict[str, List[str]]` +2. A new `cookies` parameter was added (type `List[str]`) + +```python hl_lines="6 12 13" +@app.get("/todos") +def get_todos(): + # Before + return Response( + # ... + headers={"Content-Type": "text/plain"} + ) + + # After + return Response( + # ... + headers={"Content-Type": ["text/plain"]}, + cookies=["CookieName=CookieValue"] + ) +``` + +In the same way, it can be more convenient to just append headers to the response object: + +```python hl_lines="4 5" +@app.get("/todos") +def get_todos(): + response = Response(...) + response.headers["Content-Type"].append("text/plain") + response.cookies.append("CookieName=CookieValue") +``` diff --git a/mkdocs.yml b/mkdocs.yml index 171cf36eb13..59fcdfa6a08 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,6 +10,7 @@ nav: - Tutorial: tutorial/index.md - Roadmap: roadmap.md - API reference: api/" target="_blank + - Upgrade guide: upgrade.md - Core utilities: - core/tracer.md - core/logger.md From 6faeb914bf973cfc34901e129791ee43d208dd7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Mon, 15 Aug 2022 21:04:42 +0200 Subject: [PATCH 18/35] chore(tests): move load_event helper to global utils --- tests/functional/data_classes/test_amazon_mq.py | 2 +- tests/functional/data_classes/test_lambda_function_url.py | 2 +- tests/functional/event_handler/test_api_gateway.py | 2 +- tests/functional/event_handler/test_appsync.py | 2 +- tests/functional/event_handler/test_lambda_function_url.py | 2 +- tests/functional/idempotency/conftest.py | 3 ++- tests/functional/idempotency/test_idempotency.py | 3 ++- tests/functional/parser/test_alb.py | 2 +- tests/functional/parser/test_apigw.py | 2 +- tests/functional/parser/test_apigwv2.py | 2 +- tests/functional/parser/test_cloudwatch.py | 2 +- tests/functional/parser/test_dynamodb.py | 2 +- tests/functional/parser/test_eventbridge.py | 2 +- tests/functional/parser/test_kinesis.py | 2 +- tests/functional/parser/test_s3 object_event.py | 2 +- tests/functional/parser/test_s3.py | 2 +- tests/functional/parser/test_ses.py | 2 +- tests/functional/parser/test_sns.py | 2 +- tests/functional/parser/test_sqs.py | 2 +- tests/functional/test_data_classes.py | 2 +- tests/functional/utils.py | 7 ------- 21 files changed, 22 insertions(+), 27 deletions(-) diff --git a/tests/functional/data_classes/test_amazon_mq.py b/tests/functional/data_classes/test_amazon_mq.py index a88a962c17b..9e21aa10ebf 100644 --- a/tests/functional/data_classes/test_amazon_mq.py +++ b/tests/functional/data_classes/test_amazon_mq.py @@ -2,7 +2,7 @@ from aws_lambda_powertools.utilities.data_classes.active_mq_event import ActiveMQEvent, ActiveMQMessage from aws_lambda_powertools.utilities.data_classes.rabbit_mq_event import BasicProperties, RabbitMessage, RabbitMQEvent -from tests.functional.utils import load_event +from tests.utils import load_event def test_active_mq_event(): diff --git a/tests/functional/data_classes/test_lambda_function_url.py b/tests/functional/data_classes/test_lambda_function_url.py index c27920c3392..ffc215a99ff 100644 --- a/tests/functional/data_classes/test_lambda_function_url.py +++ b/tests/functional/data_classes/test_lambda_function_url.py @@ -1,5 +1,5 @@ from aws_lambda_powertools.utilities.data_classes import LambdaFunctionUrlEvent -from tests.functional.utils import load_event +from tests.utils import load_event def test_lambda_function_url_event(): diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 74e8ae82daa..caeeba58baf 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -37,7 +37,7 @@ APIGatewayProxyEventV2, event_source, ) -from tests.functional.utils import load_event +from tests.utils import load_event @pytest.fixture diff --git a/tests/functional/event_handler/test_appsync.py b/tests/functional/event_handler/test_appsync.py index 79173e55825..329ac9ee985 100644 --- a/tests/functional/event_handler/test_appsync.py +++ b/tests/functional/event_handler/test_appsync.py @@ -7,7 +7,7 @@ from aws_lambda_powertools.event_handler.appsync import Router from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.functional.utils import load_event +from tests.utils import load_event def test_direct_resolver(): diff --git a/tests/functional/event_handler/test_lambda_function_url.py b/tests/functional/event_handler/test_lambda_function_url.py index c24c9f0e0c8..282f49bb2b4 100644 --- a/tests/functional/event_handler/test_lambda_function_url.py +++ b/tests/functional/event_handler/test_lambda_function_url.py @@ -1,6 +1,6 @@ from aws_lambda_powertools.event_handler import LambdaFunctionUrlResolver, Response, content_types from aws_lambda_powertools.utilities.data_classes import LambdaFunctionUrlEvent -from tests.functional.utils import load_event +from tests.utils import load_event def test_lambda_function_url_event(): diff --git a/tests/functional/idempotency/conftest.py b/tests/functional/idempotency/conftest.py index b5cf79727b1..328900cda07 100644 --- a/tests/functional/idempotency/conftest.py +++ b/tests/functional/idempotency/conftest.py @@ -14,7 +14,8 @@ from aws_lambda_powertools.utilities.jmespath_utils import extract_data_from_envelope from aws_lambda_powertools.utilities.validation import envelopes from tests.functional.idempotency.utils import hash_idempotency_key -from tests.functional.utils import json_serialize, load_event +from tests.functional.utils import json_serialize +from tests.utils import load_event TABLE_NAME = "TEST_TABLE" diff --git a/tests/functional/idempotency/test_idempotency.py b/tests/functional/idempotency/test_idempotency.py index 97a9166efa0..3fd8ee619fc 100644 --- a/tests/functional/idempotency/test_idempotency.py +++ b/tests/functional/idempotency/test_idempotency.py @@ -29,7 +29,8 @@ build_idempotency_update_item_stub, hash_idempotency_key, ) -from tests.functional.utils import json_serialize, load_event +from tests.functional.utils import json_serialize +from tests.utils import load_event TABLE_NAME = "TEST_TABLE" diff --git a/tests/functional/parser/test_alb.py b/tests/functional/parser/test_alb.py index d48e39f1bab..c03066c140f 100644 --- a/tests/functional/parser/test_alb.py +++ b/tests/functional/parser/test_alb.py @@ -3,7 +3,7 @@ from aws_lambda_powertools.utilities.parser import ValidationError, event_parser from aws_lambda_powertools.utilities.parser.models import AlbModel from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=AlbModel) diff --git a/tests/functional/parser/test_apigw.py b/tests/functional/parser/test_apigw.py index 35b2fdb1926..8e08869d9f3 100644 --- a/tests/functional/parser/test_apigw.py +++ b/tests/functional/parser/test_apigw.py @@ -5,7 +5,7 @@ from aws_lambda_powertools.utilities.parser.models import APIGatewayProxyEventModel from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyApiGatewayBusiness -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=MyApiGatewayBusiness, envelope=envelopes.ApiGatewayEnvelope) diff --git a/tests/functional/parser/test_apigwv2.py b/tests/functional/parser/test_apigwv2.py index d3510b185dd..676fe4bcf5d 100644 --- a/tests/functional/parser/test_apigwv2.py +++ b/tests/functional/parser/test_apigwv2.py @@ -6,7 +6,7 @@ ) from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyApiGatewayBusiness -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=MyApiGatewayBusiness, envelope=envelopes.ApiGatewayV2Envelope) diff --git a/tests/functional/parser/test_cloudwatch.py b/tests/functional/parser/test_cloudwatch.py index 7290d0bffcb..8ea2441100f 100644 --- a/tests/functional/parser/test_cloudwatch.py +++ b/tests/functional/parser/test_cloudwatch.py @@ -9,7 +9,7 @@ from aws_lambda_powertools.utilities.parser.models import CloudWatchLogsLogEvent, CloudWatchLogsModel from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyCloudWatchBusiness -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=MyCloudWatchBusiness, envelope=envelopes.CloudWatchLogsEnvelope) diff --git a/tests/functional/parser/test_dynamodb.py b/tests/functional/parser/test_dynamodb.py index 9917fac234b..c3dfee65db3 100644 --- a/tests/functional/parser/test_dynamodb.py +++ b/tests/functional/parser/test_dynamodb.py @@ -5,7 +5,7 @@ from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, event_parser from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyAdvancedDynamoBusiness, MyDynamoBusiness -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=MyDynamoBusiness, envelope=envelopes.DynamoDBStreamEnvelope) diff --git a/tests/functional/parser/test_eventbridge.py b/tests/functional/parser/test_eventbridge.py index 6242403ab35..93188a5c59f 100644 --- a/tests/functional/parser/test_eventbridge.py +++ b/tests/functional/parser/test_eventbridge.py @@ -5,7 +5,7 @@ from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, event_parser from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyAdvancedEventbridgeBusiness, MyEventbridgeBusiness -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=MyEventbridgeBusiness, envelope=envelopes.EventBridgeEnvelope) diff --git a/tests/functional/parser/test_kinesis.py b/tests/functional/parser/test_kinesis.py index 552cb6cef68..a9aa59dd9a6 100644 --- a/tests/functional/parser/test_kinesis.py +++ b/tests/functional/parser/test_kinesis.py @@ -6,7 +6,7 @@ from aws_lambda_powertools.utilities.parser.models import KinesisDataStreamModel, KinesisDataStreamRecordPayload from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyKinesisBusiness -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=MyKinesisBusiness, envelope=envelopes.KinesisDataStreamEnvelope) diff --git a/tests/functional/parser/test_s3 object_event.py b/tests/functional/parser/test_s3 object_event.py index 90c2555360d..03ecf227139 100644 --- a/tests/functional/parser/test_s3 object_event.py +++ b/tests/functional/parser/test_s3 object_event.py @@ -1,7 +1,7 @@ from aws_lambda_powertools.utilities.parser import event_parser from aws_lambda_powertools.utilities.parser.models import S3ObjectLambdaEvent from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=S3ObjectLambdaEvent) diff --git a/tests/functional/parser/test_s3.py b/tests/functional/parser/test_s3.py index 71a5dc6afe3..77e0adb7b4a 100644 --- a/tests/functional/parser/test_s3.py +++ b/tests/functional/parser/test_s3.py @@ -1,7 +1,7 @@ from aws_lambda_powertools.utilities.parser import event_parser, parse from aws_lambda_powertools.utilities.parser.models import S3Model, S3RecordModel from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=S3Model) diff --git a/tests/functional/parser/test_ses.py b/tests/functional/parser/test_ses.py index d434e2350f8..09f6ff6b76d 100644 --- a/tests/functional/parser/test_ses.py +++ b/tests/functional/parser/test_ses.py @@ -1,7 +1,7 @@ from aws_lambda_powertools.utilities.parser import event_parser from aws_lambda_powertools.utilities.parser.models import SesModel, SesRecordModel from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.functional.utils import load_event +from tests.utils import load_event @event_parser(model=SesModel) diff --git a/tests/functional/parser/test_sns.py b/tests/functional/parser/test_sns.py index b0d9ff69a9b..992871126bc 100644 --- a/tests/functional/parser/test_sns.py +++ b/tests/functional/parser/test_sns.py @@ -6,8 +6,8 @@ from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, event_parser from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyAdvancedSnsBusiness, MySnsBusiness -from tests.functional.utils import load_event from tests.functional.validator.conftest import sns_event # noqa: F401 +from tests.utils import load_event @event_parser(model=MySnsBusiness, envelope=envelopes.SnsEnvelope) diff --git a/tests/functional/parser/test_sqs.py b/tests/functional/parser/test_sqs.py index 7ca883616f2..93b2fca5d6c 100644 --- a/tests/functional/parser/test_sqs.py +++ b/tests/functional/parser/test_sqs.py @@ -5,8 +5,8 @@ from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, event_parser from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyAdvancedSqsBusiness, MySqsBusiness -from tests.functional.utils import load_event from tests.functional.validator.conftest import sqs_event # noqa: F401 +from tests.utils import load_event @event_parser(model=MySqsBusiness, envelope=envelopes.SqsEnvelope) diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py index 8a87075d16c..fe543db34cb 100644 --- a/tests/functional/test_data_classes.py +++ b/tests/functional/test_data_classes.py @@ -76,7 +76,7 @@ ) from aws_lambda_powertools.utilities.data_classes.event_source import event_source from aws_lambda_powertools.utilities.data_classes.s3_object_event import S3ObjectLambdaEvent -from tests.functional.utils import load_event +from tests.utils import load_event def test_dict_wrapper_equals(): diff --git a/tests/functional/utils.py b/tests/functional/utils.py index 6b73053e0d0..c07f4fe32cc 100644 --- a/tests/functional/utils.py +++ b/tests/functional/utils.py @@ -1,16 +1,9 @@ import base64 import json -from pathlib import Path -from typing import Any from aws_lambda_powertools.shared.json_encoder import Encoder -def load_event(file_name: str) -> Any: - path = Path(str(Path(__file__).parent.parent) + "/events/" + file_name) - return json.loads(path.read_text()) - - def str_to_b64(data: str) -> str: return base64.b64encode(data.encode()).decode("utf-8") From ba039f8d384582e92355f9baeffb47764e212e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Tue, 16 Aug 2022 11:44:40 +0200 Subject: [PATCH 19/35] Revert "chore(tests): move load_event helper to global utils" This reverts commit cb1797f0e602f1a5ba0a49ebfbb6e303a07270c2. --- tests/functional/data_classes/test_amazon_mq.py | 2 +- tests/functional/data_classes/test_lambda_function_url.py | 2 +- tests/functional/event_handler/test_api_gateway.py | 2 +- tests/functional/event_handler/test_appsync.py | 2 +- tests/functional/event_handler/test_lambda_function_url.py | 2 +- tests/functional/idempotency/conftest.py | 3 +-- tests/functional/idempotency/test_idempotency.py | 3 +-- tests/functional/parser/test_alb.py | 2 +- tests/functional/parser/test_apigw.py | 2 +- tests/functional/parser/test_apigwv2.py | 2 +- tests/functional/parser/test_cloudwatch.py | 2 +- tests/functional/parser/test_dynamodb.py | 2 +- tests/functional/parser/test_eventbridge.py | 2 +- tests/functional/parser/test_kinesis.py | 2 +- tests/functional/parser/test_s3 object_event.py | 2 +- tests/functional/parser/test_s3.py | 2 +- tests/functional/parser/test_ses.py | 2 +- tests/functional/parser/test_sns.py | 2 +- tests/functional/parser/test_sqs.py | 2 +- tests/functional/test_data_classes.py | 2 +- tests/functional/utils.py | 7 +++++++ 21 files changed, 27 insertions(+), 22 deletions(-) diff --git a/tests/functional/data_classes/test_amazon_mq.py b/tests/functional/data_classes/test_amazon_mq.py index 9e21aa10ebf..a88a962c17b 100644 --- a/tests/functional/data_classes/test_amazon_mq.py +++ b/tests/functional/data_classes/test_amazon_mq.py @@ -2,7 +2,7 @@ from aws_lambda_powertools.utilities.data_classes.active_mq_event import ActiveMQEvent, ActiveMQMessage from aws_lambda_powertools.utilities.data_classes.rabbit_mq_event import BasicProperties, RabbitMessage, RabbitMQEvent -from tests.utils import load_event +from tests.functional.utils import load_event def test_active_mq_event(): diff --git a/tests/functional/data_classes/test_lambda_function_url.py b/tests/functional/data_classes/test_lambda_function_url.py index ffc215a99ff..c27920c3392 100644 --- a/tests/functional/data_classes/test_lambda_function_url.py +++ b/tests/functional/data_classes/test_lambda_function_url.py @@ -1,5 +1,5 @@ from aws_lambda_powertools.utilities.data_classes import LambdaFunctionUrlEvent -from tests.utils import load_event +from tests.functional.utils import load_event def test_lambda_function_url_event(): diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index caeeba58baf..74e8ae82daa 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -37,7 +37,7 @@ APIGatewayProxyEventV2, event_source, ) -from tests.utils import load_event +from tests.functional.utils import load_event @pytest.fixture diff --git a/tests/functional/event_handler/test_appsync.py b/tests/functional/event_handler/test_appsync.py index 329ac9ee985..79173e55825 100644 --- a/tests/functional/event_handler/test_appsync.py +++ b/tests/functional/event_handler/test_appsync.py @@ -7,7 +7,7 @@ from aws_lambda_powertools.event_handler.appsync import Router from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.utils import load_event +from tests.functional.utils import load_event def test_direct_resolver(): diff --git a/tests/functional/event_handler/test_lambda_function_url.py b/tests/functional/event_handler/test_lambda_function_url.py index 282f49bb2b4..c24c9f0e0c8 100644 --- a/tests/functional/event_handler/test_lambda_function_url.py +++ b/tests/functional/event_handler/test_lambda_function_url.py @@ -1,6 +1,6 @@ from aws_lambda_powertools.event_handler import LambdaFunctionUrlResolver, Response, content_types from aws_lambda_powertools.utilities.data_classes import LambdaFunctionUrlEvent -from tests.utils import load_event +from tests.functional.utils import load_event def test_lambda_function_url_event(): diff --git a/tests/functional/idempotency/conftest.py b/tests/functional/idempotency/conftest.py index 328900cda07..b5cf79727b1 100644 --- a/tests/functional/idempotency/conftest.py +++ b/tests/functional/idempotency/conftest.py @@ -14,8 +14,7 @@ from aws_lambda_powertools.utilities.jmespath_utils import extract_data_from_envelope from aws_lambda_powertools.utilities.validation import envelopes from tests.functional.idempotency.utils import hash_idempotency_key -from tests.functional.utils import json_serialize -from tests.utils import load_event +from tests.functional.utils import json_serialize, load_event TABLE_NAME = "TEST_TABLE" diff --git a/tests/functional/idempotency/test_idempotency.py b/tests/functional/idempotency/test_idempotency.py index 3fd8ee619fc..97a9166efa0 100644 --- a/tests/functional/idempotency/test_idempotency.py +++ b/tests/functional/idempotency/test_idempotency.py @@ -29,8 +29,7 @@ build_idempotency_update_item_stub, hash_idempotency_key, ) -from tests.functional.utils import json_serialize -from tests.utils import load_event +from tests.functional.utils import json_serialize, load_event TABLE_NAME = "TEST_TABLE" diff --git a/tests/functional/parser/test_alb.py b/tests/functional/parser/test_alb.py index c03066c140f..d48e39f1bab 100644 --- a/tests/functional/parser/test_alb.py +++ b/tests/functional/parser/test_alb.py @@ -3,7 +3,7 @@ from aws_lambda_powertools.utilities.parser import ValidationError, event_parser from aws_lambda_powertools.utilities.parser.models import AlbModel from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=AlbModel) diff --git a/tests/functional/parser/test_apigw.py b/tests/functional/parser/test_apigw.py index 8e08869d9f3..35b2fdb1926 100644 --- a/tests/functional/parser/test_apigw.py +++ b/tests/functional/parser/test_apigw.py @@ -5,7 +5,7 @@ from aws_lambda_powertools.utilities.parser.models import APIGatewayProxyEventModel from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyApiGatewayBusiness -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=MyApiGatewayBusiness, envelope=envelopes.ApiGatewayEnvelope) diff --git a/tests/functional/parser/test_apigwv2.py b/tests/functional/parser/test_apigwv2.py index 676fe4bcf5d..d3510b185dd 100644 --- a/tests/functional/parser/test_apigwv2.py +++ b/tests/functional/parser/test_apigwv2.py @@ -6,7 +6,7 @@ ) from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyApiGatewayBusiness -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=MyApiGatewayBusiness, envelope=envelopes.ApiGatewayV2Envelope) diff --git a/tests/functional/parser/test_cloudwatch.py b/tests/functional/parser/test_cloudwatch.py index 8ea2441100f..7290d0bffcb 100644 --- a/tests/functional/parser/test_cloudwatch.py +++ b/tests/functional/parser/test_cloudwatch.py @@ -9,7 +9,7 @@ from aws_lambda_powertools.utilities.parser.models import CloudWatchLogsLogEvent, CloudWatchLogsModel from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyCloudWatchBusiness -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=MyCloudWatchBusiness, envelope=envelopes.CloudWatchLogsEnvelope) diff --git a/tests/functional/parser/test_dynamodb.py b/tests/functional/parser/test_dynamodb.py index c3dfee65db3..9917fac234b 100644 --- a/tests/functional/parser/test_dynamodb.py +++ b/tests/functional/parser/test_dynamodb.py @@ -5,7 +5,7 @@ from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, event_parser from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyAdvancedDynamoBusiness, MyDynamoBusiness -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=MyDynamoBusiness, envelope=envelopes.DynamoDBStreamEnvelope) diff --git a/tests/functional/parser/test_eventbridge.py b/tests/functional/parser/test_eventbridge.py index 93188a5c59f..6242403ab35 100644 --- a/tests/functional/parser/test_eventbridge.py +++ b/tests/functional/parser/test_eventbridge.py @@ -5,7 +5,7 @@ from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, event_parser from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyAdvancedEventbridgeBusiness, MyEventbridgeBusiness -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=MyEventbridgeBusiness, envelope=envelopes.EventBridgeEnvelope) diff --git a/tests/functional/parser/test_kinesis.py b/tests/functional/parser/test_kinesis.py index a9aa59dd9a6..552cb6cef68 100644 --- a/tests/functional/parser/test_kinesis.py +++ b/tests/functional/parser/test_kinesis.py @@ -6,7 +6,7 @@ from aws_lambda_powertools.utilities.parser.models import KinesisDataStreamModel, KinesisDataStreamRecordPayload from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyKinesisBusiness -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=MyKinesisBusiness, envelope=envelopes.KinesisDataStreamEnvelope) diff --git a/tests/functional/parser/test_s3 object_event.py b/tests/functional/parser/test_s3 object_event.py index 03ecf227139..90c2555360d 100644 --- a/tests/functional/parser/test_s3 object_event.py +++ b/tests/functional/parser/test_s3 object_event.py @@ -1,7 +1,7 @@ from aws_lambda_powertools.utilities.parser import event_parser from aws_lambda_powertools.utilities.parser.models import S3ObjectLambdaEvent from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=S3ObjectLambdaEvent) diff --git a/tests/functional/parser/test_s3.py b/tests/functional/parser/test_s3.py index 77e0adb7b4a..71a5dc6afe3 100644 --- a/tests/functional/parser/test_s3.py +++ b/tests/functional/parser/test_s3.py @@ -1,7 +1,7 @@ from aws_lambda_powertools.utilities.parser import event_parser, parse from aws_lambda_powertools.utilities.parser.models import S3Model, S3RecordModel from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=S3Model) diff --git a/tests/functional/parser/test_ses.py b/tests/functional/parser/test_ses.py index 09f6ff6b76d..d434e2350f8 100644 --- a/tests/functional/parser/test_ses.py +++ b/tests/functional/parser/test_ses.py @@ -1,7 +1,7 @@ from aws_lambda_powertools.utilities.parser import event_parser from aws_lambda_powertools.utilities.parser.models import SesModel, SesRecordModel from aws_lambda_powertools.utilities.typing import LambdaContext -from tests.utils import load_event +from tests.functional.utils import load_event @event_parser(model=SesModel) diff --git a/tests/functional/parser/test_sns.py b/tests/functional/parser/test_sns.py index 992871126bc..b0d9ff69a9b 100644 --- a/tests/functional/parser/test_sns.py +++ b/tests/functional/parser/test_sns.py @@ -6,8 +6,8 @@ from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, event_parser from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyAdvancedSnsBusiness, MySnsBusiness +from tests.functional.utils import load_event from tests.functional.validator.conftest import sns_event # noqa: F401 -from tests.utils import load_event @event_parser(model=MySnsBusiness, envelope=envelopes.SnsEnvelope) diff --git a/tests/functional/parser/test_sqs.py b/tests/functional/parser/test_sqs.py index 93b2fca5d6c..7ca883616f2 100644 --- a/tests/functional/parser/test_sqs.py +++ b/tests/functional/parser/test_sqs.py @@ -5,8 +5,8 @@ from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, event_parser from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.parser.schemas import MyAdvancedSqsBusiness, MySqsBusiness +from tests.functional.utils import load_event from tests.functional.validator.conftest import sqs_event # noqa: F401 -from tests.utils import load_event @event_parser(model=MySqsBusiness, envelope=envelopes.SqsEnvelope) diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py index fe543db34cb..8a87075d16c 100644 --- a/tests/functional/test_data_classes.py +++ b/tests/functional/test_data_classes.py @@ -76,7 +76,7 @@ ) from aws_lambda_powertools.utilities.data_classes.event_source import event_source from aws_lambda_powertools.utilities.data_classes.s3_object_event import S3ObjectLambdaEvent -from tests.utils import load_event +from tests.functional.utils import load_event def test_dict_wrapper_equals(): diff --git a/tests/functional/utils.py b/tests/functional/utils.py index c07f4fe32cc..6b73053e0d0 100644 --- a/tests/functional/utils.py +++ b/tests/functional/utils.py @@ -1,9 +1,16 @@ import base64 import json +from pathlib import Path +from typing import Any from aws_lambda_powertools.shared.json_encoder import Encoder +def load_event(file_name: str) -> Any: + path = Path(str(Path(__file__).parent.parent) + "/events/" + file_name) + return json.loads(path.read_text()) + + def str_to_b64(data: str) -> str: return base64.b64encode(data.encode()).decode("utf-8") From e2b64d56e624f1052be7539051792674d954433e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Tue, 23 Aug 2022 16:50:28 +0200 Subject: [PATCH 20/35] feat(event_handler): add e2e tests --- .gitignore | 3 + aws_lambda_powertools/__init__.py | 6 +- .../shared/headers_serializer.py | 5 +- tests/e2e/event_handler/conftest.py | 19 +-- .../e2e/event_handler/handlers/alb_handler.py | 4 +- .../handlers/api_gateway_http_handler.py | 18 +++ .../handlers/api_gateway_rest_handler.py | 18 +++ .../handlers/lambda_function_url_handler.py | 18 +++ tests/e2e/event_handler/infrastructure.py | 71 ++++++++-- .../event_handler/test_header_serializer.py | 127 +++++++++++++++++- tests/e2e/utils/data_fetcher/common.py | 6 +- tests/e2e/utils/infrastructure.py | 6 +- tests/functional/test_headers_serializer.py | 2 +- 13 files changed, 268 insertions(+), 35 deletions(-) create mode 100644 tests/e2e/event_handler/handlers/api_gateway_http_handler.py create mode 100644 tests/e2e/event_handler/handlers/api_gateway_rest_handler.py create mode 100644 tests/e2e/event_handler/handlers/lambda_function_url_handler.py diff --git a/.gitignore b/.gitignore index b776e1999c2..cc01240a405 100644 --- a/.gitignore +++ b/.gitignore @@ -305,5 +305,8 @@ site/ !404.html !docs/overrides/*.html +# CDK +.cdk + !.github/workflows/lib examples/**/sam/.aws-sam diff --git a/aws_lambda_powertools/__init__.py b/aws_lambda_powertools/__init__.py index 65b5eb86730..750ae92c4d1 100644 --- a/aws_lambda_powertools/__init__.py +++ b/aws_lambda_powertools/__init__.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -"""Top-level package for Lambda Python Powertools.""" - +from pathlib import Path +"""Top-level package for Lambda Python Powertools.""" from .logging import Logger # noqa: F401 from .metrics import Metrics, single_metric # noqa: F401 from .package_logger import set_package_logger_handler @@ -10,4 +10,6 @@ __author__ = """Amazon Web Services""" +PACKAGE_PATH = Path(__file__).parent + set_package_logger_handler() diff --git a/aws_lambda_powertools/shared/headers_serializer.py b/aws_lambda_powertools/shared/headers_serializer.py index abf5955339c..780b46d85f4 100644 --- a/aws_lambda_powertools/shared/headers_serializer.py +++ b/aws_lambda_powertools/shared/headers_serializer.py @@ -1,10 +1,9 @@ import warnings -from abc import ABC from collections import defaultdict from typing import Any, Dict, List -class BaseHeadersSerializer(ABC): +class BaseHeadersSerializer: """ Helper class to correctly serialize headers and cookies on the response payload. """ @@ -39,7 +38,7 @@ def serialize(self, headers: Dict[str, List[str]], cookies: List[str]) -> Dict[s # Duplicate headers are combined with commas and included in the headers field. combined_headers: Dict[str, str] = {} for key, values in headers.items(): - combined_headers[key] = ",".join(values) + combined_headers[key] = ", ".join(values) return {"headers": combined_headers, "cookies": cookies} diff --git a/tests/e2e/event_handler/conftest.py b/tests/e2e/event_handler/conftest.py index 32ea6820bb6..207ec443456 100644 --- a/tests/e2e/event_handler/conftest.py +++ b/tests/e2e/event_handler/conftest.py @@ -1,27 +1,28 @@ +from pathlib import Path + import pytest from tests.e2e.event_handler.infrastructure import EventHandlerStack -from tests.e2e.utils.infrastructure import deploy_once @pytest.fixture(autouse=True, scope="module") -def infrastructure(request: pytest.FixtureRequest, tmp_path_factory: pytest.TempPathFactory, worker_id: str): +def infrastructure(request: pytest.FixtureRequest, lambda_layer_arn: str): """Setup and teardown logic for E2E test infrastructure Parameters ---------- request : pytest.FixtureRequest pytest request fixture to introspect absolute path to test being executed - tmp_path_factory : pytest.TempPathFactory - pytest temporary path factory to discover shared tmp when multiple CPU processes are spun up - worker_id : str - pytest-xdist worker identification to detect whether parallelization is enabled + lambda_layer_arn : str + Lambda Layer ARN Yields ------ Dict[str, str] CloudFormation Outputs from deployed infrastructure """ - yield from deploy_once( - stack=EventHandlerStack, request=request, tmp_path_factory=tmp_path_factory, worker_id=worker_id - ) + stack = EventHandlerStack(handlers_dir=Path(f"{request.path.parent}/handlers"), layer_arn=lambda_layer_arn) + try: + yield stack.deploy() + finally: + stack.delete() diff --git a/tests/e2e/event_handler/handlers/alb_handler.py b/tests/e2e/event_handler/handlers/alb_handler.py index 8043ab85e7b..4c3f4f9dac3 100644 --- a/tests/e2e/event_handler/handlers/alb_handler.py +++ b/tests/e2e/event_handler/handlers/alb_handler.py @@ -3,13 +3,13 @@ app = ALBResolver() -@app.route("/todos", method=["GET"]) +@app.get("/todos") def hello(): return Response( status_code=200, content_type=content_types.TEXT_PLAIN, body="Hello world", - cookies=["CookieMonster; Secure", "MonsterCookie"], + cookies=["CookieMonster", "MonsterCookie"], headers={"Foo": ["bar", "zbr"]}, ) diff --git a/tests/e2e/event_handler/handlers/api_gateway_http_handler.py b/tests/e2e/event_handler/handlers/api_gateway_http_handler.py new file mode 100644 index 00000000000..1a20b730285 --- /dev/null +++ b/tests/e2e/event_handler/handlers/api_gateway_http_handler.py @@ -0,0 +1,18 @@ +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver, Response, content_types + +app = APIGatewayHttpResolver() + + +@app.get("/todos") +def hello(): + return Response( + status_code=200, + content_type=content_types.TEXT_PLAIN, + body="Hello world", + cookies=["CookieMonster", "MonsterCookie"], + headers={"Foo": ["bar", "zbr"]}, + ) + + +def lambda_handler(event, context): + return app.resolve(event, context) diff --git a/tests/e2e/event_handler/handlers/api_gateway_rest_handler.py b/tests/e2e/event_handler/handlers/api_gateway_rest_handler.py new file mode 100644 index 00000000000..2f5ad0b94fa --- /dev/null +++ b/tests/e2e/event_handler/handlers/api_gateway_rest_handler.py @@ -0,0 +1,18 @@ +from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response, content_types + +app = APIGatewayRestResolver() + + +@app.get("/todos") +def hello(): + return Response( + status_code=200, + content_type=content_types.TEXT_PLAIN, + body="Hello world", + cookies=["CookieMonster", "MonsterCookie"], + headers={"Foo": ["bar", "zbr"]}, + ) + + +def lambda_handler(event, context): + return app.resolve(event, context) diff --git a/tests/e2e/event_handler/handlers/lambda_function_url_handler.py b/tests/e2e/event_handler/handlers/lambda_function_url_handler.py new file mode 100644 index 00000000000..3fd4b46ea28 --- /dev/null +++ b/tests/e2e/event_handler/handlers/lambda_function_url_handler.py @@ -0,0 +1,18 @@ +from aws_lambda_powertools.event_handler import LambdaFunctionUrlResolver, Response, content_types + +app = LambdaFunctionUrlResolver() + + +@app.get("/todos") +def hello(): + return Response( + status_code=200, + content_type=content_types.TEXT_PLAIN, + body="Hello world", + cookies=["CookieMonster", "MonsterCookie"], + headers={"Foo": ["bar", "zbr"]}, + ) + + +def lambda_handler(event, context): + return app.resolve(event, context) diff --git a/tests/e2e/event_handler/infrastructure.py b/tests/e2e/event_handler/infrastructure.py index b43925c3b56..9be373b234f 100644 --- a/tests/e2e/event_handler/infrastructure.py +++ b/tests/e2e/event_handler/infrastructure.py @@ -1,25 +1,80 @@ from pathlib import Path +from typing import Dict, Optional from aws_cdk import CfnOutput +from aws_cdk import aws_apigateway as apigwv1 +from aws_cdk import aws_apigatewayv2_alpha as apigwv2 +from aws_cdk import aws_apigatewayv2_integrations_alpha as apigwv2integrations from aws_cdk import aws_ec2 as ec2 from aws_cdk import aws_elasticloadbalancingv2 as elbv2 from aws_cdk import aws_elasticloadbalancingv2_targets as targets +from aws_cdk.aws_lambda import Function, FunctionUrlAuthType -from tests.e2e.utils.infrastructure import BaseInfrastructureV2 +from tests.e2e.utils.infrastructure import BaseInfrastructure -class EventHandlerStack(BaseInfrastructureV2): - def __init__(self, handlers_dir: Path, feature_name: str = "event-handlers") -> None: - super().__init__(feature_name, handlers_dir) +class EventHandlerStack(BaseInfrastructure): + FEATURE_NAME = "event-handlers" + + def __init__(self, handlers_dir: Path, feature_name: str = FEATURE_NAME, layer_arn: str = "") -> None: + super().__init__(feature_name, handlers_dir, layer_arn) def create_resources(self): functions = self.create_lambda_functions() - api_gateway_http_handler_function = functions["AlbHandler"] + self._create_alb(function=functions["AlbHandler"]) + self._create_api_gateway_rest(function=functions["ApiGatewayRestHandler"]) + self._create_api_gateway_http(function=functions["ApiGatewayHttpHandler"]) + self._create_lambda_function_url(function=functions["LambdaFunctionUrlHandler"]) + + def _create_alb(self, function: Function): vpc = ec2.Vpc(self.stack, "EventHandlerVPC", max_azs=2) alb = elbv2.ApplicationLoadBalancer(self.stack, "ALB", vpc=vpc, internet_facing=True) - listener = alb.add_listener("HTTPListener", port=80) - listener.add_targets("HTTPListenerTarget", targets=[targets.LambdaTarget(api_gateway_http_handler_function)]) - CfnOutput(self.stack, "ALBDnsName", value=alb.load_balancer_dns_name) + + self._create_alb_listener(alb=alb, name="Basic", port=80, function=function) + self._create_alb_listener( + alb=alb, + name="MultiValueHeader", + port=81, + function=function, + attributes={"lambda.multi_value_headers.enabled": "true"}, + ) + + def _create_alb_listener( + self, + alb: elbv2.ApplicationLoadBalancer, + name: str, + port: int, + function: Function, + attributes: Optional[Dict[str, str]] = None, + ): + listener = alb.add_listener(name, port=port, protocol=elbv2.ApplicationProtocol.HTTP) + target = listener.add_targets(f"ALB{name}Target", targets=[targets.LambdaTarget(function)]) + if attributes: + for key, value in attributes.items(): + target.set_attribute(key, value) + CfnOutput(self.stack, f"ALB{name}ListenerPort", value=str(port)) + + def _create_api_gateway_http(self, function: Function): + apigw = apigwv2.HttpApi(self.stack, "APIGatewayHTTP", create_default_stage=True) + apigw.add_routes( + path="/todos", + methods=[apigwv2.HttpMethod.GET], + integration=apigwv2integrations.HttpLambdaIntegration("TodosIntegration", function), + ) + + CfnOutput(self.stack, "APIGatewayHTTPUrl", value=(apigw.url or "")) + + def _create_api_gateway_rest(self, function: Function): + apigw = apigwv1.RestApi(self.stack, "APIGatewayRest", deploy_options=apigwv1.StageOptions(stage_name="dev")) + + todos = apigw.root.add_resource("todos") + todos.add_method("GET", apigwv1.LambdaIntegration(function, proxy=True)) + + CfnOutput(self.stack, "APIGatewayRestUrl", value=apigw.url) + + def _create_lambda_function_url(self, function: Function): + function_url = function.add_function_url(auth_type=FunctionUrlAuthType.NONE) + CfnOutput(self.stack, "LambdaFunctionUrl", value=function_url.url) diff --git a/tests/e2e/event_handler/test_header_serializer.py b/tests/e2e/event_handler/test_header_serializer.py index aa4026b2632..2b1d51bfb3d 100644 --- a/tests/e2e/event_handler/test_header_serializer.py +++ b/tests/e2e/event_handler/test_header_serializer.py @@ -5,24 +5,137 @@ @pytest.fixture -def alb_endpoint(infrastructure: dict) -> str: +def alb_basic_listener_endpoint(infrastructure: dict) -> str: dns_name = infrastructure.get("ALBDnsName") - return f"http://{dns_name}" + port = infrastructure.get("ALBBasicListenerPort", "") + return f"http://{dns_name}:{port}" -def test_alb_headers_serializer(alb_endpoint): +@pytest.fixture +def alb_multi_value_header_listener_endpoint(infrastructure: dict) -> str: + dns_name = infrastructure.get("ALBDnsName") + port = infrastructure.get("ALBMultiValueHeaderListenerPort", "") + return f"http://{dns_name}:{port}" + + +@pytest.fixture +def apigw_rest_endpoint(infrastructure: dict) -> str: + return infrastructure.get("APIGatewayRestUrl", "") + + +@pytest.fixture +def apigw_http_endpoint(infrastructure: dict) -> str: + return infrastructure.get("APIGatewayHTTPUrl", "") + + +@pytest.fixture +def lambda_function_url_endpoint(infrastructure: dict) -> str: + return infrastructure.get("LambdaFunctionUrl", "") + + +def test_alb_headers_serializer(alb_basic_listener_endpoint): # GIVEN - url = f"{alb_endpoint}/todos" + url = f"{alb_basic_listener_endpoint}/todos" # WHEN response = data_fetcher.get_http_response(Request(method="GET", url=url)) # THEN assert response.status_code == 200 - assert response.content == "Hello world" + assert response.content == b"Hello world" assert response.headers["content-type"] == "text/plain" + # Only the last header for key "Foo" should be set assert response.headers["Foo"] == "zbr" - assert "MonsterCookie" in response.cookies - assert "CookieMonster; Secure" not in response.cookies + # Only the last cookie should be set + assert "MonsterCookie" in response.cookies.keys() + assert "CookieMonster" not in response.cookies.keys() + + +def test_alb_multi_value_headers_serializer(alb_multi_value_header_listener_endpoint): + # GIVEN + url = f"{alb_multi_value_header_listener_endpoint}/todos" + + # WHEN + response = data_fetcher.get_http_response(Request(method="GET", url=url)) + + # THEN + assert response.status_code == 200 + assert response.content == b"Hello world" + assert response.headers["content-type"] == "text/plain" + + # Only the last header for key "Foo" should be set + assert "Foo" in response.headers + foo_headers = [x.strip() for x in response.headers["Foo"].split(",")] + assert sorted(foo_headers) == ["bar", "zbr"] + + # Only the last cookie should be set + assert "MonsterCookie" in response.cookies.keys() + assert "CookieMonster" in response.cookies.keys() + + +def test_api_gateway_rest_headers_serializer(apigw_rest_endpoint): + # GIVEN + url = f"{apigw_rest_endpoint}/todos" + + # WHEN + response = data_fetcher.get_http_response(Request(method="GET", url=url)) + + # THEN + assert response.status_code == 200 + assert response.content == b"Hello world" + assert response.headers["content-type"] == "text/plain" + + # Only the last header for key "Foo" should be set + assert "Foo" in response.headers + foo_headers = [x.strip() for x in response.headers["Foo"].split(",")] + assert sorted(foo_headers) == ["bar", "zbr"] + + # Only the last cookie should be set + assert "MonsterCookie" in response.cookies.keys() + assert "CookieMonster" in response.cookies.keys() + + +def test_api_gateway_http_headers_serializer(apigw_http_endpoint): + # GIVEN + url = f"{apigw_http_endpoint}/todos" + + # WHEN + response = data_fetcher.get_http_response(Request(method="GET", url=url)) + + # THEN + assert response.status_code == 200 + assert response.content == b"Hello world" + assert response.headers["content-type"] == "text/plain" + + # Only the last header for key "Foo" should be set + assert "Foo" in response.headers + foo_headers = [x.strip() for x in response.headers["Foo"].split(",")] + assert sorted(foo_headers) == ["bar", "zbr"] + + # Only the last cookie should be set + assert "MonsterCookie" in response.cookies.keys() + assert "CookieMonster" in response.cookies.keys() + + +def test_lambda_function_url_headers_serializer(lambda_function_url_endpoint): + # GIVEN + url = f"{lambda_function_url_endpoint}todos" # the function url endpoint already has the trailing / + + # WHEN + response = data_fetcher.get_http_response(Request(method="GET", url=url)) + + # THEN + assert response.status_code == 200 + assert response.content == b"Hello world" + assert response.headers["content-type"] == "text/plain" + + # Only the last header for key "Foo" should be set + assert "Foo" in response.headers + foo_headers = [x.strip() for x in response.headers["Foo"].split(",")] + assert sorted(foo_headers) == ["bar", "zbr"] + + # Only the last cookie should be set + assert "MonsterCookie" in response.cookies.keys() + assert "CookieMonster" in response.cookies.keys() diff --git a/tests/e2e/utils/data_fetcher/common.py b/tests/e2e/utils/data_fetcher/common.py index d38f185544a..06520b1f92d 100644 --- a/tests/e2e/utils/data_fetcher/common.py +++ b/tests/e2e/utils/data_fetcher/common.py @@ -5,6 +5,9 @@ import requests as requests from mypy_boto3_lambda import LambdaClient from mypy_boto3_lambda.type_defs import InvocationResponseTypeDef +from requests import Request, Response +from requests.exceptions import RequestException +from retry import retry def get_lambda_response( @@ -16,6 +19,7 @@ def get_lambda_response( return client.invoke(FunctionName=lambda_arn, InvocationType="RequestResponse", Payload=payload), execution_time -def get_http_response(request: requests.Request) -> requests.Response: +@retry(RequestException, delay=2, jitter=1.5, tries=5) +def get_http_response(request: Request) -> Response: session = requests.Session() return session.send(request.prepare()) diff --git a/tests/e2e/utils/infrastructure.py b/tests/e2e/utils/infrastructure.py index 610f9115525..47f2d65b7eb 100644 --- a/tests/e2e/utils/infrastructure.py +++ b/tests/e2e/utils/infrastructure.py @@ -15,9 +15,11 @@ from filelock import FileLock from mypy_boto3_cloudformation import CloudFormationClient +from aws_lambda_powertools import PACKAGE_PATH from tests.e2e.utils.asset import Assets PYTHON_RUNTIME_VERSION = f"V{''.join(map(str, sys.version_info[:2]))}" +SOURCE_CODE_ROOT_PATH = PACKAGE_PATH.parent logger = logging.getLogger(__name__) @@ -48,7 +50,7 @@ def __init__(self, feature_name: str, handlers_dir: Path, layer_arn: str = "") - # NOTE: Investigate why cdk.Environment in Stack # changes synthesized asset (no object_key in asset manifest) - self.app = App() + self.app = App(outdir=str(SOURCE_CODE_ROOT_PATH / ".cdk")) self.stack = Stack(self.app, self.stack_name) self.session = boto3.Session() self.cfn: CloudFormationClient = self.session.client("cloudformation") @@ -298,7 +300,7 @@ def _create_layer(self) -> str: layer_version_name="aws-lambda-powertools-e2e-test", compatible_runtimes=[PythonVersion[PYTHON_RUNTIME_VERSION].value["runtime"]], code=Code.from_asset( - path=".", + path=str(SOURCE_CODE_ROOT_PATH), bundling=BundlingOptions( image=DockerImage.from_build( str(Path(__file__).parent), diff --git a/tests/functional/test_headers_serializer.py b/tests/functional/test_headers_serializer.py index 25f84f8cdea..bd1271b9a53 100644 --- a/tests/functional/test_headers_serializer.py +++ b/tests/functional/test_headers_serializer.py @@ -20,7 +20,7 @@ def test_headers_serializer_http_api(): assert payload == {"cookies": ["UUID=12345"], "headers": {}} payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": ["bar", "zbr"]}) - assert payload == {"cookies": ["UUID=12345", "SSID=0xdeadbeef"], "headers": {"Foo": "bar,zbr"}} + assert payload == {"cookies": ["UUID=12345", "SSID=0xdeadbeef"], "headers": {"Foo": "bar, zbr"}} def test_headers_serializer_multi_value_headers(): From 7646079a1388cd020fd61865a8747050d2872163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Tue, 23 Aug 2022 17:01:57 +0200 Subject: [PATCH 21/35] chore(deps): add latest cdk packages --- poetry.lock | 600 ++++++++++++++----------------------------------- pyproject.toml | 4 +- 2 files changed, 167 insertions(+), 437 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9b94cb96cfe..f441511131a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -8,30 +8,62 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "21.4.0" +version = "22.1.0" description = "Classes Without Boilerplate" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.5" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "aws-cdk-lib" -version = "2.23.0" +version = "2.38.1" description = "Version 2 of the AWS Cloud Development Kit library" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = "~=3.7" [package.dependencies] constructs = ">=10.0.0,<11.0.0" -jsii = ">=1.57.0,<2.0.0" +jsii = ">=1.63.2,<2.0.0" publication = ">=0.0.3" +typeguard = ">=2.13.3,<2.14.0" + +[[package]] +name = "aws-cdk.aws-apigatewayv2-alpha" +version = "2.38.1a0" +description = "The CDK Construct Library for AWS::APIGatewayv2" +category = "dev" +optional = false +python-versions = "~=3.7" + +[package.dependencies] +aws-cdk-lib = ">=2.38.1,<3.0.0" +constructs = ">=10.0.0,<11.0.0" +jsii = ">=1.63.2,<2.0.0" +publication = ">=0.0.3" +typeguard = ">=2.13.3,<2.14.0" + +[[package]] +name = "aws-cdk.aws-apigatewayv2-integrations-alpha" +version = "2.38.1a0" +description = "Integrations for AWS APIGateway V2" +category = "dev" +optional = false +python-versions = "~=3.7" + +[package.dependencies] +aws-cdk-lib = ">=2.38.1,<3.0.0" +"aws-cdk.aws-apigatewayv2-alpha" = "2.38.1.a0" +constructs = ">=10.0.0,<11.0.0" +jsii = ">=1.63.2,<2.0.0" +publication = ">=0.0.3" +typeguard = ">=2.13.3,<2.14.0" [[package]] name = "aws-xray-sdk" @@ -119,20 +151,6 @@ urllib3 = ">=1.25.4,<1.27" [package.extras] crt = ["awscrt (==0.13.8)"] -[[package]] -name = "cattrs" -version = "1.0.0" -description = "Composable complex class support for attrs." -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -attrs = ">=17.3" - -[package.extras] -dev = ["bumpversion", "wheel", "watchdog", "flake8", "tox", "coverage", "sphinx", "pytest", "hypothesis", "pendulum"] - [[package]] name = "cattrs" version = "22.1.0" @@ -187,15 +205,16 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "constructs" -version = "10.1.1" +version = "10.1.84" description = "A programming model for software-defined state" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = "~=3.7" [package.dependencies] -jsii = ">=1.57.0,<2.0.0" +jsii = ">=1.65.0,<2.0.0" publication = ">=0.0.3" +typeguard = ">=2.13.3,<2.14.0" [[package]] name = "coverage" @@ -236,8 +255,8 @@ optional = true python-versions = ">=3.6,<4.0" [package.extras] -dnssec = ["cryptography (>=2.6,<37.0)"] curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] +dnssec = ["cryptography (>=2.6,<37.0)"] doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.10.0)"] idna = ["idna (>=2.1,<4.0)"] trio = ["trio (>=0.14,<0.20)"] @@ -294,7 +313,7 @@ optional = false python-versions = "*" [package.extras] -devel = ["colorama", "jsonschema", "json-spec", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] +devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] [[package]] name = "flake8" @@ -312,7 +331,7 @@ pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "flake8-bugbear" -version = "22.7.1" +version = "22.8.22" description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." category = "dev" optional = false @@ -428,7 +447,7 @@ python-versions = "*" python-dateutil = ">=2.8.1" [package.extras] -dev = ["twine", "markdown", "flake8", "wheel"] +dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "gitdb" @@ -474,24 +493,9 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] - -[[package]] -name = "importlib-resources" -version = "5.4.0" -description = "Read resources from Python packages" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] [[package]] name = "iniconfig" @@ -510,10 +514,10 @@ optional = false python-versions = ">=3.6.1,<4.0" [package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] +requirements_deprecated_finder = ["pip-api", "pipreqs"] [[package]] name = "jinja2" @@ -539,20 +543,18 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "jsii" -version = "1.57.0" +version = "1.65.0" description = "Python client for jsii runtime" category = "dev" optional = false -python-versions = "~=3.6" +python-versions = "~=3.7" [package.dependencies] -attrs = ">=21.2,<22.0" -cattrs = [ - {version = ">=1.0.0,<1.1.0", markers = "python_version < \"3.7\""}, - {version = ">=1.8,<22.2", markers = "python_version >= \"3.7\""}, -] -importlib-resources = {version = "*", markers = "python_version < \"3.7\""} +attrs = ">=21.2,<23.0" +cattrs = ">=1.8,<22.2" +publication = ">=0.0.3" python-dateutil = "*" +typeguard = ">=2.13.3,<2.14.0" typing-extensions = ">=3.7,<5.0" [[package]] @@ -948,8 +950,8 @@ dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} typing-extensions = ">=3.7.4.3" [package.extras] -email = ["email-validator (>=1.0.3)"] dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] [[package]] name = "pyflakes" @@ -1058,7 +1060,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "pytest-forked" @@ -1084,7 +1086,7 @@ python-versions = ">=3.6" pytest = ">=5.0" [package.extras] -dev = ["pre-commit", "tox", "pytest-asyncio"] +dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "pytest-xdist" @@ -1266,6 +1268,18 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "typeguard" +version = "2.13.3" +description = "Run-time type checker for Python" +category = "dev" +optional = false +python-versions = ">=3.5.3" + +[package.extras] +doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["mypy", "pytest", "typing-extensions"] + [[package]] name = "types-requests" version = "2.28.9" @@ -1295,15 +1309,15 @@ python-versions = ">=3.7" [[package]] name = "urllib3" -version = "1.26.11" +version = "1.26.12" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] -brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] @@ -1347,8 +1361,8 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] +testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] pydantic = ["pydantic", "email-validator"] @@ -1356,32 +1370,29 @@ pydantic = ["pydantic", "email-validator"] [metadata] lock-version = "1.1" python-versions = "^3.6.2" -content-hash = "0a9e21c2f15825934ad6c786121da020c4a964c5a0dd138e0e8ae09c0865a055" +content-hash = "c5243992c30d7eb90315717e8f33f0429eca5c65204c57653977ffe9c02173b8" [metadata.files] -atomicwrites = [ - {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, -] +atomicwrites = [] attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, + {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, + {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, ] aws-cdk-lib = [ - {file = "aws-cdk-lib-2.23.0.tar.gz", hash = "sha256:3e07d1c6b320795d38567be183e56c2125b4c4492589775257aabec3d3e2a384"}, - {file = "aws_cdk_lib-2.23.0-py3-none-any.whl", hash = "sha256:1ec04a146d3364cd0fc4da08e3f8ca25e28df68abaa90641936db17a415ca4bc"}, -] -aws-xray-sdk = [ - {file = "aws-xray-sdk-2.10.0.tar.gz", hash = "sha256:9b14924fd0628cf92936055864655354003f0b1acc3e1c3ffde6403d0799dd7a"}, - {file = "aws_xray_sdk-2.10.0-py2.py3-none-any.whl", hash = "sha256:7551e81a796e1a5471ebe84844c40e8edf7c218db33506d046fec61f7495eda4"}, + {file = "aws-cdk-lib-2.38.1.tar.gz", hash = "sha256:5d56effc85fc998de5277aa87e89b2177564b43111a7e331bb8b83c0cd8e96ee"}, + {file = "aws_cdk_lib-2.38.1-py3-none-any.whl", hash = "sha256:400062e9f021412c470cbde8125ee4db68a13af1d501a03512ef46c4a026ab2e"}, ] -bandit = [ - {file = "bandit-1.7.1-py3-none-any.whl", hash = "sha256:f5acd838e59c038a159b5c621cf0f8270b279e884eadd7b782d7491c02add0d4"}, - {file = "bandit-1.7.1.tar.gz", hash = "sha256:a81b00b5436e6880fa8ad6799bc830e02032047713cbb143a12939ac67eb756c"}, +"aws-cdk.aws-apigatewayv2-alpha" = [ + {file = "aws-cdk.aws-apigatewayv2-alpha-2.38.1a0.tar.gz", hash = "sha256:a51a5cf462043f783b03b4e6a92e9fc280acff77781924d5730d53868502fbe9"}, + {file = "aws_cdk.aws_apigatewayv2_alpha-2.38.1a0-py3-none-any.whl", hash = "sha256:5345959a98b7fe405a9779fc7ea42b3453f0d550ccf257bbc4d9e9d5830c6046"}, ] -black = [ - {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, - {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, +"aws-cdk.aws-apigatewayv2-integrations-alpha" = [ + {file = "aws-cdk.aws-apigatewayv2-integrations-alpha-2.38.1a0.tar.gz", hash = "sha256:4df15306e9d4db41f818bb3b3b67cf956ee36a87d3989b94b0c98ea152dbaa01"}, + {file = "aws_cdk.aws_apigatewayv2_integrations_alpha-2.38.1a0-py3-none-any.whl", hash = "sha256:184fe6ce9718237fe69a4ccda9d7c5d62fd0adf823269d2816b4b9a57978c204"}, ] +aws-xray-sdk = [] +bandit = [] +black = [] boto3 = [ {file = "boto3-1.23.10-py3-none-any.whl", hash = "sha256:40d08614f17a69075e175c02c5d5aab69a6153fd50e40fa7057b913ac7bf40e7"}, {file = "boto3-1.23.10.tar.gz", hash = "sha256:2a4395e3241c20eef441d7443a5e6eaa0ee3f7114653fb9d9cef41587526f7bd"}, @@ -1390,16 +1401,8 @@ botocore = [ {file = "botocore-1.26.10-py3-none-any.whl", hash = "sha256:8a4a984bf901ccefe40037da11ba2abd1ddbcb3b490a492b7f218509c99fc12f"}, {file = "botocore-1.26.10.tar.gz", hash = "sha256:5df2cf7ebe34377470172bd0bbc582cf98c5cbd02da0909a14e9e2885ab3ae9c"}, ] -cattrs = [ - {file = "cattrs-1.0.0-py2.py3-none-any.whl", hash = "sha256:616972ae3dfa6e623a40ad3cb845420e64942989152774ab055e5c2b2f89f997"}, - {file = "cattrs-1.0.0.tar.gz", hash = "sha256:b7ab5cf8ad127c42eefd01410c1c6e28569a45a255ea80ed968511873c433c7a"}, - {file = "cattrs-22.1.0-py3-none-any.whl", hash = "sha256:d55c477b4672f93606e992049f15d526dc7867e6c756cd6256d4af92e2b1e364"}, - {file = "cattrs-22.1.0.tar.gz", hash = "sha256:94b67b64cf92c994f8784c40c082177dc916e0489a73a9a36b24eb18a9db40c6"}, -] -certifi = [ - {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, - {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, -] +cattrs = [] +certifi = [] charset-normalizer = [ {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, @@ -1408,145 +1411,54 @@ click = [ {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, ] -colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, -] +colorama = [] constructs = [ - {file = "constructs-10.1.1-py3-none-any.whl", hash = "sha256:c1f3deb196f54e070ded3c92c4339f73ef2b6022d35fb34908c0ebfa7ef8a640"}, - {file = "constructs-10.1.1.tar.gz", hash = "sha256:6ce0dd1352367237b5d7c51a25740482c852735d2a5e067c536acc1657f39ea5"}, -] -coverage = [ - {file = "coverage-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b"}, - {file = "coverage-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0"}, - {file = "coverage-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da"}, - {file = "coverage-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d"}, - {file = "coverage-6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739"}, - {file = "coverage-6.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971"}, - {file = "coverage-6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840"}, - {file = "coverage-6.2-cp310-cp310-win32.whl", hash = "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c"}, - {file = "coverage-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f"}, - {file = "coverage-6.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76"}, - {file = "coverage-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47"}, - {file = "coverage-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64"}, - {file = "coverage-6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9"}, - {file = "coverage-6.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d"}, - {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48"}, - {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e"}, - {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d"}, - {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17"}, - {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781"}, - {file = "coverage-6.2-cp36-cp36m-win32.whl", hash = "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a"}, - {file = "coverage-6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0"}, - {file = "coverage-6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"}, - {file = "coverage-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521"}, - {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884"}, - {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa"}, - {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64"}, - {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617"}, - {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8"}, - {file = "coverage-6.2-cp37-cp37m-win32.whl", hash = "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4"}, - {file = "coverage-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74"}, - {file = "coverage-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e"}, - {file = "coverage-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58"}, - {file = "coverage-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc"}, - {file = "coverage-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd"}, - {file = "coverage-6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953"}, - {file = "coverage-6.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475"}, - {file = "coverage-6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57"}, - {file = "coverage-6.2-cp38-cp38-win32.whl", hash = "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c"}, - {file = "coverage-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2"}, - {file = "coverage-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd"}, - {file = "coverage-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685"}, - {file = "coverage-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c"}, - {file = "coverage-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3"}, - {file = "coverage-6.2-cp39-cp39-win32.whl", hash = "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282"}, - {file = "coverage-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644"}, - {file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"}, - {file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"}, -] -dataclasses = [ - {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, - {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, -] -decorator = [ - {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, - {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, + {file = "constructs-10.1.84-py3-none-any.whl", hash = "sha256:8bc6047134f21ceea12753f5d3bfd439a4db01507ca727a3b1d4bc2bde077be7"}, + {file = "constructs-10.1.84.tar.gz", hash = "sha256:1b491afce3e73884faa42f32e94ed15fda6a70c91f700ea552ebe0ea3a0d9a0f"}, ] +coverage = [] +dataclasses = [] +decorator = [] dnspython = [ {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"}, {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"}, ] -email-validator = [ - {file = "email_validator-1.2.1-py2.py3-none-any.whl", hash = "sha256:c8589e691cf73eb99eed8d10ce0e9cbb05a0886ba920c8bcb7c82873f4c5789c"}, - {file = "email_validator-1.2.1.tar.gz", hash = "sha256:6757aea012d40516357c0ac2b1a4c31219ab2f899d26831334c5d069e8b6c3d8"}, -] +email-validator = [] eradicate = [ {file = "eradicate-2.1.0-py3-none-any.whl", hash = "sha256:8bfaca181db9227dc88bdbce4d051a9627604c2243e7d85324f6d6ce0fd08bb2"}, {file = "eradicate-2.1.0.tar.gz", hash = "sha256:aac7384ab25b1bf21c4c012de9b4bf8398945a14c98c911545b2ea50ab558014"}, ] -exceptiongroup = [ - {file = "exceptiongroup-1.0.0rc8-py3-none-any.whl", hash = "sha256:ab0a968e1ef769e55d9a596f4a89f7be9ffedbc9fdefdb77cc68cf5c33ce1035"}, - {file = "exceptiongroup-1.0.0rc8.tar.gz", hash = "sha256:6990c24f06b8d33c8065cfe43e5e8a4bfa384e0358be036af9cc60b6321bd11a"}, -] -execnet = [ - {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, - {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, -] -fastjsonschema = [ - {file = "fastjsonschema-2.16.1-py3-none-any.whl", hash = "sha256:2f7158c4de792555753d6c2277d6a2af2d406dfd97aeca21d17173561ede4fe6"}, - {file = "fastjsonschema-2.16.1.tar.gz", hash = "sha256:d6fa3ffbe719768d70e298b9fb847484e2bdfdb7241ed052b8d57a9294a8c334"}, -] +exceptiongroup = [] +execnet = [] +fastjsonschema = [] flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] flake8-bugbear = [ - {file = "flake8-bugbear-22.7.1.tar.gz", hash = "sha256:e450976a07e4f9d6c043d4f72b17ec1baf717fe37f7997009c8ae58064f88305"}, - {file = "flake8_bugbear-22.7.1-py3-none-any.whl", hash = "sha256:db5d7a831ef4412a224b26c708967ff816818cabae415e76b8c58df156c4b8e5"}, -] -flake8-builtins = [ - {file = "flake8-builtins-1.5.3.tar.gz", hash = "sha256:09998853b2405e98e61d2ff3027c47033adbdc17f9fe44ca58443d876eb00f3b"}, - {file = "flake8_builtins-1.5.3-py2.py3-none-any.whl", hash = "sha256:7706babee43879320376861897e5d1468e396a40b8918ed7bccf70e5f90b8687"}, -] -flake8-comprehensions = [ - {file = "flake8-comprehensions-3.7.0.tar.gz", hash = "sha256:6b3218b2dde8ac5959c6476cde8f41a79e823c22feb656be2710cd2a3232cef9"}, - {file = "flake8_comprehensions-3.7.0-py3-none-any.whl", hash = "sha256:a5d7aea6315bbbd6fbcb2b4e80bff6a54d1600155e26236e555d0c6fe1d62522"}, -] -flake8-debugger = [ - {file = "flake8-debugger-4.0.0.tar.gz", hash = "sha256:e43dc777f7db1481db473210101ec2df2bd39a45b149d7218a618e954177eda6"}, - {file = "flake8_debugger-4.0.0-py3-none-any.whl", hash = "sha256:82e64faa72e18d1bdd0000407502ebb8ecffa7bc027c62b9d4110ce27c091032"}, + {file = "flake8-bugbear-22.8.22.tar.gz", hash = "sha256:1d240736dc93ecad9ff21e1d521283b9461d132b3ff2170569bd5ff3dacda385"}, + {file = "flake8_bugbear-22.8.22-py3-none-any.whl", hash = "sha256:b5718bf5d0d28a4cfd15054f4706fc2936f7339ed8366b919a1979eb5df50764"}, ] +flake8-builtins = [] +flake8-comprehensions = [] +flake8-debugger = [] flake8-eradicate = [ {file = "flake8-eradicate-1.3.0.tar.gz", hash = "sha256:e4c98f00d17dc8653e3388cac2624cd81e9735de2fd4a8dcf99029633ebd7a63"}, {file = "flake8_eradicate-1.3.0-py3-none-any.whl", hash = "sha256:85a71e0c5f4e07f7c6c5fec520483561fd6bd295417d622855bdeade99242e3d"}, ] -flake8-fixme = [ - {file = "flake8-fixme-1.1.1.tar.gz", hash = "sha256:50cade07d27a4c30d4f12351478df87339e67640c83041b664724bda6d16f33a"}, - {file = "flake8_fixme-1.1.1-py2.py3-none-any.whl", hash = "sha256:226a6f2ef916730899f29ac140bed5d4a17e5aba79f00a0e3ae1eff1997cb1ac"}, -] +flake8-fixme = [] flake8-isort = [ {file = "flake8-isort-4.2.0.tar.gz", hash = "sha256:26571500cd54976bbc0cf1006ffbcd1a68dd102f816b7a1051b219616ba9fee0"}, {file = "flake8_isort-4.2.0-py3-none-any.whl", hash = "sha256:5b87630fb3719bf4c1833fd11e0d9534f43efdeba524863e15d8f14a7ef6adbf"}, ] -flake8-variables-names = [ - {file = "flake8_variables_names-0.0.4.tar.gz", hash = "sha256:d6fa0571a807c72940b5773827c5760421ea6f8206595ff0a8ecfa01e42bf2cf"}, -] -future = [ - {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, -] +flake8-variables-names = [] +future = [] ghp-import = [ {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, ] -gitdb = [ - {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"}, - {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"}, -] -gitpython = [ - {file = "GitPython-3.1.20-py3-none-any.whl", hash = "sha256:b1e1c269deab1b08ce65403cf14e10d2ef1f6c89e33ea7c5e5bb0222ea593b8a"}, - {file = "GitPython-3.1.20.tar.gz", hash = "sha256:df0e072a200703a65387b0cfdf0466e3bab729c0458cf6b7349d0e9877636519"}, -] +gitdb = [] +gitpython = [] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, @@ -1555,130 +1467,35 @@ importlib-metadata = [ {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"}, ] -importlib-resources = [ - {file = "importlib_resources-5.4.0-py3-none-any.whl", hash = "sha256:33a95faed5fc19b4bc16b29a6eeae248a3fe69dd55d4d229d2b480e23eeaad45"}, - {file = "importlib_resources-5.4.0.tar.gz", hash = "sha256:d756e2f85dd4de2ba89be0b21dba2a3bbec2e871a42a3a16719258a11f87506b"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -isort = [ - {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, - {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, -] -jinja2 = [ - {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, - {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, -] -jmespath = [ - {file = "jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"}, - {file = "jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9"}, -] +iniconfig = [] +isort = [] +jinja2 = [] +jmespath = [] jsii = [ - {file = "jsii-1.57.0-py3-none-any.whl", hash = "sha256:4888091986a9ed8d50b042cc9c35a9564dd54c19e78adb890bf06d9ffac1b325"}, - {file = "jsii-1.57.0.tar.gz", hash = "sha256:ff7a3c51c1a653dd8a4342043b5f8e40b928bc617e3141e0d5d66175d22a754b"}, -] -mako = [ - {file = "Mako-1.1.6-py2.py3-none-any.whl", hash = "sha256:afaf8e515d075b22fad7d7b8b30e4a1c90624ff2f3733a06ec125f5a5f043a57"}, - {file = "Mako-1.1.6.tar.gz", hash = "sha256:4e9e345a41924a954251b95b4b28e14a301145b544901332e658907a7464b6b2"}, -] -mando = [ - {file = "mando-0.6.4-py2.py3-none-any.whl", hash = "sha256:4ce09faec7e5192ffc3c57830e26acba0fd6cd11e1ee81af0d4df0657463bd1c"}, - {file = "mando-0.6.4.tar.gz", hash = "sha256:79feb19dc0f097daa64a1243db578e7674909b75f88ac2220f1c065c10a0d960"}, + {file = "jsii-1.65.0-py3-none-any.whl", hash = "sha256:47b939259ec465bc044aed6d3288b9b27a8edef2af21f1a8448e297b8429f154"}, + {file = "jsii-1.65.0.tar.gz", hash = "sha256:a09ef1f9864fe27457ba4233b3a0744a76d2562077d5b8b3a7f2242edc4a8293"}, ] +mako = [] +mando = [] markdown = [ {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"}, {file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"}, ] -markupsafe = [ - {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, - {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -mergedeep = [ - {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, - {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, -] -mike = [ - {file = "mike-0.6.0-py3-none-any.whl", hash = "sha256:cef9b9c803ff5c3fbb410f51f5ceb00902a9fe16d9fabd93b69c65cf481ab5a1"}, - {file = "mike-0.6.0.tar.gz", hash = "sha256:6d6239de2a60d733da2f34617e9b9a14c4b5437423b47e524f14dc96d6ce5f2f"}, -] +markupsafe = [] +mccabe = [] +mergedeep = [] +mike = [] mkdocs = [ {file = "mkdocs-1.3.1-py3-none-any.whl", hash = "sha256:fda92466393127d2da830bc6edc3a625a14b436316d1caf347690648e774c4f0"}, {file = "mkdocs-1.3.1.tar.gz", hash = "sha256:a41a2ff25ce3bbacc953f9844ba07d106233cd76c88bac1f59cb1564ac0d87ed"}, ] -mkdocs-git-revision-date-plugin = [ - {file = "mkdocs_git_revision_date_plugin-0.3.2-py3-none-any.whl", hash = "sha256:2e67956cb01823dd2418e2833f3623dee8604cdf223bddd005fe36226a56f6ef"}, -] +mkdocs-git-revision-date-plugin = [] mkdocs-material = [ {file = "mkdocs-material-8.4.1.tar.gz", hash = "sha256:92c70f94b2e1f8a05d9e05eec1c7af9dffc516802d69222329db89503c97b4f3"}, {file = "mkdocs_material-8.4.1-py2.py3-none-any.whl", hash = "sha256:319a6254819ce9d864ff79de48c43842fccfdebb43e4e6820eef75216f8cfb0a"}, ] -mkdocs-material-extensions = [ - {file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"}, - {file = "mkdocs_material_extensions-1.0.3-py3-none-any.whl", hash = "sha256:a82b70e533ce060b2a5d9eb2bc2e1be201cf61f901f93704b4acf6e3d5983a44"}, -] -mypy = [ - {file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"}, - {file = "mypy-0.971-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5"}, - {file = "mypy-0.971-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3"}, - {file = "mypy-0.971-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655"}, - {file = "mypy-0.971-cp310-cp310-win_amd64.whl", hash = "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103"}, - {file = "mypy-0.971-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca"}, - {file = "mypy-0.971-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417"}, - {file = "mypy-0.971-cp36-cp36m-win_amd64.whl", hash = "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09"}, - {file = "mypy-0.971-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8"}, - {file = "mypy-0.971-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0"}, - {file = "mypy-0.971-cp37-cp37m-win_amd64.whl", hash = "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2"}, - {file = "mypy-0.971-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27"}, - {file = "mypy-0.971-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856"}, - {file = "mypy-0.971-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71"}, - {file = "mypy-0.971-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27"}, - {file = "mypy-0.971-cp38-cp38-win_amd64.whl", hash = "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58"}, - {file = "mypy-0.971-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6"}, - {file = "mypy-0.971-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe"}, - {file = "mypy-0.971-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9"}, - {file = "mypy-0.971-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf"}, - {file = "mypy-0.971-cp39-cp39-win_amd64.whl", hash = "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0"}, - {file = "mypy-0.971-py3-none-any.whl", hash = "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9"}, - {file = "mypy-0.971.tar.gz", hash = "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56"}, -] +mkdocs-material-extensions = [] +mypy = [] mypy-boto3-appconfig = [ {file = "mypy-boto3-appconfig-1.24.36.post1.tar.gz", hash = "sha256:e1916b3754915cb411ef977083500e1f30f81f7b3aea6ff5eed1cec91944dea6"}, {file = "mypy_boto3_appconfig-1.24.36.post1-py3-none-any.whl", hash = "sha256:a5dbe549dbebf4bc7a6cfcbfa9dff89ceb4983c042b785763ee656504bdb49f6"}, @@ -1719,44 +1536,19 @@ mypy-boto3-xray = [ {file = "mypy-boto3-xray-1.24.36.post1.tar.gz", hash = "sha256:104f1ecf7f1f6278c582201e71a7ab64843d3a3fdc8f23295cf68788cc77e9bb"}, {file = "mypy_boto3_xray-1.24.36.post1-py3-none-any.whl", hash = "sha256:97b9f0686c717c8be99ac06cb52febaf71712b4e4cd0b61ed2eb5ed012a9b5fd"}, ] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -pathspec = [ - {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, - {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, -] +mypy-extensions = [] +packaging = [] +pathspec = [] pbr = [ {file = "pbr-5.10.0-py2.py3-none-any.whl", hash = "sha256:da3e18aac0a3c003e9eea1a81bd23e5a3a75d745670dcf736317b7d966887fdf"}, {file = "pbr-5.10.0.tar.gz", hash = "sha256:cfcc4ff8e698256fc17ea3ff796478b050852585aa5bae79ecd05b2ab7b39b9a"}, ] -pdoc3 = [ - {file = "pdoc3-0.10.0.tar.gz", hash = "sha256:5f22e7bcb969006738e1aa4219c75a32f34c2d62d46dc9d2fb2d3e0b0287e4b7"}, -] -platformdirs = [ - {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, - {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -publication = [ - {file = "publication-0.0.3-py2.py3-none-any.whl", hash = "sha256:0248885351febc11d8a1098d5c8e3ab2dabcf3e8c0c96db1e17ecd12b53afbe6"}, - {file = "publication-0.0.3.tar.gz", hash = "sha256:68416a0de76dddcdd2930d1c8ef853a743cc96c82416c4e4d3b5d901c6276dc4"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -py-cpuinfo = [ - {file = "py-cpuinfo-8.0.0.tar.gz", hash = "sha256:5f269be0e08e33fd959de96b34cd4aeeeacac014dd8305f70eb28d06de2345c5"}, -] +pdoc3 = [] +platformdirs = [] +pluggy = [] +publication = [] +py = [] +py-cpuinfo = [] pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, @@ -1806,46 +1598,19 @@ pygments = [ {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, ] -pymdown-extensions = [ - {file = "pymdown_extensions-9.5-py3-none-any.whl", hash = "sha256:ec141c0f4983755349f0c8710416348d1a13753976c028186ed14f190c8061c4"}, - {file = "pymdown_extensions-9.5.tar.gz", hash = "sha256:3ef2d998c0d5fa7eb09291926d90d69391283561cf6306f85cd588a5eb5befa0"}, -] +pymdown-extensions = [] pyparsing = [ {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, ] -pytest = [ - {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, - {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, -] -pytest-asyncio = [ - {file = "pytest-asyncio-0.16.0.tar.gz", hash = "sha256:7496c5977ce88c34379df64a66459fe395cd05543f0a2f837016e7144391fcfb"}, - {file = "pytest_asyncio-0.16.0-py3-none-any.whl", hash = "sha256:5f2a21273c47b331ae6aa5b36087047b4899e40f03f18397c0e65fa5cca54e9b"}, -] -pytest-benchmark = [ - {file = "pytest-benchmark-3.4.1.tar.gz", hash = "sha256:40e263f912de5a81d891619032983557d62a3d85843f9a9f30b98baea0cd7b47"}, - {file = "pytest_benchmark-3.4.1-py2.py3-none-any.whl", hash = "sha256:36d2b08c4882f6f997fd3126a3d6dfd70f3249cde178ed8bbc0b73db7c20f809"}, -] -pytest-cov = [ - {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, - {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, -] -pytest-forked = [ - {file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"}, - {file = "pytest_forked-1.4.0-py3-none-any.whl", hash = "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"}, -] -pytest-mock = [ - {file = "pytest-mock-3.6.1.tar.gz", hash = "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"}, - {file = "pytest_mock-3.6.1-py3-none-any.whl", hash = "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3"}, -] -pytest-xdist = [ - {file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"}, - {file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"}, -] -python-dateutil = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, -] +pytest = [] +pytest-asyncio = [] +pytest-benchmark = [] +pytest-cov = [] +pytest-forked = [] +pytest-mock = [] +pytest-xdist = [] +python-dateutil = [] python-snappy = [ {file = "python-snappy-0.6.1.tar.gz", hash = "sha256:b6a107ab06206acc5359d4c5632bd9b22d448702a79b3169b0c62e0fb808bb2a"}, {file = "python_snappy-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b7f920eaf46ebf41bd26f9df51c160d40f9e00b7b48471c3438cb8d027f7fb9b"}, @@ -1931,53 +1696,18 @@ pyyaml = [ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] -pyyaml-env-tag = [ - {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, - {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, -] -radon = [ - {file = "radon-5.1.0-py2.py3-none-any.whl", hash = "sha256:fa74e018197f1fcb54578af0f675d8b8e2342bd8e0b72bef8197bc4c9e645f36"}, - {file = "radon-5.1.0.tar.gz", hash = "sha256:cb1d8752e5f862fb9e20d82b5f758cbc4fb1237c92c9a66450ea0ea7bf29aeee"}, -] +pyyaml-env-tag = [] +radon = [] requests = [ {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, ] -retry = [ - {file = "retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606"}, - {file = "retry-0.9.2.tar.gz", hash = "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4"}, -] +retry = [] "ruamel.yaml" = [ {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"}, {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"}, ] -"ruamel.yaml.clib" = [ - {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6e7be2c5bcb297f5b82fee9c665eb2eb7001d1050deaba8471842979293a80b0"}, - {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:221eca6f35076c6ae472a531afa1c223b9c29377e62936f61bc8e6e8bdc5f9e7"}, - {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win32.whl", hash = "sha256:1070ba9dd7f9370d0513d649420c3b362ac2d687fe78c6e888f5b12bf8bc7bee"}, - {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:77df077d32921ad46f34816a9a16e6356d8100374579bc35e15bab5d4e9377de"}, - {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751"}, - {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527"}, - {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win32.whl", hash = "sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5"}, - {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win_amd64.whl", hash = "sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c"}, - {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502"}, - {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78"}, - {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win32.whl", hash = "sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94"}, - {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win_amd64.whl", hash = "sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468"}, - {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd"}, - {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99"}, - {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win32.whl", hash = "sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb"}, - {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe"}, - {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233"}, - {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84"}, - {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win32.whl", hash = "sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b"}, - {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277"}, - {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed"}, - {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0"}, - {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win32.whl", hash = "sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104"}, - {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7"}, - {file = "ruamel.yaml.clib-0.2.6.tar.gz", hash = "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd"}, -] +"ruamel.yaml.clib" = [] s3transfer = [ {file = "s3transfer-0.5.2-py3-none-any.whl", hash = "sha256:7a6f4c4d1fdb9a2b640244008e142cbc2cd3ae34b386584ef044dd0f27101971"}, {file = "s3transfer-0.5.2.tar.gz", hash = "sha256:95c58c194ce657a5f4fb0b9e60a84968c808888aed628cd98ab8771fe1db98ed"}, @@ -1986,14 +1716,8 @@ six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -smmap = [ - {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, - {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, -] -stevedore = [ - {file = "stevedore-3.5.0-py3-none-any.whl", hash = "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c"}, - {file = "stevedore-3.5.0.tar.gz", hash = "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335"}, -] +smmap = [] +stevedore = [] tomli = [ {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, @@ -2024,6 +1748,10 @@ typed-ast = [ {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, ] +typeguard = [ + {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"}, + {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"}, +] types-requests = [ {file = "types-requests-2.28.9.tar.gz", hash = "sha256:feaf581bd580497a47fe845d506fa3b91b484cf706ff27774e87659837de9962"}, {file = "types_requests-2.28.9-py3-none-any.whl", hash = "sha256:86cb66d3de2f53eac5c09adc42cf6547eefbd0c7e1210beca1ee751c35d96083"}, @@ -2036,7 +1764,10 @@ typing-extensions = [ {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, ] -urllib3 = [] +urllib3 = [ + {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, + {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, +] watchdog = [ {file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330"}, {file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b17d302850c8d412784d9246cfe8d7e3af6bcd45f958abb2d08a6f8bedf695d"}, @@ -2130,10 +1861,7 @@ wrapt = [ {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, ] -xenon = [ - {file = "xenon-0.9.0-py2.py3-none-any.whl", hash = "sha256:994c80c7f1c6d40596b600b93734d85a5739208f31895ef99f1e4d362caf9e35"}, - {file = "xenon-0.9.0.tar.gz", hash = "sha256:d2b9cb6c6260f771a432c1e588e51fddb17858f88f73ef641e7532f7a5f58fb8"}, -] +xenon = [] zipp = [ {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, diff --git a/pyproject.toml b/pyproject.toml index b12fdcc092a..3ff14cb2e58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,9 @@ mike = "^0.6.0" mypy = "^0.971" retry = "^0.9.2" pytest-xdist = "^2.5.0" -aws-cdk-lib = "^2.23.0" +aws-cdk-lib = { version = "^2.38.1", python = ">=3.7" } +"aws-cdk.aws-apigatewayv2-alpha" = {version = "^2.38.1-alpha.0", python = ">=3.7"} +"aws-cdk.aws-apigatewayv2-integrations-alpha" = {version = "^2.38.1-alpha.0", python = ">=3.7"} pytest-benchmark = "^3.4.1" mypy-boto3-appconfig = { version = "^1.24.29", python = ">=3.7" } mypy-boto3-cloudformation = { version = "^1.24.0", python = ">=3.7" } From 3271d755ca5541ef1ed3b2e6247bea0fe3088aa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Wed, 24 Aug 2022 11:57:14 +0200 Subject: [PATCH 22/35] chore(event_handler): address review comments --- aws_lambda_powertools/event_handler/api_gateway.py | 5 ++--- aws_lambda_powertools/shared/headers_serializer.py | 9 +++++---- .../utilities/data_classes/alb_event.py | 4 ++-- .../utilities/data_classes/api_gateway_proxy_event.py | 4 ++-- tests/functional/event_handler/test_api_gateway.py | 4 ++-- tests/functional/test_headers_serializer.py | 8 ++++---- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index d89a13602ce..190d74a813b 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -232,13 +232,12 @@ def build(self, event: BaseProxyEvent, cors: Optional[CORSConfig] = None) -> Dic self.response.base64_encoded = True self.response.body = base64.b64encode(self.response.body).decode() - payload = { + return { "statusCode": self.response.status_code, "body": self.response.body, "isBase64Encoded": self.response.base64_encoded, **event.header_serializer().serialize(headers=self.response.headers, cookies=self.response.cookies), } - return payload class BaseRouter(ABC): @@ -610,7 +609,7 @@ def _not_found(self, method: str) -> ResponseBuilder: if method == "OPTIONS": logger.debug("Pre-flight request detected. Returning CORS with null response") - headers["Access-Control-Allow-Methods"].append(",".join(sorted(self._cors_methods))) + headers["Access-Control-Allow-Methods"].append(",".join(self._cors_methods)) return ResponseBuilder(Response(status_code=204, content_type=None, headers=headers, body="")) handler = self._lookup_exception_handler(NotFoundError) diff --git a/aws_lambda_powertools/shared/headers_serializer.py b/aws_lambda_powertools/shared/headers_serializer.py index 780b46d85f4..602d05ffad8 100644 --- a/aws_lambda_powertools/shared/headers_serializer.py +++ b/aws_lambda_powertools/shared/headers_serializer.py @@ -5,7 +5,8 @@ class BaseHeadersSerializer: """ - Helper class to correctly serialize headers and cookies on the response payload. + Helper class to correctly serialize headers and cookies for Amazon API Gateway, + ALB and Lambda Function URL response payload. """ def serialize(self, headers: Dict[str, List[str]], cookies: List[str]) -> Dict[str, Any]: @@ -23,7 +24,7 @@ def serialize(self, headers: Dict[str, List[str]], cookies: List[str]) -> Dict[s raise NotImplementedError() -class HttpApiSerializer(BaseHeadersSerializer): +class HttpApiHeadersSerializer(BaseHeadersSerializer): def serialize(self, headers: Dict[str, List[str]], cookies: List[str]) -> Dict[str, Any]: """ When using HTTP APIs or LambdaFunctionURLs, everything is taken care automatically for us. @@ -81,7 +82,7 @@ def serialize(self, headers: Dict[str, List[str]], cookies: List[str]) -> Dict[s if cookies: if len(cookies) > 1: warnings.warn( - "Can't encode more than one cookie in the response. " + "Can't encode more than one cookie in the response. Sending the last cookie only. " "Did you enable multiValueHeaders on the ALB Target Group?" ) @@ -91,7 +92,7 @@ def serialize(self, headers: Dict[str, List[str]], cookies: List[str]) -> Dict[s for key, values in headers.items(): if len(values) > 1: warnings.warn( - "Can't encode more than one header value for the same key in the response. " + f"Can't encode more than one header value for the same key ('{key}') in the response. " "Did you enable multiValueHeaders on the ALB Target Group?" ) diff --git a/aws_lambda_powertools/utilities/data_classes/alb_event.py b/aws_lambda_powertools/utilities/data_classes/alb_event.py index 7f0dd8779a6..1bd49fd05b6 100644 --- a/aws_lambda_powertools/utilities/data_classes/alb_event.py +++ b/aws_lambda_powertools/utilities/data_classes/alb_event.py @@ -41,5 +41,5 @@ def header_serializer(self) -> BaseHeadersSerializer: # We can determine if the feature is enabled by looking if the event has a `multiValueHeaders` key. if self.multi_value_headers: return MultiValueHeadersSerializer() - else: - return SingleValueHeadersSerializer() + + return SingleValueHeadersSerializer() diff --git a/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py b/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py index 133a27c5e02..030d9739fa4 100644 --- a/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py +++ b/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py @@ -2,7 +2,7 @@ from aws_lambda_powertools.shared.headers_serializer import ( BaseHeadersSerializer, - HttpApiSerializer, + HttpApiHeadersSerializer, MultiValueHeadersSerializer, ) from aws_lambda_powertools.utilities.data_classes.common import ( @@ -260,4 +260,4 @@ def http_method(self) -> str: return self.request_context.http.method def header_serializer(self): - return HttpApiSerializer() + return HttpApiHeadersSerializer() diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 74e8ae82daa..f880012081d 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -266,7 +266,7 @@ def handler(event, context): assert headers["Content-Type"] == [content_types.TEXT_HTML] assert headers["Access-Control-Allow-Origin"] == ["*"] assert "Access-Control-Allow-Credentials" not in headers - assert headers["Access-Control-Allow-Headers"] == [",".join(sorted(CORSConfig._REQUIRED_HEADERS))] + assert sorted(headers["Access-Control-Allow-Headers"][0].split(",")) == sorted(CORSConfig._REQUIRED_HEADERS) # THEN for routes without cors flag return no cors headers mock_event = {"path": "/my/request", "httpMethod": "GET"} @@ -558,7 +558,7 @@ def post_no_cors(): headers = result["multiValueHeaders"] assert "Content-Type" not in headers assert "Access-Control-Allow-Origin" in result["multiValueHeaders"] - assert headers["Access-Control-Allow-Methods"] == ["DELETE,GET,OPTIONS"] + assert sorted(headers["Access-Control-Allow-Methods"][0].split(",")) == ["DELETE", "GET", "OPTIONS"] def test_custom_preflight_response(): diff --git a/tests/functional/test_headers_serializer.py b/tests/functional/test_headers_serializer.py index bd1271b9a53..bca6c884075 100644 --- a/tests/functional/test_headers_serializer.py +++ b/tests/functional/test_headers_serializer.py @@ -1,14 +1,14 @@ import warnings from aws_lambda_powertools.shared.headers_serializer import ( - HttpApiSerializer, + HttpApiHeadersSerializer, MultiValueHeadersSerializer, SingleValueHeadersSerializer, ) def test_headers_serializer_http_api(): - serializer = HttpApiSerializer() + serializer = HttpApiHeadersSerializer() payload = serializer.serialize(cookies=[], headers={}) assert payload == {"cookies": [], "headers": {}} @@ -59,10 +59,10 @@ def test_headers_serializer_single_value_headers(): assert len(w) == 2 assert str(w[-2].message) == ( - "Can't encode more than one cookie in the response. " + "Can't encode more than one cookie in the response. Sending the last cookie only. " "Did you enable multiValueHeaders on the ALB Target Group?" ) assert str(w[-1].message) == ( - "Can't encode more than one header value for the same key in the response. " + "Can't encode more than one header value for the same key ('Foo') in the response. " "Did you enable multiValueHeaders on the ALB Target Group?" ) From e5ceac6c7a175605d4f10df8169450f7254c4f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Wed, 24 Aug 2022 12:09:01 +0200 Subject: [PATCH 23/35] chore(deps): dropped python 3.6 from pyproject --- poetry.lock | 313 +++++++++++++++++++++++++++++++++---------------- pyproject.toml | 3 +- 2 files changed, 213 insertions(+), 103 deletions(-) diff --git a/poetry.lock b/poetry.lock index f441511131a..54533f932d0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -79,11 +79,11 @@ wrapt = "*" [[package]] name = "bandit" -version = "1.7.1" +version = "1.7.4" description = "Security oriented static analyser for python code." category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" [package.dependencies] colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} @@ -91,6 +91,11 @@ GitPython = ">=1.0.1" PyYAML = ">=5.3.1" stevedore = ">=1.20.0" +[package.extras] +test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)", "toml"] +toml = ["toml"] +yaml = ["pyyaml"] + [[package]] name = "black" version = "21.12b0" @@ -101,7 +106,6 @@ python-versions = ">=3.6.2" [package.dependencies] click = ">=7.1.2" -dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} mypy-extensions = ">=0.4.3" pathspec = ">=0.9.0,<1" platformdirs = ">=2" @@ -121,27 +125,27 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "boto3" -version = "1.23.10" +version = "1.24.58" description = "The AWS SDK for Python" category = "main" optional = false -python-versions = ">= 3.6" +python-versions = ">= 3.7" [package.dependencies] -botocore = ">=1.26.10,<1.27.0" +botocore = ">=1.27.58,<1.28.0" jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.5.0,<0.6.0" +s3transfer = ">=0.6.0,<0.7.0" [package.extras] crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.26.10" +version = "1.27.58" description = "Low-level, data-driven core of boto 3." category = "main" optional = false -python-versions = ">= 3.6" +python-versions = ">= 3.7" [package.dependencies] jmespath = ">=0.7.1,<2.0.0" @@ -149,7 +153,7 @@ python-dateutil = ">=2.1,<3.0.0" urllib3 = ">=1.25.4,<1.27" [package.extras] -crt = ["awscrt (==0.13.8)"] +crt = ["awscrt (==0.14.0)"] [[package]] name = "cattrs" @@ -174,22 +178,22 @@ python-versions = ">=3.6" [[package]] name = "charset-normalizer" -version = "2.0.12" +version = "2.1.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "dev" optional = false -python-versions = ">=3.5.0" +python-versions = ">=3.6.0" [package.extras] unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "8.0.4" +version = "8.1.3" description = "Composable command line interface toolkit" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -205,7 +209,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "constructs" -version = "10.1.84" +version = "10.1.85" description = "A programming model for software-defined state" category = "dev" optional = false @@ -218,26 +222,18 @@ typeguard = ">=2.13.3,<2.14.0" [[package]] name = "coverage" -version = "6.2" +version = "6.4.4" description = "Code coverage measurement for Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -tomli = {version = "*", optional = true, markers = "extra == \"toml\""} +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli"] -[[package]] -name = "dataclasses" -version = "0.8" -description = "A backport of the dataclasses module for Python 3.6" -category = "main" -optional = false -python-versions = ">=3.6, <3.7" - [[package]] name = "decorator" version = "5.1.1" @@ -331,7 +327,7 @@ pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "flake8-bugbear" -version = "22.8.22" +version = "22.8.23" description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." category = "dev" optional = false @@ -360,28 +356,27 @@ test = ["coverage", "coveralls", "mock", "pytest", "pytest-cov"] [[package]] name = "flake8-comprehensions" -version = "3.7.0" +version = "3.10.0" description = "A flake8 plugin to help you write better list/set/dict comprehensions." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -flake8 = ">=3.0,<3.2.0 || >3.2.0,<5" +flake8 = ">=3.0,<3.2.0 || >3.2.0" importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "flake8-debugger" -version = "4.0.0" +version = "4.1.2" description = "ipdb/pdb statement checker plugin for flake8" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] flake8 = ">=3.0" pycodestyle = "*" -six = "*" [[package]] name = "flake8-eradicate" @@ -462,15 +457,15 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.20" -description = "Python Git Library" +version = "3.1.27" +description = "GitPython is a python library used to interact with Git repositories" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] gitdb = ">=4.0.1,<5" -typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""} +typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} [[package]] name = "idna" @@ -521,11 +516,11 @@ requirements_deprecated_finder = ["pip-api", "pipreqs"] [[package]] name = "jinja2" -version = "3.0.3" +version = "3.1.2" description = "A very fast and expressive template engine." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] MarkupSafe = ">=2.0" @@ -535,11 +530,11 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "jmespath" -version = "0.10.0" +version = "1.0.1" description = "JSON Matching Expressions" category = "main" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = ">=3.7" [[package]] name = "jsii" @@ -559,18 +554,20 @@ typing-extensions = ">=3.7,<5.0" [[package]] name = "mako" -version = "1.1.6" -description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +version = "1.2.1" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.7" [package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} MarkupSafe = ">=0.9.2" [package.extras] babel = ["babel"] lingua = ["lingua"] +testing = ["pytest"] [[package]] name = "mando" @@ -602,11 +599,11 @@ testing = ["coverage", "pyyaml"] [[package]] name = "markupsafe" -version = "2.0.1" +version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "mccabe" @@ -880,14 +877,14 @@ markdown = ">=3.0" [[package]] name = "platformdirs" -version = "2.4.0" +version = "2.5.2" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] -docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] [[package]] @@ -946,7 +943,6 @@ optional = true python-versions = ">=3.6.1" [package.dependencies] -dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} typing-extensions = ">=3.7.4.3" [package.extras] @@ -985,22 +981,22 @@ markdown = ">=3.2" [[package]] name = "pyparsing" -version = "3.0.7" -description = "Python parsing module" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.6.8" [package.extras] diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.0.1" +version = "7.1.2" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} @@ -1076,11 +1072,11 @@ pytest = ">=3.10" [[package]] name = "pytest-mock" -version = "3.6.1" +version = "3.8.2" description = "Thin-wrapper around the mock package for easier use with pytest" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] pytest = ">=5.0" @@ -1159,21 +1155,21 @@ mando = ">=0.6,<0.7" [[package]] name = "requests" -version = "2.27.1" +version = "2.28.1" description = "Python HTTP for Humans." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.7, <4" [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} -idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +charset-normalizer = ">=2,<3" +idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" [package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "retry" @@ -1212,11 +1208,11 @@ python-versions = ">=3.5" [[package]] name = "s3transfer" -version = "0.5.2" +version = "0.6.0" description = "An Amazon S3 Transfer Manager" category = "main" optional = false -python-versions = ">= 3.6" +python-versions = ">= 3.7" [package.dependencies] botocore = ">=1.12.36,<2.0a.0" @@ -1369,8 +1365,8 @@ pydantic = ["pydantic", "email-validator"] [metadata] lock-version = "1.1" -python-versions = "^3.6.2" -content-hash = "c5243992c30d7eb90315717e8f33f0429eca5c65204c57653977ffe9c02173b8" +python-versions = "^3.7.10" +content-hash = "7eafeaf5644f693759fd097e599e85b0eeb8d32057e1971972d93cff52707a98" [metadata.files] atomicwrites = [] @@ -1391,33 +1387,86 @@ aws-cdk-lib = [ {file = "aws_cdk.aws_apigatewayv2_integrations_alpha-2.38.1a0-py3-none-any.whl", hash = "sha256:184fe6ce9718237fe69a4ccda9d7c5d62fd0adf823269d2816b4b9a57978c204"}, ] aws-xray-sdk = [] -bandit = [] +bandit = [ + {file = "bandit-1.7.4-py3-none-any.whl", hash = "sha256:412d3f259dab4077d0e7f0c11f50f650cc7d10db905d98f6520a95a18049658a"}, + {file = "bandit-1.7.4.tar.gz", hash = "sha256:2d63a8c573417bae338962d4b9b06fbc6080f74ecd955a092849e1e65c717bd2"}, +] black = [] boto3 = [ - {file = "boto3-1.23.10-py3-none-any.whl", hash = "sha256:40d08614f17a69075e175c02c5d5aab69a6153fd50e40fa7057b913ac7bf40e7"}, - {file = "boto3-1.23.10.tar.gz", hash = "sha256:2a4395e3241c20eef441d7443a5e6eaa0ee3f7114653fb9d9cef41587526f7bd"}, + {file = "boto3-1.24.58-py3-none-any.whl", hash = "sha256:edd79104b478203459a4e45a8602c83994ccd820c39fd85cc5de9aa0ee8e75d6"}, + {file = "boto3-1.24.58.tar.gz", hash = "sha256:8a888707586de7eb4ea367a4c51354d91efbe93c2e61dc02e5c98f8c8dc69584"}, ] botocore = [ - {file = "botocore-1.26.10-py3-none-any.whl", hash = "sha256:8a4a984bf901ccefe40037da11ba2abd1ddbcb3b490a492b7f218509c99fc12f"}, - {file = "botocore-1.26.10.tar.gz", hash = "sha256:5df2cf7ebe34377470172bd0bbc582cf98c5cbd02da0909a14e9e2885ab3ae9c"}, + {file = "botocore-1.27.58-py3-none-any.whl", hash = "sha256:4cc12ab3b6f195292e3e34da6aa356edb8acee51e8405d048b50ccbb64a1b9f3"}, + {file = "botocore-1.27.58.tar.gz", hash = "sha256:d904ee4743bd4b2517634b012c98dcb166f82e9d20e50d8367cd078779181ec2"}, ] cattrs = [] certifi = [] charset-normalizer = [ - {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, - {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, ] click = [ - {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, - {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] colorama = [] constructs = [ - {file = "constructs-10.1.84-py3-none-any.whl", hash = "sha256:8bc6047134f21ceea12753f5d3bfd439a4db01507ca727a3b1d4bc2bde077be7"}, - {file = "constructs-10.1.84.tar.gz", hash = "sha256:1b491afce3e73884faa42f32e94ed15fda6a70c91f700ea552ebe0ea3a0d9a0f"}, + {file = "constructs-10.1.85-py3-none-any.whl", hash = "sha256:dbcdeda26e61df8855af81e915a338345ca2d33f04b2b2861ac3a19fe98c5cfd"}, + {file = "constructs-10.1.85.tar.gz", hash = "sha256:70b2b4d75760524ff903aae0056862cd83ad9b0a5b857d9aef50597c8bbf4cfc"}, +] +coverage = [ + {file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"}, + {file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"}, + {file = "coverage-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbb0d89923c80dbd435b9cf8bba0ff55585a3cdb28cbec65f376c041472c60d"}, + {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67f9346aeebea54e845d29b487eb38ec95f2ecf3558a3cffb26ee3f0dcc3e760"}, + {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c499c14efd858b98c4e03595bf914089b98400d30789511577aa44607a1b74"}, + {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c35cca192ba700979d20ac43024a82b9b32a60da2f983bec6c0f5b84aead635c"}, + {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9cc4f107009bca5a81caef2fca843dbec4215c05e917a59dec0c8db5cff1d2aa"}, + {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f444627b3664b80d078c05fe6a850dd711beeb90d26731f11d492dcbadb6973"}, + {file = "coverage-6.4.4-cp310-cp310-win32.whl", hash = "sha256:66e6df3ac4659a435677d8cd40e8eb1ac7219345d27c41145991ee9bf4b806a0"}, + {file = "coverage-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:35ef1f8d8a7a275aa7410d2f2c60fa6443f4a64fae9be671ec0696a68525b875"}, + {file = "coverage-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1328d0c2f194ffda30a45f11058c02410e679456276bfa0bbe0b0ee87225fac"}, + {file = "coverage-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61b993f3998ee384935ee423c3d40894e93277f12482f6e777642a0141f55782"}, + {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5dd4b8e9cd0deb60e6fcc7b0647cbc1da6c33b9e786f9c79721fd303994832f"}, + {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7026f5afe0d1a933685d8f2169d7c2d2e624f6255fb584ca99ccca8c0e966fd7"}, + {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9c7b9b498eb0c0d48b4c2abc0e10c2d78912203f972e0e63e3c9dc21f15abdaa"}, + {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ee2b2fb6eb4ace35805f434e0f6409444e1466a47f620d1d5763a22600f0f892"}, + {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab066f5ab67059d1f1000b5e1aa8bbd75b6ed1fc0014559aea41a9eb66fc2ce0"}, + {file = "coverage-6.4.4-cp311-cp311-win32.whl", hash = "sha256:9d6e1f3185cbfd3d91ac77ea065d85d5215d3dfa45b191d14ddfcd952fa53796"}, + {file = "coverage-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e3d3c4cc38b2882f9a15bafd30aec079582b819bec1b8afdbde8f7797008108a"}, + {file = "coverage-6.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a095aa0a996ea08b10580908e88fbaf81ecf798e923bbe64fb98d1807db3d68a"}, + {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6f44409ab02e202b31a05dd6666797f9de2aa2b4b3534e9d450e42dea5e817"}, + {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b7101938584d67e6f45f0015b60e24a95bf8dea19836b1709a80342e01b472f"}, + {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a32ec68d721c3d714d9b105c7acf8e0f8a4f4734c811eda75ff3718570b5e3"}, + {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6a864733b22d3081749450466ac80698fe39c91cb6849b2ef8752fd7482011f3"}, + {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08002f9251f51afdcc5e3adf5d5d66bb490ae893d9e21359b085f0e03390a820"}, + {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3b2752de32c455f2521a51bd3ffb53c5b3ae92736afde67ce83477f5c1dd928"}, + {file = "coverage-6.4.4-cp37-cp37m-win32.whl", hash = "sha256:f855b39e4f75abd0dfbcf74a82e84ae3fc260d523fcb3532786bcbbcb158322c"}, + {file = "coverage-6.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ee6ae6bbcac0786807295e9687169fba80cb0617852b2fa118a99667e8e6815d"}, + {file = "coverage-6.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:564cd0f5b5470094df06fab676c6d77547abfdcb09b6c29c8a97c41ad03b103c"}, + {file = "coverage-6.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cbbb0e4cd8ddcd5ef47641cfac97d8473ab6b132dd9a46bacb18872828031685"}, + {file = "coverage-6.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6113e4df2fa73b80f77663445be6d567913fb3b82a86ceb64e44ae0e4b695de1"}, + {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d032bfc562a52318ae05047a6eb801ff31ccee172dc0d2504614e911d8fa83e"}, + {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e431e305a1f3126477abe9a184624a85308da8edf8486a863601d58419d26ffa"}, + {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cf2afe83a53f77aec067033199797832617890e15bed42f4a1a93ea24794ae3e"}, + {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:783bc7c4ee524039ca13b6d9b4186a67f8e63d91342c713e88c1865a38d0892a"}, + {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ff934ced84054b9018665ca3967fc48e1ac99e811f6cc99ea65978e1d384454b"}, + {file = "coverage-6.4.4-cp38-cp38-win32.whl", hash = "sha256:e1fabd473566fce2cf18ea41171d92814e4ef1495e04471786cbc943b89a3781"}, + {file = "coverage-6.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:4179502f210ebed3ccfe2f78bf8e2d59e50b297b598b100d6c6e3341053066a2"}, + {file = "coverage-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c0b9e9b572893cdb0a00e66cf961a238f8d870d4e1dc8e679eb8bdc2eb1b86"}, + {file = "coverage-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc600f6ec19b273da1d85817eda339fb46ce9eef3e89f220055d8696e0a06908"}, + {file = "coverage-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a98d6bf6d4ca5c07a600c7b4e0c5350cd483c85c736c522b786be90ea5bac4f"}, + {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2"}, + {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfa0b97eb904255e2ab24166071b27408f1f69c8fbda58e9c0972804851e0558"}, + {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fcbe3d9a53e013f8ab88734d7e517eb2cd06b7e689bedf22c0eb68db5e4a0a19"}, + {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:15e38d853ee224e92ccc9a851457fb1e1f12d7a5df5ae44544ce7863691c7a0d"}, + {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6913dddee2deff8ab2512639c5168c3e80b3ebb0f818fed22048ee46f735351a"}, + {file = "coverage-6.4.4-cp39-cp39-win32.whl", hash = "sha256:354df19fefd03b9a13132fa6643527ef7905712109d9c1c1903f2133d3a4e145"}, + {file = "coverage-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:1238b08f3576201ebf41f7c20bf59baa0d05da941b123c6656e42cdb668e9827"}, + {file = "coverage-6.4.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:f67cf9f406cf0d2f08a3515ce2db5b82625a7257f88aad87904674def6ddaec1"}, + {file = "coverage-6.4.4.tar.gz", hash = "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58"}, ] -coverage = [] -dataclasses = [] decorator = [] dnspython = [ {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"}, @@ -1436,12 +1485,18 @@ flake8 = [ {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] flake8-bugbear = [ - {file = "flake8-bugbear-22.8.22.tar.gz", hash = "sha256:1d240736dc93ecad9ff21e1d521283b9461d132b3ff2170569bd5ff3dacda385"}, - {file = "flake8_bugbear-22.8.22-py3-none-any.whl", hash = "sha256:b5718bf5d0d28a4cfd15054f4706fc2936f7339ed8366b919a1979eb5df50764"}, + {file = "flake8-bugbear-22.8.23.tar.gz", hash = "sha256:de0717d11124a082118dd08387b34fd86b2721642ec2d8e92be66cfa5ea7c445"}, + {file = "flake8_bugbear-22.8.23-py3-none-any.whl", hash = "sha256:1b0ebe0873d1cd55bf9f1588bfcb930db339018ef44a3981a26532daa9fd14a8"}, ] flake8-builtins = [] -flake8-comprehensions = [] -flake8-debugger = [] +flake8-comprehensions = [ + {file = "flake8-comprehensions-3.10.0.tar.gz", hash = "sha256:181158f7e7aa26a63a0a38e6017cef28c6adee71278ce56ce11f6ec9c4905058"}, + {file = "flake8_comprehensions-3.10.0-py3-none-any.whl", hash = "sha256:dad454fd3d525039121e98fa1dd90c46bc138708196a4ebbc949ad3c859adedb"}, +] +flake8-debugger = [ + {file = "flake8-debugger-4.1.2.tar.gz", hash = "sha256:52b002560941e36d9bf806fca2523dc7fb8560a295d5f1a6e15ac2ded7a73840"}, + {file = "flake8_debugger-4.1.2-py3-none-any.whl", hash = "sha256:0a5e55aeddcc81da631ad9c8c366e7318998f83ff00985a49e6b3ecf61e571bf"}, +] flake8-eradicate = [ {file = "flake8-eradicate-1.3.0.tar.gz", hash = "sha256:e4c98f00d17dc8653e3388cac2624cd81e9735de2fd4a8dcf99029633ebd7a63"}, {file = "flake8_eradicate-1.3.0-py3-none-any.whl", hash = "sha256:85a71e0c5f4e07f7c6c5fec520483561fd6bd295417d622855bdeade99242e3d"}, @@ -1458,7 +1513,10 @@ ghp-import = [ {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, ] gitdb = [] -gitpython = [] +gitpython = [ + {file = "GitPython-3.1.27-py3-none-any.whl", hash = "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d"}, + {file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"}, +] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, @@ -1469,19 +1527,69 @@ importlib-metadata = [ ] iniconfig = [] isort = [] -jinja2 = [] -jmespath = [] +jinja2 = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] +jmespath = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] jsii = [ {file = "jsii-1.65.0-py3-none-any.whl", hash = "sha256:47b939259ec465bc044aed6d3288b9b27a8edef2af21f1a8448e297b8429f154"}, {file = "jsii-1.65.0.tar.gz", hash = "sha256:a09ef1f9864fe27457ba4233b3a0744a76d2562077d5b8b3a7f2242edc4a8293"}, ] -mako = [] +mako = [ + {file = "Mako-1.2.1-py3-none-any.whl", hash = "sha256:df3921c3081b013c8a2d5ff03c18375651684921ae83fd12e64800b7da923257"}, + {file = "Mako-1.2.1.tar.gz", hash = "sha256:f054a5ff4743492f1aa9ecc47172cb33b42b9d993cffcc146c9de17e717b0307"}, +] mando = [] markdown = [ {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"}, {file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"}, ] -markupsafe = [] +markupsafe = [ + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, + {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, +] mccabe = [] mergedeep = [] mike = [] @@ -1544,7 +1652,10 @@ pbr = [ {file = "pbr-5.10.0.tar.gz", hash = "sha256:cfcc4ff8e698256fc17ea3ff796478b050852585aa5bae79ecd05b2ab7b39b9a"}, ] pdoc3 = [] -platformdirs = [] +platformdirs = [ + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, +] pluggy = [] publication = [] py = [] @@ -1599,16 +1710,19 @@ pygments = [ {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, ] pymdown-extensions = [] -pyparsing = [ - {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, - {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, +pyparsing = [] +pytest = [ + {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, + {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, ] -pytest = [] pytest-asyncio = [] pytest-benchmark = [] pytest-cov = [] pytest-forked = [] -pytest-mock = [] +pytest-mock = [ + {file = "pytest-mock-3.8.2.tar.gz", hash = "sha256:77f03f4554392558700295e05aed0b1096a20d4a60a4f3ddcde58b0c31c8fca2"}, + {file = "pytest_mock-3.8.2-py3-none-any.whl", hash = "sha256:8a9e226d6c0ef09fcf20c94eb3405c388af438a90f3e39687f84166da82d5948"}, +] pytest-xdist = [] python-dateutil = [] python-snappy = [ @@ -1698,10 +1812,7 @@ pyyaml = [ ] pyyaml-env-tag = [] radon = [] -requests = [ - {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, - {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, -] +requests = [] retry = [] "ruamel.yaml" = [ {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"}, @@ -1709,8 +1820,8 @@ retry = [] ] "ruamel.yaml.clib" = [] s3transfer = [ - {file = "s3transfer-0.5.2-py3-none-any.whl", hash = "sha256:7a6f4c4d1fdb9a2b640244008e142cbc2cd3ae34b386584ef044dd0f27101971"}, - {file = "s3transfer-0.5.2.tar.gz", hash = "sha256:95c58c194ce657a5f4fb0b9e60a84968c808888aed628cd98ab8771fe1db98ed"}, + {file = "s3transfer-0.6.0-py3-none-any.whl", hash = "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd"}, + {file = "s3transfer-0.6.0.tar.gz", hash = "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, diff --git a/pyproject.toml b/pyproject.toml index 3ff14cb2e58..f8931d367f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,6 @@ classifiers=[ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", @@ -20,7 +19,7 @@ keywords = ["aws_lambda_powertools", "aws", "tracing", "logging", "lambda", "pow license = "MIT-0" [tool.poetry.dependencies] -python = "^3.6.2" +python = "^3.7.10" aws-xray-sdk = "^2.8.0" fastjsonschema = "^2.14.5" boto3 = "^1.18" From 2aca4f06a313788bc34d4f143f3297c03fe97274 Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Wed, 24 Aug 2022 15:26:19 +0200 Subject: [PATCH 24/35] chore(event_handler): applied suggestions from code review Co-authored-by: Heitor Lessa --- .../event_handler/api_gateway.py | 3 ++- docs/core/event_handler/api_gateway.md | 10 +++---- docs/upgrade.md | 27 +++++++------------ tests/e2e/event_handler/infrastructure.py | 5 ++-- 4 files changed, 20 insertions(+), 25 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 190d74a813b..4ea2fd5b2a1 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -201,7 +201,8 @@ def _add_cors(self, cors: CORSConfig): def _add_cache_control(self, cache_control: str): """Set the specified cache control headers for 200 http responses. For non-200 `no-cache` is used.""" - self.response.headers["Cache-Control"].append(cache_control if self.response.status_code == 200 else "no-cache") + cache_control = cache_control if self.response.status_code == 200 else "no-cache" + self.response.headers["Cache-Control"].append(cache_control) def _compress(self): """Compress the response body, but only if `Accept-Encoding` headers includes gzip.""" diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index cad60cfadb8..c4cae718289 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -312,14 +312,14 @@ For convenience, these are the default values when using `CORSConfig` to enable ### Fine grained responses -You can use the `Response` class to have full control over the response, for example you might want to add additional headers, cookies, or set a custom Content-type. +You can use the `Response` class to have full control over the response. For example, you might want to add additional headers, cookies, or set a custom Content-type. ???+ info - Powertools serializes the headers and cookies according to the type of input event. - Some integrations require headers and cookies to be encoded as `multiValueHeaders`. + Powertools serializes headers and cookies according to the type of input event. + Some event sources require headers and cookies to be encoded as `multiValueHeaders`. -???+ warning - If you use the ALB integration, make sure you [enable the multi value headers feature](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers) in order to fully support this feature. +???+ warning "Using multiple values for HTTP headers in ALB?" + Make sure you [enable the multi value headers feature](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers) to serialize response headers correctly. === "fine_grained_responses.py" diff --git a/docs/upgrade.md b/docs/upgrade.md index e65c1bbfe91..bbcc2418a26 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -1,21 +1,23 @@ --- title: Upgrade guide -description: asdfasdf +description: Guide to update between major Powertools versions --- ## Migrate to v2 from v1 -The transition from Powertools for Python v1 to v2 is as painless as possible, as we strove for minimal breaking changes. -The API for event handler's Response has minor changes, but we kept the breaking changes to a bare minimum. We've also added some new features to some components. +The transition from Powertools for Python v1 to v2 is as painless as possible, as we aimed for minimal breaking changes. +Changes at a glance: + +* The API for **event handler's `Response`** has minor changes to support multi value headers and cookies. ???+ important Powertools for Python v2 drops suport for Python 3.6, following the Python 3.6 End-Of-Life (EOL) reached on December 23, 2021. ### Initial Steps -Before starting, it is highly suggested to make a copy of your current working project or create a new branch with git. +Before you start, we suggest making a copy of your current working project or create a new branch with git. 1. **Upgrade** Python to at least v3.7 @@ -25,14 +27,14 @@ Before starting, it is highly suggested to make a copy of your current working p pip install aws-lambda-powertools -U ``` -3. **Check** the following sections to see if any of your code is affected +3. **Review** the following sections to confirm whether they affect your code ## Event Handler Response (headers and cookies) -The `Response` class of the event handler utility was changed slightly: +The `Response` class of the event handler utility changed slightly: -1. The `headers` parameter now has a type signature of `Dict[str, List[str]]` -2. A new `cookies` parameter was added (type `List[str]`) +1. The `headers` parameter now expects a list of values per header (type `Dict[str, List[str]]`) +2. We introduced a new `cookies` parameter (type `List[str]`) ```python hl_lines="6 12 13" @app.get("/todos") @@ -51,12 +53,3 @@ def get_todos(): ) ``` -In the same way, it can be more convenient to just append headers to the response object: - -```python hl_lines="4 5" -@app.get("/todos") -def get_todos(): - response = Response(...) - response.headers["Content-Type"].append("text/plain") - response.cookies.append("CookieName=CookieValue") -``` diff --git a/tests/e2e/event_handler/infrastructure.py b/tests/e2e/event_handler/infrastructure.py index 9be373b234f..62421b8aac9 100644 --- a/tests/e2e/event_handler/infrastructure.py +++ b/tests/e2e/event_handler/infrastructure.py @@ -37,7 +37,7 @@ def _create_alb(self, function: Function): self._create_alb_listener( alb=alb, name="MultiValueHeader", - port=81, + port=8080, function=function, attributes={"lambda.multi_value_headers.enabled": "true"}, ) @@ -52,7 +52,7 @@ def _create_alb_listener( ): listener = alb.add_listener(name, port=port, protocol=elbv2.ApplicationProtocol.HTTP) target = listener.add_targets(f"ALB{name}Target", targets=[targets.LambdaTarget(function)]) - if attributes: + if attributes is not None: for key, value in attributes.items(): target.set_attribute(key, value) CfnOutput(self.stack, f"ALB{name}ListenerPort", value=str(port)) @@ -76,5 +76,6 @@ def _create_api_gateway_rest(self, function: Function): CfnOutput(self.stack, "APIGatewayRestUrl", value=apigw.url) def _create_lambda_function_url(self, function: Function): + # Maintenance: move auth to IAM when we create sigv4 builders function_url = function.add_function_url(auth_type=FunctionUrlAuthType.NONE) CfnOutput(self.stack, "LambdaFunctionUrl", value=function_url.url) From e6ccc69608858eff029f69e2188a05c61710d8e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Wed, 24 Aug 2022 15:29:40 +0200 Subject: [PATCH 25/35] chore(deps): remove old python version guard --- poetry.lock | 4 ++-- pyproject.toml | 32 ++++++++++++++++---------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/poetry.lock b/poetry.lock index 54533f932d0..02a8f74f446 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1365,8 +1365,8 @@ pydantic = ["pydantic", "email-validator"] [metadata] lock-version = "1.1" -python-versions = "^3.7.10" -content-hash = "7eafeaf5644f693759fd097e599e85b0eeb8d32057e1971972d93cff52707a98" +python-versions = "^3.7.4" +content-hash = "7e5f05b28cfe683dcfda4fb349d608bc1caeffb2ba1dd4095712cf597d0073b4" [metadata.files] atomicwrites = [] diff --git a/pyproject.toml b/pyproject.toml index f8931d367f6..0afa583fe22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ keywords = ["aws_lambda_powertools", "aws", "tracing", "logging", "lambda", "pow license = "MIT-0" [tool.poetry.dependencies] -python = "^3.7.10" +python = "^3.7.4" aws-xray-sdk = "^2.8.0" fastjsonschema = "^2.14.5" boto3 = "^1.18" @@ -53,24 +53,24 @@ mike = "^0.6.0" mypy = "^0.971" retry = "^0.9.2" pytest-xdist = "^2.5.0" -aws-cdk-lib = { version = "^2.38.1", python = ">=3.7" } -"aws-cdk.aws-apigatewayv2-alpha" = {version = "^2.38.1-alpha.0", python = ">=3.7"} -"aws-cdk.aws-apigatewayv2-integrations-alpha" = {version = "^2.38.1-alpha.0", python = ">=3.7"} +aws-cdk-lib = "^2.38.1" +"aws-cdk.aws-apigatewayv2-alpha" = "^2.38.1-alpha.0" +"aws-cdk.aws-apigatewayv2-integrations-alpha" = "^2.38.1-alpha.0" pytest-benchmark = "^3.4.1" -mypy-boto3-appconfig = { version = "^1.24.29", python = ">=3.7" } -mypy-boto3-cloudformation = { version = "^1.24.0", python = ">=3.7" } -mypy-boto3-cloudwatch = { version = "^1.24.35", python = ">=3.7" } -mypy-boto3-dynamodb = { version = "^1.24.27", python = ">=3.7" } -mypy-boto3-lambda = { version = "^1.24.0", python = ">=3.7" } -mypy-boto3-logs = { version = "^1.24.0", python = ">=3.7" } -mypy-boto3-secretsmanager = { version = "^1.24.11", python = ">=3.7" } -mypy-boto3-ssm = { version = "^1.24.0", python = ">=3.7" } -mypy-boto3-s3 = { version = "^1.24.0", python = ">=3.7" } -mypy-boto3-xray = { version = "^1.24.0", python = ">=3.7" } +mypy-boto3-appconfig = "^1.24.29" +mypy-boto3-cloudformation = "^1.24.0" +mypy-boto3-cloudwatch = "^1.24.35" +mypy-boto3-dynamodb = "^1.24.27" +mypy-boto3-lambda = "^1.24.0" +mypy-boto3-logs = "^1.24.0" +mypy-boto3-secretsmanager = "^1.24.11" +mypy-boto3-ssm = "^1.24.0" +mypy-boto3-s3 = "^1.24.0" +mypy-boto3-xray = "^1.24.0" types-requests = "^2.28.8" -typing-extensions = { version = "^4.3.0", python = ">=3.7" } +typing-extensions = "^4.3.0" python-snappy = "^0.6.1" -mkdocs-material = { version = "^8.3.9", python = ">=3.7" } +mkdocs-material = "^8.3.9" [tool.poetry.extras] pydantic = ["pydantic", "email-validator"] From bed825d2e8423b40b604ee866708990d95eeda3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Wed, 24 Aug 2022 16:01:49 +0200 Subject: [PATCH 26/35] chore(deps): upgrade black --- .../event_handler/api_gateway.py | 2 +- .../event_handler/appsync.py | 3 +- poetry.lock | 67 ++++++++++++++----- pyproject.toml | 3 +- tests/e2e/utils/data_fetcher/common.py | 4 +- 5 files changed, 59 insertions(+), 20 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 4ea2fd5b2a1..d4ccdfbde48 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -33,7 +33,7 @@ _SAFE_URI = "-._~()'!*:@,;" # https://www.ietf.org/rfc/rfc3986.txt # API GW/ALB decode non-safe URI chars; we must support them too _UNSAFE_URI = "%<> \[\]{}|^" # noqa: W605 -_NAMED_GROUP_BOUNDARY_PATTERN = fr"(?P\1[{_SAFE_URI}{_UNSAFE_URI}\\w]+)" +_NAMED_GROUP_BOUNDARY_PATTERN = rf"(?P\1[{_SAFE_URI}{_UNSAFE_URI}\\w]+)" class ProxyEventType(Enum): diff --git a/aws_lambda_powertools/event_handler/appsync.py b/aws_lambda_powertools/event_handler/appsync.py index 896b303cd08..4ddc51cd102 100644 --- a/aws_lambda_powertools/event_handler/appsync.py +++ b/aws_lambda_powertools/event_handler/appsync.py @@ -1,5 +1,4 @@ import logging -from abc import ABC from typing import Any, Callable, Optional, Type, TypeVar from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent @@ -10,7 +9,7 @@ AppSyncResolverEventT = TypeVar("AppSyncResolverEventT", bound=AppSyncResolverEvent) -class BaseRouter(ABC): +class BaseRouter: current_event: AppSyncResolverEventT # type: ignore[valid-type] lambda_context: LambdaContext diff --git a/poetry.lock b/poetry.lock index 02a8f74f446..43c92ec29ac 100644 --- a/poetry.lock +++ b/poetry.lock @@ -98,29 +98,25 @@ yaml = ["pyyaml"] [[package]] name = "black" -version = "21.12b0" +version = "22.6.0" description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.6.2" [package.dependencies] -click = ">=7.1.2" +click = ">=8.0.0" mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0,<1" +pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = ">=0.2.6,<2.0.0" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} -typing-extensions = [ - {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, - {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, -] +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -python2 = ["typed-ast (>=1.4.3)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] @@ -325,6 +321,19 @@ mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.7.0,<2.8.0" pyflakes = ">=2.3.0,<2.4.0" +[[package]] +name = "flake8-black" +version = "0.3.3" +description = "flake8 plugin to call black as a code style validator" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +black = ">=22.1.0" +flake8 = ">=3.0.0" +tomli = "*" + [[package]] name = "flake8-bugbear" version = "22.8.23" @@ -1250,11 +1259,11 @@ pbr = ">=2.0.0,<2.1.0 || >2.1.0" [[package]] name = "tomli" -version = "1.2.3" +version = "2.0.1" description = "A lil' TOML parser" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "typed-ast" @@ -1366,7 +1375,7 @@ pydantic = ["pydantic", "email-validator"] [metadata] lock-version = "1.1" python-versions = "^3.7.4" -content-hash = "7e5f05b28cfe683dcfda4fb349d608bc1caeffb2ba1dd4095712cf597d0073b4" +content-hash = "5f36bb2dc0877d19d3a6405df72819c3678fc97fa5ae3e31f9b125c40f39404f" [metadata.files] atomicwrites = [] @@ -1391,7 +1400,31 @@ bandit = [ {file = "bandit-1.7.4-py3-none-any.whl", hash = "sha256:412d3f259dab4077d0e7f0c11f50f650cc7d10db905d98f6520a95a18049658a"}, {file = "bandit-1.7.4.tar.gz", hash = "sha256:2d63a8c573417bae338962d4b9b06fbc6080f74ecd955a092849e1e65c717bd2"}, ] -black = [] +black = [ + {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"}, + {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"}, + {file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"}, + {file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"}, + {file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"}, + {file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"}, + {file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"}, + {file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"}, + {file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"}, + {file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"}, + {file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"}, + {file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"}, + {file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"}, + {file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"}, + {file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"}, + {file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"}, + {file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"}, + {file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"}, + {file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"}, + {file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"}, + {file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"}, + {file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"}, + {file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"}, +] boto3 = [ {file = "boto3-1.24.58-py3-none-any.whl", hash = "sha256:edd79104b478203459a4e45a8602c83994ccd820c39fd85cc5de9aa0ee8e75d6"}, {file = "boto3-1.24.58.tar.gz", hash = "sha256:8a888707586de7eb4ea367a4c51354d91efbe93c2e61dc02e5c98f8c8dc69584"}, @@ -1484,6 +1517,10 @@ flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] +flake8-black = [ + {file = "flake8-black-0.3.3.tar.gz", hash = "sha256:8211f5e20e954cb57c709acccf2f3281ce27016d4c4b989c3e51f878bb7ce12a"}, + {file = "flake8_black-0.3.3-py3-none-any.whl", hash = "sha256:7d667d0059fd1aa468de1669d77cc934b7f1feeac258d57bdae69a8e73c4cd90"}, +] flake8-bugbear = [ {file = "flake8-bugbear-22.8.23.tar.gz", hash = "sha256:de0717d11124a082118dd08387b34fd86b2721642ec2d8e92be66cfa5ea7c445"}, {file = "flake8_bugbear-22.8.23-py3-none-any.whl", hash = "sha256:1b0ebe0873d1cd55bf9f1588bfcb930db339018ef44a3981a26532daa9fd14a8"}, @@ -1830,8 +1867,8 @@ six = [ smmap = [] stevedore = [] tomli = [ - {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, - {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] typed-ast = [ {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, diff --git a/pyproject.toml b/pyproject.toml index 0afa583fe22..4b75a9822ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,13 +31,14 @@ email-validator = {version = "*", optional = true } # issue #1148 coverage = {extras = ["toml"], version = "^6.2"} pytest = "^7.0.1" -black = "^21.12b0" +black = "^22.6.0" flake8-builtins = "^1.5.3" flake8-comprehensions = "^3.7.0" flake8-debugger = "^4.0.0" flake8-fixme = "^1.1.1" flake8-isort = "^4.1.2" flake8-variables-names = "^0.0.4" +flake8-black = "^0.3.3" isort = "^5.10.1" pytest-cov = "^3.0.0" pytest-mock = "^3.5.1" diff --git a/tests/e2e/utils/data_fetcher/common.py b/tests/e2e/utils/data_fetcher/common.py index 06520b1f92d..29f97eab2de 100644 --- a/tests/e2e/utils/data_fetcher/common.py +++ b/tests/e2e/utils/data_fetcher/common.py @@ -22,4 +22,6 @@ def get_lambda_response( @retry(RequestException, delay=2, jitter=1.5, tries=5) def get_http_response(request: Request) -> Response: session = requests.Session() - return session.send(request.prepare()) + result = session.send(request.prepare()) + result.raise_for_status() + return result From 70f61dcd552fee57575d9279b609ba14c1b4a8af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Thu, 25 Aug 2022 10:04:34 +0200 Subject: [PATCH 27/35] chore(test): move cookie name to GIVEN block --- tests/functional/event_handler/test_lambda_function_url.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/functional/event_handler/test_lambda_function_url.py b/tests/functional/event_handler/test_lambda_function_url.py index c24c9f0e0c8..ae0a231d46b 100644 --- a/tests/functional/event_handler/test_lambda_function_url.py +++ b/tests/functional/event_handler/test_lambda_function_url.py @@ -28,12 +28,13 @@ def foo(): def test_lambda_function_url_event_with_cookies(): # GIVEN a Lambda Function Url type event app = LambdaFunctionUrlResolver() + cookie = "CookieMonster" @app.get("/") def foo(): assert isinstance(app.current_event, LambdaFunctionUrlEvent) assert app.lambda_context == {} - return Response(200, content_types.TEXT_PLAIN, "foo", cookies=["CookieMonster"]) + return Response(200, content_types.TEXT_PLAIN, "foo", cookies=[cookie]) # WHEN calling the event handler result = app(load_event("lambdaFunctionUrlEvent.json"), {}) @@ -41,7 +42,7 @@ def foo(): # THEN process event correctly # AND set the current_event type as LambdaFunctionUrlEvent assert result["statusCode"] == 200 - assert result["cookies"] == ["CookieMonster"] + assert result["cookies"] == [cookie] def test_lambda_function_url_no_matches(): From 1b4b18d663fffd9369bafd92153dedf047b6e62e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Thu, 25 Aug 2022 10:06:03 +0200 Subject: [PATCH 28/35] chore(docs): move constant to GIVEN block --- tests/functional/event_handler/test_api_gateway.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index f880012081d..3cc8fd9b0fe 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -98,11 +98,12 @@ def get_lambda() -> Response: def test_api_gateway_v1_cookies(): # GIVEN a Http API V1 proxy type event app = APIGatewayRestResolver() + cookie = "CookieMonster" @app.get("/my/path") def get_lambda() -> Response: assert isinstance(app.current_event, APIGatewayProxyEvent) - return Response(200, content_types.TEXT_PLAIN, "Hello world", cookies=["CookieMonster"]) + return Response(200, content_types.TEXT_PLAIN, "Hello world", cookies=[cookie]) # WHEN calling the event handler result = app(LOAD_GW_EVENT, {}) @@ -110,7 +111,7 @@ def get_lambda() -> Response: # THEN process event correctly # AND set the current_event type as APIGatewayProxyEvent assert result["statusCode"] == 200 - assert result["multiValueHeaders"]["Set-Cookie"] == ["CookieMonster"] + assert result["multiValueHeaders"]["Set-Cookie"] == [cookie] def test_api_gateway(): @@ -157,11 +158,12 @@ def my_path() -> Response: def test_api_gateway_v2_cookies(): # GIVEN a Http API V2 proxy type event app = APIGatewayHttpResolver() + cookie = "CookieMonster" @app.post("/my/path") def my_path() -> Response: assert isinstance(app.current_event, APIGatewayProxyEventV2) - return Response(200, content_types.TEXT_PLAIN, "Hello world", cookies=["CookieMonster"]) + return Response(200, content_types.TEXT_PLAIN, "Hello world", cookies=[cookie]) # WHEN calling the event handler result = app(load_event("apiGatewayProxyV2Event.json"), {}) @@ -170,7 +172,7 @@ def my_path() -> Response: # AND set the current_event type as APIGatewayProxyEventV2 assert result["statusCode"] == 200 assert result["headers"]["Content-Type"] == content_types.TEXT_PLAIN - assert result["cookies"] == ["CookieMonster"] + assert result["cookies"] == [cookie] def test_include_rule_matching(): From 8fe83986e024f04e006a95247ae2454095e345f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Thu, 25 Aug 2022 12:20:35 +0200 Subject: [PATCH 29/35] chore(tests): use table-testing --- tests/functional/test_headers_serializer.py | 102 ++++++++++++-------- 1 file changed, 64 insertions(+), 38 deletions(-) diff --git a/tests/functional/test_headers_serializer.py b/tests/functional/test_headers_serializer.py index bca6c884075..4829e7aa5d2 100644 --- a/tests/functional/test_headers_serializer.py +++ b/tests/functional/test_headers_serializer.py @@ -1,4 +1,8 @@ import warnings +from collections import defaultdict +from typing import Dict, List + +import pytest from aws_lambda_powertools.shared.headers_serializer import ( HttpApiHeadersSerializer, @@ -7,49 +11,71 @@ ) -def test_headers_serializer_http_api(): +@pytest.mark.parametrize( + "cookies,headers,result", + [ + ([], {}, {"cookies": [], "headers": {}}), + ([], {"Content-Type": ["text/html"]}, {"cookies": [], "headers": {"Content-Type": "text/html"}}), + (["UUID=12345"], {}, {"cookies": ["UUID=12345"], "headers": {}}), + ( + ["UUID=12345", "SSID=0xdeadbeef"], + {"Foo": ["bar", "zbr"]}, + {"cookies": ["UUID=12345", "SSID=0xdeadbeef"], "headers": {"Foo": "bar, zbr"}}, + ), + ], + ids=["no_cookies", "just_headers", "just_cookies", "multiple_headers_and_cookies"], +) +def test_headers_serializer_http_api(cookies: List[str], headers: Dict[str, List[str]], result: Dict): serializer = HttpApiHeadersSerializer() - - payload = serializer.serialize(cookies=[], headers={}) - assert payload == {"cookies": [], "headers": {}} - - payload = serializer.serialize(cookies=[], headers={"Content-Type": ["text/html"]}) - assert payload == {"cookies": [], "headers": {"Content-Type": "text/html"}} - - payload = serializer.serialize(cookies=["UUID=12345"], headers={}) - assert payload == {"cookies": ["UUID=12345"], "headers": {}} - - payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": ["bar", "zbr"]}) - assert payload == {"cookies": ["UUID=12345", "SSID=0xdeadbeef"], "headers": {"Foo": "bar, zbr"}} - - -def test_headers_serializer_multi_value_headers(): + payload = serializer.serialize(headers=headers, cookies=cookies) + assert payload == result + + +@pytest.mark.parametrize( + "cookies,headers,result", + [ + ([], {}, {"multiValueHeaders": defaultdict(list, **{})}), + ( + [], + {"Content-Type": ["text/html"]}, + {"multiValueHeaders": defaultdict(list, **{"Content-Type": ["text/html"]})}, + ), + (["UUID=12345"], {}, {"multiValueHeaders": defaultdict(list, **{"Set-Cookie": ["UUID=12345"]})}), + ( + ["UUID=12345", "SSID=0xdeadbeef"], + {"Foo": ["bar", "zbr"]}, + { + "multiValueHeaders": defaultdict( + list, **{"Set-Cookie": ["UUID=12345", "SSID=0xdeadbeef"], "Foo": ["bar", "zbr"]} + ) + }, + ), + ], + ids=["no_cookies", "just_headers", "just_cookies", "multiple_headers_and_cookies"], +) +def test_headers_serializer_multi_value_headers(cookies: List[str], headers: Dict[str, List[str]], result: Dict): serializer = MultiValueHeadersSerializer() - - payload = serializer.serialize(cookies=[], headers={}) - assert payload == {"multiValueHeaders": {}} - - payload = serializer.serialize(cookies=[], headers={"Content-Type": ["text/html"]}) - assert payload == {"multiValueHeaders": {"Content-Type": ["text/html"]}} - - payload = serializer.serialize(cookies=["UUID=12345"], headers={}) - assert payload == {"multiValueHeaders": {"Set-Cookie": ["UUID=12345"]}} - - payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": ["bar", "zbr"]}) - assert payload == {"multiValueHeaders": {"Set-Cookie": ["UUID=12345", "SSID=0xdeadbeef"], "Foo": ["bar", "zbr"]}} - - -def test_headers_serializer_single_value_headers(): + payload = serializer.serialize(headers=headers, cookies=cookies) + assert payload == result + + +@pytest.mark.parametrize( + "cookies,headers,result", + [ + ([], {}, {"headers": {}}), + ([], {"Content-Type": ["text/html"]}, {"headers": {"Content-Type": "text/html"}}), + (["UUID=12345"], {}, {"headers": {"Set-Cookie": "UUID=12345"}}), + ], + ids=["no_cookies", "just_headers", "just_cookies"], +) +def test_headers_serializer_single_value_headers(cookies: List[str], headers: Dict[str, List[str]], result: Dict): serializer = SingleValueHeadersSerializer() + payload = serializer.serialize(headers=headers, cookies=cookies) + assert payload == result - payload = serializer.serialize(cookies=[], headers={}) - assert payload == {"headers": {}} - payload = serializer.serialize(cookies=[], headers={"Content-Type": ["text/html"]}) - assert payload == {"headers": {"Content-Type": "text/html"}} - - payload = serializer.serialize(cookies=["UUID=12345"], headers={}) - assert payload == {"headers": {"Set-Cookie": "UUID=12345"}} +def test_headers_serializer_single_value_headers_multiple_cookies(): + serializer = SingleValueHeadersSerializer() with warnings.catch_warnings(record=True) as w: warnings.simplefilter("default") From 56b0d2cfae5536557d1ce002365c6566b48d927c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Thu, 25 Aug 2022 12:25:03 +0200 Subject: [PATCH 30/35] chore: re-added sorting of CORS headers --- aws_lambda_powertools/event_handler/api_gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index d4ccdfbde48..7cdd347dca0 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -610,7 +610,7 @@ def _not_found(self, method: str) -> ResponseBuilder: if method == "OPTIONS": logger.debug("Pre-flight request detected. Returning CORS with null response") - headers["Access-Control-Allow-Methods"].append(",".join(self._cors_methods)) + headers["Access-Control-Allow-Methods"].append(",".join(sorted(self._cors_methods))) return ResponseBuilder(Response(status_code=204, content_type=None, headers=headers, body="")) handler = self._lookup_exception_handler(NotFoundError) From fbac72889602e68184a60b43c5368ef21db6d720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Thu, 25 Aug 2022 12:28:11 +0200 Subject: [PATCH 31/35] chore: simplified tests --- tests/functional/event_handler/test_api_gateway.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 3cc8fd9b0fe..125a0f8c147 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -268,7 +268,7 @@ def handler(event, context): assert headers["Content-Type"] == [content_types.TEXT_HTML] assert headers["Access-Control-Allow-Origin"] == ["*"] assert "Access-Control-Allow-Credentials" not in headers - assert sorted(headers["Access-Control-Allow-Headers"][0].split(",")) == sorted(CORSConfig._REQUIRED_HEADERS) + assert headers["Access-Control-Allow-Headers"] == [",".join(sorted(CORSConfig._REQUIRED_HEADERS))] # THEN for routes without cors flag return no cors headers mock_event = {"path": "/my/request", "httpMethod": "GET"} @@ -560,7 +560,7 @@ def post_no_cors(): headers = result["multiValueHeaders"] assert "Content-Type" not in headers assert "Access-Control-Allow-Origin" in result["multiValueHeaders"] - assert sorted(headers["Access-Control-Allow-Methods"][0].split(",")) == ["DELETE", "GET", "OPTIONS"] + assert headers["Access-Control-Allow-Methods"] == [",".join(sorted(["DELETE", "GET", "OPTIONS"]))] def test_custom_preflight_response(): From 840e9c44ab913a5ae94d13377b71312a47680f60 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 29 Aug 2022 11:17:13 +0200 Subject: [PATCH 32/35] chore: change test name to ease scanning Signed-off-by: heitorlessa --- tests/functional/test_headers_serializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_headers_serializer.py b/tests/functional/test_headers_serializer.py index 4829e7aa5d2..a63c94d8072 100644 --- a/tests/functional/test_headers_serializer.py +++ b/tests/functional/test_headers_serializer.py @@ -74,7 +74,7 @@ def test_headers_serializer_single_value_headers(cookies: List[str], headers: Di assert payload == result -def test_headers_serializer_single_value_headers_multiple_cookies(): +def test_multiple_cookies_with_single_value_headers_serializer(): serializer = SingleValueHeadersSerializer() with warnings.catch_warnings(record=True) as w: From 60d07f7d84b19b56fe9147d76ef5c857fce843d8 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 29 Aug 2022 11:36:46 +0200 Subject: [PATCH 33/35] chore: revert parametrize into single tests like Ruben had Signed-off-by: heitorlessa --- tests/functional/test_headers_serializer.py | 29 ++++++++++++--------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/tests/functional/test_headers_serializer.py b/tests/functional/test_headers_serializer.py index a63c94d8072..8fc4d42c589 100644 --- a/tests/functional/test_headers_serializer.py +++ b/tests/functional/test_headers_serializer.py @@ -59,19 +59,24 @@ def test_headers_serializer_multi_value_headers(cookies: List[str], headers: Dic assert payload == result -@pytest.mark.parametrize( - "cookies,headers,result", - [ - ([], {}, {"headers": {}}), - ([], {"Content-Type": ["text/html"]}, {"headers": {"Content-Type": "text/html"}}), - (["UUID=12345"], {}, {"headers": {"Set-Cookie": "UUID=12345"}}), - ], - ids=["no_cookies", "just_headers", "just_cookies"], -) -def test_headers_serializer_single_value_headers(cookies: List[str], headers: Dict[str, List[str]], result: Dict): +def single_value_headers_serializer_with_cookies_only(): + content_type = "text/html" serializer = SingleValueHeadersSerializer() - payload = serializer.serialize(headers=headers, cookies=cookies) - assert payload == result + payload = serializer.serialize(headers={"Content-Type": [content_type]}, cookies=[]) + assert payload["headers"]["Content-Type"] == content_type + + +def single_value_headers_serializer_with_headers_only(): + cookie = "UUID=12345" + serializer = SingleValueHeadersSerializer() + payload = serializer.serialize(headers={}, cookies=[cookie]) + assert payload["headers"] == {"Set-Cookie": cookie} + + +def single_value_headers_serializer_with_empty_values(): + serializer = SingleValueHeadersSerializer() + payload = serializer.serialize(headers={}, cookies=[]) + assert payload == {"headers": {}} def test_multiple_cookies_with_single_value_headers_serializer(): From 8ce6c506823cd066e49bdb6f0b9d99f7ba168dd5 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 29 Aug 2022 13:20:02 +0200 Subject: [PATCH 34/35] chore: revert parametrize into single tests like Ruben had pt2 Signed-off-by: heitorlessa --- tests/functional/test_headers_serializer.py | 155 ++++++++++++-------- 1 file changed, 91 insertions(+), 64 deletions(-) diff --git a/tests/functional/test_headers_serializer.py b/tests/functional/test_headers_serializer.py index 8fc4d42c589..a5dd7696b66 100644 --- a/tests/functional/test_headers_serializer.py +++ b/tests/functional/test_headers_serializer.py @@ -1,6 +1,4 @@ -import warnings from collections import defaultdict -from typing import Dict, List import pytest @@ -11,89 +9,118 @@ ) -@pytest.mark.parametrize( - "cookies,headers,result", - [ - ([], {}, {"cookies": [], "headers": {}}), - ([], {"Content-Type": ["text/html"]}, {"cookies": [], "headers": {"Content-Type": "text/html"}}), - (["UUID=12345"], {}, {"cookies": ["UUID=12345"], "headers": {}}), - ( - ["UUID=12345", "SSID=0xdeadbeef"], - {"Foo": ["bar", "zbr"]}, - {"cookies": ["UUID=12345", "SSID=0xdeadbeef"], "headers": {"Foo": "bar, zbr"}}, - ), - ], - ids=["no_cookies", "just_headers", "just_cookies", "multiple_headers_and_cookies"], -) -def test_headers_serializer_http_api(cookies: List[str], headers: Dict[str, List[str]], result: Dict): +def test_http_api_headers_serializer(): + cookies = ["UUID=12345", "SSID=0xdeadbeef"] + header_values = ["bar", "zbr"] + headers = {"Foo": header_values} + serializer = HttpApiHeadersSerializer() payload = serializer.serialize(headers=headers, cookies=cookies) - assert payload == result - - -@pytest.mark.parametrize( - "cookies,headers,result", - [ - ([], {}, {"multiValueHeaders": defaultdict(list, **{})}), - ( - [], - {"Content-Type": ["text/html"]}, - {"multiValueHeaders": defaultdict(list, **{"Content-Type": ["text/html"]})}, - ), - (["UUID=12345"], {}, {"multiValueHeaders": defaultdict(list, **{"Set-Cookie": ["UUID=12345"]})}), - ( - ["UUID=12345", "SSID=0xdeadbeef"], - {"Foo": ["bar", "zbr"]}, - { - "multiValueHeaders": defaultdict( - list, **{"Set-Cookie": ["UUID=12345", "SSID=0xdeadbeef"], "Foo": ["bar", "zbr"]} - ) - }, - ), - ], - ids=["no_cookies", "just_headers", "just_cookies", "multiple_headers_and_cookies"], -) -def test_headers_serializer_multi_value_headers(cookies: List[str], headers: Dict[str, List[str]], result: Dict): + + assert payload["cookies"] == cookies + assert payload["headers"]["Foo"] == ", ".join(header_values) + + +def test_http_api_headers_serializer_with_empty_values(): + serializer = HttpApiHeadersSerializer() + payload = serializer.serialize(headers={}, cookies=[]) + assert payload == {"headers": {}, "cookies": []} + + +def test_http_api_headers_serializer_with_headers_only(): + content_type = "text/html" + serializer = HttpApiHeadersSerializer() + payload = serializer.serialize(headers={"Content-Type": [content_type]}, cookies=[]) + assert payload["headers"]["Content-Type"] == content_type + + +def test_http_api_headers_serializer_with_cookies_only(): + cookies = ["UUID=12345", "SSID=0xdeadbeef"] + serializer = HttpApiHeadersSerializer() + payload = serializer.serialize(headers={}, cookies=cookies) + assert payload["cookies"] == cookies + + +def test_multi_value_headers_serializer(): + cookies = ["UUID=12345", "SSID=0xdeadbeef"] + header_values = ["bar", "zbr"] + headers = {"Foo": header_values} + serializer = MultiValueHeadersSerializer() payload = serializer.serialize(headers=headers, cookies=cookies) - assert payload == result + + assert payload["multiValueHeaders"]["Set-Cookie"] == cookies + assert payload["multiValueHeaders"]["Foo"] == header_values + + +def test_multi_value_headers_serializer_with_headers_only(): + content_type = "text/html" + serializer = MultiValueHeadersSerializer() + payload = serializer.serialize(headers={"Content-Type": [content_type]}, cookies=[]) + assert payload["multiValueHeaders"]["Content-Type"] == [content_type] + + +def test_multi_value_headers_serializer_with_cookies_only(): + cookie = "UUID=12345" + serializer = MultiValueHeadersSerializer() + payload = serializer.serialize(headers={}, cookies=[cookie]) + assert payload["multiValueHeaders"]["Set-Cookie"] == [cookie] + + +def test_multi_value_headers_serializer_with_empty_values(): + serializer = MultiValueHeadersSerializer() + payload = serializer.serialize(headers={}, cookies=[]) + assert payload["multiValueHeaders"] == defaultdict(list) + + +def test_single_value_headers_serializer(): + cookie = "UUID=12345" + content_type = "text/html" + headers = {"Content-Type": [content_type]} + + serializer = SingleValueHeadersSerializer() + payload = serializer.serialize(headers=headers, cookies=[cookie]) + assert payload["headers"]["Content-Type"] == content_type + assert payload["headers"]["Set-Cookie"] == cookie -def single_value_headers_serializer_with_cookies_only(): +def test_single_value_headers_serializer_with_headers_only(): content_type = "text/html" serializer = SingleValueHeadersSerializer() payload = serializer.serialize(headers={"Content-Type": [content_type]}, cookies=[]) assert payload["headers"]["Content-Type"] == content_type -def single_value_headers_serializer_with_headers_only(): +def test_single_value_headers_serializer_with_cookies_only(): cookie = "UUID=12345" serializer = SingleValueHeadersSerializer() payload = serializer.serialize(headers={}, cookies=[cookie]) assert payload["headers"] == {"Set-Cookie": cookie} -def single_value_headers_serializer_with_empty_values(): +def test_single_value_headers_serializer_with_empty_values(): serializer = SingleValueHeadersSerializer() payload = serializer.serialize(headers={}, cookies=[]) - assert payload == {"headers": {}} + assert payload["headers"] == {} -def test_multiple_cookies_with_single_value_headers_serializer(): +def test_single_value_headers_with_multiple_cookies_warning(): + cookies = ["UUID=12345", "SSID=0xdeadbeef"] + warning_message = "Can't encode more than one cookie in the response. Sending the last cookie only." serializer = SingleValueHeadersSerializer() - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("default") - - payload = serializer.serialize(cookies=["UUID=12345", "SSID=0xdeadbeef"], headers={"Foo": ["bar", "zbr"]}) - assert payload == {"headers": {"Set-Cookie": "SSID=0xdeadbeef", "Foo": "zbr"}} - - assert len(w) == 2 - assert str(w[-2].message) == ( - "Can't encode more than one cookie in the response. Sending the last cookie only. " - "Did you enable multiValueHeaders on the ALB Target Group?" - ) - assert str(w[-1].message) == ( - "Can't encode more than one header value for the same key ('Foo') in the response. " - "Did you enable multiValueHeaders on the ALB Target Group?" - ) + with pytest.warns(match=warning_message): + payload = serializer.serialize(cookies=cookies, headers={}) + + assert payload["headers"]["Set-Cookie"] == cookies[-1] + + +def test_single_value_headers_with_multiple_header_values_warning(): + headers = {"Foo": ["bar", "zbr"]} + warning_message = "Can't encode more than one header value for the same key." + serializer = SingleValueHeadersSerializer() + + with pytest.warns(match=warning_message): + payload = serializer.serialize(cookies=[], headers=headers) + + assert payload["headers"]["Foo"] == headers["Foo"][-1] From a27d674ec1666b31a7e2ade05f5d7587a332908e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Mon, 29 Aug 2022 13:52:25 +0200 Subject: [PATCH 35/35] feat(event_handler): supppor headers with either str or List[str] values --- .../event_handler/api_gateway.py | 30 +++++++------- .../shared/headers_serializer.py | 41 +++++++++++-------- docs/upgrade.md | 6 ++- .../src/binary_responses_output.json | 4 +- .../src/compressing_responses_output.json | 6 +-- .../src/fine_grained_responses_output.json | 8 ++-- ...ting_started_rest_api_resolver_output.json | 4 +- .../src/setting_cors_output.json | 8 ++-- tests/functional/test_headers_serializer.py | 21 ++++++++++ 9 files changed, 80 insertions(+), 48 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 7cdd347dca0..11adcfc2ed6 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -7,7 +7,6 @@ import warnings import zlib from abc import ABC, abstractmethod -from collections import defaultdict from enum import Enum from functools import partial from http import HTTPStatus @@ -123,18 +122,19 @@ def __init__( self.max_age = max_age self.allow_credentials = allow_credentials - def to_dict(self) -> Dict[str, List[str]]: + def to_dict(self) -> Dict[str, str]: """Builds the configured Access-Control http headers""" - headers: Dict[str, List[str]] = defaultdict(list) - headers["Access-Control-Allow-Origin"].append(self.allow_origin) - headers["Access-Control-Allow-Headers"].append(",".join(sorted(self.allow_headers))) + headers: Dict[str, str] = { + "Access-Control-Allow-Origin": self.allow_origin, + "Access-Control-Allow-Headers": ",".join(sorted(self.allow_headers)), + } if self.expose_headers: - headers["Access-Control-Expose-Headers"].append(",".join(self.expose_headers)) + headers["Access-Control-Expose-Headers"] = ",".join(self.expose_headers) if self.max_age is not None: - headers["Access-Control-Max-Age"].append(str(self.max_age)) + headers["Access-Control-Max-Age"] = str(self.max_age) if self.allow_credentials is True: - headers["Access-Control-Allow-Credentials"].append("true") + headers["Access-Control-Allow-Credentials"] = "true" return headers @@ -146,7 +146,7 @@ def __init__( status_code: int, content_type: Optional[str], body: Union[str, bytes, None], - headers: Optional[Dict[str, List[str]]] = None, + headers: Optional[Dict[str, Union[str, List[str]]]] = None, cookies: Optional[List[str]] = None, ): """ @@ -160,7 +160,7 @@ def __init__( provided http headers body: Union[str, bytes, None] Optionally set the response body. Note: bytes body will be automatically base64 encoded - headers: dict[str, List[str]] + headers: dict[str, Union[str, List[str]]] Optionally set specific http headers. Setting "Content-Type" here would override the `content_type` value. cookies: list[str] Optionally set cookies. @@ -168,10 +168,10 @@ def __init__( self.status_code = status_code self.body = body self.base64_encoded = False - self.headers: Dict[str, List[str]] = defaultdict(list, **headers) if headers else defaultdict(list) + self.headers: Dict[str, Union[str, List[str]]] = headers if headers else {} self.cookies = cookies or [] if content_type: - self.headers.setdefault("Content-Type", [content_type]) + self.headers.setdefault("Content-Type", content_type) class Route: @@ -202,7 +202,7 @@ def _add_cors(self, cors: CORSConfig): def _add_cache_control(self, cache_control: str): """Set the specified cache control headers for 200 http responses. For non-200 `no-cache` is used.""" cache_control = cache_control if self.response.status_code == 200 else "no-cache" - self.response.headers["Cache-Control"].append(cache_control) + self.response.headers["Cache-Control"] = cache_control def _compress(self): """Compress the response body, but only if `Accept-Encoding` headers includes gzip.""" @@ -603,14 +603,14 @@ def _path_starts_with(path: str, prefix: str): def _not_found(self, method: str) -> ResponseBuilder: """Called when no matching route was found and includes support for the cors preflight response""" - headers: Dict[str, List[str]] = defaultdict(list) + headers: Dict[str, Union[str, List[str]]] = {} if self._cors: logger.debug("CORS is enabled, updating headers.") headers.update(self._cors.to_dict()) if method == "OPTIONS": logger.debug("Pre-flight request detected. Returning CORS with null response") - headers["Access-Control-Allow-Methods"].append(",".join(sorted(self._cors_methods))) + headers["Access-Control-Allow-Methods"] = ",".join(sorted(self._cors_methods)) return ResponseBuilder(Response(status_code=204, content_type=None, headers=headers, body="")) handler = self._lookup_exception_handler(NotFoundError) diff --git a/aws_lambda_powertools/shared/headers_serializer.py b/aws_lambda_powertools/shared/headers_serializer.py index 602d05ffad8..4db7effe81b 100644 --- a/aws_lambda_powertools/shared/headers_serializer.py +++ b/aws_lambda_powertools/shared/headers_serializer.py @@ -1,6 +1,6 @@ import warnings from collections import defaultdict -from typing import Any, Dict, List +from typing import Any, Dict, List, Union class BaseHeadersSerializer: @@ -9,7 +9,7 @@ class BaseHeadersSerializer: ALB and Lambda Function URL response payload. """ - def serialize(self, headers: Dict[str, List[str]], cookies: List[str]) -> Dict[str, Any]: + def serialize(self, headers: Dict[str, Union[str, List[str]]], cookies: List[str]) -> Dict[str, Any]: """ Serializes headers and cookies according to the request type. Returns a dict that can be merged with the response payload. @@ -25,7 +25,7 @@ def serialize(self, headers: Dict[str, List[str]], cookies: List[str]) -> Dict[s class HttpApiHeadersSerializer(BaseHeadersSerializer): - def serialize(self, headers: Dict[str, List[str]], cookies: List[str]) -> Dict[str, Any]: + def serialize(self, headers: Dict[str, Union[str, List[str]]], cookies: List[str]) -> Dict[str, Any]: """ When using HTTP APIs or LambdaFunctionURLs, everything is taken care automatically for us. We can directly assign a list of cookies and a dict of headers to the response payload, and the @@ -39,13 +39,16 @@ def serialize(self, headers: Dict[str, List[str]], cookies: List[str]) -> Dict[s # Duplicate headers are combined with commas and included in the headers field. combined_headers: Dict[str, str] = {} for key, values in headers.items(): - combined_headers[key] = ", ".join(values) + if isinstance(values, str): + combined_headers[key] = values + else: + combined_headers[key] = ", ".join(values) return {"headers": combined_headers, "cookies": cookies} class MultiValueHeadersSerializer(BaseHeadersSerializer): - def serialize(self, headers: Dict[str, List[str]], cookies: List[str]) -> Dict[str, Any]: + def serialize(self, headers: Dict[str, Union[str, List[str]]], cookies: List[str]) -> Dict[str, Any]: """ When using REST APIs, headers can be encoded using the `multiValueHeaders` key on the response. This is also the case when using an ALB integration with the `multiValueHeaders` option enabled. @@ -57,8 +60,11 @@ def serialize(self, headers: Dict[str, List[str]], cookies: List[str]) -> Dict[s payload: Dict[str, List[str]] = defaultdict(list) for key, values in headers.items(): - for value in values: - payload[key].append(value) + if isinstance(values, str): + payload[key].append(values) + else: + for value in values: + payload[key].append(value) if cookies: payload.setdefault("Set-Cookie", []) @@ -69,7 +75,7 @@ def serialize(self, headers: Dict[str, List[str]], cookies: List[str]) -> Dict[s class SingleValueHeadersSerializer(BaseHeadersSerializer): - def serialize(self, headers: Dict[str, List[str]], cookies: List[str]) -> Dict[str, Any]: + def serialize(self, headers: Dict[str, Union[str, List[str]]], cookies: List[str]) -> Dict[str, Any]: """ The ALB integration has `multiValueHeaders` disabled by default. If we try to set multiple headers with the same key, or more than one cookie, print a warning. @@ -90,13 +96,16 @@ def serialize(self, headers: Dict[str, List[str]], cookies: List[str]) -> Dict[s payload["headers"]["Set-Cookie"] = cookies[-1] for key, values in headers.items(): - if len(values) > 1: - warnings.warn( - f"Can't encode more than one header value for the same key ('{key}') in the response. " - "Did you enable multiValueHeaders on the ALB Target Group?" - ) - - # We can only set one header per key, send the last one - payload["headers"][key] = values[-1] + if isinstance(values, str): + payload["headers"][key] = values + else: + if len(values) > 1: + warnings.warn( + f"Can't encode more than one header value for the same key ('{key}') in the response. " + "Did you enable multiValueHeaders on the ALB Target Group?" + ) + + # We can only set one header per key, send the last one + payload["headers"][key] = values[-1] return payload diff --git a/docs/upgrade.md b/docs/upgrade.md index bbcc2418a26..91ad54e42d3 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -33,9 +33,12 @@ Before you start, we suggest making a copy of your current working project or cr The `Response` class of the event handler utility changed slightly: -1. The `headers` parameter now expects a list of values per header (type `Dict[str, List[str]]`) +1. The `headers` parameter now expects either a value or list of values per header (type `Union[str, Dict[str, List[str]]]`) 2. We introduced a new `cookies` parameter (type `List[str]`) +???+ note + Code that set headers as `Dict[str, str]` will still work unchanged. + ```python hl_lines="6 12 13" @app.get("/todos") def get_todos(): @@ -52,4 +55,3 @@ def get_todos(): cookies=["CookieName=CookieValue"] ) ``` - diff --git a/examples/event_handler_rest/src/binary_responses_output.json b/examples/event_handler_rest/src/binary_responses_output.json index 0938dee6811..ec59d251732 100644 --- a/examples/event_handler_rest/src/binary_responses_output.json +++ b/examples/event_handler_rest/src/binary_responses_output.json @@ -1,7 +1,7 @@ { "body": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjU2cHgiIGhlaWdodD0iMjU2cHgiIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj4KICAgIDx0aXRsZT5BV1MgTGFtYmRhPC90aXRsZT4KICAgIDxkZWZzPgogICAgICAgIDxsaW5lYXJHcmFkaWVudCB4MT0iMCUiIHkxPSIxMDAlIiB4Mj0iMTAwJSIgeTI9IjAlIiBpZD0ibGluZWFyR3JhZGllbnQtMSI+CiAgICAgICAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiNDODUxMUIiIG9mZnNldD0iMCUiPjwvc3RvcD4KICAgICAgICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iI0ZGOTkwMCIgb2Zmc2V0PSIxMDAlIj48L3N0b3A+CiAgICAgICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDwvZGVmcz4KICAgIDxnPgogICAgICAgIDxyZWN0IGZpbGw9InVybCgjbGluZWFyR3JhZGllbnQtMSkiIHg9IjAiIHk9IjAiIHdpZHRoPSIyNTYiIGhlaWdodD0iMjU2Ij48L3JlY3Q+CiAgICAgICAgPHBhdGggZD0iTTg5LjYyNDExMjYsMjExLjIgTDQ5Ljg5MDMyNzcsMjExLjIgTDkzLjgzNTQ4MzIsMTE5LjM0NzIgTDExMy43NDcyOCwxNjAuMzM5MiBMODkuNjI0MTEyNiwyMTEuMiBaIE05Ni43MDI5MzU3LDExMC41Njk2IEM5Ni4xNjQwODU4LDEwOS40NjU2IDk1LjA0MTQ4MTMsMTA4Ljc2NDggOTMuODE2MjM4NCwxMDguNzY0OCBMOTMuODA2NjE2MywxMDguNzY0OCBDOTIuNTcxNzUxNCwxMDguNzY4IDkxLjQ0OTE0NjYsMTA5LjQ3NTIgOTAuOTE5OTE4NywxMTAuNTg1NiBMNDEuOTEzNDIwOCwyMTMuMDIwOCBDNDEuNDM4NzE5NywyMTQuMDEyOCA0MS41MDYwNzU4LDIxNS4xNzc2IDQyLjA5NjI0NTEsMjE2LjEwODggQzQyLjY3OTk5OTQsMjE3LjAzNjggNDMuNzA2MzgwNSwyMTcuNiA0NC44MDY1MzMxLDIxNy42IEw5MS42NTQ0MjMsMjE3LjYgQzkyLjg5NTcwMjcsMjE3LjYgOTQuMDIxNTE0OSwyMTYuODg2NCA5NC41NTM5NTAxLDIxNS43Njk2IEwxMjAuMjAzODU5LDE2MS42ODk2IEMxMjAuNjE3NjE5LDE2MC44MTI4IDEyMC42MTQ0MTIsMTU5Ljc5ODQgMTIwLjE4NzgyMiwxNTguOTI4IEw5Ni43MDI5MzU3LDExMC41Njk2IFogTTIwNy45ODUxMTcsMjExLjIgTDE2OC41MDc5MjgsMjExLjIgTDEwNS4xNzM3ODksNzguNjI0IEMxMDQuNjQ0NTYxLDc3LjUxMDQgMTAzLjUxNTU0MSw3Ni44IDEwMi4yNzc0NjksNzYuOCBMNzYuNDQ3OTQzLDc2LjggTDc2LjQ3NjgwOTksNDQuOCBMMTI3LjEwMzA2Niw0NC44IEwxOTAuMTQ1MzI4LDE3Ny4zNzI4IEMxOTAuNjc0NTU2LDE3OC40ODY0IDE5MS44MDM1NzUsMTc5LjIgMTkzLjA0MTY0NywxNzkuMiBMMjA3Ljk4NTExNywxNzkuMiBMMjA3Ljk4NTExNywyMTEuMiBaIE0yMTEuMTkyNTU4LDE3Mi44IEwxOTUuMDcxOTU4LDE3Mi44IEwxMzIuMDI5Njk2LDQwLjIyNzIgQzEzMS41MDA0NjgsMzkuMTEzNiAxMzAuMzcxNDQ5LDM4LjQgMTI5LjEzMDE2OSwzOC40IEw3My4yNzI1NzYsMzguNCBDNzEuNTA1Mjc1OCwzOC40IDcwLjA2ODM0MjEsMzkuODMwNCA3MC4wNjUxMzQ0LDQxLjU5NjggTDcwLjAyOTg1MjgsNzkuOTk2OCBDNzAuMDI5ODUyOCw4MC44NDggNzAuMzYzNDI2Niw4MS42NjA4IDcwLjk2OTYzMyw4Mi4yNjI0IEM3MS41Njk0MjQ2LDgyLjg2NCA3Mi4zODQxMTQ2LDgzLjIgNzMuMjM3Mjk0MSw4My4yIEwxMDAuMjUzNTczLDgzLjIgTDE2My41OTA5MiwyMTUuNzc2IEMxNjQuMTIzMzU1LDIxNi44ODk2IDE2NS4yNDU5NiwyMTcuNiAxNjYuNDg0MDMyLDIxNy42IEwyMTEuMTkyNTU4LDIxNy42IEMyMTIuOTY2Mjc0LDIxNy42IDIxNC40LDIxNi4xNjY0IDIxNC40LDIxNC40IEwyMTQuNCwxNzYgQzIxNC40LDE3NC4yMzM2IDIxMi45NjYyNzQsMTcyLjggMjExLjE5MjU1OCwxNzIuOCBMMjExLjE5MjU1OCwxNzIuOCBaIiBmaWxsPSIjRkZGRkZGIj48L3BhdGg+CiAgICA8L2c+Cjwvc3ZnPg==", - "headers": { - "Content-Type": "image/svg+xml" + "multiValueHeaders": { + "Content-Type": ["image/svg+xml"] }, "isBase64Encoded": true, "statusCode": 200 diff --git a/examples/event_handler_rest/src/compressing_responses_output.json b/examples/event_handler_rest/src/compressing_responses_output.json index 0836b3aa726..60a63966494 100644 --- a/examples/event_handler_rest/src/compressing_responses_output.json +++ b/examples/event_handler_rest/src/compressing_responses_output.json @@ -1,8 +1,8 @@ { "statusCode": 200, - "headers": { - "Content-Type": "application/json", - "Content-Encoding": "gzip" + "multiValueHeaders": { + "Content-Type": ["application/json"], + "Content-Encoding": ["gzip"] }, "body": "H4sIAAAAAAACE42STU4DMQyFrxJl3QXln96AMyAW7sSDLCVxiJ0Kqerd8TCCUOgii1EmP/783pOPXjmw+N3L0TfB+hz8brvxtC5KGtHvfMCIkzZx0HT5MPmNnziViIr2dIYoeNr8Q1x3xHsjcVadIbkZJoq2RXU8zzQROLseQ9505NzeCNQdMJNBE+UmY4zbzjAJhWtlZ57sB84BWtul+rteH2HPlVgWARwjqXkxpklK5gmEHAQqJBMtFsGVygcKmNVRjG0wxvuzGF2L0dpVUOKMC3bfJNjJgWMrCuZk7cUp02AiD72D6WKHHwUDKbiJs6AZ0VZXKOUx4uNvzdxT+E4mLcMA+6G8nzrLQkaxkNEVrFKW2VGbJCoCY7q2V3+tiv5kGThyxfTecDWbgGz/NfYXhL6ePgF9PnFdPgMAAA==", "isBase64Encoded": true diff --git a/examples/event_handler_rest/src/fine_grained_responses_output.json b/examples/event_handler_rest/src/fine_grained_responses_output.json index 38d642145fa..1ce606839b1 100644 --- a/examples/event_handler_rest/src/fine_grained_responses_output.json +++ b/examples/event_handler_rest/src/fine_grained_responses_output.json @@ -1,9 +1,9 @@ { "statusCode": 200, - "headers": { - "Content-Type": "application/json", - "X-Transaction-Id": "3490eea9-791b-47a0-91a4-326317db61a9", - "Set-Cookie": "=; Secure; Expires=", + "multiValueHeaders": { + "Content-Type": ["application/json"], + "X-Transaction-Id": ["3490eea9-791b-47a0-91a4-326317db61a9"], + "Set-Cookie": ["=; Secure; Expires="] }, "body": "{\"todos\":[{\"userId\":1,\"id\":1,\"title\":\"delectus aut autem\",\"completed\":false},{\"userId\":1,\"id\":2,\"title\":\"quis ut nam facilis et officia qui\",\"completed\":false},{\"userId\":1,\"id\":3,\"title\":\"fugiat veniam minus\",\"completed\":false},{\"userId\":1,\"id\":4,\"title\":\"et porro tempora\",\"completed\":true},{\"userId\":1,\"id\":5,\"title\":\"laboriosam mollitia et enim quasi adipisci quia provident illum\",\"completed\":false},{\"userId\":1,\"id\":6,\"title\":\"qui ullam ratione quibusdam voluptatem quia omnis\",\"completed\":false},{\"userId\":1,\"id\":7,\"title\":\"illo expedita consequatur quia in\",\"completed\":false},{\"userId\":1,\"id\":8,\"title\":\"quo adipisci enim quam ut ab\",\"completed\":true},{\"userId\":1,\"id\":9,\"title\":\"molestiae perspiciatis ipsa\",\"completed\":false},{\"userId\":1,\"id\":10,\"title\":\"illo est ratione doloremque quia maiores aut\",\"completed\":true}]}", "isBase64Encoded": false diff --git a/examples/event_handler_rest/src/getting_started_rest_api_resolver_output.json b/examples/event_handler_rest/src/getting_started_rest_api_resolver_output.json index 2ef3714531f..24d2b5c6dbc 100644 --- a/examples/event_handler_rest/src/getting_started_rest_api_resolver_output.json +++ b/examples/event_handler_rest/src/getting_started_rest_api_resolver_output.json @@ -1,7 +1,7 @@ { "statusCode": 200, - "headers": { - "Content-Type": "application/json" + "multiValueHeaders": { + "Content-Type": ["application/json"] }, "body": "{\"todos\":[{\"userId\":1,\"id\":1,\"title\":\"delectus aut autem\",\"completed\":false},{\"userId\":1,\"id\":2,\"title\":\"quis ut nam facilis et officia qui\",\"completed\":false},{\"userId\":1,\"id\":3,\"title\":\"fugiat veniam minus\",\"completed\":false},{\"userId\":1,\"id\":4,\"title\":\"et porro tempora\",\"completed\":true},{\"userId\":1,\"id\":5,\"title\":\"laboriosam mollitia et enim quasi adipisci quia provident illum\",\"completed\":false},{\"userId\":1,\"id\":6,\"title\":\"qui ullam ratione quibusdam voluptatem quia omnis\",\"completed\":false},{\"userId\":1,\"id\":7,\"title\":\"illo expedita consequatur quia in\",\"completed\":false},{\"userId\":1,\"id\":8,\"title\":\"quo adipisci enim quam ut ab\",\"completed\":true},{\"userId\":1,\"id\":9,\"title\":\"molestiae perspiciatis ipsa\",\"completed\":false},{\"userId\":1,\"id\":10,\"title\":\"illo est ratione doloremque quia maiores aut\",\"completed\":true}]}", "isBase64Encoded": false diff --git a/examples/event_handler_rest/src/setting_cors_output.json b/examples/event_handler_rest/src/setting_cors_output.json index ca86e892d38..19660941e91 100644 --- a/examples/event_handler_rest/src/setting_cors_output.json +++ b/examples/event_handler_rest/src/setting_cors_output.json @@ -1,9 +1,9 @@ { "statusCode": 200, - "headers": { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "https://www.example.com", - "Access-Control-Allow-Headers": "Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token,X-Api-Key" + "multiValueHeaders": { + "Content-Type": ["application/json"], + "Access-Control-Allow-Origin": ["https://www.example.com"], + "Access-Control-Allow-Headers": ["Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token,X-Api-Key"] }, "body": "{\"todos\":[{\"userId\":1,\"id\":1,\"title\":\"delectus aut autem\",\"completed\":false},{\"userId\":1,\"id\":2,\"title\":\"quis ut nam facilis et officia qui\",\"completed\":false},{\"userId\":1,\"id\":3,\"title\":\"fugiat veniam minus\",\"completed\":false},{\"userId\":1,\"id\":4,\"title\":\"et porro tempora\",\"completed\":true},{\"userId\":1,\"id\":5,\"title\":\"laboriosam mollitia et enim quasi adipisci quia provident illum\",\"completed\":false},{\"userId\":1,\"id\":6,\"title\":\"qui ullam ratione quibusdam voluptatem quia omnis\",\"completed\":false},{\"userId\":1,\"id\":7,\"title\":\"illo expedita consequatur quia in\",\"completed\":false},{\"userId\":1,\"id\":8,\"title\":\"quo adipisci enim quam ut ab\",\"completed\":true},{\"userId\":1,\"id\":9,\"title\":\"molestiae perspiciatis ipsa\",\"completed\":false},{\"userId\":1,\"id\":10,\"title\":\"illo est ratione doloremque quia maiores aut\",\"completed\":true}]}", "isBase64Encoded": false diff --git a/tests/functional/test_headers_serializer.py b/tests/functional/test_headers_serializer.py index a5dd7696b66..8a27ce8baa8 100644 --- a/tests/functional/test_headers_serializer.py +++ b/tests/functional/test_headers_serializer.py @@ -34,6 +34,13 @@ def test_http_api_headers_serializer_with_headers_only(): assert payload["headers"]["Content-Type"] == content_type +def test_http_api_headers_serializer_with_single_headers_only(): + content_type = "text/html" + serializer = HttpApiHeadersSerializer() + payload = serializer.serialize(headers={"Content-Type": content_type}, cookies=[]) + assert payload["headers"]["Content-Type"] == content_type + + def test_http_api_headers_serializer_with_cookies_only(): cookies = ["UUID=12345", "SSID=0xdeadbeef"] serializer = HttpApiHeadersSerializer() @@ -60,6 +67,13 @@ def test_multi_value_headers_serializer_with_headers_only(): assert payload["multiValueHeaders"]["Content-Type"] == [content_type] +def test_multi_value_headers_serializer_with_single_headers_only(): + content_type = "text/html" + serializer = MultiValueHeadersSerializer() + payload = serializer.serialize(headers={"Content-Type": content_type}, cookies=[]) + assert payload["multiValueHeaders"]["Content-Type"] == [content_type] + + def test_multi_value_headers_serializer_with_cookies_only(): cookie = "UUID=12345" serializer = MultiValueHeadersSerializer() @@ -91,6 +105,13 @@ def test_single_value_headers_serializer_with_headers_only(): assert payload["headers"]["Content-Type"] == content_type +def test_single_value_headers_serializer_with_single_headers_only(): + content_type = "text/html" + serializer = SingleValueHeadersSerializer() + payload = serializer.serialize(headers={"Content-Type": content_type}, cookies=[]) + assert payload["headers"]["Content-Type"] == content_type + + def test_single_value_headers_serializer_with_cookies_only(): cookie = "UUID=12345" serializer = SingleValueHeadersSerializer()