1
+ from decimal import Clamped , Context , Decimal , Inexact , Overflow , Rounded , Underflow
1
2
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
3
4
4
5
from aws_lambda_powertools .utilities .data_classes .common import DictWrapper
5
6
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
+ )
6
15
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"
18
16
17
+ class TypeDeserializer :
18
+ """
19
+ Deserializes DynamoDB types to Python types.
19
20
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
22
22
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.
27
25
"""
28
26
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.
31
29
32
30
Parameters
33
31
----------
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
39
34
40
- @property
41
- def b_value (self ) -> Optional [str ]:
42
- """An attribute of type Base64-encoded binary data object
43
35
44
- Example:
45
- >>> {"B": "dGhpcyB0ZXh0IGlzIGJhc2U2NC1lbmNvZGVk"}
46
- """
47
- return self .get ("B" )
36
+ Here are the various conversions:
48
37
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
67
50
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
93
55
94
- Example:
95
- >>> {"N": "123.45"}
56
+ Returns
57
+ --------
58
+ any
59
+ Python native type converted from DynamoDB type
96
60
"""
97
- return self .get ("N" )
98
-
99
- @property
100
- def ns_value (self ) -> Optional [List [str ]]:
101
- """An attribute of type Number Set
102
61
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 " )
107
66
108
- @property
109
- def null_value (self ) -> None :
110
- """An attribute of type Null.
67
+ return deserializer (value [dynamodb_type ])
111
68
112
- Example:
113
- >>> {"NULL": True}
114
- """
69
+ def _deserialize_null (self , value : bool ) -> None :
115
70
return None
116
71
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
120
74
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 )
125
77
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
129
80
130
- Example:
131
- >>> {"SS": ["Giraffe", "Hippo" ,"Zebra"]}
132
- """
133
- return self .get ("SS" )
81
+ def _deserialize_b (self , value : bytes ) -> bytes :
82
+ return value
134
83
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 ))
139
86
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 ))
144
89
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 ))
149
92
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 ]
157
95
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 ()}
167
98
168
99
169
100
class StreamViewType (Enum ):
@@ -176,28 +107,57 @@ class StreamViewType(Enum):
176
107
177
108
178
109
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
+
179
141
@property
180
142
def approximate_creation_date_time (self ) -> Optional [int ]:
181
143
"""The approximate date and time when the stream record was created, in UNIX epoch time format."""
182
144
item = self .get ("ApproximateCreationDateTime" )
183
145
return None if item is None else int (item )
184
146
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
187
147
@property
188
- def keys (self ) -> Optional [Dict [str , AttributeValue ]]: # type: ignore[override]
148
+ def keys (self ) -> Optional [Dict [str , Any ]]: # type: ignore[override]
189
149
"""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" )
191
151
192
152
@property
193
- def new_image (self ) -> Optional [Dict [str , AttributeValue ]]:
153
+ def new_image (self ) -> Optional [Dict [str , Any ]]:
194
154
"""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" )
196
156
197
157
@property
198
- def old_image (self ) -> Optional [Dict [str , AttributeValue ]]:
158
+ def old_image (self ) -> Optional [Dict [str , Any ]]:
199
159
"""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" )
201
161
202
162
@property
203
163
def sequence_number (self ) -> Optional [str ]:
@@ -233,7 +193,7 @@ def aws_region(self) -> Optional[str]:
233
193
234
194
@property
235
195
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 ."""
237
197
stream_record = self .get ("dynamodb" )
238
198
return None if stream_record is None else StreamRecord (stream_record )
239
199
@@ -278,26 +238,18 @@ class DynamoDBStreamEvent(DictWrapper):
278
238
279
239
Example
280
240
-------
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. **
282
242
283
243
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
- )
288
244
from aws_lambda_powertools.utilities.typing import LambdaContext
289
245
290
246
291
247
@event_source(data_class=DynamoDBStreamEvent)
292
248
def lambda_handler(event: DynamoDBStreamEvent, context: LambdaContext):
293
249
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)
301
253
"""
302
254
303
255
@property
0 commit comments