modmex-lambda 0.5.1__tar.gz → 0.5.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/PKG-INFO +5 -3
  2. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/README.md +4 -2
  3. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/logging.py +35 -0
  4. modmex_lambda-0.5.3/modmex_lambda/persistence/__init__.py +2 -0
  5. modmex_lambda-0.5.3/modmex_lambda/persistence/dynamodb/__init__.py +25 -0
  6. modmex_lambda-0.5.3/modmex_lambda/persistence/dynamodb/keys.py +133 -0
  7. modmex_lambda-0.5.3/modmex_lambda/persistence/dynamodb/stream_fields.py +121 -0
  8. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/pyproject.toml +1 -1
  9. modmex_lambda-0.5.3/tests/persistence/__init__.py +1 -0
  10. modmex_lambda-0.5.3/tests/persistence/test_dynamodb_keys.py +115 -0
  11. modmex_lambda-0.5.3/tests/persistence/test_dynamodb_stream_fields.py +127 -0
  12. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/test_logging.py +44 -0
  13. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/.github/workflows/ci.yml +0 -0
  14. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/.github/workflows/release.yml +0 -0
  15. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/.gitignore +0 -0
  16. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/LICENSE +0 -0
  17. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/__init__.py +0 -0
  18. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/connectors/__init__.py +0 -0
  19. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/connectors/cloudwatch.py +0 -0
  20. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/connectors/dynamodb.py +0 -0
  21. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/connectors/eventbridge.py +0 -0
  22. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/connectors/icloudwatch.py +0 -0
  23. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/connectors/idynamodb.py +0 -0
  24. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/connectors/ieventbridge.py +0 -0
  25. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/connectors/ilambda.py +0 -0
  26. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/connectors/is3.py +0 -0
  27. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/connectors/isns.py +0 -0
  28. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/connectors/isqs.py +0 -0
  29. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/connectors/lambda_.py +0 -0
  30. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/connectors/module.py +0 -0
  31. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/connectors/s3.py +0 -0
  32. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/connectors/sns.py +0 -0
  33. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/connectors/sqs.py +0 -0
  34. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/data_classes/__init__.py +0 -0
  35. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/data_classes/api_gateway_authorizer_event.py +0 -0
  36. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/data_classes/api_gateway_proxy_event.py +0 -0
  37. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/data_classes/api_gateway_websocket_event.py +0 -0
  38. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/data_classes/cognito_user_pool_event.py +0 -0
  39. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/data_classes/common.py +0 -0
  40. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/dependencies.py +0 -0
  41. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/__init__.py +0 -0
  42. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/api_gateway.py +0 -0
  43. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/constants.py +0 -0
  44. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/content_types.py +0 -0
  45. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/cors.py +0 -0
  46. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/dependencies/__init__.py +0 -0
  47. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/dependencies/compat.py +0 -0
  48. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/dependencies/dependant.py +0 -0
  49. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/dependencies/dependency_middleware.py +0 -0
  50. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/dependencies/depends.py +0 -0
  51. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/dependencies/params.py +0 -0
  52. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/dependencies/types.py +0 -0
  53. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/exception_handler.py +0 -0
  54. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/exceptions.py +0 -0
  55. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/gateway_response.py +0 -0
  56. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/middlewares.py +0 -0
  57. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/params.py +0 -0
  58. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/request.py +0 -0
  59. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/response.py +0 -0
  60. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/routing.py +0 -0
  61. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/routing_fallbacks.py +0 -0
  62. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_handler/types.py +0 -0
  63. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/event_sources.py +0 -0
  64. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/exceptions.py +0 -0
  65. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/params.py +0 -0
  66. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/parser.py +0 -0
  67. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/request.py +0 -0
  68. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/resolver.py +0 -0
  69. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/response.py +0 -0
  70. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/routing.py +0 -0
  71. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/shared/__init__.py +0 -0
  72. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/shared/cookies.py +0 -0
  73. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/shared/headers_serializer.py +0 -0
  74. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/shared/json_encoder.py +0 -0
  75. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/shared/types.py +0 -0
  76. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/__init__.py +0 -0
  77. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/events/__init__.py +0 -0
  78. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/events/dynamodb.py +0 -0
  79. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/events/kinesis.py +0 -0
  80. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/events/s3.py +0 -0
  81. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/events/sns.py +0 -0
  82. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/events/sqs.py +0 -0
  83. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/filters/__init__.py +0 -0
  84. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/filters/content.py +0 -0
  85. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/filters/event_type.py +0 -0
  86. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/filters/latch.py +0 -0
  87. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/filters/skip.py +0 -0
  88. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/flavors/__init__.py +0 -0
  89. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/flavors/base_flavor.py +0 -0
  90. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/flavors/cdc.py +0 -0
  91. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/flavors/collect.py +0 -0
  92. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/flavors/correlate.py +0 -0
  93. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/flavors/evaluate.py +0 -0
  94. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/flavors/expired.py +0 -0
  95. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/flavors/iflavor.py +0 -0
  96. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/flavors/job.py +0 -0
  97. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/flavors/materialize.py +0 -0
  98. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/flavors/s3.py +0 -0
  99. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/flavors/sns.py +0 -0
  100. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/flavors/task.py +0 -0
  101. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/flavors/update.py +0 -0
  102. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/irules_registry.py +0 -0
  103. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/operators/__init__.py +0 -0
  104. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/operators/cloudwatch.py +0 -0
  105. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/operators/dynamodb.py +0 -0
  106. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/operators/ioperator.py +0 -0
  107. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/operators/lambda_.py +0 -0
  108. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/operators/publisher.py +0 -0
  109. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/operators/s3.py +0 -0
  110. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/operators/sns.py +0 -0
  111. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/operators/sqs.py +0 -0
  112. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/rules_registry.py +0 -0
  113. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/runner.py +0 -0
  114. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/sources/__init__.py +0 -0
  115. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/sources/base.py +0 -0
  116. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/sources/dynamodb.py +0 -0
  117. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/sources/kinesis.py +0 -0
  118. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/sources/s3.py +0 -0
  119. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/sources/sns.py +0 -0
  120. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/sources/sqs.py +0 -0
  121. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/__init__.py +0 -0
  122. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/apigateway.py +0 -0
  123. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/aws.py +0 -0
  124. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/batch.py +0 -0
  125. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/cloudwatch.py +0 -0
  126. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/concurrency.py +0 -0
  127. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/contracts.py +0 -0
  128. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/data_classes/__init__.py +0 -0
  129. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/data_classes/dynamodb.py +0 -0
  130. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/decorators.py +0 -0
  131. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/dynamodb.py +0 -0
  132. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/eventbridge.py +0 -0
  133. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/faults.py +0 -0
  134. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/filters.py +0 -0
  135. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/json_encoder.py +0 -0
  136. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/lambda_.py +0 -0
  137. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/operators.py +0 -0
  138. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/pluralize.py +0 -0
  139. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/print.py +0 -0
  140. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/retry.py +0 -0
  141. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/s3.py +0 -0
  142. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/sns.py +0 -0
  143. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/split.py +0 -0
  144. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/sqs.py +0 -0
  145. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/tags.py +0 -0
  146. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/time.py +0 -0
  147. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/stream/utils/uow.py +0 -0
  148. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/tracing.py +0 -0
  149. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/modmex_lambda/validation.py +0 -0
  150. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/poetry.lock +0 -0
  151. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/__init__.py +0 -0
  152. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/conftest.py +0 -0
  153. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/connectors/__init__.py +0 -0
  154. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/connectors/conftest.py +0 -0
  155. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/connectors/test_cloudwatch.py +0 -0
  156. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/connectors/test_dynamodb.py +0 -0
  157. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/connectors/test_lazy_clients.py +0 -0
  158. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/connectors/test_s3.py +0 -0
  159. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/connectors/test_simple_connectors.py +0 -0
  160. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/connectors/test_sns.py +0 -0
  161. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/connectors/test_sqs.py +0 -0
  162. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/data_classes/test_api_gateway_proxy_event.py +0 -0
  163. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/data_classes/test_cognito_user_pool_event.py +0 -0
  164. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/data_classes/test_common.py +0 -0
  165. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/event_handler/__init__.py +0 -0
  166. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/event_handler/test_api_gateway.py +0 -0
  167. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/event_handler/test_cors.py +0 -0
  168. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/event_handler/test_dependencies.py +0 -0
  169. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/event_handler/test_exception_handler.py +0 -0
  170. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/event_handler/test_gateway_response.py +0 -0
  171. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/event_handler/test_request.py +0 -0
  172. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/event_handler/test_response.py +0 -0
  173. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/event_handler/test_routing.py +0 -0
  174. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/shared/test_cookies_headers.py +0 -0
  175. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/shared/test_json_encoder.py +0 -0
  176. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/__init__.py +0 -0
  177. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/conftest.py +0 -0
  178. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/events/test_dynamodb.py +0 -0
  179. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/events/test_kinesis.py +0 -0
  180. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/events/test_s3.py +0 -0
  181. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/events/test_sns.py +0 -0
  182. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/events/test_sqs.py +0 -0
  183. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/filters/__init__.py +0 -0
  184. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/filters/test_content.py +0 -0
  185. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/filters/test_event_type.py +0 -0
  186. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/filters/test_latch.py +0 -0
  187. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/filters/test_skip.py +0 -0
  188. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/flavors/__init__.py +0 -0
  189. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/flavors/source_events.py +0 -0
  190. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/flavors/test_base_flavor.py +0 -0
  191. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/flavors/test_cdc.py +0 -0
  192. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/flavors/test_collect.py +0 -0
  193. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/flavors/test_correlate.py +0 -0
  194. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/flavors/test_evaluate.py +0 -0
  195. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/flavors/test_expired.py +0 -0
  196. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/flavors/test_job.py +0 -0
  197. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/flavors/test_materialize.py +0 -0
  198. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/flavors/test_s3.py +0 -0
  199. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/flavors/test_sns.py +0 -0
  200. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/flavors/test_task.py +0 -0
  201. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/flavors/test_update.py +0 -0
  202. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/test_dependency_resolver.py +0 -0
  203. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/test_rules_registry.py +0 -0
  204. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/test_runner.py +0 -0
  205. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/test_runner_pipeline.py +0 -0
  206. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/test_sources.py +0 -0
  207. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/__init__.py +0 -0
  208. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/faults.py +0 -0
  209. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_apigateway.py +0 -0
  210. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_aws.py +0 -0
  211. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_batch.py +0 -0
  212. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_cloudwatch.py +0 -0
  213. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_concurrency.py +0 -0
  214. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_decorators.py +0 -0
  215. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_dynamodb.py +0 -0
  216. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_eventbridge.py +0 -0
  217. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_filters.py +0 -0
  218. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_json_encoder.py +0 -0
  219. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_lambda.py +0 -0
  220. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_operators.py +0 -0
  221. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_pluralize.py +0 -0
  222. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_print.py +0 -0
  223. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_retry.py +0 -0
  224. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_s3.py +0 -0
  225. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_sns.py +0 -0
  226. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_split.py +0 -0
  227. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_sqs.py +0 -0
  228. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_tags.py +0 -0
  229. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_time.py +0 -0
  230. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/stream/utils/test_uow.py +0 -0
  231. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/test_lazy_imports.py +0 -0
  232. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/test_parser_event_sources.py +0 -0
  233. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/test_reexports.py +0 -0
  234. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/test_tracing.py +0 -0
  235. {modmex_lambda-0.5.1 → modmex_lambda-0.5.3}/tests/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modmex-lambda
