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