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,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
|