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,349 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
Maintainers: Eric Wilson
|
|
4
|
+
MIT License. See Project Root for the license information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import uuid
|
|
8
|
+
from datetime import UTC, datetime, timedelta, timezone
|
|
9
|
+
from typing import Any
|
|
10
|
+
import pytz # type: ignore
|
|
11
|
+
from aws_lambda_powertools import Logger
|
|
12
|
+
from dateutil.relativedelta import relativedelta
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logger = Logger()
|
|
16
|
+
|
|
17
|
+
_last_timestamp = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DatetimeUtility:
|
|
21
|
+
@staticmethod
|
|
22
|
+
def get_elapsed_time(start: datetime, end=None) -> str:
|
|
23
|
+
end = end or DatetimeUtility.get_utc_now()
|
|
24
|
+
delta: timedelta = end - start
|
|
25
|
+
|
|
26
|
+
total_seconds = delta.total_seconds()
|
|
27
|
+
days = int(total_seconds // (3600 * 24))
|
|
28
|
+
total_seconds %= 3600 * 24
|
|
29
|
+
hours = int(total_seconds // 3600)
|
|
30
|
+
total_seconds %= 3600
|
|
31
|
+
minutes = int(total_seconds // 60)
|
|
32
|
+
seconds = int(total_seconds % 60)
|
|
33
|
+
milliseconds = int(delta.microseconds / 1000)
|
|
34
|
+
timespan = f"{days} days, {hours} hours, {minutes} minutes, {seconds} seconds, {milliseconds} milliseconds"
|
|
35
|
+
|
|
36
|
+
return timespan
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def get_start_time() -> datetime:
|
|
40
|
+
return DatetimeUtility.get_utc_now()
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def get_utc_now() -> datetime:
|
|
44
|
+
# datetime.utcnow()
|
|
45
|
+
# below is the preferred over datetime.utcnow()
|
|
46
|
+
return datetime.now(timezone.utc)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def string_to_date(string_date: str | datetime) -> datetime | None:
|
|
50
|
+
"""
|
|
51
|
+
Description: takes a string value and returns it as a datetime.
|
|
52
|
+
If the value is already a datetime type, it will return it as is, otherwise
|
|
53
|
+
the returned value is None
|
|
54
|
+
string_date: str must be in format of %Y-%m-%dT%H:%M:%S.%f
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
if not string_date or str(string_date) == "None":
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
if isinstance(string_date, datetime):
|
|
61
|
+
return string_date
|
|
62
|
+
|
|
63
|
+
if "Z" in str(string_date):
|
|
64
|
+
string_date = str(string_date).replace("Z", "+00:00")
|
|
65
|
+
string_date = str(string_date)
|
|
66
|
+
string_date = string_date.replace(" ", "T")
|
|
67
|
+
string_date = string_date.replace("Z", "")
|
|
68
|
+
string_date = string_date.replace("+00:00", "")
|
|
69
|
+
# todo determine the format
|
|
70
|
+
date_formats = [
|
|
71
|
+
"%Y-%m-%dT%H:%M:%S.%f",
|
|
72
|
+
"%Y-%m-%dT%H:%M:%S",
|
|
73
|
+
"%Y-%m-%d",
|
|
74
|
+
"%m-%d-%Y",
|
|
75
|
+
"%m-%d-%y",
|
|
76
|
+
"%Y/%m/%d",
|
|
77
|
+
"%m/%d/%Y",
|
|
78
|
+
"%m/%d/%y",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
result: datetime | None = None
|
|
82
|
+
try:
|
|
83
|
+
if isinstance(string_date, str):
|
|
84
|
+
for date_format in date_formats:
|
|
85
|
+
try:
|
|
86
|
+
result = datetime.strptime(string_date, date_format)
|
|
87
|
+
break
|
|
88
|
+
except ValueError:
|
|
89
|
+
pass
|
|
90
|
+
# if nothing the we need to raise an error
|
|
91
|
+
if result is None:
|
|
92
|
+
raise ValueError(f"Unable to parse date: {string_date}")
|
|
93
|
+
|
|
94
|
+
elif isinstance(string_date, datetime):
|
|
95
|
+
result = string_date
|
|
96
|
+
else:
|
|
97
|
+
logger.warning(
|
|
98
|
+
{
|
|
99
|
+
"metric_filter": "string_to_date_warning",
|
|
100
|
+
"datetime": string_date,
|
|
101
|
+
"action": "returning none",
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
except Exception as e: # noqa: E722, pylint: disable=W0718
|
|
105
|
+
msg = {
|
|
106
|
+
"metric_filter": "string_to_date_error",
|
|
107
|
+
"datetime": string_date,
|
|
108
|
+
"error": str(e),
|
|
109
|
+
"action": "returning none",
|
|
110
|
+
"type": type(string_date).__name__,
|
|
111
|
+
"accepted_formats": date_formats,
|
|
112
|
+
}
|
|
113
|
+
logger.error(msg=msg)
|
|
114
|
+
|
|
115
|
+
raise RuntimeError(msg) from e
|
|
116
|
+
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def to_datetime(
|
|
121
|
+
value, default: datetime | None = None, tzinfo=UTC
|
|
122
|
+
) -> datetime | None:
|
|
123
|
+
"""
|
|
124
|
+
Description: takes a value and attempts to turn it into a datetime object
|
|
125
|
+
Returns: datetime or None
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
result = DatetimeUtility.string_to_date(value)
|
|
129
|
+
|
|
130
|
+
if result is None and default is not None:
|
|
131
|
+
if not isinstance(default, datetime):
|
|
132
|
+
default = DatetimeUtility.string_to_date(value)
|
|
133
|
+
result = default
|
|
134
|
+
|
|
135
|
+
if result and isinstance(result, datetime):
|
|
136
|
+
result = result.replace(tzinfo=tzinfo)
|
|
137
|
+
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def to_datetime_utc(value, default: datetime | None = None) -> datetime | None:
|
|
142
|
+
"""
|
|
143
|
+
Description: takes a value and attempts to turn it into a datetime object
|
|
144
|
+
Returns: datetime or None
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
result = DatetimeUtility.to_datetime(value, default, tzinfo=UTC)
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def to_date_string(value: Any, default: datetime | None | str = None) -> str | None:
|
|
152
|
+
"""
|
|
153
|
+
Description: takes a value and attempts to turn it into a datetime object
|
|
154
|
+
Returns: datetime or None
|
|
155
|
+
"""
|
|
156
|
+
value = DatetimeUtility.to_datetime(value=value)
|
|
157
|
+
result = DatetimeUtility.to_string(value, date_format="%Y-%m-%d")
|
|
158
|
+
if result is None and default is not None:
|
|
159
|
+
if isinstance(default, datetime):
|
|
160
|
+
result = DatetimeUtility.to_string(default, date_format="%Y-%m-%d")
|
|
161
|
+
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def to_time_string(value, default: datetime | None = None) -> str | None:
|
|
166
|
+
"""
|
|
167
|
+
Description: takes a value and attempts to turn it into a datetime object
|
|
168
|
+
Returns: datetime or None
|
|
169
|
+
"""
|
|
170
|
+
value = DatetimeUtility.to_datetime(value=value)
|
|
171
|
+
result = f"{DatetimeUtility.to_string(value, date_format='%H:%M:%S')}+00:00"
|
|
172
|
+
if result is None and default is not None:
|
|
173
|
+
result = default
|
|
174
|
+
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def to_string(
|
|
179
|
+
value: datetime, date_format: str = "%Y-%m-%d-%H-%M-%S-%f"
|
|
180
|
+
) -> str | None:
|
|
181
|
+
"""
|
|
182
|
+
Description: takes a string value and returns it as a datetime.
|
|
183
|
+
If the value is already a datetime type, it will return it as is, otherwise
|
|
184
|
+
the returned value is None
|
|
185
|
+
"""
|
|
186
|
+
# todo determine the format
|
|
187
|
+
if not value:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
result = value.strftime(date_format)
|
|
191
|
+
|
|
192
|
+
return result
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def datetime_from_uuid1(uuid1: uuid.UUID) -> datetime:
|
|
196
|
+
"""
|
|
197
|
+
Converts a uuid1 to a datetime
|
|
198
|
+
"""
|
|
199
|
+
ns = 0x01B21DD213814000
|
|
200
|
+
timestamp = datetime.fromtimestamp(
|
|
201
|
+
(uuid1.time - ns) * 100 / 1e9, tz=timezone.utc
|
|
202
|
+
)
|
|
203
|
+
return timestamp
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def fromtimestamp(value: float, default=None) -> datetime:
|
|
207
|
+
result = default
|
|
208
|
+
try:
|
|
209
|
+
if "-" in str(value):
|
|
210
|
+
value = float(str(value).replace("-", "."))
|
|
211
|
+
result = datetime.fromtimestamp(float(value))
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.error(str(e))
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def uuid1_utc(node=0, clock_seq=0, timestamp=None):
|
|
220
|
+
global _last_timestamp # pylint: disable=w0603
|
|
221
|
+
|
|
222
|
+
if not timestamp:
|
|
223
|
+
timestamp = float(DatetimeUtility.get_utc_now().timestamp())
|
|
224
|
+
if isinstance(timestamp, datetime):
|
|
225
|
+
timestamp = timestamp.timestamp()
|
|
226
|
+
|
|
227
|
+
nanoseconds = int(timestamp * 1e9)
|
|
228
|
+
# import time
|
|
229
|
+
# t = time.time_ns()
|
|
230
|
+
# ns = int(t * 1e9)
|
|
231
|
+
|
|
232
|
+
# 0x01b21dd213814000 is the number of 100-ns intervals between the
|
|
233
|
+
# UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00.
|
|
234
|
+
timestamp = nanoseconds // 100 + 0x01B21DD213814000
|
|
235
|
+
if _last_timestamp is not None and timestamp <= _last_timestamp:
|
|
236
|
+
timestamp = _last_timestamp + 1
|
|
237
|
+
_last_timestamp = timestamp
|
|
238
|
+
if clock_seq is None:
|
|
239
|
+
import random
|
|
240
|
+
|
|
241
|
+
clock_seq = random.getrandbits(14) # instead of stable storage
|
|
242
|
+
time_low = timestamp & 0xFFFFFFFF
|
|
243
|
+
time_mid = (timestamp >> 32) & 0xFFFF
|
|
244
|
+
time_hi_version = (timestamp >> 48) & 0x0FFF
|
|
245
|
+
clock_seq_low = clock_seq & 0xFF
|
|
246
|
+
clock_seq_hi_variant = (clock_seq >> 8) & 0x3F
|
|
247
|
+
if node is None:
|
|
248
|
+
node = uuid.getnode()
|
|
249
|
+
return uuid.UUID(
|
|
250
|
+
fields=(
|
|
251
|
+
time_low,
|
|
252
|
+
time_mid,
|
|
253
|
+
time_hi_version,
|
|
254
|
+
clock_seq_hi_variant,
|
|
255
|
+
clock_seq_low,
|
|
256
|
+
node,
|
|
257
|
+
),
|
|
258
|
+
version=1,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
def add_month(dt: datetime, months: int = 1) -> datetime:
|
|
263
|
+
"""Add a month to the current date
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
dt (datetime): datetime
|
|
267
|
+
months (int): the number of months
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
datetime: X Month(s) added to the input dt
|
|
271
|
+
"""
|
|
272
|
+
new_date = dt + relativedelta(months=+months)
|
|
273
|
+
new_date = new_date + relativedelta(microseconds=-1)
|
|
274
|
+
|
|
275
|
+
return new_date
|
|
276
|
+
|
|
277
|
+
@staticmethod
|
|
278
|
+
def add_days(dt: datetime, days: int = 1) -> datetime:
|
|
279
|
+
"""Add a day to the current date
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
dt (datetime): datetime
|
|
283
|
+
days (int): the number of days, use a negative number to subtract
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
datetime: X days added to the input dt
|
|
287
|
+
"""
|
|
288
|
+
new_date = dt + relativedelta(days=+days)
|
|
289
|
+
new_date = new_date + relativedelta(microseconds=-1)
|
|
290
|
+
|
|
291
|
+
return new_date
|
|
292
|
+
|
|
293
|
+
@staticmethod
|
|
294
|
+
def add_minutes(dt: datetime, minutes: int = 1) -> datetime:
|
|
295
|
+
"""Add a month to the current date
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
dt (datetime): datetime
|
|
299
|
+
months (int): the number of months
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
datetime: One Month added to the input dt
|
|
303
|
+
"""
|
|
304
|
+
new_date = dt + relativedelta(minutes=+minutes)
|
|
305
|
+
new_date = new_date + relativedelta(microseconds=-1)
|
|
306
|
+
|
|
307
|
+
return new_date
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def to_timezone(utc_datetime: datetime, timezone_name: str) -> datetime:
|
|
311
|
+
"""_summary_
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
utc_datetime (datetime): datetime in utc
|
|
315
|
+
timezone (str): 'US/Eastern', 'US/Mountain', etc
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
datetime: in the correct timezone
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
tz = pytz.timezone(timezone_name)
|
|
322
|
+
result = utc_datetime.astimezone(tz)
|
|
323
|
+
return result
|
|
324
|
+
|
|
325
|
+
@staticmethod
|
|
326
|
+
def get_timestamp(value: datetime | None | str) -> float:
|
|
327
|
+
"""Get a timestamp from a date or 0.0"""
|
|
328
|
+
if value is None:
|
|
329
|
+
return 0.0
|
|
330
|
+
if not isinstance(value, datetime):
|
|
331
|
+
value = DatetimeUtility.to_datetime_utc(value=value)
|
|
332
|
+
|
|
333
|
+
if not isinstance(value, datetime):
|
|
334
|
+
return 0.0
|
|
335
|
+
ts = value.timestamp()
|
|
336
|
+
return ts
|
|
337
|
+
|
|
338
|
+
@staticmethod
|
|
339
|
+
def get_timestamp_or_none(value: datetime | None | str) -> float | None:
|
|
340
|
+
"""Get a timestamp from a date or None"""
|
|
341
|
+
if value is None:
|
|
342
|
+
return None
|
|
343
|
+
if not isinstance(value, datetime):
|
|
344
|
+
value = DatetimeUtility.to_datetime_utc(value=value)
|
|
345
|
+
|
|
346
|
+
if not isinstance(value, datetime):
|
|
347
|
+
return None
|
|
348
|
+
ts = value.timestamp()
|
|
349
|
+
return ts
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
Maintainers: Eric Wilson
|
|
4
|
+
MIT License. See Project Root for the license information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from typing import Any, Dict, List, Union
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DecimalConversionUtility:
|
|
12
|
+
"""
|
|
13
|
+
Utility class for handling decimal conversions between Python types and DynamoDB.
|
|
14
|
+
|
|
15
|
+
DynamoDB stores all numbers as Decimal types, but Python applications often
|
|
16
|
+
expect int or float types. This utility provides conversion methods to handle
|
|
17
|
+
the transformation seamlessly.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def convert_decimals_to_native_types(data: Any) -> Any:
|
|
22
|
+
"""
|
|
23
|
+
Recursively converts Decimal objects to native Python types (int or float).
|
|
24
|
+
|
|
25
|
+
This is typically used when deserializing data from DynamoDB, where all
|
|
26
|
+
numbers are stored as Decimal objects but the application expects native
|
|
27
|
+
Python numeric types.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
data: The data structure to convert. Can be dict, list, or any other type.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The data structure with Decimal objects converted to int or float.
|
|
34
|
+
"""
|
|
35
|
+
if isinstance(data, dict):
|
|
36
|
+
return {
|
|
37
|
+
key: DecimalConversionUtility.convert_decimals_to_native_types(value)
|
|
38
|
+
for key, value in data.items()
|
|
39
|
+
}
|
|
40
|
+
elif isinstance(data, list):
|
|
41
|
+
return [
|
|
42
|
+
DecimalConversionUtility.convert_decimals_to_native_types(item)
|
|
43
|
+
for item in data
|
|
44
|
+
]
|
|
45
|
+
elif isinstance(data, Decimal):
|
|
46
|
+
# Convert Decimal to int if it's a whole number, otherwise to float
|
|
47
|
+
if data % 1 == 0:
|
|
48
|
+
return int(data)
|
|
49
|
+
else:
|
|
50
|
+
return float(data)
|
|
51
|
+
else:
|
|
52
|
+
return data
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def convert_native_types_to_decimals(data: Any) -> Any:
|
|
56
|
+
"""
|
|
57
|
+
Recursively converts native Python numeric types (int, float) to Decimal objects.
|
|
58
|
+
|
|
59
|
+
This is typically used when serializing data for DynamoDB, where all
|
|
60
|
+
numbers should be stored as Decimal objects for precision.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
data: The data structure to convert. Can be dict, list, or any other type.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
The data structure with int and float objects converted to Decimal.
|
|
67
|
+
"""
|
|
68
|
+
if isinstance(data, dict):
|
|
69
|
+
return {
|
|
70
|
+
key: DecimalConversionUtility.convert_native_types_to_decimals(value)
|
|
71
|
+
for key, value in data.items()
|
|
72
|
+
}
|
|
73
|
+
elif isinstance(data, list):
|
|
74
|
+
return [
|
|
75
|
+
DecimalConversionUtility.convert_native_types_to_decimals(item)
|
|
76
|
+
for item in data
|
|
77
|
+
]
|
|
78
|
+
elif isinstance(data, float):
|
|
79
|
+
# Convert float to Decimal using string representation for precision
|
|
80
|
+
return Decimal(str(data))
|
|
81
|
+
elif isinstance(data, int) and not isinstance(data, bool):
|
|
82
|
+
# Convert int to Decimal, but exclude bool (which is a subclass of int)
|
|
83
|
+
return Decimal(data)
|
|
84
|
+
else:
|
|
85
|
+
return data
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def is_numeric_type(value: Any) -> bool:
|
|
89
|
+
"""
|
|
90
|
+
Check if a value is a numeric type (int, float, or Decimal).
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
value: The value to check.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
True if the value is a numeric type, False otherwise.
|
|
97
|
+
"""
|
|
98
|
+
return isinstance(value, (int, float, Decimal)) and not isinstance(value, bool)
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def safe_decimal_conversion(value: Any, default: Any = None) -> Union[Decimal, Any]:
|
|
102
|
+
"""
|
|
103
|
+
Safely convert a value to Decimal, returning a default if conversion fails.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
value: The value to convert to Decimal.
|
|
107
|
+
default: The default value to return if conversion fails.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Decimal representation of the value, or the default if conversion fails.
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
if isinstance(value, Decimal):
|
|
114
|
+
return value
|
|
115
|
+
elif isinstance(value, (int, float)):
|
|
116
|
+
return Decimal(str(value))
|
|
117
|
+
elif isinstance(value, str):
|
|
118
|
+
return Decimal(value)
|
|
119
|
+
else:
|
|
120
|
+
return default
|
|
121
|
+
except (ValueError, TypeError, ArithmeticError):
|
|
122
|
+
return default
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def format_decimal_for_display(value: Decimal, precision: int = 2) -> str:
|
|
126
|
+
"""
|
|
127
|
+
Format a Decimal value for display with specified precision.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
value: The Decimal value to format.
|
|
131
|
+
precision: Number of decimal places to display.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Formatted string representation of the Decimal.
|
|
135
|
+
"""
|
|
136
|
+
if not isinstance(value, Decimal):
|
|
137
|
+
value = DecimalConversionUtility.safe_decimal_conversion(value, Decimal('0'))
|
|
138
|
+
|
|
139
|
+
format_string = f"{{:.{precision}f}}"
|
|
140
|
+
return format_string.format(float(value))
|
|
@@ -0,0 +1,32 @@
|
|
|
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 List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DictionaryUtilitiy:
|
|
11
|
+
"""
|
|
12
|
+
A class to provide utility methods for working with dictionaries.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def find_dict_by_name(
|
|
17
|
+
dict_list: List[dict], key_field: str, name: str
|
|
18
|
+
) -> List[dict] | dict | str:
|
|
19
|
+
"""
|
|
20
|
+
Searches for dictionaries in a list where the key 'name' matches the specified value.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
dict_list (list): A list of dictionaries to search through.
|
|
24
|
+
key_field (str): The key to search for in each dictionary.
|
|
25
|
+
name (str): The value to search for in the 'key_field' key.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
list: A list of dictionaries where the 'key_field' key matches the specified value.
|
|
29
|
+
"""
|
|
30
|
+
# List comprehension to filter dictionaries that have the 'name' key equal to the specified name
|
|
31
|
+
|
|
32
|
+
return [d for d in dict_list if d.get(key_field) == name]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
Maintainers: Eric Wilson
|
|
4
|
+
MIT License. See Project Root for the license information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
import shutil
|
|
10
|
+
import tempfile
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
from aws_lambda_powertools import Logger
|
|
14
|
+
|
|
15
|
+
logger = Logger()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FileOperations:
|
|
19
|
+
"""
|
|
20
|
+
General File Operations
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def makedirs(path):
|
|
28
|
+
"""Create a directory and all sub directories."""
|
|
29
|
+
abs_path = os.path.abspath(path)
|
|
30
|
+
os.makedirs(abs_path, exist_ok=True)
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def clean_directory(path: str):
|
|
34
|
+
"""Clean / Delete all files and directories and sub directories"""
|
|
35
|
+
if path is None:
|
|
36
|
+
return
|
|
37
|
+
if path == "/":
|
|
38
|
+
raise ValueError("Cannot delete root directory")
|
|
39
|
+
|
|
40
|
+
abs_path = os.path.abspath(path)
|
|
41
|
+
if os.path.exists(abs_path):
|
|
42
|
+
items = os.listdir(abs_path)
|
|
43
|
+
for item in items:
|
|
44
|
+
path = os.path.join(abs_path, item)
|
|
45
|
+
if os.path.exists(path):
|
|
46
|
+
try:
|
|
47
|
+
if os.path.isdir(path):
|
|
48
|
+
shutil.rmtree(path)
|
|
49
|
+
elif os.path.isfile(path):
|
|
50
|
+
os.remove(path)
|
|
51
|
+
|
|
52
|
+
except Exception as e: # pylint: disable=W0718
|
|
53
|
+
logger.exception(f"clean up error {str(e)}")
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def get_directory_name(path: str):
|
|
57
|
+
"""
|
|
58
|
+
Get the directory path from a path that is either a directory
|
|
59
|
+
or a path to a file.
|
|
60
|
+
"""
|
|
61
|
+
dirname = os.path.dirname(path)
|
|
62
|
+
return dirname
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def read_file(path: str, encoding: str = "utf-8") -> str:
|
|
66
|
+
"""
|
|
67
|
+
Read a file
|
|
68
|
+
"""
|
|
69
|
+
logger.debug(f"reading file {path}")
|
|
70
|
+
with open(path, "r", encoding=encoding) as file:
|
|
71
|
+
data = file.read()
|
|
72
|
+
return data
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def write_to_file(path: str, data: str, append: bool = False) -> str:
|
|
76
|
+
"""
|
|
77
|
+
Write to a file
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
return FileOperations.write_file(path=path, output=data, append=append)
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def write_file(path: str, output: str, append: bool = False) -> str:
|
|
84
|
+
"""
|
|
85
|
+
Writes to a file
|
|
86
|
+
Args:
|
|
87
|
+
path (str): path
|
|
88
|
+
output (str): text to write to the file
|
|
89
|
+
append (bool): if true this operation will append to the file
|
|
90
|
+
otherwise it will overwrite. the default is to overwrite
|
|
91
|
+
Returns:
|
|
92
|
+
str: path to the file
|
|
93
|
+
"""
|
|
94
|
+
dirname = FileOperations.get_directory_name(path)
|
|
95
|
+
FileOperations.makedirs(dirname)
|
|
96
|
+
mode = "a" if append else "w"
|
|
97
|
+
|
|
98
|
+
if output is None:
|
|
99
|
+
output = ""
|
|
100
|
+
with open(path, mode=mode, encoding="utf-8") as file:
|
|
101
|
+
file.write(output)
|
|
102
|
+
|
|
103
|
+
return path
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def get_file_extension(file_name: str, include_dot: bool = False):
|
|
107
|
+
"""Get the extension of a file"""
|
|
108
|
+
logger.debug(f"getting extension for {file_name}")
|
|
109
|
+
# get the last part of a string after a period .
|
|
110
|
+
extention = os.path.splitext(file_name)[1]
|
|
111
|
+
logger.debug(f"extention is {extention}")
|
|
112
|
+
|
|
113
|
+
if not include_dot:
|
|
114
|
+
if str(extention).startswith("."):
|
|
115
|
+
extention = str(extention).removeprefix(".")
|
|
116
|
+
logger.debug(f"extension after prefix removal: {extention}")
|
|
117
|
+
|
|
118
|
+
return extention
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def get_tmp_directory() -> str:
|
|
122
|
+
"""
|
|
123
|
+
Get the temp directory
|
|
124
|
+
"""
|
|
125
|
+
# are we in an aws lambda function?
|
|
126
|
+
if os.environ.get("AWS_LAMBDA_FUNCTION_NAME"):
|
|
127
|
+
# we are in a lambda function /tmp is the only place
|
|
128
|
+
# we can write to
|
|
129
|
+
if not os.path.exists("/tmp"):
|
|
130
|
+
raise ValueError("Temp directory does not exist.")
|
|
131
|
+
|
|
132
|
+
tmp_dir = "/tmp"
|
|
133
|
+
return tmp_dir
|
|
134
|
+
|
|
135
|
+
return tempfile.gettempdir()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
Maintainers: Eric Wilson
|
|
4
|
+
MIT License. See Project Root for the license information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from urllib.parse import unquote
|
|
8
|
+
from typing import Dict, Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HttpUtility:
|
|
12
|
+
"""HTTP Utilities"""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def get_query_params(query_string: str) -> Dict[str, Any]:
|
|
16
|
+
"""
|
|
17
|
+
Get the query parameters from a query string
|
|
18
|
+
returns a dictionary of key value pairs
|
|
19
|
+
"""
|
|
20
|
+
if not query_string:
|
|
21
|
+
return {}
|
|
22
|
+
|
|
23
|
+
params = {}
|
|
24
|
+
if query_string:
|
|
25
|
+
for param in query_string.split("&"):
|
|
26
|
+
key, value = param.split("=")
|
|
27
|
+
params[key] = unquote(value)
|
|
28
|
+
return params
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def get_query_param(query_string: str | None, key: str) -> str | None:
|
|
32
|
+
"""Get a query parameter from a query string"""
|
|
33
|
+
|
|
34
|
+
if not query_string:
|
|
35
|
+
return None
|
|
36
|
+
params = HttpUtility.get_query_params(query_string)
|
|
37
|
+
if key in params:
|
|
38
|
+
return params[key]
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def decode_url(url: str):
|
|
43
|
+
"""Decodes a URL"""
|
|
44
|
+
|
|
45
|
+
# sometimes a paylaod will have a + added instead of the space
|
|
46
|
+
# or the space encoded value of %2B
|
|
47
|
+
url = url.replace("+", " ")
|
|
48
|
+
return unquote(url)
|
|
File without changes
|