44from typing import Any , Dict , Optional
55
66import boto3
7+ from boto3 .dynamodb .types import TypeDeserializer
78from botocore .config import Config
9+ from botocore .exceptions import ClientError
810
911from aws_lambda_powertools .shared import constants
1012from aws_lambda_powertools .utilities .idempotency import BasePersistenceLayer
@@ -79,13 +81,14 @@ def __init__(
7981
8082 self ._boto_config = boto_config or Config ()
8183 self ._boto3_session = boto3_session or boto3 .session .Session ()
84+ self ._client = self ._boto3_session .client ("dynamodb" , config = self ._boto_config )
85+
8286 if sort_key_attr == key_attr :
8387 raise ValueError (f"key_attr [{ key_attr } ] and sort_key_attr [{ sort_key_attr } ] cannot be the same!" )
8488
8589 if static_pk_value is None :
8690 static_pk_value = f"idempotency#{ os .getenv (constants .LAMBDA_FUNCTION_NAME_ENV , '' )} "
8791
88- self ._table = None
8992 self .table_name = table_name
9093 self .key_attr = key_attr
9194 self .static_pk_value = static_pk_value
@@ -95,31 +98,15 @@ def __init__(
9598 self .status_attr = status_attr
9699 self .data_attr = data_attr
97100 self .validation_key_attr = validation_key_attr
98- super (DynamoDBPersistenceLayer , self ).__init__ ()
99101
100- @property
101- def table (self ):
102- """
103- Caching property to store boto3 dynamodb Table resource
102+ self ._deserializer = TypeDeserializer ()
104103
105- """
106- if self ._table :
107- return self ._table
108- ddb_resource = self ._boto3_session .resource ("dynamodb" , config = self ._boto_config )
109- self ._table = ddb_resource .Table (self .table_name )
110- return self ._table
111-
112- @table .setter
113- def table (self , table ):
114- """
115- Allow table instance variable to be set directly, primarily for use in tests
116- """
117- self ._table = table
104+ super (DynamoDBPersistenceLayer , self ).__init__ ()
118105
119106 def _get_key (self , idempotency_key : str ) -> dict :
120107 if self .sort_key_attr :
121- return {self .key_attr : self .static_pk_value , self .sort_key_attr : idempotency_key }
122- return {self .key_attr : idempotency_key }
108+ return {self .key_attr : { "S" : self .static_pk_value } , self .sort_key_attr : { "S" : idempotency_key } }
109+ return {self .key_attr : { "S" : idempotency_key } }
123110
124111 def _item_to_data_record (self , item : Dict [str , Any ]) -> DataRecord :
125112 """
@@ -136,36 +123,39 @@ def _item_to_data_record(self, item: Dict[str, Any]) -> DataRecord:
136123 representation of item
137124
138125 """
126+ data = self ._deserializer .deserialize ({"M" : item })
139127 return DataRecord (
140- idempotency_key = item [self .key_attr ],
141- status = item [self .status_attr ],
142- expiry_timestamp = item [self .expiry_attr ],
143- in_progress_expiry_timestamp = item .get (self .in_progress_expiry_attr ),
144- response_data = item .get (self .data_attr ),
145- payload_hash = item .get (self .validation_key_attr ),
128+ idempotency_key = data [self .key_attr ],
129+ status = data [self .status_attr ],
130+ expiry_timestamp = data [self .expiry_attr ],
131+ in_progress_expiry_timestamp = data .get (self .in_progress_expiry_attr ),
132+ response_data = data .get (self .data_attr ),
133+ payload_hash = data .get (self .validation_key_attr ),
146134 )
147135
148136 def _get_record (self , idempotency_key ) -> DataRecord :
149- response = self .table .get_item (Key = self ._get_key (idempotency_key ), ConsistentRead = True )
150-
137+ response = self ._client .get_item (
138+ TableName = self .table_name , Key = self ._get_key (idempotency_key ), ConsistentRead = True
139+ )
151140 try :
152141 item = response ["Item" ]
153- except KeyError :
154- raise IdempotencyItemNotFoundError
142+ except KeyError as exc :
143+ raise IdempotencyItemNotFoundError from exc
155144 return self ._item_to_data_record (item )
156145
157146 def _put_record (self , data_record : DataRecord ) -> None :
158147 item = {
159148 ** self ._get_key (data_record .idempotency_key ),
160- self .expiry_attr : data_record .expiry_timestamp ,
161- self .status_attr : data_record .status ,
149+ self .key_attr : {"S" : data_record .idempotency_key },
150+ self .expiry_attr : {"N" : str (data_record .expiry_timestamp )},
151+ self .status_attr : {"S" : data_record .status },
162152 }
163153
164154 if data_record .in_progress_expiry_timestamp is not None :
165- item [self .in_progress_expiry_attr ] = data_record .in_progress_expiry_timestamp
155+ item [self .in_progress_expiry_attr ] = { "N" : str ( data_record .in_progress_expiry_timestamp )}
166156
167- if self .payload_validation_enabled :
168- item [self .validation_key_attr ] = data_record .payload_hash
157+ if self .payload_validation_enabled and data_record . payload_hash :
158+ item [self .validation_key_attr ] = { "S" : data_record .payload_hash }
169159
170160 now = datetime .datetime .now ()
171161 try :
@@ -199,8 +189,8 @@ def _put_record(self, data_record: DataRecord) -> None:
199189 condition_expression = (
200190 f"{ idempotency_key_not_exist } OR { idempotency_expiry_expired } OR ({ inprogress_expiry_expired } )"
201191 )
202-
203- self .table . put_item (
192+ self . _client . put_item (
193+ TableName = self .table_name ,
204194 Item = item ,
205195 ConditionExpression = condition_expression ,
206196 ExpressionAttributeNames = {
@@ -210,22 +200,28 @@ def _put_record(self, data_record: DataRecord) -> None:
210200 "#status" : self .status_attr ,
211201 },
212202 ExpressionAttributeValues = {
213- ":now" : int (now .timestamp ()),
214- ":now_in_millis" : int (now .timestamp () * 1000 ),
215- ":inprogress" : STATUS_CONSTANTS ["INPROGRESS" ],
203+ ":now" : { "N" : str ( int (now .timestamp ()))} ,
204+ ":now_in_millis" : { "N" : str ( int (now .timestamp () * 1000 ))} ,
205+ ":inprogress" : { "S" : STATUS_CONSTANTS ["INPROGRESS" ]} ,
216206 },
217207 )
218- except self .table .meta .client .exceptions .ConditionalCheckFailedException :
219- logger .debug (f"Failed to put record for already existing idempotency key: { data_record .idempotency_key } " )
220- raise IdempotencyItemAlreadyExistsError
208+ except ClientError as exc :
209+ error_code = exc .response .get ("Error" , {}).get ("Code" )
210+ if error_code == "ConditionalCheckFailedException" :
211+ logger .debug (
212+ f"Failed to put record for already existing idempotency key: { data_record .idempotency_key } "
213+ )
214+ raise IdempotencyItemAlreadyExistsError from exc
215+ else :
216+ raise
221217
222218 def _update_record (self , data_record : DataRecord ):
223219 logger .debug (f"Updating record for idempotency key: { data_record .idempotency_key } " )
224220 update_expression = "SET #response_data = :response_data, #expiry = :expiry, " "#status = :status"
225221 expression_attr_values = {
226- ":expiry" : data_record .expiry_timestamp ,
227- ":response_data" : data_record .response_data ,
228- ":status" : data_record .status ,
222+ ":expiry" : { "N" : str ( data_record .expiry_timestamp )} ,
223+ ":response_data" : { "S" : data_record .response_data } ,
224+ ":status" : { "S" : data_record .status } ,
229225 }
230226 expression_attr_names = {
231227 "#expiry" : self .expiry_attr ,
@@ -235,7 +231,7 @@ def _update_record(self, data_record: DataRecord):
235231
236232 if self .payload_validation_enabled :
237233 update_expression += ", #validation_key = :validation_key"
238- expression_attr_values [":validation_key" ] = data_record .payload_hash
234+ expression_attr_values [":validation_key" ] = { "S" : data_record .payload_hash }
239235 expression_attr_names ["#validation_key" ] = self .validation_key_attr
240236
241237 kwargs = {
@@ -245,8 +241,8 @@ def _update_record(self, data_record: DataRecord):
245241 "ExpressionAttributeNames" : expression_attr_names ,
246242 }
247243
248- self .table .update_item (** kwargs )
244+ self ._client .update_item (TableName = self . table_name , ** kwargs )
249245
250246 def _delete_record (self , data_record : DataRecord ) -> None :
251247 logger .debug (f"Deleting record for idempotency key: { data_record .idempotency_key } " )
252- self .table .delete_item (Key = self ._get_key (data_record .idempotency_key ))
248+ self ._client .delete_item (TableName = self . table_name , Key = { ** self ._get_key (data_record .idempotency_key )} )
0 commit comments