metaflow 2.15.21__py2.py3-none-any.whl → 2.16.0__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 +7 -1
- metaflow/cli.py +16 -1
- metaflow/cli_components/init_cmd.py +1 -0
- metaflow/cli_components/run_cmds.py +6 -2
- metaflow/client/core.py +22 -30
- metaflow/datastore/task_datastore.py +0 -1
- metaflow/debug.py +5 -0
- metaflow/decorators.py +230 -70
- metaflow/extension_support/__init__.py +15 -8
- metaflow/extension_support/_empty_file.py +2 -2
- metaflow/flowspec.py +80 -53
- metaflow/graph.py +24 -2
- metaflow/meta_files.py +13 -0
- metaflow/metadata_provider/metadata.py +7 -1
- metaflow/metaflow_config.py +5 -0
- metaflow/metaflow_environment.py +82 -25
- metaflow/metaflow_version.py +1 -1
- metaflow/package/__init__.py +664 -0
- metaflow/packaging_sys/__init__.py +870 -0
- metaflow/packaging_sys/backend.py +113 -0
- metaflow/packaging_sys/distribution_support.py +153 -0
- metaflow/packaging_sys/tar_backend.py +86 -0
- metaflow/packaging_sys/utils.py +91 -0
- metaflow/packaging_sys/v1.py +476 -0
- metaflow/plugins/airflow/airflow.py +5 -1
- metaflow/plugins/airflow/airflow_cli.py +15 -4
- metaflow/plugins/argo/argo_workflows.py +15 -4
- metaflow/plugins/argo/argo_workflows_cli.py +16 -4
- metaflow/plugins/aws/batch/batch.py +22 -3
- metaflow/plugins/aws/batch/batch_cli.py +3 -0
- metaflow/plugins/aws/batch/batch_decorator.py +13 -5
- metaflow/plugins/aws/step_functions/step_functions.py +4 -1
- metaflow/plugins/aws/step_functions/step_functions_cli.py +15 -4
- metaflow/plugins/cards/card_decorator.py +0 -5
- metaflow/plugins/kubernetes/kubernetes.py +8 -1
- metaflow/plugins/kubernetes/kubernetes_cli.py +3 -0
- metaflow/plugins/kubernetes/kubernetes_decorator.py +13 -5
- metaflow/plugins/package_cli.py +25 -23
- metaflow/plugins/parallel_decorator.py +4 -2
- metaflow/plugins/pypi/bootstrap.py +8 -2
- metaflow/plugins/pypi/conda_decorator.py +39 -82
- metaflow/plugins/pypi/conda_environment.py +6 -2
- metaflow/plugins/pypi/pypi_decorator.py +4 -4
- metaflow/plugins/test_unbounded_foreach_decorator.py +2 -2
- metaflow/plugins/timeout_decorator.py +0 -1
- metaflow/plugins/uv/bootstrap.py +11 -0
- metaflow/plugins/uv/uv_environment.py +4 -2
- metaflow/pylint_wrapper.py +5 -1
- metaflow/runner/click_api.py +5 -4
- metaflow/runner/subprocess_manager.py +14 -2
- metaflow/runtime.py +37 -11
- metaflow/task.py +91 -7
- metaflow/user_configs/config_options.py +13 -8
- metaflow/user_configs/config_parameters.py +0 -4
- metaflow/user_decorators/__init__.py +0 -0
- metaflow/user_decorators/common.py +144 -0
- metaflow/user_decorators/mutable_flow.py +499 -0
- metaflow/user_decorators/mutable_step.py +424 -0
- metaflow/user_decorators/user_flow_decorator.py +263 -0
- metaflow/user_decorators/user_step_decorator.py +712 -0
- metaflow/util.py +4 -1
- metaflow/version.py +1 -1
- {metaflow-2.15.21.dist-info → metaflow-2.16.0.dist-info}/METADATA +2 -2
- {metaflow-2.15.21.dist-info → metaflow-2.16.0.dist-info}/RECORD +71 -60
- metaflow/info_file.py +0 -25
- metaflow/package.py +0 -203
- metaflow/user_configs/config_decorators.py +0 -568
- {metaflow-2.15.21.data → metaflow-2.16.0.data}/data/share/metaflow/devtools/Makefile +0 -0
- {metaflow-2.15.21.data → metaflow-2.16.0.data}/data/share/metaflow/devtools/Tiltfile +0 -0
- {metaflow-2.15.21.data → metaflow-2.16.0.data}/data/share/metaflow/devtools/pick_services.sh +0 -0
- {metaflow-2.15.21.dist-info → metaflow-2.16.0.dist-info}/WHEEL +0 -0
- {metaflow-2.15.21.dist-info → metaflow-2.16.0.dist-info}/entry_points.txt +0 -0
- {metaflow-2.15.21.dist-info → metaflow-2.16.0.dist-info}/licenses/LICENSE +0 -0
- {metaflow-2.15.21.dist-info → metaflow-2.16.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,712 @@
|
|
1
|
+
import inspect
|
2
|
+
import json
|
3
|
+
import re
|
4
|
+
import types
|
5
|
+
|
6
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, TYPE_CHECKING, Union
|
7
|
+
|
8
|
+
from metaflow.debug import debug
|
9
|
+
from metaflow.exception import MetaflowException
|
10
|
+
from metaflow.user_configs.config_parameters import (
|
11
|
+
resolve_delayed_evaluator,
|
12
|
+
unpack_delayed_evaluator,
|
13
|
+
)
|
14
|
+
|
15
|
+
from .common import ClassPath_Trie
|
16
|
+
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
import metaflow.datastore.inputs
|
19
|
+
import metaflow.decorators
|
20
|
+
import metaflow.flowspec
|
21
|
+
import metaflow.user_decorators.mutable_step
|
22
|
+
|
23
|
+
USER_SKIP_STEP = {}
|
24
|
+
|
25
|
+
|
26
|
+
class UserStepDecoratorMeta(type):
|
27
|
+
_all_registered_decorators = ClassPath_Trie()
|
28
|
+
_do_not_register = set()
|
29
|
+
_import_modules = set()
|
30
|
+
|
31
|
+
def __new__(mcs, name, bases, namespace, **_kwargs):
|
32
|
+
cls = super().__new__(mcs, name, bases, namespace)
|
33
|
+
cls.decorator_name = getattr(
|
34
|
+
cls, "_decorator_name", f"{cls.__module__}.{cls.__name__}"
|
35
|
+
)
|
36
|
+
effective_module = getattr(cls, "_original_module", cls.__module__)
|
37
|
+
if not effective_module.startswith(
|
38
|
+
"metaflow."
|
39
|
+
) and not effective_module.startswith("metaflow_extensions."):
|
40
|
+
mcs._import_modules.add(effective_module)
|
41
|
+
|
42
|
+
if (
|
43
|
+
name in ("FlowMutator", "UserStepDecorator")
|
44
|
+
or cls.decorator_name in mcs._do_not_register
|
45
|
+
):
|
46
|
+
return cls
|
47
|
+
|
48
|
+
# We inject a __init_subclass__ method so we can figure out if there
|
49
|
+
# are subclasses. We want to register as decorators only the ones that do
|
50
|
+
# not have a subclass. The logic is that everything is registered and if
|
51
|
+
# a subclass shows up, we will unregister the parent class leaving only those
|
52
|
+
# classes that do not have any subclasses registered.
|
53
|
+
@classmethod
|
54
|
+
def do_unregister(cls_, **_kwargs):
|
55
|
+
for base in cls_.__bases__:
|
56
|
+
if isinstance(base, UserStepDecoratorMeta):
|
57
|
+
# If the base is a UserStepDecoratorMeta, we unregister it
|
58
|
+
# so that we don't have any decorators that are not the
|
59
|
+
# most derived one.
|
60
|
+
mcs._all_registered_decorators.remove(base.decorator_name)
|
61
|
+
# Also make sure we don't register again
|
62
|
+
mcs._do_not_register.add(base.decorator_name)
|
63
|
+
|
64
|
+
cls.__init_subclass__ = do_unregister
|
65
|
+
mcs._all_registered_decorators.insert(cls.decorator_name, cls)
|
66
|
+
return cls
|
67
|
+
|
68
|
+
def __str__(cls):
|
69
|
+
return "%s(%s)" % (
|
70
|
+
cls.__name__ if cls.__name__ != "WrapClass" else "UserStepDecorator",
|
71
|
+
getattr(cls, "decorator_name", None),
|
72
|
+
)
|
73
|
+
|
74
|
+
@classmethod
|
75
|
+
def all_decorators(mcs) -> Dict[str, "UserStepDecoratorMeta"]:
|
76
|
+
"""
|
77
|
+
Get all registered decorators using the minimally unique classpath name
|
78
|
+
|
79
|
+
Returns
|
80
|
+
-------
|
81
|
+
Dict[str, UserStepDecoratorBase]
|
82
|
+
A dictionary mapping decorator names to their classes.
|
83
|
+
"""
|
84
|
+
mcs._check_init()
|
85
|
+
return mcs._all_registered_decorators.get_unique_prefixes()
|
86
|
+
|
87
|
+
@classmethod
|
88
|
+
def get_decorator_by_name(
|
89
|
+
mcs, decorator_name: str
|
90
|
+
) -> Optional[Union["UserStepDecoratorBase", "metaflow.decorators.Decorator"]]:
|
91
|
+
"""
|
92
|
+
Get a decorator by its name.
|
93
|
+
|
94
|
+
Parameters
|
95
|
+
----------
|
96
|
+
decorator_name: str
|
97
|
+
The name of the decorator to retrieve.
|
98
|
+
|
99
|
+
Returns
|
100
|
+
-------
|
101
|
+
Optional[UserStepDecoratorBase]
|
102
|
+
The decorator class if found, None otherwise.
|
103
|
+
"""
|
104
|
+
mcs._check_init()
|
105
|
+
return mcs._all_registered_decorators.unique_prefix_value(decorator_name)
|
106
|
+
|
107
|
+
@classmethod
|
108
|
+
def get_decorator_name(mcs, decorator_type: type) -> Optional[str]:
|
109
|
+
"""
|
110
|
+
Get the minimally unique classpath name for a decorator type.
|
111
|
+
|
112
|
+
Parameters
|
113
|
+
----------
|
114
|
+
decorator_type: type
|
115
|
+
The type of the decorator to retrieve the name for.
|
116
|
+
|
117
|
+
Returns
|
118
|
+
-------
|
119
|
+
Optional[str]
|
120
|
+
The minimally unique classpath name if found, None otherwise.
|
121
|
+
"""
|
122
|
+
mcs._check_init()
|
123
|
+
return mcs._all_registered_decorators.unique_prefix_for_type(decorator_type)
|
124
|
+
|
125
|
+
@classmethod
|
126
|
+
def _check_init(mcs):
|
127
|
+
# Delay importing STEP_DECORATORS until we actually need it
|
128
|
+
if not mcs._all_registered_decorators.inited:
|
129
|
+
from metaflow.plugins import STEP_DECORATORS
|
130
|
+
|
131
|
+
mcs._all_registered_decorators.init([(t.name, t) for t in STEP_DECORATORS])
|
132
|
+
|
133
|
+
|
134
|
+
class UserStepDecoratorBase(metaclass=UserStepDecoratorMeta):
|
135
|
+
_step_field = None
|
136
|
+
_allowed_args = False
|
137
|
+
_allowed_kwargs = False
|
138
|
+
|
139
|
+
def __init__(self, *args, **kwargs):
|
140
|
+
arg = None
|
141
|
+
self._args = args
|
142
|
+
self._kwargs = {}
|
143
|
+
# If nothing is set, the user statically defined the decorator
|
144
|
+
self._special_kwargs = {"_statically_defined": True, "_inserted_by": None}
|
145
|
+
for k, v in kwargs.items():
|
146
|
+
if k in ("_statically_defined", "_inserted_by"):
|
147
|
+
# These are special arguments that we do not want to pass to the step
|
148
|
+
# decorator
|
149
|
+
self._special_kwargs[k] = v
|
150
|
+
else:
|
151
|
+
self._kwargs[k] = v
|
152
|
+
|
153
|
+
if self._args:
|
154
|
+
if isinstance(self._args[0], UserStepDecoratorBase):
|
155
|
+
arg = self._args[0]._my_step
|
156
|
+
else:
|
157
|
+
arg = self._args[0]
|
158
|
+
|
159
|
+
if arg and callable(arg) and hasattr(arg, "is_step"):
|
160
|
+
# This means the decorator is bare like @MyDecorator
|
161
|
+
# and the first argument is the step
|
162
|
+
self._set_my_step(arg)
|
163
|
+
self._args = args[1:] # The rest of the args are the decorator args
|
164
|
+
|
165
|
+
if self._args and not self._allowed_args:
|
166
|
+
raise MetaflowException("%s does not allow arguments" % str(self))
|
167
|
+
if self._kwargs:
|
168
|
+
if not self._allowed_kwargs:
|
169
|
+
raise MetaflowException("%s does not allow keyword arguments" % self)
|
170
|
+
elif isinstance(self._allowed_kwargs, list) and any(
|
171
|
+
a not in self._allowed_kwargs for a in self._kwargs
|
172
|
+
):
|
173
|
+
raise MetaflowException(
|
174
|
+
"%s only allows the following keyword arguments: %s"
|
175
|
+
% (self, str(self._allowed_args))
|
176
|
+
)
|
177
|
+
|
178
|
+
def __get__(self, instance, owner):
|
179
|
+
# Required so that we "present" as a step when the step decorator is
|
180
|
+
# of the form
|
181
|
+
# @MyStepDecorator
|
182
|
+
# @step
|
183
|
+
# def my_step(self):
|
184
|
+
# pass
|
185
|
+
#
|
186
|
+
# This is *not* called for something like:
|
187
|
+
# @MyStepDecorator()
|
188
|
+
# @step
|
189
|
+
# def my_step(self):
|
190
|
+
# pass
|
191
|
+
# because in that case, we will have called __call__ below and that already
|
192
|
+
# returns a function and that __get__ function will be called.
|
193
|
+
|
194
|
+
return self().__get__(instance, owner)
|
195
|
+
|
196
|
+
def __call__(
|
197
|
+
self,
|
198
|
+
step: Optional[
|
199
|
+
Union[
|
200
|
+
Callable[["metaflow.decorators.FlowSpecDerived"], None],
|
201
|
+
Callable[["metaflow.decorators.FlowSpecDerived", Any], None],
|
202
|
+
]
|
203
|
+
] = None,
|
204
|
+
**kwargs,
|
205
|
+
) -> Union[
|
206
|
+
Callable[["metaflow.decorators.FlowSpecDerived"], None],
|
207
|
+
Callable[["metaflow.decorators.FlowSpecDerived", Any], None],
|
208
|
+
]:
|
209
|
+
# The only kwargs here are just special kwargs (not user facing since those
|
210
|
+
# are passed in the constructor)
|
211
|
+
self._special_kwargs.update(kwargs)
|
212
|
+
if step:
|
213
|
+
if isinstance(step, UserStepDecoratorBase):
|
214
|
+
step = step._my_step
|
215
|
+
|
216
|
+
return self._set_my_step(step)
|
217
|
+
elif not self._my_step:
|
218
|
+
# This means that somehow the initialization did not happen properly
|
219
|
+
# so this may have been applied to a non step
|
220
|
+
raise MetaflowException("%s can only be applied to a step function" % self)
|
221
|
+
return self._my_step
|
222
|
+
|
223
|
+
def add_or_raise(
|
224
|
+
self,
|
225
|
+
step: Union[
|
226
|
+
Callable[["metaflow.decorators.FlowSpecDerived"], None],
|
227
|
+
Callable[["metaflow.decorators.FlowSpecDerived", Any], None],
|
228
|
+
],
|
229
|
+
statically_defined: bool,
|
230
|
+
duplicates: int,
|
231
|
+
inserted_by: Optional[str] = None,
|
232
|
+
):
|
233
|
+
from metaflow.user_decorators.mutable_step import MutableStep
|
234
|
+
|
235
|
+
existing_deco = [
|
236
|
+
d
|
237
|
+
for d in getattr(step, self._step_field)
|
238
|
+
if d.decorator_name == self.decorator_name
|
239
|
+
]
|
240
|
+
|
241
|
+
if not existing_deco:
|
242
|
+
self(step, _statically_defined=statically_defined, _inserted_by=inserted_by)
|
243
|
+
elif duplicates == MutableStep.IGNORE:
|
244
|
+
# If we are ignoring duplicates, we just return
|
245
|
+
debug.userconf_exec(
|
246
|
+
"Ignoring duplicate decorator %s on step %s from %s"
|
247
|
+
% (self, step.__name__, inserted_by)
|
248
|
+
)
|
249
|
+
return
|
250
|
+
elif duplicates == MutableStep.OVERRIDE:
|
251
|
+
# If we are overriding, we remove the existing decorator and add this one
|
252
|
+
debug.userconf_exec(
|
253
|
+
"Overriding decorator %s on step %s from %s"
|
254
|
+
% (self, step.__name__, inserted_by)
|
255
|
+
)
|
256
|
+
setattr(
|
257
|
+
step,
|
258
|
+
self._step_field,
|
259
|
+
[
|
260
|
+
d
|
261
|
+
for d in getattr(step, self._step_field)
|
262
|
+
if d.decorator_name != self.decorator_name
|
263
|
+
],
|
264
|
+
)
|
265
|
+
self(step, _statically_defined=statically_defined, _inserted_by=inserted_by)
|
266
|
+
elif duplicates == MutableStep.ERROR:
|
267
|
+
if statically_defined:
|
268
|
+
# Prevent circular dep
|
269
|
+
from metaflow.decorators import DuplicateStepDecoratorException
|
270
|
+
|
271
|
+
raise DuplicateStepDecoratorException(self.__class__, step)
|
272
|
+
|
273
|
+
def _set_my_step(
|
274
|
+
self,
|
275
|
+
step: Union[
|
276
|
+
Callable[["metaflow.decorators.FlowSpecDerived"], None],
|
277
|
+
Callable[["metaflow.decorators.FlowSpecDerived", Any], None],
|
278
|
+
],
|
279
|
+
) -> Union[
|
280
|
+
Callable[["metaflow.decorators.FlowSpecDerived"], None],
|
281
|
+
Callable[["metaflow.decorators.FlowSpecDerived", Any], None],
|
282
|
+
]:
|
283
|
+
self._my_step = step
|
284
|
+
if self._step_field is None:
|
285
|
+
raise RuntimeError(
|
286
|
+
"UserStepDecorator is not properly overloaded; missing _step_field. "
|
287
|
+
"This is a Metaflow bug, please contact support."
|
288
|
+
)
|
289
|
+
# When we set the step, we can now determine if we are statically defined or
|
290
|
+
# not. We can't do it much earlier because the decorator itself may be defined
|
291
|
+
# (ie: @user_step_decorator is statically defined) but it will only be a static
|
292
|
+
# decorator when the user applies it to a step function.
|
293
|
+
self.statically_defined = self._special_kwargs["_statically_defined"]
|
294
|
+
self.inserted_by = self._special_kwargs["_inserted_by"]
|
295
|
+
|
296
|
+
getattr(self._my_step, self._step_field).append(self)
|
297
|
+
return self._my_step
|
298
|
+
|
299
|
+
def __str__(self):
|
300
|
+
return str(self.__class__)
|
301
|
+
|
302
|
+
@classmethod
|
303
|
+
def extract_args_kwargs_from_decorator_spec(
|
304
|
+
cls, deco_spec: str
|
305
|
+
) -> Tuple[List[Any], Dict[str, Any]]:
|
306
|
+
if len(deco_spec) == 0:
|
307
|
+
return [], {}
|
308
|
+
args = []
|
309
|
+
kwargs = {}
|
310
|
+
for a in re.split(r""",(?=[\s\w]+=)""", deco_spec):
|
311
|
+
name, val = a.split("=", 1)
|
312
|
+
try:
|
313
|
+
val_parsed = json.loads(val.strip().replace('\\"', '"'))
|
314
|
+
except json.JSONDecodeError:
|
315
|
+
# In this case, we try to convert to either an int or a float or
|
316
|
+
# leave as is. Prefer ints if possible.
|
317
|
+
try:
|
318
|
+
val_parsed = int(val.strip())
|
319
|
+
except ValueError:
|
320
|
+
try:
|
321
|
+
val_parsed = float(val.strip())
|
322
|
+
except ValueError:
|
323
|
+
val_parsed = val.strip()
|
324
|
+
try:
|
325
|
+
pos = int(name)
|
326
|
+
except ValueError:
|
327
|
+
kwargs[name.strip()] = val_parsed
|
328
|
+
else:
|
329
|
+
# Extend args list if needed to accommodate position
|
330
|
+
while len(args) <= pos:
|
331
|
+
args.append(None)
|
332
|
+
args[pos] = val_parsed
|
333
|
+
debug.userconf_exec(
|
334
|
+
"Parsed decorator spec for %s: %s"
|
335
|
+
% (cls.decorator_name, str((args, kwargs)))
|
336
|
+
)
|
337
|
+
return args, kwargs
|
338
|
+
|
339
|
+
@classmethod
|
340
|
+
def parse_decorator_spec(cls, deco_spec: str) -> Optional["UserStepDecoratorBase"]:
|
341
|
+
if len(deco_spec) == 0:
|
342
|
+
return cls()
|
343
|
+
args, kwargs = cls.extract_args_kwargs_from_decorator_spec(deco_spec)
|
344
|
+
return cls(*args, **kwargs)
|
345
|
+
|
346
|
+
def make_decorator_spec(self):
|
347
|
+
self.external_init()
|
348
|
+
attrs = {}
|
349
|
+
if self._args:
|
350
|
+
attrs.update({i: v for i, v in enumerate(self._args) if v is not None})
|
351
|
+
if self._kwargs:
|
352
|
+
attrs.update({k: v for k, v in self._kwargs.items() if v is not None})
|
353
|
+
if attrs:
|
354
|
+
attr_list = []
|
355
|
+
# We dump simple types directly as string to get around the nightmare quote
|
356
|
+
# escaping but for more complex types (typically dictionaries or lists),
|
357
|
+
# we dump using JSON.
|
358
|
+
for k, v in attrs.items():
|
359
|
+
if isinstance(v, (int, float, str)):
|
360
|
+
attr_list.append("%s=%s" % (k, str(v)))
|
361
|
+
else:
|
362
|
+
attr_list.append("%s=%s" % (k, json.dumps(v).replace('"', '\\"')))
|
363
|
+
|
364
|
+
attrstr = ",".join(attr_list)
|
365
|
+
return "%s:%s" % (self.decorator_name, attrstr)
|
366
|
+
else:
|
367
|
+
return self.decorator_name
|
368
|
+
|
369
|
+
def get_args_kwargs(self) -> Tuple[List[Any], Dict[str, Any]]:
|
370
|
+
"""
|
371
|
+
Get the arguments and keyword arguments of the decorator.
|
372
|
+
|
373
|
+
Returns
|
374
|
+
-------
|
375
|
+
Tuple[List[Any], Dict[str, Any]]
|
376
|
+
A tuple containing a list of arguments and a dictionary of keyword arguments.
|
377
|
+
"""
|
378
|
+
return list(self._args), dict(self._kwargs)
|
379
|
+
|
380
|
+
def init(self, *args, **kwargs):
|
381
|
+
pass
|
382
|
+
|
383
|
+
def external_init(self):
|
384
|
+
# You can use config values in the arguments to a UserStepDecoratorBase
|
385
|
+
# so we resolve those as well
|
386
|
+
self._args = [resolve_delayed_evaluator(arg) for arg in self._args]
|
387
|
+
self._kwargs, _ = unpack_delayed_evaluator(self._kwargs)
|
388
|
+
self._kwargs = {
|
389
|
+
k: resolve_delayed_evaluator(v) for k, v in self._kwargs.items()
|
390
|
+
}
|
391
|
+
if self._args or self._kwargs:
|
392
|
+
if "init" not in self.__class__.__dict__:
|
393
|
+
raise MetaflowException(
|
394
|
+
"%s is used with arguments but does not implement init" % self
|
395
|
+
)
|
396
|
+
|
397
|
+
self.init(*self._args, **self._kwargs)
|
398
|
+
|
399
|
+
|
400
|
+
class UserStepDecorator(UserStepDecoratorBase):
|
401
|
+
_step_field = "wrappers"
|
402
|
+
_allowed_args = False
|
403
|
+
_allowed_kwargs = True
|
404
|
+
|
405
|
+
def init(self, *args, **kwargs):
|
406
|
+
"""
|
407
|
+
Implement this method if your UserStepDecorator takes arguments. It replaces the
|
408
|
+
__init__ method in traditional Python classes.
|
409
|
+
|
410
|
+
|
411
|
+
As an example:
|
412
|
+
```
|
413
|
+
class MyDecorator(UserStepDecorator):
|
414
|
+
def init(self, *args, **kwargs):
|
415
|
+
self.arg1 = kwargs.get("arg1", None)
|
416
|
+
self.arg2 = kwargs.get("arg2", None)
|
417
|
+
# Do something with the arguments
|
418
|
+
```
|
419
|
+
|
420
|
+
can then be used as
|
421
|
+
```
|
422
|
+
@MyDecorator(arg1=42, arg2=conf_expr("config.my_arg2"))
|
423
|
+
@step
|
424
|
+
def start(self):
|
425
|
+
pass
|
426
|
+
```
|
427
|
+
"""
|
428
|
+
super().init()
|
429
|
+
|
430
|
+
def pre_step(
|
431
|
+
self,
|
432
|
+
step_name: str,
|
433
|
+
flow: "metaflow.flowspec.FlowSpec",
|
434
|
+
inputs: Optional["metaflow.datastore.inputs.Inputs"] = None,
|
435
|
+
) -> Optional[Callable[["metaflow.flowspec.FlowSpec", Optional[Any]], Any]]:
|
436
|
+
"""
|
437
|
+
Implement this method to perform any action prior to the execution of a step.
|
438
|
+
|
439
|
+
It should return either None to execute anything wrapped by this step decorator
|
440
|
+
as usual or a callable that will be called instead.
|
441
|
+
|
442
|
+
Parameters
|
443
|
+
----------
|
444
|
+
step_name: str
|
445
|
+
The name of the step being decorated.
|
446
|
+
flow: FlowSpec
|
447
|
+
The flow object to which the step belongs.
|
448
|
+
inputs: Optional[List[FlowSpec]]
|
449
|
+
The inputs to the step being decorated. This is only provided for join steps
|
450
|
+
and is None for all other steps.
|
451
|
+
|
452
|
+
Returns
|
453
|
+
-------
|
454
|
+
Optional[Callable[FlowSpec, Optional[Any]]]
|
455
|
+
An optional function to use instead of the wrapped step. Note that the function
|
456
|
+
returned should match the signature of the step being wrapped (join steps
|
457
|
+
take an additional "inputs" argument).
|
458
|
+
"""
|
459
|
+
return None
|
460
|
+
|
461
|
+
def post_step(
|
462
|
+
self,
|
463
|
+
step_name: str,
|
464
|
+
flow: "metaflow.flowspec.FlowSpec",
|
465
|
+
exception: Optional[Exception] = None,
|
466
|
+
):
|
467
|
+
"""
|
468
|
+
Implement this method to perform any action after the execution of a step.
|
469
|
+
|
470
|
+
If the step (or any code being wrapped by this decorator) raises an exception,
|
471
|
+
it will be passed here and can either be caught (in which case the step will
|
472
|
+
be considered as successful) or re-raised (in which case the entire step
|
473
|
+
will be considered a failure unless another decorator catches the execption).
|
474
|
+
|
475
|
+
Note that this method executes *before* artifacts are stored in the datastore
|
476
|
+
so it is able to modify, add or remove artifacts from `flow`.
|
477
|
+
|
478
|
+
Parameters
|
479
|
+
----------
|
480
|
+
step_name: str
|
481
|
+
The name of the step being decorated.
|
482
|
+
flow: FlowSpec
|
483
|
+
The flow object to which the step belongs.
|
484
|
+
exception: Optional[Exception]
|
485
|
+
The exception raised during the step execution, if any.
|
486
|
+
"""
|
487
|
+
if exception:
|
488
|
+
raise exception
|
489
|
+
|
490
|
+
@property
|
491
|
+
def skip_step(self) -> Union[bool, Dict[str, Any]]:
|
492
|
+
"""
|
493
|
+
Returns whether or not the step (or rather anything wrapped by this decorator)
|
494
|
+
should be skipped
|
495
|
+
|
496
|
+
Returns
|
497
|
+
-------
|
498
|
+
Union[bool, Dict[str, Any]]
|
499
|
+
False if the step should not be skipped. True if it should be skipped and
|
500
|
+
a dictionary if it should be skipped and the values passed in used as
|
501
|
+
the arguments to the self.next call.
|
502
|
+
"""
|
503
|
+
return getattr(self, "_skip_step", False)
|
504
|
+
|
505
|
+
@skip_step.setter
|
506
|
+
def skip_step(self, value: Union[bool, Dict[str, Any]]):
|
507
|
+
"""
|
508
|
+
Set the skip_step property. You can set it to:
|
509
|
+
- True to skip the step
|
510
|
+
- False to not skip the step (default)
|
511
|
+
- A dictionary with the keys valid in the `self.next` call.
|
512
|
+
|
513
|
+
Parameters
|
514
|
+
----------
|
515
|
+
value: Union[bool, Dict[str, Any]]
|
516
|
+
True/False or a dictionary with the keys valid in the `self.next` call.
|
517
|
+
"""
|
518
|
+
self._skip_step = value
|
519
|
+
|
520
|
+
|
521
|
+
def user_step_decorator(*args, **kwargs):
|
522
|
+
"""
|
523
|
+
Use this decorator to transform a generator function into a user step decorator.
|
524
|
+
|
525
|
+
As an example:
|
526
|
+
|
527
|
+
```
|
528
|
+
@user_step_decorator
|
529
|
+
def timing(step_name, flow, inputs):
|
530
|
+
start_time = time.time()
|
531
|
+
yield
|
532
|
+
end_time = time.time()
|
533
|
+
flow.artifact_total_time = end_time - start_time
|
534
|
+
print(f"Step {step_name} took {flow.artifact_total_time} seconds")
|
535
|
+
```
|
536
|
+
which can then be used as:
|
537
|
+
|
538
|
+
```
|
539
|
+
@timing
|
540
|
+
@step
|
541
|
+
def start(self):
|
542
|
+
print("Hello, world!")
|
543
|
+
```
|
544
|
+
|
545
|
+
Your generator should:
|
546
|
+
- yield at most once -- if you do not yield, the step will not execute.
|
547
|
+
- yield:
|
548
|
+
- None
|
549
|
+
- a callable that will replace whatever is being wrapped (it
|
550
|
+
should have the same parameters as the wrapped function, namely, it should
|
551
|
+
be a
|
552
|
+
Callable[[FlowSpec, Inputs], Optional[Union[Dict[str, Any]]]]).
|
553
|
+
Note that the return type is a bit different -- you can return:
|
554
|
+
- None: no special behavior;
|
555
|
+
- A dictionary containing parameters for `self.next()`.
|
556
|
+
- a dictionary to skip the step. An empty dictionary is equivalent
|
557
|
+
to just skipping the step. A full dictionary will pass the arguments
|
558
|
+
to the `self.next()` call -- this allows you to modify the behavior
|
559
|
+
of `self.next` (for example, changing the `foreach` values. We provide
|
560
|
+
USER_SKIP_STEP as a special value that is equivalent to {}.
|
561
|
+
|
562
|
+
|
563
|
+
You are able to catch exceptions thrown by the yield statement (ie: coming from the
|
564
|
+
wrapped code). Catching and not re-raising the exception will make the step
|
565
|
+
successful.
|
566
|
+
|
567
|
+
Note that you are able to modify the step's artifact after the yield.
|
568
|
+
|
569
|
+
For more complex use cases, you can use the `UserStepDecorator` class directly which
|
570
|
+
allows more control.
|
571
|
+
"""
|
572
|
+
if args:
|
573
|
+
# If we have args, we either had @user_step_decorator with no argument or we had
|
574
|
+
# @user_step_decorator(arg="foo") and transformed it into
|
575
|
+
# @user_step_decorator(step, arg="foo")
|
576
|
+
obj = args[0]
|
577
|
+
name = f"{obj.__module__}.{obj.__name__}"
|
578
|
+
|
579
|
+
if not isinstance(obj, types.FunctionType) or not inspect.isgeneratorfunction(
|
580
|
+
obj
|
581
|
+
):
|
582
|
+
raise MetaflowException(
|
583
|
+
"@user_step_decorator can only decorate generator functions."
|
584
|
+
)
|
585
|
+
sig = inspect.signature(obj)
|
586
|
+
arg_count = len(sig.parameters)
|
587
|
+
if kwargs:
|
588
|
+
if arg_count != 4:
|
589
|
+
raise MetaflowException(
|
590
|
+
"@user_step_decorator(<kwargs>) can only decorate generator "
|
591
|
+
"functions with 4 arguments (step_name, flow, inputs, attributes)"
|
592
|
+
)
|
593
|
+
elif arg_count not in (3, 4):
|
594
|
+
raise MetaflowException(
|
595
|
+
"@user_step_decorator can only decorator generator functions with 3 or "
|
596
|
+
"4 arguments (step_name, flow, inputs [, attributes])."
|
597
|
+
)
|
598
|
+
|
599
|
+
class WrapClass(UserStepDecorator):
|
600
|
+
_allowed_args = False
|
601
|
+
_allowed_kwargs = True
|
602
|
+
_step_field = "wrappers"
|
603
|
+
_decorator_name = name
|
604
|
+
_original_module = obj.__module__
|
605
|
+
|
606
|
+
def __init__(self, *args, **kwargs):
|
607
|
+
super().__init__(*args, **kwargs)
|
608
|
+
self._generator = obj
|
609
|
+
|
610
|
+
def init(self, *args, **kwargs):
|
611
|
+
if args:
|
612
|
+
raise MetaflowException(
|
613
|
+
"%s does not allow arguments, only keyword arguments"
|
614
|
+
% str(self)
|
615
|
+
)
|
616
|
+
self._kwargs = kwargs
|
617
|
+
|
618
|
+
def pre_step(self, step_name, flow, inputs):
|
619
|
+
if arg_count == 4:
|
620
|
+
self._generator = self._generator(
|
621
|
+
step_name, flow, inputs, self._kwargs
|
622
|
+
)
|
623
|
+
else:
|
624
|
+
self._generator = self._generator(step_name, flow, inputs)
|
625
|
+
v = self._generator.send(None)
|
626
|
+
if isinstance(v, dict):
|
627
|
+
# We are skipping the step
|
628
|
+
if v:
|
629
|
+
self.skip_step = v
|
630
|
+
else:
|
631
|
+
self.skip_step = True
|
632
|
+
return None
|
633
|
+
return v
|
634
|
+
|
635
|
+
def post_step(self, step_name, flow, exception=None):
|
636
|
+
try:
|
637
|
+
if exception:
|
638
|
+
self._generator.throw(exception)
|
639
|
+
else:
|
640
|
+
self._generator.send(None)
|
641
|
+
except StopIteration:
|
642
|
+
pass
|
643
|
+
else:
|
644
|
+
raise MetaflowException(" %s should only yield once" % self)
|
645
|
+
|
646
|
+
return WrapClass
|
647
|
+
else:
|
648
|
+
# Capture arguments passed to user_step_decorator
|
649
|
+
def wrap(f):
|
650
|
+
return user_step_decorator(f, **kwargs)
|
651
|
+
|
652
|
+
return wrap
|
653
|
+
|
654
|
+
|
655
|
+
class StepMutator(UserStepDecoratorBase):
|
656
|
+
"""
|
657
|
+
Derive from this class to implement a step mutator.
|
658
|
+
|
659
|
+
A step mutator allows you to introspect a step and add decorators to it. You can
|
660
|
+
use values available through configurations to determine how to mutate the step.
|
661
|
+
|
662
|
+
There are two main methods provided:
|
663
|
+
- pre_mutate: called as early as possible right after configuration values are read.
|
664
|
+
- mutate: called right after all the command line is parsed but before any
|
665
|
+
Metaflow decorators are applied.
|
666
|
+
"""
|
667
|
+
|
668
|
+
_step_field = "config_decorators"
|
669
|
+
_allowed_args = True
|
670
|
+
_allowed_kwargs = True
|
671
|
+
|
672
|
+
def init(self, *args, **kwargs):
|
673
|
+
"""
|
674
|
+
Implement this method if you wish for your StepMutator to take in arguments.
|
675
|
+
|
676
|
+
Your step-mutator can then look like:
|
677
|
+
|
678
|
+
@MyMutator(arg1, arg2)
|
679
|
+
@step
|
680
|
+
def my_step(self):
|
681
|
+
pass
|
682
|
+
|
683
|
+
It is an error to use your mutator with arguments but not implement this method.
|
684
|
+
"""
|
685
|
+
super().init()
|
686
|
+
|
687
|
+
def pre_mutate(
|
688
|
+
self, mutable_step: "metaflow.user_decorators.mutable_step.MutableStep"
|
689
|
+
) -> None:
|
690
|
+
"""
|
691
|
+
Method called right after all configuration values are read.
|
692
|
+
|
693
|
+
Parameters
|
694
|
+
----------
|
695
|
+
mutable_step : metaflow.user_decorators.mutable_step.MutableStep
|
696
|
+
A representation of this step
|
697
|
+
"""
|
698
|
+
return None
|
699
|
+
|
700
|
+
def mutate(
|
701
|
+
self, mutable_step: "metaflow.user_decorators.mutable_step.MutableStep"
|
702
|
+
) -> None:
|
703
|
+
"""
|
704
|
+
Method called right before the first Metaflow decorator is applied. This
|
705
|
+
means that the command line, including all `--with` options has been parsed.
|
706
|
+
|
707
|
+
Parameters
|
708
|
+
----------
|
709
|
+
mutable_step : metaflow.user_decorators.mutable_step.MutableStep
|
710
|
+
A representation of this step
|
711
|
+
"""
|
712
|
+
return None
|