From f7412992a8ef1f4ec2f0cf08efbf172ec02b5fa8 Mon Sep 17 00:00:00 2001 From: Ran Isenberg Date: Thu, 11 Aug 2022 19:08:46 +0300 Subject: [PATCH 1/4] feat: Add Parser official support for Lambda Function UR --- .../utilities/parser/envelopes/__init__.py | 2 + .../parser/envelopes/lambda_function_url.py | 32 +++++ .../utilities/parser/models/__init__.py | 2 + .../utilities/parser/models/apigwv2.py | 2 +- .../parser/models/lambda_function_url.py | 18 +++ tests/functional/parser/schemas.py | 5 + .../parser/test_lambda_function_url.py | 128 ++++++++++++++++++ 7 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 aws_lambda_powertools/utilities/parser/envelopes/lambda_function_url.py create mode 100644 aws_lambda_powertools/utilities/parser/models/lambda_function_url.py create mode 100644 tests/functional/parser/test_lambda_function_url.py diff --git a/aws_lambda_powertools/utilities/parser/envelopes/__init__.py b/aws_lambda_powertools/utilities/parser/envelopes/__init__.py index 1b118d28117..7d42fd81ad6 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/__init__.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/__init__.py @@ -5,6 +5,7 @@ from .dynamodb import DynamoDBStreamEnvelope from .event_bridge import EventBridgeEnvelope from .kinesis import KinesisDataStreamEnvelope +from .lambda_function_url import LambdaFunctionUrlEnvelope from .sns import SnsEnvelope, SnsSqsEnvelope from .sqs import SqsEnvelope @@ -15,6 +16,7 @@ "DynamoDBStreamEnvelope", "EventBridgeEnvelope", "KinesisDataStreamEnvelope", + "LambdaFunctionUrlEnvelope", "SnsEnvelope", "SnsSqsEnvelope", "SqsEnvelope", diff --git a/aws_lambda_powertools/utilities/parser/envelopes/lambda_function_url.py b/aws_lambda_powertools/utilities/parser/envelopes/lambda_function_url.py new file mode 100644 index 00000000000..dc4981a9ce1 --- /dev/null +++ b/aws_lambda_powertools/utilities/parser/envelopes/lambda_function_url.py @@ -0,0 +1,32 @@ +import logging +from typing import Any, Dict, Optional, Type, Union + +from ..models import LambdaFunctionUrlModel +from ..types import Model +from .base import BaseEnvelope + +logger = logging.getLogger(__name__) + + +class LambdaFunctionUrlEnvelope(BaseEnvelope): + """Lambda function URL envelope to extract data within body key""" + + def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Type[Model]) -> Optional[Model]: + """Parses data found with model provided + + Parameters + ---------- + data : Dict + Lambda event to be parsed + model : Type[Model] + Data model provided to parse after extracting data using envelope + + Returns + ------- + Any + Parsed detail payload with model provided + """ + logger.debug(f"Parsing incoming data with Lambda function URL {LambdaFunctionUrlModel}") + parsed_envelope: LambdaFunctionUrlModel = LambdaFunctionUrlModel.parse_obj(data) + logger.debug(f"Parsing event payload in `detail` with {model}") + return self._parse(data=parsed_envelope.body, model=model) diff --git a/aws_lambda_powertools/utilities/parser/models/__init__.py b/aws_lambda_powertools/utilities/parser/models/__init__.py index e3fb50a2d5d..11ab6501fa9 100644 --- a/aws_lambda_powertools/utilities/parser/models/__init__.py +++ b/aws_lambda_powertools/utilities/parser/models/__init__.py @@ -18,6 +18,7 @@ from .dynamodb import DynamoDBStreamChangedRecordModel, DynamoDBStreamModel, DynamoDBStreamRecordModel from .event_bridge import EventBridgeModel from .kinesis import KinesisDataStreamModel, KinesisDataStreamRecord, KinesisDataStreamRecordPayload +from .lambda_function_url import LambdaFunctionUrlModel from .s3 import S3Model, S3RecordModel from .s3_object_event import ( S3ObjectConfiguration, @@ -66,6 +67,7 @@ "KinesisDataStreamModel", "KinesisDataStreamRecord", "KinesisDataStreamRecordPayload", + "LambdaFunctionUrlModel", "S3Model", "S3RecordModel", "S3ObjectLambdaEvent", diff --git a/aws_lambda_powertools/utilities/parser/models/apigwv2.py b/aws_lambda_powertools/utilities/parser/models/apigwv2.py index f97dad3bcb0..cb1f830bb47 100644 --- a/aws_lambda_powertools/utilities/parser/models/apigwv2.py +++ b/aws_lambda_powertools/utilities/parser/models/apigwv2.py @@ -20,7 +20,7 @@ class RequestContextV2AuthorizerIam(BaseModel): principalOrgId: Optional[str] userArn: Optional[str] userId: Optional[str] - cognitoIdentity: RequestContextV2AuthorizerIamCognito + cognitoIdentity: Optional[RequestContextV2AuthorizerIamCognito] class RequestContextV2AuthorizerJwt(BaseModel): diff --git a/aws_lambda_powertools/utilities/parser/models/lambda_function_url.py b/aws_lambda_powertools/utilities/parser/models/lambda_function_url.py new file mode 100644 index 00000000000..2088ab9fa04 --- /dev/null +++ b/aws_lambda_powertools/utilities/parser/models/lambda_function_url.py @@ -0,0 +1,18 @@ +from aws_lambda_powertools.utilities.parser.models import APIGatewayProxyEventV2Model + + +class LambdaFunctionUrlModel(APIGatewayProxyEventV2Model): + """AWS Lambda Function URL model + + Notes: + ----- + Lambda Function URL follows the API Gateway HTTP APIs Payload Format Version 2.0. + + Keys related to API Gateway features not available in Function URL use a sentinel value (e.g.`routeKey`, `stage`). + + Documentation: + - https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html + - https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads + """ + + pass diff --git a/tests/functional/parser/schemas.py b/tests/functional/parser/schemas.py index 8ff56f703a7..79a74f8eb53 100644 --- a/tests/functional/parser/schemas.py +++ b/tests/functional/parser/schemas.py @@ -86,3 +86,8 @@ class MyCloudWatchBusiness(BaseModel): class MyApiGatewayBusiness(BaseModel): message: str username: str + + +class MyALambdaFuncUrlBusiness(BaseModel): + message: str + username: str diff --git a/tests/functional/parser/test_lambda_function_url.py b/tests/functional/parser/test_lambda_function_url.py new file mode 100644 index 00000000000..a63a4e25884 --- /dev/null +++ b/tests/functional/parser/test_lambda_function_url.py @@ -0,0 +1,128 @@ +from aws_lambda_powertools.utilities.parser import envelopes, event_parser +from aws_lambda_powertools.utilities.parser.models import LambdaFunctionUrlModel +from aws_lambda_powertools.utilities.typing import LambdaContext +from tests.functional.parser.schemas import MyALambdaFuncUrlBusiness +from tests.functional.utils import load_event + + +@event_parser(model=MyALambdaFuncUrlBusiness, envelope=envelopes.LambdaFunctionUrlEnvelope) +def handle_lambda_func_url_with_envelope(event: MyALambdaFuncUrlBusiness, _: LambdaContext): + assert event.message == "Hello" + assert event.username == "Ran" + + +@event_parser(model=LambdaFunctionUrlModel) +def handle_lambda_func_url_event(event: LambdaFunctionUrlModel, _: LambdaContext): + return event + + +def test_lambda_func_url_event_with_envelope(): + event = load_event("lambdaFunctionUrlEvent.json") + event["body"] = '{"message": "Hello", "username": "Ran"}' + handle_lambda_func_url_with_envelope(event, LambdaContext()) + + +def test_lambda_function_url_event(): + json_event = load_event("lambdaFunctionUrlEvent.json") + event: LambdaFunctionUrlModel = handle_lambda_func_url_event(json_event, LambdaContext()) + + assert event.version == "2.0" + assert event.routeKey == "$default" + + assert event.rawQueryString == "" + + assert event.cookies is None + + headers = event.headers + assert len(headers) == 20 + + assert event.queryStringParameters is None + + assert event.isBase64Encoded is False + assert event.body is None + assert event.pathParameters is None + assert event.stageVariables is None + + request_context = event.requestContext + + assert request_context.accountId == "anonymous" + assert request_context.apiId is not None + assert request_context.domainName == ".lambda-url.us-east-1.on.aws" + assert request_context.domainPrefix == "" + assert request_context.requestId == "id" + assert request_context.routeKey == "$default" + assert request_context.stage == "$default" + assert request_context.time is not None + convert_time = int(round(request_context.timeEpoch.timestamp() * 1000)) + assert convert_time == 1659687279885 + assert request_context.authorizer is None + + http = request_context.http + assert http.method == "GET" + assert http.path == "/" + assert http.protocol == "HTTP/1.1" + assert str(http.sourceIp) == "123.123.123.123/32" + assert http.userAgent == "agent" + + assert request_context.authorizer is None + + +def test_lambda_function_url_event_iam(): + json_event = load_event("lambdaFunctionUrlIAMEvent.json") + event: LambdaFunctionUrlModel = handle_lambda_func_url_event(json_event, LambdaContext()) + + assert event.version == "2.0" + assert event.routeKey == "$default" + + assert event.rawQueryString == "parameter1=value1¶meter1=value2¶meter2=value" + + cookies = event.cookies + assert len(cookies) == 2 + assert cookies[0] == "cookie1" + + headers = event.headers + assert len(headers) == 2 + + query_string_parameters = event.queryStringParameters + assert len(query_string_parameters) == 2 + assert query_string_parameters.get("parameter2") == "value" + + assert event.isBase64Encoded is False + assert event.body == "Hello from client!" + assert event.pathParameters is None + assert event.stageVariables is None + + request_context = event.requestContext + + assert request_context.accountId == "123456789012" + assert request_context.apiId is not None + assert request_context.domainName == ".lambda-url.us-west-2.on.aws" + assert request_context.domainPrefix == "" + assert request_context.requestId == "id" + assert request_context.routeKey == "$default" + assert request_context.stage == "$default" + assert request_context.time is not None + convert_time = int(round(request_context.timeEpoch.timestamp() * 1000)) + assert convert_time == 1583348638390 + + http = request_context.http + assert http.method == "POST" + assert http.path == "/my/path" + assert http.protocol == "HTTP/1.1" + assert str(http.sourceIp) == "123.123.123.123/32" + assert http.userAgent == "agent" + + authorizer = request_context.authorizer + assert authorizer is not None + assert authorizer.jwt is None + assert authorizer.lambda_value is None + + iam = authorizer.iam + assert iam is not None + assert iam.accessKey == "AKIA..." + assert iam.accountId == "111122223333" + assert iam.callerId == "AIDA..." + assert iam.cognitoIdentity is None + assert iam.principalOrgId is None + assert iam.userId == "AIDA..." + assert iam.userArn == "arn:aws:iam::111122223333:user/example-user" From a0ef46d6ddbe9490ccfd5eff605c271c6da0c087 Mon Sep 17 00:00:00 2001 From: Ran Isenberg Date: Thu, 11 Aug 2022 19:58:58 +0300 Subject: [PATCH 2/4] Update parser.md --- docs/utilities/parser.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/utilities/parser.md b/docs/utilities/parser.md index cb69cf9699b..84bc6af017d 100644 --- a/docs/utilities/parser.md +++ b/docs/utilities/parser.md @@ -2,6 +2,7 @@ title: Parser description: Utility --- + This utility provides data parsing and deep validation using [Pydantic](https://pydantic-docs.helpmanual.io/). @@ -166,6 +167,7 @@ Parser comes with the following built-in models: | **SnsModel** | Lambda Event Source payload for Amazon Simple Notification Service | | **APIGatewayProxyEventModel** | Lambda Event Source payload for Amazon API Gateway | | **APIGatewayProxyEventV2Model** | Lambda Event Source payload for Amazon API Gateway v2 payload | +| **LambdaFunctionUrlModel** | Lambda Event Source payload for Lambda function URL payload | ### extending built-in models @@ -305,6 +307,7 @@ Parser comes with the following built-in envelopes, where `Model` in the return | **SnsSqsEnvelope** | 1. Parses data using `SqsModel`.
2. Parses SNS records in `body` key using `SnsNotificationModel`.
3. Parses data in `Message` key using your model and return them in a list. | `List[Model]` | | **ApiGatewayEnvelope** | 1. Parses data using `APIGatewayProxyEventModel`.
2. Parses `body` key using your model and returns it. | `Model` | | **ApiGatewayV2Envelope** | 1. Parses data using `APIGatewayProxyEventV2Model`.
2. Parses `body` key using your model and returns it. | `Model` | +| **LambdaFunctionUrlEnvelope** | 1. Parses data using `LambdaFunctionUrlModel`.
2. Parses `body` key using your model and returns it. | `Model` | ### Bringing your own envelope From dd7f5dd9e9fe8a9505a4dc56d731eb91928658b3 Mon Sep 17 00:00:00 2001 From: Ran Isenberg <60175085+ran-isenberg@users.noreply.github.com> Date: Tue, 16 Aug 2022 17:03:22 +0300 Subject: [PATCH 3/4] Update aws_lambda_powertools/utilities/parser/envelopes/lambda_function_url.py Co-authored-by: Ruben Fonseca --- .../utilities/parser/envelopes/lambda_function_url.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/parser/envelopes/lambda_function_url.py b/aws_lambda_powertools/utilities/parser/envelopes/lambda_function_url.py index dc4981a9ce1..e54fb081b65 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/lambda_function_url.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/lambda_function_url.py @@ -26,7 +26,7 @@ def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Type[Model]) Any Parsed detail payload with model provided """ - logger.debug(f"Parsing incoming data with Lambda function URL {LambdaFunctionUrlModel}") + logger.debug(f"Parsing incoming data with Lambda function URL model {LambdaFunctionUrlModel}") parsed_envelope: LambdaFunctionUrlModel = LambdaFunctionUrlModel.parse_obj(data) logger.debug(f"Parsing event payload in `detail` with {model}") return self._parse(data=parsed_envelope.body, model=model) From 23932290fca02aaed56e601474807a4c69da6f65 Mon Sep 17 00:00:00 2001 From: Ran Isenberg <60175085+ran-isenberg@users.noreply.github.com> Date: Tue, 16 Aug 2022 17:03:30 +0300 Subject: [PATCH 4/4] Update docs/utilities/parser.md Co-authored-by: Ruben Fonseca --- docs/utilities/parser.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/utilities/parser.md b/docs/utilities/parser.md index 84bc6af017d..97b005a9fb5 100644 --- a/docs/utilities/parser.md +++ b/docs/utilities/parser.md @@ -167,7 +167,7 @@ Parser comes with the following built-in models: | **SnsModel** | Lambda Event Source payload for Amazon Simple Notification Service | | **APIGatewayProxyEventModel** | Lambda Event Source payload for Amazon API Gateway | | **APIGatewayProxyEventV2Model** | Lambda Event Source payload for Amazon API Gateway v2 payload | -| **LambdaFunctionUrlModel** | Lambda Event Source payload for Lambda function URL payload | +| **LambdaFunctionUrlModel** | Lambda Event Source payload for Lambda Function URL payload | ### extending built-in models