edda-framework 0.6.0__tar.gz → 0.7.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.
- {edda_framework-0.6.0 → edda_framework-0.7.0}/.gitignore +1 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/PKG-INFO +3 -1
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/index.md +1 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/integrations/mcp.md +2 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/integrations/opentelemetry.md +4 -0
- edda_framework-0.7.0/docs/viewer-ui/images/compensation-execution.png +0 -0
- edda_framework-0.7.0/docs/viewer-ui/images/detail-page-loan-approval.png +0 -0
- edda_framework-0.7.0/docs/viewer-ui/images/detail-page-match-case.png +0 -0
- edda_framework-0.7.0/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
- edda_framework-0.7.0/docs/viewer-ui/images/start-workflow-form-pydantic.png +0 -0
- edda_framework-0.7.0/docs/viewer-ui/images/wait-event-visualization.png +0 -0
- edda_framework-0.7.0/docs/viewer-ui/images/workflow-list-view.png +0 -0
- edda_framework-0.7.0/docs/viewer-ui/images/workflow-selection-dropdown.png +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/viewer-ui/setup.md +11 -1
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/viewer-ui/visualization.md +38 -33
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/storage/protocol.py +18 -4
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/storage/sqlalchemy_storage.py +105 -5
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/viewer_ui/app.py +552 -126
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/viewer_ui/components.py +81 -68
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/viewer_ui/data_service.py +42 -3
- edda_framework-0.7.0/edda/viewer_ui/theme.py +200 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/pyproject.toml +4 -2
- edda_framework-0.7.0/tests/test_viewer_pagination.py +318 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/uv.lock +21 -17
- {edda_framework-0.6.0 → edda_framework-0.7.0}/zensical.toml +8 -9
- edda_framework-0.6.0/docs/markdown.md +0 -98
- edda_framework-0.6.0/docs/viewer-ui/images/compensation-execution.png +0 -0
- edda_framework-0.6.0/docs/viewer-ui/images/conditional-branching-diagram.png +0 -0
- edda_framework-0.6.0/docs/viewer-ui/images/detail-overview-panel.png +0 -0
- edda_framework-0.6.0/docs/viewer-ui/images/execution-history-panel.png +0 -0
- edda_framework-0.6.0/docs/viewer-ui/images/form-generation-example.png +0 -0
- edda_framework-0.6.0/docs/viewer-ui/images/hybrid-diagram-example.png +0 -0
- edda_framework-0.6.0/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
- edda_framework-0.6.0/docs/viewer-ui/images/start-workflow-dialog.png +0 -0
- edda_framework-0.6.0/docs/viewer-ui/images/status-badges-example.png +0 -0
- edda_framework-0.6.0/docs/viewer-ui/images/wait-event-visualization.png +0 -0
- edda_framework-0.6.0/docs/viewer-ui/images/workflow-list-view.png +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/.github/workflows/ci.yml +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/.github/workflows/docs.yml +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/.github/workflows/release.yml +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/.python-version +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/Justfile +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/LICENSE +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/README.md +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/demo_app.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/core-features/durable-execution/replay.md +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/core-features/events/cloudevents-http-binding.md +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/core-features/events/wait-event.md +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/core-features/hooks.md +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/core-features/retry.md +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/core-features/saga-compensation.md +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/core-features/transactional-outbox.md +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/core-features/workflows-activities.md +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/examples/ecommerce.md +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/examples/events.md +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/examples/fastapi-integration.md +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/examples/saga.md +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/examples/simple.md +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/getting-started/concepts.md +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/getting-started/first-workflow.md +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/getting-started/installation.md +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/getting-started/quick-start.md +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/viewer-ui/images/cloudevents-cli-trigger.png +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/__init__.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/activity.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/app.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/compensation.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/context.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/events.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/exceptions.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/hooks.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/integrations/__init__.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/integrations/mcp/__init__.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/integrations/mcp/decorators.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/integrations/mcp/server.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/integrations/opentelemetry/__init__.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/integrations/opentelemetry/hooks.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/locking.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/outbox/__init__.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/outbox/relayer.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/outbox/transactional.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/pydantic_utils.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/replay.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/retry.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/serialization/__init__.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/serialization/base.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/serialization/json.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/storage/__init__.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/storage/models.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/viewer_ui/__init__.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/visualizer/__init__.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/visualizer/ast_analyzer.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/visualizer/mermaid_generator.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/workflow.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/wsgi.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/__init__.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/cancellable_workflow.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/compensation_workflow.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/event_waiting_app.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/event_waiting_workflow.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/event_waiting_workflow_complete.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/mcp/README.md +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/mcp/order_processing_mcp.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/mcp/prompts_example.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/mcp/remote_server_example.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/mcp/simple_mcp_server.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/observability_with_logfire.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/observability_with_opentelemetry.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/pydantic_saga.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/retry_example.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/retry_with_compensation.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/simple_workflow.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/typeddict_example.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/with_outbox.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/__init__.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/conftest.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/integrations/__init__.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/integrations/mcp/__init__.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/integrations/mcp/test_cancel.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/integrations/mcp/test_integration.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/integrations/mcp/test_jsonrpc.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/integrations/mcp/test_prompts.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/integrations/mcp/test_server.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/integrations/opentelemetry/__init__.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/integrations/opentelemetry/test_hooks.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_activity.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_activity_retry.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_activity_sync.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_app.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_ast_analyzer.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_atomic_wait_event.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_binary_data.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_cloudevents_http_binding.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_compensation.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_compensation_crash_recovery.py.wip +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_concurrent_outbox.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_context.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_ctx_session.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_distributed_event_delivery.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_events.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_lock_race_condition.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_lock_timeout_customization.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_locking.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_multidb_storage.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_outbox.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_pydantic_activity.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_pydantic_enum.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_pydantic_events.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_pydantic_saga.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_pydantic_utils.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_received_event.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_replay.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_retry_policy.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_saga_parameter_extraction.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_serialization.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_skip_locked.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_stale_workflow_recovery.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_storage.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_storage_mysql.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_storage_postgresql.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_transactions.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_viewer_pydantic_form.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_viewer_start_saga.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_wait_timer.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_workflow.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_workflow_auto_register.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_workflow_cancellation.py +0 -0
- {edda_framework-0.6.0 → edda_framework-0.7.0}/viewer_app.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: edda-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -30,11 +30,13 @@ Requires-Dist: sqlalchemy[asyncio]>=2.0.0
|
|
|
30
30
|
Requires-Dist: uvloop>=0.22.1
|
|
31
31
|
Provides-Extra: dev
|
|
32
32
|
Requires-Dist: black>=25.9.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: mcp>=1.22.0; extra == 'dev'
|
|
33
34
|
Requires-Dist: mypy>=1.18.2; extra == 'dev'
|
|
34
35
|
Requires-Dist: pytest-asyncio>=1.2.0; extra == 'dev'
|
|
35
36
|
Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
|
|
36
37
|
Requires-Dist: pytest>=8.4.2; extra == 'dev'
|
|
37
38
|
Requires-Dist: ruff>=0.14.2; extra == 'dev'
|
|
39
|
+
Requires-Dist: starlette>=0.40.0; extra == 'dev'
|
|
38
40
|
Requires-Dist: testcontainers[mysql]>=4.0.0; extra == 'dev'
|
|
39
41
|
Requires-Dist: testcontainers[postgres]>=4.0.0; extra == 'dev'
|
|
40
42
|
Requires-Dist: tsuno>=0.1.3; extra == 'dev'
|
|
@@ -46,6 +46,7 @@ Edda's waiting functions make it ideal for time-based and event-driven business
|
|
|
46
46
|
- **📦 Scheduled Notifications**: Shipping updates, subscription renewals, appointment reminders
|
|
47
47
|
|
|
48
48
|
**Waiting functions**:
|
|
49
|
+
|
|
49
50
|
- `wait_timer(duration_seconds)`: Wait for a relative duration
|
|
50
51
|
- `wait_until(until_time)`: Wait until an absolute datetime (e.g., campaign end date)
|
|
51
52
|
- `wait_event(event_type)`: Wait for external events (near real-time response)
|
|
@@ -133,6 +133,7 @@ Output: {
|
|
|
133
133
|
```
|
|
134
134
|
|
|
135
135
|
The status tool provides progress metadata for efficient polling:
|
|
136
|
+
|
|
136
137
|
- **Completed Activities**: Number of activities that have finished
|
|
137
138
|
- **Suggested Poll Interval**: Recommended wait time before checking again (5000ms for running, 10000ms for waiting)
|
|
138
139
|
|
|
@@ -169,6 +170,7 @@ Output: {
|
|
|
169
170
|
```
|
|
170
171
|
|
|
171
172
|
The cancel tool:
|
|
173
|
+
|
|
172
174
|
- Only works on workflows with status `running`, `waiting_for_event`, or `waiting_for_timer`
|
|
173
175
|
- Automatically executes SAGA compensation transactions to roll back side effects
|
|
174
176
|
- Returns an error for already completed, failed, or cancelled workflows
|
|
@@ -76,11 +76,13 @@ workflow:order_workflow (parent)
|
|
|
76
76
|
## Span Attributes
|
|
77
77
|
|
|
78
78
|
**Workflow Spans**:
|
|
79
|
+
|
|
79
80
|
- `edda.workflow.instance_id`
|
|
80
81
|
- `edda.workflow.name`
|
|
81
82
|
- `edda.workflow.cancelled` (when cancelled)
|
|
82
83
|
|
|
83
84
|
**Activity Spans**:
|
|
85
|
+
|
|
84
86
|
- `edda.activity.id` (e.g., "reserve_inventory:1")
|
|
85
87
|
- `edda.activity.name`
|
|
86
88
|
- `edda.activity.is_replaying`
|
|
@@ -107,10 +109,12 @@ When `enable_metrics=True`:
|
|
|
107
109
|
OpenTelemetryHooks automatically inherits trace context from multiple sources, with the following priority:
|
|
108
110
|
|
|
109
111
|
1. **Explicit `_trace_context` in input_data** (highest priority)
|
|
112
|
+
|
|
110
113
|
- Extracted from CloudEvents extension attributes
|
|
111
114
|
- Useful for cross-service trace propagation
|
|
112
115
|
|
|
113
116
|
2. **Current active span** (e.g., from ASGI/WSGI middleware)
|
|
117
|
+
|
|
114
118
|
- Automatically detected using `trace.get_current_span()`
|
|
115
119
|
- Works with OpenTelemetry instrumentation middleware
|
|
116
120
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -44,6 +44,16 @@ Then open http://localhost:8080 in your browser.
|
|
|
44
44
|
|
|
45
45
|
*The Viewer UI shows all workflow instances with status badges and action buttons*
|
|
46
46
|
|
|
47
|
+
**Pydantic Form Generation:**
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
*Auto-generated form fields based on Pydantic model type hints (str, float, int)*
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
*Dropdown showing all registered workflows with event_handler=True*
|
|
56
|
+
|
|
47
57
|
## Three Ways to Run the Viewer
|
|
48
58
|
|
|
49
59
|
### Method 1: Command Line (Recommended)
|
|
@@ -295,7 +305,7 @@ lsof -ti:8080 | xargs kill -9
|
|
|
295
305
|
|
|
296
306
|
Once workflows appear, you'll see them with color-coded status badges:
|
|
297
307
|
|
|
298
|
-

|
|
299
309
|
|
|
300
310
|
*Workflow instances displayed with status badges (Completed ✅, Running ⏳, Failed ❌, Waiting ⏸️, Cancelled 🚫, etc.)*
|
|
301
311
|
|
|
@@ -34,15 +34,22 @@ The Edda Viewer UI provides workflow visualization and monitoring.
|
|
|
34
34
|
|
|
35
35
|
Workflows with conditional logic (if/else, match/case) are visualized with labeled branches:
|
|
36
36
|
|
|
37
|
-

|
|
38
38
|
|
|
39
|
-
*Example: `loan_approval_workflow`
|
|
39
|
+
*Example: `loan_approval_workflow` detail page. Note the **Execution Flow diagram** (bottom left) showing conditional branches with "sufficient" vs "insufficient" paths based on credit score.*
|
|
40
40
|
|
|
41
41
|
The diagram clearly shows:
|
|
42
|
+
|
|
42
43
|
- Decision points (diamond shapes in some diagrams)
|
|
43
44
|
- Branch labels indicating conditions
|
|
44
45
|
- Different execution paths based on runtime data
|
|
45
46
|
|
|
47
|
+
**Match/Case Pattern:**
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
*Example: `match_case_workflow` showing multiple case branches with different handlers*
|
|
52
|
+
|
|
46
53
|
## Viewing Workflows
|
|
47
54
|
|
|
48
55
|
### Workflow List
|
|
@@ -68,42 +75,46 @@ The main page shows all workflow instances:
|
|
|
68
75
|
- 🚫 **Cancelled**: Manually cancelled (orange)
|
|
69
76
|
- 🔄 **Compensating**: Executing compensations (purple)
|
|
70
77
|
|
|
71
|
-

|
|
72
79
|
|
|
73
80
|
### Workflow List View
|
|
74
81
|
|
|
75
|
-
The main page displays workflow instances as interactive cards:
|
|
82
|
+
The main page displays workflow instances as interactive cards with search and filter capabilities:
|
|
76
83
|
|
|
77
84
|

|
|
78
85
|
|
|
86
|
+
*Workflow list with search filter bar, status filter, date range picker, and pagination controls*
|
|
87
|
+
|
|
79
88
|
### Workflow Detail Page
|
|
80
89
|
|
|
81
|
-
Click on a workflow instance to see:
|
|
90
|
+
Click on a workflow instance to see the detail page:
|
|
91
|
+
|
|
92
|
+

|
|
82
93
|
|
|
83
|
-
|
|
84
|
-
- Instance ID, status, timestamps
|
|
85
|
-
- Input parameters (JSON)
|
|
86
|
-
- Output result (if completed)
|
|
94
|
+
*Workflow detail page showing the Overview Panel (top), Execution Flow diagram (bottom left), and Activity Details panel (bottom right).*
|
|
87
95
|
|
|
88
|
-
|
|
96
|
+
1. **Overview Panel** (top):
|
|
89
97
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
- Compensation flow (if applicable)
|
|
98
|
+
- Instance ID, status, timestamps
|
|
99
|
+
- Input parameters (JSON)
|
|
100
|
+
- Output result (if completed)
|
|
94
101
|
|
|
95
|
-
|
|
102
|
+
2. **Execution Flow** (bottom left):
|
|
96
103
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
- Event data (for wait_event)
|
|
101
|
-
- Error messages (if failed)
|
|
104
|
+
- Visual graph of workflow structure
|
|
105
|
+
- Color-coded execution status
|
|
106
|
+
- Compensation flow (if applicable)
|
|
102
107
|
|
|
103
|
-
|
|
108
|
+
3. **Activity Details** (bottom right):
|
|
109
|
+
|
|
110
|
+
- Step-by-step execution log
|
|
111
|
+
- Activity results
|
|
112
|
+
- Event data (for wait_event)
|
|
113
|
+
- Error messages (if failed)
|
|
104
114
|
|
|
105
115
|
4. **Actions**:
|
|
106
|
-
|
|
116
|
+
|
|
117
|
+
- **Cancel**: Stop running workflow
|
|
107
118
|
|
|
108
119
|
## Starting Workflows from Viewer
|
|
109
120
|
|
|
@@ -113,21 +124,15 @@ The Viewer UI can start workflows with automatic form generation:
|
|
|
113
124
|
|
|
114
125
|
For Pydantic-based workflows, the Viewer generates input forms automatically.
|
|
115
126
|
|
|
116
|
-

|
|
117
128
|
|
|
118
|
-
*Example: Workflow selection
|
|
129
|
+
*Example: Workflow selection dropdown showing all registered workflows*
|
|
119
130
|
|
|
120
131
|
The form generator supports various field types based on Pydantic model definitions:
|
|
121
132
|
|
|
122
|
-

|
|
123
|
-
|
|
124
|
-
*Example: Auto-generated form showing different field types (text inputs, number inputs, checkboxes, etc.)*
|
|
125
|
-
|
|
126
|
-
For complex workflows with nested models:
|
|
127
|
-
|
|
128
133
|

|
|
129
134
|
|
|
130
|
-
*
|
|
135
|
+
*Auto-generated form with nested Pydantic models (items list, shipping_address) showing text inputs, number inputs, and nested model containers*
|
|
131
136
|
|
|
132
137
|
### Starting Workflow
|
|
133
138
|
|
|
@@ -166,7 +171,7 @@ When a workflow is cancelled or fails, and compensations run, the diagram shows:
|
|
|
166
171
|
|
|
167
172
|

|
|
168
173
|
|
|
169
|
-
*
|
|
174
|
+
*Saga compensation flow showing rollback activities (cancel_flight_ticket, cancel_hotel_room)*
|
|
170
175
|
|
|
171
176
|
## Event Waiting Visualization
|
|
172
177
|
|
|
@@ -174,7 +179,7 @@ Workflows can wait for external events using `wait_event()`. The Viewer displays
|
|
|
174
179
|
|
|
175
180
|

|
|
176
181
|
|
|
177
|
-
*
|
|
182
|
+
*Workflow with wait_event showing event type, timeout, and activity details panel*
|
|
178
183
|
|
|
179
184
|
The diagram shows:
|
|
180
185
|
- Hexagon node: Event wait point
|
|
@@ -238,20 +238,34 @@ class StorageProtocol(Protocol):
|
|
|
238
238
|
async def list_instances(
|
|
239
239
|
self,
|
|
240
240
|
limit: int = 50,
|
|
241
|
+
page_token: str | None = None,
|
|
241
242
|
status_filter: str | None = None,
|
|
242
|
-
|
|
243
|
+
workflow_name_filter: str | None = None,
|
|
244
|
+
instance_id_filter: str | None = None,
|
|
245
|
+
started_after: datetime | None = None,
|
|
246
|
+
started_before: datetime | None = None,
|
|
247
|
+
) -> dict[str, Any]:
|
|
243
248
|
"""
|
|
244
|
-
List workflow instances with
|
|
249
|
+
List workflow instances with cursor-based pagination and filtering.
|
|
245
250
|
|
|
246
251
|
This method JOINs workflow_instances with workflow_definitions to
|
|
247
252
|
return instances along with their source code.
|
|
248
253
|
|
|
249
254
|
Args:
|
|
250
|
-
limit: Maximum number of instances to return
|
|
255
|
+
limit: Maximum number of instances to return per page
|
|
256
|
+
page_token: Cursor for pagination (format: "ISO_DATETIME||INSTANCE_ID")
|
|
251
257
|
status_filter: Optional status filter (e.g., "running", "completed", "failed")
|
|
258
|
+
workflow_name_filter: Optional workflow name filter (partial match, case-insensitive)
|
|
259
|
+
instance_id_filter: Optional instance ID filter (partial match, case-insensitive)
|
|
260
|
+
started_after: Filter instances started after this datetime (inclusive)
|
|
261
|
+
started_before: Filter instances started before this datetime (inclusive)
|
|
252
262
|
|
|
253
263
|
Returns:
|
|
254
|
-
|
|
264
|
+
Dictionary containing:
|
|
265
|
+
- instances: List of workflow instances, ordered by started_at DESC
|
|
266
|
+
- next_page_token: Cursor for the next page, or None if no more pages
|
|
267
|
+
- has_more: Boolean indicating if there are more pages
|
|
268
|
+
|
|
255
269
|
Each instance contains: instance_id, workflow_name, source_hash,
|
|
256
270
|
owner_service, status, current_activity_id, started_at, updated_at,
|
|
257
271
|
input_data, source_code, output_data, locked_by, locked_at
|
|
@@ -774,11 +774,17 @@ class SQLAlchemyStorage:
|
|
|
774
774
|
async def list_instances(
|
|
775
775
|
self,
|
|
776
776
|
limit: int = 50,
|
|
777
|
+
page_token: str | None = None,
|
|
777
778
|
status_filter: str | None = None,
|
|
778
|
-
|
|
779
|
-
|
|
779
|
+
workflow_name_filter: str | None = None,
|
|
780
|
+
instance_id_filter: str | None = None,
|
|
781
|
+
started_after: datetime | None = None,
|
|
782
|
+
started_before: datetime | None = None,
|
|
783
|
+
) -> dict[str, Any]:
|
|
784
|
+
"""List workflow instances with cursor-based pagination and filtering."""
|
|
780
785
|
session = self._get_session_for_operation()
|
|
781
786
|
async with self._session_scope(session) as session:
|
|
787
|
+
# Base query with JOIN
|
|
782
788
|
stmt = (
|
|
783
789
|
select(WorkflowInstance, WorkflowDefinition.source_code)
|
|
784
790
|
.join(
|
|
@@ -788,17 +794,105 @@ class SQLAlchemyStorage:
|
|
|
788
794
|
WorkflowInstance.source_hash == WorkflowDefinition.source_hash,
|
|
789
795
|
),
|
|
790
796
|
)
|
|
791
|
-
.order_by(
|
|
792
|
-
|
|
797
|
+
.order_by(
|
|
798
|
+
WorkflowInstance.started_at.desc(),
|
|
799
|
+
WorkflowInstance.instance_id.desc(),
|
|
800
|
+
)
|
|
793
801
|
)
|
|
794
802
|
|
|
803
|
+
# Apply cursor-based pagination (page_token format: "ISO_DATETIME||INSTANCE_ID")
|
|
804
|
+
if page_token:
|
|
805
|
+
# Parse page_token: || separates datetime and instance_id
|
|
806
|
+
separator = "||"
|
|
807
|
+
if separator in page_token:
|
|
808
|
+
cursor_time_str, cursor_id = page_token.split(separator, 1)
|
|
809
|
+
cursor_time = datetime.fromisoformat(cursor_time_str)
|
|
810
|
+
# Use _make_datetime_comparable for SQLite compatibility
|
|
811
|
+
started_at_comparable = self._make_datetime_comparable(
|
|
812
|
+
WorkflowInstance.started_at
|
|
813
|
+
)
|
|
814
|
+
# For SQLite, also wrap the cursor_time in func.datetime()
|
|
815
|
+
cursor_time_comparable: Any
|
|
816
|
+
if self.engine.dialect.name == "sqlite":
|
|
817
|
+
cursor_time_comparable = func.datetime(cursor_time_str)
|
|
818
|
+
else:
|
|
819
|
+
cursor_time_comparable = cursor_time
|
|
820
|
+
# For DESC order, we want rows where (started_at, instance_id) < cursor
|
|
821
|
+
stmt = stmt.where(
|
|
822
|
+
or_(
|
|
823
|
+
started_at_comparable < cursor_time_comparable,
|
|
824
|
+
and_(
|
|
825
|
+
started_at_comparable == cursor_time_comparable,
|
|
826
|
+
WorkflowInstance.instance_id < cursor_id,
|
|
827
|
+
),
|
|
828
|
+
)
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
# Apply status filter
|
|
795
832
|
if status_filter:
|
|
796
833
|
stmt = stmt.where(WorkflowInstance.status == status_filter)
|
|
797
834
|
|
|
835
|
+
# Apply workflow name and/or instance ID filter (partial match, case-insensitive)
|
|
836
|
+
# When both filters have the same value (unified search), use OR logic
|
|
837
|
+
if workflow_name_filter and instance_id_filter:
|
|
838
|
+
if workflow_name_filter == instance_id_filter:
|
|
839
|
+
# Unified search: match either workflow name OR instance ID
|
|
840
|
+
stmt = stmt.where(
|
|
841
|
+
or_(
|
|
842
|
+
WorkflowInstance.workflow_name.ilike(f"%{workflow_name_filter}%"),
|
|
843
|
+
WorkflowInstance.instance_id.ilike(f"%{instance_id_filter}%"),
|
|
844
|
+
)
|
|
845
|
+
)
|
|
846
|
+
else:
|
|
847
|
+
# Separate filters: match both (AND logic)
|
|
848
|
+
stmt = stmt.where(
|
|
849
|
+
WorkflowInstance.workflow_name.ilike(f"%{workflow_name_filter}%")
|
|
850
|
+
)
|
|
851
|
+
stmt = stmt.where(WorkflowInstance.instance_id.ilike(f"%{instance_id_filter}%"))
|
|
852
|
+
elif workflow_name_filter:
|
|
853
|
+
stmt = stmt.where(WorkflowInstance.workflow_name.ilike(f"%{workflow_name_filter}%"))
|
|
854
|
+
elif instance_id_filter:
|
|
855
|
+
stmt = stmt.where(WorkflowInstance.instance_id.ilike(f"%{instance_id_filter}%"))
|
|
856
|
+
|
|
857
|
+
# Apply date range filters (use _make_datetime_comparable for SQLite)
|
|
858
|
+
if started_after or started_before:
|
|
859
|
+
started_at_comparable = self._make_datetime_comparable(WorkflowInstance.started_at)
|
|
860
|
+
if started_after:
|
|
861
|
+
started_after_comparable: Any
|
|
862
|
+
if self.engine.dialect.name == "sqlite":
|
|
863
|
+
started_after_comparable = func.datetime(started_after.isoformat())
|
|
864
|
+
else:
|
|
865
|
+
started_after_comparable = started_after
|
|
866
|
+
stmt = stmt.where(started_at_comparable >= started_after_comparable)
|
|
867
|
+
if started_before:
|
|
868
|
+
started_before_comparable: Any
|
|
869
|
+
if self.engine.dialect.name == "sqlite":
|
|
870
|
+
started_before_comparable = func.datetime(started_before.isoformat())
|
|
871
|
+
else:
|
|
872
|
+
started_before_comparable = started_before
|
|
873
|
+
stmt = stmt.where(started_at_comparable <= started_before_comparable)
|
|
874
|
+
|
|
875
|
+
# Fetch limit+1 to determine if there are more pages
|
|
876
|
+
stmt = stmt.limit(limit + 1)
|
|
877
|
+
|
|
798
878
|
result = await session.execute(stmt)
|
|
799
879
|
rows = result.all()
|
|
800
880
|
|
|
801
|
-
|
|
881
|
+
# Determine has_more and next_page_token
|
|
882
|
+
has_more = len(rows) > limit
|
|
883
|
+
if has_more:
|
|
884
|
+
rows = rows[:limit] # Trim to actual limit
|
|
885
|
+
|
|
886
|
+
# Generate next_page_token from last row
|
|
887
|
+
next_page_token: str | None = None
|
|
888
|
+
if has_more and rows:
|
|
889
|
+
last_instance = rows[-1][0]
|
|
890
|
+
# Format: ISO_DATETIME||INSTANCE_ID (using || as separator)
|
|
891
|
+
next_page_token = (
|
|
892
|
+
f"{last_instance.started_at.isoformat()}||{last_instance.instance_id}"
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
instances = [
|
|
802
896
|
{
|
|
803
897
|
"instance_id": instance.instance_id,
|
|
804
898
|
"workflow_name": instance.workflow_name,
|
|
@@ -820,6 +914,12 @@ class SQLAlchemyStorage:
|
|
|
820
914
|
for instance, source_code in rows
|
|
821
915
|
]
|
|
822
916
|
|
|
917
|
+
return {
|
|
918
|
+
"instances": instances,
|
|
919
|
+
"next_page_token": next_page_token,
|
|
920
|
+
"has_more": has_more,
|
|
921
|
+
}
|
|
922
|
+
|
|
823
923
|
# -------------------------------------------------------------------------
|
|
824
924
|
# Distributed Locking Methods (ALWAYS use separate session/transaction)
|
|
825
925
|
# -------------------------------------------------------------------------
|