3
- Version: 0.5.1
3
+ Version: 0.5.3
4
4
  Summary: Ultra-lightweight AWS Lambda utilities for API Gateway routing and event handling.
5
5
  Author: Modmex
6
6
  License: MIT
@@ -597,15 +597,17 @@ from modmex_lambda import Logger
597
597
  logger = Logger()
598
598
 
599
599
 
600
+ @logger.inject_lambda_context(log_event=True)
600
601
  def lambda_handler(event, context):
601
- logger.set_context(context=context, event=event)
602
602
  logger.append_keys(tenant_id="mx")
603
603
  logger.info("request received")
604
604
  ```
605
605
 
606
606
  The logger emits structured JSON, reads `LOG_LEVEL`, uses `SERVICE_NAME` or
607
607
  `AWS_LAMBDA_FUNCTION_NAME` when no service is passed, and can extract Lambda
608
- request IDs and API Gateway correlation IDs.
608
+ request IDs and API Gateway correlation IDs. The `inject_lambda_context`
609
+ decorator resets logger state by default for warm Lambda invocations, injects
610
+ Lambda metadata, and can optionally log the incoming event.
609
611
 
610
612
  ## Tracing
611
613
 
@@ -574,15 +574,17 @@ from modmex_lambda import Logger
574
574
  logger = Logger()
575
575
 
576
576
 
577
+ @logger.inject_lambda_context(log_event=True)
577
578
  def lambda_handler(event, context):
578
- logger.set_context(context=context, event=event)
579
579
  logger.append_keys(tenant_id="mx")
580
580
  logger.info("request received")
581
581
  ```
