4
4
from typing import Any , Dict , Optional
5
5
6
6
import boto3
7
+ from boto3 .dynamodb .types import TypeDeserializer
7
8
from botocore .config import Config
9
+ from botocore .exceptions import ClientError
8
10
9
11
from aws_lambda_powertools .shared import constants
10
12
from aws_lambda_powertools .utilities .idempotency import BasePersistenceLayer
@@ -79,13 +81,14 @@ def __init__(
79
81
80
82
self ._boto_config = boto_config or Config ()
81
83
self ._boto3_session = boto3_session or boto3 .session .Session ()
84
+ self ._client = self ._boto3_session .client ("dynamodb" , config = self ._boto_config )
85
+
82
86
if sort_key_attr == key_attr :
83
87
raise ValueError (f"key_attr [{ key_attr } ] and sort_key_attr [{ sort_key_attr } ] cannot be the same!" )
84
88
85
89
if static_pk_value is None :
86
90
static_pk_value = f"idempotency#{ os .getenv (constants .LAMBDA_FUNCTION_NAME_ENV , '' )} "
87
91
88
- self ._table = None
89
92
self .table_name = table_name
90
93
self .key_attr = key_attr
91
94
self .static_pk_value = static_pk_value
@@ -95,31 +98,15 @@ def __init__(
95
98
self .status_attr = status_attr
96
99
self .data_attr = data_attr
97
100
self .validation_key_attr = validation_key_attr
98
- super (DynamoDBPersistenceLayer , self ).__init__ ()
99
101
100
- @property
101
- def table (self ):
102
- """
103
- Caching property to store boto3 dynamodb Table resource
102
+ self ._deserializer = TypeDeserializer ()
104
103
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__ ()
118
105
119
106
def _get_key (self , idempotency_key : str ) -> dict :
120
107
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 } }
123
110
124
111
def _item_to_data_record (self , item : Dict [str , Any ]) -> DataRecord :
125
112
"""
@@ -136,36 +123,39 @@ def _item_to_data_record(self, item: Dict[str, Any]) -> DataRecord:
136
123
representation of item
137
124
138
125
"""
126
+ data = self ._deserializer .deserialize ({"M" : item })
139
127
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 ),
146
134
)
147
135
148
136
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
+ )
151
140
try :
152
141
item = response ["Item" ]
153
- except KeyError :
154
- raise IdempotencyItemNotFoundError
142
+ except KeyError as exc :
143
+ raise IdempotencyItemNotFoundError from exc
155
144
return self ._item_to_data_record (item )
156
145
157
146
def _put_record (self , data_record : DataRecord ) -> None :
158
147
item = {
159
148
** 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 },
162
152
}
163
153
164
154
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 )}
166
156
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 }
169
159
170
160
now = datetime .datetime .now ()
171
161
try :
@@ -199,8 +189,8 @@ def _put_record(self, data_record: DataRecord) -> None:
199
189
condition_expression = (
200
190
f"{ idempotency_key_not_exist } OR { idempotency_expiry_expired } OR ({ inprogress_expiry_expired } )"
201
191
)
202
-
203
- self .table . put_item (
192
+ self . _client . put_item (
193
+ TableName = self .table_name ,
204
194
Item = item ,
205
195
ConditionExpression = condition_expression ,
206
196
ExpressionAttributeNames = {
@@ -210,22 +200,28 @@ def _put_record(self, data_record: DataRecord) -> None:
210
200
"#status" : self .status_attr ,
211
201
},
212
202
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" ]} ,
216
206
},
217
207
)
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
221
217
222
218
def _update_record (self , data_record : DataRecord ):
223
219
logger .debug (f"Updating record for idempotency key: { data_record .idempotency_key } " )
224
220
update_expression = "SET #response_data = :response_data, #expiry = :expiry, " "#status = :status"
225
221
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 } ,
229
225
}
230
226
expression_attr_names = {
231
227
"#expiry" : self .expiry_attr ,
@@ -235,7 +231,7 @@ def _update_record(self, data_record: DataRecord):
235
231
236
232
if self .payload_validation_enabled :
237
233
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 }
239
235
expression_attr_names ["#validation_key" ] = self .validation_key_attr
240
236
241
237
kwargs = {
@@ -245,8 +241,8 @@ def _update_record(self, data_record: DataRecord):
245
241
"ExpressionAttributeNames" : expression_attr_names ,
246
242
}
247
243
248
- self .table .update_item (** kwargs )
244
+ self ._client .update_item (TableName = self . table_name , ** kwargs )
249
245
250
246
def _delete_record (self , data_record : DataRecord ) -> None :
251
247
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