Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(parser): add models for API GW Websockets events #5597

Merged
merged 15 commits into from
Nov 24, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .apigw import ApiGatewayEnvelope
from .apigw_websocket_api import ApiGatewayWebSocketEnvelope
from .apigwv2 import ApiGatewayV2Envelope
from .base import BaseEnvelope
from .bedrock_agent import BedrockAgentEnvelope
Expand All @@ -17,6 +18,7 @@
__all__ = [
"ApiGatewayEnvelope",
"ApiGatewayV2Envelope",
"ApiGatewayWebSocketEnvelope",
"BedrockAgentEnvelope",
"CloudWatchLogsEnvelope",
"DynamoDBStreamEnvelope",
Expand Down
anafalcao marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any

from aws_lambda_powertools.utilities.parser.envelopes.base import BaseEnvelope
from aws_lambda_powertools.utilities.parser.models import APIGatewayWebSocketMessageEventModel

if TYPE_CHECKING:
from aws_lambda_powertools.utilities.parser.types import Model

logger = logging.getLogger(__name__)


class ApiGatewayWebSocketEnvelope(BaseEnvelope):
"""API Gateway WebSockets API envelope to extract data within body key of messages routes
anafalcao marked this conversation as resolved.
Show resolved Hide resolved
(not disconnect or connect)"""

def parse(self, data: dict[str, Any] | Any | None, model: type[Model]) -> Model | None:
"""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 WebSockets model {APIGatewayWebSocketMessageEventModel}",
)
parsed_envelope: APIGatewayWebSocketMessageEventModel = APIGatewayWebSocketMessageEventModel.model_validate(
data,
)
logger.debug(f"Parsing event payload in `detail` with {model}")
return self._parse(data=parsed_envelope.body, model=model)
18 changes: 18 additions & 0 deletions aws_lambda_powertools/utilities/parser/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
APIGatewayEventRequestContext,
APIGatewayProxyEventModel,
)
from .apigw_websocket import (
APIGatewayWebSocketConnectEventModel,
APIGatewayWebSocketConnectEventRequestContext,
APIGatewayWebSocketDisconnectEventModel,
APIGatewayWebSocketDisconnectEventRequestContext,
APIGatewayWebSocketEventIdentity,
APIGatewayWebSocketEventRequestContextBase,
APIGatewayWebSocketMessageEventModel,
APIGatewayWebSocketMessageEventRequestContext,
)
from .apigwv2 import (
ApiGatewayAuthorizerRequestV2,
APIGatewayProxyEventV2Model,
Expand Down Expand Up @@ -105,6 +115,14 @@
__all__ = [
"APIGatewayProxyEventV2Model",
"ApiGatewayAuthorizerRequestV2",
"APIGatewayWebSocketEventIdentity",
"APIGatewayWebSocketMessageEventModel",
"APIGatewayWebSocketMessageEventRequestContext",
"APIGatewayWebSocketConnectEventModel",
"APIGatewayWebSocketConnectEventRequestContext",
"APIGatewayWebSocketDisconnectEventRequestContext",
"APIGatewayWebSocketDisconnectEventModel",
"APIGatewayWebSocketEventRequestContextBase",
"RequestContextV2",
"RequestContextV2Http",
"RequestContextV2Authorizer",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from datetime import datetime
from typing import Dict, List, Literal, Optional, Type, Union

from pydantic import BaseModel, ConfigDict
from pydantic.networks import IPvAnyNetwork


class APIGatewayWebSocketEventIdentity(BaseModel):
anafalcao marked this conversation as resolved.
Show resolved Hide resolved
model_config = ConfigDict(populate_by_name=True)
anafalcao marked this conversation as resolved.
Show resolved Hide resolved

sourceIp: IPvAnyNetwork
anafalcao marked this conversation as resolved.
Show resolved Hide resolved
userAgent: Optional[str] = None
anafalcao marked this conversation as resolved.
Show resolved Hide resolved

class APIGatewayWebSocketEventRequestContextBase(BaseModel):
extendedRequestId: str
requestTime: str
stage: str
connectedAt: datetime
requestTimeEpoch: datetime
identity: APIGatewayWebSocketEventIdentity
requestId: str
domainName: str
connectionId: str
apiId: str


class APIGatewayWebSocketMessageEventRequestContext(APIGatewayWebSocketEventRequestContextBase):
routeKey: str
messageId: str
eventType: Literal["MESSAGE"]
messageDirection: Literal["IN", "OUT"]


class APIGatewayWebSocketConnectEventRequestContext(APIGatewayWebSocketEventRequestContextBase):
routeKey: Literal["$connect"]
eventType: Literal["CONNECT"]
messageDirection: Literal["IN"]


class APIGatewayWebSocketDisconnectEventRequestContext(APIGatewayWebSocketEventRequestContextBase):
routeKey: Literal["$disconnect"]
disconnectStatusCode: int
eventType: Literal["DISCONNECT"]
messageDirection: Literal["IN"]
disconnectReason: str


class APIGatewayWebSocketConnectEventModel(BaseModel):
headers: Dict[str, str]
multiValueHeaders: Dict[str, List[str]]
requestContext: APIGatewayWebSocketConnectEventRequestContext
isBase64Encoded: bool


class APIGatewayWebSocketDisconnectEventModel(BaseModel):
headers: Dict[str, str]
multiValueHeaders: Dict[str, List[str]]
requestContext: APIGatewayWebSocketDisconnectEventRequestContext
isBase64Encoded: bool


class APIGatewayWebSocketMessageEventModel(BaseModel):
requestContext: APIGatewayWebSocketMessageEventRequestContext
isBase64Encoded: bool
body: Optional[Union[str, Type[BaseModel]]] = None
anafalcao marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 5 additions & 1 deletion docs/utilities/parser.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ The example above uses `SqsModel`. Other built-in models can be found below.
| **ApiGatewayAuthorizerRequest** | Lambda Event Source payload for Amazon API Gateway Lambda Authorizer with Request |
| **APIGatewayProxyEventV2Model** | Lambda Event Source payload for Amazon API Gateway v2 payload |
| **ApiGatewayAuthorizerRequestV2** | Lambda Event Source payload for Amazon API Gateway v2 Lambda Authorizer |
| **APIGatewayWebSocketMessageEventModel** | Lambda Event Source payload for Amazon API Gateway WebSocket API message body |
| **APIGatewayWebSocketConnectEventModel** | Lambda Event Source payload for Amazon API Gateway WebSocket API $connect message |
| **APIGatewayWebSocketDisconnectEventModel** | Lambda Event Source payload for Amazon API Gateway WebSocket API $disconnect message |
| **BedrockAgentEventModel** | Lambda Event Source payload for Bedrock Agents |
| **CloudFormationCustomResourceCreateModel** | Lambda Event Source payload for AWS CloudFormation `CREATE` operation |
| **CloudFormationCustomResourceUpdateModel** | Lambda Event Source payload for AWS CloudFormation `UPDATE` operation |
Expand Down Expand Up @@ -188,8 +191,9 @@ You can use pre-built envelopes provided by the Parser to extract and parse spec
| **KinesisFirehoseEnvelope** | 1. Parses data using `KinesisFirehoseModel` 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` |
| **ApiGatewayEnvelope** | 1. Parses data using `APIGatewayProxyEventModel`. ``2. Parses `body` key using your model`` and returns it. | `Model` |
| **ApiGatewayWebSocketEnvelope** | 1. Parses data using `APIGatewayWebSocketMessageEventModel`. ``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` |
| **KafkaEnvelope** | 1. Parses data using `KafkaRecordModel`. ``2. Parses `value` key using your model`` and returns it. | `Model` |
| **VpcLatticeEnvelope** | 1. Parses data using `VpcLatticeModel`. ``2. Parses `value` key using your model`` and returns it. | `Model` |
Expand Down
40 changes: 40 additions & 0 deletions tests/events/apiGatewayWebSocketApiConnect.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"headers": {
"Host": "fjnq7njcv2.execute-api.us-east-1.amazonaws.com",
"Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits",
"Sec-WebSocket-Key": "+W5xw47OHh3OTFsWKjGu9Q==",
"Sec-WebSocket-Version": "13",
"X-Amzn-Trace-Id": "Root=1-6731ebfc-08e1e656421db73c5d2eef31",
"X-Forwarded-For": "166.90.225.1",
"X-Forwarded-Port": "443",
"X-Forwarded-Proto": "https"
},
"multiValueHeaders": {
"Host": ["fjnq7njcv2.execute-api.us-east-1.amazonaws.com"],
"Sec-WebSocket-Extensions": ["permessage-deflate; client_max_window_bits"],
"Sec-WebSocket-Key": ["+W5xw47OHh3OTFsWKjGu9Q=="],
"Sec-WebSocket-Version": ["13"],
"X-Amzn-Trace-Id": ["Root=1-6731ebfc-08e1e656421db73c5d2eef31"],
"X-Forwarded-For": ["166.90.225.1"],
"X-Forwarded-Port": ["443"],
"X-Forwarded-Proto": ["https"]
},
"requestContext": {
"routeKey": "$connect",
"eventType": "CONNECT",
"extendedRequestId": "BFHPhFe3IAMF95g=",
"requestTime": "11/Nov/2024:11:35:24 +0000",
"messageDirection": "IN",
"stage": "prod",
"connectedAt": 1731324924553,
"requestTimeEpoch": 1731324924561,
"identity": {
"sourceIp": "166.90.225.1"
},
"requestId": "BFHPhFe3IAMF95g=",
"domainName": "asasasas.execute-api.us-east-1.amazonaws.com",
"connectionId": "BFHPhfCWIAMCKlQ=",
"apiId": "asasasas"
},
"isBase64Encoded": false
}
34 changes: 34 additions & 0 deletions tests/events/apiGatewayWebSocketApiDisconnect.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"headers": {
"Host": "asasasas.execute-api.us-east-1.amazonaws.com",
"x-api-key": "",
"X-Forwarded-For": "",
"x-restapi": ""
},
"multiValueHeaders": {
"Host": ["asasasas.execute-api.us-east-1.amazonaws.com"],
"x-api-key": [""],
"X-Forwarded-For": [""],
"x-restapi": [""]
},
"requestContext": {
"routeKey": "$disconnect",
"disconnectStatusCode": 1005,
"eventType": "DISCONNECT",
"extendedRequestId": "BFbOeE87IAMF31w=",
"requestTime": "11/Nov/2024:13:51:49 +0000",
"messageDirection": "IN",
"disconnectReason": "Client-side close frame status not set",
"stage": "prod",
"connectedAt": 1731332735513,
"requestTimeEpoch": 1731333109875,
"identity": {
"sourceIp": "166.90.225.1"
},
"requestId": "BFbOeE87IAMF31w=",
"domainName": "asasasas.execute-api.us-east-1.amazonaws.com",
"connectionId": "BFaT_fALIAMCKug=",
"apiId": "asasasas"
},
"isBase64Encoded": false
}
22 changes: 22 additions & 0 deletions tests/events/apiGatewayWebSocketApiMessage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"requestContext": {
"routeKey": "chat",
"messageId": "BFaVtfGSIAMCKug=",
"eventType": "MESSAGE",
"extendedRequestId": "BFaVtH2HoAMFZEQ=",
"requestTime": "11/Nov/2024:13:45:46 +0000",
"messageDirection": "IN",
"stage": "prod",
"connectedAt": 1731332735513,
"requestTimeEpoch": 1731332746514,
"identity": {
"sourceIp": "166.90.225.1"
},
"requestId": "BFaVtH2HoAMFZEQ=",
"domainName": "asasasas.execute-api.us-east-1.amazonaws.com",
"connectionId": "BFaT_fALIAMCKug=",
"apiId": "asasasas"
},
"body": "{\"action\": \"chat\", \"message\": \"Hello from client\"}",
"isBase64Encoded": false
}
5 changes: 5 additions & 0 deletions tests/unit/parser/_pydantic/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ class MyApiGatewayBusiness(BaseModel):
username: str


class MyApiGatewayWebSocketBusiness(BaseModel):
message: str
action: str


class MyALambdaFuncUrlBusiness(BaseModel):
message: str
username: str
Expand Down
Loading
Loading