Skip to content

Commit f9a715d

Browse files
author
Ran Isenberg
committed
RFC: Validate incoming and outgoing events utility #95
1 parent 81539a0 commit f9a715d

File tree

8 files changed

+516
-79
lines changed

8 files changed

+516
-79
lines changed

Diff for: README.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray,
1313
* **[Logging](https://awslabs.github.io/aws-lambda-powertools-python/core/logger/)** - Structured logging made easier, and decorator to enrich structured logging with key Lambda context details
1414
* **[Metrics](https://awslabs.github.io/aws-lambda-powertools-python/core/metrics/)** - Custom Metrics created asynchronously via CloudWatch Embedded Metric Format (EMF)
1515
* **[Bring your own middleware](https://awslabs.github.io/aws-lambda-powertools-python/utilities/middleware_factory/)** - Decorator factory to create your own middleware to run logic before, and after each Lambda invocation
16+
* **[Validation](https://)** -
1617

1718
### Installation
1819

Diff for: aws_lambda_powertools/validation/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Validation utility
2+
"""
3+
4+
5+
__all__ = ["*"]

Diff for: aws_lambda_powertools/validation/schemas.py

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from datetime import date, datetime
2+
from typing import Any, Dict, List, Optional
3+
4+
from pydantic import BaseModel, root_validator
5+
from typing_extensions import Literal
6+
7+
8+
class DynamoScheme(BaseModel):
9+
ApproximateCreationDateTime: date
10+
Keys: Dict[Literal['id'], Dict[Literal['S'], str]]
11+
NewImage: Optional[Dict[str, Any]] = {}
12+
OldImage: Optional[Dict[str, Any]] = {}
13+
SequenceNumber: str
14+
SizeBytes: int
15+
StreamViewType: Literal['NEW_AND_OLD_IMAGES', 'KEYS_ONLY', 'NEW_IMAGE', 'OLD_IMAGE']
16+
17+
@root_validator
18+
def check_one_image_exists(cls, values):
19+
newimg, oldimg = values.get('NewImage'), values.get('OldImage')
20+
stream_type = values.get('StreamViewType')
21+
if stream_type == 'NEW_AND_OLD_IMAGES' and not newimg and not oldimg:
22+
raise TypeError('DynamoDB streams schema failed validation, missing both new & old stream images')
23+
return values
24+
25+
26+
class DynamoRecordSchema(BaseModel):
27+
eventID: str
28+
eventName: Literal['INSERT', 'MODIFY', 'REMOVE']
29+
eventVersion: float
30+
eventSource: Literal['aws:dynamodb']
31+
awsRegion: str
32+
eventSourceARN: str
33+
dynamodb: DynamoScheme
34+
35+
36+
class DynamoStreamSchema(BaseModel):
37+
Records: List[DynamoRecordSchema]
38+
39+
40+
class EventBridgeSchema(BaseModel):
41+
version: str
42+
id: str # noqa: A003,VNE003
43+
source: str
44+
account: int
45+
time: datetime
46+
region: str
47+
resources: List[str]
48+
detail: Dict[str, Any]
49+
50+
51+
class SqsSchema(BaseModel):
52+
todo : str
53+
54+
55+
class SnsSchema(BaseModel):
56+
todo: str

Diff for: aws_lambda_powertools/validation/validator.py

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import logging
2+
from abc import ABC, abstractmethod
3+
from typing import Any, Callable, Dict
4+
5+
from pydantic import BaseModel, ValidationError
6+
7+
from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
8+
from aws_lambda_powertools.validation.schemas import DynamoStreamSchema, EventBridgeSchema
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class Envelope(ABC):
14+
15+
def _parse_user_dict_schema(self, user_event : Dict[str, Any], inbound_schema_model : BaseModel) -> Any:
16+
logger.debug('parsing user dictionary schema')
17+
try:
18+
return inbound_schema_model(**user_event)
19+
except (ValidationError, TypeError):
20+
logger.exception("Valdation exception while extracting user custom schema")
21+
raise
22+
23+
def _parse_user_json_string_schema(self, user_event : str, inbound_schema_model : BaseModel) -> Any:
24+
logger.debug('parsing user dictionary schema')
25+
try:
26+
return inbound_schema_model.parse_raw(user_event)
27+
except (ValidationError, TypeError):
28+
logger.exception("Valdation exception while extracting user custom schema")
29+
raise
30+
31+
@abstractmethod
32+
def parse(event : Dict[str, Any], inbound_schema_model : BaseModel) -> Any:
33+
return NotImplemented
34+
35+
36+
class UserEnvelope(Envelope):
37+
def parse(self, event : Dict[str, Any], inbound_schema_model : BaseModel) -> Any:
38+
try:
39+
return inbound_schema_model(**event)
40+
except (ValidationError, TypeError):
41+
logger.exception("Valdation exception received from input user custom envelope event")
42+
raise
43+
44+
45+
class EventBridgeEnvelope(Envelope):
46+
47+
def parse(self, event : Dict[str, Any], inbound_schema_model : BaseModel) -> Any:
48+
try:
49+
parsed_envelope = EventBridgeSchema(**event)
50+
except (ValidationError, TypeError):
51+
logger.exception("Valdation exception received from input eventbridge event")
52+
raise
53+
return self._parse_user_dict_schema(parsed_envelope.detail, inbound_schema_model)
54+
55+
56+
class DynamoEnvelope(Envelope):
57+
58+
def parse(self, event : Dict[str, Any], inbound_schema_model : BaseModel) -> Any:
59+
try:
60+
parsed_envelope = DynamoStreamSchema(**event)
61+
except (ValidationError, TypeError):
62+
logger.exception("Valdation exception received from input dynamodb stream event")
63+
raise
64+
output = []
65+
for record in parsed_envelope.Records:
66+
parsed_new_image = {} if not record.dynamodb.NewImage else self._parse_user_dict_schema(record.dynamodb.NewImage, inbound_schema_model) # noqa: E501
67+
parsed_old_image = {} if not record.dynamodb.OldImage else self._parse_user_dict_schema(record.dynamodb.OldImage, inbound_schema_model) # noqa: E501
68+
output.append({'new': parsed_new_image, 'old': parsed_old_image})
69+
return output
70+
71+
72+
@lambda_handler_decorator
73+
def validator(handler : Callable[[Dict, Any], Any], event : Dict[str, Any], context : Dict[str, Any],
74+
inbound_schema_model : BaseModel, outbound_schema_model : BaseModel,
75+
envelope : Envelope) -> Any:
76+
"""Decorator to create validation for lambda handlers events - both inbound and outbound
77+
78+
As Lambda follows (event, context) signature we can remove some of the boilerplate
79+
and also capture any exception any Lambda function throws or its response as metadata
80+
81+
Example
82+
-------
83+
**Lambda function using validation decorator**
84+
85+
@validator(inbound=inbound_schema_model, outbound=outbound_schema_model)
86+
def handler(parsed_event_model, context):
87+
...
88+
89+
Parameters
90+
----------
91+
todo add
92+
93+
Raises
94+
------
95+
err
96+
TypeError or pydantic.ValidationError or any exception raised by the lambda handler itself
97+
"""
98+
lambda_handler_name = handler.__name__
99+
logger.debug("Validating inbound schema")
100+
parsed_event_model = envelope.parse(event, inbound_schema_model)
101+
try:
102+
logger.debug(f"Calling handler {lambda_handler_name}")
103+
response = handler({'orig': event, 'custom': parsed_event_model}, context)
104+
logger.debug("Received lambda handler response successfully")
105+
logger.debug(response)
106+
except Exception:
107+
logger.exception(f"Exception received from {lambda_handler_name}")
108+
raise
109+
110+
try:
111+
logger.debug("Validating outbound response schema")
112+
outbound_schema_model(**response)
113+
except (ValidationError, TypeError):
114+
logger.exception(f"Validation exception received from {lambda_handler_name} response event")
115+
raise
116+
return response

0 commit comments

Comments
 (0)