modmex-lambda 0.2.0__tar.gz → 0.4.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.
- modmex_lambda-0.4.0/PKG-INFO +1107 -0
- modmex_lambda-0.4.0/README.md +1084 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/__init__.py +20 -11
- modmex_lambda-0.4.0/modmex_lambda/connectors/__init__.py +28 -0
- modmex_lambda-0.4.0/modmex_lambda/connectors/cloudwatch.py +17 -0
- modmex_lambda-0.4.0/modmex_lambda/connectors/dynamodb.py +123 -0
- modmex_lambda-0.4.0/modmex_lambda/connectors/eventbridge.py +19 -0
- modmex_lambda-0.4.0/modmex_lambda/connectors/icloudwatch.py +14 -0
- modmex_lambda-0.4.0/modmex_lambda/connectors/idynamodb.py +68 -0
- modmex_lambda-0.4.0/modmex_lambda/connectors/ieventbridge.py +14 -0
- modmex_lambda-0.4.0/modmex_lambda/connectors/ilambda.py +14 -0
- modmex_lambda-0.4.0/modmex_lambda/connectors/is3.py +30 -0
- modmex_lambda-0.4.0/modmex_lambda/connectors/isns.py +20 -0
- modmex_lambda-0.4.0/modmex_lambda/connectors/isqs.py +21 -0
- modmex_lambda-0.4.0/modmex_lambda/connectors/lambda_.py +17 -0
- modmex_lambda-0.4.0/modmex_lambda/connectors/module.py +67 -0
- modmex_lambda-0.4.0/modmex_lambda/connectors/s3.py +43 -0
- modmex_lambda-0.4.0/modmex_lambda/connectors/sns.py +31 -0
- modmex_lambda-0.4.0/modmex_lambda/connectors/sqs.py +31 -0
- modmex_lambda-0.4.0/modmex_lambda/data_classes/__init__.py +132 -0
- modmex_lambda-0.4.0/modmex_lambda/dependencies.py +106 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/__init__.py +11 -10
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/api_gateway.py +7 -7
- modmex_lambda-0.4.0/modmex_lambda/event_handler/dependencies/__init__.py +37 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/dependencies/depends.py +4 -52
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/routing.py +43 -1
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/types.py +1 -1
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/logging.py +48 -14
- modmex_lambda-0.4.0/modmex_lambda/stream/__init__.py +3 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/events/dynamodb.py +123 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/events/kinesis.py +44 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/events/s3.py +66 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/events/sns.py +54 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/events/sqs.py +46 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/filters/content.py +9 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/filters/event_type.py +17 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/filters/latch.py +21 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/filters/skip.py +18 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/flavors/__init__.py +0 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/flavors/base_flavor.py +140 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/flavors/cdc.py +70 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/flavors/collect.py +105 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/flavors/correlate.py +114 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/flavors/evaluate.py +235 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/flavors/expired.py +104 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/flavors/iflavor.py +19 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/flavors/job.py +294 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/flavors/materialize.py +65 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/flavors/s3.py +63 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/flavors/sns.py +59 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/flavors/task.py +135 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/flavors/update.py +171 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/irules_registry.py +18 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/operators/__init__.py +1 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/operators/cloudwatch.py +29 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/operators/dynamodb.py +391 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/operators/ioperator.py +14 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/operators/lambda_.py +29 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/operators/publisher.py +99 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/operators/s3.py +131 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/operators/sns.py +58 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/operators/sqs.py +83 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/rules_registry.py +38 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/runner.py +93 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/sources/__init__.py +53 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/sources/base.py +60 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/sources/dynamodb.py +71 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/sources/kinesis.py +51 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/sources/s3.py +51 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/sources/sns.py +51 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/sources/sqs.py +51 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/__init__.py +0 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/apigateway.py +41 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/aws.py +6 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/batch.py +9 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/cloudwatch.py +11 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/concurrency.py +54 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/contracts.py +45 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/data_classes/__init__.py +0 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/data_classes/dynamodb.py +17 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/decorators.py +9 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/dynamodb.py +140 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/eventbridge.py +25 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/faults.py +100 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/filters.py +15 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/json_encoder.py +12 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/lambda_.py +11 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/operators.py +76 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/opt.py +15 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/pluralize.py +13 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/print.py +24 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/retry.py +28 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/s3.py +52 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/sns.py +11 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/split.py +32 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/sqs.py +19 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/tags.py +33 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/time.py +17 -0
- modmex_lambda-0.4.0/modmex_lambda/stream/utils/uow.py +110 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/poetry.lock +173 -22
- modmex_lambda-0.4.0/pyproject.toml +50 -0
- modmex_lambda-0.4.0/tests/__init__.py +0 -0
- modmex_lambda-0.4.0/tests/connectors/__init__.py +0 -0
- modmex_lambda-0.4.0/tests/connectors/conftest.py +12 -0
- modmex_lambda-0.4.0/tests/connectors/test_cloudwatch.py +46 -0
- modmex_lambda-0.4.0/tests/connectors/test_dynamodb.py +344 -0
- modmex_lambda-0.4.0/tests/connectors/test_lazy_clients.py +73 -0
- modmex_lambda-0.4.0/tests/connectors/test_s3.py +59 -0
- modmex_lambda-0.4.0/tests/connectors/test_simple_connectors.py +74 -0
- modmex_lambda-0.4.0/tests/connectors/test_sns.py +39 -0
- modmex_lambda-0.4.0/tests/connectors/test_sqs.py +41 -0
- modmex_lambda-0.4.0/tests/event_handler/__init__.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/event_handler/test_api_gateway.py +51 -33
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/event_handler/test_dependencies.py +1 -1
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/event_handler/test_routing.py +21 -1
- modmex_lambda-0.4.0/tests/stream/__init__.py +0 -0
- modmex_lambda-0.4.0/tests/stream/conftest.py +14 -0
- modmex_lambda-0.4.0/tests/stream/events/test_dynamodb.py +122 -0
- modmex_lambda-0.4.0/tests/stream/events/test_kinesis.py +26 -0
- modmex_lambda-0.4.0/tests/stream/events/test_s3.py +54 -0
- modmex_lambda-0.4.0/tests/stream/events/test_sns.py +28 -0
- modmex_lambda-0.4.0/tests/stream/events/test_sqs.py +27 -0
- modmex_lambda-0.4.0/tests/stream/filters/__init__.py +0 -0
- modmex_lambda-0.4.0/tests/stream/filters/test_content.py +38 -0
- modmex_lambda-0.4.0/tests/stream/filters/test_event_type.py +38 -0
- modmex_lambda-0.4.0/tests/stream/filters/test_latch.py +66 -0
- modmex_lambda-0.4.0/tests/stream/filters/test_skip.py +16 -0
- modmex_lambda-0.4.0/tests/stream/flavors/__init__.py +0 -0
- modmex_lambda-0.4.0/tests/stream/flavors/source_events.py +58 -0
- modmex_lambda-0.4.0/tests/stream/flavors/test_base_flavor.py +66 -0
- modmex_lambda-0.4.0/tests/stream/flavors/test_cdc.py +268 -0
- modmex_lambda-0.4.0/tests/stream/flavors/test_collect.py +89 -0
- modmex_lambda-0.4.0/tests/stream/flavors/test_correlate.py +132 -0
- modmex_lambda-0.4.0/tests/stream/flavors/test_evaluate.py +522 -0
- modmex_lambda-0.4.0/tests/stream/flavors/test_expired.py +104 -0
- modmex_lambda-0.4.0/tests/stream/flavors/test_job.py +280 -0
- modmex_lambda-0.4.0/tests/stream/flavors/test_materialize.py +137 -0
- modmex_lambda-0.4.0/tests/stream/flavors/test_s3.py +118 -0
- modmex_lambda-0.4.0/tests/stream/flavors/test_sns.py +80 -0
- modmex_lambda-0.4.0/tests/stream/flavors/test_task.py +223 -0
- modmex_lambda-0.4.0/tests/stream/flavors/test_update.py +194 -0
- modmex_lambda-0.4.0/tests/stream/test_dependency_resolver.py +58 -0
- modmex_lambda-0.4.0/tests/stream/test_rules_registry.py +93 -0
- modmex_lambda-0.4.0/tests/stream/test_runner.py +76 -0
- modmex_lambda-0.4.0/tests/stream/test_runner_pipeline.py +217 -0
- modmex_lambda-0.4.0/tests/stream/test_sources.py +315 -0
- modmex_lambda-0.4.0/tests/stream/utils/__init__.py +0 -0
- modmex_lambda-0.4.0/tests/stream/utils/faults.py +117 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_apigateway.py +29 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_aws.py +12 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_batch.py +20 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_cloudwatch.py +41 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_concurrency.py +61 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_decorators.py +18 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_dynamodb.py +248 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_eventbridge.py +127 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_filters.py +23 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_json_encoder.py +21 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_lambda.py +35 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_operators.py +191 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_pluralize.py +10 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_print.py +70 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_retry.py +29 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_s3.py +144 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_sns.py +38 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_split.py +85 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_sqs.py +80 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_tags.py +50 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_time.py +36 -0
- modmex_lambda-0.4.0/tests/stream/utils/test_uow.py +109 -0
- modmex_lambda-0.4.0/tests/test_lazy_imports.py +71 -0
- modmex_lambda-0.4.0/tests/test_logging.py +110 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/test_reexports.py +11 -5
- modmex_lambda-0.2.0/PKG-INFO +0 -446
- modmex_lambda-0.2.0/README.md +0 -431
- modmex_lambda-0.2.0/modmex_lambda/data_classes/__init__.py +0 -49
- modmex_lambda-0.2.0/modmex_lambda/event_handler/dependencies/__init__.py +0 -13
- modmex_lambda-0.2.0/pyproject.toml +0 -30
- modmex_lambda-0.2.0/tests/test_logging.py +0 -50
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/.github/workflows/ci.yml +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/.github/workflows/release.yml +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/.gitignore +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/LICENSE +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/data_classes/api_gateway_authorizer_event.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/data_classes/api_gateway_proxy_event.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/data_classes/api_gateway_websocket_event.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/data_classes/cognito_user_pool_event.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/data_classes/common.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/constants.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/content_types.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/cors.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/dependencies/compat.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/dependencies/dependant.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/dependencies/dependency_middleware.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/dependencies/params.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/dependencies/types.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/exception_handler.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/exceptions.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/gateway_response.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/middlewares.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/params.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/request.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/response.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/routing_fallbacks.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_sources.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/exceptions.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/params.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/parser.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/request.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/resolver.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/response.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/routing.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/shared/__init__.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/shared/cookies.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/shared/headers_serializer.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/shared/json_encoder.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/shared/types.py +0 -0
- {modmex_lambda-0.2.0/tests → modmex_lambda-0.4.0/modmex_lambda/stream/events}/__init__.py +0 -0
- {modmex_lambda-0.2.0/tests/event_handler → modmex_lambda-0.4.0/modmex_lambda/stream/filters}/__init__.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/validation.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/conftest.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/data_classes/test_api_gateway_proxy_event.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/data_classes/test_cognito_user_pool_event.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/data_classes/test_common.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/event_handler/test_cors.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/event_handler/test_exception_handler.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/event_handler/test_gateway_response.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/event_handler/test_request.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/event_handler/test_response.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/shared/test_cookies_headers.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/shared/test_json_encoder.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/test_parser_event_sources.py +0 -0
- {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/test_validation.py +0 -0
|
@@ -0,0 +1,1107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: modmex-lambda
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Ultra-lightweight AWS Lambda utilities for API Gateway routing and event handling.
|
|
5
|
+
Author: Modmex
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Python: <4.0,>=3.10
|
|
14
|
+
Requires-Dist: boto3<2.0.0,>=1.24.66
|
|
15
|
+
Requires-Dist: injector<1.0.0,>=0.24.0
|
|
16
|
+
Requires-Dist: modmex<2.0.0,>=1.1.10
|
|
17
|
+
Requires-Dist: pydash<9.0.0,>=5.1.2
|
|
18
|
+
Requires-Dist: reactivex<5.0.0,>=4.0.4
|
|
19
|
+
Requires-Dist: typing-extensions<5.0.0,>=4.5.0
|
|
20
|
+
Provides-Extra: injector
|
|
21
|
+
Requires-Dist: injector<1.0.0,>=0.24.0; extra == 'injector'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# modmex-lambda
|
|
25
|
+
|
|
26
|
+
AWS Lambda utilities for API Gateway and event-driven workloads.
|
|
27
|
+
|
|
28
|
+
[](https://github.com/modmex/modmex/actions/workflows/ci.yml)
|
|
29
|
+
[](https://codecov.io/gh/modmex/modmex-lambda)
|
|
30
|
+
[](https://pypi.org/project/modmex-lambda/)
|
|
31
|
+
[](https://pypi.org/project/modmex-lambda/)
|
|
32
|
+
[](https://github.com/modmex/modmex-lambda/blob/main/LICENSE)
|
|
33
|
+
|
|
34
|
+
`modmex-lambda` is a Lambda utility layer, not an ASGI framework. It focuses on
|
|
35
|
+
API Gateway proxy events, fast routing, request binding, response serialization,
|
|
36
|
+
middleware, dependency injection, stream sources, parsing, and structured
|
|
37
|
+
logging.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install modmex-lambda
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
`injector` support is included for REST and stream dependency resolution:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install modmex-lambda
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
With Poetry:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
poetry add modmex-lambda
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## API Gateway Resolvers
|
|
59
|
+
|
|
60
|
+
Choose the resolver that matches the API Gateway payload version used by your
|
|
61
|
+
Lambda integration:
|
|
62
|
+
|
|
63
|
+
- `APIGatewayRestResolver` for REST API payload v1.
|
|
64
|
+
- `APIGatewayHttpResolver` for HTTP API payload v2 and Lambda Function URLs.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from modmex_lambda import APIGatewayHttpResolver
|
|
68
|
+
|
|
69
|
+
app = APIGatewayHttpResolver()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.get("/ping")
|
|
73
|
+
def ping():
|
|
74
|
+
return {"message": "pong"}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def handler(event, context):
|
|
78
|
+
return app.resolve(event, context)
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The internal base resolver is intentionally not exported from the package root;
|
|
83
|
+
application code should select REST or HTTP explicitly.
|
|
84
|
+
|
|
85
|
+
## Routing
|
|
86
|
+
|
|
87
|
+
Routes are declared with decorators:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
@app.get("/users/<user_id>")
|
|
91
|
+
def get_user(user_id: int):
|
|
92
|
+
return {"user_id": user_id}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@app.post("/users", status_code=201)
|
|
96
|
+
def create_user():
|
|
97
|
+
return {"id": 42}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Supported route decorators include `get`, `post`, `put`, `patch`, `delete`,
|
|
101
|
+
`options`, and `any`.
|
|
102
|
+
|
|
103
|
+
You can also declare routes on a standalone router and include it in the
|
|
104
|
+
resolver:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from modmex_lambda import APIGatewayHttpResolver
|
|
108
|
+
from modmex_lambda.routing import Router
|
|
109
|
+
|
|
110
|
+
app = APIGatewayHttpResolver()
|
|
111
|
+
router = Router()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@router.get("/health")
|
|
115
|
+
def health():
|
|
116
|
+
return {"ok": True}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
app.include_router(router)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Routers can also strip deployment prefixes:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
app = APIGatewayHttpResolver(strip_prefixes=["/prod"])
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Request Binding
|
|
129
|
+
|
|
130
|
+
Use `typing.Annotated` with the public parameter markers:
|
|
131
|
+
|
|
132
|
+
- `Path()`
|
|
133
|
+
- `Query()`
|
|
134
|
+
- `Header()`
|
|
135
|
+
- `Cookie()`
|
|
136
|
+
- `Body()`
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from typing import Annotated
|
|
140
|
+
|
|
141
|
+
from modmex import BaseModel
|
|
142
|
+
from modmex_lambda import APIGatewayHttpResolver, Request
|
|
143
|
+
from modmex_lambda.event_handler.params import Body, Header, Path, Query
|
|
144
|
+
|
|
145
|
+
app = APIGatewayHttpResolver()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class CreateUserRequest(BaseModel):
|
|
149
|
+
name: str
|
|
150
|
+
age: int | None = None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@app.post("/users", status_code=201)
|
|
154
|
+
def create_user(
|
|
155
|
+
payload: Annotated[CreateUserRequest, Body()],
|
|
156
|
+
tenant_id: Annotated[str, Header(name="x-tenant-id")],
|
|
157
|
+
request: Request,
|
|
158
|
+
):
|
|
159
|
+
return {
|
|
160
|
+
"id": 42,
|
|
161
|
+
"tenant_id": tenant_id,
|
|
162
|
+
"route": request.route,
|
|
163
|
+
"payload": payload.model_dump(),
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@app.get("/users/<user_id>")
|
|
168
|
+
def get_user(
|
|
169
|
+
user_id: Annotated[int, Path()],
|
|
170
|
+
include_orders: Annotated[bool, Query()] = False,
|
|
171
|
+
):
|
|
172
|
+
return {"user_id": user_id, "include_orders": include_orders}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
For headers, simple scalar parameters can use `Header(name="x-header-name")`.
|
|
176
|
+
Header models are also supported; field names are exposed as dash-case aliases.
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
class HeaderModel(BaseModel):
|
|
180
|
+
x_tenant_id: str
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@app.get("/me")
|
|
184
|
+
def me(headers: Annotated[HeaderModel, Header()]):
|
|
185
|
+
return {"tenant": headers.x_tenant_id}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Responses
|
|
189
|
+
|
|
190
|
+
Route return values are converted to API Gateway proxy responses:
|
|
191
|
+
|
|
192
|
+
- `dict` and `list` become JSON responses.
|
|
193
|
+
- `str` becomes a text response.
|
|
194
|
+
- `bytes` are base64 encoded.
|
|
195
|
+
- `None` returns an empty response.
|
|
196
|
+
- `(body, status_code)` sets the response status.
|
|
197
|
+
- `Response` gives full control over status, headers, cookies, and content type.
|
|
198
|
+
|
|
199
|
+
Use plain return values for simple JSON endpoints:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from modmex import BaseModel
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class User(BaseModel):
|
|
206
|
+
id: int
|
|
207
|
+
name: str
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@app.get("/users/<user_id>")
|
|
211
|
+
def get_user(user_id: int):
|
|
212
|
+
user = User(id=user_id, name="Ada")
|
|
213
|
+
return user.model_dump()
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@app.post("/users", status_code=201)
|
|
217
|
+
def create_user():
|
|
218
|
+
user = User(id=42, name="Ada")
|
|
219
|
+
return user.model_dump()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@app.delete("/users/<user_id>")
|
|
223
|
+
def delete_user(user_id: int):
|
|
224
|
+
return {"deleted": user_id}, 202
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Use `Response` when the endpoint needs explicit response metadata. If you are
|
|
228
|
+
returning the same `User` model, pass `user.model_dump_json()` as the body and
|
|
229
|
+
set `content_type="application/json"`:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
from modmex_lambda import Response
|
|
233
|
+
from modmex_lambda.shared.cookies import Cookie
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@app.get("/session")
|
|
237
|
+
def session():
|
|
238
|
+
user = User(id=42, name="Ada")
|
|
239
|
+
return Response(
|
|
240
|
+
status_code=200,
|
|
241
|
+
content_type="application/json",
|
|
242
|
+
body=user.model_dump_json(),
|
|
243
|
+
headers={"x-app": "users"},
|
|
244
|
+
cookies=[
|
|
245
|
+
Cookie(
|
|
246
|
+
"session",
|
|
247
|
+
"abc",
|
|
248
|
+
path="/",
|
|
249
|
+
http_only=True,
|
|
250
|
+
secure=True,
|
|
251
|
+
max_age=3600,
|
|
252
|
+
),
|
|
253
|
+
],
|
|
254
|
+
)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`Response` accepts:
|
|
258
|
+
|
|
259
|
+
- `status_code`: the HTTP status code returned to API Gateway.
|
|
260
|
+
- `body`: a JSON-serializable object, `str`, `bytes`, or `None`.
|
|
261
|
+
- `content_type`: sets `Content-Type` unless the header is already present.
|
|
262
|
+
- `headers`: a mapping of header names to a string or list of strings.
|
|
263
|
+
- `cookies`: a list of `Cookie` objects.
|
|
264
|
+
- `compress`: overrides route-level gzip compression for that response.
|
|
265
|
+
|
|
266
|
+
When `Content-Type` starts with `application/json`, non-string bodies are
|
|
267
|
+
serialized with the app serializer. Binary bodies are base64 encoded.
|
|
268
|
+
|
|
269
|
+
For `modmex` models, prefer `model_dump()` when returning plain JSON objects.
|
|
270
|
+
Use `model_dump_json()` when you already need to build a `Response` and want to
|
|
271
|
+
send the serialized JSON string directly with `content_type="application/json"`.
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
@app.get("/avatar/<user_id>")
|
|
275
|
+
def avatar(user_id: int):
|
|
276
|
+
image_bytes = load_avatar(user_id)
|
|
277
|
+
return Response(
|
|
278
|
+
status_code=200,
|
|
279
|
+
content_type="image/png",
|
|
280
|
+
body=image_bytes,
|
|
281
|
+
headers={"Cache-Control": "max-age=3600"},
|
|
282
|
+
)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Route options can add response behavior without constructing `Response` in every
|
|
286
|
+
handler:
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
@app.get("/report", cache_control="max-age=60", compress=True)
|
|
290
|
+
def report():
|
|
291
|
+
return {"items": build_report()}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Compression is applied only when the request includes `Accept-Encoding: gzip`.
|
|
295
|
+
REST API responses use `multiValueHeaders`; HTTP API responses use the v2
|
|
296
|
+
`headers` and `cookies` shape.
|
|
297
|
+
|
|
298
|
+
## Middleware
|
|
299
|
+
|
|
300
|
+
Middleware receives the resolver instance and a `next_middleware` callable.
|
|
301
|
+
Global middleware can be registered with `use` or `@app.middleware`; route
|
|
302
|
+
middleware can be attached per route.
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
from modmex_lambda import Response
|
|
306
|
+
from modmex_lambda.event_handler.middlewares import NextMiddleware
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@app.middleware
|
|
310
|
+
def require_auth(app: APIGatewayHttpResolver, next_middleware: NextMiddleware) -> Response:
|
|
311
|
+
if app.current_event.headers.get("x-auth") != "ok":
|
|
312
|
+
return Response(status_code=401, body={"message": "Unauthorized"})
|
|
313
|
+
return next_middleware(app)
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Middleware also wraps routing fallbacks, so `404` and `405` responses still flow
|
|
317
|
+
through the middleware chain.
|
|
318
|
+
|
|
319
|
+
## Dependency Injection
|
|
320
|
+
|
|
321
|
+
`Depends` supports nested dependency trees, request-aware dependencies,
|
|
322
|
+
per-invocation caching, and overrides for tests.
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
from typing import Annotated
|
|
326
|
+
|
|
327
|
+
from modmex_lambda import Depends, Request
|
|
328
|
+
from modmex_lambda.event_handler.params import Path
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class UserRepository:
|
|
332
|
+
def __init__(self, *, tenant_id: str, token: str):
|
|
333
|
+
self.tenant_id = tenant_id
|
|
334
|
+
self.token = token
|
|
335
|
+
|
|
336
|
+
def get_user(self, user_id: int) -> dict:
|
|
337
|
+
# Replace this with a database or service call.
|
|
338
|
+
return {"id": user_id, "tenant_id": self.tenant_id}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def get_token() -> str:
|
|
342
|
+
return "token"
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def get_tenant_id(request: Request) -> str:
|
|
346
|
+
return request.headers["x-tenant-id"]
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def get_user_repository(
|
|
350
|
+
tenant_id: Annotated[str, Depends(get_tenant_id)],
|
|
351
|
+
token: Annotated[str, Depends(get_token)],
|
|
352
|
+
) -> UserRepository:
|
|
353
|
+
return UserRepository(tenant_id=tenant_id, token=token)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@app.get("/users/<user_id>")
|
|
357
|
+
def get_user(
|
|
358
|
+
user_id: Annotated[int, Path()],
|
|
359
|
+
repository: Annotated[UserRepository, Depends(get_user_repository)],
|
|
360
|
+
):
|
|
361
|
+
return repository.get_user(user_id)
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
For constructor-heavy services, pass an `InjectorDependencyResolver` to the app.
|
|
365
|
+
`Depends()` without a callable uses the parameter annotation as the dependency
|
|
366
|
+
token.
|
|
367
|
+
|
|
368
|
+
```python
|
|
369
|
+
from typing import Annotated
|
|
370
|
+
|
|
371
|
+
from injector import Injector, Module, inject, provider, singleton
|
|
372
|
+
from modmex_lambda import APIGatewayHttpResolver, Depends, InjectorDependencyResolver
|
|
373
|
+
from modmex_lambda.event_handler.params import Path
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class Settings:
|
|
377
|
+
def __init__(self, tenant_id: str):
|
|
378
|
+
self.tenant_id = tenant_id
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class UserRepository:
|
|
382
|
+
def __init__(self, settings: Settings):
|
|
383
|
+
self.settings = settings
|
|
384
|
+
|
|
385
|
+
def get_user(self, user_id: int) -> dict:
|
|
386
|
+
return {"id": user_id, "tenant_id": self.settings.tenant_id}
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class UserService:
|
|
390
|
+
def __init__(self, repository: UserRepository):
|
|
391
|
+
self.repository = repository
|
|
392
|
+
|
|
393
|
+
def get_user(self, user_id: int) -> dict:
|
|
394
|
+
return self.repository.get_user(user_id)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class AppModule(Module):
|
|
398
|
+
@singleton
|
|
399
|
+
@provider
|
|
400
|
+
def provide_settings(self) -> Settings:
|
|
401
|
+
return Settings(tenant_id="mx")
|
|
402
|
+
|
|
403
|
+
@singleton
|
|
404
|
+
@provider
|
|
405
|
+
@inject
|
|
406
|
+
def provide_repository(self, settings: Settings) -> UserRepository:
|
|
407
|
+
return UserRepository(settings)
|
|
408
|
+
|
|
409
|
+
@singleton
|
|
410
|
+
@provider
|
|
411
|
+
@inject
|
|
412
|
+
def provide_service(self, repository: UserRepository) -> UserService:
|
|
413
|
+
return UserService(repository)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
container = Injector([AppModule()])
|
|
417
|
+
dependency_resolver = InjectorDependencyResolver(container)
|
|
418
|
+
app = APIGatewayHttpResolver(dependency_resolver=dependency_resolver)
|
|
419
|
+
|
|
420
|
+
@app.get("/users/<user_id>")
|
|
421
|
+
def get_user(
|
|
422
|
+
user_id: Annotated[int, Path()],
|
|
423
|
+
service: Annotated[UserService, Depends()],
|
|
424
|
+
):
|
|
425
|
+
return service.get_user(user_id)
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Disable dependency caching when a dependency must run every time:
|
|
429
|
+
|
|
430
|
+
```python
|
|
431
|
+
def next_counter() -> int:
|
|
432
|
+
...
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@app.get("/counter")
|
|
436
|
+
def counter(value: Annotated[int, Depends(next_counter, use_cache=False)]):
|
|
437
|
+
return {"value": value}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
For tests, set `app.dependency_overrides`:
|
|
441
|
+
|
|
442
|
+
```python
|
|
443
|
+
app.dependency_overrides[get_token] = lambda: "test-token"
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
## Exception Handling
|
|
447
|
+
|
|
448
|
+
Built-in error responses:
|
|
449
|
+
|
|
450
|
+
- request validation errors return `400`.
|
|
451
|
+
- `NotFoundError` returns `404`.
|
|
452
|
+
- `MethodNotAllowedError` returns `405`.
|
|
453
|
+
- `UnauthorizedError` returns `401`.
|
|
454
|
+
- `ForbiddenError` returns `403`.
|
|
455
|
+
|
|
456
|
+
Custom handlers can be registered per exception type. The most specific handler
|
|
457
|
+
wins.
|
|
458
|
+
|
|
459
|
+
```python
|
|
460
|
+
from modmex_lambda import Response
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
class DomainError(Exception):
|
|
464
|
+
pass
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@app.exception_handler(DomainError)
|
|
468
|
+
def on_domain_error(exc: DomainError):
|
|
469
|
+
return Response(status_code=409, body={"message": str(exc)})
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
If a custom exception handler raises, the resolver falls back to the default
|
|
473
|
+
error response when one exists.
|
|
474
|
+
|
|
475
|
+
## CORS
|
|
476
|
+
|
|
477
|
+
Pass `CORSConfig` to the resolver to add CORS headers and automatic preflight
|
|
478
|
+
behavior.
|
|
479
|
+
|
|
480
|
+
```python
|
|
481
|
+
from modmex_lambda import APIGatewayHttpResolver
|
|
482
|
+
from modmex_lambda.event_handler.cors import CORSConfig
|
|
483
|
+
|
|
484
|
+
app = APIGatewayHttpResolver(
|
|
485
|
+
cors=CORSConfig(
|
|
486
|
+
allow_origin="https://app.example",
|
|
487
|
+
allow_headers=["X-Tenant-Id"],
|
|
488
|
+
allow_credentials=True,
|
|
489
|
+
),
|
|
490
|
+
)
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## Parser
|
|
494
|
+
|
|
495
|
+
```python
|
|
496
|
+
from modmex_lambda.parser import event_parser, parse
|
|
497
|
+
|
|
498
|
+
parsed = parse(event={"name": "Ada"}, model=MyModel)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@event_parser(model=MyModel)
|
|
502
|
+
def lambda_handler(event: MyModel, context):
|
|
503
|
+
...
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
## Event Source Data Classes
|
|
507
|
+
|
|
508
|
+
Current scoped data classes include:
|
|
509
|
+
|
|
510
|
+
- `APIGatewayProxyEvent` and `APIGatewayProxyEventV2`
|
|
511
|
+
- `APIGatewayRestEvent` and `APIGatewayHttpEvent` aliases
|
|
512
|
+
- `APIGatewayAuthorizerEvent`
|
|
513
|
+
- `APIGatewayWebSocketEvent`
|
|
514
|
+
- Cognito User Pool trigger wrappers
|
|
515
|
+
|
|
516
|
+
```python
|
|
517
|
+
from modmex_lambda.data_classes import APIGatewayHttpEvent
|
|
518
|
+
from modmex_lambda.event_sources import event_source
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
@event_source(data_class=APIGatewayHttpEvent)
|
|
522
|
+
def lambda_handler(event: APIGatewayHttpEvent, context):
|
|
523
|
+
return {"path": event.path}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
## Validation
|
|
527
|
+
|
|
528
|
+
Modmex is the default validation and coercion engine. It is used for path,
|
|
529
|
+
query, header, cookie, and body parameters declared with `Annotated`, and it is
|
|
530
|
+
paired with the default JSON serializer for common values like enums, dates,
|
|
531
|
+
datetimes, decimals, and dataclasses.
|
|
532
|
+
|
|
533
|
+
```python
|
|
534
|
+
from datetime import date
|
|
535
|
+
from decimal import Decimal
|
|
536
|
+
from enum import Enum
|
|
537
|
+
from typing import Annotated
|
|
538
|
+
|
|
539
|
+
from modmex import BaseModel
|
|
540
|
+
from modmex_lambda import APIGatewayHttpResolver
|
|
541
|
+
from modmex_lambda.event_handler.params import Body, Path, Query
|
|
542
|
+
|
|
543
|
+
app = APIGatewayHttpResolver()
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
class Plan(str, Enum):
|
|
547
|
+
FREE = "free"
|
|
548
|
+
PRO = "pro"
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
class CreateAccount(BaseModel):
|
|
552
|
+
name: str
|
|
553
|
+
plan: Plan = Plan.FREE
|
|
554
|
+
trial_ends_on: date | None = None
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
class Account(BaseModel):
|
|
558
|
+
id: int
|
|
559
|
+
name: str
|
|
560
|
+
plan: Plan
|
|
561
|
+
balance: Decimal
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
@app.post("/accounts", status_code=201)
|
|
565
|
+
def create_account(payload: Annotated[CreateAccount, Body()]):
|
|
566
|
+
account = Account(
|
|
567
|
+
id=42,
|
|
568
|
+
name=payload.name,
|
|
569
|
+
plan=payload.plan,
|
|
570
|
+
balance=Decimal("0.00"),
|
|
571
|
+
)
|
|
572
|
+
# Return model_dump() when you want the response body to be a JSON object.
|
|
573
|
+
return account.model_dump()
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
@app.get("/accounts/<account_id>")
|
|
577
|
+
def get_account(
|
|
578
|
+
account_id: Annotated[int, Path()],
|
|
579
|
+
include_usage: Annotated[bool, Query()] = False,
|
|
580
|
+
):
|
|
581
|
+
return {
|
|
582
|
+
"id": account_id,
|
|
583
|
+
"include_usage": include_usage,
|
|
584
|
+
"created_on": date(2026, 1, 1),
|
|
585
|
+
}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
If validation fails, the resolver returns `400` with a compact validation error
|
|
589
|
+
payload. For domain-specific errors, register an exception handler and return a
|
|
590
|
+
`Response` with the shape your API expects.
|
|
591
|
+
|
|
592
|
+
## Logging
|
|
593
|
+
|
|
594
|
+
```python
|
|
595
|
+
from modmex_lambda import Logger
|
|
596
|
+
|
|
597
|
+
logger = Logger()
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def lambda_handler(event, context):
|
|
601
|
+
logger.set_context(context=context, event=event)
|
|
602
|
+
logger.append_keys(tenant_id="mx")
|
|
603
|
+
logger.info("request received")
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
The logger emits structured JSON, reads `LOG_LEVEL`, uses `SERVICE_NAME` or
|
|
607
|
+
`AWS_LAMBDA_FUNCTION_NAME` when no service is passed, and can extract Lambda
|
|
608
|
+
request IDs and API Gateway correlation IDs.
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
## Stream Handlers
|
|
612
|
+
|
|
613
|
+
Event-driven Lambda handlers live under `modmex_lambda.stream`. Use them for
|
|
614
|
+
listeners and triggers backed by SQS, SNS, Kinesis, DynamoDB Streams, S3, and
|
|
615
|
+
other common AWS event sources.
|
|
616
|
+
|
|
617
|
+
```python
|
|
618
|
+
from modmex_lambda.stream.flavors.cdc import CdcRule, ChangeDataCapture
|
|
619
|
+
from modmex_lambda.stream.rules_registry import RulesRegistry
|
|
620
|
+
from modmex_lambda.stream.sources import dynamodb_source
|
|
621
|
+
from modmex_lambda.stream.utils.contracts import DynamoDBEvent, Uow
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def to_user_created_event(uow: Uow[DynamoDBEvent]) -> DynamoDBEvent:
|
|
625
|
+
user = uow["event"]["raw"]["new"]
|
|
626
|
+
return {
|
|
627
|
+
"id": f"user-created:{user['id']}",
|
|
628
|
+
"type": "user-created",
|
|
629
|
+
"partition_key": user["id"],
|
|
630
|
+
"user": user,
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
rule: CdcRule[DynamoDBEvent] = {
|
|
635
|
+
"id": "publish-user-created",
|
|
636
|
+
"event_type": "USER-created",
|
|
637
|
+
"to_event": to_user_created_event,
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
registry = RulesRegistry().registry(
|
|
642
|
+
ChangeDataCapture[DynamoDBEvent](rule)
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
@dynamodb_source(registry)
|
|
647
|
+
def handler(event, context):
|
|
648
|
+
return {"statusCode": 200}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
Streams include source normalizers, rule registries, filters, flavors, AWS
|
|
652
|
+
connectors, and operators for common event-driven patterns.
|
|
653
|
+
|
|
654
|
+
## Stream Core Concepts
|
|
655
|
+
|
|
656
|
+
`modmex_lambda.stream` is source-first: a source parses a raw AWS Lambda event
|
|
657
|
+
into units of work, binds a registry, and runs one or more flavor pipelines.
|
|
658
|
+
|
|
659
|
+
```text
|
|
660
|
+
AWS event source -> Source -> UOWs -> RulesRegistry -> Flavor(s)
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
Use it when one Lambda batch should run several independent reactions while
|
|
664
|
+
keeping the rest of the batch moving if one record fails.
|
|
665
|
+
|
|
666
|
+
Common reactions include:
|
|
667
|
+
|
|
668
|
+
- publish domain events to EventBridge;
|
|
669
|
+
- store and correlate events;
|
|
670
|
+
- update DynamoDB materialized views;
|
|
671
|
+
- write messages to SNS or SQS;
|
|
672
|
+
- write objects to S3;
|
|
673
|
+
- execute domain tasks.
|
|
674
|
+
|
|
675
|
+
### Sources
|
|
676
|
+
|
|
677
|
+
A source answers “where did this Lambda event come from?” Built-in sources
|
|
678
|
+
normalize DynamoDB Streams, Kinesis, S3, SNS, and SQS events.
|
|
679
|
+
|
|
680
|
+
```python
|
|
681
|
+
from modmex_lambda.stream.sources import (
|
|
682
|
+
DynamoDBSource,
|
|
683
|
+
KinesisSource,
|
|
684
|
+
S3Source,
|
|
685
|
+
SnsSource,
|
|
686
|
+
SqsSource,
|
|
687
|
+
dynamodb_source,
|
|
688
|
+
kinesis_source,
|
|
689
|
+
s3_source,
|
|
690
|
+
sns_source,
|
|
691
|
+
sqs_source,
|
|
692
|
+
)
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
Class and decorator helpers accept the same runtime options:
|
|
696
|
+
|
|
697
|
+
```python
|
|
698
|
+
handler = KinesisSource(
|
|
699
|
+
registry,
|
|
700
|
+
concurrency=False,
|
|
701
|
+
on_next=on_next,
|
|
702
|
+
on_error=on_error,
|
|
703
|
+
on_completed=on_completed,
|
|
704
|
+
dependency_resolver=dependency_resolver,
|
|
705
|
+
).handle
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
`DynamoDBSource` also accepts parser options when table attributes use custom
|
|
709
|
+
names:
|
|
710
|
+
|
|
711
|
+
```python
|
|
712
|
+
DynamoDBSource(
|
|
713
|
+
registry,
|
|
714
|
+
parser_options={
|
|
715
|
+
"pk_fn": "pk",
|
|
716
|
+
"sk_fn": "sk",
|
|
717
|
+
"discriminator_fn": "discriminator",
|
|
718
|
+
"event_type_prefix": "ENTITY",
|
|
719
|
+
},
|
|
720
|
+
)
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
### Registry And Rules
|
|
724
|
+
|
|
725
|
+
A registry is the explicit list of flavor instances a source should run:
|
|
726
|
+
|
|
727
|
+
```python
|
|
728
|
+
from modmex_lambda.stream.flavors.cdc import ChangeDataCapture
|
|
729
|
+
from modmex_lambda.stream.flavors.materialize import Materialize
|
|
730
|
+
from modmex_lambda.stream.rules_registry import RulesRegistry
|
|
731
|
+
|
|
732
|
+
registry = (
|
|
733
|
+
RulesRegistry()
|
|
734
|
+
.registry(ChangeDataCapture({
|
|
735
|
+
"id": "thing-cdc",
|
|
736
|
+
"event_type": "THING-created",
|
|
737
|
+
"to_event": to_event,
|
|
738
|
+
}))
|
|
739
|
+
.registry(Materialize({
|
|
740
|
+
"id": "thing-materialized",
|
|
741
|
+
"event_type": "thing-created",
|
|
742
|
+
"to_update_request": to_update_request,
|
|
743
|
+
}))
|
|
744
|
+
)
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
All built-in flavor rules share:
|
|
748
|
+
|
|
749
|
+
- `id`: unique pipeline id.
|
|
750
|
+
- `event_type`: string, list of strings, or callable matcher.
|
|
751
|
+
- `filters`: optional content filters that receive `(uow, rule)`.
|
|
752
|
+
|
|
753
|
+
### Unit Of Work
|
|
754
|
+
|
|
755
|
+
Every source creates serializable UOW dictionaries:
|
|
756
|
+
|
|
757
|
+
```python
|
|
758
|
+
{
|
|
759
|
+
"pipeline": "thing-cdc",
|
|
760
|
+
"record": {...}, # original AWS record
|
|
761
|
+
"event": {
|
|
762
|
+
"id": "event-id",
|
|
763
|
+
"type": "thing-created",
|
|
764
|
+
"timestamp": 1548967022000,
|
|
765
|
+
"partition_key": "thing-1",
|
|
766
|
+
"tags": {...},
|
|
767
|
+
},
|
|
768
|
+
}
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
DynamoDB stream events also include `event["raw"]` with the mapped `new` and
|
|
772
|
+
`old` images.
|
|
773
|
+
|
|
774
|
+
### Runtime Callbacks
|
|
775
|
+
|
|
776
|
+
Sources expose lifecycle callbacks that are useful in tests, metrics, and
|
|
777
|
+
custom observability:
|
|
778
|
+
|
|
779
|
+
```python
|
|
780
|
+
completed = []
|
|
781
|
+
errors = []
|
|
782
|
+
items = []
|
|
783
|
+
|
|
784
|
+
handler = KinesisSource(
|
|
785
|
+
registry,
|
|
786
|
+
concurrency=False,
|
|
787
|
+
on_next=lambda pipeline_id, uow: items.append((pipeline_id, uow)),
|
|
788
|
+
on_error=lambda pipeline_id, error: errors.append((pipeline_id, error)),
|
|
789
|
+
on_completed=lambda pipeline_id: completed.append(pipeline_id),
|
|
790
|
+
).handle
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
Use `concurrency=False` in tests when deterministic order matters.
|
|
794
|
+
|
|
795
|
+
### Publisher Options
|
|
796
|
+
|
|
797
|
+
Flavors that publish to EventBridge use a shared publisher. Configure it with
|
|
798
|
+
`publisher_options`:
|
|
799
|
+
|
|
800
|
+
```python
|
|
801
|
+
ChangeDataCapture(
|
|
802
|
+
{
|
|
803
|
+
"id": "thing-cdc",
|
|
804
|
+
"event_type": "THING-created",
|
|
805
|
+
"to_event": to_event,
|
|
806
|
+
},
|
|
807
|
+
publisher_options={
|
|
808
|
+
"bus_name": "domain-events",
|
|
809
|
+
"source": "things.write-model",
|
|
810
|
+
"batch_size": 10,
|
|
811
|
+
},
|
|
812
|
+
)
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
If `bus_name` is omitted, the publisher uses `BUS_NAME`. If `source` is omitted,
|
|
816
|
+
it uses `BUS_SRC` or `custom`.
|
|
817
|
+
|
|
818
|
+
### Shared Dependency Injection
|
|
819
|
+
|
|
820
|
+
REST handlers and stream handlers can share the same `injector.Injector`. Add
|
|
821
|
+
`AwsConnectorsModule` when stream flavors should resolve the built-in AWS
|
|
822
|
+
connectors through DI.
|
|
823
|
+
|
|
824
|
+
```python
|
|
825
|
+
from injector import Injector
|
|
826
|
+
from modmex_lambda import AwsConnectorsModule, InjectorDependencyResolver
|
|
827
|
+
|
|
828
|
+
container = Injector([AwsConnectorsModule(), AppModule()])
|
|
829
|
+
dependency_resolver = InjectorDependencyResolver(container)
|
|
830
|
+
|
|
831
|
+
stream_handler = KinesisSource(
|
|
832
|
+
registry,
|
|
833
|
+
dependency_resolver=dependency_resolver,
|
|
834
|
+
).handle
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
## Event-Driven Patterns
|
|
838
|
+
|
|
839
|
+
Use stream flavors as named building blocks for common AWS Lambda event
|
|
840
|
+
workflows. Each flavor listens to normalized units of work (`uow`), filters by
|
|
841
|
+
rule, and then performs one focused job.
|
|
842
|
+
|
|
843
|
+
### Change Data Capture
|
|
844
|
+
|
|
845
|
+
Use `ChangeDataCapture` when a DynamoDB Stream represents changes in your
|
|
846
|
+
system of record and you want to publish domain events from those changes.
|
|
847
|
+
|
|
848
|
+
Typical flow:
|
|
849
|
+
|
|
850
|
+
```text
|
|
851
|
+
DynamoDB table -> DynamoDB Stream -> ChangeDataCapture -> EventBridge
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
For example, an inserted `USER` item can become a `user-created` event. This is
|
|
855
|
+
useful when your write model is DynamoDB and other services should react without
|
|
856
|
+
calling the writer directly.
|
|
857
|
+
|
|
858
|
+
```python
|
|
859
|
+
from modmex_lambda.stream.flavors.cdc import ChangeDataCapture
|
|
860
|
+
from modmex_lambda.stream.rules_registry import RulesRegistry
|
|
861
|
+
from modmex_lambda.stream.sources import DynamoDBSource
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def to_thing_created(uow):
|
|
865
|
+
thing = uow["event"]["raw"]["new"]
|
|
866
|
+
return {
|
|
867
|
+
"id": thing["id"],
|
|
868
|
+
"type": "thing-created",
|
|
869
|
+
"timestamp": uow["event"]["timestamp"],
|
|
870
|
+
"partition_key": thing["id"],
|
|
871
|
+
"thing": {
|
|
872
|
+
"id": thing["id"],
|
|
873
|
+
"name": thing["name"],
|
|
874
|
+
},
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
registry = RulesRegistry().registry(
|
|
879
|
+
ChangeDataCapture(
|
|
880
|
+
{
|
|
881
|
+
"id": "thing-cdc",
|
|
882
|
+
"event_type": "THING-created",
|
|
883
|
+
"to_event": to_thing_created,
|
|
884
|
+
},
|
|
885
|
+
publisher_options={
|
|
886
|
+
"bus_name": "domain-events",
|
|
887
|
+
"source": "things.write-model",
|
|
888
|
+
},
|
|
889
|
+
)
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
handler = DynamoDBSource(registry, concurrency=False).handle
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
### Materialize
|
|
896
|
+
|
|
897
|
+
Use `Materialize` when a service listens to domain events and updates a local
|
|
898
|
+
read model or projection.
|
|
899
|
+
|
|
900
|
+
Typical flow:
|
|
901
|
+
|
|
902
|
+
```text
|
|
903
|
+
EventBridge/Kinesis/SQS -> Materialize -> DynamoDB read model
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
For example, an `order-paid` event can update a customer summary table. You
|
|
907
|
+
materialize so queries stay local and fast, and each service owns the model it
|
|
908
|
+
needs instead of querying another service synchronously.
|
|
909
|
+
|
|
910
|
+
```python
|
|
911
|
+
from modmex_lambda.stream.flavors.materialize import Materialize
|
|
912
|
+
from modmex_lambda.stream.rules_registry import RulesRegistry
|
|
913
|
+
from modmex_lambda.stream.sources import KinesisSource
|
|
914
|
+
from modmex_lambda.stream.utils.dynamodb import update_expression
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def to_thing_view_update(uow):
|
|
918
|
+
thing = uow["event"]["thing"]
|
|
919
|
+
return {
|
|
920
|
+
"Key": {
|
|
921
|
+
"pk": thing["id"],
|
|
922
|
+
"sk": "THING",
|
|
923
|
+
},
|
|
924
|
+
**update_expression({
|
|
925
|
+
"name": thing["name"],
|
|
926
|
+
"timestamp": uow["event"]["timestamp"],
|
|
927
|
+
}),
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
registry = RulesRegistry().registry(
|
|
932
|
+
Materialize({
|
|
933
|
+
"id": "materialize-thing",
|
|
934
|
+
"event_type": "thing-created",
|
|
935
|
+
"to_update_request": to_thing_view_update,
|
|
936
|
+
})
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
handler = KinesisSource(registry, concurrency=False).handle
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
Use `split_on` and `split_target_field` when one event updates several records,
|
|
943
|
+
for example one `order-created` event materializing each order item.
|
|
944
|
+
|
|
945
|
+
### Control And Orchestration
|
|
946
|
+
|
|
947
|
+
Use the control pattern when one business process depends on several events
|
|
948
|
+
happening over time. It is useful for sagas, process managers, and long-running
|
|
949
|
+
coordination without a central synchronous transaction.
|
|
950
|
+
|
|
951
|
+
The usual pieces are:
|
|
952
|
+
|
|
953
|
+
- `Collect`: stores incoming events by a correlation key.
|
|
954
|
+
- `Correlate`: writes secondary correlation records when one event should be
|
|
955
|
+
findable by another key.
|
|
956
|
+
- `Evaluate`: checks collected/correlated events and emits higher-order events
|
|
957
|
+
when a condition is satisfied.
|
|
958
|
+
|
|
959
|
+
Typical flow:
|
|
960
|
+
|
|
961
|
+
```text
|
|
962
|
+
events -> Collect -> DynamoDB control table -> Evaluate -> EventBridge
|
|
963
|
+
^
|
|
964
|
+
|
|
|
965
|
+
Correlate
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
For example, an order saga might collect `order-created`,
|
|
969
|
+
`payment-authorized`, and `inventory-reserved`. Once `Evaluate` sees the
|
|
970
|
+
required events, it emits `order-ready-to-ship`. Downstream services can keep
|
|
971
|
+
reacting through EventBridge, SNS, SQS, or Kinesis.
|
|
972
|
+
|
|
973
|
+
First Lambda: listen to the event stream and collect the events by order id.
|
|
974
|
+
|
|
975
|
+
```python
|
|
976
|
+
from modmex_lambda.stream.flavors.collect import Collect
|
|
977
|
+
from modmex_lambda.stream.rules_registry import RulesRegistry
|
|
978
|
+
from modmex_lambda.stream.sources import KinesisSource
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
collect_registry = RulesRegistry().registry(
|
|
982
|
+
Collect({
|
|
983
|
+
"id": "collect-order-events",
|
|
984
|
+
"event_type": ["order-created", "payment-authorized"],
|
|
985
|
+
"correlation_key": "order.id",
|
|
986
|
+
"include_raw": False,
|
|
987
|
+
"expire": "order-correlation-expired",
|
|
988
|
+
})
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
handler = KinesisSource(collect_registry, concurrency=False).handle
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
Second Lambda: consume the DynamoDB stream from the collection table.
|
|
995
|
+
`Correlate` writes correlation records and `Evaluate` checks whether the
|
|
996
|
+
workflow is ready.
|
|
997
|
+
|
|
998
|
+
```python
|
|
999
|
+
from modmex_lambda.stream.flavors.correlate import Correlate
|
|
1000
|
+
from modmex_lambda.stream.flavors.evaluate import Evaluate
|
|
1001
|
+
from modmex_lambda.stream.rules_registry import RulesRegistry
|
|
1002
|
+
from modmex_lambda.stream.sources import DynamoDBSource
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def order_is_ready(uow):
|
|
1006
|
+
types = [event["type"] for event in uow["correlated"]]
|
|
1007
|
+
return "order-created" in types and "payment-authorized" in types
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
control_registry = (
|
|
1011
|
+
RulesRegistry()
|
|
1012
|
+
.registry(Correlate({
|
|
1013
|
+
"id": "correlate-order",
|
|
1014
|
+
"event_type": ["order-created", "payment-authorized"],
|
|
1015
|
+
"correlation_key": "order.id",
|
|
1016
|
+
"correlation_key_suffix": "ready",
|
|
1017
|
+
}))
|
|
1018
|
+
.registry(Evaluate(
|
|
1019
|
+
{
|
|
1020
|
+
"id": "order-ready",
|
|
1021
|
+
"event_type": ["order-created", "payment-authorized"],
|
|
1022
|
+
"correlation_key_suffix": "ready",
|
|
1023
|
+
"expression": order_is_ready,
|
|
1024
|
+
"emit": "order-ready",
|
|
1025
|
+
},
|
|
1026
|
+
publisher_options={
|
|
1027
|
+
"bus_name": "domain-events",
|
|
1028
|
+
"source": "orders.control",
|
|
1029
|
+
},
|
|
1030
|
+
))
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
handler = DynamoDBSource(control_registry, concurrency=False).handle
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
### Event Hub
|
|
1037
|
+
|
|
1038
|
+
EventBridge is a natural hub for domain events. Flavors like
|
|
1039
|
+
`ChangeDataCapture`, `Evaluate`, and the publisher operator can put events on
|
|
1040
|
+
the bus; consumers can then use `kinesis_source`, `sqs_source`, `sns_source`, or
|
|
1041
|
+
`dynamodb_source` depending on the integration shape.
|
|
1042
|
+
|
|
1043
|
+
Use SNS when the target contract is topic fan-out and SQS when the target needs
|
|
1044
|
+
durable queue semantics. Use EventBridge when events are part of the domain
|
|
1045
|
+
language and should be routed by event type, source, account, or bus.
|
|
1046
|
+
|
|
1047
|
+
### Other Flavors
|
|
1048
|
+
|
|
1049
|
+
- `Task`: runs arbitrary business logic for matching events and can optionally
|
|
1050
|
+
emit a follow-up event.
|
|
1051
|
+
- `Job`: uses a DynamoDB job record to drive paginated work and emit or update
|
|
1052
|
+
per-item results.
|
|
1053
|
+
- `Expired`: consumes DynamoDB TTL `REMOVE` records and publishes expiration
|
|
1054
|
+
events.
|
|
1055
|
+
- `S3`: writes objects to S3.
|
|
1056
|
+
- `Sns`: publishes messages to SNS.
|
|
1057
|
+
- `Update`: queries, gets, and updates DynamoDB records.
|
|
1058
|
+
|
|
1059
|
+
`Task` callbacks receive the current `Task` flavor instance. Use `task.rule`
|
|
1060
|
+
for rule configuration and `task.resolve(...)` for dependencies bound by the
|
|
1061
|
+
source or registry:
|
|
1062
|
+
|
|
1063
|
+
```python
|
|
1064
|
+
from pydash import get
|
|
1065
|
+
|
|
1066
|
+
from modmex_lambda.stream.flavors.task import Task
|
|
1067
|
+
from modmex_lambda.stream.rules_registry import RulesRegistry
|
|
1068
|
+
from modmex_lambda.stream.sources import KinesisSource
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
class ResumeAnalyzerService:
|
|
1072
|
+
def analyze(self, application):
|
|
1073
|
+
return {
|
|
1074
|
+
"application_id": application["id"],
|
|
1075
|
+
"confidence": 0.87,
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
def analyze_resume(uow, task):
|
|
1080
|
+
analyzer = task.resolve(ResumeAnalyzerService)
|
|
1081
|
+
return analyzer.analyze(get(uow, "event.application"))
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
registry = RulesRegistry().registry(
|
|
1085
|
+
Task({
|
|
1086
|
+
"id": "analyze-resume",
|
|
1087
|
+
"event_type": "resume-analysis-requested",
|
|
1088
|
+
"execute": analyze_resume,
|
|
1089
|
+
"emit": lambda uow, task, template: {
|
|
1090
|
+
**template,
|
|
1091
|
+
"type": "resume-analyzed",
|
|
1092
|
+
"application": {
|
|
1093
|
+
**get(uow, "event.application"),
|
|
1094
|
+
"analysis": uow["result"],
|
|
1095
|
+
"pipeline": task.rule["id"],
|
|
1096
|
+
},
|
|
1097
|
+
},
|
|
1098
|
+
})
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
handler = KinesisSource(registry, concurrency=False).handle
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
## Limitations
|
|
1105
|
+
|
|
1106
|
+
- OpenAPI/Swagger generation is not implemented.
|
|
1107
|
+
- Async API Gateway resolver pipelines are not implemented yet.
|