runnable 0.34.0a1__py3-none-any.whl → 1.0.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.
Potentially problematic release.
This version of runnable might be problematic. Click here for more details.
- extensions/catalog/any_path.py +13 -2
- extensions/job_executor/__init__.py +7 -5
- extensions/job_executor/emulate.py +106 -0
- extensions/job_executor/k8s.py +8 -8
- extensions/job_executor/local_container.py +13 -14
- extensions/nodes/__init__.py +0 -0
- extensions/nodes/conditional.py +243 -0
- extensions/nodes/fail.py +72 -0
- extensions/nodes/map.py +350 -0
- extensions/nodes/parallel.py +159 -0
- extensions/nodes/stub.py +89 -0
- extensions/nodes/success.py +72 -0
- extensions/nodes/task.py +92 -0
- extensions/pipeline_executor/__init__.py +27 -27
- extensions/pipeline_executor/argo.py +52 -46
- extensions/pipeline_executor/emulate.py +112 -0
- extensions/pipeline_executor/local.py +4 -4
- extensions/pipeline_executor/local_container.py +19 -79
- extensions/pipeline_executor/mocked.py +5 -9
- extensions/pipeline_executor/retry.py +6 -10
- runnable/__init__.py +2 -11
- runnable/catalog.py +6 -23
- runnable/cli.py +145 -48
- runnable/context.py +520 -28
- runnable/datastore.py +51 -54
- runnable/defaults.py +12 -34
- runnable/entrypoints.py +82 -440
- runnable/exceptions.py +35 -34
- runnable/executor.py +13 -20
- runnable/gantt.py +1141 -0
- runnable/graph.py +1 -1
- runnable/names.py +1 -1
- runnable/nodes.py +20 -16
- runnable/parameters.py +108 -51
- runnable/sdk.py +125 -204
- runnable/tasks.py +62 -85
- runnable/utils.py +6 -268
- runnable-1.0.0.dist-info/METADATA +122 -0
- runnable-1.0.0.dist-info/RECORD +73 -0
- {runnable-0.34.0a1.dist-info → runnable-1.0.0.dist-info}/entry_points.txt +9 -8
- extensions/nodes/nodes.py +0 -778
- extensions/nodes/torch.py +0 -273
- extensions/nodes/torch_config.py +0 -76
- extensions/tasks/torch.py +0 -286
- extensions/tasks/torch_config.py +0 -76
- runnable-0.34.0a1.dist-info/METADATA +0 -267
- runnable-0.34.0a1.dist-info/RECORD +0 -67
- {runnable-0.34.0a1.dist-info → runnable-1.0.0.dist-info}/WHEEL +0 -0
- {runnable-0.34.0a1.dist-info → runnable-1.0.0.dist-info}/licenses/LICENSE +0 -0
extensions/nodes/task.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
from pydantic import ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
from runnable import datastore, defaults
|
|
8
|
+
from runnable.datastore import StepLog
|
|
9
|
+
from runnable.defaults import MapVariableType
|
|
10
|
+
from runnable.nodes import ExecutableNode
|
|
11
|
+
from runnable.tasks import BaseTaskType, create_task
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(defaults.LOGGER_NAME)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TaskNode(ExecutableNode):
|
|
17
|
+
"""
|
|
18
|
+
A node of type Task.
|
|
19
|
+
|
|
20
|
+
This node does the actual function execution of the graph in all cases.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
executable: BaseTaskType = Field(exclude=True)
|
|
24
|
+
node_type: str = Field(default="task", serialization_alias="type")
|
|
25
|
+
|
|
26
|
+
# It is technically not allowed as parse_from_config filters them.
|
|
27
|
+
# This is just to get the task level configuration to be present during serialization.
|
|
28
|
+
model_config = ConfigDict(extra="allow")
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def parse_from_config(cls, config: Dict[str, Any]) -> "TaskNode":
|
|
32
|
+
# separate task config from node config
|
|
33
|
+
task_config = {
|
|
34
|
+
k: v for k, v in config.items() if k not in TaskNode.model_fields.keys()
|
|
35
|
+
}
|
|
36
|
+
node_config = {
|
|
37
|
+
k: v for k, v in config.items() if k in TaskNode.model_fields.keys()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
executable = create_task(task_config)
|
|
41
|
+
return cls(executable=executable, **node_config, **task_config)
|
|
42
|
+
|
|
43
|
+
def get_summary(self) -> Dict[str, Any]:
|
|
44
|
+
summary = {
|
|
45
|
+
"name": self.name,
|
|
46
|
+
"type": self.node_type,
|
|
47
|
+
"executable": self.executable.get_summary(),
|
|
48
|
+
"catalog": self._get_catalog_settings(),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return summary
|
|
52
|
+
|
|
53
|
+
def execute(
|
|
54
|
+
self,
|
|
55
|
+
mock=False,
|
|
56
|
+
map_variable: MapVariableType = None,
|
|
57
|
+
attempt_number: int = 1,
|
|
58
|
+
) -> StepLog:
|
|
59
|
+
"""
|
|
60
|
+
All that we do in runnable is to come to this point where we actually execute the command.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
executor (_type_): The executor class
|
|
64
|
+
mock (bool, optional): If we should just mock and not execute. Defaults to False.
|
|
65
|
+
map_variable (dict, optional): If the node is part of internal branch. Defaults to None.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
StepAttempt: The attempt object
|
|
69
|
+
"""
|
|
70
|
+
step_log = self._context.run_log_store.get_step_log(
|
|
71
|
+
self._get_step_log_name(map_variable), self._context.run_id
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if not mock:
|
|
75
|
+
# Do not run if we are mocking the execution, could be useful for caching and dry runs
|
|
76
|
+
attempt_log = self.executable.execute_command(map_variable=map_variable)
|
|
77
|
+
attempt_log.attempt_number = attempt_number
|
|
78
|
+
else:
|
|
79
|
+
attempt_log = datastore.StepAttempt(
|
|
80
|
+
status=defaults.SUCCESS,
|
|
81
|
+
start_time=str(datetime.now()),
|
|
82
|
+
end_time=str(datetime.now()),
|
|
83
|
+
attempt_number=attempt_number,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
logger.info(f"attempt_log: {attempt_log}")
|
|
87
|
+
logger.info(f"Step {self.name} completed with status: {attempt_log.status}")
|
|
88
|
+
|
|
89
|
+
step_log.status = attempt_log.status
|
|
90
|
+
step_log.attempts.append(attempt_log)
|
|
91
|
+
|
|
92
|
+
return step_log
|
|
@@ -13,7 +13,7 @@ from runnable import (
|
|
|
13
13
|
utils,
|
|
14
14
|
)
|
|
15
15
|
from runnable.datastore import DataCatalog, JsonParameter, RunLog, StepLog
|
|
16
|
-
from runnable.defaults import
|
|
16
|
+
from runnable.defaults import MapVariableType
|
|
17
17
|
from runnable.executor import BasePipelineExecutor
|
|
18
18
|
from runnable.graph import Graph
|
|
19
19
|
from runnable.nodes import BaseNode
|
|
@@ -40,7 +40,7 @@ class GenericPipelineExecutor(BasePipelineExecutor):
|
|
|
40
40
|
|
|
41
41
|
@property
|
|
42
42
|
def _context(self):
|
|
43
|
-
assert context.run_context
|
|
43
|
+
assert isinstance(context.run_context, context.PipelineContext)
|
|
44
44
|
return context.run_context
|
|
45
45
|
|
|
46
46
|
def _get_parameters(self) -> Dict[str, JsonParameter]:
|
|
@@ -104,7 +104,7 @@ class GenericPipelineExecutor(BasePipelineExecutor):
|
|
|
104
104
|
)
|
|
105
105
|
|
|
106
106
|
# Update run_config
|
|
107
|
-
run_config =
|
|
107
|
+
run_config = self._context.model_dump()
|
|
108
108
|
logger.debug(f"run_config as seen by executor: {run_config}")
|
|
109
109
|
self._context.run_log_store.set_run_config(
|
|
110
110
|
run_id=self._context.run_id, run_config=run_config
|
|
@@ -154,13 +154,15 @@ class GenericPipelineExecutor(BasePipelineExecutor):
|
|
|
154
154
|
data_catalogs = []
|
|
155
155
|
for name_pattern in node_catalog_settings.get(stage) or []:
|
|
156
156
|
if stage == "get":
|
|
157
|
-
data_catalog = self._context.
|
|
157
|
+
data_catalog = self._context.catalog.get(
|
|
158
158
|
name=name_pattern,
|
|
159
159
|
)
|
|
160
160
|
|
|
161
161
|
elif stage == "put":
|
|
162
|
-
data_catalog = self._context.
|
|
163
|
-
name=name_pattern,
|
|
162
|
+
data_catalog = self._context.catalog.put(
|
|
163
|
+
name=name_pattern,
|
|
164
|
+
allow_file_not_found_exc=allow_file_no_found_exc,
|
|
165
|
+
store_copy=node_catalog_settings.get("store_copy", True),
|
|
164
166
|
)
|
|
165
167
|
else:
|
|
166
168
|
raise Exception(f"Stage {stage} not supported")
|
|
@@ -189,14 +191,15 @@ class GenericPipelineExecutor(BasePipelineExecutor):
|
|
|
189
191
|
map_variable=map_variable,
|
|
190
192
|
)
|
|
191
193
|
task_console.save_text(log_file_name)
|
|
194
|
+
task_console.export_text(clear=True)
|
|
192
195
|
# Put the log file in the catalog
|
|
193
|
-
self._context.
|
|
196
|
+
self._context.catalog.put(name=log_file_name)
|
|
194
197
|
os.remove(log_file_name)
|
|
195
198
|
|
|
196
199
|
def _execute_node(
|
|
197
200
|
self,
|
|
198
201
|
node: BaseNode,
|
|
199
|
-
map_variable:
|
|
202
|
+
map_variable: MapVariableType = None,
|
|
200
203
|
mock: bool = False,
|
|
201
204
|
):
|
|
202
205
|
"""
|
|
@@ -250,6 +253,10 @@ class GenericPipelineExecutor(BasePipelineExecutor):
|
|
|
250
253
|
console.print(f"Summary of the step: {step_log.internal_name}")
|
|
251
254
|
console.print(step_log.get_summary(), style=defaults.info_style)
|
|
252
255
|
|
|
256
|
+
self.add_task_log_to_catalog(
|
|
257
|
+
name=self._context_node.internal_name, map_variable=map_variable
|
|
258
|
+
)
|
|
259
|
+
|
|
253
260
|
self._context_node = None
|
|
254
261
|
|
|
255
262
|
self._context.run_log_store.add_step_log(step_log, self._context.run_id)
|
|
@@ -266,7 +273,7 @@ class GenericPipelineExecutor(BasePipelineExecutor):
|
|
|
266
273
|
"""
|
|
267
274
|
step_log.code_identities.append(utils.get_git_code_identity())
|
|
268
275
|
|
|
269
|
-
def execute_from_graph(self, node: BaseNode, map_variable:
|
|
276
|
+
def execute_from_graph(self, node: BaseNode, map_variable: MapVariableType = None):
|
|
270
277
|
"""
|
|
271
278
|
This is the entry point to from the graph execution.
|
|
272
279
|
|
|
@@ -315,8 +322,6 @@ class GenericPipelineExecutor(BasePipelineExecutor):
|
|
|
315
322
|
node.execute_as_graph(map_variable=map_variable)
|
|
316
323
|
return
|
|
317
324
|
|
|
318
|
-
task_console.export_text(clear=True)
|
|
319
|
-
|
|
320
325
|
task_name = node._resolve_map_placeholders(node.internal_name, map_variable)
|
|
321
326
|
console.print(
|
|
322
327
|
f":runner: Executing the node {task_name} ... ", style="bold color(208)"
|
|
@@ -324,7 +329,7 @@ class GenericPipelineExecutor(BasePipelineExecutor):
|
|
|
324
329
|
self.trigger_node_execution(node=node, map_variable=map_variable)
|
|
325
330
|
|
|
326
331
|
def trigger_node_execution(
|
|
327
|
-
self, node: BaseNode, map_variable:
|
|
332
|
+
self, node: BaseNode, map_variable: MapVariableType = None
|
|
328
333
|
):
|
|
329
334
|
"""
|
|
330
335
|
Call this method only if we are responsible for traversing the graph via
|
|
@@ -342,7 +347,7 @@ class GenericPipelineExecutor(BasePipelineExecutor):
|
|
|
342
347
|
pass
|
|
343
348
|
|
|
344
349
|
def _get_status_and_next_node_name(
|
|
345
|
-
self, current_node: BaseNode, dag: Graph, map_variable:
|
|
350
|
+
self, current_node: BaseNode, dag: Graph, map_variable: MapVariableType = None
|
|
346
351
|
) -> tuple[str, str]:
|
|
347
352
|
"""
|
|
348
353
|
Given the current node and the graph, returns the name of the next node to execute.
|
|
@@ -380,7 +385,7 @@ class GenericPipelineExecutor(BasePipelineExecutor):
|
|
|
380
385
|
|
|
381
386
|
return step_log.status, next_node_name
|
|
382
387
|
|
|
383
|
-
def execute_graph(self, dag: Graph, map_variable:
|
|
388
|
+
def execute_graph(self, dag: Graph, map_variable: MapVariableType = None):
|
|
384
389
|
"""
|
|
385
390
|
The parallelization is controlled by the nodes and not by this function.
|
|
386
391
|
|
|
@@ -409,7 +414,7 @@ class GenericPipelineExecutor(BasePipelineExecutor):
|
|
|
409
414
|
dag.internal_branch_name or "Graph",
|
|
410
415
|
map_variable,
|
|
411
416
|
)
|
|
412
|
-
branch_execution_task =
|
|
417
|
+
branch_execution_task = context.progress.add_task(
|
|
413
418
|
f"[dark_orange]Executing {branch_task_name}",
|
|
414
419
|
total=1,
|
|
415
420
|
)
|
|
@@ -429,7 +434,7 @@ class GenericPipelineExecutor(BasePipelineExecutor):
|
|
|
429
434
|
|
|
430
435
|
depth = " " * ((task_name.count(".")) or 1 - 1)
|
|
431
436
|
|
|
432
|
-
task_execution =
|
|
437
|
+
task_execution = context.progress.add_task(
|
|
433
438
|
f"{depth}Executing {task_name}", total=1
|
|
434
439
|
)
|
|
435
440
|
|
|
@@ -440,20 +445,20 @@ class GenericPipelineExecutor(BasePipelineExecutor):
|
|
|
440
445
|
)
|
|
441
446
|
|
|
442
447
|
if status == defaults.SUCCESS:
|
|
443
|
-
|
|
448
|
+
context.progress.update(
|
|
444
449
|
task_execution,
|
|
445
450
|
description=f"{depth}[green] {task_name} Completed",
|
|
446
451
|
completed=True,
|
|
447
452
|
overflow="fold",
|
|
448
453
|
)
|
|
449
454
|
else:
|
|
450
|
-
|
|
455
|
+
context.progress.update(
|
|
451
456
|
task_execution,
|
|
452
457
|
description=f"{depth}[red] {task_name} Failed",
|
|
453
458
|
completed=True,
|
|
454
459
|
) # type ignore
|
|
455
460
|
except Exception as e: # noqa: E722
|
|
456
|
-
|
|
461
|
+
context.progress.update(
|
|
457
462
|
task_execution,
|
|
458
463
|
description=f"{depth}[red] {task_name} Errored",
|
|
459
464
|
completed=True,
|
|
@@ -461,11 +466,6 @@ class GenericPipelineExecutor(BasePipelineExecutor):
|
|
|
461
466
|
console.print(e, style=defaults.error_style)
|
|
462
467
|
logger.exception(e)
|
|
463
468
|
raise
|
|
464
|
-
finally:
|
|
465
|
-
# Add task log to the catalog
|
|
466
|
-
self.add_task_log_to_catalog(
|
|
467
|
-
name=working_on.internal_name, map_variable=map_variable
|
|
468
|
-
)
|
|
469
469
|
|
|
470
470
|
console.rule(style="[dark orange]")
|
|
471
471
|
|
|
@@ -475,7 +475,7 @@ class GenericPipelineExecutor(BasePipelineExecutor):
|
|
|
475
475
|
current_node = next_node_name
|
|
476
476
|
|
|
477
477
|
if branch_execution_task:
|
|
478
|
-
|
|
478
|
+
context.progress.update(
|
|
479
479
|
branch_execution_task,
|
|
480
480
|
description=f"[green3] {branch_task_name} completed",
|
|
481
481
|
completed=True,
|
|
@@ -567,7 +567,7 @@ class GenericPipelineExecutor(BasePipelineExecutor):
|
|
|
567
567
|
|
|
568
568
|
return effective_node_config
|
|
569
569
|
|
|
570
|
-
def fan_out(self, node: BaseNode, map_variable:
|
|
570
|
+
def fan_out(self, node: BaseNode, map_variable: MapVariableType = None):
|
|
571
571
|
"""
|
|
572
572
|
This method is used to appropriately fan-out the execution of a composite node.
|
|
573
573
|
This is only useful when we want to execute a composite node during 3rd party orchestrators.
|
|
@@ -599,7 +599,7 @@ class GenericPipelineExecutor(BasePipelineExecutor):
|
|
|
599
599
|
|
|
600
600
|
node.fan_out(map_variable=map_variable)
|
|
601
601
|
|
|
602
|
-
def fan_in(self, node: BaseNode, map_variable:
|
|
602
|
+
def fan_in(self, node: BaseNode, map_variable: MapVariableType = None):
|
|
603
603
|
"""
|
|
604
604
|
This method is used to appropriately fan-in after the execution of a composite node.
|
|
605
605
|
This is only useful when we want to execute a composite node during 3rd party orchestrators.
|
|
@@ -20,13 +20,13 @@ from pydantic import (
|
|
|
20
20
|
from pydantic.alias_generators import to_camel
|
|
21
21
|
from ruamel.yaml import YAML
|
|
22
22
|
|
|
23
|
-
from extensions.nodes.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
from extensions.nodes.conditional import ConditionalNode
|
|
24
|
+
from extensions.nodes.map import MapNode
|
|
25
|
+
from extensions.nodes.parallel import ParallelNode
|
|
26
|
+
from extensions.nodes.task import TaskNode
|
|
27
27
|
from extensions.pipeline_executor import GenericPipelineExecutor
|
|
28
|
-
from runnable import defaults
|
|
29
|
-
from runnable.defaults import
|
|
28
|
+
from runnable import defaults
|
|
29
|
+
from runnable.defaults import MapVariableType
|
|
30
30
|
from runnable.graph import Graph, search_node_by_internal_name
|
|
31
31
|
from runnable.nodes import BaseNode
|
|
32
32
|
|
|
@@ -307,6 +307,7 @@ class DagTask(BaseModelWIthConfig):
|
|
|
307
307
|
template: str # Should be name of a container template or dag template
|
|
308
308
|
arguments: Optional[Arguments] = Field(default=None)
|
|
309
309
|
with_param: Optional[str] = Field(default=None)
|
|
310
|
+
when_param: Optional[str] = Field(default=None, serialization_alias="when")
|
|
310
311
|
depends: Optional[str] = Field(default=None)
|
|
311
312
|
|
|
312
313
|
|
|
@@ -451,7 +452,7 @@ class ArgoExecutor(GenericPipelineExecutor):
|
|
|
451
452
|
"""
|
|
452
453
|
|
|
453
454
|
service_name: str = "argo"
|
|
454
|
-
|
|
455
|
+
_should_setup_run_log_at_traversal: bool = PrivateAttr(default=False)
|
|
455
456
|
mock: bool = False
|
|
456
457
|
|
|
457
458
|
model_config = ConfigDict(
|
|
@@ -533,13 +534,13 @@ class ArgoExecutor(GenericPipelineExecutor):
|
|
|
533
534
|
parameters: Optional[list[Parameter]],
|
|
534
535
|
task_name: str,
|
|
535
536
|
):
|
|
536
|
-
map_variable:
|
|
537
|
+
map_variable: MapVariableType = {}
|
|
537
538
|
for parameter in parameters or []:
|
|
538
539
|
map_variable[parameter.name] = ( # type: ignore
|
|
539
540
|
"{{inputs.parameters." + str(parameter.name) + "}}"
|
|
540
541
|
)
|
|
541
542
|
|
|
542
|
-
fan_command =
|
|
543
|
+
fan_command = self._context.get_fan_command(
|
|
543
544
|
mode=mode,
|
|
544
545
|
node=node,
|
|
545
546
|
run_id=self._run_id_as_parameter,
|
|
@@ -563,6 +564,8 @@ class ArgoExecutor(GenericPipelineExecutor):
|
|
|
563
564
|
outputs: Optional[Outputs] = None
|
|
564
565
|
if mode == "out" and node.node_type == "map":
|
|
565
566
|
outputs = Outputs(parameters=[OutputParameter(name="iterate-on")])
|
|
567
|
+
if mode == "out" and node.node_type == "conditional":
|
|
568
|
+
outputs = Outputs(parameters=[OutputParameter(name="case")])
|
|
566
569
|
|
|
567
570
|
container_template = ContainerTemplate(
|
|
568
571
|
name=task_name,
|
|
@@ -586,7 +589,7 @@ class ArgoExecutor(GenericPipelineExecutor):
|
|
|
586
589
|
task_name: str,
|
|
587
590
|
inputs: Optional[Inputs] = None,
|
|
588
591
|
) -> ContainerTemplate:
|
|
589
|
-
assert node.node_type in ["task", "
|
|
592
|
+
assert node.node_type in ["task", "success", "stub", "fail"]
|
|
590
593
|
|
|
591
594
|
node_override = None
|
|
592
595
|
if hasattr(node, "overrides"):
|
|
@@ -602,17 +605,17 @@ class ArgoExecutor(GenericPipelineExecutor):
|
|
|
602
605
|
|
|
603
606
|
inputs = inputs or Inputs(parameters=[])
|
|
604
607
|
|
|
605
|
-
map_variable:
|
|
608
|
+
map_variable: MapVariableType = {}
|
|
606
609
|
for parameter in inputs.parameters or []:
|
|
607
610
|
map_variable[parameter.name] = ( # type: ignore
|
|
608
611
|
"{{inputs.parameters." + str(parameter.name) + "}}"
|
|
609
612
|
)
|
|
610
613
|
|
|
611
614
|
# command = "runnable execute-single-node"
|
|
612
|
-
command =
|
|
615
|
+
command = self._context.get_node_callable_command(
|
|
613
616
|
node=node,
|
|
614
|
-
over_write_run_id=self._run_id_as_parameter,
|
|
615
617
|
map_variable=map_variable,
|
|
618
|
+
over_write_run_id=self._run_id_as_parameter,
|
|
616
619
|
log_level=self._log_level_as_parameter,
|
|
617
620
|
)
|
|
618
621
|
|
|
@@ -649,7 +652,7 @@ class ArgoExecutor(GenericPipelineExecutor):
|
|
|
649
652
|
def _set_env_vars_to_task(
|
|
650
653
|
self, working_on: BaseNode, container_template: CoreContainerTemplate
|
|
651
654
|
):
|
|
652
|
-
if working_on.node_type not in ["task"
|
|
655
|
+
if working_on.node_type not in ["task"]:
|
|
653
656
|
return
|
|
654
657
|
|
|
655
658
|
global_envs: dict[str, str] = {}
|
|
@@ -711,6 +714,7 @@ class ArgoExecutor(GenericPipelineExecutor):
|
|
|
711
714
|
assert parent_dag_template.dag
|
|
712
715
|
|
|
713
716
|
parent_dag_template.dag.tasks.append(on_failure_task)
|
|
717
|
+
|
|
714
718
|
self._gather_tasks_for_dag_template(
|
|
715
719
|
on_failure_dag,
|
|
716
720
|
dag=dag,
|
|
@@ -722,6 +726,7 @@ class ArgoExecutor(GenericPipelineExecutor):
|
|
|
722
726
|
# - We are using withParam and arguments of the map template to send that value in
|
|
723
727
|
# - The map template should receive that value as a parameter into the template.
|
|
724
728
|
# - The task then start to use it as inputs.parameters.iterate-on
|
|
729
|
+
# the when param should be an evaluation
|
|
725
730
|
|
|
726
731
|
def _gather_tasks_for_dag_template(
|
|
727
732
|
self,
|
|
@@ -757,7 +762,7 @@ class ArgoExecutor(GenericPipelineExecutor):
|
|
|
757
762
|
depends = task_name
|
|
758
763
|
|
|
759
764
|
match working_on.node_type:
|
|
760
|
-
case "task" | "success" | "stub":
|
|
765
|
+
case "task" | "success" | "stub" | "fail":
|
|
761
766
|
template_of_container = self._create_container_template(
|
|
762
767
|
working_on,
|
|
763
768
|
task_name=task_name,
|
|
@@ -767,9 +772,11 @@ class ArgoExecutor(GenericPipelineExecutor):
|
|
|
767
772
|
|
|
768
773
|
self._templates.append(template_of_container)
|
|
769
774
|
|
|
770
|
-
case "map" | "parallel":
|
|
771
|
-
assert
|
|
772
|
-
working_on,
|
|
775
|
+
case "map" | "parallel" | "conditional":
|
|
776
|
+
assert (
|
|
777
|
+
isinstance(working_on, MapNode)
|
|
778
|
+
or isinstance(working_on, ParallelNode)
|
|
779
|
+
or isinstance(working_on, ConditionalNode)
|
|
773
780
|
)
|
|
774
781
|
node_type = working_on.node_type
|
|
775
782
|
|
|
@@ -792,7 +799,8 @@ class ArgoExecutor(GenericPipelineExecutor):
|
|
|
792
799
|
)
|
|
793
800
|
|
|
794
801
|
# Add the composite task
|
|
795
|
-
with_param = None
|
|
802
|
+
with_param: Optional[str] = None
|
|
803
|
+
when_param: Optional[str] = None
|
|
796
804
|
added_parameters = parameters or []
|
|
797
805
|
branches = {}
|
|
798
806
|
if node_type == "map":
|
|
@@ -807,22 +815,34 @@ class ArgoExecutor(GenericPipelineExecutor):
|
|
|
807
815
|
elif node_type == "parallel":
|
|
808
816
|
assert isinstance(working_on, ParallelNode)
|
|
809
817
|
branches = working_on.branches
|
|
818
|
+
elif node_type == "conditional":
|
|
819
|
+
assert isinstance(working_on, ConditionalNode)
|
|
820
|
+
branches = working_on.branches
|
|
821
|
+
when_param = (
|
|
822
|
+
f"{{{{tasks.{task_name}-fan-out.outputs.parameters.case}}}}"
|
|
823
|
+
)
|
|
810
824
|
else:
|
|
811
825
|
raise ValueError("Invalid node type")
|
|
812
826
|
|
|
813
827
|
fan_in_depends = ""
|
|
814
828
|
|
|
815
829
|
for name, branch in branches.items():
|
|
830
|
+
match_when = branch.internal_branch_name.split(".")[-1]
|
|
816
831
|
name = (
|
|
817
832
|
name.replace(" ", "-").replace(".", "-").replace("_", "-")
|
|
818
833
|
)
|
|
819
834
|
|
|
835
|
+
if node_type == "conditional":
|
|
836
|
+
assert isinstance(working_on, ConditionalNode)
|
|
837
|
+
when_param = f"'{match_when}' == {{{{tasks.{task_name}-fan-out.outputs.parameters.case}}}}"
|
|
838
|
+
|
|
820
839
|
branch_task = DagTask(
|
|
821
840
|
name=f"{task_name}-{name}",
|
|
822
841
|
template=f"{task_name}-{name}",
|
|
823
842
|
depends=f"{task_name}-fan-out.Succeeded",
|
|
824
843
|
arguments=Arguments(parameters=added_parameters),
|
|
825
844
|
with_param=with_param,
|
|
845
|
+
when_param=when_param,
|
|
826
846
|
)
|
|
827
847
|
composite_template.dag.tasks.append(branch_task)
|
|
828
848
|
|
|
@@ -836,6 +856,8 @@ class ArgoExecutor(GenericPipelineExecutor):
|
|
|
836
856
|
),
|
|
837
857
|
)
|
|
838
858
|
|
|
859
|
+
assert isinstance(branch, Graph)
|
|
860
|
+
|
|
839
861
|
self._gather_tasks_for_dag_template(
|
|
840
862
|
dag_template=branch_template,
|
|
841
863
|
dag=branch,
|
|
@@ -862,28 +884,6 @@ class ArgoExecutor(GenericPipelineExecutor):
|
|
|
862
884
|
|
|
863
885
|
self._templates.append(composite_template)
|
|
864
886
|
|
|
865
|
-
case "torch":
|
|
866
|
-
from extensions.nodes.torch import TorchNode
|
|
867
|
-
|
|
868
|
-
assert isinstance(working_on, TorchNode)
|
|
869
|
-
# TODO: Need to add multi-node functionality
|
|
870
|
-
# Check notes on the torch node
|
|
871
|
-
|
|
872
|
-
template_of_container = self._create_container_template(
|
|
873
|
-
working_on,
|
|
874
|
-
task_name=task_name,
|
|
875
|
-
inputs=Inputs(parameters=parameters),
|
|
876
|
-
)
|
|
877
|
-
assert template_of_container.container is not None
|
|
878
|
-
|
|
879
|
-
if working_on.node_type == "task":
|
|
880
|
-
self._expose_secrets_to_task(
|
|
881
|
-
working_on,
|
|
882
|
-
container_template=template_of_container.container,
|
|
883
|
-
)
|
|
884
|
-
|
|
885
|
-
self._templates.append(template_of_container)
|
|
886
|
-
|
|
887
887
|
self._handle_failures(
|
|
888
888
|
working_on,
|
|
889
889
|
dag,
|
|
@@ -958,7 +958,7 @@ class ArgoExecutor(GenericPipelineExecutor):
|
|
|
958
958
|
f,
|
|
959
959
|
)
|
|
960
960
|
|
|
961
|
-
def _implicitly_fail(self, node: BaseNode, map_variable:
|
|
961
|
+
def _implicitly_fail(self, node: BaseNode, map_variable: MapVariableType):
|
|
962
962
|
assert self._context.dag
|
|
963
963
|
_, current_branch = search_node_by_internal_name(
|
|
964
964
|
dag=self._context.dag, internal_name=node.internal_name
|
|
@@ -1005,7 +1005,7 @@ class ArgoExecutor(GenericPipelineExecutor):
|
|
|
1005
1005
|
|
|
1006
1006
|
self._implicitly_fail(node, map_variable)
|
|
1007
1007
|
|
|
1008
|
-
def fan_out(self, node: BaseNode, map_variable:
|
|
1008
|
+
def fan_out(self, node: BaseNode, map_variable: MapVariableType = None):
|
|
1009
1009
|
# This could be the first step of the graph
|
|
1010
1010
|
self._use_volumes()
|
|
1011
1011
|
|
|
@@ -1025,7 +1025,13 @@ class ArgoExecutor(GenericPipelineExecutor):
|
|
|
1025
1025
|
with open("/tmp/output.txt", mode="w", encoding="utf-8") as myfile:
|
|
1026
1026
|
json.dump(iterate_on.get_value(), myfile, indent=4)
|
|
1027
1027
|
|
|
1028
|
-
|
|
1028
|
+
if node.node_type == "conditional":
|
|
1029
|
+
assert isinstance(node, ConditionalNode)
|
|
1030
|
+
|
|
1031
|
+
with open("/tmp/output.txt", mode="w", encoding="utf-8") as myfile:
|
|
1032
|
+
json.dump(node.get_parameter_value(), myfile, indent=4)
|
|
1033
|
+
|
|
1034
|
+
def fan_in(self, node: BaseNode, map_variable: MapVariableType = None):
|
|
1029
1035
|
self._use_volumes()
|
|
1030
1036
|
super().fan_in(node, map_variable)
|
|
1031
1037
|
|
|
@@ -1036,9 +1042,9 @@ class ArgoExecutor(GenericPipelineExecutor):
|
|
|
1036
1042
|
case "chunked-fs":
|
|
1037
1043
|
self._context.run_log_store.log_folder = self._container_log_location
|
|
1038
1044
|
|
|
1039
|
-
match self._context.
|
|
1045
|
+
match self._context.catalog.service_name:
|
|
1040
1046
|
case "file-system":
|
|
1041
|
-
self._context.
|
|
1047
|
+
self._context.catalog.catalog_location = (
|
|
1042
1048
|
self._container_catalog_location
|
|
1043
1049
|
)
|
|
1044
1050
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import shlex
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from pydantic import PrivateAttr
|
|
7
|
+
|
|
8
|
+
from extensions.pipeline_executor import GenericPipelineExecutor
|
|
9
|
+
from runnable import defaults
|
|
10
|
+
from runnable.defaults import MapVariableType
|
|
11
|
+
from runnable.nodes import BaseNode
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(defaults.LOGGER_NAME)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Emulator(GenericPipelineExecutor):
|
|
17
|
+
"""
|
|
18
|
+
In the mode of local execution, we run everything on the local computer.
|
|
19
|
+
|
|
20
|
+
This has some serious implications on the amount of time it would take to complete the run.
|
|
21
|
+
Also ensure that the local compute is good enough for the compute to happen of all the steps.
|
|
22
|
+
|
|
23
|
+
Example config:
|
|
24
|
+
|
|
25
|
+
```yaml
|
|
26
|
+
pipeline-executor:
|
|
27
|
+
type: local
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
service_name: str = "emulator"
|
|
33
|
+
|
|
34
|
+
_should_setup_run_log_at_traversal: bool = PrivateAttr(default=True)
|
|
35
|
+
|
|
36
|
+
def trigger_node_execution(
|
|
37
|
+
self, node: BaseNode, map_variable: MapVariableType = None
|
|
38
|
+
):
|
|
39
|
+
"""
|
|
40
|
+
In this mode of execution, we prepare for the node execution and execute the node
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
node (BaseNode): [description]
|
|
44
|
+
map_variable (str, optional): [description]. Defaults to ''.
|
|
45
|
+
"""
|
|
46
|
+
command = self._context.get_node_callable_command(
|
|
47
|
+
node, map_variable=map_variable
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
self.run_click_command(command)
|
|
51
|
+
# execute the command in a forked process
|
|
52
|
+
|
|
53
|
+
step_log = self._context.run_log_store.get_step_log(
|
|
54
|
+
node._get_step_log_name(map_variable), self._context.run_id
|
|
55
|
+
)
|
|
56
|
+
if step_log.status != defaults.SUCCESS:
|
|
57
|
+
msg = "Node execution inside the emulate failed. Please check the logs.\n"
|
|
58
|
+
logger.error(msg)
|
|
59
|
+
step_log.status = defaults.FAIL
|
|
60
|
+
self._context.run_log_store.add_step_log(step_log, self._context.run_id)
|
|
61
|
+
|
|
62
|
+
def execute_node(self, node: BaseNode, map_variable: MapVariableType = None):
|
|
63
|
+
"""
|
|
64
|
+
For local execution, we just execute the node.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
node (BaseNode): _description_
|
|
68
|
+
map_variable (dict[str, str], optional): _description_. Defaults to None.
|
|
69
|
+
"""
|
|
70
|
+
self._execute_node(node=node, map_variable=map_variable)
|
|
71
|
+
|
|
72
|
+
def run_click_command(self, command: str) -> str:
|
|
73
|
+
"""
|
|
74
|
+
Execute a Click-based CLI command in the current virtual environment.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
args: List of Click command arguments (including subcommands and options)
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Combined stdout/stderr output as string
|
|
81
|
+
"""
|
|
82
|
+
# For Click commands installed via setup.py entry_points
|
|
83
|
+
# command = [sys.executable, '-m', 'your_package.cli'] + args
|
|
84
|
+
|
|
85
|
+
# For direct module execution
|
|
86
|
+
sub_command = [sys.executable, "-m", "runnable.cli"] + shlex.split(command)[1:]
|
|
87
|
+
|
|
88
|
+
process = subprocess.Popen(
|
|
89
|
+
sub_command,
|
|
90
|
+
stdout=subprocess.PIPE,
|
|
91
|
+
stderr=subprocess.STDOUT,
|
|
92
|
+
universal_newlines=True,
|
|
93
|
+
bufsize=1,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
output = []
|
|
97
|
+
try:
|
|
98
|
+
while True:
|
|
99
|
+
line = process.stdout.readline() # type: ignore
|
|
100
|
+
if not line and process.poll() is not None:
|
|
101
|
+
break
|
|
102
|
+
print(line, end="")
|
|
103
|
+
output.append(line)
|
|
104
|
+
finally:
|
|
105
|
+
process.stdout.close() # type: ignore
|
|
106
|
+
|
|
107
|
+
if process.returncode != 0:
|
|
108
|
+
raise subprocess.CalledProcessError(
|
|
109
|
+
process.returncode, command, "".join(output)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return "".join(output)
|