highway-dsl 0.0.3__py3-none-any.whl → 1.0.3__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.
- highway_dsl/workflow_dsl.py +108 -34
- {highway_dsl-0.0.3.dist-info → highway_dsl-1.0.3.dist-info}/METADATA +77 -8
- highway_dsl-1.0.3.dist-info/RECORD +7 -0
- highway_dsl-0.0.3.dist-info/RECORD +0 -7
- {highway_dsl-0.0.3.dist-info → highway_dsl-1.0.3.dist-info}/WHEEL +0 -0
- {highway_dsl-0.0.3.dist-info → highway_dsl-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {highway_dsl-0.0.3.dist-info → highway_dsl-1.0.3.dist-info}/top_level.txt +0 -0
highway_dsl/workflow_dsl.py
CHANGED
|
@@ -39,6 +39,9 @@ class BaseOperator(BaseModel, ABC):
|
|
|
39
39
|
retry_policy: Optional[RetryPolicy] = None
|
|
40
40
|
timeout_policy: Optional[TimeoutPolicy] = None
|
|
41
41
|
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
42
|
+
is_internal_loop_task: bool = Field(
|
|
43
|
+
default=False, exclude=True
|
|
44
|
+
) # Mark if task is internal to a loop
|
|
42
45
|
|
|
43
46
|
model_config = ConfigDict(use_enum_values=True, arbitrary_types_allowed=True)
|
|
44
47
|
|
|
@@ -91,7 +94,16 @@ class ParallelOperator(BaseOperator):
|
|
|
91
94
|
|
|
92
95
|
class ForEachOperator(BaseOperator):
|
|
93
96
|
items: str
|
|
94
|
-
|
|
97
|
+
loop_body: List[
|
|
98
|
+
Union[
|
|
99
|
+
TaskOperator,
|
|
100
|
+
ConditionOperator,
|
|
101
|
+
WaitOperator,
|
|
102
|
+
ParallelOperator,
|
|
103
|
+
"ForEachOperator",
|
|
104
|
+
"WhileOperator",
|
|
105
|
+
]
|
|
106
|
+
] = Field(default_factory=list)
|
|
95
107
|
operator_type: OperatorType = Field(OperatorType.FOREACH, frozen=True)
|
|
96
108
|
|
|
97
109
|
|
|
@@ -204,12 +216,30 @@ class WorkflowBuilder:
|
|
|
204
216
|
self._current_task: Optional[str] = None
|
|
205
217
|
self.parent = parent
|
|
206
218
|
|
|
219
|
+
def _add_task(
|
|
220
|
+
self,
|
|
221
|
+
task: Union[
|
|
222
|
+
TaskOperator,
|
|
223
|
+
ConditionOperator,
|
|
224
|
+
WaitOperator,
|
|
225
|
+
ParallelOperator,
|
|
226
|
+
ForEachOperator,
|
|
227
|
+
WhileOperator,
|
|
228
|
+
],
|
|
229
|
+
**kwargs,
|
|
230
|
+
) -> None:
|
|
231
|
+
dependencies = kwargs.get("dependencies", [])
|
|
232
|
+
if self._current_task and not dependencies:
|
|
233
|
+
dependencies.append(self._current_task)
|
|
234
|
+
|
|
235
|
+
task.dependencies = sorted(list(set(dependencies)))
|
|
236
|
+
|
|
237
|
+
self.workflow.add_task(task)
|
|
238
|
+
self._current_task = task.task_id
|
|
239
|
+
|
|
207
240
|
def task(self, task_id: str, function: str, **kwargs) -> "WorkflowBuilder":
|
|
208
241
|
task = TaskOperator(task_id=task_id, function=function, **kwargs)
|
|
209
|
-
|
|
210
|
-
task.dependencies.append(self._current_task)
|
|
211
|
-
self.workflow.add_task(task)
|
|
212
|
-
self._current_task = task_id
|
|
242
|
+
self._add_task(task, **kwargs)
|
|
213
243
|
return self
|
|
214
244
|
|
|
215
245
|
def condition(
|
|
@@ -220,8 +250,8 @@ class WorkflowBuilder:
|
|
|
220
250
|
if_false: Callable[["WorkflowBuilder"], "WorkflowBuilder"],
|
|
221
251
|
**kwargs,
|
|
222
252
|
) -> "WorkflowBuilder":
|
|
223
|
-
true_builder = if_true(WorkflowBuilder(f"{
|
|
224
|
-
false_builder = if_false(WorkflowBuilder(f"{
|
|
253
|
+
true_builder = if_true(WorkflowBuilder(f"{task_id}_true", parent=self))
|
|
254
|
+
false_builder = if_false(WorkflowBuilder(f"{task_id}_false", parent=self))
|
|
225
255
|
|
|
226
256
|
true_tasks = list(true_builder.workflow.tasks.keys())
|
|
227
257
|
false_tasks = list(false_builder.workflow.tasks.keys())
|
|
@@ -234,14 +264,17 @@ class WorkflowBuilder:
|
|
|
234
264
|
**kwargs,
|
|
235
265
|
)
|
|
236
266
|
|
|
237
|
-
|
|
238
|
-
task.dependencies.append(self._current_task)
|
|
239
|
-
|
|
240
|
-
self.workflow.add_task(task)
|
|
267
|
+
self._add_task(task, **kwargs)
|
|
241
268
|
|
|
242
269
|
for task_obj in true_builder.workflow.tasks.values():
|
|
270
|
+
# Only add the condition task as dependency, preserve original dependencies
|
|
271
|
+
if task_id not in task_obj.dependencies:
|
|
272
|
+
task_obj.dependencies.append(task_id)
|
|
243
273
|
self.workflow.add_task(task_obj)
|
|
244
274
|
for task_obj in false_builder.workflow.tasks.values():
|
|
275
|
+
# Only add the condition task as dependency, preserve original dependencies
|
|
276
|
+
if task_id not in task_obj.dependencies:
|
|
277
|
+
task_obj.dependencies.append(task_id)
|
|
245
278
|
self.workflow.add_task(task_obj)
|
|
246
279
|
|
|
247
280
|
self._current_task = task_id
|
|
@@ -251,10 +284,7 @@ class WorkflowBuilder:
|
|
|
251
284
|
self, task_id: str, wait_for: Union[timedelta, datetime, str], **kwargs
|
|
252
285
|
) -> "WorkflowBuilder":
|
|
253
286
|
task = WaitOperator(task_id=task_id, wait_for=wait_for, **kwargs)
|
|
254
|
-
|
|
255
|
-
task.dependencies.append(self._current_task)
|
|
256
|
-
self.workflow.add_task(task)
|
|
257
|
-
self._current_task = task_id
|
|
287
|
+
self._add_task(task, **kwargs)
|
|
258
288
|
return self
|
|
259
289
|
|
|
260
290
|
def parallel(
|
|
@@ -263,10 +293,12 @@ class WorkflowBuilder:
|
|
|
263
293
|
branches: Dict[str, Callable[["WorkflowBuilder"], "WorkflowBuilder"]],
|
|
264
294
|
**kwargs,
|
|
265
295
|
) -> "WorkflowBuilder":
|
|
266
|
-
branch_builders = {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
296
|
+
branch_builders = {}
|
|
297
|
+
for name, branch_func in branches.items():
|
|
298
|
+
branch_builder = branch_func(
|
|
299
|
+
WorkflowBuilder(f"{task_id}_{name}", parent=self)
|
|
300
|
+
)
|
|
301
|
+
branch_builders[name] = branch_builder
|
|
270
302
|
|
|
271
303
|
branch_tasks = {
|
|
272
304
|
name: list(builder.workflow.tasks.keys())
|
|
@@ -275,27 +307,60 @@ class WorkflowBuilder:
|
|
|
275
307
|
|
|
276
308
|
task = ParallelOperator(task_id=task_id, branches=branch_tasks, **kwargs)
|
|
277
309
|
|
|
278
|
-
|
|
279
|
-
task.dependencies.append(self._current_task)
|
|
280
|
-
|
|
281
|
-
self.workflow.add_task(task)
|
|
310
|
+
self._add_task(task, **kwargs)
|
|
282
311
|
|
|
283
312
|
for builder in branch_builders.values():
|
|
284
313
|
for task_obj in builder.workflow.tasks.values():
|
|
314
|
+
# Only add the parallel task as dependency to non-internal tasks,
|
|
315
|
+
# preserve original dependencies
|
|
316
|
+
if (
|
|
317
|
+
not getattr(task_obj, "is_internal_loop_task", False)
|
|
318
|
+
and task_id not in task_obj.dependencies
|
|
319
|
+
):
|
|
320
|
+
task_obj.dependencies.append(task_id)
|
|
285
321
|
self.workflow.add_task(task_obj)
|
|
286
322
|
|
|
287
323
|
self._current_task = task_id
|
|
288
324
|
return self
|
|
289
325
|
|
|
290
326
|
def foreach(
|
|
291
|
-
self,
|
|
327
|
+
self,
|
|
328
|
+
task_id: str,
|
|
329
|
+
items: str,
|
|
330
|
+
loop_body: Callable[["WorkflowBuilder"], "WorkflowBuilder"],
|
|
331
|
+
**kwargs,
|
|
292
332
|
) -> "WorkflowBuilder":
|
|
333
|
+
# Create a temporary builder for the loop body.
|
|
334
|
+
temp_builder = WorkflowBuilder(f"{task_id}_loop", parent=self)
|
|
335
|
+
loop_builder = loop_body(temp_builder)
|
|
336
|
+
loop_tasks = list(loop_builder.workflow.tasks.values())
|
|
337
|
+
|
|
338
|
+
# Mark all loop body tasks as internal to prevent parallel dependency injection
|
|
339
|
+
for task_obj in loop_tasks:
|
|
340
|
+
task_obj.is_internal_loop_task = True
|
|
341
|
+
|
|
342
|
+
# Create the foreach operator
|
|
293
343
|
task = ForEachOperator(
|
|
294
|
-
task_id=task_id,
|
|
344
|
+
task_id=task_id,
|
|
345
|
+
items=items,
|
|
346
|
+
loop_body=loop_tasks,
|
|
347
|
+
**kwargs,
|
|
295
348
|
)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
self.
|
|
349
|
+
|
|
350
|
+
# Add the foreach task to workflow to establish initial dependencies
|
|
351
|
+
self._add_task(task, **kwargs)
|
|
352
|
+
|
|
353
|
+
# Add the foreach task as dependency to the FIRST task in the loop body
|
|
354
|
+
# and preserve the original dependency chain within the loop
|
|
355
|
+
if loop_tasks:
|
|
356
|
+
first_task = loop_tasks[0]
|
|
357
|
+
if task_id not in first_task.dependencies:
|
|
358
|
+
first_task.dependencies.append(task_id)
|
|
359
|
+
|
|
360
|
+
# Add all loop tasks to workflow
|
|
361
|
+
for task_obj in loop_tasks:
|
|
362
|
+
self.workflow.add_task(task_obj)
|
|
363
|
+
|
|
299
364
|
self._current_task = task_id
|
|
300
365
|
return self
|
|
301
366
|
|
|
@@ -306,9 +371,13 @@ class WorkflowBuilder:
|
|
|
306
371
|
loop_body: Callable[["WorkflowBuilder"], "WorkflowBuilder"],
|
|
307
372
|
**kwargs,
|
|
308
373
|
) -> "WorkflowBuilder":
|
|
309
|
-
loop_builder = loop_body(WorkflowBuilder(f"{
|
|
374
|
+
loop_builder = loop_body(WorkflowBuilder(f"{task_id}_loop", parent=self))
|
|
310
375
|
loop_tasks = list(loop_builder.workflow.tasks.values())
|
|
311
376
|
|
|
377
|
+
# Mark all loop body tasks as internal to prevent parallel dependency injection
|
|
378
|
+
for task_obj in loop_tasks:
|
|
379
|
+
task_obj.is_internal_loop_task = True
|
|
380
|
+
|
|
312
381
|
task = WhileOperator(
|
|
313
382
|
task_id=task_id,
|
|
314
383
|
condition=condition,
|
|
@@ -316,13 +385,18 @@ class WorkflowBuilder:
|
|
|
316
385
|
**kwargs,
|
|
317
386
|
)
|
|
318
387
|
|
|
319
|
-
|
|
320
|
-
task.dependencies.append(self._current_task)
|
|
388
|
+
self._add_task(task, **kwargs)
|
|
321
389
|
|
|
322
|
-
|
|
390
|
+
# Fix: Only add the while task as dependency to the FIRST task in the loop body
|
|
391
|
+
# and preserve the original dependency chain within the loop
|
|
392
|
+
if loop_tasks:
|
|
393
|
+
first_task = loop_tasks[0]
|
|
394
|
+
if task_id not in first_task.dependencies:
|
|
395
|
+
first_task.dependencies.append(task_id)
|
|
323
396
|
|
|
324
|
-
|
|
325
|
-
|
|
397
|
+
# Add all loop tasks to workflow without modifying their dependencies further
|
|
398
|
+
for task_obj in loop_tasks:
|
|
399
|
+
self.workflow.add_task(task_obj)
|
|
326
400
|
|
|
327
401
|
self._current_task = task_id
|
|
328
402
|
return self
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: highway_dsl
|
|
3
|
-
Version:
|
|
4
|
-
Summary: A domain specific language (DSL) for defining and managing data processing pipelines.
|
|
3
|
+
Version: 1.0.3
|
|
4
|
+
Summary: A stable domain specific language (DSL) for defining and managing data processing pipelines and workflow engines.
|
|
5
5
|
Author-email: Farseed Ashouri <farseed.ashouri@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/rodmena-limited/highway_dsl
|
|
@@ -25,21 +25,29 @@ Dynamic: license-file
|
|
|
25
25
|
|
|
26
26
|
[](https://badge.fury.io/py/highway-dsl)
|
|
27
27
|
[](https://opensource.org/licenses/MIT)
|
|
28
|
+
[](https://pypi.org/project/highway-dsl/)
|
|
29
|
+
[](https://github.com/rodmena-limited/highway_dsl/actions/workflows/publish.yml)
|
|
28
30
|
|
|
29
31
|
**Highway DSL** is a Python-based domain-specific language for defining complex workflows in a clear, concise, and fluent manner. It is part of the larger **Highway** project, an advanced workflow engine capable of running complex DAG-based workflows.
|
|
30
32
|
|
|
33
|
+
## Version 1.0.3 - Stable Release
|
|
34
|
+
|
|
35
|
+
This is a stable release with important bug fixes and enhancements, including a critical fix for the ForEach operator dependency management issue.
|
|
36
|
+
|
|
31
37
|
## Features
|
|
32
38
|
|
|
33
39
|
* **Fluent API:** A powerful and intuitive `WorkflowBuilder` for defining workflows programmatically.
|
|
34
40
|
* **Pydantic-based:** All models are built on Pydantic, providing robust data validation, serialization, and documentation.
|
|
35
41
|
* **Rich Operators:** A comprehensive set of operators for handling various workflow scenarios:
|
|
36
|
-
* `Task`
|
|
37
|
-
* `Condition` (if/else)
|
|
38
|
-
* `Parallel`
|
|
39
|
-
* `ForEach`
|
|
40
|
-
* `Wait`
|
|
41
|
-
* `While`
|
|
42
|
+
* `Task` - Basic workflow steps
|
|
43
|
+
* `Condition` (if/else) - Conditional branching
|
|
44
|
+
* `Parallel` - Execute multiple branches simultaneously
|
|
45
|
+
* `ForEach` - Iterate over collections with proper dependency management
|
|
46
|
+
* `Wait` - Pause execution for scheduled tasks
|
|
47
|
+
* `While` - Execute loops based on conditions
|
|
48
|
+
* **Fixed ForEach Bug:** Proper encapsulation of loop body tasks to prevent unwanted "grandparent" dependencies from containing parallel operators.
|
|
42
49
|
* **YAML/JSON Interoperability:** Workflows can be defined in Python and exported to YAML or JSON, and vice-versa.
|
|
50
|
+
* **Retry and Timeout Policies:** Built-in error handling and execution time management.
|
|
43
51
|
* **Extensible:** The DSL is designed to be extensible with custom operators and policies.
|
|
44
52
|
|
|
45
53
|
## Installation
|
|
@@ -135,6 +143,63 @@ builder.task("finalize_product", "workflows.tasks.finalize_product", dependencie
|
|
|
135
143
|
workflow = builder.build()
|
|
136
144
|
```
|
|
137
145
|
|
|
146
|
+
### For-Each Loops with Proper Dependency Management
|
|
147
|
+
|
|
148
|
+
Fixed bug where foreach loops were incorrectly inheriting dependencies from containing parallel operators:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
# This loop now properly encapsulates its internal tasks
|
|
152
|
+
builder.foreach(
|
|
153
|
+
"process_items",
|
|
154
|
+
items="{{data.items}}",
|
|
155
|
+
loop_body=lambda fb: fb.task("process_item", "processor.handle_item", args=["{{item.id}}"])
|
|
156
|
+
# Loop body tasks only have proper dependencies, not unwanted "grandparent" dependencies
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Retry Policies
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from highway_dsl import RetryPolicy
|
|
164
|
+
from datetime import timedelta
|
|
165
|
+
|
|
166
|
+
builder.task(
|
|
167
|
+
"reliable_task",
|
|
168
|
+
"service.operation",
|
|
169
|
+
retry_policy=RetryPolicy(
|
|
170
|
+
max_retries=5,
|
|
171
|
+
delay=timedelta(seconds=10),
|
|
172
|
+
backoff_factor=2.0
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Timeout Policies
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from highway_dsl import TimeoutPolicy
|
|
181
|
+
from datetime import timedelta
|
|
182
|
+
|
|
183
|
+
builder.task(
|
|
184
|
+
"timed_task",
|
|
185
|
+
"service.operation",
|
|
186
|
+
timeout_policy=TimeoutPolicy(
|
|
187
|
+
timeout=timedelta(hours=1),
|
|
188
|
+
kill_on_timeout=True
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## What's New in Version 1.0.2
|
|
194
|
+
|
|
195
|
+
### Bug Fixes
|
|
196
|
+
* **Fixed ForEach Operator Bug**: Resolved issue where foreach loops were incorrectly getting "grandparent" dependencies from containing parallel operators. Loop body tasks are now properly encapsulated and only depend on their parent loop operator and internal chain dependencies.
|
|
197
|
+
|
|
198
|
+
### Enhancements
|
|
199
|
+
* **Improved Loop Dependency Management**: While loops and ForEach loops now properly encapsulate their internal dependencies without being affected by containing parallel operators.
|
|
200
|
+
* **Better Error Handling**: Enhanced error handling throughout the DSL.
|
|
201
|
+
* **Comprehensive Test Suite**: Added functional tests for all example workflows to ensure consistency.
|
|
202
|
+
|
|
138
203
|
## Development
|
|
139
204
|
|
|
140
205
|
To set up the development environment:
|
|
@@ -158,3 +223,7 @@ pytest
|
|
|
158
223
|
```bash
|
|
159
224
|
mypy .
|
|
160
225
|
```
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
MIT License
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
highway_dsl/__init__.py,sha256=mr1oMylxliFwu2VO2qpyM3sVQwYIoPL2P6JE-6ZuF7M,507
|
|
2
|
+
highway_dsl/workflow_dsl.py,sha256=bhCKDPrMaIkEI4HduKoeqd2VlZsK8wjr8RURifPufGU,14700
|
|
3
|
+
highway_dsl-1.0.3.dist-info/licenses/LICENSE,sha256=qdFq1H66BvKg67mf4-WGpFwtG2u_dNknxuJDQ1_ubaY,1072
|
|
4
|
+
highway_dsl-1.0.3.dist-info/METADATA,sha256=8rf2NIvAJ5pgDXzs9r2VjgePNqXTOlJbOL0nHRTvsSg,7366
|
|
5
|
+
highway_dsl-1.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
highway_dsl-1.0.3.dist-info/top_level.txt,sha256=_5uX-bbBsQ2rsi1XMr7WRyKbr6ack5GqVBcy-QjF1C8,12
|
|
7
|
+
highway_dsl-1.0.3.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
highway_dsl/__init__.py,sha256=mr1oMylxliFwu2VO2qpyM3sVQwYIoPL2P6JE-6ZuF7M,507
|
|
2
|
-
highway_dsl/workflow_dsl.py,sha256=yMTmFr5bbjxfVTleCvSsDZ__n9C7qH39RdzajkUEmiI,11882
|
|
3
|
-
highway_dsl-0.0.3.dist-info/licenses/LICENSE,sha256=qdFq1H66BvKg67mf4-WGpFwtG2u_dNknxuJDQ1_ubaY,1072
|
|
4
|
-
highway_dsl-0.0.3.dist-info/METADATA,sha256=--TFErjeBDZ1mAyNHk30CQavLuKWAaoxgHK7xFpT-Ok,4612
|
|
5
|
-
highway_dsl-0.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
-
highway_dsl-0.0.3.dist-info/top_level.txt,sha256=_5uX-bbBsQ2rsi1XMr7WRyKbr6ack5GqVBcy-QjF1C8,12
|
|
7
|
-
highway_dsl-0.0.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|