boto3-assist 0.18.0__py3-none-any.whl → 0.20.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.
@@ -15,11 +15,11 @@ from boto3.dynamodb.conditions import (
15
15
  ComparisonCondition,
16
16
  ConditionBase,
17
17
  )
18
- from boto3_assist.dynamodb.dynamodb_connection import DynamoDBConnection
19
- from boto3_assist.dynamodb.dynamodb_helpers import DynamoDBHelpers
20
- from boto3_assist.dynamodb.dynamodb_model_base import DynamoDBModelBase
21
- from boto3_assist.utilities.string_utility import StringUtility
22
-
18
+ from .dynamodb_connection import DynamoDBConnection
19
+ from .dynamodb_helpers import DynamoDBHelpers
20
+ from .dynamodb_model_base import DynamoDBModelBase
21
+ from ..utilities.string_utility import StringUtility
22
+ from .dynamodb_index import DynamoDBIndex
23
23
 
24
24
  logger = Logger()
25
25
 
@@ -69,9 +69,9 @@ class DynamoDB(DynamoDBConnection):
69
69
  """
70
70
  Save an item to the database
71
71
  Args:
72
- item (dict): DynamoDB Dictionay Object or DynamoDBModelBase. Supports the "client" or
72
+ item (dict): DynamoDB Dictionary Object or DynamoDBModelBase. Supports the "client" or
73
73
  "resource" syntax
74
- table_name (str): The DyamoDb Table Name
74
+ table_name (str): The DynamoDb Table Name
75
75
  source (str, optional): The source of the call, used for logging. Defaults to None.
76
76
 
77
77
  Raises:
@@ -98,7 +98,7 @@ class DynamoDB(DynamoDBConnection):
98
98
  except Exception as e: # pylint: disable=w0718
99
99
  logger.exception(e)
100
100
  raise RuntimeError(
101
- "An error occured during model converation. The entry was not saved. "
101
+ "An error occurred during model conversion. The entry was not saved. "
102
102
  ) from e
103
103
 
104
104
  if isinstance(item, dict):
@@ -279,11 +279,11 @@ class DynamoDB(DynamoDBConnection):
279
279
 
280
280
  def query(
281
281
  self,
282
- key: dict | Key | ConditionBase | ComparisonCondition,
282
+ key: dict | Key | ConditionBase | ComparisonCondition | DynamoDBIndex,
283
+ table_name: str,
283
284
  *,
284
285
  index_name: Optional[str] = None,
285
286
  ascending: bool = False,
286
- table_name: Optional[str] = None,
287
287
  source: Optional[str] = None,
288
288
  strongly_consistent: bool = False,
289
289
  projection_expression: Optional[str] = None,
@@ -305,6 +305,17 @@ class DynamoDB(DynamoDBConnection):
305
305
  """
306
306
 
307
307
  logger.debug({"action": "query", "source": source})
308
+ if not key:
309
+ raise ValueError("Query failed: key must be provided.")
310
+
311
+ if not table_name:
312
+ raise ValueError("Query failed: table_name must be provided.")
313
+
314
+ if isinstance(key, DynamoDBIndex):
315
+ if not index_name:
316
+ index_name = key.name
317
+ # turn it into a key expected by dynamodb
318
+ key = key.key()
308
319
 
309
320
  kwargs: dict = {}
310
321
  if index_name:
@@ -330,7 +341,16 @@ class DynamoDB(DynamoDBConnection):
330
341
  raise ValueError("Query failed: table_name must be provided.")
331
342
 
332
343
  table = self.dynamodb_resource.Table(table_name)
333
- response = dict(table.query(**kwargs))
344
+ response: dict = {}
345
+ try:
346
+ response = dict(table.query(**kwargs))
347
+ except Exception as e: # pylint: disable=w0718
348
+ logger.exception(
349
+ {"source": f"{source}", "metric_filter": "query", "error": str(e)}
350
+ )
351
+ response = {"exception": str(e)}
352
+ if self.raise_on_error:
353
+ raise e
334
354
 
335
355
  return response
336
356
 
