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,113 @@
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 Optional, List
8
+ from typing import TYPE_CHECKING
9
+
10
+ from aws_lambda_powertools import Logger
11
+ from boto3_assist.connection import Connection
12
+
13
+
14
+ if TYPE_CHECKING:
15
+ from mypy_boto3_dynamodb import DynamoDBClient, DynamoDBServiceResource
16
+ else:
17
+ DynamoDBClient = object
18
+ DynamoDBServiceResource = object
19
+
20
+
21
+ logger = Logger()
22
+
23
+
24
+ class DynamoDBConnection(Connection):
25
+ """DB Environment"""
26
+
27
+ def __init__(
28
+ self,
29
+ *,
30
+ aws_profile: Optional[str] = None,
31
+ aws_region: Optional[str] = None,
32
+ aws_end_point_url: Optional[str] = None,
33
+ aws_access_key_id: Optional[str] = None,
34
+ aws_secret_access_key: Optional[str] = None,
35
+ assume_role_arn: Optional[str] = None,
36
+ assume_role_chain: Optional[List[str]] = None,
37
+ assume_role_duration_seconds: Optional[int] = 3600,
38
+ ) -> None:
39
+ super().__init__(
40
+ service_name="dynamodb",
41
+ aws_profile=aws_profile,
42
+ aws_region=aws_region,
43
+ aws_access_key_id=aws_access_key_id,
44
+ aws_secret_access_key=aws_secret_access_key,
45
+ aws_end_point_url=aws_end_point_url,
46
+ assume_role_arn=assume_role_arn,
47
+ assume_role_chain=assume_role_chain,
48
+ assume_role_duration_seconds=assume_role_duration_seconds,
49
+ )
50
+
51
+ self.__dynamodb_client: DynamoDBClient | None = None
52
+ self.__dynamodb_resource: DynamoDBServiceResource | None = None
53
+
54
+ self.raise_on_error: bool = True
55
+
56
+ @property
57
+ def client(self) -> DynamoDBClient:
58
+ """DynamoDB Client Connection"""
59
+ if self.__dynamodb_client is None:
60
+ logger.info("Creating DynamoDB Client")
61
+ self.__dynamodb_client = self.session.client
62
+
63
+ if self.raise_on_error and self.__dynamodb_client is None:
64
+ raise RuntimeError("DynamoDB Client is not available")
65
+ return self.__dynamodb_client
66
+
67
+ @client.setter
68
+ def client(self, value: DynamoDBClient):
69
+ logger.info("Setting DynamoDB Client")
70
+ self.__dynamodb_client = value
71
+
72
+ @property
73
+ def dynamodb_client(self) -> DynamoDBClient:
74
+ """
75
+ DynamoDB Client Connection
76
+ - Backward Compatible. You should use client instead
77
+ """
78
+ return self.client
79
+
80
+ @dynamodb_client.setter
81
+ def dynamodb_client(self, value: DynamoDBClient):
82
+ logger.info("Setting DynamoDB Client")
83
+ self.__dynamodb_client = value
84
+
85
+ @property
86
+ def resource(self) -> DynamoDBServiceResource:
87
+ """DynamoDB Resource Connection"""
88
+ if self.__dynamodb_resource is None:
89
+ logger.info("Creating DynamoDB Resource")
90
+ self.__dynamodb_resource = self.session.resource
91
+
92
+ if self.raise_on_error and self.__dynamodb_resource is None:
93
+ raise RuntimeError("DynamoDB Resource is not available")
94
+
95
+ return self.__dynamodb_resource
96
+
97
+ @resource.setter
98
+ def resource(self, value: DynamoDBServiceResource):
99
+ logger.info("Setting DynamoDB Resource")
100
+ self.__dynamodb_resource = value
101
+
102
+ @property
103
+ def dynamodb_resource(self) -> DynamoDBServiceResource:
104
+ """
105
+ DynamoDB Resource Connection
106
+ - Backward Compatible. You should use resource instead
107
+ """
108
+ return self.resource
109
+
110
+ @dynamodb_resource.setter
111
+ def dynamodb_resource(self, value: DynamoDBServiceResource):
112
+ logger.info("Setting DynamoDB Resource")
113
+ self.__dynamodb_resource = value
@@ -0,0 +1,333 @@
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, Any, Dict
8
+
9
+ from boto3.dynamodb.conditions import ConditionBase, Key, And, Equals
10
+ from aws_lambda_powertools import Logger
11
+ from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex
12
+
13
+ logger = Logger()
14
+
15
+
16
+ class DynamoDBHelpers:
17
+ """Dynamo DB Helper Functions"""
18
+
19
+ def __init__(self) -> None:
20
+ pass
21
+
22
+ def get_filter_expressions(
23
+ self, key: ConditionBase | And | Equals
24
+ ) -> Dict[str, Any] | None:
25
+ """Get the filter expression"""
26
+ value = None
27
+ try:
28
+ keys: List[Dict[str, Any]] = []
29
+ expression = {
30
+ "expression_format": key.expression_format,
31
+ "expression_operator": key.expression_operator,
32
+ "keys": keys, # Initialize 'keys' as an empty list helps with mypy linting
33
+ }
34
+
35
+ exp = key.get_expression()
36
+ key_values = exp["values"]
37
+ for v in key_values:
38
+ kv = self._get_key_info(v)
39
+ k: dict[str, dict[str, Any]] = {"key": kv}
40
+
41
+ if k:
42
+ try:
43
+ keys.append(k)
44
+ except Exception as e: # pylint: disable=w0718
45
+ logger.error({"exception": str(e)})
46
+
47
+ if isinstance(keys, list):
48
+ expression["keys"] = keys
49
+ else:
50
+ expression["keys"] = []
51
+
52
+ expression["sort"] = self.get_key_sort(key)
53
+
54
+ value = expression
55
+ except Exception as e: # pylint: disable=w0718
56
+ logger.error(str(e))
57
+
58
+ return value
59
+
60
+ def _get_key_info(self, value: ConditionBase | And | Key) -> dict[str, Any]:
61
+ """
62
+ Get Key Information. This is helpful for logging and
63
+ visualizing what the key looks like
64
+ """
65
+ key_value: Any = None
66
+ key_name: Any = None
67
+ values = {}
68
+ if isinstance(value, Key):
69
+ key_name = value.name
70
+ elif isinstance(value, str):
71
+ key_value = value
72
+ else:
73
+ key_values = value.get_expression()["values"]
74
+ key: Key = key_values[0]
75
+ key_name = key.name
76
+ key_value = key_values[1]
77
+
78
+ try:
79
+ index = 0
80
+ sub_values = value.get_expression()["values"]
81
+ if sub_values:
82
+ for (
83
+ v
84
+ ) in sub_values: # value._values: # pylint: disable=w0212,w0012,
85
+ if index > 0:
86
+ values[f"value_{index}"] = v
87
+ index += 1
88
+ except: # noqa e722, pylint: disable=w0702
89
+ pass
90
+
91
+ key_info: Dict[str, Any] = {
92
+ "name": key_name,
93
+ "key": key_value,
94
+ "expression_format": (
95
+ None if not isinstance(value, And) else value.expression_format
96
+ ),
97
+ "expression_operator": (
98
+ None if not isinstance(value, And) else value.expression_operator
99
+ ),
100
+ "has_grouped_values": (
101
+ None if not isinstance(value, And) else value.has_grouped_values
102
+ ),
103
+ "values": values,
104
+ }
105
+
106
+ return key_info
107
+
108
+ def get_key_sort(self, condition: ConditionBase) -> str:
109
+ """Gets the sort key"""
110
+ try:
111
+ and_values: ConditionBase = condition.get_expression()["values"][1]
112
+ keys = and_values.get_expression()["values"]
113
+ # second is the sort (element 0 is pk)
114
+ sort = str(keys[1])
115
+ return sort
116
+ except Exception as e: # pylint: disable=w0718
117
+ logger.error({"exception": str(e)})
118
+ return "unknown"
119
+
120
+ def wrap_response(self, items, dynamodb_response: dict, diagnostics) -> dict:
121
+ """A wrapper for response data"""
122
+ last_key = dynamodb_response.get("LastEvaluatedKey", None)
123
+ more = last_key is not None
124
+
125
+ # conform the dynamodb responses
126
+ response = {
127
+ "Items": items,
128
+ "LastKey": last_key,
129
+ "Count": dynamodb_response.get("Count"),
130
+ "Scanned": dynamodb_response.get("ScannedCount"),
131
+ "MoreRecords": more,
132
+ "Diagnostics": diagnostics,
133
+ }
134
+
135
+ return response
136
+
137
+ def wrap_collection_response(self, collection: List[dict]) -> dict[str, List]:
138
+ """
139
+ Wraps Up Some usefull information when dealing with
140
+
141
+ """
142
+ response: dict[str, Any] = {"Items": [], "Batches": []}
143
+ record_start: int = 0
144
+ total_count = 0
145
+ total_scanned_count = 0
146
+ record_end = 0
147
+ for item in collection:
148
+ record_start += 1
149
+ record_end = record_end + len(item["Items"])
150
+ response["Items"].extend(item["Items"])
151
+
152
+ batch: dict[str, Any] = {}
153
+ if "LastEvaluatedKey" in item:
154
+ batch["LastKey"] = item["LastEvaluatedKey"]
155
+
156
+ if "Count" in item:
157
+ batch["Count"] = item["Count"]
158
+ total_count += item["Count"]
159
+
160
+ if "ScannedCount" in item:
161
+ batch["ScannedCount"] = item["ScannedCount"]
162
+ total_scanned_count += item["ScannedCount"]
163
+
164
+ batch["Records"] = {"start": record_start, "end": record_end}
165
+
166
+ response["Batches"].append(batch)
167
+
168
+ record_start = record_end
169
+
170
+ response["Count"] = total_count
171
+ response["ScannedCount"] = total_scanned_count
172
+
173
+ return response
174
+
175
+ @staticmethod
176
+ def validate_dynamodb_format(item):
177
+ """validate_dynamodb_format"""
178
+
179
+ def validate_attribute(key, value, path):
180
+ logger.debug({"key": key, "value": value, "path": path})
181
+ if not isinstance(value, dict):
182
+ return (
183
+ False,
184
+ f"Error at key [{path}]: Expected a dictionary, got {type(value).__name__}. Review [{path}] it should have an M ",
185
+ )
186
+ if len(value) != 1:
187
+ return (
188
+ False,
189
+ f"Error at {path}: Dictionary should contain exactly one key-value pair",
190
+ )
191
+ type_key = list(value.keys())[0]
192
+ if type_key not in [
193
+ "S",
194
+ "N",
195
+ "B",
196
+ "SS",
197
+ "NS",
198
+ "BS",
199
+ "M",
200
+ "L",
201
+ "NULL",
202
+ "BOOL",
203
+ ]:
204
+ return False, f"Error at {path}: Invalid type key '{type_key}'"
205
+ type_value = value[type_key]
206
+ if type_key == "S" and not isinstance(type_value, str):
207
+ return (
208
+ False,
209
+ f"Error at {path}: Expected a string for type 'S', got {type(type_value).__name__}",
210
+ )
211
+ if type_key == "N" and not isinstance(type_value, (int, float, str)):
212
+ return (
213
+ False,
214
+ f"Error at {path}: Expected a number for type 'N', got {type(type_value).__name__}",
215
+ )
216
+ if type_key == "B" and not isinstance(type_value, bytes):
217
+ return (
218
+ False,
219
+ f"Error at {path}: Expected bytes for type 'B', got {type(type_value).__name__}",
220
+ )
221
+ if type_key == "SS" and not (
222
+ isinstance(type_value, list)
223
+ and all(isinstance(i, str) for i in type_value)
224
+ ):
225
+ return (
226
+ False,
227
+ f"Error at {path}: Expected a list of strings for type 'SS', got {type(type_value).__name__}",
228
+ )
229
+ if type_key == "NS" and not (
230
+ isinstance(type_value, list)
231
+ and all(isinstance(i, (int, float, str)) for i in type_value)
232
+ ):
233
+ return (
234
+ False,
235
+ f"Error at {path}: Expected a list of numbers for type 'NS', got {type(type_value).__name__}",
236
+ )
237
+ if type_key == "BS" and not (
238
+ isinstance(type_value, list)
239
+ and all(isinstance(i, bytes) for i in type_value)
240
+ ):
241
+ return (
242
+ False,
243
+ f"Error at {path}: Expected a list of bytes for type 'BS', got {type(type_value).__name__}",
244
+ )
245
+ if type_key == "M":
246
+ if not isinstance(type_value, dict):
247
+ return (
248
+ False,
249
+ f"Error at {path}: Expected a dictionary for type 'M', got {type(type_value).__name__}",
250
+ )
251
+ for k, v in type_value.items():
252
+ valid, error = validate_attribute(k, v, f"{path}.{k}")
253
+ if not valid:
254
+ return False, error
255
+ if type_key == "L":
256
+ if not isinstance(type_value, list):
257
+ return (
258
+ False,
259
+ f"Error at {path}: Expected a list for type 'L', got {type(type_value).__name__}",
260
+ )
261
+ for index, item in enumerate(type_value):
262
+ valid, error = validate_attribute(
263
+ f"{index}", item, f"{path}[{index}]"
264
+ )
265
+ if not valid:
266
+ return False, error
267
+ if type_key == "NULL" and type_value is not True:
268
+ return (
269
+ False,
270
+ f"Error at {path}: Expected True for type 'NULL', got {type(type_value).__name__}",
271
+ )
272
+ if type_key == "BOOL" and not isinstance(type_value, bool):
273
+ return (
274
+ False,
275
+ f"Error at {path}: Expected a boolean for type 'BOOL', got {type(type_value).__name__}",
276
+ )
277
+ return True, None
278
+
279
+ if not isinstance(item, dict):
280
+ return False, f"Error: Item ({item}) should be a dictionary"
281
+ for key, value in item.items():
282
+ if not isinstance(key, str):
283
+ return (
284
+ False,
285
+ f"Error: Key '{key}' should be a string, got {type(key).__name__}",
286
+ )
287
+ valid, error = validate_attribute(key, value, key)
288
+ if not valid:
289
+ return False, error
290
+ return True, None
291
+
292
+ @staticmethod
293
+ def clean_null_values(item):
294
+ """
295
+ Recursively traverse the dictionary and handle "null" values.
296
+ Args:
297
+ item (dict or list): The dictionary or list to clean.
298
+ Returns:
299
+ The cleaned dictionary or list.
300
+ """
301
+ if isinstance(item, dict):
302
+ cleaned = {}
303
+ for k, v in item.items():
304
+ if v == "null":
305
+ print(f"Found 'null' value at key: {k}, replacing with None")
306
+ cleaned[k] = "" # Or handle it as you see fit
307
+ elif isinstance(v, (dict, list)):
308
+ cleaned[k] = DynamoDBHelpers.clean_null_values(v)
309
+ else:
310
+ cleaned[k] = v
311
+ return cleaned
312
+ elif isinstance(item, list):
313
+ return [DynamoDBHelpers.clean_null_values(i) for i in item]
314
+ else:
315
+ return item
316
+
317
+ def keys_to_dictionary(self, keys: List[DynamoDBIndex]) -> dict:
318
+ """_summary_
319
+
320
+ Args:
321
+ keys (List[DynamoDBKey]): _description_
322
+
323
+ Returns:
324
+ dict: _description_
325
+ """
326
+ key_dict: dict = {}
327
+ for key in keys:
328
+ if key.partition_key and key.partition_key.value:
329
+ key_dict[key.partition_key.attribute_name] = key.partition_key.value
330
+ if key.sort_key.attribute_name and key.sort_key.value:
331
+ key_dict[key.sort_key.attribute_name] = key.sort_key.value
332
+
333
+ return key_dict
@@ -0,0 +1,102 @@
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
+ import json
9
+ import gzip
10
+ from typing import List
11
+ from aws_lambda_powertools import Logger
12
+ from botocore.exceptions import ClientError
13
+ from boto3_assist.dynamodb.dynamodb import DynamoDB
14
+ from boto3_assist.dynamodb.dynamodb_helpers import DynamoDBHelpers
15
+
16
+ logger = Logger()
17
+
18
+
19
+ class DynamoDBImporter:
20
+ """
21
+ Import files to your database
22
+ Currently supports json files
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ *,
28
+ table_name: str,
29
+ db: DynamoDB,
30
+ ):
31
+ self.table_name = table_name
32
+ self.db = db
33
+
34
+ def import_json_file(self, json_file_path: str) -> None:
35
+ """Import a json or gzip-compressed json file into the database"""
36
+
37
+ if os.path.exists(json_file_path) is False:
38
+ raise FileNotFoundError(f"File not found: {json_file_path}")
39
+ if json_file_path.endswith(".gz"):
40
+ data = self.read_gzip_file(json_file_path)
41
+ elif json_file_path.endswith(".json"):
42
+ with open(json_file_path, "r", encoding="utf-8") as json_file:
43
+ data = json.load(json_file)
44
+ else:
45
+ raise ValueError(f"Unsupported file type: {json_file_path}")
46
+
47
+ # table = self.db.dynamodb_resource.Table(self.table_name)
48
+ # with table.batch_writer() as batch:
49
+ for item in data:
50
+ try:
51
+ item = DynamoDBHelpers.clean_null_values(item=item)
52
+
53
+ self.db.save(item=item, table_name=self.table_name)
54
+ # batch.put_item(Item=item)
55
+
56
+ except ClientError as e:
57
+ logger.exception(str(e))
58
+ raise e
59
+
60
+ def import_json_files(self, json_file_paths: list[str]) -> None:
61
+ """Import multiple json files into the database"""
62
+ for json_file_path in json_file_paths:
63
+ if os.path.exists(json_file_path) is False:
64
+ raise FileNotFoundError(f"File not found: {json_file_path}")
65
+ else:
66
+ if json_file_path.endswith(".gz") or json_file_path.endswith(".json"):
67
+ self.import_json_file(json_file_path)
68
+ else:
69
+ if os.path.isdir(json_file_path):
70
+ logger.warning(
71
+ f"Unsupported sub directory import {json_file_path}. "
72
+ "Skipping import on this file."
73
+ )
74
+ else:
75
+ logger.warning(
76
+ f"Unsupported file type: {json_file_path}. "
77
+ "Skipping import on this file. Files should end with .gz or .json"
78
+ )
79
+
80
+ def read_gzip_file(self, file_path: str) -> List[dict]:
81
+ """
82
+ Reads a gzip file
83
+ Args:
84
+ file_path (str): path to the gzip file
85
+
86
+ Returns:
87
+ List[dict]: list of items
88
+ """
89
+ try:
90
+ with gzip.open(file_path, "rt", encoding="utf-8") as f:
91
+ data = json.load(f)
92
+ if isinstance(data, list):
93
+ return [item["Item"] for item in data]
94
+ except json.JSONDecodeError:
95
+ pass
96
+
97
+ # If not a valid array, read line by line
98
+ items = []
99
+ with gzip.open(file_path, "rt", encoding="utf-8") as f:
100
+ for line in f:
101
+ items.append(json.loads(line)["Item"])
102
+ return items