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.
Files changed (67) hide show
  1. boto3_assist/__init__.py +0 -0
  2. boto3_assist/aws_config.py +199 -0
  3. boto3_assist/aws_lambda/event_info.py +414 -0
  4. boto3_assist/aws_lambda/mock_context.py +5 -0
  5. boto3_assist/boto3session.py +87 -0
  6. boto3_assist/cloudwatch/cloudwatch_connection.py +84 -0
  7. boto3_assist/cloudwatch/cloudwatch_connection_tracker.py +17 -0
  8. boto3_assist/cloudwatch/cloudwatch_log_connection.py +62 -0
  9. boto3_assist/cloudwatch/cloudwatch_logs.py +39 -0
  10. boto3_assist/cloudwatch/cloudwatch_query.py +191 -0
  11. boto3_assist/cognito/cognito_authorizer.py +169 -0
  12. boto3_assist/cognito/cognito_connection.py +59 -0
  13. boto3_assist/cognito/cognito_utility.py +514 -0
  14. boto3_assist/cognito/jwks_cache.py +21 -0
  15. boto3_assist/cognito/user.py +27 -0
  16. boto3_assist/connection.py +146 -0
  17. boto3_assist/connection_tracker.py +120 -0
  18. boto3_assist/dynamodb/dynamodb.py +1206 -0
  19. boto3_assist/dynamodb/dynamodb_connection.py +113 -0
  20. boto3_assist/dynamodb/dynamodb_helpers.py +333 -0
  21. boto3_assist/dynamodb/dynamodb_importer.py +102 -0
  22. boto3_assist/dynamodb/dynamodb_index.py +507 -0
  23. boto3_assist/dynamodb/dynamodb_iservice.py +29 -0
  24. boto3_assist/dynamodb/dynamodb_key.py +130 -0
  25. boto3_assist/dynamodb/dynamodb_model_base.py +382 -0
  26. boto3_assist/dynamodb/dynamodb_model_base_interfaces.py +34 -0
  27. boto3_assist/dynamodb/dynamodb_re_indexer.py +165 -0
  28. boto3_assist/dynamodb/dynamodb_reindexer.py +165 -0
  29. boto3_assist/dynamodb/dynamodb_reserved_words.py +52 -0
  30. boto3_assist/dynamodb/dynamodb_reserved_words.txt +573 -0
  31. boto3_assist/dynamodb/readme.md +68 -0
  32. boto3_assist/dynamodb/troubleshooting.md +7 -0
  33. boto3_assist/ec2/ec2_connection.py +57 -0
  34. boto3_assist/environment_services/__init__.py +0 -0
  35. boto3_assist/environment_services/environment_loader.py +128 -0
  36. boto3_assist/environment_services/environment_variables.py +219 -0
  37. boto3_assist/erc/__init__.py +64 -0
  38. boto3_assist/erc/ecr_connection.py +57 -0
  39. boto3_assist/errors/custom_exceptions.py +46 -0
  40. boto3_assist/http_status_codes.py +80 -0
  41. boto3_assist/models/serializable_model.py +9 -0
  42. boto3_assist/role_assumption_mixin.py +38 -0
  43. boto3_assist/s3/s3.py +64 -0
  44. boto3_assist/s3/s3_bucket.py +67 -0
  45. boto3_assist/s3/s3_connection.py +76 -0
  46. boto3_assist/s3/s3_event_data.py +168 -0
  47. boto3_assist/s3/s3_object.py +695 -0
  48. boto3_assist/securityhub/securityhub.py +150 -0
  49. boto3_assist/securityhub/securityhub_connection.py +57 -0
  50. boto3_assist/session_setup_mixin.py +70 -0
  51. boto3_assist/ssm/connection.py +57 -0
  52. boto3_assist/ssm/parameter_store/parameter_store.py +116 -0
  53. boto3_assist/utilities/datetime_utility.py +349 -0
  54. boto3_assist/utilities/decimal_conversion_utility.py +140 -0
  55. boto3_assist/utilities/dictionary_utility.py +32 -0
  56. boto3_assist/utilities/file_operations.py +135 -0
  57. boto3_assist/utilities/http_utility.py +48 -0
  58. boto3_assist/utilities/logging_utility.py +0 -0
  59. boto3_assist/utilities/numbers_utility.py +329 -0
  60. boto3_assist/utilities/serialization_utility.py +664 -0
  61. boto3_assist/utilities/string_utility.py +337 -0
  62. boto3_assist/version.py +1 -0
  63. boto3_assist-0.32.0.dist-info/METADATA +76 -0
  64. boto3_assist-0.32.0.dist-info/RECORD +67 -0
  65. boto3_assist-0.32.0.dist-info/WHEEL +4 -0
  66. boto3_assist-0.32.0.dist-info/licenses/LICENSE-EXPLAINED.txt +11 -0
  67. 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