boto3-assist 0.27.0__tar.gz → 0.28.0__tar.gz

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 (156) hide show
  1. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/.gitignore +2 -0
  2. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/PKG-INFO +1 -1
  3. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/pyproject.toml +1 -1
  4. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/utilities/serialization_utility.py +81 -0
  5. boto3_assist-0.28.0/src/boto3_assist/version.py +1 -0
  6. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/utilities/serialization_utility_test.py +146 -1
  7. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/utilities/string_utility_test.py +1 -1
  8. boto3_assist-0.27.0/.pypirc +0 -13
  9. boto3_assist-0.27.0/src/boto3_assist/version.py +0 -1
  10. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/.env.docker +0 -0
  11. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/.env.docker.001 +0 -0
  12. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/.env.docker.nosql.workbench +0 -0
  13. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/.env.unittest +0 -0
  14. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/.vscode/launch.json +0 -0
  15. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/.vscode/settings.json +0 -0
  16. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/.vscode/tasks.json +0 -0
  17. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/LICENSE-EXPLAINED.txt +0 -0
  18. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/LICENSE.txt +0 -0
  19. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/README.md +0 -0
  20. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/aws_regions_with_status.csv +0 -0
  21. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/aws_regions_with_status.json +0 -0
  22. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/devops/build.py +0 -0
  23. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/devops/readme.md +0 -0
  24. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/examples/__init__.py +0 -0
  25. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/examples/cloudwatch/log_report.py +0 -0
  26. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/examples/dynamodb/models/order_item_model.py +0 -0
  27. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/examples/dynamodb/models/order_model.py +0 -0
  28. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/examples/dynamodb/models/product_model.py +0 -0
  29. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/examples/dynamodb/models/user_model.py +0 -0
  30. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/examples/dynamodb/models/user_post_model.py +0 -0
  31. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/examples/dynamodb/order_example/main.py +0 -0
  32. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/examples/dynamodb/order_example/products.json +0 -0
  33. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/examples/dynamodb/services/order_item_service.py +0 -0
  34. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/examples/dynamodb/services/order_service.py +0 -0
  35. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/examples/dynamodb/services/product_service.py +0 -0
  36. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/examples/dynamodb/services/table_service.py +0 -0
  37. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/examples/dynamodb/services/user_post_service.py +0 -0
  38. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/examples/dynamodb/services/user_service.py +0 -0
  39. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/examples/dynamodb/services/user_service_client_example.py +0 -0
  40. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/examples/dynamodb/services/user_service_resource_example.py +0 -0
  41. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/examples/dynamodb/user_post_example/main.py +0 -0
  42. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/examples/ec2/regions_report.py +0 -0
  43. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/module-headers.txt +0 -0
  44. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/mypy.ini +0 -0
  45. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/publish_to_pypi.py +0 -0
  46. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/publish_to_pypi.sh +0 -0
  47. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/pysetup.py +0 -0
  48. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/pysetup.sh +0 -0
  49. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/requirements-dev.txt +0 -0
  50. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/requirements.dev.txt +0 -0
  51. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/requirements.txt +0 -0
  52. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/run-checks.sh +0 -0
  53. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/run_unit_tests.sh +0 -0
  54. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/setup.sh +0 -0
  55. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/__init__.py +0 -0
  56. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/aws_config.py +0 -0
  57. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/aws_lambda/event_info.py +0 -0
  58. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/aws_lambda/mock_context.py +0 -0
  59. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/boto3session.py +0 -0
  60. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/cloudwatch/cloudwatch_connection.py +0 -0
  61. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/cloudwatch/cloudwatch_connection_tracker.py +0 -0
  62. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/cloudwatch/cloudwatch_log_connection.py +0 -0
  63. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/cloudwatch/cloudwatch_logs.py +0 -0
  64. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/cloudwatch/cloudwatch_query.py +0 -0
  65. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/cognito/cognito_authorizer.py +0 -0
  66. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/cognito/cognito_connection.py +0 -0
  67. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/cognito/cognito_utility.py +0 -0
  68. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/cognito/jwks_cache.py +0 -0
  69. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/cognito/user.py +0 -0
  70. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/connection.py +0 -0
  71. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/connection_tracker.py +0 -0
  72. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/dynamodb/dynamodb.py +0 -0
  73. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/dynamodb/dynamodb_connection.py +0 -0
  74. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/dynamodb/dynamodb_helpers.py +0 -0
  75. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/dynamodb/dynamodb_importer.py +0 -0
  76. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/dynamodb/dynamodb_index.py +0 -0
  77. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/dynamodb/dynamodb_iservice.py +0 -0
  78. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/dynamodb/dynamodb_key.py +0 -0
  79. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/dynamodb/dynamodb_model_base.py +0 -0
  80. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/dynamodb/dynamodb_model_base_interfaces.py +0 -0
  81. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/dynamodb/dynamodb_re_indexer.py +0 -0
  82. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/dynamodb/dynamodb_reindexer.py +0 -0
  83. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.py +0 -0
  84. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.txt +0 -0
  85. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/dynamodb/readme.md +0 -0
  86. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/dynamodb/troubleshooting.md +0 -0
  87. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/ec2/ec2_connection.py +0 -0
  88. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/environment_services/__init__.py +0 -0
  89. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/environment_services/environment_loader.py +0 -0
  90. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/environment_services/environment_variables.py +0 -0
  91. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/erc/__init__.py +0 -0
  92. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/erc/ecr_connection.py +0 -0
  93. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/errors/custom_exceptions.py +0 -0
  94. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/http_status_codes.py +0 -0
  95. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/models/serializable_model.py +0 -0
  96. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/role_assumption_mixin.py +0 -0
  97. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/s3/s3.py +0 -0
  98. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/s3/s3_bucket.py +0 -0
  99. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/s3/s3_connection.py +0 -0
  100. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/s3/s3_event_data.py +0 -0
  101. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/s3/s3_object.py +0 -0
  102. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/securityhub/securityhub.py +0 -0
  103. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/securityhub/securityhub_connection.py +0 -0
  104. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/session_setup_mixin.py +0 -0
  105. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/ssm/connection.py +0 -0
  106. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/ssm/parameter_store/parameter_store.py +0 -0
  107. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/utilities/datetime_utility.py +0 -0
  108. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/utilities/dictionary_utility.py +0 -0
  109. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/utilities/file_operations.py +0 -0
  110. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/utilities/http_utility.py +0 -0
  111. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/utilities/logging_utility.py +0 -0
  112. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/utilities/numbers_utility.py +0 -0
  113. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/src/boto3_assist/utilities/string_utility.py +0 -0
  114. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/__init__.py +0 -0
  115. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/integration/cross_account_connection_test.py +0 -0
  116. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/integration/tenant.py +0 -0
  117. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/integration/tenant_services.py +0 -0
  118. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/aws_config_test.py +0 -0
  119. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/common/db_test_helpers.py +0 -0
  120. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/dynamodb_tests/__init__.py +0 -0
  121. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/dynamodb_tests/db_models/cms/base.py +0 -0
  122. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/dynamodb_tests/db_models/cms/content_block.py +0 -0
  123. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/dynamodb_tests/db_models/cms/page.py +0 -0
  124. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/dynamodb_tests/db_models/cms/template.py +0 -0
  125. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/dynamodb_tests/db_models/simple_model.py +0 -0
  126. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/dynamodb_tests/db_models/task.py +0 -0
  127. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/dynamodb_tests/db_models/user_model.py +0 -0
  128. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/dynamodb_tests/db_models/user_required_fields_model.py +0 -0
  129. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/dynamodb_tests/dynamodb_fail_if_exists_test.py +0 -0
  130. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/dynamodb_tests/dynamodb_model_base_test.py +0 -0
  131. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/dynamodb_tests/dynamodb_model_projections_test.py +0 -0
  132. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/dynamodb_tests/dynamodb_model_serializtion_test.py +0 -0
  133. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/dynamodb_tests/dynamodb_moto_sorting_test.py +0 -0
  134. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/dynamodb_tests/dynamodb_primary_key_get_test.py +0 -0
  135. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/dynamodb_tests/dynamodb_primary_key_sort_test.py +0 -0
  136. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/dynamodb_tests/dynamodb_query_test.py +0 -0
  137. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/dynamodb_tests/dynamodb_reindex_test.py +0 -0
  138. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/examples_test/__init__.py +0 -0
  139. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/examples_test/user_service_test.py +0 -0
  140. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/lambda_tests/__init__.py +0 -0
  141. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/lambda_tests/event_info_test.py +0 -0
  142. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/models_tests/__init__.py +0 -0
  143. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/models_tests/models/person.py +0 -0
  144. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/models_tests/models/user.py +0 -0
  145. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/models_tests/serializable_model_person_test.py +0 -0
  146. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/models_tests/serializable_model_user_test.py +0 -0
  147. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/models_tests/serializable_model_wide_test.py +0 -0
  148. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/parameter_store/__init__.py +0 -0
  149. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/parameter_store/parameter_store_test.py +0 -0
  150. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/s3/__init__.py +0 -0
  151. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/s3/files/test.txt +0 -0
  152. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/s3/s3_event_data_test.py +0 -0
  153. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/s3/s3_file_delete_test.py +0 -0
  154. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/s3/s3_file_upload_test.py +0 -0
  155. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/session_tests/test_boto3_session_manager.py +0 -0
  156. {boto3_assist-0.27.0 → boto3_assist-0.28.0}/tests/unit/utilities/__init__.py +0 -0
