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.

Files changed (49) hide show
  1. extensions/catalog/any_path.py +13 -2
  2. extensions/job_executor/__init__.py +7 -5
  3. extensions/job_executor/emulate.py +106 -0
  4. extensions/job_executor/k8s.py +8 -8
  5. extensions/job_executor/local_container.py +13 -14
  6. extensions/nodes/__init__.py +0 -0
  7. extensions/nodes/conditional.py +243 -0
  8. extensions/nodes/fail.py +72 -0
  9. extensions/nodes/map.py +350 -0
  10. extensions/nodes/parallel.py +159 -0
  11. extensions/nodes/stub.py +89 -0
  12. extensions/nodes/success.py +72 -0
  13. extensions/nodes/task.py +92 -0
  14. extensions/pipeline_executor/__init__.py +27 -27
  15. extensions/pipeline_executor/argo.py +52 -46
  16. extensions/pipeline_executor/emulate.py +112 -0
  17. extensions/pipeline_executor/local.py +4 -4
  18. extensions/pipeline_executor/local_container.py +19 -79
  19. extensions/pipeline_executor/mocked.py +5 -9
  20. extensions/pipeline_executor/retry.py +6 -10
  21. runnable/__init__.py +2 -11
  22. runnable/catalog.py +6 -23
  23. runnable/cli.py +145 -48
  24. runnable/context.py +520 -28
  25. runnable/datastore.py +51 -54
  26. runnable/defaults.py +12 -34
  27. runnable/entrypoints.py +82 -440
  28. runnable/exceptions.py +35 -34
  29. runnable/executor.py +13 -20
  30. runnable/gantt.py +1141 -0
  31. runnable/graph.py +1 -1
  32. runnable/names.py +1 -1
  33. runnable/nodes.py +20 -16
  34. runnable/parameters.py +108 -51
  35. runnable/sdk.py +125 -204
  36. runnable/tasks.py +62 -85
  37. runnable/utils.py +6 -268
  38. runnable-1.0.0.dist-info/METADATA +122 -0
  39. runnable-1.0.0.dist-info/RECORD +73 -0
  40. {runnable-0.34.0a1.dist-info → runnable-1.0.0.dist-info}/entry_points.txt +9 -8
  41. extensions/nodes/nodes.py +0 -778
  42. extensions/nodes/torch.py +0 -273
  43. extensions/nodes/torch_config.py +0 -76
  44. extensions/tasks/torch.py +0 -286
  45. extensions/tasks/torch_config.py +0 -76
  46. runnable-0.34.0a1.dist-info/METADATA +0 -267
  47. runnable-0.34.0a1.dist-info/RECORD +0 -67
  48. {runnable-0.34.0a1.dist-info → runnable-1.0.0.dist-info}/WHEEL +0 -0
  49. {runnable-0.34.0a1.dist-info → runnable-1.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -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 TypeMapVariable
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 = utils.get_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.catalog_handler.get(
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.catalog_handler.put(
163
- name=name_pattern, allow_file_not_found_exc=allow_file_no_found_exc
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.catalog_handler.put(name=log_file_name)
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: TypeMapVariable = None,
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: TypeMapVariable = None):
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: TypeMapVariable = None
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: TypeMapVariable = None
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: TypeMapVariable = None):
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 = self._context.progress.add_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 = self._context.progress.add_task(
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
- self._context.progress.update(
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
- self._context.progress.update(
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
- self._context.progress.update(
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
- self._context.progress.update(
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: TypeMapVariable = None):
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: TypeMapVariable = None):
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.nodes import MapNode, ParallelNode, TaskNode
24
-
25
- # TODO: Should be part of a wider refactor
26
- # from extensions.nodes.torch import TorchNode
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, utils
29
- from runnable.defaults import TypeMapVariable
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
- _is_local: bool = False
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: TypeMapVariable = {}
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 = utils.get_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", "torch", "success", "stub", "fail"]
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: TypeMapVariable = {}
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 = utils.get_node_execution_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", "torch"]:
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 isinstance(working_on, MapNode) or isinstance(
772
- working_on, ParallelNode
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: TypeMapVariable):
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: TypeMapVariable = None):
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
- def fan_in(self, node: BaseNode, map_variable: TypeMapVariable = None):
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.catalog_handler.service_name:
1045
+ match self._context.catalog.service_name:
1040
1046
  case "file-system":
1041
- self._context.catalog_handler.catalog_location = (
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)