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,664 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ Maintainers: Eric Wilson
4
+ MIT License. See Project Root for the license information.
5
+ """
6
+
7
+ import datetime as dt
8
+ import decimal
9
+ import inspect
10
+ import json
11
+ import typing
12
+ import uuid
13
+ from datetime import datetime
14
+ from decimal import Decimal
15
+ from typing import Any, Dict, List, TypeVar
16
+ from aws_lambda_powertools import Logger
17
+ from boto3_assist.utilities.string_utility import StringUtility
18
+
19
+ T = TypeVar("T")
20
+
21
+
22
+ logger = Logger()
23
+
24
+
25
+ class SerializableModel:
26
+ """Library to Serialize object to a DynamoDB Format or other dictionary"""
27
+
28
+ T = TypeVar("T", bound="SerializableModel")
29
+
30
+ def __init__(self):
31
+ pass
32
+
33
+ def map(
34
+ self: T,
35
+ source: Dict[str, Any] | "SerializableModel" | None,
36
+ coerce: bool = True,
37
+ ) -> T:
38
+ """
39
+ Map the source dictionary to the target object.
40
+
41
+ Args:
42
+ - source: The dictionary to map from.
43
+ - target: The object to map to.
44
+ """
45
+ mapped = Serialization.map(source=source, target=self, coerce=coerce)
46
+ if mapped is None:
47
+ raise ValueError("Unable to map source to target")
48
+
49
+ return mapped
50
+
51
+ def dict(self) -> Dict[str, Any]:
52
+ """
53
+ Same as .to_dictionary
54
+
55
+ """
56
+ return self.to_dictionary()
57
+
58
+ def to_dictionary(self) -> Dict[str, Any]:
59
+ """
60
+ Convert the object to a dictionary. Same as .dict()
61
+ """
62
+ # return Serialization.convert_object_to_dict(self)
63
+ return Serialization.to_dict(
64
+ instance=self, serialize_fn=lambda x: x, include_none=True
65
+ )
66
+
67
+ def to_wide_dictionary(self) -> Dict:
68
+ """
69
+ Dumps an object to dictionary structure
70
+ """
71
+
72
+ dump = Serialization.to_wide_dictionary(model=self)
73
+
74
+ return dump
75
+
76
+
77
+ class JsonEncoder(json.JSONEncoder):
78
+ """
79
+ This class is used to serialize python generics which implement a __json_encode__ method
80
+ and where the recipient does not require type hinting for deserialization.
81
+ If type hinting is required, use GenericJsonEncoder
82
+ """
83
+
84
+ def default(self, o):
85
+ # First, check if the object has a custom encoding method
86
+ if hasattr(o, "__json_encode__"):
87
+ return o.__json_encode__()
88
+
89
+ # check for dictionary
90
+ if hasattr(o, "__dict__"):
91
+ return {k: v for k, v in o.__dict__.items() if not k.startswith("_")}
92
+
93
+ # Handling datetime.datetime objects specifically
94
+ elif isinstance(o, datetime):
95
+ return o.isoformat()
96
+ # handle decimal wrappers
97
+ elif isinstance(o, Decimal):
98
+ return float(o)
99
+
100
+ logger.info(f"JsonEncoder failing back: ${type(o)}")
101
+
102
+ # Fallback to the base class implementation for other types
103
+
104
+ try:
105
+ return super().default(o)
106
+ except TypeError:
107
+ # If an object does not have a __dict__ attribute, you might want to handle it differently.
108
+ # For example, you could choose to return str(o) or implement other specific cases.
109
+ return str(
110
+ o
111
+ ) # Or any other way you wish to serialize objects without __dict__
112
+
113
+
114
+ class JsonConversions:
115
+ """
116
+ Json Conversion Utility
117
+ Used for snake_case to camelCase and vice versa
118
+ """
119
+
120
+ @staticmethod
121
+ def string_to_json_obj(
122
+ value: str | list | dict, raise_on_error: bool = True, retry: int = 0
123
+ ) -> typing.Union[dict, typing.Any, None]:
124
+ """
125
+ Converts a string to a JSON object.
126
+
127
+ Args:
128
+ value: The value to convert (string, list, or dict).
129
+ raise_on_error: Whether to raise an exception on error.
130
+ retry: The number of retry attempts made.
131
+
132
+ Returns:
133
+ The converted JSON object, or the original value if conversion fails.
134
+ """
135
+ # Handle empty/None values
136
+ if not value:
137
+ return {}
138
+
139
+ # Return dicts unchanged
140
+ if isinstance(value, dict):
141
+ return value
142
+
143
+ # Check retry limit early
144
+ if retry > 5:
145
+ raise RuntimeError("Too many attempts to convert string to JSON")
146
+
147
+ try:
148
+ # Convert to string if needed
149
+ if not isinstance(value, str):
150
+ value = str(value)
151
+
152
+ # Clean up the string
153
+ value = value.replace("\n", "").strip()
154
+ if value.startswith("'") and value.endswith("'"):
155
+ value = value.strip("'").strip()
156
+
157
+ # Parse JSON
158
+ parsed_value = json.loads(value)
159
+
160
+ # Handle nested string JSON (recursive case)
161
+ if isinstance(parsed_value, str):
162
+ return JsonConversions.string_to_json_obj(parsed_value, raise_on_error, retry + 1)
163
+
164
+ return parsed_value
165
+
166
+ except json.JSONDecodeError as e:
167
+ # Try to fix malformed JSON with single quotes
168
+ if "Expecting property name enclosed in double quotes" in str(e) and retry < 5:
169
+ if isinstance(value, str):
170
+ fixed_json = JsonConversions.convert_bad_json_string(value)
171
+ return JsonConversions.string_to_json_obj(fixed_json, raise_on_error, retry + 1)
172
+
173
+ if raise_on_error:
174
+ raise e
175
+ return {}
176
+
177
+ except Exception as e:
178
+ if raise_on_error:
179
+ logger.exception({"source": "string_to_json_obj", "error": str(e), "value": value})
180
+ raise e
181
+
182
+ logger.warning({"source": "string_to_json_obj", "returning_original": True, "value": value})
183
+ return value
184
+
185
+
186
+ @staticmethod
187
+ def convert_bad_json_string(bad_json: str) -> str:
188
+ """
189
+ Fixes malformed JSON by converting single quotes to double quotes.
190
+
191
+ Args:
192
+ bad_json: Malformed JSON string with single quotes.
193
+
194
+ Returns:
195
+ Fixed JSON string with proper double quotes.
196
+ """
197
+ # Use a placeholder to safely swap quotes
198
+ return bad_json.replace("'", "§§§").replace('"', "'").replace("§§§", '"')
199
+
200
+ @staticmethod
201
+ def _camel_to_snake(value: str) -> str:
202
+ """Converts a camelCase to a snake_case"""
203
+ return StringUtility.camel_to_snake(value)
204
+
205
+ @staticmethod
206
+ def _snake_to_camel(value: str) -> str:
207
+ """Converts a value from snake_case to camelCase"""
208
+ return StringUtility.snake_to_camel(value)
209
+
210
+ @staticmethod
211
+ def _convert_keys(data, convert_func, deep: bool = True):
212
+ """
213
+ Recursively converts dictionary keys using the provided convert_func.
214
+
215
+ Parameters:
216
+ data: The input data (dict, list, or other) to process.
217
+ convert_func: Function to convert the keys (e.g. camel_to_snake or snake_to_camel).
218
+ deep (bool): If True (default), convert keys in all nested dictionaries.
219
+ If False, only convert the keys at the current level.
220
+ """
221
+ if isinstance(data, dict):
222
+ new_dict = {}
223
+ for key, value in data.items():
224
+ new_key = convert_func(key)
225
+ # Only process nested structures if deep is True.
226
+ new_dict[new_key] = (
227
+ JsonConversions._convert_keys(value, convert_func, deep)
228
+ if deep
229
+ else value
230
+ )
231
+ return new_dict
232
+ elif isinstance(data, list):
233
+ # For lists, if deep conversion is enabled, process each element.
234
+ return [
235
+ (
236
+ JsonConversions._convert_keys(item, convert_func, deep)
237
+ if deep
238
+ else item
239
+ )
240
+ for item in data
241
+ ]
242
+ else:
243
+ return data
244
+
245
+ @staticmethod
246
+ def json_camel_to_snake(data, deep: bool = True):
247
+ """Converts all keys in the JSON structure from camelCase to snake_case.
248
+
249
+ Parameters:
250
+ data: The JSON-like structure (dict or list) to process.
251
+ deep (bool): If True, process keys in all nested dictionaries; if False, only at the first level.
252
+ """
253
+ return JsonConversions._convert_keys(
254
+ data, JsonConversions._camel_to_snake, deep
255
+ )
256
+
257
+ @staticmethod
258
+ def json_snake_to_camel(data, deep: bool = True):
259
+ """Converts all keys in the JSON structure from snake_case to camelCase.
260
+
261
+ Parameters:
262
+ data: The JSON-like structure (dict or list) to process.
263
+ deep (bool): If True, process keys in all nested dictionaries; if False, only at the first level.
264
+ """
265
+ return JsonConversions._convert_keys(
266
+ data, JsonConversions._snake_to_camel, deep
267
+ )
268
+
269
+ # # Example usage:
270
+ # if __name__ == "__main__":
271
+ # sample_json = {
272
+ # "firstName": "John",
273
+ # "lastName": "Doe",
274
+ # "address": {"streetAddress": "21 2nd Street", "city": "New York"},
275
+ # "phoneNumbers": [
276
+ # {"phoneType": "home", "phoneNumber": "2125551234"},
277
+ # {"phoneType": "fax", "phoneNumber": "6465554567"},
278
+ # ],
279
+ # }
280
+
281
+ # print("Original JSON:")
282
+ # print(sample_json)
283
+
284
+ # # Convert from camelCase to snake_case on all levels.
285
+ # snake_json_deep = json_camel_to_snake(sample_json, deep=True)
286
+ # print("\nConverted to snake_case (deep conversion):")
287
+ # print(snake_json_deep)
288
+
289
+ # # Convert from camelCase to snake_case only at the first level.
290
+ # snake_json_shallow = json_camel_to_snake(sample_json, deep=False)
291
+ # print("\nConverted to snake_case (first-level only):")
292
+ # print(snake_json_shallow)
293
+
294
+ # # Convert back from snake_case to camelCase on all levels.
295
+ # camel_json_deep = json_snake_to_camel(snake_json_deep, deep=True)
296
+ # print("\nConverted back to camelCase (deep conversion):")
297
+ # print(camel_json_deep)
298
+
299
+
300
+ class Serialization:
301
+ """
302
+ Serialization Class
303
+ """
304
+
305
+ @staticmethod
306
+ def convert_object_to_dict(model: object) -> Dict | List:
307
+ """
308
+ Dumps an object to dictionary structure
309
+ """
310
+
311
+ dump = Serialization.to_dict(
312
+ instance=model, serialize_fn=lambda x: x, include_none=True
313
+ )
314
+
315
+ return dump
316
+
317
+ @staticmethod
318
+ def to_wide_dictionary(model: object) -> Dict:
319
+ """
320
+ Dumps an object to dictionary structure
321
+ """
322
+
323
+ dump = Serialization.to_dict(
324
+ instance=model, serialize_fn=lambda x: x, include_none=True
325
+ )
326
+
327
+ # have a dictionary now let's flatten out
328
+ flat_dict = {}
329
+ for key, value in dump.items():
330
+ if isinstance(value, dict):
331
+ for sub_key, sub_value in value.items():
332
+ flat_dict[f"{key}_{sub_key}"] = sub_value
333
+ elif isinstance(value, list):
334
+ for i, sub_value in enumerate(value):
335
+ sub_dict = Serialization.to_wide_dictionary(sub_value)
336
+ for sub_key, sub_value in sub_dict.items():
337
+ flat_dict[f"{key}_{i}_{sub_key}"] = sub_value
338
+ else:
339
+ flat_dict[key] = value
340
+
341
+ return flat_dict
342
+
343
+ @staticmethod
344
+ def map(source: object, target: T, coerce: bool = True) -> T | None:
345
+ """Map an object from one object to another"""
346
+ source_dict: dict | object
347
+ if isinstance(source, dict):
348
+ source_dict = source
349
+ else:
350
+ source_dict = Serialization.convert_object_to_dict(source)
351
+ if not isinstance(source_dict, dict):
352
+ return None
353
+ return Serialization._load_properties(
354
+ source=source_dict, target=target, coerce=coerce
355
+ )
356
+
357
+ @staticmethod
358
+ def to_wide_dictionary_list(
359
+ data: Dict[str, Any] | List[Dict[str, Any]],
360
+ remove_collisions: bool = True,
361
+ raise_error_on_collision: bool = False,
362
+ ) -> List[Dict[str, Any]]:
363
+ """
364
+ Converts a dictionary or list of dictionaries to a list of dictionaries.
365
+
366
+ :param data: Dictionary or list of dictionaries to be converted
367
+ :param remove_collisions: If True, removes duplicate keys from the dictionaries
368
+ :return: List of dictionaries
369
+ """
370
+
371
+ collisions = []
372
+
373
+ def recursive_flatten(prefix, obj):
374
+ """
375
+ Recursively flattens a JSON object.
376
+
377
+ :param prefix: Current key prefix
378
+ :param obj: Object to flatten
379
+ :return: List of flattened dictionaries
380
+ """
381
+ if isinstance(obj, list):
382
+ result = []
383
+ for _, item in enumerate(obj):
384
+ x = recursive_flatten("", item)
385
+ result.extend(x)
386
+ return result
387
+ elif isinstance(obj, dict):
388
+ result = [{}]
389
+ for key, value in obj.items():
390
+ sub_result = recursive_flatten(
391
+ f"{prefix}_{key}" if prefix else key, value
392
+ )
393
+ new_result = []
394
+ for entry in result:
395
+ for sub_entry in sub_result:
396
+ # remove any collisions
397
+
398
+ for k in entry:
399
+ if k in sub_entry:
400
+ if k not in collisions:
401
+ logger.debug(f"Collision detected: {k}")
402
+ collisions.append(k)
403
+ merged = entry.copy()
404
+ merged.update(sub_entry)
405
+ new_result.append(merged)
406
+ result = new_result
407
+ return result
408
+ else:
409
+ return [{prefix: obj}] if prefix else []
410
+
411
+ results = recursive_flatten("", data)
412
+ if remove_collisions:
413
+ results = Serialization.remove_collisions(results, collisions)
414
+
415
+ if raise_error_on_collision and len(collisions) > 0:
416
+ raise ValueError(f"Duplicate keys detected: {collisions}")
417
+
418
+ return results
419
+
420
+ @staticmethod
421
+ def remove_collisions(
422
+ data: List[Dict[str, Any]], collisions: List[str]
423
+ ) -> List[Dict[str, Any]]:
424
+ """
425
+ Removes collisions from a list of dictionaries.
426
+
427
+ :param data: List of dictionaries
428
+ :param collisions: List of collision keys
429
+ :return: List of dictionaries with collisions removed
430
+ """
431
+ for c in collisions:
432
+ for r in data:
433
+ if c in r:
434
+ del r[c]
435
+ return data
436
+
437
+ @staticmethod
438
+ def _load_properties(
439
+ source: dict,
440
+ target: T,
441
+ coerce: bool = True,
442
+ ) -> T | None:
443
+ """
444
+ Converts a source dictionary to a target object.
445
+
446
+ Args:
447
+ source (dict): The source dictionary containing properties.
448
+ target (T): The target object to populate.
449
+ coerce (bool): If True, attempts to convert values to the target attribute types. If False, raises an error for type mismatches.
450
+
451
+ Returns:
452
+ T | None: The populated target object, or None if an error occurred.
453
+ """
454
+ # Ensure target is an instance of the class
455
+ if isinstance(target, type):
456
+ target = target()
457
+
458
+ # Convert source to a dictionary if it has a __dict__ attribute
459
+ if hasattr(source, "__dict__"):
460
+ source = source.__dict__
461
+
462
+ if hasattr(target, "__actively_serializing_data__"):
463
+ setattr(target, "__actively_serializing_data__", True)
464
+
465
+ for key, value in source.items():
466
+ if isinstance(target, dict):
467
+ # our target is a dictionary, so we need to handle this differently
468
+ target[key] = value
469
+ elif Serialization.has_attribute(target, key):
470
+ attr = getattr(target, key)
471
+ expected_type = type(attr)
472
+
473
+ try:
474
+ if isinstance(attr, (int, float, str, bool)):
475
+ if not isinstance(value, expected_type):
476
+ if coerce:
477
+ # Attempt to coerce the value to the expected type
478
+ try:
479
+ if isinstance(value, list) and expected_type is str:
480
+ value = "".join(value)
481
+
482
+ value = expected_type(value)
483
+ except ValueError as e:
484
+ logger.warning(
485
+ f"Warning coercing attribute {key} with value {value}: {e}"
486
+ )
487
+ # TODO: should we set numbers to 0 or a NaN or raise an error
488
+
489
+ setattr(target, key, value)
490
+ # raise ValueError( # pylint: disable=w0707
491
+ # f"Type mismatch for attribute {key}. Expected {expected_type}, got {type(value)}."
492
+ # )
493
+ else:
494
+ raise ValueError(
495
+ f"Type mismatch for attribute {key}. Expected {expected_type}, got {type(value)}."
496
+ )
497
+ setattr(target, key, value)
498
+ elif isinstance(attr, type(None)):
499
+ setattr(target, key, value)
500
+ elif isinstance(attr, list) and isinstance(value, list):
501
+ attr.clear()
502
+ attr.extend(value)
503
+ elif isinstance(attr, dict) and isinstance(value, dict):
504
+ Serialization._load_properties(value, attr, coerce=coerce)
505
+ elif hasattr(attr, "__dict__") and isinstance(value, dict):
506
+ Serialization._load_properties(value, attr, coerce=coerce)
507
+ else:
508
+ setattr(target, key, value)
509
+ except ValueError as e:
510
+ logger.error(
511
+ f"Error setting attribute {key} with value {value}: {e}"
512
+ )
513
+ raise
514
+ except Exception as e: # pylint: disable=w0718
515
+ if not Serialization.has_setter(target, key):
516
+ logger.warning(
517
+ f"Error warning attempting to set attribute {key} with value {value}: {e}. "
518
+ "This usually occurs on properties that don't have setters. "
519
+ "You should add a setter (even with a pass action) for this property or "
520
+ "decorate it with the @exclude_from_serialization to avoid this warning."
521
+ )
522
+ else:
523
+ raise e
524
+
525
+ if hasattr(target, "__actively_serializing_data__"):
526
+ setattr(target, "__actively_serializing_data__", False)
527
+
528
+ return target
529
+
530
+ @staticmethod
531
+ def has_setter(obj: object, attr_name: str) -> bool:
532
+ """Check if the given attribute has a setter defined."""
533
+ cls = obj.__class__
534
+ if not hasattr(cls, attr_name):
535
+ return False
536
+ attr = getattr(cls, attr_name, None)
537
+ return isinstance(attr, property) and attr.fset is not None
538
+
539
+ @staticmethod
540
+ def has_attribute(obj: object, attribute_name: str) -> bool:
541
+ """Check if an object has an attribute"""
542
+ try:
543
+ return hasattr(obj, attribute_name)
544
+ except AttributeError:
545
+ return False
546
+ except Exception as e: # pylint: disable=w0718
547
+ raise RuntimeError(
548
+ "Failed to serialize the object. \n"
549
+ "You may have some validation that is preventing this routine "
550
+ "from completing. Such as a None checker on a getter. \n\n"
551
+ "To work around this create a boolean (bool) property named __actively_serializing_data__. \n"
552
+ "e.g. self.__actively_serializing_data__: bool = False\n\n"
553
+ "Only issue/raise your exception if __actively_serializing_data__ is not True. \n\n"
554
+ "e.g. if not self.some_property and not self.__actively_serializing_data__:\n"
555
+ ' raise ValueError("some_property must be set")\n\n'
556
+ "This procedure will update the property from False to True while serializing, "
557
+ "then back to False once serialization is complete. "
558
+ ) from e
559
+
560
+ @staticmethod
561
+ def to_dict(
562
+ instance: SerializableModel | dict,
563
+ serialize_fn,
564
+ include_none: bool = True,
565
+ ) -> Dict[str, Any]:
566
+ """To Dict / Dictionary"""
567
+
568
+ if instance is None:
569
+ return {}
570
+
571
+ if isinstance(instance, dict):
572
+ return instance
573
+
574
+ def is_primitive(value):
575
+ """Check if the value is a primitive data type."""
576
+ return isinstance(value, (str, int, bool, type(None)))
577
+
578
+ def serialize_value(value):
579
+ """Serialize the value using the provided function."""
580
+
581
+ if isinstance(value, SerializableModel):
582
+ return serialize_fn(
583
+ Serialization.to_dict(
584
+ instance=value,
585
+ serialize_fn=lambda x: x,
586
+ include_none=include_none,
587
+ )
588
+ )
589
+ if isinstance(value, dt.datetime):
590
+ return serialize_fn(value.isoformat())
591
+ elif isinstance(value, float):
592
+ v = serialize_fn(decimal.Decimal(str(value)))
593
+ return v
594
+ elif isinstance(value, decimal.Decimal):
595
+ return serialize_fn(value)
596
+ elif isinstance(value, uuid.UUID):
597
+ return serialize_fn(str(value))
598
+ elif isinstance(value, (bytes, bytearray)):
599
+ return serialize_fn(value.hex())
600
+ elif is_primitive(value):
601
+ return serialize_fn(value)
602
+ elif isinstance(value, list):
603
+ return serialize_fn([serialize_value(v) for v in value])
604
+ elif isinstance(value, dict):
605
+ return serialize_fn({k: serialize_value(v) for k, v in value.items()})
606
+ else:
607
+ return serialize_fn(
608
+ Serialization.to_dict(
609
+ value,
610
+ serialize_fn,
611
+ include_none=include_none,
612
+ )
613
+ )
614
+
615
+ instance_dict = Serialization._add_properties(
616
+ instance, serialize_value, include_none=include_none
617
+ )
618
+
619
+ return instance_dict
620
+
621
+ @staticmethod
622
+ def _add_properties(
623
+ instance: SerializableModel,
624
+ serialize_value,
625
+ include_none: bool = True,
626
+ ) -> dict:
627
+ instance_dict = {}
628
+
629
+ # Add instance variables
630
+ for attr, value in instance.__dict__.items():
631
+ if str(attr) == "T":
632
+ continue
633
+ # don't get the private properties
634
+ if not str(attr).startswith("_"):
635
+ if value is not None or include_none:
636
+ instance_dict[attr] = serialize_value(value)
637
+
638
+ # Add properties
639
+ for name, _ in inspect.getmembers(
640
+ instance.__class__, predicate=inspect.isdatadescriptor
641
+ ):
642
+ prop = None
643
+ try:
644
+ prop = getattr(instance.__class__, name)
645
+ except AttributeError:
646
+ continue
647
+ if isinstance(prop, property):
648
+ # Exclude properties marked with the exclude_from_serialization decorator
649
+ # Check if the property should be excluded
650
+ exclude = getattr(prop.fget, "exclude_from_serialization", False)
651
+ if exclude:
652
+ continue
653
+
654
+ # Skip TypeVar T or instances of DynamoDBModelBase
655
+ if str(name) == "T":
656
+ continue
657
+
658
+ # don't get the private properties
659
+ if not str(name).startswith("_"):
660
+ value = getattr(instance, name)
661
+ if value is not None or include_none:
662
+ instance_dict[name] = serialize_value(value)
663
+
664
+ return instance_dict