@@ -40,7 +40,7 @@ def exclude_indexes_from_serialization(method):
40
40
 
41
41
 
42
42
  class DynamoDBModelBase(SerializableModel):
43
- """DyanmoDb Model Base"""
43
+ """DynamoDb Model Base"""
44
44
 
45
45
  T = TypeVar("T", bound="DynamoDBModelBase")
46
46
 
@@ -53,6 +53,9 @@ class DynamoDBModelBase(SerializableModel):
53
53
  self.__auto_generate_projections: bool = auto_generate_projections
54
54
  self.__actively_serializing_data__: bool = False
55
55
 
56
+ def serialization_in_progress(self) -> bool:
57
+ return self.__actively_serializing_data__
58
+
56
59
  @property
57
60
  @exclude_from_serialization
58
61
  def indexes(self) -> DynamoDBIndexes:
@@ -0,0 +1,165 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ Maintainers: Eric Wilson
4
+ MIT License. See Project Root for the license information.
5
+ """
6
+
7
+ import json
8
+ from typing import Any, Dict, Optional, List, Type
9
+ from aws_lambda_powertools import Logger
10
+ from boto3_assist.dynamodb.dynamodb import DynamoDB
11
+ from boto3_assist.dynamodb.dynamodb_model_base import DynamoDBModelBase
12
+ from boto3_assist.utilities.serialization_utility import Serialization
13
+ from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex
14
+ from boto3_assist.dynamodb.dynamodb_iservice import IDynamoDBService
15
+
16
+ logger = Logger()
17
+
18
+
19
+ class DynamoDBReIndexer:
20
+ """ReIndexing your database"""
21
+
22
+ def __init__(
23
+ self,
24
+ table_name: str,
25
+ *,
26
+ db: Optional[DynamoDB] = None,
27
+ aws_profile: Optional[str] = None,
28
+ aws_region: Optional[str] = None,
29
+ aws_end_point_url: Optional[str] = None,
30
+ aws_access_key_id: Optional[str] = None,
31
+ aws_secret_access_key: Optional[str] = None,
32
+ ):
33
+ self.table_name = table_name
34
+ self.db: DynamoDB = db or DynamoDB(
35
+ aws_profile=aws_profile,
36
+ aws_region=aws_region,
37
+ aws_end_point_url=aws_end_point_url,
38
+ aws_access_key_id=aws_access_key_id,
39
+ aws_secret_access_key=aws_secret_access_key,
40
+ )
41
+
42
+ def reindex_item(
43
+ self,
44
+ original_primary_key: dict,
45
+ model: DynamoDBModelBase,
46
+ *,
47
+ dry_run: bool = False,
48
+ inplace: bool = True,
49
+ leave_original_record: bool = False,
50
+ service_cls: Type[IDynamoDBService] | None,
51
+ ):
52
+ """
53
+ Reindex the record
54
+
55
+ Args:
56
+ original_primary_key (dict): The original primary key of the record to be reindexed.
57
+ This is either the partition_key or a composite key (partition_key, sort_key)
58
+ model (DynamoDBModelBase): A model instance that will be used to serialize the new keys
59
+ into a dictionary. It must inherit from DynamoDBModelBase
60
+
61
+ dry_run (bool, optional): Ability to log the actions without executing them. Defaults to False.
62
+ inplace (bool, optional): Ability to just update the indexes only.
63
+ No other fields will be updated, however you can't update the primary_key (partition/sort key)
64
+ with this action since they are immutable.
65
+ Defaults to True.
66
+ leave_original_record (bool, optional): _description_. Defaults to False.
67
+ """
68
+
69
+ if inplace:
70
+ keys: List[DynamoDBIndex] = model.list_keys()
71
+ # Update the item in DynamoDB with new keys
72
+ self.update_item_in_dynamodb(
73
+ original_primary_key=original_primary_key, keys=keys, dry_run=dry_run
74
+ )
75
+ # todo: add some additional error handling here and throw a more
76
+ # descriptive error if they try to use a different primary
77
+ # pk or sk, which you can't do. If that's the case
78
+ else:
79
+ # add the new one first and optionally delete the older one
80
+ # once we are succesfull
81
+ try:
82
+ # save the new one first
83
+ service_instance: Optional[IDynamoDBService] = (
84
+ service_cls(db=self.db) if callable(service_cls) else None
85
+ )
86
+
87
+ if service_instance:
88
+ service_instance.save(model=model)
89
+ else:
90
+ self.db.save(
91
+ item=model, table_name=self.table_name, source="reindex"
92
+ )
93
+
94
+ # then delete the old on
95
+ if not leave_original_record:
96
+ self.db.delete(
97
+ table_name=self.table_name, primary_key=original_primary_key
98
+ )
99
+ except Exception as e: # pylint: disable=broad-except
100
+ logger.error(str(e))
101
+ raise RuntimeError(str(e)) from e
102
+ # this gets a little more trick as we need to delete the item
103
+
104
+ def load_model(
105
+ self, db_item: dict, db_model: DynamoDBModelBase
106
+ ) -> DynamoDBModelBase | None:
107
+ """load the model which will serialze the dynamodb dictionary to an instance of an object"""
108
+
109
+ base_model = Serialization.map(db_item, db_model)
110
+ return base_model
111
+
112
+ def update_item_in_dynamodb(
113
+ self,
114
+ original_primary_key: dict,
115
+ keys: List[DynamoDBIndex],
116
+ dry_run: bool = False,
117
+ ):
118
+ """Update the dynamodb item"""
119
+ dictionary = self.db.helpers.keys_to_dictionary(keys=keys)
120
+
121
+ update_expression = self.build_update_expression(dictionary)
122
+ expression_attribute_values = self.build_expression_attribute_values(dictionary)
123
+
124
+ if not dry_run:
125
+ self.db.update_item(
126
+ table_name=self.table_name,
127
+ key=original_primary_key,
128
+ update_expression=update_expression,
129
+ expression_attribute_values=expression_attribute_values,
130
+ )
131
+ else:
132
+ print("Dry run: Skipping Update item")
133
+ print(f"{json.dumps(original_primary_key, indent=4)}")
134
+ print(f"{update_expression}")
135
+ print(f"{json.dumps(expression_attribute_values, indent=4)}")
136
+
137
+ def build_update_expression(self, updated_keys: Dict[str, Any]) -> str:
138
+ """
139
+ Build the expression for updating the item
140
+
141
+ Args:
142
+ updated_keys (Dict[str, Any]): _description_
143
+
144
+ Returns:
145
+ str: _description_
146
+ """
147
+ update_expression = "SET " + ", ".join(
148
+ f"{k} = :{k}" for k in updated_keys.keys()
149
+ )
150
+ return update_expression
151
+
152
+ def build_expression_attribute_values(
153
+ self, updated_keys: Dict[str, Any]
154
+ ) -> Dict[str, Any]:
155
+ """
156
+ Build the expression attribute values for the update expression
157
+
158
+ Args:
159
+ updated_keys (Dict[str, Any]): _description_
160
+
161
+ Returns:
162
+ Dict[str, Any]: _description_
163
+ """
164
+ expression_attribute_values = {f":{k}": v for k, v in updated_keys.items()}
165
+ return expression_attribute_values
@@ -470,7 +470,7 @@ class Serialization:
470
470
  "To work around this create a boolean (bool) property named __actively_serializing_data__. \n"
471
471
  "e.g. self.__actively_serializing_data__: bool = False\n\n"
472
472
  "Only issue/raise your exception if __actively_serializing_data__ is not True. \n\n"
473
- "e.g. if not self.some_propert and not self.__actively_serializing_data__:\n"
473
+ "e.g. if not self.some_property and not self.__actively_serializing_data__:\n"
474
474
  ' raise ValueError("some_property must be set")\n\n'
475
475
  "This procedure will update the property from False to True while serializing, "
476
476
  "then back to False once serialization is complete. "
boto3_assist/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.18.0'
1
+ __version__ = '0.20.0'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boto3_assist
3
- Version: 0.18.0
3
+ Version: 0.20.0
4
4
  Summary: Additional boto3 wrappers to make your life a little easier
5
5
  Author-email: Eric Wilson <boto3-assist@geekcafe.com>
6
6
  License-File: LICENSE-EXPLAINED.txt
@@ -6,7 +6,7 @@ boto3_assist/connection_tracker.py,sha256=UgfR9RlvXf3A4ssMr3gDMpw89ka8mSRvJn4M34
6
6
  boto3_assist/http_status_codes.py,sha256=G0zRSWenwavYKETvDF9tNVUXQz3Ae2gXdBETYbjvJe8,3284
7
7
  boto3_assist/role_assumption_mixin.py,sha256=PMUU5yC2FUBjFD1UokVkRY3CPB5zTw85AhIB5BMtbc8,1031
8
8
  boto3_assist/session_setup_mixin.py,sha256=X-JQKyyaWNA8Z8kKgf2V2I5vsiLAH8udLTX_xepnsdQ,3140
9
- boto3_assist/version.py,sha256=xLQqGjpDoJw8wh7LhnSmkR_X41AAFaOxbyFZXuEbtfY,23
9
+ boto3_assist/version.py,sha256=pJLteH9r0aOnAwnvPatmr2QSz7lO2BDXON0AKkXx0Pg,23
10
10
  boto3_assist/aws_lambda/event_info.py,sha256=OkZ4WzuGaHEu_T8sB188KBgShAJhZpWASALKRGBOhMg,14648
11
11
  boto3_assist/aws_lambda/mock_context.py,sha256=LPjHP-3YSoY6iPl1kPqJDwSVf1zLNTcukUunDtYcbK0,116
12
12
  boto3_assist/cloudwatch/cloudwatch_connection.py,sha256=mnGWaLSQpHh5EeY7Ek_2o9JKHJxOELIYtQVMX1IaHn4,2480
@@ -19,15 +19,16 @@ boto3_assist/cognito/cognito_connection.py,sha256=deuXR3cNHz0mCYff2k0LfAvK--9Okq
19
19
  boto3_assist/cognito/cognito_utility.py,sha256=IVZAg58nHG1U7uxe7FsTYpqwwZiwwdIBGiVTZuLCFqg,18417
20
20
  boto3_assist/cognito/jwks_cache.py,sha256=1Y9r-YfQ8qrgZN5xYPvjUEEV0vthbdcPdAIaPbZP7kU,373
21
21
  boto3_assist/cognito/user.py,sha256=qc44qLx3gwq6q2zMxcPQze1EjeZwy5Kuav93vbe-4WU,820
22
- boto3_assist/dynamodb/dynamodb.py,sha256=Xkt-dRFVqvlUkDf8P_h9VX91qfY9kLxNgpV8e-QUzbk,15992
22
+ boto3_assist/dynamodb/dynamodb.py,sha256=zq2Hqhmd3aXjnd1GAmYq5TkjMPiq_pHcMW6hClSv13Y,16679
23
23
  boto3_assist/dynamodb/dynamodb_connection.py,sha256=D4KmVpMpE0OuVOwW5g4JBWllUNkwy0hMXEGUiToAMBc,3608
24
24
  boto3_assist/dynamodb/dynamodb_helpers.py,sha256=RoRRqKjdwfC-2-gvlkLvCoNWhIoMrHm-68dkyhXI_Xk,12080
25
25
  boto3_assist/dynamodb/dynamodb_importer.py,sha256=nCKsyRQeMqDSf0Q5mQ_X_oVIg4PRnu0hcUzZnBli610,3471
26
26
  boto3_assist/dynamodb/dynamodb_index.py,sha256=D0Lq121qk1cXeMetPeqnzvv6CXd0XfEygfdUXaljLG8,8551
27
27
  boto3_assist/dynamodb/dynamodb_iservice.py,sha256=O9Aj0PFEvcuk2vhARifWTFnUwcQW5EXzwZS478Hm-N0,796
28
28
  boto3_assist/dynamodb/dynamodb_key.py,sha256=YD7o1EUlwVBQ55p9YCTKqAUU_np4nqtLIHnmp-BeolM,1803
29
- boto3_assist/dynamodb/dynamodb_model_base.py,sha256=OoSZ841YLHWTV_yx8MvVc6oHZEnvdqTYPhH9LTZ4M_M,11931
29
+ boto3_assist/dynamodb/dynamodb_model_base.py,sha256=ceVKmequ2S7zY-8IkQwmtNvGV8GtaEraFBy1c2Y5P4A,12031
30
30
  boto3_assist/dynamodb/dynamodb_model_base_interfaces.py,sha256=yT4zDRI8vP15WVOHnCvY3FsEy_QSIta5-bnUby70Xow,747
31
+ boto3_assist/dynamodb/dynamodb_re_indexer.py,sha256=Y0qRrvpmjS68w8ci6Au7Hg02W_ktqhUGGALeN-9XTls,6164
31
32
  boto3_assist/dynamodb/dynamodb_reindexer.py,sha256=bCj6KIU0fQOgjkkiq9yF51PFZZr4Y9Lu3-hPlmsPG0Y,6164
32
33
  boto3_assist/dynamodb/dynamodb_reserved_words.py,sha256=p0irNBSqGe4rd2FwWQqbRJWrNr4svdbWiyIXmz9lj4c,1937
33
34
  boto3_assist/dynamodb/dynamodb_reserved_words.txt,sha256=rvctS63Cv3i9SHmPq2Unmj6RZyQ-OMqxUXsNhtbg1is,4136
@@ -54,10 +55,10 @@ boto3_assist/utilities/file_operations.py,sha256=IYhJkh8wUPMvGnyDRRa9yOCDdHN9wR3
54
55
  boto3_assist/utilities/http_utility.py,sha256=_K39Fq0V4QcgklAWctUktuMjqXDTwgMld77IOUfR2zc,1282
55
56
  boto3_assist/utilities/logging_utility.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
57
  boto3_assist/utilities/numbers_utility.py,sha256=wzv9d0uXT_2_ZHHio7LBzibwxPqhGpvbq9HinrVn_4A,10160
57
- boto3_assist/utilities/serialization_utility.py,sha256=tYsFWvO1QTyhPgrYfC9etnMXKFYtHC78OAtvMGHuV9w,21646
58
+ boto3_assist/utilities/serialization_utility.py,sha256=Jc6H0cpcZjLO7tdyUZdBWHzItduLkw6sh2YQh8Hc8D8,21647
58
59
  boto3_assist/utilities/string_utility.py,sha256=XxUIz19L2LFFTRDAAmdPa8Qhn40u9yO7g4nULFuvg0M,11033
59
- boto3_assist-0.18.0.dist-info/METADATA,sha256=PwQUzbuw2bGPgZ8mw3GROwhjW3FnIy6FI4rESPYZRgc,2875
60
- boto3_assist-0.18.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
61
- boto3_assist-0.18.0.dist-info/licenses/LICENSE-EXPLAINED.txt,sha256=WFREvTpfTjPjDHpOLADxJpCKpIla3Ht87RUUGii4ODU,606
62
- boto3_assist-0.18.0.dist-info/licenses/LICENSE.txt,sha256=PXDhFWS5L5aOTkVhNvoitHKbAkgxqMI2uUPQyrnXGiI,1105
63
- boto3_assist-0.18.0.dist-info/RECORD,,
60
+ boto3_assist-0.20.0.dist-info/METADATA,sha256=fTDDcy1YzJ4vDZFqic4B1-veECbBopRgj9Yyj0FTeEI,2875
61
+ boto3_assist-0.20.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
62
+ boto3_assist-0.20.0.dist-info/licenses/LICENSE-EXPLAINED.txt,sha256=WFREvTpfTjPjDHpOLADxJpCKpIla3Ht87RUUGii4ODU,606
63
+ boto3_assist-0.20.0.dist-info/licenses/LICENSE.txt,sha256=PXDhFWS5L5aOTkVhNvoitHKbAkgxqMI2uUPQyrnXGiI,1105
64
+ boto3_assist-0.20.0.dist-info/RECORD,,