nextpipe 0.7.3__tar.gz → 0.7.4.dev0__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.
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/PKG-INFO +1 -1
- nextpipe-0.7.4.dev0/nextpipe/__about__.py +1 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/nextpipe/decorators.py +113 -19
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/nextpipe/flow.py +71 -4
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/pyproject.toml +2 -2
- nextpipe-0.7.4.dev0/tests/pipelines/foreach-2-pred.json.golden +80 -0
- nextpipe-0.7.4.dev0/tests/pipelines/foreach.json.golden +65 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/test_integration.py +4 -3
- nextpipe-0.7.4.dev0/tests/test_optional.py +246 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/uv.lock +38 -38
- nextpipe-0.7.3/nextpipe/__about__.py +0 -1
- nextpipe-0.7.3/tests/pipelines/foreach-2-pred.json.golden +0 -65
- nextpipe-0.7.3/tests/pipelines/foreach.json.golden +0 -56
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/.github/workflows/lint.yml +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/.github/workflows/release.yml +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/.github/workflows/test.yml +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/.gitignore +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/.nextmv/bump_requirements.sh +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/.prettierrc.yml +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/.python-version +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/LICENSE.md +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/README.md +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/RELEASE.md +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/nextpipe/__init__.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/nextpipe/config.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/nextpipe/constants.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/nextpipe/graph.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/nextpipe/schema.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/nextpipe/threads.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/nextpipe/uplink.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/nextpipe/utils.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/nextpipe.code-workspace +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/.gitignore +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/README.md +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/__init__.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/apps/echo/.gitignore +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/apps/echo/README.md +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/apps/echo/app.yaml +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/apps/echo/main.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/apps/echo/requirements.txt +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/apps/echo-multi/.gitignore +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/apps/echo-multi/README.md +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/apps/echo-multi/app.yaml +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/apps/echo-multi/inputs/data.csv +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/apps/echo-multi/inputs/input.xlsx +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/apps/echo-multi/main.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/apps/echo-multi/requirements.txt +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/deploy/app.yaml +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/deploy/main.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/deploy/requirements.txt +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/pipelines/appapp.json +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/pipelines/appapp.json.golden +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/pipelines/appapp.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/pipelines/chain.json +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/pipelines/chain.json.golden +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/pipelines/chain.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/pipelines/complex.json +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/pipelines/complex.json.golden +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/pipelines/complex.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/pipelines/fail.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/pipelines/foreach-2-pred.json +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/pipelines/foreach-2-pred.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/pipelines/foreach.json +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/pipelines/foreach.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/pipelines/inputs/data.csv +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/pipelines/inputs/input.xlsx +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/pipelines/multifile.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/test_flow.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/test_graph.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/test_schema.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/test_threads.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/test_uplink.py +0 -0
- {nextpipe-0.7.3 → nextpipe-0.7.4.dev0}/tests/test_version.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "v0.7.4.dev0"
|
|
@@ -45,6 +45,7 @@ app
|
|
|
45
45
|
Decorator to mark a step as a Nextmv Application.
|
|
46
46
|
"""
|
|
47
47
|
|
|
48
|
+
import inspect
|
|
48
49
|
import typing
|
|
49
50
|
from collections.abc import Callable
|
|
50
51
|
from enum import Enum
|
|
@@ -177,17 +178,37 @@ class Step:
|
|
|
177
178
|
|
|
178
179
|
return hasattr(self, "needs")
|
|
179
180
|
|
|
181
|
+
def is_optional(self):
|
|
182
|
+
"""
|
|
183
|
+
Check if the step is optional.
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
bool
|
|
188
|
+
True if the step is optional, False otherwise.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
return hasattr(self, "optional")
|
|
192
|
+
|
|
180
193
|
def skip(self):
|
|
181
194
|
"""
|
|
182
195
|
Check if the step should be skipped.
|
|
183
196
|
|
|
197
|
+
Deprecated: skipping is decided by the runner, which also passes the
|
|
198
|
+
step's inputs to the condition and propagates skips. Use
|
|
199
|
+
`is_optional()` to check whether a step has a condition.
|
|
200
|
+
|
|
184
201
|
Returns
|
|
185
202
|
-------
|
|
186
203
|
bool
|
|
187
204
|
True if the step should be skipped, False otherwise.
|
|
188
205
|
"""
|
|
189
206
|
|
|
190
|
-
|
|
207
|
+
deprecated(
|
|
208
|
+
"Step.skip",
|
|
209
|
+
"Step.skip() is deprecated and will be removed in a future release. Use Step.is_optional() instead.",
|
|
210
|
+
)
|
|
211
|
+
return self.is_optional() and not self.optional.condition(self)
|
|
191
212
|
|
|
192
213
|
def is_repeat(self):
|
|
193
214
|
"""
|
|
@@ -435,9 +456,12 @@ class Optional:
|
|
|
435
456
|
condition : callable
|
|
436
457
|
A function that takes a step and returns a boolean indicating
|
|
437
458
|
whether the step should be executed or not.
|
|
459
|
+
skip_downstream : bool
|
|
460
|
+
Whether skipping this step also skips the steps that depend only on
|
|
461
|
+
it.
|
|
438
462
|
"""
|
|
439
463
|
|
|
440
|
-
def __init__(self, condition: callable):
|
|
464
|
+
def __init__(self, condition: callable, skip_downstream: bool = False):
|
|
441
465
|
"""
|
|
442
466
|
Initialize an Optional object.
|
|
443
467
|
|
|
@@ -446,9 +470,45 @@ class Optional:
|
|
|
446
470
|
condition : callable
|
|
447
471
|
A function that takes a step and returns a boolean indicating
|
|
448
472
|
whether the step should be executed or not.
|
|
473
|
+
skip_downstream : bool, optional
|
|
474
|
+
Whether skipping this step also skips the steps that depend only
|
|
475
|
+
on it, by default False.
|
|
449
476
|
"""
|
|
450
477
|
|
|
451
478
|
self.condition = condition
|
|
479
|
+
self.skip_downstream = skip_downstream
|
|
480
|
+
|
|
481
|
+
def evaluate(self, step: Step, inputs: list[typing.Any]) -> bool:
|
|
482
|
+
"""
|
|
483
|
+
Evaluate the condition.
|
|
484
|
+
|
|
485
|
+
Conditions with a single required parameter only receive the step.
|
|
486
|
+
Conditions with more required parameters (or `*args`) additionally
|
|
487
|
+
receive the inputs of the step (the results of its predecessors, or the
|
|
488
|
+
flow input if it has none).
|
|
489
|
+
|
|
490
|
+
Parameters
|
|
491
|
+
----------
|
|
492
|
+
step : Step
|
|
493
|
+
The step the condition belongs to.
|
|
494
|
+
inputs : list[Any]
|
|
495
|
+
The inputs of the step.
|
|
496
|
+
|
|
497
|
+
Returns
|
|
498
|
+
-------
|
|
499
|
+
bool
|
|
500
|
+
True if the step should be executed, False otherwise.
|
|
501
|
+
"""
|
|
502
|
+
|
|
503
|
+
# Parameters with defaults are not counted, and `self` of bound methods
|
|
504
|
+
# is not part of the signature.
|
|
505
|
+
params = inspect.signature(self.condition).parameters.values()
|
|
506
|
+
positional = (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
507
|
+
required = [p for p in params if p.kind in positional and p.default is inspect.Parameter.empty]
|
|
508
|
+
varargs = any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in params)
|
|
509
|
+
if len(required) <= 1 and not varargs:
|
|
510
|
+
return self.condition(step)
|
|
511
|
+
return self.condition(step, *inputs)
|
|
452
512
|
|
|
453
513
|
def __repr__(self):
|
|
454
514
|
"""
|
|
@@ -460,10 +520,10 @@ class Optional:
|
|
|
460
520
|
A string representation of the optional condition.
|
|
461
521
|
"""
|
|
462
522
|
|
|
463
|
-
return f"StepOnlyIf({self.condition})"
|
|
523
|
+
return f"StepOnlyIf({self.condition}, skip_downstream={self.skip_downstream})"
|
|
464
524
|
|
|
465
525
|
|
|
466
|
-
def optional(condition: Callable[
|
|
526
|
+
def optional(condition: Callable[..., bool], skip_downstream: bool = False):
|
|
467
527
|
"""
|
|
468
528
|
Decorator to mark a step as optional.
|
|
469
529
|
|
|
@@ -474,41 +534,75 @@ def optional(condition: Callable[[Step], bool]):
|
|
|
474
534
|
```
|
|
475
535
|
|
|
476
536
|
This is used to determine whether the step should be executed or not. The
|
|
477
|
-
condition is
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
537
|
+
condition is evaluated at runtime, right before the step would run. It
|
|
538
|
+
receives the step and, if it accepts more arguments, the inputs of the step
|
|
539
|
+
(the results of its predecessors, or the flow input if it has none).
|
|
540
|
+
|
|
541
|
+
A skipped step has the status `skipped` and its result is `None`. By
|
|
542
|
+
default, only the step itself is skipped: its successors still run and
|
|
543
|
+
receive `None` as its result.
|
|
544
|
+
|
|
545
|
+
With `skip_downstream=True`, the skip propagates: a step whose
|
|
546
|
+
predecessors were all skipped this way is skipped as well (and so on). A
|
|
547
|
+
step with at least one other predecessor still runs and receives `None`
|
|
548
|
+
for each skipped one. This allows modeling if/else branches that are
|
|
549
|
+
merged again by a common successor.
|
|
481
550
|
|
|
482
551
|
Parameters
|
|
483
552
|
----------
|
|
484
|
-
condition : Callable[
|
|
553
|
+
condition : Callable[..., bool]
|
|
485
554
|
The condition to evaluate. This is a callable that takes the step
|
|
486
|
-
|
|
487
|
-
should be executed or not.
|
|
555
|
+
(and optionally the inputs of the step) as arguments and returns a
|
|
556
|
+
boolean indicating whether the step should be executed or not.
|
|
557
|
+
skip_downstream : bool, optional
|
|
558
|
+
Whether skipping this step also skips the steps that depend only on
|
|
559
|
+
it, by default False.
|
|
488
560
|
|
|
489
561
|
Example
|
|
490
562
|
-------
|
|
491
|
-
In this example
|
|
492
|
-
|
|
563
|
+
In this example either `solve_small` or `solve_large` (and its successor
|
|
564
|
+
`polish`) is executed, depending on the input. `report` merges both branches.
|
|
493
565
|
|
|
494
566
|
```
|
|
495
|
-
from nextpipe import FlowSpec, log, optional, step
|
|
567
|
+
from nextpipe import FlowSpec, log, needs, optional, step
|
|
496
568
|
|
|
497
569
|
|
|
498
570
|
class Flow(FlowSpec):
|
|
499
|
-
@optional(condition=lambda step: step.get_id() == "step1")
|
|
500
571
|
@step
|
|
501
|
-
def
|
|
502
|
-
|
|
572
|
+
def prepare(data: dict) -> dict:
|
|
573
|
+
return data
|
|
503
574
|
|
|
575
|
+
@optional(condition=lambda step, data: len(data["orders"]) <= 10, skip_downstream=True)
|
|
576
|
+
@needs(predecessors=[prepare])
|
|
577
|
+
@step
|
|
578
|
+
def solve_small(data: dict) -> dict:
|
|
579
|
+
return {"solver": "small"}
|
|
504
580
|
|
|
505
|
-
|
|
581
|
+
@optional(condition=lambda step, data: len(data["orders"]) > 10, skip_downstream=True)
|
|
582
|
+
@needs(predecessors=[prepare])
|
|
583
|
+
@step
|
|
584
|
+
def solve_large(data: dict) -> dict:
|
|
585
|
+
return {"solver": "large"}
|
|
586
|
+
|
|
587
|
+
@needs(predecessors=[solve_large])
|
|
588
|
+
@step
|
|
589
|
+
def polish(result: dict) -> dict: # skipped if solve_large is skipped
|
|
590
|
+
return result | {"polished": True}
|
|
591
|
+
|
|
592
|
+
@needs(predecessors=[solve_small, polish])
|
|
593
|
+
@step
|
|
594
|
+
def report(small: dict | None, large: dict | None) -> dict:
|
|
595
|
+
return small or large
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
flow = Flow("DecisionFlow", {"orders": [1, 2, 3]})
|
|
506
599
|
flow.run()
|
|
600
|
+
log(flow.get_result(flow.report)) # {'solver': 'small'}
|
|
507
601
|
```
|
|
508
602
|
"""
|
|
509
603
|
|
|
510
604
|
def decorator(function):
|
|
511
|
-
function.step.optional = Optional(condition)
|
|
605
|
+
function.step.optional = Optional(condition, skip_downstream)
|
|
512
606
|
return function
|
|
513
607
|
|
|
514
608
|
return decorator
|
|
@@ -59,6 +59,9 @@ STATUS_SUCCEEDED = "succeeded"
|
|
|
59
59
|
STATUS_FAILED = "failed"
|
|
60
60
|
"""Status constant indicating a failed step or node."""
|
|
61
61
|
|
|
62
|
+
STATUS_SKIPPED = "skipped"
|
|
63
|
+
"""Status constant indicating a skipped step or node."""
|
|
64
|
+
|
|
62
65
|
|
|
63
66
|
class FlowStep:
|
|
64
67
|
"""
|
|
@@ -89,6 +92,10 @@ class FlowStep:
|
|
|
89
92
|
Thread lock for this step.
|
|
90
93
|
done : bool
|
|
91
94
|
Whether this step has completed execution.
|
|
95
|
+
skipped : bool
|
|
96
|
+
Whether this step was skipped.
|
|
97
|
+
skip_downstream : bool
|
|
98
|
+
Whether the skip of this step propagates to its successors.
|
|
92
99
|
successors : list[FlowStep]
|
|
93
100
|
Steps that depend on this step.
|
|
94
101
|
predecessors : list[FlowStep]
|
|
@@ -121,6 +128,8 @@ class FlowStep:
|
|
|
121
128
|
self.docstring = docstring
|
|
122
129
|
self.lock = threading.Lock()
|
|
123
130
|
self.done = False
|
|
131
|
+
self.skipped = False
|
|
132
|
+
self.skip_downstream = False
|
|
124
133
|
self.successors: list[FlowStep] = []
|
|
125
134
|
self.predecessors: list[FlowStep] = []
|
|
126
135
|
self.nodes: list[FlowNode] = []
|
|
@@ -162,7 +171,7 @@ class FlowNode:
|
|
|
162
171
|
id : str
|
|
163
172
|
Unique identifier for this node.
|
|
164
173
|
status : str
|
|
165
|
-
Current execution status (pending, running, succeeded, failed).
|
|
174
|
+
Current execution status (pending, running, succeeded, failed, skipped).
|
|
166
175
|
error : str or None
|
|
167
176
|
Error message if the node execution failed.
|
|
168
177
|
predecessors : list[FlowNode]
|
|
@@ -804,6 +813,10 @@ class Runner:
|
|
|
804
813
|
predecessor_inputs = {}
|
|
805
814
|
for predecessor in step.predecessors:
|
|
806
815
|
predecessor_results = [res.result for res in predecessor.nodes]
|
|
816
|
+
if predecessor.skipped:
|
|
817
|
+
# Skipped predecessors contribute a single None input.
|
|
818
|
+
predecessor_inputs[predecessor] = [None]
|
|
819
|
+
continue
|
|
807
820
|
if predecessor.definition.is_foreach():
|
|
808
821
|
# Make sure the result is in fact a list.
|
|
809
822
|
if len(predecessor_results) != 1 or not isinstance(predecessor_results[0], list):
|
|
@@ -1138,6 +1151,53 @@ class Runner:
|
|
|
1138
1151
|
output = node.parent.definition.function(*inputs)
|
|
1139
1152
|
return output
|
|
1140
1153
|
|
|
1154
|
+
@staticmethod
|
|
1155
|
+
def __skipped_by_predecessors(step: FlowStep) -> bool:
|
|
1156
|
+
"""
|
|
1157
|
+
Check whether a step is skipped because of its predecessors.
|
|
1158
|
+
|
|
1159
|
+
This is the case if all of its predecessors were skipped with
|
|
1160
|
+
`skip_downstream`. A step with at least one other predecessor still runs
|
|
1161
|
+
and receives `None` for each skipped one (e.g., merging branches).
|
|
1162
|
+
|
|
1163
|
+
Parameters
|
|
1164
|
+
----------
|
|
1165
|
+
step : FlowStep
|
|
1166
|
+
The step to check. All its predecessors must be done.
|
|
1167
|
+
|
|
1168
|
+
Returns
|
|
1169
|
+
-------
|
|
1170
|
+
bool
|
|
1171
|
+
True if the step is skipped because of its predecessors.
|
|
1172
|
+
"""
|
|
1173
|
+
|
|
1174
|
+
return bool(step.predecessors) and all(p.skipped and p.skip_downstream for p in step.predecessors)
|
|
1175
|
+
|
|
1176
|
+
def __should_skip(self, step: FlowStep) -> bool:
|
|
1177
|
+
"""
|
|
1178
|
+
Determine whether a step should be skipped.
|
|
1179
|
+
|
|
1180
|
+
A step is skipped if it is skipped because of its predecessors (see
|
|
1181
|
+
`__skipped_by_predecessors`) or if it is decorated with `@optional` and
|
|
1182
|
+
its condition is not met.
|
|
1183
|
+
|
|
1184
|
+
Parameters
|
|
1185
|
+
----------
|
|
1186
|
+
step : FlowStep
|
|
1187
|
+
The step to check. All its predecessors must be done.
|
|
1188
|
+
|
|
1189
|
+
Returns
|
|
1190
|
+
-------
|
|
1191
|
+
bool
|
|
1192
|
+
True if the step should be skipped, False otherwise.
|
|
1193
|
+
"""
|
|
1194
|
+
|
|
1195
|
+
if self.__skipped_by_predecessors(step):
|
|
1196
|
+
return True
|
|
1197
|
+
if not step.definition.is_optional():
|
|
1198
|
+
return False
|
|
1199
|
+
return not step.definition.optional.evaluate(step.definition, self.spec._get_inputs(step))
|
|
1200
|
+
|
|
1141
1201
|
def __create_job(self, node: FlowNode, inputs: list[Any] | Any) -> threads.Job:
|
|
1142
1202
|
"""
|
|
1143
1203
|
Create a job for executing a step.
|
|
@@ -1207,14 +1267,21 @@ class Runner:
|
|
|
1207
1267
|
# No more steps to run at this point. Wait for the remaining tasks to finish.
|
|
1208
1268
|
break
|
|
1209
1269
|
open_steps.remove(step)
|
|
1210
|
-
# Skip the step if
|
|
1211
|
-
if
|
|
1270
|
+
# Skip the step if its predecessors were skipped (with skip_downstream) or its condition is not met
|
|
1271
|
+
if self.__should_skip(step):
|
|
1212
1272
|
utils.log_internal(f"Skipping step {step.definition.get_id()}")
|
|
1213
1273
|
# Create dummy node
|
|
1214
1274
|
node = FlowNode(step, 0)
|
|
1215
|
-
node.status =
|
|
1275
|
+
node.status = STATUS_SKIPPED
|
|
1216
1276
|
node.result = None
|
|
1277
|
+
node.done = True
|
|
1217
1278
|
step.nodes.append(node)
|
|
1279
|
+
step.skipped = True
|
|
1280
|
+
# Propagated skips keep propagating, own skips only if requested
|
|
1281
|
+
step.skip_downstream = (
|
|
1282
|
+
self.__skipped_by_predecessors(step) or step.definition.optional.skip_downstream
|
|
1283
|
+
)
|
|
1284
|
+
step.done = True
|
|
1218
1285
|
closed_steps.add(step)
|
|
1219
1286
|
open_steps.update(step.successors)
|
|
1220
1287
|
self.uplink.submit_update(self.graph._to_uplink_dto())
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
[
|
|
2
|
+
[
|
|
3
|
+
{
|
|
4
|
+
"options": {},
|
|
5
|
+
"solution": {
|
|
6
|
+
"echo": {
|
|
7
|
+
"data": {
|
|
8
|
+
"hello": "world!"
|
|
9
|
+
},
|
|
10
|
+
"args": [
|
|
11
|
+
"-param=0"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"assets": [],
|
|
16
|
+
"metrics": {
|
|
17
|
+
"run": {
|
|
18
|
+
"duration": 0.123
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"stats": {
|
|
24
|
+
"count": 1
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
{
|
|
30
|
+
"options": {},
|
|
31
|
+
"solution": {
|
|
32
|
+
"echo": {
|
|
33
|
+
"data": {
|
|
34
|
+
"hello": "world!"
|
|
35
|
+
},
|
|
36
|
+
"args": [
|
|
37
|
+
"-param=1"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"assets": [],
|
|
42
|
+
"metrics": {
|
|
43
|
+
"run": {
|
|
44
|
+
"duration": 0.123
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"stats": {
|
|
50
|
+
"count": 1
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
{
|
|
56
|
+
"options": {},
|
|
57
|
+
"solution": {
|
|
58
|
+
"echo": {
|
|
59
|
+
"data": {
|
|
60
|
+
"hello": "world!"
|
|
61
|
+
},
|
|
62
|
+
"args": [
|
|
63
|
+
"-param=2"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"assets": [],
|
|
68
|
+
"metrics": {
|
|
69
|
+
"run": {
|
|
70
|
+
"duration": 0.123
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"stats": {
|
|
76
|
+
"count": 1
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[
|
|
2
|
+
[
|
|
3
|
+
{
|
|
4
|
+
"options": {},
|
|
5
|
+
"solution": {
|
|
6
|
+
"echo": {
|
|
7
|
+
"data": {
|
|
8
|
+
"hello": "world!"
|
|
9
|
+
},
|
|
10
|
+
"args": [
|
|
11
|
+
"-param=0"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"assets": [],
|
|
16
|
+
"metrics": {
|
|
17
|
+
"run": {
|
|
18
|
+
"duration": 0.123
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
{
|
|
25
|
+
"options": {},
|
|
26
|
+
"solution": {
|
|
27
|
+
"echo": {
|
|
28
|
+
"data": {
|
|
29
|
+
"hello": "world!"
|
|
30
|
+
},
|
|
31
|
+
"args": [
|
|
32
|
+
"-param=1"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"assets": [],
|
|
37
|
+
"metrics": {
|
|
38
|
+
"run": {
|
|
39
|
+
"duration": 0.123
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
[
|
|
45
|
+
{
|
|
46
|
+
"options": {},
|
|
47
|
+
"solution": {
|
|
48
|
+
"echo": {
|
|
49
|
+
"data": {
|
|
50
|
+
"hello": "world!"
|
|
51
|
+
},
|
|
52
|
+
"args": [
|
|
53
|
+
"-param=2"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"assets": [],
|
|
58
|
+
"metrics": {
|
|
59
|
+
"run": {
|
|
60
|
+
"duration": 0.123
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
]
|
|
@@ -147,9 +147,10 @@ class TestExample(unittest.TestCase):
|
|
|
147
147
|
# FOREACH
|
|
148
148
|
config.comparison_configuration.json_processing_config = goldie.ConfigProcessJson(
|
|
149
149
|
replacements=[
|
|
150
|
-
|
|
151
|
-
goldie.JsonReplacement(path="$[
|
|
152
|
-
goldie.JsonReplacement(path="$[
|
|
150
|
+
# A join step receives one entry per parallel run, each holding one result per predecessor
|
|
151
|
+
goldie.JsonReplacement(path="$[0][0].metrics.run.duration", value=0.123),
|
|
152
|
+
goldie.JsonReplacement(path="$[1][0].metrics.run.duration", value=0.123),
|
|
153
|
+
goldie.JsonReplacement(path="$[2][0].metrics.run.duration", value=0.123),
|
|
153
154
|
],
|
|
154
155
|
)
|
|
155
156
|
goldie.run_file_unittest(
|