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