@@ -170,3 +170,5 @@ cython_debug/
170
170
  .pysetup.json
171
171
 
172
172
  activate.sh
173
+ .pypirc
174
+ .DS_Store
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boto3_assist
3
- Version: 0.27.0
3
+ Version: 0.28.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
@@ -15,7 +15,7 @@ addopts = "-m 'not integration'"
15
15
 
16
16
  [project]
17
17
  name = "boto3_assist"
18
- version = "0.27.0"
18
+ version = "0.28.0"
19
19
 
20
20
  authors = [
21
21
  { name="Eric Wilson", email="boto3-assist@geekcafe.com" }
@@ -8,6 +8,7 @@ import datetime as dt
8
8
  import decimal
9
9
  import inspect
10
10
  import json
11
+ import typing
11
12
  import uuid
12
13
  from datetime import datetime
13
14
  from decimal import Decimal
@@ -116,6 +117,86 @@ class JsonConversions:
116
117
  Used for snake_case to camelCase and vice versa
117
118
  """
118
119
 
120
+ @staticmethod
121
+ def string_to_json_obj(
122
+ value: str | list | dict, raise_on_error: bool = True, retry: int = 0
123
+ ) -> typing.Union[dict, typing.Any, None]:
124
+ """
125
+ Converts a string to a JSON object.
126
+
127
+ Args:
128
+ value: The value to convert (string, list, or dict).
129
+ raise_on_error: Whether to raise an exception on error.
130
+ retry: The number of retry attempts made.
131
+
132
+ Returns:
133
+ The converted JSON object, or the original value if conversion fails.
134
+ """
135
+ # Handle empty/None values
136
+ if not value:
137
+ return {}
138
+
139
+ # Return dicts unchanged
140
+ if isinstance(value, dict):
141
+ return value
142
+
143
+ # Check retry limit early
144
+ if retry > 5:
145
+ raise RuntimeError("Too many attempts to convert string to JSON")
146
+
147
+ try:
148
+ # Convert to string if needed
149
+ if not isinstance(value, str):
150
+ value = str(value)
151
+
152
+ # Clean up the string
153
+ value = value.replace("\n", "").strip()
154
+ if value.startswith("'") and value.endswith("'"):
155
+ value = value.strip("'").strip()
156
+
157
+ # Parse JSON
158
+ parsed_value = json.loads(value)
159
+
160
+ # Handle nested string JSON (recursive case)
161
+ if isinstance(parsed_value, str):
162
+ return JsonConversions.string_to_json_obj(parsed_value, raise_on_error, retry + 1)
163
+
164
+ return parsed_value
165
+
166
+ except json.JSONDecodeError as e:
167
+ # Try to fix malformed JSON with single quotes
168
+ if "Expecting property name enclosed in double quotes" in str(e) and retry < 5:
169
+ if isinstance(value, str):
170
+ fixed_json = JsonConversions.convert_bad_json_string(value)
171
+ return JsonConversions.string_to_json_obj(fixed_json, raise_on_error, retry + 1)
172
+
173
+ if raise_on_error:
174
+ raise e
175
+ return {}
176
+
177
+ except Exception as e:
178
+ if raise_on_error:
179
+ logger.exception({"source": "string_to_json_obj", "error": str(e), "value": value})
180
+ raise e
181
+
182
+ logger.warning({"source": "string_to_json_obj", "returning_original": True, "value": value})
183
+ return value
184
+
185
+
186
+ @staticmethod
187
+ def convert_bad_json_string(bad_json: str) -> str:
188
+ """
189
+ Fixes malformed JSON by converting single quotes to double quotes.
190
+
191
+ Args:
192
+ bad_json: Malformed JSON string with single quotes.
193
+
194
+ Returns:
195
+ Fixed JSON string with proper double quotes.
196
+ """
197
+ # Use a placeholder to safely swap quotes
198
+ return bad_json.replace("'", "§§§").replace('"', "'").replace("§§§", '"')
199
+
119
200
  @staticmethod
120
201
  def _camel_to_snake(value: str) -> str:
121
202
  """Converts a camelCase to a snake_case"""
@@ -0,0 +1 @@
1
+ __version__ = "0.28.0"
@@ -5,11 +5,12 @@ MIT License. See Project Root for the license information.
5
5
  """
6
6
 
7
7
  import unittest
8
+ import json
8
9
  from datetime import datetime, UTC
9
10
  from datetime import timedelta
10
11
  from typing import cast
11
12
  from typing import Optional, List
12
- from boto3_assist.utilities.serialization_utility import Serialization
13
+ from boto3_assist.utilities.serialization_utility import Serialization, JsonConversions
13
14
  from boto3_assist.dynamodb.dynamodb_model_base import (
14
15
  DynamoDBModelBase,
15
16
  exclude_indexes_from_serialization,
@@ -264,3 +265,147 @@ class SerializationUnitTest(unittest.TestCase):
264
265
  self.assertIsNone(active_subscription.get("sk"))
265
266
  self.assertIsNone(active_subscription.get("gsi0_pk"))
266
267
  self.assertIsNone(active_subscription.get("gsi0_sk"))
268
+
269
+
270
+ class JsonConversionsUnitTest(unittest.TestCase):
271
+ """Unit tests for JsonConversions.string_to_json_obj function"""
272
+
273
+ def setUp(self):
274
+ """Set up common test data"""
275
+ self.sample_dict = {"name": "John", "age": 30}
276
+ self.sample_json = '{"name": "John", "age": 30}'
277
+
278
+ def test_valid_json_conversions(self):
279
+ """Test converting valid JSON strings and data types"""
280
+ test_cases = [
281
+ (
282
+ '{"name": "John", "age": 30, "active": true}',
283
+ {"name": "John", "age": 30, "active": True},
284
+ ),
285
+ (
286
+ '[{"name": "John"}, {"name": "Jane"}]',
287
+ [{"name": "John"}, {"name": "Jane"}],
288
+ ),
289
+ (
290
+ '{"message": "Hello 🌍", "symbol": "©"}',
291
+ {"message": "Hello 🌍", "symbol": "©"},
292
+ ),
293
+ (
294
+ '{"name": "John", "middle_name": null}',
295
+ {"name": "John", "middle_name": None},
296
+ ),
297
+ ]
298
+
299
+ for json_input, expected in test_cases:
300
+ with self.subTest(json_input=json_input):
301
+ result = JsonConversions.string_to_json_obj(json_input)
302
+ self.assertEqual(result, expected)
303
+
304
+ def test_empty_and_none_inputs(self):
305
+ """Test edge cases with empty/None inputs"""
306
+ test_cases = [
307
+ ("", {}),
308
+ (None, {}),
309
+ ]
310
+
311
+ for input_val, expected in test_cases:
312
+ with self.subTest(input_val=input_val):
313
+ result = JsonConversions.string_to_json_obj(input_val)
314
+ self.assertEqual(result, expected)
315
+
316
+ def test_passthrough_types(self):
317
+ """Test that certain types pass through unchanged"""
318
+ input_dict = {"name": "John", "age": 30}
319
+ result = JsonConversions.string_to_json_obj(input_dict)
320
+ self.assertEqual(result, input_dict)
321
+ self.assertIs(result, input_dict)
322
+
323
+ def test_string_preprocessing(self):
324
+ """Test string preprocessing (whitespace, quotes)"""
325
+ test_cases = [
326
+ ('\n {"name": "John", "age": 30} \n', self.sample_dict),
327
+ ('\'{"name": "John", "age": 30}\'', self.sample_dict),
328
+ ('"{\\"name\\": \\"John\\", \\"age\\": 30}"', self.sample_dict),
329
+ ]
330
+
331
+ for json_input, expected in test_cases:
332
+ with self.subTest(json_input=json_input[:20] + "..."):
333
+ result = JsonConversions.string_to_json_obj(json_input)
334
+ self.assertEqual(result, expected)
335
+
336
+ def test_malformed_json_auto_fix(self):
337
+ """Test auto-fixing of malformed JSON"""
338
+ bad_json = "{'name': 'John', 'age': 30}"
339
+ result = JsonConversions.string_to_json_obj(bad_json)
340
+ self.assertEqual(result, self.sample_dict)
341
+
342
+ def test_error_handling_with_raise_on_error_true(self):
343
+ """Test error handling when raise_on_error=True"""
344
+ test_cases = [
345
+ ("{'name': 'John', 'age': 30, 'invalid'}", json.JSONDecodeError),
346
+ ('{"valid": "json"}', RuntimeError), # retry=10 triggers RuntimeError
347
+ ]
348
+
349
+ for invalid_input, expected_exception in test_cases:
350
+ with self.subTest(invalid_input=invalid_input):
351
+ with self.assertRaises(expected_exception):
352
+ if expected_exception == RuntimeError:
353
+ JsonConversions.string_to_json_obj(invalid_input, retry=10)
354
+ else:
355
+ JsonConversions.string_to_json_obj(
356
+ invalid_input, raise_on_error=True
357
+ )
358
+
359
+ def test_error_handling_with_raise_on_error_false(self):
360
+ """Test graceful error handling when raise_on_error=False"""
361
+ test_cases = [
362
+ ("{'name': 'John', 'age': 30, 'invalid'}", {}),
363
+ (12345, 12345), # Non-string input
364
+ ]
365
+
366
+ for invalid_input, expected in test_cases:
367
+ with self.subTest(invalid_input=invalid_input):
368
+ result = JsonConversions.string_to_json_obj(
369
+ invalid_input, raise_on_error=False
370
+ )
371
+ self.assertEqual(result, expected)
372
+
373
+ def test_retry_limit(self):
374
+ """Test retry limit enforcement"""
375
+ with self.assertRaises(RuntimeError) as context:
376
+ JsonConversions.string_to_json_obj(self.sample_json, retry=6)
377
+ self.assertIn("Too many attempts", str(context.exception))
378
+
379
+ def test_complex_nested_structure(self):
380
+ """Test complex nested JSON structure"""
381
+ complex_json = """{
382
+ "user": {"name": "John Doe", "details": {"age": 30, "preferences": ["reading", "coding"]}},
383
+ "metadata": {"version": 1.2}
384
+ }"""
385
+
386
+ result = JsonConversions.string_to_json_obj(complex_json)
387
+
388
+ # Verify key nested values
389
+ self.assertEqual(result["user"]["name"], "John Doe")
390
+ self.assertEqual(result["user"]["details"]["age"], 30)
391
+ self.assertEqual(
392
+ result["user"]["details"]["preferences"], ["reading", "coding"]
393
+ )
394
+ self.assertEqual(result["metadata"]["version"], 1.2)
395
+
396
+ def test_data_type_preservation(self):
397
+ """Test that JSON data types are properly preserved"""
398
+ json_with_types = """{
399
+ "string_val": "text", "int_val": 42, "float_val": 3.14,
400
+ "bool_true": true, "bool_false": false, "null_val": null
401
+ }"""
402
+
403
+ result = JsonConversions.string_to_json_obj(json_with_types)
404
+
405
+ # Verify all data types
406
+ self.assertEqual(result["string_val"], "text")
407
+ self.assertEqual(result["int_val"], 42)
408
+ self.assertEqual(result["float_val"], 3.14)
409
+ self.assertTrue(result["bool_true"])
410
+ self.assertFalse(result["bool_false"])
411
+ self.assertIsNone(result["null_val"])
@@ -17,7 +17,7 @@ class StringUtilityUnitTest(unittest.TestCase):
17
17
  "String Utility Tests"
18
18
 
19
19
  def test_uuid_idempotency(self):
20
- """Testing Idempotnent UUID generation."""
20
+ """Testing Idempotent UUID generation."""
21
21
  # must be consistent
22
22
  namespace: uuid.UUID = uuid.UUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
23
23
 
@@ -1,13 +0,0 @@
1
- [distutils]
2
- index-servers =
3
- pypi
4
- testpypi
5
-
6
- [pypi]
7
- username = __token__
8
- password = pypi-AgEIcHlwaS5vcmcCJGE0NDBkMmNlLWIzMWUtNDRjZC1iOGQxLTdiYmIzYmZkNWE0NAACKlszLCI3YzZmZTIzYS04MDVkLTQ0YjYtODlkZS04YTJiODJiNjBmMDQiXQAABiA_5UuF03-RSqW69zwSeC9I5rR8EIR4uZ8Zcj9p73XCXA
9
-
10
- [testpypi]
11
- repository = https://test.pypi.org/legacy/
12
- username = __token__
13
- password = pypi-AgENdGVzdC5weXBpLm9yZwIkZmEwOTVlMTAtMzE5ZC00MjU5LTk2ZjUtNjkzODg4MWIxMjE4AAIqWzMsImE3YzE2NzI0LTZjYmQtNDQzZi1iYzYyLTNhM2I3NWI4NDg5OSJdAAAGIGQlPqWLM1ziye_G9C1kLOG47bBEAAxTQmnnpaimREd1
@@ -1 +0,0 @@
1
- __version__ = "0.27.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes