Skip to content

Commit b25b2aa

Browse files
authored
Merge pull request #192 from heitorlessa/docs/parser
docs: new parser utility
2 parents e545820 + 592cd56 commit b25b2aa

File tree

16 files changed

+670
-82
lines changed

16 files changed

+670
-82
lines changed

aws_lambda_powertools/utilities/parser/__init__.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@
22
"""
33
from . import envelopes
44
from .envelopes import BaseEnvelope
5-
from .exceptions import ModelValidationError
6-
from .parser import event_parser
7-
from .pydantic import BaseModel, root_validator, validator
5+
from .parser import event_parser, parse
6+
from .pydantic import BaseModel, Field, ValidationError, root_validator, validator
87

98
__all__ = [
109
"event_parser",
10+
"parse",
1111
"envelopes",
1212
"BaseEnvelope",
1313
"BaseModel",
14+
"Field",
1415
"validator",
1516
"root_validator",
16-
"ModelValidationError",
17+
"ValidationError",
1718
]
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from .base import BaseEnvelope
2-
from .dynamodb import DynamoDBEnvelope
2+
from .dynamodb import DynamoDBStreamEnvelope
33
from .event_bridge import EventBridgeEnvelope
44
from .sqs import SqsEnvelope
55

6-
__all__ = ["DynamoDBEnvelope", "EventBridgeEnvelope", "SqsEnvelope", "BaseEnvelope"]
6+
__all__ = ["DynamoDBStreamEnvelope", "EventBridgeEnvelope", "SqsEnvelope", "BaseEnvelope"]

aws_lambda_powertools/utilities/parser/envelopes/base.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import logging
22
from abc import ABC, abstractmethod
3-
from typing import Any, Dict, Union
3+
from typing import Any, Dict, Optional, TypeVar, Union
44

5-
from pydantic import BaseModel
5+
from ..types import Model
66

77
logger = logging.getLogger(__name__)
88

@@ -11,14 +11,14 @@ class BaseEnvelope(ABC):
1111
"""ABC implementation for creating a supported Envelope"""
1212

1313
@staticmethod
14-
def _parse(data: Union[Dict[str, Any], str], model: BaseModel) -> Any:
14+
def _parse(data: Optional[Union[Dict[str, Any], Any]], model: Model) -> Union[Model, None]:
1515
"""Parses envelope data against model provided
1616
1717
Parameters
1818
----------
1919
data : Dict
2020
Data to be parsed and validated
21-
model
21+
model : Model
2222
Data model to parse and validate data against
2323
2424
Returns
@@ -38,7 +38,7 @@ def _parse(data: Union[Dict[str, Any], str], model: BaseModel) -> Any:
3838
return model.parse_obj(data)
3939

4040
@abstractmethod
41-
def parse(self, data: Dict[str, Any], model: BaseModel):
41+
def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Model):
4242
"""Implementation to parse data against envelope model, then against the data model
4343
4444
NOTE: Call `_parse` method to fully parse data with model provided.
@@ -56,3 +56,8 @@ def parse(...):
5656
return self._parse(data=parsed_envelope.detail, model=data_model)
5757
"""
5858
return NotImplemented # pragma: no cover
59+
60+
61+
# Generic to support type annotations throughout parser
62+
# Note: Can't be defined under types.py due to circular dependency
63+
Envelope = TypeVar("Envelope", bound=BaseEnvelope)
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,44 @@
11
import logging
2-
from typing import Any, Dict, List
3-
4-
from pydantic import BaseModel
5-
from typing_extensions import Literal
2+
from typing import Any, Dict, List, Optional, Union
63

74
from ..models import DynamoDBStreamModel
5+
from ..types import Model
86
from .base import BaseEnvelope
97

108
logger = logging.getLogger(__name__)
119

1210

13-
class DynamoDBEnvelope(BaseEnvelope):
11+
class DynamoDBStreamEnvelope(BaseEnvelope):
1412
""" DynamoDB Stream Envelope to extract data within NewImage/OldImage
1513
1614
Note: Values are the parsed models. Images' values can also be None, and
1715
length of the list is the record's amount in the original event.
1816
"""
1917

