runnable 0.1.0__py3-none-any.whl → 0.2.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 +686 -0
  6. runnable/defaults.py +179 -0
  7. runnable/entrypoints.py +484 -0
  8. runnable/exceptions.py +94 -0
  9. runnable/executor.py +431 -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 +226 -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 +714 -0
  18. runnable/extensions/executor/argo/__init__.py +0 -0
  19. runnable/extensions/executor/argo/implementation.py +1182 -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 +69 -0
  26. runnable/extensions/executor/local_container/__init__.py +0 -0
  27. runnable/extensions/executor/local_container/implementation.py +367 -0
  28. runnable/extensions/executor/mocked/__init__.py +0 -0
  29. runnable/extensions/executor/mocked/implementation.py +220 -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 +675 -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 +399 -0
  56. runnable/names.py +546 -0
  57. runnable/nodes.py +489 -0
  58. runnable/parameters.py +183 -0
  59. runnable/pickler.py +102 -0
  60. runnable/sdk.py +470 -0
  61. runnable/secrets.py +95 -0
  62. runnable/tasks.py +392 -0
  63. runnable/utils.py +630 -0
  64. runnable-0.2.0.dist-info/METADATA +437 -0
  65. runnable-0.2.0.dist-info/RECORD +69 -0
  66. runnable-0.2.0.dist-info/entry_points.txt +44 -0
  67. runnable-0.1.0.dist-info/METADATA +0 -16
  68. runnable-0.1.0.dist-info/RECORD +0 -6
  69. /runnable/{.gitkeep → extensions/__init__.py} +0 -0
  70. {runnable-0.1.0.dist-info → runnable-0.2.0.dist-info}/LICENSE +0 -0
  71. {runnable-0.1.0.dist-info → runnable-0.2.0.dist-info}/WHEEL +0 -0
