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.
- 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
|
+
)
|