From 7dc97b355b6c79b20706e62f0f323900a2574428 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Tue, 12 Apr 2022 18:59:10 -0700 Subject: [PATCH 1/2] fix(docs): Extract parser code examples Changes: - Extract code examples - Run isort and black - Fix python code examples - Update line highlights - Add make task Related to: - #1064 --- Makefile | 7 + .../utilities/parser/parser_envelope.py | 35 ++ .../parser/parser_event_bridge_envelope.py | 26 ++ .../parser/parser_event_bridge_model.py | 16 + .../parser/parser_event_parser_decorator.py | 44 +++ .../parser/parser_extending_builtin_models.py | 52 +++ .../utilities/parser/parser_model_export.py | 32 ++ .../utilities/parser/parser_models.py | 16 + .../utilities/parser/parser_parse_function.py | 39 +++ .../utilities/parser/parser_validator.py | 14 + .../utilities/parser/parser_validator_all.py | 16 + .../utilities/parser/parser_validator_root.py | 23 ++ docs/utilities/parser.md | 321 ++---------------- 13 files changed, 339 insertions(+), 302 deletions(-) create mode 100644 docs/examples/utilities/parser/parser_envelope.py create mode 100644 docs/examples/utilities/parser/parser_event_bridge_envelope.py create mode 100644 docs/examples/utilities/parser/parser_event_bridge_model.py create mode 100644 docs/examples/utilities/parser/parser_event_parser_decorator.py create mode 100644 docs/examples/utilities/parser/parser_extending_builtin_models.py create mode 100644 docs/examples/utilities/parser/parser_model_export.py create mode 100644 docs/examples/utilities/parser/parser_models.py create mode 100644 docs/examples/utilities/parser/parser_parse_function.py create mode 100644 docs/examples/utilities/parser/parser_validator.py create mode 100644 docs/examples/utilities/parser/parser_validator_all.py create mode 100644 docs/examples/utilities/parser/parser_validator_root.py diff --git a/Makefile b/Makefile index 73667eb5f58..a430f61b0c6 100644 --- a/Makefile +++ b/Makefile @@ -90,3 +90,10 @@ changelog: mypy: poetry run mypy --pretty aws_lambda_powertools + +format-examples: + poetry run isort docs/examples + poetry run black docs/examples/*/*/*.py + +lint-examples: + poetry run python3 -m py_compile docs/examples/*/*/*.py diff --git a/docs/examples/utilities/parser/parser_envelope.py b/docs/examples/utilities/parser/parser_envelope.py new file mode 100644 index 00000000000..529dc630523 --- /dev/null +++ b/docs/examples/utilities/parser/parser_envelope.py @@ -0,0 +1,35 @@ +from aws_lambda_powertools.utilities.parser import BaseModel, envelopes, event_parser, parse +from aws_lambda_powertools.utilities.typing import LambdaContext + + +class UserModel(BaseModel): + username: str + password1: str + password2: str + + +payload = { + "version": "0", + "id": "6a7e8feb-b491-4cf7-a9f1-bf3703467718", + "detail-type": "CustomerSignedUp", + "source": "CustomerService", + "account": "111122223333", + "time": "2020-10-22T18:43:48Z", + "region": "us-west-1", + "resources": ["some_additional_"], + "detail": { + "username": "universe", + "password1": "myp@ssword", + "password2": "repeat password", + }, +} + +ret = parse(model=UserModel, envelope=envelopes.EventBridgeEnvelope, event=payload) + +# Parsed model only contains our actual model, not the entire EventBridge + Payload parsed +assert ret.password1 == ret.password2 + +# Same behaviour but using our decorator +@event_parser(model=UserModel, envelope=envelopes.EventBridgeEnvelope) +def handler(event: UserModel, context: LambdaContext): + assert event.password1 == event.password2 diff --git a/docs/examples/utilities/parser/parser_event_bridge_envelope.py b/docs/examples/utilities/parser/parser_event_bridge_envelope.py new file mode 100644 index 00000000000..83a7067c2f5 --- /dev/null +++ b/docs/examples/utilities/parser/parser_event_bridge_envelope.py @@ -0,0 +1,26 @@ +from typing import Any, Dict, Optional, TypeVar, Union + +from aws_lambda_powertools.utilities.parser import BaseEnvelope, BaseModel +from aws_lambda_powertools.utilities.parser.models import EventBridgeModel + +Model = TypeVar("Model", bound=BaseModel) + + +class EventBridgeEnvelope(BaseEnvelope): + def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Model) -> Optional[Model]: + """Parses data found with model provided + + Parameters + ---------- + data : Dict + Lambda event to be parsed + model : Model + Data model provided to parse after extracting data using envelope + + Returns + ------- + Any + Parsed detail payload with model provided + """ + parsed_envelope = EventBridgeModel.parse_obj(data) + return self._parse(data=parsed_envelope.detail, model=model) diff --git a/docs/examples/utilities/parser/parser_event_bridge_model.py b/docs/examples/utilities/parser/parser_event_bridge_model.py new file mode 100644 index 00000000000..286d9f2dc57 --- /dev/null +++ b/docs/examples/utilities/parser/parser_event_bridge_model.py @@ -0,0 +1,16 @@ +from datetime import datetime +from typing import Any, Dict, List + +from aws_lambda_powertools.utilities.parser import BaseModel, Field + + +class EventBridgeModel(BaseModel): + version: str + id: str # noqa: A003,VNE003 + source: str + account: str + time: datetime + region: str + resources: List[str] + detail_type: str = Field(None, alias="detail-type") + detail: Dict[str, Any] diff --git a/docs/examples/utilities/parser/parser_event_parser_decorator.py b/docs/examples/utilities/parser/parser_event_parser_decorator.py new file mode 100644 index 00000000000..d0aa9340eb6 --- /dev/null +++ b/docs/examples/utilities/parser/parser_event_parser_decorator.py @@ -0,0 +1,44 @@ +import json +from typing import List, Optional + +from aws_lambda_powertools.utilities.parser import BaseModel, event_parser +from aws_lambda_powertools.utilities.typing import LambdaContext + + +class OrderItem(BaseModel): + id: int + quantity: int + description: str + + +class Order(BaseModel): + id: int + description: str + items: List[OrderItem] # nesting models are supported + optional_field: Optional[str] # this field may or may not be available when parsing + + +@event_parser(model=Order) +def handler(event: Order, context: LambdaContext): + print(event.id) + print(event.description) + print(event.items) + + order_items = [item for item in event.items] + ... + + +payload = { + "id": 10876546789, + "description": "My order", + "items": [ + { + "id": 1015938732, + "quantity": 1, + "description": "item xpto", + }, + ], +} + +handler(event=payload, context=LambdaContext()) +handler(event=json.dumps(payload), context=LambdaContext()) # also works if event is a JSON string diff --git a/docs/examples/utilities/parser/parser_extending_builtin_models.py b/docs/examples/utilities/parser/parser_extending_builtin_models.py new file mode 100644 index 00000000000..b02e3176983 --- /dev/null +++ b/docs/examples/utilities/parser/parser_extending_builtin_models.py @@ -0,0 +1,52 @@ +from typing import List, Optional + +from aws_lambda_powertools.utilities.parser import BaseModel, parse +from aws_lambda_powertools.utilities.parser.models import EventBridgeModel + + +class OrderItem(BaseModel): + id: int + quantity: int + description: str + + +class Order(BaseModel): + id: int + description: str + items: List[OrderItem] + + +class OrderEventModel(EventBridgeModel): + detail: Order + + +payload = { + "version": "0", + "id": "6a7e8feb-b491-4cf7-a9f1-bf3703467718", + "detail-type": "OrderPurchased", + "source": "OrderService", + "account": "111122223333", + "time": "2020-10-22T18:43:48Z", + "region": "us-west-1", + "resources": ["some_additional"], + "detail": { + "id": 10876546789, + "description": "My order", + "items": [ + { + "id": 1015938732, + "quantity": 1, + "description": "item xpto", + }, + ], + }, +} + +ret = parse(model=OrderEventModel, event=payload) + +assert ret.source == "OrderService" +assert ret.detail.description == "My order" +assert ret.detail_type == "OrderPurchased" # we rename it to snake_case since detail-type is an invalid name + +for order_item in ret.detail.items: + ... diff --git a/docs/examples/utilities/parser/parser_model_export.py b/docs/examples/utilities/parser/parser_model_export.py new file mode 100644 index 00000000000..e03277820f6 --- /dev/null +++ b/docs/examples/utilities/parser/parser_model_export.py @@ -0,0 +1,32 @@ +from aws_lambda_powertools.utilities import Logger +from aws_lambda_powertools.utilities.parser import BaseModel, ValidationError, parse, validator + +logger = Logger(service="user") + + +class UserModel(BaseModel): + username: str + password1: str + password2: str + + +payload = { + "username": "universe", + "password1": "myp@ssword", + "password2": "repeat password", +} + + +def my_function(): + try: + return parse(model=UserModel, event=payload) + except ValidationError as e: + logger.exception(e.json()) + return {"status_code": 400, "message": "Invalid username"} + + +User: UserModel = my_function() +user_dict = User.dict() +user_json = User.json() +user_json_schema_as_dict = User.schema() +user_json_schema_as_json = User.schema_json(indent=2) diff --git a/docs/examples/utilities/parser/parser_models.py b/docs/examples/utilities/parser/parser_models.py new file mode 100644 index 00000000000..030e27a5135 --- /dev/null +++ b/docs/examples/utilities/parser/parser_models.py @@ -0,0 +1,16 @@ +from typing import List, Optional + +from aws_lambda_powertools.utilities.parser import BaseModel + + +class OrderItem(BaseModel): + id: int + quantity: int + description: str + + +class Order(BaseModel): + id: int + description: str + items: List[OrderItem] # nesting models are supported + optional_field: Optional[str] # this field may or may not be available when parsing diff --git a/docs/examples/utilities/parser/parser_parse_function.py b/docs/examples/utilities/parser/parser_parse_function.py new file mode 100644 index 00000000000..834fe8e1197 --- /dev/null +++ b/docs/examples/utilities/parser/parser_parse_function.py @@ -0,0 +1,39 @@ +from typing import List, Optional + +from aws_lambda_powertools.utilities.parser import BaseModel, ValidationError, parse + + +class OrderItem(BaseModel): + id: int + quantity: int + description: str + + +class Order(BaseModel): + id: int + description: str + items: List[OrderItem] # nesting models are supported + optional_field: Optional[str] # this field may or may not be available when parsing + + +payload = { + "id": 10876546789, + "description": "My order", + "items": [ + { + # this will cause a validation error + "id": [1015938732], + "quantity": 1, + "description": "item xpto", + } + ], +} + + +def my_function(): + try: + parsed_payload: Order = parse(event=payload, model=Order) + # payload dict is now parsed into our model + return parsed_payload.items + except ValidationError: + return {"status_code": 400, "message": "Invalid order"} diff --git a/docs/examples/utilities/parser/parser_validator.py b/docs/examples/utilities/parser/parser_validator.py new file mode 100644 index 00000000000..aa6f42fb9b0 --- /dev/null +++ b/docs/examples/utilities/parser/parser_validator.py @@ -0,0 +1,14 @@ +from aws_lambda_powertools.utilities.parser import BaseModel, parse, validator + + +class HelloWorldModel(BaseModel): + message: str + + @validator("message") + def is_hello_world(cls, v): + if v != "hello world": + raise ValueError("Message must be hello world!") + return v + + +parse(model=HelloWorldModel, event={"message": "hello universe"}) diff --git a/docs/examples/utilities/parser/parser_validator_all.py b/docs/examples/utilities/parser/parser_validator_all.py new file mode 100644 index 00000000000..d2905e812bb --- /dev/null +++ b/docs/examples/utilities/parser/parser_validator_all.py @@ -0,0 +1,16 @@ +from aws_lambda_powertools.utilities.parser import BaseModel, parse, validator + + +class HelloWorldModel(BaseModel): + message: str + sender: str + + @validator("*") + def has_whitespace(cls, v): + if " " not in v: + raise ValueError("Must have whitespace...") + + return v + + +parse(model=HelloWorldModel, event={"message": "hello universe", "sender": "universe"}) diff --git a/docs/examples/utilities/parser/parser_validator_root.py b/docs/examples/utilities/parser/parser_validator_root.py new file mode 100644 index 00000000000..934866d2a1a --- /dev/null +++ b/docs/examples/utilities/parser/parser_validator_root.py @@ -0,0 +1,23 @@ +from aws_lambda_powertools.utilities.parser import BaseModel, parse, root_validator + + +class UserModel(BaseModel): + username: str + password1: str + password2: str + + @root_validator + def check_passwords_match(cls, values): + pw1, pw2 = values.get("password1"), values.get("password2") + if pw1 is not None and pw2 is not None and pw1 != pw2: + raise ValueError("passwords do not match") + return values + + +payload = { + "username": "universe", + "password1": "myp@ssword", + "password2": "repeat password", +} + +parse(model=UserModel, event=payload) diff --git a/docs/utilities/parser.md b/docs/utilities/parser.md index c17e2f173c5..e31f29cfcd4 100644 --- a/docs/utilities/parser.md +++ b/docs/utilities/parser.md @@ -29,19 +29,7 @@ Install parser's extra dependencies using **`pip install aws-lambda-powertools[p You can define models to parse incoming events by inheriting from `BaseModel`. ```python title="Defining an Order data model" -from aws_lambda_powertools.utilities.parser import BaseModel -from typing import List, Optional - -class OrderItem(BaseModel): - id: int - quantity: int - description: str - -class Order(BaseModel): - id: int - description: str - items: List[OrderItem] # nesting models are supported - optional_field: Optional[str] # this field may or may not be available when parsing +--8<-- "docs/examples/utilities/parser/parser_models.py" ``` These are simply Python classes that inherit from BaseModel. **Parser** enforces type hints declared in your model at runtime. @@ -59,93 +47,16 @@ Use the decorator for fail fast scenarios where you want your Lambda function to ???+ note **This decorator will replace the `event` object with the parsed model if successful**. This means you might be careful when nesting other decorators that expect `event` to be a `dict`. -```python hl_lines="18" title="Parsing and validating upon invocation with event_parser decorator" -from aws_lambda_powertools.utilities.parser import event_parser, BaseModel -from aws_lambda_powertools.utilities.typing import LambdaContext -from typing import List, Optional - -import json - -class OrderItem(BaseModel): - id: int - quantity: int - description: str - -class Order(BaseModel): - id: int - description: str - items: List[OrderItem] # nesting models are supported - optional_field: Optional[str] # this field may or may not be available when parsing - - -@event_parser(model=Order) -def handler(event: Order, context: LambdaContext): - print(event.id) - print(event.description) - print(event.items) - - order_items = [item for item in event.items] - ... - -payload = { - "id": 10876546789, - "description": "My order", - "items": [ - { - "id": 1015938732, - "quantity": 1, - "description": "item xpto" - } - ] -} - -handler(event=payload, context=LambdaContext()) -handler(event=json.dumps(payload), context=LambdaContext()) # also works if event is a JSON string +```python hl_lines="21" title="Parsing and validating upon invocation with event_parser decorator" +--8<-- "docs/examples/utilities/parser/parser_event_parser_decorator.py" ``` ### parse function Use this standalone function when you want more control over the data validation process, for example returning a 400 error for malformed payloads. -```python hl_lines="21 30" title="Using standalone parse function for more flexibility" -from aws_lambda_powertools.utilities.parser import parse, BaseModel, ValidationError -from typing import List, Optional - -class OrderItem(BaseModel): - id: int - quantity: int - description: str - -class Order(BaseModel): - id: int - description: str - items: List[OrderItem] # nesting models are supported - optional_field: Optional[str] # this field may or may not be available when parsing - - -payload = { - "id": 10876546789, - "description": "My order", - "items": [ - { - # this will cause a validation error - "id": [1015938732], - "quantity": 1, - "description": "item xpto" - } - ] -} - -def my_function(): - try: - parsed_payload: Order = parse(event=payload, model=Order) - # payload dict is now parsed into our model - return parsed_payload.items - except ValidationError: - return { - "status_code": 400, - "message": "Invalid order" - } +```python hl_lines="24 35" title="Using standalone parse function for more flexibility" +--8<-- "docs/examples/utilities/parser/parser_parse_function.py" ``` ## Built-in models @@ -174,56 +85,8 @@ You can extend them to include your own models, and yet have all other known fie ???+ tip For Mypy users, we only allow type override for fields where payload is injected e.g. `detail`, `body`, etc. - -```python hl_lines="16-17 28 41" title="Extending EventBridge model as an example" -from aws_lambda_powertools.utilities.parser import parse, BaseModel -from aws_lambda_powertools.utilities.parser.models import EventBridgeModel - -from typing import List, Optional - -class OrderItem(BaseModel): - id: int - quantity: int - description: str - -class Order(BaseModel): - id: int - description: str - items: List[OrderItem] - -class OrderEventModel(EventBridgeModel): - detail: Order - -payload = { - "version": "0", - "id": "6a7e8feb-b491-4cf7-a9f1-bf3703467718", - "detail-type": "OrderPurchased", - "source": "OrderService", - "account": "111122223333", - "time": "2020-10-22T18:43:48Z", - "region": "us-west-1", - "resources": ["some_additional"], - "detail": { - "id": 10876546789, - "description": "My order", - "items": [ - { - "id": 1015938732, - "quantity": 1, - "description": "item xpto" - } - ] - } -} - -ret = parse(model=OrderEventModel, event=payload) - -assert ret.source == "OrderService" -assert ret.detail.description == "My order" -assert ret.detail_type == "OrderPurchased" # we rename it to snake_case since detail-type is an invalid name - -for order_item in ret.detail.items: - ... +```python hl_lines="19-20 32 45" title="Extending EventBridge model as an example" +--8<-- "docs/examples/utilities/parser/parser_extending_builtin_models.py" ``` **What's going on here, you might ask**: @@ -248,40 +111,8 @@ Envelopes can be used via `envelope` parameter available in both `parse` functio Here's an example of parsing a model found in an event coming from EventBridge, where all you want is what's inside the `detail` key. -```python hl_lines="18-22 25 31" title="Parsing payload in a given key only using envelope feature" -from aws_lambda_powertools.utilities.parser import event_parser, parse, BaseModel, envelopes -from aws_lambda_powertools.utilities.typing import LambdaContext - -class UserModel(BaseModel): - username: str - password1: str - password2: str - -payload = { - "version": "0", - "id": "6a7e8feb-b491-4cf7-a9f1-bf3703467718", - "detail-type": "CustomerSignedUp", - "source": "CustomerService", - "account": "111122223333", - "time": "2020-10-22T18:43:48Z", - "region": "us-west-1", - "resources": ["some_additional_"], - "detail": { - "username": "universe", - "password1": "myp@ssword", - "password2": "repeat password" - } -} - -ret = parse(model=UserModel, envelope=envelopes.EventBridgeEnvelope, event=payload) - -# Parsed model only contains our actual model, not the entire EventBridge + Payload parsed -assert ret.password1 == ret.password2 - -# Same behaviour but using our decorator -@event_parser(model=UserModel, envelope=envelopes.EventBridgeEnvelope) -def handler(event: UserModel, context: LambdaContext): - assert event.password1 == event.password2 +```python hl_lines="20-24 27 33" title="Parsing payload in a given key only using envelope feature" +--8<-- "docs/examples/utilities/parser/parser_envelope.py" ``` **What's going on here, you might ask**: @@ -316,53 +147,13 @@ Here's a snippet of how the EventBridge envelope we demonstrated previously is i === "EventBridge Model" ```python - from datetime import datetime - from typing import Any, Dict, List - - from aws_lambda_powertools.utilities.parser import BaseModel, Field - - - class EventBridgeModel(BaseModel): - version: str - id: str # noqa: A003,VNE003 - source: str - account: str - time: datetime - region: str - resources: List[str] - detail_type: str = Field(None, alias="detail-type") - detail: Dict[str, Any] + --8<-- "docs/examples/utilities/parser/parser_event_bridge_model.py" ``` === "EventBridge Envelope" - ```python hl_lines="8 10 25 26" - from aws_lambda_powertools.utilities.parser import BaseEnvelope, models - from aws_lambda_powertools.utilities.parser.models import EventBridgeModel - - from typing import Any, Dict, Optional, TypeVar - - Model = TypeVar("Model", bound=BaseModel) - - class EventBridgeEnvelope(BaseEnvelope): - - def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Model) -> Optional[Model]: - """Parses data found with model provided - - Parameters - ---------- - data : Dict - Lambda event to be parsed - model : Model - Data model provided to parse after extracting data using envelope - - Returns - ------- - Any - Parsed detail payload with model provided - """ - parsed_envelope = EventBridgeModel.parse_obj(data) - return self._parse(data=parsed_envelope.detail, model=model) + ```python hl_lines="9-10 25 26" + --8<-- "docs/examples/utilities/parser/parser_event_bridge_envelope.py" ``` **What's going on here, you might ask**: @@ -393,19 +184,8 @@ Keep the following in mind regardless of which decorator you end up using it: Quick validation to verify whether the field `message` has the value of `hello world`. -```python hl_lines="6" title="Data field validation with validator" -from aws_lambda_powertools.utilities.parser import parse, BaseModel, validator - -class HelloWorldModel(BaseModel): - message: str - - @validator('message') - def is_hello_world(cls, v): - if v != "hello world": - raise ValueError("Message must be hello world!") - return v - -parse(model=HelloWorldModel, event={"message": "hello universe"}) +```python hl_lines="7" title="Data field validation with validator" +--8<-- "docs/examples/utilities/parser/parser_validator.py" ``` If you run as-is, you should expect the following error with the message we provided in our exception: @@ -417,21 +197,8 @@ message Alternatively, you can pass `'*'` as an argument for the decorator so that you can validate every value available. -```python hl_lines="7" title="Validating all data fields with custom logic" -from aws_lambda_powertools.utilities.parser import parse, BaseModel, validator - -class HelloWorldModel(BaseModel): - message: str - sender: str - - @validator('*') - def has_whitespace(cls, v): - if ' ' not in v: - raise ValueError("Must have whitespace...") - - return v - -parse(model=HelloWorldModel, event={"message": "hello universe", "sender": "universe"}) +```python hl_lines="8" title="Validating all data fields with custom logic" +--8<-- "docs/examples/utilities/parser/parser_validator_all.py" ``` ### validating entire model @@ -439,27 +206,7 @@ parse(model=HelloWorldModel, event={"message": "hello universe", "sender": "univ `root_validator` can help when you have a complex validation mechanism. For example finding whether data has been omitted, comparing field values, etc. ```python title="Comparing and validating multiple fields at once with root_validator" -from aws_lambda_powertools.utilities.parser import parse, BaseModel, root_validator - -class UserModel(BaseModel): - username: str - password1: str - password2: str - - @root_validator - def check_passwords_match(cls, values): - pw1, pw2 = values.get('password1'), values.get('password2') - if pw1 is not None and pw2 is not None and pw1 != pw2: - raise ValueError('passwords do not match') - return values - -payload = { - "username": "universe", - "password1": "myp@ssword", - "password2": "repeat password" -} - -parse(model=UserModel, event=payload) +--8<-- "docs/examples/utilities/parser/parser_validator_root.py" ``` ???+ info @@ -474,38 +221,8 @@ There are number of advanced use cases well documented in Pydantic's doc such as Two possible unknown use cases are Models and exception' serialization. Models have methods to [export them](https://pydantic-docs.helpmanual.io/usage/exporting_models/) as `dict`, `JSON`, `JSON Schema`, and Validation exceptions can be exported as JSON. -```python hl_lines="21 28-31" title="Converting data models in various formats" -from aws_lambda_powertools.utilities import Logger -from aws_lambda_powertools.utilities.parser import parse, BaseModel, ValidationError, validator - -logger = Logger(service="user") - -class UserModel(BaseModel): - username: str - password1: str - password2: str - -payload = { - "username": "universe", - "password1": "myp@ssword", - "password2": "repeat password" -} - -def my_function(): - try: - return parse(model=UserModel, event=payload) - except ValidationError as e: - logger.exception(e.json()) - return { - "status_code": 400, - "message": "Invalid username" - } - -User: UserModel = my_function() -user_dict = User.dict() -user_json = User.json() -user_json_schema_as_dict = User.schema() -user_json_schema_as_json = User.schema_json(indent=2) +```python hl_lines="24 29-32" title="Converting data models in various formats" +--8<-- "docs/examples/utilities/parser/parser_model_export.py" ``` These can be quite useful when manipulating models that later need to be serialized as inputs for services like DynamoDB, EventBridge, etc. From 31fe45ec28d7ac9579d509351be214cabecd9c3e Mon Sep 17 00:00:00 2001 From: Simon Thulbourn Date: Thu, 28 Apr 2022 17:34:36 +0100 Subject: [PATCH 2/2] Revert "fix(parser): Add missing fields for SESEvent (#1027)" (#1190) This reverts commit 797a10afac80544e2d69bcb7d624909436f2b12a. --- .../utilities/parser/models/__init__.py | 8 -- .../utilities/parser/models/ses.py | 34 +----- tests/events/sesEventS3.json | 114 ------------------ tests/functional/parser/test_ses.py | 58 +-------- 4 files changed, 8 insertions(+), 206 deletions(-) delete mode 100644 tests/events/sesEventS3.json diff --git a/aws_lambda_powertools/utilities/parser/models/__init__.py b/aws_lambda_powertools/utilities/parser/models/__init__.py index 34c8e6ce6a1..e3fb50a2d5d 100644 --- a/aws_lambda_powertools/utilities/parser/models/__init__.py +++ b/aws_lambda_powertools/utilities/parser/models/__init__.py @@ -37,11 +37,7 @@ SesModel, SesReceipt, SesReceiptAction, - SesReceiptActionBase, - SesReceiptBounceAction, - SesReceiptS3Action, SesReceiptVerdict, - SesReceiptWorkmailAction, SesRecordModel, ) from .sns import SnsModel, SnsNotificationModel, SnsRecordModel @@ -88,10 +84,6 @@ "SesMailHeaders", "SesReceipt", "SesReceiptAction", - "SesReceiptActionBase", - "SesReceiptBounceAction", - "SesReceiptWorkmailAction", - "SesReceiptS3Action", "SesReceiptVerdict", "SnsModel", "SnsNotificationModel", diff --git a/aws_lambda_powertools/utilities/parser/models/ses.py b/aws_lambda_powertools/utilities/parser/models/ses.py index 7cd655ea28c..70fd2e83978 100644 --- a/aws_lambda_powertools/utilities/parser/models/ses.py +++ b/aws_lambda_powertools/utilities/parser/models/ses.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import List, Optional, Union +from typing import List, Optional from pydantic import BaseModel, Field from pydantic.networks import EmailStr @@ -12,38 +12,12 @@ class SesReceiptVerdict(BaseModel): status: Literal["PASS", "FAIL", "GRAY", "PROCESSING_FAILED"] -class SesReceiptActionBase(BaseModel): - topicArn: Optional[str] - - -class SesReceiptAction(SesReceiptActionBase): +class SesReceiptAction(BaseModel): type: Literal["Lambda"] # noqa A003,VNE003 invocationType: Literal["Event"] functionArn: str -class SesReceiptS3Action(SesReceiptActionBase): - type: Literal["S3"] # noqa A003,VNE003 - topicArn: str - bucketName: str - objectKey: str - - -class SesReceiptBounceAction(SesReceiptActionBase): - type: Literal["Bounce"] # noqa A003,VNE003 - topicArn: str - smtpReplyCode: str - message: str - sender: str - statusCode: str - - -class SesReceiptWorkmailAction(SesReceiptActionBase): - type: Literal["WorkMail"] # noqa A003,VNE003 - topicArn: str - organizationArn: str - - class SesReceipt(BaseModel): timestamp: datetime processingTimeMillis: PositiveInt @@ -51,10 +25,8 @@ class SesReceipt(BaseModel): spamVerdict: SesReceiptVerdict virusVerdict: SesReceiptVerdict spfVerdict: SesReceiptVerdict - dkimVerdict: SesReceiptVerdict dmarcVerdict: SesReceiptVerdict - dmarcPolicy: Optional[Literal["quarantine", "reject", "none"]] - action: Union[SesReceiptAction, SesReceiptS3Action, SesReceiptBounceAction, SesReceiptWorkmailAction] + action: SesReceiptAction class SesMailHeaders(BaseModel): diff --git a/tests/events/sesEventS3.json b/tests/events/sesEventS3.json deleted file mode 100644 index dbea2d42ce1..00000000000 --- a/tests/events/sesEventS3.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "Records": [ - { - "eventVersion": "1.0", - "ses": { - "receipt": { - "timestamp": "2015-09-11T20:32:33.936Z", - "processingTimeMillis": 406, - "recipients": [ - "recipient@example.com" - ], - "spamVerdict": { - "status": "PASS" - }, - "virusVerdict": { - "status": "PASS" - }, - "spfVerdict": { - "status": "PASS" - }, - "dkimVerdict": { - "status": "PASS" - }, - "dmarcVerdict": { - "status": "PASS" - }, - "dmarcPolicy": "reject", - "action": { - "type": "S3", - "topicArn": "arn:aws:sns:us-east-1:012345678912:example-topic", - "bucketName": "my-S3-bucket", - "objectKey": "email" - } - }, - "mail": { - "timestamp": "2015-09-11T20:32:33.936Z", - "source": "0000014fbe1c09cf-7cb9f704-7531-4e53-89a1-5fa9744f5eb6-000000@amazonses.com", - "messageId": "d6iitobk75ur44p8kdnnp7g2n800", - "destination": [ - "recipient@example.com" - ], - "headersTruncated": false, - "headers": [ - { - "name": "Return-Path", - "value": "<0000014fbe1c09cf-7cb9f704-7531-4e53-89a1-5fa9744f5eb6-000000@amazonses.com>" - }, - { - "name": "Received", - "value": "from a9-183.smtp-out.amazonses.com (a9-183.smtp-out.amazonses.com [54.240.9.183]) by inbound-smtp.us-east-1.amazonaws.com with SMTP id d6iitobk75ur44p8kdnnp7g2n800 for recipient@example.com; Fri, 11 Sep 2015 20:32:33 +0000 (UTC)" - }, - { - "name": "DKIM-Signature", - "value": "v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple; s=ug7nbtf4gccmlpwj322ax3p6ow6yfsug; d=amazonses.com; t=1442003552; h=From:To:Subject:MIME-Version:Content-Type:Content-Transfer-Encoding:Date:Message-ID:Feedback-ID; bh=DWr3IOmYWoXCA9ARqGC/UaODfghffiwFNRIb2Mckyt4=; b=p4ukUDSFqhqiub+zPR0DW1kp7oJZakrzupr6LBe6sUuvqpBkig56UzUwc29rFbJF hlX3Ov7DeYVNoN38stqwsF8ivcajXpQsXRC1cW9z8x875J041rClAjV7EGbLmudVpPX 4hHst1XPyX5wmgdHIhmUuh8oZKpVqGi6bHGzzf7g=" - }, - { - "name": "From", - "value": "sender@example.com" - }, - { - "name": "To", - "value": "recipient@example.com" - }, - { - "name": "Subject", - "value": "Example subject" - }, - { - "name": "MIME-Version", - "value": "1.0" - }, - { - "name": "Content-Type", - "value": "text/plain; charset=UTF-8" - }, - { - "name": "Content-Transfer-Encoding", - "value": "7bit" - }, - { - "name": "Date", - "value": "Fri, 11 Sep 2015 20:32:32 +0000" - }, - { - "name": "Message-ID", - "value": "<61967230-7A45-4A9D-BEC9-87CBCF2211C9@example.com>" - }, - { - "name": "X-SES-Outgoing", - "value": "2015.09.11-54.240.9.183" - }, - { - "name": "Feedback-ID", - "value": "1.us-east-1.Krv2FKpFdWV+KUYw3Qd6wcpPJ4Sv/pOPpEPSHn2u2o4=:AmazonSES" - } - ], - "commonHeaders": { - "returnPath": "0000014fbe1c09cf-7cb9f704-7531-4e53-89a1-5fa9744f5eb6-000000@amazonses.com", - "from": [ - "sender@example.com" - ], - "date": "Fri, 11 Sep 2015 20:32:32 +0000", - "to": [ - "recipient@example.com" - ], - "messageId": "<61967230-7A45-4A9D-BEC9-87CBCF2211C9@example.com>", - "subject": "Example subject" - } - } - }, - "eventSource": "aws:ses" - } - ] -} diff --git a/tests/functional/parser/test_ses.py b/tests/functional/parser/test_ses.py index 34a44253514..d434e2350f8 100644 --- a/tests/functional/parser/test_ses.py +++ b/tests/functional/parser/test_ses.py @@ -1,22 +1,11 @@ from aws_lambda_powertools.utilities.parser import event_parser -from aws_lambda_powertools.utilities.parser.models import ( - SesModel, - SesReceiptBounceAction, - SesReceiptWorkmailAction, - SesRecordModel, -) +from aws_lambda_powertools.utilities.parser.models import SesModel, SesRecordModel from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.utils import load_event @event_parser(model=SesModel) -def handle_ses(event: SesModel, _: LambdaContext) -> SesModel: - return event - - -def test_ses_trigger_lambda_event(): - event_dict = load_event("sesEvent.json") - event = handle_ses(event_dict, LambdaContext()) +def handle_ses(event: SesModel, _: LambdaContext): expected_address = "johndoe@example.com" records = event.Records record: SesRecordModel = records[0] @@ -40,10 +29,6 @@ def test_ses_trigger_lambda_event(): assert common_headers.to == [expected_address] assert common_headers.messageId == "<0123456789example.com>" assert common_headers.subject == "Test Subject" - assert common_headers.cc is None - assert common_headers.bcc is None - assert common_headers.sender is None - assert common_headers.reply_to is None receipt = record.ses.receipt convert_time = int(round(receipt.timestamp.timestamp() * 1000)) assert convert_time == 0 @@ -53,45 +38,12 @@ def test_ses_trigger_lambda_event(): assert receipt.virusVerdict.status == "PASS" assert receipt.spfVerdict.status == "PASS" assert receipt.dmarcVerdict.status == "PASS" - assert receipt.dmarcVerdict.status == "PASS" - assert receipt.dmarcPolicy is None action = receipt.action assert action.type == "Lambda" assert action.functionArn == "arn:aws:lambda:us-west-2:012345678912:function:Example" assert action.invocationType == "Event" - assert action.topicArn is None - -def test_ses_trigger_event_s3(): - event_dict = load_event("sesEventS3.json") - event = handle_ses(event_dict, LambdaContext()) - records = list(event.Records) - record = records[0] - receipt = record.ses.receipt - assert receipt.dmarcPolicy == "reject" - action = record.ses.receipt.action - assert action.type == "S3" - assert action.topicArn == "arn:aws:sns:us-east-1:012345678912:example-topic" - assert action.bucketName == "my-S3-bucket" - assert action.objectKey == "email" - - -def test_ses_trigger_event_bounce(): - event_dict = { - "type": "Bounce", - "topicArn": "arn:aws:sns:us-east-1:123456789012:topic:my-topic", - "smtpReplyCode": "5.1.1", - "message": "message", - "sender": "sender", - "statusCode": "550", - } - SesReceiptBounceAction(**event_dict) - -def test_ses_trigger_event_work_mail(): - event_dict = { - "type": "WorkMail", - "topicArn": "arn:aws:sns:us-east-1:123456789012:topic:my-topic", - "organizationArn": "arn", - } - SesReceiptWorkmailAction(**event_dict) +def test_ses_trigger_event(): + event_dict = load_event("sesEvent.json") + handle_ses(event_dict, LambdaContext())