runnable 0.16.0__py3-none-any.whl → 0.17.1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- runnable/context.py +3 -1
- runnable/datastore.py +14 -3
- runnable/executor.py +1 -3
- runnable/sdk.py +40 -99
- {runnable-0.16.0.dist-info → runnable-0.17.1.dist-info}/METADATA +1 -1
- {runnable-0.16.0.dist-info → runnable-0.17.1.dist-info}/RECORD +9 -9
- {runnable-0.16.0.dist-info → runnable-0.17.1.dist-info}/WHEEL +0 -0
- {runnable-0.16.0.dist-info → runnable-0.17.1.dist-info}/entry_points.txt +0 -0
- {runnable-0.16.0.dist-info → runnable-0.17.1.dist-info}/licenses/LICENSE +0 -0
runnable/context.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import Dict, List, Optional
|
1
|
+
from typing import Any, Dict, List, Optional
|
2
2
|
|
3
3
|
from pydantic import BaseModel, ConfigDict, Field, SerializeAsAny
|
4
4
|
from rich.progress import Progress
|
@@ -29,6 +29,8 @@ class Context(BaseModel):
|
|
29
29
|
from_sdk: bool = False
|
30
30
|
|
31
31
|
run_id: str = ""
|
32
|
+
object_serialisation: bool = True
|
33
|
+
return_objects: Dict[str, Any] = {}
|
32
34
|
|
33
35
|
tag: str = ""
|
34
36
|
variables: Dict[str, str] = {}
|
runnable/datastore.py
CHANGED
@@ -98,22 +98,33 @@ class ObjectParameter(BaseModel):
|
|
98
98
|
@computed_field # type: ignore
|
99
99
|
@property
|
100
100
|
def description(self) -> str:
|
101
|
-
|
101
|
+
if context.run_context.object_serialisation:
|
102
|
+
return f"Pickled object stored in catalog as: {self.value}"
|
103
|
+
|
104
|
+
return f"Object stored in memory as: {self.value}"
|
102
105
|
|
103
106
|
@property
|
104
107
|
def file_name(self) -> str:
|
105
108
|
return f"{self.value}{context.run_context.pickler.extension}"
|
106
109
|
|
107
110
|
def get_value(self) -> Any:
|
108
|
-
#
|
109
|
-
|
111
|
+
# If there was no serialisation, return the object from the return objects
|
112
|
+
if not context.run_context.object_serialisation:
|
113
|
+
return context.run_context.return_objects[self.value]
|
110
114
|
|
115
|
+
# If the object was serialised, get it from the catalog
|
116
|
+
catalog_handler = context.run_context.catalog_handler
|
111
117
|
catalog_handler.get(name=self.file_name, run_id=context.run_context.run_id)
|
112
118
|
obj = context.run_context.pickler.load(path=self.file_name)
|
113
119
|
os.remove(self.file_name) # Remove after loading
|
114
120
|
return obj
|
115
121
|
|
116
122
|
def put_object(self, data: Any) -> None:
|
123
|
+
if not context.run_context.object_serialisation:
|
124
|
+
context.run_context.return_objects[self.value] = data
|
125
|
+
return
|
126
|
+
|
127
|
+
# If the object was serialised, put it in the catalog
|
117
128
|
context.run_context.pickler.dump(data=data, path=self.file_name)
|
118
129
|
|
119
130
|
catalog_handler = context.run_context.catalog_handler
|
runnable/executor.py
CHANGED
@@ -34,9 +34,7 @@ class BaseExecutor(ABC, BaseModel):
|
|
34
34
|
service_name: str = ""
|
35
35
|
service_type: str = "executor"
|
36
36
|
|
37
|
-
_is_local: bool = (
|
38
|
-
False # This is a flag to indicate whether the executor is local or not.
|
39
|
-
)
|
37
|
+
_is_local: bool = PrivateAttr(default=False)
|
40
38
|
|
41
39
|
model_config = ConfigDict(extra="forbid")
|
42
40
|
|
runnable/sdk.py
CHANGED
@@ -84,7 +84,7 @@ class BaseTraversal(ABC, BaseModel):
|
|
84
84
|
next_node: str = Field(default="", serialization_alias="next_node")
|
85
85
|
terminate_with_success: bool = Field(default=False, exclude=True)
|
86
86
|
terminate_with_failure: bool = Field(default=False, exclude=True)
|
87
|
-
on_failure:
|
87
|
+
on_failure: Optional[Pipeline] = Field(default=None)
|
88
88
|
|
89
89
|
model_config = ConfigDict(extra="forbid")
|
90
90
|
|
@@ -117,18 +117,6 @@ class BaseTraversal(ABC, BaseModel):
|
|
117
117
|
|
118
118
|
return other
|
119
119
|
|
120
|
-
def depends_on(self, node: StepType) -> Self:
|
121
|
-
assert not isinstance(node, Success)
|
122
|
-
assert not isinstance(node, Fail)
|
123
|
-
|
124
|
-
if node.next_node:
|
125
|
-
raise Exception(
|
126
|
-
f"The {node} node already has a next node: {node.next_node}"
|
127
|
-
)
|
128
|
-
|
129
|
-
node.next_node = self.name
|
130
|
-
return self
|
131
|
-
|
132
120
|
@model_validator(mode="after")
|
133
121
|
def validate_terminations(self) -> Self:
|
134
122
|
if self.terminate_with_failure and self.terminate_with_success:
|
@@ -175,7 +163,6 @@ class BaseTask(BaseTraversal):
|
|
175
163
|
if isinstance(x, str):
|
176
164
|
task_returns.append(TaskReturns(name=x, kind="json"))
|
177
165
|
continue
|
178
|
-
|
179
166
|
# Its already task returns
|
180
167
|
task_returns.append(x)
|
181
168
|
|
@@ -188,6 +175,9 @@ class BaseTask(BaseTraversal):
|
|
188
175
|
"A node not being terminated must have a user defined next node"
|
189
176
|
)
|
190
177
|
|
178
|
+
if self.on_failure:
|
179
|
+
self.on_failure = self.on_failure.steps[0].name # type: ignore
|
180
|
+
|
191
181
|
return TaskNode.parse_from_config(
|
192
182
|
self.model_dump(exclude_none=True, by_alias=True)
|
193
183
|
)
|
@@ -605,8 +595,6 @@ class Pipeline(BaseModel):
|
|
605
595
|
The order of steps is important as it determines the order of execution.
|
606
596
|
Any on failure behavior should the first step in ```on_failure``` pipelines.
|
607
597
|
|
608
|
-
|
609
|
-
|
610
598
|
on_failure (List[List[Pipeline], optional): A list of Pipelines to execute in case of failure.
|
611
599
|
|
612
600
|
For example, for the below pipeline:
|
@@ -624,7 +612,7 @@ class Pipeline(BaseModel):
|
|
624
612
|
|
625
613
|
"""
|
626
614
|
|
627
|
-
steps: List[
|
615
|
+
steps: List[StepType]
|
628
616
|
name: str = ""
|
629
617
|
description: str = ""
|
630
618
|
|
@@ -637,114 +625,67 @@ class Pipeline(BaseModel):
|
|
637
625
|
_dag: graph.Graph = PrivateAttr()
|
638
626
|
model_config = ConfigDict(extra="forbid")
|
639
627
|
|
640
|
-
def _validate_path(self, path: List[StepType], failure_path: bool = False) -> None:
|
641
|
-
# TODO: Drastically simplify this
|
642
|
-
# Check if one and only one step terminates with success
|
643
|
-
# Check no more than one step terminates with failure
|
644
|
-
|
645
|
-
reached_success = False
|
646
|
-
reached_failure = False
|
647
|
-
|
648
|
-
for step in path:
|
649
|
-
if step.terminate_with_success:
|
650
|
-
if reached_success:
|
651
|
-
raise Exception(
|
652
|
-
"A pipeline cannot have more than one step that terminates with success"
|
653
|
-
)
|
654
|
-
reached_success = True
|
655
|
-
continue
|
656
|
-
if step.terminate_with_failure:
|
657
|
-
if reached_failure:
|
658
|
-
raise Exception(
|
659
|
-
"A pipeline cannot have more than one step that terminates with failure"
|
660
|
-
)
|
661
|
-
reached_failure = True
|
662
|
-
|
663
|
-
if not reached_success and not reached_failure:
|
664
|
-
raise Exception(
|
665
|
-
"A pipeline must have at least one step that terminates with success"
|
666
|
-
)
|
667
|
-
|
668
|
-
def _construct_path(self, path: List[StepType]) -> None:
|
669
|
-
prev_step = path[0]
|
670
|
-
|
671
|
-
for step in path:
|
672
|
-
if step == prev_step:
|
673
|
-
continue
|
674
|
-
|
675
|
-
if prev_step.terminate_with_success or prev_step.terminate_with_failure:
|
676
|
-
raise Exception(
|
677
|
-
f"A step that terminates with success/failure cannot have a next step: {prev_step}"
|
678
|
-
)
|
679
|
-
|
680
|
-
if prev_step.next_node and prev_step.next_node not in ["success", "fail"]:
|
681
|
-
raise Exception(f"Step already has a next node: {prev_step} ")
|
682
|
-
|
683
|
-
prev_step.next_node = step.name
|
684
|
-
prev_step = step
|
685
|
-
|
686
628
|
def model_post_init(self, __context: Any) -> None:
|
687
629
|
"""
|
688
630
|
The sequence of steps can either be:
|
689
|
-
[step1, step2,..., stepN
|
631
|
+
[step1, step2,..., stepN]
|
690
632
|
indicates:
|
691
633
|
- step1 > step2 > ... > stepN
|
692
634
|
- We expect terminate with success or fail to be explicitly stated on a step.
|
693
635
|
- If it is stated, the step cannot have a next step defined apart from "success" and "fail".
|
694
|
-
|
695
|
-
The inner list of steps is only to accommodate on-failure behaviors.
|
696
|
-
- For sake of simplicity, lets assume that it has the same behavior as the happy pipeline.
|
697
|
-
- A task which was already seen should not be part of this.
|
698
|
-
- There should be at least one step which terminates with success
|
699
|
-
|
700
636
|
Any definition of pipeline should have one node that terminates with success.
|
701
637
|
"""
|
702
|
-
#
|
703
|
-
#
|
638
|
+
# The last step of the pipeline is defaulted to be a success step
|
639
|
+
# unless it is explicitly stated to terminate with failure.
|
640
|
+
terminal_step: StepType = self.steps[-1]
|
641
|
+
if not terminal_step.terminate_with_failure:
|
642
|
+
terminal_step.terminate_with_success = True
|
704
643
|
|
705
|
-
|
706
|
-
|
644
|
+
# assert that there is only one termination node with success or failure
|
645
|
+
# Assert that there are no duplicate step names
|
646
|
+
observed: Dict[str, str] = {}
|
647
|
+
count_termination: int = 0
|
707
648
|
|
708
649
|
for step in self.steps:
|
709
650
|
if isinstance(
|
710
651
|
step, (Stub, PythonTask, NotebookTask, ShellTask, Parallel, Map)
|
711
652
|
):
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
# Check all paths are valid and construct the path
|
720
|
-
paths = [success_path] + on_failure_paths
|
721
|
-
failure_path = False
|
722
|
-
for path in paths:
|
723
|
-
self._validate_path(path, failure_path)
|
724
|
-
self._construct_path(path)
|
725
|
-
|
726
|
-
failure_path = True
|
653
|
+
if step.terminate_with_success or step.terminate_with_failure:
|
654
|
+
count_termination += 1
|
655
|
+
if step.name in observed:
|
656
|
+
raise Exception(
|
657
|
+
f"Step names should be unique. Found duplicate: {step.name}"
|
658
|
+
)
|
659
|
+
observed[step.name] = step.name
|
727
660
|
|
728
|
-
|
661
|
+
if count_termination > 1:
|
662
|
+
raise AssertionError(
|
663
|
+
"A pipeline can only have one termination node with success or failure"
|
664
|
+
)
|
729
665
|
|
730
|
-
|
731
|
-
|
732
|
-
|
666
|
+
# link the steps by assigning the next_node name to be that name of the node
|
667
|
+
# immediately after it.
|
668
|
+
for i in range(len(self.steps) - 1):
|
669
|
+
self.steps[i] >> self.steps[i + 1]
|
733
670
|
|
734
|
-
|
735
|
-
|
671
|
+
# Add any on_failure pipelines to the steps
|
672
|
+
gathered_on_failure: List[StepType] = []
|
673
|
+
for step in self.steps:
|
674
|
+
if step.on_failure:
|
675
|
+
gathered_on_failure.extend(step.on_failure.steps)
|
736
676
|
|
737
677
|
self._dag = graph.Graph(
|
738
|
-
start_at=
|
678
|
+
start_at=self.steps[0].name,
|
739
679
|
description=self.description,
|
740
680
|
internal_branch_name=self.internal_branch_name,
|
741
681
|
)
|
742
682
|
|
743
|
-
|
683
|
+
self.steps.extend(gathered_on_failure)
|
684
|
+
|
685
|
+
for step in self.steps:
|
744
686
|
self._dag.add_node(step.create_node())
|
745
687
|
|
746
|
-
|
747
|
-
self._dag.add_terminal_nodes()
|
688
|
+
self._dag.add_terminal_nodes()
|
748
689
|
|
749
690
|
self._dag.check_graph()
|
750
691
|
|
@@ -1,23 +1,23 @@
|
|
1
1
|
runnable/__init__.py,sha256=KqpLDTD1CfdEj2aDyEkSn2KW-_83qyrRrrWLc5lZVM4,624
|
2
2
|
runnable/catalog.py,sha256=MiEmb-18liAKmgeMdDF41VVn0ZEAVLP8hR33oacQ1zs,4930
|
3
3
|
runnable/cli.py,sha256=01zmzOdynEmLI4vWDtSHQ6y1od_Jlc8G1RF69fi2L8g,8446
|
4
|
-
runnable/context.py,sha256=
|
5
|
-
runnable/datastore.py,sha256=
|
4
|
+
runnable/context.py,sha256=by5uepmuCP0dmM9BmsliXihSes5QEFejwAsmekcqylE,1388
|
5
|
+
runnable/datastore.py,sha256=9y5enzn6AXLHLdwvgkdjGPrBkVlrcjfbaAHsst-lJzg,32466
|
6
6
|
runnable/defaults.py,sha256=3o9IVGryyCE6PoQTOoaIaHHTbJGEzmdXMcwzOhwAYoI,3518
|
7
7
|
runnable/entrypoints.py,sha256=67gPBiIIS4Kd9g6LdoGCraRJPda8K1i7Lp7XcD2iY5k,18913
|
8
8
|
runnable/exceptions.py,sha256=LFbp0-Qxg2PAMLEVt7w2whhBxSG-5pzUEv5qN-Rc4_c,3003
|
9
|
-
runnable/executor.py,sha256=
|
9
|
+
runnable/executor.py,sha256=Rafu9EECrNq1LBkJmS6KYCekchP5ufrR04mHWG-JzqQ,15543
|
10
10
|
runnable/graph.py,sha256=jVjikRLR-so3b2ufmNKpEQ_Ny68qN4bcGDAdXBRKiCY,16574
|
11
11
|
runnable/names.py,sha256=vn92Kv9ANROYSZX6Z4z1v_WA3WiEdIYmG6KEStBFZug,8134
|
12
12
|
runnable/nodes.py,sha256=YU9u7r1ESzui1uVtJ1dgwdv1ozyJnF2k-MCFieT8CLI,17519
|
13
13
|
runnable/parameters.py,sha256=g_bJurLjuppFDiDpfFqy6BRF36o_EY0OC5APl7HJFok,5450
|
14
14
|
runnable/pickler.py,sha256=ydJ_eti_U1F4l-YacFp7BWm6g5vTn04UXye25S1HVok,2684
|
15
|
-
runnable/sdk.py,sha256=
|
15
|
+
runnable/sdk.py,sha256=xN5F4XX8r5wCN131kgN2xG7MkNm0bSGJ3Ukw8prHYJ8,31444
|
16
16
|
runnable/secrets.py,sha256=PXcEJw-4WPzeWRLfsatcPPyr1zkqgHzdRWRcS9vvpvM,2354
|
17
17
|
runnable/tasks.py,sha256=JnIIYQf3YUidHXIN6hiUIfDnegc7_rJMNXuHW4WS9ig,29378
|
18
18
|
runnable/utils.py,sha256=wqyN7lMW56cBqyE59iDE6_i2HXPkvEUCQ-66UQnIwTA,19993
|
19
|
-
runnable-0.
|
20
|
-
runnable-0.
|
21
|
-
runnable-0.
|
22
|
-
runnable-0.
|
23
|
-
runnable-0.
|
19
|
+
runnable-0.17.1.dist-info/METADATA,sha256=ST_BmhGguYwYrDH0DlUEhR9QCJz9QM09BraYWO9TJbU,10102
|
20
|
+
runnable-0.17.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
21
|
+
runnable-0.17.1.dist-info/entry_points.txt,sha256=I92DYldRrCb9HCsoum8GjC2UsQrWpuw2kawXTZpkIz4,1559
|
22
|
+
runnable-0.17.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
23
|
+
runnable-0.17.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|