runnable 0.50.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.
- extensions/README.md +0 -0
- extensions/__init__.py +0 -0
- extensions/catalog/README.md +0 -0
- extensions/catalog/any_path.py +214 -0
- extensions/catalog/file_system.py +52 -0
- extensions/catalog/minio.py +72 -0
- extensions/catalog/pyproject.toml +14 -0
- extensions/catalog/s3.py +11 -0
- extensions/job_executor/README.md +0 -0
- extensions/job_executor/__init__.py +236 -0
- extensions/job_executor/emulate.py +70 -0
- extensions/job_executor/k8s.py +553 -0
- extensions/job_executor/k8s_job_spec.yaml +37 -0
- extensions/job_executor/local.py +35 -0
- extensions/job_executor/local_container.py +161 -0
- extensions/job_executor/pyproject.toml +16 -0
- extensions/nodes/README.md +0 -0
- extensions/nodes/__init__.py +0 -0
- extensions/nodes/conditional.py +301 -0
- extensions/nodes/fail.py +78 -0
- extensions/nodes/loop.py +394 -0
- extensions/nodes/map.py +477 -0
- extensions/nodes/parallel.py +281 -0
- extensions/nodes/pyproject.toml +15 -0
- extensions/nodes/stub.py +93 -0
- extensions/nodes/success.py +78 -0
- extensions/nodes/task.py +156 -0
- extensions/pipeline_executor/README.md +0 -0
- extensions/pipeline_executor/__init__.py +871 -0
- extensions/pipeline_executor/argo.py +1266 -0
- extensions/pipeline_executor/emulate.py +119 -0
- extensions/pipeline_executor/local.py +226 -0
- extensions/pipeline_executor/local_container.py +369 -0
- extensions/pipeline_executor/mocked.py +159 -0
- extensions/pipeline_executor/pyproject.toml +16 -0
- extensions/run_log_store/README.md +0 -0
- extensions/run_log_store/__init__.py +0 -0
- extensions/run_log_store/any_path.py +100 -0
- extensions/run_log_store/chunked_fs.py +122 -0
- extensions/run_log_store/chunked_minio.py +141 -0
- extensions/run_log_store/file_system.py +91 -0
- extensions/run_log_store/generic_chunked.py +549 -0
- extensions/run_log_store/minio.py +114 -0
- extensions/run_log_store/pyproject.toml +15 -0
- extensions/secrets/README.md +0 -0
- extensions/secrets/dotenv.py +62 -0
- extensions/secrets/pyproject.toml +15 -0
- runnable/__init__.py +108 -0
- runnable/catalog.py +141 -0
- runnable/cli.py +484 -0
- runnable/context.py +730 -0
- runnable/datastore.py +1058 -0
- runnable/defaults.py +159 -0
- runnable/entrypoints.py +390 -0
- runnable/exceptions.py +137 -0
- runnable/executor.py +561 -0
- runnable/gantt.py +1646 -0
- runnable/graph.py +501 -0
- runnable/names.py +546 -0
- runnable/nodes.py +593 -0
- runnable/parameters.py +217 -0
- runnable/pickler.py +96 -0
- runnable/sdk.py +1277 -0
- runnable/secrets.py +92 -0
- runnable/tasks.py +1268 -0
- runnable/telemetry.py +142 -0
- runnable/utils.py +423 -0
- runnable-0.50.0.dist-info/METADATA +189 -0
- runnable-0.50.0.dist-info/RECORD +72 -0
- runnable-0.50.0.dist-info/WHEEL +4 -0
- runnable-0.50.0.dist-info/entry_points.txt +53 -0
- runnable-0.50.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
from multiprocessing import Pool
|
|
4
|
+
from typing import Any, Dict, Optional, cast
|
|
5
|
+
|
|
6
|
+
from pydantic import Field, field_serializer
|
|
7
|
+
|
|
8
|
+
from runnable import console, defaults, exceptions
|
|
9
|
+
from runnable.defaults import IterableParameterModel
|
|
10
|
+
from runnable.graph import Graph, create_graph
|
|
11
|
+
from runnable.nodes import CompositeNode
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(defaults.LOGGER_NAME)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ParallelNode(CompositeNode):
|
|
17
|
+
"""
|
|
18
|
+
A composite node containing many graph objects within itself.
|
|
19
|
+
|
|
20
|
+
The structure is generally:
|
|
21
|
+
ParallelNode:
|
|
22
|
+
Branch A:
|
|
23
|
+
Sub graph definition
|
|
24
|
+
Branch B:
|
|
25
|
+
Sub graph definition
|
|
26
|
+
. . .
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
node_type: str = Field(default="parallel", serialization_alias="type")
|
|
31
|
+
branches: Dict[str, Graph]
|
|
32
|
+
|
|
33
|
+
def get_summary(self) -> Dict[str, Any]:
|
|
34
|
+
summary = {
|
|
35
|
+
"name": self.name,
|
|
36
|
+
"type": self.node_type,
|
|
37
|
+
"branches": [branch.get_summary() for branch in self.branches.values()],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return summary
|
|
41
|
+
|
|
42
|
+
@field_serializer("branches")
|
|
43
|
+
def ser_branches(self, branches: Dict[str, Graph]) -> Dict[str, Graph]:
|
|
44
|
+
ret: Dict[str, Graph] = {}
|
|
45
|
+
|
|
46
|
+
for branch_name, branch in branches.items():
|
|
47
|
+
ret[branch_name.split(".")[-1]] = branch
|
|
48
|
+
|
|
49
|
+
return ret
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def parse_from_config(cls, config: Dict[str, Any]) -> "ParallelNode":
|
|
53
|
+
internal_name = cast(str, config.get("internal_name"))
|
|
54
|
+
|
|
55
|
+
config_branches = config.pop("branches", {})
|
|
56
|
+
branches = {}
|
|
57
|
+
for branch_name, branch_config in config_branches.items():
|
|
58
|
+
sub_graph = create_graph(
|
|
59
|
+
deepcopy(branch_config),
|
|
60
|
+
internal_branch_name=internal_name + "." + branch_name,
|
|
61
|
+
)
|
|
62
|
+
branches[internal_name + "." + branch_name] = sub_graph
|
|
63
|
+
|
|
64
|
+
if not branches:
|
|
65
|
+
raise Exception("A parallel node should have branches")
|
|
66
|
+
return cls(branches=branches, **config)
|
|
67
|
+
|
|
68
|
+
def _get_branch_by_name(self, branch_name: str) -> Graph:
|
|
69
|
+
if branch_name in self.branches:
|
|
70
|
+
return self.branches[branch_name]
|
|
71
|
+
|
|
72
|
+
raise Exception(f"Branch {branch_name} does not exist")
|
|
73
|
+
|
|
74
|
+
def fan_out(
|
|
75
|
+
self,
|
|
76
|
+
iter_variable: Optional[IterableParameterModel] = None,
|
|
77
|
+
):
|
|
78
|
+
"""
|
|
79
|
+
The general fan out method for a node of type Parallel.
|
|
80
|
+
This method assumes that the step log has already been created.
|
|
81
|
+
|
|
82
|
+
3rd party orchestrators should create the step log and use this method to create the branch logs.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
executor (BaseExecutor): The executor class as defined by the config
|
|
86
|
+
iter_variable (dict, optional): If the node is part of a map node. Defaults to None.
|
|
87
|
+
"""
|
|
88
|
+
# Prepare the branch logs
|
|
89
|
+
for internal_branch_name, _ in self.branches.items():
|
|
90
|
+
effective_branch_name = self._resolve_map_placeholders(
|
|
91
|
+
internal_branch_name, iter_variable=iter_variable
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
branch_log = self._context.run_log_store.get_branch_log(
|
|
96
|
+
effective_branch_name, self._context.run_id
|
|
97
|
+
)
|
|
98
|
+
console.print(f"Branch log already exists for {effective_branch_name}")
|
|
99
|
+
except (exceptions.BranchLogNotFoundError, exceptions.EntityNotFoundError):
|
|
100
|
+
branch_log = self._context.run_log_store.create_branch_log(
|
|
101
|
+
effective_branch_name
|
|
102
|
+
)
|
|
103
|
+
console.print(f"Branch log created for {effective_branch_name}")
|
|
104
|
+
|
|
105
|
+
branch_log.status = defaults.PROCESSING
|
|
106
|
+
self._context.run_log_store.add_branch_log(branch_log, self._context.run_id)
|
|
107
|
+
|
|
108
|
+
def execute_as_graph(
|
|
109
|
+
self,
|
|
110
|
+
iter_variable: Optional[IterableParameterModel] = None,
|
|
111
|
+
):
|
|
112
|
+
"""
|
|
113
|
+
This function does the actual execution of the sub-branches of the parallel node.
|
|
114
|
+
|
|
115
|
+
From a design perspective, this function should not be called if the execution is 3rd party orchestrated.
|
|
116
|
+
|
|
117
|
+
The modes that render the job specifications, do not need to interact with this node at all as they have their
|
|
118
|
+
own internal mechanisms of handing parallel states.
|
|
119
|
+
If they do not, you can find a way using as-is nodes as hack nodes.
|
|
120
|
+
|
|
121
|
+
The execution of a dag, could result in
|
|
122
|
+
* The dag being completely executed with a definite (fail, success) state in case of
|
|
123
|
+
local or local-container execution
|
|
124
|
+
* The dag being in a processing state with PROCESSING status in case of local-aws-batch
|
|
125
|
+
|
|
126
|
+
Only fail state is considered failure during this phase of execution.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
executor (Executor): The Executor as per the use config
|
|
130
|
+
**kwargs: Optional kwargs passed around
|
|
131
|
+
"""
|
|
132
|
+
self.fan_out(iter_variable=iter_variable)
|
|
133
|
+
|
|
134
|
+
# Check if parallel execution is enabled and supported
|
|
135
|
+
enable_parallel = getattr(
|
|
136
|
+
self._context.pipeline_executor, "enable_parallel", False
|
|
137
|
+
)
|
|
138
|
+
supports_parallel_writes = getattr(
|
|
139
|
+
self._context.run_log_store, "supports_parallel_writes", False
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Check if we're using a local executor (local or local-container)
|
|
143
|
+
executor_service_name = getattr(
|
|
144
|
+
self._context.pipeline_executor, "service_name", ""
|
|
145
|
+
)
|
|
146
|
+
is_local_executor = executor_service_name in ["local", "local-container"]
|
|
147
|
+
|
|
148
|
+
if enable_parallel and is_local_executor:
|
|
149
|
+
if not supports_parallel_writes:
|
|
150
|
+
logger.warning(
|
|
151
|
+
"Parallel execution was requested but the run log store does not support parallel writes. "
|
|
152
|
+
"Falling back to sequential execution. Consider using a run log store with "
|
|
153
|
+
"supports_parallel_writes=True for parallel execution."
|
|
154
|
+
)
|
|
155
|
+
self._execute_sequentially(iter_variable)
|
|
156
|
+
else:
|
|
157
|
+
logger.info("Executing branches in parallel")
|
|
158
|
+
self._execute_in_parallel(iter_variable)
|
|
159
|
+
else:
|
|
160
|
+
self._execute_sequentially(iter_variable)
|
|
161
|
+
|
|
162
|
+
self.fan_in(iter_variable=iter_variable)
|
|
163
|
+
|
|
164
|
+
def _execute_sequentially(
|
|
165
|
+
self,
|
|
166
|
+
iter_variable: Optional[IterableParameterModel] = None,
|
|
167
|
+
):
|
|
168
|
+
"""Execute branches sequentially (original behavior)."""
|
|
169
|
+
for _, branch in self.branches.items():
|
|
170
|
+
self._context.pipeline_executor.execute_graph(
|
|
171
|
+
branch, iter_variable=iter_variable
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def _execute_in_parallel(
|
|
175
|
+
self,
|
|
176
|
+
iter_variable: Optional[IterableParameterModel] = None,
|
|
177
|
+
):
|
|
178
|
+
"""Execute branches in parallel using multiprocessing."""
|
|
179
|
+
from runnable.entrypoints import execute_single_branch
|
|
180
|
+
|
|
181
|
+
# Prepare arguments for each branch
|
|
182
|
+
branch_args = []
|
|
183
|
+
for branch_name, branch in self.branches.items():
|
|
184
|
+
branch_args.append((branch_name, branch, self._context, iter_variable))
|
|
185
|
+
|
|
186
|
+
# Use multiprocessing Pool to execute branches in parallel
|
|
187
|
+
with Pool() as pool:
|
|
188
|
+
results = pool.starmap(execute_single_branch, branch_args)
|
|
189
|
+
|
|
190
|
+
# Check if any branch failed
|
|
191
|
+
if not all(results):
|
|
192
|
+
failed_branches = [
|
|
193
|
+
branch_name
|
|
194
|
+
for (branch_name, _, _, _), result in zip(branch_args, results)
|
|
195
|
+
if not result
|
|
196
|
+
]
|
|
197
|
+
logger.error(f"The following branches failed: {failed_branches}")
|
|
198
|
+
# Note: The actual failure handling and status update will be done in fan_in()
|
|
199
|
+
|
|
200
|
+
def fan_in(
|
|
201
|
+
self,
|
|
202
|
+
iter_variable: Optional[IterableParameterModel] = None,
|
|
203
|
+
):
|
|
204
|
+
"""
|
|
205
|
+
The general fan in method for a node of type Parallel.
|
|
206
|
+
|
|
207
|
+
3rd party orchestrators should use this method to find the status of the composite step.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
executor (BaseExecutor): The executor class as defined by the config
|
|
211
|
+
iter_variable (dict, optional): If the node is part of a map. Defaults to None.
|
|
212
|
+
"""
|
|
213
|
+
effective_internal_name = self._resolve_map_placeholders(
|
|
214
|
+
self.internal_name, iter_variable=iter_variable
|
|
215
|
+
)
|
|
216
|
+
step_success_bool = True
|
|
217
|
+
for internal_branch_name, _ in self.branches.items():
|
|
218
|
+
effective_branch_name = self._resolve_map_placeholders(
|
|
219
|
+
internal_branch_name, iter_variable=iter_variable
|
|
220
|
+
)
|
|
221
|
+
branch_log = self._context.run_log_store.get_branch_log(
|
|
222
|
+
effective_branch_name, self._context.run_id
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if branch_log.status != defaults.SUCCESS:
|
|
226
|
+
step_success_bool = False
|
|
227
|
+
|
|
228
|
+
# Collate all the results and update the status of the step
|
|
229
|
+
|
|
230
|
+
step_log = self._context.run_log_store.get_step_log(
|
|
231
|
+
effective_internal_name, self._context.run_id
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if step_success_bool: # If none failed
|
|
235
|
+
step_log.status = defaults.SUCCESS
|
|
236
|
+
else:
|
|
237
|
+
step_log.status = defaults.FAIL
|
|
238
|
+
|
|
239
|
+
self._context.run_log_store.add_step_log(step_log, self._context.run_id)
|
|
240
|
+
|
|
241
|
+
# If we failed, return without parameter rollback
|
|
242
|
+
if not step_log.status == defaults.SUCCESS:
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
# Roll back parameters from all branches to parent scope
|
|
246
|
+
parent_params = self._context.run_log_store.get_parameters(
|
|
247
|
+
self._context.run_id, internal_branch_name=self.internal_branch_name
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
for internal_branch_name, _ in self.branches.items():
|
|
251
|
+
effective_branch_name = self._resolve_map_placeholders(
|
|
252
|
+
internal_branch_name, iter_variable=iter_variable
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
branch_params = self._context.run_log_store.get_parameters(
|
|
256
|
+
self._context.run_id, internal_branch_name=effective_branch_name
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Merge branch parameters into parent (overwrite with branch values)
|
|
260
|
+
# If multiple branches set the same parameter, last one wins
|
|
261
|
+
parent_params.update(branch_params)
|
|
262
|
+
|
|
263
|
+
self._context.run_log_store.set_parameters(
|
|
264
|
+
parameters=parent_params,
|
|
265
|
+
run_id=self._context.run_id,
|
|
266
|
+
internal_branch_name=self.internal_branch_name,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
async def execute_as_graph_async(
|
|
270
|
+
self,
|
|
271
|
+
iter_variable: Optional[IterableParameterModel] = None,
|
|
272
|
+
):
|
|
273
|
+
"""Async parallel execution."""
|
|
274
|
+
self.fan_out(iter_variable=iter_variable) # sync - just creates branch logs
|
|
275
|
+
|
|
276
|
+
for _, branch in self.branches.items():
|
|
277
|
+
await self._context.pipeline_executor.execute_graph_async(
|
|
278
|
+
branch, iter_variable=iter_variable
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
self.fan_in(iter_variable=iter_variable) # sync - just collates status
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nodes"
|
|
3
|
+
version = "0.0.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = []
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
[build-system]
|
|
11
|
+
requires = ["hatchling"]
|
|
12
|
+
build-backend = "hatchling.build"
|
|
13
|
+
|
|
14
|
+
[tool.hatch.build.targets.wheel]
|
|
15
|
+
packages = ["."]
|
extensions/nodes/stub.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
from runnable import datastore, defaults
|
|
8
|
+
from runnable.datastore import StepLog
|
|
9
|
+
from runnable.defaults import IterableParameterModel
|
|
10
|
+
from runnable.nodes import ExecutableNode
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(defaults.LOGGER_NAME)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StubNode(ExecutableNode):
|
|
16
|
+
"""
|
|
17
|
+
Stub is a convenience design node.
|
|
18
|
+
It always returns success in the attempt log and does nothing.
|
|
19
|
+
|
|
20
|
+
This node is very similar to pass state in Step functions.
|
|
21
|
+
|
|
22
|
+
This node type could be handy when designing the pipeline and stubbing functions
|
|
23
|
+
--8<-- [start:stub_reference]
|
|
24
|
+
An stub execution node of the pipeline.
|
|
25
|
+
Please refer to define pipeline/tasks/stub for more information.
|
|
26
|
+
|
|
27
|
+
As part of the dag definition, a stub task is defined as follows:
|
|
28
|
+
|
|
29
|
+
dag:
|
|
30
|
+
steps:
|
|
31
|
+
stub_task: # The name of the node
|
|
32
|
+
type: stub
|
|
33
|
+
on_failure: The name of the step to traverse in case of failure
|
|
34
|
+
next: The next node to execute after this task, use "success" to terminate the pipeline successfully
|
|
35
|
+
or "fail" to terminate the pipeline with an error.
|
|
36
|
+
|
|
37
|
+
It can take arbritary number of parameters, which is handy to temporarily silence a task node.
|
|
38
|
+
--8<-- [end:stub_reference]
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
node_type: str = Field(default="stub", serialization_alias="type")
|
|
42
|
+
model_config = ConfigDict(extra="ignore")
|
|
43
|
+
|
|
44
|
+
def get_summary(self) -> Dict[str, Any]:
|
|
45
|
+
summary = {
|
|
46
|
+
"name": self.name,
|
|
47
|
+
"type": self.node_type,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return summary
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def parse_from_config(cls, config: Dict[str, Any]) -> "StubNode":
|
|
54
|
+
return cls(**config)
|
|
55
|
+
|
|
56
|
+
def execute(
|
|
57
|
+
self,
|
|
58
|
+
mock=False,
|
|
59
|
+
iter_variable: Optional[IterableParameterModel] = None,
|
|
60
|
+
attempt_number: int = 1,
|
|
61
|
+
) -> StepLog:
|
|
62
|
+
"""
|
|
63
|
+
Do Nothing node.
|
|
64
|
+
We just send an success attempt log back to the caller
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
executor ([type]): [description]
|
|
68
|
+
mock (bool, optional): [description]. Defaults to False.
|
|
69
|
+
iter_variable (str, optional): [description]. Defaults to ''.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
[type]: [description]
|
|
73
|
+
"""
|
|
74
|
+
step_log = self._context.run_log_store.get_step_log(
|
|
75
|
+
self._get_step_log_name(iter_variable), self._context.run_id
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
attempt_log = datastore.StepAttempt(
|
|
79
|
+
status=defaults.SUCCESS,
|
|
80
|
+
start_time=str(datetime.now()),
|
|
81
|
+
end_time=str(datetime.now()),
|
|
82
|
+
attempt_number=attempt_number,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
self._context.pipeline_executor.add_code_identities(
|
|
86
|
+
node=self, attempt_log=attempt_log
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
step_log.status = attempt_log.status
|
|
90
|
+
|
|
91
|
+
step_log.attempts.append(attempt_log)
|
|
92
|
+
|
|
93
|
+
return step_log
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any, Dict, Optional, cast
|
|
3
|
+
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
|
|
6
|
+
from runnable import datastore, defaults
|
|
7
|
+
from runnable.datastore import StepLog
|
|
8
|
+
from runnable.defaults import IterableParameterModel
|
|
9
|
+
from runnable.nodes import TerminalNode
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SuccessNode(TerminalNode):
|
|
13
|
+
"""
|
|
14
|
+
A leaf node of the graph that represents a success node
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
node_type: str = Field(default="success", serialization_alias="type")
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def parse_from_config(cls, config: Dict[str, Any]) -> "SuccessNode":
|
|
21
|
+
return cast("SuccessNode", super().parse_from_config(config))
|
|
22
|
+
|
|
23
|
+
def get_summary(self) -> Dict[str, Any]:
|
|
24
|
+
summary = {
|
|
25
|
+
"name": self.name,
|
|
26
|
+
"type": self.node_type,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return summary
|
|
30
|
+
|
|
31
|
+
def execute(
|
|
32
|
+
self,
|
|
33
|
+
mock=False,
|
|
34
|
+
iter_variable: Optional[IterableParameterModel] = None,
|
|
35
|
+
attempt_number: int = 1,
|
|
36
|
+
) -> StepLog:
|
|
37
|
+
"""
|
|
38
|
+
Execute the success node.
|
|
39
|
+
Set the run or branch log status to success.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
executor (_type_): The executor class
|
|
43
|
+
mock (bool, optional): If we should just mock and not perform anything. Defaults to False.
|
|
44
|
+
iter_variable (dict, optional): If the node belongs to an internal branch. Defaults to None.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
StepAttempt: The step attempt object
|
|
48
|
+
"""
|
|
49
|
+
step_log = self._context.run_log_store.get_step_log(
|
|
50
|
+
self._get_step_log_name(iter_variable), self._context.run_id
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
attempt_log = datastore.StepAttempt(
|
|
54
|
+
status=defaults.SUCCESS,
|
|
55
|
+
start_time=str(datetime.now()),
|
|
56
|
+
end_time=str(datetime.now()),
|
|
57
|
+
attempt_number=attempt_number,
|
|
58
|
+
retry_indicator=self._context.retry_indicator,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Add code identities to the attempt
|
|
62
|
+
self._context.pipeline_executor.add_code_identities(
|
|
63
|
+
node=self, attempt_log=attempt_log
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
run_or_branch_log = self._context.run_log_store.get_branch_log(
|
|
67
|
+
self._get_branch_log_name(iter_variable), self._context.run_id
|
|
68
|
+
)
|
|
69
|
+
run_or_branch_log.status = defaults.SUCCESS
|
|
70
|
+
self._context.run_log_store.add_branch_log(
|
|
71
|
+
run_or_branch_log, self._context.run_id
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
step_log.status = attempt_log.status
|
|
75
|
+
|
|
76
|
+
step_log.attempts.append(attempt_log)
|
|
77
|
+
|
|
78
|
+
return step_log
|
extensions/nodes/task.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
from runnable import datastore, defaults
|
|
8
|
+
from runnable.datastore import StepLog
|
|
9
|
+
from runnable.defaults import IterableParameterModel
|
|
10
|
+
from runnable.nodes import ExecutableNode
|
|
11
|
+
from runnable.tasks import BaseTaskType, create_task
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(defaults.LOGGER_NAME)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TaskNode(ExecutableNode):
|
|
17
|
+
"""
|
|
18
|
+
A node of type Task.
|
|
19
|
+
|
|
20
|
+
This node does the actual function execution of the graph in all cases.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
executable: BaseTaskType = Field(exclude=True)
|
|
24
|
+
node_type: str = Field(default="task", serialization_alias="type")
|
|
25
|
+
|
|
26
|
+
# It is technically not allowed as parse_from_config filters them.
|
|
27
|
+
# This is just to get the task level configuration to be present during serialization.
|
|
28
|
+
model_config = ConfigDict(extra="allow")
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def parse_from_config(cls, config: Dict[str, Any]) -> "TaskNode":
|
|
32
|
+
# separate task config from node config
|
|
33
|
+
task_config = {
|
|
34
|
+
k: v for k, v in config.items() if k not in TaskNode.model_fields.keys()
|
|
35
|
+
}
|
|
36
|
+
node_config = {
|
|
37
|
+
k: v for k, v in config.items() if k in TaskNode.model_fields.keys()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
executable = create_task(task_config)
|
|
41
|
+
return cls(executable=executable, **node_config, **task_config)
|
|
42
|
+
|
|
43
|
+
def get_summary(self) -> Dict[str, Any]:
|
|
44
|
+
summary = {
|
|
45
|
+
"name": self.name,
|
|
46
|
+
"type": self.node_type,
|
|
47
|
+
"executable": self.executable.get_summary(),
|
|
48
|
+
"catalog": self._get_catalog_settings(),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return summary
|
|
52
|
+
|
|
53
|
+
def execute(
|
|
54
|
+
self,
|
|
55
|
+
mock=False,
|
|
56
|
+
iter_variable: Optional[IterableParameterModel] = None,
|
|
57
|
+
attempt_number: int = 1,
|
|
58
|
+
) -> StepLog:
|
|
59
|
+
"""
|
|
60
|
+
All that we do in runnable is to come to this point where we actually execute the command.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
executor (_type_): The executor class
|
|
64
|
+
mock (bool, optional): If we should just mock and not execute. Defaults to False.
|
|
65
|
+
iter_variable: Optional iteration variable if the node is part of internal branch. Defaults to None.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
StepAttempt: The attempt object
|
|
69
|
+
"""
|
|
70
|
+
step_log = self._context.run_log_store.get_step_log(
|
|
71
|
+
self._get_step_log_name(iter_variable), self._context.run_id
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Set the branch scope for parameter operations
|
|
75
|
+
self.executable.internal_branch_name = self._get_branch_log_name(iter_variable)
|
|
76
|
+
|
|
77
|
+
if not mock:
|
|
78
|
+
# Do not run if we are mocking the execution, could be useful for caching and dry runs
|
|
79
|
+
attempt_log = self.executable.execute_command(iter_variable=iter_variable)
|
|
80
|
+
attempt_log.attempt_number = attempt_number
|
|
81
|
+
attempt_log.retry_indicator = self._context.retry_indicator
|
|
82
|
+
else:
|
|
83
|
+
attempt_log = datastore.StepAttempt(
|
|
84
|
+
status=defaults.SUCCESS,
|
|
85
|
+
start_time=str(datetime.now()),
|
|
86
|
+
end_time=str(datetime.now()),
|
|
87
|
+
attempt_number=attempt_number,
|
|
88
|
+
retry_indicator=self._context.retry_indicator,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Add code identities to the attempt
|
|
92
|
+
self._context.pipeline_executor.add_code_identities(
|
|
93
|
+
node=self, attempt_log=attempt_log
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
logger.info(f"attempt_log: {attempt_log}")
|
|
97
|
+
logger.info(f"Step {self.name} completed with status: {attempt_log.status}")
|
|
98
|
+
|
|
99
|
+
step_log.status = attempt_log.status
|
|
100
|
+
step_log.attempts.append(attempt_log)
|
|
101
|
+
|
|
102
|
+
return step_log
|
|
103
|
+
|
|
104
|
+
async def execute_async(
|
|
105
|
+
self,
|
|
106
|
+
iter_variable: Optional[IterableParameterModel] = None,
|
|
107
|
+
attempt_number: int = 1,
|
|
108
|
+
mock: bool = False,
|
|
109
|
+
) -> StepLog:
|
|
110
|
+
"""Async task execution with fallback to sync."""
|
|
111
|
+
step_log = self._context.run_log_store.get_step_log(
|
|
112
|
+
self._get_step_log_name(iter_variable), self._context.run_id
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Set the branch scope for parameter operations
|
|
116
|
+
self.executable.internal_branch_name = self._get_branch_log_name(iter_variable)
|
|
117
|
+
|
|
118
|
+
if not mock:
|
|
119
|
+
# Get event_callback from executor
|
|
120
|
+
event_callback = self._context.pipeline_executor._event_callback
|
|
121
|
+
|
|
122
|
+
# Try async first, fall back to sync
|
|
123
|
+
try:
|
|
124
|
+
attempt_log = await self.executable.execute_command_async(
|
|
125
|
+
iter_variable=iter_variable,
|
|
126
|
+
event_callback=event_callback,
|
|
127
|
+
)
|
|
128
|
+
except NotImplementedError:
|
|
129
|
+
# Task doesn't support async, fall back to sync
|
|
130
|
+
attempt_log = self.executable.execute_command(
|
|
131
|
+
iter_variable=iter_variable
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
attempt_log.attempt_number = attempt_number
|
|
135
|
+
attempt_log.retry_indicator = self._context.retry_indicator
|
|
136
|
+
else:
|
|
137
|
+
attempt_log = datastore.StepAttempt(
|
|
138
|
+
status=defaults.SUCCESS,
|
|
139
|
+
start_time=str(datetime.now()),
|
|
140
|
+
end_time=str(datetime.now()),
|
|
141
|
+
attempt_number=attempt_number,
|
|
142
|
+
retry_indicator=self._context.retry_indicator,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Add code identities to the attempt
|
|
146
|
+
self._context.pipeline_executor.add_code_identities(
|
|
147
|
+
node=self, attempt_log=attempt_log
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
logger.info(f"attempt_log: {attempt_log}")
|
|
151
|
+
logger.info(f"Step {self.name} completed with status: {attempt_log.status}")
|
|
152
|
+
|
|
153
|
+
step_log.status = attempt_log.status
|
|
154
|
+
step_log.attempts.append(attempt_log)
|
|
155
|
+
|
|
156
|
+
return step_log
|
|
File without changes
|