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

docs: new parser utility #192

Merged
merged 29 commits into from
Oct 25, 2020
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4d9b2a9
docs: initial sketch of parser docs
heitorlessa Oct 14, 2020
a6477cf
Merge branch 'develop' into docs/parser
heitorlessa Oct 14, 2020
190703b
docs: initial structure for parser docs
heitorlessa Oct 14, 2020
f0a8a18
docs: add 101 parsing events content
heitorlessa Oct 20, 2020
bf294ee
fix: parse high level import
heitorlessa Oct 20, 2020
96fc444
chore: typo on code generation tool
heitorlessa Oct 20, 2020
ba7cd29
docs: use non-hello world model to better exemplify parsing
heitorlessa Oct 20, 2020
cb8d6a0
fix: ensures parser can take json strings as input
heitorlessa Oct 20, 2020
0ed746b
docs: add data model validation section
heitorlessa Oct 20, 2020
75dc529
docs: add envelope section
heitorlessa Oct 22, 2020
e1eac2d
chore: typo in list
heitorlessa Oct 23, 2020
4b6ecbf
docs: add extending built-in models
heitorlessa Oct 23, 2020
d434f48
docs: ensure examples can be copied/pasted as-is
heitorlessa Oct 23, 2020
ab1f0d2
Merge branch 'docs/parser' of https://github.com/heitorlessa/aws-lamb…
heitorlessa Oct 23, 2020
4e738e9
improv: export Pydantic ValidationError instead of our own
heitorlessa Oct 23, 2020
6c359d5
fix: remove malformed 3.1. sentence
heitorlessa Oct 23, 2020
9b9f4f1
fix: debug logging in envelopes before each parsing
heitorlessa Oct 23, 2020
2732760
chore: spacing
heitorlessa Oct 23, 2020
5f1ad0a
fix: generic type to match ABC bound class
heitorlessa Oct 23, 2020
d2148f2
docs: add a FAQ section
heitorlessa Oct 23, 2020
234afd9
docs: add cold start data
heitorlessa Oct 23, 2020
9e42863
Merge branch 'develop' into docs/parser
heitorlessa Oct 23, 2020
3750d55
improv: address Koudai's PR feedback
heitorlessa Oct 24, 2020
ffbde1d
fix: _parse return type
heitorlessa Oct 24, 2020
33fec71
improv: address Koudai's PR feedback on mypy
heitorlessa Oct 25, 2020
de53605
docs: reorder extending models as parse fn wasn't introduced
heitorlessa Oct 25, 2020
57681a2
docs: reorder data validation; improve envelopes section
heitorlessa Oct 25, 2020
eeabc0f
docs: address Ran's feedback
heitorlessa Oct 25, 2020
592cd56
docs: add a note that decorator will replace the event
heitorlessa Oct 25, 2020
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
9 changes: 5 additions & 4 deletions aws_lambda_powertools/utilities/parser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
"""
from . import envelopes
from .envelopes import BaseEnvelope
from .exceptions import ModelValidationError
from .parser import event_parser
from .pydantic import BaseModel, root_validator, validator
from .parser import event_parser, parse
from .pydantic import BaseModel, Field, ValidationError, root_validator, validator

__all__ = [
"event_parser",
"parse",
"envelopes",
"BaseEnvelope",
"BaseModel",
"Field",
"validator",
"root_validator",
"ModelValidationError",
"ValidationError",
]
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .base import BaseEnvelope
from .dynamodb import DynamoDBEnvelope
from .dynamodb import DynamoDBStreamEnvelope
from .event_bridge import EventBridgeEnvelope
from .sqs import SqsEnvelope

__all__ = ["DynamoDBEnvelope", "EventBridgeEnvelope", "SqsEnvelope", "BaseEnvelope"]
__all__ = ["DynamoDBStreamEnvelope", "EventBridgeEnvelope", "SqsEnvelope", "BaseEnvelope"]
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
logger = logging.getLogger(__name__)


class DynamoDBEnvelope(BaseEnvelope):
class DynamoDBStreamEnvelope(BaseEnvelope):
""" DynamoDB Stream Envelope to extract data within NewImage/OldImage

