Skip to content

Commit 47cd711

Browse files
author
Ran Isenberg
committed
feat: Advanced parser utility (pydantic)
1 parent 3418767 commit 47cd711

24 files changed

+384
-486
lines changed
Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
1-
"""Validation utility
1+
"""Advanced parser utility
22
"""
3-
from .envelopes import DynamoDBEnvelope, EventBridgeEnvelope, SnsEnvelope, SqsEnvelope, UserEnvelope
4-
from .validator import validate, validator
3+
from .envelopes import Envelope, InvalidEnvelopeError, parse_envelope
4+
from .parser import parser
55

6-
__all__ = [
7-
"UserEnvelope",
8-
"DynamoDBEnvelope",
9-
"EventBridgeEnvelope",
10-
"SnsEnvelope",
11-
"SqsEnvelope",
12-
"validate",
13-
"validator",
14-
]
6+
__all__ = ["InvalidEnvelopeError", "Envelope", "parse_envelope", "parser"]
Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
from .base import UserEnvelope
2-
from .dynamodb import DynamoDBEnvelope
3-
from .event_bridge import EventBridgeEnvelope
4-
from .sns import SnsEnvelope
5-
from .sqs import SqsEnvelope
1+
from .envelopes import Envelope, InvalidEnvelopeError, parse_envelope
62

7-
__all__ = ["UserEnvelope", "DynamoDBEnvelope", "EventBridgeEnvelope", "SqsEnvelope", "SnsEnvelope"]
3+
__all__ = ["InvalidEnvelopeError", "Envelope", "parse_envelope"]

aws_lambda_powertools/utilities/advanced_parser/envelopes/base.py

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,35 +8,26 @@
88

99

1010
class BaseEnvelope(ABC):
11-
def _parse_user_dict_schema(self, user_event: Dict[str, Any], inbound_schema_model: BaseModel) -> Any:
11+
def _parse_user_dict_schema(self, user_event: Dict[str, Any], schema: BaseModel) -> Any:
1212
logger.debug("parsing user dictionary schema")
1313
try:
14-
return inbound_schema_model(**user_event)
14+
return schema(**user_event)
1515
except (ValidationError, TypeError):
1616
logger.exception("Validation exception while extracting user custom schema")
1717
raise
1818

19-
def _parse_user_json_string_schema(self, user_event: str, inbound_schema_model: BaseModel) -> Any:
19+
def _parse_user_json_string_schema(self, user_event: str, schema: BaseModel) -> Any:
2020
logger.debug("parsing user dictionary schema")
21-
if inbound_schema_model == str:
21+
if schema == str:
2222
logger.debug("input is string, returning")
2323
return user_event
2424
logger.debug("trying to parse as json encoded string")
2525
try:
26-
return inbound_schema_model.parse_raw(user_event)
26+
return schema.parse_raw(user_event)
2727
except (ValidationError, TypeError):
2828
logger.exception("Validation exception while extracting user custom schema")
2929
raise
3030

3131
@abstractmethod
32-
def parse(self, event: Dict[str, Any], inbound_schema_model: BaseModel) -> Any:
32+
def parse(self, event: Dict[str, Any], schema: BaseModel):
3333
return NotImplemented
34-
35-
36-
class UserEnvelope(BaseEnvelope):
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("Validation exception received from input user custom envelopes event")
42-
raise

aws_lambda_powertools/utilities/advanced_parser/envelopes/dynamodb.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111

1212
class DynamoDBEnvelope(BaseEnvelope):
13-
def parse(self, event: Dict[str, Any], inbound_schema_model: BaseModel) -> Any:
13+
def parse(self, event: Dict[str, Any], schema: BaseModel) -> Any:
1414
try:
1515
parsed_envelope = DynamoDBSchema(**event)
1616
except (ValidationError, TypeError):
@@ -19,14 +19,10 @@ def parse(self, event: Dict[str, Any], inbound_schema_model: BaseModel) -> Any:
1919
output = []
2020
for record in parsed_envelope.Records:
2121
parsed_new_image = (
22-
{}
23-
if not record.dynamodb.NewImage
24-
else self._parse_user_dict_schema(record.dynamodb.NewImage, inbound_schema_model)
22+
None if not record.dynamodb.NewImage else self._parse_user_dict_schema(record.dynamodb.NewImage, schema)
2523
) # noqa: E501
2624
parsed_old_image = (
27-
{}
28-
if not record.dynamodb.OldImage
29-
else self._parse_user_dict_schema(record.dynamodb.OldImage, inbound_schema_model)
25+
None if not record.dynamodb.OldImage else self._parse_user_dict_schema(record.dynamodb.OldImage, schema)
3026
) # noqa: E501
31-
output.append({"new": parsed_new_image, "old": parsed_old_image})
27+
output.append({"NewImage": parsed_new_image, "OldImage": parsed_old_image})
3228
return output
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import logging
2+
from enum import Enum
3+
from typing import Any, Dict
4+
5+
from pydantic import BaseModel
6+
7+
from aws_lambda_powertools.utilities.advanced_parser.envelopes.base import BaseEnvelope
8+
from aws_lambda_powertools.utilities.advanced_parser.envelopes.dynamodb import DynamoDBEnvelope
9+
from aws_lambda_powertools.utilities.advanced_parser.envelopes.event_bridge import EventBridgeEnvelope
10+
from aws_lambda_powertools.utilities.advanced_parser.envelopes.sqs import SqsEnvelope
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
"""Built-in envelopes"""
16+
17+
18+
class Envelope(str, Enum):
19+
SQS = "sqs"
20+
EVENTBRIDGE = "eventbridge"
21+
DYNAMODB_STREAM = "dynamodb_stream"
22+
23+
24+
class InvalidEnvelopeError(Exception):
25+
"""Input envelope is not one of the Envelope enum values"""
26+
27+
28+
# enum to BaseEnvelope handler class
29+
__ENVELOPE_MAPPING = {
30+
Envelope.SQS: SqsEnvelope,
31+
Envelope.DYNAMODB_STREAM: DynamoDBEnvelope,
32+
Envelope.EVENTBRIDGE: EventBridgeEnvelope,
33+
}
34+
35+
36+
def parse_envelope(event: Dict[str, Any], envelope: Envelope, schema: BaseModel):
37+
envelope_handler: BaseEnvelope = __ENVELOPE_MAPPING.get(envelope)
38+
if envelope_handler is None:
39+
logger.exception("envelope must be an instance of Envelope enum")
40+
raise InvalidEnvelopeError("envelope must be an instance of Envelope enum")
41+
logger.debug(f"Parsing and validating event schema, envelope={str(envelope.value)}")
42+
return envelope_handler().parse(event=event, schema=schema)

aws_lambda_powertools/utilities/advanced_parser/envelopes/event_bridge.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010

1111

1212
class EventBridgeEnvelope(BaseEnvelope):
13-
def parse(self, event: Dict[str, Any], inbound_schema_model: BaseModel) -> Any:
13+
def parse(self, event: Dict[str, Any], schema: BaseModel) -> Any:
1414
try:
1515
parsed_envelope = EventBridgeSchema(**event)
1616
except (ValidationError, TypeError):
1717
logger.exception("Validation exception received from input eventbridge event")
1818
raise
19-
return self._parse_user_dict_schema(parsed_envelope.detail, inbound_schema_model)
19+
return self._parse_user_dict_schema(parsed_envelope.detail, schema)

aws_lambda_powertools/utilities/advanced_parser/envelopes/sns.py

Lines changed: 0 additions & 19 deletions
This file was deleted.
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import logging
2-
from typing import Any, Dict
2+
from typing import Any, Dict, List
33

44
from pydantic import BaseModel, ValidationError
55

@@ -10,14 +10,13 @@
1010

1111

1212
class SqsEnvelope(BaseEnvelope):
13-
def parse(self, event: Dict[str, Any], inbound_schema_model: BaseModel) -> Any:
13+
def parse(self, event: Dict[str, Any], schema: BaseModel) -> List[BaseModel]:
1414
try:
1515
parsed_envelope = SqsSchema(**event)
1616
except (ValidationError, TypeError):
1717
logger.exception("Validation exception received from input sqs event")
1818
raise
1919
output = []
2020
for record in parsed_envelope.Records:
21-
parsed_msg = self._parse_user_json_string_schema(record.body, inbound_schema_model)
22-
output.append({"body": parsed_msg, "attributes": record.messageAttributes})
21+
output.append(self._parse_user_json_string_schema(record.body, schema))
2322
return output
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import logging
2+
from typing import Any, Callable, Dict, Optional
3+
4+
from pydantic import BaseModel, ValidationError
5+
6+
from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
7+
from aws_lambda_powertools.utilities.advanced_parser.envelopes import Envelope, parse_envelope
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
@lambda_handler_decorator
13+
def parser(
14+
handler: Callable[[Dict, Any], Any],
15+
event: Dict[str, Any],
16+
context: Dict[str, Any],
17+
schema: BaseModel,
18+
envelope: Optional[Envelope] = None,
19+
) -> Any:
20+
"""Decorator to conduct advanced parsing & validation for lambda handlers events
21+
22+
As Lambda follows (event, context) signature we can remove some of the boilerplate
23+
and also capture any exception any Lambda function throws as metadata.
24+
Event will be the parsed & validated BaseModel pydantic object of the input type "schema"
25+
26+
Example
27+
-------
28+
**Lambda function using validation decorator**
29+
30+
@parser(schema=MyBusiness, envelope=envelopes.EVENTBRIDGE)
31+
def handler(event: inbound_schema_model , context: LambdaContext):
32+
...
33+
34+
Parameters
35+
----------
36+
todo add
37+
38+
Raises
39+
------
40+
err
41+
TypeError or pydantic.ValidationError or any exception raised by the lambda handler itself
42+
"""
43+
lambda_handler_name = handler.__name__
44+
parsed_event = None
45+
if envelope is None:
46+
try:
47+
logger.debug("Parsing and validating event schema, no envelope is used")
48+
parsed_event = schema(**event)
49+
except (ValidationError, TypeError):
50+
logger.exception("Validation exception received from input event")
51+
raise
52+
else:
53+
parsed_event = parse_envelope(event, envelope, schema)
54+
55+
logger.debug(f"Calling handler {lambda_handler_name}")
56+
handler(parsed_event, context)
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
from .dynamodb import DynamoDBSchema
1+
from .dynamodb import DynamoDBSchema, DynamoRecordSchema, DynamoScheme
22
from .event_bridge import EventBridgeSchema
3-
from .sns import SnsSchema
4-
from .sqs import SqsSchema
3+
from .sqs import SqsRecordSchema, SqsSchema
54

65
__all__ = [
76
"DynamoDBSchema",
87
"EventBridgeSchema",
9-
"SnsSchema",
8+
"DynamoScheme",
9+
"DynamoRecordSchema",
1010
"SqsSchema",
11+
"SqsRecordSchema",
1112
]

aws_lambda_powertools/utilities/advanced_parser/schemas/dynamodb.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66

77

88
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]] = {}
9+
ApproximateCreationDateTime: Optional[date]
10+
Keys: Dict[str, Dict[str, Any]]
11+
NewImage: Optional[Dict[str, Any]]
12+
OldImage: Optional[Dict[str, Any]]
1313
SequenceNumber: str
1414
SizeBytes: int
1515
StreamViewType: Literal["NEW_AND_OLD_IMAGES", "KEYS_ONLY", "NEW_IMAGE", "OLD_IMAGE"]
@@ -23,6 +23,11 @@ def check_one_image_exists(cls, values):
2323
return values
2424

2525

26+
class UserIdentity(BaseModel):
27+
type: Literal["Service"] # noqa: VNE003, A003
28+
principalId: Literal["dynamodb.amazonaws.com"]
29+
30+
2631
class DynamoRecordSchema(BaseModel):
2732
eventID: str
2833
eventName: Literal["INSERT", "MODIFY", "REMOVE"]
@@ -31,6 +36,7 @@ class DynamoRecordSchema(BaseModel):
3136
awsRegion: str
3237
eventSourceARN: str
3338
dynamodb: DynamoScheme
39+
userIdentity: Optional[UserIdentity]
3440

3541

3642
class DynamoDBSchema(BaseModel):
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
from datetime import datetime
22
from typing import Any, Dict, List
33

4-
from pydantic import BaseModel
4+
from pydantic import BaseModel, Field
55

66

77
class EventBridgeSchema(BaseModel):
88
version: str
99
id: str # noqa: A003,VNE003
1010
source: str
11-
account: int
11+
account: str
1212
time: datetime
1313
region: str
1414
resources: List[str]
15+
detailtype: str = Field(None, alias="detail-type")
1516
detail: Dict[str, Any]

aws_lambda_powertools/utilities/advanced_parser/schemas/sns.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

aws_lambda_powertools/utilities/advanced_parser/schemas/sqs.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class SqsAttributesSchema(BaseModel):
1414
SenderId: str
1515
SentTimestamp: datetime
1616
SequenceNumber: Optional[str]
17+
AWSTraceHeader: Optional[str]
1718

1819

1920
class SqsMsgAttributeSchema(BaseModel):
@@ -50,7 +51,7 @@ class SqsRecordSchema(BaseModel):
5051
attributes: SqsAttributesSchema
5152
messageAttributes: Dict[str, SqsMsgAttributeSchema]
5253
md5OfBody: str
53-
md5OfMessageAttributes: str
54+
md5OfMessageAttributes: Optional[str]
5455
eventSource: Literal["aws:sqs"]
5556
eventSourceARN: str
5657
awsRegion: str

0 commit comments

Comments
 (0)