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 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
- return f"Pickled object stored in catalog as: {self.value}"
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
- # Get the pickled object
109
- catalog_handler = context.run_context.catalog_handler
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: str = Field(default="", alias="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[Union[StepType, List["Pipeline"]]]
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, [step11, step12,..., step1N], [step21, step22,...,]]
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
- # TODO: Bug with repeat names
703
- # TODO: https://github.com/AstraZeneca/runnable/issues/156
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
- success_path: List[StepType] = []
706
- on_failure_paths: List[List[StepType]] = []
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
- success_path.append(step)
713
- continue
714
- # on_failure_paths.append(step)
715
-
716
- if not success_path:
717
- raise Exception("There should be some success path")
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
- all_steps: List[StepType] = []
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
- for path in paths:
731
- for step in path:
732
- all_steps.append(step)
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
- seen = set()
735
- unique = [x for x in all_steps if not (x in seen or seen.add(x))] # type: ignore
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=all_steps[0].name,
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
- for step in unique:
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
- if self.add_terminal_nodes:
747
- self._dag.add_terminal_nodes()
688
+ self._dag.add_terminal_nodes()
748
689
 
749
690
  self._dag.check_graph()
750
691
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runnable
3
- Version: 0.16.0
3
+ Version: 0.17.1
4
4
  Summary: Add your description here
5
5
  Author-email: "Vammi, Vijay" <vijay.vammi@astrazeneca.com>
6
6
  License-File: LICENSE
@@ -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=pLw_n_5U-FM8-9-41YnkzETX94KrBAZWjxPgjm0O7hk,1305
5
- runnable/datastore.py,sha256=a1pT_P8TNcqQB-di2_uga7y-zS3TqUCb7sFhdxmVKGY,31907
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=cS30EC2Pfz8OzzEVcUYrVIyvGboKVUw5jKG2l72-UfM,15606
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=hwdk2dLmJsOTs2GnOlayw8WfliyeZFpA6Tcnp3tgblg,33370
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.16.0.dist-info/METADATA,sha256=JMu8mSsxMWr_wF246saRXV80D8_8cXMOPZrK6Pr9X6k,10102
20
- runnable-0.16.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
- runnable-0.16.0.dist-info/entry_points.txt,sha256=I92DYldRrCb9HCsoum8GjC2UsQrWpuw2kawXTZpkIz4,1559
22
- runnable-0.16.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
23
- runnable-0.16.0.dist-info/RECORD,,
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,,