Note: Values are the parsed models. Images' values can also be None, and
Expand All @@ -32,8 +32,10 @@ def parse(self, data: Dict[str, Any], model: BaseModel) -> List[Dict[Literal["Ne
List
List of records parsed with model provided
"""
logger.debug(f"Parsing incoming data with DynamoDB Stream model {DynamoDBStreamModel}")
parsed_envelope = DynamoDBStreamModel(**data)
heitorlessa marked this conversation as resolved.
Show resolved Hide resolved
output = []
logger.debug(f"Parsing DynamoDB Stream new and old records with {model}")
for record in parsed_envelope.Records:
output.append(
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,7 @@ def parse(self, data: Dict[str, Any], model: BaseModel) -> BaseModel:
Any
Parsed detail payload with model provided
"""
logger.debug(f"Parsing incoming data with EventBridge model {EventBridgeModel}")
parsed_envelope = EventBridgeModel(**data)
logger.debug(f"Parsing event payload in `detail` with {model}")
return self._parse(data=parsed_envelope.detail, model=model)
4 changes: 3 additions & 1 deletion aws_lambda_powertools/utilities/parser/envelopes/sqs.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class SqsEnvelope(BaseEnvelope):
all items in the list will be parsed as str and npt as JSON (and vice versa)
"""

def parse(self, data: Dict[str, Any], model: Union[BaseModel, str]) -> List[Union[BaseModel, str]]:
def parse(self, data: Dict[str, Any], model: Union[BaseModel, str]) -> List[BaseModel]:
heitorlessa marked this conversation as resolved.
Show resolved Hide resolved
"""Parses records found with model provided

Parameters
Expand All @@ -34,8 +34,10 @@ def parse(self, data: Dict[str, Any], model: Union[BaseModel, str]) -> List[Unio
List
List of records parsed with model provided
"""
logger.debug(f"Parsing incoming data with SQS model {SqsModel}")
parsed_envelope = SqsModel(**data)
output = []
logger.debug(f"Parsing SQS records in `body` with {model}")
for record in parsed_envelope.Records:
output.append(self._parse(record.body, model))
heitorlessa marked this conversation as resolved.
Show resolved Hide resolved
return output
4 changes: 0 additions & 4 deletions aws_lambda_powertools/utilities/parser/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,5 @@ class InvalidEnvelopeError(Exception):
"""Input envelope is not callable and instance of BaseEnvelope"""


class ModelValidationError(Exception):
"""Input data does not conform with model"""


class InvalidModelTypeError(Exception):
"""Input data model does not implement BaseModel"""
33 changes: 17 additions & 16 deletions aws_lambda_powertools/utilities/parser/parser.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import logging
from typing import Any, Callable, Dict, Optional
from typing import Any, Callable, Dict, Optional, Type, TypeVar

from pydantic import BaseModel, ValidationError
from pydantic import BaseModel

from ...middleware_factory import lambda_handler_decorator
from ..typing import LambdaContext
from .envelopes.base import BaseEnvelope
from .exceptions import InvalidEnvelopeError, InvalidModelTypeError, ModelValidationError
from .exceptions import InvalidEnvelopeError, InvalidModelTypeError

Model = TypeVar("Model", bound=BaseModel)
Envelope = TypeVar("Envelope", bound=BaseEnvelope)
logger = logging.getLogger(__name__)


Expand All @@ -16,8 +18,8 @@ def event_parser(
handler: Callable[[Dict, Any], Any],
event: Dict[str, Any],
context: LambdaContext,
model: BaseModel,
envelope: Optional[BaseEnvelope] = None,
model: Type[Model],
envelope: Optional[Type[Envelope]] = None,
) -> Any:
"""Lambda handler decorator to parse & validate events using Pydantic models

Expand Down Expand Up @@ -72,7 +74,7 @@ def handler(event: Order, context: LambdaContext):

Raises
------
ModelValidationError
ValidationError
When input event does not conform with model provided
InvalidModelTypeError
When model given does not implement BaseModel
Expand All @@ -84,7 +86,7 @@ def handler(event: Order, context: LambdaContext):
return handler(parsed_event, context)


def parse(event: Dict[str, Any], model: BaseModel, envelope: Optional[BaseEnvelope] = None) -> Any:
def parse(event: Dict[str, Any], model: Type[Model], envelope: Optional[Type[Envelope]] = None) -> Any:
heitorlessa marked this conversation as resolved.
Show resolved Hide resolved
"""Standalone function to parse & validate events using Pydantic models

Typically used when you need fine-grained control over error handling compared to event_parser decorator.
Expand All @@ -94,7 +96,7 @@ def parse(event: Dict[str, Any], model: BaseModel, envelope: Optional[BaseEnvelo

**Lambda handler decorator to parse & validate event**

from aws_lambda_powertools.utilities.parser.exceptions import ModelValidationError
from aws_lambda_powertools.utilities.parser import ValidationError

class Order(BaseModel):
id: int
Expand All @@ -104,7 +106,7 @@ class Order(BaseModel):
def handler(event: Order, context: LambdaContext):
try:
parse(model=Order)
except ModelValidationError:
except ValidationError:
...

**Lambda handler decorator to parse & validate event - using built-in envelope**
Expand All @@ -117,7 +119,7 @@ class Order(BaseModel):
def handler(event: Order, context: LambdaContext):
try:
parse(model=Order, envelope=envelopes.EVENTBRIDGE)
except ModelValidationError:
except ValidationError:
...

Parameters
Expand All @@ -131,7 +133,7 @@ def handler(event: Order, context: LambdaContext):

Raises
------
ModelValidationError
ValidationError
When input event does not conform with model provided
InvalidModelTypeError
When model given does not implement BaseModel
Expand All @@ -144,13 +146,12 @@ def handler(event: Order, context: LambdaContext):
return envelope().parse(data=event, model=model)
except AttributeError:
raise InvalidEnvelopeError(f"Envelope must implement BaseEnvelope, envelope={envelope}")
except (ValidationError, TypeError) as e:
raise ModelValidationError(f"Input event does not conform with model, envelope={envelope}") from e

try:
logger.debug("Parsing and validating event model; no envelope used")
if isinstance(event, str):
return model.parse_raw(event)

return model.parse_obj(event)
except (ValidationError, TypeError) as e:
raise ModelValidationError("Input event does not conform with model") from e
except AttributeError:
raise InvalidModelTypeError("Input model must implement BaseModel")
raise InvalidModelTypeError(f"Input model must implement BaseModel, model={model}")
Loading