boto3-assist 0.34.0__tar.gz → 0.35.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 (198) hide show
  1. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/.gitignore +2 -0
  2. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/PKG-INFO +1 -1
  3. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/pyproject.toml +1 -1
  4. boto3_assist-0.35.0/src/boto3_assist/sqs/__init__.py +12 -0
  5. boto3_assist-0.35.0/src/boto3_assist/sqs/sqs_connection.py +88 -0
  6. boto3_assist-0.35.0/src/boto3_assist/sqs/sqs_queue.py +307 -0
  7. boto3_assist-0.35.0/src/boto3_assist/version.py +1 -0
  8. boto3_assist-0.35.0/tests/unit/utilities/case_transformation/files/user_import.json +125 -0
  9. boto3_assist-0.35.0/tests/unit/utilities/case_transformation/test_case_transformation.py +129 -0
  10. boto3_assist-0.34.0/examples/dynamodb/RUNTIME_KEY_DEBUGGING_SUMMARY.md +0 -138
  11. boto3_assist-0.34.0/examples/dynamodb/runtime_key_debugging_example.py +0 -317
  12. boto3_assist-0.34.0/src/boto3_assist/version.py +0 -1
  13. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/.env.docker +0 -0
  14. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/.env.docker.001 +0 -0
  15. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/.env.docker.nosql.workbench +0 -0
  16. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/.env.unittest +0 -0
  17. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/.vscode/launch.json +0 -0
  18. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/.vscode/settings.json +0 -0
  19. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/.vscode/tasks.json +0 -0
  20. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/.windsurf/rules/cascade.yaml +0 -0
  21. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/LICENSE-EXPLAINED.txt +0 -0
  22. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/LICENSE.txt +0 -0
  23. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/README.md +0 -0
  24. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/aws_regions_with_status.csv +0 -0
  25. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/aws_regions_with_status.json +0 -0
  26. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/devops/build.py +0 -0
  27. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/devops/readme.md +0 -0
  28. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/design-patterns.md +0 -0
  29. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/001-guide-single-table-design.md +0 -0
  30. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/002-guide-defining-models.md +0 -0
  31. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/003-guide-service-layers.md +0 -0
  32. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/004-guide-testing-with-moto.md +0 -0
  33. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/005-guide-projections-and-reserved-keywords.md +0 -0
  34. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/006-guide-how-dynamodb-stores-data.md +0 -0
  35. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/007-guide-batch-operations.md +0 -0
  36. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/008-guide-transactions.md +0 -0
  37. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/009-guide-conditional-writes.md +0 -0
  38. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/010-guide-update-expressions.md +0 -0
  39. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/issues/BOTO3_ASSIST_BEFORE_AFTER.md +0 -0
  40. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/issues/BOTO3_ASSIST_DECIMAL_PATTERN.md +0 -0
  41. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/issues/BOTO3_ASSIST_IMPLEMENTATION_CHECKLIST.md +0 -0
  42. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/overview.md +0 -0
  43. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/roadmap.md +0 -0
  44. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/tech-debt.md +0 -0
  45. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/unit-test-patterns.md +0 -0
  46. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/__init__.py +0 -0
  47. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/cloudwatch/log_report.py +0 -0
  48. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/QUICK_REFERENCE_KEY_DEBUGGING.md +0 -0
  49. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/batch_operations_example.py +0 -0
  50. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/conditional_writes_example.py +0 -0
  51. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/debug_keys_example.py +0 -0
  52. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/decimal_conversion_example.py +0 -0
  53. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/models/order_item_model.py +0 -0
  54. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/models/order_model.py +0 -0
  55. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/models/product_model.py +0 -0
  56. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/models/user_model.py +0 -0
  57. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/models/user_post_model.py +0 -0
  58. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/order_example/main.py +0 -0
  59. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/order_example/products.json +0 -0
  60. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/services/order_item_service.py +0 -0
  61. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/services/order_service.py +0 -0
  62. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/services/product_service.py +0 -0
  63. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/services/table_service.py +0 -0
  64. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/services/user_post_service.py +0 -0
  65. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/services/user_service.py +0 -0
  66. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/services/user_service_client_example.py +0 -0
  67. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/services/user_service_resource_example.py +0 -0
  68. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/transactions_example.py +0 -0
  69. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/update_expressions_example.py +0 -0
  70. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/user_post_example/main.py +0 -0
  71. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/ec2/regions_report.py +0 -0
  72. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/module-headers.txt +0 -0
  73. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/mypy.ini +0 -0
  74. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/publish_to_pypi.py +0 -0
  75. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/publish_to_pypi.sh +0 -0
  76. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/pysetup.py +0 -0
  77. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/pysetup.sh +0 -0
  78. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/requirements.dev.txt +0 -0
  79. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/requirements.txt +0 -0
  80. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/run-checks.sh +0 -0
  81. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/run-unit-tests.sh +0 -0
  82. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/__init__.py +0 -0
  83. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/aws_config.py +0 -0
  84. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/aws_lambda/event_info.py +0 -0
  85. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/aws_lambda/mock_context.py +0 -0
  86. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/boto3session.py +0 -0
  87. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cloudwatch/cloudwatch_connection.py +0 -0
  88. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cloudwatch/cloudwatch_connection_tracker.py +0 -0
  89. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cloudwatch/cloudwatch_log_connection.py +0 -0
  90. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cloudwatch/cloudwatch_logs.py +0 -0
  91. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cloudwatch/cloudwatch_query.py +0 -0
  92. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cognito/cognito_authorizer.py +0 -0
  93. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cognito/cognito_connection.py +0 -0
  94. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cognito/cognito_utility.py +0 -0
  95. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cognito/jwks_cache.py +0 -0
  96. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cognito/user.py +0 -0
  97. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/connection.py +0 -0
  98. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/connection_tracker.py +0 -0
  99. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb.py +0 -0
  100. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_connection.py +0 -0
  101. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_helpers.py +0 -0
  102. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_importer.py +0 -0
  103. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_index.py +0 -0
  104. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_iservice.py +0 -0
  105. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_key.py +0 -0
  106. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_model_base.py +0 -0
  107. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_model_base_interfaces.py +0 -0
  108. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_re_indexer.py +0 -0
  109. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_reindexer.py +0 -0
  110. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.py +0 -0
  111. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.txt +0 -0
  112. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/readme.md +0 -0
  113. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/troubleshooting.md +0 -0
  114. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/ec2/ec2_connection.py +0 -0
  115. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/environment_services/__init__.py +0 -0
  116. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/environment_services/environment_loader.py +0 -0
  117. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/environment_services/environment_variables.py +0 -0
  118. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/erc/__init__.py +0 -0
  119. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/erc/ecr_connection.py +0 -0
  120. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/errors/custom_exceptions.py +0 -0
  121. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/http_status_codes.py +0 -0
  122. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/models/serializable_model.py +0 -0
  123. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/role_assumption_mixin.py +0 -0
  124. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/s3/s3.py +0 -0
  125. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/s3/s3_bucket.py +0 -0
  126. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/s3/s3_connection.py +0 -0
  127. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/s3/s3_event_data.py +0 -0
  128. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/s3/s3_object.py +0 -0
  129. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/securityhub/securityhub.py +0 -0
  130. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/securityhub/securityhub_connection.py +0 -0
  131. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/session_setup_mixin.py +0 -0
  132. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/ssm/connection.py +0 -0
  133. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/ssm/parameter_store/parameter_store.py +0 -0
  134. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/utilities/datetime_utility.py +0 -0
  135. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/utilities/decimal_conversion_utility.py +0 -0
  136. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/utilities/dictionary_utility.py +0 -0
  137. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/utilities/file_operations.py +0 -0
  138. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/utilities/http_utility.py +0 -0
  139. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/utilities/logging_utility.py +0 -0
  140. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/utilities/numbers_utility.py +0 -0
  141. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/utilities/serialization_utility.py +0 -0
  142. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/utilities/string_utility.py +0 -0
  143. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/__init__.py +0 -0
  144. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/integration/cross_account_connection_test.py +0 -0
  145. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/integration/tenant.py +0 -0
  146. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/integration/tenant_services.py +0 -0
  147. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/aws_config_test.py +0 -0
  148. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/common/db_test_helpers.py +0 -0
  149. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb/decimal_backward_compatibility_test.py +0 -0
  150. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb/decimal_conversion_integration_test.py +0 -0
  151. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb/test_dynamodb_key_to_dict.py +0 -0
  152. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/__init__.py +0 -0
  153. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/db_models/cms/base.py +0 -0
  154. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/db_models/cms/content_block.py +0 -0
  155. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/db_models/cms/page.py +0 -0
  156. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/db_models/cms/template.py +0 -0
  157. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/db_models/simple_model.py +0 -0
  158. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/db_models/task.py +0 -0
  159. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/db_models/user_model.py +0 -0
  160. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/db_models/user_required_fields_model.py +0 -0
  161. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_batch_operations_test.py +0 -0
  162. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_conditional_test.py +0 -0
  163. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_fail_if_exists_test.py +0 -0
  164. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_model_base_test.py +0 -0
  165. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_model_merge_test.py +0 -0
  166. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_model_projections_test.py +0 -0
  167. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_model_serializtion_test.py +0 -0
  168. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_moto_sorting_test.py +0 -0
  169. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_primary_key_get_test.py +0 -0
  170. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_primary_key_sort_test.py +0 -0
  171. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_query_test.py +0 -0
  172. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_reindex_test.py +0 -0
  173. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_transactions_test.py +0 -0
  174. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_update_expressions_test.py +0 -0
  175. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/examples_test/README.md +0 -0
  176. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/examples_test/__init__.py +0 -0
  177. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/examples_test/order_service_test.py +0 -0
  178. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/examples_test/user_service_test.py +0 -0
  179. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/lambda_tests/__init__.py +0 -0
  180. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/lambda_tests/event_info_test.py +0 -0
  181. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/models_tests/__init__.py +0 -0
  182. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/models_tests/models/person.py +0 -0
  183. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/models_tests/models/user.py +0 -0
  184. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/models_tests/serializable_model_person_test.py +0 -0
  185. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/models_tests/serializable_model_user_test.py +0 -0
  186. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/models_tests/serializable_model_wide_test.py +0 -0
  187. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/parameter_store/__init__.py +0 -0
  188. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/parameter_store/parameter_store_test.py +0 -0
  189. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/s3/__init__.py +0 -0
  190. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/s3/files/test.txt +0 -0
  191. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/s3/s3_event_data_test.py +0 -0
  192. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/s3/s3_file_delete_test.py +0 -0
  193. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/s3/s3_file_upload_test.py +0 -0
  194. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/session_tests/test_boto3_session_manager.py +0 -0
  195. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/utilities/__init__.py +0 -0
  196. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/utilities/decimal_conversion_utility_test.py +0 -0
  197. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/utilities/serialization_utility_test.py +0 -0
  198. {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/utilities/string_utility_test.py +0 -0
@@ -173,3 +173,5 @@ activate.sh
173
173
  .pypirc
174
174
  .DS_Store
175
175
  .prompts
176
+ .output
177
+ .outputs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boto3_assist
3
- Version: 0.34.0
3
+ Version: 0.35.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.34.0"
18
+ version = "0.35.0"
19
19
 
20
20
  authors = [
21
21
  { name="Eric Wilson", email="boto3-assist@geekcafe.com" }
@@ -0,0 +1,12 @@
1
+ """
2
+ SQS module for boto3-assist.
3
+
4
+ Geek Cafe, LLC
5
+ Maintainers: Eric Wilson
6
+ MIT License. See Project Root for the license information.
7
+ """
8
+
9
+ from boto3_assist.sqs.sqs_connection import SQSConnection
10
+ from boto3_assist.sqs.sqs_queue import SQSQueue
11
+
12
+ __all__ = ["SQSConnection", "SQSQueue"]
@@ -0,0 +1,88 @@
1
+ """
2
+ SQS Connection module.
3
+
4
+ Geek Cafe, LLC
5
+ Maintainers: Eric Wilson
6
+ MIT License. See Project Root for the license information.
7
+ """
8
+
9
+ from typing import Optional, TYPE_CHECKING
10
+
11
+ from aws_lambda_powertools import Logger
12
+
13
+ from boto3_assist.connection import Connection
14
+
15
+ if TYPE_CHECKING:
16
+ from mypy_boto3_sqs import SQSClient, SQSServiceResource
17
+ else:
18
+ SQSClient = object
19
+ SQSServiceResource = object
20
+
21
+
22
+ logger = Logger(child=True)
23
+
24
+
25
+ class SQSConnection(Connection):
26
+ """SQS Connection wrapper."""
27
+
28
+ def __init__(
29
+ self,
30
+ *,
31
+ aws_profile: Optional[str] = None,
32
+ aws_region: Optional[str] = None,
33
+ aws_end_point_url: Optional[str] = None,
34
+ aws_access_key_id: Optional[str] = None,
35
+ aws_secret_access_key: Optional[str] = None,
36
+ ) -> None:
37
+ """
38
+ Initialize SQS connection.
39
+
40
+ Args:
41
+ aws_profile: AWS profile name
42
+ aws_region: AWS region
43
+ aws_end_point_url: Custom endpoint URL (for LocalStack, etc.)
44
+ aws_access_key_id: AWS access key ID
45
+ aws_secret_access_key: AWS secret access key
46
+ """
47
+ super().__init__(
48
+ service_name="sqs",
49
+ aws_profile=aws_profile,
50
+ aws_region=aws_region,
51
+ aws_access_key_id=aws_access_key_id,
52
+ aws_secret_access_key=aws_secret_access_key,
53
+ aws_end_point_url=aws_end_point_url,
54
+ )
55
+
56
+ self.__client: SQSClient | None = None
57
+ self.__resource: SQSServiceResource | None = None
58
+
59
+ @property
60
+ def client(self) -> SQSClient:
61
+ """Get SQS client."""
62
+ if self.__client is None:
63
+ self.__client = self.session.client
64
+ return self.__client
65
+
66
+ @client.setter
67
+ def client(self, value: SQSClient) -> None:
68
+ """Set SQS client."""
69
+ logger.info("Setting SQS Client")
70
+ self.__client = value
71
+
72
+ @property
73
+ def resource(self) -> SQSServiceResource:
74
+ """Get SQS resource."""
75
+ if self.__resource is None:
76
+ logger.info("Creating SQS Resource")
77
+ self.__resource = self.session.resource
78
+
79
+ if self.raise_on_error and self.__resource is None:
80
+ raise RuntimeError("SQS Resource is not available")
81
+
82
+ return self.__resource
83
+
84
+ @resource.setter
85
+ def resource(self, value: SQSServiceResource) -> None:
86
+ """Set SQS resource."""
87
+ logger.info("Setting SQS Resource")
88
+ self.__resource = value
@@ -0,0 +1,307 @@
1
+ """
2
+ SQS Queue operations module.
3
+
4
+ Geek Cafe, LLC
5
+ Maintainers: Eric Wilson
6
+ MIT License. See Project Root for the license information.
7
+ """
8
+
9
+ import json
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from aws_lambda_powertools import Logger
13
+
14
+ from boto3_assist.sqs.sqs_connection import SQSConnection
15
+
16
+ logger = Logger(child=True)
17
+
18
+
19
+ class SQSQueue(SQSConnection):
20
+ """SQS Queue operations wrapper."""
21
+
22
+ def __init__(
23
+ self,
24
+ *,
25
+ aws_profile: Optional[str] = None,
26
+ aws_region: Optional[str] = None,
27
+ aws_end_point_url: Optional[str] = None,
28
+ aws_access_key_id: Optional[str] = None,
29
+ aws_secret_access_key: Optional[str] = None,
30
+ ) -> None:
31
+ """
32
+ Initialize SQS Queue.
33
+
34
+ Args:
35
+ aws_profile: AWS profile name
36
+ aws_region: AWS region
37
+ aws_end_point_url: Custom endpoint URL (for LocalStack, etc.)
38
+ aws_access_key_id: AWS access key ID
39
+ aws_secret_access_key: AWS secret access key
40
+ """
41
+ super().__init__(
42
+ aws_profile=aws_profile,
43
+ aws_region=aws_region,
44
+ aws_end_point_url=aws_end_point_url,
45
+ aws_access_key_id=aws_access_key_id,
46
+ aws_secret_access_key=aws_secret_access_key,
47
+ )
48
+
49
+ def send_message(
50
+ self,
51
+ queue_url: str,
52
+ message_body: str,
53
+ delay_seconds: int = 0,
54
+ message_attributes: Optional[Dict[str, Any]] = None,
55
+ message_group_id: Optional[str] = None,
56
+ message_deduplication_id: Optional[str] = None,
57
+ ) -> str:
58
+ """
59
+ Send a message to an SQS queue.
60
+
61
+ Args:
62
+ queue_url: The URL of the SQS queue
63
+ message_body: The message body (string)
64
+ delay_seconds: Delay before message becomes visible (0-900 seconds)
65
+ message_attributes: Optional message attributes
66
+ message_group_id: Required for FIFO queues
67
+ message_deduplication_id: Required for FIFO queues without content-based deduplication
68
+
69
+ Returns:
70
+ The message ID of the sent message
71
+ """
72
+ params: Dict[str, Any] = {
73
+ "QueueUrl": queue_url,
74
+ "MessageBody": message_body,
75
+ }
76
+
77
+ if delay_seconds > 0:
78
+ params["DelaySeconds"] = min(delay_seconds, 900) # SQS max is 900 seconds
79
+
80
+ if message_attributes:
81
+ params["MessageAttributes"] = message_attributes
82
+
83
+ if message_group_id:
84
+ params["MessageGroupId"] = message_group_id
85
+
86
+ if message_deduplication_id:
87
+ params["MessageDeduplicationId"] = message_deduplication_id
88
+
89
+ response = self.client.send_message(**params)
90
+ message_id = response.get("MessageId", "")
91
+
92
+ logger.debug(f"Sent message to {queue_url}: {message_id}")
93
+ return message_id
94
+
95
+ def send_message_batch(
96
+ self,
97
+ queue_url: str,
98
+ entries: List[Dict[str, Any]],
99
+ ) -> Dict[str, Any]:
100
+ """
101
+ Send multiple messages to an SQS queue in a batch.
102
+
103
+ Args:
104
+ queue_url: The URL of the SQS queue
105
+ entries: List of message entries, each with 'Id' and 'MessageBody'
106
+
107
+ Returns:
108
+ Response containing 'Successful' and 'Failed' lists
109
+ """
110
+ response = self.client.send_message_batch(
111
+ QueueUrl=queue_url,
112
+ Entries=entries,
113
+ )
114
+
115
+ successful = response.get("Successful", [])
116
+ failed = response.get("Failed", [])
117
+
118
+ if failed:
119
+ logger.warning(f"Failed to send {len(failed)} messages to {queue_url}")
120
+
121
+ logger.debug(f"Sent batch of {len(successful)} messages to {queue_url}")
122
+ return response
123
+
124
+ def receive_messages(
125
+ self,
126
+ queue_url: str,
127
+ max_number_of_messages: int = 1,
128
+ wait_time_seconds: int = 0,
129
+ visibility_timeout: Optional[int] = None,
130
+ message_attribute_names: Optional[List[str]] = None,
131
+ ) -> List[Dict[str, Any]]:
132
+ """
133
+ Receive messages from an SQS queue.
134
+
135
+ Args:
136
+ queue_url: The URL of the SQS queue
137
+ max_number_of_messages: Maximum number of messages to receive (1-10)
138
+ wait_time_seconds: Long polling wait time (0-20 seconds)
139
+ visibility_timeout: Override the queue's default visibility timeout
140
+ message_attribute_names: List of message attribute names to retrieve
141
+
142
+ Returns:
143
+ List of messages
144
+ """
145
+ params: Dict[str, Any] = {
146
+ "QueueUrl": queue_url,
147
+ "MaxNumberOfMessages": min(max_number_of_messages, 10),
148
+ "WaitTimeSeconds": min(wait_time_seconds, 20),
149
+ }
150
+
151
+ if visibility_timeout is not None:
152
+ params["VisibilityTimeout"] = visibility_timeout
153
+
154
+ if message_attribute_names:
155
+ params["MessageAttributeNames"] = message_attribute_names
156
+
157
+ response = self.client.receive_message(**params)
158
+ messages = response.get("Messages", [])
159
+
160
+ logger.debug(f"Received {len(messages)} messages from {queue_url}")
161
+ return messages
162
+
163
+ def delete_message(
164
+ self,
165
+ queue_url: str,
166
+ receipt_handle: str,
167
+ ) -> None:
168
+ """
169
+ Delete a message from an SQS queue.
170
+
171
+ Args:
172
+ queue_url: The URL of the SQS queue
173
+ receipt_handle: The receipt handle of the message to delete
174
+ """
175
+ self.client.delete_message(
176
+ QueueUrl=queue_url,
177
+ ReceiptHandle=receipt_handle,
178
+ )
179
+ logger.debug(f"Deleted message from {queue_url}")
180
+
181
+ def delete_message_batch(
182
+ self,
183
+ queue_url: str,
184
+ entries: List[Dict[str, str]],
185
+ ) -> Dict[str, Any]:
186
+ """
187
+ Delete multiple messages from an SQS queue in a batch.
188
+
189
+ Args:
190
+ queue_url: The URL of the SQS queue
191
+ entries: List of entries with 'Id' and 'ReceiptHandle'
192
+
193
+ Returns:
194
+ Response containing 'Successful' and 'Failed' lists
195
+ """
196
+ response = self.client.delete_message_batch(
197
+ QueueUrl=queue_url,
198
+ Entries=entries,
199
+ )
200
+
201
+ logger.debug(f"Deleted batch of messages from {queue_url}")
202
+ return response
203
+
204
+ def get_queue_url(self, queue_name: str) -> str:
205
+ """
206
+ Get the URL of an SQS queue by name.
207
+
208
+ Args:
209
+ queue_name: The name of the queue
210
+
211
+ Returns:
212
+ The queue URL
213
+ """
214
+ response = self.client.get_queue_url(QueueName=queue_name)
215
+ return response["QueueUrl"]
216
+
217
+ def get_queue_attributes(
218
+ self,
219
+ queue_url: str,
220
+ attribute_names: Optional[List[str]] = None,
221
+ ) -> Dict[str, str]:
222
+ """
223
+ Get attributes of an SQS queue.
224
+
225
+ Args:
226
+ queue_url: The URL of the SQS queue
227
+ attribute_names: List of attribute names to retrieve (default: All)
228
+
229
+ Returns:
230
+ Dictionary of queue attributes
231
+ """
232
+ params: Dict[str, Any] = {"QueueUrl": queue_url}
233
+
234
+ if attribute_names:
235
+ params["AttributeNames"] = attribute_names
236
+ else:
237
+ params["AttributeNames"] = ["All"]
238
+
239
+ response = self.client.get_queue_attributes(**params)
240
+ return response.get("Attributes", {})
241
+
242
+ def purge_queue(self, queue_url: str) -> None:
243
+ """
244
+ Purge all messages from an SQS queue.
245
+
246
+ Args:
247
+ queue_url: The URL of the SQS queue
248
+
249
+ Note: This action can only be performed once every 60 seconds.
250
+ """
251
+ self.client.purge_queue(QueueUrl=queue_url)
252
+ logger.info(f"Purged queue: {queue_url}")
253
+
254
+ def change_message_visibility(
255
+ self,
256
+ queue_url: str,
257
+ receipt_handle: str,
258
+ visibility_timeout: int,
259
+ ) -> None:
260
+ """
261
+ Change the visibility timeout of a message.
262
+
263
+ Args:
264
+ queue_url: The URL of the SQS queue
265
+ receipt_handle: The receipt handle of the message
266
+ visibility_timeout: New visibility timeout in seconds (0-43200)
267
+ """
268
+ self.client.change_message_visibility(
269
+ QueueUrl=queue_url,
270
+ ReceiptHandle=receipt_handle,
271
+ VisibilityTimeout=visibility_timeout,
272
+ )
273
+ logger.debug(f"Changed visibility timeout for message in {queue_url}")
274
+
275
+ def send_json_message(
276
+ self,
277
+ queue_url: str,
278
+ message: Dict[str, Any],
279
+ delay_seconds: int = 0,
280
+ message_attributes: Optional[Dict[str, Any]] = None,
281
+ message_group_id: Optional[str] = None,
282
+ message_deduplication_id: Optional[str] = None,
283
+ ) -> str:
284
+ """
285
+ Send a JSON message to an SQS queue.
286
+
287
+ Convenience method that serializes a dict to JSON.
288
+
289
+ Args:
290
+ queue_url: The URL of the SQS queue
291
+ message: Dictionary to serialize and send
292
+ delay_seconds: Delay before message becomes visible
293
+ message_attributes: Optional message attributes
294
+ message_group_id: Required for FIFO queues
295
+ message_deduplication_id: Required for FIFO queues
296
+
297
+ Returns:
298
+ The message ID of the sent message
299
+ """
300
+ return self.send_message(
301
+ queue_url=queue_url,
302
+ message_body=json.dumps(message),
303
+ delay_seconds=delay_seconds,
304
+ message_attributes=message_attributes,
305
+ message_group_id=message_group_id,
306
+ message_deduplication_id=message_deduplication_id,
307
+ )
@@ -0,0 +1 @@
1
+ __version__ = "0.35.0"
@@ -0,0 +1,125 @@
1
+ [
2
+ {
3
+ "id": "1765833213-933783:e9fef866-d9fa-11f0-8000-000000000000",
4
+ "created_by_id": "user_123",
5
+ "updated_by_id": "user_123",
6
+ "deleted_by_id": null,
7
+ "version": 1.0,
8
+ "tenant_id": "tenant_123",
9
+ "uploaded_by_id": null,
10
+ "bucket": "test-destination-bucket",
11
+ "category": "user_import",
12
+ "checksum": null,
13
+ "content_type": "text/csv",
14
+ "created_utc": "2025-12-15T21:13:33.933805+00:00",
15
+ "created_utc_ts": 1765833213.933805,
16
+ "deleted_utc": null,
17
+ "deleted_utc_ts": null,
18
+ "description": null,
19
+ "directory_id": null,
20
+ "errors": null,
21
+ "extension": ".csv",
22
+ "is_hidden": false,
23
+ "key": "tenants/tenant_123/users/user_123/files/1765833213-933783:e9fef866-d9fa-11f0-8000-000000000000/path/input.csv",
24
+ "lineage": "original",
25
+ "metadata": {
26
+ "converter": {
27
+ "version": "0.14.0",
28
+ "timestamp": "2025-12-15T16:13:34.362980",
29
+ "name": "input-file-converter"
30
+ },
31
+ "user_import": "conversion-required",
32
+ "converted_status": "completed",
33
+ "converted_file_id": "39e2c7e9-61c8-4d8b-81bc-b8d717cc99c7",
34
+ "converted_file_name": "input-csv-converted.csv",
35
+ "converted_reason": "cp1252"
36
+ },
37
+ "mime_type": "text/csv",
38
+ "model_name": "file",
39
+ "model_name_plural": "files",
40
+ "model_version": "1.0.0",
41
+ "modified_utc": "2025-12-15T21:13:34.365121+00:00",
42
+ "modified_utc_ts": 1765833214.365121,
43
+ "name": "input.csv",
44
+ "owner_id": "user_123",
45
+ "parent_id": null,
46
+ "retention_policy": "permanent",
47
+ "root_id": "1765833213-933783:e9fef866-d9fa-11f0-8000-000000000000",
48
+ "size": 0,
49
+ "state": "ready",
50
+ "status": "active",
51
+ "table_name": null,
52
+ "uploaded_utc": null,
53
+ "uploaded_utc_ts": null,
54
+ "user_id": "user_123",
55
+ "version_id": "1765833213-933783:e9fef866-d9fa-11f0-8000-000000000000",
56
+ "version_ts_utc": 1765833213.933805,
57
+ "virtual_path": "/input.csv",
58
+ "visibility": "private"
59
+ },
60
+ {
61
+ "id": "39e2c7e9-61c8-4d8b-81bc-b8d717cc99c7",
62
+ "created_by_id": "user_123",
63
+ "updated_by_id": "user_123",
64
+ "deleted_by_id": null,
65
+ "version": 1.0,
66
+ "tenant_id": "tenant_123",
67
+ "uploaded_by_id": null,
68
+ "bucket": "test-destination-bucket",
69
+ "category": "user_import",
70
+ "checksum": null,
71
+ "content_type": null,
72
+ "created_utc": "2025-12-15T21:13:34.344200+00:00",
73
+ "created_utc_ts": 1765833214.3442,
74
+ "deleted_utc": null,
75
+ "deleted_utc_ts": null,
76
+ "description": null,
77
+ "directory_id": null,
78
+ "errors": null,
79
+ "extension": ".csv",
80
+ "is_hidden": false,
81
+ "key": "tenants/tenant_123/users/user_123/files/39e2c7e9-61c8-4d8b-81bc-b8d717cc99c7/path/input-csv-converted.csv",
82
+ "lineage": "derived",
83
+ "metadata": {
84
+ "transformation": {
85
+ "type": "converted",
86
+ "operation": "cp1252_to_csv"
87
+ },
88
+ "user_import": "approved",
89
+ "column_names": [
90
+ "id",
91
+ "time",
92
+ "first_name",
93
+ "last_name",
94
+ "email"
95
+ ],
96
+ "converter": {
97
+ "version": "0.14.0",
98
+ "timestamp": "2025-12-15T16:13:34.342777",
99
+ "name": "analysis-input-file-converter"
100
+ }
101
+ },
102
+ "mime_type": null,
103
+ "model_name": "file",
104
+ "model_name_plural": "files",
105
+ "model_version": "3.0.0",
106
+ "modified_utc": "2025-12-15T21:13:34.344220+00:00",
107
+ "modified_utc_ts": 1765833214.34422,
108
+ "name": "input-csv-converted.csv",
109
+ "owner_id": "user_123",
110
+ "parent_id": "1765833213-933783:e9fef866-d9fa-11f0-8000-000000000000",
111
+ "retention_policy": "permanent",
112
+ "root_id": "1765833213-933783:e9fef866-d9fa-11f0-8000-000000000000",
113
+ "size": 0,
114
+ "state": "ready",
115
+ "status": "active",
116
+ "table_name": null,
117
+ "uploaded_utc": null,
118
+ "uploaded_utc_ts": null,
119
+ "user_id": "user_123",
120
+ "version_id": "39e2c7e9-61c8-4d8b-81bc-b8d717cc99c7",
121
+ "version_ts_utc": 1765833214.3442,
122
+ "virtual_path": "/input-csv-converted.csv",
123
+ "visibility": "private"
124
+ }
125
+ ]
@@ -0,0 +1,129 @@
1
+ """Unit tests for case transformation utilities."""
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ from boto3_assist.utilities.string_utility import StringUtility
10
+
11
+
12
+ class TestCaseTransformation:
13
+ """Tests for snake_case to camelCase conversion."""
14
+
15
+ @pytest.fixture
16
+ def files_dir(self) -> Path:
17
+ """Return the path to the files subdirectory."""
18
+ return Path(__file__).parent / "files"
19
+
20
+ @pytest.fixture
21
+ def output_dir(self) -> Path:
22
+ """Return the path to the .output directory, creating it if needed."""
23
+ output_path = Path(__file__).parent / ".output"
24
+ output_path.mkdir(exist_ok=True)
25
+ return output_path
26
+
27
+ def convert_keys_to_camel_case(self, obj):
28
+ """Recursively convert all dictionary keys from snake_case to camelCase."""
29
+ if isinstance(obj, dict):
30
+ return {
31
+ StringUtility.snake_to_camel(key): self.convert_keys_to_camel_case(
32
+ value
33
+ )
34
+ for key, value in obj.items()
35
+ }
36
+ elif isinstance(obj, list):
37
+ return [self.convert_keys_to_camel_case(item) for item in obj]
38
+ else:
39
+ return obj
40
+
41
+ def test_snake_to_camel_basic(self):
42
+ """Test basic snake_case to camelCase conversion."""
43
+ assert StringUtility.snake_to_camel("hello_world") == "helloWorld"
44
+ assert StringUtility.snake_to_camel("created_by_id") == "createdById"
45
+ assert StringUtility.snake_to_camel("tenant_id") == "tenantId"
46
+ assert StringUtility.snake_to_camel("single") == "single"
47
+
48
+ def test_convert_files_to_camel_case(self, files_dir: Path, output_dir: Path):
49
+ """
50
+ Loop through all JSON files in the files subdirectory,
51
+ convert keys from snake_case to camelCase, and write output
52
+ to the .output directory for visual inspection.
53
+ """
54
+ json_files = list(files_dir.glob("*.json"))
55
+ assert len(json_files) > 0, "No JSON files found in files directory"
56
+
57
+ for json_file in json_files:
58
+ # Read the input file
59
+ with open(json_file, "r", encoding="utf-8") as f:
60
+ data = json.load(f)
61
+
62
+ # Convert all keys from snake_case to camelCase
63
+ converted_data = self.convert_keys_to_camel_case(data)
64
+
65
+ # Write to output directory with same filename
66
+ output_file = output_dir / json_file.name
67
+ with open(output_file, "w", encoding="utf-8") as f:
68
+ json.dump(converted_data, f, indent=4)
69
+
70
+ # Verify the output file was created
71
+ assert output_file.exists(), f"Output file {output_file} was not created"
72
+
73
+ # Verify some key conversions occurred
74
+ if isinstance(converted_data, list) and len(converted_data) > 0:
75
+ first_item = converted_data[0]
76
+ # Check that snake_case keys were converted
77
+ assert (
78
+ "createdById" in first_item
79
+ ), "Expected 'createdById' key after conversion"
80
+ assert (
81
+ "tenantId" in first_item
82
+ ), "Expected 'tenantId' key after conversion"
83
+ assert (
84
+ "created_by_id" not in first_item
85
+ ), "Key 'created_by_id' should have been converted"
86
+
87
+ print(f"Converted {json_file.name} -> {output_file}")
88
+
89
+ def test_nested_conversion(self, files_dir: Path, output_dir: Path):
90
+ """Test that nested dictionaries are also converted."""
91
+ json_files = list(files_dir.glob("*.json"))
92
+
93
+ for json_file in json_files:
94
+ with open(json_file, "r", encoding="utf-8") as f:
95
+ data = json.load(f)
96
+
97
+ converted_data = self.convert_keys_to_camel_case(data)
98
+
99
+ # Check nested metadata conversion
100
+ if isinstance(converted_data, list):
101
+ for item in converted_data:
102
+ if "metadata" in item and isinstance(item["metadata"], dict):
103
+ metadata = item["metadata"]
104
+ # Verify nested keys are converted
105
+ if "converter" in metadata:
106
+ converter = metadata["converter"]
107
+ # Keys inside nested dicts should also be camelCase
108
+ assert "version" in converter or "timestamp" in converter
109
+ if "columnNames" in metadata:
110
+ # This was column_names in original
111
+ assert isinstance(metadata["columnNames"], list)
112
+
113
+ def test_all_files_processed(self, files_dir: Path, output_dir: Path):
114
+ """Verify all files in the files directory are processed."""
115
+ json_files = list(files_dir.glob("*.json"))
116
+
117
+ for json_file in json_files:
118
+ with open(json_file, "r", encoding="utf-8") as f:
119
+ data = json.load(f)
120
+
121
+ converted_data = self.convert_keys_to_camel_case(data)
122
+ output_file = output_dir / json_file.name
123
+
124
+ with open(output_file, "w", encoding="utf-8") as f:
125
+ json.dump(converted_data, f, indent=4)
126
+
127
+ # Verify output count matches input count
128
+ output_files = list(output_dir.glob("*.json"))
129
+ assert len(output_files) >= len(json_files), "Not all files were converted"