edda-framework 0.13.0__tar.gz → 0.14.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 (209) hide show
  1. {edda_framework-0.13.0 → edda_framework-0.14.0}/PKG-INFO +1 -1
  2. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/channels.py +27 -0
  3. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/storage/protocol.py +12 -0
  4. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/storage/sqlalchemy_storage.py +20 -0
  5. {edda_framework-0.13.0 → edda_framework-0.14.0}/pyproject.toml +1 -1
  6. edda_framework-0.14.0/tests/test_channel_mode_locking.py +201 -0
  7. {edda_framework-0.13.0 → edda_framework-0.14.0}/uv.lock +1 -1
  8. {edda_framework-0.13.0 → edda_framework-0.14.0}/.github/workflows/ci.yml +0 -0
  9. {edda_framework-0.13.0 → edda_framework-0.14.0}/.github/workflows/docs.yml +0 -0
  10. {edda_framework-0.13.0 → edda_framework-0.14.0}/.github/workflows/release.yml +0 -0
  11. {edda_framework-0.13.0 → edda_framework-0.14.0}/.gitignore +0 -0
  12. {edda_framework-0.13.0 → edda_framework-0.14.0}/.gitmodules +0 -0
  13. {edda_framework-0.13.0 → edda_framework-0.14.0}/.python-version +0 -0
  14. {edda_framework-0.13.0 → edda_framework-0.14.0}/Justfile +0 -0
  15. {edda_framework-0.13.0 → edda_framework-0.14.0}/LICENSE +0 -0
  16. {edda_framework-0.13.0 → edda_framework-0.14.0}/README.md +0 -0
  17. {edda_framework-0.13.0 → edda_framework-0.14.0}/demo_app.py +0 -0
  18. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/api/reference.md +0 -0
  19. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/core-features/durable-execution/replay.md +0 -0
  20. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/core-features/events/cloudevents-http-binding.md +0 -0
  21. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/core-features/events/postgres-notify.md +0 -0
  22. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/core-features/events/wait-event.md +0 -0
  23. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/core-features/hooks.md +0 -0
  24. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/core-features/messages.md +0 -0
  25. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/core-features/retry.md +0 -0
  26. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/core-features/saga-compensation.md +0 -0
  27. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/core-features/transactional-outbox.md +0 -0
  28. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/core-features/workflows-activities.md +0 -0
  29. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/examples/ecommerce.md +0 -0
  30. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/examples/events.md +0 -0
  31. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/examples/fastapi-integration.md +0 -0
  32. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/examples/saga.md +0 -0
  33. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/examples/simple.md +0 -0
  34. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/getting-started/concepts.md +0 -0
  35. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/getting-started/first-workflow.md +0 -0
  36. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/getting-started/installation.md +0 -0
  37. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/getting-started/quick-start.md +0 -0
  38. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/index.md +0 -0
  39. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/integrations/mcp.md +0 -0
  40. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/integrations/mirascope.md +0 -0
  41. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/integrations/opentelemetry.md +0 -0
  42. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/integrations/pydantic-rpc.md +0 -0
  43. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/viewer-ui/images/cloudevents-cli-trigger.png +0 -0
  44. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/viewer-ui/images/compensation-execution.png +0 -0
  45. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/viewer-ui/images/detail-page-loan-approval.png +0 -0
  46. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/viewer-ui/images/detail-page-match-case.png +0 -0
  47. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
  48. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/viewer-ui/images/start-workflow-form-pydantic.png +0 -0
  49. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/viewer-ui/images/wait-event-visualization.png +0 -0
  50. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/viewer-ui/images/workflow-list-view.png +0 -0
  51. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/viewer-ui/images/workflow-selection-dropdown.png +0 -0
  52. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/viewer-ui/setup.md +0 -0
  53. {edda_framework-0.13.0 → edda_framework-0.14.0}/docs/viewer-ui/visualization.md +0 -0
  54. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/__init__.py +0 -0
  55. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/activity.py +0 -0
  56. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/app.py +0 -0
  57. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/compensation.py +0 -0
  58. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/context.py +0 -0
  59. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/exceptions.py +0 -0
  60. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/hooks.py +0 -0
  61. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/integrations/__init__.py +0 -0
  62. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/integrations/mcp/__init__.py +0 -0
  63. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/integrations/mcp/decorators.py +0 -0
  64. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/integrations/mcp/server.py +0 -0
  65. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/integrations/mirascope/__init__.py +0 -0
  66. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/integrations/mirascope/agent.py +0 -0
  67. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/integrations/mirascope/call.py +0 -0
  68. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/integrations/mirascope/decorator.py +0 -0
  69. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/integrations/mirascope/types.py +0 -0
  70. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/integrations/opentelemetry/__init__.py +0 -0
  71. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/integrations/opentelemetry/hooks.py +0 -0
  72. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/locking.py +0 -0
  73. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/migrations/mysql/20251217000000_initial_schema.sql +0 -0
  74. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/migrations/postgresql/20251217000000_initial_schema.sql +0 -0
  75. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/migrations/sqlite/20251217000000_initial_schema.sql +0 -0
  76. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/outbox/__init__.py +0 -0
  77. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/outbox/relayer.py +0 -0
  78. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/outbox/transactional.py +0 -0
  79. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/pydantic_utils.py +0 -0
  80. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/replay.py +0 -0
  81. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/retry.py +0 -0
  82. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/serialization/__init__.py +0 -0
  83. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/serialization/base.py +0 -0
  84. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/serialization/json.py +0 -0
  85. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/storage/__init__.py +0 -0
  86. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/storage/migrations.py +0 -0
  87. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/storage/models.py +0 -0
  88. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/storage/notify_base.py +0 -0
  89. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/storage/pg_notify.py +0 -0
  90. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/viewer_ui/__init__.py +0 -0
  91. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/viewer_ui/app.py +0 -0
  92. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/viewer_ui/components.py +0 -0
  93. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/viewer_ui/data_service.py +0 -0
  94. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/viewer_ui/theme.py +0 -0
  95. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/visualizer/__init__.py +0 -0
  96. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/visualizer/ast_analyzer.py +0 -0
  97. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/visualizer/mermaid_generator.py +0 -0
  98. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/workflow.py +0 -0
  99. {edda_framework-0.13.0 → edda_framework-0.14.0}/edda/wsgi.py +0 -0
  100. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/__init__.py +0 -0
  101. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/cancellable_workflow.py +0 -0
  102. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/compensation_workflow.py +0 -0
  103. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/event_waiting_app.py +0 -0
  104. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/event_waiting_workflow.py +0 -0
  105. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/event_waiting_workflow_complete.py +0 -0
  106. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/long_running_loop.py +0 -0
  107. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/mcp/README.md +0 -0
  108. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/mcp/order_processing_mcp.py +0 -0
  109. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/mcp/prompts_example.py +0 -0
  110. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/mcp/remote_server_example.py +0 -0
  111. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/mcp/simple_mcp_server.py +0 -0
  112. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/message_passing.py +0 -0
  113. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/mirascope/__init__.py +0 -0
  114. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/mirascope/durable_agent.py +0 -0
  115. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/mirascope/multi_turn.py +0 -0
  116. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/mirascope/simple_call.py +0 -0
  117. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/mirascope/with_tools.py +0 -0
  118. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/observability_with_logfire.py +0 -0
  119. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/observability_with_opentelemetry.py +0 -0
  120. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/pydantic_rpc_integration.py +0 -0
  121. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/pydantic_saga.py +0 -0
  122. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/retry_example.py +0 -0
  123. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/retry_with_compensation.py +0 -0
  124. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/simple_workflow.py +0 -0
  125. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/typeddict_example.py +0 -0
  126. {edda_framework-0.13.0 → edda_framework-0.14.0}/examples/with_outbox.py +0 -0
  127. {edda_framework-0.13.0 → edda_framework-0.14.0}/schema/.dbmate.yml +0 -0
  128. {edda_framework-0.13.0 → edda_framework-0.14.0}/schema/.git +0 -0
  129. {edda_framework-0.13.0 → edda_framework-0.14.0}/schema/.gitignore +0 -0
  130. {edda_framework-0.13.0 → edda_framework-0.14.0}/schema/LICENSE +0 -0
  131. {edda_framework-0.13.0 → edda_framework-0.14.0}/schema/README.md +0 -0
  132. {edda_framework-0.13.0 → edda_framework-0.14.0}/schema/docs/column-values.md +0 -0
  133. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/__init__.py +0 -0
  134. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/conftest.py +0 -0
  135. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/integrations/__init__.py +0 -0
  136. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/integrations/mcp/__init__.py +0 -0
  137. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/integrations/mcp/test_cancel.py +0 -0
  138. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/integrations/mcp/test_integration.py +0 -0
  139. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/integrations/mcp/test_jsonrpc.py +0 -0
  140. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/integrations/mcp/test_prompts.py +0 -0
  141. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/integrations/mcp/test_server.py +0 -0
  142. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/integrations/mirascope/__init__.py +0 -0
  143. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/integrations/mirascope/test_agent.py +0 -0
  144. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/integrations/mirascope/test_call.py +0 -0
  145. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/integrations/mirascope/test_decorator.py +0 -0
  146. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/integrations/mirascope/test_types.py +0 -0
  147. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/integrations/opentelemetry/__init__.py +0 -0
  148. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/integrations/opentelemetry/test_hooks.py +0 -0
  149. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_activity.py +0 -0
  150. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_activity_retry.py +0 -0
  151. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_activity_sync.py +0 -0
  152. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_app.py +0 -0
  153. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_ast_analyzer.py +0 -0
  154. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_atomic_wait_event.py +0 -0
  155. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_auto_migration.py +0 -0
  156. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_binary_data.py +0 -0
  157. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_channel_competing.py +0 -0
  158. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_channel_direct.py +0 -0
  159. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_channel_transactional.py +0 -0
  160. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_cloudevents_http_binding.py +0 -0
  161. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_compensation.py +0 -0
  162. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_compensation_crash_recovery.py.wip +0 -0
  163. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_concurrent_outbox.py +0 -0
  164. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_context.py +0 -0
  165. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_cross_language_channel.py +0 -0
  166. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_ctx_session.py +0 -0
  167. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_distributed_event_delivery.py +0 -0
  168. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_events.py +0 -0
  169. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_instance_id_routing.py +0 -0
  170. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_lock_race_condition.py +0 -0
  171. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_lock_timeout_customization.py +0 -0
  172. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_locking.py +0 -0
  173. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_message_cleanup.py +0 -0
  174. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_message_delivery_lock.py +0 -0
  175. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_messages.py +0 -0
  176. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_migrations_integration.py +0 -0
  177. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_multidb_storage.py +0 -0
  178. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_outbox.py +0 -0
  179. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_pg_notify.py +0 -0
  180. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_polling_optimization.py +0 -0
  181. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_pydantic_activity.py +0 -0
  182. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_pydantic_enum.py +0 -0
  183. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_pydantic_events.py +0 -0
  184. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_pydantic_saga.py +0 -0
  185. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_pydantic_utils.py +0 -0
  186. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_receive_timeout.py +0 -0
  187. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_received_event.py +0 -0
  188. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_recur.py +0 -0
  189. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_recur_cleanup.py +0 -0
  190. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_replay.py +0 -0
  191. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_retry_policy.py +0 -0
  192. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_saga_parameter_extraction.py +0 -0
  193. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_serialization.py +0 -0
  194. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_skip_locked.py +0 -0
  195. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_stale_workflow_recovery.py +0 -0
  196. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_storage.py +0 -0
  197. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_storage_mysql.py +0 -0
  198. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_storage_postgresql.py +0 -0
  199. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_transactions.py +0 -0
  200. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_viewer_pagination.py +0 -0
  201. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_viewer_pydantic_form.py +0 -0
  202. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_viewer_start_saga.py +0 -0
  203. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_wait_timer.py +0 -0
  204. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_workflow.py +0 -0
  205. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_workflow_auto_register.py +0 -0
  206. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_workflow_cancellation.py +0 -0
  207. {edda_framework-0.13.0 → edda_framework-0.14.0}/tests/test_workflow_resumption.py +0 -0
  208. {edda_framework-0.13.0 → edda_framework-0.14.0}/viewer_app.py +0 -0
  209. {edda_framework-0.13.0 → edda_framework-0.14.0}/zensical.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edda-framework
