edda-framework 0.1.0__tar.gz → 0.2.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.1.0 → edda_framework-0.2.0}/PKG-INFO +49 -1
- {edda_framework-0.1.0 → edda_framework-0.2.0}/README.md +46 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/demo_app.py +6 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/core-features/workflows-activities.md +64 -9
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/__init__.py +2 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/activity.py +32 -19
- edda_framework-0.2.0/edda/wsgi.py +77 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/pyproject.toml +3 -1
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_activity.py +12 -6
- edda_framework-0.2.0/tests/test_activity_sync.py +218 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/uv.lock +14 -1
- {edda_framework-0.1.0 → edda_framework-0.2.0}/.github/workflows/ci.yml +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/.github/workflows/docs.yml +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/.github/workflows/release.yml +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/.gitignore +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/.python-version +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/Justfile +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/LICENSE +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/core-features/durable-execution/replay.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/core-features/events/cloudevents-http-binding.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/core-features/events/wait-event.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/core-features/hooks.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/core-features/retry.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/core-features/saga-compensation.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/core-features/transactional-outbox.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/examples/ecommerce.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/examples/events.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/examples/fastapi-integration.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/examples/saga.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/examples/simple.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/getting-started/concepts.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/getting-started/first-workflow.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/getting-started/installation.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/getting-started/quick-start.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/index.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/markdown.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/viewer-ui/images/cloudevents-cli-trigger.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/viewer-ui/images/compensation-execution.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/viewer-ui/images/conditional-branching-diagram.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/viewer-ui/images/detail-overview-panel.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/viewer-ui/images/execution-history-panel.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/viewer-ui/images/form-generation-example.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/viewer-ui/images/hybrid-diagram-example.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/viewer-ui/images/start-workflow-dialog.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/viewer-ui/images/status-badges-example.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/viewer-ui/images/wait-event-visualization.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/viewer-ui/images/workflow-list-view.png +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/viewer-ui/setup.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/docs/viewer-ui/visualization.md +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/app.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/compensation.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/context.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/events.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/exceptions.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/hooks.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/locking.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/outbox/__init__.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/outbox/relayer.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/outbox/transactional.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/pydantic_utils.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/replay.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/retry.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/serialization/__init__.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/serialization/base.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/serialization/json.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/storage/__init__.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/storage/models.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/storage/protocol.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/storage/sqlalchemy_storage.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/viewer_ui/__init__.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/viewer_ui/app.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/viewer_ui/components.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/viewer_ui/data_service.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/visualizer/__init__.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/visualizer/ast_analyzer.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/visualizer/mermaid_generator.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/edda/workflow.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/examples/__init__.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/examples/cancellable_workflow.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/examples/compensation_workflow.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/examples/event_waiting_app.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/examples/event_waiting_workflow.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/examples/event_waiting_workflow_complete.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/examples/observability_with_logfire.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/examples/pydantic_saga.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/examples/retry_example.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/examples/retry_with_compensation.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/examples/simple_workflow.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/examples/typeddict_example.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/examples/with_outbox.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/__init__.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/conftest.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_activity_retry.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_app.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_ast_analyzer.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_atomic_wait_event.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_binary_data.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_cloudevents_http_binding.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_compensation.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_compensation_crash_recovery.py.wip +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_concurrent_outbox.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_context.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_ctx_session.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_distributed_event_delivery.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_events.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_lock_race_condition.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_lock_timeout_customization.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_locking.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_multidb_storage.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_outbox.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_pydantic_activity.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_pydantic_enum.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_pydantic_events.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_pydantic_saga.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_pydantic_utils.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_received_event.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_replay.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_retry_policy.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_saga_parameter_extraction.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_serialization.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_skip_locked.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_stale_workflow_recovery.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_storage.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_storage_mysql.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_storage_postgresql.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_transactions.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_viewer_pydantic_form.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_viewer_start_saga.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_wait_timer.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_workflow.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_workflow_auto_register.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/tests/test_workflow_cancellation.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/viewer_app.py +0 -0
- {edda_framework-0.1.0 → edda_framework-0.2.0}/zensical.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: edda-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -20,7 +20,9 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
20
20
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
21
21
|
Classifier: Topic :: System :: Distributed Computing
|
|
22
22
|
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: a2wsgi>=1.10.0
|
|
23
24
|
Requires-Dist: aiosqlite>=0.21.0
|
|
25
|
+
Requires-Dist: anyio>=4.0.0
|
|
24
26
|
Requires-Dist: cloudevents>=1.12.0
|
|
25
27
|
Requires-Dist: httpx>=0.28.1
|
|
26
28
|
Requires-Dist: pydantic>=2.0.0
|
|
@@ -75,6 +77,7 @@ For detailed documentation, visit [https://i2y.github.io/edda/](https://i2y.gith
|
|
|
75
77
|
- 📦 **Transactional Outbox**: Reliable event publishing with guaranteed delivery
|
|
76
78
|
- ☁️ **CloudEvents Support**: Native support for CloudEvents protocol
|
|
77
79
|
- ⏱️ **Event & Timer Waiting**: Free up worker resources while waiting for events or timers, resume on any available worker
|
|
80
|
+
- 🌍 **ASGI/WSGI Support**: Deploy with your preferred server (uvicorn, gunicorn, uWSGI)
|
|
78
81
|
|
|
79
82
|
## Use Cases
|
|
80
83
|
|
|
@@ -638,6 +641,51 @@ api.mount("/workflows", edda_app)
|
|
|
638
641
|
|
|
639
642
|
This works with any ASGI framework (Starlette, FastAPI, Quart, etc.)
|
|
640
643
|
|
|
644
|
+
### WSGI Integration
|
|
645
|
+
|
|
646
|
+
For WSGI environments (gunicorn, uWSGI, Flask, Django), use the WSGI adapter:
|
|
647
|
+
|
|
648
|
+
```python
|
|
649
|
+
from edda import EddaApp
|
|
650
|
+
from edda.wsgi import create_wsgi_app
|
|
651
|
+
|
|
652
|
+
# Create Edda app
|
|
653
|
+
edda_app = EddaApp(db_url="sqlite:///workflow.db")
|
|
654
|
+
|
|
655
|
+
# Convert to WSGI
|
|
656
|
+
wsgi_application = create_wsgi_app(edda_app)
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
**Running with WSGI servers:**
|
|
660
|
+
|
|
661
|
+
```bash
|
|
662
|
+
# With Gunicorn
|
|
663
|
+
gunicorn demo_app:wsgi_application --workers 4
|
|
664
|
+
|
|
665
|
+
# With uWSGI
|
|
666
|
+
uwsgi --http :8000 --wsgi-file demo_app.py --callable wsgi_application
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
**Sync Activities**: For WSGI environments or legacy codebases, you can write synchronous activities:
|
|
670
|
+
|
|
671
|
+
```python
|
|
672
|
+
from edda import activity, WorkflowContext
|
|
673
|
+
|
|
674
|
+
@activity
|
|
675
|
+
def process_payment(ctx: WorkflowContext, amount: float) -> dict:
|
|
676
|
+
# Sync function - automatically executed in thread pool
|
|
677
|
+
# No async/await needed!
|
|
678
|
+
return {"status": "paid", "amount": amount}
|
|
679
|
+
|
|
680
|
+
@workflow
|
|
681
|
+
async def payment_workflow(ctx: WorkflowContext, order_id: str) -> dict:
|
|
682
|
+
# Workflows still use async (for deterministic replay)
|
|
683
|
+
result = await process_payment(ctx, 99.99, activity_id="pay:1")
|
|
684
|
+
return result
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
**Performance note**: ASGI servers (uvicorn, hypercorn) are recommended for better performance with Edda's async architecture. WSGI support is provided for compatibility with existing infrastructure and users who prefer synchronous programming.
|
|
688
|
+
|
|
641
689
|
## Observability Hooks
|
|
642
690
|
|
|
643
691
|
Extend Edda with custom observability without coupling to specific tools:
|
|
@@ -27,6 +27,7 @@ For detailed documentation, visit [https://i2y.github.io/edda/](https://i2y.gith
|
|
|
27
27
|
- 📦 **Transactional Outbox**: Reliable event publishing with guaranteed delivery
|
|
28
28
|
- ☁️ **CloudEvents Support**: Native support for CloudEvents protocol
|
|
29
29
|
- ⏱️ **Event & Timer Waiting**: Free up worker resources while waiting for events or timers, resume on any available worker
|
|
30
|
+
- 🌍 **ASGI/WSGI Support**: Deploy with your preferred server (uvicorn, gunicorn, uWSGI)
|
|
30
31
|
|
|
31
32
|
## Use Cases
|
|
32
33
|
|
|
@@ -590,6 +591,51 @@ api.mount("/workflows", edda_app)
|
|
|
590
591
|
|
|
591
592
|
This works with any ASGI framework (Starlette, FastAPI, Quart, etc.)
|
|
592
593
|
|
|
594
|
+
### WSGI Integration
|
|
595
|
+
|
|
596
|
+
For WSGI environments (gunicorn, uWSGI, Flask, Django), use the WSGI adapter:
|
|
597
|
+
|
|
598
|
+
```python
|
|
599
|
+
from edda import EddaApp
|
|
600
|
+
from edda.wsgi import create_wsgi_app
|
|
601
|
+
|
|
602
|
+
# Create Edda app
|
|
603
|
+
edda_app = EddaApp(db_url="sqlite:///workflow.db")
|
|
604
|
+
|
|
605
|
+
# Convert to WSGI
|
|
606
|
+
wsgi_application = create_wsgi_app(edda_app)
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
**Running with WSGI servers:**
|
|
610
|
+
|
|
611
|
+
```bash
|
|
612
|
+
# With Gunicorn
|
|
613
|
+
gunicorn demo_app:wsgi_application --workers 4
|
|
614
|
+
|
|
615
|
+
# With uWSGI
|
|
616
|
+
uwsgi --http :8000 --wsgi-file demo_app.py --callable wsgi_application
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**Sync Activities**: For WSGI environments or legacy codebases, you can write synchronous activities:
|
|
620
|
+
|
|
621
|
+
```python
|
|
622
|
+
from edda import activity, WorkflowContext
|
|
623
|
+
|
|
624
|
+
@activity
|
|
625
|
+
def process_payment(ctx: WorkflowContext, amount: float) -> dict:
|
|
626
|
+
# Sync function - automatically executed in thread pool
|
|
627
|
+
# No async/await needed!
|
|
628
|
+
return {"status": "paid", "amount": amount}
|
|
629
|
+
|
|
630
|
+
@workflow
|
|
631
|
+
async def payment_workflow(ctx: WorkflowContext, order_id: str) -> dict:
|
|
632
|
+
# Workflows still use async (for deterministic replay)
|
|
633
|
+
result = await process_payment(ctx, 99.99, activity_id="pay:1")
|
|
634
|
+
return result
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
**Performance note**: ASGI servers (uvicorn, hypercorn) are recommended for better performance with Edda's async architecture. WSGI support is provided for compatibility with existing infrastructure and users who prefer synchronous programming.
|
|
638
|
+
|
|
593
639
|
## Observability Hooks
|
|
594
640
|
|
|
595
641
|
Extend Edda with custom observability without coupling to specific tools:
|
|
@@ -1817,3 +1817,9 @@ async def scheduled_order_shipment_workflow(
|
|
|
1817
1817
|
# Export as ASGI application
|
|
1818
1818
|
# No need to manually register event handlers!
|
|
1819
1819
|
application = app
|
|
1820
|
+
|
|
1821
|
+
# Export as WSGI application (for gunicorn, uWSGI, etc.)
|
|
1822
|
+
# Usage: gunicorn demo_app:wsgi_application --workers 4
|
|
1823
|
+
from edda.wsgi import create_wsgi_app
|
|
1824
|
+
|
|
1825
|
+
wsgi_application = create_wsgi_app(app)
|
|
@@ -121,6 +121,46 @@ async def create_order_with_db(ctx: WorkflowContext, order_id: str):
|
|
|
121
121
|
return {"order_id": order_id}
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
+
### Sync Activities (WSGI Compatibility)
|
|
125
|
+
|
|
126
|
+
For WSGI environments (gunicorn, uWSGI) or legacy codebases, Edda supports synchronous activities:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from edda import activity, WorkflowContext
|
|
130
|
+
|
|
131
|
+
@activity
|
|
132
|
+
def create_user_record(ctx: WorkflowContext, user_id: str, email: str) -> dict:
|
|
133
|
+
"""Sync activity - executed in thread pool"""
|
|
134
|
+
# Traditional sync code - no async/await needed!
|
|
135
|
+
user = User(user_id=user_id, email=email)
|
|
136
|
+
db.session.add(user)
|
|
137
|
+
db.session.commit()
|
|
138
|
+
return {"user_id": user.id}
|
|
139
|
+
|
|
140
|
+
@activity
|
|
141
|
+
async def async_activity(ctx: WorkflowContext, data: str) -> dict:
|
|
142
|
+
"""Async activity - recommended for I/O operations"""
|
|
143
|
+
result = await httpx.get(f"https://api.example.com/{data}")
|
|
144
|
+
return result.json()
|
|
145
|
+
|
|
146
|
+
@workflow
|
|
147
|
+
async def mixed_workflow(ctx: WorkflowContext, user_id: str) -> dict:
|
|
148
|
+
# Workflows are always async (for deterministic replay)
|
|
149
|
+
# But can call both sync and async activities
|
|
150
|
+
user = await create_user_record(ctx, user_id, "user@example.com", activity_id="create:1")
|
|
151
|
+
data = await async_activity(ctx, user_id, activity_id="fetch:1")
|
|
152
|
+
return {"user": user, "data": data}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**When to use sync activities:**
|
|
156
|
+
|
|
157
|
+
- ✅ Existing sync codebases (Flask, Django)
|
|
158
|
+
- ✅ WSGI deployments (gunicorn, uWSGI)
|
|
159
|
+
- ✅ Libraries without async support
|
|
160
|
+
- ✅ Simple CPU-bound operations
|
|
161
|
+
|
|
162
|
+
**Performance note:** Async activities are recommended for I/O-bound operations (database queries, HTTP requests, file I/O) for better performance. Sync activities are executed in a thread pool to avoid blocking the event loop.
|
|
163
|
+
|
|
124
164
|
## Retry Policies
|
|
125
165
|
|
|
126
166
|
Activities automatically retry on failure with exponential backoff. This provides resilience against transient failures like network timeouts or temporary service unavailability.
|
|
@@ -755,26 +795,41 @@ async def order_workflow(ctx: WorkflowContext, order_id: str, amount: float):
|
|
|
755
795
|
pass
|
|
756
796
|
```
|
|
757
797
|
|
|
758
|
-
### 4.
|
|
798
|
+
### 4. Choose Async or Sync Appropriately
|
|
759
799
|
|
|
760
|
-
|
|
800
|
+
✅ **Preferred: Async activities** (better performance for I/O)
|
|
761
801
|
|
|
762
802
|
```python
|
|
763
803
|
@activity
|
|
764
|
-
def
|
|
765
|
-
#
|
|
766
|
-
|
|
804
|
+
async def fetch_user_data(ctx: WorkflowContext, user_id: str) -> dict:
|
|
805
|
+
# Async I/O operations (recommended)
|
|
806
|
+
result = await httpx.get(f"https://api.example.com/users/{user_id}")
|
|
807
|
+
return result.json()
|
|
767
808
|
```
|
|
768
809
|
|
|
769
|
-
✅ **
|
|
810
|
+
✅ **Valid: Sync activities** (WSGI compatibility, legacy code)
|
|
770
811
|
|
|
771
812
|
```python
|
|
772
813
|
@activity
|
|
773
|
-
|
|
774
|
-
#
|
|
775
|
-
|
|
814
|
+
def process_legacy_data(ctx: WorkflowContext, data: str) -> dict:
|
|
815
|
+
# Sync operations (executed in thread pool)
|
|
816
|
+
result = legacy_library.process(data) # No async support
|
|
817
|
+
return {"processed": result}
|
|
776
818
|
```
|
|
777
819
|
|
|
820
|
+
✅ **Good: Mix sync and async in same workflow**
|
|
821
|
+
|
|
822
|
+
```python
|
|
823
|
+
@workflow
|
|
824
|
+
async def order_workflow(ctx: WorkflowContext, order_id: str) -> dict:
|
|
825
|
+
# Both sync and async activities work fine
|
|
826
|
+
user = await create_user_record(ctx, order_id, activity_id="user:1") # Sync
|
|
827
|
+
payment = await process_payment(ctx, 99.99, activity_id="pay:1") # Async
|
|
828
|
+
return {"user": user, "payment": payment}
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
**Performance tip**: Prefer async activities for I/O-bound operations (database queries, HTTP requests, file I/O). Use sync activities when integrating with legacy code or libraries without async support.
|
|
832
|
+
|
|
778
833
|
## Next Steps
|
|
779
834
|
|
|
780
835
|
- **[Durable Execution](durable-execution/replay.md)**: Learn how Edda ensures workflows never lose progress
|
|
@@ -30,6 +30,7 @@ from edda.hooks import HooksBase, WorkflowHooks
|
|
|
30
30
|
from edda.outbox import OutboxRelayer, send_event_transactional
|
|
31
31
|
from edda.retry import RetryPolicy
|
|
32
32
|
from edda.workflow import workflow
|
|
33
|
+
from edda.wsgi import create_wsgi_app
|
|
33
34
|
|
|
34
35
|
__version__ = "0.1.0"
|
|
35
36
|
|
|
@@ -53,4 +54,5 @@ __all__ = [
|
|
|
53
54
|
"RetryPolicy",
|
|
54
55
|
"RetryExhaustedError",
|
|
55
56
|
"TerminalError",
|
|
57
|
+
"create_wsgi_app",
|
|
56
58
|
]
|
|
@@ -13,6 +13,8 @@ import time
|
|
|
13
13
|
from collections.abc import Callable
|
|
14
14
|
from typing import Any, TypeVar, cast
|
|
15
15
|
|
|
16
|
+
import anyio
|
|
17
|
+
|
|
16
18
|
from edda.context import WorkflowContext
|
|
17
19
|
from edda.exceptions import RetryExhaustedError, TerminalError, WorkflowCancelledException
|
|
18
20
|
from edda.pydantic_utils import (
|
|
@@ -40,12 +42,13 @@ class Activity:
|
|
|
40
42
|
Initialize activity wrapper.
|
|
41
43
|
|
|
42
44
|
Args:
|
|
43
|
-
func: The async function to wrap
|
|
45
|
+
func: The async or sync function to wrap
|
|
44
46
|
retry_policy: Optional retry policy for this activity.
|
|
45
47
|
If None, uses the default policy from EddaApp.
|
|
46
48
|
"""
|
|
47
49
|
self.func = func
|
|
48
50
|
self.name = func.__name__
|
|
51
|
+
self.is_async = inspect.iscoroutinefunction(func)
|
|
49
52
|
self.retry_policy = retry_policy
|
|
50
53
|
functools.update_wrapper(self, func)
|
|
51
54
|
|
|
@@ -284,8 +287,12 @@ class Activity:
|
|
|
284
287
|
"kwargs": {k: to_json_dict(v) for k, v in kwargs.items()},
|
|
285
288
|
}
|
|
286
289
|
|
|
287
|
-
# Execute the activity function
|
|
288
|
-
|
|
290
|
+
# Execute the activity function (sync or async)
|
|
291
|
+
if self.is_async:
|
|
292
|
+
result = await self.func(ctx, *args, **kwargs)
|
|
293
|
+
else:
|
|
294
|
+
# Run sync function in thread pool to avoid blocking
|
|
295
|
+
result = await anyio.to_thread.run_sync(self.func, ctx, *args, **kwargs)
|
|
289
296
|
|
|
290
297
|
# Convert Pydantic model result to JSON dict for storage
|
|
291
298
|
result_for_storage = to_json_dict(result)
|
|
@@ -369,7 +376,7 @@ class Activity:
|
|
|
369
376
|
return self.retry_policy
|
|
370
377
|
|
|
371
378
|
# Priority 2: App-level policy (EddaApp default_retry_policy)
|
|
372
|
-
#
|
|
379
|
+
# Set by ReplayEngine when creating WorkflowContext (edda/replay.py)
|
|
373
380
|
if hasattr(ctx, "_app_retry_policy") and ctx._app_retry_policy is not None:
|
|
374
381
|
return cast(RetryPolicy, ctx._app_retry_policy)
|
|
375
382
|
|
|
@@ -426,8 +433,9 @@ def activity(
|
|
|
426
433
|
"""
|
|
427
434
|
Decorator for defining activities (atomic units of work) with automatic retry.
|
|
428
435
|
|
|
429
|
-
Activities
|
|
430
|
-
parameter, followed by any other parameters.
|
|
436
|
+
Activities can be async or sync functions that take a WorkflowContext as the first
|
|
437
|
+
parameter, followed by any other parameters. Sync functions are executed in a
|
|
438
|
+
thread pool to avoid blocking the event loop.
|
|
431
439
|
|
|
432
440
|
Activities are automatically wrapped in a transaction, ensuring that
|
|
433
441
|
activity execution, history recording, and event sending are atomic.
|
|
@@ -443,34 +451,39 @@ def activity(
|
|
|
443
451
|
Workflow function.
|
|
444
452
|
|
|
445
453
|
Example:
|
|
446
|
-
>>> @activity #
|
|
447
|
-
...
|
|
448
|
-
... # Your business logic here
|
|
454
|
+
>>> @activity # Sync activity (no async/await)
|
|
455
|
+
... def reserve_inventory(ctx: WorkflowContext, order_id: str) -> dict:
|
|
456
|
+
... # Your business logic here (executed in thread pool)
|
|
457
|
+
... return {"reservation_id": "123"}
|
|
458
|
+
|
|
459
|
+
>>> @activity # Async activity (recommended for I/O-bound operations)
|
|
460
|
+
... async def reserve_inventory_async(ctx: WorkflowContext, order_id: str) -> dict:
|
|
461
|
+
... # Async I/O operations
|
|
449
462
|
... return {"reservation_id": "123"}
|
|
450
463
|
|
|
451
464
|
>>> from edda.retry import RetryPolicy, AGGRESSIVE_RETRY
|
|
452
465
|
>>> @activity(retry_policy=AGGRESSIVE_RETRY) # Custom retry policy
|
|
453
|
-
...
|
|
466
|
+
... def process_payment(ctx: WorkflowContext, amount: float) -> dict:
|
|
454
467
|
... # Fast retries for low-latency services
|
|
455
468
|
... return {"status": "completed"}
|
|
456
469
|
|
|
457
470
|
>>> @activity # Non-idempotent operations cached during replay
|
|
458
|
-
...
|
|
471
|
+
... def charge_credit_card(ctx: WorkflowContext, amount: float) -> dict:
|
|
459
472
|
... # External API call - result is cached, won't be called again on replay
|
|
460
473
|
... # If this fails, automatic retry with exponential backoff
|
|
461
474
|
... return {"transaction_id": "txn_123"}
|
|
462
475
|
|
|
463
476
|
>>> from edda.exceptions import TerminalError
|
|
464
477
|
>>> @activity
|
|
465
|
-
...
|
|
466
|
-
... user =
|
|
478
|
+
... def validate_user(ctx: WorkflowContext, user_id: str) -> dict:
|
|
479
|
+
... user = fetch_user(user_id) # No await needed for sync
|
|
467
480
|
... if not user:
|
|
468
481
|
... # Don't retry - user doesn't exist
|
|
469
482
|
... raise TerminalError(f"User {user_id} not found")
|
|
470
483
|
... return {"user_id": user_id, "name": user.name}
|
|
471
484
|
|
|
472
485
|
Args:
|
|
473
|
-
func: Async function to wrap as an activity
|
|
486
|
+
func: Async or sync function to wrap as an activity
|
|
474
487
|
retry_policy: Optional retry policy for this activity.
|
|
475
488
|
If None, uses the default policy from EddaApp.
|
|
476
489
|
|
|
@@ -480,14 +493,14 @@ def activity(
|
|
|
480
493
|
Raises:
|
|
481
494
|
RetryExhaustedError: When all retry attempts are exhausted
|
|
482
495
|
TerminalError: For non-retryable errors (no retry attempted)
|
|
496
|
+
|
|
497
|
+
Sync activities are executed in a thread pool. For I/O-bound operations
|
|
498
|
+
(database queries, HTTP requests, etc.), async activities are recommended
|
|
499
|
+
for better performance.
|
|
483
500
|
"""
|
|
484
501
|
|
|
485
502
|
def decorator(f: F) -> F:
|
|
486
|
-
#
|
|
487
|
-
if not inspect.iscoroutinefunction(f):
|
|
488
|
-
raise TypeError(f"Activity {f.__name__} must be an async function")
|
|
489
|
-
|
|
490
|
-
# Create the Activity wrapper with retry policy
|
|
503
|
+
# Create the Activity wrapper with retry policy (supports both sync and async)
|
|
491
504
|
activity_wrapper = Activity(f, retry_policy=retry_policy)
|
|
492
505
|
|
|
493
506
|
# Mark as activity for introspection
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WSGI adapter for Edda framework.
|
|
3
|
+
|
|
4
|
+
This module provides a WSGI adapter that wraps EddaApp (ASGI) for use with
|
|
5
|
+
WSGI servers like gunicorn or uWSGI.
|
|
6
|
+
|
|
7
|
+
The adapter uses a2wsgi to convert the ASGI interface to WSGI.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from a2wsgi import ASGIMiddleware
|
|
13
|
+
|
|
14
|
+
from edda.app import EddaApp
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_wsgi_app(edda_app: EddaApp) -> Any:
|
|
18
|
+
"""
|
|
19
|
+
Create a WSGI-compatible application from an EddaApp instance.
|
|
20
|
+
|
|
21
|
+
This function wraps an EddaApp (ASGI) with a2wsgi's ASGIMiddleware,
|
|
22
|
+
making it compatible with WSGI servers like gunicorn or uWSGI.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
edda_app: An initialized EddaApp instance
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
A WSGI-compatible application callable
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
Basic usage with EddaApp::
|
|
32
|
+
|
|
33
|
+
from edda import EddaApp
|
|
34
|
+
from edda.wsgi import create_wsgi_app
|
|
35
|
+
from edda.storage.sqlalchemy_storage import SQLAlchemyStorage
|
|
36
|
+
|
|
37
|
+
# Create storage and EddaApp
|
|
38
|
+
storage = SQLAlchemyStorage("sqlite:///edda.db")
|
|
39
|
+
app = EddaApp(storage=storage)
|
|
40
|
+
|
|
41
|
+
# Create WSGI application
|
|
42
|
+
wsgi_app = create_wsgi_app(app)
|
|
43
|
+
|
|
44
|
+
Running with gunicorn::
|
|
45
|
+
|
|
46
|
+
# In your module (e.g., demo_app.py):
|
|
47
|
+
from edda import EddaApp
|
|
48
|
+
from edda.wsgi import create_wsgi_app
|
|
49
|
+
|
|
50
|
+
application = EddaApp(...) # ASGI
|
|
51
|
+
wsgi_application = create_wsgi_app(application) # WSGI
|
|
52
|
+
|
|
53
|
+
# Command line:
|
|
54
|
+
$ gunicorn demo_app:wsgi_application --workers 4
|
|
55
|
+
|
|
56
|
+
Running with uWSGI::
|
|
57
|
+
|
|
58
|
+
$ uwsgi --http :8000 --wsgi-file demo_app.py --callable wsgi_application
|
|
59
|
+
|
|
60
|
+
Background tasks (auto-resume, timer checks, etc.) will run in each
|
|
61
|
+
worker process.
|
|
62
|
+
|
|
63
|
+
For production deployments, ASGI servers (uvicorn, hypercorn) are
|
|
64
|
+
recommended for better performance with Edda's async architecture.
|
|
65
|
+
WSGI support is provided for compatibility with existing infrastructure
|
|
66
|
+
and for users who prefer synchronous programming with sync activities.
|
|
67
|
+
|
|
68
|
+
See Also:
|
|
69
|
+
- :class:`edda.app.EddaApp`: The main ASGI application class
|
|
70
|
+
- :func:`edda.activity.activity`: Decorator supporting sync activities
|
|
71
|
+
"""
|
|
72
|
+
# Type ignore due to a2wsgi's strict ASGI type checking
|
|
73
|
+
# EddaApp implements ASGI 3.0 interface correctly
|
|
74
|
+
return ASGIMiddleware(edda_app) # type: ignore[arg-type]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
__all__ = ["create_wsgi_app"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "edda-framework"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Lightweight Durable Execution Framework"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Yasushi Itoh", email = "6240399+i2y@users.noreply.github.com" }
|
|
@@ -22,6 +22,8 @@ classifiers = [
|
|
|
22
22
|
]
|
|
23
23
|
|
|
24
24
|
dependencies = [
|
|
25
|
+
"anyio>=4.0.0",
|
|
26
|
+
"a2wsgi>=1.10.0",
|
|
25
27
|
"cloudevents>=1.12.0",
|
|
26
28
|
"httpx>=0.28.1",
|
|
27
29
|
"pydantic>=2.0.0",
|
|
@@ -42,14 +42,20 @@ class TestActivityDecorator:
|
|
|
42
42
|
assert my_test_activity.__name__ == "my_test_activity"
|
|
43
43
|
assert my_test_activity.__doc__ == "Test activity docstring."
|
|
44
44
|
|
|
45
|
-
async def
|
|
46
|
-
"""Test that decorator
|
|
45
|
+
async def test_activity_decorator_supports_sync(self):
|
|
46
|
+
"""Test that decorator supports both sync and async functions."""
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
@activity
|
|
49
|
+
def sync_function(ctx: WorkflowContext) -> dict:
|
|
50
|
+
return {"result": "sync"}
|
|
51
|
+
|
|
52
|
+
# Verify it's a valid activity
|
|
53
|
+
assert hasattr(sync_function, "_is_activity")
|
|
54
|
+
assert sync_function._is_activity is True
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
# Verify it's detected as a sync function
|
|
57
|
+
assert hasattr(sync_function, "is_async")
|
|
58
|
+
assert sync_function.is_async is False
|
|
53
59
|
|
|
54
60
|
async def test_activity_decorator_creates_wrapper(self):
|
|
55
61
|
"""Test that decorator creates Activity wrapper."""
|