boto3-assist 0.20.0__py3-none-any.whl → 0.22.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.
@@ -6,6 +6,8 @@ MIT License. See Project Root for the license information.
6
6
 
7
7
  import os
8
8
  from typing import List, Optional, overload, Dict, Any
9
+ from botocore.exceptions import ClientError
10
+ from boto3.dynamodb.conditions import Attr
9
11
 
10
12
  from aws_lambda_powertools import Logger
11
13
  from boto3.dynamodb.conditions import (
@@ -65,17 +67,22 @@ class DynamoDB(DynamoDBConnection):
65
67
  item: dict | DynamoDBModelBase,
66
68
  table_name: str,
67
69
  source: Optional[str] = None,
70
+ fail_if_exists: bool = False,
68
71
  ) -> dict:
69
72
  """
70
73
  Save an item to the database
71
74
  Args:
72
- item (dict): DynamoDB Dictionary Object or DynamoDBModelBase. Supports the "client" or
73
- "resource" syntax
75
+ item (dict): DynamoDB Dictionary Object or DynamoDBModelBase.
76
+ Supports the "client" or "resource" syntax
74
77
  table_name (str): The DynamoDb Table Name
75
78
  source (str, optional): The source of the call, used for logging. Defaults to None.
79
+ fail_if_exists (bool, optional): Only allow it to insert once.
80
+ Fail if it already exits. This is useful for loggers, historical records,
81
+ tasks, etc. that should only be created once
76
82
 
77
83
  Raises:
78
- e: Any Error Raised
84
+ ClientError: Client specific errors
85
+ Exception: Any Error Raised
79
86
 
80
87
  Returns:
81
88
  dict: The Response from DynamoDB's put_item actions.
@@ -85,7 +92,7 @@ class DynamoDB(DynamoDBConnection):
85
92
 
86
93
  try:
87
94
  if not isinstance(item, dict):
88
- # attemp to convert it
95
+ # attempt to convert it
89
96
  if not isinstance(item, DynamoDBModelBase):
90
97
  raise RuntimeError(
91
98
  f"Item is not a dictionary or DynamoDBModelBase. Type: {type(item).__name__}. "
@@ -106,19 +113,49 @@ class DynamoDB(DynamoDBConnection):
106
113
 
107
114
  if isinstance(item, dict) and isinstance(next(iter(item.values())), dict):
108
115
  # Use boto3.client syntax
109
- response = dict(
110
- self.dynamodb_client.put_item(TableName=table_name, Item=item)
111
- )
116
+ # client API style
117
+ params = {
118
+ "TableName": table_name,
119
+ "Item": item,
120
+ }
121
+ if fail_if_exists:
122
+ # only insert if the item does *not* already exist
123
+ params["ConditionExpression"] = (
124
+ "attribute_not_exists(#pk) AND attribute_not_exists(#sk)"
125
+ )
126
+ params["ExpressionAttributeNames"] = {"#pk": "pk", "#sk": "sk"}
127
+ response = dict(self.dynamodb_client.put_item(**params))
128
+
112
129
  else:
113
130
  # Use boto3.resource syntax
114
131
  table = self.dynamodb_resource.Table(table_name)
115
- response = dict(table.put_item(Item=item)) # type: ignore[arg-type]
132
+ if fail_if_exists:
133
+ cond = Attr("pk").not_exists() & Attr("sk").not_exists()
134
+ response = dict(table.put_item(Item=item, ConditionExpression=cond))
135
+ else:
136
+ response = dict(table.put_item(Item=item))
137
+ # response = dict(table.put_item(Item=item)) # type: ignore[arg-type]
138
+
139
+ except ClientError as e:
140
+ if (
141
+ fail_if_exists
142
+ and e.response["Error"]["Code"] == "ConditionalCheckFailedException"
143
+ ):
144
+ raise RuntimeError(
145
+ f"Item with pk={item['pk']} already exists in {table_name} "
146
+ f"and fail_if_exists was set to {fail_if_exists}"
147
+ ) from e
148
+
149
+ logger.exception(
150
+ {"source": f"{source}", "metric_filter": "put_item", "error": str(e)}
151
+ )
152
+ raise
116
153
 
117
154
  except Exception as e: # pylint: disable=w0718
118
155
  logger.exception(
119
156
  {"source": f"{source}", "metric_filter": "put_item", "error": str(e)}
120
157
  )
121
- raise e
158
+ raise
122
159
 
123
160
  return response
124
161
 
@@ -49,7 +49,52 @@ class DynamoDBKey:
49
49
  def build_key(*key_value_pairs) -> str:
50
50
  """
