diff --git a/aws_lambda_powertools/utilities/parser/envelopes/__init__.py b/aws_lambda_powertools/utilities/parser/envelopes/__init__.py
index 10c70272c7d..e6f63c4792d 100644
--- a/aws_lambda_powertools/utilities/parser/envelopes/__init__.py
+++ b/aws_lambda_powertools/utilities/parser/envelopes/__init__.py
@@ -1,3 +1,4 @@
+from .apigw import ApiGatewayEnvelope
from .base import BaseEnvelope
from .cloudwatch import CloudWatchLogsEnvelope
from .dynamodb import DynamoDBStreamEnvelope
@@ -7,6 +8,7 @@
from .sqs import SqsEnvelope
__all__ = [
+ "ApiGatewayEnvelope",
"CloudWatchLogsEnvelope",
"DynamoDBStreamEnvelope",
"EventBridgeEnvelope",
diff --git a/aws_lambda_powertools/utilities/parser/envelopes/apigw.py b/aws_lambda_powertools/utilities/parser/envelopes/apigw.py
new file mode 100644
index 00000000000..6b74a3037e9
--- /dev/null
+++ b/aws_lambda_powertools/utilities/parser/envelopes/apigw.py
@@ -0,0 +1,32 @@
+import logging
+from typing import Any, Dict, Optional, Type, Union
+
+from ..models import APIGatewayProxyEventModel
+from ..types import Model
+from .base import BaseEnvelope
+
+logger = logging.getLogger(__name__)
+
+
+class ApiGatewayEnvelope(BaseEnvelope):
+ """API Gateway 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 {APIGatewayProxyEventModel}")
+ parsed_envelope = APIGatewayProxyEventModel.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 923d5d057c3..0e59b2197a8 100644
--- a/aws_lambda_powertools/utilities/parser/models/__init__.py
+++ b/aws_lambda_powertools/utilities/parser/models/__init__.py
@@ -1,4 +1,10 @@
from .alb import AlbModel, AlbRequestContext, AlbRequestContextData
+from .apigw import (
+ APIGatewayEventAuthorizer,
+ APIGatewayEventIdentity,
+ APIGatewayEventRequestContext,
+ APIGatewayProxyEventModel,
+)
from .cloudwatch import CloudWatchLogsData, CloudWatchLogsDecode, CloudWatchLogsLogEvent, CloudWatchLogsModel
from .dynamodb import DynamoDBStreamChangedRecordModel, DynamoDBStreamModel, DynamoDBStreamRecordModel
from .event_bridge import EventBridgeModel
@@ -70,4 +76,8 @@
"SqsRecordModel",
"SqsMsgAttributeModel",
"SqsAttributesModel",
+ "APIGatewayProxyEventModel",
+ "APIGatewayEventRequestContext",
+ "APIGatewayEventAuthorizer",
+ "APIGatewayEventIdentity",
]
diff --git a/aws_lambda_powertools/utilities/parser/models/apigw.py b/aws_lambda_powertools/utilities/parser/models/apigw.py
new file mode 100644
index 00000000000..de968e20ecf
--- /dev/null
+++ b/aws_lambda_powertools/utilities/parser/models/apigw.py
@@ -0,0 +1,92 @@
+from datetime import datetime
+from typing import Any, Dict, List, Optional
+
+from pydantic import BaseModel, root_validator
+from pydantic.networks import IPvAnyNetwork
+
+from ..types import Literal
+
+
+class ApiGatewayUserCertValidity(BaseModel):
+ notBefore: str
+ notAfter: str
+
+
+class ApiGatewayUserCert(BaseModel):
+ clientCertPem: str
+ subjectDN: str
+ issuerDN: str
+ serialNumber: str
+ validity: ApiGatewayUserCertValidity
+
+
+class APIGatewayEventIdentity(BaseModel):
+ accessKey: Optional[str]
+ accountId: Optional[str]
+ apiKey: Optional[str]
+ apiKeyId: Optional[str]
+ caller: Optional[str]
+ cognitoAuthenticationProvider: Optional[str]
+ cognitoAuthenticationType: Optional[str]
+ cognitoIdentityId: Optional[str]
+ cognitoIdentityPoolId: Optional[str]
+ principalOrgId: Optional[str]
+ sourceIp: IPvAnyNetwork
+ user: Optional[str]
+ userAgent: Optional[str]
+ userArn: Optional[str]
+ clientCert: Optional[ApiGatewayUserCert]
+
+
+class APIGatewayEventAuthorizer(BaseModel):
+ claims: Optional[Dict[str, Any]]
+ scopes: Optional[List[str]]
+
+
+class APIGatewayEventRequestContext(BaseModel):
+ accountId: str
+ apiId: str
+ authorizer: APIGatewayEventAuthorizer
+ stage: str
+ protocol: str
+ identity: APIGatewayEventIdentity
+ requestId: str
+ requestTime: str
+ requestTimeEpoch: datetime
+ resourceId: Optional[str]
+ resourcePath: str
+ domainName: Optional[str]
+ domainPrefix: Optional[str]
+ extendedRequestId: Optional[str]
+ httpMethod: Literal["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
+ path: str
+ connectedAt: Optional[datetime]
+ connectionId: Optional[str]
+ eventType: Optional[Literal["CONNECT", "MESSAGE", "DISCONNECT"]]
+ messageDirection: Optional[str]
+ messageId: Optional[str]
+ routeKey: Optional[str]
+ operationName: Optional[str]
+
+
+class APIGatewayProxyEventModel(BaseModel):
+ version: str
+ resource: str
+ path: str
+ httpMethod: Literal["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
+ headers: Dict[str, str]
+ multiValueHeaders: Dict[str, List[str]]
+ queryStringParameters: Optional[Dict[str, str]]
+ multiValueQueryStringParameters: Optional[Dict[str, List[str]]]
+ requestContext: APIGatewayEventRequestContext
+ pathParameters: Optional[Dict[str, str]]
+ stageVariables: Optional[Dict[str, str]]
+ isBase64Encoded: bool
+ body: str
+
+ @root_validator()
+ def check_message_id(cls, values):
+ message_id, event_type = values.get("messageId"), values.get("eventType")
+ if message_id is not None and event_type != "MESSAGE":
+ raise TypeError("messageId is available only when the `eventType` is `MESSAGE`")
+ return values
diff --git a/docs/utilities/parser.md b/docs/utilities/parser.md
index 7c39b1ffd0a..7fa78a5e372 100644
--- a/docs/utilities/parser.md
+++ b/docs/utilities/parser.md
@@ -161,6 +161,7 @@ Parser comes with the following built-in models:
| **KinesisDataStreamModel** | Lambda Event Source payload for Amazon Kinesis Data Streams |
| **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 |
### extending built-in models
@@ -294,16 +295,16 @@ 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]` |
-
+| 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` |
### 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/apiGatewayProxyEvent.json b/tests/events/apiGatewayProxyEvent.json
index 1fed04a25bf..8bc72b7ce78 100644
--- a/tests/events/apiGatewayProxyEvent.json
+++ b/tests/events/apiGatewayProxyEvent.json
@@ -49,10 +49,20 @@
"cognitoIdentityId": null,
"cognitoIdentityPoolId": null,
"principalOrgId": null,
- "sourceIp": "IP",
+ "sourceIp": "192.168.0.1/32",
"user": null,
"userAgent": "user-agent",
- "userArn": null
+ "userArn": null,
+ "clientCert": {
+ "clientCertPem": "CERT_CONTENT",
+ "subjectDN": "www.example.com",
+ "issuerDN": "Example issuer",
+ "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1",
+ "validity": {
+ "notBefore": "May 28 12:30:02 2019 GMT",
+ "notAfter": "Aug 5 09:36:04 2021 GMT"
+ }
+ }
},
"path": "/my/path",
"protocol": "HTTP/1.1",
@@ -67,4 +77,4 @@
"stageVariables": null,
"body": "Hello from Lambda!",
"isBase64Encoded": true
-}
+}
\ No newline at end of file
diff --git a/tests/functional/parser/schemas.py b/tests/functional/parser/schemas.py
index a944b4f09c0..8ff56f703a7 100644
--- a/tests/functional/parser/schemas.py
+++ b/tests/functional/parser/schemas.py
@@ -81,3 +81,8 @@ class MyKinesisBusiness(BaseModel):
class MyCloudWatchBusiness(BaseModel):
my_message: str
user: str
+
+
+class MyApiGatewayBusiness(BaseModel):
+ message: str
+ username: str
diff --git a/tests/functional/parser/test_apigw.py b/tests/functional/parser/test_apigw.py
new file mode 100644
index 00000000000..333654f3f89
--- /dev/null
+++ b/tests/functional/parser/test_apigw.py
@@ -0,0 +1,102 @@
+from aws_lambda_powertools.utilities.parser import envelopes, event_parser
+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.parser.utils import load_event
+
+
+@event_parser(model=MyApiGatewayBusiness, envelope=envelopes.ApiGatewayEnvelope)
+def handle_apigw_with_envelope(event: MyApiGatewayBusiness, _: LambdaContext):
+ assert event.message == "Hello"
+ assert event.username == "Ran"
+
+
+@event_parser(model=APIGatewayProxyEventModel)
+def handle_apigw_event(event: APIGatewayProxyEventModel, _: LambdaContext):
+ assert event.body == "Hello from Lambda!"
+ return event
+
+
+def test_apigw_event_with_envelope():
+ event = load_event("apiGatewayProxyEvent.json")
+ event["body"] = '{"message": "Hello", "username": "Ran"}'
+ handle_apigw_with_envelope(event, LambdaContext())
+
+
+def test_apigw_event():
+ event = load_event("apiGatewayProxyEvent.json")
+ parsed_event: APIGatewayProxyEventModel = handle_apigw_event(event, LambdaContext())
+ assert parsed_event.version == event["version"]
+ assert parsed_event.resource == event["resource"]
+ assert parsed_event.path == event["path"]
+ assert parsed_event.headers == event["headers"]
+ assert parsed_event.multiValueHeaders == event["multiValueHeaders"]
+ assert parsed_event.queryStringParameters == event["queryStringParameters"]
+ assert parsed_event.multiValueQueryStringParameters == event["multiValueQueryStringParameters"]
+
+ request_context = parsed_event.requestContext
+ assert request_context.accountId == event["requestContext"]["accountId"]
+ assert request_context.apiId == event["requestContext"]["apiId"]
+
+ authorizer = request_context.authorizer
+ assert authorizer.claims is None
+ assert authorizer.scopes is None
+
+ assert request_context.domainName == event["requestContext"]["domainName"]
+ assert request_context.domainPrefix == event["requestContext"]["domainPrefix"]
+ assert request_context.extendedRequestId == event["requestContext"]["extendedRequestId"]
+ assert request_context.httpMethod == event["requestContext"]["httpMethod"]
+
+ identity = request_context.identity
+ assert identity.accessKey == event["requestContext"]["identity"]["accessKey"]
+ assert identity.accountId == event["requestContext"]["identity"]["accountId"]
+ assert identity.caller == event["requestContext"]["identity"]["caller"]
+ assert (
+ identity.cognitoAuthenticationProvider == event["requestContext"]["identity"]["cognitoAuthenticationProvider"]
+ )
+ assert identity.cognitoAuthenticationType == event["requestContext"]["identity"]["cognitoAuthenticationType"]
+ assert identity.cognitoIdentityId == event["requestContext"]["identity"]["cognitoIdentityId"]
+ assert identity.cognitoIdentityPoolId == event["requestContext"]["identity"]["cognitoIdentityPoolId"]
+ assert identity.principalOrgId == event["requestContext"]["identity"]["principalOrgId"]
+ assert str(identity.sourceIp) == event["requestContext"]["identity"]["sourceIp"]
+ assert identity.user == event["requestContext"]["identity"]["user"]
+ assert identity.userAgent == event["requestContext"]["identity"]["userAgent"]
+ assert identity.userArn == event["requestContext"]["identity"]["userArn"]
+ assert identity.clientCert is not None
+ assert identity.clientCert.clientCertPem == event["requestContext"]["identity"]["clientCert"]["clientCertPem"]
+ assert identity.clientCert.subjectDN == event["requestContext"]["identity"]["clientCert"]["subjectDN"]
+ assert identity.clientCert.issuerDN == event["requestContext"]["identity"]["clientCert"]["issuerDN"]
+ assert identity.clientCert.serialNumber == event["requestContext"]["identity"]["clientCert"]["serialNumber"]
+ assert (
+ identity.clientCert.validity.notBefore
+ == event["requestContext"]["identity"]["clientCert"]["validity"]["notBefore"]
+ )
+ assert (
+ identity.clientCert.validity.notAfter
+ == event["requestContext"]["identity"]["clientCert"]["validity"]["notAfter"]
+ )
+
+ assert request_context.path == event["requestContext"]["path"]
+ assert request_context.protocol == event["requestContext"]["protocol"]
+ assert request_context.requestId == event["requestContext"]["requestId"]
+ assert request_context.requestTime == event["requestContext"]["requestTime"]
+ convert_time = int(round(request_context.requestTimeEpoch.timestamp() * 1000))
+ assert convert_time == 1583349317135
+ assert request_context.resourceId == event["requestContext"]["resourceId"]
+ assert request_context.resourcePath == event["requestContext"]["resourcePath"]
+ assert request_context.stage == event["requestContext"]["stage"]
+
+ assert parsed_event.pathParameters == event["pathParameters"]
+ assert parsed_event.stageVariables == event["stageVariables"]
+ assert parsed_event.body == event["body"]
+ assert parsed_event.isBase64Encoded == event["isBase64Encoded"]
+
+ assert request_context.connectedAt is None
+ assert request_context.connectionId is None
+ assert request_context.eventType is None
+ assert request_context.messageDirection is None
+ assert request_context.messageId is None
+ assert request_context.routeKey is None
+ assert request_context.operationName is None
+ assert identity.apiKey is None
+ assert identity.apiKeyId is None