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,507 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
Maintainers: Eric Wilson
|
|
4
|
+
MIT License. See Project Root for the license information.
|
|
5
|
+
https://github.com/geekcafe/boto3-assist
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from typing import Optional, Any
|
|
10
|
+
from boto3.dynamodb.conditions import (
|
|
11
|
+
ConditionBase,
|
|
12
|
+
Key,
|
|
13
|
+
Equals,
|
|
14
|
+
ComparisonCondition,
|
|
15
|
+
And,
|
|
16
|
+
)
|
|
17
|
+
from boto3_assist.dynamodb.dynamodb_key import DynamoDBKey
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DynamoDBIndexes:
|
|
21
|
+
"""Track the indexes"""
|
|
22
|
+
|
|
23
|
+
PRIMARY_INDEX = "primary"
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
self.__indexes: dict[str, DynamoDBIndex] = {}
|
|
27
|
+
|
|
28
|
+
def remove_primary(self):
|
|
29
|
+
"""Remove the primary index"""
|
|
30
|
+
if DynamoDBIndexes.PRIMARY_INDEX in self.__indexes:
|
|
31
|
+
del self.__indexes[DynamoDBIndexes.PRIMARY_INDEX]
|
|
32
|
+
|
|
33
|
+
def add_primary(self, index: DynamoDBIndex):
|
|
34
|
+
"""Add an index"""
|
|
35
|
+
index.name = DynamoDBIndexes.PRIMARY_INDEX
|
|
36
|
+
|
|
37
|
+
if index.name in self.__indexes:
|
|
38
|
+
raise ValueError(
|
|
39
|
+
f"The index {index.name} is already defined in your model somewhere. "
|
|
40
|
+
"This error is generated to protect you from unforeseen issues. "
|
|
41
|
+
"If you models are inheriting from other models, you may have the primary defined twice."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
self.__indexes[DynamoDBIndexes.PRIMARY_INDEX] = index
|
|
45
|
+
|
|
46
|
+
def add_secondary(self, index: DynamoDBIndex):
|
|
47
|
+
"""Add a GSI/LSI index"""
|
|
48
|
+
if index.name is None:
|
|
49
|
+
raise ValueError("Index name cannot be None")
|
|
50
|
+
|
|
51
|
+
# if the index already exists, raise an exception
|
|
52
|
+
if index.name in self.__indexes:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"The index {index.name} is already defined in your model somewhere. "
|
|
55
|
+
"This error is generated to protect you from unforseen issues. "
|
|
56
|
+
"If you models are inheriting from other models, you may have the primary defined twice."
|
|
57
|
+
)
|
|
58
|
+
if index.name == DynamoDBIndexes.PRIMARY_INDEX:
|
|
59
|
+
raise ValueError(f"Index {index.name} is reserved for the primary index")
|
|
60
|
+
if index.partition_key is None:
|
|
61
|
+
raise ValueError("Index must have a partition key")
|
|
62
|
+
|
|
63
|
+
# check if the index.partition_key.attribute_name is already in the index
|
|
64
|
+
for _, v in self.__indexes.items():
|
|
65
|
+
if v.partition_key.attribute_name == index.partition_key.attribute_name:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"The attribute {index.partition_key.attribute_name} is already being used by index "
|
|
68
|
+
f"{v.name}. "
|
|
69
|
+
f"Reusing this attribute would over write the value on index {v.name}"
|
|
70
|
+
)
|
|
71
|
+
# check if the gsi1.sort_key.attribute_name exists
|
|
72
|
+
if index.sort_key is not None:
|
|
73
|
+
for _, v in self.__indexes.items():
|
|
74
|
+
if v.sort_key.attribute_name == index.sort_key.attribute_name:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"The attribute {index.sort_key.attribute_name} is already being used by index "
|
|
77
|
+
f"{v.name}. "
|
|
78
|
+
f"Reusing this attribute would over write the value on index {v.name}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
self.__indexes[index.name] = index
|
|
82
|
+
|
|
83
|
+
def get(self, index_name: str) -> DynamoDBIndex:
|
|
84
|
+
"""Get an index"""
|
|
85
|
+
if index_name not in self.__indexes:
|
|
86
|
+
raise ValueError(f"Index {index_name} not found")
|
|
87
|
+
return self.__indexes[index_name]
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def primary(self) -> DynamoDBIndex | None:
|
|
91
|
+
"""Get the primary index"""
|
|
92
|
+
if DynamoDBIndexes.PRIMARY_INDEX not in self.__indexes:
|
|
93
|
+
return None
|
|
94
|
+
# raise ValueError("Primary index not found")
|
|
95
|
+
return self.__indexes[DynamoDBIndexes.PRIMARY_INDEX]
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def secondaries(self) -> dict[str, DynamoDBIndex]:
|
|
99
|
+
"""Get the secondary indexes"""
|
|
100
|
+
# get all indexes that are not the primary index
|
|
101
|
+
indexes = {
|
|
102
|
+
k: v
|
|
103
|
+
for k, v in self.__indexes.items()
|
|
104
|
+
if k != DynamoDBIndexes.PRIMARY_INDEX
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return indexes
|
|
108
|
+
|
|
109
|
+
def values(self) -> list[DynamoDBIndex]:
|
|
110
|
+
"""Get the values of the indexes"""
|
|
111
|
+
return list(self.__indexes.values())
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class DynamoDBIndex:
|
|
115
|
+
"""A DynamoDB Index"""
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
index_name: Optional[str] = None,
|
|
120
|
+
partition_key: Optional[DynamoDBKey] = None,
|
|
121
|
+
sort_key: Optional[DynamoDBKey] = None,
|
|
122
|
+
description: Optional[str] = None,
|
|
123
|
+
):
|
|
124
|
+
self.name: Optional[str] = index_name
|
|
125
|
+
self.description: Optional[str] = description
|
|
126
|
+
"""Optional description information. Used for self documentation."""
|
|
127
|
+
self.__pk: Optional[DynamoDBKey] = partition_key
|
|
128
|
+
self.__sk: Optional[DynamoDBKey] = sort_key
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def partition_key(self) -> DynamoDBKey:
|
|
132
|
+
"""Get the primary key"""
|
|
133
|
+
if not self.__pk:
|
|
134
|
+
self.__pk = DynamoDBKey()
|
|
135
|
+
return self.__pk
|
|
136
|
+
|
|
137
|
+
@partition_key.setter
|
|
138
|
+
def partition_key(self, value: DynamoDBKey):
|
|
139
|
+
self.__pk = value
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def sort_key(self) -> DynamoDBKey:
|
|
143
|
+
"""Get the sort key"""
|
|
144
|
+
if not self.__sk:
|
|
145
|
+
self.__sk = DynamoDBKey()
|
|
146
|
+
return self.__sk
|
|
147
|
+
|
|
148
|
+
@sort_key.setter
|
|
149
|
+
def sort_key(self, value: DynamoDBKey | None):
|
|
150
|
+
self.__sk = value
|
|
151
|
+
|
|
152
|
+
def to_dict(self, include_sort_key: bool = True) -> dict[str, str]:
|
|
153
|
+
"""
|
|
154
|
+
Return a dictionary representation of this index's keys for debugging.
|
|
155
|
+
|
|
156
|
+
This is particularly useful for:
|
|
157
|
+
- Debugging key generation logic
|
|
158
|
+
- Logging DynamoDB operations
|
|
159
|
+
- Verifying composite key structure
|
|
160
|
+
- Testing key values
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
include_sort_key: Whether to include the sort key (default: True)
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Dictionary with partition key and optionally sort key.
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
>>> index = DynamoDBIndex()
|
|
170
|
+
>>> index.partition_key.attribute_name = "pk"
|
|
171
|
+
>>> index.partition_key.value = lambda: "user#123"
|
|
172
|
+
>>> index.sort_key.attribute_name = "sk"
|
|
173
|
+
>>> index.sort_key.value = lambda: "user#123"
|
|
174
|
+
>>> index.to_dict()
|
|
175
|
+
{'pk': 'user#123', 'sk': 'user#123'}
|
|
176
|
+
|
|
177
|
+
>>> # Partition key only
|
|
178
|
+
>>> index.to_dict(include_sort_key=False)
|
|
179
|
+
{'pk': 'user#123'}
|
|
180
|
+
|
|
181
|
+
>>> # Useful for debugging
|
|
182
|
+
>>> print(f"Querying with key: {index.to_dict()}")
|
|
183
|
+
Querying with key: {'pk': 'user#123', 'sk': 'user#123'}
|
|
184
|
+
"""
|
|
185
|
+
result = {}
|
|
186
|
+
|
|
187
|
+
# Always include partition key
|
|
188
|
+
if self.__pk:
|
|
189
|
+
result[self.partition_key.attribute_name] = self.partition_key.value
|
|
190
|
+
|
|
191
|
+
# Optionally include sort key
|
|
192
|
+
if include_sort_key and self.__sk and self.sort_key.attribute_name:
|
|
193
|
+
try:
|
|
194
|
+
result[self.sort_key.attribute_name] = self.sort_key.value
|
|
195
|
+
except ValueError:
|
|
196
|
+
# Sort key value not set, skip it
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
return result
|
|
200
|
+
|
|
201
|
+
def debug_info(
|
|
202
|
+
self,
|
|
203
|
+
*,
|
|
204
|
+
include_sort_key: bool = True,
|
|
205
|
+
condition: str = "begins_with",
|
|
206
|
+
low_value: Any = None,
|
|
207
|
+
high_value: Any = None,
|
|
208
|
+
) -> dict[str, Any]:
|
|
209
|
+
"""
|
|
210
|
+
Return detailed debugging information about this index and how it would be queried.
|
|
211
|
+
|
|
212
|
+
This is useful for understanding:
|
|
213
|
+
- What keys are defined
|
|
214
|
+
- What condition would be used in a query
|
|
215
|
+
- What the actual key values are
|
|
216
|
+
- What index name would be used
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
include_sort_key: Whether to include the sort key (default: True)
|
|
220
|
+
condition: The condition type being used (default: "begins_with")
|
|
221
|
+
low_value: Low value for "between" condition
|
|
222
|
+
high_value: High value for "between" condition
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Dictionary with debugging information including keys, condition, and index details.
|
|
226
|
+
|
|
227
|
+
Example:
|
|
228
|
+
>>> index = product.indexes.get("gsi1")
|
|
229
|
+
>>> debug = index.debug_info(condition="begins_with")
|
|
230
|
+
>>> print(debug)
|
|
231
|
+
{
|
|
232
|
+
'index_name': 'gsi1',
|
|
233
|
+
'partition_key': {
|
|
234
|
+
'attribute': 'gsi1_pk',
|
|
235
|
+
'value': 'category#electronics'
|
|
236
|
+
},
|
|
237
|
+
'sort_key': {
|
|
238
|
+
'attribute': 'gsi1_sk',
|
|
239
|
+
'value': 'product#prod_123',
|
|
240
|
+
'condition': 'begins_with'
|
|
241
|
+
},
|
|
242
|
+
'keys_dict': {'gsi1_pk': 'category#electronics', 'gsi1_sk': 'product#prod_123'},
|
|
243
|
+
'query_type': 'GSI' or 'Primary'
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
>>> # Check condition type
|
|
247
|
+
>>> if debug['sort_key']['condition'] == 'begins_with':
|
|
248
|
+
... print("This query uses begins_with")
|
|
249
|
+
"""
|
|
250
|
+
result = {
|
|
251
|
+
'index_name': self.name,
|
|
252
|
+
'query_type': 'Primary' if self.name == DynamoDBIndexes.PRIMARY_INDEX else 'GSI/LSI'
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# Partition key info
|
|
256
|
+
if self.__pk:
|
|
257
|
+
result['partition_key'] = {
|
|
258
|
+
'attribute': self.partition_key.attribute_name,
|
|
259
|
+
'value': self.partition_key.value
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
# Sort key info with condition
|
|
263
|
+
if include_sort_key and self.__sk and self.sort_key.attribute_name:
|
|
264
|
+
try:
|
|
265
|
+
sk_info = {
|
|
266
|
+
'attribute': self.sort_key.attribute_name,
|
|
267
|
+
'value': self.sort_key.value,
|
|
268
|
+
'condition': condition
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# Add range info for between condition
|
|
272
|
+
if condition == "between" and low_value is not None and high_value is not None:
|
|
273
|
+
sk_info['low_value'] = low_value
|
|
274
|
+
sk_info['high_value'] = high_value
|
|
275
|
+
sk_info['full_range'] = {
|
|
276
|
+
'low': f"{self.sort_key.value}{low_value}",
|
|
277
|
+
'high': f"{self.sort_key.value}{high_value}"
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
result['sort_key'] = sk_info
|
|
281
|
+
except ValueError:
|
|
282
|
+
# Sort key value not set
|
|
283
|
+
result['sort_key'] = {
|
|
284
|
+
'attribute': self.sort_key.attribute_name,
|
|
285
|
+
'value': None,
|
|
286
|
+
'condition': condition,
|
|
287
|
+
'note': 'Sort key value not set'
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# Include the keys dictionary for convenience
|
|
291
|
+
result['keys_dict'] = self.to_dict(include_sort_key=include_sort_key)
|
|
292
|
+
|
|
293
|
+
return result
|
|
294
|
+
|
|
295
|
+
def key(
|
|
296
|
+
self,
|
|
297
|
+
*,
|
|
298
|
+
include_sort_key: bool = True,
|
|
299
|
+
condition: str = "begins_with",
|
|
300
|
+
low_value: Any = None,
|
|
301
|
+
high_value: Any = None,
|
|
302
|
+
query_key: bool = False,
|
|
303
|
+
# sk_value_2: Optional[str | int | float] = None,
|
|
304
|
+
) -> dict | Key | ConditionBase | ComparisonCondition | Equals:
|
|
305
|
+
"""Get the key for a given index"""
|
|
306
|
+
key: dict | Key | ConditionBase | ComparisonCondition | Equals
|
|
307
|
+
|
|
308
|
+
if query_key:
|
|
309
|
+
key = self._build_query_key(
|
|
310
|
+
include_sort_key=include_sort_key,
|
|
311
|
+
condition=condition,
|
|
312
|
+
low_value=low_value,
|
|
313
|
+
high_value=high_value,
|
|
314
|
+
)
|
|
315
|
+
return key
|
|
316
|
+
|
|
317
|
+
elif self.name == DynamoDBIndexes.PRIMARY_INDEX and include_sort_key:
|
|
318
|
+
# this is a direct primary key which is used in a get call
|
|
319
|
+
# this is different than query keys
|
|
320
|
+
key = {}
|
|
321
|
+
key[self.partition_key.attribute_name] = self.partition_key.value
|
|
322
|
+
|
|
323
|
+
if self.sort_key and self.sort_key.attribute_name:
|
|
324
|
+
key[self.sort_key.attribute_name] = self.sort_key.value
|
|
325
|
+
|
|
326
|
+
return key
|
|
327
|
+
|
|
328
|
+
# catch all (TODO: decide if this is the best pattern or should we raise an error)
|
|
329
|
+
key = self._build_query_key(
|
|
330
|
+
include_sort_key=include_sort_key,
|
|
331
|
+
condition=condition,
|
|
332
|
+
low_value=low_value,
|
|
333
|
+
high_value=high_value,
|
|
334
|
+
)
|
|
335
|
+
return key
|
|
336
|
+
|
|
337
|
+
def _build_query_key(
|
|
338
|
+
self,
|
|
339
|
+
*,
|
|
340
|
+
include_sort_key: bool = True,
|
|
341
|
+
condition: str = "begins_with",
|
|
342
|
+
low_value: Any = None,
|
|
343
|
+
high_value: Any = None,
|
|
344
|
+
) -> And | Equals:
|
|
345
|
+
"""Get the GSI index name and key"""
|
|
346
|
+
|
|
347
|
+
key: And | Equals = Key(f"{self.partition_key.attribute_name}").eq(
|
|
348
|
+
self.partition_key.value
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if (
|
|
352
|
+
include_sort_key
|
|
353
|
+
and self.sort_key.attribute_name
|
|
354
|
+
and (
|
|
355
|
+
self.sort_key.value
|
|
356
|
+
or (low_value is not None and high_value is not None)
|
|
357
|
+
)
|
|
358
|
+
):
|
|
359
|
+
# if self.sk_value_2:
|
|
360
|
+
if low_value is not None and high_value is not None:
|
|
361
|
+
match condition:
|
|
362
|
+
case "between":
|
|
363
|
+
low = f"{self.sort_key.value}{low_value}"
|
|
364
|
+
high = f"{self.sort_key.value}{high_value}"
|
|
365
|
+
key = key & Key(f"{self.sort_key.attribute_name}").between(
|
|
366
|
+
low, high
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
else:
|
|
370
|
+
match condition:
|
|
371
|
+
case "begins_with":
|
|
372
|
+
key = key & Key(f"{self.sort_key.attribute_name}").begins_with(
|
|
373
|
+
self.sort_key.value
|
|
374
|
+
)
|
|
375
|
+
case "eq":
|
|
376
|
+
key = key & Key(f"{self.sort_key.attribute_name}").eq(
|
|
377
|
+
self.sort_key.value
|
|
378
|
+
)
|
|
379
|
+
case "gt":
|
|
380
|
+
key = key & Key(f"{self.sort_key.attribute_name}").gt(
|
|
381
|
+
self.sort_key.value
|
|
382
|
+
)
|
|
383
|
+
case "gte":
|
|
384
|
+
key = key & Key(f"{self.sort_key.attribute_name}").gte(
|
|
385
|
+
self.sort_key.value
|
|
386
|
+
)
|
|
387
|
+
case "lt":
|
|
388
|
+
key = key & Key(f"{self.sort_key.attribute_name}").lt(
|
|
389
|
+
self.sort_key.value
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
return key
|
|
393
|
+
|
|
394
|
+
@staticmethod
|
|
395
|
+
def extract_key_values(
|
|
396
|
+
key_expression: And | Equals,
|
|
397
|
+
index: Optional[str | DynamoDBIndex] = None
|
|
398
|
+
) -> dict[str, Any]:
|
|
399
|
+
"""
|
|
400
|
+
Extract key values and condition information from a boto3 Key condition expression.
|
|
401
|
+
|
|
402
|
+
This is useful for debugging queries at runtime to see exactly what values
|
|
403
|
+
are being used in the KeyConditionExpression.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
key_expression: The Key condition expression (from key() or _build_query_key())
|
|
407
|
+
index: Optional index name (str) or DynamoDBIndex object to include in results
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Dictionary containing:
|
|
411
|
+
- index_name: str (if index parameter provided)
|
|
412
|
+
- partition_key: {'attribute': str, 'value': str}
|
|
413
|
+
- sort_key: {'attribute': str, 'value': str, 'operator': str, 'format': str} (if present)
|
|
414
|
+
|
|
415
|
+
Example:
|
|
416
|
+
>>> index = model.indexes.get("gsi1")
|
|
417
|
+
>>> key_expr = index.key(query_key=True, condition="begins_with")
|
|
418
|
+
>>> debug = DynamoDBIndex.extract_key_values(key_expr, index)
|
|
419
|
+
>>> print(debug)
|
|
420
|
+
{
|
|
421
|
+
'index_name': 'gsi1',
|
|
422
|
+
'partition_key': {
|
|
423
|
+
'attribute': 'gsi1_pk',
|
|
424
|
+
'value': 'inbox#support#status#open'
|
|
425
|
+
},
|
|
426
|
+
'sort_key': {
|
|
427
|
+
'attribute': 'gsi1_sk',
|
|
428
|
+
'value': 'priority#medium#ts#',
|
|
429
|
+
'operator': 'begins_with',
|
|
430
|
+
'format': '{operator}({0}, {1})'
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
>>> # Or pass just the index name
|
|
435
|
+
>>> debug = DynamoDBIndex.extract_key_values(key_expr, "gsi1")
|
|
436
|
+
|
|
437
|
+
>>> # Quick access to values
|
|
438
|
+
>>> pk_value = debug['partition_key']['value']
|
|
439
|
+
>>> sk_value = debug['sort_key']['value']
|
|
440
|
+
>>> condition = debug['sort_key']['operator']
|
|
441
|
+
>>> index_name = debug.get('index_name')
|
|
442
|
+
"""
|
|
443
|
+
result = {}
|
|
444
|
+
|
|
445
|
+
# Include index name if provided
|
|
446
|
+
if index is not None:
|
|
447
|
+
if isinstance(index, str):
|
|
448
|
+
result['index_name'] = index
|
|
449
|
+
elif isinstance(index, DynamoDBIndex):
|
|
450
|
+
result['index_name'] = index.name
|
|
451
|
+
|
|
452
|
+
try:
|
|
453
|
+
# The key_expression._values is a list of conditions
|
|
454
|
+
# [0] is the partition key (Equals condition)
|
|
455
|
+
# [1] is the sort key (ComparisonCondition) if present
|
|
456
|
+
|
|
457
|
+
if hasattr(key_expression, '_values') and len(key_expression._values) > 0:
|
|
458
|
+
# Extract partition key
|
|
459
|
+
pk_condition = key_expression._values[0]
|
|
460
|
+
if hasattr(pk_condition, '_values') and len(pk_condition._values) >= 2:
|
|
461
|
+
pk_attr = pk_condition._values[0]
|
|
462
|
+
result['partition_key'] = {
|
|
463
|
+
'attribute': pk_attr.name if hasattr(pk_attr, 'name') else str(pk_attr),
|
|
464
|
+
'value': pk_condition._values[1]
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
# Extract sort key if present
|
|
468
|
+
if len(key_expression._values) > 1:
|
|
469
|
+
sk_condition = key_expression._values[1]
|
|
470
|
+
if hasattr(sk_condition, '_values'):
|
|
471
|
+
sk_attr = sk_condition._values[0] if len(sk_condition._values) > 0 else None
|
|
472
|
+
sk_info = {
|
|
473
|
+
'attribute': sk_attr.name if (sk_attr and hasattr(sk_attr, 'name')) else str(sk_attr),
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
# Get value(s)
|
|
477
|
+
if len(sk_condition._values) > 1:
|
|
478
|
+
sk_info['value'] = sk_condition._values[1]
|
|
479
|
+
|
|
480
|
+
# For 'between' condition, there are two values
|
|
481
|
+
if len(sk_condition._values) > 2:
|
|
482
|
+
sk_info['value_low'] = sk_condition._values[1]
|
|
483
|
+
sk_info['value_high'] = sk_condition._values[2]
|
|
484
|
+
del sk_info['value'] # Remove single value key
|
|
485
|
+
|
|
486
|
+
# Get operator and format
|
|
487
|
+
if hasattr(sk_condition, 'expression_operator'):
|
|
488
|
+
sk_info['operator'] = sk_condition.expression_operator
|
|
489
|
+
if hasattr(sk_condition, 'expression_format'):
|
|
490
|
+
sk_info['format'] = sk_condition.expression_format
|
|
491
|
+
|
|
492
|
+
result['sort_key'] = sk_info
|
|
493
|
+
|
|
494
|
+
# If no _values found, handle single Equals condition (no sort key)
|
|
495
|
+
elif isinstance(key_expression, Equals):
|
|
496
|
+
if hasattr(key_expression, '_values') and len(key_expression._values) >= 2:
|
|
497
|
+
pk_attr = key_expression._values[0]
|
|
498
|
+
result['partition_key'] = {
|
|
499
|
+
'attribute': pk_attr.name if hasattr(pk_attr, 'name') else str(pk_attr),
|
|
500
|
+
'value': key_expression._values[1]
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
except (AttributeError, IndexError) as e:
|
|
504
|
+
result['error'] = f"Unable to extract key values: {str(e)}"
|
|
505
|
+
result['note'] = "The Key expression structure may have changed"
|
|
506
|
+
|
|
507
|
+
return result
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
from boto3_assist.dynamodb.dynamodb import DynamoDB
|
|
4
|
+
from boto3_assist.dynamodb.dynamodb_model_base import DynamoDBModelBase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class IDynamoDBService(ABC):
|
|
8
|
+
"""DynamoDB Service Interface"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, db: DynamoDB) -> None:
|
|
11
|
+
self.db = db
|
|
12
|
+
super().__init__()
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def db(self) -> DynamoDB:
|
|
17
|
+
"""Property that returns a DynamoDB service resource."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
@db.setter
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def db(self, val: DynamoDB) -> None:
|
|
23
|
+
"""Property setter for the DynamoDB service resource."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def save(self, model: DynamoDBModelBase) -> Any:
|
|
28
|
+
"""Save a DynamoDBModelBase instance to the database."""
|
|
29
|
+
pass
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
Maintainers: Eric Wilson
|
|
4
|
+
MIT License. See Project Root for the license information.
|
|
5
|
+
https://github.com/geekcafe/boto3-assist
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from typing import Callable, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DynamoDBKey:
|
|
13
|
+
"""DynamoDB Key"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
attribute_name: Optional[str] = None,
|
|
18
|
+
value: Optional[str | Callable[[], str]] = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
self.__attribute_name: Optional[str] = attribute_name
|
|
21
|
+
self.__value: Optional[str | Callable[[], str]] = value
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def attribute_name(self) -> str:
|
|
25
|
+
"""Get the name"""
|
|
26
|
+
if self.__attribute_name is None:
|
|
27
|
+
raise ValueError("The Attribute Name is not set")
|
|
28
|
+
return self.__attribute_name
|
|
29
|
+
|
|
30
|
+
@attribute_name.setter
|
|
31
|
+
def attribute_name(self, value: str):
|
|
32
|
+
self.__attribute_name = value
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def value(self) -> Optional[str | Callable[[], str]]:
|
|
36
|
+
"""Get the value"""
|
|
37
|
+
|
|
38
|
+
if self.__value is None:
|
|
39
|
+
raise ValueError("Value is not set")
|
|
40
|
+
if callable(self.__value):
|
|
41
|
+
return self.__value()
|
|
42
|
+
return self.__value
|
|
43
|
+
|
|
44
|
+
@value.setter
|
|
45
|
+
def value(self, value: Optional[str | Callable[[], str]]):
|
|
46
|
+
self.__value = value
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> dict[str, str]:
|
|
49
|
+
"""
|
|
50
|
+
Return a dictionary representation of this key for debugging.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Dictionary with attribute name as key and value as the value.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> key = DynamoDBKey(attribute_name="pk", value="user#123")
|
|
57
|
+
>>> key.to_dict()
|
|
58
|
+
{'pk': 'user#123'}
|
|
59
|
+
|
|
60
|
+
>>> # With lambda
|
|
61
|
+
>>> key = DynamoDBKey(attribute_name="pk", value=lambda: "user#456")
|
|
62
|
+
>>> key.to_dict()
|
|
63
|
+
{'pk': 'user#456'}
|
|
64
|
+
"""
|
|
65
|
+
return {self.attribute_name: self.value}
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def build_key(*key_value_pairs) -> str:
|
|
69
|
+
"""
|
|
70
|
+
Static method to build a key based on provided key-value pairs.
|
|
71
|
+
- Stops appending if any value is None.
|
|
72
|
+
- However a value of "" (empty string) will continue the chain.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
76
|
+
("user",self.model.user_id)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
pk: user#<user-id>
|
|
80
|
+
pk: user#123456789
|
|
81
|
+
|
|
82
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
83
|
+
("xref", self.model.xref_type),
|
|
84
|
+
("id", self.model.xref_pk),
|
|
85
|
+
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
sk: xref#<xref-type>#id#<some-id>
|
|
89
|
+
sk: xref#task#id#123456789
|
|
90
|
+
|
|
91
|
+
# example two has a leading "domain" (crm)
|
|
92
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
93
|
+
("crm", "")
|
|
94
|
+
("xref", self.model.xref_type),
|
|
95
|
+
("id", self.model.xref_pk),
|
|
96
|
+
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
sk: crm#xref#<xref-type>#id#<some-id>
|
|
100
|
+
sk: crm#xref#task#id#123456789
|
|
101
|
+
|
|
102
|
+
# using None stops the key build
|
|
103
|
+
# useful when doing begins with
|
|
104
|
+
|
|
105
|
+
# assume self.model.xref_pk is None
|
|
106
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
107
|
+
("xref", self.model.xref_type),
|
|
108
|
+
("id", self.model.xref_pk),
|
|
109
|
+
|
|
110
|
+
)
|
|
111
|
+
# results with a key of
|
|
112
|
+
# which would get all of the users tasks assuming the pk was still
|
|
113
|
+
# the same
|
|
114
|
+
sk: xref#<xref-type>#id#
|
|
115
|
+
sk: xref#task#id#
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
parts = []
|
|
119
|
+
for key, value in key_value_pairs:
|
|
120
|
+
prefix = f"{key}#" if key else ""
|
|
121
|
+
if value is None:
|
|
122
|
+
parts.append(f"{prefix}")
|
|
123
|
+
break
|
|
124
|
+
elif len(str(value).strip()) == 0:
|
|
125
|
+
parts.append(f"{key}")
|
|
126
|
+
else:
|
|
127
|
+
parts.append(f"{prefix}{value}")
|
|
128
|
+
key_str = "#".join(parts)
|
|
129
|
+
|
|
130
|
+
return key_str
|