boto3-assist 0.32.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- boto3_assist/__init__.py +0 -0
- boto3_assist/aws_config.py +199 -0
- boto3_assist/aws_lambda/event_info.py +414 -0
- boto3_assist/aws_lambda/mock_context.py +5 -0
- boto3_assist/boto3session.py +87 -0
- boto3_assist/cloudwatch/cloudwatch_connection.py +84 -0
- boto3_assist/cloudwatch/cloudwatch_connection_tracker.py +17 -0
- boto3_assist/cloudwatch/cloudwatch_log_connection.py +62 -0
- boto3_assist/cloudwatch/cloudwatch_logs.py +39 -0
- boto3_assist/cloudwatch/cloudwatch_query.py +191 -0
- boto3_assist/cognito/cognito_authorizer.py +169 -0
- boto3_assist/cognito/cognito_connection.py +59 -0
- boto3_assist/cognito/cognito_utility.py +514 -0
- boto3_assist/cognito/jwks_cache.py +21 -0
- boto3_assist/cognito/user.py +27 -0
- boto3_assist/connection.py +146 -0
- boto3_assist/connection_tracker.py +120 -0
- boto3_assist/dynamodb/dynamodb.py +1206 -0
- boto3_assist/dynamodb/dynamodb_connection.py +113 -0
- boto3_assist/dynamodb/dynamodb_helpers.py +333 -0
- boto3_assist/dynamodb/dynamodb_importer.py +102 -0
- boto3_assist/dynamodb/dynamodb_index.py +507 -0
- boto3_assist/dynamodb/dynamodb_iservice.py +29 -0
- boto3_assist/dynamodb/dynamodb_key.py +130 -0
- boto3_assist/dynamodb/dynamodb_model_base.py +382 -0
- boto3_assist/dynamodb/dynamodb_model_base_interfaces.py +34 -0
- boto3_assist/dynamodb/dynamodb_re_indexer.py +165 -0
- boto3_assist/dynamodb/dynamodb_reindexer.py +165 -0
- boto3_assist/dynamodb/dynamodb_reserved_words.py +52 -0
- boto3_assist/dynamodb/dynamodb_reserved_words.txt +573 -0
- boto3_assist/dynamodb/readme.md +68 -0
- boto3_assist/dynamodb/troubleshooting.md +7 -0
- boto3_assist/ec2/ec2_connection.py +57 -0
- boto3_assist/environment_services/__init__.py +0 -0
- boto3_assist/environment_services/environment_loader.py +128 -0
- boto3_assist/environment_services/environment_variables.py +219 -0
- boto3_assist/erc/__init__.py +64 -0
- boto3_assist/erc/ecr_connection.py +57 -0
- boto3_assist/errors/custom_exceptions.py +46 -0
- boto3_assist/http_status_codes.py +80 -0
- boto3_assist/models/serializable_model.py +9 -0
- boto3_assist/role_assumption_mixin.py +38 -0
- boto3_assist/s3/s3.py +64 -0
- boto3_assist/s3/s3_bucket.py +67 -0
- boto3_assist/s3/s3_connection.py +76 -0
- boto3_assist/s3/s3_event_data.py +168 -0
- boto3_assist/s3/s3_object.py +695 -0
- boto3_assist/securityhub/securityhub.py +150 -0
- boto3_assist/securityhub/securityhub_connection.py +57 -0
- boto3_assist/session_setup_mixin.py +70 -0
- boto3_assist/ssm/connection.py +57 -0
- boto3_assist/ssm/parameter_store/parameter_store.py +116 -0
- boto3_assist/utilities/datetime_utility.py +349 -0
- boto3_assist/utilities/decimal_conversion_utility.py +140 -0
- boto3_assist/utilities/dictionary_utility.py +32 -0
- boto3_assist/utilities/file_operations.py +135 -0
- boto3_assist/utilities/http_utility.py +48 -0
- boto3_assist/utilities/logging_utility.py +0 -0
- boto3_assist/utilities/numbers_utility.py +329 -0
- boto3_assist/utilities/serialization_utility.py +664 -0
- boto3_assist/utilities/string_utility.py +337 -0
- boto3_assist/version.py +1 -0
- boto3_assist-0.32.0.dist-info/METADATA +76 -0
- boto3_assist-0.32.0.dist-info/RECORD +67 -0
- boto3_assist-0.32.0.dist-info/WHEEL +4 -0
- boto3_assist-0.32.0.dist-info/licenses/LICENSE-EXPLAINED.txt +11 -0
- boto3_assist-0.32.0.dist-info/licenses/LICENSE.txt +21 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
Maintainers: Eric Wilson
|
|
4
|
+
MIT License. See Project Root for the license information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import datetime as dt
|
|
9
|
+
|
|
10
|
+
# import decimal
|
|
11
|
+
# import inspect
|
|
12
|
+
# import uuid
|
|
13
|
+
from typing import TypeVar, List, Dict, Any
|
|
14
|
+
from boto3.dynamodb.types import TypeSerializer, TypeDeserializer
|
|
15
|
+
from boto3_assist.utilities.serialization_utility import Serialization
|
|
16
|
+
from boto3_assist.utilities.decimal_conversion_utility import DecimalConversionUtility
|
|
17
|
+
from boto3_assist.dynamodb.dynamodb_helpers import DynamoDBHelpers
|
|
18
|
+
from boto3_assist.dynamodb.dynamodb_index import (
|
|
19
|
+
DynamoDBIndexes,
|
|
20
|
+
DynamoDBIndex,
|
|
21
|
+
)
|
|
22
|
+
from boto3_assist.dynamodb.dynamodb_reserved_words import DynamoDBReservedWords
|
|
23
|
+
from boto3_assist.utilities.datetime_utility import DatetimeUtility
|
|
24
|
+
from boto3_assist.models.serializable_model import SerializableModel
|
|
25
|
+
from boto3_assist.utilities.string_utility import StringUtility
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def exclude_from_serialization(method):
|
|
29
|
+
"""
|
|
30
|
+
Decorator to mark methods or properties to be excluded from serialization.
|
|
31
|
+
"""
|
|
32
|
+
method.exclude_from_serialization = True
|
|
33
|
+
return method
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def exclude_indexes_from_serialization(method):
|
|
37
|
+
"""
|
|
38
|
+
Decorator to mark methods or properties to be excluded from serialization.
|
|
39
|
+
"""
|
|
40
|
+
method.exclude_indexes_from_serialization = True
|
|
41
|
+
return method
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DynamoDBModelBase(SerializableModel):
|
|
45
|
+
"""DynamoDb Model Base"""
|
|
46
|
+
|
|
47
|
+
T = TypeVar("T", bound="DynamoDBModelBase")
|
|
48
|
+
|
|
49
|
+
def __init__(self, auto_generate_projections: bool = True) -> None:
|
|
50
|
+
self.__projection_expression: str | None = None
|
|
51
|
+
self.__projection_expression_attribute_names: dict | None = None
|
|
52
|
+
self.__helpers: DynamoDBHelpers | None = None
|
|
53
|
+
self.__indexes: DynamoDBIndexes | None = None
|
|
54
|
+
self.__reserved_words: DynamoDBReservedWords = DynamoDBReservedWords()
|
|
55
|
+
self.__auto_generate_projections: bool = auto_generate_projections
|
|
56
|
+
self.__actively_serializing_data__: bool = False
|
|
57
|
+
|
|
58
|
+
def serialization_in_progress(self) -> bool:
|
|
59
|
+
return self.__actively_serializing_data__
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
@exclude_from_serialization
|
|
63
|
+
def indexes(self) -> DynamoDBIndexes:
|
|
64
|
+
"""Gets the indexes"""
|
|
65
|
+
# although this is marked as excluded, the indexes are add
|
|
66
|
+
# but in a more specialized way
|
|
67
|
+
if self.__indexes is None:
|
|
68
|
+
self.__indexes = DynamoDBIndexes()
|
|
69
|
+
return self.__indexes
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
@exclude_from_serialization
|
|
73
|
+
def projection_expression(self) -> str | None:
|
|
74
|
+
"""Gets the projection expression"""
|
|
75
|
+
prop_list: List[str] = []
|
|
76
|
+
if self.__projection_expression is None and self.auto_generate_projections:
|
|
77
|
+
props = self.to_dictionary()
|
|
78
|
+
# turn props to a list[str]
|
|
79
|
+
prop_list = list(props.keys())
|
|
80
|
+
else:
|
|
81
|
+
if self.__projection_expression:
|
|
82
|
+
prop_list = self.__projection_expression.split(",")
|
|
83
|
+
prop_list = [p.strip() for p in prop_list]
|
|
84
|
+
|
|
85
|
+
if len(prop_list) == 0:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
transformed_list = self.__reserved_words.tranform_projections(prop_list)
|
|
89
|
+
self.projection_expression = ",".join(transformed_list)
|
|
90
|
+
|
|
91
|
+
return self.__projection_expression
|
|
92
|
+
|
|
93
|
+
@projection_expression.setter
|
|
94
|
+
def projection_expression(self, value: str | None):
|
|
95
|
+
self.__projection_expression = value
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
@exclude_from_serialization
|
|
99
|
+
def auto_generate_projections(self) -> bool:
|
|
100
|
+
"""Gets the auto generate projections"""
|
|
101
|
+
return self.__auto_generate_projections
|
|
102
|
+
|
|
103
|
+
@auto_generate_projections.setter
|
|
104
|
+
def auto_generate_projections(self, value: bool):
|
|
105
|
+
self.__auto_generate_projections = value
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
@exclude_from_serialization
|
|
109
|
+
def projection_expression_attribute_names(self) -> dict | None:
|
|
110
|
+
"""
|
|
111
|
+
Gets the projection expression attribute names
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
if (
|
|
115
|
+
self.__projection_expression_attribute_names is None
|
|
116
|
+
and self.auto_generate_projections
|
|
117
|
+
):
|
|
118
|
+
props = self.to_dictionary()
|
|
119
|
+
# turn props to a list[str]
|
|
120
|
+
prop_list = list(props.keys())
|
|
121
|
+
self.projection_expression_attribute_names = (
|
|
122
|
+
self.__reserved_words.transform_attributes(prop_list)
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
if self.projection_expression:
|
|
126
|
+
expression_list = self.projection_expression.replace("#", "").split(",")
|
|
127
|
+
self.projection_expression_attribute_names = (
|
|
128
|
+
self.__reserved_words.transform_attributes(expression_list)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return self.__projection_expression_attribute_names
|
|
132
|
+
|
|
133
|
+
@projection_expression_attribute_names.setter
|
|
134
|
+
def projection_expression_attribute_names(self, value: dict | None):
|
|
135
|
+
self.__projection_expression_attribute_names = value
|
|
136
|
+
|
|
137
|
+
def map(self: T, item: Dict[str, Any] | DynamoDBModelBase | None) -> T:
|
|
138
|
+
"""
|
|
139
|
+
Map the item to the instance. If the item is a DynamoDBModelBase,
|
|
140
|
+
it will be converted to a dictionary first and then mapped.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
self (T): The Type of object you are converting it to.
|
|
144
|
+
item (dict | DynamoDBModelBase): _description_
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
ValueError: If the object is not a dictionary or DynamoDBModelBase
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
T | None: An object of type T with properties set matching
|
|
151
|
+
that of the dictionary object or None
|
|
152
|
+
"""
|
|
153
|
+
if item is None:
|
|
154
|
+
item = {}
|
|
155
|
+
|
|
156
|
+
if isinstance(item, DynamoDBModelBase):
|
|
157
|
+
item = item.to_resource_dictionary()
|
|
158
|
+
|
|
159
|
+
if isinstance(item, dict):
|
|
160
|
+
# Handle DynamoDB response structures
|
|
161
|
+
if "ResponseMetadata" in item:
|
|
162
|
+
# Full DynamoDB response with metadata
|
|
163
|
+
response: dict | None = item.get("Item")
|
|
164
|
+
if response is None:
|
|
165
|
+
response = {}
|
|
166
|
+
item = response
|
|
167
|
+
elif "Item" in item and not any(
|
|
168
|
+
key in item for key in ["id", "name", "pk", "sk"]
|
|
169
|
+
):
|
|
170
|
+
# Response with Item key but no direct model attributes (likely a DynamoDB response)
|
|
171
|
+
# This handles cases like {'Item': {...}} or {'Item': {...}, 'Count': 1}
|
|
172
|
+
item = item.get("Item", {})
|
|
173
|
+
|
|
174
|
+
# Convert any Decimal objects to native Python types for easier handling
|
|
175
|
+
item = DecimalConversionUtility.convert_decimals_to_native_types(item)
|
|
176
|
+
|
|
177
|
+
else:
|
|
178
|
+
raise ValueError("Item must be a dictionary or DynamoDBModelBase")
|
|
179
|
+
# attempt to map it
|
|
180
|
+
return DynamoDBSerializer.map(source=item, target=self)
|
|
181
|
+
|
|
182
|
+
def to_client_dictionary(self, include_indexes: bool = True):
|
|
183
|
+
"""
|
|
184
|
+
Convert the instance to a dictionary suitable for DynamoDB client.
|
|
185
|
+
"""
|
|
186
|
+
return DynamoDBSerializer.to_client_dictionary(
|
|
187
|
+
self, include_indexes=include_indexes
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def to_resource_dictionary(
|
|
191
|
+
self, include_indexes: bool = True, include_none: bool = False
|
|
192
|
+
):
|
|
193
|
+
"""
|
|
194
|
+
Convert the instance to a dictionary suitable for DynamoDB resource.
|
|
195
|
+
"""
|
|
196
|
+
return DynamoDBSerializer.to_resource_dictionary(
|
|
197
|
+
self, include_indexes=include_indexes, include_none=include_none
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def to_dict(self, include_none: bool = True):
|
|
201
|
+
"""
|
|
202
|
+
Convert the instance to a dictionary suitable for DynamoDB client.
|
|
203
|
+
"""
|
|
204
|
+
return self.to_dictionary(include_none=include_none)
|
|
205
|
+
|
|
206
|
+
def to_dictionary(self, include_none: bool = True):
|
|
207
|
+
"""
|
|
208
|
+
Convert the instance to a dictionary without an indexes/keys.
|
|
209
|
+
Useful for turning an object into a dictionary for serialization.
|
|
210
|
+
This is the same as to_resource_dictionary(include_indexes=False)
|
|
211
|
+
"""
|
|
212
|
+
return DynamoDBSerializer.to_resource_dictionary(
|
|
213
|
+
self, include_indexes=False, include_none=include_none
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def get_key(self, index_name: str) -> DynamoDBIndex:
|
|
217
|
+
"""Get the index name and key"""
|
|
218
|
+
|
|
219
|
+
if index_name is None:
|
|
220
|
+
raise ValueError("Index name cannot be None")
|
|
221
|
+
|
|
222
|
+
return self.indexes.get(index_name)
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def generate_uuid(sortable: bool = True) -> str:
|
|
226
|
+
if sortable:
|
|
227
|
+
return StringUtility.generate_sortable_uuid()
|
|
228
|
+
|
|
229
|
+
return StringUtility.generate_uuid()
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
@exclude_from_serialization
|
|
233
|
+
def helpers(self) -> DynamoDBHelpers:
|
|
234
|
+
"""Get the helpers"""
|
|
235
|
+
if self.__helpers is None:
|
|
236
|
+
self.__helpers = DynamoDBHelpers()
|
|
237
|
+
return self.__helpers
|
|
238
|
+
|
|
239
|
+
def list_keys(self, exclude_pk: bool = False) -> List[DynamoDBIndex]:
|
|
240
|
+
"""List the keys"""
|
|
241
|
+
values = self.indexes.values()
|
|
242
|
+
if exclude_pk:
|
|
243
|
+
values = [v for v in values if not v.name == DynamoDBIndexes.PRIMARY_INDEX]
|
|
244
|
+
|
|
245
|
+
return values
|
|
246
|
+
|
|
247
|
+
def to_timestamp_or_none(self, value: str | dt.datetime | None) -> float | None:
|
|
248
|
+
"""
|
|
249
|
+
Convert a value to a timestamp (float) or None
|
|
250
|
+
|
|
251
|
+
Exceptions:
|
|
252
|
+
ValueError: If the value is not a datetime string or datetime
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
if isinstance(value, str):
|
|
256
|
+
# value = dt.datetime.fromisoformat(value)
|
|
257
|
+
value = DatetimeUtility.to_datetime_utc(value)
|
|
258
|
+
|
|
259
|
+
if value is None:
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
if isinstance(value, dt.datetime):
|
|
263
|
+
return value.timestamp()
|
|
264
|
+
|
|
265
|
+
raise ValueError(
|
|
266
|
+
"Value must be a None, a string in a valid datetime format or datetime"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def to_utc(self, value: str | dt.datetime | None) -> dt.datetime | None:
|
|
270
|
+
"""
|
|
271
|
+
Convert a datetime to UTC. This ensures all datetimes are stored in UTC format
|
|
272
|
+
|
|
273
|
+
Exceptions:
|
|
274
|
+
ValueError: If the value is not a datetime string or datetime
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
value = DatetimeUtility.to_datetime_utc(value)
|
|
278
|
+
return value
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class DynamoDBSerializer:
|
|
282
|
+
"""Library to Serialize object to a DynamoDB Format"""
|
|
283
|
+
|
|
284
|
+
T = TypeVar("T", bound=DynamoDBModelBase)
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def map(source: dict, target: T) -> T:
|
|
288
|
+
"""
|
|
289
|
+
Map the source dictionary to the target object.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
- source: The dictionary to map from.
|
|
293
|
+
- target: The object to map to.
|
|
294
|
+
"""
|
|
295
|
+
mapped = Serialization.map(source, target)
|
|
296
|
+
if mapped is None:
|
|
297
|
+
raise ValueError("Unable to map source to target")
|
|
298
|
+
|
|
299
|
+
return mapped
|
|
300
|
+
|
|
301
|
+
@staticmethod
|
|
302
|
+
def to_client_dictionary(
|
|
303
|
+
instance: DynamoDBModelBase, include_indexes: bool = True
|
|
304
|
+
) -> Dict[str, Any]:
|
|
305
|
+
"""
|
|
306
|
+
Convert a Python class instance to a dictionary suitable for DynamoDB client.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
- instance: The class instance to be converted.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
- dict: A dictionary representation of the class instance suitable for DynamoDB client.
|
|
313
|
+
"""
|
|
314
|
+
serializer = TypeSerializer()
|
|
315
|
+
d = Serialization.to_dict(instance, serializer.serialize)
|
|
316
|
+
|
|
317
|
+
if include_indexes:
|
|
318
|
+
d = DynamoDBSerializer._add_indexes(instance=instance, instance_dict=d)
|
|
319
|
+
|
|
320
|
+
return d
|
|
321
|
+
|
|
322
|
+
@staticmethod
|
|
323
|
+
def to_resource_dictionary(
|
|
324
|
+
instance: DynamoDBModelBase,
|
|
325
|
+
include_indexes: bool = True,
|
|
326
|
+
include_none: bool = False,
|
|
327
|
+
) -> Dict[str, Any]:
|
|
328
|
+
"""
|
|
329
|
+
Convert a Python class instance to a dictionary suitable for DynamoDB resource.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
- instance: The class instance to be converted.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
- dict: A dictionary representation of the class instance suitable for DynamoDB resource.
|
|
336
|
+
"""
|
|
337
|
+
d = Serialization.to_dict(
|
|
338
|
+
instance,
|
|
339
|
+
lambda x: x,
|
|
340
|
+
include_none=include_none,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
if include_indexes:
|
|
344
|
+
d = DynamoDBSerializer._add_indexes(instance=instance, instance_dict=d)
|
|
345
|
+
|
|
346
|
+
return d
|
|
347
|
+
|
|
348
|
+
@staticmethod
|
|
349
|
+
def _add_indexes(instance: DynamoDBModelBase, instance_dict: dict) -> dict:
|
|
350
|
+
if not issubclass(type(instance), DynamoDBModelBase):
|
|
351
|
+
return instance_dict
|
|
352
|
+
|
|
353
|
+
if instance.indexes is None:
|
|
354
|
+
return instance_dict
|
|
355
|
+
|
|
356
|
+
primary = instance.indexes.primary
|
|
357
|
+
|
|
358
|
+
if primary:
|
|
359
|
+
instance_dict[primary.partition_key.attribute_name] = (
|
|
360
|
+
primary.partition_key.value
|
|
361
|
+
)
|
|
362
|
+
if (
|
|
363
|
+
primary.sort_key.attribute_name is not None
|
|
364
|
+
and primary.sort_key.value is not None
|
|
365
|
+
):
|
|
366
|
+
instance_dict[primary.sort_key.attribute_name] = primary.sort_key.value
|
|
367
|
+
|
|
368
|
+
secondaries = instance.indexes.secondaries
|
|
369
|
+
|
|
370
|
+
key: DynamoDBIndex
|
|
371
|
+
for _, key in secondaries.items():
|
|
372
|
+
if (
|
|
373
|
+
key.partition_key.attribute_name is not None
|
|
374
|
+
and key.partition_key.value is not None
|
|
375
|
+
):
|
|
376
|
+
instance_dict[key.partition_key.attribute_name] = (
|
|
377
|
+
key.partition_key.value
|
|
378
|
+
)
|
|
379
|
+
if key.sort_key.value is not None and key.sort_key.value is not None:
|
|
380
|
+
instance_dict[key.sort_key.attribute_name] = key.sort_key.value
|
|
381
|
+
|
|
382
|
+
return instance_dict
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
Maintainers: Eric Wilson
|
|
4
|
+
MIT License. See Project Root for the license information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Protocol, Optional
|
|
8
|
+
from boto3.dynamodb.conditions import (
|
|
9
|
+
And,
|
|
10
|
+
Equals,
|
|
11
|
+
# NotEquals,
|
|
12
|
+
# Or,
|
|
13
|
+
# GreaterThan,
|
|
14
|
+
# GreaterThanEquals,
|
|
15
|
+
# LessThan,
|
|
16
|
+
# LessThanEquals,
|
|
17
|
+
# In,
|
|
18
|
+
# Between,
|
|
19
|
+
# Contains,
|
|
20
|
+
# BeginsWith,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HasKeys(Protocol):
|
|
25
|
+
"""Interface for classes that have primary and sort keys"""
|
|
26
|
+
|
|
27
|
+
def get_pk(self, index_name: str) -> Optional[str]:
|
|
28
|
+
"""Interface to get_pk"""
|
|
29
|
+
|
|
30
|
+
def get_sk(self, index_name: str) -> Optional[str]:
|
|
31
|
+
"""Interface to get_sk"""
|
|
32
|
+
|
|
33
|
+
def get_key(self, index_name: str) -> And | Equals:
|
|
34
|
+
"""Get the index name and key"""
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
Maintainers: Eric Wilson
|
|
4
|
+
MIT License. See Project Root for the license information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any, Dict, Optional, List, Type
|
|
9
|
+
from aws_lambda_powertools import Logger
|
|
10
|
+
from boto3_assist.dynamodb.dynamodb import DynamoDB
|
|
11
|
+
from boto3_assist.dynamodb.dynamodb_model_base import DynamoDBModelBase
|
|
12
|
+
from boto3_assist.utilities.serialization_utility import Serialization
|
|
13
|
+
from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex
|
|
14
|
+
from boto3_assist.dynamodb.dynamodb_iservice import IDynamoDBService
|
|
15
|
+
|
|
16
|
+
logger = Logger()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DynamoDBReIndexer:
|
|
20
|
+
"""ReIndexing your database"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
table_name: str,
|
|
25
|
+
*,
|
|
26
|
+
db: Optional[DynamoDB] = None,
|
|
27
|
+
aws_profile: Optional[str] = None,
|
|
28
|
+
aws_region: Optional[str] = None,
|
|
29
|
+
aws_end_point_url: Optional[str] = None,
|
|
30
|
+
aws_access_key_id: Optional[str] = None,
|
|
31
|
+
aws_secret_access_key: Optional[str] = None,
|
|
32
|
+
):
|
|
33
|
+
self.table_name = table_name
|
|
34
|
+
self.db: DynamoDB = db or DynamoDB(
|
|
35
|
+
aws_profile=aws_profile,
|
|
36
|
+
aws_region=aws_region,
|
|
37
|
+
aws_end_point_url=aws_end_point_url,
|
|
38
|
+
aws_access_key_id=aws_access_key_id,
|
|
39
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def reindex_item(
|
|
43
|
+
self,
|
|
44
|
+
original_primary_key: dict,
|
|
45
|
+
model: DynamoDBModelBase,
|
|
46
|
+
*,
|
|
47
|
+
dry_run: bool = False,
|
|
48
|
+
inplace: bool = True,
|
|
49
|
+
leave_original_record: bool = False,
|
|
50
|
+
service_cls: Type[IDynamoDBService] | None,
|
|
51
|
+
):
|
|
52
|
+
"""
|
|
53
|
+
Reindex the record
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
original_primary_key (dict): The original primary key of the record to be reindexed.
|
|
57
|
+
This is either the partition_key or a composite key (partition_key, sort_key)
|
|
58
|
+
model (DynamoDBModelBase): A model instance that will be used to serialize the new keys
|
|
59
|
+
into a dictionary. It must inherit from DynamoDBModelBase
|
|
60
|
+
|
|
61
|
+
dry_run (bool, optional): Ability to log the actions without executing them. Defaults to False.
|
|
62
|
+
inplace (bool, optional): Ability to just update the indexes only.
|
|
63
|
+
No other fields will be updated, however you can't update the primary_key (partition/sort key)
|
|
64
|
+
with this action since they are immutable.
|
|
65
|
+
Defaults to True.
|
|
66
|
+
leave_original_record (bool, optional): _description_. Defaults to False.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
if inplace:
|
|
70
|
+
keys: List[DynamoDBIndex] = model.list_keys()
|
|
71
|
+
# Update the item in DynamoDB with new keys
|
|
72
|
+
self.update_item_in_dynamodb(
|
|
73
|
+
original_primary_key=original_primary_key, keys=keys, dry_run=dry_run
|
|
74
|
+
)
|
|
75
|
+
# todo: add some additional error handling here and throw a more
|
|
76
|
+
# descriptive error if they try to use a different primary
|
|
77
|
+
# pk or sk, which you can't do. If that's the case
|
|
78
|
+
else:
|
|
79
|
+
# add the new one first and optionally delete the older one
|
|
80
|
+
# once we are successful
|
|
81
|
+
try:
|
|
82
|
+
# save the new one first
|
|
83
|
+
service_instance: Optional[IDynamoDBService] = (
|
|
84
|
+
service_cls(db=self.db) if callable(service_cls) else None
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if service_instance:
|
|
88
|
+
service_instance.save(model=model)
|
|
89
|
+
else:
|
|
90
|
+
self.db.save(
|
|
91
|
+
item=model, table_name=self.table_name, source="reindex"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# then delete the old on
|
|
95
|
+
if not leave_original_record:
|
|
96
|
+
self.db.delete(
|
|
97
|
+
table_name=self.table_name, primary_key=original_primary_key
|
|
98
|
+
)
|
|
99
|
+
except Exception as e: # pylint: disable=broad-except
|
|
100
|
+
logger.error(str(e))
|
|
101
|
+
raise RuntimeError(str(e)) from e
|
|
102
|
+
# this gets a little more trick as we need to delete the item
|
|
103
|
+
|
|
104
|
+
def load_model(
|
|
105
|
+
self, db_item: dict, db_model: DynamoDBModelBase
|
|
106
|
+
) -> DynamoDBModelBase | None:
|
|
107
|
+
"""load the model which will serialze the dynamodb dictionary to an instance of an object"""
|
|
108
|
+
|
|
109
|
+
base_model = Serialization.map(db_item, db_model)
|
|
110
|
+
return base_model
|
|
111
|
+
|
|
112
|
+
def update_item_in_dynamodb(
|
|
113
|
+
self,
|
|
114
|
+
original_primary_key: dict,
|
|
115
|
+
keys: List[DynamoDBIndex],
|
|
116
|
+
dry_run: bool = False,
|
|
117
|
+
):
|
|
118
|
+
"""Update the dynamodb item"""
|
|
119
|
+
dictionary = self.db.helpers.keys_to_dictionary(keys=keys)
|
|
120
|
+
|
|
121
|
+
update_expression = self.build_update_expression(dictionary)
|
|
122
|
+
expression_attribute_values = self.build_expression_attribute_values(dictionary)
|
|
123
|
+
|
|
124
|
+
if not dry_run:
|
|
125
|
+
self.db.update_item(
|
|
126
|
+
table_name=self.table_name,
|
|
127
|
+
key=original_primary_key,
|
|
128
|
+
update_expression=update_expression,
|
|
129
|
+
expression_attribute_values=expression_attribute_values,
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
print("Dry run: Skipping Update item")
|
|
133
|
+
print(f"{json.dumps(original_primary_key, indent=4)}")
|
|
134
|
+
print(f"{update_expression}")
|
|
135
|
+
print(f"{json.dumps(expression_attribute_values, indent=4)}")
|
|
136
|
+
|
|
137
|
+
def build_update_expression(self, updated_keys: Dict[str, Any]) -> str:
|
|
138
|
+
"""
|
|
139
|
+
Build the expression for updating the item
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
updated_keys (Dict[str, Any]): _description_
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
str: _description_
|
|
146
|
+
"""
|
|
147
|
+
update_expression = "SET " + ", ".join(
|
|
148
|
+
f"{k} = :{k}" for k in updated_keys.keys()
|
|
149
|
+
)
|
|
150
|
+
return update_expression
|
|
151
|
+
|
|
152
|
+
def build_expression_attribute_values(
|
|
153
|
+
self, updated_keys: Dict[str, Any]
|
|
154
|
+
) -> Dict[str, Any]:
|
|
155
|
+
"""
|
|
156
|
+
Build the expression attribute values for the update expression
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
updated_keys (Dict[str, Any]): _description_
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Dict[str, Any]: _description_
|
|
163
|
+
"""
|
|
164
|
+
expression_attribute_values = {f":{k}": v for k, v in updated_keys.items()}
|
|
165
|
+
return expression_attribute_values
|