20-
def parse(self, data: Dict[str, Any], model: BaseModel) -> List[Dict[Literal["NewImage", "OldImage"], BaseModel]]:
18+
def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Model) -> List[Dict[str, Optional[Model]]]:
2119
"""Parses DynamoDB Stream records found in either NewImage and OldImage with model provided
2220
2321
Parameters
2422
----------
2523
data : Dict
2624
Lambda event to be parsed
27-
model : BaseModel
25+
model : Model
2826
Data model provided to parse after extracting data using envelope
2927
3028
Returns
3129
-------
3230
List
33-
List of records parsed with model provided
31+
List of dictionaries with NewImage and OldImage records parsed with model provided
3432
"""
35-
parsed_envelope = DynamoDBStreamModel(**data)
33+
logger.debug(f"Parsing incoming data with DynamoDB Stream model {DynamoDBStreamModel}")
34+
parsed_envelope = DynamoDBStreamModel.parse_obj(data)
3635
output = []
36+
logger.debug(f"Parsing DynamoDB Stream new and old records with {model}")
3737
for record in parsed_envelope.Records:
3838
output.append(
3939
{
40-
"NewImage": self._parse(record.dynamodb.NewImage, model),
41-
"OldImage": self._parse(record.dynamodb.OldImage, model),
40+
"NewImage": self._parse(data=record.dynamodb.NewImage, model=model),
41+
"OldImage": self._parse(data=record.dynamodb.OldImage, model=model),
4242
}
4343
)
44-
# noinspection PyTypeChecker
4544
return output
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import logging
2-
from typing import Any, Dict
3-
4-
from pydantic import BaseModel
2+
from typing import Any, Dict, Optional, Union
53

64
from ..models import EventBridgeModel
5+
from ..types import Model
76
from .base import BaseEnvelope
87

98
logger = logging.getLogger(__name__)
@@ -12,20 +11,22 @@
1211
class EventBridgeEnvelope(BaseEnvelope):
1312
"""EventBridge envelope to extract data within detail key"""
1413

15-
def parse(self, data: Dict[str, Any], model: BaseModel) -> BaseModel:
14+
def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Model) -> Optional[Model]:
1615
"""Parses data found with model provided
1716
1817
Parameters
1918
----------
2019
data : Dict
2120
Lambda event to be parsed
22-
model : BaseModel
21+
model : Model
2322
Data model provided to parse after extracting data using envelope
2423
2524
Returns
2625
-------
2726
Any
2827
Parsed detail payload with model provided
2928
"""
30-
parsed_envelope = EventBridgeModel(**data)
29+
logger.debug(f"Parsing incoming data with EventBridge model {EventBridgeModel}")
30+
parsed_envelope = EventBridgeModel.parse_obj(data)
31+
logger.debug(f"Parsing event payload in `detail` with {model}")
3132
return self._parse(data=parsed_envelope.detail, model=model)

aws_lambda_powertools/utilities/parser/envelopes/sqs.py

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import logging
2-
from typing import Any, Dict, List, Union
3-
4-
from pydantic import BaseModel
2+
from typing import Any, Dict, List, Optional, Union
53

64
from ..models import SqsModel
5+
from ..types import Model
76
from .base import BaseEnvelope
87

98
logger = logging.getLogger(__name__)
@@ -19,23 +18,25 @@ class SqsEnvelope(BaseEnvelope):
1918
all items in the list will be parsed as str and npt as JSON (and vice versa)
2019
"""
2120

22-
def parse(self, data: Dict[str, Any], model: Union[BaseModel, str]) -> List[Union[BaseModel, str]]:
21+
def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Model) -> List[Optional[Model]]:
2322
"""Parses records found with model provided
2423
2524
Parameters
2625
----------
2726
data : Dict
2827
Lambda event to be parsed
29-
model : BaseModel
28+
model : Model
3029
Data model provided to parse after extracting data using envelope
3130
3231
Returns
3332
-------
3433
List
3534
List of records parsed with model provided
3635
"""
37-
parsed_envelope = SqsModel(**data)
36+
logger.debug(f"Parsing incoming data with SQS model {SqsModel}")
37+
parsed_envelope = SqsModel.parse_obj(data)
3838
output = []
39+
logger.debug(f"Parsing SQS records in `body` with {model}")
3940
for record in parsed_envelope.Records:
40-
output.append(self._parse(record.body, model))
41+
output.append(self._parse(data=record.body, model=model))
4142
return output

aws_lambda_powertools/utilities/parser/exceptions.py

-4
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,5 @@ class InvalidEnvelopeError(Exception):
22
"""Input envelope is not callable and instance of BaseEnvelope"""
33

44

5-
class ModelValidationError(Exception):
6-
"""Input data does not conform with model"""
7-
8-
95
class InvalidModelTypeError(Exception):
106
"""Input data model does not implement BaseModel"""

aws_lambda_powertools/utilities/parser/parser.py

+20-22
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
11
import logging
22
from typing import Any, Callable, Dict, Optional
33

4-
from pydantic import BaseModel, ValidationError
5-
64
from ...middleware_factory import lambda_handler_decorator
75
from ..typing import LambdaContext
8-
from .envelopes.base import BaseEnvelope
9-
from .exceptions import InvalidEnvelopeError, InvalidModelTypeError, ModelValidationError
6+
from .envelopes.base import Envelope
7+
from .exceptions import InvalidEnvelopeError, InvalidModelTypeError
8+
from .types import Model
109

1110
logger = logging.getLogger(__name__)
1211

1312

1413
@lambda_handler_decorator
1514
def event_parser(
16-
handler: Callable[[Dict, Any], Any],
15+
handler: Callable[[Any, LambdaContext], Any],
1716
event: Dict[str, Any],
1817
context: LambdaContext,
19-
model: BaseModel,
20-
envelope: Optional[BaseEnvelope] = None,
18+
model: Model,
19+
envelope: Optional[Envelope] = None,
2120
) -> Any:
2221
"""Lambda handler decorator to parse & validate events using Pydantic models
2322
@@ -65,14 +64,14 @@ def handler(event: Order, context: LambdaContext):
6564
Lambda event to be parsed & validated
6665
context: LambdaContext
6766
Lambda context object
68-
model: BaseModel
67+
model: Model
6968
Your data model that will replace the event.
70-
envelope: BaseEnvelope
69+
envelope: Envelope
7170
Optional envelope to extract the model from
7271
7372
Raises
7473
------
75-
ModelValidationError
74+
ValidationError
7675
When input event does not conform with model provided
7776
InvalidModelTypeError
7877
When model given does not implement BaseModel
@@ -84,7 +83,7 @@ def handler(event: Order, context: LambdaContext):
8483
return handler(parsed_event, context)
8584

