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
@@ -1,649 +0,0 @@
|
|
1
|
-
import copy
|
2
|
-
import logging
|
3
|
-
import os
|
4
|
-
from abc import abstractmethod
|
5
|
-
from typing import Dict, List, Optional, cast
|
6
|
-
|
7
|
-
from runnable import (
|
8
|
-
console,
|
9
|
-
context,
|
10
|
-
defaults,
|
11
|
-
exceptions,
|
12
|
-
integration,
|
13
|
-
parameters,
|
14
|
-
task_console,
|
15
|
-
utils,
|
16
|
-
)
|
17
|
-
from runnable.datastore import DataCatalog, JsonParameter, RunLog, StepLog
|
18
|
-
from runnable.defaults import TypeMapVariable
|
19
|
-
from runnable.executor import BaseExecutor
|
20
|
-
from runnable.extensions.nodes import TaskNode
|
21
|
-
from runnable.graph import Graph
|
22
|
-
from runnable.nodes import BaseNode
|
23
|
-
|
24
|
-
logger = logging.getLogger(defaults.LOGGER_NAME)
|
25
|
-
|
26
|
-
|
27
|
-
class GenericExecutor(BaseExecutor):
|
28
|
-
"""
|
29
|
-
The skeleton of an executor class.
|
30
|
-
Any implementation of an executor should inherit this class and over-ride accordingly.
|
31
|
-
|
32
|
-
This is a loaded base class which has a lot of methods already implemented for "typical" executions.
|
33
|
-
Look at the function docs to understand how to use them appropriately.
|
34
|
-
|
35
|
-
For any implementation:
|
36
|
-
1). Who/when should the run log be set up?
|
37
|
-
2). Who/When should the step log be set up?
|
38
|
-
|
39
|
-
"""
|
40
|
-
|
41
|
-
service_name: str = ""
|
42
|
-
service_type: str = "executor"
|
43
|
-
|
44
|
-
@property
|
45
|
-
def _context(self):
|
46
|
-
return context.run_context
|
47
|
-
|
48
|
-
def _get_parameters(self) -> Dict[str, JsonParameter]:
|
49
|
-
"""
|
50
|
-
Consolidate the parameters from the environment variables
|
51
|
-
and the parameters file.
|
52
|
-
|
53
|
-
The parameters defined in the environment variables take precedence over the parameters file.
|
54
|
-
|
55
|
-
Returns:
|
56
|
-
_type_: _description_
|
57
|
-
"""
|
58
|
-
params: Dict[str, JsonParameter] = {}
|
59
|
-
if self._context.parameters_file:
|
60
|
-
user_defined = utils.load_yaml(self._context.parameters_file) or {}
|
61
|
-
|
62
|
-
for key, value in user_defined.items():
|
63
|
-
params[key] = JsonParameter(value=value, kind="json")
|
64
|
-
|
65
|
-
# Update these with some from the environment variables
|
66
|
-
params.update(parameters.get_user_set_parameters())
|
67
|
-
logger.debug(f"parameters as seen by executor: {params}")
|
68
|
-
return params
|
69
|
-
|
70
|
-
def _set_up_run_log(self, exists_ok=False):
|
71
|
-
"""
|
72
|
-
Create a run log and put that in the run log store
|
73
|
-
|
74
|
-
If exists_ok, we allow the run log to be already present in the run log store.
|
75
|
-
"""
|
76
|
-
try:
|
77
|
-
attempt_run_log = self._context.run_log_store.get_run_log_by_id(run_id=self._context.run_id, full=False)
|
78
|
-
|
79
|
-
logger.warning(f"The run log by id: {self._context.run_id} already exists, is this designed?")
|
80
|
-
raise exceptions.RunLogExistsError(
|
81
|
-
f"The run log by id: {self._context.run_id} already exists and is {attempt_run_log.status}"
|
82
|
-
)
|
83
|
-
except exceptions.RunLogNotFoundError:
|
84
|
-
pass
|
85
|
-
except exceptions.RunLogExistsError:
|
86
|
-
if exists_ok:
|
87
|
-
return
|
88
|
-
raise
|
89
|
-
|
90
|
-
# Consolidate and get the parameters
|
91
|
-
params = self._get_parameters()
|
92
|
-
|
93
|
-
self._context.run_log_store.create_run_log(
|
94
|
-
run_id=self._context.run_id,
|
95
|
-
tag=self._context.tag,
|
96
|
-
status=defaults.PROCESSING,
|
97
|
-
dag_hash=self._context.dag_hash,
|
98
|
-
)
|
99
|
-
# Any interaction with run log store attributes should happen via API if available.
|
100
|
-
self._context.run_log_store.set_parameters(run_id=self._context.run_id, parameters=params)
|
101
|
-
|
102
|
-
# Update run_config
|
103
|
-
run_config = utils.get_run_config()
|
104
|
-
logger.debug(f"run_config as seen by executor: {run_config}")
|
105
|
-
self._context.run_log_store.set_run_config(run_id=self._context.run_id, run_config=run_config)
|
106
|
-
|
107
|
-
def prepare_for_graph_execution(self):
|
108
|
-
"""
|
109
|
-
This method should be called prior to calling execute_graph.
|
110
|
-
Perform any steps required before doing the graph execution.
|
111
|
-
|
112
|
-
The most common implementation is to prepare a run log for the run if the run uses local interactive compute.
|
113
|
-
|
114
|
-
But in cases of actual rendering the job specs (eg: AWS step functions, K8's) we check if the services are OK.
|
115
|
-
We do not set up a run log as its not relevant.
|
116
|
-
"""
|
117
|
-
|
118
|
-
integration.validate(self, self._context.run_log_store)
|
119
|
-
integration.configure_for_traversal(self, self._context.run_log_store)
|
120
|
-
|
121
|
-
integration.validate(self, self._context.catalog_handler)
|
122
|
-
integration.configure_for_traversal(self, self._context.catalog_handler)
|
123
|
-
|
124
|
-
integration.validate(self, self._context.secrets_handler)
|
125
|
-
integration.configure_for_traversal(self, self._context.secrets_handler)
|
126
|
-
|
127
|
-
self._set_up_run_log()
|
128
|
-
|
129
|
-
def prepare_for_node_execution(self):
|
130
|
-
"""
|
131
|
-
Perform any modifications to the services prior to execution of the node.
|
132
|
-
|
133
|
-
Args:
|
134
|
-
node (Node): [description]
|
135
|
-
map_variable (dict, optional): [description]. Defaults to None.
|
136
|
-
"""
|
137
|
-
integration.validate(self, self._context.run_log_store)
|
138
|
-
integration.configure_for_execution(self, self._context.run_log_store)
|
139
|
-
|
140
|
-
integration.validate(self, self._context.catalog_handler)
|
141
|
-
integration.configure_for_execution(self, self._context.catalog_handler)
|
142
|
-
|
143
|
-
integration.validate(self, self._context.secrets_handler)
|
144
|
-
integration.configure_for_execution(self, self._context.secrets_handler)
|
145
|
-
|
146
|
-
def _sync_catalog(self, stage: str, synced_catalogs=None) -> Optional[List[DataCatalog]]:
|
147
|
-
"""
|
148
|
-
1). Identify the catalog settings by over-riding node settings with the global settings.
|
149
|
-
2). For stage = get:
|
150
|
-
Identify the catalog items that are being asked to get from the catalog
|
151
|
-
And copy them to the local compute data folder
|
152
|
-
3). For stage = put:
|
153
|
-
Identify the catalog items that are being asked to put into the catalog
|
154
|
-
Copy the items from local compute folder to the catalog
|
155
|
-
4). Add the items onto the step log according to the stage
|
156
|
-
|
157
|
-
Args:
|
158
|
-
node (Node): The current node being processed
|
159
|
-
step_log (StepLog): The step log corresponding to that node
|
160
|
-
stage (str): One of get or put
|
161
|
-
|
162
|
-
Raises:
|
163
|
-
Exception: If the stage is not in one of get/put
|
164
|
-
|
165
|
-
"""
|
166
|
-
if stage not in ["get", "put"]:
|
167
|
-
msg = (
|
168
|
-
"Catalog service only accepts get/put possible actions as part of node execution."
|
169
|
-
f"Sync catalog of the executor: {self.service_name} asks for {stage} which is not accepted"
|
170
|
-
)
|
171
|
-
logger.exception(msg)
|
172
|
-
raise Exception(msg)
|
173
|
-
|
174
|
-
try:
|
175
|
-
node_catalog_settings = self._context_node._get_catalog_settings()
|
176
|
-
except exceptions.TerminalNodeError:
|
177
|
-
return None
|
178
|
-
|
179
|
-
if not (node_catalog_settings and stage in node_catalog_settings):
|
180
|
-
logger.info("No catalog settings found for stage: %s", stage)
|
181
|
-
# Nothing to get/put from the catalog
|
182
|
-
return None
|
183
|
-
|
184
|
-
compute_data_folder = self.get_effective_compute_data_folder()
|
185
|
-
|
186
|
-
data_catalogs = []
|
187
|
-
for name_pattern in node_catalog_settings.get(stage) or []:
|
188
|
-
if stage == "get":
|
189
|
-
data_catalog = self._context.catalog_handler.get(
|
190
|
-
name=name_pattern, run_id=self._context.run_id, compute_data_folder=compute_data_folder
|
191
|
-
)
|
192
|
-
|
193
|
-
elif stage == "put":
|
194
|
-
data_catalog = self._context.catalog_handler.put(
|
195
|
-
name=name_pattern,
|
196
|
-
run_id=self._context.run_id,
|
197
|
-
compute_data_folder=compute_data_folder,
|
198
|
-
synced_catalogs=synced_catalogs,
|
199
|
-
)
|
200
|
-
|
201
|
-
logger.debug(f"Added data catalog: {data_catalog} to step log")
|
202
|
-
data_catalogs.extend(data_catalog)
|
203
|
-
|
204
|
-
return data_catalogs
|
205
|
-
|
206
|
-
def get_effective_compute_data_folder(self) -> str:
|
207
|
-
"""
|
208
|
-
Get the effective compute data folder for the given stage.
|
209
|
-
If there is nothing to catalog, we return None.
|
210
|
-
|
211
|
-
The default is the compute data folder of the catalog but this can be over-ridden by the node.
|
212
|
-
|
213
|
-
Args:
|
214
|
-
stage (str): The stage we are in the process of cataloging
|
215
|
-
|
216
|
-
|
217
|
-
Returns:
|
218
|
-
str: The compute data folder as defined by the node defaulting to catalog handler
|
219
|
-
"""
|
220
|
-
compute_data_folder = self._context.catalog_handler.compute_data_folder
|
221
|
-
|
222
|
-
catalog_settings = self._context_node._get_catalog_settings()
|
223
|
-
effective_compute_data_folder = catalog_settings.get("compute_data_folder", "") or compute_data_folder
|
224
|
-
|
225
|
-
return effective_compute_data_folder
|
226
|
-
|
227
|
-
@property
|
228
|
-
def step_attempt_number(self) -> int:
|
229
|
-
"""
|
230
|
-
The attempt number of the current step.
|
231
|
-
Orchestrators should use this step to submit multiple attempts of the job.
|
232
|
-
|
233
|
-
Returns:
|
234
|
-
int: The attempt number of the current step. Defaults to 1.
|
235
|
-
"""
|
236
|
-
return int(os.environ.get(defaults.ATTEMPT_NUMBER, 1))
|
237
|
-
|
238
|
-
def _execute_node(self, node: BaseNode, map_variable: TypeMapVariable = None, mock: bool = False, **kwargs):
|
239
|
-
"""
|
240
|
-
This is the entry point when we do the actual execution of the function.
|
241
|
-
DO NOT Over-ride this function.
|
242
|
-
|
243
|
-
While in interactive execution, we just compute, in 3rd party interactive execution, we need to reach
|
244
|
-
this function.
|
245
|
-
|
246
|
-
In most cases,
|
247
|
-
* We get the corresponding step_log of the node and the parameters.
|
248
|
-
* We sync the catalog to GET any data sets that are in the catalog
|
249
|
-
* We call the execute method of the node for the actual compute and retry it as many times as asked.
|
250
|
-
* If the node succeeds, we get any of the user defined metrics provided by the user.
|
251
|
-
* We sync the catalog to PUT any data sets that are in the catalog.
|
252
|
-
|
253
|
-
Args:
|
254
|
-
node (Node): The node to execute
|
255
|
-
map_variable (dict, optional): If the node is of a map state, map_variable is the value of the iterable.
|
256
|
-
Defaults to None.
|
257
|
-
"""
|
258
|
-
logger.info(f"Trying to execute node: {node.internal_name}, attempt : {self.step_attempt_number}")
|
259
|
-
|
260
|
-
self._context_node = node
|
261
|
-
|
262
|
-
data_catalogs_get: Optional[List[DataCatalog]] = self._sync_catalog(stage="get")
|
263
|
-
logger.debug(f"data_catalogs_get: {data_catalogs_get}")
|
264
|
-
|
265
|
-
step_log = node.execute(
|
266
|
-
map_variable=map_variable,
|
267
|
-
attempt_number=self.step_attempt_number,
|
268
|
-
mock=mock,
|
269
|
-
**kwargs,
|
270
|
-
)
|
271
|
-
|
272
|
-
data_catalogs_put: Optional[List[DataCatalog]] = self._sync_catalog(stage="put")
|
273
|
-
logger.debug(f"data_catalogs_put: {data_catalogs_put}")
|
274
|
-
|
275
|
-
step_log.add_data_catalogs(data_catalogs_get or [])
|
276
|
-
step_log.add_data_catalogs(data_catalogs_put or [])
|
277
|
-
|
278
|
-
console.print(f"Summary of the step: {step_log.internal_name}")
|
279
|
-
console.print(step_log.get_summary(), style=defaults.info_style)
|
280
|
-
|
281
|
-
self._context_node = None # type: ignore
|
282
|
-
|
283
|
-
self._context.run_log_store.add_step_log(step_log, self._context.run_id)
|
284
|
-
|
285
|
-
def add_code_identities(self, node: BaseNode, step_log: StepLog, **kwargs):
|
286
|
-
"""
|
287
|
-
Add code identities specific to the implementation.
|
288
|
-
|
289
|
-
The Base class has an implementation of adding git code identities.
|
290
|
-
|
291
|
-
Args:
|
292
|
-
step_log (object): The step log object
|
293
|
-
node (BaseNode): The node we are adding the step log for
|
294
|
-
"""
|
295
|
-
step_log.code_identities.append(utils.get_git_code_identity())
|
296
|
-
|
297
|
-
def execute_from_graph(self, node: BaseNode, map_variable: TypeMapVariable = None, **kwargs):
|
298
|
-
"""
|
299
|
-
This is the entry point to from the graph execution.
|
300
|
-
|
301
|
-
While the self.execute_graph is responsible for traversing the graph, this function is responsible for
|
302
|
-
actual execution of the node.
|
303
|
-
|
304
|
-
If the node type is:
|
305
|
-
* task : We can delegate to _execute_node after checking the eligibility for re-run in cases of a re-run
|
306
|
-
* success: We can delegate to _execute_node
|
307
|
-
* fail: We can delegate to _execute_node
|
308
|
-
|
309
|
-
For nodes that are internally graphs:
|
310
|
-
* parallel: Delegate the responsibility of execution to the node.execute_as_graph()
|
311
|
-
* dag: Delegate the responsibility of execution to the node.execute_as_graph()
|
312
|
-
* map: Delegate the responsibility of execution to the node.execute_as_graph()
|
313
|
-
|
314
|
-
Transpilers will NEVER use this method and will NEVER call ths method.
|
315
|
-
This method should only be used by interactive executors.
|
316
|
-
|
317
|
-
Args:
|
318
|
-
node (Node): The node to execute
|
319
|
-
map_variable (dict, optional): If the node if of a map state, this corresponds to the value of iterable.
|
320
|
-
Defaults to None.
|
321
|
-
"""
|
322
|
-
step_log = self._context.run_log_store.create_step_log(node.name, node._get_step_log_name(map_variable))
|
323
|
-
|
324
|
-
self.add_code_identities(node=node, step_log=step_log)
|
325
|
-
|
326
|
-
step_log.step_type = node.node_type
|
327
|
-
step_log.status = defaults.PROCESSING
|
328
|
-
|
329
|
-
self._context.run_log_store.add_step_log(step_log, self._context.run_id)
|
330
|
-
|
331
|
-
logger.info(f"Executing node: {node.get_summary()}")
|
332
|
-
|
333
|
-
# Add the step log to the database as per the situation.
|
334
|
-
# If its a terminal node, complete it now
|
335
|
-
if node.node_type in ["success", "fail"]:
|
336
|
-
self._execute_node(node, map_variable=map_variable, **kwargs)
|
337
|
-
return
|
338
|
-
|
339
|
-
# We call an internal function to iterate the sub graphs and execute them
|
340
|
-
if node.is_composite:
|
341
|
-
node.execute_as_graph(map_variable=map_variable, **kwargs)
|
342
|
-
return
|
343
|
-
|
344
|
-
task_console.export_text(clear=True)
|
345
|
-
|
346
|
-
task_name = node._resolve_map_placeholders(node.internal_name, map_variable)
|
347
|
-
console.print(f":runner: Executing the node {task_name} ... ", style="bold color(208)")
|
348
|
-
self.trigger_job(node=node, map_variable=map_variable, **kwargs)
|
349
|
-
|
350
|
-
log_file_name = utils.make_log_file_name(node=node, map_variable=map_variable)
|
351
|
-
task_console.save_text(log_file_name, clear=True)
|
352
|
-
|
353
|
-
self._context.catalog_handler.put(name=log_file_name, run_id=self._context.run_id)
|
354
|
-
os.remove(log_file_name)
|
355
|
-
|
356
|
-
def trigger_job(self, node: BaseNode, map_variable: TypeMapVariable = None, **kwargs):
|
357
|
-
"""
|
358
|
-
Call this method only if we are responsible for traversing the graph via
|
359
|
-
execute_from_graph().
|
360
|
-
|
361
|
-
We are not prepared to execute node as of now.
|
362
|
-
|
363
|
-
Args:
|
364
|
-
node (BaseNode): The node to execute
|
365
|
-
map_variable (str, optional): If the node if of a map state, this corresponds to the value of iterable.
|
366
|
-
Defaults to ''.
|
367
|
-
|
368
|
-
NOTE: We do not raise an exception as this method is not required by many extensions
|
369
|
-
"""
|
370
|
-
pass
|
371
|
-
|
372
|
-
def _get_status_and_next_node_name(self, current_node: BaseNode, dag: Graph, map_variable: TypeMapVariable = None):
|
373
|
-
"""
|
374
|
-
Given the current node and the graph, returns the name of the next node to execute.
|
375
|
-
|
376
|
-
The name is always relative the graph that the node resides in.
|
377
|
-
|
378
|
-
If the current node succeeded, we return the next node as per the graph.
|
379
|
-
If the current node failed, we return the on failure node of the node (if provided) or the global one.
|
380
|
-
|
381
|
-
This method is only used by interactive executors i.e local and local-container
|
382
|
-
|
383
|
-
Args:
|
384
|
-
current_node (BaseNode): The current node.
|
385
|
-
dag (Graph): The dag we are traversing.
|
386
|
-
map_variable (dict): If the node belongs to a map branch.
|
387
|
-
|
388
|
-
"""
|
389
|
-
|
390
|
-
step_log = self._context.run_log_store.get_step_log(
|
391
|
-
current_node._get_step_log_name(map_variable), self._context.run_id
|
392
|
-
)
|
393
|
-
logger.info(f"Finished executing the node {current_node} with status {step_log.status}")
|
394
|
-
|
395
|
-
try:
|
396
|
-
next_node_name = current_node._get_next_node()
|
397
|
-
except exceptions.TerminalNodeError:
|
398
|
-
next_node_name = ""
|
399
|
-
|
400
|
-
if step_log.status == defaults.FAIL:
|
401
|
-
next_node_name = dag.get_fail_node().name
|
402
|
-
if current_node._get_on_failure_node():
|
403
|
-
next_node_name = current_node._get_on_failure_node()
|
404
|
-
|
405
|
-
return step_log.status, next_node_name
|
406
|
-
|
407
|
-
def execute_graph(self, dag: Graph, map_variable: TypeMapVariable = None, **kwargs):
|
408
|
-
"""
|
409
|
-
The parallelization is controlled by the nodes and not by this function.
|
410
|
-
|
411
|
-
Transpilers should over ride this method to do the translation of dag to the platform specific way.
|
412
|
-
Interactive methods should use this to traverse and execute the dag.
|
413
|
-
- Use execute_from_graph to handle sub-graphs
|
414
|
-
|
415
|
-
Logically the method should:
|
416
|
-
* Start at the dag.start_at of the dag.
|
417
|
-
* Call the self.execute_from_graph(node)
|
418
|
-
* depending upon the status of the execution, either move to the success node or failure node.
|
419
|
-
|
420
|
-
Args:
|
421
|
-
dag (Graph): The directed acyclic graph to traverse and execute.
|
422
|
-
map_variable (dict, optional): If the node if of a map state, this corresponds to the value of the iterable.
|
423
|
-
Defaults to None.
|
424
|
-
"""
|
425
|
-
current_node = dag.start_at
|
426
|
-
previous_node = None
|
427
|
-
logger.info(f"Running the execution with {current_node}")
|
428
|
-
|
429
|
-
branch_execution_task = None
|
430
|
-
branch_task_name: str = ""
|
431
|
-
if dag.internal_branch_name:
|
432
|
-
branch_task_name = BaseNode._resolve_map_placeholders(
|
433
|
-
dag.internal_branch_name or "Graph",
|
434
|
-
map_variable,
|
435
|
-
)
|
436
|
-
branch_execution_task = self._context.progress.add_task(
|
437
|
-
f"[dark_orange]Executing {branch_task_name}",
|
438
|
-
total=1,
|
439
|
-
)
|
440
|
-
|
441
|
-
while True:
|
442
|
-
working_on = dag.get_node_by_name(current_node)
|
443
|
-
task_name = working_on._resolve_map_placeholders(working_on.internal_name, map_variable)
|
444
|
-
|
445
|
-
if previous_node == current_node:
|
446
|
-
raise Exception("Potentially running in a infinite loop")
|
447
|
-
|
448
|
-
previous_node = current_node
|
449
|
-
|
450
|
-
logger.debug(f"Creating execution log for {working_on}")
|
451
|
-
|
452
|
-
depth = " " * ((task_name.count(".")) or 1 - 1)
|
453
|
-
|
454
|
-
task_execution = self._context.progress.add_task(f"{depth}Executing {task_name}", total=1)
|
455
|
-
|
456
|
-
try:
|
457
|
-
self.execute_from_graph(working_on, map_variable=map_variable, **kwargs)
|
458
|
-
status, next_node_name = self._get_status_and_next_node_name(
|
459
|
-
current_node=working_on, dag=dag, map_variable=map_variable
|
460
|
-
)
|
461
|
-
|
462
|
-
if status == defaults.SUCCESS:
|
463
|
-
self._context.progress.update(
|
464
|
-
task_execution,
|
465
|
-
description=f"{depth}[green] {task_name} Completed",
|
466
|
-
completed=True,
|
467
|
-
overflow="fold",
|
468
|
-
)
|
469
|
-
else:
|
470
|
-
self._context.progress.update(
|
471
|
-
task_execution, description=f"{depth}[red] {task_name} Failed", completed=True
|
472
|
-
) # type ignore
|
473
|
-
except Exception as e: # noqa: E722
|
474
|
-
self._context.progress.update(
|
475
|
-
task_execution,
|
476
|
-
description=f"{depth}[red] {task_name} Errored",
|
477
|
-
completed=True,
|
478
|
-
)
|
479
|
-
console.print(e, style=defaults.error_style)
|
480
|
-
logger.exception(e)
|
481
|
-
raise
|
482
|
-
|
483
|
-
console.rule(style="[dark orange]")
|
484
|
-
|
485
|
-
if working_on.node_type in ["success", "fail"]:
|
486
|
-
break
|
487
|
-
|
488
|
-
current_node = next_node_name
|
489
|
-
|
490
|
-
if branch_execution_task:
|
491
|
-
self._context.progress.update(
|
492
|
-
branch_execution_task, description=f"[green3] {branch_task_name} completed", completed=True
|
493
|
-
)
|
494
|
-
|
495
|
-
run_log = self._context.run_log_store.get_branch_log(
|
496
|
-
working_on._get_branch_log_name(map_variable), self._context.run_id
|
497
|
-
)
|
498
|
-
|
499
|
-
branch = "graph"
|
500
|
-
if working_on.internal_branch_name:
|
501
|
-
branch = working_on.internal_branch_name
|
502
|
-
|
503
|
-
logger.info(f"Finished execution of the {branch} with status {run_log.status}")
|
504
|
-
|
505
|
-
# We are in the root dag
|
506
|
-
if dag == self._context.dag:
|
507
|
-
run_log = cast(RunLog, run_log)
|
508
|
-
console.print("Completed Execution, Summary:", style="bold color(208)")
|
509
|
-
console.print(run_log.get_summary(), style=defaults.info_style)
|
510
|
-
|
511
|
-
def send_return_code(self, stage="traversal"):
|
512
|
-
"""
|
513
|
-
Convenience function used by pipeline to send return code to the caller of the cli
|
514
|
-
|
515
|
-
Raises:
|
516
|
-
Exception: If the pipeline execution failed
|
517
|
-
"""
|
518
|
-
run_id = self._context.run_id
|
519
|
-
|
520
|
-
run_log = self._context.run_log_store.get_run_log_by_id(run_id=run_id, full=False)
|
521
|
-
if run_log.status == defaults.FAIL:
|
522
|
-
raise exceptions.ExecutionFailedError(run_id=run_id)
|
523
|
-
|
524
|
-
def _resolve_executor_config(self, node: BaseNode):
|
525
|
-
"""
|
526
|
-
The overrides section can contain specific over-rides to an global executor config.
|
527
|
-
To avoid too much clutter in the dag definition, we allow the configuration file to have overrides block.
|
528
|
-
The nodes can over-ride the global config by referring to key in the overrides.
|
529
|
-
|
530
|
-
This function also applies variables to the effective node config.
|
531
|
-
|
532
|
-
For example:
|
533
|
-
# configuration.yaml
|
534
|
-
execution:
|
535
|
-
type: cloud-implementation
|
536
|
-
config:
|
537
|
-
k1: v1
|
538
|
-
k3: v3
|
539
|
-
overrides:
|
540
|
-
custom_config:
|
541
|
-
k1: v11
|
542
|
-
k2: v2 # Could be a mapping internally.
|
543
|
-
|
544
|
-
# in pipeline definition.yaml
|
545
|
-
dag:
|
546
|
-
steps:
|
547
|
-
step1:
|
548
|
-
overrides:
|
549
|
-
cloud-implementation: custom_config
|
550
|
-
|
551
|
-
This method should resolve the node_config to {'k1': 'v11', 'k2': 'v2', 'k3': 'v3'}
|
552
|
-
|
553
|
-
Args:
|
554
|
-
node (BaseNode): The current node being processed.
|
555
|
-
|
556
|
-
"""
|
557
|
-
effective_node_config = copy.deepcopy(self.model_dump())
|
558
|
-
try:
|
559
|
-
ctx_node_config = node._get_executor_config(self.service_name)
|
560
|
-
except exceptions.TerminalNodeError:
|
561
|
-
# Some modes request for effective node config even for success or fail nodes
|
562
|
-
return utils.apply_variables(effective_node_config, self._context.variables)
|
563
|
-
|
564
|
-
if ctx_node_config:
|
565
|
-
if ctx_node_config not in self.overrides:
|
566
|
-
raise Exception(f"No override of key: {ctx_node_config} found in the overrides section")
|
567
|
-
|
568
|
-
effective_node_config.update(self.overrides[ctx_node_config])
|
569
|
-
|
570
|
-
effective_node_config = utils.apply_variables(effective_node_config, self._context.variables)
|
571
|
-
logger.debug(f"Effective node config: {effective_node_config}")
|
572
|
-
|
573
|
-
return effective_node_config
|
574
|
-
|
575
|
-
@abstractmethod
|
576
|
-
def execute_job(self, node: TaskNode):
|
577
|
-
"""
|
578
|
-
Executor specific way of executing a job (python function or a notebook).
|
579
|
-
|
580
|
-
Interactive executors should execute the job.
|
581
|
-
Transpilers should write the instructions.
|
582
|
-
|
583
|
-
Args:
|
584
|
-
node (BaseNode): The job node to execute
|
585
|
-
|
586
|
-
Raises:
|
587
|
-
NotImplementedError: Executors should choose to extend this functionality or not.
|
588
|
-
"""
|
589
|
-
raise NotImplementedError
|
590
|
-
|
591
|
-
def fan_out(self, node: BaseNode, map_variable: TypeMapVariable = None):
|
592
|
-
"""
|
593
|
-
This method is used to appropriately fan-out the execution of a composite node.
|
594
|
-
This is only useful when we want to execute a composite node during 3rd party orchestrators.
|
595
|
-
|
596
|
-
Reason: Transpilers typically try to run the leaf nodes but do not have any capacity to do anything for the
|
597
|
-
step which is composite. By calling this fan-out before calling the leaf nodes, we have an opportunity to
|
598
|
-
do the right set up (creating the step log, exposing the parameters, etc.) for the composite step.
|
599
|
-
|
600
|
-
All 3rd party orchestrators should use this method to fan-out the execution of a composite node.
|
601
|
-
This ensures:
|
602
|
-
- The dot path notation is preserved, this method should create the step and call the node's fan out to
|
603
|
-
create the branch logs and let the 3rd party do the actual step execution.
|
604
|
-
- Gives 3rd party orchestrators an opportunity to set out the required for running a composite node.
|
605
|
-
|
606
|
-
Args:
|
607
|
-
node (BaseNode): The node to fan-out
|
608
|
-
map_variable (dict, optional): If the node if of a map state,.Defaults to None.
|
609
|
-
|
610
|
-
"""
|
611
|
-
step_log = self._context.run_log_store.create_step_log(
|
612
|
-
node.name, node._get_step_log_name(map_variable=map_variable)
|
613
|
-
)
|
614
|
-
|
615
|
-
self.add_code_identities(node=node, step_log=step_log)
|
616
|
-
|
617
|
-
step_log.step_type = node.node_type
|
618
|
-
step_log.status = defaults.PROCESSING
|
619
|
-
self._context.run_log_store.add_step_log(step_log, self._context.run_id)
|
620
|
-
|
621
|
-
node.fan_out(executor=self, map_variable=map_variable)
|
622
|
-
|
623
|
-
def fan_in(self, node: BaseNode, map_variable: TypeMapVariable = None):
|
624
|
-
"""
|
625
|
-
This method is used to appropriately fan-in after the execution of a composite node.
|
626
|
-
This is only useful when we want to execute a composite node during 3rd party orchestrators.
|
627
|
-
|
628
|
-
Reason: Transpilers typically try to run the leaf nodes but do not have any capacity to do anything for the
|
629
|
-
step which is composite. By calling this fan-in after calling the leaf nodes, we have an opportunity to
|
630
|
-
act depending upon the status of the individual branches.
|
631
|
-
|
632
|
-
All 3rd party orchestrators should use this method to fan-in the execution of a composite node.
|
633
|
-
This ensures:
|
634
|
-
- Gives the renderer's the control on where to go depending upon the state of the composite node.
|
635
|
-
- The status of the step and its underlying branches are correctly updated.
|
636
|
-
|
637
|
-
Args:
|
638
|
-
node (BaseNode): The node to fan-in
|
639
|
-
map_variable (dict, optional): If the node if of a map state,.Defaults to None.
|
640
|
-
|
641
|
-
"""
|
642
|
-
node.fan_in(executor=self, map_variable=map_variable)
|
643
|
-
|
644
|
-
step_log = self._context.run_log_store.get_step_log(
|
645
|
-
node._get_step_log_name(map_variable=map_variable), self._context.run_id
|
646
|
-
)
|
647
|
-
|
648
|
-
if step_log.status == defaults.FAIL:
|
649
|
-
raise Exception(f"Step {node.name} failed")
|
File without changes
|