Skip to content

Commit 0483a79

Browse files
committed
feat: Add Parser official support for Lambda Function UR
1 parent 8e24ae3 commit 0483a79

File tree

8 files changed

+189
-2
lines changed

8 files changed

+189
-2
lines changed

aws_lambda_powertools/utilities/data_classes/lambda_function_url_event.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class LambdaFunctionUrlEvent(APIGatewayProxyEventV2):
77
Notes:
88
-----
99
Lambda Function URL follows the API Gateway HTTP APIs Payload Format Version 2.0.
10-
10+
1111
Keys related to API Gateway features not available in Function URL use a sentinel value (e.g.`routeKey`, `stage`).
1212
1313
Documentation:

aws_lambda_powertools/utilities/parser/envelopes/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .dynamodb import DynamoDBStreamEnvelope
66
from .event_bridge import EventBridgeEnvelope
77
from .kinesis import KinesisDataStreamEnvelope
8+
from .lambda_function_url import LambdaFunctionUrlEnvelope
89
from .sns import SnsEnvelope, SnsSqsEnvelope
910
from .sqs import SqsEnvelope
1011

@@ -15,6 +16,7 @@
1516
"DynamoDBStreamEnvelope",
1617
"EventBridgeEnvelope",
1718
"KinesisDataStreamEnvelope",
19+
"LambdaFunctionUrlEnvelope",
1820
"SnsEnvelope",
1921
"SnsSqsEnvelope",
2022
"SqsEnvelope",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import logging
2+
from typing import Any, Dict, Optional, Type, Union
3+
4+
from ..models import LambdaFunctionUrlModel
5+
from ..types import Model
6+
from .base import BaseEnvelope
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
class LambdaFunctionUrlEnvelope(BaseEnvelope):
12+
"""Lambda function URL envelope to extract data within body key"""
13+
14+
def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Type[Model]) -> Optional[Model]:
15+
"""Parses data found with model provided
16+
17+
Parameters
18+
----------
19+
data : Dict
20+
Lambda event to be parsed
21+
model : Type[Model]
22+
Data model provided to parse after extracting data using envelope
23+
24+
Returns
25+
-------
26+
Any
27+
Parsed detail payload with model provided
28+
"""
29+
logger.debug(f"Parsing incoming data with Lambda function URL {LambdaFunctionUrlModel}")
30+
parsed_envelope: LambdaFunctionUrlModel = LambdaFunctionUrlModel.parse_obj(data)
31+
logger.debug(f"Parsing event payload in `detail` with {model}")
32+
return self._parse(data=parsed_envelope.body, model=model)

