ob-metaflow 2.12.39.1__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 +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/metaflow_config.py +5 -0
- metaflow/parameters.py +11 -4
- metaflow/plugins/aws/secrets_manager/aws_secrets_manager_secrets_provider.py +13 -10
- metaflow/plugins/cards/card_creator.py +1 -0
- metaflow/plugins/cards/card_decorator.py +46 -8
- metaflow/runner/click_api.py +175 -39
- metaflow/runner/deployer_impl.py +6 -1
- metaflow/runner/metaflow_runner.py +6 -1
- 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
- {ob_metaflow-2.12.39.1.dist-info → ob_metaflow-2.13.0.1.dist-info}/METADATA +2 -2
- {ob_metaflow-2.12.39.1.dist-info → ob_metaflow-2.13.0.1.dist-info}/RECORD +28 -28
- {ob_metaflow-2.12.39.1.dist-info → ob_metaflow-2.13.0.1.dist-info}/LICENSE +0 -0
- {ob_metaflow-2.12.39.1.dist-info → ob_metaflow-2.13.0.1.dist-info}/WHEEL +0 -0
- {ob_metaflow-2.12.39.1.dist-info → ob_metaflow-2.13.0.1.dist-info}/entry_points.txt +0 -0
- {ob_metaflow-2.12.39.1.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
|
@@ -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/parameters.py
CHANGED
|
@@ -359,7 +359,7 @@ class Parameter(object):
|
|
|
359
359
|
"show_default": show_default,
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
def init(self):
|
|
362
|
+
def init(self, ignore_errors=False):
|
|
363
363
|
# Prevent circular import
|
|
364
364
|
from .user_configs.config_parameters import (
|
|
365
365
|
resolve_delayed_evaluator,
|
|
@@ -367,14 +367,21 @@ class Parameter(object):
|
|
|
367
367
|
)
|
|
368
368
|
|
|
369
369
|
# Resolve any value from configurations
|
|
370
|
-
self.kwargs = unpack_delayed_evaluator(self.kwargs)
|
|
371
|
-
|
|
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
|
+
}
|
|
372
377
|
|
|
373
378
|
# This was the behavior before configs: values specified in args would override
|
|
374
379
|
# stuff in kwargs which is what we implement here as well
|
|
375
380
|
for key, value in self._override_kwargs.items():
|
|
376
381
|
if value is not None:
|
|
377
|
-
self.kwargs[key] =
|
|
382
|
+
self.kwargs[key] = resolve_delayed_evaluator(
|
|
383
|
+
value, ignore_errors=ignore_errors
|
|
384
|
+
)
|
|
378
385
|
# Set two default values if no-one specified them
|
|
379
386
|
self.kwargs.setdefault("required", False)
|
|
380
387
|
self.kwargs.setdefault("show_default", True)
|
|
@@ -50,24 +50,27 @@ class AwsSecretsManagerSecretsProvider(SecretsProvider):
|
|
|
50
50
|
The secret payload from AWS is EITHER a string OR a binary blob.
|
|
51
51
|
|
|
52
52
|
If the secret contains a string payload ("SecretString"):
|
|
53
|
-
- if the `
|
|
53
|
+
- if the `json` option is True (default):
|
|
54
54
|
{SecretString} will be parsed as a JSON. If successfully parsed, AND the JSON contains a
|
|
55
55
|
top-level object, each entry K/V in the object will also be converted to an entry in the result. V will
|
|
56
56
|
always be casted to a string (if not already a string).
|
|
57
|
-
- If `
|
|
58
|
-
{SecretString} will be returned as a single entry in the result,
|
|
57
|
+
- If `json` option is False:
|
|
58
|
+
{SecretString} will be returned as a single entry in the result, where the key is either:
|
|
59
|
+
- the `secret_id`, OR
|
|
60
|
+
- the value set by `options={"env_var_name": custom_env_var_name}`.
|
|
59
61
|
|
|
60
|
-
Otherwise, the secret contains a binary blob payload ("SecretBinary")
|
|
61
|
-
- The result
|
|
62
|
+
Otherwise, if the secret contains a binary blob payload ("SecretBinary"):
|
|
63
|
+
- The result dict contains '{SecretName}': '{SecretBinary}', where {SecretBinary} is a base64-encoded string.
|
|
62
64
|
|
|
63
|
-
All keys in the result are sanitized to be more valid environment variable names. This is done on a best
|
|
65
|
+
All keys in the result are sanitized to be more valid environment variable names. This is done on a best-effort
|
|
64
66
|
basis. Further validation is expected to be done by the invoking @secrets decorator itself.
|
|
65
67
|
|
|
66
|
-
:param secret_id: ARN or friendly name of the secret
|
|
67
|
-
:param options:
|
|
68
|
-
:param role: AWS IAM Role ARN to assume before reading the secret
|
|
69
|
-
:return:
|
|
68
|
+
:param secret_id: ARN or friendly name of the secret.
|
|
69
|
+
:param options: Dictionary of additional options. E.g., `options={"env_var_name": custom_env_var_name}`.
|
|
70
|
+
:param role: AWS IAM Role ARN to assume before reading the secret.
|
|
71
|
+
:return: Dictionary of environment variables. All keys and values are strings.
|
|
70
72
|
"""
|
|
73
|
+
|
|
71
74
|
import botocore
|
|
72
75
|
from metaflow.plugins.aws.aws_client import get_aws_client
|
|
73
76
|
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import tempfile
|
|
5
|
+
|
|
1
6
|
from metaflow.decorators import StepDecorator
|
|
2
7
|
from metaflow.metaflow_current import current
|
|
8
|
+
from metaflow.user_configs.config_options import ConfigInput
|
|
9
|
+
from metaflow.user_configs.config_parameters import dump_config_values
|
|
3
10
|
from metaflow.util import to_unicode
|
|
11
|
+
|
|
4
12
|
from .component_serializer import CardComponentCollector, get_card_class
|
|
5
13
|
from .card_creator import CardCreator
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
# from metaflow import get_metadata
|
|
9
|
-
import re
|
|
10
|
-
|
|
11
14
|
from .exception import CARD_ID_PATTERN, TYPE_CHECK_REGEX
|
|
12
15
|
|
|
13
16
|
ASYNC_TIMEOUT = 30
|
|
@@ -111,6 +114,14 @@ class CardDecorator(StepDecorator):
|
|
|
111
114
|
self._logger = logger
|
|
112
115
|
self.card_options = None
|
|
113
116
|
|
|
117
|
+
# We check for configuration options. We do this here before they are
|
|
118
|
+
# converted to properties.
|
|
119
|
+
self._config_values = [
|
|
120
|
+
(config.name, ConfigInput.make_key_name(config.name))
|
|
121
|
+
for _, config in flow._get_parameters()
|
|
122
|
+
if config.IS_CONFIG_PARAMETER
|
|
123
|
+
]
|
|
124
|
+
|
|
114
125
|
self.card_options = self.attributes["options"]
|
|
115
126
|
|
|
116
127
|
evt_name = "step-init"
|
|
@@ -146,6 +157,18 @@ class CardDecorator(StepDecorator):
|
|
|
146
157
|
self._task_datastore = task_datastore
|
|
147
158
|
self._metadata = metadata
|
|
148
159
|
|
|
160
|
+
# If we have configs, we need to dump them to a file so we can re-use them
|
|
161
|
+
# when calling the card creation subprocess.
|
|
162
|
+
if self._config_values:
|
|
163
|
+
with tempfile.NamedTemporaryFile(
|
|
164
|
+
mode="w", encoding="utf-8", delete=False
|
|
165
|
+
) as config_file:
|
|
166
|
+
config_value = dump_config_values(flow)
|
|
167
|
+
json.dump(config_value, config_file)
|
|
168
|
+
self._config_file_name = config_file.name
|
|
169
|
+
else:
|
|
170
|
+
self._config_file_name = None
|
|
171
|
+
|
|
149
172
|
card_type = self.attributes["type"]
|
|
150
173
|
card_class = get_card_class(card_type)
|
|
151
174
|
|
|
@@ -179,7 +202,7 @@ class CardDecorator(StepDecorator):
|
|
|
179
202
|
# we need to ensure that `current.card` has `CardComponentCollector` instantiated only once.
|
|
180
203
|
if not self._is_event_registered("pre-step"):
|
|
181
204
|
self._register_event("pre-step")
|
|
182
|
-
self._set_card_creator(CardCreator(self._create_top_level_args()))
|
|
205
|
+
self._set_card_creator(CardCreator(self._create_top_level_args(flow)))
|
|
183
206
|
|
|
184
207
|
current._update_env(
|
|
185
208
|
{"card": CardComponentCollector(self._logger, self.card_creator)}
|
|
@@ -223,6 +246,13 @@ class CardDecorator(StepDecorator):
|
|
|
223
246
|
self.card_creator.create(mode="render", final=True, **create_options)
|
|
224
247
|
self.card_creator.create(mode="refresh", final=True, **create_options)
|
|
225
248
|
|
|
249
|
+
# Unlink the config file if it exists
|
|
250
|
+
if self._config_file_name:
|
|
251
|
+
try:
|
|
252
|
+
os.unlink(self._config_file_name)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
pass
|
|
255
|
+
|
|
226
256
|
@staticmethod
|
|
227
257
|
def _options(mapping):
|
|
228
258
|
for k, v in mapping.items():
|
|
@@ -232,9 +262,13 @@ class CardDecorator(StepDecorator):
|
|
|
232
262
|
for value in v:
|
|
233
263
|
yield "--%s" % k
|
|
234
264
|
if not isinstance(value, bool):
|
|
235
|
-
|
|
265
|
+
if isinstance(value, tuple):
|
|
266
|
+
for val in value:
|
|
267
|
+
yield to_unicode(val)
|
|
268
|
+
else:
|
|
269
|
+
yield to_unicode(value)
|
|
236
270
|
|
|
237
|
-
def _create_top_level_args(self):
|
|
271
|
+
def _create_top_level_args(self, flow):
|
|
238
272
|
top_level_options = {
|
|
239
273
|
"quiet": True,
|
|
240
274
|
"metadata": self._metadata.TYPE,
|
|
@@ -247,4 +281,8 @@ class CardDecorator(StepDecorator):
|
|
|
247
281
|
# We don't provide --with as all execution is taking place in
|
|
248
282
|
# the context of the main process
|
|
249
283
|
}
|
|
284
|
+
if self._config_values:
|
|
285
|
+
top_level_options["config-value"] = self._config_values
|
|
286
|
+
top_level_options["local-config-file"] = self._config_file_name
|
|
287
|
+
|
|
250
288
|
return list(self._options(top_level_options))
|
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
|
|