582
582
 
583
583
  The logger emits structured JSON, reads `LOG_LEVEL`, uses `SERVICE_NAME` or
584
584
  `AWS_LAMBDA_FUNCTION_NAME` when no service is passed, and can extract Lambda
585
- request IDs and API Gateway correlation IDs.
585
+ request IDs and API Gateway correlation IDs. The `inject_lambda_context`
586
+ decorator resets logger state by default for warm Lambda invocations, injects
587
+ Lambda metadata, and can optionally log the incoming event.
586
588
 
587
589
  ## Tracing
588
590
 
@@ -7,6 +7,7 @@ import os
7
7
  import sys
8
8
  import traceback
9
9
  from datetime import datetime, timezone
10
+ from functools import wraps
10
11
  from threading import RLock
11
12
  from typing import Any, Callable, TextIO
12
13
 
@@ -58,6 +59,28 @@ class Logger:
58
59
  self._context = context
59
60
  self._event = event
60
61
 
62
+ def inject_lambda_context(
63
+ self,
64
+ func: Callable[..., Any] | None = None,
65
+ *,
66
+ log_event: bool = False,
67
+ ) -> Callable[..., Any]:
68
+ def decorator(handler: Callable[..., Any]) -> Callable[..., Any]:
69
+ @wraps(handler)
70
+ def wrapper(event: dict[str, Any], context: object, *args: Any, **kwargs: Any) -> Any:
71
+ self.clear_state()
72
+ self.set_context(context=context, event=event)
73
+ self._append_lambda_context_keys(context)
74
+ if log_event:
75
+ self.info("lambda event", event=event)
76
+ return handler(event, context, *args, **kwargs)
77
+
78
+ return wrapper
79
+
80
+ if func is None:
81
+ return decorator
82
+ return decorator(func)
83
+
61
84
  def append_keys(self, **kwargs: Any) -> None:
