modmex-lambda 0.5.5__tar.gz → 0.5.7__tar.gz

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