runnable 0.13.0__py3-none-any.whl → 0.16.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.
- runnable/__init__.py +1 -12
- runnable/catalog.py +29 -5
- runnable/cli.py +268 -215
- runnable/context.py +10 -3
- runnable/datastore.py +212 -53
- runnable/defaults.py +13 -55
- runnable/entrypoints.py +270 -183
- runnable/exceptions.py +28 -2
- runnable/executor.py +133 -86
- runnable/graph.py +37 -13
- runnable/nodes.py +50 -22
- runnable/parameters.py +27 -8
- runnable/pickler.py +1 -1
- runnable/sdk.py +230 -66
- runnable/secrets.py +3 -1
- runnable/tasks.py +99 -41
- runnable/utils.py +59 -39
- {runnable-0.13.0.dist-info → runnable-0.16.0.dist-info}/METADATA +28 -31
- runnable-0.16.0.dist-info/RECORD +23 -0
- {runnable-0.13.0.dist-info → runnable-0.16.0.dist-info}/WHEEL +1 -1
- runnable-0.16.0.dist-info/entry_points.txt +45 -0
- runnable/extensions/__init__.py +0 -0
- runnable/extensions/catalog/__init__.py +0 -21
- runnable/extensions/catalog/file_system/__init__.py +0 -0
- runnable/extensions/catalog/file_system/implementation.py +0 -234
- runnable/extensions/catalog/k8s_pvc/__init__.py +0 -0
- runnable/extensions/catalog/k8s_pvc/implementation.py +0 -16
- runnable/extensions/catalog/k8s_pvc/integration.py +0 -59
- runnable/extensions/executor/__init__.py +0 -649
- runnable/extensions/executor/argo/__init__.py +0 -0
- runnable/extensions/executor/argo/implementation.py +0 -1194
- runnable/extensions/executor/argo/specification.yaml +0 -51
- runnable/extensions/executor/k8s_job/__init__.py +0 -0
- runnable/extensions/executor/k8s_job/implementation_FF.py +0 -259
- runnable/extensions/executor/k8s_job/integration_FF.py +0 -69
- runnable/extensions/executor/local.py +0 -69
- runnable/extensions/executor/local_container/__init__.py +0 -0
- runnable/extensions/executor/local_container/implementation.py +0 -446
- runnable/extensions/executor/mocked/__init__.py +0 -0
- runnable/extensions/executor/mocked/implementation.py +0 -154
- runnable/extensions/executor/retry/__init__.py +0 -0
- runnable/extensions/executor/retry/implementation.py +0 -168
- runnable/extensions/nodes.py +0 -870
- runnable/extensions/run_log_store/__init__.py +0 -0
- runnable/extensions/run_log_store/chunked_file_system/__init__.py +0 -0
- runnable/extensions/run_log_store/chunked_file_system/implementation.py +0 -111
- runnable/extensions/run_log_store/chunked_k8s_pvc/__init__.py +0 -0
- runnable/extensions/run_log_store/chunked_k8s_pvc/implementation.py +0 -21
- runnable/extensions/run_log_store/chunked_k8s_pvc/integration.py +0 -61
- runnable/extensions/run_log_store/db/implementation_FF.py +0 -157
- runnable/extensions/run_log_store/db/integration_FF.py +0 -0
- runnable/extensions/run_log_store/file_system/__init__.py +0 -0
- runnable/extensions/run_log_store/file_system/implementation.py +0 -140
- runnable/extensions/run_log_store/generic_chunked.py +0 -557
- runnable/extensions/run_log_store/k8s_pvc/__init__.py +0 -0
- runnable/extensions/run_log_store/k8s_pvc/implementation.py +0 -21
- runnable/extensions/run_log_store/k8s_pvc/integration.py +0 -56
- runnable/extensions/secrets/__init__.py +0 -0
- runnable/extensions/secrets/dotenv/__init__.py +0 -0
- runnable/extensions/secrets/dotenv/implementation.py +0 -100
- runnable/integration.py +0 -192
- runnable-0.13.0.dist-info/RECORD +0 -63
- runnable-0.13.0.dist-info/entry_points.txt +0 -41
- {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(
|
39
|
+
parameters[key.lower()] = JsonParameter(
|
40
|
+
kind="json", value=json.loads(value)
|
41
|
+
)
|
38
42
|
except json.decoder.JSONDecodeError:
|
39
|
-
logger.warning(
|
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],
|
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(
|
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 [
|
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(
|
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 = {
|
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(
|
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
|
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
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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 {
|
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(
|
489
|
+
raise AssertionError(
|
490
|
+
"A node not being terminated must have a user defined next node"
|
491
|
+
)
|
448
492
|
|
449
|
-
node = ParallelNode(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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
|
-
|
703
|
-
|
704
|
-
|
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
|
-
|
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(
|
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.
|
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
|
-
|
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.
|
754
|
-
# We are not working with
|
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.
|
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(
|
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
|
-
|
840
|
+
|
779
841
|
run_context.executor.execute_graph(dag=run_context.dag)
|
780
842
|
|
781
|
-
if not run_context.executor.
|
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(
|
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(
|
852
|
+
progress.update(
|
853
|
+
pipeline_execution_task,
|
854
|
+
description="[green] Success",
|
855
|
+
completed=True,
|
856
|
+
)
|
788
857
|
else:
|
789
|
-
progress.update(
|
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(
|
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.
|
797
|
-
return run_context.run_log_store.get_run_log_by_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(
|
95
|
+
raise exceptions.SecretNotFoundError(
|
96
|
+
secret_name=name, secret_setting="environment variables"
|
97
|
+
)
|