runnable 0.13.0__py3-none-any.whl → 0.16.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. runnable/__init__.py +1 -12
  2. runnable/catalog.py +29 -5
  3. runnable/cli.py +268 -215
  4. runnable/context.py +10 -3
  5. runnable/datastore.py +212 -53
  6. runnable/defaults.py +13 -55
  7. runnable/entrypoints.py +270 -183
  8. runnable/exceptions.py +28 -2
  9. runnable/executor.py +133 -86
  10. runnable/graph.py +37 -13
  11. runnable/nodes.py +50 -22
  12. runnable/parameters.py +27 -8
  13. runnable/pickler.py +1 -1
  14. runnable/sdk.py +230 -66
  15. runnable/secrets.py +3 -1
  16. runnable/tasks.py +99 -41
  17. runnable/utils.py +59 -39
  18. {runnable-0.13.0.dist-info → runnable-0.16.0.dist-info}/METADATA +28 -31
  19. runnable-0.16.0.dist-info/RECORD +23 -0
  20. {runnable-0.13.0.dist-info → runnable-0.16.0.dist-info}/WHEEL +1 -1
  21. runnable-0.16.0.dist-info/entry_points.txt +45 -0
  22. runnable/extensions/__init__.py +0 -0
  23. runnable/extensions/catalog/__init__.py +0 -21
  24. runnable/extensions/catalog/file_system/__init__.py +0 -0
  25. runnable/extensions/catalog/file_system/implementation.py +0 -234
  26. runnable/extensions/catalog/k8s_pvc/__init__.py +0 -0
  27. runnable/extensions/catalog/k8s_pvc/implementation.py +0 -16
  28. runnable/extensions/catalog/k8s_pvc/integration.py +0 -59
  29. runnable/extensions/executor/__init__.py +0 -649
  30. runnable/extensions/executor/argo/__init__.py +0 -0
  31. runnable/extensions/executor/argo/implementation.py +0 -1194
  32. runnable/extensions/executor/argo/specification.yaml +0 -51
  33. runnable/extensions/executor/k8s_job/__init__.py +0 -0
  34. runnable/extensions/executor/k8s_job/implementation_FF.py +0 -259
  35. runnable/extensions/executor/k8s_job/integration_FF.py +0 -69
  36. runnable/extensions/executor/local.py +0 -69
  37. runnable/extensions/executor/local_container/__init__.py +0 -0
  38. runnable/extensions/executor/local_container/implementation.py +0 -446
  39. runnable/extensions/executor/mocked/__init__.py +0 -0
  40. runnable/extensions/executor/mocked/implementation.py +0 -154
  41. runnable/extensions/executor/retry/__init__.py +0 -0
  42. runnable/extensions/executor/retry/implementation.py +0 -168
  43. runnable/extensions/nodes.py +0 -870
  44. runnable/extensions/run_log_store/__init__.py +0 -0
  45. runnable/extensions/run_log_store/chunked_file_system/__init__.py +0 -0
  46. runnable/extensions/run_log_store/chunked_file_system/implementation.py +0 -111
  47. runnable/extensions/run_log_store/chunked_k8s_pvc/__init__.py +0 -0
  48. runnable/extensions/run_log_store/chunked_k8s_pvc/implementation.py +0 -21
  49. runnable/extensions/run_log_store/chunked_k8s_pvc/integration.py +0 -61
  50. runnable/extensions/run_log_store/db/implementation_FF.py +0 -157
  51. runnable/extensions/run_log_store/db/integration_FF.py +0 -0
  52. runnable/extensions/run_log_store/file_system/__init__.py +0 -0
  53. runnable/extensions/run_log_store/file_system/implementation.py +0 -140
  54. runnable/extensions/run_log_store/generic_chunked.py +0 -557
  55. runnable/extensions/run_log_store/k8s_pvc/__init__.py +0 -0
  56. runnable/extensions/run_log_store/k8s_pvc/implementation.py +0 -21
  57. runnable/extensions/run_log_store/k8s_pvc/integration.py +0 -56
  58. runnable/extensions/secrets/__init__.py +0 -0
  59. runnable/extensions/secrets/dotenv/__init__.py +0 -0
  60. runnable/extensions/secrets/dotenv/implementation.py +0 -100
  61. runnable/integration.py +0 -192
  62. runnable-0.13.0.dist-info/RECORD +0 -63
  63. runnable-0.13.0.dist-info/entry_points.txt +0 -41
  64. {runnable-0.13.0.dist-info → runnable-0.16.0.dist-info/licenses}/LICENSE +0 -0
