Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit bf0ae43

Browse files
shanabheitorlessa
andauthoredOct 19, 2022
feat(data-classes): replace AttributeValue in DynamoDBStreamEvent with deserialized Python values (#1619)
Co-authored-by: heitorlessa <[email protected]>
1 parent 2864b46 commit bf0ae43

File tree

7 files changed

+225
-309
lines changed

7 files changed

+225
-309
lines changed
 

‎aws_lambda_powertools/utilities/batch/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,10 +323,10 @@ def lambda_handler(event, context: LambdaContext):
323323
@tracer.capture_method
324324
def record_handler(record: DynamoDBRecord):
325325
logger.info(record.dynamodb.new_image)
326-
payload: dict = json.loads(record.dynamodb.new_image.get("item").s_value)
326+
payload: dict = json.loads(record.dynamodb.new_image.get("item"))
327327
# alternatively:
328-
# changes: Dict[str, dynamo_db_stream_event.AttributeValue] = record.dynamodb.new_image # noqa: E800
329-
# payload = change.get("Message").raw_event -> {"S": "<payload>"}
328+
# changes: Dict[str, Any] = record.dynamodb.new_image # noqa: E800
329+
# payload = change.get("Message") -> "<payload>"
330330
...
331331
332332
@logger.inject_lambda_context

‎aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py

Lines changed: 107 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,169 +1,100 @@
1+
from decimal import Clamped, Context, Decimal, Inexact, Overflow, Rounded, Underflow
12
from enum import Enum
2-
from typing import Any, Dict, Iterator, List, Optional, Union
3+
from typing import Any, Callable, Dict, Iterator, Optional, Sequence, Set
34

45
from aws_lambda_powertools.utilities.data_classes.common import DictWrapper
56

7+
# NOTE: DynamoDB supports up to 38 digits precision
8+
# Therefore, this ensures our Decimal follows what's stored in the table
9+
DYNAMODB_CONTEXT = Context(
10+
Emin=-128,
11+
Emax=126,
12+
prec=38,
13+
traps=[Clamped, Overflow, Inexact, Rounded, Underflow],
14+
)
615

7-
class AttributeValueType(Enum):
8-
Binary = "B"
9-
BinarySet = "BS"
10-
Boolean = "BOOL"
11-
List = "L"
12-
Map = "M"
13-
Number = "N"
14-
NumberSet = "NS"
15-
Null = "NULL"
16-
String = "S"
17-
StringSet = "SS"
1816

17+
class TypeDeserializer:
18+
"""
19+
Deserializes DynamoDB types to Python types.
1920
20-
class AttributeValue(DictWrapper):
21-
"""Represents the data for an attribute
21+
It's based on boto3's [DynamoDB TypeDeserializer](https://boto3.amazonaws.com/v1/documentation/api/latest/_modules/boto3/dynamodb/types.html). # noqa: E501
2222
23-
Documentation:
24-
--------------
25-
- https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_streams_AttributeValue.html
26-
- https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html
23+
The only notable difference is that for Binary (`B`, `BS`) values we return Python Bytes directly,
24+
since we don't support Python 2.
2725
"""
2826

29-
def __init__(self, data: Dict[str, Any]):
30-
"""AttributeValue constructor
27+
def deserialize(self, value: Dict) -> Any:
28+
"""Deserialize DynamoDB data types into Python types.
3129
3230
Parameters
3331
----------
34-
data: Dict[str, Any]
35-
Raw lambda event dict
36-
"""
37-
super().__init__(data)
38-
self.dynamodb_type = list(data.keys())[0]
32+
value: Any
33+
DynamoDB value to be deserialized to a python type
3934
40-
@property
41-
def b_value(self) -> Optional[str]:
42-
"""An attribute of type Base64-encoded binary data object
4335
44-
Example:
45-
>>> {"B": "dGhpcyB0ZXh0IGlzIGJhc2U2NC1lbmNvZGVk"}
46-
"""
47-
return self.get("B")
36+
Here are the various conversions:
4837
49-
@property
50-
def bs_value(self) -> Optional[List[str]]:
51-
"""An attribute of type Array of Base64-encoded binary data objects
52-
53-
Example:
54-
>>> {"BS": ["U3Vubnk=", "UmFpbnk=", "U25vd3k="]}
55-
"""
56-
return self.get("BS")
57-
58-
@property
59-
def bool_value(self) -> Optional[bool]:
60-
"""An attribute of type Boolean
61-
62-
Example:
63-
>>> {"BOOL": True}
64-
"""
65-
item = self.get("BOOL")
66-
return None if item is None else bool(item)
38+
DynamoDB Python
39+
-------- ------
40+
{'NULL': True} None
41+
{'BOOL': True/False} True/False
42+
{'N': str(value)} str(value)
43+
{'S': string} string
44+
{'B': bytes} bytes
45+
{'NS': [str(value)]} set([str(value)])
46+
{'SS': [string]} set([string])
47+
{'BS': [bytes]} set([bytes])
48+
{'L': list} list
49+
{'M': dict} dict
6750
68-
@property
69-
def list_value(self) -> Optional[List["AttributeValue"]]:
70-
"""An attribute of type Array of AttributeValue objects
71-
72-
Example:
73-
>>> {"L": [ {"S": "Cookies"} , {"S": "Coffee"}, {"N": "3.14159"}]}
74-
"""
75-
item = self.get("L")
76-
return None if item is None else [AttributeValue(v) for v in item]
77-
78-
@property
79-
def map_value(self) -> Optional[Dict[str, "AttributeValue"]]:
80-
"""An attribute of type String to AttributeValue object map
81-
82-
Example:
83-
>>> {"M": {"Name": {"S": "Joe"}, "Age": {"N": "35"}}}
84-
"""
85-
return _attribute_value_dict(self._data, "M")
86-
87-
@property
88-
def n_value(self) -> Optional[str]:
89-
"""An attribute of type Number
90-
91-
Numbers are sent across the network to DynamoDB as strings, to maximize compatibility across languages
92-
and libraries. However, DynamoDB treats them as number type attributes for mathematical operations.
51+
Parameters
52+
----------
53+
value: Any
54+
DynamoDB value to be deserialized to a python type
9355
94-
Example:
95-
>>> {"N": "123.45"}
56+
Returns
57+
--------
58+
any
59+
Python native type converted from DynamoDB type
9660
"""
97-
return self.get("N")
98-
99-
@property
100-
def ns_value(self) -> Optional[List[str]]:
101-
"""An attribute of type Number Set
10261

103-
Example:
104-
>>> {"NS": ["42.2", "-19", "7.5", "3.14"]}
105-
"""
106-
return self.get("NS")
62+
dynamodb_type = list(value.keys())[0]
63+
deserializer: Optional[Callable] = getattr(self, f"_deserialize_{dynamodb_type}".lower(), None)
64+
if deserializer is None:
65+
raise TypeError(f"Dynamodb type {dynamodb_type} is not supported")
10766

108-
@property
109-
def null_value(self) -> None:
110-
"""An attribute of type Null.
67+
return deserializer(value[dynamodb_type])
11168

112-
Example:
113-
>>> {"NULL": True}
114-
"""
69+
def _deserialize_null(self, value: bool) -> None:
11570
return None
11671

117-
@property
118-
def s_value(self) -> Optional[str]:
119-
"""An attribute of type String
72+
def _deserialize_bool(self, value: bool) -> bool:
73+
return value
12074

121-
Example:
122-
>>> {"S": "Hello"}
123-
"""
124-
return self.get("S")
75+
def _deserialize_n(self, value: str) -> Decimal:
76+
return DYNAMODB_CONTEXT.create_decimal(value)
12577

126-
@property
127-
def ss_value(self) -> Optional[List[str]]:
128-
"""An attribute of type Array of strings
78+
def _deserialize_s(self, value: str) -> str:
79+
return value
12980

130-
Example:
131-
>>> {"SS": ["Giraffe", "Hippo" ,"Zebra"]}
132-
"""
133-
return self.get("SS")
81+
def _deserialize_b(self, value: bytes) -> bytes:
82+
return value
13483

135-
@property
136-
def get_type(self) -> AttributeValueType:
137-
"""Get the attribute value type based on the contained data"""
138-
return AttributeValueType(self.dynamodb_type)
84+
def _deserialize_ns(self, value: Sequence[str]) -> Set[Decimal]:
85+
return set(map(self._deserialize_n, value))
13986

140-
@property
141-
def l_value(self) -> Optional[List["AttributeValue"]]:
142-
"""Alias of list_value"""
143-
return self.list_value
87+
def _deserialize_ss(self, value: Sequence[str]) -> Set[str]:
88+
return set(map(self._deserialize_s, value))
14489

145-
@property
146-
def m_value(self) -> Optional[Dict[str, "AttributeValue"]]:
147-
"""Alias of map_value"""
148-
return self.map_value
90+
def _deserialize_bs(self, value: Sequence[bytes]) -> Set[bytes]:
91+
return set(map(self._deserialize_b, value))
14992

150-
@property
151-
def get_value(self) -> Union[Optional[bool], Optional[str], Optional[List], Optional[Dict]]:
152-
"""Get the attribute value"""
153-
try:
154-
return getattr(self, f"{self.dynamodb_type.lower()}_value")
155-
except AttributeError:
156-
raise TypeError(f"Dynamodb type {self.dynamodb_type} is not supported")
93+
def _deserialize_l(self, value: Sequence[Dict]) -> Sequence[Any]:
94+
return [self.deserialize(v) for v in value]
15795

158-
159-
def _attribute_value_dict(attr_values: Dict[str, dict], key: str) -> Optional[Dict[str, AttributeValue]]:
160-
"""A dict of type String to AttributeValue object map
161-
162-
Example:
163-
>>> {"NewImage": {"Id": {"S": "xxx-xxx"}, "Value": {"N": "35"}}}
164-
"""
165-
attr_values_dict = attr_values.get(key)
166-
return None if attr_values_dict is None else {k: AttributeValue(v) for k, v in attr_values_dict.items()}
96+
def _deserialize_m(self, value: Dict) -> Dict:
97+
return {k: self.deserialize(v) for k, v in value.items()}
16798

16899

169100
class StreamViewType(Enum):
@@ -176,28 +107,57 @@ class StreamViewType(Enum):
176107

177108

178109
class StreamRecord(DictWrapper):
110+
_deserializer = TypeDeserializer()
111+
112+
def __init__(self, data: Dict[str, Any]):
113+
"""StreamRecord constructor
114+
Parameters
115+
----------
116+
data: Dict[str, Any]
117+
Represents the dynamodb dict inside DynamoDBStreamEvent's records
118+
"""
119+
super().__init__(data)
120+
self._deserializer = TypeDeserializer()
121+
122+
def _deserialize_dynamodb_dict(self, key: str) -> Optional[Dict[str, Any]]:
123+
"""Deserialize DynamoDB records available in `Keys`, `NewImage`, and `OldImage`
124+
125+
Parameters
126+
----------
127+
key : str
128+
DynamoDB key (e.g., Keys, NewImage, or OldImage)
129+
130+
Returns
131+
-------
132+
Optional[Dict[str, Any]]
133+
Deserialized records in Python native types
134+
"""
135+
dynamodb_dict = self._data.get(key)
136+
if dynamodb_dict is None:
137+
return None
138+
139+
return {k: self._deserializer.deserialize(v) for k, v in dynamodb_dict.items()}
140+
179141
@property
180142
def approximate_creation_date_time(self) -> Optional[int]:
181143
"""The approximate date and time when the stream record was created, in UNIX epoch time format."""
182144
item = self.get("ApproximateCreationDateTime")
183145
return None if item is None else int(item)
184146

185-
# NOTE: This override breaks the Mapping protocol of DictWrapper, it's left here for backwards compatibility with
186-
# a 'type: ignore' comment. See #1516 for discussion
187147
@property
188-
def keys(self) -> Optional[Dict[str, AttributeValue]]: # type: ignore[override]
148+
def keys(self) -> Optional[Dict[str, Any]]: # type: ignore[override]
189149
"""The primary key attribute(s) for the DynamoDB item that was modified."""
190-
return _attribute_value_dict(self._data, "Keys")
150+
return self._deserialize_dynamodb_dict("Keys")
191151

192152
@property
193-
def new_image(self) -> Optional[Dict[str, AttributeValue]]:
153+
def new_image(self) -> Optional[Dict[str, Any]]:
194154
"""The item in the DynamoDB table as it appeared after it was modified."""
195-
return _attribute_value_dict(self._data, "NewImage")
155+
return self._deserialize_dynamodb_dict("NewImage")
196156

197157
@property
198-
def old_image(self) -> Optional[Dict[str, AttributeValue]]:
158+
def old_image(self) -> Optional[Dict[str, Any]]:
199159
"""The item in the DynamoDB table as it appeared before it was modified."""
200-
return _attribute_value_dict(self._data, "OldImage")
160+
return self._deserialize_dynamodb_dict("OldImage")
201161

202162
@property
203163
def sequence_number(self) -> Optional[str]:
@@ -233,7 +193,7 @@ def aws_region(self) -> Optional[str]:
233193

234194
@property
235195
def dynamodb(self) -> Optional[StreamRecord]:
236-
"""The main body of the stream record, containing all the DynamoDB-specific fields."""
196+
"""The main body of the stream record, containing all the DynamoDB-specific dicts."""
237197
stream_record = self.get("dynamodb")
238198
return None if stream_record is None else StreamRecord(stream_record)
239199

@@ -278,26 +238,18 @@ class DynamoDBStreamEvent(DictWrapper):
278238
279239
Example
280240
-------
281-
**Process dynamodb stream events and use get_type and get_value for handling conversions**
241+
**Process dynamodb stream events. DynamoDB types are automatically converted to their equivalent Python values.**
282242
283243
from aws_lambda_powertools.utilities.data_classes import event_source, DynamoDBStreamEvent
284-
from aws_lambda_powertools.utilities.data_classes.dynamo_db_stream_event import (
285-
AttributeValueType,
286-
AttributeValue,
287-
)
288244
from aws_lambda_powertools.utilities.typing import LambdaContext
289245
290246
291247
@event_source(data_class=DynamoDBStreamEvent)
292248
def lambda_handler(event: DynamoDBStreamEvent, context: LambdaContext):
293249
for record in event.records:
294-
key: AttributeValue = record.dynamodb.keys["id"]
295-
if key == AttributeValueType.Number:
296-
assert key.get_value == key.n_value
297-
print(key.get_value)
298-
elif key == AttributeValueType.Map:
299-
assert key.get_value == key.map_value
300-
print(key.get_value)
250+
# {"N": "123.45"} => Decimal("123.45")
251+
key: str = record.dynamodb.keys["id"]
252+
print(key)
301253
"""
302254

303255
@property

‎docs/upgrade.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Changes at a glance:
1414
* The **legacy SQS batch processor** was removed.
1515
* The **Idempotency key** format changed slightly, invalidating all the existing cached results.
1616
* The **Feature Flags and AppConfig Parameter utility** API calls have changed and you must update your IAM permissions.
17+
* The **`DynamoDBStreamEvent`** replaced `AttributeValue` with native Python types.
1718

1819
???+ important
1920
Powertools for Python v2 drops suport for Python 3.6, following the Python 3.6 End-Of-Life (EOL) reached on December 23, 2021.
@@ -161,3 +162,47 @@ Using qualified names prevents distinct functions with the same name to contend
161162
AWS AppConfig deprecated the current API (GetConfiguration) - [more details here](https://github.com/awslabs/aws-lambda-powertools-python/issues/1506#issuecomment-1266645884).
162163
163164
You must update your IAM permissions to allow `appconfig:GetLatestConfiguration` and `appconfig:StartConfigurationSession`. There are no code changes required.
165+
166+
## DynamoDBStreamEvent in Event Source Data Classes
167+
168+
???+ info
169+
This also applies if you're using [**`BatchProcessor`**](https://awslabs.github.io/aws-lambda-powertools-python/latest/utilities/batch/#processing-messages-from-dynamodb){target="_blank"} to handle DynamoDB Stream events.
170+
171+
You will now receive native Python types when accessing DynamoDB records via `keys`, `new_image`, and `old_image` attributes in `DynamoDBStreamEvent`.
172+
173+
Previously, you'd receive a `AttributeValue` instance and need to deserialize each item to the type you'd want for convenience, or to the type DynamoDB stored via `get_value` method.
174+
175+
With this change, you can access data deserialized as stored in DynamoDB, and no longer need to recursively deserialize nested objects (Maps) if you had them.
176+
177+
???+ note
178+
For a lossless conversion of DynamoDB `Number` type, we follow AWS Python SDK (boto3) approach and convert to `Decimal`.
179+
180+
```python hl_lines="15-20 24-25"
181+
from aws_lambda_powertools.utilities.data_classes.dynamo_db_stream_event import (
182+
DynamoDBStreamEvent,
183+
DynamoDBRecordEventName
184+
)
185+
186+
def send_to_sqs(data: Dict):
187+
body = json.dumps(data)
188+
...
189+
190+
@event_source(data_class=DynamoDBStreamEvent)
191+
def lambda_handler(event: DynamoDBStreamEvent, context):
192+
for record in event.records:
193+
194+
# BEFORE
195+
new_image: Dict[str, AttributeValue] = record.dynamodb.new_image
196+
event_type: AttributeValue = new_image["eventType"].get_value
197+
if event_type == "PENDING":
198+
# deserialize attribute value into Python native type
199+
# NOTE: nested objects would need additional logic
200+
data = {k: v.get_value for k, v in image.items()}
201+
send_to_sqs(data)
202+
203+
# AFTER
204+
new_image: Dict[str, Any] = record.dynamodb.new_image
205+
if new_image.get("eventType") == "PENDING":
206+
send_to_sqs(new_image) # Here new_image is just a Python Dict type
207+
208+
```

‎docs/utilities/batch.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -506,9 +506,9 @@ Processing batches from Kinesis works in four stages:
506506
@tracer.capture_method
507507
def record_handler(record: DynamoDBRecord):
508508
logger.info(record.dynamodb.new_image)
509-
payload: dict = json.loads(record.dynamodb.new_image.get("Message").get_value)
509+
payload: dict = json.loads(record.dynamodb.new_image.get("Message"))
510510
# alternatively:
511-
# changes: Dict[str, dynamo_db_stream_event.AttributeValue] = record.dynamodb.new_image
511+
# changes: Dict[str, Any] = record.dynamodb.new_image
512512
# payload = change.get("Message").raw_event -> {"S": "<payload>"}
513513
...
514514

@@ -538,10 +538,10 @@ Processing batches from Kinesis works in four stages:
538538
@tracer.capture_method
539539
def record_handler(record: DynamoDBRecord):
540540
logger.info(record.dynamodb.new_image)
541-
payload: dict = json.loads(record.dynamodb.new_image.get("item").s_value)
541+
payload: dict = json.loads(record.dynamodb.new_image.get("item"))
542542
# alternatively:
543-
# changes: Dict[str, dynamo_db_stream_event.AttributeValue] = record.dynamodb.new_image
544-
# payload = change.get("Message").raw_event -> {"S": "<payload>"}
543+
# changes: Dict[str, Any] = record.dynamodb.new_image
544+
# payload = change.get("Message") -> "<payload>"
545545
...
546546

547547
@logger.inject_lambda_context

‎docs/utilities/data_classes.md

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -797,9 +797,9 @@ This example is based on the AWS Cognito docs for [Verify Auth Challenge Respons
797797

798798
### DynamoDB Streams
799799

800-
The DynamoDB data class utility provides the base class for `DynamoDBStreamEvent`, a typed class for
801-
attributes values (`AttributeValue`), as well as enums for stream view type (`StreamViewType`) and event type
800+
The DynamoDB data class utility provides the base class for `DynamoDBStreamEvent`, as well as enums for stream view type (`StreamViewType`) and event type.
802801
(`DynamoDBRecordEventName`).
802+
The class automatically deserializes DynamoDB types into their equivalent Python types.
803803

804804
=== "app.py"
805805

@@ -823,21 +823,15 @@ attributes values (`AttributeValue`), as well as enums for stream view type (`St
823823

824824
```python
825825
from aws_lambda_powertools.utilities.data_classes import event_source, DynamoDBStreamEvent
826-
from aws_lambda_powertools.utilities.data_classes.dynamo_db_stream_event import AttributeValueType, AttributeValue
827826
from aws_lambda_powertools.utilities.typing import LambdaContext
828827

829828

830829
@event_source(data_class=DynamoDBStreamEvent)
831830
def lambda_handler(event: DynamoDBStreamEvent, context: LambdaContext):
832831
for record in event.records:
833-
key: AttributeValue = record.dynamodb.keys["id"]
834-
if key == AttributeValueType.Number:
835-
# {"N": "123.45"} => "123.45"
836-
assert key.get_value == key.n_value
837-
print(key.get_value)
838-
elif key == AttributeValueType.Map:
839-
assert key.get_value == key.map_value
840-
print(key.get_value)
832+
# {"N": "123.45"} => Decimal("123.45")
833+
key: str = record.dynamodb.keys["id"]
834+
print(key)
841835
```
842836

843837
### EventBridge

‎tests/functional/test_data_classes.py

Lines changed: 59 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import datetime
33
import json
44
import zipfile
5+
from decimal import Clamped, Context, Inexact, Overflow, Rounded, Underflow
56
from secrets import compare_digest
67
from urllib.parse import quote_plus
78

@@ -75,8 +76,6 @@
7576
ConnectContactFlowInitiationMethod,
7677
)
7778
from aws_lambda_powertools.utilities.data_classes.dynamo_db_stream_event import (
78-
AttributeValue,
79-
AttributeValueType,
8079
DynamoDBRecordEventName,
8180
DynamoDBStreamEvent,
8281
StreamRecord,
@@ -490,7 +489,13 @@ def test_connect_contact_flow_event_all():
490489
assert event.parameters == {"ParameterOne": "One", "ParameterTwo": "Two"}
491490

492491

493-
def test_dynamo_db_stream_trigger_event():
492+
def test_dynamodb_stream_trigger_event():
493+
decimal_context = Context(
494+
Emin=-128,
495+
Emax=126,
496+
prec=38,
497+
traps=[Clamped, Overflow, Inexact, Rounded, Underflow],
498+
)
494499
event = DynamoDBStreamEvent(load_event("dynamoStreamEvent.json"))
495500

496501
records = list(event.records)
@@ -502,20 +507,8 @@ def test_dynamo_db_stream_trigger_event():
502507
assert dynamodb.approximate_creation_date_time is None
503508
keys = dynamodb.keys
504509
assert keys is not None
505-
id_key = keys["Id"]
506-
assert id_key.b_value is None
507-
assert id_key.bs_value is None
508-
assert id_key.bool_value is None
509-
assert id_key.list_value is None
510-
assert id_key.map_value is None
511-
assert id_key.n_value == "101"
512-
assert id_key.ns_value is None
513-
assert id_key.null_value is None
514-
assert id_key.s_value is None
515-
assert id_key.ss_value is None
516-
message_key = dynamodb.new_image["Message"]
517-
assert message_key is not None
518-
assert message_key.s_value == "New item!"
510+
assert keys["Id"] == decimal_context.create_decimal(101)
511+
assert dynamodb.new_image["Message"] == "New item!"
519512
assert dynamodb.old_image is None
520513
assert dynamodb.sequence_number == "111"
521514
assert dynamodb.size_bytes == 26
@@ -528,129 +521,61 @@ def test_dynamo_db_stream_trigger_event():
528521
assert record.user_identity is None
529522

530523

531-
def test_dynamo_attribute_value_b_value():
532-
example_attribute_value = {"B": "dGhpcyB0ZXh0IGlzIGJhc2U2NC1lbmNvZGVk"}
533-
534-
attribute_value = AttributeValue(example_attribute_value)
535-
536-
assert attribute_value.get_type == AttributeValueType.Binary
537-
assert attribute_value.b_value == attribute_value.get_value
538-
539-
540-
def test_dynamo_attribute_value_bs_value():
541-
example_attribute_value = {"BS": ["U3Vubnk=", "UmFpbnk=", "U25vd3k="]}
542-
543-
attribute_value = AttributeValue(example_attribute_value)
544-
545-
assert attribute_value.get_type == AttributeValueType.BinarySet
546-
assert attribute_value.bs_value == attribute_value.get_value
547-
548-
549-
def test_dynamo_attribute_value_bool_value():
550-
example_attribute_value = {"BOOL": True}
551-
552-
attribute_value = AttributeValue(example_attribute_value)
553-
554-
assert attribute_value.get_type == AttributeValueType.Boolean
555-
assert attribute_value.bool_value == attribute_value.get_value
556-
557-
558-
def test_dynamo_attribute_value_list_value():
559-
example_attribute_value = {"L": [{"S": "Cookies"}, {"S": "Coffee"}, {"N": "3.14159"}]}
560-
attribute_value = AttributeValue(example_attribute_value)
561-
list_value = attribute_value.list_value
562-
assert list_value is not None
563-
item = list_value[0]
564-
assert item.s_value == "Cookies"
565-
assert attribute_value.get_type == AttributeValueType.List
566-
assert attribute_value.l_value == attribute_value.list_value
567-
assert attribute_value.list_value == attribute_value.get_value
568-
569-
570-
def test_dynamo_attribute_value_map_value():
571-
example_attribute_value = {"M": {"Name": {"S": "Joe"}, "Age": {"N": "35"}}}
572-
573-
attribute_value = AttributeValue(example_attribute_value)
574-
575-
map_value = attribute_value.map_value
576-
assert map_value is not None
577-
item = map_value["Name"]
578-
assert item.s_value == "Joe"
579-
assert attribute_value.get_type == AttributeValueType.Map
580-
assert attribute_value.m_value == attribute_value.map_value
581-
assert attribute_value.map_value == attribute_value.get_value
582-
583-
584-
def test_dynamo_attribute_value_n_value():
585-
example_attribute_value = {"N": "123.45"}
586-
587-
attribute_value = AttributeValue(example_attribute_value)
588-
589-
assert attribute_value.get_type == AttributeValueType.Number
590-
assert attribute_value.n_value == attribute_value.get_value
591-
592-
593-
def test_dynamo_attribute_value_ns_value():
594-
example_attribute_value = {"NS": ["42.2", "-19", "7.5", "3.14"]}
595-
596-
attribute_value = AttributeValue(example_attribute_value)
597-
598-
assert attribute_value.get_type == AttributeValueType.NumberSet
599-
assert attribute_value.ns_value == attribute_value.get_value
600-
601-
602-
def test_dynamo_attribute_value_null_value():
603-
example_attribute_value = {"NULL": True}
604-
605-
attribute_value = AttributeValue(example_attribute_value)
606-
607-
assert attribute_value.get_type == AttributeValueType.Null
608-
assert attribute_value.null_value is None
609-
assert attribute_value.null_value == attribute_value.get_value
610-
611-
612-
def test_dynamo_attribute_value_s_value():
613-
example_attribute_value = {"S": "Hello"}
614-
615-
attribute_value = AttributeValue(example_attribute_value)
616-
617-
assert attribute_value.get_type == AttributeValueType.String
618-
assert attribute_value.s_value == attribute_value.get_value
619-
620-
621-
def test_dynamo_attribute_value_ss_value():
622-
example_attribute_value = {"SS": ["Giraffe", "Hippo", "Zebra"]}
623-
624-
attribute_value = AttributeValue(example_attribute_value)
625-
626-
assert attribute_value.get_type == AttributeValueType.StringSet
627-
assert attribute_value.ss_value == attribute_value.get_value
628-
629-
630-
def test_dynamo_attribute_value_type_error():
631-
example_attribute_value = {"UNSUPPORTED": "'value' should raise a type error"}
632-
633-
attribute_value = AttributeValue(example_attribute_value)
634-
635-
with pytest.raises(TypeError):
636-
print(attribute_value.get_value)
637-
with pytest.raises(ValueError):
638-
print(attribute_value.get_type)
639-
640-
641-
def test_stream_record_keys_with_valid_keys():
642-
attribute_value = {"Foo": "Bar"}
643-
record = StreamRecord({"Keys": {"Key1": attribute_value}})
644-
assert record.keys == {"Key1": AttributeValue(attribute_value)}
524+
def test_dynamodb_stream_record_deserialization():
525+
byte_list = [s.encode("utf-8") for s in ["item1", "item2"]]
526+
decimal_context = Context(
527+
Emin=-128,
528+
Emax=126,
529+
prec=38,
530+
traps=[Clamped, Overflow, Inexact, Rounded, Underflow],
531+
)
532+
data = {
533+
"Keys": {"key1": {"attr1": "value1"}},
534+
"NewImage": {
535+
"Name": {"S": "Joe"},
536+
"Age": {"N": "35"},
537+
"TypesMap": {
538+
"M": {
539+
"string": {"S": "value"},
540+
"number": {"N": "100"},
541+
"bool": {"BOOL": True},
542+
"dict": {"M": {"key": {"S": "value"}}},
543+
"stringSet": {"SS": ["item1", "item2"]},
544+
"numberSet": {"NS": ["100", "200", "300"]},
545+
"binary": {"B": b"\x00"},
546+
"byteSet": {"BS": byte_list},
547+
"list": {"L": [{"S": "item1"}, {"N": "3.14159"}, {"BOOL": False}]},
548+
"null": {"NULL": True},
549+
},
550+
},
551+
},
552+
}
553+
record = StreamRecord(data)
554+
assert record.new_image == {
555+
"Name": "Joe",
556+
"Age": decimal_context.create_decimal("35"),
557+
"TypesMap": {
558+
"string": "value",
559+
"number": decimal_context.create_decimal("100"),
560+
"bool": True,
561+
"dict": {"key": "value"},
562+
"stringSet": {"item1", "item2"},
563+
"numberSet": {decimal_context.create_decimal(n) for n in ["100", "200", "300"]},
564+
"binary": b"\x00",
565+
"byteSet": set(byte_list),
566+
"list": ["item1", decimal_context.create_decimal("3.14159"), False],
567+
"null": None,
568+
},
569+
}
645570

646571

647-
def test_stream_record_keys_with_no_keys():
572+
def test_dynamodb_stream_record_keys_with_no_keys():
648573
record = StreamRecord({})
649574
assert record.keys is None
650575

651576

652-
def test_stream_record_keys_overrides_dict_wrapper_keys():
653-
data = {"Keys": {"key1": {"attr1": "value1"}}}
577+
def test_dynamodb_stream_record_keys_overrides_dict_wrapper_keys():
578+
data = {"Keys": {"key1": {"N": "101"}}}
654579
record = StreamRecord(data)
655580
assert record.keys != data.keys()
656581

‎tests/functional/test_utilities_batch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def handler(record: KinesisStreamRecord):
129129
@pytest.fixture(scope="module")
130130
def dynamodb_record_handler() -> Callable:
131131
def handler(record: DynamoDBRecord):
132-
body = record.dynamodb.new_image.get("Message").get_value
132+
body = record.dynamodb.new_image.get("Message")
133133
if "fail" in body:
134134
raise Exception("Failed to process record.")
135135
return body

0 commit comments

Comments
 (0)
Please sign in to comment.