boto3-assist 0.28.0__py3-none-any.whl → 0.29.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.
@@ -21,6 +21,7 @@ from .dynamodb_connection import DynamoDBConnection
21
21
  from .dynamodb_helpers import DynamoDBHelpers
22
22
  from .dynamodb_model_base import DynamoDBModelBase
23
23
  from ..utilities.string_utility import StringUtility
24
+ from ..utilities.decimal_conversion_utility import DecimalConversionUtility
24
25
  from .dynamodb_index import DynamoDBIndex
25
26
 
26
27
  logger = Logger()
@@ -51,17 +52,34 @@ class DynamoDB(DynamoDBConnection):
51
52
  aws_region=aws_region,
52
53
  aws_end_point_url=aws_end_point_url,
53
54
  aws_access_key_id=aws_access_key_id,
54
- aws_secret_access_key=aws_secret_access_key,
55
55
  assume_role_arn=assume_role_arn,
56
56
  assume_role_chain=assume_role_chain,
57
57
  assume_role_duration_seconds=assume_role_duration_seconds,
58
58
  )
59
59
  self.helpers: DynamoDBHelpers = DynamoDBHelpers()
60
- self.log_dynamodb_item_size = (
61
- str(os.getenv("LOG_DYNAMODB_ITEM_SIZE", "false")).lower() == "true"
60
+ self.log_dynamodb_item_size: bool = bool(
61
+ os.getenv("LOG_DYNAMODB_ITEM_SIZE", "False").lower() == "true"
62
+ )
63
+ self.convert_decimals: bool = bool(
64
+ os.getenv("DYNAMODB_CONVERT_DECIMALS", "True").lower() == "true"
62
65
  )
63
66
  logger.setLevel(os.getenv("LOG_LEVEL", "INFO"))
64
67
 
68
+ def _apply_decimal_conversion(self, response: Dict[str, Any]) -> Dict[str, Any]:
69
+ """
70
+ Apply decimal conversion to DynamoDB response if enabled.
71
+
72
+ Args:
73
+ response: The DynamoDB response dictionary
74
+
75
+ Returns:
76
+ The response with decimal conversion applied if enabled
77
+ """
78
+ if not self.convert_decimals:
79
+ return response
80
+
81
+ return DecimalConversionUtility.convert_decimals_to_native_types(response)
82
+
65
83
  def save(
66
84
  self,
67
85
  item: dict | DynamoDBModelBase,
@@ -283,7 +301,8 @@ class DynamoDB(DynamoDBConnection):
283
301
  if self.raise_on_error:
284
302
  raise e
285
303
 
286
- return response
304
+ # Apply decimal conversion to the response
305
+ return self._apply_decimal_conversion(response)
287
306
 
288
307
  def update_item(
289
308
  self,
@@ -391,7 +410,8 @@ class DynamoDB(DynamoDBConnection):
391
410
  if self.raise_on_error:
392
411
  raise e
393
412
 
394
- return response
413
+ # Apply decimal conversion to the response
414
+ return self._apply_decimal_conversion(response)
395
415
 
396
416
  @overload
397
417
  def delete(self, *, table_name: str, model: DynamoDBModelBase) -> dict:
@@ -11,8 +11,9 @@ import datetime as dt
11
11
  # import inspect
12
12
  # import uuid
13
13
  from typing import TypeVar, List, Dict, Any
14
- from boto3.dynamodb.types import TypeSerializer
14
+ from boto3.dynamodb.types import TypeSerializer, TypeDeserializer
15
15
  from boto3_assist.utilities.serialization_utility import Serialization
16
+ from boto3_assist.utilities.decimal_conversion_utility import DecimalConversionUtility
16
17
  from boto3_assist.dynamodb.dynamodb_helpers import DynamoDBHelpers
17
18
  from boto3_assist.dynamodb.dynamodb_index import (
18
19
  DynamoDBIndexes,
@@ -156,14 +157,20 @@ class DynamoDBModelBase(SerializableModel):
156
157
  item = item.to_resource_dictionary()
157
158
 
158
159
  if isinstance(item, dict):
159
- # see if this is coming directly from dynamodb
160
+ # Handle DynamoDB response structures
160
161
  if "ResponseMetadata" in item:
162
+ # Full DynamoDB response with metadata
161
163
  response: dict | None = item.get("Item")
162
-
163
164
  if response is None:
164
165
  response = {}
165
-
166
166
  item = response
167
+ elif "Item" in item and not any(key in item for key in ["id", "name", "pk", "sk"]):
168
+ # Response with Item key but no direct model attributes (likely a DynamoDB response)
169
+ # This handles cases like {'Item': {...}} or {'Item': {...}, 'Count': 1}
170
+ item = item.get("Item", {})
171
+
172
+ # Convert any Decimal objects to native Python types for easier handling
173
+ item = DecimalConversionUtility.convert_decimals_to_native_types(item)
167
174
 
168
175
  else:
169
176
  raise ValueError("Item must be a dictionary or DynamoDBModelBase")
@@ -283,6 +290,7 @@ class DynamoDBSerializer:
283
290
 
284
291
  return mapped
285
292
 
293
+
286
294
  @staticmethod
287
295
  def to_client_dictionary(
288
296
  instance: DynamoDBModelBase, include_indexes: bool = True
@@ -0,0 +1,140 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ Maintainers: Eric Wilson
4
+ MIT License. See Project Root for the license information.
5
+ """
6
+
7
+ from decimal import Decimal
8
+ from typing import Any, Dict, List, Union
9
+
10
+
11
+ class DecimalConversionUtility:
12
+ """
13
+ Utility class for handling decimal conversions between Python types and DynamoDB.
14
+
15
+ DynamoDB stores all numbers as Decimal types, but Python applications often
16
+ expect int or float types. This utility provides conversion methods to handle
17
+ the transformation seamlessly.
18
+ """
19
+
20
+ @staticmethod
21
+ def convert_decimals_to_native_types(data: Any) -> Any:
22
+ """
23
+ Recursively converts Decimal objects to native Python types (int or float).
24
+
25
+ This is typically used when deserializing data from DynamoDB, where all
26
+ numbers are stored as Decimal objects but the application expects native
27
+ Python numeric types.
28
+
29
+ Args:
30
+ data: The data structure to convert. Can be dict, list, or any other type.
31
+
32
+ Returns:
33
+ The data structure with Decimal objects converted to int or float.
34
+ """
35
+ if isinstance(data, dict):
36
+ return {
37
+ key: DecimalConversionUtility.convert_decimals_to_native_types(value)
38
+ for key, value in data.items()
39
+ }
40
+ elif isinstance(data, list):
41
+ return [
42
+ DecimalConversionUtility.convert_decimals_to_native_types(item)
43
+ for item in data
44
+ ]
45
+ elif isinstance(data, Decimal):
46
+ # Convert Decimal to int if it's a whole number, otherwise to float
47
+ if data % 1 == 0:
48
+ return int(data)
49
+ else:
50
+ return float(data)
51
+ else:
52
+ return data
53
+
54
+ @staticmethod
55
+ def convert_native_types_to_decimals(data: Any) -> Any:
56
+ """
57
+ Recursively converts native Python numeric types (int, float) to Decimal objects.
58
+
59
+ This is typically used when serializing data for DynamoDB, where all
60
+ numbers should be stored as Decimal objects for precision.
61
+
62
+ Args:
63
+ data: The data structure to convert. Can be dict, list, or any other type.
64
+
65
+ Returns:
66
+ The data structure with int and float objects converted to Decimal.
67
+ """
68
+ if isinstance(data, dict):
69
+ return {
70
+ key: DecimalConversionUtility.convert_native_types_to_decimals(value)
71
+ for key, value in data.items()
72
+ }
73
+ elif isinstance(data, list):
74
+ return [
75
+ DecimalConversionUtility.convert_native_types_to_decimals(item)
76
+ for item in data
77
+ ]
78
+ elif isinstance(data, float):
79
+ # Convert float to Decimal using string representation for precision
80
+ return Decimal(str(data))
81
+ elif isinstance(data, int) and not isinstance(data, bool):
82
+ # Convert int to Decimal, but exclude bool (which is a subclass of int)
83
+ return Decimal(data)
84
+ else:
85
+ return data
86
+
87
+ @staticmethod
88
+ def is_numeric_type(value: Any) -> bool:
89
+ """
90
+ Check if a value is a numeric type (int, float, or Decimal).
91
+
92
+ Args:
93
+ value: The value to check.
94
+
95
+ Returns:
96
+ True if the value is a numeric type, False otherwise.
97
+ """
98
+ return isinstance(value, (int, float, Decimal)) and not isinstance(value, bool)
99
+
100
+ @staticmethod
101
+ def safe_decimal_conversion(value: Any, default: Any = None) -> Union[Decimal, Any]:
102
+ """
103
+ Safely convert a value to Decimal, returning a default if conversion fails.
104
+
105
+ Args:
106
+ value: The value to convert to Decimal.
107
+ default: The default value to return if conversion fails.
108
+
109
+ Returns:
110
+ Decimal representation of the value, or the default if conversion fails.
111
+ """
112
+ try:
113
+ if isinstance(value, Decimal):
114
+ return value
115
+ elif isinstance(value, (int, float)):
116
+ return Decimal(str(value))
117
+ elif isinstance(value, str):
118
+ return Decimal(value)
119
+ else:
120
+ return default
121
+ except (ValueError, TypeError, ArithmeticError):
122
+ return default
123
+
124
+ @staticmethod
125
+ def format_decimal_for_display(value: Decimal, precision: int = 2) -> str:
126
+ """
127
+ Format a Decimal value for display with specified precision.
128
+
129
+ Args:
130
+ value: The Decimal value to format.
131
+ precision: Number of decimal places to display.
132
+
133
+ Returns:
134
+ Formatted string representation of the Decimal.
135
+ """
136
+ if not isinstance(value, Decimal):
137
+ value = DecimalConversionUtility.safe_decimal_conversion(value, Decimal('0'))
138
+
139
+ format_string = f"{{:.{precision}f}}"
140
+ return format_string.format(float(value))
boto3_assist/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.28.0"
1
+ __version__ = "0.29.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boto3_assist
3
- Version: 0.28.0
3
+ Version: 0.29.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=MRQGtOXBhcDKeeNOL0LiB-cllo6kfd8_KGJOvaDp0XQ,23
9
+ boto3_assist/version.py,sha256=RvbohpnoJRSLAwJOFoWTtu5c5Vpi5vWAfsKR12hwJVI,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,14 +19,14 @@ 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=TiO1lnQGiWxaWZ5-nnaWMMThFl3PMkD2r5ApIxEhBC8,18495
22
+ boto3_assist/dynamodb/dynamodb.py,sha256=vZvGerbiUy3J9KYjHHcoU97F_Fi85HYMK1KOzdMebc0,19330
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=MKQU4837rjYExMGCucGe2HBkPQPGszb8O6LR9EZBqek,8902
27
27
  boto3_assist/dynamodb/dynamodb_iservice.py,sha256=O9Aj0PFEvcuk2vhARifWTFnUwcQW5EXzwZS478Hm-N0,796
28
28
  boto3_assist/dynamodb/dynamodb_key.py,sha256=4IYnG4a99AjdOKUcDaWhNF_lvZJRZcKOIPzBQQzVdB0,3294
29
- boto3_assist/dynamodb/dynamodb_model_base.py,sha256=Bgnjs62lHTqqJ2nZbPV1JHvcK6d2-aaRsJtcc8DEqKk,12291
29
+ boto3_assist/dynamodb/dynamodb_model_base.py,sha256=SjV43lkP14YjfCH7j9KfEZAC89FYV3MLM6Ci40vTccY,12952
30
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
@@ -52,6 +52,7 @@ boto3_assist/securityhub/securityhub_connection.py,sha256=hWfcj9gjS2lNXUObyw4cSh
52
52
  boto3_assist/ssm/connection.py,sha256=gYpKn5HsUR3hcRUqJzF5QcTITCk0DReq9KhoE_8-Htg,1370
53
53
  boto3_assist/ssm/parameter_store/parameter_store.py,sha256=2ISi-SmR29mESHFH-onJkxPX1aThIgBRojA3ZoNcP9s,3949
54
54
  boto3_assist/utilities/datetime_utility.py,sha256=yQa9winN661Gt837zeQQWl4ARMYtZcU4pQEYMnTESC0,11171
55
+ boto3_assist/utilities/decimal_conversion_utility.py,sha256=E87lXpgOTDQpHU2wCE916YzNQeIFSc_nl02le0o9v4E,5037
55
56
  boto3_assist/utilities/dictionary_utility.py,sha256=IrN5Q3gJ_KWQ_3KCjyXEJyV8oLk2n3tQOO52dVbY6sk,1002
56
57
  boto3_assist/utilities/file_operations.py,sha256=IYhJkh8wUPMvGnyDRRa9yOCDdHN9wR3N6m_xpJS51TM,3949
57
58
  boto3_assist/utilities/http_utility.py,sha256=_K39Fq0V4QcgklAWctUktuMjqXDTwgMld77IOUfR2zc,1282
@@ -59,8 +60,8 @@ boto3_assist/utilities/logging_utility.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
59
60
  boto3_assist/utilities/numbers_utility.py,sha256=wzv9d0uXT_2_ZHHio7LBzibwxPqhGpvbq9HinrVn_4A,10160
60
61
  boto3_assist/utilities/serialization_utility.py,sha256=m5wRZNeWW9VltQPVNziR27OGKO3MDJm6mFmcDHwN-n4,24479
61
62
  boto3_assist/utilities/string_utility.py,sha256=XxUIz19L2LFFTRDAAmdPa8Qhn40u9yO7g4nULFuvg0M,11033
62
- boto3_assist-0.28.0.dist-info/METADATA,sha256=XrwSSxvnnUASG0OPp4ulP99LgF8CTTcwExXgXbZG1UE,2879
63
- boto3_assist-0.28.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
64
- boto3_assist-0.28.0.dist-info/licenses/LICENSE-EXPLAINED.txt,sha256=WFREvTpfTjPjDHpOLADxJpCKpIla3Ht87RUUGii4ODU,606
65
- boto3_assist-0.28.0.dist-info/licenses/LICENSE.txt,sha256=PXDhFWS5L5aOTkVhNvoitHKbAkgxqMI2uUPQyrnXGiI,1105
66
- boto3_assist-0.28.0.dist-info/RECORD,,
63
+ boto3_assist-0.29.0.dist-info/METADATA,sha256=LqQDqSXK3yioOtRe_GZipXzZKhY56CeWJlPECd2ZSsg,2879
64
+ boto3_assist-0.29.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
65
+ boto3_assist-0.29.0.dist-info/licenses/LICENSE-EXPLAINED.txt,sha256=WFREvTpfTjPjDHpOLADxJpCKpIla3Ht87RUUGii4ODU,606
66
+ boto3_assist-0.29.0.dist-info/licenses/LICENSE.txt,sha256=PXDhFWS5L5aOTkVhNvoitHKbAkgxqMI2uUPQyrnXGiI,1105
67
+ boto3_assist-0.29.0.dist-info/RECORD,,