1+ from decimal import Clamped , Context , Decimal , Inexact , Overflow , Rounded , Underflow
12from 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
45from 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
169100class StreamViewType (Enum ):
@@ -176,28 +107,57 @@ class StreamViewType(Enum):
176107
177108
178109class 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
0 commit comments