Description
Use case
I happened upon using the discriminator
to allow my lambdas to handle multiple different event sources, e.g. aws.events
and aws.s3
. Using Pydantic's TypeAdapter
and Field
classes, we can create an adapter which looks at a specific field (in this case source
) to determine how to parse the data, without having to do the expensive try-validate-fail-try_next_model pattern. This allows us to decorate a lambda in such a way that it can automatically cast different one of several event types to stricter subtypes
EventBridgeSource = Annotated[
Union[S3EventNotificationEventBridgeModel, ScheduledNotificationEventBridgeModel],
Field(discriminator="source"),
]
EventBridgeModelAdapter = TypeAdapter(Union[EventBridgeSource, EventBridgeModel])
Solution/User Experience
This fails with the stock event_parser
so I had to bodge event_parser
to get it to work. I think the issue is trying to cast the input TypeAdapter
into a TypeAdapter
again, throwing PydanticSchemaGenerationError
. It should be straightforward to allow _retrieve_or_set_model_from_cache
to check if the input is already a TypeAdapter
and return that from the cache rather than trying to wrap it.
Setup
from typing import Any, Callable, Literal, Optional, TypeVar, Union
from typing_extensions import Annotated
from pydantic import BaseModel, Field, ValidationError, TypeAdapter
# from aws_lambda_powertools.utilities.parser import event_parser
from aws_lambda_powertools.utilities.parser.envelopes.base import Envelope
from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
from aws_lambda_powertools.utilities.parser.models.event_bridge import EventBridgeModel
from aws_lambda_powertools.utilities.parser.models.s3 import (
S3EventNotificationEventBridgeDetailModel,
)
T = TypeVar("T")
class S3EventNotificationEventBridgeModel(EventBridgeModel):
detail: S3EventNotificationEventBridgeDetailModel
source: Literal["aws.s3"]
class ScheduledNotificationEventBridgeModel(EventBridgeModel):
source: Literal["aws.events"]
EventBridgeSource = Annotated[
Union[S3EventNotificationEventBridgeModel, ScheduledNotificationEventBridgeModel],
Field(discriminator="source"),
]
EventBridgeModelAdapter = TypeAdapter(Union[EventBridgeSource, EventBridgeModel])
class LambdaContext:
function_name: str = "test-function"
memory_limit_in_mb: int = 128
invoked_function_arn: str = f"arn:aws:lambda:us-east-1:123456789012:test-function"
aws_request_id: str = 'e7a150fa-ed3d-4fbb-8158-e470b60621da'
@lambda_handler_decorator
def event_parser(
handler: Callable[..., 'EventParserReturnType'],
event: dict[str, Any],
context: LambdaContext = None,
model: Optional[type[T]] = None,
envelope: Optional[type[Envelope]] = None,
**kwargs: Any,
) -> 'EventParserReturnType':
parsed_event = EventBridgeModelAdapter.validate_python(event)
return handler(parsed_event, context, **kwargs)
@event_parser(model=EventBridgeModelAdapter)
def handler(event: EventBridgeModelAdapter, context = None):
print(f'Got event, parsed into {type(event)=}')
Testing
object_event = {
"version": "0",
"id": "55ec5937-9835-fb01-3122-a054f478882b",
"detail-type": "Object Created",
"source": "aws.s3",
"account": "539101081866",
"time": "2024-10-16T22:58:07Z",
"region": "us-east-1",
"resources": [
"arn:aws:s3:::bpa-sandbox-mcdermott"
],
"detail": {
"version": "0",
"bucket": {
"name": "bpa-sandbox-mcdermott"
},
"object": {
"key": "testbed/hello3.txt",
"size": 12,
},
"requester": "539101081866",
}
}
scheduled_event = {
"version": "0",
"id": "ac8bd595-adcc-cd98-cc06-5e8fad5d9565",
"detail-type": "Scheduled Event",
"source": "aws.events",
"account": "084286224071",
"time": "2024-10-23T09:00:00Z",
"region": "us-east-1",
"resources": [
"arn:aws:events:us-east-1:084286224071:rule/rl-live-StatesIntegration-RuleDailyDownloadWorkflo-1KZH76DGASA21"
],
"detail": {}
}
fallback_event = {
"version": "0",
"id": "ac8bd595-adcc-cd98-cc06-5e8fad5d9565",
"detail-type": "Scheduled Event",
"source": "aws.other",
"account": "084286224071",
"time": "2024-10-23T09:00:00Z",
"region": "us-east-1",
"resources": [
"arn:aws:events:us-east-1:084286224071:rule/rl-live-StatesIntegration-RuleDailyDownloadWorkflo-1KZH76DGASA21"
],
"detail": {}
}
handler(object_event, LambdaContext())
handler(scheduled_event, LambdaContext())
handler(fallback_event, LambdaContext())
results
Got event, parsed into type(event)=<class '__main__.S3EventNotificationEventBridgeModel'>
Got event, parsed into type(event)=<class '__main__.ScheduledNotificationEventBridgeModel'>
Got event, parsed into type(event)=<class 'aws_lambda_powertools.utilities.parser.models.event_bridge.EventBridgeModel'>
Alternative solutions
Not sure how else to implement this.
Acknowledgment
- This feature request meets Powertools for AWS Lambda (Python) Tenets
- Should this be considered in other Powertools for AWS Lambda languages? i.e. Java, TypeScript, and .NET
Metadata
Metadata
Assignees
Type
Projects
Status