ob-metaflow 2.12.36.3__py2.py3-none-any.whl → 2.13.0.1__py2.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.
Potentially problematic release.
This version of ob-metaflow might be problematic. Click here for more details.
- metaflow/__init__.py +3 -0
- metaflow/cli.py +180 -718
- metaflow/cli_args.py +17 -0
- metaflow/cli_components/__init__.py +0 -0
- metaflow/cli_components/dump_cmd.py +96 -0
- metaflow/cli_components/init_cmd.py +51 -0
- metaflow/cli_components/run_cmds.py +360 -0
- metaflow/cli_components/step_cmd.py +189 -0
- metaflow/cli_components/utils.py +140 -0
- metaflow/cmd/develop/stub_generator.py +9 -2
- metaflow/datastore/flow_datastore.py +2 -2
- metaflow/decorators.py +63 -2
- metaflow/exception.py +8 -2
- metaflow/extension_support/plugins.py +41 -27
- metaflow/flowspec.py +175 -23
- metaflow/graph.py +28 -27
- metaflow/includefile.py +50 -22
- metaflow/lint.py +35 -20
- metaflow/metaflow_config.py +6 -1
- metaflow/package.py +17 -3
- metaflow/parameters.py +87 -23
- metaflow/plugins/__init__.py +4 -0
- metaflow/plugins/airflow/airflow_cli.py +1 -0
- metaflow/plugins/argo/argo_workflows.py +41 -1
- metaflow/plugins/argo/argo_workflows_cli.py +1 -0
- metaflow/plugins/argo/argo_workflows_deployer_objects.py +47 -1
- metaflow/plugins/aws/batch/batch_decorator.py +2 -2
- metaflow/plugins/aws/secrets_manager/aws_secrets_manager_secrets_provider.py +13 -10
- metaflow/plugins/aws/step_functions/step_functions.py +32 -0
- metaflow/plugins/aws/step_functions/step_functions_cli.py +1 -0
- metaflow/plugins/aws/step_functions/step_functions_deployer_objects.py +3 -0
- metaflow/plugins/cards/card_creator.py +1 -0
- metaflow/plugins/cards/card_decorator.py +46 -8
- metaflow/plugins/datatools/s3/s3op.py +3 -3
- metaflow/plugins/kubernetes/kubernetes_cli.py +1 -1
- metaflow/plugins/kubernetes/kubernetes_decorator.py +2 -2
- metaflow/plugins/pypi/bootstrap.py +196 -61
- metaflow/plugins/pypi/conda_decorator.py +20 -10
- metaflow/plugins/pypi/conda_environment.py +76 -21
- metaflow/plugins/pypi/micromamba.py +42 -15
- metaflow/plugins/pypi/pip.py +8 -3
- metaflow/plugins/pypi/pypi_decorator.py +11 -9
- metaflow/plugins/timeout_decorator.py +2 -2
- metaflow/runner/click_api.py +240 -50
- metaflow/runner/deployer.py +1 -1
- metaflow/runner/deployer_impl.py +8 -3
- metaflow/runner/metaflow_runner.py +10 -2
- metaflow/runner/nbdeploy.py +2 -0
- metaflow/runner/nbrun.py +1 -1
- metaflow/runner/subprocess_manager.py +3 -1
- metaflow/runner/utils.py +41 -19
- metaflow/runtime.py +111 -73
- metaflow/sidecar/sidecar_worker.py +1 -1
- metaflow/user_configs/__init__.py +0 -0
- metaflow/user_configs/config_decorators.py +563 -0
- metaflow/user_configs/config_options.py +548 -0
- metaflow/user_configs/config_parameters.py +405 -0
- metaflow/util.py +17 -0
- metaflow/version.py +1 -1
- {ob_metaflow-2.12.36.3.dist-info → ob_metaflow-2.13.0.1.dist-info}/METADATA +3 -2
- {ob_metaflow-2.12.36.3.dist-info → ob_metaflow-2.13.0.1.dist-info}/RECORD +65 -55
- {ob_metaflow-2.12.36.3.dist-info → ob_metaflow-2.13.0.1.dist-info}/LICENSE +0 -0
- {ob_metaflow-2.12.36.3.dist-info → ob_metaflow-2.13.0.1.dist-info}/WHEEL +0 -0
- {ob_metaflow-2.12.36.3.dist-info → ob_metaflow-2.13.0.1.dist-info}/entry_points.txt +0 -0
- {ob_metaflow-2.12.36.3.dist-info → ob_metaflow-2.13.0.1.dist-info}/top_level.txt +0 -0
metaflow/lint.py
CHANGED
|
@@ -52,7 +52,7 @@ def check_reserved_words(graph):
|
|
|
52
52
|
msg = "Step name *%s* is a reserved word. Choose another name for the " "step."
|
|
53
53
|
for node in graph:
|
|
54
54
|
if node.name in RESERVED:
|
|
55
|
-
raise LintWarn(msg % node.name)
|
|
55
|
+
raise LintWarn(msg % node.name, node.func_lineno, node.source_file)
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
@linter.ensure_fundamentals
|
|
@@ -76,9 +76,9 @@ def check_that_end_is_end(graph):
|
|
|
76
76
|
node = graph["end"]
|
|
77
77
|
|
|
78
78
|
if node.has_tail_next or node.invalid_tail_next:
|
|
79
|
-
raise LintWarn(msg0, node.tail_next_lineno)
|
|
79
|
+
raise LintWarn(msg0, node.tail_next_lineno, node.source_file)
|
|
80
80
|
if node.num_args > 1:
|
|
81
|
-
raise LintWarn(msg1, node.tail_next_lineno)
|
|
81
|
+
raise LintWarn(msg1, node.tail_next_lineno, node.source_file)
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
@linter.ensure_fundamentals
|
|
@@ -90,7 +90,7 @@ def check_step_names(graph):
|
|
|
90
90
|
)
|
|
91
91
|
for node in graph:
|
|
92
92
|
if re.search("[^a-z0-9_]", node.name) or node.name[0] == "_":
|
|
93
|
-
raise LintWarn(msg.format(node), node.func_lineno)
|
|
93
|
+
raise LintWarn(msg.format(node), node.func_lineno, node.source_file)
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
@linter.ensure_fundamentals
|
|
@@ -108,11 +108,11 @@ def check_num_args(graph):
|
|
|
108
108
|
msg2 = "Step *{0.name}* is missing the 'self' argument."
|
|
109
109
|
for node in graph:
|
|
110
110
|
if node.num_args > 2:
|
|
111
|
-
raise LintWarn(msg0.format(node), node.func_lineno)
|
|
111
|
+
raise LintWarn(msg0.format(node), node.func_lineno, node.source_file)
|
|
112
112
|
elif node.num_args == 2 and node.type != "join":
|
|
113
|
-
raise LintWarn(msg1.format(node), node.func_lineno)
|
|
113
|
+
raise LintWarn(msg1.format(node), node.func_lineno, node.source_file)
|
|
114
114
|
elif node.num_args == 0:
|
|
115
|
-
raise LintWarn(msg2.format(node), node.func_lineno)
|
|
115
|
+
raise LintWarn(msg2.format(node), node.func_lineno, node.source_file)
|
|
116
116
|
|
|
117
117
|
|
|
118
118
|
@linter.ensure_static_graph
|
|
@@ -125,7 +125,7 @@ def check_static_transitions(graph):
|
|
|
125
125
|
)
|
|
126
126
|
for node in graph:
|
|
127
127
|
if node.type != "end" and not node.has_tail_next:
|
|
128
|
-
raise LintWarn(msg.format(node), node.func_lineno)
|
|
128
|
+
raise LintWarn(msg.format(node), node.func_lineno, node.source_file)
|
|
129
129
|
|
|
130
130
|
|
|
131
131
|
@linter.ensure_static_graph
|
|
@@ -138,7 +138,7 @@ def check_valid_transitions(graph):
|
|
|
138
138
|
)
|
|
139
139
|
for node in graph:
|
|
140
140
|
if node.type != "end" and node.has_tail_next and node.invalid_tail_next:
|
|
141
|
-
raise LintWarn(msg.format(node), node.tail_next_lineno)
|
|
141
|
+
raise LintWarn(msg.format(node), node.tail_next_lineno, node.source_file)
|
|
142
142
|
|
|
143
143
|
|
|
144
144
|
@linter.ensure_static_graph
|
|
@@ -151,7 +151,11 @@ def check_unknown_transitions(graph):
|
|
|
151
151
|
for node in graph:
|
|
152
152
|
unknown = [n for n in node.out_funcs if n not in graph]
|
|
153
153
|
if unknown:
|
|
154
|
-
raise LintWarn(
|
|
154
|
+
raise LintWarn(
|
|
155
|
+
msg.format(node, step=unknown[0]),
|
|
156
|
+
node.tail_next_lineno,
|
|
157
|
+
node.source_file,
|
|
158
|
+
)
|
|
155
159
|
|
|
156
160
|
|
|
157
161
|
@linter.ensure_acyclicity
|
|
@@ -167,7 +171,9 @@ def check_for_acyclicity(graph):
|
|
|
167
171
|
for n in node.out_funcs:
|
|
168
172
|
if n in seen:
|
|
169
173
|
path = "->".join(seen + [n])
|
|
170
|
-
raise LintWarn(
|
|
174
|
+
raise LintWarn(
|
|
175
|
+
msg.format(path), node.tail_next_lineno, node.source_file
|
|
176
|
+
)
|
|
171
177
|
else:
|
|
172
178
|
check_path(graph[n], seen + [n])
|
|
173
179
|
|
|
@@ -195,7 +201,7 @@ def check_for_orphans(graph):
|
|
|
195
201
|
orphans = nodeset - seen
|
|
196
202
|
if orphans:
|
|
197
203
|
orphan = graph[list(orphans)[0]]
|
|
198
|
-
raise LintWarn(msg.format(orphan), orphan.func_lineno)
|
|
204
|
+
raise LintWarn(msg.format(orphan), orphan.func_lineno, orphan.source_file)
|
|
199
205
|
|
|
200
206
|
|
|
201
207
|
@linter.ensure_static_graph
|
|
@@ -230,7 +236,9 @@ def check_split_join_balance(graph):
|
|
|
230
236
|
if split_stack:
|
|
231
237
|
_, split_roots = split_stack.pop()
|
|
232
238
|
roots = ", ".join(split_roots)
|
|
233
|
-
raise LintWarn(
|
|
239
|
+
raise LintWarn(
|
|
240
|
+
msg0.format(roots=roots), node.func_lineno, node.source_file
|
|
241
|
+
)
|
|
234
242
|
elif node.type == "join":
|
|
235
243
|
if split_stack:
|
|
236
244
|
_, split_roots = split_stack[-1]
|
|
@@ -243,9 +251,10 @@ def check_split_join_balance(graph):
|
|
|
243
251
|
node, paths=paths, num_roots=len(split_roots), roots=roots
|
|
244
252
|
),
|
|
245
253
|
node.func_lineno,
|
|
254
|
+
node.source_file,
|
|
246
255
|
)
|
|
247
256
|
else:
|
|
248
|
-
raise LintWarn(msg2.format(node), node.func_lineno)
|
|
257
|
+
raise LintWarn(msg2.format(node), node.func_lineno, node.source_file)
|
|
249
258
|
|
|
250
259
|
# check that incoming steps come from the same lineage
|
|
251
260
|
# (no cross joins)
|
|
@@ -256,7 +265,7 @@ def check_split_join_balance(graph):
|
|
|
256
265
|
return tuple(graph[n].split_parents)
|
|
257
266
|
|
|
258
267
|
if not all_equal(map(parents, node.in_funcs)):
|
|
259
|
-
raise LintWarn(msg3.format(node), node.func_lineno)
|
|
268
|
+
raise LintWarn(msg3.format(node), node.func_lineno, node.source_file)
|
|
260
269
|
|
|
261
270
|
for n in node.out_funcs:
|
|
262
271
|
traverse(graph[n], new_stack)
|
|
@@ -276,7 +285,9 @@ def check_empty_foreaches(graph):
|
|
|
276
285
|
if node.type == "foreach":
|
|
277
286
|
joins = [n for n in node.out_funcs if graph[n].type == "join"]
|
|
278
287
|
if joins:
|
|
279
|
-
raise LintWarn(
|
|
288
|
+
raise LintWarn(
|
|
289
|
+
msg.format(node, join=joins[0]), node.func_lineno, node.source_file
|
|
290
|
+
)
|
|
280
291
|
|
|
281
292
|
|
|
282
293
|
@linter.ensure_static_graph
|
|
@@ -290,7 +301,7 @@ def check_parallel_step_after_next(graph):
|
|
|
290
301
|
if node.parallel_foreach and not all(
|
|
291
302
|
graph[out_node].parallel_step for out_node in node.out_funcs
|
|
292
303
|
):
|
|
293
|
-
raise LintWarn(msg.format(node))
|
|
304
|
+
raise LintWarn(msg.format(node), node.func_lineno, node.source_file)
|
|
294
305
|
|
|
295
306
|
|
|
296
307
|
@linter.ensure_static_graph
|
|
@@ -303,7 +314,9 @@ def check_join_followed_by_parallel_step(graph):
|
|
|
303
314
|
)
|
|
304
315
|
for node in graph:
|
|
305
316
|
if node.parallel_step and not graph[node.out_funcs[0]].type == "join":
|
|
306
|
-
raise LintWarn(
|
|
317
|
+
raise LintWarn(
|
|
318
|
+
msg.format(node.out_funcs[0]), node.func_lineno, node.source_file
|
|
319
|
+
)
|
|
307
320
|
|
|
308
321
|
|
|
309
322
|
@linter.ensure_static_graph
|
|
@@ -318,7 +331,9 @@ def check_parallel_foreach_calls_parallel_step(graph):
|
|
|
318
331
|
for node2 in graph:
|
|
319
332
|
if node2.out_funcs and node.name in node2.out_funcs:
|
|
320
333
|
if not node2.parallel_foreach:
|
|
321
|
-
raise LintWarn(
|
|
334
|
+
raise LintWarn(
|
|
335
|
+
msg.format(node, node2), node.func_lineno, node.source_file
|
|
336
|
+
)
|
|
322
337
|
|
|
323
338
|
|
|
324
339
|
@linter.ensure_non_nested_foreach
|
|
@@ -331,4 +346,4 @@ def check_nested_foreach(graph):
|
|
|
331
346
|
for node in graph:
|
|
332
347
|
if node.type == "foreach":
|
|
333
348
|
if any(graph[p].type == "foreach" for p in node.split_parents):
|
|
334
|
-
raise LintWarn(msg.format(node))
|
|
349
|
+
raise LintWarn(msg.format(node), node.func_lineno, node.source_file)
|
metaflow/metaflow_config.py
CHANGED
|
@@ -441,7 +441,7 @@ ESCAPE_HATCH_WARNING = from_conf("ESCAPE_HATCH_WARNING", True)
|
|
|
441
441
|
###
|
|
442
442
|
# Debug configuration
|
|
443
443
|
###
|
|
444
|
-
DEBUG_OPTIONS = ["subcommand", "sidecar", "s3client", "tracing", "stubgen"]
|
|
444
|
+
DEBUG_OPTIONS = ["subcommand", "sidecar", "s3client", "tracing", "stubgen", "userconf"]
|
|
445
445
|
|
|
446
446
|
for typ in DEBUG_OPTIONS:
|
|
447
447
|
vars()["DEBUG_%s" % typ.upper()] = from_conf("DEBUG_%s" % typ.upper(), False)
|
|
@@ -511,6 +511,11 @@ MAX_CPU_PER_TASK = from_conf("MAX_CPU_PER_TASK")
|
|
|
511
511
|
# lexicographic ordering of attempts. This won't work if MAX_ATTEMPTS > 99.
|
|
512
512
|
MAX_ATTEMPTS = 6
|
|
513
513
|
|
|
514
|
+
# Feature flag (experimental features that are *explicitly* unsupported)
|
|
515
|
+
|
|
516
|
+
# Process configs even when using the click_api for Runner/Deployer
|
|
517
|
+
CLICK_API_PROCESS_CONFIG = from_conf("CLICK_API_PROCESS_CONFIG", False)
|
|
518
|
+
|
|
514
519
|
|
|
515
520
|
# PINNED_CONDA_LIBS are the libraries that metaflow depends on for execution
|
|
516
521
|
# and are needed within a conda environment
|
metaflow/package.py
CHANGED
|
@@ -6,6 +6,7 @@ import time
|
|
|
6
6
|
import json
|
|
7
7
|
from io import BytesIO
|
|
8
8
|
|
|
9
|
+
from .user_configs.config_parameters import CONFIG_FILE, dump_config_values
|
|
9
10
|
from .extension_support import EXT_PKG, package_mfext_all
|
|
10
11
|
from .metaflow_config import DEFAULT_PACKAGE_SUFFIXES
|
|
11
12
|
from .exception import MetaflowException
|
|
@@ -151,11 +152,23 @@ class MetaflowPackage(object):
|
|
|
151
152
|
for path_tuple in self._walk(flowdir, suffixes=self.suffixes):
|
|
152
153
|
yield path_tuple
|
|
153
154
|
|
|
155
|
+
def _add_configs(self, tar):
|
|
156
|
+
buf = BytesIO()
|
|
157
|
+
buf.write(json.dumps(dump_config_values(self._flow)).encode("utf-8"))
|
|
158
|
+
self._add_file(tar, os.path.basename(CONFIG_FILE), buf)
|
|
159
|
+
|
|
154
160
|
def _add_info(self, tar):
|
|
155
|
-
info = tarfile.TarInfo(os.path.basename(INFO_FILE))
|
|
156
|
-
env = self.environment.get_environment_info(include_ext_info=True)
|
|
157
161
|
buf = BytesIO()
|
|
158
|
-
buf.write(
|
|
162
|
+
buf.write(
|
|
163
|
+
json.dumps(
|
|
164
|
+
self.environment.get_environment_info(include_ext_info=True)
|
|
165
|
+
).encode("utf-8")
|
|
166
|
+
)
|
|
167
|
+
self._add_file(tar, os.path.basename(INFO_FILE), buf)
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def _add_file(tar, filename, buf):
|
|
171
|
+
info = tarfile.TarInfo(filename)
|
|
159
172
|
buf.seek(0)
|
|
160
173
|
info.size = len(buf.getvalue())
|
|
161
174
|
# Setting this default to Dec 3, 2019
|
|
@@ -175,6 +188,7 @@ class MetaflowPackage(object):
|
|
|
175
188
|
fileobj=buf, mode="w:gz", compresslevel=3, dereference=True
|
|
176
189
|
) as tar:
|
|
177
190
|
self._add_info(tar)
|
|
191
|
+
self._add_configs(tar)
|
|
178
192
|
for path, arcname in self.path_tuples():
|
|
179
193
|
tar.add(path, arcname=arcname, recursive=False, filter=no_mtime)
|
|
180
194
|
|
metaflow/parameters.py
CHANGED
|
@@ -3,7 +3,7 @@ import json
|
|
|
3
3
|
from contextlib import contextmanager
|
|
4
4
|
from threading import local
|
|
5
5
|
|
|
6
|
-
from typing import Any, Callable, Dict, NamedTuple, Optional, Type, Union
|
|
6
|
+
from typing import Any, Callable, Dict, NamedTuple, Optional, TYPE_CHECKING, Type, Union
|
|
7
7
|
|
|
8
8
|
from metaflow._vendor import click
|
|
9
9
|
|
|
@@ -14,6 +14,9 @@ from .exception import (
|
|
|
14
14
|
MetaflowException,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from .user_configs.config_parameters import ConfigValue
|
|
19
|
+
|
|
17
20
|
try:
|
|
18
21
|
# Python2
|
|
19
22
|
strtype = basestring
|
|
@@ -32,6 +35,7 @@ ParameterContext = NamedTuple(
|
|
|
32
35
|
("parameter_name", str),
|
|
33
36
|
("logger", Callable[..., None]),
|
|
34
37
|
("ds_type", str),
|
|
38
|
+
("configs", Optional["ConfigValue"]),
|
|
35
39
|
],
|
|
36
40
|
)
|
|
37
41
|
|
|
@@ -72,6 +76,16 @@ def flow_context(flow_cls):
|
|
|
72
76
|
context_proto = None
|
|
73
77
|
|
|
74
78
|
|
|
79
|
+
def replace_flow_context(flow_cls):
|
|
80
|
+
"""
|
|
81
|
+
Replace the current flow context with a new flow class. This is used
|
|
82
|
+
when we change the current flow class after having run user configuration functions
|
|
83
|
+
"""
|
|
84
|
+
current_flow.flow_cls_stack = current_flow.flow_cls_stack[1:]
|
|
85
|
+
current_flow.flow_cls_stack.insert(0, flow_cls)
|
|
86
|
+
current_flow.flow_cls = current_flow.flow_cls_stack[0]
|
|
87
|
+
|
|
88
|
+
|
|
75
89
|
class JSONTypeClass(click.ParamType):
|
|
76
90
|
name = "JSON"
|
|
77
91
|
|
|
@@ -210,12 +224,18 @@ class DeployTimeField(object):
|
|
|
210
224
|
def deploy_time_eval(value):
|
|
211
225
|
if isinstance(value, DeployTimeField):
|
|
212
226
|
return value(deploy_time=True)
|
|
227
|
+
elif isinstance(value, DelayedEvaluationParameter):
|
|
228
|
+
return value(return_str=True)
|
|
213
229
|
else:
|
|
214
230
|
return value
|
|
215
231
|
|
|
216
232
|
|
|
217
233
|
# this is called by cli.main
|
|
218
|
-
def set_parameter_context(flow_name, echo, datastore):
|
|
234
|
+
def set_parameter_context(flow_name, echo, datastore, configs):
|
|
235
|
+
from .user_configs.config_parameters import (
|
|
236
|
+
ConfigValue,
|
|
237
|
+
) # Prevent circular dependency
|
|
238
|
+
|
|
219
239
|
global context_proto
|
|
220
240
|
context_proto = ParameterContext(
|
|
221
241
|
flow_name=flow_name,
|
|
@@ -223,6 +243,7 @@ def set_parameter_context(flow_name, echo, datastore):
|
|
|
223
243
|
parameter_name=None,
|
|
224
244
|
logger=echo,
|
|
225
245
|
ds_type=datastore.TYPE,
|
|
246
|
+
configs=ConfigValue(dict(configs)),
|
|
226
247
|
)
|
|
227
248
|
|
|
228
249
|
|
|
@@ -279,7 +300,11 @@ class Parameter(object):
|
|
|
279
300
|
----------
|
|
280
301
|
name : str
|
|
281
302
|
User-visible parameter name.
|
|
282
|
-
default : str
|
|
303
|
+
default : Union[str, float, int, bool, Dict[str, Any],
|
|
304
|
+
Callable[
|
|
305
|
+
[ParameterContext], Union[str, float, int, bool, Dict[str, Any]]
|
|
306
|
+
],
|
|
307
|
+
], optional, default None
|
|
283
308
|
Default value for the parameter. Use a special `JSONType` class to
|
|
284
309
|
indicate that the value must be a valid JSON object. A function
|
|
285
310
|
implies that the parameter corresponds to a *deploy-time parameter*.
|
|
@@ -288,15 +313,19 @@ class Parameter(object):
|
|
|
288
313
|
If `default` is not specified, define the parameter type. Specify
|
|
289
314
|
one of `str`, `float`, `int`, `bool`, or `JSONType`. If None, defaults
|
|
290
315
|
to the type of `default` or `str` if none specified.
|
|
291
|
-
help : str, optional
|
|
316
|
+
help : str, optional, default None
|
|
292
317
|
Help text to show in `run --help`.
|
|
293
|
-
required : bool, default
|
|
294
|
-
Require that the user specified a value for the parameter.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
318
|
+
required : bool, optional, default None
|
|
319
|
+
Require that the user specified a value for the parameter. Note that if
|
|
320
|
+
a default is provide, the required flag is ignored.
|
|
321
|
+
A value of None is equivalent to False.
|
|
322
|
+
show_default : bool, optional, default None
|
|
323
|
+
If True, show the default value in the help text. A value of None is equivalent
|
|
324
|
+
to True.
|
|
298
325
|
"""
|
|
299
326
|
|
|
327
|
+
IS_CONFIG_PARAMETER = False
|
|
328
|
+
|
|
300
329
|
def __init__(
|
|
301
330
|
self,
|
|
302
331
|
name: str,
|
|
@@ -307,31 +336,60 @@ class Parameter(object):
|
|
|
307
336
|
int,
|
|
308
337
|
bool,
|
|
309
338
|
Dict[str, Any],
|
|
310
|
-
Callable[
|
|
339
|
+
Callable[
|
|
340
|
+
[ParameterContext], Union[str, float, int, bool, Dict[str, Any]]
|
|
341
|
+
],
|
|
311
342
|
]
|
|
312
343
|
] = None,
|
|
313
344
|
type: Optional[
|
|
314
345
|
Union[Type[str], Type[float], Type[int], Type[bool], JSONTypeClass]
|
|
315
346
|
] = None,
|
|
316
347
|
help: Optional[str] = None,
|
|
317
|
-
required: bool =
|
|
318
|
-
show_default: bool =
|
|
348
|
+
required: Optional[bool] = None,
|
|
349
|
+
show_default: Optional[bool] = None,
|
|
319
350
|
**kwargs: Dict[str, Any]
|
|
320
351
|
):
|
|
321
352
|
self.name = name
|
|
322
353
|
self.kwargs = kwargs
|
|
323
|
-
|
|
354
|
+
self._override_kwargs = {
|
|
324
355
|
"default": default,
|
|
325
356
|
"type": type,
|
|
326
357
|
"help": help,
|
|
327
358
|
"required": required,
|
|
328
359
|
"show_default": show_default,
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
def init(self, ignore_errors=False):
|
|
363
|
+
# Prevent circular import
|
|
364
|
+
from .user_configs.config_parameters import (
|
|
365
|
+
resolve_delayed_evaluator,
|
|
366
|
+
unpack_delayed_evaluator,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Resolve any value from configurations
|
|
370
|
+
self.kwargs = unpack_delayed_evaluator(self.kwargs, ignore_errors=ignore_errors)
|
|
371
|
+
# Do it one item at a time so errors are ignored at that level (as opposed to
|
|
372
|
+
# at the entire kwargs leve)
|
|
373
|
+
self.kwargs = {
|
|
374
|
+
k: resolve_delayed_evaluator(v, ignore_errors=ignore_errors)
|
|
375
|
+
for k, v in self.kwargs.items()
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
# This was the behavior before configs: values specified in args would override
|
|
379
|
+
# stuff in kwargs which is what we implement here as well
|
|
380
|
+
for key, value in self._override_kwargs.items():
|
|
381
|
+
if value is not None:
|
|
382
|
+
self.kwargs[key] = resolve_delayed_evaluator(
|
|
383
|
+
value, ignore_errors=ignore_errors
|
|
384
|
+
)
|
|
385
|
+
# Set two default values if no-one specified them
|
|
386
|
+
self.kwargs.setdefault("required", False)
|
|
387
|
+
self.kwargs.setdefault("show_default", True)
|
|
388
|
+
|
|
389
|
+
# Continue processing kwargs free of any configuration values :)
|
|
332
390
|
|
|
333
391
|
# TODO: check that the type is one of the supported types
|
|
334
|
-
param_type = self.kwargs["type"] = self._get_type(kwargs)
|
|
392
|
+
param_type = self.kwargs["type"] = self._get_type(self.kwargs)
|
|
335
393
|
|
|
336
394
|
reserved_params = [
|
|
337
395
|
"params",
|
|
@@ -356,23 +414,27 @@ class Parameter(object):
|
|
|
356
414
|
raise MetaflowException(
|
|
357
415
|
"Parameter name '%s' is a reserved "
|
|
358
416
|
"word. Please use a different "
|
|
359
|
-
"name for your parameter." % (name)
|
|
417
|
+
"name for your parameter." % (self.name)
|
|
360
418
|
)
|
|
361
419
|
|
|
362
420
|
# make sure the user is not trying to pass a function in one of the
|
|
363
421
|
# fields that don't support function-values yet
|
|
364
422
|
for field in ("show_default", "separator", "required"):
|
|
365
|
-
if callable(kwargs.get(field)):
|
|
423
|
+
if callable(self.kwargs.get(field)):
|
|
366
424
|
raise MetaflowException(
|
|
367
425
|
"Parameter *%s*: Field '%s' cannot "
|
|
368
|
-
"have a function as its value" % (name, field)
|
|
426
|
+
"have a function as its value" % (self.name, field)
|
|
369
427
|
)
|
|
370
428
|
|
|
371
429
|
# default can be defined as a function
|
|
372
430
|
default_field = self.kwargs.get("default")
|
|
373
431
|
if callable(default_field) and not isinstance(default_field, DeployTimeField):
|
|
374
432
|
self.kwargs["default"] = DeployTimeField(
|
|
375
|
-
|
|
433
|
+
self.name,
|
|
434
|
+
param_type,
|
|
435
|
+
"default",
|
|
436
|
+
self.kwargs["default"],
|
|
437
|
+
return_str=True,
|
|
376
438
|
)
|
|
377
439
|
|
|
378
440
|
# note that separator doesn't work with DeployTimeFields unless you
|
|
@@ -381,7 +443,7 @@ class Parameter(object):
|
|
|
381
443
|
if self.separator and not self.is_string_type:
|
|
382
444
|
raise MetaflowException(
|
|
383
445
|
"Parameter *%s*: Separator is only allowed "
|
|
384
|
-
"for string parameters." % name
|
|
446
|
+
"for string parameters." % self.name
|
|
385
447
|
)
|
|
386
448
|
|
|
387
449
|
def __repr__(self):
|
|
@@ -437,7 +499,9 @@ def add_custom_parameters(deploy_mode=False):
|
|
|
437
499
|
flow_cls = getattr(current_flow, "flow_cls", None)
|
|
438
500
|
if flow_cls is None:
|
|
439
501
|
return cmd
|
|
440
|
-
parameters = [
|
|
502
|
+
parameters = [
|
|
503
|
+
p for _, p in flow_cls._get_parameters() if not p.IS_CONFIG_PARAMETER
|
|
504
|
+
]
|
|
441
505
|
for arg in parameters[::-1]:
|
|
442
506
|
kwargs = arg.option_kwargs(deploy_mode)
|
|
443
507
|
cmd.params.insert(0, click.Option(("--" + arg.name,), **kwargs))
|
metaflow/plugins/__init__.py
CHANGED
|
@@ -164,6 +164,10 @@ def get_plugin_cli():
|
|
|
164
164
|
return resolve_plugins("cli")
|
|
165
165
|
|
|
166
166
|
|
|
167
|
+
def get_plugin_cli_path():
|
|
168
|
+
return resolve_plugins("cli", path_only=True)
|
|
169
|
+
|
|
170
|
+
|
|
167
171
|
STEP_DECORATORS = resolve_plugins("step_decorator")
|
|
168
172
|
FLOW_DECORATORS = resolve_plugins("flow_decorator")
|
|
169
173
|
ENVIRONMENTS = resolve_plugins("environment")
|
|
@@ -283,6 +283,7 @@ def make_flow(
|
|
|
283
283
|
):
|
|
284
284
|
# Attach @kubernetes.
|
|
285
285
|
decorators._attach_decorators(obj.flow, [KubernetesDecorator.name])
|
|
286
|
+
decorators._init(obj.flow)
|
|
286
287
|
|
|
287
288
|
decorators._init_step_decorators(
|
|
288
289
|
obj.flow, obj.graph, obj.environment, obj.flow_datastore, obj.logger
|
|
@@ -61,6 +61,7 @@ from metaflow.plugins.kubernetes.kubernetes import (
|
|
|
61
61
|
)
|
|
62
62
|
from metaflow.plugins.kubernetes.kubernetes_jobsets import KubernetesArgoJobSet
|
|
63
63
|
from metaflow.unbounded_foreach import UBF_CONTROL, UBF_TASK
|
|
64
|
+
from metaflow.user_configs.config_options import ConfigInput
|
|
64
65
|
from metaflow.util import (
|
|
65
66
|
compress_list,
|
|
66
67
|
dict_to_cli_options,
|
|
@@ -169,6 +170,7 @@ class ArgoWorkflows(object):
|
|
|
169
170
|
self.enable_heartbeat_daemon = enable_heartbeat_daemon
|
|
170
171
|
self.enable_error_msg_capture = enable_error_msg_capture
|
|
171
172
|
self.parameters = self._process_parameters()
|
|
173
|
+
self.config_parameters = self._process_config_parameters()
|
|
172
174
|
self.triggers, self.trigger_options = self._process_triggers()
|
|
173
175
|
self._schedule, self._timezone = self._get_schedule()
|
|
174
176
|
|
|
@@ -456,6 +458,10 @@ class ArgoWorkflows(object):
|
|
|
456
458
|
"case-insensitive." % param.name
|
|
457
459
|
)
|
|
458
460
|
seen.add(norm)
|
|
461
|
+
# NOTE: We skip config parameters as these do not have dynamic values,
|
|
462
|
+
# and need to be treated differently.
|
|
463
|
+
if param.IS_CONFIG_PARAMETER:
|
|
464
|
+
continue
|
|
459
465
|
|
|
460
466
|
extra_attrs = {}
|
|
461
467
|
if param.kwargs.get("type") == JSONType:
|
|
@@ -489,6 +495,7 @@ class ArgoWorkflows(object):
|
|
|
489
495
|
# execution - which needs to be fixed imminently.
|
|
490
496
|
if not is_required or default_value is not None:
|
|
491
497
|
default_value = json.dumps(default_value)
|
|
498
|
+
|
|
492
499
|
parameters[param.name] = dict(
|
|
493
500
|
name=param.name,
|
|
494
501
|
value=default_value,
|
|
@@ -499,6 +506,27 @@ class ArgoWorkflows(object):
|
|
|
499
506
|
)
|
|
500
507
|
return parameters
|
|
501
508
|
|
|
509
|
+
def _process_config_parameters(self):
|
|
510
|
+
parameters = []
|
|
511
|
+
seen = set()
|
|
512
|
+
for var, param in self.flow._get_parameters():
|
|
513
|
+
if not param.IS_CONFIG_PARAMETER:
|
|
514
|
+
continue
|
|
515
|
+
# Throw an exception if the parameter is specified twice.
|
|
516
|
+
norm = param.name.lower()
|
|
517
|
+
if norm in seen:
|
|
518
|
+
raise MetaflowException(
|
|
519
|
+
"Parameter *%s* is specified twice. "
|
|
520
|
+
"Note that parameter names are "
|
|
521
|
+
"case-insensitive." % param.name
|
|
522
|
+
)
|
|
523
|
+
seen.add(norm)
|
|
524
|
+
|
|
525
|
+
parameters.append(
|
|
526
|
+
dict(name=param.name, kv_name=ConfigInput.make_key_name(param.name))
|
|
527
|
+
)
|
|
528
|
+
return parameters
|
|
529
|
+
|
|
502
530
|
def _process_triggers(self):
|
|
503
531
|
# Impute triggers for Argo Workflow Template specified through @trigger and
|
|
504
532
|
# @trigger_on_finish decorators
|
|
@@ -521,8 +549,13 @@ class ArgoWorkflows(object):
|
|
|
521
549
|
# convert them to lower case since Metaflow parameters are case
|
|
522
550
|
# insensitive.
|
|
523
551
|
seen = set()
|
|
552
|
+
# NOTE: We skip config parameters as their values can not be set through event payloads
|
|
524
553
|
params = set(
|
|
525
|
-
[
|
|
554
|
+
[
|
|
555
|
+
param.name.lower()
|
|
556
|
+
for var, param in self.flow._get_parameters()
|
|
557
|
+
if not param.IS_CONFIG_PARAMETER
|
|
558
|
+
]
|
|
526
559
|
)
|
|
527
560
|
trigger_deco = self.flow._flow_decorators.get("trigger")[0]
|
|
528
561
|
trigger_deco.format_deploytime_value()
|
|
@@ -1721,6 +1754,13 @@ class ArgoWorkflows(object):
|
|
|
1721
1754
|
metaflow_version["production_token"] = self.production_token
|
|
1722
1755
|
env["METAFLOW_VERSION"] = json.dumps(metaflow_version)
|
|
1723
1756
|
|
|
1757
|
+
# map config values
|
|
1758
|
+
cfg_env = {
|
|
1759
|
+
param["name"]: param["kv_name"] for param in self.config_parameters
|
|
1760
|
+
}
|
|
1761
|
+
if cfg_env:
|
|
1762
|
+
env["METAFLOW_FLOW_CONFIG_VALUE"] = json.dumps(cfg_env)
|
|
1763
|
+
|
|
1724
1764
|
# Set the template inputs and outputs for passing state. Very simply,
|
|
1725
1765
|
# the container template takes in input-paths as input and outputs
|
|
1726
1766
|
# the task-id (which feeds in as input-paths to the subsequent task).
|
|
@@ -470,6 +470,7 @@ def make_flow(
|
|
|
470
470
|
decorators._attach_decorators(
|
|
471
471
|
obj.flow, [KubernetesDecorator.name, EnvironmentDecorator.name]
|
|
472
472
|
)
|
|
473
|
+
decorators._init(obj.flow)
|
|
473
474
|
|
|
474
475
|
decorators._init_step_decorators(
|
|
475
476
|
obj.flow, obj.graph, obj.environment, obj.flow_datastore, obj.logger
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import json
|
|
3
|
+
import time
|
|
3
4
|
import tempfile
|
|
4
5
|
from typing import ClassVar, Optional
|
|
5
6
|
|
|
@@ -97,6 +98,7 @@ class ArgoWorkflowsTriggeredRun(TriggeredRun):
|
|
|
97
98
|
)
|
|
98
99
|
|
|
99
100
|
command_obj = self.deployer.spm.get(pid)
|
|
101
|
+
command_obj.sync_wait()
|
|
100
102
|
return command_obj.process.returncode == 0
|
|
101
103
|
|
|
102
104
|
def unsuspend(self, **kwargs) -> bool:
|
|
@@ -131,6 +133,7 @@ class ArgoWorkflowsTriggeredRun(TriggeredRun):
|
|
|
131
133
|
)
|
|
132
134
|
|
|
133
135
|
command_obj = self.deployer.spm.get(pid)
|
|
136
|
+
command_obj.sync_wait()
|
|
134
137
|
return command_obj.process.returncode == 0
|
|
135
138
|
|
|
136
139
|
def terminate(self, **kwargs) -> bool:
|
|
@@ -165,8 +168,50 @@ class ArgoWorkflowsTriggeredRun(TriggeredRun):
|
|
|
165
168
|
)
|
|
166
169
|
|
|
167
170
|
command_obj = self.deployer.spm.get(pid)
|
|
171
|
+
command_obj.sync_wait()
|
|
168
172
|
return command_obj.process.returncode == 0
|
|
169
173
|
|
|
174
|
+
def wait_for_completion(self, timeout: Optional[int] = None):
|
|
175
|
+
"""
|
|
176
|
+
Wait for the workflow to complete or timeout.
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
timeout : int, optional, default None
|
|
181
|
+
Maximum time in seconds to wait for workflow completion.
|
|
182
|
+
If None, waits indefinitely.
|
|
183
|
+
|
|
184
|
+
Raises
|
|
185
|
+
------
|
|
186
|
+
TimeoutError
|
|
187
|
+
If the workflow does not complete within the specified timeout period.
|
|
188
|
+
"""
|
|
189
|
+
start_time = time.time()
|
|
190
|
+
check_interval = 5
|
|
191
|
+
while self.is_running:
|
|
192
|
+
if timeout is not None and (time.time() - start_time) > timeout:
|
|
193
|
+
raise TimeoutError(
|
|
194
|
+
"Workflow did not complete within specified timeout."
|
|
195
|
+
)
|
|
196
|
+
time.sleep(check_interval)
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def is_running(self):
|
|
200
|
+
"""
|
|
201
|
+
Check if the workflow is currently running.
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
bool
|
|
206
|
+
True if the workflow status is either 'Pending' or 'Running',
|
|
207
|
+
False otherwise.
|
|
208
|
+
"""
|
|
209
|
+
workflow_status = self.status
|
|
210
|
+
# full list of all states present here:
|
|
211
|
+
# https://github.com/argoproj/argo-workflows/blob/main/pkg/apis/workflow/v1alpha1/workflow_types.go#L54
|
|
212
|
+
# we only consider non-terminal states to determine if the workflow has not finished
|
|
213
|
+
return workflow_status is not None and workflow_status in ["Pending", "Running"]
|
|
214
|
+
|
|
170
215
|
@property
|
|
171
216
|
def status(self) -> Optional[str]:
|
|
172
217
|
"""
|
|
@@ -319,6 +364,7 @@ class ArgoWorkflowsDeployedFlow(DeployedFlow):
|
|
|
319
364
|
)
|
|
320
365
|
|
|
321
366
|
command_obj = self.deployer.spm.get(pid)
|
|
367
|
+
command_obj.sync_wait()
|
|
322
368
|
return command_obj.process.returncode == 0
|
|
323
369
|
|
|
324
370
|
def trigger(self, **kwargs) -> ArgoWorkflowsTriggeredRun:
|
|
@@ -361,7 +407,7 @@ class ArgoWorkflowsDeployedFlow(DeployedFlow):
|
|
|
361
407
|
content = handle_timeout(
|
|
362
408
|
attribute_file_fd, command_obj, self.deployer.file_read_timeout
|
|
363
409
|
)
|
|
364
|
-
|
|
410
|
+
command_obj.sync_wait()
|
|
365
411
|
if command_obj.process.returncode == 0:
|
|
366
412
|
return ArgoWorkflowsTriggeredRun(
|
|
367
413
|
deployer=self.deployer, content=content
|
|
@@ -138,8 +138,8 @@ class BatchDecorator(StepDecorator):
|
|
|
138
138
|
supports_conda_environment = True
|
|
139
139
|
target_platform = "linux-64"
|
|
140
140
|
|
|
141
|
-
def
|
|
142
|
-
super(BatchDecorator, self).
|
|
141
|
+
def init(self):
|
|
142
|
+
super(BatchDecorator, self).init()
|
|
143
143
|
|
|
144
144
|
# If no docker image is explicitly specified, impute a default image.
|
|
145
145
|
if not self.attributes["image"]:
|