51
51
  Static method to build a key based on provided key-value pairs.
52
- Stops appending if any value is None.
52
+ - Stops appending if any value is None.
53
+ - However a value of "" (empty string) will continue the chain.
54
+
55
+ Example:
56
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(
57
+ ("user",self.model.user_id)
58
+ )
59
+
60
+ pk: user#<user-id>
61
+ pk: user#123456789
62
+
63
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
64
+ ("xref", self.model.xref_type),
65
+ ("id", self.model.xref_pk),
66
+
67
+ )
68
+
69
+ sk: xref#<xref-type>#id#<some-id>
70
+ sk: xref#task#id#123456789
71
+
72
+ # example two has a leading "domain" (crm)
73
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
74
+ ("crm", "")
75
+ ("xref", self.model.xref_type),
76
+ ("id", self.model.xref_pk),
77
+
78
+ )
79
+
80
+ sk: crm#xref#<xref-type>#id#<some-id>
81
+ sk: crm#xref#task#id#123456789
82
+
83
+ # using None stops the key build
84
+ # useful when doing begins with
85
+
86
+ # assume self.model.xref_pk is None
87
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
88
+ ("xref", self.model.xref_type),
89
+ ("id", self.model.xref_pk),
90
+
91
+ )
92
+ # results with a key of
93
+ # which would get all of the users tasks assuming the pk was still
94
+ # the same
95
+ sk: xref#<xref-type>#id#
96
+ sk: xref#task#id#
97
+
53
98
  """
54
99
  parts = []
55
100
  for key, value in key_value_pairs:
@@ -57,6 +102,8 @@ class DynamoDBKey:
57
102
  if value is None:
58
103
  parts.append(f"{prefix}")
59
104
  break
105
+ elif len(str(value).strip()) == 0:
106
+ parts.append(f"{key}")
60
107
  else:
61
108
  parts.append(f"{prefix}{value}")
62
109
  key_str = "#".join(parts)
@@ -25,10 +25,10 @@ class HasKeys(Protocol):
25
25
  """Interface for classes that have primary and sort keys"""
26
26
 
27
27
  def get_pk(self, index_name: str) -> Optional[str]:
28
- """Inteface to get_pk"""
28
+ """Interface to get_pk"""
29
29
 
30
30
  def get_sk(self, index_name: str) -> Optional[str]:
31
- """Inteface to get_sk"""
31
+ """Interface to get_sk"""
32
32
 
33
33
  def get_key(self, index_name: str) -> And | Equals:
34
34
  """Get the index name and key"""
@@ -12,7 +12,6 @@ from aws_lambda_powertools import Logger
12
12
  from dateutil.relativedelta import relativedelta
13
13
 
14
14
 
15
-
16
15
  logger = Logger()
17
16
 
18
17
  _last_timestamp = None
@@ -43,11 +42,10 @@ class DatetimeUtility:
43
42
  @staticmethod
44
43
  def get_utc_now() -> datetime:
45
44
  # datetime.utcnow()
46
- # below is the prefered over datetime.utcnow()
45
+ # below is the preferred over datetime.utcnow()
47
46
  return datetime.now(timezone.utc)
48
47
 
49
48
  @staticmethod
50
-
51
49
  def string_to_date(string_date: str | datetime) -> datetime | None:
52
50
  """
