From 0a067fd9b6f39ecad196153bb8e6b09de13ee4ef Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sat, 5 Jun 2021 23:25:19 -0700 Subject: [PATCH 1/7] feat(data-classes): add AttributeValueType to DynamoDBStreamEvent Changes: Add new enum AttributeValueType for the type of AttributeValue Add new method `get_type` to return said enum --- .../data_classes/dynamo_db_stream_event.py | 18 +++++ tests/functional/test_data_classes.py | 67 +++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py b/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py index bc3a4a82995..a19f7d88a56 100644 --- a/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py +++ b/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py @@ -4,12 +4,30 @@ from aws_lambda_powertools.utilities.data_classes.common import DictWrapper +class AttributeValueType(Enum): + Binary = "B" + BinarySet = "BS" + Boolean = "BOOL" + List = "L" + Map = "M" + Number = "N" + NumberSet = "NS" + Null = "NULL" + String = "S" + StringSet = "SS" + + class AttributeValue(DictWrapper): """Represents the data for an attribute Documentation: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_streams_AttributeValue.html """ + @property + def get_type(self) -> AttributeValueType: + """Get the attribute value type based on the contained data""" + return AttributeValueType(list(self.raw_event.keys())[0]) + @property def b_value(self) -> Optional[str]: """An attribute of type Base64-encoded binary data object diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py index 60dfc591897..47446d6b6ce 100644 --- a/tests/functional/test_data_classes.py +++ b/tests/functional/test_data_classes.py @@ -58,6 +58,7 @@ ) from aws_lambda_powertools.utilities.data_classes.dynamo_db_stream_event import ( AttributeValue, + AttributeValueType, DynamoDBRecordEventName, DynamoDBStreamEvent, StreamViewType, @@ -443,6 +444,30 @@ def test_dynamo_db_stream_trigger_event(): assert record.user_identity is None +def test_dynamo_attribute_value_b_value(): + example_attribute_value = {"B": "dGhpcyB0ZXh0IGlzIGJhc2U2NC1lbmNvZGVk"} + + attribute_value = AttributeValue(example_attribute_value) + + assert attribute_value.get_type == AttributeValueType.Binary + + +def test_dynamo_attribute_value_bs_value(): + example_attribute_value = {"BS": ["U3Vubnk=", "UmFpbnk=", "U25vd3k="]} + + attribute_value = AttributeValue(example_attribute_value) + + assert attribute_value.get_type == AttributeValueType.BinarySet + + +def test_dynamo_attribute_value_bool_value(): + example_attribute_value = {"BOOL": True} + + attribute_value = AttributeValue(example_attribute_value) + + assert attribute_value.get_type == AttributeValueType.Boolean + + def test_dynamo_attribute_value_list_value(): example_attribute_value = {"L": [{"S": "Cookies"}, {"S": "Coffee"}, {"N": "3.14159"}]} attribute_value = AttributeValue(example_attribute_value) @@ -450,6 +475,7 @@ def test_dynamo_attribute_value_list_value(): assert list_value is not None item = list_value[0] assert item.s_value == "Cookies" + assert attribute_value.get_type == AttributeValueType.List def test_dynamo_attribute_value_map_value(): @@ -461,6 +487,47 @@ def test_dynamo_attribute_value_map_value(): assert map_value is not None item = map_value["Name"] assert item.s_value == "Joe" + assert attribute_value.get_type == AttributeValueType.Map + + +def test_dynamo_attribute_value_n_value(): + example_attribute_value = {"N": "123.45"} + + attribute_value = AttributeValue(example_attribute_value) + + assert attribute_value.get_type == AttributeValueType.Number + + +def test_dynamo_attribute_value_ns_value(): + example_attribute_value = {"NS": ["42.2", "-19", "7.5", "3.14"]} + + attribute_value = AttributeValue(example_attribute_value) + + assert attribute_value.get_type == AttributeValueType.NumberSet + + +def test_dynamo_attribute_value_null_value(): + example_attribute_value = {"NULL": True} + + attribute_value = AttributeValue(example_attribute_value) + + assert attribute_value.get_type == AttributeValueType.Null + + +def test_dynamo_attribute_value_s_value(): + example_attribute_value = {"S": "Hello"} + + attribute_value = AttributeValue(example_attribute_value) + + assert attribute_value.get_type == AttributeValueType.String + + +def test_dynamo_attribute_value_ss_value(): + example_attribute_value = {"SS": ["Giraffe", "Hippo", "Zebra"]} + + attribute_value = AttributeValue(example_attribute_value) + + assert attribute_value.get_type == AttributeValueType.StringSet def test_event_bridge_event(): From 987c559d75df7ddaddbe5840680c71be0b33fe2f Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 6 Jun 2021 22:13:58 -0700 Subject: [PATCH 2/7] feat(data-classes): add `value` property to AttributeValue --- .../data_classes/dynamo_db_stream_event.py | 39 ++++++++++++++++--- tests/functional/test_data_classes.py | 12 ++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py b/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py index a19f7d88a56..54be552bac4 100644 --- a/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py +++ b/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Dict, Iterator, List, Optional +from typing import Any, Dict, Iterator, List, Optional, Union from aws_lambda_powertools.utilities.data_classes.common import DictWrapper @@ -23,10 +23,16 @@ class AttributeValue(DictWrapper): Documentation: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_streams_AttributeValue.html """ - @property - def get_type(self) -> AttributeValueType: - """Get the attribute value type based on the contained data""" - return AttributeValueType(list(self.raw_event.keys())[0]) + def __init__(self, data: Dict[str, Any]): + """AttributeValue constructor + + Parameters + ---------- + data: Dict[str, Any] + Raw lambda event dict + """ + super().__init__(data) + self.dynamodb_type = list(data.keys())[0] @property def b_value(self) -> Optional[str]: @@ -124,6 +130,29 @@ def ss_value(self) -> Optional[List[str]]: """ return self.get("SS") + @property + def get_type(self) -> AttributeValueType: + """Get the attribute value type based on the contained data""" + return AttributeValueType(self.dynamodb_type) + + @property + def value(self) -> Union[Optional[bool], Optional[str], Optional[List], Optional[Dict]]: + """Get the attribute value""" + try: + return getattr(self, f"{self.dynamodb_type.lower()}_value") + except AttributeError: + raise TypeError(f"Dynamodb type {self.dynamodb_type} is not supported") + + @property + def l_value(self) -> Optional[List["AttributeValue"]]: + """Alias of list_value""" + return self.list_value + + @property + def m_value(self) -> Optional[Dict[str, "AttributeValue"]]: + """Alias of map_value""" + return self.map_value + def _attribute_value_dict(attr_values: Dict[str, dict], key: str) -> Optional[Dict[str, AttributeValue]]: """A dict of type String to AttributeValue object map diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py index 47446d6b6ce..4245869a165 100644 --- a/tests/functional/test_data_classes.py +++ b/tests/functional/test_data_classes.py @@ -450,6 +450,7 @@ def test_dynamo_attribute_value_b_value(): attribute_value = AttributeValue(example_attribute_value) assert attribute_value.get_type == AttributeValueType.Binary + assert attribute_value.b_value == attribute_value.value def test_dynamo_attribute_value_bs_value(): @@ -458,6 +459,7 @@ def test_dynamo_attribute_value_bs_value(): attribute_value = AttributeValue(example_attribute_value) assert attribute_value.get_type == AttributeValueType.BinarySet + assert attribute_value.bs_value == attribute_value.value def test_dynamo_attribute_value_bool_value(): @@ -466,6 +468,7 @@ def test_dynamo_attribute_value_bool_value(): attribute_value = AttributeValue(example_attribute_value) assert attribute_value.get_type == AttributeValueType.Boolean + assert attribute_value.bool_value == attribute_value.value def test_dynamo_attribute_value_list_value(): @@ -476,6 +479,8 @@ def test_dynamo_attribute_value_list_value(): item = list_value[0] assert item.s_value == "Cookies" assert attribute_value.get_type == AttributeValueType.List + assert attribute_value.l_value == attribute_value.list_value + assert attribute_value.list_value == attribute_value.value def test_dynamo_attribute_value_map_value(): @@ -488,6 +493,8 @@ def test_dynamo_attribute_value_map_value(): item = map_value["Name"] assert item.s_value == "Joe" assert attribute_value.get_type == AttributeValueType.Map + assert attribute_value.m_value == attribute_value.map_value + assert attribute_value.map_value == attribute_value.value def test_dynamo_attribute_value_n_value(): @@ -496,6 +503,7 @@ def test_dynamo_attribute_value_n_value(): attribute_value = AttributeValue(example_attribute_value) assert attribute_value.get_type == AttributeValueType.Number + assert attribute_value.n_value == attribute_value.value def test_dynamo_attribute_value_ns_value(): @@ -504,6 +512,7 @@ def test_dynamo_attribute_value_ns_value(): attribute_value = AttributeValue(example_attribute_value) assert attribute_value.get_type == AttributeValueType.NumberSet + assert attribute_value.ns_value == attribute_value.value def test_dynamo_attribute_value_null_value(): @@ -512,6 +521,7 @@ def test_dynamo_attribute_value_null_value(): attribute_value = AttributeValue(example_attribute_value) assert attribute_value.get_type == AttributeValueType.Null + assert attribute_value.null_value == attribute_value.value def test_dynamo_attribute_value_s_value(): @@ -520,6 +530,7 @@ def test_dynamo_attribute_value_s_value(): attribute_value = AttributeValue(example_attribute_value) assert attribute_value.get_type == AttributeValueType.String + assert attribute_value.s_value == attribute_value.value def test_dynamo_attribute_value_ss_value(): @@ -528,6 +539,7 @@ def test_dynamo_attribute_value_ss_value(): attribute_value = AttributeValue(example_attribute_value) assert attribute_value.get_type == AttributeValueType.StringSet + assert attribute_value.ss_value == attribute_value.value def test_event_bridge_event(): From 6f6dd210f93bb52e90defccda85a9f9bb50f3fe1 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 6 Jun 2021 22:19:43 -0700 Subject: [PATCH 3/7] test(data-classes): add missing test for TypeError --- tests/functional/test_data_classes.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py index 4245869a165..07e41f23793 100644 --- a/tests/functional/test_data_classes.py +++ b/tests/functional/test_data_classes.py @@ -542,6 +542,17 @@ def test_dynamo_attribute_value_ss_value(): assert attribute_value.ss_value == attribute_value.value +def test_dynamo_attribute_value_type_error(): + example_attribute_value = {"UNSUPPORTED": "'value' should raise a type error"} + + attribute_value = AttributeValue(example_attribute_value) + + with pytest.raises(TypeError): + print(attribute_value.value) + with pytest.raises(ValueError): + print(attribute_value.get_type) + + def test_event_bridge_event(): event = EventBridgeEvent(load_event("eventBridgeEvent.json")) From e663b668b785c4930fad3e5305e4b0df7d015654 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 6 Jun 2021 22:22:37 -0700 Subject: [PATCH 4/7] chore: bump ci From 989b756e35c63f37bc4a4b3715f108bba613fef3 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Mon, 7 Jun 2021 19:52:50 -0700 Subject: [PATCH 5/7] chore: bump ci From 91f50c406e11f8dfa9bef40d1c2c0943b27b8a38 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Mon, 7 Jun 2021 20:49:55 -0700 Subject: [PATCH 6/7] refactor(data-classes): change to get_value and update docs - Refactor value to get_value - Add code example --- .../data_classes/dynamo_db_stream_event.py | 46 +++++++++++++++---- tests/functional/test_data_classes.py | 22 ++++----- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py b/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py index 54be552bac4..8dad9151eea 100644 --- a/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py +++ b/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py @@ -20,7 +20,10 @@ class AttributeValueType(Enum): class AttributeValue(DictWrapper): """Represents the data for an attribute - Documentation: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_streams_AttributeValue.html + Documentation: + -------------- + - https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_streams_AttributeValue.html + - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html """ def __init__(self, data: Dict[str, Any]): @@ -135,14 +138,6 @@ def get_type(self) -> AttributeValueType: """Get the attribute value type based on the contained data""" return AttributeValueType(self.dynamodb_type) - @property - def value(self) -> Union[Optional[bool], Optional[str], Optional[List], Optional[Dict]]: - """Get the attribute value""" - try: - return getattr(self, f"{self.dynamodb_type.lower()}_value") - except AttributeError: - raise TypeError(f"Dynamodb type {self.dynamodb_type} is not supported") - @property def l_value(self) -> Optional[List["AttributeValue"]]: """Alias of list_value""" @@ -153,6 +148,14 @@ def m_value(self) -> Optional[Dict[str, "AttributeValue"]]: """Alias of map_value""" return self.map_value + @property + def get_value(self) -> Union[Optional[bool], Optional[str], Optional[List], Optional[Dict]]: + """Get the attribute value""" + try: + return getattr(self, f"{self.dynamodb_type.lower()}_value") + except AttributeError: + raise TypeError(f"Dynamodb type {self.dynamodb_type} is not supported") + def _attribute_value_dict(attr_values: Dict[str, dict], key: str) -> Optional[Dict[str, AttributeValue]]: """A dict of type String to AttributeValue object map @@ -271,6 +274,31 @@ class DynamoDBStreamEvent(DictWrapper): Documentation: ------------- - https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html + + Example + ------- + **Process dynamodb stream events and use get_type and get_value for handling conversions** + + from aws_lambda_powertools.utilities.data_classes import event_source, DynamoDBStreamEvent + from aws_lambda_powertools.utilities.data_classes.dynamo_db_stream_event import ( + AttributeValueType, + AttributeValue, + ) + from aws_lambda_powertools.utilities.typing import LambdaContext + + + @event_source(data_class=DynamoDBStreamEvent) + def lambda_handler(event: DynamoDBStreamEvent, context: LambdaContext): + for record in event.records: + key: AttributeValue = record.dynamodb.keys["id"] + if key == AttributeValueType.Number: + assert key.get_value == key.n_value + print(key.get_value) + elif key == AttributeValueType.Map: + assert key.get_value == key.map_value + print(key.get_value) + + """ @property diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py index 07e41f23793..8b412860694 100644 --- a/tests/functional/test_data_classes.py +++ b/tests/functional/test_data_classes.py @@ -450,7 +450,7 @@ def test_dynamo_attribute_value_b_value(): attribute_value = AttributeValue(example_attribute_value) assert attribute_value.get_type == AttributeValueType.Binary - assert attribute_value.b_value == attribute_value.value + assert attribute_value.b_value == attribute_value.get_value def test_dynamo_attribute_value_bs_value(): @@ -459,7 +459,7 @@ def test_dynamo_attribute_value_bs_value(): attribute_value = AttributeValue(example_attribute_value) assert attribute_value.get_type == AttributeValueType.BinarySet - assert attribute_value.bs_value == attribute_value.value + assert attribute_value.bs_value == attribute_value.get_value def test_dynamo_attribute_value_bool_value(): @@ -468,7 +468,7 @@ def test_dynamo_attribute_value_bool_value(): attribute_value = AttributeValue(example_attribute_value) assert attribute_value.get_type == AttributeValueType.Boolean - assert attribute_value.bool_value == attribute_value.value + assert attribute_value.bool_value == attribute_value.get_value def test_dynamo_attribute_value_list_value(): @@ -480,7 +480,7 @@ def test_dynamo_attribute_value_list_value(): assert item.s_value == "Cookies" assert attribute_value.get_type == AttributeValueType.List assert attribute_value.l_value == attribute_value.list_value - assert attribute_value.list_value == attribute_value.value + assert attribute_value.list_value == attribute_value.get_value def test_dynamo_attribute_value_map_value(): @@ -494,7 +494,7 @@ def test_dynamo_attribute_value_map_value(): assert item.s_value == "Joe" assert attribute_value.get_type == AttributeValueType.Map assert attribute_value.m_value == attribute_value.map_value - assert attribute_value.map_value == attribute_value.value + assert attribute_value.map_value == attribute_value.get_value def test_dynamo_attribute_value_n_value(): @@ -503,7 +503,7 @@ def test_dynamo_attribute_value_n_value(): attribute_value = AttributeValue(example_attribute_value) assert attribute_value.get_type == AttributeValueType.Number - assert attribute_value.n_value == attribute_value.value + assert attribute_value.n_value == attribute_value.get_value def test_dynamo_attribute_value_ns_value(): @@ -512,7 +512,7 @@ def test_dynamo_attribute_value_ns_value(): attribute_value = AttributeValue(example_attribute_value) assert attribute_value.get_type == AttributeValueType.NumberSet - assert attribute_value.ns_value == attribute_value.value + assert attribute_value.ns_value == attribute_value.get_value def test_dynamo_attribute_value_null_value(): @@ -521,7 +521,7 @@ def test_dynamo_attribute_value_null_value(): attribute_value = AttributeValue(example_attribute_value) assert attribute_value.get_type == AttributeValueType.Null - assert attribute_value.null_value == attribute_value.value + assert attribute_value.null_value == attribute_value.get_value def test_dynamo_attribute_value_s_value(): @@ -530,7 +530,7 @@ def test_dynamo_attribute_value_s_value(): attribute_value = AttributeValue(example_attribute_value) assert attribute_value.get_type == AttributeValueType.String - assert attribute_value.s_value == attribute_value.value + assert attribute_value.s_value == attribute_value.get_value def test_dynamo_attribute_value_ss_value(): @@ -539,7 +539,7 @@ def test_dynamo_attribute_value_ss_value(): attribute_value = AttributeValue(example_attribute_value) assert attribute_value.get_type == AttributeValueType.StringSet - assert attribute_value.ss_value == attribute_value.value + assert attribute_value.ss_value == attribute_value.get_value def test_dynamo_attribute_value_type_error(): @@ -548,7 +548,7 @@ def test_dynamo_attribute_value_type_error(): attribute_value = AttributeValue(example_attribute_value) with pytest.raises(TypeError): - print(attribute_value.value) + print(attribute_value.get_value) with pytest.raises(ValueError): print(attribute_value.get_type) From ca79f4a0d5f1243ac9d8f478f789191b4f51e3ed Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Mon, 7 Jun 2021 21:02:01 -0700 Subject: [PATCH 7/7] chore: remove ws --- .../utilities/data_classes/dynamo_db_stream_event.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py b/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py index 8dad9151eea..1ec3d6157bf 100644 --- a/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py +++ b/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py @@ -297,8 +297,6 @@ def lambda_handler(event: DynamoDBStreamEvent, context: LambdaContext): elif key == AttributeValueType.Map: assert key.get_value == key.map_value print(key.get_value) - - """ @property