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.

Files changed (65) hide show
  1. metaflow/__init__.py +3 -0
  2. metaflow/cli.py +180 -718
  3. metaflow/cli_args.py +17 -0
  4. metaflow/cli_components/__init__.py +0 -0
  5. metaflow/cli_components/dump_cmd.py +96 -0
  6. metaflow/cli_components/init_cmd.py +51 -0
  7. metaflow/cli_components/run_cmds.py +360 -0
  8. metaflow/cli_components/step_cmd.py +189 -0
  9. metaflow/cli_components/utils.py +140 -0
  10. metaflow/cmd/develop/stub_generator.py +9 -2
  11. metaflow/datastore/flow_datastore.py +2 -2
  12. metaflow/decorators.py +63 -2
  13. metaflow/exception.py +8 -2
  14. metaflow/extension_support/plugins.py +41 -27
  15. metaflow/flowspec.py +175 -23
  16. metaflow/graph.py +28 -27
  17. metaflow/includefile.py +50 -22
  18. metaflow/lint.py +35 -20
  19. metaflow/metaflow_config.py +6 -1
  20. metaflow/package.py +17 -3
  21. metaflow/parameters.py +87 -23
  22. metaflow/plugins/__init__.py +4 -0
  23. metaflow/plugins/airflow/airflow_cli.py +1 -0
  24. metaflow/plugins/argo/argo_workflows.py +41 -1
  25. metaflow/plugins/argo/argo_workflows_cli.py +1 -0
  26. metaflow/plugins/argo/argo_workflows_deployer_objects.py +47 -1
  27. metaflow/plugins/aws/batch/batch_decorator.py +2 -2
  28. metaflow/plugins/aws/secrets_manager/aws_secrets_manager_secrets_provider.py +13 -10
  29. metaflow/plugins/aws/step_functions/step_functions.py +32 -0
  30. metaflow/plugins/aws/step_functions/step_functions_cli.py +1 -0
  31. metaflow/plugins/aws/step_functions/step_functions_deployer_objects.py +3 -0
  32. metaflow/plugins/cards/card_creator.py +1 -0
  33. metaflow/plugins/cards/card_decorator.py +46 -8
  34. metaflow/plugins/datatools/s3/s3op.py +3 -3
  35. metaflow/plugins/kubernetes/kubernetes_cli.py +1 -1
  36. metaflow/plugins/kubernetes/kubernetes_decorator.py +2 -2
  37. metaflow/plugins/pypi/bootstrap.py +196 -61
  38. metaflow/plugins/pypi/conda_decorator.py +20 -10
  39. metaflow/plugins/pypi/conda_environment.py +76 -21
  40. metaflow/plugins/pypi/micromamba.py +42 -15
  41. metaflow/plugins/pypi/pip.py +8 -3
  42. metaflow/plugins/pypi/pypi_decorator.py +11 -9
  43. metaflow/plugins/timeout_decorator.py +2 -2
  44. metaflow/runner/click_api.py +240 -50
  45. metaflow/runner/deployer.py +1 -1
  46. metaflow/runner/deployer_impl.py +8 -3
  47. metaflow/runner/metaflow_runner.py +10 -2
  48. metaflow/runner/nbdeploy.py +2 -0
  49. metaflow/runner/nbrun.py +1 -1
  50. metaflow/runner/subprocess_manager.py +3 -1
  51. metaflow/runner/utils.py +41 -19
  52. metaflow/runtime.py +111 -73
  53. metaflow/sidecar/sidecar_worker.py +1 -1
  54. metaflow/user_configs/__init__.py +0 -0
  55. metaflow/user_configs/config_decorators.py +563 -0
  56. metaflow/user_configs/config_options.py +548 -0
  57. metaflow/user_configs/config_parameters.py +405 -0
  58. metaflow/util.py +17 -0
  59. metaflow/version.py +1 -1
  60. {ob_metaflow-2.12.36.3.dist-info → ob_metaflow-2.13.0.1.dist-info}/METADATA +3 -2
  61. {ob_metaflow-2.12.36.3.dist-info → ob_metaflow-2.13.0.1.dist-info}/RECORD +65 -55
  62. {ob_metaflow-2.12.36.3.dist-info → ob_metaflow-2.13.0.1.dist-info}/LICENSE +0 -0
  63. {ob_metaflow-2.12.36.3.dist-info → ob_metaflow-2.13.0.1.dist-info}/WHEEL +0 -0
  64. {ob_metaflow-2.12.36.3.dist-info → ob_metaflow-2.13.0.1.dist-info}/entry_points.txt +0 -0
  65. {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(msg.format(node, step=unknown[0]), node.tail_next_lineno)
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(msg.format(path), node.tail_next_lineno)
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(msg0.format(roots=roots))
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(msg.format(node, join=joins[0]))
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(msg.format(node.out_funcs[0]))
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(msg.format(node, node2))
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)
@@ -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(json.dumps(env).encode("utf-8"))
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 or float or int or bool or `JSONType` or a function.
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 False
294
- Require that the user specified a value for the parameter.
295
- `required=True` implies that the `default` is not used.
296
- show_default : bool, default True
297
- If True, show the default value in the help text.
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[[], Union[str, float, int, bool, Dict[str, Any]]],
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 = False,
318
- show_default: bool = True,
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
- for k, v in {
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
- }.items():
330
- if v is not None:
331
- self.kwargs[k] = v
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
- name, param_type, "default", self.kwargs["default"], return_str=True
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 = [p for _, p in flow_cls._get_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))
@@ -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
- [param.name.lower() for var, param in self.flow._get_parameters()]
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 __init__(self, attributes=None, statically_defined=False):
142
- super(BatchDecorator, self).__init__(attributes, statically_defined)
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"]: