modmex-lambda 0.5.5__tar.gz → 0.5.7__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.
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/PKG-INFO +1 -1
- modmex_lambda-0.5.7/modmex_lambda/persistence/dynamodb/keys.py +212 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/persistence/dynamodb/materialized_views.py +1 -1
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/pyproject.toml +1 -1
- modmex_lambda-0.5.7/tests/persistence/test_dynamodb_keys.py +203 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/persistence/test_dynamodb_materialized_views.py +2 -2
- modmex_lambda-0.5.5/modmex_lambda/persistence/dynamodb/keys.py +0 -133
- modmex_lambda-0.5.5/tests/persistence/test_dynamodb_keys.py +0 -115
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/.github/workflows/ci.yml +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/.github/workflows/release.yml +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/.gitignore +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/LICENSE +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/README.md +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/connectors/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/connectors/cloudwatch.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/connectors/dynamodb.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/connectors/eventbridge.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/connectors/icloudwatch.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/connectors/idynamodb.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/connectors/ieventbridge.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/connectors/ilambda.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/connectors/is3.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/connectors/isns.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/connectors/isqs.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/connectors/lambda_.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/connectors/module.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/connectors/s3.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/connectors/sns.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/connectors/sqs.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/data_classes/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/data_classes/api_gateway_authorizer_event.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/data_classes/api_gateway_proxy_event.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/data_classes/api_gateway_websocket_event.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/data_classes/cognito_user_pool_event.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/data_classes/common.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/dependencies.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/api_gateway.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/constants.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/content_types.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/cors.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/dependencies/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/dependencies/compat.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/dependencies/dependant.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/dependencies/dependency_middleware.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/dependencies/depends.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/dependencies/params.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/dependencies/types.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/exception_handler.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/exceptions.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/gateway_response.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/middlewares.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/params.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/request.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/response.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/routing.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/routing_fallbacks.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_handler/types.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/event_sources.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/exceptions.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/logging.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/params.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/parser.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/persistence/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/persistence/dynamodb/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/persistence/dynamodb/expressions.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/persistence/dynamodb/stream_fields.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/request.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/resolver.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/response.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/routing.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/shared/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/shared/cookies.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/shared/headers_serializer.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/shared/json_encoder.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/shared/types.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/events/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/events/dynamodb.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/events/kinesis.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/events/s3.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/events/sns.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/events/sqs.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/filters/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/filters/content.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/filters/event_type.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/filters/latch.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/filters/skip.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/flavors/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/flavors/base_flavor.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/flavors/cdc.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/flavors/collect.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/flavors/correlate.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/flavors/evaluate.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/flavors/expired.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/flavors/iflavor.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/flavors/job.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/flavors/materialize.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/flavors/s3.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/flavors/sns.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/flavors/task.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/flavors/update.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/irules_registry.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/operators/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/operators/cloudwatch.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/operators/dynamodb.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/operators/ioperator.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/operators/lambda_.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/operators/publisher.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/operators/s3.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/operators/sns.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/operators/sqs.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/rules_registry.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/runner.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/sources/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/sources/base.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/sources/dynamodb.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/sources/kinesis.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/sources/s3.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/sources/sns.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/sources/sqs.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/apigateway.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/aws.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/batch.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/cloudwatch.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/concurrency.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/contracts.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/data_classes/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/data_classes/dynamodb.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/decorators.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/dynamodb.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/eventbridge.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/faults.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/filters.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/json_encoder.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/lambda_.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/operators.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/pluralize.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/print.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/retry.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/s3.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/sns.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/split.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/sqs.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/tags.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/time.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/stream/utils/uow.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/tracing.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/validation.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/poetry.lock +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/conftest.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/connectors/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/connectors/conftest.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/connectors/test_cloudwatch.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/connectors/test_dynamodb.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/connectors/test_lazy_clients.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/connectors/test_s3.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/connectors/test_simple_connectors.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/connectors/test_sns.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/connectors/test_sqs.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/data_classes/test_api_gateway_proxy_event.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/data_classes/test_cognito_user_pool_event.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/data_classes/test_common.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/event_handler/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/event_handler/test_api_gateway.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/event_handler/test_cors.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/event_handler/test_dependencies.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/event_handler/test_exception_handler.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/event_handler/test_gateway_response.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/event_handler/test_request.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/event_handler/test_response.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/event_handler/test_routing.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/persistence/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/persistence/test_dynamodb_expressions.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/persistence/test_dynamodb_stream_fields.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/shared/test_cookies_headers.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/shared/test_json_encoder.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/conftest.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/events/test_dynamodb.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/events/test_kinesis.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/events/test_s3.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/events/test_sns.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/events/test_sqs.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/filters/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/filters/test_content.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/filters/test_event_type.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/filters/test_latch.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/filters/test_skip.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/flavors/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/flavors/source_events.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/flavors/test_base_flavor.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/flavors/test_cdc.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/flavors/test_collect.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/flavors/test_correlate.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/flavors/test_evaluate.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/flavors/test_expired.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/flavors/test_job.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/flavors/test_materialize.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/flavors/test_s3.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/flavors/test_sns.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/flavors/test_task.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/flavors/test_update.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/test_dependency_resolver.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/test_rules_registry.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/test_runner.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/test_runner_pipeline.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/test_sources.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/__init__.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/faults.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_apigateway.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_aws.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_batch.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_cloudwatch.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_concurrency.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_decorators.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_dynamodb.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_eventbridge.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_filters.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_json_encoder.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_lambda.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_operators.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_pluralize.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_print.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_retry.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_s3.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_sns.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_split.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_sqs.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_tags.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_time.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/stream/utils/test_uow.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/test_lazy_imports.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/test_logging.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/test_parser_event_sources.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/test_reexports.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/test_tracing.py +0 -0
- {modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/test_validation.py +0 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Reusable DynamoDB key strategies.
|
|
2
|
+
|
|
3
|
+
These helpers keep key-shaping decisions explicit while letting repositories
|
|
4
|
+
stay focused on persistence behavior.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Callable, Mapping
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
AggregateNameResolver = Callable[..., str]
|
|
16
|
+
AggregateIdResolver = Callable[..., str]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class KeyStrategy(ABC):
|
|
20
|
+
"""Build DynamoDB primary keys for ids and entities."""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def key_for_id(self, record_id: Any, **context: Any) -> dict[str, str]:
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def key_for_record(self, record: Any, **context: Any) -> dict[str, str]:
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class SingleEntityKeyStrategy(KeyStrategy):
|
|
33
|
+
"""Use the record id as pk and a fixed discriminator as sk."""
|
|
34
|
+
|
|
35
|
+
discriminator: str
|
|
36
|
+
separator: str = "#"
|
|
37
|
+
|
|
38
|
+
def key_for_id(self, record_id: Any, **context: Any) -> dict[str, str]:
|
|
39
|
+
return {
|
|
40
|
+
"pk": f"{self.discriminator}{self.separator}{record_id}",
|
|
41
|
+
"sk": self.discriminator,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def key_for_record(self, record: Any, **context: Any) -> dict[str, str]:
|
|
45
|
+
return self.key_for_id(_record_attr(record, "id"), **context)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class TenantScopedSortKeyStrategy(KeyStrategy):
|
|
50
|
+
"""Use discriminator + record id as pk and tenant name + tenant id as sk."""
|
|
51
|
+
|
|
52
|
+
discriminator: str
|
|
53
|
+
tenant_name: str = "tenant"
|
|
54
|
+
tenant_field: str = "tenant_id"
|
|
55
|
+
separator: str = "#"
|
|
56
|
+
|
|
57
|
+
def key_for_id(self, record_id: Any, **context: Any) -> dict[str, str]:
|
|
58
|
+
tenant_id = _context_value(context, self.tenant_field)
|
|
59
|
+
return {
|
|
60
|
+
"pk": f"{self.discriminator}{self.separator}{record_id}",
|
|
61
|
+
"sk": f"{self.tenant_name}{self.separator}{tenant_id}",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
def key_for_record(self, record: Any, **context: Any) -> dict[str, str]:
|
|
65
|
+
tenant_id = _context_or_record_value(context, record, self.tenant_field)
|
|
66
|
+
return self.key_for_id(_record_attr(record, "id"), **{self.tenant_field: tenant_id})
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class TenantPartitionKeyStrategy(KeyStrategy):
|
|
72
|
+
"""Use tenant name + tenant id as pk and discriminator + record id as sk."""
|
|
73
|
+
|
|
74
|
+
discriminator: str
|
|
75
|
+
tenant_name: str = "tenant"
|
|
76
|
+
tenant_field: str = "tenant_id"
|
|
77
|
+
separator: str = "#"
|
|
78
|
+
|
|
79
|
+
def key_for_id(self, record_id: Any, **context: Any) -> dict[str, str]:
|
|
80
|
+
tenant_id = _context_value(context, self.tenant_field)
|
|
81
|
+
return {
|
|
82
|
+
"pk": f"{self.tenant_name}{self.separator}{tenant_id}",
|
|
83
|
+
"sk": f"{self.discriminator}{self.separator}{record_id}",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
def key_for_record(self, record: Any, **context: Any) -> dict[str, str]:
|
|
87
|
+
tenant_id = _context_or_record_value(context, record, self.tenant_field)
|
|
88
|
+
return self.key_for_id(_record_attr(record, "id"), **{self.tenant_field: tenant_id})
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True)
|
|
91
|
+
class AggregateKeyStrategy(KeyStrategy):
|
|
92
|
+
"""Use aggregate name + aggregate id as pk and discriminator + record id as sk."""
|
|
93
|
+
|
|
94
|
+
discriminator: str
|
|
95
|
+
aggregate_name: str | AggregateNameResolver
|
|
96
|
+
aggregate_field: str | AggregateIdResolver
|
|
97
|
+
separator: str = "#"
|
|
98
|
+
|
|
99
|
+
def key_for_id(self, record_id: Any, **context: Any) -> dict[str, str]:
|
|
100
|
+
aggregate_name = self._resolve_aggregate_name(
|
|
101
|
+
record=None,
|
|
102
|
+
record_id=record_id,
|
|
103
|
+
context=context,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
aggregate_id = self._resolve_aggregate_id(
|
|
107
|
+
record=None,
|
|
108
|
+
record_id=record_id,
|
|
109
|
+
context=context,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return self._build_key(
|
|
113
|
+
aggregate_name=aggregate_name,
|
|
114
|
+
aggregate_id=aggregate_id,
|
|
115
|
+
record_id=record_id,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def key_for_record(self, record: Any, **context: Any) -> dict[str, str]:
|
|
119
|
+
record_id = _record_attr(record, "id")
|
|
120
|
+
|
|
121
|
+
aggregate_name = self._resolve_aggregate_name(
|
|
122
|
+
record=record,
|
|
123
|
+
record_id=record_id,
|
|
124
|
+
context=context,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
aggregate_id = self._resolve_aggregate_id(
|
|
128
|
+
record=record,
|
|
129
|
+
record_id=record_id,
|
|
130
|
+
context=context,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return self._build_key(
|
|
134
|
+
aggregate_name=aggregate_name,
|
|
135
|
+
aggregate_id=aggregate_id,
|
|
136
|
+
record_id=record_id,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def _build_key(
|
|
140
|
+
self,
|
|
141
|
+
*,
|
|
142
|
+
aggregate_name: str,
|
|
143
|
+
aggregate_id: Any,
|
|
144
|
+
record_id: Any,
|
|
145
|
+
) -> dict[str, str]:
|
|
146
|
+
return {
|
|
147
|
+
"pk": f"{aggregate_name}{self.separator}{aggregate_id}",
|
|
148
|
+
"sk": f"{self.discriminator}{self.separator}{record_id}",
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
def _resolve_aggregate_name(
|
|
152
|
+
self,
|
|
153
|
+
*,
|
|
154
|
+
record: Any | None,
|
|
155
|
+
record_id: Any,
|
|
156
|
+
context: Mapping[str, Any],
|
|
157
|
+
) -> str:
|
|
158
|
+
if callable(self.aggregate_name):
|
|
159
|
+
return self.aggregate_name(
|
|
160
|
+
record=record,
|
|
161
|
+
record_id=record_id,
|
|
162
|
+
context=context,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return self.aggregate_name
|
|
166
|
+
|
|
167
|
+
def _resolve_aggregate_id(
|
|
168
|
+
self,
|
|
169
|
+
*,
|
|
170
|
+
record: Any | None,
|
|
171
|
+
record_id: Any,
|
|
172
|
+
context: Mapping[str, Any],
|
|
173
|
+
) -> Any:
|
|
174
|
+
if callable(self.aggregate_field):
|
|
175
|
+
return self.aggregate_field(
|
|
176
|
+
record=record,
|
|
177
|
+
record_id=record_id,
|
|
178
|
+
context=context,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if record is None:
|
|
182
|
+
return _context_value(context, self.aggregate_field)
|
|
183
|
+
|
|
184
|
+
return _context_or_record_value(context, record, self.aggregate_field)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _context_value(context: dict[str, Any], field_name: str) -> Any:
|
|
189
|
+
if field_name in context and context[field_name] is not None:
|
|
190
|
+
return context[field_name]
|
|
191
|
+
raise KeyError(f"Missing required key context: {field_name}")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _context_or_record_value(context: dict[str, Any], record: Any, field_name: str) -> Any:
|
|
195
|
+
if field_name in context and context[field_name] is not None:
|
|
196
|
+
return context[field_name]
|
|
197
|
+
value = _record_attr(record, field_name)
|
|
198
|
+
if value is not None:
|
|
199
|
+
return value
|
|
200
|
+
raise AttributeError(f"record is missing required field: {field_name}")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _record_attr(record: Any, field_name: str) -> Any:
|
|
204
|
+
if isinstance(record, dict):
|
|
205
|
+
try:
|
|
206
|
+
return record[field_name]
|
|
207
|
+
except KeyError as exc:
|
|
208
|
+
raise AttributeError(f"record is missing required field: {field_name}") from exc
|
|
209
|
+
try:
|
|
210
|
+
return getattr(record, field_name)
|
|
211
|
+
except AttributeError as exc:
|
|
212
|
+
raise AttributeError(f"record is missing required field: {field_name}") from exc
|
{modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/modmex_lambda/persistence/dynamodb/materialized_views.py
RENAMED
|
@@ -55,7 +55,7 @@ class MaterializedViewMixin(DynamoDBUpdateRequestMixin):
|
|
|
55
55
|
|
|
56
56
|
def materialized_key(self, uow: dict[str, Any], entity: dict[str, Any]) -> dict[str, Any]:
|
|
57
57
|
return {
|
|
58
|
-
"pk": entity
|
|
58
|
+
"pk": entity.get('pk') or entity.get('id'),
|
|
59
59
|
"sk": entity.get("sk", self.discriminator),
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "modmex-lambda"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.7"
|
|
8
8
|
description = "Ultra-lightweight AWS Lambda utilities for API Gateway routing and event handling."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10,<4.0"
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from modmex_lambda.persistence.dynamodb import (
|
|
8
|
+
AggregateKeyStrategy,
|
|
9
|
+
KeyStrategy,
|
|
10
|
+
SingleEntityKeyStrategy,
|
|
11
|
+
TenantPartitionKeyStrategy,
|
|
12
|
+
TenantScopedSortKeyStrategy,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Thing:
|
|
18
|
+
id: str
|
|
19
|
+
tenant_id: str | None = None
|
|
20
|
+
aggregate_id: str | None = None
|
|
21
|
+
account_id: str | None = None
|
|
22
|
+
order_id: str | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_key_strategy_is_abstract() -> None:
|
|
26
|
+
with pytest.raises(TypeError):
|
|
27
|
+
KeyStrategy()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_single_record_key_strategy_uses_id_as_pk_and_discriminator_as_sk() -> None:
|
|
31
|
+
strategy = SingleEntityKeyStrategy("thing")
|
|
32
|
+
|
|
33
|
+
assert strategy.key_for_id("thing-1") == {"pk": "thing#thing-1", "sk": "thing"}
|
|
34
|
+
assert strategy.key_for_record(Thing(id="thing-2")) == {"pk": "thing#thing-2", "sk": "thing"}
|
|
35
|
+
assert strategy.key_for_record({"id": "thing-3"}) == {"pk": "thing#thing-3", "sk": "thing"}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_tenant_scoped_sort_key_strategy_uses_record_id_and_tenant_scoped_sk() -> None:
|
|
39
|
+
strategy = TenantScopedSortKeyStrategy("thing")
|
|
40
|
+
|
|
41
|
+
assert strategy.key_for_id("thing-1", tenant_id="acme") == {
|
|
42
|
+
"pk": "thing#thing-1",
|
|
43
|
+
"sk": "tenant#acme",
|
|
44
|
+
}
|
|
45
|
+
assert strategy.key_for_record(Thing(id="thing-2", tenant_id="modmex")) == {
|
|
46
|
+
"pk": "thing#thing-2",
|
|
47
|
+
"sk": "tenant#modmex",
|
|
48
|
+
}
|
|
49
|
+
assert strategy.key_for_record(Thing(id="thing-3", tenant_id="ignored"), tenant_id="acme") == {
|
|
50
|
+
"pk": "thing#thing-3",
|
|
51
|
+
"sk": "tenant#acme",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_tenant_scoped_sort_key_strategy_supports_custom_field_and_separator() -> None:
|
|
56
|
+
strategy = TenantScopedSortKeyStrategy(
|
|
57
|
+
"thing",
|
|
58
|
+
tenant_name="account",
|
|
59
|
+
separator=":",
|
|
60
|
+
tenant_field="account_id",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
assert strategy.key_for_record(Thing(id="thing-1", account_id="acct-1")) == {
|
|
64
|
+
"pk": "thing:thing-1",
|
|
65
|
+
"sk": "account:acct-1",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_tenant_partition_key_strategy_uses_tenant_as_pk_and_record_scoped_sk() -> None:
|
|
70
|
+
strategy = TenantPartitionKeyStrategy("thing")
|
|
71
|
+
|
|
72
|
+
assert strategy.key_for_id("thing-1", tenant_id="acme") == {
|
|
73
|
+
"pk": "tenant#acme",
|
|
74
|
+
"sk": "thing#thing-1",
|
|
75
|
+
}
|
|
76
|
+
assert strategy.key_for_record(Thing(id="thing-2", tenant_id="modmex")) == {
|
|
77
|
+
"pk": "tenant#modmex",
|
|
78
|
+
"sk": "thing#thing-2",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_tenant_partition_key_strategy_supports_custom_field_and_separator() -> None:
|
|
83
|
+
strategy = TenantPartitionKeyStrategy(
|
|
84
|
+
"thing",
|
|
85
|
+
tenant_name="account",
|
|
86
|
+
separator=":",
|
|
87
|
+
tenant_field="account_id",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
assert strategy.key_for_record(Thing(id="thing-1", account_id="acct-1")) == {
|
|
91
|
+
"pk": "account:acct-1",
|
|
92
|
+
"sk": "thing:thing-1",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_aggregate_key_strategy_uses_aggregate_pk_and_record_sk() -> None:
|
|
97
|
+
strategy = AggregateKeyStrategy("item", "order", "aggregate_id")
|
|
98
|
+
|
|
99
|
+
assert strategy.key_for_id("item-1", aggregate_id="order-1") == {
|
|
100
|
+
"pk": "order#order-1",
|
|
101
|
+
"sk": "item#item-1",
|
|
102
|
+
}
|
|
103
|
+
assert strategy.key_for_record(Thing(id="item-2", aggregate_id="order-2")) == {
|
|
104
|
+
"pk": "order#order-2",
|
|
105
|
+
"sk": "item#item-2",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_aggregate_key_strategy_supports_custom_field_and_separator() -> None:
|
|
110
|
+
strategy = AggregateKeyStrategy("item", "order", separator=":", aggregate_field="order_id")
|
|
111
|
+
|
|
112
|
+
assert strategy.key_for_record(Thing(id="item-1", order_id="order-1")) == {
|
|
113
|
+
"pk": "order:order-1",
|
|
114
|
+
"sk": "item:item-1",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_aggregate_key_strategy_for_entity_with_callable() -> None:
|
|
119
|
+
strategy = AggregateKeyStrategy(
|
|
120
|
+
discriminator="audit",
|
|
121
|
+
aggregate_name=lambda record, **_: record.entity_type,
|
|
122
|
+
aggregate_field=lambda record, **_: record.entity_id,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class Audit:
|
|
127
|
+
id: str
|
|
128
|
+
entity_type: str
|
|
129
|
+
entity_id: str
|
|
130
|
+
|
|
131
|
+
entity = Audit(id="audit-1", entity_type="order", entity_id="order-1")
|
|
132
|
+
|
|
133
|
+
assert strategy.key_for_record(entity) == {
|
|
134
|
+
"pk": "order#order-1",
|
|
135
|
+
"sk": "audit#audit-1",
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_aggregate_key_strategy_for_id_with_callable() -> None:
|
|
140
|
+
strategy = AggregateKeyStrategy(
|
|
141
|
+
discriminator="audit",
|
|
142
|
+
aggregate_name=lambda context, **_: context["entity_type"],
|
|
143
|
+
aggregate_field=lambda context, **_: context["entity_id"],
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
assert strategy.key_for_id(
|
|
147
|
+
"audit-1",
|
|
148
|
+
entity_type="order",
|
|
149
|
+
entity_id="order-1",
|
|
150
|
+
) == {
|
|
151
|
+
"pk": "order#order-1",
|
|
152
|
+
"sk": "audit#audit-1",
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_aggregate_key_strategy_callable_aggregate_name_supports_record_and_id() -> None:
|
|
157
|
+
@dataclass
|
|
158
|
+
class Audit:
|
|
159
|
+
id: str
|
|
160
|
+
entity_type: str
|
|
161
|
+
entity_id: str
|
|
162
|
+
|
|
163
|
+
strategy = AggregateKeyStrategy(
|
|
164
|
+
discriminator="audit",
|
|
165
|
+
aggregate_name=lambda record=None, context=None, **_: (
|
|
166
|
+
record.entity_type if record is not None else context["entity_type"]
|
|
167
|
+
),
|
|
168
|
+
aggregate_field=lambda record=None, context=None, **_: (
|
|
169
|
+
record.entity_id if record is not None else context["entity_id"]
|
|
170
|
+
),
|
|
171
|
+
separator="#",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
entity = Audit(
|
|
175
|
+
id="audit-1",
|
|
176
|
+
entity_type="order",
|
|
177
|
+
entity_id="order-1",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
assert strategy.key_for_record(entity) == {
|
|
181
|
+
"pk": "order#order-1",
|
|
182
|
+
"sk": "audit#audit-1",
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
assert strategy.key_for_id(
|
|
186
|
+
"audit-1",
|
|
187
|
+
entity_type="order",
|
|
188
|
+
entity_id="order-1",
|
|
189
|
+
) == {
|
|
190
|
+
"pk": "order#order-1",
|
|
191
|
+
"sk": "audit#audit-1",
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_required_context_errors_are_explicit() -> None:
|
|
197
|
+
strategy = TenantPartitionKeyStrategy("thing")
|
|
198
|
+
|
|
199
|
+
with pytest.raises(KeyError, match="tenant_id"):
|
|
200
|
+
strategy.key_for_id("thing-1")
|
|
201
|
+
|
|
202
|
+
with pytest.raises(AttributeError, match="tenant_id"):
|
|
203
|
+
strategy.key_for_record(Thing(id="thing-1"))
|
{modmex_lambda-0.5.5 → modmex_lambda-0.5.7}/tests/persistence/test_dynamodb_materialized_views.py
RENAMED
|
@@ -56,7 +56,7 @@ def test_materialized_view_mixin_replicates_event_entity_with_stream_fields(monk
|
|
|
56
56
|
"timestamp": 1548967022000,
|
|
57
57
|
"thing": {
|
|
58
58
|
"id": "thing-1",
|
|
59
|
-
"pk": "
|
|
59
|
+
"pk": "thing#thing-1",
|
|
60
60
|
"sk": "thing",
|
|
61
61
|
"name": "Desk",
|
|
62
62
|
},
|
|
@@ -65,7 +65,7 @@ def test_materialized_view_mixin_replicates_event_entity_with_stream_fields(monk
|
|
|
65
65
|
|
|
66
66
|
request = ThingMaterializer().to_materialized_update_request(uow)
|
|
67
67
|
|
|
68
|
-
assert request["Key"] == {"pk": "thing-1", "sk": "thing"}
|
|
68
|
+
assert request["Key"] == {"pk": "thing#thing-1", "sk": "thing"}
|
|
69
69
|
assert request["ExpressionAttributeValues"][":id"] == "thing-1"
|
|
70
70
|
assert request["ExpressionAttributeValues"][":name"] == "Desk"
|
|
71
71
|
assert request["ExpressionAttributeValues"][":discriminator"] == "thing"
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
"""Reusable DynamoDB key strategies.
|
|
2
|
-
|
|
3
|
-
These helpers keep key-shaping decisions explicit while letting repositories
|
|
4
|
-
stay focused on persistence behavior.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
from abc import ABC, abstractmethod
|
|
10
|
-
from dataclasses import dataclass
|
|
11
|
-
from typing import Any
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class KeyStrategy(ABC):
|
|
15
|
-
"""Build DynamoDB primary keys for ids and entities."""
|
|
16
|
-
|
|
17
|
-
@abstractmethod
|
|
18
|
-
def key_for_id(self, entity_id: Any, **context: Any) -> dict[str, str]:
|
|
19
|
-
raise NotImplementedError
|
|
20
|
-
|
|
21
|
-
@abstractmethod
|
|
22
|
-
def key_for_entity(self, entity: Any, **context: Any) -> dict[str, str]:
|
|
23
|
-
raise NotImplementedError
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@dataclass(frozen=True)
|
|
27
|
-
class SingleEntityKeyStrategy(KeyStrategy):
|
|
28
|
-
"""Use the entity id as pk and a fixed discriminator as sk."""
|
|
29
|
-
|
|
30
|
-
discriminator: str
|
|
31
|
-
|
|
32
|
-
def key_for_id(self, entity_id: Any, **context: Any) -> dict[str, str]:
|
|
33
|
-
return {
|
|
34
|
-
"pk": str(entity_id),
|
|
35
|
-
"sk": self.discriminator,
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
def key_for_entity(self, entity: Any, **context: Any) -> dict[str, str]:
|
|
39
|
-
return self.key_for_id(_entity_attr(entity, "id"), **context)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@dataclass(frozen=True)
|
|
43
|
-
class TenantScopedSortKeyStrategy(KeyStrategy):
|
|
44
|
-
"""Use entity id as pk and discriminator plus tenant id as sk."""
|
|
45
|
-
|
|
46
|
-
discriminator: str
|
|
47
|
-
separator: str = "#"
|
|
48
|
-
tenant_field: str = "tenant_id"
|
|
49
|
-
|
|
50
|
-
def key_for_id(self, entity_id: Any, **context: Any) -> dict[str, str]:
|
|
51
|
-
tenant_id = _context_value(context, self.tenant_field)
|
|
52
|
-
return {
|
|
53
|
-
"pk": str(entity_id),
|
|
54
|
-
"sk": self._sort_key(tenant_id),
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
def key_for_entity(self, entity: Any, **context: Any) -> dict[str, str]:
|
|
58
|
-
tenant_id = _context_or_entity_value(context, entity, self.tenant_field)
|
|
59
|
-
return self.key_for_id(_entity_attr(entity, "id"), **{self.tenant_field: tenant_id})
|
|
60
|
-
|
|
61
|
-
def _sort_key(self, tenant_id: Any) -> str:
|
|
62
|
-
return f"{self.discriminator}{self.separator}{tenant_id}"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
@dataclass(frozen=True)
|
|
66
|
-
class TenantPartitionKeyStrategy(KeyStrategy):
|
|
67
|
-
"""Use tenant id as pk and discriminator plus entity id as sk."""
|
|
68
|
-
|
|
69
|
-
discriminator: str
|
|
70
|
-
separator: str = "#"
|
|
71
|
-
tenant_field: str = "tenant_id"
|
|
72
|
-
|
|
73
|
-
def key_for_id(self, entity_id: Any, **context: Any) -> dict[str, str]:
|
|
74
|
-
tenant_id = _context_value(context, self.tenant_field)
|
|
75
|
-
return {
|
|
76
|
-
"pk": str(tenant_id),
|
|
77
|
-
"sk": self._sort_key(entity_id),
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
def key_for_entity(self, entity: Any, **context: Any) -> dict[str, str]:
|
|
81
|
-
tenant_id = _context_or_entity_value(context, entity, self.tenant_field)
|
|
82
|
-
return self.key_for_id(_entity_attr(entity, "id"), **{self.tenant_field: tenant_id})
|
|
83
|
-
|
|
84
|
-
def _sort_key(self, entity_id: Any) -> str:
|
|
85
|
-
return f"{self.discriminator}{self.separator}{entity_id}"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
@dataclass(frozen=True)
|
|
89
|
-
class AggregateKeyStrategy(KeyStrategy):
|
|
90
|
-
"""Use aggregate id as pk and entity name plus entity id as sk."""
|
|
91
|
-
|
|
92
|
-
aggregate_name: str
|
|
93
|
-
entity_name: str
|
|
94
|
-
separator: str = "#"
|
|
95
|
-
aggregate_field: str = "aggregate_id"
|
|
96
|
-
|
|
97
|
-
def key_for_id(self, entity_id: Any, **context: Any) -> dict[str, str]:
|
|
98
|
-
aggregate_id = _context_value(context, self.aggregate_field)
|
|
99
|
-
return {
|
|
100
|
-
"pk": f"{self.aggregate_name}{self.separator}{aggregate_id}",
|
|
101
|
-
"sk": f"{self.entity_name}{self.separator}{entity_id}",
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
def key_for_entity(self, entity: Any, **context: Any) -> dict[str, str]:
|
|
105
|
-
aggregate_id = _context_or_entity_value(context, entity, self.aggregate_field)
|
|
106
|
-
return self.key_for_id(_entity_attr(entity, "id"), **{self.aggregate_field: aggregate_id})
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _context_value(context: dict[str, Any], field_name: str) -> Any:
|
|
110
|
-
if field_name in context and context[field_name] is not None:
|
|
111
|
-
return context[field_name]
|
|
112
|
-
raise KeyError(f"Missing required key context: {field_name}")
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _context_or_entity_value(context: dict[str, Any], entity: Any, field_name: str) -> Any:
|
|
116
|
-
if field_name in context and context[field_name] is not None:
|
|
117
|
-
return context[field_name]
|
|
118
|
-
value = _entity_attr(entity, field_name)
|
|
119
|
-
if value is not None:
|
|
120
|
-
return value
|
|
121
|
-
raise AttributeError(f"Entity is missing required field: {field_name}")
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def _entity_attr(entity: Any, field_name: str) -> Any:
|
|
125
|
-
if isinstance(entity, dict):
|
|
126
|
-
try:
|
|
127
|
-
return entity[field_name]
|
|
128
|
-
except KeyError as exc:
|
|
129
|
-
raise AttributeError(f"Entity is missing required field: {field_name}") from exc
|
|
130
|
-
try:
|
|
131
|
-
return getattr(entity, field_name)
|
|
132
|
-
except AttributeError as exc:
|
|
133
|
-
raise AttributeError(f"Entity is missing required field: {field_name}") from exc
|