edda-framework 0.7.0__tar.gz → 0.9.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 (174) hide show
  1. {edda_framework-0.7.0 → edda_framework-0.9.0}/Justfile +110 -0
  2. {edda_framework-0.7.0 → edda_framework-0.9.0}/PKG-INFO +109 -9
  3. {edda_framework-0.7.0 → edda_framework-0.9.0}/README.md +108 -8
  4. {edda_framework-0.7.0 → edda_framework-0.9.0}/demo_app.py +422 -36
  5. edda_framework-0.9.0/docs/api/reference.md +143 -0
  6. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/core-features/durable-execution/replay.md +1 -1
  7. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/core-features/events/wait-event.md +17 -10
  8. edda_framework-0.9.0/docs/core-features/messages.md +590 -0
  9. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/core-features/retry.md +5 -3
  10. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/core-features/workflows-activities.md +175 -2
  11. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/examples/events.md +8 -8
  12. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/getting-started/first-workflow.md +1 -1
  13. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/index.md +6 -4
  14. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/integrations/mcp.md +87 -1
  15. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/integrations/opentelemetry.md +1 -1
  16. edda_framework-0.9.0/docs/integrations/pydantic-rpc.md +193 -0
  17. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/__init__.py +39 -5
  18. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/app.py +383 -223
  19. edda_framework-0.9.0/edda/channels.py +1017 -0
  20. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/compensation.py +22 -22
  21. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/context.py +105 -52
  22. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/integrations/opentelemetry/hooks.py +7 -2
  23. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/locking.py +130 -67
  24. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/replay.py +312 -82
  25. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/storage/models.py +142 -24
  26. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/storage/protocol.py +539 -118
  27. edda_framework-0.9.0/edda/storage/sqlalchemy_storage.py +3447 -0
  28. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/viewer_ui/app.py +6 -1
  29. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/viewer_ui/data_service.py +19 -22
  30. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/workflow.py +43 -0
  31. {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/cancellable_workflow.py +3 -4
  32. {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/event_waiting_app.py +9 -9
  33. {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/event_waiting_workflow.py +1 -1
  34. {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/event_waiting_workflow_complete.py +3 -3
  35. edda_framework-0.9.0/examples/long_running_loop.py +274 -0
  36. {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/mcp/order_processing_mcp.py +5 -5
  37. {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/mcp/prompts_example.py +5 -5
  38. {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/mcp/remote_server_example.py +3 -3
  39. {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/mcp/simple_mcp_server.py +1 -1
  40. edda_framework-0.9.0/examples/message_passing.py +263 -0
  41. {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/observability_with_logfire.py +6 -6
  42. {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/observability_with_opentelemetry.py +3 -4
  43. edda_framework-0.9.0/examples/pydantic_rpc_integration.py +461 -0
  44. {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/pydantic_saga.py +1 -0
  45. {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/retry_example.py +18 -19
  46. {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/retry_with_compensation.py +29 -32
  47. {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/typeddict_example.py +0 -1
  48. {edda_framework-0.7.0 → edda_framework-0.9.0}/pyproject.toml +7 -2
  49. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/conftest.py +43 -10
  50. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/integrations/mcp/test_cancel.py +2 -2
  51. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/integrations/mcp/test_server.py +4 -4
  52. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_atomic_wait_event.py +80 -52
  53. edda_framework-0.9.0/tests/test_auto_migration.py +389 -0
  54. edda_framework-0.9.0/tests/test_channel_competing.py +432 -0
  55. edda_framework-0.9.0/tests/test_channel_transactional.py +351 -0
  56. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_distributed_event_delivery.py +28 -15
  57. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_events.py +115 -57
  58. edda_framework-0.9.0/tests/test_instance_id_routing.py +366 -0
  59. edda_framework-0.9.0/tests/test_message_cleanup.py +198 -0
  60. edda_framework-0.9.0/tests/test_message_delivery_lock.py +313 -0
  61. edda_framework-0.9.0/tests/test_messages.py +477 -0
  62. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_multidb_storage.py +48 -17
  63. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_pydantic_events.py +34 -27
  64. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_received_event.py +56 -38
  65. edda_framework-0.9.0/tests/test_recur.py +581 -0
  66. edda_framework-0.9.0/tests/test_recur_cleanup.py +329 -0
  67. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_stale_workflow_recovery.py +3 -0
  68. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_storage.py +54 -18
  69. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_wait_timer.py +12 -12
  70. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_workflow_cancellation.py +46 -22
  71. edda_framework-0.9.0/tests/test_workflow_resumption.py +259 -0
  72. {edda_framework-0.7.0 → edda_framework-0.9.0}/uv.lock +195 -17
  73. {edda_framework-0.7.0 → edda_framework-0.9.0}/viewer_app.py +2 -3
  74. {edda_framework-0.7.0 → edda_framework-0.9.0}/zensical.toml +15 -0
  75. edda_framework-0.7.0/edda/events.py +0 -505
  76. edda_framework-0.7.0/edda/storage/sqlalchemy_storage.py +0 -1909
  77. {edda_framework-0.7.0 → edda_framework-0.9.0}/.github/workflows/ci.yml +0 -0
  78. {edda_framework-0.7.0 → edda_framework-0.9.0}/.github/workflows/docs.yml +0 -0
  79. {edda_framework-0.7.0 → edda_framework-0.9.0}/.github/workflows/release.yml +0 -0
  80. {edda_framework-0.7.0 → edda_framework-0.9.0}/.gitignore +0 -0
  81. {edda_framework-0.7.0 → edda_framework-0.9.0}/.python-version +0 -0
  82. {edda_framework-0.7.0 → edda_framework-0.9.0}/LICENSE +0 -0
  83. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/core-features/events/cloudevents-http-binding.md +0 -0
  84. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/core-features/hooks.md +0 -0
  85. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/core-features/saga-compensation.md +0 -0
  86. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/core-features/transactional-outbox.md +0 -0
  87. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/examples/ecommerce.md +0 -0
  88. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/examples/fastapi-integration.md +0 -0
  89. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/examples/saga.md +0 -0
  90. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/examples/simple.md +0 -0
  91. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/getting-started/concepts.md +0 -0
  92. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/getting-started/installation.md +0 -0
  93. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/getting-started/quick-start.md +0 -0
  94. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/images/cloudevents-cli-trigger.png +0 -0
  95. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/images/compensation-execution.png +0 -0
  96. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/images/detail-page-loan-approval.png +0 -0
  97. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/images/detail-page-match-case.png +0 -0
  98. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
  99. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/images/start-workflow-form-pydantic.png +0 -0
  100. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/images/wait-event-visualization.png +0 -0
  101. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/images/workflow-list-view.png +0 -0
  102. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/images/workflow-selection-dropdown.png +0 -0
  103. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/setup.md +0 -0
  104. {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/visualization.md +0 -0
  105. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/activity.py +0 -0
  106. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/exceptions.py +0 -0
  107. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/hooks.py +0 -0
  108. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/integrations/__init__.py +0 -0
  109. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/integrations/mcp/__init__.py +0 -0
  110. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/integrations/mcp/decorators.py +0 -0
  111. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/integrations/mcp/server.py +0 -0
  112. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/integrations/opentelemetry/__init__.py +0 -0
  113. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/outbox/__init__.py +0 -0
  114. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/outbox/relayer.py +0 -0
  115. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/outbox/transactional.py +0 -0
  116. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/pydantic_utils.py +0 -0
  117. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/retry.py +0 -0
  118. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/serialization/__init__.py +0 -0
  119. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/serialization/base.py +0 -0
  120. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/serialization/json.py +0 -0
  121. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/storage/__init__.py +0 -0
  122. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/viewer_ui/__init__.py +0 -0
  123. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/viewer_ui/components.py +0 -0
  124. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/viewer_ui/theme.py +0 -0
  125. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/visualizer/__init__.py +0 -0
  126. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/visualizer/ast_analyzer.py +0 -0
  127. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/visualizer/mermaid_generator.py +0 -0
  128. {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/wsgi.py +0 -0
  129. {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/__init__.py +0 -0
  130. {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/compensation_workflow.py +0 -0
  131. {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/mcp/README.md +0 -0
  132. {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/simple_workflow.py +0 -0
  133. {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/with_outbox.py +0 -0
  134. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/__init__.py +0 -0
  135. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/integrations/__init__.py +0 -0
  136. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/integrations/mcp/__init__.py +0 -0
  137. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/integrations/mcp/test_integration.py +0 -0
  138. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/integrations/mcp/test_jsonrpc.py +0 -0
  139. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/integrations/mcp/test_prompts.py +0 -0
  140. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/integrations/opentelemetry/__init__.py +0 -0
  141. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/integrations/opentelemetry/test_hooks.py +0 -0
  142. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_activity.py +0 -0
  143. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_activity_retry.py +0 -0
  144. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_activity_sync.py +0 -0
  145. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_app.py +0 -0
  146. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_ast_analyzer.py +0 -0
  147. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_binary_data.py +0 -0
  148. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_cloudevents_http_binding.py +0 -0
  149. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_compensation.py +0 -0
  150. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_compensation_crash_recovery.py.wip +0 -0
  151. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_concurrent_outbox.py +0 -0
  152. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_context.py +0 -0
  153. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_ctx_session.py +0 -0
  154. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_lock_race_condition.py +0 -0
  155. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_lock_timeout_customization.py +0 -0
  156. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_locking.py +0 -0
  157. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_outbox.py +0 -0
  158. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_pydantic_activity.py +0 -0
  159. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_pydantic_enum.py +0 -0
  160. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_pydantic_saga.py +0 -0
  161. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_pydantic_utils.py +0 -0
  162. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_replay.py +0 -0
  163. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_retry_policy.py +0 -0
  164. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_saga_parameter_extraction.py +0 -0
  165. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_serialization.py +0 -0
  166. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_skip_locked.py +0 -0
  167. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_storage_mysql.py +0 -0
  168. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_storage_postgresql.py +0 -0
  169. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_transactions.py +0 -0
  170. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_viewer_pagination.py +0 -0
  171. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_viewer_pydantic_form.py +0 -0
  172. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_viewer_start_saga.py +0 -0
  173. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_workflow.py +0 -0
  174. {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_workflow_auto_register.py +0 -0
@@ -31,18 +31,22 @@ test-file FILE:
31
31
 
32
32
  # Format code with black
33
33
  format:
34
+ @uv sync --extra dev --quiet
34
35
  uv run black edda tests
35
36
 
36
37
  # Check code formatting
37
38
  format-check:
39
+ @uv sync --extra dev --quiet
38
40
  uv run black --check edda tests
39
41
 
40
42
  # Lint code with ruff
41
43
  lint:
44
+ @uv sync --extra dev --quiet
42
45
  uv run ruff check edda tests
43
46
 
44
47
  # Type check with mypy
45
48
  type-check:
49
+ @uv sync --extra dev --quiet
46
50
  uv run mypy edda
47
51
 
48
52
  # Run all checks (format, lint, type-check, test)
@@ -50,6 +54,7 @@ check: format-check lint type-check test
50
54
 
51
55
  # Auto-fix issues (format + lint with auto-fix)
52
56
  fix:
57
+ @uv sync --extra dev --quiet
53
58
  uv run black edda tests
54
59
  uv run ruff check --fix edda tests
55
60
 
@@ -116,6 +121,18 @@ viewer-demo DB='demo.db' PORT='8080':
116
121
  just viewer {{DB}} {{PORT}} "--import-module demo_app"
117
122
 
118
123
 
124
+ # Build documentation (clears cache for fresh API reference)
125
+ docs:
126
+ rm -rf .cache site
127
+ uv run zensical build
128
+
129
+ # Serve documentation locally (clears cache for fresh API reference)
130
+ docs-serve:
131
+ @lsof -ti :8000 | xargs kill -15 2>/dev/null || true
132
+ @sleep 1
133
+ rm -rf .cache site
134
+ uv run zensical serve
135
+
119
136
  # Clean build artifacts and caches
120
137
  clean:
121
138
  rm -rf .pytest_cache
@@ -123,9 +140,102 @@ clean:
123
140
  rm -rf .coverage
124
141
  rm -rf .mypy_cache
125
142
  rm -rf .ruff_cache
143
+ rm -rf .cache
144
+ rm -rf site
126
145
  rm -rf dist
127
146
  rm -rf build
128
147
  rm -rf *.egg-info
129
148
  find . -type d -name __pycache__ -exec rm -rf {} +
130
149
  find . -type f -name "*.pyc" -delete
131
150
  rm -f demo.db
151
+
152
+ # Helper recipe for point-to-point test (uses bash for command substitution)
153
+ _test-point-to-point LOG_FILE:
154
+ #!/usr/bin/env bash
155
+ set -e
156
+ echo "=== Test 3: Point-to-Point Mode (Direct Message) ==="
157
+ echo "Starting receiver..."
158
+ curl -s -X POST http://localhost:8001/ \
159
+ -H "Content-Type: application/cloudevents+json" \
160
+ -d '{"specversion":"1.0","type":"direct_message_receiver_workflow","source":"test","id":"receiver-1","data":{"receiver_id":"receiver-1"}}' > /dev/null
161
+ sleep 2
162
+ # Parse instance_id from server log (format: [RECEIVER] Instance ID: workflow-uuid)
163
+ # Strip ANSI color codes before grepping
164
+ INSTANCE_ID=$(sed 's/\x1b\[[0-9;]*m//g' "{{LOG_FILE}}" | grep -o '\[RECEIVER\] Instance ID: [^ ]*' | tail -1 | cut -d' ' -f4)
165
+ echo "Receiver instance_id: $INSTANCE_ID"
166
+ if [ -z "$INSTANCE_ID" ]; then
167
+ echo "ERROR: Could not extract instance_id from server log"
168
+ echo "Log content (stripped):"
169
+ sed 's/\x1b\[[0-9;]*m//g' "{{LOG_FILE}}" | grep -i receiver || echo "(no receiver logs found)"
170
+ exit 1
171
+ fi
172
+ echo "Sending direct message to receiver..."
173
+ curl -s -X POST http://localhost:8001/ \
174
+ -H "Content-Type: application/cloudevents+json" \
175
+ -d "{\"specversion\":\"1.0\",\"type\":\"direct_message_sender_workflow\",\"source\":\"test\",\"id\":\"sender-1\",\"data\":{\"target_instance_id\":\"$INSTANCE_ID\",\"message\":\"Hello from sender!\"}}"
176
+ echo ""
177
+ sleep 5
178
+ # Verify receiver got the message (strip ANSI codes)
179
+ if sed 's/\x1b\[[0-9;]*m//g' "{{LOG_FILE}}" | grep -q "\[RECEIVER\] Received message:"; then
180
+ echo "✓ Point-to-Point message delivered successfully!"
181
+ else
182
+ echo "✗ Point-to-Point message delivery failed"
183
+ fi
184
+
185
+ # Test message passing (competing, broadcast, and point-to-point modes)
186
+ test-messages:
187
+ #!/usr/bin/env bash
188
+ set -e
189
+ LOG_FILE=$(mktemp)
190
+ echo "=== Message Passing Test ==="
191
+ echo "Server log: $LOG_FILE"
192
+ echo "Starting demo app in background..."
193
+ rm -f demo.db
194
+ uv sync --extra server --quiet
195
+ PYTHONUNBUFFERED=1 uv run tsuno demo_app:application --bind 127.0.0.1:8001 --workers 1 > "$LOG_FILE" 2>&1 &
196
+ SERVER_PID=$!
197
+ sleep 2
198
+
199
+ echo ""
200
+ echo "=== Test 1: Competing Mode (Job Worker) ==="
201
+ echo "Starting worker..."
202
+ curl -s -X POST http://localhost:8001/ \
203
+ -H "Content-Type: application/cloudevents+json" \
204
+ -d '{"specversion":"1.0","type":"job_worker_workflow","source":"test","id":"worker-1","data":{"worker_id":"worker-1"}}'
205
+ echo ""
206
+ sleep 1
207
+ echo "Publishing job..."
208
+ curl -s -X POST http://localhost:8001/ \
209
+ -H "Content-Type: application/cloudevents+json" \
210
+ -d '{"specversion":"1.0","type":"job_publisher_workflow","source":"test","id":"job-1","data":{"task":"test-task"}}'
211
+ echo ""
212
+ sleep 2
213
+
214
+ echo ""
215
+ echo "=== Test 2: Broadcast Mode (Notification) ==="
216
+ echo "Starting notification service..."
217
+ curl -s -X POST http://localhost:8001/ \
218
+ -H "Content-Type: application/cloudevents+json" \
219
+ -d '{"specversion":"1.0","type":"notification_service_workflow","source":"test","id":"service-1","data":{"service_id":"service-1"}}'
220
+ echo ""
221
+ sleep 1
222
+ echo "Publishing notification..."
223
+ curl -s -X POST http://localhost:8001/ \
224
+ -H "Content-Type: application/cloudevents+json" \
225
+ -d '{"specversion":"1.0","type":"notification_publisher_workflow","source":"test","id":"notification-1","data":{"message":"Test notification"}}'
226
+ echo ""
227
+ sleep 2
228
+
229
+ just _test-point-to-point "$LOG_FILE"
230
+
231
+ echo ""
232
+ echo "=== Stopping demo app ==="
233
+ kill -15 $SERVER_PID 2>/dev/null || true
234
+ sleep 1
235
+
236
+ echo ""
237
+ echo "=== Server Log ==="
238
+ cat "$LOG_FILE"
239
+ rm -f "$LOG_FILE"
240
+ echo ""
241
+ echo "Done!"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edda-framework
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: Lightweight Durable Execution Framework
5
5
  Project-URL: Homepage, https://github.com/i2y/edda
6
6
  Project-URL: Documentation, https://github.com/i2y/edda#readme
@@ -65,6 +65,7 @@ Description-Content-Type: text/markdown
65
65
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
66
66
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
67
67
  [![Documentation](https://img.shields.io/badge/docs-latest-green.svg)](https://i2y.github.io/edda/)
68
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/i2y/edda)
68
69
 
69
70
  ## Overview
70
71
 
@@ -85,6 +86,7 @@ For detailed documentation, visit [https://i2y.github.io/edda/](https://i2y.gith
85
86
  - 📦 **Transactional Outbox**: Reliable event publishing with guaranteed delivery
86
87
  - ☁️ **CloudEvents Support**: Native support for CloudEvents protocol
87
88
  - ⏱️ **Event & Timer Waiting**: Free up worker resources while waiting for events or timers, resume on any available worker
89
+ - 📬 **Channel-based Messaging**: Actor-model style communication with competing (job queue) and broadcast (fan-out) modes
88
90
  - 🤖 **MCP Integration**: Expose durable workflows as AI tools via Model Context Protocol
89
91
  - 🌍 **ASGI/WSGI Support**: Deploy with your preferred server (uvicorn, gunicorn, uWSGI)
90
92
 
@@ -107,14 +109,14 @@ Edda's waiting functions make it ideal for time-based and event-driven business
107
109
  - **📦 Scheduled Notifications**: Shipping updates, subscription renewals, appointment reminders
108
110
 
109
111
  **Waiting functions**:
110
- - `wait_timer(duration_seconds)`: Wait for a relative duration
111
- - `wait_until(until_time)`: Wait until an absolute datetime (e.g., campaign end date)
112
+ - `sleep(seconds)`: Wait for a relative duration
113
+ - `sleep_until(target_time)`: Wait until an absolute datetime (e.g., campaign end date)
112
114
  - `wait_event(event_type)`: Wait for external events (near real-time response)
113
115
 
114
116
  ```python
115
117
  @workflow
116
118
  async def onboarding_reminder(ctx: WorkflowContext, user_id: str):
117
- await wait_timer(ctx, duration_seconds=3*24*60*60) # Wait 3 days
119
+ await sleep(ctx, seconds=3*24*60*60) # Wait 3 days
118
120
  if not await check_completed(ctx, user_id):
119
121
  await send_reminder(ctx, user_id)
120
122
  ```
@@ -166,7 +168,7 @@ graph TB
166
168
 
167
169
  - Multiple workers can run simultaneously across different pods/servers
168
170
  - Each workflow instance runs on only one worker at a time (automatic coordination)
169
- - `wait_event()` and `wait_timer()` free up worker resources while waiting, resume on any worker when event arrives or timer expires
171
+ - `wait_event()` and `sleep()` free up worker resources while waiting, resume on any worker when event arrives or timer expires
170
172
  - Automatic crash recovery with stale lock cleanup and workflow auto-resume
171
173
 
172
174
  ## Quick Start
@@ -486,7 +488,10 @@ Multiple workers can safely process workflows using database-based exclusive con
486
488
 
487
489
  app = EddaApp(
488
490
  db_url="postgresql://localhost/workflows", # Shared database for coordination
489
- service_name="order-service"
491
+ service_name="order-service",
492
+ # Connection pool settings (optional)
493
+ pool_size=5, # Concurrent connections
494
+ max_overflow=10, # Additional burst capacity
490
495
  )
491
496
  ```
492
497
 
@@ -614,10 +619,32 @@ async def payment_workflow(ctx: WorkflowContext, order_id: str):
614
619
  return payment_event.data
615
620
  ```
616
621
 
617
- **wait_timer() for time-based waiting**:
622
+ **ReceivedEvent attributes**: The `wait_event()` function returns a `ReceivedEvent` object:
623
+
624
+ ```python
625
+ event = await wait_event(ctx, "payment.completed")
626
+ amount = event.data["amount"] # Event payload (dict or bytes)
627
+ source = event.metadata.source # CloudEvents source
628
+ event_type = event.metadata.type # CloudEvents type
629
+ extensions = event.extensions # CloudEvents extensions
630
+ ```
631
+
632
+ **Timeout handling with EventTimeoutError**:
633
+
634
+ ```python
635
+ from edda import wait_event, EventTimeoutError
636
+
637
+ try:
638
+ event = await wait_event(ctx, "payment.completed", timeout_seconds=60)
639
+ except EventTimeoutError:
640
+ # Handle timeout (e.g., cancel order, send reminder)
641
+ await cancel_order(ctx, order_id)
642
+ ```
643
+
644
+ **sleep() for time-based waiting**:
618
645
 
619
646
  ```python
620
- from edda import wait_timer
647
+ from edda import sleep
621
648
 
622
649
  @workflow
623
650
  async def order_with_timeout(ctx: WorkflowContext, order_id: str):
@@ -625,7 +652,7 @@ async def order_with_timeout(ctx: WorkflowContext, order_id: str):
625
652
  await create_order(ctx, order_id)
626
653
 
627
654
  # Wait 60 seconds for payment
628
- await wait_timer(ctx, duration_seconds=60)
655
+ await sleep(ctx, seconds=60)
629
656
 
630
657
  # Check payment status
631
658
  return await check_payment(ctx, order_id)
@@ -639,6 +666,79 @@ async def order_with_timeout(ctx: WorkflowContext, order_id: str):
639
666
 
640
667
  **For technical details**, see [Multi-Worker Continuations](local-docs/distributed-coroutines.md).
641
668
 
669
+ ### Channel-based Messaging
670
+
671
+ Edda provides channel-based messaging for workflow-to-workflow communication with two delivery modes:
672
+
673
+ ```python
674
+ from edda import workflow, subscribe, receive, publish, send_to, WorkflowContext
675
+
676
+ # Job Worker - processes jobs exclusively (competing mode)
677
+ @workflow
678
+ async def job_worker(ctx: WorkflowContext, worker_id: str):
679
+ # Subscribe with competing mode - each job goes to ONE worker only
680
+ await subscribe(ctx, channel="jobs", mode="competing")
681
+
682
+ while True:
683
+ job = await receive(ctx, channel="jobs") # Get next job
684
+ await process_job(ctx, job.data)
685
+ await ctx.recur(worker_id) # Continue processing
686
+
687
+ # Notification Handler - receives ALL messages (broadcast mode)
688
+ @workflow
689
+ async def notification_handler(ctx: WorkflowContext, handler_id: str):
690
+ # Subscribe with broadcast mode - ALL handlers receive each message
691
+ await subscribe(ctx, channel="notifications", mode="broadcast")
692
+
693
+ while True:
694
+ msg = await receive(ctx, channel="notifications")
695
+ await send_notification(ctx, msg.data)
696
+ await ctx.recur(handler_id)
697
+
698
+ # Publish to channel (all subscribers or one competing subscriber)
699
+ await publish(ctx, channel="jobs", data={"task": "send_report"})
700
+
701
+ # Direct message to specific workflow instance
702
+ await send_to(ctx, instance_id="workflow-123", channel="approval", data={"approved": True})
703
+ ```
704
+
705
+ **Delivery modes**:
706
+ - **`competing`**: Each message goes to exactly ONE subscriber (job queue/task distribution)
707
+ - **`broadcast`**: Each message goes to ALL subscribers (notifications/fan-out)
708
+
709
+ **Key features**:
710
+ - **Channel-based messaging**: Messages are delivered to workflows waiting on specific channels
711
+ - **Competing vs Broadcast**: Choose semantics per subscription
712
+ - **Direct messaging**: `send_to()` for workflow-to-workflow communication
713
+ - **Database-backed**: All messages are persisted for durability
714
+ - **Lock-first delivery**: Safe for multi-worker environments
715
+
716
+ ### Workflow Recurrence
717
+
718
+ Long-running workflows can use `ctx.recur()` to restart with fresh history while maintaining the same instance ID. This is essential for workflows that run indefinitely (job workers, notification handlers, etc.):
719
+
720
+ ```python
721
+ from edda import workflow, subscribe, receive, WorkflowContext
722
+
723
+ @workflow
724
+ async def job_worker(ctx: WorkflowContext, worker_id: str):
725
+ await subscribe(ctx, channel="jobs", mode="competing")
726
+
727
+ # Process one job
728
+ job = await receive(ctx, channel="jobs")
729
+ await process_job(ctx, job.data)
730
+
731
+ # Archive history and restart with same instance_id
732
+ # Prevents unbounded history growth
733
+ await ctx.recur(worker_id)
734
+ ```
735
+
736
+ **Key benefits**:
737
+ - **Prevents history growth**: Archives old history, starts fresh
738
+ - **Maintains instance ID**: Same workflow continues logically
739
+ - **Preserves subscriptions**: Channel subscriptions survive recurrence
740
+ - **Enables infinite loops**: Essential for long-running workers
741
+
642
742
  ### ASGI Integration
643
743
 
644
744
  Edda runs as an ASGI application:
@@ -7,6 +7,7 @@
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
8
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
9
9
  [![Documentation](https://img.shields.io/badge/docs-latest-green.svg)](https://i2y.github.io/edda/)
10
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/i2y/edda)
10
11
 
11
12
  ## Overview
12
13
 
@@ -27,6 +28,7 @@ For detailed documentation, visit [https://i2y.github.io/edda/](https://i2y.gith
27
28
  - 📦 **Transactional Outbox**: Reliable event publishing with guaranteed delivery
28
29
  - ☁️ **CloudEvents Support**: Native support for CloudEvents protocol
29
30
  - ⏱️ **Event & Timer Waiting**: Free up worker resources while waiting for events or timers, resume on any available worker
31
+ - 📬 **Channel-based Messaging**: Actor-model style communication with competing (job queue) and broadcast (fan-out) modes
30
32
  - 🤖 **MCP Integration**: Expose durable workflows as AI tools via Model Context Protocol
31
33
  - 🌍 **ASGI/WSGI Support**: Deploy with your preferred server (uvicorn, gunicorn, uWSGI)
32
34
 
@@ -49,14 +51,14 @@ Edda's waiting functions make it ideal for time-based and event-driven business
49
51
  - **📦 Scheduled Notifications**: Shipping updates, subscription renewals, appointment reminders
50
52
 
51
53
  **Waiting functions**:
52
- - `wait_timer(duration_seconds)`: Wait for a relative duration
53
- - `wait_until(until_time)`: Wait until an absolute datetime (e.g., campaign end date)
54
+ - `sleep(seconds)`: Wait for a relative duration
55
+ - `sleep_until(target_time)`: Wait until an absolute datetime (e.g., campaign end date)
54
56
  - `wait_event(event_type)`: Wait for external events (near real-time response)
55
57
 
56
58
  ```python
57
59
  @workflow
58
60
  async def onboarding_reminder(ctx: WorkflowContext, user_id: str):
59
- await wait_timer(ctx, duration_seconds=3*24*60*60) # Wait 3 days
61
+ await sleep(ctx, seconds=3*24*60*60) # Wait 3 days
60
62
  if not await check_completed(ctx, user_id):
61
63
  await send_reminder(ctx, user_id)
62
64
  ```
@@ -108,7 +110,7 @@ graph TB
108
110
 
109
111
  - Multiple workers can run simultaneously across different pods/servers
110
112
  - Each workflow instance runs on only one worker at a time (automatic coordination)
111
- - `wait_event()` and `wait_timer()` free up worker resources while waiting, resume on any worker when event arrives or timer expires
113
+ - `wait_event()` and `sleep()` free up worker resources while waiting, resume on any worker when event arrives or timer expires
112
114
  - Automatic crash recovery with stale lock cleanup and workflow auto-resume
113
115
 
114
116
  ## Quick Start
@@ -428,7 +430,10 @@ Multiple workers can safely process workflows using database-based exclusive con
428
430
 
429
431
  app = EddaApp(
430
432
  db_url="postgresql://localhost/workflows", # Shared database for coordination
431
- service_name="order-service"
433
+ service_name="order-service",
434
+ # Connection pool settings (optional)
435
+ pool_size=5, # Concurrent connections
436
+ max_overflow=10, # Additional burst capacity
432
437
  )
433
438
  ```
434
439
 
@@ -556,10 +561,32 @@ async def payment_workflow(ctx: WorkflowContext, order_id: str):
556
561
  return payment_event.data
557
562
  ```
558
563
 
559
- **wait_timer() for time-based waiting**:
564
+ **ReceivedEvent attributes**: The `wait_event()` function returns a `ReceivedEvent` object:
565
+
566
+ ```python
567
+ event = await wait_event(ctx, "payment.completed")
568
+ amount = event.data["amount"] # Event payload (dict or bytes)
569
+ source = event.metadata.source # CloudEvents source
570
+ event_type = event.metadata.type # CloudEvents type
571
+ extensions = event.extensions # CloudEvents extensions
572
+ ```
573
+
574
+ **Timeout handling with EventTimeoutError**:
575
+
576
+ ```python
577
+ from edda import wait_event, EventTimeoutError
578
+
579
+ try:
580
+ event = await wait_event(ctx, "payment.completed", timeout_seconds=60)
581
+ except EventTimeoutError:
582
+ # Handle timeout (e.g., cancel order, send reminder)
583
+ await cancel_order(ctx, order_id)
584
+ ```
585
+
586
+ **sleep() for time-based waiting**:
560
587
 
561
588
  ```python
562
- from edda import wait_timer
589
+ from edda import sleep
563
590
 
564
591
  @workflow
565
592
  async def order_with_timeout(ctx: WorkflowContext, order_id: str):
@@ -567,7 +594,7 @@ async def order_with_timeout(ctx: WorkflowContext, order_id: str):
567
594
  await create_order(ctx, order_id)
568
595
 
569
596
  # Wait 60 seconds for payment
570
- await wait_timer(ctx, duration_seconds=60)
597
+ await sleep(ctx, seconds=60)
571
598
 
572
599
  # Check payment status
573
600
  return await check_payment(ctx, order_id)
@@ -581,6 +608,79 @@ async def order_with_timeout(ctx: WorkflowContext, order_id: str):
581
608
 
582
609
  **For technical details**, see [Multi-Worker Continuations](local-docs/distributed-coroutines.md).
583
610
 
611
+ ### Channel-based Messaging
612
+
613
+ Edda provides channel-based messaging for workflow-to-workflow communication with two delivery modes:
614
+
615
+ ```python
616
+ from edda import workflow, subscribe, receive, publish, send_to, WorkflowContext
617
+
618
+ # Job Worker - processes jobs exclusively (competing mode)
619
+ @workflow
620
+ async def job_worker(ctx: WorkflowContext, worker_id: str):
621
+ # Subscribe with competing mode - each job goes to ONE worker only
622
+ await subscribe(ctx, channel="jobs", mode="competing")
623
+
624
+ while True:
625
+ job = await receive(ctx, channel="jobs") # Get next job
626
+ await process_job(ctx, job.data)
627
+ await ctx.recur(worker_id) # Continue processing
628
+
629
+ # Notification Handler - receives ALL messages (broadcast mode)
630
+ @workflow
631
+ async def notification_handler(ctx: WorkflowContext, handler_id: str):
632
+ # Subscribe with broadcast mode - ALL handlers receive each message
633
+ await subscribe(ctx, channel="notifications", mode="broadcast")
634
+
635
+ while True:
636
+ msg = await receive(ctx, channel="notifications")
637
+ await send_notification(ctx, msg.data)
638
+ await ctx.recur(handler_id)
639
+
640
+ # Publish to channel (all subscribers or one competing subscriber)
641
+ await publish(ctx, channel="jobs", data={"task": "send_report"})
642
+
643
+ # Direct message to specific workflow instance
644
+ await send_to(ctx, instance_id="workflow-123", channel="approval", data={"approved": True})
645
+ ```
646
+
647
+ **Delivery modes**:
648
+ - **`competing`**: Each message goes to exactly ONE subscriber (job queue/task distribution)
649
+ - **`broadcast`**: Each message goes to ALL subscribers (notifications/fan-out)
650
+
651
+ **Key features**:
652
+ - **Channel-based messaging**: Messages are delivered to workflows waiting on specific channels
653
+ - **Competing vs Broadcast**: Choose semantics per subscription
654
+ - **Direct messaging**: `send_to()` for workflow-to-workflow communication
655
+ - **Database-backed**: All messages are persisted for durability
656
+ - **Lock-first delivery**: Safe for multi-worker environments
657
+
658
+ ### Workflow Recurrence
659
+
660
+ Long-running workflows can use `ctx.recur()` to restart with fresh history while maintaining the same instance ID. This is essential for workflows that run indefinitely (job workers, notification handlers, etc.):
661
+
662
+ ```python
663
+ from edda import workflow, subscribe, receive, WorkflowContext
664
+
665
+ @workflow
666
+ async def job_worker(ctx: WorkflowContext, worker_id: str):
667
+ await subscribe(ctx, channel="jobs", mode="competing")
668
+
669
+ # Process one job
670
+ job = await receive(ctx, channel="jobs")
671
+ await process_job(ctx, job.data)
672
+
673
+ # Archive history and restart with same instance_id
674
+ # Prevents unbounded history growth
675
+ await ctx.recur(worker_id)
676
+ ```
677
+
678
+ **Key benefits**:
679
+ - **Prevents history growth**: Archives old history, starts fresh
680
+ - **Maintains instance ID**: Same workflow continues logically
681
+ - **Preserves subscriptions**: Channel subscriptions survive recurrence
682
+ - **Enables infinite loops**: Essential for long-running workers
683
+
584
684
  ### ASGI Integration
585
685
 
586
686
  Edda runs as an ASGI application: