runnable 0.10.0__py3-none-any.whl → 0.11.1__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.
- runnable/__init__.py +16 -8
- runnable/catalog.py +8 -1
- runnable/cli.py +2 -0
- runnable/context.py +5 -3
- runnable/datastore.py +96 -12
- runnable/defaults.py +9 -9
- runnable/entrypoints.py +43 -24
- runnable/exceptions.py +4 -0
- runnable/extensions/catalog/file_system/implementation.py +8 -1
- runnable/extensions/executor/__init__.py +87 -29
- runnable/extensions/executor/argo/implementation.py +8 -4
- runnable/extensions/executor/local/implementation.py +1 -0
- runnable/extensions/nodes.py +90 -13
- runnable/extensions/run_log_store/chunked_file_system/implementation.py +6 -1
- runnable/extensions/run_log_store/file_system/implementation.py +6 -0
- runnable/graph.py +11 -0
- runnable/integration.py +4 -17
- runnable/nodes.py +9 -0
- runnable/parameters.py +3 -1
- runnable/sdk.py +65 -16
- runnable/tasks.py +135 -53
- runnable/utils.py +2 -1
- {runnable-0.10.0.dist-info → runnable-0.11.1.dist-info}/METADATA +2 -3
- {runnable-0.10.0.dist-info → runnable-0.11.1.dist-info}/RECORD +27 -31
- {runnable-0.10.0.dist-info → runnable-0.11.1.dist-info}/entry_points.txt +0 -4
- runnable/experiment_tracker.py +0 -139
- runnable/extensions/experiment_tracker/__init__.py +0 -0
- runnable/extensions/experiment_tracker/mlflow/__init__.py +0 -0
- runnable/extensions/experiment_tracker/mlflow/implementation.py +0 -94
- {runnable-0.10.0.dist-info → runnable-0.11.1.dist-info}/LICENSE +0 -0
- {runnable-0.10.0.dist-info → runnable-0.11.1.dist-info}/WHEEL +0 -0
runnable/sdk.py
CHANGED
@@ -15,10 +15,17 @@ from pydantic import (
|
|
15
15
|
field_validator,
|
16
16
|
model_validator,
|
17
17
|
)
|
18
|
-
from rich import
|
18
|
+
from rich.progress import (
|
19
|
+
BarColumn,
|
20
|
+
Progress,
|
21
|
+
SpinnerColumn,
|
22
|
+
TextColumn,
|
23
|
+
TimeElapsedColumn,
|
24
|
+
)
|
25
|
+
from rich.table import Column
|
19
26
|
from typing_extensions import Self
|
20
27
|
|
21
|
-
from runnable import defaults, entrypoints, graph, utils
|
28
|
+
from runnable import console, defaults, entrypoints, graph, utils
|
22
29
|
from runnable.extensions.nodes import (
|
23
30
|
FailNode,
|
24
31
|
MapNode,
|
@@ -39,6 +46,10 @@ def pickled(name: str) -> TaskReturns:
|
|
39
46
|
return TaskReturns(name=name, kind="object")
|
40
47
|
|
41
48
|
|
49
|
+
def metric(name: str) -> TaskReturns:
|
50
|
+
return TaskReturns(name=name, kind="metric")
|
51
|
+
|
52
|
+
|
42
53
|
class Catalog(BaseModel):
|
43
54
|
"""
|
44
55
|
Use to instruct a task to sync data from/to the central catalog.
|
@@ -65,7 +76,7 @@ class Catalog(BaseModel):
|
|
65
76
|
|
66
77
|
class BaseTraversal(ABC, BaseModel):
|
67
78
|
name: str
|
68
|
-
next_node: str = Field(default="",
|
79
|
+
next_node: str = Field(default="", serialization_alias="next_node")
|
69
80
|
terminate_with_success: bool = Field(default=False, exclude=True)
|
70
81
|
terminate_with_failure: bool = Field(default=False, exclude=True)
|
71
82
|
on_failure: str = Field(default="", alias="on_failure")
|
@@ -77,6 +88,12 @@ class BaseTraversal(ABC, BaseModel):
|
|
77
88
|
def internal_name(self) -> str:
|
78
89
|
return self.name
|
79
90
|
|
91
|
+
def __hash__(self):
|
92
|
+
"""
|
93
|
+
Needed to Uniqueize DataCatalog objects.
|
94
|
+
"""
|
95
|
+
return hash(self.name)
|
96
|
+
|
80
97
|
def __rshift__(self, other: StepType) -> StepType:
|
81
98
|
if self.next_node:
|
82
99
|
raise Exception(f"The node {self} already has a next node: {self.next_node}")
|
@@ -174,6 +191,7 @@ class BaseTask(BaseTraversal):
|
|
174
191
|
catalog: Optional[Catalog] = Field(default=None, alias="catalog")
|
175
192
|
overrides: Dict[str, Any] = Field(default_factory=dict, alias="overrides")
|
176
193
|
returns: List[Union[str, TaskReturns]] = Field(default_factory=list, alias="returns")
|
194
|
+
secrets: List[str] = Field(default_factory=list)
|
177
195
|
|
178
196
|
@field_validator("returns", mode="before")
|
179
197
|
@classmethod
|
@@ -195,7 +213,7 @@ class BaseTask(BaseTraversal):
|
|
195
213
|
if not (self.terminate_with_failure or self.terminate_with_success):
|
196
214
|
raise AssertionError("A node not being terminated must have a user defined next node")
|
197
215
|
|
198
|
-
return TaskNode.parse_from_config(self.model_dump(exclude_none=True))
|
216
|
+
return TaskNode.parse_from_config(self.model_dump(exclude_none=True, by_alias=True))
|
199
217
|
|
200
218
|
|
201
219
|
class PythonTask(BaseTask):
|
@@ -291,9 +309,9 @@ class NotebookTask(BaseTask):
|
|
291
309
|
|
292
310
|
"""
|
293
311
|
|
294
|
-
notebook: str = Field(
|
312
|
+
notebook: str = Field(serialization_alias="command")
|
295
313
|
|
296
|
-
notebook_output_path: Optional[str] = Field(default=None, alias="notebook_output_path")
|
314
|
+
notebook_output_path: Optional[str] = Field(default=None, alias="notebook_output_path", validate_default=True)
|
297
315
|
optional_ploomber_args: Optional[Dict[str, Any]] = Field(default=None, alias="optional_ploomber_args")
|
298
316
|
|
299
317
|
@computed_field
|
@@ -360,7 +378,7 @@ class Stub(BaseTraversal):
|
|
360
378
|
|
361
379
|
"""
|
362
380
|
|
363
|
-
model_config = ConfigDict(extra="
|
381
|
+
model_config = ConfigDict(extra="ignore")
|
364
382
|
catalog: Optional[Catalog] = Field(default=None, alias="catalog")
|
365
383
|
|
366
384
|
def create_node(self) -> StubNode:
|
@@ -520,7 +538,7 @@ class Pipeline(BaseModel):
|
|
520
538
|
_dag: graph.Graph = PrivateAttr()
|
521
539
|
model_config = ConfigDict(extra="forbid")
|
522
540
|
|
523
|
-
def _validate_path(self, path: List[StepType]) -> None:
|
541
|
+
def _validate_path(self, path: List[StepType], failure_path: bool = False) -> None:
|
524
542
|
# Check if one and only one step terminates with success
|
525
543
|
# Check no more than one step terminates with failure
|
526
544
|
|
@@ -538,7 +556,7 @@ class Pipeline(BaseModel):
|
|
538
556
|
raise Exception("A pipeline cannot have more than one step that terminates with failure")
|
539
557
|
reached_failure = True
|
540
558
|
|
541
|
-
if not reached_success:
|
559
|
+
if not reached_success and not reached_failure:
|
542
560
|
raise Exception("A pipeline must have at least one step that terminates with success")
|
543
561
|
|
544
562
|
def _construct_path(self, path: List[StepType]) -> None:
|
@@ -588,11 +606,21 @@ class Pipeline(BaseModel):
|
|
588
606
|
|
589
607
|
# Check all paths are valid and construct the path
|
590
608
|
paths = [success_path] + on_failure_paths
|
609
|
+
failure_path = False
|
591
610
|
for path in paths:
|
592
|
-
self._validate_path(path)
|
611
|
+
self._validate_path(path, failure_path)
|
593
612
|
self._construct_path(path)
|
594
613
|
|
595
|
-
|
614
|
+
failure_path = True
|
615
|
+
|
616
|
+
all_steps: List[StepType] = []
|
617
|
+
|
618
|
+
for path in paths:
|
619
|
+
for step in path:
|
620
|
+
all_steps.append(step)
|
621
|
+
|
622
|
+
seen = set()
|
623
|
+
unique = [x for x in all_steps if not (x in seen or seen.add(x))] # type: ignore
|
596
624
|
|
597
625
|
self._dag = graph.Graph(
|
598
626
|
start_at=all_steps[0].name,
|
@@ -600,7 +628,7 @@ class Pipeline(BaseModel):
|
|
600
628
|
internal_branch_name=self.internal_branch_name,
|
601
629
|
)
|
602
630
|
|
603
|
-
for step in
|
631
|
+
for step in unique:
|
604
632
|
self._dag.add_node(step.create_node())
|
605
633
|
|
606
634
|
if self.add_terminal_nodes:
|
@@ -669,8 +697,9 @@ class Pipeline(BaseModel):
|
|
669
697
|
|
670
698
|
run_context.dag = graph.create_graph(dag_definition)
|
671
699
|
|
672
|
-
print("Working with context:")
|
673
|
-
print(run_context)
|
700
|
+
console.print("Working with context:")
|
701
|
+
console.print(run_context)
|
702
|
+
console.rule(style="[dark orange]")
|
674
703
|
|
675
704
|
if not run_context.executor._local:
|
676
705
|
# We are not working with non local executor
|
@@ -686,8 +715,28 @@ class Pipeline(BaseModel):
|
|
686
715
|
# Prepare for graph execution
|
687
716
|
run_context.executor.prepare_for_graph_execution()
|
688
717
|
|
689
|
-
|
690
|
-
|
718
|
+
with Progress(
|
719
|
+
SpinnerColumn(spinner_name="runner"),
|
720
|
+
TextColumn("[progress.description]{task.description}", table_column=Column(ratio=2)),
|
721
|
+
BarColumn(table_column=Column(ratio=1), style="dark_orange"),
|
722
|
+
TimeElapsedColumn(table_column=Column(ratio=1)),
|
723
|
+
console=console,
|
724
|
+
expand=True,
|
725
|
+
) as progress:
|
726
|
+
try:
|
727
|
+
run_context.progress = progress
|
728
|
+
pipeline_execution_task = progress.add_task("[dark_orange] Starting execution .. ", total=1)
|
729
|
+
run_context.executor.execute_graph(dag=run_context.dag)
|
730
|
+
|
731
|
+
run_log = run_context.run_log_store.get_run_log_by_id(run_id=run_context.run_id, full=False)
|
732
|
+
|
733
|
+
if run_log.status == defaults.SUCCESS:
|
734
|
+
progress.update(pipeline_execution_task, description="[green] Success", completed=True)
|
735
|
+
else:
|
736
|
+
progress.update(pipeline_execution_task, description="[red] Failed", completed=True)
|
737
|
+
except Exception as e: # noqa: E722
|
738
|
+
console.print(e, style=defaults.error_style)
|
739
|
+
progress.update(pipeline_execution_task, description="[red] Errored execution", completed=True)
|
691
740
|
|
692
741
|
if run_context.executor._local:
|
693
742
|
return run_context.run_log_store.get_run_log_by_id(run_id=run_context.run_id)
|
runnable/tasks.py
CHANGED
@@ -9,14 +9,21 @@ import sys
|
|
9
9
|
from datetime import datetime
|
10
10
|
from pickle import PicklingError
|
11
11
|
from string import Template
|
12
|
-
from typing import Any, Dict, List, Literal, Tuple
|
12
|
+
from typing import Any, Dict, List, Literal, Optional, Tuple
|
13
13
|
|
14
14
|
from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator
|
15
|
+
from rich.console import Console
|
15
16
|
from stevedore import driver
|
16
17
|
|
17
18
|
import runnable.context as context
|
18
|
-
from runnable import defaults, parameters, utils
|
19
|
-
from runnable.datastore import
|
19
|
+
from runnable import defaults, exceptions, parameters, utils
|
20
|
+
from runnable.datastore import (
|
21
|
+
JsonParameter,
|
22
|
+
MetricParameter,
|
23
|
+
ObjectParameter,
|
24
|
+
Parameter,
|
25
|
+
StepAttempt,
|
26
|
+
)
|
20
27
|
from runnable.defaults import TypeMapVariable
|
21
28
|
|
22
29
|
logger = logging.getLogger(defaults.LOGGER_NAME)
|
@@ -26,9 +33,12 @@ logging.getLogger("stevedore").setLevel(logging.CRITICAL)
|
|
26
33
|
# TODO: Can we add memory peak, cpu usage, etc. to the metrics?
|
27
34
|
|
28
35
|
|
36
|
+
console = Console(file=io.StringIO())
|
37
|
+
|
38
|
+
|
29
39
|
class TaskReturns(BaseModel):
|
30
40
|
name: str
|
31
|
-
kind: Literal["json", "object"] = Field(default="json")
|
41
|
+
kind: Literal["json", "object", "metric"] = Field(default="json")
|
32
42
|
|
33
43
|
|
34
44
|
class BaseTaskType(BaseModel):
|
@@ -36,11 +46,14 @@ class BaseTaskType(BaseModel):
|
|
36
46
|
|
37
47
|
task_type: str = Field(serialization_alias="command_type")
|
38
48
|
node_name: str = Field(exclude=True)
|
39
|
-
secrets:
|
49
|
+
secrets: List[str] = Field(default_factory=list)
|
40
50
|
returns: List[TaskReturns] = Field(default_factory=list, alias="returns")
|
41
51
|
|
42
52
|
model_config = ConfigDict(extra="forbid")
|
43
53
|
|
54
|
+
def get_summary(self) -> Dict[str, Any]:
|
55
|
+
return self.model_dump(by_alias=True, exclude_none=True)
|
56
|
+
|
44
57
|
@property
|
45
58
|
def _context(self):
|
46
59
|
return context.run_context
|
@@ -60,15 +73,14 @@ class BaseTaskType(BaseModel):
|
|
60
73
|
raise NotImplementedError()
|
61
74
|
|
62
75
|
def set_secrets_as_env_variables(self):
|
63
|
-
for key
|
76
|
+
for key in self.secrets:
|
64
77
|
secret_value = context.run_context.secrets_handler.get(key)
|
65
|
-
|
66
|
-
os.environ[value] = secret_value
|
78
|
+
os.environ[key] = secret_value
|
67
79
|
|
68
80
|
def delete_secrets_from_env_variables(self):
|
69
|
-
for
|
70
|
-
if
|
71
|
-
del os.environ[
|
81
|
+
for key in self.secrets:
|
82
|
+
if key in os.environ:
|
83
|
+
del os.environ[key]
|
72
84
|
|
73
85
|
def execute_command(
|
74
86
|
self,
|
@@ -98,12 +110,15 @@ class BaseTaskType(BaseModel):
|
|
98
110
|
self.set_secrets_as_env_variables()
|
99
111
|
try:
|
100
112
|
yield
|
113
|
+
except Exception as e: # pylint: disable=broad-except
|
114
|
+
logger.exception(e)
|
101
115
|
finally:
|
102
116
|
self.delete_secrets_from_env_variables()
|
103
117
|
|
104
118
|
@contextlib.contextmanager
|
105
119
|
def execution_context(self, map_variable: TypeMapVariable = None, allow_complex: bool = True):
|
106
120
|
params = self._context.run_log_store.get_parameters(run_id=self._context.run_id).copy()
|
121
|
+
logger.info(f"Parameters available for the execution: {params}")
|
107
122
|
|
108
123
|
for param_name, param in params.items():
|
109
124
|
# Any access to unreduced param should be replaced.
|
@@ -118,20 +133,28 @@ class BaseTaskType(BaseModel):
|
|
118
133
|
if context_param in params:
|
119
134
|
params[param_name].value = params[context_param].value
|
120
135
|
|
136
|
+
logger.debug(f"Resolved parameters: {params}")
|
137
|
+
|
121
138
|
if not allow_complex:
|
122
139
|
params = {key: value for key, value in params.items() if isinstance(value, JsonParameter)}
|
123
140
|
|
124
|
-
log_file_name = self.node_name
|
141
|
+
log_file_name = self.node_name # + ".execution.log"
|
125
142
|
if map_variable:
|
126
143
|
for _, value in map_variable.items():
|
127
144
|
log_file_name += "_" + str(value)
|
128
145
|
|
146
|
+
log_file_name = "".join(x for x in log_file_name if x.isalnum()) + ".execution.log"
|
147
|
+
|
129
148
|
log_file = open(log_file_name, "w")
|
130
149
|
|
131
150
|
f = io.StringIO()
|
132
151
|
try:
|
133
152
|
with contextlib.redirect_stdout(f):
|
153
|
+
# with contextlib.nullcontext():
|
134
154
|
yield params
|
155
|
+
print(console.file.getvalue()) # type: ignore
|
156
|
+
except Exception as e: # pylint: disable=broad-except
|
157
|
+
logger.exception(e)
|
135
158
|
finally:
|
136
159
|
print(f.getvalue()) # print to console
|
137
160
|
log_file.write(f.getvalue()) # Print to file
|
@@ -140,15 +163,13 @@ class BaseTaskType(BaseModel):
|
|
140
163
|
log_file.close()
|
141
164
|
|
142
165
|
# Put the log file in the catalog
|
143
|
-
catalog_handler =
|
144
|
-
catalog_handler.put(name=log_file.name, run_id=context.run_context.run_id)
|
166
|
+
self._context.catalog_handler.put(name=log_file.name, run_id=context.run_context.run_id)
|
145
167
|
os.remove(log_file.name)
|
146
168
|
|
147
169
|
# Update parameters
|
170
|
+
# This should only update the parameters that are changed at the root level.
|
148
171
|
self._context.run_log_store.set_parameters(parameters=params, run_id=self._context.run_id)
|
149
172
|
|
150
|
-
return True # To suppress exceptions
|
151
|
-
|
152
173
|
|
153
174
|
def task_return_to_parameter(task_return: TaskReturns, value: Any) -> Parameter:
|
154
175
|
# implicit support for pydantic models
|
@@ -161,6 +182,9 @@ def task_return_to_parameter(task_return: TaskReturns, value: Any) -> Parameter:
|
|
161
182
|
if task_return.kind == "json":
|
162
183
|
return JsonParameter(kind="json", value=value)
|
163
184
|
|
185
|
+
if task_return.kind == "metric":
|
186
|
+
return MetricParameter(kind="metric", value=value)
|
187
|
+
|
164
188
|
if task_return.kind == "object":
|
165
189
|
obj = ObjectParameter(value=task_return.name, kind="object")
|
166
190
|
obj.put_object(data=value)
|
@@ -197,13 +221,22 @@ class PythonTaskType(BaseTaskType): # pylint: disable=too-few-public-methods
|
|
197
221
|
imported_module = importlib.import_module(module)
|
198
222
|
f = getattr(imported_module, func)
|
199
223
|
|
200
|
-
filtered_parameters = parameters.filter_arguments_for_func(f, params.copy(), map_variable)
|
201
|
-
logger.info(f"Calling {func} from {module} with {filtered_parameters}")
|
202
|
-
|
203
224
|
try:
|
204
|
-
|
225
|
+
try:
|
226
|
+
filtered_parameters = parameters.filter_arguments_for_func(f, params.copy(), map_variable)
|
227
|
+
logger.info(f"Calling {func} from {module} with {filtered_parameters}")
|
228
|
+
user_set_parameters = f(**filtered_parameters) # This is a tuple or single value
|
229
|
+
except Exception as e:
|
230
|
+
console.log(e, style=defaults.error_style, markup=False)
|
231
|
+
raise exceptions.CommandCallError(f"Function call: {self.command} did not succeed.\n") from e
|
232
|
+
|
205
233
|
attempt_log.input_parameters = params.copy()
|
206
234
|
|
235
|
+
if map_variable:
|
236
|
+
attempt_log.input_parameters.update(
|
237
|
+
{k: JsonParameter(value=v, kind="json") for k, v in map_variable.items()}
|
238
|
+
)
|
239
|
+
|
207
240
|
if self.returns:
|
208
241
|
if not isinstance(user_set_parameters, tuple): # make it a tuple
|
209
242
|
user_set_parameters = (user_set_parameters,)
|
@@ -212,6 +245,7 @@ class PythonTaskType(BaseTaskType): # pylint: disable=too-few-public-methods
|
|
212
245
|
raise ValueError("Returns task signature does not match the function returns")
|
213
246
|
|
214
247
|
output_parameters: Dict[str, Parameter] = {}
|
248
|
+
metrics: Dict[str, Parameter] = {}
|
215
249
|
|
216
250
|
for i, task_return in enumerate(self.returns):
|
217
251
|
output_parameter = task_return_to_parameter(
|
@@ -219,6 +253,9 @@ class PythonTaskType(BaseTaskType): # pylint: disable=too-few-public-methods
|
|
219
253
|
value=user_set_parameters[i],
|
220
254
|
)
|
221
255
|
|
256
|
+
if task_return.kind == "metric":
|
257
|
+
metrics[task_return.name] = output_parameter
|
258
|
+
|
222
259
|
param_name = task_return.name
|
223
260
|
if map_variable:
|
224
261
|
for _, v in map_variable.items():
|
@@ -227,14 +264,15 @@ class PythonTaskType(BaseTaskType): # pylint: disable=too-few-public-methods
|
|
227
264
|
output_parameters[param_name] = output_parameter
|
228
265
|
|
229
266
|
attempt_log.output_parameters = output_parameters
|
267
|
+
attempt_log.user_defined_metrics = metrics
|
230
268
|
params.update(output_parameters)
|
231
269
|
|
232
270
|
attempt_log.status = defaults.SUCCESS
|
233
271
|
except Exception as _e:
|
234
|
-
msg = f"Call to the function {self.command}
|
235
|
-
|
236
|
-
|
237
|
-
|
272
|
+
msg = f"Call to the function {self.command} did not succeed.\n"
|
273
|
+
attempt_log.message = msg
|
274
|
+
console.print_exception(show_locals=False)
|
275
|
+
console.log(_e, style=defaults.error_style)
|
238
276
|
|
239
277
|
attempt_log.end_time = str(datetime.now())
|
240
278
|
|
@@ -246,7 +284,7 @@ class NotebookTaskType(BaseTaskType):
|
|
246
284
|
|
247
285
|
task_type: str = Field(default="notebook", serialization_alias="command_type")
|
248
286
|
command: str
|
249
|
-
notebook_output_path: str = Field(default=
|
287
|
+
notebook_output_path: Optional[str] = Field(default=None, validate_default=True)
|
250
288
|
optional_ploomber_args: dict = {}
|
251
289
|
|
252
290
|
@field_validator("command")
|
@@ -288,7 +326,7 @@ class NotebookTaskType(BaseTaskType):
|
|
288
326
|
import ploomber_engine as pm
|
289
327
|
from ploomber_engine.ipython import PloomberClient
|
290
328
|
|
291
|
-
notebook_output_path = self.notebook_output_path
|
329
|
+
notebook_output_path = self.notebook_output_path or ""
|
292
330
|
|
293
331
|
with self.execution_context(
|
294
332
|
map_variable=map_variable, allow_complex=False
|
@@ -296,7 +334,6 @@ class NotebookTaskType(BaseTaskType):
|
|
296
334
|
if map_variable:
|
297
335
|
for key, value in map_variable.items():
|
298
336
|
notebook_output_path += "_" + str(value)
|
299
|
-
|
300
337
|
params[key] = value
|
301
338
|
|
302
339
|
notebook_params = {k: v.get_value() for k, v in params.items()}
|
@@ -394,15 +431,17 @@ class ShellTaskType(BaseTaskType):
|
|
394
431
|
|
395
432
|
# Expose secrets as environment variables
|
396
433
|
if self.secrets:
|
397
|
-
for key
|
434
|
+
for key in self.secrets:
|
398
435
|
secret_value = context.run_context.secrets_handler.get(key)
|
399
|
-
subprocess_env[
|
436
|
+
subprocess_env[key] = secret_value
|
400
437
|
|
401
438
|
with self.execution_context(map_variable=map_variable, allow_complex=False) as params:
|
402
439
|
subprocess_env.update({k: v.get_value() for k, v in params.items()})
|
403
440
|
|
404
441
|
# Json dumps all runnable environment variables
|
405
442
|
for key, value in subprocess_env.items():
|
443
|
+
if isinstance(value, str):
|
444
|
+
continue
|
406
445
|
subprocess_env[key] = json.dumps(value)
|
407
446
|
|
408
447
|
collect_delimiter = "=== COLLECT ==="
|
@@ -411,37 +450,80 @@ class ShellTaskType(BaseTaskType):
|
|
411
450
|
logger.info(f"Executing shell command: {command}")
|
412
451
|
|
413
452
|
capture = False
|
414
|
-
return_keys =
|
453
|
+
return_keys = {x.name: x for x in self.returns}
|
415
454
|
|
416
|
-
|
455
|
+
proc = subprocess.Popen(
|
417
456
|
command,
|
418
457
|
shell=True,
|
419
458
|
env=subprocess_env,
|
420
459
|
stdout=subprocess.PIPE,
|
421
460
|
stderr=subprocess.PIPE,
|
422
461
|
text=True,
|
423
|
-
)
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
462
|
+
)
|
463
|
+
result = proc.communicate()
|
464
|
+
logger.debug(result)
|
465
|
+
logger.info(proc.returncode)
|
466
|
+
|
467
|
+
if proc.returncode != 0:
|
468
|
+
msg = ",".join(result[1].split("\n"))
|
469
|
+
attempt_log.status = defaults.FAIL
|
470
|
+
attempt_log.end_time = str(datetime.now())
|
471
|
+
attempt_log.message = msg
|
472
|
+
console.print(msg, style=defaults.error_style)
|
473
|
+
return attempt_log
|
474
|
+
|
475
|
+
# for stderr
|
476
|
+
for line in result[1].split("\n"):
|
477
|
+
if line.strip() == "":
|
478
|
+
continue
|
479
|
+
console.print(line, style=defaults.warning_style)
|
480
|
+
|
481
|
+
output_parameters: Dict[str, Parameter] = {}
|
482
|
+
metrics: Dict[str, Parameter] = {}
|
483
|
+
|
484
|
+
# only from stdout
|
485
|
+
for line in result[0].split("\n"):
|
486
|
+
if line.strip() == "":
|
487
|
+
continue
|
488
|
+
|
489
|
+
logger.info(line)
|
490
|
+
console.print(line)
|
491
|
+
|
492
|
+
if line.strip() == collect_delimiter:
|
493
|
+
# The lines from now on should be captured
|
494
|
+
capture = True
|
495
|
+
continue
|
496
|
+
|
497
|
+
if capture:
|
498
|
+
key, value = line.strip().split("=", 1)
|
499
|
+
if key in return_keys:
|
500
|
+
task_return = return_keys[key]
|
501
|
+
|
502
|
+
try:
|
503
|
+
value = json.loads(value)
|
504
|
+
except json.JSONDecodeError:
|
505
|
+
value = value
|
506
|
+
|
507
|
+
output_parameter = task_return_to_parameter(
|
508
|
+
task_return=task_return,
|
509
|
+
value=value,
|
510
|
+
)
|
511
|
+
|
512
|
+
if task_return.kind == "metric":
|
513
|
+
metrics[task_return.name] = output_parameter
|
514
|
+
|
515
|
+
param_name = task_return.name
|
516
|
+
if map_variable:
|
517
|
+
for _, v in map_variable.items():
|
518
|
+
param_name = f"{param_name}_{v}"
|
519
|
+
|
520
|
+
output_parameters[param_name] = output_parameter
|
521
|
+
|
522
|
+
attempt_log.output_parameters = output_parameters
|
523
|
+
attempt_log.user_defined_metrics = metrics
|
524
|
+
params.update(output_parameters)
|
525
|
+
|
526
|
+
attempt_log.status = defaults.SUCCESS
|
445
527
|
|
446
528
|
attempt_log.end_time = str(datetime.now())
|
447
529
|
return attempt_log
|
runnable/utils.py
CHANGED
@@ -538,7 +538,7 @@ def get_provider_by_name_and_type(service_type: str, service_details: defaults.S
|
|
538
538
|
if "config" in service_details:
|
539
539
|
service_config = service_details.get("config", {})
|
540
540
|
|
541
|
-
logger.
|
541
|
+
logger.debug(f"Trying to get a service of {service_type} of the name {service_name} with config: {service_config}")
|
542
542
|
try:
|
543
543
|
mgr = driver.DriverManager(
|
544
544
|
namespace=namespace,
|
@@ -548,6 +548,7 @@ def get_provider_by_name_and_type(service_type: str, service_details: defaults.S
|
|
548
548
|
)
|
549
549
|
return mgr.driver
|
550
550
|
except Exception as _e:
|
551
|
+
logger.exception(f"Could not find the service of type: {service_type} with config: {service_details}")
|
551
552
|
raise Exception(f"Could not find the service of type: {service_type} with config: {service_details}") from _e
|
552
553
|
|
553
554
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: runnable
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.11.1
|
4
4
|
Summary: A Compute agnostic pipelining software
|
5
5
|
Home-page: https://github.com/vijayvammi/runnable
|
6
6
|
License: Apache-2.0
|
@@ -15,13 +15,12 @@ Classifier: Programming Language :: Python :: 3.11
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
16
16
|
Provides-Extra: database
|
17
17
|
Provides-Extra: docker
|
18
|
-
Provides-Extra: mlflow
|
19
18
|
Provides-Extra: notebook
|
20
19
|
Requires-Dist: click
|
21
20
|
Requires-Dist: click-plugins (>=1.1.1,<2.0.0)
|
22
21
|
Requires-Dist: dill (>=0.3.8,<0.4.0)
|
23
22
|
Requires-Dist: docker ; extra == "docker"
|
24
|
-
Requires-Dist: mlflow-skinny
|
23
|
+
Requires-Dist: mlflow-skinny
|
25
24
|
Requires-Dist: ploomber-engine (>=0.0.31,<0.0.32) ; extra == "notebook"
|
26
25
|
Requires-Dist: pydantic (>=2.5,<3.0)
|
27
26
|
Requires-Dist: rich (>=13.5.2,<14.0.0)
|