53
51
  Description: takes a string value and returns it as a datetime.
@@ -269,7 +267,7 @@ class DatetimeUtility:
269
267
  months (int): the number of months
270
268
 
271
269
  Returns:
272
- datetime: One Month added to the input dt
270
+ datetime: X Month(s) added to the input dt
273
271
  """
274
272
  new_date = dt + relativedelta(months=+months)
275
273
  new_date = new_date + relativedelta(microseconds=-1)
@@ -278,14 +276,14 @@ class DatetimeUtility:
278
276
 
279
277
  @staticmethod
280
278
  def add_days(dt: datetime, days: int = 1) -> datetime:
281
- """Add a month to the current date
279
+ """Add a day to the current date
282
280
 
283
281
  Args:
284
282
  dt (datetime): datetime
285
- months (int): the number of months
283
+ days (int): the number of days, use a negative number to subtract
286
284
 
287
285
  Returns:
288
- datetime: One Month added to the input dt
286
+ datetime: X days added to the input dt
289
287
  """
290
288
  new_date = dt + relativedelta(days=+days)
291
289
  new_date = new_date + relativedelta(microseconds=-1)
@@ -314,7 +312,7 @@ class DatetimeUtility:
314
312
 
315
313
  Args:
316
314
  utc_datetime (datetime): datetime in utc
317
- timezone (str): 'US/Eastern', 'US/Moutain', etc
315
+ timezone (str): 'US/Eastern', 'US/Mountain', etc
318
316
 
319
317
  Returns:
320
318
  datetime: in the correct timezone
@@ -326,7 +324,7 @@ class DatetimeUtility:
326
324
 
327
325
  @staticmethod
328
326
  def get_timestamp(value: datetime | None | str) -> float:
329
- """Get a timestampe from a date or 0.0"""
327
+ """Get a timestamp from a date or 0.0"""
330
328
  if value is None:
331
329
  return 0.0
332
330
  if not isinstance(value, datetime):
@@ -339,7 +337,7 @@ class DatetimeUtility:
339
337
 
340
338
  @staticmethod
341
339
  def get_timestamp_or_none(value: datetime | None | str) -> float | None:
342
- """Get a timestampe from a date or None"""
340
+ """Get a timestamp from a date or None"""
343
341
  if value is None:
344
342
  return None
345
343
  if not isinstance(value, datetime):
boto3_assist/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.20.0'
1
+ __version__ = '0.22.0'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boto3_assist
3
- Version: 0.20.0
3
+ Version: 0.22.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=pJLteH9r0aOnAwnvPatmr2QSz7lO2BDXON0AKkXx0Pg,23
9
+ boto3_assist/version.py,sha256=h1iZs_ySY6is5xPKVypDpl9q8RFwvTXfv3Urt9ZSKSQ,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,15 @@ 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=zq2Hqhmd3aXjnd1GAmYq5TkjMPiq_pHcMW6hClSv13Y,16679
22
+ boto3_assist/dynamodb/dynamodb.py,sha256=2Xg9_aqcXzZm-4hASt6Z-mCR0gSs6iUDzLdAqzPn81o,18370
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
- boto3_assist/dynamodb/dynamodb_key.py,sha256=YD7o1EUlwVBQ55p9YCTKqAUU_np4nqtLIHnmp-BeolM,1803
28
+ boto3_assist/dynamodb/dynamodb_key.py,sha256=4IYnG4a99AjdOKUcDaWhNF_lvZJRZcKOIPzBQQzVdB0,3294
29
29
  boto3_assist/dynamodb/dynamodb_model_base.py,sha256=ceVKmequ2S7zY-8IkQwmtNvGV8GtaEraFBy1c2Y5P4A,12031
