edda-framework 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl

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/__init__.py CHANGED
@@ -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
  ]
edda/activity.py CHANGED
@@ -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
- result = await self.func(ctx, *args, **kwargs)
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
- # Note: This will be implemented in Phase 5 (edda/app.py)
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 are async functions that take a WorkflowContext as the first
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 # Uses default retry policy (5 attempts, exponential backoff)
447
- ... async def reserve_inventory(ctx: WorkflowContext, order_id: str) -> dict:
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
- ... async def process_payment(ctx: WorkflowContext, amount: float) -> dict:
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
- ... async def charge_credit_card(ctx: WorkflowContext, amount: float) -> dict:
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
- ... async def validate_user(ctx: WorkflowContext, user_id: str) -> dict:
466
- ... user = await fetch_user(user_id)
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
- # Verify the function is async
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
edda/wsgi.py ADDED
@@ -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
  Metadata-Version: 2.4
2
2
  Name: edda-framework
3
- Version: 0.1.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:
@@ -1,5 +1,5 @@
1
- edda/__init__.py,sha256=a8Rp16Qmj4iuO1MXmrpO_oGIMLY-vkg-vW6z-CATBas,1596
2
- edda/activity.py,sha256=pDKs3rh4QpnRnIEAHi3rxEXkDL71pBE_La7skde9vJg,20320
1
+ edda/__init__.py,sha256=gmJd0ooVbGNMOLlSj-r6rEt3IkY-FZYCFjhWjIltqlk,1657
2
+ edda/activity.py,sha256=nRm9eBrr0lFe4ZRQ2whyZ6mo5xd171ITIVhqytUhOpw,21025
3
3
  edda/app.py,sha256=YxURvhaC1dKcymOwuS1PHU6_iOl_5cZWse2vAKSyX8w,37471
4
4
  edda/compensation.py,sha256=CmnyJy4jAklVrtLJodNOcj6vxET6pdarxM1Yx2RHlL4,11898
5
5
  edda/context.py,sha256=YZKBNtblRcaFqte1Y9t2cIP3JHzK-5Tu40x5i5FHtnU,17789
@@ -11,6 +11,7 @@ edda/pydantic_utils.py,sha256=dGVPNrrttDeq1k233PopCtjORYjZitsgASPfPnO6R10,9056
11
11
  edda/replay.py,sha256=5RIRd0q2ZrH9iiiy35eOUii2cipYg9dlua56OAXvIk4,32499
12
12
  edda/retry.py,sha256=t4_E1skrhotA1XWHTLbKi-DOgCMasOUnhI9OT-O_eCE,6843
13
13
  edda/workflow.py,sha256=daSppYAzgXkjY_9-HS93Zi7_tPR6srmchxY5YfwgU-4,7239
14
+ edda/wsgi.py,sha256=1pGE5fhHpcsYnDR8S3NEFKWUs5P0JK4roTAzX9BsIj0,2391
14
15
  edda/outbox/__init__.py,sha256=azXG1rtheJEjOyoWmMsBeR2jp8Bz02R3wDEd5tQnaWA,424
15
16
  edda/outbox/relayer.py,sha256=2tnN1aOQ8pKWfwEGIlYwYLLwyOKXBjZ4XZsIr1HjgK4,9454
16
17
  edda/outbox/transactional.py,sha256=LFfUjunqRlGibaINi-efGXFFivWGO7v3mhqrqyGW6Nw,3808
@@ -28,8 +29,8 @@ edda/viewer_ui/data_service.py,sha256=mXV6bL6REa_UKsk8xMGBIFbsbLpIxe91lX3wgn-FOj
28
29
  edda/visualizer/__init__.py,sha256=DOpDstNhR0VcXAs_eMKxaL30p_0u4PKZ4o2ndnYhiRo,343
29
30
  edda/visualizer/ast_analyzer.py,sha256=plmx7C9X_X35xLY80jxOL3ljg3afXxBePRZubqUIkxY,13663
30
31
  edda/visualizer/mermaid_generator.py,sha256=XWa2egoOTNDfJEjPcwoxwQmblUqXf7YInWFjFRI1QGo,12457
31
- edda_framework-0.1.0.dist-info/METADATA,sha256=6T8Pt-0Y-SkZwsqKVSvMqEhkVlutu_XHbUUtWSF5Urw,26318
32
- edda_framework-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
33
- edda_framework-0.1.0.dist-info/entry_points.txt,sha256=dPH47s6UoJgUZxHoeSMqZsQkLaSE-SGLi-gh88k2WrU,48
34
- edda_framework-0.1.0.dist-info/licenses/LICENSE,sha256=udxb-V7_cYKTHqW7lNm48rxJ-Zpf0WAY_PyGDK9BPCo,1069
35
- edda_framework-0.1.0.dist-info/RECORD,,
32
+ edda_framework-0.2.0.dist-info/METADATA,sha256=9qiWE872ENCjRcSC4ezcz5y1v1g5TfpLRgIiQUJXgHc,27823
33
+ edda_framework-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
34
+ edda_framework-0.2.0.dist-info/entry_points.txt,sha256=dPH47s6UoJgUZxHoeSMqZsQkLaSE-SGLi-gh88k2WrU,48
35
+ edda_framework-0.2.0.dist-info/licenses/LICENSE,sha256=udxb-V7_cYKTHqW7lNm48rxJ-Zpf0WAY_PyGDK9BPCo,1069
36
+ edda_framework-0.2.0.dist-info/RECORD,,