8685

87-
def parse(event: Dict[str, Any], model: BaseModel, envelope: Optional[BaseEnvelope] = None) -> Any:
86+
def parse(event: Dict[str, Any], model: Model, envelope: Optional[Envelope] = None) -> Model:
8887
"""Standalone function to parse & validate events using Pydantic models
8988
9089
Typically used when you need fine-grained control over error handling compared to event_parser decorator.
@@ -94,7 +93,7 @@ def parse(event: Dict[str, Any], model: BaseModel, envelope: Optional[BaseEnvelo
9493
9594
**Lambda handler decorator to parse & validate event**
9695
97-
from aws_lambda_powertools.utilities.parser.exceptions import ModelValidationError
96+
from aws_lambda_powertools.utilities.parser import ValidationError
9897
9998
class Order(BaseModel):
10099
id: int
@@ -104,7 +103,7 @@ class Order(BaseModel):
104103
def handler(event: Order, context: LambdaContext):
105104
try:
106105
parse(model=Order)
107-
except ModelValidationError:
106+
except ValidationError:
108107
...
109108
110109
**Lambda handler decorator to parse & validate event - using built-in envelope**
@@ -117,21 +116,21 @@ class Order(BaseModel):
117116
def handler(event: Order, context: LambdaContext):
118117
try:
119118
parse(model=Order, envelope=envelopes.EVENTBRIDGE)
120-
except ModelValidationError:
119+
except ValidationError:
121120
...
122121
123122
Parameters
124123
----------
125124
event: Dict
126125
Lambda event to be parsed & validated
127-
model: BaseModel
126+
model: Model
128127
Your data model that will replace the event
129-
envelope: BaseEnvelope
128+
envelope: Envelope
130129
Optional envelope to extract the model from
131130
132131
Raises
133132
------
134-
ModelValidationError
133+
ValidationError
135134
When input event does not conform with model provided
136135
InvalidModelTypeError
137136
When model given does not implement BaseModel
@@ -144,13 +143,12 @@ def handler(event: Order, context: LambdaContext):
144143
return envelope().parse(data=event, model=model)
145144
except AttributeError:
146145
raise InvalidEnvelopeError(f"Envelope must implement BaseEnvelope, envelope={envelope}")
147-
except (ValidationError, TypeError) as e:
148-
raise ModelValidationError(f"Input event does not conform with model, envelope={envelope}") from e
149146

150147
try:
151148
logger.debug("Parsing and validating event model; no envelope used")
149+
if isinstance(event, str):
150+
return model.parse_raw(event)
151+
152152
return model.parse_obj(event)
153-
except (ValidationError, TypeError) as e:
154-
raise ModelValidationError("Input event does not conform with model") from e
155153
except AttributeError:
156-
raise InvalidModelTypeError("Input model must implement BaseModel")
154+
raise InvalidModelTypeError(f"Input model must implement BaseModel, model={model}")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Generics and other shared types used across parser"""
2+
from typing import TypeVar
3+
4+
from pydantic import BaseModel
5+
6+
Model = TypeVar("Model", bound=BaseModel)

0 commit comments

Comments
 (0)