30
- boto3_assist/dynamodb/dynamodb_model_base_interfaces.py,sha256=yT4zDRI8vP15WVOHnCvY3FsEy_QSIta5-bnUby70Xow,747
30
+ boto3_assist/dynamodb/dynamodb_model_base_interfaces.py,sha256=SFw-yK7TDPL4cK52bpn2zMm5G4mX7eYNU7eFytEw0-A,749
31
31
  boto3_assist/dynamodb/dynamodb_re_indexer.py,sha256=Y0qRrvpmjS68w8ci6Au7Hg02W_ktqhUGGALeN-9XTls,6164
32
32
  boto3_assist/dynamodb/dynamodb_reindexer.py,sha256=bCj6KIU0fQOgjkkiq9yF51PFZZr4Y9Lu3-hPlmsPG0Y,6164
33
33
  boto3_assist/dynamodb/dynamodb_reserved_words.py,sha256=p0irNBSqGe4rd2FwWQqbRJWrNr4svdbWiyIXmz9lj4c,1937
@@ -49,7 +49,7 @@ boto3_assist/securityhub/securityhub.py,sha256=ne-J_v4DaCVZm5YgJa_-LKVomLJQo5Gpw
49
49
  boto3_assist/securityhub/securityhub_connection.py,sha256=hWfcj9gjS2lNXUObyw4cShtveoqJPIp8kKFuz-fz1J4,1449
50
50
  boto3_assist/ssm/connection.py,sha256=gYpKn5HsUR3hcRUqJzF5QcTITCk0DReq9KhoE_8-Htg,1370
51
51
  boto3_assist/ssm/parameter_store/parameter_store.py,sha256=2ISi-SmR29mESHFH-onJkxPX1aThIgBRojA3ZoNcP9s,3949
52
- boto3_assist/utilities/datetime_utility.py,sha256=kxUV-pYraTx18gaRb2LxXB7sRTBxffgksdOszcM3fg8,11150
52
+ boto3_assist/utilities/datetime_utility.py,sha256=yQa9winN661Gt837zeQQWl4ARMYtZcU4pQEYMnTESC0,11171
53
53
  boto3_assist/utilities/dictionary_utility.py,sha256=IrN5Q3gJ_KWQ_3KCjyXEJyV8oLk2n3tQOO52dVbY6sk,1002
54
54
  boto3_assist/utilities/file_operations.py,sha256=IYhJkh8wUPMvGnyDRRa9yOCDdHN9wR3N6m_xpJS51TM,3949
55
55
  boto3_assist/utilities/http_utility.py,sha256=_K39Fq0V4QcgklAWctUktuMjqXDTwgMld77IOUfR2zc,1282
@@ -57,8 +57,8 @@ boto3_assist/utilities/logging_utility.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
57
57
  boto3_assist/utilities/numbers_utility.py,sha256=wzv9d0uXT_2_ZHHio7LBzibwxPqhGpvbq9HinrVn_4A,10160
58
58
  boto3_assist/utilities/serialization_utility.py,sha256=Jc6H0cpcZjLO7tdyUZdBWHzItduLkw6sh2YQh8Hc8D8,21647
59
59
  boto3_assist/utilities/string_utility.py,sha256=XxUIz19L2LFFTRDAAmdPa8Qhn40u9yO7g4nULFuvg0M,11033
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,,
60
+ boto3_assist-0.22.0.dist-info/METADATA,sha256=fvChz9xIsO1PiE8HD8zzg4m1FMytpFhDJJMJcM6BKWw,2875
61
+ boto3_assist-0.22.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
62
+ boto3_assist-0.22.0.dist-info/licenses/LICENSE-EXPLAINED.txt,sha256=WFREvTpfTjPjDHpOLADxJpCKpIla3Ht87RUUGii4ODU,606
63
+ boto3_assist-0.22.0.dist-info/licenses/LICENSE.txt,sha256=PXDhFWS5L5aOTkVhNvoitHKbAkgxqMI2uUPQyrnXGiI,1105
64
+ boto3_assist-0.22.0.dist-info/RECORD,,