Skip to content

Commit

Permalink
feat(data-classes): add AttributeValueType to DynamoDBStreamEvent (#462)
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael Brewer authored Jun 8, 2021
1 parent b029b5c commit a65e55c
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,42 @@
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


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
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]):
"""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]:
"""An attribute of type Base64-encoded binary data object
Expand Down Expand Up @@ -106,6 +133,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 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

@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
Expand Down Expand Up @@ -224,6 +274,29 @@ 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
Expand Down
90 changes: 90 additions & 0 deletions tests/functional/test_data_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
)
from aws_lambda_powertools.utilities.data_classes.dynamo_db_stream_event import (
AttributeValue,
AttributeValueType,
DynamoDBRecordEventName,
DynamoDBStreamEvent,
StreamViewType,
Expand Down Expand Up @@ -443,13 +444,43 @@ 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
assert attribute_value.b_value == attribute_value.get_value


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
assert attribute_value.bs_value == attribute_value.get_value


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
assert attribute_value.bool_value == attribute_value.get_value


def test_dynamo_attribute_value_list_value():
example_attribute_value = {"L": [{"S": "Cookies"}, {"S": "Coffee"}, {"N": "3.14159"}]}
attribute_value = AttributeValue(example_attribute_value)
list_value = 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
assert attribute_value.l_value == attribute_value.list_value
assert attribute_value.list_value == attribute_value.get_value


def test_dynamo_attribute_value_map_value():
Expand All @@ -461,6 +492,65 @@ 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
assert attribute_value.m_value == attribute_value.map_value
assert attribute_value.map_value == attribute_value.get_value


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
assert attribute_value.n_value == attribute_value.get_value


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
assert attribute_value.ns_value == attribute_value.get_value


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
assert attribute_value.null_value == attribute_value.get_value


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
assert attribute_value.s_value == attribute_value.get_value


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
assert attribute_value.ss_value == attribute_value.get_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.get_value)
with pytest.raises(ValueError):
print(attribute_value.get_type)


def test_event_bridge_event():
Expand Down

0 comments on commit a65e55c

Please sign in to comment.