runnable 0.12.3__py3-none-any.whl → 0.14.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 +0 -11
- runnable/catalog.py +27 -5
- runnable/cli.py +122 -26
- runnable/datastore.py +71 -35
- runnable/defaults.py +0 -1
- runnable/entrypoints.py +107 -32
- runnable/exceptions.py +6 -2
- runnable/executor.py +28 -9
- runnable/graph.py +37 -12
- runnable/integration.py +7 -2
- runnable/nodes.py +15 -17
- runnable/parameters.py +27 -8
- runnable/pickler.py +1 -1
- runnable/sdk.py +101 -33
- runnable/secrets.py +3 -1
- runnable/tasks.py +246 -34
- runnable/utils.py +41 -13
- {runnable-0.12.3.dist-info → runnable-0.14.0.dist-info}/METADATA +25 -31
- runnable-0.14.0.dist-info/RECORD +24 -0
- {runnable-0.12.3.dist-info → runnable-0.14.0.dist-info}/WHEEL +1 -1
- runnable-0.14.0.dist-info/entry_points.txt +40 -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/__init__.py +0 -0
- runnable/extensions/executor/local/implementation.py +0 -71
- 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 -855
- 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-0.12.3.dist-info/RECORD +0 -64
- runnable-0.12.3.dist-info/entry_points.txt +0 -41
- {runnable-0.12.3.dist-info → runnable-0.14.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,12 @@ from runnable.extensions.nodes import (
|
|
35
34
|
SuccessNode,
|
36
35
|
TaskNode,
|
37
36
|
)
|
37
|
+
from runnable import console, defaults, entrypoints, exceptions, graph, utils
|
38
38
|
from runnable.nodes import TraversalNode
|
39
39
|
from runnable.tasks import TaskReturns
|
40
40
|
|
41
|
+
# TODO: This might have to be an extension
|
42
|
+
|
41
43
|
logger = logging.getLogger(defaults.LOGGER_NAME)
|
42
44
|
|
43
45
|
StepType = Union["Stub", "PythonTask", "NotebookTask", "ShellTask", "Parallel", "Map"]
|
@@ -66,7 +68,9 @@ class Catalog(BaseModel):
|
|
66
68
|
|
67
69
|
"""
|
68
70
|
|
69
|
-
model_config = ConfigDict(
|
71
|
+
model_config = ConfigDict(
|
72
|
+
extra="forbid"
|
73
|
+
) # Need to be for command, would be validated later
|
70
74
|
# Note: compute_data_folder was confusing to explain, might be introduced later.
|
71
75
|
# compute_data_folder: str = Field(default="", alias="compute_data_folder")
|
72
76
|
get: List[str] = Field(default_factory=list, alias="get")
|
@@ -95,14 +99,18 @@ class BaseTraversal(ABC, BaseModel):
|
|
95
99
|
|
96
100
|
def __rshift__(self, other: StepType) -> StepType:
|
97
101
|
if self.next_node:
|
98
|
-
raise Exception(
|
102
|
+
raise Exception(
|
103
|
+
f"The node {self} already has a next node: {self.next_node}"
|
104
|
+
)
|
99
105
|
self.next_node = other.name
|
100
106
|
|
101
107
|
return other
|
102
108
|
|
103
109
|
def __lshift__(self, other: TraversalNode) -> TraversalNode:
|
104
110
|
if other.next_node:
|
105
|
-
raise Exception(
|
111
|
+
raise Exception(
|
112
|
+
f"The {other} node already has a next node: {other.next_node}"
|
113
|
+
)
|
106
114
|
other.next_node = self.name
|
107
115
|
|
108
116
|
return other
|
@@ -112,7 +120,9 @@ class BaseTraversal(ABC, BaseModel):
|
|
112
120
|
assert not isinstance(node, Fail)
|
113
121
|
|
114
122
|
if node.next_node:
|
115
|
-
raise Exception(
|
123
|
+
raise Exception(
|
124
|
+
f"The {node} node already has a next node: {node.next_node}"
|
125
|
+
)
|
116
126
|
|
117
127
|
node.next_node = self.name
|
118
128
|
return self
|
@@ -124,7 +134,9 @@ class BaseTraversal(ABC, BaseModel):
|
|
124
134
|
|
125
135
|
if self.terminate_with_failure or self.terminate_with_success:
|
126
136
|
if self.next_node and self.next_node not in ["success", "fail"]:
|
127
|
-
raise AssertionError(
|
137
|
+
raise AssertionError(
|
138
|
+
"A node being terminated cannot have a user defined next node"
|
139
|
+
)
|
128
140
|
|
129
141
|
if self.terminate_with_failure:
|
130
142
|
self.next_node = "fail"
|
@@ -135,8 +147,7 @@ class BaseTraversal(ABC, BaseModel):
|
|
135
147
|
return self
|
136
148
|
|
137
149
|
@abstractmethod
|
138
|
-
def create_node(self) -> TraversalNode:
|
139
|
-
...
|
150
|
+
def create_node(self) -> TraversalNode: ...
|
140
151
|
|
141
152
|
|
142
153
|
class BaseTask(BaseTraversal):
|
@@ -146,12 +157,16 @@ class BaseTask(BaseTraversal):
|
|
146
157
|
|
147
158
|
catalog: Optional[Catalog] = Field(default=None, alias="catalog")
|
148
159
|
overrides: Dict[str, Any] = Field(default_factory=dict, alias="overrides")
|
149
|
-
returns: List[Union[str, TaskReturns]] = Field(
|
160
|
+
returns: List[Union[str, TaskReturns]] = Field(
|
161
|
+
default_factory=list, alias="returns"
|
162
|
+
)
|
150
163
|
secrets: List[str] = Field(default_factory=list)
|
151
164
|
|
152
165
|
@field_validator("returns", mode="before")
|
153
166
|
@classmethod
|
154
|
-
def serialize_returns(
|
167
|
+
def serialize_returns(
|
168
|
+
cls, returns: List[Union[str, TaskReturns]]
|
169
|
+
) -> List[TaskReturns]:
|
155
170
|
task_returns = []
|
156
171
|
|
157
172
|
for x in returns:
|
@@ -167,9 +182,13 @@ class BaseTask(BaseTraversal):
|
|
167
182
|
def create_node(self) -> TaskNode:
|
168
183
|
if not self.next_node:
|
169
184
|
if not (self.terminate_with_failure or self.terminate_with_success):
|
170
|
-
raise AssertionError(
|
185
|
+
raise AssertionError(
|
186
|
+
"A node not being terminated must have a user defined next node"
|
187
|
+
)
|
171
188
|
|
172
|
-
return TaskNode.parse_from_config(
|
189
|
+
return TaskNode.parse_from_config(
|
190
|
+
self.model_dump(exclude_none=True, by_alias=True)
|
191
|
+
)
|
173
192
|
|
174
193
|
|
175
194
|
class PythonTask(BaseTask):
|
@@ -326,7 +345,9 @@ class NotebookTask(BaseTask):
|
|
326
345
|
"""
|
327
346
|
|
328
347
|
notebook: str = Field(serialization_alias="command")
|
329
|
-
optional_ploomber_args: Optional[Dict[str, Any]] = Field(
|
348
|
+
optional_ploomber_args: Optional[Dict[str, Any]] = Field(
|
349
|
+
default=None, alias="optional_ploomber_args"
|
350
|
+
)
|
330
351
|
|
331
352
|
@computed_field
|
332
353
|
def command_type(self) -> str:
|
@@ -416,7 +437,9 @@ class Stub(BaseTraversal):
|
|
416
437
|
def create_node(self) -> StubNode:
|
417
438
|
if not self.next_node:
|
418
439
|
if not (self.terminate_with_failure or self.terminate_with_success):
|
419
|
-
raise AssertionError(
|
440
|
+
raise AssertionError(
|
441
|
+
"A node not being terminated must have a user defined next node"
|
442
|
+
)
|
420
443
|
|
421
444
|
return StubNode.parse_from_config(self.model_dump(exclude_none=True))
|
422
445
|
|
@@ -439,14 +462,23 @@ class Parallel(BaseTraversal):
|
|
439
462
|
@computed_field # type: ignore
|
440
463
|
@property
|
441
464
|
def graph_branches(self) -> Dict[str, graph.Graph]:
|
442
|
-
return {
|
465
|
+
return {
|
466
|
+
name: pipeline._dag.model_copy() for name, pipeline in self.branches.items()
|
467
|
+
}
|
443
468
|
|
444
469
|
def create_node(self) -> ParallelNode:
|
445
470
|
if not self.next_node:
|
446
471
|
if not (self.terminate_with_failure or self.terminate_with_success):
|
447
|
-
raise AssertionError(
|
472
|
+
raise AssertionError(
|
473
|
+
"A node not being terminated must have a user defined next node"
|
474
|
+
)
|
448
475
|
|
449
|
-
node = ParallelNode(
|
476
|
+
node = ParallelNode(
|
477
|
+
name=self.name,
|
478
|
+
branches=self.graph_branches,
|
479
|
+
internal_name="",
|
480
|
+
next_node=self.next_node,
|
481
|
+
)
|
450
482
|
return node
|
451
483
|
|
452
484
|
|
@@ -483,7 +515,9 @@ class Map(BaseTraversal):
|
|
483
515
|
def create_node(self) -> MapNode:
|
484
516
|
if not self.next_node:
|
485
517
|
if not (self.terminate_with_failure or self.terminate_with_success):
|
486
|
-
raise AssertionError(
|
518
|
+
raise AssertionError(
|
519
|
+
"A node not being terminated must have a user defined next node"
|
520
|
+
)
|
487
521
|
|
488
522
|
node = MapNode(
|
489
523
|
name=self.name,
|
@@ -596,16 +630,22 @@ class Pipeline(BaseModel):
|
|
596
630
|
for step in path:
|
597
631
|
if step.terminate_with_success:
|
598
632
|
if reached_success:
|
599
|
-
raise Exception(
|
633
|
+
raise Exception(
|
634
|
+
"A pipeline cannot have more than one step that terminates with success"
|
635
|
+
)
|
600
636
|
reached_success = True
|
601
637
|
continue
|
602
638
|
if step.terminate_with_failure:
|
603
639
|
if reached_failure:
|
604
|
-
raise Exception(
|
640
|
+
raise Exception(
|
641
|
+
"A pipeline cannot have more than one step that terminates with failure"
|
642
|
+
)
|
605
643
|
reached_failure = True
|
606
644
|
|
607
645
|
if not reached_success and not reached_failure:
|
608
|
-
raise Exception(
|
646
|
+
raise Exception(
|
647
|
+
"A pipeline must have at least one step that terminates with success"
|
648
|
+
)
|
609
649
|
|
610
650
|
def _construct_path(self, path: List[StepType]) -> None:
|
611
651
|
prev_step = path[0]
|
@@ -615,7 +655,9 @@ class Pipeline(BaseModel):
|
|
615
655
|
continue
|
616
656
|
|
617
657
|
if prev_step.terminate_with_success or prev_step.terminate_with_failure:
|
618
|
-
raise Exception(
|
658
|
+
raise Exception(
|
659
|
+
f"A step that terminates with success/failure cannot have a next step: {prev_step}"
|
660
|
+
)
|
619
661
|
|
620
662
|
if prev_step.next_node and prev_step.next_node not in ["success", "fail"]:
|
621
663
|
raise Exception(f"Step already has a next node: {prev_step} ")
|
@@ -646,7 +688,9 @@ class Pipeline(BaseModel):
|
|
646
688
|
on_failure_paths: List[List[StepType]] = []
|
647
689
|
|
648
690
|
for step in self.steps:
|
649
|
-
if isinstance(
|
691
|
+
if isinstance(
|
692
|
+
step, (Stub, PythonTask, NotebookTask, ShellTask, Parallel, Map)
|
693
|
+
):
|
650
694
|
success_path.append(step)
|
651
695
|
continue
|
652
696
|
# on_failure_paths.append(step)
|
@@ -731,7 +775,9 @@ class Pipeline(BaseModel):
|
|
731
775
|
logger.setLevel(log_level)
|
732
776
|
|
733
777
|
run_id = utils.generate_run_id(run_id=run_id)
|
734
|
-
configuration_file = os.environ.get(
|
778
|
+
configuration_file = os.environ.get(
|
779
|
+
"RUNNABLE_CONFIGURATION_FILE", configuration_file
|
780
|
+
)
|
735
781
|
run_context = entrypoints.prepare_configurations(
|
736
782
|
configuration_file=configuration_file,
|
737
783
|
run_id=run_id,
|
@@ -740,7 +786,9 @@ class Pipeline(BaseModel):
|
|
740
786
|
)
|
741
787
|
|
742
788
|
run_context.execution_plan = defaults.EXECUTION_PLAN.CHAINED.value
|
743
|
-
utils.set_runnable_environment_variables(
|
789
|
+
utils.set_runnable_environment_variables(
|
790
|
+
run_id=run_id, configuration_file=configuration_file, tag=tag
|
791
|
+
)
|
744
792
|
|
745
793
|
dag_definition = self._dag.model_dump(by_alias=True, exclude_none=True)
|
746
794
|
|
@@ -767,7 +815,9 @@ class Pipeline(BaseModel):
|
|
767
815
|
|
768
816
|
with Progress(
|
769
817
|
SpinnerColumn(spinner_name="runner"),
|
770
|
-
TextColumn(
|
818
|
+
TextColumn(
|
819
|
+
"[progress.description]{task.description}", table_column=Column(ratio=2)
|
820
|
+
),
|
771
821
|
BarColumn(table_column=Column(ratio=1), style="dark_orange"),
|
772
822
|
TimeElapsedColumn(table_column=Column(ratio=1)),
|
773
823
|
console=console,
|
@@ -775,23 +825,41 @@ class Pipeline(BaseModel):
|
|
775
825
|
) as progress:
|
776
826
|
try:
|
777
827
|
run_context.progress = progress
|
778
|
-
pipeline_execution_task = progress.add_task(
|
828
|
+
pipeline_execution_task = progress.add_task(
|
829
|
+
"[dark_orange] Starting execution .. ", total=1
|
830
|
+
)
|
779
831
|
run_context.executor.execute_graph(dag=run_context.dag)
|
780
832
|
|
781
833
|
if not run_context.executor._local:
|
782
834
|
return {}
|
783
835
|
|
784
|
-
run_log = run_context.run_log_store.get_run_log_by_id(
|
836
|
+
run_log = run_context.run_log_store.get_run_log_by_id(
|
837
|
+
run_id=run_context.run_id, full=False
|
838
|
+
)
|
785
839
|
|
786
840
|
if run_log.status == defaults.SUCCESS:
|
787
|
-
progress.update(
|
841
|
+
progress.update(
|
842
|
+
pipeline_execution_task,
|
843
|
+
description="[green] Success",
|
844
|
+
completed=True,
|
845
|
+
)
|
788
846
|
else:
|
789
|
-
progress.update(
|
847
|
+
progress.update(
|
848
|
+
pipeline_execution_task,
|
849
|
+
description="[red] Failed",
|
850
|
+
completed=True,
|
851
|
+
)
|
790
852
|
raise exceptions.ExecutionFailedError(run_context.run_id)
|
791
853
|
except Exception as e: # noqa: E722
|
792
854
|
console.print(e, style=defaults.error_style)
|
793
|
-
progress.update(
|
855
|
+
progress.update(
|
856
|
+
pipeline_execution_task,
|
857
|
+
description="[red] Errored execution",
|
858
|
+
completed=True,
|
859
|
+
)
|
794
860
|
raise
|
795
861
|
|
796
862
|
if run_context.executor._local:
|
797
|
-
return run_context.run_log_store.get_run_log_by_id(
|
863
|
+
return run_context.run_log_store.get_run_log_by_id(
|
864
|
+
run_id=run_context.run_id
|
865
|
+
)
|
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
|
+
)
|