3
- Version: 0.13.0
3
+ Version: 0.14.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
@@ -129,6 +129,24 @@ class WaitForTimerException(Exception):
129
129
  super().__init__(f"Waiting for timer: {timer_id}")
130
130
 
131
131
 
132
+ class ChannelModeConflictError(Exception):
133
+ """
134
+ Raised when subscribing with a different mode than the channel's established mode.
135
+
136
+ A channel's mode is locked when the first subscription is created. Subsequent
137
+ subscriptions must use the same mode.
138
+ """
139
+
140
+ def __init__(self, channel: str, existing_mode: str, requested_mode: str) -> None:
141
+ self.channel = channel
142
+ self.existing_mode = existing_mode
143
+ self.requested_mode = requested_mode
144
+ super().__init__(
145
+ f"Channel '{channel}' is already configured as '{existing_mode}' mode. "
146
+ f"Cannot subscribe with '{requested_mode}' mode."
147
+ )
148
+
149
+
132
150
  # =============================================================================
133
151
  # Subscription Functions
134
152
  # =============================================================================
@@ -150,6 +168,10 @@ async def subscribe(
150
168
  - "competing": Each message goes to only one subscriber (work queue pattern)
151
169
  - "direct": Receive messages sent via send_to() to this instance
152
170
 
171
+ Raises:
172
+ ChannelModeConflictError: If the channel is already configured with a different mode
173
+ ValueError: If mode is not 'broadcast', 'competing', or 'direct'
174
+
153
175
  The "direct" mode is syntactic sugar that subscribes to "channel:instance_id" internally,
154
176
  allowing simpler code when receiving direct messages:
155
177
 
@@ -204,6 +226,11 @@ async def subscribe(
204
226
  f"Invalid subscription mode: {mode}. Must be 'broadcast', 'competing', or 'direct'"
205
227
  )
206
228
 
229
+ # Check for mode conflict
230
+ existing_mode = await ctx.storage.get_channel_mode(actual_channel)
231
+ if existing_mode is not None and existing_mode != actual_mode:
232
+ raise ChannelModeConflictError(channel, existing_mode, mode)
233
+
207
234
  await ctx.storage.subscribe_to_channel(ctx.instance_id, actual_channel, actual_mode)
208
235
 
209
236
 
@@ -990,6 +990,18 @@ class StorageProtocol(Protocol):
990
990
  """
991
991
  ...
992
992
 
993
+ async def get_channel_mode(self, channel: str) -> str | None:
994
+ """
995
+ Get the mode for a channel (from any existing subscription).
996
+
997
+ Args:
998
+ channel: Channel name
999
+
1000
+ Returns:
1001
+ The mode ('broadcast' or 'competing') or None if no subscriptions exist
1002
+ """
1003
+ ...
1004
+
993
1005
  async def register_channel_receive_and_release_lock(
994
1006
  self,
995
1007
  instance_id: str,
@@ -3170,6 +3170,26 @@ class SQLAlchemyStorage:
3170
3170
  "cursor_message_id": subscription.cursor_message_id,
3171
3171
  }
3172
3172
 
3173
+ async def get_channel_mode(self, channel: str) -> str | None:
3174
+ """
3175
+ Get the mode for a channel (from any existing subscription).
3176
+
3177
+ Args:
3178
+ channel: Channel name
3179
+
3180
+ Returns:
3181
+ The mode ('broadcast' or 'competing') or None if no subscriptions exist
3182
+ """
3183
+ session = self._get_session_for_operation()
3184
+ async with self._session_scope(session) as session:
3185
+ result = await session.execute(
3186
+ select(ChannelSubscription.mode)
3187
+ .where(ChannelSubscription.channel == channel)
3188
+ .limit(1)
3189
+ )
3190
+ row = result.scalar_one_or_none()
3191
+ return row
3192
+
3173
3193
  async def register_channel_receive_and_release_lock(
3174
3194
  self,
3175
3195
  instance_id: str,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "edda-framework"
3
- version = "0.13.0"
3
+ version = "0.14.0"
4
4
  description = "Lightweight Durable Execution Framework"
5
5
  authors = [
6
6
  { name = "Yasushi Itoh", email = "6240399+i2y@users.noreply.github.com" }
@@ -0,0 +1,201 @@
1
+ """
2
+ Tests for channel mode locking functionality.
3
+
4
+ Ensures that once a channel is subscribed with a specific mode (broadcast/competing),
5
+ subsequent subscriptions with a different mode are rejected with ChannelModeConflictError.
6
+ """
7
+
8
+ import pytest
9
+ import pytest_asyncio
10
+
11
+ from edda.channels import ChannelModeConflictError, subscribe
12
+ from edda.context import WorkflowContext
13
+
14
+
15
+ @pytest.mark.asyncio
16
+ class TestChannelModeLocking:
17
+ """Test suite for channel mode locking."""
18
+
19
+ @pytest_asyncio.fixture
20
+ async def workflow_instances(self, sqlite_storage, create_test_instance):
21
+ """Create multiple workflow instances for testing."""
22
+ instances = []
23
+ for i in range(1, 4):
24
+ instance_id = f"mode-lock-test-{i}"
25
+ await create_test_instance(
26
+ instance_id=instance_id,
27
+ workflow_name="test_workflow",
28
+ owner_service="test-service",
29
+ input_data={"test": True},
30
+ )
31
+ await sqlite_storage.update_instance_status(instance_id, "running")
32
+ instances.append(instance_id)
33
+ return instances
34
+
35
+ async def test_broadcast_then_competing_raises_error(self, sqlite_storage, workflow_instances):
36
+ """Test that subscribing with competing mode after broadcast raises error."""
37
+ ctx1 = WorkflowContext(
38
+ instance_id=workflow_instances[0],
39
+ workflow_name="test_workflow",
40
+ storage=sqlite_storage,
41
+ worker_id="worker-1",
42
+ is_replaying=False,
43
+ )
44
+
45
+ # Subscribe with broadcast mode first
46
+ await subscribe(ctx1, "test-channel-1", mode="broadcast")
47
+
48
+ # Verify mode was stored
49
+ mode = await sqlite_storage.get_channel_mode("test-channel-1")
50
+ assert mode == "broadcast"
51
+
52
+ # Try to subscribe with competing mode - should fail
53
+ ctx2 = WorkflowContext(
54
+ instance_id=workflow_instances[1],
55
+ workflow_name="test_workflow",
56
+ storage=sqlite_storage,
57
+ worker_id="worker-1",
58
+ is_replaying=False,
59
+ )
60
+
61
+ with pytest.raises(ChannelModeConflictError) as exc_info:
62
+ await subscribe(ctx2, "test-channel-1", mode="competing")
63
+
64
+ assert exc_info.value.channel == "test-channel-1"
65
+ assert exc_info.value.existing_mode == "broadcast"
66
+ assert exc_info.value.requested_mode == "competing"
67
+
68
+ async def test_competing_then_broadcast_raises_error(self, sqlite_storage, workflow_instances):
69
+ """Test that subscribing with broadcast mode after competing raises error."""
70
+ ctx1 = WorkflowContext(
71
+ instance_id=workflow_instances[0],
72
+ workflow_name="test_workflow",
73
+ storage=sqlite_storage,
74
+ worker_id="worker-1",
75
+ is_replaying=False,
76
+ )
77
+
78
+ # Subscribe with competing mode first
79
+ await subscribe(ctx1, "test-channel-2", mode="competing")
80
+
81
+ # Verify mode was stored
82
+ mode = await sqlite_storage.get_channel_mode("test-channel-2")
83
+ assert mode == "competing"
84
+
85
+ # Try to subscribe with broadcast mode - should fail
86
+ ctx2 = WorkflowContext(
87
+ instance_id=workflow_instances[1],
88
+ workflow_name="test_workflow",
89
+ storage=sqlite_storage,
90
+ worker_id="worker-1",
91
+ is_replaying=False,
92
+ )
93
+
94
+ with pytest.raises(ChannelModeConflictError) as exc_info:
95
+ await subscribe(ctx2, "test-channel-2", mode="broadcast")
96
+
97
+ assert exc_info.value.channel == "test-channel-2"
98
+ assert exc_info.value.existing_mode == "competing"
99
+ assert exc_info.value.requested_mode == "broadcast"
100
+
101
+ async def test_same_mode_subscription_allowed(self, sqlite_storage, workflow_instances):
102
+ """Test that multiple subscriptions with the same mode are allowed."""
103
+ # Subscribe first instance with broadcast
104
+ ctx1 = WorkflowContext(
105
+ instance_id=workflow_instances[0],
106
+ workflow_name="test_workflow",
107
+ storage=sqlite_storage,
108
+ worker_id="worker-1",
109
+ is_replaying=False,
110
+ )
111
+ await subscribe(ctx1, "test-channel-3", mode="broadcast")
112
+
113
+ # Subscribe second instance with same mode - should succeed
114
+ ctx2 = WorkflowContext(
115
+ instance_id=workflow_instances[1],
116
+ workflow_name="test_workflow",
117
+ storage=sqlite_storage,
118
+ worker_id="worker-1",
119
+ is_replaying=False,
120
+ )
121
+ await subscribe(ctx2, "test-channel-3", mode="broadcast")
122
+
123
+ # Subscribe third instance with same mode - should succeed
124
+ ctx3 = WorkflowContext(
125
+ instance_id=workflow_instances[2],
126
+ workflow_name="test_workflow",
127
+ storage=sqlite_storage,
128
+ worker_id="worker-1",
129
+ is_replaying=False,
130
+ )
131
+ await subscribe(ctx3, "test-channel-3", mode="broadcast")
132
+
133
+ # All three should be subscribed
134
+ sub1 = await sqlite_storage.get_channel_subscription(
135
+ workflow_instances[0], "test-channel-3"
136
+ )
137
+ sub2 = await sqlite_storage.get_channel_subscription(
138
+ workflow_instances[1], "test-channel-3"
139
+ )
140
+ sub3 = await sqlite_storage.get_channel_subscription(
141
+ workflow_instances[2], "test-channel-3"
142
+ )
143
+
144
+ assert sub1 is not None
145
+ assert sub2 is not None
146
+ assert sub3 is not None
147
+
148
+ async def test_different_channels_independent_modes(self, sqlite_storage, workflow_instances):
149
+ """Test that different channels can have different modes."""
150
+ ctx1 = WorkflowContext(
151
+ instance_id=workflow_instances[0],
152
+ workflow_name="test_workflow",
153
+ storage=sqlite_storage,
154
+ worker_id="worker-1",
155
+ is_replaying=False,
156
+ )
157
+
158
+ # Channel A with broadcast
159
+ await subscribe(ctx1, "channel-a", mode="broadcast")
160
+
161
+ # Channel B with competing - should succeed (different channel)
162
+ await subscribe(ctx1, "channel-b", mode="competing")
163
+
164
+ # Verify modes
165
+ mode_a = await sqlite_storage.get_channel_mode("channel-a")
166
+ mode_b = await sqlite_storage.get_channel_mode("channel-b")
167
+
168
+ assert mode_a == "broadcast"
169
+ assert mode_b == "competing"
170
+
171
+ async def test_get_channel_mode_returns_none_for_new_channel(self, sqlite_storage):
172
+ """Test that get_channel_mode returns None for channels with no subscriptions."""
173
+ mode = await sqlite_storage.get_channel_mode("nonexistent-channel")
174
+ assert mode is None
175
+
176
+ async def test_error_message_is_informative(self, sqlite_storage, workflow_instances):
177
+ """Test that ChannelModeConflictError has a clear message."""
178
+ ctx1 = WorkflowContext(
179
+ instance_id=workflow_instances[0],
180
+ workflow_name="test_workflow",
181
+ storage=sqlite_storage,
182
+ worker_id="worker-1",
183
+ is_replaying=False,
184
+ )
185
+ await subscribe(ctx1, "msg-test-channel", mode="broadcast")
186
+
187
+ ctx2 = WorkflowContext(
188
+ instance_id=workflow_instances[1],
189
+ workflow_name="test_workflow",
190
+ storage=sqlite_storage,
191
+ worker_id="worker-1",
192
+ is_replaying=False,
193
+ )
194
+
195
+ with pytest.raises(ChannelModeConflictError) as exc_info:
196
+ await subscribe(ctx2, "msg-test-channel", mode="competing")
197
+
198
+ error_msg = str(exc_info.value)
199
+ assert "msg-test-channel" in error_msg
200
+ assert "broadcast" in error_msg
201
+ assert "competing" in error_msg
@@ -715,7 +715,7 @@ wheels = [
715
715
 
716
716
  [[package]]
717
717
  name = "edda-framework"
718
- version = "0.13.0"
718
+ version = "0.14.0"
719
719
  source = { editable = "." }
720
720
  dependencies = [
721
721
  { name = "a2wsgi" },
File without changes