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.
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/.gitignore +2 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/PKG-INFO +1 -1
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/pyproject.toml +1 -1
- boto3_assist-0.35.0/src/boto3_assist/sqs/__init__.py +12 -0
- boto3_assist-0.35.0/src/boto3_assist/sqs/sqs_connection.py +88 -0
- boto3_assist-0.35.0/src/boto3_assist/sqs/sqs_queue.py +307 -0
- boto3_assist-0.35.0/src/boto3_assist/version.py +1 -0
- boto3_assist-0.35.0/tests/unit/utilities/case_transformation/files/user_import.json +125 -0
- boto3_assist-0.35.0/tests/unit/utilities/case_transformation/test_case_transformation.py +129 -0
- boto3_assist-0.34.0/examples/dynamodb/RUNTIME_KEY_DEBUGGING_SUMMARY.md +0 -138
- boto3_assist-0.34.0/examples/dynamodb/runtime_key_debugging_example.py +0 -317
- boto3_assist-0.34.0/src/boto3_assist/version.py +0 -1
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/.env.docker +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/.env.docker.001 +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/.env.docker.nosql.workbench +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/.env.unittest +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/.vscode/launch.json +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/.vscode/settings.json +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/.vscode/tasks.json +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/.windsurf/rules/cascade.yaml +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/LICENSE-EXPLAINED.txt +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/LICENSE.txt +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/README.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/aws_regions_with_status.csv +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/aws_regions_with_status.json +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/devops/build.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/devops/readme.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/design-patterns.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/001-guide-single-table-design.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/002-guide-defining-models.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/003-guide-service-layers.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/004-guide-testing-with-moto.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/005-guide-projections-and-reserved-keywords.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/006-guide-how-dynamodb-stores-data.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/007-guide-batch-operations.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/008-guide-transactions.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/009-guide-conditional-writes.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/help/dynamodb/010-guide-update-expressions.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/issues/BOTO3_ASSIST_BEFORE_AFTER.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/issues/BOTO3_ASSIST_DECIMAL_PATTERN.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/issues/BOTO3_ASSIST_IMPLEMENTATION_CHECKLIST.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/overview.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/roadmap.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/tech-debt.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/docs/unit-test-patterns.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/__init__.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/cloudwatch/log_report.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/QUICK_REFERENCE_KEY_DEBUGGING.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/batch_operations_example.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/conditional_writes_example.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/debug_keys_example.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/decimal_conversion_example.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/models/order_item_model.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/models/order_model.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/models/product_model.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/models/user_model.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/models/user_post_model.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/order_example/main.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/order_example/products.json +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/services/order_item_service.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/services/order_service.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/services/product_service.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/services/table_service.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/services/user_post_service.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/services/user_service.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/services/user_service_client_example.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/services/user_service_resource_example.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/transactions_example.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/update_expressions_example.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/dynamodb/user_post_example/main.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/examples/ec2/regions_report.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/module-headers.txt +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/mypy.ini +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/publish_to_pypi.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/publish_to_pypi.sh +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/pysetup.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/pysetup.sh +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/requirements.dev.txt +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/requirements.txt +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/run-checks.sh +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/run-unit-tests.sh +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/__init__.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/aws_config.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/aws_lambda/event_info.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/aws_lambda/mock_context.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/boto3session.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cloudwatch/cloudwatch_connection.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cloudwatch/cloudwatch_connection_tracker.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cloudwatch/cloudwatch_log_connection.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cloudwatch/cloudwatch_logs.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cloudwatch/cloudwatch_query.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cognito/cognito_authorizer.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cognito/cognito_connection.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cognito/cognito_utility.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cognito/jwks_cache.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/cognito/user.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/connection.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/connection_tracker.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_connection.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_helpers.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_importer.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_index.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_iservice.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_key.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_model_base.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_model_base_interfaces.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_re_indexer.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_reindexer.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.txt +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/readme.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/dynamodb/troubleshooting.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/ec2/ec2_connection.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/environment_services/__init__.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/environment_services/environment_loader.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/environment_services/environment_variables.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/erc/__init__.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/erc/ecr_connection.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/errors/custom_exceptions.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/http_status_codes.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/models/serializable_model.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/role_assumption_mixin.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/s3/s3.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/s3/s3_bucket.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/s3/s3_connection.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/s3/s3_event_data.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/s3/s3_object.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/securityhub/securityhub.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/securityhub/securityhub_connection.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/session_setup_mixin.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/ssm/connection.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/ssm/parameter_store/parameter_store.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/utilities/datetime_utility.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/utilities/decimal_conversion_utility.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/utilities/dictionary_utility.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/utilities/file_operations.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/utilities/http_utility.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/utilities/logging_utility.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/utilities/numbers_utility.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/utilities/serialization_utility.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/src/boto3_assist/utilities/string_utility.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/__init__.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/integration/cross_account_connection_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/integration/tenant.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/integration/tenant_services.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/aws_config_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/common/db_test_helpers.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb/decimal_backward_compatibility_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb/decimal_conversion_integration_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb/test_dynamodb_key_to_dict.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/__init__.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/db_models/cms/base.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/db_models/cms/content_block.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/db_models/cms/page.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/db_models/cms/template.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/db_models/simple_model.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/db_models/task.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/db_models/user_model.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/db_models/user_required_fields_model.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_batch_operations_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_conditional_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_fail_if_exists_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_model_base_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_model_merge_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_model_projections_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_model_serializtion_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_moto_sorting_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_primary_key_get_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_primary_key_sort_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_query_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_reindex_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_transactions_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/dynamodb_tests/dynamodb_update_expressions_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/examples_test/README.md +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/examples_test/__init__.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/examples_test/order_service_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/examples_test/user_service_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/lambda_tests/__init__.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/lambda_tests/event_info_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/models_tests/__init__.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/models_tests/models/person.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/models_tests/models/user.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/models_tests/serializable_model_person_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/models_tests/serializable_model_user_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/models_tests/serializable_model_wide_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/parameter_store/__init__.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/parameter_store/parameter_store_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/s3/__init__.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/s3/files/test.txt +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/s3/s3_event_data_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/s3/s3_file_delete_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/s3/s3_file_upload_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/session_tests/test_boto3_session_manager.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/utilities/__init__.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/utilities/decimal_conversion_utility_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/utilities/serialization_utility_test.py +0 -0
- {boto3_assist-0.34.0 → boto3_assist-0.35.0}/tests/unit/utilities/string_utility_test.py +0 -0
|
@@ -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"
|