metaflow 2.15.20__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.
Files changed (74) hide show
  1. metaflow/__init__.py +7 -1
  2. metaflow/cli.py +16 -1
  3. metaflow/cli_components/init_cmd.py +1 -0
  4. metaflow/cli_components/run_cmds.py +6 -2
  5. metaflow/client/core.py +22 -30
  6. metaflow/datastore/task_datastore.py +0 -1
  7. metaflow/debug.py +5 -0
  8. metaflow/decorators.py +230 -70
  9. metaflow/extension_support/__init__.py +15 -8
  10. metaflow/extension_support/_empty_file.py +2 -2
  11. metaflow/flowspec.py +80 -53
  12. metaflow/graph.py +24 -2
  13. metaflow/meta_files.py +13 -0
  14. metaflow/metadata_provider/metadata.py +7 -1
  15. metaflow/metaflow_config.py +5 -0
  16. metaflow/metaflow_environment.py +82 -25
  17. metaflow/metaflow_version.py +1 -1
  18. metaflow/package/__init__.py +664 -0
  19. metaflow/packaging_sys/__init__.py +870 -0
  20. metaflow/packaging_sys/backend.py +113 -0
  21. metaflow/packaging_sys/distribution_support.py +153 -0
  22. metaflow/packaging_sys/tar_backend.py +86 -0
  23. metaflow/packaging_sys/utils.py +91 -0
  24. metaflow/packaging_sys/v1.py +476 -0
  25. metaflow/plugins/airflow/airflow.py +5 -1
  26. metaflow/plugins/airflow/airflow_cli.py +15 -4
  27. metaflow/plugins/argo/argo_workflows.py +23 -17
  28. metaflow/plugins/argo/argo_workflows_cli.py +16 -4
  29. metaflow/plugins/aws/batch/batch.py +22 -3
  30. metaflow/plugins/aws/batch/batch_cli.py +3 -0
  31. metaflow/plugins/aws/batch/batch_decorator.py +13 -5
  32. metaflow/plugins/aws/step_functions/step_functions.py +4 -1
  33. metaflow/plugins/aws/step_functions/step_functions_cli.py +15 -4
  34. metaflow/plugins/cards/card_decorator.py +0 -5
  35. metaflow/plugins/kubernetes/kubernetes.py +8 -1
  36. metaflow/plugins/kubernetes/kubernetes_cli.py +3 -0
  37. metaflow/plugins/kubernetes/kubernetes_decorator.py +13 -5
  38. metaflow/plugins/package_cli.py +25 -23
  39. metaflow/plugins/parallel_decorator.py +4 -2
  40. metaflow/plugins/pypi/bootstrap.py +8 -2
  41. metaflow/plugins/pypi/conda_decorator.py +39 -82
  42. metaflow/plugins/pypi/conda_environment.py +6 -2
  43. metaflow/plugins/pypi/pypi_decorator.py +4 -4
  44. metaflow/plugins/test_unbounded_foreach_decorator.py +2 -2
  45. metaflow/plugins/timeout_decorator.py +0 -1
  46. metaflow/plugins/uv/bootstrap.py +11 -0
  47. metaflow/plugins/uv/uv_environment.py +4 -2
  48. metaflow/pylint_wrapper.py +5 -1
  49. metaflow/runner/click_api.py +5 -4
  50. metaflow/runner/subprocess_manager.py +14 -2
  51. metaflow/runtime.py +37 -11
  52. metaflow/task.py +91 -7
  53. metaflow/user_configs/config_options.py +13 -8
  54. metaflow/user_configs/config_parameters.py +0 -4
  55. metaflow/user_decorators/__init__.py +0 -0
  56. metaflow/user_decorators/common.py +144 -0
  57. metaflow/user_decorators/mutable_flow.py +499 -0
  58. metaflow/user_decorators/mutable_step.py +424 -0
  59. metaflow/user_decorators/user_flow_decorator.py +263 -0
  60. metaflow/user_decorators/user_step_decorator.py +712 -0
  61. metaflow/util.py +4 -1
  62. metaflow/version.py +1 -1
  63. {metaflow-2.15.20.dist-info → metaflow-2.16.0.dist-info}/METADATA +2 -2
  64. {metaflow-2.15.20.dist-info → metaflow-2.16.0.dist-info}/RECORD +71 -60
  65. metaflow/info_file.py +0 -25
  66. metaflow/package.py +0 -203
  67. metaflow/user_configs/config_decorators.py +0 -568
  68. {metaflow-2.15.20.data → metaflow-2.16.0.data}/data/share/metaflow/devtools/Makefile +0 -0
  69. {metaflow-2.15.20.data → metaflow-2.16.0.data}/data/share/metaflow/devtools/Tiltfile +0 -0
  70. {metaflow-2.15.20.data → metaflow-2.16.0.data}/data/share/metaflow/devtools/pick_services.sh +0 -0
  71. {metaflow-2.15.20.dist-info → metaflow-2.16.0.dist-info}/WHEEL +0 -0
  72. {metaflow-2.15.20.dist-info → metaflow-2.16.0.dist-info}/entry_points.txt +0 -0
  73. {metaflow-2.15.20.dist-info → metaflow-2.16.0.dist-info}/licenses/LICENSE +0 -0
  74. {metaflow-2.15.20.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