diff --git a/aws_lambda_powertools/utilities/parser/envelopes/__init__.py b/aws_lambda_powertools/utilities/parser/envelopes/__init__.py
index e6f63c4792d..1b118d28117 100644
--- a/aws_lambda_powertools/utilities/parser/envelopes/__init__.py
+++ b/aws_lambda_powertools/utilities/parser/envelopes/__init__.py
@@ -1,4 +1,5 @@
from .apigw import ApiGatewayEnvelope
+from .apigwv2 import ApiGatewayV2Envelope
from .base import BaseEnvelope
from .cloudwatch import CloudWatchLogsEnvelope
from .dynamodb import DynamoDBStreamEnvelope
@@ -9,6 +10,7 @@
__all__ = [
"ApiGatewayEnvelope",
+ "ApiGatewayV2Envelope",
"CloudWatchLogsEnvelope",
"DynamoDBStreamEnvelope",
"EventBridgeEnvelope",
diff --git a/aws_lambda_powertools/utilities/parser/envelopes/apigwv2.py b/aws_lambda_powertools/utilities/parser/envelopes/apigwv2.py
new file mode 100644
index 00000000000..a627e4da0e5
--- /dev/null
+++ b/aws_lambda_powertools/utilities/parser/envelopes/apigwv2.py
@@ -0,0 +1,32 @@
+import logging
+from typing import Any, Dict, Optional, Type, Union
+
+from ..models import APIGatewayProxyEventV2Model
+from ..types import Model
+from .base import BaseEnvelope
+
+logger = logging.getLogger(__name__)
+
+
+class ApiGatewayV2Envelope(BaseEnvelope):
+ """API Gateway V2 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 Api Gateway model V2 {APIGatewayProxyEventV2Model}")
+ parsed_envelope = APIGatewayProxyEventV2Model.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 0e59b2197a8..e3fb50a2d5d 100644
--- a/aws_lambda_powertools/utilities/parser/models/__init__.py
+++ b/aws_lambda_powertools/utilities/parser/models/__init__.py
@@ -5,6 +5,15 @@
APIGatewayEventRequestContext,
APIGatewayProxyEventModel,
)
+from .apigwv2 import (
+ APIGatewayProxyEventV2Model,
+ RequestContextV2,
+ RequestContextV2Authorizer,
+ RequestContextV2AuthorizerIam,
+ RequestContextV2AuthorizerIamCognito,
+ RequestContextV2AuthorizerJwt,
+ RequestContextV2Http,
+)
from .cloudwatch import CloudWatchLogsData, CloudWatchLogsDecode, CloudWatchLogsLogEvent, CloudWatchLogsModel
from .dynamodb import DynamoDBStreamChangedRecordModel, DynamoDBStreamModel, DynamoDBStreamRecordModel
from .event_bridge import EventBridgeModel
@@ -35,6 +44,13 @@
from .sqs import SqsAttributesModel, SqsModel, SqsMsgAttributeModel, SqsRecordModel
__all__ = [
+ "APIGatewayProxyEventV2Model",
+ "RequestContextV2",
+ "RequestContextV2Http",
+ "RequestContextV2Authorizer",
+ "RequestContextV2AuthorizerJwt",
+ "RequestContextV2AuthorizerIam",
+ "RequestContextV2AuthorizerIamCognito",
"CloudWatchLogsData",
"CloudWatchLogsDecode",
"CloudWatchLogsLogEvent",
diff --git a/aws_lambda_powertools/utilities/parser/models/apigwv2.py b/aws_lambda_powertools/utilities/parser/models/apigwv2.py
new file mode 100644
index 00000000000..4243315bb21
--- /dev/null
+++ b/aws_lambda_powertools/utilities/parser/models/apigwv2.py
@@ -0,0 +1,71 @@
+from datetime import datetime
+from typing import Any, Dict, List, Optional
+
+from pydantic import BaseModel, Field
+from pydantic.networks import IPvAnyNetwork
+
+from ..types import Literal
+
+
+class RequestContextV2AuthorizerIamCognito(BaseModel):
+ amr: List[str]
+ identityId: str
+ identityPoolId: str
+
+
+class RequestContextV2AuthorizerIam(BaseModel):
+ accessKey: Optional[str]
+ accountId: Optional[str]
+ callerId: Optional[str]
+ principalOrgId: Optional[str]
+ userArn: Optional[str]
+ userId: Optional[str]
+ cognitoIdentity: RequestContextV2AuthorizerIamCognito
+
+
+class RequestContextV2AuthorizerJwt(BaseModel):
+ claims: Dict[str, Any]
+ scopes: List[str]
+
+
+class RequestContextV2Authorizer(BaseModel):
+ jwt: Optional[RequestContextV2AuthorizerJwt]
+ iam: Optional[RequestContextV2AuthorizerIam]
+ lambda_value: Optional[Dict[str, Any]] = Field(None, alias="lambda")
+
+
+class RequestContextV2Http(BaseModel):
+ method: Literal["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
+ path: str
+ protocol: str
+ sourceIp: IPvAnyNetwork
+ userAgent: str
+
+
+class RequestContextV2(BaseModel):
+ accountId: str
+ apiId: str
+ authorizer: Optional[RequestContextV2Authorizer]
+ domainName: str
+ domainPrefix: str
+ requestId: str
+ routeKey: str
+ stage: str
+ time: str
+ timeEpoch: datetime
+ http: RequestContextV2Http
+
+
+class APIGatewayProxyEventV2Model(BaseModel):
+ version: str
+ routeKey: str
+ rawPath: str
+ rawQueryString: str
+ cookies: Optional[List[str]]
+ headers: Dict[str, str]
+ queryStringParameters: Dict[str, str]
+ pathParameters: Optional[Dict[str, str]]
+ stageVariables: Optional[Dict[str, str]]
+ requestContext: RequestContextV2
+ body: str
+ isBase64Encoded: bool
diff --git a/docs/utilities/parser.md b/docs/utilities/parser.md
index 83fca6b6741..11dbaca48a8 100644
--- a/docs/utilities/parser.md
+++ b/docs/utilities/parser.md
@@ -162,6 +162,7 @@ Parser comes with the following built-in models:
| **SesModel** | Lambda Event Source payload for Amazon Simple Email Service |
| **SnsModel** | Lambda Event Source payload for Amazon Simple Notification Service |
| **APIGatewayProxyEvent** | Lambda Event Source payload for Amazon API Gateway |
+| **APIGatewayProxyEventV2Model** | Lambda Event Source payload for Amazon API Gateway v2 payload |
### extending built-in models
@@ -295,17 +296,17 @@ Here's an example of parsing a model found in an event coming from EventBridge,
Parser comes with the following built-in envelopes, where `Model` in the return section is your given model.
-| Envelope name | Behaviour | Return |
-| -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
-| **DynamoDBStreamEnvelope** | 1. Parses data using `DynamoDBStreamModel`.
2. Parses records in `NewImage` and `OldImage` keys using your model.
3. Returns a list with a dictionary containing `NewImage` and `OldImage` keys | `List[Dict[str, Optional[Model]]]` |
-| **EventBridgeEnvelope** | 1. Parses data using `EventBridgeModel`.
2. Parses `detail` key using your model and returns it. | `Model` |
-| **SqsEnvelope** | 1. Parses data using `SqsModel`.
2. Parses records in `body` key using your model and return them in a list. | `List[Model]` |
-| **CloudWatchLogsEnvelope** | 1. Parses data using `CloudwatchLogsModel` which will base64 decode and decompress it.
2. Parses records in `message` key using your model and return them in a list. | `List[Model]` |
-| **KinesisDataStreamEnvelope** | 1. Parses data using `KinesisDataStreamModel` which will base64 decode it.
2. Parses records in in `Records` key using your model and returns them in a list. | `List[Model]` |
-| **SnsEnvelope** | 1. Parses data using `SnsModel`.
2. Parses records in `body` key using your model and return them in a list. | `List[Model]` |
-| **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` |
-
+| Envelope name | Behaviour | Return |
+| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
+| **DynamoDBStreamEnvelope** | 1. Parses data using `DynamoDBStreamModel`.
2. Parses records in `NewImage` and `OldImage` keys using your model.
3. Returns a list with a dictionary containing `NewImage` and `OldImage` keys | `List[Dict[str, Optional[Model]]]` |
+| **EventBridgeEnvelope** | 1. Parses data using `EventBridgeModel`.
2. Parses `detail` key using your model and returns it. | `Model` |
+| **SqsEnvelope** | 1. Parses data using `SqsModel`.
2. Parses records in `body` key using your model and return them in a list. | `List[Model]` |
+| **CloudWatchLogsEnvelope** | 1. Parses data using `CloudwatchLogsModel` which will base64 decode and decompress it.
2. Parses records in `message` key using your model and return them in a list. | `List[Model]` |
+| **KinesisDataStreamEnvelope** | 1. Parses data using `KinesisDataStreamModel` which will base64 decode it.
2. Parses records in in `Records` key using your model and returns them in a list. | `List[Model]` |
+| **SnsEnvelope** | 1. Parses data using `SnsModel`.
2. Parses records in `body` key using your model and return them in a list. | `List[Model]` |
+| **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` |
### Bringing your own envelope
You can create your own Envelope model and logic by inheriting from `BaseEnvelope`, and implementing the `parse` method.
diff --git a/tests/events/apiGatewayProxyV2Event.json b/tests/events/apiGatewayProxyV2Event.json
index 4d0cfdf5703..5e001934fee 100644
--- a/tests/events/apiGatewayProxyV2Event.json
+++ b/tests/events/apiGatewayProxyV2Event.json
@@ -36,7 +36,7 @@
"method": "POST",
"path": "/my/path",
"protocol": "HTTP/1.1",
- "sourceIp": "IP",
+ "sourceIp": "192.168.0.1/32",
"userAgent": "agent"
},
"requestId": "id",
@@ -54,4 +54,4 @@
"stageVariable1": "value1",
"stageVariable2": "value2"
}
-}
+}
\ No newline at end of file
diff --git a/tests/events/apiGatewayProxyV2IamEvent.json b/tests/events/apiGatewayProxyV2IamEvent.json
index 73d50d78a4a..43f33e1678d 100644
--- a/tests/events/apiGatewayProxyV2IamEvent.json
+++ b/tests/events/apiGatewayProxyV2IamEvent.json
@@ -29,7 +29,9 @@
"accountId": "1234567890",
"callerId": "AROA7ZJZYVRE7C3DUXHH6:CognitoIdentityCredentials",
"cognitoIdentity": {
- "amr" : ["foo"],
+ "amr": [
+ "foo"
+ ],
"identityId": "us-east-1:3f291106-8703-466b-8f2b-3ecee1ca56ce",
"identityPoolId": "us-east-1:4f291106-8703-466b-8f2b-3ecee1ca56ce"
},
@@ -47,7 +49,7 @@
"method": "GET",
"path": "/my/path",
"protocol": "HTTP/1.1",
- "sourceIp": "IP",
+ "sourceIp": "192.168.0.1/32",
"userAgent": "agent"
}
},
@@ -57,4 +59,4 @@
},
"body": "{\r\n\t\"a\": 1\r\n}",
"isBase64Encoded": false
-}
+}
\ No newline at end of file
diff --git a/tests/events/apiGatewayProxyV2LambdaAuthorizerEvent.json b/tests/events/apiGatewayProxyV2LambdaAuthorizerEvent.json
index 75d1574f854..cae3130de80 100644
--- a/tests/events/apiGatewayProxyV2LambdaAuthorizerEvent.json
+++ b/tests/events/apiGatewayProxyV2LambdaAuthorizerEvent.json
@@ -37,7 +37,7 @@
"method": "GET",
"path": "/my/path",
"protocol": "HTTP/1.1",
- "sourceIp": "IP",
+ "sourceIp": "192.168.0.1/32",
"userAgent": "agent"
}
},
@@ -47,4 +47,4 @@
},
"body": "{\r\n\t\"a\": 1\r\n}",
"isBase64Encoded": false
-}
+}
\ No newline at end of file
diff --git a/tests/functional/parser/test_apigwv2.py b/tests/functional/parser/test_apigwv2.py
new file mode 100644
index 00000000000..ee6a4790cd4
--- /dev/null
+++ b/tests/functional/parser/test_apigwv2.py
@@ -0,0 +1,92 @@
+from aws_lambda_powertools.utilities.parser import envelopes, event_parser
+from aws_lambda_powertools.utilities.parser.models import (
+ APIGatewayProxyEventV2Model,
+ RequestContextV2,
+ RequestContextV2Authorizer,
+)
+from aws_lambda_powertools.utilities.typing import LambdaContext
+from tests.functional.parser.schemas import MyApiGatewayBusiness
+from tests.functional.utils import load_event
+
+
+@event_parser(model=MyApiGatewayBusiness, envelope=envelopes.ApiGatewayV2Envelope)
+def handle_apigw_with_envelope(event: MyApiGatewayBusiness, _: LambdaContext):
+ assert event.message == "Hello"
+ assert event.username == "Ran"
+
+
+@event_parser(model=APIGatewayProxyEventV2Model)
+def handle_apigw_event(event: APIGatewayProxyEventV2Model, _: LambdaContext):
+ return event
+
+
+def test_apigw_v2_event_with_envelope():
+ event = load_event("apiGatewayProxyV2Event.json")
+ event["body"] = '{"message": "Hello", "username": "Ran"}'
+ handle_apigw_with_envelope(event, LambdaContext())
+
+
+def test_apigw_v2_event_jwt_authorizer():
+ event = load_event("apiGatewayProxyV2Event.json")
+ parsed_event: APIGatewayProxyEventV2Model = handle_apigw_event(event, LambdaContext())
+ assert parsed_event.version == event["version"]
+ assert parsed_event.routeKey == event["routeKey"]
+ assert parsed_event.rawPath == event["rawPath"]
+ assert parsed_event.rawQueryString == event["rawQueryString"]
+ assert parsed_event.cookies == event["cookies"]
+ assert parsed_event.cookies[0] == "cookie1"
+ assert parsed_event.headers == event["headers"]
+ assert parsed_event.queryStringParameters == event["queryStringParameters"]
+ assert parsed_event.queryStringParameters["parameter2"] == "value"
+
+ request_context = parsed_event.requestContext
+ assert request_context.accountId == event["requestContext"]["accountId"]
+ assert request_context.apiId == event["requestContext"]["apiId"]
+ assert request_context.authorizer.jwt.claims == event["requestContext"]["authorizer"]["jwt"]["claims"]
+ assert request_context.authorizer.jwt.scopes == event["requestContext"]["authorizer"]["jwt"]["scopes"]
+ assert request_context.domainName == event["requestContext"]["domainName"]
+ assert request_context.domainPrefix == event["requestContext"]["domainPrefix"]
+
+ http = request_context.http
+ assert http.method == "POST"
+ assert http.path == "/my/path"
+ assert http.protocol == "HTTP/1.1"
+ assert str(http.sourceIp) == "192.168.0.1/32"
+ assert http.userAgent == "agent"
+
+ assert request_context.requestId == event["requestContext"]["requestId"]
+ assert request_context.routeKey == event["requestContext"]["routeKey"]
+ assert request_context.stage == event["requestContext"]["stage"]
+ assert request_context.time == event["requestContext"]["time"]
+ convert_time = int(round(request_context.timeEpoch.timestamp() * 1000))
+ assert convert_time == event["requestContext"]["timeEpoch"]
+ assert parsed_event.body == event["body"]
+ assert parsed_event.pathParameters == event["pathParameters"]
+ assert parsed_event.isBase64Encoded == event["isBase64Encoded"]
+ assert parsed_event.stageVariables == event["stageVariables"]
+
+
+def test_api_gateway_proxy_v2_event_lambda_authorizer():
+ event = load_event("apiGatewayProxyV2LambdaAuthorizerEvent.json")
+ parsed_event: APIGatewayProxyEventV2Model = handle_apigw_event(event, LambdaContext())
+ request_context: RequestContextV2 = parsed_event.requestContext
+ assert request_context is not None
+ lambda_props: RequestContextV2Authorizer = request_context.authorizer.lambda_value
+ assert lambda_props is not None
+ assert lambda_props["key"] == "value"
+
+
+def test_api_gateway_proxy_v2_event_iam_authorizer():
+ event = load_event("apiGatewayProxyV2IamEvent.json")
+ parsed_event: APIGatewayProxyEventV2Model = handle_apigw_event(event, LambdaContext())
+ iam = parsed_event.requestContext.authorizer.iam
+ assert iam is not None
+ assert iam.accessKey == "ARIA2ZJZYVUEREEIHAKY"
+ assert iam.accountId == "1234567890"
+ assert iam.callerId == "AROA7ZJZYVRE7C3DUXHH6:CognitoIdentityCredentials"
+ assert iam.cognitoIdentity.amr == ["foo"]
+ assert iam.cognitoIdentity.identityId == "us-east-1:3f291106-8703-466b-8f2b-3ecee1ca56ce"
+ assert iam.cognitoIdentity.identityPoolId == "us-east-1:4f291106-8703-466b-8f2b-3ecee1ca56ce"
+ assert iam.principalOrgId == "AwsOrgId"
+ assert iam.userArn == "arn:aws:iam::1234567890:user/Admin"
+ assert iam.userId == "AROA2ZJZYVRE7Y3TUXHH6"
diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py
index f56d0700e6f..07648f84ee9 100644
--- a/tests/functional/test_data_classes.py
+++ b/tests/functional/test_data_classes.py
@@ -743,7 +743,7 @@ def test_api_gateway_proxy_v2_event():
assert http.method == "POST"
assert http.path == "/my/path"
assert http.protocol == "HTTP/1.1"
- assert http.source_ip == "IP"
+ assert http.source_ip == "192.168.0.1/32"
assert http.user_agent == "agent"
assert request_context.request_id == event["requestContext"]["requestId"]