runnable/parameters.py CHANGED
@@ -15,6 +15,8 @@ from runnable.utils import remove_prefix
15
15
 
16
16
  logger = logging.getLogger(defaults.LOGGER_NAME)
17
17
 
18
+ # TODO: Revisit this, it might be a bit too complicated than required
19
+
18
20
 
19
21
  def get_user_set_parameters(remove: bool = False) -> Dict[str, JsonParameter]:
20
22
  """
@@ -34,9 +36,13 @@ def get_user_set_parameters(remove: bool = False) -> Dict[str, JsonParameter]:
34
36
  if env_var.startswith(defaults.PARAMETER_PREFIX):
35
37
  key = remove_prefix(env_var, defaults.PARAMETER_PREFIX)
36
38
  try:
37
- parameters[key.lower()] = JsonParameter(kind="json", value=json.loads(value))
39
+ parameters[key.lower()] = JsonParameter(
40
+ kind="json", value=json.loads(value)
41
+ )
38
42
  except json.decoder.JSONDecodeError:
39
- logger.warning(f"Parameter {key} could not be JSON decoded, adding the literal value")
43
+ logger.warning(
44
+ f"Parameter {key} could not be JSON decoded, adding the literal value"
45
+ )
40
46
  parameters[key.lower()] = JsonParameter(kind="json", value=value)
41
47
 
42
48
  if remove:
@@ -52,7 +58,9 @@ def serialize_parameter_as_str(value: Any) -> str:
52
58
 
53
59
 
54
60
  def filter_arguments_for_func(
55
- func: Callable[..., Any], params: Dict[str, Any], map_variable: TypeMapVariable = None
61
+ func: Callable[..., Any],
62
+ params: Dict[str, Any],
63
+ map_variable: TypeMapVariable = None,
56
64
  ) -> Dict[str, Any]:
57
65
  """
58
66
  Inspects the function to be called as part of the pipeline to find the arguments of the function.