aws_lambda_powertools/utilities/parser/models/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .dynamodb import DynamoDBStreamChangedRecordModel, DynamoDBStreamModel, DynamoDBStreamRecordModel
1919
from .event_bridge import EventBridgeModel
2020
from .kinesis import KinesisDataStreamModel, KinesisDataStreamRecord, KinesisDataStreamRecordPayload
21+
from .lambda_function_url import LambdaFunctionUrlModel
2122
from .s3 import S3Model, S3RecordModel
2223
from .s3_object_event import (
2324
S3ObjectConfiguration,
@@ -66,6 +67,7 @@
6667
"KinesisDataStreamModel",
6768
"KinesisDataStreamRecord",
6869
"KinesisDataStreamRecordPayload",
70+
"LambdaFunctionUrlModel",
6971
"S3Model",
7072
"S3RecordModel",
7173
"S3ObjectLambdaEvent",

aws_lambda_powertools/utilities/parser/models/apigwv2.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class RequestContextV2AuthorizerIam(BaseModel):
2020
principalOrgId: Optional[str]
2121
userArn: Optional[str]
2222
userId: Optional[str]
23-
cognitoIdentity: RequestContextV2AuthorizerIamCognito
23+
cognitoIdentity: Optional[RequestContextV2AuthorizerIamCognito]
2424

2525

2626
class RequestContextV2AuthorizerJwt(BaseModel):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from aws_lambda_powertools.utilities.parser.models import APIGatewayProxyEventV2Model
2+
3+
4+
class LambdaFunctionUrlModel(APIGatewayProxyEventV2Model):
5+
"""AWS Lambda Function URL model
6+
7+
Notes:
8+
-----
9+
Lambda Function URL follows the API Gateway HTTP APIs Payload Format Version 2.0.
10+
11+
Keys related to API Gateway features not available in Function URL use a sentinel value (e.g.`routeKey`, `stage`).
12+
13+
Documentation:
14+
- https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html
15+
- https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads
16+
"""
17+
18+
pass

tests/functional/parser/schemas.py

+5
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,8 @@ class MyCloudWatchBusiness(BaseModel):
8686
class MyApiGatewayBusiness(BaseModel):
8787
message: str
8888
username: str
89+
90+
91+
class MyALambdaFuncUrlBusiness(BaseModel):
92+
message: str
93+
username: str
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
from aws_lambda_powertools.utilities.parser import envelopes, event_parser
2+
from aws_lambda_powertools.utilities.parser.models import LambdaFunctionUrlModel
3+
from aws_lambda_powertools.utilities.typing import LambdaContext
4+
from tests.functional.parser.schemas import MyALambdaFuncUrlBusiness
5+
from tests.functional.utils import load_event
6+
7+
8+
@event_parser(model=MyALambdaFuncUrlBusiness, envelope=envelopes.LambdaFunctionUrlEnvelope)
9+
def handle_lambda_func_url_with_envelope(event: MyALambdaFuncUrlBusiness, _: LambdaContext):
10+
assert event.message == "Hello"
11+
assert event.username == "Ran"
12+
13+
14+
@event_parser(model=LambdaFunctionUrlModel)
15+
def handle_lambda_func_url_event(event: LambdaFunctionUrlModel, _: LambdaContext):
16+
return event
17+
18+
19+
def test_lambda_func_url_event_with_envelope():
20+
event = load_event("lambdaFunctionUrlEvent.json")
21+
event["body"] = '{"message": "Hello", "username": "Ran"}'
22+
handle_lambda_func_url_with_envelope(event, LambdaContext())
23+
24+
25+
def test_lambda_function_url_event():
26+
json_event = load_event("lambdaFunctionUrlEvent.json")
27+
event: LambdaFunctionUrlModel = handle_lambda_func_url_event(json_event, LambdaContext())
28+
29+
assert event.version == "2.0"
30+
assert event.routeKey == "$default"
31+
32+
assert event.rawQueryString == ""
33+
34+
assert event.cookies is None
35+
36+
headers = event.headers
37+
assert len(headers) == 20
38+
39+
assert event.queryStringParameters is None
40+
41+
assert event.isBase64Encoded is False
42+
assert event.body is None
43+
assert event.pathParameters is None
44+
assert event.stageVariables is None
45+
46+
request_context = event.requestContext
47+
48+
assert request_context.accountId == "anonymous"
49+
assert request_context.apiId is not None
50+
assert request_context.domainName == "<url-id>.lambda-url.us-east-1.on.aws"
51+
assert request_context.domainPrefix == "<url-id>"
52+
assert request_context.requestId == "id"
53+
assert request_context.routeKey == "$default"
54+
assert request_context.stage == "$default"
55+
assert request_context.time is not None
56+
convert_time = int(round(request_context.timeEpoch.timestamp() * 1000))
57+
assert convert_time == 1659687279885
58+
assert request_context.authorizer is None
59+
60+
http = request_context.http
61+
assert http.method == "GET"
62+
assert http.path == "/"
63+
assert http.protocol == "HTTP/1.1"
64+
assert str(http.sourceIp) == "123.123.123.123/32"
65+
assert http.userAgent == "agent"
66+
67+
assert request_context.authorizer is None
68+
69+
70+
def test_lambda_function_url_event_iam():
71+
json_event = load_event("lambdaFunctionUrlIAMEvent.json")
72+
event: LambdaFunctionUrlModel = handle_lambda_func_url_event(json_event, LambdaContext())
73+
74+
assert event.version == "2.0"
75+
assert event.routeKey == "$default"
76+
77+
assert event.rawQueryString == "parameter1=value1&parameter1=value2&parameter2=value"
78+
79+
cookies = event.cookies
80+
assert len(cookies) == 2
81+
assert cookies[0] == "cookie1"
82+
83+
headers = event.headers
84+
assert len(headers) == 2
85+
86+
query_string_parameters = event.queryStringParameters
87+
assert len(query_string_parameters) == 2
88+
assert query_string_parameters.get("parameter2") == "value"
89+
90+
assert event.isBase64Encoded is False
91+
assert event.body == "Hello from client!"
92+
assert event.pathParameters is None
93+
assert event.stageVariables is None
94+
95+
request_context = event.requestContext
96+
97+
assert request_context.accountId == "123456789012"
98+
assert request_context.apiId is not None
99+
assert request_context.domainName == "<url-id>.lambda-url.us-west-2.on.aws"
100+
assert request_context.domainPrefix == "<url-id>"
101+
assert request_context.requestId == "id"
102+
assert request_context.routeKey == "$default"
103+
assert request_context.stage == "$default"
104+
assert request_context.time is not None
105+
convert_time = int(round(request_context.timeEpoch.timestamp() * 1000))
106+
assert convert_time == 1583348638390
107+
108+
http = request_context.http
109+
assert http.method == "POST"
110+
assert http.path == "/my/path"
111+
assert http.protocol == "HTTP/1.1"
112+
assert str(http.sourceIp) == "123.123.123.123/32"
113+
assert http.userAgent == "agent"
114+
115+
authorizer = request_context.authorizer
116+
assert authorizer is not None
117+
assert authorizer.jwt is None
118+
assert authorizer.lambda_value is None
119+
120+
iam = authorizer.iam
121+
assert iam is not None
122+
assert iam.accessKey == "AKIA..."
123+
assert iam.accountId == "111122223333"
124+
assert iam.callerId == "AIDA..."
125+
assert iam.cognitoIdentity is None
126+
assert iam.principalOrgId is None
127+
assert iam.userId == "AIDA..."
128+
assert iam.userArn == "arn:aws:iam::111122223333:user/example-user"

0 commit comments

Comments
 (0)