62
85
  with self._lock:
63
86
  self._persistent_keys.update(kwargs)
@@ -138,6 +161,18 @@ class Logger:
138
161
  return message % args
139
162
  return message
140
163
 
164
+ def _append_lambda_context_keys(self, context: object | None) -> None:
165
+ if context is None:
166
+ return
167
+
168
+ keys = {
169
+ "function_name": getattr(context, "function_name", None),
170
+ "function_memory_size": getattr(context, "memory_limit_in_mb", None),
171
+ "function_arn": getattr(context, "invoked_function_arn", None),
172
+ "function_request_id": getattr(context, "aws_request_id", None),
173
+ }
174
+ self.append_keys(**{key: value for key, value in keys.items() if value is not None})
175
+
141
176
  def _extract_request_id(self) -> str | None:
142
177
  if self._context is None:
143
178
  return None
@@ -0,0 +1,2 @@
1
+ """Persistence helpers for common modmex-lambda application patterns."""
2
+
@@ -0,0 +1,25 @@
1
+ """DynamoDB persistence helpers."""
2
+
3
+ from modmex_lambda.persistence.dynamodb.keys import (
4
+ AggregateKeyStrategy,
5
+ KeyStrategy,
6
+ SingleEntityKeyStrategy,
7
+ TenantPartitionKeyStrategy,
8
+ TenantScopedSortKeyStrategy,
9
+ )
10
+ from modmex_lambda.persistence.dynamodb.stream_fields import (
11
+ DefaultStreamFieldsStrategy,
12
+ StreamFieldsStrategy,
13
+ stream_entity_fields,
14
+ )
15
+
16
+ __all__ = [
17
+ "AggregateKeyStrategy",
18
+ "KeyStrategy",
19
+ "SingleEntityKeyStrategy",
20
+ "DefaultStreamFieldsStrategy",
21
+ "StreamFieldsStrategy",
22
+ "TenantPartitionKeyStrategy",
23
+ "TenantScopedSortKeyStrategy",
24
+ "stream_entity_fields",
25
+ ]
@@ -0,0 +1,133 @@
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
@@ -0,0 +1,121 @@
1
+ """Fields used by modmex-lambda stream-compatible DynamoDB items."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from abc import ABC, abstractmethod
7
+ from dataclasses import dataclass, field
8
+ from typing import Any
9
+
10
+ from modmex_lambda.stream.utils.time import now, ttl as stream_ttl
11
+
12
+
13
+ def stream_entity_fields(
14
+ discriminator: str,
15
+ *,
16
+ timestamp: int,
17
+ deleted: bool | None = None,
18
+ latched: bool = False,
19
+ ttl: int | None = None,
20
+ awsregion: str | None = None,
21
+ ) -> dict[str, Any]:
22
+ """Build standard fields consumed by modmex-lambda stream processors."""
23
+
24
+ fields = {
25
+ "discriminator": discriminator,
26
+ "deleted": deleted,
27
+ "latched": latched,
28
+ "ttl": ttl,
29
+ "awsregion": awsregion if awsregion is not None else os.getenv("REGION"),
30
+ "timestamp": timestamp,
31
+ }
32
+ return fields
33
+
34
+
35
+ class StreamFieldsStrategy(ABC):
36
+ """Build stream-compatible item fields for DynamoDB writes."""
37
+
38
+ @abstractmethod
39
+ def fields_for_save(
40
+ self,
41
+ data: dict[str, Any],
42
+ *,
43
+ timestamp: int | None = None,
44
+ ttl: int | None = None,
45
+ ) -> dict[str, Any]:
46
+ raise NotImplementedError
47
+
48
+ @abstractmethod
49
+ def fields_for_delete(
50
+ self,
51
+ data: dict[str, Any],
52
+ *,
53
+ timestamp: int | None = None,
54
+ ttl: int | None = None,
55
+ ) -> dict[str, Any]:
56
+ raise NotImplementedError
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class DefaultStreamFieldsStrategy(StreamFieldsStrategy):
61
+ """Default stream field contract used by modmex-lambda stream processors."""
62
+
63
+ discriminator: str
64
+ key_fields: tuple[str, ...] = field(default=("pk", "sk"))
65
+ use_ttl: bool = False
66
+ days_ttl: int = 30
67
+
68
+ def fields_for_save(
69
+ self,
70
+ data: dict[str, Any],
71
+ *,
72
+ timestamp: int | None = None,
73
+ ttl: int | None = None,
74
+ ) -> dict[str, Any]:
75
+ timestamp = self._timestamp(timestamp)
76
+ return {
77
+ **self._without_key_fields(data),
78
+ **stream_entity_fields(
79
+ self.discriminator,
80
+ timestamp=timestamp,
81
+ deleted=None,
82
+ latched=False,
83
+ ttl=self._ttl(timestamp, ttl),
84
+ ),
85
+ }
86
+
87
+ def fields_for_delete(
88
+ self,
89
+ data: dict[str, Any],
90
+ *,
91
+ timestamp: int | None = None,
92
+ ttl: int | None = None,
93
+ ) -> dict[str, Any]:
94
+ timestamp = self._timestamp(timestamp)
95
+ return {
96
+ **self._without_key_fields(data),
97
+ **stream_entity_fields(
98
+ self.discriminator,
99
+ timestamp=timestamp,
100
+ deleted=True,
101
+ latched=False,
102
+ ttl=self._ttl(timestamp, ttl),
103
+ ),
104
+ }
105
+
106
+ def _without_key_fields(self, data: dict[str, Any]) -> dict[str, Any]:
107
+ return {
108
+ key: value
109
+ for key, value in data.items()
110
+ if key not in self.key_fields
111
+ }
112
+
113
+ def _timestamp(self, timestamp: int | None) -> int:
114
+ return timestamp if timestamp is not None else now()
115
+
116
+ def _ttl(self, timestamp: int, ttl: int | None) -> int | None:
117
+ if ttl is not None:
118
+ return ttl
119
+ if self.use_ttl:
120
+ return stream_ttl(timestamp, self.days_ttl)
121
+ return None
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "modmex-lambda"
7
- version = "0.5.1"
7
+ version = "0.5.3"
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,115 @@
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_entity_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-1", "sk": "thing"}
34
+ assert strategy.key_for_entity(Thing(id="thing-2")) == {"pk": "thing-2", "sk": "thing"}
35
+ assert strategy.key_for_entity({"id": "thing-3"}) == {"pk": "thing-3", "sk": "thing"}
36
+
37
+
38
+ def test_tenant_scoped_sort_key_strategy_uses_entity_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-1",
43
+ "sk": "thing#acme",
44
+ }
45
+ assert strategy.key_for_entity(Thing(id="thing-2", tenant_id="modmex")) == {
46
+ "pk": "thing-2",
47
+ "sk": "thing#modmex",
48
+ }
49
+ assert strategy.key_for_entity(Thing(id="thing-3", tenant_id="ignored"), tenant_id="acme") == {
50
+ "pk": "thing-3",
51
+ "sk": "thing#acme",
52
+ }
53
+
54
+
55
+ def test_tenant_scoped_sort_key_strategy_supports_custom_field_and_separator() -> None:
56
+ strategy = TenantScopedSortKeyStrategy("thing", separator=":", tenant_field="account_id")
57
+
58
+ assert strategy.key_for_entity(Thing(id="thing-1", account_id="acct-1")) == {
59
+ "pk": "thing-1",
60
+ "sk": "thing:acct-1",
61
+ }
62
+
63
+
64
+ def test_tenant_partition_key_strategy_uses_tenant_as_pk_and_entity_scoped_sk() -> None:
65
+ strategy = TenantPartitionKeyStrategy("thing")
66
+
67
+ assert strategy.key_for_id("thing-1", tenant_id="acme") == {
68
+ "pk": "acme",
69
+ "sk": "thing#thing-1",
70
+ }
71
+ assert strategy.key_for_entity(Thing(id="thing-2", tenant_id="modmex")) == {
72
+ "pk": "modmex",
73
+ "sk": "thing#thing-2",
74
+ }
75
+
76
+
77
+ def test_tenant_partition_key_strategy_supports_custom_field_and_separator() -> None:
78
+ strategy = TenantPartitionKeyStrategy("thing", separator=":", tenant_field="account_id")
79
+
80
+ assert strategy.key_for_entity(Thing(id="thing-1", account_id="acct-1")) == {
81
+ "pk": "acct-1",
82
+ "sk": "thing:thing-1",
83
+ }
84
+
85
+
86
+ def test_aggregate_key_strategy_uses_aggregate_pk_and_entity_sk() -> None:
87
+ strategy = AggregateKeyStrategy("order", "item")
88
+
89
+ assert strategy.key_for_id("item-1", aggregate_id="order-1") == {
90
+ "pk": "order#order-1",
91
+ "sk": "item#item-1",
92
+ }
93
+ assert strategy.key_for_entity(Thing(id="item-2", aggregate_id="order-2")) == {
94
+ "pk": "order#order-2",
95
+ "sk": "item#item-2",
96
+ }
97
+
98
+
99
+ def test_aggregate_key_strategy_supports_custom_field_and_separator() -> None:
100
+ strategy = AggregateKeyStrategy("order", "item", separator=":", aggregate_field="order_id")
101
+
102
+ assert strategy.key_for_entity(Thing(id="item-1", order_id="order-1")) == {
103
+ "pk": "order:order-1",
104
+ "sk": "item:item-1",
105
+ }
106
+
107
+
108
+ def test_required_context_errors_are_explicit() -> None:
109
+ strategy = TenantPartitionKeyStrategy("thing")
110
+
111
+ with pytest.raises(KeyError, match="tenant_id"):
112
+ strategy.key_for_id("thing-1")
113
+
114
+ with pytest.raises(AttributeError, match="tenant_id"):
115
+ strategy.key_for_entity(Thing(id="thing-1"))
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from modmex_lambda.persistence.dynamodb import (
6
+ DefaultStreamFieldsStrategy,
7
+ StreamFieldsStrategy,
8
+ stream_entity_fields,
9
+ )
10
+
11
+
12
+ def test_stream_entity_fields_builds_standard_stream_contract_fields(monkeypatch) -> None:
13
+ monkeypatch.setenv("REGION", "us-west-2")
14
+
15
+ fields = stream_entity_fields(
16
+ "thing",
17
+ timestamp=1548967022000,
18
+ deleted=True,
19
+ latched=False,
20
+ ttl=1549053422,
21
+ )
22
+
23
+ assert fields == {
24
+ "discriminator": "thing",
25
+ "deleted": True,
26
+ "latched": False,
27
+ "ttl": 1549053422,
28
+ "awsregion": "us-west-2",
29
+ "timestamp": 1548967022000,
30
+ }
31
+
32
+
33
+ def test_stream_entity_fields_allows_explicit_region(monkeypatch) -> None:
34
+ monkeypatch.setenv("REGION", "us-west-2")
35
+
36
+ fields = stream_entity_fields(
37
+ "thing",
38
+ timestamp=1548967022000,
39
+ awsregion="us-east-1",
40
+ )
41
+
42
+ assert fields == {
43
+ "discriminator": "thing",
44
+ "deleted": None,
45
+ "latched": False,
46
+ "ttl": None,
47
+ "awsregion": "us-east-1",
48
+ "timestamp": 1548967022000,
49
+ }
50
+
51
+
52
+ def test_stream_fields_strategy_is_abstract() -> None:
53
+ with pytest.raises(TypeError):
54
+ StreamFieldsStrategy()
55
+
56
+
57
+ def test_default_stream_fields_strategy_builds_save_fields(monkeypatch) -> None:
58
+ monkeypatch.setenv("REGION", "us-west-2")
59
+ strategy = DefaultStreamFieldsStrategy("thing")
60
+
61
+ fields = strategy.fields_for_save(
62
+ {
63
+ "pk": "thing-1",
64
+ "sk": "thing",
65
+ "name": "Desk",
66
+ "discriminator": "wrong",
67
+ "timestamp": 1,
68
+ },
69
+ timestamp=1548967022000,
70
+ ttl=1549053422,
71
+ )
72
+
73
+ assert fields == {
74
+ "name": "Desk",
75
+ "discriminator": "thing",
76
+ "deleted": None,
77
+ "latched": False,
78
+ "ttl": 1549053422,
79
+ "awsregion": "us-west-2",
80
+ "timestamp": 1548967022000,
81
+ }
82
+
83
+
84
+ def test_default_stream_fields_strategy_builds_delete_fields(monkeypatch) -> None:
85
+ monkeypatch.setenv("REGION", "us-west-2")
86
+ strategy = DefaultStreamFieldsStrategy("thing")
87
+
88
+ fields = strategy.fields_for_delete(
89
+ {
90
+ "pk": "thing-1",
91
+ "sk": "thing",
92
+ "name": "Desk",
93
+ "deleted": None,
94
+ },
95
+ timestamp=1548967022000,
96
+ )
97
+
98
+ assert fields == {
99
+ "name": "Desk",
100
+ "discriminator": "thing",
101
+ "deleted": True,
102
+ "latched": False,
103
+ "ttl": None,
104
+ "awsregion": "us-west-2",
105
+ "timestamp": 1548967022000,
106
+ }
107
+
108
+
109
+ def test_default_stream_fields_strategy_can_calculate_timestamp(monkeypatch) -> None:
110
+ monkeypatch.setattr("modmex_lambda.persistence.dynamodb.stream_fields.now", lambda: 1548967022000)
111
+ monkeypatch.setenv("REGION", "us-west-2")
112
+ strategy = DefaultStreamFieldsStrategy("thing")
113
+
114
+ fields = strategy.fields_for_save({"name": "Desk"})
115
+
116
+ assert fields["timestamp"] == 1548967022000
117
+ assert fields["ttl"] is None
118
+
119
+
120
+ def test_default_stream_fields_strategy_can_calculate_ttl_from_configuration(monkeypatch) -> None:
121
+ monkeypatch.setattr("modmex_lambda.persistence.dynamodb.stream_fields.now", lambda: 1000)
122
+ strategy = DefaultStreamFieldsStrategy("thing", use_ttl=True, days_ttl=2)
123
+
124
+ fields = strategy.fields_for_save({"name": "Desk"})
125
+
126
+ assert fields["timestamp"] == 1000
127
+ assert fields["ttl"] == 172801