metaflow 2.12.39__py2.py3-none-any.whl → 2.13.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.
- metaflow/__init__.py +1 -1
- metaflow/cli.py +111 -36
- metaflow/cli_args.py +2 -2
- metaflow/cli_components/run_cmds.py +3 -1
- metaflow/datastore/flow_datastore.py +2 -2
- metaflow/exception.py +8 -2
- metaflow/flowspec.py +48 -36
- metaflow/graph.py +28 -27
- metaflow/includefile.py +2 -2
- metaflow/lint.py +35 -20
- metaflow/metadata_provider/heartbeat.py +23 -8
- metaflow/metaflow_config.py +7 -0
- metaflow/parameters.py +11 -4
- metaflow/plugins/argo/argo_client.py +0 -2
- metaflow/plugins/argo/argo_workflows.py +86 -104
- metaflow/plugins/argo/argo_workflows_cli.py +0 -1
- metaflow/plugins/argo/argo_workflows_decorator.py +2 -4
- metaflow/plugins/argo/argo_workflows_deployer_objects.py +42 -0
- metaflow/plugins/argo/jobset_input_paths.py +0 -1
- metaflow/plugins/aws/aws_utils.py +6 -1
- metaflow/plugins/aws/batch/batch_client.py +1 -3
- metaflow/plugins/aws/batch/batch_decorator.py +11 -11
- metaflow/plugins/aws/secrets_manager/aws_secrets_manager_secrets_provider.py +13 -10
- metaflow/plugins/aws/step_functions/dynamo_db_client.py +0 -3
- metaflow/plugins/aws/step_functions/production_token.py +1 -1
- metaflow/plugins/aws/step_functions/step_functions.py +1 -1
- metaflow/plugins/aws/step_functions/step_functions_cli.py +0 -1
- metaflow/plugins/aws/step_functions/step_functions_decorator.py +0 -1
- metaflow/plugins/aws/step_functions/step_functions_deployer_objects.py +0 -1
- metaflow/plugins/cards/card_creator.py +1 -0
- metaflow/plugins/cards/card_decorator.py +46 -8
- metaflow/plugins/kubernetes/kube_utils.py +55 -1
- metaflow/plugins/kubernetes/kubernetes.py +33 -80
- metaflow/plugins/kubernetes/kubernetes_cli.py +22 -5
- metaflow/plugins/kubernetes/kubernetes_decorator.py +49 -2
- metaflow/plugins/kubernetes/kubernetes_job.py +3 -6
- metaflow/plugins/kubernetes/kubernetes_jobsets.py +22 -5
- metaflow/plugins/pypi/bootstrap.py +249 -81
- metaflow/plugins/pypi/conda_environment.py +83 -27
- metaflow/plugins/pypi/micromamba.py +77 -36
- metaflow/plugins/pypi/pip.py +9 -6
- metaflow/plugins/pypi/utils.py +4 -2
- metaflow/runner/click_api.py +175 -39
- metaflow/runner/deployer_impl.py +6 -1
- metaflow/runner/metaflow_runner.py +6 -1
- metaflow/runner/utils.py +5 -0
- metaflow/user_configs/config_options.py +87 -34
- metaflow/user_configs/config_parameters.py +44 -25
- metaflow/util.py +2 -2
- metaflow/version.py +1 -1
- {metaflow-2.12.39.dist-info → metaflow-2.13.1.dist-info}/METADATA +2 -2
- {metaflow-2.12.39.dist-info → metaflow-2.13.1.dist-info}/RECORD +56 -56
- {metaflow-2.12.39.dist-info → metaflow-2.13.1.dist-info}/WHEEL +1 -1
- {metaflow-2.12.39.dist-info → metaflow-2.13.1.dist-info}/LICENSE +0 -0
- {metaflow-2.12.39.dist-info → metaflow-2.13.1.dist-info}/entry_points.txt +0 -0
- {metaflow-2.12.39.dist-info → metaflow-2.13.1.dist-info}/top_level.txt +0 -0
metaflow/runner/click_api.py
CHANGED
@@ -18,6 +18,7 @@ import json
|
|
18
18
|
from collections import OrderedDict
|
19
19
|
from typing import Any, Callable, Dict, List, Optional
|
20
20
|
from typing import OrderedDict as TOrderedDict
|
21
|
+
from typing import Tuple as TTuple
|
21
22
|
from typing import Union
|
22
23
|
|
23
24
|
from metaflow import FlowSpec, Parameter
|
@@ -38,8 +39,16 @@ from metaflow._vendor.typeguard import TypeCheckError, check_type
|
|
38
39
|
from metaflow.decorators import add_decorator_options
|
39
40
|
from metaflow.exception import MetaflowException
|
40
41
|
from metaflow.includefile import FilePathClass
|
42
|
+
from metaflow.metaflow_config import CLICK_API_PROCESS_CONFIG
|
41
43
|
from metaflow.parameters import JSONTypeClass, flow_context
|
42
|
-
from metaflow.user_configs.config_options import
|
44
|
+
from metaflow.user_configs.config_options import (
|
45
|
+
ConfigValue,
|
46
|
+
ConvertDictOrStr,
|
47
|
+
ConvertPath,
|
48
|
+
LocalFileInput,
|
49
|
+
MultipleTuple,
|
50
|
+
config_options_with_config_input,
|
51
|
+
)
|
43
52
|
|
44
53
|
# Define a recursive type alias for JSON
|
45
54
|
JSON = Union[Dict[str, "JSON"], List["JSON"], str, int, float, bool, None]
|
@@ -58,6 +67,7 @@ click_to_python_types = {
|
|
58
67
|
JSONTypeClass: JSON,
|
59
68
|
FilePathClass: str,
|
60
69
|
LocalFileInput: str,
|
70
|
+
MultipleTuple: TTuple[str, Union[JSON, ConfigValue]],
|
61
71
|
}
|
62
72
|
|
63
73
|
|
@@ -68,7 +78,7 @@ def _method_sanity_check(
|
|
68
78
|
defaults: TOrderedDict[str, Any],
|
69
79
|
**kwargs
|
70
80
|
) -> Dict[str, Any]:
|
71
|
-
method_params = {"args": {}, "options": {}}
|
81
|
+
method_params = {"args": {}, "options": {}, "defaults": defaults}
|
72
82
|
|
73
83
|
possible_params = OrderedDict()
|
74
84
|
possible_params.update(possible_arg_params)
|
@@ -90,10 +100,26 @@ def _method_sanity_check(
|
|
90
100
|
% (supplied_k, annotations[supplied_k], defaults[supplied_k])
|
91
101
|
)
|
92
102
|
|
93
|
-
#
|
94
|
-
|
95
|
-
|
96
|
-
|
103
|
+
# Clean up values to make them into what click expects
|
104
|
+
if annotations[supplied_k] == JSON:
|
105
|
+
# JSON should be a string (json dumps)
|
106
|
+
supplied_v = json.dumps(supplied_v)
|
107
|
+
elif supplied_k == "config_value":
|
108
|
+
# Special handling of config value because we need to go look in the tuple
|
109
|
+
new_list = []
|
110
|
+
for cfg_name, cfg_value in supplied_v:
|
111
|
+
if isinstance(cfg_value, ConfigValue):
|
112
|
+
# ConfigValue should be JSONified and converted to a string
|
113
|
+
new_list.append((cfg_name, json.dumps(cfg_value.to_dict())))
|
114
|
+
elif isinstance(cfg_value, dict):
|
115
|
+
# ConfigValue passed as a dictionary
|
116
|
+
new_list.append((cfg_name, json.dumps(cfg_value)))
|
117
|
+
else:
|
118
|
+
raise TypeError(
|
119
|
+
"Invalid type for a config-value, expected a ConfigValue or "
|
120
|
+
"dict but got '%s'" % type(cfg_value)
|
121
|
+
)
|
122
|
+
supplied_v = new_list
|
97
123
|
|
98
124
|
if supplied_k in possible_arg_params:
|
99
125
|
cli_name = possible_arg_params[supplied_k].opts[0].strip("-")
|
@@ -188,35 +214,56 @@ loaded_modules = {}
|
|
188
214
|
def extract_flow_class_from_file(flow_file: str) -> FlowSpec:
|
189
215
|
if not os.path.exists(flow_file):
|
190
216
|
raise FileNotFoundError("Flow file not present at '%s'" % flow_file)
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
217
|
+
|
218
|
+
flow_dir = os.path.dirname(os.path.abspath(flow_file))
|
219
|
+
path_was_added = False
|
220
|
+
|
221
|
+
# Only add to path if it's not already there
|
222
|
+
if flow_dir not in sys.path:
|
223
|
+
sys.path.insert(0, flow_dir)
|
224
|
+
path_was_added = True
|
225
|
+
|
226
|
+
try:
|
227
|
+
# Check if the module has already been loaded
|
228
|
+
if flow_file in loaded_modules:
|
229
|
+
module = loaded_modules[flow_file]
|
230
|
+
else:
|
231
|
+
# Load the module if it's not already loaded
|
232
|
+
spec = importlib.util.spec_from_file_location("module", flow_file)
|
233
|
+
module = importlib.util.module_from_spec(spec)
|
234
|
+
spec.loader.exec_module(module)
|
235
|
+
# Cache the loaded module
|
236
|
+
loaded_modules[flow_file] = module
|
237
|
+
classes = inspect.getmembers(module, inspect.isclass)
|
238
|
+
|
239
|
+
flow_cls = None
|
240
|
+
for _, kls in classes:
|
241
|
+
if kls != FlowSpec and issubclass(kls, FlowSpec):
|
242
|
+
if flow_cls is not None:
|
243
|
+
raise MetaflowException(
|
244
|
+
"Multiple FlowSpec classes found in %s" % flow_file
|
245
|
+
)
|
246
|
+
flow_cls = kls
|
247
|
+
|
248
|
+
if flow_cls is None:
|
249
|
+
raise MetaflowException("No FlowSpec class found in %s" % flow_file)
|
250
|
+
return flow_cls
|
251
|
+
finally:
|
252
|
+
# Only remove from path if we added it
|
253
|
+
if path_was_added:
|
254
|
+
try:
|
255
|
+
sys.path.remove(flow_dir)
|
256
|
+
except ValueError:
|
257
|
+
# User's code might have removed it already
|
258
|
+
pass
|
213
259
|
|
214
260
|
|
215
261
|
class MetaflowAPI(object):
|
216
|
-
def __init__(self, parent=None, flow_cls=None, **kwargs):
|
262
|
+
def __init__(self, parent=None, flow_cls=None, config_input=None, **kwargs):
|
217
263
|
self._parent = parent
|
218
264
|
self._chain = [{self._API_NAME: kwargs}]
|
219
265
|
self._flow_cls = flow_cls
|
266
|
+
self._config_input = config_input
|
220
267
|
self._cached_computed_parameters = None
|
221
268
|
|
222
269
|
@property
|
@@ -238,11 +285,19 @@ class MetaflowAPI(object):
|
|
238
285
|
flow_cls = extract_flow_class_from_file(flow_file)
|
239
286
|
|
240
287
|
with flow_context(flow_cls) as _:
|
241
|
-
|
288
|
+
cli_collection, config_input = config_options_with_config_input(
|
289
|
+
cli_collection
|
290
|
+
)
|
291
|
+
cli_collection = add_decorator_options(cli_collection)
|
242
292
|
|
243
293
|
def getattr_wrapper(_self, name):
|
244
294
|
# Functools.partial do not automatically bind self (no __get__)
|
245
|
-
|
295
|
+
with flow_context(flow_cls) as _:
|
296
|
+
# We also wrap this in the proper flow context because since commands
|
297
|
+
# are loaded lazily, we need the proper flow context to compute things
|
298
|
+
# like parameters. If we do not do this, the outer flow's context will
|
299
|
+
# be used.
|
300
|
+
return _self._internal_getattr(_self, name)
|
246
301
|
|
247
302
|
class_dict = {
|
248
303
|
"__module__": "metaflow",
|
@@ -272,7 +327,12 @@ class MetaflowAPI(object):
|
|
272
327
|
defaults,
|
273
328
|
**kwargs,
|
274
329
|
)
|
275
|
-
return to_return(
|
330
|
+
return to_return(
|
331
|
+
parent=None,
|
332
|
+
flow_cls=flow_cls,
|
333
|
+
config_input=config_input,
|
334
|
+
**method_params,
|
335
|
+
)
|
276
336
|
|
277
337
|
m = _method
|
278
338
|
m.__name__ = cli_collection.name
|
@@ -313,8 +373,12 @@ class MetaflowAPI(object):
|
|
313
373
|
for k, v in options.items():
|
314
374
|
if isinstance(v, list):
|
315
375
|
for i in v:
|
316
|
-
|
317
|
-
|
376
|
+
if isinstance(i, tuple):
|
377
|
+
components.append("--%s" % k)
|
378
|
+
components.extend(map(str, i))
|
379
|
+
else:
|
380
|
+
components.append("--%s" % k)
|
381
|
+
components.append(str(i))
|
318
382
|
else:
|
319
383
|
components.append("--%s" % k)
|
320
384
|
if v != "flag":
|
@@ -323,21 +387,93 @@ class MetaflowAPI(object):
|
|
323
387
|
return components
|
324
388
|
|
325
389
|
def _compute_flow_parameters(self):
|
326
|
-
if
|
390
|
+
if (
|
391
|
+
self._flow_cls is None
|
392
|
+
or self._config_input is None
|
393
|
+
or self._parent is not None
|
394
|
+
):
|
327
395
|
raise RuntimeError(
|
328
396
|
"Computing flow-level parameters for a non start API. "
|
329
397
|
"Please report to the Metaflow team."
|
330
398
|
)
|
331
|
-
|
332
|
-
# would involve processing the options at least partially. We will do this
|
333
|
-
# before GA but for now making it work for regular parameters
|
399
|
+
|
334
400
|
if self._cached_computed_parameters is not None:
|
335
401
|
return self._cached_computed_parameters
|
336
402
|
self._cached_computed_parameters = []
|
403
|
+
|
404
|
+
config_options = None
|
405
|
+
if CLICK_API_PROCESS_CONFIG:
|
406
|
+
with flow_context(self._flow_cls) as _:
|
407
|
+
# We are going to resolve the configs first and then get the parameters.
|
408
|
+
# Note that configs may update/add parameters so the order is important
|
409
|
+
# Since part of the processing of configs happens by click, we need to
|
410
|
+
# "fake" it.
|
411
|
+
|
412
|
+
# Extract any config options as well as datastore and quiet options
|
413
|
+
method_params = self._chain[0][self._API_NAME]
|
414
|
+
opts = method_params["options"]
|
415
|
+
defaults = method_params["defaults"]
|
416
|
+
|
417
|
+
ds = opts.get("datastore", defaults["datastore"])
|
418
|
+
quiet = opts.get("quiet", defaults["quiet"])
|
419
|
+
is_default = False
|
420
|
+
config_file = opts.get("config-file")
|
421
|
+
if config_file is None:
|
422
|
+
is_default = True
|
423
|
+
config_file = defaults.get("config_file")
|
424
|
+
|
425
|
+
if config_file:
|
426
|
+
config_file = map(
|
427
|
+
lambda x: (x[0], ConvertPath.convert_value(x[1], is_default)),
|
428
|
+
config_file,
|
429
|
+
)
|
430
|
+
|
431
|
+
is_default = False
|
432
|
+
config_value = opts.get("config-value")
|
433
|
+
if config_value is None:
|
434
|
+
is_default = True
|
435
|
+
config_value = defaults.get("config_value")
|
436
|
+
|
437
|
+
if config_value:
|
438
|
+
config_value = map(
|
439
|
+
lambda x: (
|
440
|
+
x[0],
|
441
|
+
ConvertDictOrStr.convert_value(x[1], is_default),
|
442
|
+
),
|
443
|
+
config_value,
|
444
|
+
)
|
445
|
+
|
446
|
+
if (config_file is None) ^ (config_value is None):
|
447
|
+
# If we have one, we should have the other
|
448
|
+
raise MetaflowException(
|
449
|
+
"Options were not properly set -- this is an internal error."
|
450
|
+
)
|
451
|
+
|
452
|
+
if config_file:
|
453
|
+
# Process both configurations; the second one will return all the merged
|
454
|
+
# configuration options properly processed.
|
455
|
+
self._config_input.process_configs(
|
456
|
+
self._flow_cls.__name__, "config_file", config_file, quiet, ds
|
457
|
+
)
|
458
|
+
config_options = self._config_input.process_configs(
|
459
|
+
self._flow_cls.__name__, "config_value", config_value, quiet, ds
|
460
|
+
)
|
461
|
+
|
462
|
+
# At this point, we are like in start() in cli.py -- we obtained the
|
463
|
+
# properly processed config_options which we can now use to process
|
464
|
+
# the config decorators (including CustomStep/FlowDecorators)
|
465
|
+
# Note that if CLICK_API_PROCESS_CONFIG is False, we still do this because
|
466
|
+
# it will init all parameters (config_options will be None)
|
467
|
+
# We ignore any errors if we don't check the configs in the click API.
|
468
|
+
new_cls = self._flow_cls._process_config_decorators(
|
469
|
+
config_options, ignore_errors=not CLICK_API_PROCESS_CONFIG
|
470
|
+
)
|
471
|
+
if new_cls:
|
472
|
+
self._flow_cls = new_cls
|
473
|
+
|
337
474
|
for _, param in self._flow_cls._get_parameters():
|
338
475
|
if param.IS_CONFIG_PARAMETER:
|
339
476
|
continue
|
340
|
-
param.init()
|
341
477
|
self._cached_computed_parameters.append(param)
|
342
478
|
return self._cached_computed_parameters
|
343
479
|
|
metaflow/runner/deployer_impl.py
CHANGED
@@ -61,8 +61,13 @@ class DeployerImpl(object):
|
|
61
61
|
"of DeployerImpl."
|
62
62
|
)
|
63
63
|
|
64
|
+
from metaflow.parameters import flow_context
|
65
|
+
|
64
66
|
if "metaflow.cli" in sys.modules:
|
65
|
-
|
67
|
+
# Reload the CLI with an "empty" flow -- this will remove any configuration
|
68
|
+
# options. They are re-added in from_cli (called below).
|
69
|
+
with flow_context(None) as _:
|
70
|
+
importlib.reload(sys.modules["metaflow.cli"])
|
66
71
|
from metaflow.cli import start
|
67
72
|
from metaflow.runner.click_api import MetaflowAPI
|
68
73
|
|
@@ -245,8 +245,13 @@ class Runner(object):
|
|
245
245
|
# This ability is made possible by the statement:
|
246
246
|
# 'from .metaflow_runner import Runner' in '__init__.py'
|
247
247
|
|
248
|
+
from metaflow.parameters import flow_context
|
249
|
+
|
248
250
|
if "metaflow.cli" in sys.modules:
|
249
|
-
|
251
|
+
# Reload the CLI with an "empty" flow -- this will remove any configuration
|
252
|
+
# options. They are re-added in from_cli (called below).
|
253
|
+
with flow_context(None) as _:
|
254
|
+
importlib.reload(sys.modules["metaflow.cli"])
|
250
255
|
from metaflow.cli import start
|
251
256
|
from metaflow.runner.click_api import MetaflowAPI
|
252
257
|
|
metaflow/runner/utils.py
CHANGED
@@ -111,6 +111,11 @@ def read_from_fifo_when_ready(
|
|
111
111
|
poll.register(fifo_fd, select.POLLIN)
|
112
112
|
max_timeout = 3 # Wait for 10 * 3 = 30 ms after last write
|
113
113
|
while True:
|
114
|
+
if check_process_exited(command_obj) and command_obj.process.returncode != 0:
|
115
|
+
raise CalledProcessError(
|
116
|
+
command_obj.process.returncode, command_obj.command
|
117
|
+
)
|
118
|
+
|
114
119
|
if timeout < 0:
|
115
120
|
raise TimeoutError("Timeout while waiting for the file content")
|
116
121
|
|
@@ -15,7 +15,7 @@ from ..util import get_username
|
|
15
15
|
|
16
16
|
_CONVERT_PREFIX = "@!c!@:"
|
17
17
|
_DEFAULT_PREFIX = "@!d!@:"
|
18
|
-
_NO_FILE = "@!n
|
18
|
+
_NO_FILE = "@!n!@:"
|
19
19
|
|
20
20
|
_CONVERTED_DEFAULT = _CONVERT_PREFIX + _DEFAULT_PREFIX
|
21
21
|
_CONVERTED_NO_FILE = _CONVERT_PREFIX + _NO_FILE
|
@@ -41,7 +41,8 @@ class ConvertPath(click.Path):
|
|
41
41
|
is_default = False
|
42
42
|
if value and value.startswith(_DEFAULT_PREFIX):
|
43
43
|
is_default = True
|
44
|
-
value =
|
44
|
+
value = value[len(_DEFAULT_PREFIX) :]
|
45
|
+
value = super().convert(value, param, ctx)
|
45
46
|
return self.convert_value(value, is_default)
|
46
47
|
|
47
48
|
@staticmethod
|
@@ -170,7 +171,15 @@ class ConfigInput:
|
|
170
171
|
cls.loaded_configs = all_configs
|
171
172
|
return cls.loaded_configs.get(config_name, None)
|
172
173
|
|
173
|
-
def process_configs(
|
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
|
+
):
|
174
183
|
from ..cli import echo_always, echo_dev_null # Prevent circular import
|
175
184
|
from ..flowspec import _FlowState # Prevent circular import
|
176
185
|
|
@@ -198,31 +207,32 @@ class ConfigInput:
|
|
198
207
|
# - the default (including None if there is no default). If the default is
|
199
208
|
# not None, it will start with _CONVERTED_DEFAULT since Click will make
|
200
209
|
# the value go through ConvertPath or ConvertDictOrStr
|
201
|
-
#
|
210
|
+
# - the actual value passed through prefixed with _CONVERT_PREFIX
|
202
211
|
|
203
212
|
debug.userconf_exec(
|
204
213
|
"Processing configs for %s -- incoming values: %s"
|
205
|
-
% (
|
214
|
+
% (param_name, str(param_value))
|
206
215
|
)
|
207
216
|
|
208
217
|
do_return = self._value_values is None and self._path_values is None
|
209
218
|
# We only keep around non default values. We could simplify by checking just one
|
210
219
|
# value and if it is default it means all are but this doesn't seem much more effort
|
211
220
|
# and is clearer
|
212
|
-
if
|
221
|
+
if param_name == "config_value":
|
213
222
|
self._value_values = {
|
214
223
|
k.lower(): v
|
215
|
-
for k, v in
|
224
|
+
for k, v in param_value
|
216
225
|
if v is not None and not v.startswith(_CONVERTED_DEFAULT)
|
217
226
|
}
|
218
227
|
else:
|
219
228
|
self._path_values = {
|
220
229
|
k.lower(): v
|
221
|
-
for k, v in
|
230
|
+
for k, v in param_value
|
222
231
|
if v is not None and not v.startswith(_CONVERTED_DEFAULT)
|
223
232
|
}
|
224
233
|
if do_return:
|
225
|
-
# One of
|
234
|
+
# One of values["value"] or values["path"] is None -- we are in the first
|
235
|
+
# go around
|
226
236
|
debug.userconf_exec("Incomplete config options; waiting for more")
|
227
237
|
return None
|
228
238
|
|
@@ -233,8 +243,13 @@ class ConfigInput:
|
|
233
243
|
# down configurations (like METAFLOW_FLOW_CONFIG) could still be present and
|
234
244
|
# would cause an issue -- we can ignore those as the kv. values should trump
|
235
245
|
# everything else.
|
246
|
+
# NOTE: These are all *non default* keys
|
236
247
|
all_keys = set(self._value_values).union(self._path_values)
|
237
|
-
|
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)
|
238
253
|
has_all_kv = all_keys and all(
|
239
254
|
self._value_values.get(k, "").startswith(_CONVERT_PREFIX + "kv.")
|
240
255
|
for k in all_keys
|
@@ -244,27 +259,35 @@ class ConfigInput:
|
|
244
259
|
to_return = {}
|
245
260
|
|
246
261
|
if not has_all_kv:
|
247
|
-
# Check that the user didn't provide *both* a path and a value.
|
262
|
+
# Check that the user didn't provide *both* a path and a value. Again, these
|
263
|
+
# are only user-provided (not defaults)
|
248
264
|
common_keys = set(self._value_values or []).intersection(
|
249
265
|
[k for k, v in self._path_values.items()] or []
|
250
266
|
)
|
251
267
|
if common_keys:
|
252
|
-
|
268
|
+
exc = click.UsageError(
|
253
269
|
"Cannot provide both a value and a file for the same configuration. "
|
254
270
|
"Found such values for '%s'" % "', '".join(common_keys)
|
255
271
|
)
|
272
|
+
if click_obj:
|
273
|
+
click_obj.delayed_config_exception = exc
|
274
|
+
return None
|
275
|
+
raise exc
|
256
276
|
|
257
|
-
all_values = dict(self._path_values
|
258
|
-
all_values.update(self._value_values
|
277
|
+
all_values = dict(self._path_values)
|
278
|
+
all_values.update(self._value_values)
|
259
279
|
|
260
280
|
debug.userconf_exec("All config values: %s" % str(all_values))
|
261
281
|
|
262
282
|
merged_configs = {}
|
283
|
+
# Now look at everything (including defaults)
|
263
284
|
for name, (val, is_path) in self._defaults.items():
|
264
285
|
n = name.lower()
|
265
286
|
if n in all_values:
|
287
|
+
# We have the value provided by the user -- use that.
|
266
288
|
merged_configs[n] = all_values[n]
|
267
289
|
else:
|
290
|
+
# No value provided by the user -- use the default
|
268
291
|
if isinstance(val, DeployTimeField):
|
269
292
|
# This supports a default value that is a deploy-time field (similar
|
270
293
|
# to Parameter).)
|
@@ -273,25 +296,27 @@ class ConfigInput:
|
|
273
296
|
# of circularity. Note also that quiet and datastore are *eager*
|
274
297
|
# options so are available here.
|
275
298
|
param_ctx = ParameterContext(
|
276
|
-
flow_name=
|
299
|
+
flow_name=flow_name,
|
277
300
|
user_name=get_username(),
|
278
301
|
parameter_name=n,
|
279
|
-
logger=(
|
280
|
-
|
281
|
-
),
|
282
|
-
ds_type=ctx.params["datastore"],
|
302
|
+
logger=(echo_dev_null if quiet else echo_always),
|
303
|
+
ds_type=datastore,
|
283
304
|
configs=None,
|
284
305
|
)
|
285
306
|
val = val.fun(param_ctx)
|
286
307
|
if is_path:
|
287
308
|
# This is a file path
|
288
|
-
merged_configs[n] = ConvertPath.convert_value(val,
|
309
|
+
merged_configs[n] = ConvertPath.convert_value(val, True)
|
289
310
|
else:
|
290
311
|
# This is a value
|
291
|
-
merged_configs[n] = ConvertDictOrStr.convert_value(val,
|
312
|
+
merged_configs[n] = ConvertDictOrStr.convert_value(val, True)
|
292
313
|
else:
|
293
314
|
debug.userconf_exec("Fast path due to pre-processed values")
|
294
315
|
merged_configs = self._value_values
|
316
|
+
|
317
|
+
if click_obj:
|
318
|
+
click_obj.has_config_options = True
|
319
|
+
|
295
320
|
debug.userconf_exec("Configs merged with defaults: %s" % str(merged_configs))
|
296
321
|
|
297
322
|
missing_configs = set()
|
@@ -302,20 +327,27 @@ class ConfigInput:
|
|
302
327
|
if val is None:
|
303
328
|
missing_configs.add(name)
|
304
329
|
continue
|
305
|
-
if val.startswith(_CONVERTED_NO_FILE):
|
306
|
-
no_file.append(name)
|
307
|
-
continue
|
308
330
|
if val.startswith(_CONVERTED_DEFAULT_NO_FILE):
|
309
331
|
no_default_file.append(name)
|
310
332
|
continue
|
333
|
+
if val.startswith(_CONVERTED_NO_FILE):
|
334
|
+
no_file.append(name)
|
335
|
+
continue
|
336
|
+
|
311
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) :]
|
312
340
|
if val.startswith("kv."):
|
313
341
|
# This means to load it from a file
|
314
342
|
read_value = self.get_config(val[3:])
|
315
343
|
if read_value is None:
|
316
|
-
|
344
|
+
exc = click.UsageError(
|
317
345
|
"Could not find configuration '%s' in INFO file" % val
|
318
346
|
)
|
347
|
+
if click_obj:
|
348
|
+
click_obj.delayed_config_exception = exc
|
349
|
+
return None
|
350
|
+
raise exc
|
319
351
|
flow_cls._flow_state[_FlowState.CONFIGS][name] = read_value
|
320
352
|
to_return[name] = ConfigValue(read_value)
|
321
353
|
else:
|
@@ -348,13 +380,27 @@ class ConfigInput:
|
|
348
380
|
% (merged_configs[missing][len(_CONVERTED_DEFAULT_NO_FILE) :], missing)
|
349
381
|
)
|
350
382
|
if msgs:
|
351
|
-
|
383
|
+
exc = click.UsageError(
|
352
384
|
"Bad values passed for configuration options: %s" % ", ".join(msgs)
|
353
385
|
)
|
386
|
+
if click_obj:
|
387
|
+
click_obj.delayed_config_exception = exc
|
388
|
+
return None
|
389
|
+
raise exc
|
354
390
|
|
355
391
|
debug.userconf_exec("Finalized configs: %s" % str(to_return))
|
356
392
|
return to_return
|
357
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
|
+
|
358
404
|
def __str__(self):
|
359
405
|
return repr(self)
|
360
406
|
|
@@ -399,7 +445,7 @@ class LocalFileInput(click.Path):
|
|
399
445
|
return "LocalFileInput"
|
400
446
|
|
401
447
|
|
402
|
-
def
|
448
|
+
def config_options_with_config_input(cmd):
|
403
449
|
help_strs = []
|
404
450
|
required_names = []
|
405
451
|
defaults = {}
|
@@ -407,7 +453,7 @@ def config_options(cmd):
|
|
407
453
|
parsers = {}
|
408
454
|
flow_cls = getattr(current_flow, "flow_cls", None)
|
409
455
|
if flow_cls is None:
|
410
|
-
return cmd
|
456
|
+
return cmd, None
|
411
457
|
|
412
458
|
parameters = [p for _, p in flow_cls._get_parameters() if p.IS_CONFIG_PARAMETER]
|
413
459
|
# List all the configuration options
|
@@ -432,20 +478,22 @@ def config_options(cmd):
|
|
432
478
|
parsers[arg.name.lower()] = arg.parser
|
433
479
|
|
434
480
|
if not config_seen:
|
435
|
-
# No configurations -- don't add anything
|
436
|
-
|
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
|
437
484
|
|
438
485
|
help_str = (
|
439
486
|
"Configuration options for the flow. "
|
440
|
-
"Multiple configurations can be specified."
|
487
|
+
"Multiple configurations can be specified. Cannot be used with resume."
|
441
488
|
)
|
442
489
|
help_str = "\n\n".join([help_str] + help_strs)
|
443
|
-
|
490
|
+
config_input = ConfigInput(required_names, defaults, parsers)
|
491
|
+
cb_func = config_input.process_configs_click
|
444
492
|
|
445
493
|
cmd.params.insert(
|
446
494
|
0,
|
447
495
|
click.Option(
|
448
|
-
["--config-value", "
|
496
|
+
["--config-value", "config_value"],
|
449
497
|
nargs=2,
|
450
498
|
multiple=True,
|
451
499
|
type=MultipleTuple([click.Choice(config_seen), ConvertDictOrStr()]),
|
@@ -470,7 +518,7 @@ def config_options(cmd):
|
|
470
518
|
cmd.params.insert(
|
471
519
|
0,
|
472
520
|
click.Option(
|
473
|
-
["--config", "
|
521
|
+
["--config", "config_file"],
|
474
522
|
nargs=2,
|
475
523
|
multiple=True,
|
476
524
|
type=MultipleTuple([click.Choice(config_seen), ConvertPath()]),
|
@@ -492,4 +540,9 @@ def config_options(cmd):
|
|
492
540
|
required=False,
|
493
541
|
),
|
494
542
|
)
|
543
|
+
return cmd, config_input
|
544
|
+
|
545
|
+
|
546
|
+
def config_options(cmd):
|
547
|
+
cmd, _ = config_options_with_config_input(cmd)
|
495
548
|
return cmd
|