runnable 0.1.0__py3-none-any.whl → 0.3.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.
Files changed (71) hide show
  1. runnable/__init__.py +34 -0
  2. runnable/catalog.py +141 -0
  3. runnable/cli.py +272 -0
  4. runnable/context.py +34 -0
  5. runnable/datastore.py +687 -0
  6. runnable/defaults.py +182 -0
  7. runnable/entrypoints.py +448 -0
  8. runnable/exceptions.py +94 -0
  9. runnable/executor.py +421 -0
  10. runnable/experiment_tracker.py +139 -0
  11. runnable/extensions/catalog/__init__.py +21 -0
  12. runnable/extensions/catalog/file_system/__init__.py +0 -0
  13. runnable/extensions/catalog/file_system/implementation.py +227 -0
  14. runnable/extensions/catalog/k8s_pvc/__init__.py +0 -0
  15. runnable/extensions/catalog/k8s_pvc/implementation.py +16 -0
  16. runnable/extensions/catalog/k8s_pvc/integration.py +59 -0
  17. runnable/extensions/executor/__init__.py +725 -0
  18. runnable/extensions/executor/argo/__init__.py +0 -0
  19. runnable/extensions/executor/argo/implementation.py +1183 -0
  20. runnable/extensions/executor/argo/specification.yaml +51 -0
  21. runnable/extensions/executor/k8s_job/__init__.py +0 -0
  22. runnable/extensions/executor/k8s_job/implementation_FF.py +259 -0
  23. runnable/extensions/executor/k8s_job/integration_FF.py +69 -0
  24. runnable/extensions/executor/local/__init__.py +0 -0
  25. runnable/extensions/executor/local/implementation.py +70 -0
  26. runnable/extensions/executor/local_container/__init__.py +0 -0
  27. runnable/extensions/executor/local_container/implementation.py +361 -0
  28. runnable/extensions/executor/mocked/__init__.py +0 -0
  29. runnable/extensions/executor/mocked/implementation.py +189 -0
  30. runnable/extensions/experiment_tracker/__init__.py +0 -0
  31. runnable/extensions/experiment_tracker/mlflow/__init__.py +0 -0
  32. runnable/extensions/experiment_tracker/mlflow/implementation.py +94 -0
  33. runnable/extensions/nodes.py +655 -0
  34. runnable/extensions/run_log_store/__init__.py +0 -0
  35. runnable/extensions/run_log_store/chunked_file_system/__init__.py +0 -0
  36. runnable/extensions/run_log_store/chunked_file_system/implementation.py +106 -0
  37. runnable/extensions/run_log_store/chunked_k8s_pvc/__init__.py +0 -0
  38. runnable/extensions/run_log_store/chunked_k8s_pvc/implementation.py +21 -0
  39. runnable/extensions/run_log_store/chunked_k8s_pvc/integration.py +61 -0
  40. runnable/extensions/run_log_store/db/implementation_FF.py +157 -0
  41. runnable/extensions/run_log_store/db/integration_FF.py +0 -0
  42. runnable/extensions/run_log_store/file_system/__init__.py +0 -0
  43. runnable/extensions/run_log_store/file_system/implementation.py +136 -0
  44. runnable/extensions/run_log_store/generic_chunked.py +541 -0
  45. runnable/extensions/run_log_store/k8s_pvc/__init__.py +0 -0
  46. runnable/extensions/run_log_store/k8s_pvc/implementation.py +21 -0
  47. runnable/extensions/run_log_store/k8s_pvc/integration.py +56 -0
  48. runnable/extensions/secrets/__init__.py +0 -0
  49. runnable/extensions/secrets/dotenv/__init__.py +0 -0
  50. runnable/extensions/secrets/dotenv/implementation.py +100 -0
  51. runnable/extensions/secrets/env_secrets/__init__.py +0 -0
  52. runnable/extensions/secrets/env_secrets/implementation.py +42 -0
  53. runnable/graph.py +464 -0
  54. runnable/integration.py +205 -0
  55. runnable/interaction.py +404 -0
  56. runnable/names.py +546 -0
  57. runnable/nodes.py +501 -0
  58. runnable/parameters.py +183 -0
  59. runnable/pickler.py +102 -0
  60. runnable/sdk.py +472 -0
  61. runnable/secrets.py +95 -0
  62. runnable/tasks.py +395 -0
  63. runnable/utils.py +630 -0
  64. runnable-0.3.0.dist-info/METADATA +437 -0
  65. runnable-0.3.0.dist-info/RECORD +69 -0
  66. {runnable-0.1.0.dist-info → runnable-0.3.0.dist-info}/WHEEL +1 -1
  67. runnable-0.3.0.dist-info/entry_points.txt +44 -0
  68. runnable-0.1.0.dist-info/METADATA +0 -16
  69. runnable-0.1.0.dist-info/RECORD +0 -6
  70. /runnable/{.gitkeep → extensions/__init__.py} +0 -0
  71. {runnable-0.1.0.dist-info → runnable-0.3.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,655 @@
1
+ import copy
2
+ import logging
3
+ from collections import OrderedDict
4
+ from copy import deepcopy
5
+ from datetime import datetime
6
+ from typing import Any, Dict, Optional, cast
7
+
8
+ from pydantic import ConfigDict, Field, ValidationInfo, field_serializer, field_validator
9
+ from typing_extensions import Annotated
10
+
11
+ from runnable import defaults, utils
12
+ from runnable.datastore import StepAttempt
13
+ from runnable.defaults import TypeMapVariable
14
+ from runnable.graph import Graph, create_graph
15
+ from runnable.nodes import CompositeNode, ExecutableNode, TerminalNode
16
+ from runnable.tasks import BaseTaskType, create_task
17
+
18
+ logger = logging.getLogger(defaults.LOGGER_NAME)
19
+
20
+
21
+ class TaskNode(ExecutableNode):
22
+ """
23
+ A node of type Task.
24
+
25
+ This node does the actual function execution of the graph in all cases.
26
+ """
27
+
28
+ executable: BaseTaskType = Field(exclude=True)
29
+ node_type: str = Field(default="task", serialization_alias="type")
30
+
31
+ # It is technically not allowed as parse_from_config filters them.
32
+ # This is just to get the task level configuration to be present during serialization.
33
+ model_config = ConfigDict(extra="allow")
34
+
35
+ @classmethod
36
+ def parse_from_config(cls, config: Dict[str, Any]) -> "TaskNode":
37
+ # separate task config from node config
38
+ task_config = {k: v for k, v in config.items() if k not in TaskNode.model_fields.keys()}
39
+ node_config = {k: v for k, v in config.items() if k in TaskNode.model_fields.keys()}
40
+
41
+ task_config["node_name"] = config.get("name")
42
+
43
+ executable = create_task(task_config)
44
+ return cls(executable=executable, **node_config, **task_config)
45
+
46
+ def execute(
47
+ self,
48
+ mock=False,
49
+ params: Optional[Dict[str, Any]] = None,
50
+ map_variable: TypeMapVariable = None,
51
+ **kwargs,
52
+ ) -> StepAttempt:
53
+ """
54
+ All that we do in runnable is to come to this point where we actually execute the command.
55
+
56
+ Args:
57
+ executor (_type_): The executor class
58
+ mock (bool, optional): If we should just mock and not execute. Defaults to False.
59
+ map_variable (dict, optional): If the node is part of internal branch. Defaults to None.
60
+
61
+ Returns:
62
+ StepAttempt: The attempt object
63
+ """
64
+ print("Executing task:", self._context.executor._context_node)
65
+ # Here is where the juice is
66
+ attempt_log = self._context.run_log_store.create_attempt_log()
67
+ try:
68
+ attempt_log.start_time = str(datetime.now())
69
+ attempt_log.status = defaults.SUCCESS
70
+ attempt_log.input_parameters = copy.deepcopy(params)
71
+ if not mock:
72
+ # Do not run if we are mocking the execution, could be useful for caching and dry runs
73
+ output_parameters = self.executable.execute_command(map_variable=map_variable, params=params)
74
+ attempt_log.output_parameters = output_parameters
75
+ except Exception as _e: # pylint: disable=W0703
76
+ logger.exception("Task failed")
77
+ attempt_log.status = defaults.FAIL
78
+ attempt_log.message = str(_e)
79
+ finally:
80
+ attempt_log.end_time = str(datetime.now())
81
+ attempt_log.duration = utils.get_duration_between_datetime_strings(
82
+ attempt_log.start_time, attempt_log.end_time
83
+ )
84
+ return attempt_log
85
+
86
+
87
+ class FailNode(TerminalNode):
88
+ """
89
+ A leaf node of the graph that represents a failure node
90
+ """
91
+
92
+ node_type: str = Field(default="fail", serialization_alias="type")
93
+
94
+ @classmethod
95
+ def parse_from_config(cls, config: Dict[str, Any]) -> "FailNode":
96
+ return cast("FailNode", super().parse_from_config(config))
97
+
98
+ def execute(
99
+ self,
100
+ mock=False,
101
+ params: Optional[Dict[str, Any]] = None,
102
+ map_variable: TypeMapVariable = None,
103
+ **kwargs,
104
+ ) -> StepAttempt:
105
+ """
106
+ Execute the failure node.
107
+ Set the run or branch log status to failure.
108
+
109
+ Args:
110
+ executor (_type_): the executor class
111
+ mock (bool, optional): If we should just mock and not do the actual execution. Defaults to False.
112
+ map_variable (dict, optional): If the node belongs to internal branches. Defaults to None.
113
+
114
+ Returns:
115
+ StepAttempt: The step attempt object
116
+ """
117
+ attempt_log = self._context.run_log_store.create_attempt_log()
118
+ try:
119
+ attempt_log.start_time = str(datetime.now())
120
+ attempt_log.status = defaults.SUCCESS
121
+ attempt_log.input_parameters = params
122
+ #  could be a branch or run log
123
+ run_or_branch_log = self._context.run_log_store.get_branch_log(
124
+ self._get_branch_log_name(map_variable), self._context.run_id
125
+ )
126
+ run_or_branch_log.status = defaults.FAIL
127
+ self._context.run_log_store.add_branch_log(run_or_branch_log, self._context.run_id)
128
+ except BaseException: # pylint: disable=W0703
129
+ logger.exception("Fail node execution failed")
130
+ finally:
131
+ attempt_log.status = defaults.SUCCESS # This is a dummy node, so we ignore errors and mark SUCCESS
132
+ attempt_log.end_time = str(datetime.now())
133
+ attempt_log.duration = utils.get_duration_between_datetime_strings(
134
+ attempt_log.start_time, attempt_log.end_time
135
+ )
136
+ return attempt_log
137
+
138
+
139
+ class SuccessNode(TerminalNode):
140
+ """
141
+ A leaf node of the graph that represents a success node
142
+ """
143
+
144
+ node_type: str = Field(default="success", serialization_alias="type")
145
+
146
+ @classmethod
147
+ def parse_from_config(cls, config: Dict[str, Any]) -> "SuccessNode":
148
+ return cast("SuccessNode", super().parse_from_config(config))
149
+
150
+ def execute(
151
+ self,
152
+ mock=False,
153
+ params: Optional[Dict[str, Any]] = None,
154
+ map_variable: TypeMapVariable = None,
155
+ **kwargs,
156
+ ) -> StepAttempt:
157
+ """
158
+ Execute the success node.
159
+ Set the run or branch log status to success.
160
+
161
+ Args:
162
+ executor (_type_): The executor class
163
+ mock (bool, optional): If we should just mock and not perform anything. Defaults to False.
164
+ map_variable (dict, optional): If the node belongs to an internal branch. Defaults to None.
165
+
166
+ Returns:
167
+ StepAttempt: The step attempt object
168
+ """
169
+ attempt_log = self._context.run_log_store.create_attempt_log()
170
+ try:
171
+ attempt_log.start_time = str(datetime.now())
172
+ attempt_log.status = defaults.SUCCESS
173
+ attempt_log.input_parameters = params
174
+ #  could be a branch or run log
175
+ run_or_branch_log = self._context.run_log_store.get_branch_log(
176
+ self._get_branch_log_name(map_variable), self._context.run_id
177
+ )
178
+ run_or_branch_log.status = defaults.SUCCESS
179
+ self._context.run_log_store.add_branch_log(run_or_branch_log, self._context.run_id)
180
+ except BaseException: # pylint: disable=W0703
181
+ logger.exception("Success node execution failed")
182
+ finally:
183
+ attempt_log.status = defaults.SUCCESS # This is a dummy node and we make sure we mark it as success
184
+ attempt_log.end_time = str(datetime.now())
185
+ attempt_log.duration = utils.get_duration_between_datetime_strings(
186
+ attempt_log.start_time, attempt_log.end_time
187
+ )
188
+ return attempt_log
189
+
190
+
191
+ class ParallelNode(CompositeNode):
192
+ """
193
+ A composite node containing many graph objects within itself.
194
+
195
+ The structure is generally:
196
+ ParallelNode:
197
+ Branch A:
198
+ Sub graph definition
199
+ Branch B:
200
+ Sub graph definition
201
+ . . .
202
+
203
+ """
204
+
205
+ node_type: str = Field(default="parallel", serialization_alias="type")
206
+ branches: Dict[str, Graph]
207
+ is_composite: bool = Field(default=True, exclude=True)
208
+
209
+ @field_serializer("branches")
210
+ def ser_branches(self, branches: Dict[str, Graph]) -> Dict[str, Graph]:
211
+ ret: Dict[str, Graph] = {}
212
+
213
+ for branch_name, branch in branches.items():
214
+ ret[branch_name.split(".")[-1]] = branch
215
+
216
+ return ret
217
+
218
+ @classmethod
219
+ def parse_from_config(cls, config: Dict[str, Any]) -> "ParallelNode":
220
+ internal_name = cast(str, config.get("internal_name"))
221
+
222
+ config_branches = config.pop("branches", {})
223
+ branches = {}
224
+ for branch_name, branch_config in config_branches.items():
225
+ sub_graph = create_graph(
226
+ deepcopy(branch_config),
227
+ internal_branch_name=internal_name + "." + branch_name,
228
+ )
229
+ branches[internal_name + "." + branch_name] = sub_graph
230
+
231
+ if not branches:
232
+ raise Exception("A parallel node should have branches")
233
+ return cls(branches=branches, **config)
234
+
235
+ def _get_branch_by_name(self, branch_name: str) -> Graph:
236
+ if branch_name in self.branches:
237
+ return self.branches[branch_name]
238
+
239
+ raise Exception(f"Branch {branch_name} does not exist")
240
+
241
+ def fan_out(self, map_variable: TypeMapVariable = None, **kwargs):
242
+ """
243
+ The general fan out method for a node of type Parallel.
244
+ This method assumes that the step log has already been created.
245
+
246
+ 3rd party orchestrators should create the step log and use this method to create the branch logs.
247
+
248
+ Args:
249
+ executor (BaseExecutor): The executor class as defined by the config
250
+ map_variable (dict, optional): If the node is part of a map node. Defaults to None.
251
+ """
252
+ # Prepare the branch logs
253
+ for internal_branch_name, _ in self.branches.items():
254
+ effective_branch_name = self._resolve_map_placeholders(internal_branch_name, map_variable=map_variable)
255
+
256
+ branch_log = self._context.run_log_store.create_branch_log(effective_branch_name)
257
+ branch_log.status = defaults.PROCESSING
258
+ self._context.run_log_store.add_branch_log(branch_log, self._context.run_id)
259
+
260
+ def execute_as_graph(self, map_variable: TypeMapVariable = None, **kwargs):
261
+ """
262
+ This function does the actual execution of the sub-branches of the parallel node.
263
+
264
+ From a design perspective, this function should not be called if the execution is 3rd party orchestrated.
265
+
266
+ The modes that render the job specifications, do not need to interact with this node at all as they have their
267
+ own internal mechanisms of handing parallel states.
268
+ If they do not, you can find a way using as-is nodes as hack nodes.
269
+
270
+ The execution of a dag, could result in
271
+ * The dag being completely executed with a definite (fail, success) state in case of
272
+ local or local-container execution
273
+ * The dag being in a processing state with PROCESSING status in case of local-aws-batch
274
+
275
+ Only fail state is considered failure during this phase of execution.
276
+
277
+ Args:
278
+ executor (Executor): The Executor as per the use config
279
+ **kwargs: Optional kwargs passed around
280
+ """
281
+
282
+ self.fan_out(map_variable=map_variable, **kwargs)
283
+
284
+ for _, branch in self.branches.items():
285
+ self._context.executor.execute_graph(branch, map_variable=map_variable, **kwargs)
286
+
287
+ self.fan_in(map_variable=map_variable, **kwargs)
288
+
289
+ def fan_in(self, map_variable: TypeMapVariable = None, **kwargs):
290
+ """
291
+ The general fan in method for a node of type Parallel.
292
+
293
+ 3rd party orchestrators should use this method to find the status of the composite step.
294
+
295
+ Args:
296
+ executor (BaseExecutor): The executor class as defined by the config
297
+ map_variable (dict, optional): If the node is part of a map. Defaults to None.
298
+ """
299
+ step_success_bool = True
300
+ for internal_branch_name, _ in self.branches.items():
301
+ effective_branch_name = self._resolve_map_placeholders(internal_branch_name, map_variable=map_variable)
302
+ branch_log = self._context.run_log_store.get_branch_log(effective_branch_name, self._context.run_id)
303
+ if branch_log.status != defaults.SUCCESS:
304
+ step_success_bool = False
305
+
306
+ # Collate all the results and update the status of the step
307
+ effective_internal_name = self._resolve_map_placeholders(self.internal_name, map_variable=map_variable)
308
+ step_log = self._context.run_log_store.get_step_log(effective_internal_name, self._context.run_id)
309
+
310
+ if step_success_bool: #  If none failed
311
+ step_log.status = defaults.SUCCESS
312
+ else:
313
+ step_log.status = defaults.FAIL
314
+
315
+ self._context.run_log_store.add_step_log(step_log, self._context.run_id)
316
+
317
+
318
+ class MapNode(CompositeNode):
319
+ """
320
+ A composite node that contains ONE graph object within itself that has to be executed with an iterable.
321
+
322
+ The structure is generally:
323
+ MapNode:
324
+ branch
325
+
326
+ The config is expected to have a variable 'iterate_on' and iterate_as which are looked for in the parameters.
327
+ for iter_variable in parameters['iterate_on']:
328
+ Execute the Branch by sending {'iterate_as': iter_variable}
329
+
330
+ The internal naming convention creates branches dynamically based on the iteration value
331
+ """
332
+
333
+ node_type: str = Field(default="map", serialization_alias="type")
334
+ iterate_on: str
335
+ iterate_as: str
336
+ branch: Graph
337
+ is_composite: bool = True
338
+
339
+ @classmethod
340
+ def parse_from_config(cls, config: Dict[str, Any]) -> "MapNode":
341
+ internal_name = cast(str, config.get("internal_name"))
342
+
343
+ config_branch = config.pop("branch", {})
344
+ if not config_branch:
345
+ raise Exception("A map node should have a branch")
346
+
347
+ branch = create_graph(
348
+ deepcopy(config_branch),
349
+ internal_branch_name=internal_name + "." + defaults.MAP_PLACEHOLDER,
350
+ )
351
+ return cls(branch=branch, **config)
352
+
353
+ def _get_branch_by_name(self, branch_name: str) -> Graph:
354
+ """
355
+ Retrieve a branch by name.
356
+
357
+ In the case of a Map Object, the branch naming is dynamic as it is parameterized on iterable.
358
+ This method takes no responsibility in checking the validity of the naming.
359
+
360
+ Returns a Graph Object
361
+
362
+ Args:
363
+ branch_name (str): The name of the branch to retrieve
364
+
365
+ Raises:
366
+ Exception: If the branch by that name does not exist
367
+ """
368
+ return self.branch
369
+
370
+ def fan_out(self, map_variable: TypeMapVariable = None, **kwargs):
371
+ """
372
+ The general method to fan out for a node of type map.
373
+ This method assumes that the step log has already been created.
374
+
375
+ 3rd party orchestrators should call this method to create the individual branch logs.
376
+
377
+ Args:
378
+ executor (BaseExecutor): The executor class as defined by the config
379
+ map_variable (dict, optional): If the node is part of map. Defaults to None.
380
+ """
381
+ iterate_on = self._context.run_log_store.get_parameters(self._context.run_id)[self.iterate_on]
382
+
383
+ # Prepare the branch logs
384
+ for iter_variable in iterate_on:
385
+ effective_branch_name = self._resolve_map_placeholders(
386
+ self.internal_name + "." + str(iter_variable), map_variable=map_variable
387
+ )
388
+ branch_log = self._context.run_log_store.create_branch_log(effective_branch_name)
389
+ branch_log.status = defaults.PROCESSING
390
+ self._context.run_log_store.add_branch_log(branch_log, self._context.run_id)
391
+
392
+ def execute_as_graph(self, map_variable: TypeMapVariable = None, **kwargs):
393
+ """
394
+ This function does the actual execution of the branch of the map node.
395
+
396
+ From a design perspective, this function should not be called if the execution is 3rd party orchestrated.
397
+
398
+ The modes that render the job specifications, do not need to interact with this node at all as
399
+ they have their own internal mechanisms of handing map states or dynamic parallel states.
400
+ If they do not, you can find a way using as-is nodes as hack nodes.
401
+
402
+ The actual logic is :
403
+ * We iterate over the iterable as mentioned in the config
404
+ * For every value in the iterable we call the executor.execute_graph(branch, iterate_as: iter_variable)
405
+
406
+ The execution of a dag, could result in
407
+ * The dag being completely executed with a definite (fail, success) state in case of local
408
+ or local-container execution
409
+ * The dag being in a processing state with PROCESSING status in case of local-aws-batch
410
+
411
+ Only fail state is considered failure during this phase of execution.
412
+
413
+ Args:
414
+ executor (Executor): The Executor as per the use config
415
+ map_variable (dict): The map variables the graph belongs to
416
+ **kwargs: Optional kwargs passed around
417
+ """
418
+
419
+ iterate_on = None
420
+ try:
421
+ iterate_on = self._context.run_log_store.get_parameters(self._context.run_id)[self.iterate_on]
422
+ except KeyError:
423
+ raise Exception(
424
+ f"Expected parameter {self.iterate_on} not present in Run Log parameters, was it ever set before?"
425
+ )
426
+
427
+ if not isinstance(iterate_on, list):
428
+ raise Exception("Only list is allowed as a valid iterator type")
429
+
430
+ self.fan_out(map_variable=map_variable, **kwargs)
431
+
432
+ for iter_variable in iterate_on:
433
+ effective_map_variable = map_variable or OrderedDict()
434
+ effective_map_variable[self.iterate_as] = iter_variable
435
+
436
+ self._context.executor.execute_graph(self.branch, map_variable=effective_map_variable, **kwargs)
437
+
438
+ self.fan_in(map_variable=map_variable, **kwargs)
439
+
440
+ def fan_in(self, map_variable: TypeMapVariable = None, **kwargs):
441
+ """
442
+ The general method to fan in for a node of type map.
443
+
444
+ 3rd party orchestrators should call this method to find the status of the step log.
445
+
446
+ Args:
447
+ executor (BaseExecutor): The executor class as defined by the config
448
+ map_variable (dict, optional): If the node is part of map node. Defaults to None.
449
+ """
450
+ iterate_on = self._context.run_log_store.get_parameters(self._context.run_id)[self.iterate_on]
451
+ # # Find status of the branches
452
+ step_success_bool = True
453
+
454
+ for iter_variable in iterate_on:
455
+ effective_branch_name = self._resolve_map_placeholders(
456
+ self.internal_name + "." + str(iter_variable), map_variable=map_variable
457
+ )
458
+ branch_log = self._context.run_log_store.get_branch_log(effective_branch_name, self._context.run_id)
459
+ if branch_log.status != defaults.SUCCESS:
460
+ step_success_bool = False
461
+
462
+ # Collate all the results and update the status of the step
463
+ effective_internal_name = self._resolve_map_placeholders(self.internal_name, map_variable=map_variable)
464
+ step_log = self._context.run_log_store.get_step_log(effective_internal_name, self._context.run_id)
465
+
466
+ if step_success_bool: #  If none failed and nothing is waiting
467
+ step_log.status = defaults.SUCCESS
468
+ else:
469
+ step_log.status = defaults.FAIL
470
+
471
+ self._context.run_log_store.add_step_log(step_log, self._context.run_id)
472
+
473
+
474
+ class DagNode(CompositeNode):
475
+ """
476
+ A composite node that internally holds a dag.
477
+
478
+ The structure is generally:
479
+ DagNode:
480
+ dag_definition: A YAML file that holds the dag in 'dag' block
481
+
482
+ The config is expected to have a variable 'dag_definition'.
483
+ """
484
+
485
+ node_type: str = Field(default="dag", serialization_alias="type")
486
+ dag_definition: str
487
+ branch: Graph
488
+ is_composite: bool = True
489
+ internal_branch_name: Annotated[str, Field(validate_default=True)] = ""
490
+
491
+ @field_validator("internal_branch_name")
492
+ @classmethod
493
+ def validate_internal_branch_name(cls, internal_branch_name: str, info: ValidationInfo):
494
+ internal_name = info.data["internal_name"]
495
+ return internal_name + "." + defaults.DAG_BRANCH_NAME
496
+
497
+ @field_validator("dag_definition")
498
+ @classmethod
499
+ def validate_dag_definition(cls, value):
500
+ if not value.endswith(".yaml"): # TODO: Might have a problem with the SDK
501
+ raise ValueError("dag_definition must be a YAML file")
502
+ return value
503
+
504
+ @classmethod
505
+ def parse_from_config(cls, config: Dict[str, Any]) -> "DagNode":
506
+ internal_name = cast(str, config.get("internal_name"))
507
+
508
+ if "dag_definition" not in config:
509
+ raise Exception(f"No dag definition found in {config}")
510
+
511
+ dag_config = utils.load_yaml(config["dag_definition"])
512
+ if "dag" not in dag_config:
513
+ raise Exception("No DAG found in dag_definition, please provide it in dag block")
514
+
515
+ branch = create_graph(dag_config["dag"], internal_branch_name=internal_name + "." + defaults.DAG_BRANCH_NAME)
516
+
517
+ return cls(branch=branch, **config)
518
+
519
+ def _get_branch_by_name(self, branch_name: str):
520
+ """
521
+ Retrieve a branch by name.
522
+ The name is expected to follow a dot path convention.
523
+
524
+ Returns a Graph Object
525
+
526
+ Args:
527
+ branch_name (str): The name of the branch to retrieve
528
+
529
+ Raises:
530
+ Exception: If the branch_name is not 'dag'
531
+ """
532
+ if branch_name != self.internal_branch_name:
533
+ raise Exception(f"Node of type {self.node_type} only allows a branch of name {defaults.DAG_BRANCH_NAME}")
534
+
535
+ return self.branch
536
+
537
+ def fan_out(self, map_variable: TypeMapVariable = None, **kwargs):
538
+ """
539
+ The general method to fan out for a node of type dag.
540
+ The method assumes that the step log has already been created.
541
+
542
+ Args:
543
+ executor (BaseExecutor): The executor class as defined by the config
544
+ map_variable (dict, optional): _description_. Defaults to None.
545
+ """
546
+ effective_branch_name = self._resolve_map_placeholders(self.internal_branch_name, map_variable=map_variable)
547
+
548
+ branch_log = self._context.run_log_store.create_branch_log(effective_branch_name)
549
+ branch_log.status = defaults.PROCESSING
550
+ self._context.run_log_store.add_branch_log(branch_log, self._context.run_id)
551
+
552
+ def execute_as_graph(self, map_variable: TypeMapVariable = None, **kwargs):
553
+ """
554
+ This function does the actual execution of the branch of the dag node.
555
+
556
+ From a design perspective, this function should not be called if the execution is 3rd party orchestrated.
557
+
558
+ The modes that render the job specifications, do not need to interact with this node at all
559
+ as they have their own internal mechanisms of handling sub dags.
560
+ If they do not, you can find a way using as-is nodes as hack nodes.
561
+
562
+ The actual logic is :
563
+ * We just execute the branch as with any other composite nodes
564
+ * The branch name is called 'dag'
565
+
566
+ The execution of a dag, could result in
567
+ * The dag being completely executed with a definite (fail, success) state in case of
568
+ local or local-container execution
569
+ * The dag being in a processing state with PROCESSING status in case of local-aws-batch
570
+
571
+ Only fail state is considered failure during this phase of execution.
572
+
573
+ Args:
574
+ executor (Executor): The Executor as per the use config
575
+ **kwargs: Optional kwargs passed around
576
+ """
577
+ self.fan_out(map_variable=map_variable, **kwargs)
578
+ self._context.executor.execute_graph(self.branch, map_variable=map_variable, **kwargs)
579
+ self.fan_in(map_variable=map_variable, **kwargs)
580
+
581
+ def fan_in(self, map_variable: TypeMapVariable = None, **kwargs):
582
+ """
583
+ The general method to fan in for a node of type dag.
584
+
585
+ 3rd party orchestrators should call this method to find the status of the step log.
586
+
587
+ Args:
588
+ executor (BaseExecutor): The executor class as defined by the config
589
+ map_variable (dict, optional): If the node is part of type dag. Defaults to None.
590
+ """
591
+ step_success_bool = True
592
+ effective_branch_name = self._resolve_map_placeholders(self.internal_branch_name, map_variable=map_variable)
593
+ effective_internal_name = self._resolve_map_placeholders(self.internal_name, map_variable=map_variable)
594
+
595
+ branch_log = self._context.run_log_store.get_branch_log(effective_branch_name, self._context.run_id)
596
+ if branch_log.status != defaults.SUCCESS:
597
+ step_success_bool = False
598
+
599
+ step_log = self._context.run_log_store.get_step_log(effective_internal_name, self._context.run_id)
600
+ step_log.status = defaults.PROCESSING
601
+
602
+ if step_success_bool: #  If none failed and nothing is waiting
603
+ step_log.status = defaults.SUCCESS
604
+ else:
605
+ step_log.status = defaults.FAIL
606
+
607
+ self._context.run_log_store.add_step_log(step_log, self._context.run_id)
608
+
609
+
610
+ class StubNode(ExecutableNode):
611
+ """
612
+ Stub is a convenience design node.
613
+
614
+ It always returns success in the attempt log and does nothing.
615
+
616
+ This node is very similar to pass state in Step functions.
617
+
618
+ This node type could be handy when designing the pipeline and stubbing functions
619
+ """
620
+
621
+ node_type: str = Field(default="stub", serialization_alias="type")
622
+ model_config = ConfigDict(extra="allow")
623
+
624
+ @classmethod
625
+ def parse_from_config(cls, config: Dict[str, Any]) -> "StubNode":
626
+ return cls(**config)
627
+
628
+ def execute(
629
+ self,
630
+ mock=False,
631
+ params: Optional[Dict[str, Any]] = None,
632
+ map_variable: TypeMapVariable = None,
633
+ **kwargs,
634
+ ) -> StepAttempt:
635
+ """
636
+ Do Nothing node.
637
+ We just send an success attempt log back to the caller
638
+
639
+ Args:
640
+ executor ([type]): [description]
641
+ mock (bool, optional): [description]. Defaults to False.
642
+ map_variable (str, optional): [description]. Defaults to ''.
643
+
644
+ Returns:
645
+ [type]: [description]
646
+ """
647
+ attempt_log = self._context.run_log_store.create_attempt_log()
648
+ attempt_log.input_parameters = params
649
+
650
+ attempt_log.start_time = str(datetime.now())
651
+ attempt_log.status = defaults.SUCCESS # This is a dummy node and always will be success
652
+
653
+ attempt_log.end_time = str(datetime.now())
654
+ attempt_log.duration = utils.get_duration_between_datetime_strings(attempt_log.start_time, attempt_log.end_time)
655
+ return attempt_log
File without changes