pyrelay-workflow 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Developer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyrelay-workflow
3
+ Version: 0.1.0
4
+ Summary: A lightweight, async-first Python workflow engine with dynamic branching and rich visual dashboarding.
5
+ Author-email: Developer <developer@example.com>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: rich>=12.0.0
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
17
+ Requires-Dist: pytest-asyncio>=0.18.0; extra == "dev"
18
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
19
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ # โšก pyrelay
23
+
24
+ [![PyPI Version](https://img.shields.io/badge/pypi-v0.1.0-blue.svg)](https://pypi.org/project/pyrelay/)
25
+ [![Python versions](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-blue.svg)](https://www.python.org/)
26
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://opensource.org/licenses/MIT)
27
+
28
+ `pyrelay` is a lightweight, high-performance, async-first Python workflow engine. It allows developers to model, execute, and monitor complex task pipelines with native pythonic flows (`if/else` branching), zero complex DSLs, live terminal dashboards, and interactive web visualizers.
29
+
30
+ ---
31
+
32
+ ## โœจ Features
33
+
34
+ - **๐Ÿ Native Python Branching**: Use standard Python `if/else` and loops. The execution graph is discovered dynamically at runtime.
35
+ - **โšก Async-First Architecture**: Built natively on top of Python `asyncio`. Executes independent tasks concurrently.
36
+ - **๐Ÿงต Auto-Offloading for Sync Tasks**: Run blocking synchronous functions without stalling the async event loop (automatically offloaded to thread executors).
37
+ - **โฑ๏ธ Fault Tolerance**: Custom task-level retry policies, exponential backoff, and timeouts out-of-the-box.
38
+ - **๐Ÿ–ฅ๏ธ Live Console Dashboard**: Animated status trees, progress bars, and log streams in your terminal powered by `rich`.
39
+ - **๐ŸŒ Draggable HTML/JS Visualizer**: Compile any workflow run into a standalone dark-mode HTML file containing a responsive network chart powered by Vis-Network.
40
+
41
+ ---
42
+
43
+ ## ๐Ÿ“ฆ Installation
44
+
45
+ Initialize your project and install `pyrelay`:
46
+
47
+ ```bash
48
+ pip install pyrelay
49
+ ```
50
+
51
+ *(Requires Python 3.9+)*
52
+
53
+ ---
54
+
55
+ ## ๐Ÿš€ Quickstart
56
+
57
+ Create a workflow by wrapping functions with `@task` and `@workflow`. Awaited tasks automatically map dependencies based on their execution order!
58
+
59
+ ```python
60
+ import asyncio
61
+ from pyrelay import task, workflow, WorkflowRun
62
+ from pyrelay.dashboard import ConsoleDashboard
63
+
64
+ @task(retries=2, retry_delay=0.5)
65
+ async def fetch_user_data(user_id: int):
66
+ await asyncio.sleep(0.5)
67
+ return {"id": user_id, "name": "Alice", "status": "active"}
68
+
69
+ @task()
70
+ async def process_account(user: dict):
71
+ await asyncio.sleep(0.3)
72
+ return f"Account processed for {user['name']}"
73
+
74
+ @workflow(name="Account Pipeline")
75
+ async def account_flow(user_id: int):
76
+ user = await fetch_user_data(user_id)
77
+ result = await process_account(user)
78
+ return result
79
+
80
+ if __name__ == "__main__":
81
+ run = WorkflowRun(account_flow)
82
+
83
+ # Attach the live CLI dashboard
84
+ ConsoleDashboard(run)
85
+
86
+ # Run the workflow blocking/sync
87
+ run.run_sync(user_id=101)
88
+ ```
89
+
90
+ ---
91
+
92
+ ## ๐ŸŒฟ Dynamic Branching (Option B)
93
+
94
+ In `pyrelay`, branching is simply Python. The engine tracks exactly which path was taken and marks skipped branches appropriately in reports.
95
+
96
+ ```python
97
+ @task()
98
+ async def process_premium_user(user: dict):
99
+ return "VIP treatment applied"
100
+
101
+ @task()
102
+ async def process_standard_user(user: dict):
103
+ return "Standard access configured"
104
+
105
+ @workflow(name="User Routing")
106
+ async def user_routing_flow(user: dict):
107
+ # Branching happens at runtime based on real data values
108
+ if user["is_premium"]:
109
+ result = await process_premium_user(user)
110
+ else:
111
+ result = await process_standard_user(user)
112
+ return result
113
+ ```
114
+
115
+ ---
116
+
117
+ ## ๐ŸŽจ Visualization Telemetry
118
+
119
+ Generating an interactive, visual representation of your executed workflow is a single method call:
120
+
121
+ ```python
122
+ # Save interactive HTML dashboard
123
+ run.visualize("workflow_run.html")
124
+ ```
125
+
126
+ Open `workflow_run.html` in any browser to get a premium dark-mode interface where you can:
127
+ 1. Pan and zoom around the execution DAG.
128
+ 2. Review node colors indicating statuses: **Green (Success)**, **Red (Failed)**, **Blue (Running)**, **Grey (Pending)**.
129
+ 3. Click any node to slide open a telemetry inspect panel containing:
130
+ - Inputs and outputs
131
+ - Time elapsed, start/end timestamps
132
+ - Retry counts and exceptions
133
+
134
+ ---
135
+
136
+ ## โš™๏ธ Configuration Properties
137
+
138
+ The `@task` decorator accepts the following metadata options:
139
+
140
+ | Property | Type | Default | Description |
141
+ | :--- | :--- | :--- | :--- |
142
+ | `name` | `str` | *Function Name* | Identifier for the task (used in dashboard and graphs). |
143
+ | `retries` | `int` | `0` | Number of attempts to retry if the function raises an exception. |
144
+ | `retry_delay` | `float` | `1.0` | Initial delay (seconds) before attempting a retry. |
145
+ | `backoff_factor`| `float` | `2.0` | Exponential multiplier for consecutive retries (delay * backoff_factor^attempt). |
146
+ | `timeout` | `float` | `None` | Max seconds before cancelling execution and marking the task as failed. |
147
+
148
+ ---
149
+
150
+ ## ๐Ÿงช Running Tests
151
+
152
+ To run the unit and integration test suite, install development dependencies and run `pytest`:
153
+
154
+ ```bash
155
+ pip install -e ".[dev]"
156
+ pytest tests/
157
+ ```
158
+
159
+ ---
160
+
161
+ ## ๐Ÿ“„ License
162
+
163
+ Distributed under the MIT License. See `LICENSE` for details.
@@ -0,0 +1,142 @@
1
+ # โšก pyrelay
2
+
3
+ [![PyPI Version](https://img.shields.io/badge/pypi-v0.1.0-blue.svg)](https://pypi.org/project/pyrelay/)
4
+ [![Python versions](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-blue.svg)](https://www.python.org/)
5
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ `pyrelay` is a lightweight, high-performance, async-first Python workflow engine. It allows developers to model, execute, and monitor complex task pipelines with native pythonic flows (`if/else` branching), zero complex DSLs, live terminal dashboards, and interactive web visualizers.
8
+
9
+ ---
10
+
11
+ ## โœจ Features
12
+
13
+ - **๐Ÿ Native Python Branching**: Use standard Python `if/else` and loops. The execution graph is discovered dynamically at runtime.
14
+ - **โšก Async-First Architecture**: Built natively on top of Python `asyncio`. Executes independent tasks concurrently.
15
+ - **๐Ÿงต Auto-Offloading for Sync Tasks**: Run blocking synchronous functions without stalling the async event loop (automatically offloaded to thread executors).
16
+ - **โฑ๏ธ Fault Tolerance**: Custom task-level retry policies, exponential backoff, and timeouts out-of-the-box.
17
+ - **๐Ÿ–ฅ๏ธ Live Console Dashboard**: Animated status trees, progress bars, and log streams in your terminal powered by `rich`.
18
+ - **๐ŸŒ Draggable HTML/JS Visualizer**: Compile any workflow run into a standalone dark-mode HTML file containing a responsive network chart powered by Vis-Network.
19
+
20
+ ---
21
+
22
+ ## ๐Ÿ“ฆ Installation
23
+
24
+ Initialize your project and install `pyrelay`:
25
+
26
+ ```bash
27
+ pip install pyrelay
28
+ ```
29
+
30
+ *(Requires Python 3.9+)*
31
+
32
+ ---
33
+
34
+ ## ๐Ÿš€ Quickstart
35
+
36
+ Create a workflow by wrapping functions with `@task` and `@workflow`. Awaited tasks automatically map dependencies based on their execution order!
37
+
38
+ ```python
39
+ import asyncio
40
+ from pyrelay import task, workflow, WorkflowRun
41
+ from pyrelay.dashboard import ConsoleDashboard
42
+
43
+ @task(retries=2, retry_delay=0.5)
44
+ async def fetch_user_data(user_id: int):
45
+ await asyncio.sleep(0.5)
46
+ return {"id": user_id, "name": "Alice", "status": "active"}
47
+
48
+ @task()
49
+ async def process_account(user: dict):
50
+ await asyncio.sleep(0.3)
51
+ return f"Account processed for {user['name']}"
52
+
53
+ @workflow(name="Account Pipeline")
54
+ async def account_flow(user_id: int):
55
+ user = await fetch_user_data(user_id)
56
+ result = await process_account(user)
57
+ return result
58
+
59
+ if __name__ == "__main__":
60
+ run = WorkflowRun(account_flow)
61
+
62
+ # Attach the live CLI dashboard
63
+ ConsoleDashboard(run)
64
+
65
+ # Run the workflow blocking/sync
66
+ run.run_sync(user_id=101)
67
+ ```
68
+
69
+ ---
70
+
71
+ ## ๐ŸŒฟ Dynamic Branching (Option B)
72
+
73
+ In `pyrelay`, branching is simply Python. The engine tracks exactly which path was taken and marks skipped branches appropriately in reports.
74
+
75
+ ```python
76
+ @task()
77
+ async def process_premium_user(user: dict):
78
+ return "VIP treatment applied"
79
+
80
+ @task()
81
+ async def process_standard_user(user: dict):
82
+ return "Standard access configured"
83
+
84
+ @workflow(name="User Routing")
85
+ async def user_routing_flow(user: dict):
86
+ # Branching happens at runtime based on real data values
87
+ if user["is_premium"]:
88
+ result = await process_premium_user(user)
89
+ else:
90
+ result = await process_standard_user(user)
91
+ return result
92
+ ```
93
+
94
+ ---
95
+
96
+ ## ๐ŸŽจ Visualization Telemetry
97
+
98
+ Generating an interactive, visual representation of your executed workflow is a single method call:
99
+
100
+ ```python
101
+ # Save interactive HTML dashboard
102
+ run.visualize("workflow_run.html")
103
+ ```
104
+
105
+ Open `workflow_run.html` in any browser to get a premium dark-mode interface where you can:
106
+ 1. Pan and zoom around the execution DAG.
107
+ 2. Review node colors indicating statuses: **Green (Success)**, **Red (Failed)**, **Blue (Running)**, **Grey (Pending)**.
108
+ 3. Click any node to slide open a telemetry inspect panel containing:
109
+ - Inputs and outputs
110
+ - Time elapsed, start/end timestamps
111
+ - Retry counts and exceptions
112
+
113
+ ---
114
+
115
+ ## โš™๏ธ Configuration Properties
116
+
117
+ The `@task` decorator accepts the following metadata options:
118
+
119
+ | Property | Type | Default | Description |
120
+ | :--- | :--- | :--- | :--- |
121
+ | `name` | `str` | *Function Name* | Identifier for the task (used in dashboard and graphs). |
122
+ | `retries` | `int` | `0` | Number of attempts to retry if the function raises an exception. |
123
+ | `retry_delay` | `float` | `1.0` | Initial delay (seconds) before attempting a retry. |
124
+ | `backoff_factor`| `float` | `2.0` | Exponential multiplier for consecutive retries (delay * backoff_factor^attempt). |
125
+ | `timeout` | `float` | `None` | Max seconds before cancelling execution and marking the task as failed. |
126
+
127
+ ---
128
+
129
+ ## ๐Ÿงช Running Tests
130
+
131
+ To run the unit and integration test suite, install development dependencies and run `pytest`:
132
+
133
+ ```bash
134
+ pip install -e ".[dev]"
135
+ pytest tests/
136
+ ```
137
+
138
+ ---
139
+
140
+ ## ๐Ÿ“„ License
141
+
142
+ Distributed under the MIT License. See `LICENSE` for details.
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyrelay-workflow"
7
+ version = "0.1.0"
8
+ description = "A lightweight, async-first Python workflow engine with dynamic branching and rich visual dashboarding."
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "Developer", email = "developer@example.com" }
12
+ ]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ ]
20
+ requires-python = ">=3.9"
21
+ dependencies = [
22
+ "rich>=12.0.0",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ dev = [
27
+ "pytest>=7.0.0",
28
+ "pytest-asyncio>=0.18.0",
29
+ "ruff>=0.1.0",
30
+ "mypy>=1.0.0",
31
+ ]
32
+
33
+ [tool.ruff]
34
+ line-length = 88
35
+ target-version = "py39"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,14 @@
1
+ from .core import task, workflow, WorkflowRun
2
+ from .exceptions import WorkflowError, TaskFailedError, TaskTimeoutError
3
+ # Import visualizer to trigger WorkflowRun monkeypatching
4
+ from . import visualizer
5
+
6
+ __all__ = [
7
+ "task",
8
+ "workflow",
9
+ "WorkflowRun",
10
+ "WorkflowError",
11
+ "TaskFailedError",
12
+ "TaskTimeoutError",
13
+ ]
14
+
@@ -0,0 +1,220 @@
1
+ import asyncio
2
+ import contextvars
3
+ from datetime import datetime
4
+ from enum import Enum
5
+ from typing import Any, Callable, Dict, Optional, Set
6
+ import uuid
7
+
8
+ # Async context storage for active workflow run
9
+ active_run_context: contextvars.ContextVar[Optional["WorkflowRun"]] = contextvars.ContextVar(
10
+ "active_run_context", default=None
11
+ )
12
+
13
+
14
+ class TaskStatus(str, Enum):
15
+ PENDING = "PENDING"
16
+ running = "RUNNING" # Case-insensitive compatibility
17
+ RUNNING = "RUNNING"
18
+ SUCCESS = "SUCCESS"
19
+ FAILED = "FAILED"
20
+ SKIPPED = "SKIPPED"
21
+
22
+
23
+ class Task:
24
+ """Decorator wrapper that stores task metadata and intercepts calls inside workflows."""
25
+
26
+ def __init__(
27
+ self,
28
+ func: Callable[..., Any],
29
+ name: Optional[str] = None,
30
+ retries: int = 0,
31
+ retry_delay: float = 1.0,
32
+ backoff_factor: float = 2.0,
33
+ timeout: Optional[float] = None,
34
+ ):
35
+ self.func = func
36
+ self.name = name or func.__name__
37
+ self.retries = retries
38
+ self.retry_delay = retry_delay
39
+ self.backoff_factor = backoff_factor
40
+ self.timeout = timeout
41
+ self.is_coroutine = asyncio.iscoroutinefunction(func)
42
+
43
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
44
+ run = active_run_context.get()
45
+ if not run:
46
+ # Standalone invocation (useful for unit testing)
47
+ return self.func(*args, **kwargs)
48
+
49
+ return run.execute_task(self, args, kwargs)
50
+
51
+
52
+ class TaskInstance:
53
+ """Tracks state and telemetry for a single task execution."""
54
+
55
+ def __init__(self, task: Task, run_id: str):
56
+ self.id = str(uuid.uuid4())
57
+ self.task = task
58
+ self.name = task.name
59
+ self.run_id = run_id
60
+ self.status = TaskStatus.PENDING
61
+
62
+ self.start_time: Optional[datetime] = None
63
+ self.end_time: Optional[datetime] = None
64
+ self.duration: float = 0.0
65
+
66
+ self.attempts = 0
67
+ self.inputs: Dict[str, Any] = {}
68
+ self.result: Any = None
69
+ self.error: Optional[str] = None
70
+ self.predecessors: Set[str] = set()
71
+
72
+ def start(self, inputs: Dict[str, Any]) -> None:
73
+ self.status = TaskStatus.RUNNING
74
+ self.start_time = datetime.now()
75
+ self.inputs = inputs
76
+ self.attempts += 1
77
+
78
+ def complete(self, result: Any) -> None:
79
+ self.status = TaskStatus.SUCCESS
80
+ self.end_time = datetime.now()
81
+ if self.start_time:
82
+ self.duration = (self.end_time - self.start_time).total_seconds()
83
+ self.result = result
84
+
85
+ def fail(self, error: Exception) -> None:
86
+ self.status = TaskStatus.FAILED
87
+ self.end_time = datetime.now()
88
+ if self.start_time:
89
+ self.duration = (self.end_time - self.start_time).total_seconds()
90
+ self.error = f"{type(error).__name__}: {str(error)}"
91
+
92
+ def skip(self) -> None:
93
+ self.status = TaskStatus.SKIPPED
94
+
95
+ def to_dict(self) -> Dict[str, Any]:
96
+ return {
97
+ "id": self.id,
98
+ "name": self.name,
99
+ "status": self.status.value,
100
+ "start_time": self.start_time.isoformat() if self.start_time else None,
101
+ "end_time": self.end_time.isoformat() if self.end_time else None,
102
+ "duration": self.duration,
103
+ "attempts": self.attempts,
104
+ "inputs": str(self.inputs),
105
+ "result": str(self.result) if self.result is not None else None,
106
+ "error": self.error,
107
+ "predecessors": list(self.predecessors),
108
+ }
109
+
110
+
111
+ class Workflow:
112
+ """Blueprint representing a workflow defined with `@workflow`."""
113
+
114
+ def __init__(self, func: Callable[..., Any], name: Optional[str] = None):
115
+ self.func = func
116
+ self.name = name or func.__name__
117
+
118
+ def run(self, *args: Any, **kwargs: Any) -> "WorkflowRun":
119
+ run = WorkflowRun(self)
120
+ return run.run_sync(*args, **kwargs)
121
+
122
+ async def run_async(self, *args: Any, **kwargs: Any) -> "WorkflowRun":
123
+ run = WorkflowRun(self)
124
+ await run.run_async(*args, **kwargs)
125
+ return run
126
+
127
+
128
+ class WorkflowRun:
129
+ """Coordinates the execution trace and lifecycle events for a single pipeline run."""
130
+
131
+ def __init__(self, workflow: Workflow):
132
+ self.id = str(uuid.uuid4())
133
+ self.workflow = workflow
134
+ self.status = "PENDING"
135
+ self.start_time: Optional[datetime] = None
136
+ self.end_time: Optional[datetime] = None
137
+ self.duration: float = 0.0
138
+ self.result: Any = None
139
+ self.error: Optional[str] = None
140
+
141
+ self.tasks: Dict[str, TaskInstance] = {}
142
+ self.event_callbacks: List[Callable[[str, Any], None]] = []
143
+ self.executor_hook: Optional[Callable[[Task, tuple, dict], Any]] = None
144
+
145
+ def add_event_callback(self, callback: Callable[[str, Any], None]) -> None:
146
+ self.event_callbacks.append(callback)
147
+
148
+ def emit_event(self, event_type: str, data: Any) -> None:
149
+ for cb in self.event_callbacks:
150
+ try:
151
+ cb(event_type, data)
152
+ except Exception:
153
+ pass
154
+
155
+ def get_frontier(self) -> Set[str]:
156
+ """Identifies completed tasks that have no dependent downstream runs.
157
+
158
+ Uses:
159
+ Frontier = {Completed} - {Predecessors of any task}
160
+ """
161
+ all_completed = {
162
+ t_id for t_id, t in self.tasks.items()
163
+ if t.status in (TaskStatus.SUCCESS, TaskStatus.FAILED, TaskStatus.SKIPPED)
164
+ }
165
+
166
+ all_predecessors = set()
167
+ for t in self.tasks.values():
168
+ all_predecessors.update(t.predecessors)
169
+
170
+ return all_completed - all_predecessors
171
+
172
+ def execute_task(self, task: Task, args: tuple, kwargs: dict) -> Any:
173
+ if not self.executor_hook:
174
+ raise RuntimeError("Engine runner not initialized on this WorkflowRun.")
175
+ return self.executor_hook(task, args, kwargs)
176
+
177
+ def run_sync(self, *args: Any, **kwargs: Any) -> "WorkflowRun":
178
+ try:
179
+ loop = asyncio.get_event_loop()
180
+ except RuntimeError:
181
+ loop = asyncio.new_event_loop()
182
+ asyncio.set_event_loop(loop)
183
+
184
+ if loop.is_running():
185
+ # Jupyter and FastAPI loops are already running; patch to prevent loop collision
186
+ import nest_asyncio
187
+ nest_asyncio.apply()
188
+
189
+ loop.run_until_complete(self.run_async(*args, **kwargs))
190
+ return self
191
+
192
+ async def run_async(self, *args: Any, **kwargs: Any) -> None:
193
+ from .engine import ExecutionEngine
194
+ engine = ExecutionEngine(self)
195
+ await engine.run(args, kwargs)
196
+
197
+
198
+ def task(
199
+ name: Optional[str] = None,
200
+ retries: int = 0,
201
+ retry_delay: float = 1.0,
202
+ backoff_factor: float = 2.0,
203
+ timeout: Optional[float] = None,
204
+ ) -> Callable[[Callable[..., Any]], Task]:
205
+ def decorator(func: Callable[..., Any]) -> Task:
206
+ return Task(
207
+ func=func,
208
+ name=name,
209
+ retries=retries,
210
+ retry_delay=retry_delay,
211
+ backoff_factor=backoff_factor,
212
+ timeout=timeout,
213
+ )
214
+ return decorator
215
+
216
+
217
+ def workflow(name: Optional[str] = None) -> Callable[[Callable[..., Any]], Workflow]:
218
+ def decorator(func: Callable[..., Any]) -> Workflow:
219
+ return Workflow(func=func, name=name)
220
+ return decorator