runnable/pickler.py ADDED
@@ -0,0 +1,102 @@
1
+ import pickle
2
+ from abc import ABC, abstractmethod
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+ import runnable.context as context
8
+
9
+
10
+ class BasePickler(ABC, BaseModel):
11
+ """
12
+ The base class for all picklers.
13
+
14
+ We are still in the process of hardening the design of this class.
15
+ For now, we are just going to use pickle.
16
+ """
17
+
18
+ extension: str = ""
19
+ service_name: str = ""
20
+ service_type: str = "pickler"
21
+ model_config = ConfigDict(extra="forbid")
22
+
23
+ @property
24
+ def _context(self):
25
+ return context.run_context
26
+
27
+ @abstractmethod
28
+ def dump(self, data: Any, path: str):
29
+ """
30
+ Dump an object to the specified path.
31
+ The path is the full path.
32
+
33
+ To correctly identify the pickler from possible implementations, we use the extension.
34
+ An extension is added automatically, if not provided.
35
+
36
+ Args:
37
+ data (Any): The object to pickle
38
+ path (str): The path to save the pickle file
39
+
40
+ Raises:
41
+ NotImplementedError: Base class has no implementation
42
+ """
43
+ raise NotImplementedError
44
+
45
+ @abstractmethod
46
+ def load(self, path: str) -> Any:
47
+ """
48
+ Load the object from the specified path.
49
+
50
+ To correctly identify the pickler from possible implementations, we use the extension.
51
+ An extension is added automatically, if not provided.
52
+
53
+ Args:
54
+ path (str): The path to load the pickled file from.
55
+
56
+ Raises:
57
+ NotImplementedError: Base class has no implementation.
58
+ """
59
+ raise NotImplementedError
60
+
61
+
62
+ class NativePickler(BasePickler):
63
+ """
64
+ Uses native python pickle to load and dump files
65
+ """
66
+
67
+ extension: str = ".pickle"
68
+ service_name: str = "pickle"
69
+
70
+ def dump(self, data: Any, path: str):
71
+ """
72
+ Dump an object to the specified path.
73
+ The path is the full path.
74
+
75
+ Args:
76
+ data (Any): The data to pickle
77
+ path (str): The path to save the pickle file
78
+ """
79
+ if not path.endswith(self.extension):
80
+ path = path + self.extension
81
+
82
+ with open(path, "wb") as f:
83
+ pickle.dump(data, f, pickle.HIGHEST_PROTOCOL)
84
+
85
+ def load(self, path: str) -> Any:
86
+ """
87
+ Load the object from the specified path.
88
+
89
+ Args:
90
+ path (str): The path to load the object from.
91
+
92
+ Returns:
93
+ Any: The data loaded from the file.
94
+ """
95
+ if not path.endswith(self.extension):
96
+ path = path + self.extension
97
+
98
+ data = None
99
+ with open(path, "rb") as f:
100
+ data = pickle.load(f)
101
+
102
+ return data
runnable/sdk.py ADDED
@@ -0,0 +1,470 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any, Dict, List, Optional, Union
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, computed_field, field_validator, model_validator
9
+ from rich import print
10
+ from ruamel.yaml import YAML
11
+ from typing_extensions import Self
12
+
13
+ from runnable import defaults, entrypoints, graph, utils
14
+ from runnable.extensions.nodes import FailNode, MapNode, ParallelNode, StubNode, SuccessNode, TaskNode
15
+ from runnable.nodes import TraversalNode
16
+
17
+ logger = logging.getLogger(defaults.LOGGER_NAME)
18
+
19
+ StepType = Union["Stub", "Task", "Success", "Fail", "Parallel", "Map"]
20
+ TraversalTypes = Union["Stub", "Task", "Parallel", "Map"]
21
+
22
+
23
+ ALLOWED_COMMAND_TYPES = ["shell", "python", "notebook"]
24
+
25
+
26
+ class Catalog(BaseModel):
27
+ """
28
+ Use to instruct a task to sync data from/to the central catalog.
29
+ Please refer to [concepts](concepts/catalog.md) for more information.
30
+
31
+ Attributes:
32
+ get (List[str]): List of glob patterns to get from central catalog to the compute data folder.
33
+ put (List[str]): List of glob patterns to put into central catalog from the compute data folder.
34
+
35
+ Examples:
36
+ >>> from magnus import Catalog, Task
37
+ >>> catalog = Catalog(compute_data_folder="/path/to/data", get=["*.csv"], put=["*.csv"])
38
+
39
+ >>> task = Task(name="task", catalog=catalog, command="echo 'hello'")
40
+
41
+ """
42
+
43
+ model_config = ConfigDict(extra="forbid") # Need to be for command, would be validated later
44
+ # Note: compute_data_folder was confusing to explain, might be introduced later.
45
+ # compute_data_folder: str = Field(default="", alias="compute_data_folder")
46
+ get: List[str] = Field(default_factory=list, alias="get")
47
+ put: List[str] = Field(default_factory=list, alias="put")
48
+
49
+
50
+ class BaseTraversal(ABC, BaseModel):
51
+ name: str
52
+ next_node: str = Field(default="", alias="next")
53
+ terminate_with_success: bool = Field(default=False, exclude=True)
54
+ terminate_with_failure: bool = Field(default=False, exclude=True)
55
+ on_failure: str = Field(default="", alias="on_failure")
56
+
57
+ model_config = ConfigDict(extra="forbid")
58
+
59
+ @computed_field # type: ignore
60
+ @property
61
+ def internal_name(self) -> str:
62
+ return self.name
63
+
64
+ def __rshift__(self, other: StepType) -> StepType:
65
+ if self.next_node:
66
+ raise Exception(f"The node {self} already has a next node: {self.next_node}")
67
+ self.next_node = other.name
68
+
69
+ return other
70
+
71
+ def __lshift__(self, other: TraversalNode) -> TraversalNode:
72
+ if other.next_node:
73
+ raise Exception(f"The {other} node already has a next node: {other.next_node}")
74
+ other.next_node = self.name
75
+
76
+ return other
77
+
78
+ def depends_on(self, node: StepType) -> Self:
79
+ assert not isinstance(node, Success)
80
+ assert not isinstance(node, Fail)
81
+
82
+ if node.next_node:
83
+ raise Exception(f"The {node} node already has a next node: {node.next_node}")
84
+
85
+ node.next_node = self.name
86
+ return self
87
+
88
+ @model_validator(mode="after")
89
+ def validate_terminations(self) -> Self:
90
+ if self.terminate_with_failure and self.terminate_with_success:
91
+ raise AssertionError("A node cannot terminate with success and failure")
92
+
93
+ if self.terminate_with_failure or self.terminate_with_success:
94
+ if self.next_node and self.next_node not in ["success", "fail"]:
95
+ raise AssertionError("A node being terminated cannot have a user defined next node")
96
+
97
+ if self.terminate_with_failure:
98
+ self.next_node = "fail"
99
+
100
+ if self.terminate_with_success:
101
+ self.next_node = "success"
102
+
103
+ return self
104
+
105
+ @abstractmethod
106
+ def create_node(self) -> TraversalNode:
107
+ ...
108
+
109
+
110
+ class Task(BaseTraversal):
111
+ """
112
+ An execution node of the pipeline.
113
+ Please refer to [concepts](concepts/task.md) for more information.
114
+
115
+ Attributes:
116
+ name (str): The name of the node.
117
+ command (str): The command to execute.
118
+
119
+ - For python functions, [dotted path](concepts/task.md/#python_functions) to the function.
120
+ - For shell commands: command to execute in the shell.
121
+ - For notebooks: path to the notebook.
122
+ command_type (str): The type of command to execute.
123
+ Can be one of "shell", "python", or "notebook".
124
+ catalog (Optional[Catalog]): The catalog to sync data from/to.
125
+ Please see Catalog about the structure of the catalog.
126
+ overrides (Dict[str, Any]): Any overrides to the command.
127
+ Individual tasks can override the global configuration config by referring to the
128
+ specific override.
129
+
130
+ For example,
131
+ ### Global configuration
132
+ ```yaml
133
+ executor:
134
+ type: local-container
135
+ config:
136
+ docker_image: "magnus/magnus:latest"
137
+ overrides:
138
+ custom_docker_image:
139
+ docker_image: "magnus/magnus:custom"
140
+ ```
141
+ ### Task specific configuration
142
+ ```python
143
+ task = Task(name="task", command="echo 'hello'", command_type="shell",
144
+ overrides={'local-container': custom_docker_image})
145
+ ```
146
+ notebook_output_path (Optional[str]): The path to save the notebook output.
147
+ Only used when command_type is 'notebook', defaults to command+_out.ipynb
148
+ optional_ploomber_args (Optional[Dict[str, Any]]): Any optional ploomber args.
149
+ Only used when command_type is 'notebook', defaults to {}
150
+ output_cell_tag (Optional[str]): The tag of the output cell.
151
+ Only used when command_type is 'notebook', defaults to "magnus_output"
152
+ terminate_with_failure (bool): Whether to terminate the pipeline with a failure after this node.
153
+ terminate_with_success (bool): Whether to terminate the pipeline with a success after this node.
154
+ on_failure (str): The name of the node to execute if the step fails.
155
+
156
+ """
157
+
158
+ command: str = Field(alias="command")
159
+ command_type: str = Field(default="python")
160
+ catalog: Optional[Catalog] = Field(default=None, alias="catalog")
161
+ overrides: Dict[str, Any] = Field(default_factory=dict, alias="overrides")
162
+
163
+ notebook_output_path: Optional[str] = Field(default=None, alias="notebook_output_path")
164
+ optional_ploomber_args: Optional[Dict[str, Any]] = Field(default=None, alias="optional_ploomber_args")
165
+ output_cell_tag: Optional[str] = Field(default=None, alias="output_cell_tag")
166
+
167
+ @field_validator("command_type", mode="before")
168
+ @classmethod
169
+ def validate_command_type(cls, value: str) -> str:
170
+ if value not in ALLOWED_COMMAND_TYPES:
171
+ raise ValueError(f"Invalid command_type: {value}")
172
+ return value
173
+
174
+ @model_validator(mode="after")
175
+ def check_notebook_args(self) -> "Task":
176
+ if self.command_type != "notebook":
177
+ assert (
178
+ self.notebook_output_path is None
179
+ ), "Only command_types of 'notebook' can be used with notebook_output_path"
180
+
181
+ assert (
182
+ self.optional_ploomber_args is None
183
+ ), "Only command_types of 'notebook' can be used with optional_ploomber_args"
184
+
185
+ assert self.output_cell_tag is None, "Only command_types of 'notebook' can be used with output_cell_tag"
186
+ return self
187
+
188
+ def create_node(self) -> TaskNode:
189
+ if not self.next_node:
190
+ if not (self.terminate_with_failure or self.terminate_with_success):
191
+ raise AssertionError("A node not being terminated must have a user defined next node")
192
+ return TaskNode.parse_from_config(self.model_dump(exclude_none=True))
193
+
194
+
195
+ class Stub(BaseTraversal):
196
+ """
197
+ A node that does nothing.
198
+
199
+ A stub node can tak arbitrary number of arguments.
200
+ Please refer to [concepts](concepts/stub.md) for more information.
201
+
202
+ Attributes:
203
+ name (str): The name of the node.
204
+ terminate_with_failure (bool): Whether to terminate the pipeline with a failure after this node.
205
+ terminate_with_success (bool): Whether to terminate the pipeline with a success after this node.
206
+
207
+ """
208
+
209
+ model_config = ConfigDict(extra="allow")
210
+ catalog: Optional[Catalog] = Field(default=None, alias="catalog")
211
+
212
+ def create_node(self) -> StubNode:
213
+ if not self.next_node:
214
+ if not (self.terminate_with_failure or self.terminate_with_success):
215
+ raise AssertionError("A node not being terminated must have a user defined next node")
216
+
217
+ return StubNode.parse_from_config(self.model_dump(exclude_none=True))
218
+
219
+
220
+ class Parallel(BaseTraversal):
221
+ """
222
+ A node that executes multiple branches in parallel.
223
+ Please refer to [concepts](concepts/parallel.md) for more information.
224
+
225
+ Attributes:
226
+ name (str): The name of the node.
227
+ branches (Dict[str, Pipeline]): A dictionary of branches to execute in parallel.
228
+ terminate_with_failure (bool): Whether to terminate the pipeline with a failure after this node.
229
+ terminate_with_success (bool): Whether to terminate the pipeline with a success after this node.
230
+ on_failure (str): The name of the node to execute if any of the branches fail.
231
+ """
232
+
233
+ branches: Dict[str, "Pipeline"]
234
+
235
+ @computed_field # type: ignore
236
+ @property
237
+ def graph_branches(self) -> Dict[str, graph.Graph]:
238
+ return {name: pipeline._dag.model_copy() for name, pipeline in self.branches.items()}
239
+
240
+ def create_node(self) -> ParallelNode:
241
+ if not self.next_node:
242
+ if not (self.terminate_with_failure or self.terminate_with_success):
243
+ raise AssertionError("A node not being terminated must have a user defined next node")
244
+
245
+ node = ParallelNode(name=self.name, branches=self.graph_branches, internal_name="", next_node=self.next_node)
246
+ return node
247
+
248
+
249
+ class Map(BaseTraversal):
250
+ """
251
+ A node that iterates over a list of items and executes a pipeline for each item.
252
+ Please refer to [concepts](concepts/map.md) for more information.
253
+
254
+ Attributes:
255
+ branch: The pipeline to execute for each item.
256
+
257
+ iterate_on: The name of the parameter to iterate over.
258
+ The parameter should be defined either by previous steps or statically at the start of execution.
259
+
260
+ iterate_as: The name of the iterable to be passed to functions.
261
+
262
+
263
+ overrides (Dict[str, Any]): Any overrides to the command.
264
+
265
+ """
266
+
267
+ branch: "Pipeline"
268
+ iterate_on: str
269
+ iterate_as: str
270
+ overrides: Dict[str, Any] = Field(default_factory=dict)
271
+
272
+ @computed_field # type: ignore
273
+ @property
274
+ def graph_branch(self) -> graph.Graph:
275
+ return self.branch._dag.model_copy()
276
+
277
+ def create_node(self) -> MapNode:
278
+ if not self.next_node:
279
+ if not (self.terminate_with_failure or self.terminate_with_success):
280
+ raise AssertionError("A node not being terminated must have a user defined next node")
281
+
282
+ node = MapNode(
283
+ name=self.name,
284
+ branch=self.graph_branch,
285
+ internal_name="",
286
+ next_node=self.next_node,
287
+ iterate_on=self.iterate_on,
288
+ iterate_as=self.iterate_as,
289
+ overrides=self.overrides,
290
+ )
291
+
292
+ return node
293
+
294
+
295
+ class Success(BaseModel):
296
+ """
297
+ A node that represents a successful execution of the pipeline.
298
+
299
+ Most often, there is no need to use this node as nodes can be instructed to
300
+ terminate_with_success and pipeline with add_terminal_nodes=True.
301
+
302
+ Attributes:
303
+ name (str): The name of the node.
304
+ """
305
+
306
+ name: str = "success"
307
+
308
+ @computed_field # type: ignore
309
+ @property
310
+ def internal_name(self) -> str:
311
+ return self.name
312
+
313
+ def create_node(self) -> SuccessNode:
314
+ return SuccessNode.parse_from_config(self.model_dump())
315
+
316
+
317
+ class Fail(BaseModel):
318
+ """
319
+ A node that represents a failed execution of the pipeline.
320
+
321
+ Most often, there is no need to use this node as nodes can be instructed to
322
+ terminate_with_failure and pipeline with add_terminal_nodes=True.
323
+
324
+ Attributes:
325
+ name (str): The name of the node.
326
+ """
327
+
328
+ name: str = "fail"
329
+
330
+ @computed_field # type: ignore
331
+ @property
332
+ def internal_name(self) -> str:
333
+ return self.name
334
+
335
+ def create_node(self) -> FailNode:
336
+ return FailNode.parse_from_config(self.model_dump())
337
+
338
+
339
+ class Pipeline(BaseModel):
340
+ """
341
+ A Pipeline is a directed acyclic graph of Steps that define a workflow.
342
+
343
+ Attributes:
344
+ steps (List[Stub | Task | Parallel | Map | Success | Fail]): A list of Steps that make up the Pipeline.
345
+ start_at (Stub | Task | Parallel | Map): The name of the first Step in the Pipeline.
346
+ name (str, optional): The name of the Pipeline. Defaults to "".
347
+ description (str, optional): A description of the Pipeline. Defaults to "".
348
+ add_terminal_nodes (bool, optional): Whether to add terminal nodes to the Pipeline. Defaults to True.
349
+
350
+ The default behavior is to add "success" and "fail" nodes to the Pipeline.
351
+ To add custom success and fail nodes, set add_terminal_nodes=False and create success
352
+ and fail nodes manually.
353
+
354
+ """
355
+
356
+ steps: List[StepType]
357
+ start_at: TraversalTypes
358
+ name: str = ""
359
+ description: str = ""
360
+ add_terminal_nodes: bool = True # Adds "success" and "fail" nodes
361
+
362
+ internal_branch_name: str = ""
363
+
364
+ _dag: graph.Graph = PrivateAttr()
365
+ model_config = ConfigDict(extra="forbid")
366
+
367
+ def model_post_init(self, __context: Any) -> None:
368
+ self.steps = [model.model_copy(deep=True) for model in self.steps]
369
+
370
+ self._dag = graph.Graph(
371
+ start_at=self.start_at.name,
372
+ description=self.description,
373
+ internal_branch_name=self.internal_branch_name,
374
+ )
375
+
376
+ for step in self.steps:
377
+ if step.name == self.start_at.name:
378
+ if isinstance(step, Success) or isinstance(step, Fail):
379
+ raise Exception("A success or fail node cannot be the start_at of the graph")
380
+ assert step.next_node
381
+ self._dag.add_node(step.create_node())
382
+
383
+ if self.add_terminal_nodes:
384
+ self._dag.add_terminal_nodes()
385
+
386
+ self._dag.check_graph()
387
+
388
+ def execute(
389
+ self,
390
+ configuration_file: str = "",
391
+ run_id: str = "",
392
+ tag: str = "",
393
+ parameters_file: str = "",
394
+ use_cached: str = "",
395
+ log_level: str = defaults.LOG_LEVEL,
396
+ output_pipeline_definition: str = "magnus-pipeline.yaml",
397
+ ):
398
+ """
399
+ *Execute* the Pipeline.
400
+
401
+ Execution of pipeline could either be:
402
+
403
+ Traverse and execute all the steps of the pipeline, eg. [local execution](configurations/executors/local.md).
404
+
405
+ Or create the ```yaml``` representation of the pipeline for other executors.
406
+
407
+ Please refer to [concepts](concepts/executor.md) for more information.
408
+
409
+ Args:
410
+ configuration_file (str, optional): The path to the configuration file. Defaults to "".
411
+ The configuration file can be overridden by the environment variable MAGNUS_CONFIGURATION_FILE.
412
+
413
+ run_id (str, optional): The ID of the run. Defaults to "".
414
+ tag (str, optional): The tag of the run. Defaults to "".
415
+ Use to group multiple runs.
416
+
417
+ parameters_file (str, optional): The path to the parameters file. Defaults to "".
418
+ use_cached (str, optional): Whether to use cached results. Defaults to "".
419
+ Provide the run_id of the older execution to recover.
420
+
421
+ log_level (str, optional): The log level. Defaults to defaults.LOG_LEVEL.
422
+ output_pipeline_definition (str, optional): The path to the output pipeline definition file.
423
+ Defaults to "magnus-pipeline.yaml".
424
+
425
+ Only applicable for the execution via SDK for non ```local``` executors.
426
+ """
427
+ from runnable.extensions.executor.local.implementation import LocalExecutor
428
+ from runnable.extensions.executor.mocked.implementation import MockedExecutor
429
+
430
+ logger.setLevel(log_level)
431
+
432
+ run_id = utils.generate_run_id(run_id=run_id)
433
+ configuration_file = os.environ.get("MAGNUS_CONFIGURATION_FILE", configuration_file)
434
+ run_context = entrypoints.prepare_configurations(
435
+ configuration_file=configuration_file,
436
+ run_id=run_id,
437
+ tag=tag,
438
+ parameters_file=parameters_file,
439
+ use_cached=use_cached,
440
+ )
441
+
442
+ run_context.execution_plan = defaults.EXECUTION_PLAN.CHAINED.value
443
+ utils.set_magnus_environment_variables(run_id=run_id, configuration_file=configuration_file, tag=tag)
444
+
445
+ dag_definition = self._dag.model_dump(by_alias=True, exclude_none=True)
446
+
447
+ run_context.dag = graph.create_graph(dag_definition)
448
+
449
+ print("Working with context:")
450
+ print(run_context)
451
+
452
+ if not (isinstance(run_context.executor, LocalExecutor) or isinstance(run_context.executor, MockedExecutor)):
453
+ logger.debug(run_context.dag.model_dump(by_alias=True))
454
+ yaml = YAML()
455
+
456
+ with open(output_pipeline_definition, "w", encoding="utf-8") as f:
457
+ yaml.dump(
458
+ {"dag": run_context.dag.model_dump(by_alias=True, exclude_none=True)},
459
+ f,
460
+ )
461
+
462
+ return
463
+
464
+ # Prepare for graph execution
465
+ run_context.executor.prepare_for_graph_execution()
466
+
467
+ logger.info("Executing the graph")
468
+ run_context.executor.execute_graph(dag=run_context.dag)
469
+
470
+ return run_context.run_log_store.get_run_log_by_id(run_id=run_context.run_id)
runnable/secrets.py ADDED
@@ -0,0 +1,95 @@
1
+ import logging
2
+ import os
3
+ from abc import ABC, abstractmethod
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+ import runnable.context as context
8
+ from runnable import defaults, exceptions
9
+
10
+ logger = logging.getLogger(defaults.LOGGER_NAME)
11
+
12
+
13
+ # --8<-- [start:docs]
14
+ class BaseSecrets(ABC, BaseModel):
15
+ """
16
+ A base class for Secrets Handler.
17
+ All implementations should extend this class.
18
+
19
+ Raises:
20
+ NotImplementedError: Base class and not implemented
21
+ """
22
+
23
+ service_name: str = ""
24
+ service_type: str = "secrets"
25
+ model_config = ConfigDict(extra="forbid")
26
+
27
+ @property
28
+ def _context(self):
29
+ return context.run_context
30
+
31
+ @abstractmethod
32
+ def get(self, name: str, **kwargs) -> str:
33
+ """
34
+ Return the secret by name.
35
+
36
+ Args:
37
+ name (str): The name of the secret to return.
38
+
39
+ Raises:
40
+ NotImplementedError: Base class and hence not implemented.
41
+ exceptions.SecretNotFoundError: Secret not found in the secrets manager.
42
+ """
43
+ raise NotImplementedError
44
+
45
+
46
+ # --8<-- [end:docs]
47
+
48
+
49
+ class DoNothingSecretManager(BaseSecrets):
50
+ """
51
+ Does nothing secret manager
52
+ """
53
+
54
+ service_name: str = "do-nothing"
55
+
56
+ def get(self, name: str, **kwargs) -> str:
57
+ """
58
+ If a name is provided, return None else return empty dict.
59
+
60
+ Args:
61
+ name (str): The name of the secret to retrieve
62
+
63
+ Raises:
64
+ exceptions.SecretNotFoundError: Secret not found in the secrets manager.
65
+
66
+ Returns:
67
+ [str]: The value of the secret
68
+ """
69
+ return ""
70
+
71
+
72
+ class EnvSecretsManager(BaseSecrets):
73
+ """
74
+ A secret manager which uses environment variables for secrets.
75
+ """
76
+
77
+ service_name: str = "env-secrets"
78
+
79
+ def get(self, name: str, **kwargs) -> str:
80
+ """
81
+ If a name is provided, return None else return empty dict.
82
+
83
+ Args:
84
+ name (str): The name of the secret to retrieve
85
+
86
+ Raises:
87
+ exceptions.SecretNotFoundError: Secret not found in the secrets manager.
88
+
89
+ Returns:
90
+ [str]: The value of the secret
91
+ """
92
+ try:
93
+ return os.environ[name]
94
+ except KeyError:
95
+ raise exceptions.SecretNotFoundError(secret_name=name, secret_setting="environment variables")