@@ -96,11 +104,16 @@ def filter_arguments_for_func(
96
104
  # No parameter of this name was provided
97
105
  if value.default == inspect.Parameter.empty:
98
106
  # No default value is given in the function signature. error as parameter is required.
99
- raise ValueError(f"Parameter {name} is required for {func.__name__} but not provided")
107
+ raise ValueError(
108
+ f"Parameter {name} is required for {func.__name__} but not provided"
109
+ )
100
110
  # default value is given in the function signature, nothing further to do.
101
111
  continue
102
112
 
103
- if type(value.annotation) in [BaseModel, pydantic._internal._model_construction.ModelMetaclass]:
113
+ if type(value.annotation) in [
114
+ BaseModel,
115
+ pydantic._internal._model_construction.ModelMetaclass,
116
+ ]:
104
117
  # We try to cast it as a pydantic model if asked
105
118
  named_param = params[name].get_value()
106
119
 
@@ -110,7 +123,9 @@ def filter_arguments_for_func(
110
123
 
111
124
  bound_model = bind_args_for_pydantic_model(named_param, value.annotation)
112
125
  bound_args[name] = bound_model
113
- unassigned_params = unassigned_params.difference(bound_model.model_fields.keys())
126
+ unassigned_params = unassigned_params.difference(
127
+ bound_model.model_fields.keys()
128
+ )
114
129
 
115
130
  elif value.annotation in [str, int, float, bool]:
116
131
  # Cast it if its a primitive type. Ensure the type matches the annotation.
@@ -120,12 +135,16 @@ def filter_arguments_for_func(
120
135
 
121
136
  unassigned_params.remove(name)
122
137
 
123
- params = {key: params[key] for key in unassigned_params} # remove keys from params if they are assigned
138
+ params = {
139
+ key: params[key] for key in unassigned_params
140
+ } # remove keys from params if they are assigned
124
141
 
125
142
  return bound_args
126
143
 
127
144
 
128
- def bind_args_for_pydantic_model(params: Dict[str, Any], model: Type[BaseModel]) -> BaseModel:
145
+ def bind_args_for_pydantic_model(
146
+ params: Dict[str, Any], model: Type[BaseModel]
147
+ ) -> BaseModel:
129
148
  class EasyModel(model): # type: ignore
130
149
  model_config = ConfigDict(extra="ignore")
131
150
 
runnable/pickler.py CHANGED
@@ -9,7 +9,7 @@ import runnable.context as context
9
9
 
10
10
  class BasePickler(ABC, BaseModel):
11
11
  """
12
- The base class for all picklers.
12
+ The base class for all pickler.
13
13
 
14
14
  We are still in the process of hardening the design of this class.
15
15
  For now, we are just going to use pickle.
runnable/sdk.py CHANGED
@@ -26,8 +26,7 @@ from rich.progress import (
26
26
  from rich.table import Column
27
27
  from typing_extensions import Self
28
28
 
29
- from runnable import console, defaults, entrypoints, exceptions, graph, utils
30
- from runnable.extensions.nodes import (
29
+ from extensions.nodes.nodes import (
31
30
  FailNode,
32
31
  MapNode,
33
32
  ParallelNode,
@@ -35,9 +34,14 @@ from runnable.extensions.nodes import (
35
34
  SuccessNode,
36
35
  TaskNode,
37
36
  )
37
+ from runnable import console, defaults, entrypoints, exceptions, graph, utils
38
+ from runnable.executor import BaseJobExecutor, BasePipelineExecutor
38
39
  from runnable.nodes import TraversalNode
40
+ from runnable.tasks import BaseTaskType as RunnableTask
39
41
  from runnable.tasks import TaskReturns
40
42
 
43
+ # TODO: This might have to be an extension
44
+
41
45
  logger = logging.getLogger(defaults.LOGGER_NAME)
42
46
 
43
47
  StepType = Union["Stub", "PythonTask", "NotebookTask", "ShellTask", "Parallel", "Map"]
@@ -66,7 +70,9 @@ class Catalog(BaseModel):
66
70
 
67
71
  """
68
72
 
69
- model_config = ConfigDict(extra="forbid") # Need to be for command, would be validated later
73
+ model_config = ConfigDict(
74
+ extra="forbid"
75
+ ) # Need to be for command, would be validated later
70
76
  # Note: compute_data_folder was confusing to explain, might be introduced later.
71
77
  # compute_data_folder: str = Field(default="", alias="compute_data_folder")
72
78
  get: List[str] = Field(default_factory=list, alias="get")
@@ -95,14 +101,18 @@ class BaseTraversal(ABC, BaseModel):
95
101
 
96
102
  def __rshift__(self, other: StepType) -> StepType:
97
103
  if self.next_node:
98
- raise Exception(f"The node {self} already has a next node: {self.next_node}")
104
+ raise Exception(
105
+ f"The node {self} already has a next node: {self.next_node}"
106
+ )
99
107
  self.next_node = other.name
100
108
 
101
109
  return other
102
110
 
103
111
  def __lshift__(self, other: TraversalNode) -> TraversalNode:
104
112
  if other.next_node:
105
- raise Exception(f"The {other} node already has a next node: {other.next_node}")
113
+ raise Exception(
114
+ f"The {other} node already has a next node: {other.next_node}"
115
+ )
106
116
  other.next_node = self.name
107
117
 
108
118
  return other
@@ -112,7 +122,9 @@ class BaseTraversal(ABC, BaseModel):
112
122
  assert not isinstance(node, Fail)
113
123
 
114
124
  if node.next_node:
115
- raise Exception(f"The {node} node already has a next node: {node.next_node}")
125
+ raise Exception(
126
+ f"The {node} node already has a next node: {node.next_node}"
127
+ )
116
128
 
117
129
  node.next_node = self.name
118
130
  return self
@@ -124,7 +136,9 @@ class BaseTraversal(ABC, BaseModel):
124
136
 
125
137
  if self.terminate_with_failure or self.terminate_with_success:
126
138
  if self.next_node and self.next_node not in ["success", "fail"]:
127
- raise AssertionError("A node being terminated cannot have a user defined next node")
139
+ raise AssertionError(
140
+ "A node being terminated cannot have a user defined next node"
141
+ )
128
142
 
129
143
  if self.terminate_with_failure:
130
144
  self.next_node = "fail"
@@ -135,8 +149,7 @@ class BaseTraversal(ABC, BaseModel):
135
149
  return self
136
150
 
137
151
  @abstractmethod
138
- def create_node(self) -> TraversalNode:
139
- ...
152
+ def create_node(self) -> TraversalNode: ...
140
153
 
141
154
 
142
155
  class BaseTask(BaseTraversal):
@@ -146,12 +159,16 @@ class BaseTask(BaseTraversal):
146
159
 
147
160
  catalog: Optional[Catalog] = Field(default=None, alias="catalog")
148
161
  overrides: Dict[str, Any] = Field(default_factory=dict, alias="overrides")
149
- returns: List[Union[str, TaskReturns]] = Field(default_factory=list, alias="returns")
162
+ returns: List[Union[str, TaskReturns]] = Field(
163
+ default_factory=list, alias="returns"
164
+ )
150
165
  secrets: List[str] = Field(default_factory=list)
151
166
 
152
167
  @field_validator("returns", mode="before")
153
168
  @classmethod
154
- def serialize_returns(cls, returns: List[Union[str, TaskReturns]]) -> List[TaskReturns]:
169
+ def serialize_returns(
170
+ cls, returns: List[Union[str, TaskReturns]]
171
+ ) -> List[TaskReturns]:
155
172
  task_returns = []
156
173
 
157
174
  for x in returns:
@@ -167,9 +184,18 @@ class BaseTask(BaseTraversal):
167
184
  def create_node(self) -> TaskNode:
168
185
  if not self.next_node:
169
186
  if not (self.terminate_with_failure or self.terminate_with_success):
170
- raise AssertionError("A node not being terminated must have a user defined next node")
187
+ raise AssertionError(
188
+ "A node not being terminated must have a user defined next node"
189
+ )
171
190
 
172
- return TaskNode.parse_from_config(self.model_dump(exclude_none=True, by_alias=True))
191
+ return TaskNode.parse_from_config(
192
+ self.model_dump(exclude_none=True, by_alias=True)
193
+ )
194
+
195
+ def create_job(self) -> RunnableTask:
196
+ raise NotImplementedError(
197
+ "This method should be implemented in the child class"
198
+ )
173
199
 
174
200
 
175
201
  class PythonTask(BaseTask):
@@ -254,6 +280,11 @@ class PythonTask(BaseTask):
254
280
 
255
281
  return f"{module}.{name}"
256
282
 
283
+ def create_job(self) -> RunnableTask:
284
+ self.terminate_with_success = True
285
+ node = self.create_node()
286
+ return node.executable
287
+
257
288
 
258
289
  class NotebookTask(BaseTask):
259
290
  """
@@ -326,12 +357,19 @@ class NotebookTask(BaseTask):
326
357
  """
327
358
 
328
359
  notebook: str = Field(serialization_alias="command")
329
- optional_ploomber_args: Optional[Dict[str, Any]] = Field(default=None, alias="optional_ploomber_args")
360
+ optional_ploomber_args: Optional[Dict[str, Any]] = Field(
361
+ default=None, alias="optional_ploomber_args"
362
+ )
330
363
 
331
364
  @computed_field
332
365
  def command_type(self) -> str:
333
366
  return "notebook"
334
367
 
368
+ def create_job(self) -> RunnableTask:
369
+ self.terminate_with_success = True
370
+ node = self.create_node()
371
+ return node.executable
372
+
335
373
 
336
374
  class ShellTask(BaseTask):
337
375
  """
@@ -416,7 +454,9 @@ class Stub(BaseTraversal):
416
454
  def create_node(self) -> StubNode:
417
455
  if not self.next_node:
418
456
  if not (self.terminate_with_failure or self.terminate_with_success):
419
- raise AssertionError("A node not being terminated must have a user defined next node")
457
+ raise AssertionError(
458
+ "A node not being terminated must have a user defined next node"
459
+ )
420
460
 
421
461
  return StubNode.parse_from_config(self.model_dump(exclude_none=True))
422
462
 
@@ -439,14 +479,23 @@ class Parallel(BaseTraversal):
439
479
  @computed_field # type: ignore
440
480
  @property
441
481
  def graph_branches(self) -> Dict[str, graph.Graph]:
442
- return {name: pipeline._dag.model_copy() for name, pipeline in self.branches.items()}
482
+ return {
483
+ name: pipeline._dag.model_copy() for name, pipeline in self.branches.items()
484
+ }
443
485
 
444
486
  def create_node(self) -> ParallelNode:
445
487
  if not self.next_node:
446
488
  if not (self.terminate_with_failure or self.terminate_with_success):
447
- raise AssertionError("A node not being terminated must have a user defined next node")
489
+ raise AssertionError(
490
+ "A node not being terminated must have a user defined next node"
491
+ )
448
492
 
449
- node = ParallelNode(name=self.name, branches=self.graph_branches, internal_name="", next_node=self.next_node)
493
+ node = ParallelNode(
494
+ name=self.name,
495
+ branches=self.graph_branches,
496
+ internal_name="",
497
+ next_node=self.next_node,
498
+ )
450
499
  return node
451
500
 
452
501
 
@@ -483,7 +532,9 @@ class Map(BaseTraversal):
483
532
  def create_node(self) -> MapNode:
484
533
  if not self.next_node:
485
534
  if not (self.terminate_with_failure or self.terminate_with_success):
486
- raise AssertionError("A node not being terminated must have a user defined next node")
535
+ raise AssertionError(
536
+ "A node not being terminated must have a user defined next node"
537
+ )
487
538
 
488
539
  node = MapNode(
489
540
  name=self.name,
@@ -587,6 +638,7 @@ class Pipeline(BaseModel):
587
638
  model_config = ConfigDict(extra="forbid")
588
639
 
589
640
  def _validate_path(self, path: List[StepType], failure_path: bool = False) -> None:
641
+ # TODO: Drastically simplify this
590
642
  # Check if one and only one step terminates with success
591
643
  # Check no more than one step terminates with failure
592
644
 
@@ -596,16 +648,22 @@ class Pipeline(BaseModel):
596
648
  for step in path:
597
649
  if step.terminate_with_success:
598
650
  if reached_success:
599
- raise Exception("A pipeline cannot have more than one step that terminates with success")
651
+ raise Exception(
652
+ "A pipeline cannot have more than one step that terminates with success"
653
+ )
600
654
  reached_success = True
601
655
  continue
602
656
  if step.terminate_with_failure:
603
657
  if reached_failure:
604
- raise Exception("A pipeline cannot have more than one step that terminates with failure")
658
+ raise Exception(
659
+ "A pipeline cannot have more than one step that terminates with failure"
660
+ )
605
661
  reached_failure = True
606
662
 
607
663
  if not reached_success and not reached_failure:
608
- raise Exception("A pipeline must have at least one step that terminates with success")
664
+ raise Exception(
665
+ "A pipeline must have at least one step that terminates with success"
666
+ )
609
667
 
610
668
  def _construct_path(self, path: List[StepType]) -> None:
611
669
  prev_step = path[0]
@@ -615,7 +673,9 @@ class Pipeline(BaseModel):
615
673
  continue
616
674
 
617
675
  if prev_step.terminate_with_success or prev_step.terminate_with_failure:
618
- raise Exception(f"A step that terminates with success/failure cannot have a next step: {prev_step}")
676
+ raise Exception(
677
+ f"A step that terminates with success/failure cannot have a next step: {prev_step}"
678
+ )
619
679
 
620
680
  if prev_step.next_node and prev_step.next_node not in ["success", "fail"]:
621
681
  raise Exception(f"Step already has a next node: {prev_step} ")
@@ -646,7 +706,9 @@ class Pipeline(BaseModel):
646
706
  on_failure_paths: List[List[StepType]] = []
647
707
 
648
708
  for step in self.steps:
649
- if isinstance(step, (Stub, PythonTask, NotebookTask, ShellTask, Parallel, Map)):
709
+ if isinstance(
710
+ step, (Stub, PythonTask, NotebookTask, ShellTask, Parallel, Map)
711
+ ):
650
712
  success_path.append(step)
651
713
  continue
652
714
  # on_failure_paths.append(step)
@@ -690,6 +752,16 @@ class Pipeline(BaseModel):
690
752
  dag_definition = self._dag.model_dump(by_alias=True, exclude_none=True)
691
753
  return graph.create_graph(dag_definition)
692
754
 
755
+ def _is_called_for_definition(self) -> bool:
756
+ """
757
+ If the run context is set, we are coming in only to get the pipeline definition.
758
+ """
759
+ from runnable.context import run_context
760
+
761
+ if run_context is None:
762
+ return False
763
+ return True
764
+
693
765
  def execute(
694
766
  self,
695
767
  configuration_file: str = "",
@@ -699,39 +771,20 @@ class Pipeline(BaseModel):
699
771
  log_level: str = defaults.LOG_LEVEL,
700
772
  ):
701
773
  """
702
- *Execute* the Pipeline.
703
-
704
- Execution of pipeline could either be:
705
-
706
- Traverse and execute all the steps of the pipeline, eg. [local execution](configurations/executors/local.md).
707
-
708
- Or create the representation of the pipeline for other executors.
709
-
710
- Please refer to [concepts](concepts/executor.md) for more information.
711
-
712
- Args:
713
- configuration_file (str, optional): The path to the configuration file. Defaults to "".
714
- The configuration file can be overridden by the environment variable RUNNABLE_CONFIGURATION_FILE.
715
-
716
- run_id (str, optional): The ID of the run. Defaults to "".
717
- tag (str, optional): The tag of the run. Defaults to "".
718
- Use to group multiple runs.
719
-
720
- parameters_file (str, optional): The path to the parameters file. Defaults to "".
721
-
722
- log_level (str, optional): The log level. Defaults to defaults.LOG_LEVEL.
774
+ Overloaded method:
775
+ - Could be called by the user when executing the pipeline via SDK
776
+ - Could be called by the system itself when getting the pipeline definition
723
777
  """
724
-
725
- # py_to_yaml is used by non local executors to generate the yaml representation of the pipeline.
726
- py_to_yaml = os.environ.get("RUNNABLE_PY_TO_YAML", "false")
727
-
728
- if py_to_yaml == "true":
778
+ if self._is_called_for_definition():
779
+ # Immediately return as this call is only for getting the pipeline definition
729
780
  return {}
730
781
 
731
782
  logger.setLevel(log_level)
732
783
 
733
784
  run_id = utils.generate_run_id(run_id=run_id)
734
- configuration_file = os.environ.get("RUNNABLE_CONFIGURATION_FILE", configuration_file)
785
+ configuration_file = os.environ.get(
786
+ "RUNNABLE_CONFIGURATION_FILE", configuration_file
787
+ )
735
788
  run_context = entrypoints.prepare_configurations(
736
789
  configuration_file=configuration_file,
737
790
  run_id=run_id,
@@ -739,19 +792,22 @@ class Pipeline(BaseModel):
739
792
  parameters_file=parameters_file,
740
793
  )
741
794
 
742
- run_context.execution_plan = defaults.EXECUTION_PLAN.CHAINED.value
743
- utils.set_runnable_environment_variables(run_id=run_id, configuration_file=configuration_file, tag=tag)
795
+ assert isinstance(run_context.executor, BasePipelineExecutor)
744
796
 
745
- dag_definition = self._dag.model_dump(by_alias=True, exclude_none=True)
797
+ utils.set_runnable_environment_variables(
798
+ run_id=run_id, configuration_file=configuration_file, tag=tag
799
+ )
746
800
 
801
+ dag_definition = self._dag.model_dump(by_alias=True, exclude_none=True)
802
+ run_context.from_sdk = True
747
803
  run_context.dag = graph.create_graph(dag_definition)
748
804
 
749
805
  console.print("Working with context:")
750
806
  console.print(run_context)
751
807
  console.rule(style="[dark orange]")
752
808
 
753
- if not run_context.executor._local:
754
- # We are not working with non local executor
809
+ if not run_context.executor._is_local:
810
+ # We are not working with executor that does not work in local environment
755
811
  import inspect
756
812
 
757
813
  caller_stack = inspect.stack()[1]
@@ -761,37 +817,145 @@ class Pipeline(BaseModel):
761
817
  module_to_call = f"{module_name}.{caller_stack.function}"
762
818
 
763
819
  run_context.pipeline_file = f"{module_to_call}.py"
820
+ run_context.from_sdk = True
764
821
 
765
822
  # Prepare for graph execution
766
- run_context.executor.prepare_for_graph_execution()
823
+ run_context.executor._set_up_run_log(exists_ok=False)
767
824
 
768
825
  with Progress(
769
826
  SpinnerColumn(spinner_name="runner"),
770
- TextColumn("[progress.description]{task.description}", table_column=Column(ratio=2)),
827
+ TextColumn(
828
+ "[progress.description]{task.description}", table_column=Column(ratio=2)
829
+ ),
771
830
  BarColumn(table_column=Column(ratio=1), style="dark_orange"),
772
831
  TimeElapsedColumn(table_column=Column(ratio=1)),
773
832
  console=console,
774
833
  expand=True,
775
834
  ) as progress:
835
+ pipeline_execution_task = progress.add_task(
836
+ "[dark_orange] Starting execution .. ", total=1
837
+ )
776
838
  try:
777
839
  run_context.progress = progress
778
- pipeline_execution_task = progress.add_task("[dark_orange] Starting execution .. ", total=1)
840
+
779
841
  run_context.executor.execute_graph(dag=run_context.dag)
780
842
 
781
- if not run_context.executor._local:
843
+ if not run_context.executor._is_local:
844
+ # non local executors just traverse the graph and do nothing
782
845
  return {}
783
846
 
784
- run_log = run_context.run_log_store.get_run_log_by_id(run_id=run_context.run_id, full=False)
847
+ run_log = run_context.run_log_store.get_run_log_by_id(
848
+ run_id=run_context.run_id, full=False
849
+ )
785
850
 
786
851
  if run_log.status == defaults.SUCCESS:
787
- progress.update(pipeline_execution_task, description="[green] Success", completed=True)
852
+ progress.update(
853
+ pipeline_execution_task,
854
+ description="[green] Success",
855
+ completed=True,
856
+ )
788
857
  else:
789
- progress.update(pipeline_execution_task, description="[red] Failed", completed=True)
858
+ progress.update(
859
+ pipeline_execution_task,
860
+ description="[red] Failed",
861
+ completed=True,
862
+ )
790
863
  raise exceptions.ExecutionFailedError(run_context.run_id)
791
864
  except Exception as e: # noqa: E722
792
865
  console.print(e, style=defaults.error_style)
793
- progress.update(pipeline_execution_task, description="[red] Errored execution", completed=True)
866
+ progress.update(
867
+ pipeline_execution_task,
868
+ description="[red] Errored execution",
869
+ completed=True,
870
+ )
794
871
  raise
795
872
 
796
- if run_context.executor._local:
797
- return run_context.run_log_store.get_run_log_by_id(run_id=run_context.run_id)
873
+ if run_context.executor._is_local:
874
+ return run_context.run_log_store.get_run_log_by_id(
875
+ run_id=run_context.run_id
876
+ )
877
+
878
+
879
+ class Job(BaseModel):
880
+ name: str
881
+ task: BaseTask
882
+
883
+ def return_task(self) -> RunnableTask:
884
+ return self.task.create_job()
885
+
886
+ def return_catalog_settings(self) -> Optional[List[str]]:
887
+ if self.task.catalog is None:
888
+ return []
889
+ return self.task.catalog.put
890
+
891
+ def _is_called_for_definition(self) -> bool:
892
+ """
893
+ If the run context is set, we are coming in only to get the pipeline definition.
894
+ """
895
+ from runnable.context import run_context
896
+
897
+ if run_context is None:
898
+ return False
899
+ return True
900
+
901
+ def execute(
902
+ self,
903
+ configuration_file: str = "",
904
+ job_id: str = "",
905
+ tag: str = "",
906
+ parameters_file: str = "",
907
+ log_level: str = defaults.LOG_LEVEL,
908
+ ):
909
+ if self._is_called_for_definition():
910
+ # Immediately return as this call is only for getting the job definition
911
+ return {}
912
+ logger.setLevel(log_level)
913
+
914
+ run_id = utils.generate_run_id(run_id=job_id)
915
+ configuration_file = os.environ.get(
916
+ "RUNNABLE_CONFIGURATION_FILE", configuration_file
917
+ )
918
+ run_context = entrypoints.prepare_configurations(
919
+ configuration_file=configuration_file,
920
+ run_id=run_id,
921
+ tag=tag,
922
+ parameters_file=parameters_file,
923
+ is_job=True,
924
+ )
925
+
926
+ assert isinstance(run_context.executor, BaseJobExecutor)
927
+ run_context.from_sdk = True
928
+
929
+ utils.set_runnable_environment_variables(
930
+ run_id=run_id, configuration_file=configuration_file, tag=tag
931
+ )
932
+
933
+ console.print("Working with context:")
934
+ console.print(run_context)
935
+ console.rule(style="[dark orange]")
936
+
937
+ if not run_context.executor._is_local:
938
+ # We are not working with executor that does not work in local environment
939
+ import inspect
940
+
941
+ caller_stack = inspect.stack()[1]
942
+ relative_to_root = str(Path(caller_stack.filename).relative_to(Path.cwd()))
943
+
944
+ module_name = re.sub(r"\b.py\b", "", relative_to_root.replace("/", "."))
945
+ module_to_call = f"{module_name}.{caller_stack.function}"
946
+
947
+ run_context.job_definition_file = f"{module_to_call}.py"
948
+
949
+ job = self.task.create_job()
950
+ catalog_settings = self.return_catalog_settings()
951
+
952
+ run_context.executor.submit_job(job, catalog_settings=catalog_settings)
953
+
954
+ logger.info(
955
+ "Executing the job from the user. We are still in the caller's compute environment"
956
+ )
957
+
958
+ if run_context.executor._is_local:
959
+ return run_context.run_log_store.get_run_log_by_id(
960
+ run_id=run_context.run_id
961
+ )
runnable/secrets.py CHANGED
@@ -92,4 +92,6 @@ class EnvSecretsManager(BaseSecrets):
92
92
  try:
93
93
  return os.environ[name]
94
94
  except KeyError:
95
- raise exceptions.SecretNotFoundError(secret_name=name, secret_setting="environment variables")
95
+ raise exceptions.SecretNotFoundError(
96
+ secret_name=name, secret_setting="environment variables"
97
+ )