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.
Files changed (233) hide show
  1. modmex_lambda-0.4.0/PKG-INFO +1107 -0
  2. modmex_lambda-0.4.0/README.md +1084 -0
  3. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/__init__.py +20 -11
  4. modmex_lambda-0.4.0/modmex_lambda/connectors/__init__.py +28 -0
  5. modmex_lambda-0.4.0/modmex_lambda/connectors/cloudwatch.py +17 -0
  6. modmex_lambda-0.4.0/modmex_lambda/connectors/dynamodb.py +123 -0
  7. modmex_lambda-0.4.0/modmex_lambda/connectors/eventbridge.py +19 -0
  8. modmex_lambda-0.4.0/modmex_lambda/connectors/icloudwatch.py +14 -0
  9. modmex_lambda-0.4.0/modmex_lambda/connectors/idynamodb.py +68 -0
  10. modmex_lambda-0.4.0/modmex_lambda/connectors/ieventbridge.py +14 -0
  11. modmex_lambda-0.4.0/modmex_lambda/connectors/ilambda.py +14 -0
  12. modmex_lambda-0.4.0/modmex_lambda/connectors/is3.py +30 -0
  13. modmex_lambda-0.4.0/modmex_lambda/connectors/isns.py +20 -0
  14. modmex_lambda-0.4.0/modmex_lambda/connectors/isqs.py +21 -0
  15. modmex_lambda-0.4.0/modmex_lambda/connectors/lambda_.py +17 -0
  16. modmex_lambda-0.4.0/modmex_lambda/connectors/module.py +67 -0
  17. modmex_lambda-0.4.0/modmex_lambda/connectors/s3.py +43 -0
  18. modmex_lambda-0.4.0/modmex_lambda/connectors/sns.py +31 -0
  19. modmex_lambda-0.4.0/modmex_lambda/connectors/sqs.py +31 -0
  20. modmex_lambda-0.4.0/modmex_lambda/data_classes/__init__.py +132 -0
  21. modmex_lambda-0.4.0/modmex_lambda/dependencies.py +106 -0
  22. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/__init__.py +11 -10
  23. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/api_gateway.py +7 -7
  24. modmex_lambda-0.4.0/modmex_lambda/event_handler/dependencies/__init__.py +37 -0
  25. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/dependencies/depends.py +4 -52
  26. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/routing.py +43 -1
  27. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/types.py +1 -1
  28. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/logging.py +48 -14
  29. modmex_lambda-0.4.0/modmex_lambda/stream/__init__.py +3 -0
  30. modmex_lambda-0.4.0/modmex_lambda/stream/events/dynamodb.py +123 -0
  31. modmex_lambda-0.4.0/modmex_lambda/stream/events/kinesis.py +44 -0
  32. modmex_lambda-0.4.0/modmex_lambda/stream/events/s3.py +66 -0
  33. modmex_lambda-0.4.0/modmex_lambda/stream/events/sns.py +54 -0
  34. modmex_lambda-0.4.0/modmex_lambda/stream/events/sqs.py +46 -0
  35. modmex_lambda-0.4.0/modmex_lambda/stream/filters/content.py +9 -0
  36. modmex_lambda-0.4.0/modmex_lambda/stream/filters/event_type.py +17 -0
  37. modmex_lambda-0.4.0/modmex_lambda/stream/filters/latch.py +21 -0
  38. modmex_lambda-0.4.0/modmex_lambda/stream/filters/skip.py +18 -0
  39. modmex_lambda-0.4.0/modmex_lambda/stream/flavors/__init__.py +0 -0
  40. modmex_lambda-0.4.0/modmex_lambda/stream/flavors/base_flavor.py +140 -0
  41. modmex_lambda-0.4.0/modmex_lambda/stream/flavors/cdc.py +70 -0
  42. modmex_lambda-0.4.0/modmex_lambda/stream/flavors/collect.py +105 -0
  43. modmex_lambda-0.4.0/modmex_lambda/stream/flavors/correlate.py +114 -0
  44. modmex_lambda-0.4.0/modmex_lambda/stream/flavors/evaluate.py +235 -0
  45. modmex_lambda-0.4.0/modmex_lambda/stream/flavors/expired.py +104 -0
  46. modmex_lambda-0.4.0/modmex_lambda/stream/flavors/iflavor.py +19 -0
  47. modmex_lambda-0.4.0/modmex_lambda/stream/flavors/job.py +294 -0
  48. modmex_lambda-0.4.0/modmex_lambda/stream/flavors/materialize.py +65 -0
  49. modmex_lambda-0.4.0/modmex_lambda/stream/flavors/s3.py +63 -0
  50. modmex_lambda-0.4.0/modmex_lambda/stream/flavors/sns.py +59 -0
  51. modmex_lambda-0.4.0/modmex_lambda/stream/flavors/task.py +135 -0
  52. modmex_lambda-0.4.0/modmex_lambda/stream/flavors/update.py +171 -0
  53. modmex_lambda-0.4.0/modmex_lambda/stream/irules_registry.py +18 -0
  54. modmex_lambda-0.4.0/modmex_lambda/stream/operators/__init__.py +1 -0
  55. modmex_lambda-0.4.0/modmex_lambda/stream/operators/cloudwatch.py +29 -0
  56. modmex_lambda-0.4.0/modmex_lambda/stream/operators/dynamodb.py +391 -0
  57. modmex_lambda-0.4.0/modmex_lambda/stream/operators/ioperator.py +14 -0
  58. modmex_lambda-0.4.0/modmex_lambda/stream/operators/lambda_.py +29 -0
  59. modmex_lambda-0.4.0/modmex_lambda/stream/operators/publisher.py +99 -0
  60. modmex_lambda-0.4.0/modmex_lambda/stream/operators/s3.py +131 -0
  61. modmex_lambda-0.4.0/modmex_lambda/stream/operators/sns.py +58 -0
  62. modmex_lambda-0.4.0/modmex_lambda/stream/operators/sqs.py +83 -0
  63. modmex_lambda-0.4.0/modmex_lambda/stream/rules_registry.py +38 -0
  64. modmex_lambda-0.4.0/modmex_lambda/stream/runner.py +93 -0
  65. modmex_lambda-0.4.0/modmex_lambda/stream/sources/__init__.py +53 -0
  66. modmex_lambda-0.4.0/modmex_lambda/stream/sources/base.py +60 -0
  67. modmex_lambda-0.4.0/modmex_lambda/stream/sources/dynamodb.py +71 -0
  68. modmex_lambda-0.4.0/modmex_lambda/stream/sources/kinesis.py +51 -0
  69. modmex_lambda-0.4.0/modmex_lambda/stream/sources/s3.py +51 -0
  70. modmex_lambda-0.4.0/modmex_lambda/stream/sources/sns.py +51 -0
  71. modmex_lambda-0.4.0/modmex_lambda/stream/sources/sqs.py +51 -0
  72. modmex_lambda-0.4.0/modmex_lambda/stream/utils/__init__.py +0 -0
  73. modmex_lambda-0.4.0/modmex_lambda/stream/utils/apigateway.py +41 -0
  74. modmex_lambda-0.4.0/modmex_lambda/stream/utils/aws.py +6 -0
  75. modmex_lambda-0.4.0/modmex_lambda/stream/utils/batch.py +9 -0
  76. modmex_lambda-0.4.0/modmex_lambda/stream/utils/cloudwatch.py +11 -0
  77. modmex_lambda-0.4.0/modmex_lambda/stream/utils/concurrency.py +54 -0
  78. modmex_lambda-0.4.0/modmex_lambda/stream/utils/contracts.py +45 -0
  79. modmex_lambda-0.4.0/modmex_lambda/stream/utils/data_classes/__init__.py +0 -0
  80. modmex_lambda-0.4.0/modmex_lambda/stream/utils/data_classes/dynamodb.py +17 -0
  81. modmex_lambda-0.4.0/modmex_lambda/stream/utils/decorators.py +9 -0
  82. modmex_lambda-0.4.0/modmex_lambda/stream/utils/dynamodb.py +140 -0
  83. modmex_lambda-0.4.0/modmex_lambda/stream/utils/eventbridge.py +25 -0
  84. modmex_lambda-0.4.0/modmex_lambda/stream/utils/faults.py +100 -0
  85. modmex_lambda-0.4.0/modmex_lambda/stream/utils/filters.py +15 -0
  86. modmex_lambda-0.4.0/modmex_lambda/stream/utils/json_encoder.py +12 -0
  87. modmex_lambda-0.4.0/modmex_lambda/stream/utils/lambda_.py +11 -0
  88. modmex_lambda-0.4.0/modmex_lambda/stream/utils/operators.py +76 -0
  89. modmex_lambda-0.4.0/modmex_lambda/stream/utils/opt.py +15 -0
  90. modmex_lambda-0.4.0/modmex_lambda/stream/utils/pluralize.py +13 -0
  91. modmex_lambda-0.4.0/modmex_lambda/stream/utils/print.py +24 -0
  92. modmex_lambda-0.4.0/modmex_lambda/stream/utils/retry.py +28 -0
  93. modmex_lambda-0.4.0/modmex_lambda/stream/utils/s3.py +52 -0
  94. modmex_lambda-0.4.0/modmex_lambda/stream/utils/sns.py +11 -0
  95. modmex_lambda-0.4.0/modmex_lambda/stream/utils/split.py +32 -0
  96. modmex_lambda-0.4.0/modmex_lambda/stream/utils/sqs.py +19 -0
  97. modmex_lambda-0.4.0/modmex_lambda/stream/utils/tags.py +33 -0
  98. modmex_lambda-0.4.0/modmex_lambda/stream/utils/time.py +17 -0
  99. modmex_lambda-0.4.0/modmex_lambda/stream/utils/uow.py +110 -0
  100. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/poetry.lock +173 -22
  101. modmex_lambda-0.4.0/pyproject.toml +50 -0
  102. modmex_lambda-0.4.0/tests/__init__.py +0 -0
  103. modmex_lambda-0.4.0/tests/connectors/__init__.py +0 -0
  104. modmex_lambda-0.4.0/tests/connectors/conftest.py +12 -0
  105. modmex_lambda-0.4.0/tests/connectors/test_cloudwatch.py +46 -0
  106. modmex_lambda-0.4.0/tests/connectors/test_dynamodb.py +344 -0
  107. modmex_lambda-0.4.0/tests/connectors/test_lazy_clients.py +73 -0
  108. modmex_lambda-0.4.0/tests/connectors/test_s3.py +59 -0
  109. modmex_lambda-0.4.0/tests/connectors/test_simple_connectors.py +74 -0
  110. modmex_lambda-0.4.0/tests/connectors/test_sns.py +39 -0
  111. modmex_lambda-0.4.0/tests/connectors/test_sqs.py +41 -0
  112. modmex_lambda-0.4.0/tests/event_handler/__init__.py +0 -0
  113. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/event_handler/test_api_gateway.py +51 -33
  114. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/event_handler/test_dependencies.py +1 -1
  115. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/event_handler/test_routing.py +21 -1
  116. modmex_lambda-0.4.0/tests/stream/__init__.py +0 -0
  117. modmex_lambda-0.4.0/tests/stream/conftest.py +14 -0
  118. modmex_lambda-0.4.0/tests/stream/events/test_dynamodb.py +122 -0
  119. modmex_lambda-0.4.0/tests/stream/events/test_kinesis.py +26 -0
  120. modmex_lambda-0.4.0/tests/stream/events/test_s3.py +54 -0
  121. modmex_lambda-0.4.0/tests/stream/events/test_sns.py +28 -0
  122. modmex_lambda-0.4.0/tests/stream/events/test_sqs.py +27 -0
  123. modmex_lambda-0.4.0/tests/stream/filters/__init__.py +0 -0
  124. modmex_lambda-0.4.0/tests/stream/filters/test_content.py +38 -0
  125. modmex_lambda-0.4.0/tests/stream/filters/test_event_type.py +38 -0
  126. modmex_lambda-0.4.0/tests/stream/filters/test_latch.py +66 -0
  127. modmex_lambda-0.4.0/tests/stream/filters/test_skip.py +16 -0
  128. modmex_lambda-0.4.0/tests/stream/flavors/__init__.py +0 -0
  129. modmex_lambda-0.4.0/tests/stream/flavors/source_events.py +58 -0
  130. modmex_lambda-0.4.0/tests/stream/flavors/test_base_flavor.py +66 -0
  131. modmex_lambda-0.4.0/tests/stream/flavors/test_cdc.py +268 -0
  132. modmex_lambda-0.4.0/tests/stream/flavors/test_collect.py +89 -0
  133. modmex_lambda-0.4.0/tests/stream/flavors/test_correlate.py +132 -0
  134. modmex_lambda-0.4.0/tests/stream/flavors/test_evaluate.py +522 -0
  135. modmex_lambda-0.4.0/tests/stream/flavors/test_expired.py +104 -0
  136. modmex_lambda-0.4.0/tests/stream/flavors/test_job.py +280 -0
  137. modmex_lambda-0.4.0/tests/stream/flavors/test_materialize.py +137 -0
  138. modmex_lambda-0.4.0/tests/stream/flavors/test_s3.py +118 -0
  139. modmex_lambda-0.4.0/tests/stream/flavors/test_sns.py +80 -0
  140. modmex_lambda-0.4.0/tests/stream/flavors/test_task.py +223 -0
  141. modmex_lambda-0.4.0/tests/stream/flavors/test_update.py +194 -0
  142. modmex_lambda-0.4.0/tests/stream/test_dependency_resolver.py +58 -0
  143. modmex_lambda-0.4.0/tests/stream/test_rules_registry.py +93 -0
  144. modmex_lambda-0.4.0/tests/stream/test_runner.py +76 -0
  145. modmex_lambda-0.4.0/tests/stream/test_runner_pipeline.py +217 -0
  146. modmex_lambda-0.4.0/tests/stream/test_sources.py +315 -0
  147. modmex_lambda-0.4.0/tests/stream/utils/__init__.py +0 -0
  148. modmex_lambda-0.4.0/tests/stream/utils/faults.py +117 -0
  149. modmex_lambda-0.4.0/tests/stream/utils/test_apigateway.py +29 -0
  150. modmex_lambda-0.4.0/tests/stream/utils/test_aws.py +12 -0
  151. modmex_lambda-0.4.0/tests/stream/utils/test_batch.py +20 -0
  152. modmex_lambda-0.4.0/tests/stream/utils/test_cloudwatch.py +41 -0
  153. modmex_lambda-0.4.0/tests/stream/utils/test_concurrency.py +61 -0
  154. modmex_lambda-0.4.0/tests/stream/utils/test_decorators.py +18 -0
  155. modmex_lambda-0.4.0/tests/stream/utils/test_dynamodb.py +248 -0
  156. modmex_lambda-0.4.0/tests/stream/utils/test_eventbridge.py +127 -0
  157. modmex_lambda-0.4.0/tests/stream/utils/test_filters.py +23 -0
  158. modmex_lambda-0.4.0/tests/stream/utils/test_json_encoder.py +21 -0
  159. modmex_lambda-0.4.0/tests/stream/utils/test_lambda.py +35 -0
  160. modmex_lambda-0.4.0/tests/stream/utils/test_operators.py +191 -0
  161. modmex_lambda-0.4.0/tests/stream/utils/test_pluralize.py +10 -0
  162. modmex_lambda-0.4.0/tests/stream/utils/test_print.py +70 -0
  163. modmex_lambda-0.4.0/tests/stream/utils/test_retry.py +29 -0
  164. modmex_lambda-0.4.0/tests/stream/utils/test_s3.py +144 -0
  165. modmex_lambda-0.4.0/tests/stream/utils/test_sns.py +38 -0
  166. modmex_lambda-0.4.0/tests/stream/utils/test_split.py +85 -0
  167. modmex_lambda-0.4.0/tests/stream/utils/test_sqs.py +80 -0
  168. modmex_lambda-0.4.0/tests/stream/utils/test_tags.py +50 -0
  169. modmex_lambda-0.4.0/tests/stream/utils/test_time.py +36 -0
  170. modmex_lambda-0.4.0/tests/stream/utils/test_uow.py +109 -0
  171. modmex_lambda-0.4.0/tests/test_lazy_imports.py +71 -0
  172. modmex_lambda-0.4.0/tests/test_logging.py +110 -0
  173. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/test_reexports.py +11 -5
  174. modmex_lambda-0.2.0/PKG-INFO +0 -446
  175. modmex_lambda-0.2.0/README.md +0 -431
  176. modmex_lambda-0.2.0/modmex_lambda/data_classes/__init__.py +0 -49
  177. modmex_lambda-0.2.0/modmex_lambda/event_handler/dependencies/__init__.py +0 -13
  178. modmex_lambda-0.2.0/pyproject.toml +0 -30
  179. modmex_lambda-0.2.0/tests/test_logging.py +0 -50
  180. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/.github/workflows/ci.yml +0 -0
  181. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/.github/workflows/release.yml +0 -0
  182. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/.gitignore +0 -0
  183. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/LICENSE +0 -0
  184. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/data_classes/api_gateway_authorizer_event.py +0 -0
  185. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/data_classes/api_gateway_proxy_event.py +0 -0
  186. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/data_classes/api_gateway_websocket_event.py +0 -0
  187. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/data_classes/cognito_user_pool_event.py +0 -0
  188. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/data_classes/common.py +0 -0
  189. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/constants.py +0 -0
  190. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/content_types.py +0 -0
  191. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/cors.py +0 -0
  192. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/dependencies/compat.py +0 -0
  193. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/dependencies/dependant.py +0 -0
  194. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/dependencies/dependency_middleware.py +0 -0
  195. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/dependencies/params.py +0 -0
  196. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/dependencies/types.py +0 -0
  197. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/exception_handler.py +0 -0
  198. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/exceptions.py +0 -0
  199. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/gateway_response.py +0 -0
  200. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/middlewares.py +0 -0
  201. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/params.py +0 -0
  202. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/request.py +0 -0
  203. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/response.py +0 -0
  204. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_handler/routing_fallbacks.py +0 -0
  205. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/event_sources.py +0 -0
  206. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/exceptions.py +0 -0
  207. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/params.py +0 -0
  208. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/parser.py +0 -0
  209. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/request.py +0 -0
  210. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/resolver.py +0 -0
  211. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/response.py +0 -0
  212. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/routing.py +0 -0
  213. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/shared/__init__.py +0 -0
  214. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/shared/cookies.py +0 -0
  215. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/shared/headers_serializer.py +0 -0
  216. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/shared/json_encoder.py +0 -0
  217. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/shared/types.py +0 -0
  218. {modmex_lambda-0.2.0/tests → modmex_lambda-0.4.0/modmex_lambda/stream/events}/__init__.py +0 -0
  219. {modmex_lambda-0.2.0/tests/event_handler → modmex_lambda-0.4.0/modmex_lambda/stream/filters}/__init__.py +0 -0
  220. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/modmex_lambda/validation.py +0 -0
  221. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/conftest.py +0 -0
  222. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/data_classes/test_api_gateway_proxy_event.py +0 -0
  223. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/data_classes/test_cognito_user_pool_event.py +0 -0
  224. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/data_classes/test_common.py +0 -0
  225. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/event_handler/test_cors.py +0 -0
  226. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/event_handler/test_exception_handler.py +0 -0
  227. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/event_handler/test_gateway_response.py +0 -0
  228. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/event_handler/test_request.py +0 -0
  229. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/event_handler/test_response.py +0 -0
  230. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/shared/test_cookies_headers.py +0 -0
  231. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/shared/test_json_encoder.py +0 -0
  232. {modmex_lambda-0.2.0 → modmex_lambda-0.4.0}/tests/test_parser_event_sources.py +0 -0
  233. {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
+ [![CI](https://img.shields.io/github/actions/workflow/status/modmex/modmex/ci.yml?branch=main&logo=github&label=CI)](https://github.com/modmex/modmex/actions/workflows/ci.yml)
29
+ [![Coverage](https://img.shields.io/codecov/c/github/modmex/modmex-lambda?label=coverage)](https://codecov.io/gh/modmex/modmex-lambda)
30
+ [![PyPI](https://img.shields.io/pypi/v/modmex-lambda.svg)](https://pypi.org/project/modmex-lambda/)
31
+ [![Python Versions](https://img.shields.io/pypi/pyversions/modmex-lambda.svg)](https://pypi.org/project/modmex-lambda/)
32
+ [![License](https://img.shields.io/github/license/modmex/modmex-lambda.svg)](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.