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
@@ -0,0 +1,548 @@
1
+ import importlib
2
+ import json
3
+ import os
4
+
5
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
6
+
7
+ from metaflow._vendor import click
8
+ from metaflow.debug import debug
9
+
10
+ from .config_parameters import CONFIG_FILE, ConfigValue
11
+ from ..exception import MetaflowException, MetaflowInternalError
12
+ from ..parameters import DeployTimeField, ParameterContext, current_flow
13
+ from ..util import get_username
14
+
15
+
16
+ _CONVERT_PREFIX = "@!c!@:"
17
+ _DEFAULT_PREFIX = "@!d!@:"
18
+ _NO_FILE = "@!n!@:"
19
+
20
+ _CONVERTED_DEFAULT = _CONVERT_PREFIX + _DEFAULT_PREFIX
21
+ _CONVERTED_NO_FILE = _CONVERT_PREFIX + _NO_FILE
22
+ _CONVERTED_DEFAULT_NO_FILE = _CONVERTED_DEFAULT + _NO_FILE
23
+
24
+
25
+ def _load_config_values(info_file: Optional[str] = None) -> Optional[Dict[Any, Any]]:
26
+ if info_file is None:
27
+ info_file = os.path.basename(CONFIG_FILE)
28
+ try:
29
+ with open(info_file, encoding="utf-8") as contents:
30
+ return json.load(contents).get("user_configs", {})
31
+ except IOError:
32
+ return None
33
+
34
+
35
+ class ConvertPath(click.Path):
36
+ name = "ConvertPath"
37
+
38
+ def convert(self, value, param, ctx):
39
+ if isinstance(value, str) and value.startswith(_CONVERT_PREFIX):
40
+ return value
41
+ is_default = False
42
+ if value and value.startswith(_DEFAULT_PREFIX):
43
+ is_default = True
44
+ value = value[len(_DEFAULT_PREFIX) :]
45
+ value = super().convert(value, param, ctx)
46
+ return self.convert_value(value, is_default)
47
+
48
+ @staticmethod
49
+ def mark_as_default(value):
50
+ if value is None:
51
+ return None
52
+ return _DEFAULT_PREFIX + str(value)
53
+
54
+ @staticmethod
55
+ def convert_value(value, is_default):
56
+ default_str = _DEFAULT_PREFIX if is_default else ""
57
+ if value is None:
58
+ return None
59
+ try:
60
+ with open(value, "r", encoding="utf-8") as f:
61
+ content = f.read()
62
+ except OSError:
63
+ return _CONVERT_PREFIX + default_str + _NO_FILE + value
64
+ return _CONVERT_PREFIX + default_str + content
65
+
66
+
67
+ class ConvertDictOrStr(click.ParamType):
68
+ name = "ConvertDictOrStr"
69
+
70
+ def convert(self, value, param, ctx):
71
+ is_default = False
72
+ if isinstance(value, str):
73
+ if value.startswith(_CONVERT_PREFIX):
74
+ return value
75
+ if value.startswith(_DEFAULT_PREFIX):
76
+ is_default = True
77
+ value = value[len(_DEFAULT_PREFIX) :]
78
+
79
+ return self.convert_value(value, is_default)
80
+
81
+ @staticmethod
82
+ def convert_value(value, is_default):
83
+ default_str = _DEFAULT_PREFIX if is_default else ""
84
+ if value is None:
85
+ return None
86
+
87
+ if isinstance(value, dict):
88
+ return _CONVERT_PREFIX + default_str + json.dumps(value)
89
+
90
+ if value.startswith(_CONVERT_PREFIX):
91
+ return value
92
+
93
+ return _CONVERT_PREFIX + default_str + value
94
+
95
+ @staticmethod
96
+ def mark_as_default(value):
97
+ if value is None:
98
+ return None
99
+ if isinstance(value, dict):
100
+ return _DEFAULT_PREFIX + json.dumps(value)
101
+ return _DEFAULT_PREFIX + str(value)
102
+
103
+
104
+ class MultipleTuple(click.Tuple):
105
+ # Small wrapper around a click.Tuple to allow the environment variable for
106
+ # configurations to be a JSON string. Otherwise the default behavior is splitting
107
+ # by whitespace which is totally not what we want
108
+ # You can now pass multiple configuration options through an environment variable
109
+ # using something like:
110
+ # METAFLOW_FLOW_CONFIG_VALUE='{"config1": {"key0": "value0"}, "config2": {"key1": "value1"}}'
111
+ # or METAFLOW_FLOW_CONFIG='{"config1": "file1", "config2": "file2"}'
112
+
113
+ def split_envvar_value(self, rv):
114
+ loaded = json.loads(rv)
115
+ return list(
116
+ item if isinstance(item, str) else json.dumps(item)
117
+ for pair in loaded.items()
118
+ for item in pair
119
+ )
120
+
121
+
122
+ class ConfigInput:
123
+ # ConfigInput is an internal class responsible for processing all the --config and
124
+ # --config-value options.
125
+ # It gathers information from the --local-config-file (to figure out
126
+ # where options are stored) and is also responsible for processing any `--config` or
127
+ # `--config-value` options. Note that the process_configs function will be called
128
+ # *twice* (once for the configs and another for the config-values). This makes
129
+ # this function a little bit more tricky. We need to wait for both calls before
130
+ # being able to process anything.
131
+
132
+ # It will then store this information in the flow spec for use later in processing.
133
+ # It is stored in the flow spec to avoid being global to support the Runner.
134
+
135
+ loaded_configs = None # type: Optional[Dict[str, Dict[Any, Any]]]
136
+ config_file = None # type: Optional[str]
137
+
138
+ def __init__(
139
+ self,
140
+ req_configs: List[str],
141
+ defaults: Dict[str, Tuple[Union[str, Dict[Any, Any]], bool]],
142
+ parsers: Dict[str, Union[str, Callable[[str], Dict[Any, Any]]]],
143
+ ):
144
+ self._req_configs = set(req_configs)
145
+ self._defaults = defaults
146
+ self._parsers = parsers
147
+ self._path_values = None
148
+ self._value_values = None
149
+
150
+ @staticmethod
151
+ def make_key_name(name: str) -> str:
152
+ # Special mark to indicate that the configuration value is not content or a file
153
+ # name but a value that should be read in the config file (effectively where
154
+ # the value has already been materialized).
155
+ return "kv." + name.lower()
156
+
157
+ @classmethod
158
+ def set_config_file(cls, config_file: str):
159
+ cls.config_file = config_file
160
+
161
+ @classmethod
162
+ def get_config(cls, config_name: str) -> Optional[Dict[Any, Any]]:
163
+ if cls.loaded_configs is None:
164
+ all_configs = _load_config_values(cls.config_file)
165
+ if all_configs is None:
166
+ raise MetaflowException(
167
+ "Could not load expected configuration values "
168
+ "from the CONFIG_PARAMETERS file. This is a Metaflow bug. "
169
+ "Please contact support."
170
+ )
171
+ cls.loaded_configs = all_configs
172
+ return cls.loaded_configs.get(config_name, None)
173
+
174
+ def process_configs(
175
+ self,
176
+ flow_name: str,
177
+ param_name: str,
178
+ param_value: Dict[str, Optional[str]],
179
+ quiet: bool,
180
+ datastore: str,
181
+ click_obj: Optional[Any] = None,
182
+ ):
183
+ from ..cli import echo_always, echo_dev_null # Prevent circular import
184
+ from ..flowspec import _FlowState # Prevent circular import
185
+
186
+ flow_cls = getattr(current_flow, "flow_cls", None)
187
+ if flow_cls is None:
188
+ # This is an error
189
+ raise MetaflowInternalError(
190
+ "Config values should be processed for a FlowSpec"
191
+ )
192
+
193
+ # This function is called by click when processing all the --config and
194
+ # --config-value options.
195
+ # The value passed in is a list of tuples (name, value).
196
+ # Click will provide:
197
+ # - all the defaults if nothing is provided on the command line
198
+ # - provide *just* the passed in value if anything is provided on the command
199
+ # line.
200
+ #
201
+ # We need to get all config and config-value options and click will call this
202
+ # function twice. We will first get all the values on the command line and
203
+ # *then* merge with the defaults to form a full set of values.
204
+ # We therefore get a full set of values where:
205
+ # - the name will correspond to the configuration name
206
+ # - the value will be:
207
+ # - the default (including None if there is no default). If the default is
208
+ # not None, it will start with _CONVERTED_DEFAULT since Click will make
209
+ # the value go through ConvertPath or ConvertDictOrStr
210
+ # - the actual value passed through prefixed with _CONVERT_PREFIX
211
+
212
+ debug.userconf_exec(
213
+ "Processing configs for %s -- incoming values: %s"
214
+ % (param_name, str(param_value))
215
+ )
216
+
217
+ do_return = self._value_values is None and self._path_values is None
218
+ # We only keep around non default values. We could simplify by checking just one
219
+ # value and if it is default it means all are but this doesn't seem much more effort
220
+ # and is clearer
221
+ if param_name == "config_value":
222
+ self._value_values = {
223
+ k.lower(): v
224
+ for k, v in param_value
225
+ if v is not None and not v.startswith(_CONVERTED_DEFAULT)
226
+ }
227
+ else:
228
+ self._path_values = {
229
+ k.lower(): v
230
+ for k, v in param_value
231
+ if v is not None and not v.startswith(_CONVERTED_DEFAULT)
232
+ }
233
+ if do_return:
234
+ # One of values["value"] or values["path"] is None -- we are in the first
235
+ # go around
236
+ debug.userconf_exec("Incomplete config options; waiting for more")
237
+ return None
238
+
239
+ # The second go around, we process all the values and merge them.
240
+
241
+ # If we are processing options that start with kv., we know we are in a subprocess
242
+ # and ignore other stuff. In particular, environment variables used to pass
243
+ # down configurations (like METAFLOW_FLOW_CONFIG) could still be present and
244
+ # would cause an issue -- we can ignore those as the kv. values should trump
245
+ # everything else.
246
+ # NOTE: These are all *non default* keys
247
+ all_keys = set(self._value_values).union(self._path_values)
248
+
249
+ if all_keys and click_obj:
250
+ click_obj.has_cl_config_options = True
251
+ # Make sure we have at least some non default keys (we need some if we have
252
+ # all kv)
253
+ has_all_kv = all_keys and all(
254
+ self._value_values.get(k, "").startswith(_CONVERT_PREFIX + "kv.")
255
+ for k in all_keys
256
+ )
257
+
258
+ flow_cls._flow_state[_FlowState.CONFIGS] = {}
259
+ to_return = {}
260
+
261
+ if not has_all_kv:
262
+ # Check that the user didn't provide *both* a path and a value. Again, these
263
+ # are only user-provided (not defaults)
264
+ common_keys = set(self._value_values or []).intersection(
265
+ [k for k, v in self._path_values.items()] or []
266
+ )
267
+ if common_keys:
268
+ exc = click.UsageError(
269
+ "Cannot provide both a value and a file for the same configuration. "
270
+ "Found such values for '%s'" % "', '".join(common_keys)
271
+ )
272
+ if click_obj:
273
+ click_obj.delayed_config_exception = exc
274
+ return None
275
+ raise exc
276
+
277
+ all_values = dict(self._path_values)
278
+ all_values.update(self._value_values)
279
+
280
+ debug.userconf_exec("All config values: %s" % str(all_values))
281
+
282
+ merged_configs = {}
283
+ # Now look at everything (including defaults)
284
+ for name, (val, is_path) in self._defaults.items():
285
+ n = name.lower()
286
+ if n in all_values:
287
+ # We have the value provided by the user -- use that.
288
+ merged_configs[n] = all_values[n]
289
+ else:
290
+ # No value provided by the user -- use the default
291
+ if isinstance(val, DeployTimeField):
292
+ # This supports a default value that is a deploy-time field (similar
293
+ # to Parameter).)
294
+ # We will form our own context and pass it down -- note that you cannot
295
+ # use configs in the default value of configs as this introduces a bit
296
+ # of circularity. Note also that quiet and datastore are *eager*
297
+ # options so are available here.
298
+ param_ctx = ParameterContext(
299
+ flow_name=flow_name,
300
+ user_name=get_username(),
301
+ parameter_name=n,
302
+ logger=(echo_dev_null if quiet else echo_always),
303
+ ds_type=datastore,
304
+ configs=None,
305
+ )
306
+ val = val.fun(param_ctx)
307
+ if is_path:
308
+ # This is a file path
309
+ merged_configs[n] = ConvertPath.convert_value(val, True)
310
+ else:
311
+ # This is a value
312
+ merged_configs[n] = ConvertDictOrStr.convert_value(val, True)
313
+ else:
314
+ debug.userconf_exec("Fast path due to pre-processed values")
315
+ merged_configs = self._value_values
316
+
317
+ if click_obj:
318
+ click_obj.has_config_options = True
319
+
320
+ debug.userconf_exec("Configs merged with defaults: %s" % str(merged_configs))
321
+
322
+ missing_configs = set()
323
+ no_file = []
324
+ no_default_file = []
325
+ msgs = []
326
+ for name, val in merged_configs.items():
327
+ if val is None:
328
+ missing_configs.add(name)
329
+ continue
330
+ if val.startswith(_CONVERTED_DEFAULT_NO_FILE):
331
+ no_default_file.append(name)
332
+ continue
333
+ if val.startswith(_CONVERTED_NO_FILE):
334
+ no_file.append(name)
335
+ continue
336
+
337
+ val = val[len(_CONVERT_PREFIX) :] # Remove the _CONVERT_PREFIX
338
+ if val.startswith(_DEFAULT_PREFIX): # Remove the _DEFAULT_PREFIX if needed
339
+ val = val[len(_DEFAULT_PREFIX) :]
340
+ if val.startswith("kv."):
341
+ # This means to load it from a file
342
+ read_value = self.get_config(val[3:])
343
+ if read_value is None:
344
+ exc = click.UsageError(
345
+ "Could not find configuration '%s' in INFO file" % val
346
+ )
347
+ if click_obj:
348
+ click_obj.delayed_config_exception = exc
349
+ return None
350
+ raise exc
351
+ flow_cls._flow_state[_FlowState.CONFIGS][name] = read_value
352
+ to_return[name] = ConfigValue(read_value)
353
+ else:
354
+ if self._parsers[name]:
355
+ read_value = self._call_parser(self._parsers[name], val)
356
+ else:
357
+ try:
358
+ read_value = json.loads(val)
359
+ except json.JSONDecodeError as e:
360
+ msgs.append(
361
+ "configuration value for '%s' is not valid JSON: %s"
362
+ % (name, e)
363
+ )
364
+ continue
365
+ # TODO: Support YAML
366
+ flow_cls._flow_state[_FlowState.CONFIGS][name] = read_value
367
+ to_return[name] = ConfigValue(read_value)
368
+
369
+ reqs = missing_configs.intersection(self._req_configs)
370
+ for missing in reqs:
371
+ msgs.append("missing configuration for '%s'" % missing)
372
+ for missing in no_file:
373
+ msgs.append(
374
+ "configuration file '%s' could not be read for '%s'"
375
+ % (merged_configs[missing][len(_CONVERTED_NO_FILE) :], missing)
376
+ )
377
+ for missing in no_default_file:
378
+ msgs.append(
379
+ "default configuration file '%s' could not be read for '%s'"
380
+ % (merged_configs[missing][len(_CONVERTED_DEFAULT_NO_FILE) :], missing)
381
+ )
382
+ if msgs:
383
+ exc = click.UsageError(
384
+ "Bad values passed for configuration options: %s" % ", ".join(msgs)
385
+ )
386
+ if click_obj:
387
+ click_obj.delayed_config_exception = exc
388
+ return None
389
+ raise exc
390
+
391
+ debug.userconf_exec("Finalized configs: %s" % str(to_return))
392
+ return to_return
393
+
394
+ def process_configs_click(self, ctx, param, value):
395
+ return self.process_configs(
396
+ ctx.obj.flow.name,
397
+ param.name,
398
+ value,
399
+ ctx.params["quiet"],
400
+ ctx.params["datastore"],
401
+ click_obj=ctx.obj,
402
+ )
403
+
404
+ def __str__(self):
405
+ return repr(self)
406
+
407
+ def __repr__(self):
408
+ return "ConfigInput"
409
+
410
+ @staticmethod
411
+ def _call_parser(parser, val):
412
+ if isinstance(parser, str):
413
+ if len(parser) and parser[0] == ".":
414
+ parser = "metaflow" + parser
415
+ path, func = parser.rsplit(".", 1)
416
+ try:
417
+ func_module = importlib.import_module(path)
418
+ except ImportError as e:
419
+ raise ValueError("Cannot locate parser %s" % parser) from e
420
+ parser = getattr(func_module, func, None)
421
+ if parser is None or not callable(parser):
422
+ raise ValueError(
423
+ "Parser %s is either not part of %s or not a callable"
424
+ % (func, path)
425
+ )
426
+ return parser(val)
427
+
428
+
429
+ class LocalFileInput(click.Path):
430
+ # Small wrapper around click.Path to set the value from which to read configuration
431
+ # values. This is set immediately upon processing the --local-config-file
432
+ # option and will therefore then be available when processing any of the other
433
+ # --config options (which will call ConfigInput.process_configs
434
+ name = "LocalFileInput"
435
+
436
+ def convert(self, value, param, ctx):
437
+ v = super().convert(value, param, ctx)
438
+ ConfigInput.set_config_file(value)
439
+ return v
440
+
441
+ def __str__(self):
442
+ return repr(self)
443
+
444
+ def __repr__(self):
445
+ return "LocalFileInput"
446
+
447
+
448
+ def config_options_with_config_input(cmd):
449
+ help_strs = []
450
+ required_names = []
451
+ defaults = {}
452
+ config_seen = set()
453
+ parsers = {}
454
+ flow_cls = getattr(current_flow, "flow_cls", None)
455
+ if flow_cls is None:
456
+ return cmd, None
457
+
458
+ parameters = [p for _, p in flow_cls._get_parameters() if p.IS_CONFIG_PARAMETER]
459
+ # List all the configuration options
460
+ for arg in parameters[::-1]:
461
+ kwargs = arg.option_kwargs(False)
462
+ if arg.name.lower() in config_seen:
463
+ msg = (
464
+ "Multiple configurations use the same name '%s'. Note that names are "
465
+ "case-insensitive. Please change the "
466
+ "names of some of your configurations" % arg.name
467
+ )
468
+ raise MetaflowException(msg)
469
+ config_seen.add(arg.name.lower())
470
+ if kwargs["required"]:
471
+ required_names.append(arg.name)
472
+
473
+ defaults[arg.name.lower()] = (
474
+ arg.kwargs.get("default", None),
475
+ arg._default_is_file,
476
+ )
477
+ help_strs.append(" - %s: %s" % (arg.name.lower(), kwargs.get("help", "")))
478
+ parsers[arg.name.lower()] = arg.parser
479
+
480
+ if not config_seen:
481
+ # No configurations -- don't add anything; we set it to False so that it
482
+ # can be checked whether or not we called this.
483
+ return cmd, False
484
+
485
+ help_str = (
486
+ "Configuration options for the flow. "
487
+ "Multiple configurations can be specified. Cannot be used with resume."
488
+ )
489
+ help_str = "\n\n".join([help_str] + help_strs)
490
+ config_input = ConfigInput(required_names, defaults, parsers)
491
+ cb_func = config_input.process_configs_click
492
+
493
+ cmd.params.insert(
494
+ 0,
495
+ click.Option(
496
+ ["--config-value", "config_value"],
497
+ nargs=2,
498
+ multiple=True,
499
+ type=MultipleTuple([click.Choice(config_seen), ConvertDictOrStr()]),
500
+ callback=cb_func,
501
+ help=help_str,
502
+ envvar="METAFLOW_FLOW_CONFIG_VALUE",
503
+ show_default=False,
504
+ default=[
505
+ (
506
+ k,
507
+ (
508
+ ConvertDictOrStr.mark_as_default(v[0])
509
+ if not callable(v[0]) and not v[1]
510
+ else None
511
+ ),
512
+ )
513
+ for k, v in defaults.items()
514
+ ],
515
+ required=False,
516
+ ),
517
+ )
518
+ cmd.params.insert(
519
+ 0,
520
+ click.Option(
521
+ ["--config", "config_file"],
522
+ nargs=2,
523
+ multiple=True,
524
+ type=MultipleTuple([click.Choice(config_seen), ConvertPath()]),
525
+ callback=cb_func,
526
+ help=help_str,
527
+ envvar="METAFLOW_FLOW_CONFIG",
528
+ show_default=False,
529
+ default=[
530
+ (
531
+ k,
532
+ (
533
+ ConvertPath.mark_as_default(v[0])
534
+ if not callable(v[0]) and v[1]
535
+ else None
536
+ ),
537
+ )
538
+ for k, v in defaults.items()
539
+ ],
540
+ required=False,
541
+ ),
542
+ )
543
+ return cmd, config_input
544
+
545
+
546
+ def config_options(cmd):
547
+ cmd, _ = config_options_with_config_input(cmd)
548
+ return cmd