boto3-assist 0.28.0__tar.gz → 0.30.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 (169) hide show
  1. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/.gitignore +2 -1
  2. boto3_assist-0.30.0/.windsurf/rules/cascade.yaml +11 -0
  3. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/PKG-INFO +1 -1
  4. boto3_assist-0.30.0/docs/defining-models.md +237 -0
  5. boto3_assist-0.30.0/docs/defining-services.md +150 -0
  6. boto3_assist-0.30.0/docs/design-patterns.md +2468 -0
  7. boto3_assist-0.30.0/docs/issues/BOTO3_ASSIST_BEFORE_AFTER.md +362 -0
  8. boto3_assist-0.30.0/docs/issues/BOTO3_ASSIST_DECIMAL_PATTERN.md +446 -0
  9. boto3_assist-0.30.0/docs/issues/BOTO3_ASSIST_IMPLEMENTATION_CHECKLIST.md +327 -0
  10. boto3_assist-0.30.0/docs/unit-test-patterns.md +683 -0
  11. boto3_assist-0.30.0/examples/dynamodb/decimal_conversion_example.py +285 -0
  12. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/models/product_model.py +6 -0
  13. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/order_example/products.json +15 -0
  14. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/pyproject.toml +1 -1
  15. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb.py +25 -5
  16. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_helpers.py +11 -13
  17. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_model_base.py +12 -4
  18. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_re_indexer.py +1 -1
  19. boto3_assist-0.30.0/src/boto3_assist/utilities/decimal_conversion_utility.py +140 -0
  20. boto3_assist-0.30.0/src/boto3_assist/version.py +1 -0
  21. boto3_assist-0.30.0/tests/unit/dynamodb/decimal_backward_compatibility_test.py +611 -0
  22. boto3_assist-0.30.0/tests/unit/dynamodb/decimal_conversion_integration_test.py +286 -0
  23. boto3_assist-0.30.0/tests/unit/dynamodb_tests/dynamodb_query_test.py +135 -0
  24. boto3_assist-0.30.0/tests/unit/utilities/decimal_conversion_utility_test.py +268 -0
  25. boto3_assist-0.28.0/src/boto3_assist/version.py +0 -1
  26. boto3_assist-0.28.0/tests/unit/dynamodb_tests/dynamodb_query_test.py +0 -66
  27. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/.env.docker +0 -0
  28. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/.env.docker.001 +0 -0
  29. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/.env.docker.nosql.workbench +0 -0
  30. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/.env.unittest +0 -0
  31. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/.vscode/launch.json +0 -0
  32. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/.vscode/settings.json +0 -0
  33. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/.vscode/tasks.json +0 -0
  34. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/LICENSE-EXPLAINED.txt +0 -0
  35. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/LICENSE.txt +0 -0
  36. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/README.md +0 -0
  37. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/aws_regions_with_status.csv +0 -0
  38. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/aws_regions_with_status.json +0 -0
  39. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/devops/build.py +0 -0
  40. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/devops/readme.md +0 -0
  41. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/__init__.py +0 -0
  42. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/cloudwatch/log_report.py +0 -0
  43. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/models/order_item_model.py +0 -0
  44. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/models/order_model.py +0 -0
  45. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/models/user_model.py +0 -0
  46. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/models/user_post_model.py +0 -0
  47. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/order_example/main.py +0 -0
  48. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/services/order_item_service.py +0 -0
  49. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/services/order_service.py +0 -0
  50. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/services/product_service.py +0 -0
  51. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/services/table_service.py +0 -0
  52. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/services/user_post_service.py +0 -0
  53. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/services/user_service.py +0 -0
  54. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/services/user_service_client_example.py +0 -0
  55. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/services/user_service_resource_example.py +0 -0
  56. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/user_post_example/main.py +0 -0
  57. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/ec2/regions_report.py +0 -0
  58. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/module-headers.txt +0 -0
  59. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/mypy.ini +0 -0
  60. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/publish_to_pypi.py +0 -0
  61. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/publish_to_pypi.sh +0 -0
  62. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/pysetup.py +0 -0
  63. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/pysetup.sh +0 -0
  64. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/requirements-dev.txt +0 -0
  65. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/requirements.dev.txt +0 -0
  66. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/requirements.txt +0 -0
  67. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/run-checks.sh +0 -0
  68. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/run_unit_tests.sh +0 -0
  69. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/setup.sh +0 -0
  70. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/__init__.py +0 -0
  71. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/aws_config.py +0 -0
  72. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/aws_lambda/event_info.py +0 -0
  73. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/aws_lambda/mock_context.py +0 -0
  74. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/boto3session.py +0 -0
  75. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cloudwatch/cloudwatch_connection.py +0 -0
  76. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cloudwatch/cloudwatch_connection_tracker.py +0 -0
  77. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cloudwatch/cloudwatch_log_connection.py +0 -0
  78. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cloudwatch/cloudwatch_logs.py +0 -0
  79. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cloudwatch/cloudwatch_query.py +0 -0
  80. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cognito/cognito_authorizer.py +0 -0
  81. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cognito/cognito_connection.py +0 -0
  82. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cognito/cognito_utility.py +0 -0
  83. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cognito/jwks_cache.py +0 -0
  84. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cognito/user.py +0 -0
  85. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/connection.py +0 -0
  86. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/connection_tracker.py +0 -0
  87. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_connection.py +0 -0
  88. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_importer.py +0 -0
  89. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_index.py +0 -0
  90. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_iservice.py +0 -0
  91. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_key.py +0 -0
  92. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_model_base_interfaces.py +0 -0
  93. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_reindexer.py +0 -0
  94. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.py +0 -0
  95. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.txt +0 -0
  96. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/readme.md +0 -0
  97. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/troubleshooting.md +0 -0
  98. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/ec2/ec2_connection.py +0 -0
  99. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/environment_services/__init__.py +0 -0
  100. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/environment_services/environment_loader.py +0 -0
  101. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/environment_services/environment_variables.py +0 -0
  102. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/erc/__init__.py +0 -0
  103. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/erc/ecr_connection.py +0 -0
  104. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/errors/custom_exceptions.py +0 -0
  105. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/http_status_codes.py +0 -0
  106. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/models/serializable_model.py +0 -0
  107. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/role_assumption_mixin.py +0 -0
  108. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/s3/s3.py +0 -0
  109. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/s3/s3_bucket.py +0 -0
  110. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/s3/s3_connection.py +0 -0
  111. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/s3/s3_event_data.py +0 -0
  112. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/s3/s3_object.py +0 -0
  113. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/securityhub/securityhub.py +0 -0
  114. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/securityhub/securityhub_connection.py +0 -0
  115. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/session_setup_mixin.py +0 -0
  116. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/ssm/connection.py +0 -0
  117. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/ssm/parameter_store/parameter_store.py +0 -0
  118. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/utilities/datetime_utility.py +0 -0
  119. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/utilities/dictionary_utility.py +0 -0
  120. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/utilities/file_operations.py +0 -0
  121. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/utilities/http_utility.py +0 -0
  122. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/utilities/logging_utility.py +0 -0
  123. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/utilities/numbers_utility.py +0 -0
  124. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/utilities/serialization_utility.py +0 -0
  125. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/utilities/string_utility.py +0 -0
  126. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/__init__.py +0 -0
  127. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/integration/cross_account_connection_test.py +0 -0
  128. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/integration/tenant.py +0 -0
  129. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/integration/tenant_services.py +0 -0
  130. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/aws_config_test.py +0 -0
  131. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/common/db_test_helpers.py +0 -0
  132. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/__init__.py +0 -0
  133. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/db_models/cms/base.py +0 -0
  134. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/db_models/cms/content_block.py +0 -0
  135. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/db_models/cms/page.py +0 -0
  136. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/db_models/cms/template.py +0 -0
  137. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/db_models/simple_model.py +0 -0
  138. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/db_models/task.py +0 -0
  139. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/db_models/user_model.py +0 -0
  140. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/db_models/user_required_fields_model.py +0 -0
  141. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/dynamodb_fail_if_exists_test.py +0 -0
  142. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/dynamodb_model_base_test.py +0 -0
  143. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/dynamodb_model_projections_test.py +0 -0
  144. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/dynamodb_model_serializtion_test.py +0 -0
  145. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/dynamodb_moto_sorting_test.py +0 -0
  146. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/dynamodb_primary_key_get_test.py +0 -0
  147. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/dynamodb_primary_key_sort_test.py +0 -0
  148. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/dynamodb_reindex_test.py +0 -0
  149. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/examples_test/__init__.py +0 -0
  150. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/examples_test/user_service_test.py +0 -0
  151. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/lambda_tests/__init__.py +0 -0
  152. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/lambda_tests/event_info_test.py +0 -0
  153. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/models_tests/__init__.py +0 -0
  154. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/models_tests/models/person.py +0 -0
  155. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/models_tests/models/user.py +0 -0
  156. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/models_tests/serializable_model_person_test.py +0 -0
  157. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/models_tests/serializable_model_user_test.py +0 -0
  158. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/models_tests/serializable_model_wide_test.py +0 -0
  159. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/parameter_store/__init__.py +0 -0
  160. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/parameter_store/parameter_store_test.py +0 -0
  161. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/s3/__init__.py +0 -0
  162. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/s3/files/test.txt +0 -0
  163. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/s3/s3_event_data_test.py +0 -0
  164. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/s3/s3_file_delete_test.py +0 -0
  165. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/s3/s3_file_upload_test.py +0 -0
  166. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/session_tests/test_boto3_session_manager.py +0 -0
  167. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/utilities/__init__.py +0 -0
  168. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/utilities/serialization_utility_test.py +0 -0
  169. {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/utilities/string_utility_test.py +0 -0
@@ -171,4 +171,5 @@ cython_debug/
171
171
 
172
172
  activate.sh
173
173
  .pypirc
174
- .DS_Store
174
+ .DS_Store
175
+ .prompts
@@ -0,0 +1,11 @@
1
+ rules:
2
+ - name: Defining Models
3
+ path: docs/defining-models.md
4
+ description: Model layer design patterns
5
+ - name: Design Patterns
6
+ path: docs/design-patterns.md
7
+ description: Design patterns for building scalable SaaS applications
8
+ - name: Designing Services
9
+ path: docs/designing-services.md
10
+ description: Service layer design patterns
11
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boto3_assist
3
- Version: 0.28.0
3
+ Version: 0.30.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
@@ -0,0 +1,237 @@
1
+ # Defining DynamoDB Models with boto3-assist
2
+
3
+ This guide explains how to create and define DynamoDB models using the `DynamoDBModelBase` class provided by `boto3-assist`. Following these patterns ensures consistency, scalability, and easy integration with the `boto3-assist` ecosystem.
4
+
5
+ ## 1. Introduction to DynamoDBModelBase
6
+
7
+ The `DynamoDBModelBase` is the foundation for all DynamoDB models in this framework. It provides a rich set of features out of the box, including:
8
+
9
+ - **Automatic Serialization**: Convert model instances to and from DynamoDB-compatible dictionaries.
10
+ - **Index Management**: A structured way to define primary keys and global secondary indexes (GSIs).
11
+ - **Data Mapping**: Easily map raw DynamoDB response data to your model instances.
12
+ - **Helper Utilities**: Access to utility functions for common tasks like timestamp conversion and UUID generation.
13
+
14
+ ## 2. Creating a Basic Model
15
+
16
+ All models should inherit from `DynamoDBModelBase`. Here is an example of a simple `Product` model:
17
+
18
+ ```python
19
+ import datetime
20
+ from typing import Optional
21
+ from boto3_assist.dynamodb.dynamodb_model_base import DynamoDBModelBase
22
+ from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
23
+
24
+ class Product(DynamoDBModelBase):
25
+ def __init__(
26
+ self,
27
+ id: Optional[str] = None,
28
+ name: Optional[str] = None,
29
+ price: float = 0.0,
30
+ description: Optional[str] = None,
31
+ sku: Optional[str] = None,
32
+ ):
33
+ super().__init__()
34
+
35
+ self.id: Optional[str] = id
36
+ self.name: Optional[str] = name
37
+ self.price: float = price
38
+ self.description: Optional[str] = description
39
+ self.sku: Optional[str] = sku
40
+
41
+ # Initialize the indexes
42
+ self._setup_indexes()
43
+
44
+ def _setup_indexes(self):
45
+ # Index definitions will go here
46
+ pass
47
+ ```
48
+
49
+ **Key Principles**:
50
+
51
+ - **Inheritance**: Your model must inherit from `DynamoDBModelBase`.
52
+ - **Constructor**: Define your model's attributes in the `__init__` method. Call `super().__init__()` at the beginning.
53
+ - **Index Setup**: Call a private method (e.g., `_setup_indexes()`) at the end of the constructor to define your keys and indexes.
54
+
55
+ ## 3. Setting Up Indexes
56
+
57
+ DynamoDB keys and indexes are defined within the `_setup_indexes` method using the `DynamoDBIndex` and `DynamoDBKey` classes.
58
+
59
+ ### Primary Key
60
+
61
+ Every model must have a primary key. The primary key is defined as a `DynamoDBIndex` and added to the model's `indexes` collection.
62
+
63
+ ```python
64
+ def _setup_indexes(self):
65
+ primary = DynamoDBIndex()
66
+ primary.name = "primary"
67
+ primary.partition_key.attribute_name = "pk"
68
+ primary.partition_key.value = lambda: DynamoDBKey.build_key(("product", self.id))
69
+ primary.sort_key.attribute_name = "sk"
70
+ primary.sort_key.value = lambda: DynamoDBKey.build_key(("product", self.id))
71
+ self.indexes.add_primary(primary)
72
+ ```
73
+
74
+ - `DynamoDBIndex()`: Creates a new index definition.
75
+ - `partition_key` and `sort_key`: Define the attributes and values for your keys.
76
+ - `attribute_name`: The name of the attribute in the DynamoDB table (e.g., `pk`, `sk`).
77
+ - `value`: A **lambda function** that dynamically generates the key value. Using a lambda is crucial for ensuring the key is generated with the most current attribute values.
78
+ - `DynamoDBKey.build_key()`: A helper to construct composite keys with a consistent separator.
79
+ - `self.indexes.add_primary()`: Registers the index as the primary key.
80
+
81
+ ### Global Secondary Indexes (GSIs)
82
+
83
+ You can add GSIs to support additional query patterns. GSIs are also defined as `DynamoDBIndex` objects and added using `add_secondary()`.
84
+
85
+ Here’s how to add a GSI to query all products by name:
86
+
87
+ ```python
88
+ # Inside _setup_indexes method
89
+
90
+ self.indexes.add_secondary(
91
+ DynamoDBIndex(
92
+ index_name="gsi0",
93
+ partition_key=DynamoDBKey(
94
+ attribute_name="gsi0_pk",
95
+ # Use a static value for the partition key to query all products
96
+ value=lambda: DynamoDBKey.build_key(("products", ""))
97
+ ),
98
+ sort_key=DynamoDBKey(
99
+ attribute_name="gsi0_sk",
100
+ value=lambda: DynamoDBKey.build_key(("name", self.name))
101
+ ),
102
+ )
103
+ )
104
+ ```
105
+
106
+ - `index_name`: The name of the GSI in your DynamoDB table (e.g., `gsi0`).
107
+ - `add_secondary()`: Registers the index as a GSI.
108
+
109
+ ### Advanced GSI Patterns
110
+
111
+ Your models can support more complex query patterns by combining static partition keys with simple or composite sort keys.
112
+
113
+ #### Querying All Items with a Composite Sort Key
114
+
115
+ This pattern is useful when you want to retrieve all items of a specific type and sort them by multiple fields. For example, to get all users sorted by `last_name` and then `first_name`.
116
+
117
+ ```python
118
+ # Inside _setup_indexes for a UserModel
119
+
120
+ # GSI to list all users, sorted by last name, then first name
121
+ gsi_by_lastname = DynamoDBIndex(
122
+ index_name="gsi2",
123
+ partition_key=DynamoDBKey(
124
+ attribute_name="gsi2_pk",
125
+ # Static partition key to group all users together
126
+ value=lambda: DynamoDBKey.build_key(("users", None))
127
+ ),
128
+ sort_key=DynamoDBKey(
129
+ attribute_name="gsi2_sk",
130
+ # Composite sort key
131
+ value=lambda: DynamoDBKey.build_key(
132
+ ("lastname", self.last_name), ("firstname", self.first_name)
133
+ )
134
+ )
135
+ )
136
+ self.indexes.add_secondary(gsi_by_lastname)
137
+ ```
138
+
139
+ **How it works**:
140
+ - **Static Partition Key**: `DynamoDBKey.build_key(("users", None))` generates a static partition key (e.g., `"users"`). This forces all user items into the same item collection within the GSI, allowing you to query all of them at once.
141
+ - **Composite Sort Key**: `DynamoDBKey.build_key(("lastname", self.last_name), ("firstname", self.first_name))` creates a sort key like `lastname#Smith#firstname#John`. This enables lexicographical sorting, first by last name and then by first name.
142
+
143
+
144
+ ### What the Generated Keys Look Like
145
+
146
+ It's helpful to visualize what these key definitions produce. Given a `Product` instance:
147
+
148
+ ```python
149
+ product = Product(id='abc-123', name='Mjolnir')
150
+ ```
151
+
152
+ When you serialize this model using `to_resource_dictionary()`, the `boto3-assist` framework will generate the following key attributes based on the `lambda` functions in your index setup:
153
+
154
+ - **`pk`**: `product#abc-123`
155
+ - **`sk`**: `product#abc-123`
156
+ - **`gsi0_pk`**: `products`
157
+ - **`gsi0_sk`**: `name#Mjolnir`
158
+
159
+ Your final item in DynamoDB would look something like this:
160
+
161
+ ```json
162
+ {
163
+ "pk": "product#abc-123",
164
+ "sk": "product#abc-123",
165
+ "gsi0_pk": "products",
166
+ "gsi0_sk": "name#Mjolnir",
167
+ "id": "abc-123",
168
+ "name": "Mjolnir",
169
+ "price": 0.0,
170
+ "description": null,
171
+ "sku": null
172
+ }
173
+ ```
174
+
175
+ This structure allows you to:
176
+ - Fetch the product directly using its `pk` and `sk`.
177
+ - Query all products on `gsi0` (where `gsi0_pk` is `"products"`) and sort them by name (`gsi0_sk`).
178
+
179
+ ## 4. Serialization and Deserialization
180
+
181
+ `DynamoDBModelBase` provides powerful methods for serialization (Python object to dictionary) and deserialization (dictionary to Python object).
182
+
183
+ ### Deserialization with `map()`
184
+
185
+ The `map()` method is the primary way to populate a model instance from a dictionary. It intelligently handles various DynamoDB response formats.
186
+
187
+ ```python
188
+ # Raw DynamoDB item
189
+ dynamodb_item = {
190
+ 'pk': {'S': 'product#123'},
191
+ 'sk': {'S': 'product#123'},
192
+ 'name': {'S': 'Mjolnir'},
193
+ 'price': {'N': '9999.99'}
194
+ }
195
+
196
+ # Create an empty model and map the data
197
+ product = Product().map(dynamodb_item)
198
+
199
+ print(product.name) # Output: Mjolnir
200
+ print(product.price) # Output: 9999.99
201
+ ```
202
+
203
+ As noted in a previous session, the `map()` method can handle:
204
+ - Full DynamoDB responses: `{'Item': {...}, 'ResponseMetadata': {...}}`
205
+ - Item-only responses: `{'Item': {...}}`
206
+ - Plain dictionaries.
207
+
208
+ ### Serialization
209
+
210
+ There are several methods to convert a model instance to a dictionary:
211
+
212
+ - `to_dictionary()`: Returns a dictionary of the model's attributes, excluding any index attributes. This is useful for general-purpose serialization.
213
+ - `to_resource_dictionary()`: Returns a dictionary suitable for the Boto3 DynamoDB **Resource** API, including index attributes.
214
+ - `to_client_dictionary()`: Returns a dictionary suitable for the Boto3 DynamoDB **Client** API, with values serialized into DynamoDB's type format (e.g., `{'S': 'value'}`).
215
+
216
+ ```python
217
+ product = Product(id='456', name='Stormbreaker', price=8500.0)
218
+
219
+ # Get a simple dictionary of attributes
220
+ plain_dict = product.to_dictionary()
221
+ # {'id': '456', 'name': 'Stormbreaker', 'price': 8500.0, ...}
222
+
223
+ # Get a dictionary for the DynamoDB Resource API
224
+ resource_dict = product.to_resource_dictionary()
225
+ # {'pk': 'product#456', 'sk': 'product#456', 'id': '456', ...}
226
+
227
+ # Get a dictionary for the DynamoDB Client API
228
+ client_dict = product.to_client_dictionary()
229
+ # {'pk': {'S': 'product#456'}, 'sk': {'S': 'product#456'}, ...}
230
+ ```
231
+
232
+ ## 5. Best Practices
233
+
234
+ - **Models as Data Transfer Objects (DTOs)**: Models should only contain data and serialization logic. Avoid adding business logic or direct database calls (e.g., a `save()` method). Keep that logic in your service layer.
235
+ - **Use Lambda for Keys**: Always use `lambda` functions for key values to ensure they are generated at the time of serialization.
236
+ - **Consistent Naming**: Follow consistent naming conventions for your indexes and attributes (e.g., `gsi0`, `gsi1_pk`, `gsi1_sk`).
237
+ - **Call `_setup_indexes` in `__init__`**: Ensure your indexes are defined every time a model is instantiated.
@@ -0,0 +1,150 @@
1
+ # Defining Services for DynamoDB Operations
2
+
3
+ This guide explains how to create a service layer to handle business logic and interact with DynamoDB using `boto3-assist`. The service layer is responsible for all Create, Read, Update, Delete, and List (CRUDL) operations.
4
+
5
+ ## 1. The Role of the Service Layer
6
+
7
+ The service layer acts as an intermediary between your application's handlers (e.g., API endpoints) and the database. Its primary responsibilities are:
8
+
9
+ - **Encapsulating Business Logic**: All logic related to data manipulation resides here.
10
+ - **Interacting with the Database**: Services are the only part of your application that should directly call the `DynamoDB` class.
11
+ - **Using Models**: Services use the `DynamoDBModelBase` models to pass data to and from the database.
12
+ - **Error Handling**: Managing database exceptions and returning consistent responses.
13
+
14
+ By centralizing database interactions in a service layer, you create a clear separation of concerns, making your application easier to maintain, test, and scale.
15
+
16
+ ## 2. Creating a Basic Service
17
+
18
+ A service is a Python class that initializes an instance of the `boto3_assist.dynamodb.DynamoDB` class. It also needs the name of the DynamoDB table it will be interacting with, which is typically stored in an environment variable.
19
+
20
+ Here is a basic `ProductService`:
21
+
22
+ ```python
23
+ import os
24
+ from typing import Optional
25
+ from boto3_assist.dynamodb.dynamodb import DynamoDB
26
+ from ..models.product_model import Product
27
+
28
+ class ProductService:
29
+ def __init__(self, db: Optional[DynamoDB] = None):
30
+ self.db = db or DynamoDB()
31
+ self.table_name = os.environ.get("APP_TABLE_NAME", "products-table")
32
+
33
+ # CRUDL methods will go here
34
+ ```
35
+
36
+ - **Initialization**: The constructor accepts an optional `DynamoDB` instance, which is useful for dependency injection during testing. If one isn't provided, it creates a new instance.
37
+ - **Table Name**: The service gets the table name from an environment variable, providing a sensible default.
38
+
39
+ ## 3. Implementing CRUDL Operations
40
+
41
+ Here’s how to implement the standard CRUDL operations in your service.
42
+
43
+ ### Create
44
+
45
+ The `create` method uses the `db.save()` method to persist a new item to the database. It takes a model instance, converts it to a dictionary, and saves it.
46
+
47
+ ```python
48
+ def create_product(self, product_data: dict) -> Product:
49
+ """Creates a new product."""
50
+ product = Product().map(product_data)
51
+
52
+ # The to_resource_dictionary() method includes the pk, sk, and any GSI keys
53
+ item_to_save = product.to_resource_dictionary()
54
+
55
+ self.db.save(item=item_to_save, table_name=self.table_name)
56
+
57
+ return product
58
+ ```
59
+
60
+ ### Read (Get by ID)
61
+
62
+ The `get` method retrieves a single item by its primary key. You create a model instance, set its ID, and pass it to the `db.get()` method.
63
+
64
+ ```python
65
+ def get_product_by_id(self, product_id: str) -> Optional[Product]:
66
+ """Retrieves a product by its ID."""
67
+ # Create a model with the ID to identify the key
68
+ model_to_find = Product(id=product_id)
69
+
70
+ response = self.db.get(model=model_to_find, table_name=self.table_name)
71
+
72
+ item = response.get("Item")
73
+ if not item:
74
+ return None
75
+
76
+ return Product().map(item)
77
+ ```
78
+
79
+ ### Update
80
+
81
+ Updates are typically handled using a "get-then-save" pattern. You first retrieve the existing item, map the updates to it, and then save it back to the database. This ensures you don't accidentally overwrite data.
82
+
83
+ ```python
84
+ def update_product(self, product_id: str, updates: dict) -> Optional[Product]:
85
+ """Updates an existing product."""
86
+ # 1. Get the existing product
87
+ existing_product = self.get_product_by_id(product_id)
88
+ if not existing_product:
89
+ return None
90
+
91
+ # 2. Map the updates to the model
92
+ existing_product.map(updates)
93
+
94
+ # 3. Save it back to the database
95
+ item_to_save = existing_product.to_resource_dictionary()
96
+ self.db.save(item=item_to_save, table_name=self.table_name)
97
+
98
+ return existing_product
99
+ ```
100
+
101
+ ### Delete
102
+
103
+ The `delete` method removes an item from the database using its primary key. Similar to the `get` method, you pass a model instance with the ID set.
104
+
105
+ ```python
106
+ def delete_product(self, product_id: str) -> bool:
107
+ """Deletes a product by its ID."""
108
+ product_to_delete = Product(id=product_id)
109
+
110
+ try:
111
+ self.db.delete(model=product_to_delete, table_name=self.table_name)
112
+ return True
113
+ except Exception as e:
114
+ # Log the error
115
+ print(f"Error deleting product {product_id}: {e}")
116
+ return False
117
+ ```
118
+
119
+ ### List (Query)
120
+
121
+ To list items, you typically query a Global Secondary Index (GSI). The `db.query()` method is used for this. You need to provide the index name and a `KeyConditionExpression`.
122
+
123
+ Here’s how to list all products, sorted by name, using the `gsi0` we defined in the model documentation:
124
+
125
+ ```python
126
+ from boto3.dynamodb.conditions import Key
127
+
128
+ def list_all_products(self):
129
+ """Lists all products, sorted by name."""
130
+ # This key queries for all items where the GSI partition key is 'products'.
131
+ # This is based on the GSI we defined in the Product model.
132
+ key_condition = Key('gsi0_pk').eq(Product().get_key('gsi0').partition_key.value())
133
+
134
+ response = self.db.query(
135
+ key=key_condition,
136
+ index_name="gsi0",
137
+ table_name=self.table_name,
138
+ ascending=True
139
+ )
140
+
141
+ items = response.get("Items", [])
142
+ return [Product().map(item) for item in items]
143
+ ```
144
+
145
+ ## 4. Best Practices
146
+
147
+ - **Dependency Injection**: Always allow the `DynamoDB` instance to be injected into your service's constructor. This is critical for testing.
148
+ - **Environment Variables**: Load sensitive information like table names from environment variables, not hardcoded strings.
149
+ - **Use Models**: Leverage your `DynamoDBModelBase` models for all data interactions. This ensures your keys are generated correctly and your data is properly serialized.
150
+ - **Separation of Concerns**: Keep business logic inside the service. API handlers should only be responsible for parsing requests and formatting responses.