metaflow 2.12.36__py2.py3-none-any.whl → 2.12.37__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 (45) hide show
  1. metaflow/__init__.py +3 -0
  2. metaflow/cli.py +84 -697
  3. metaflow/cli_args.py +17 -0
  4. metaflow/cli_components/__init__.py +0 -0
  5. metaflow/cli_components/dump_cmd.py +96 -0
  6. metaflow/cli_components/init_cmd.py +51 -0
  7. metaflow/cli_components/run_cmds.py +358 -0
  8. metaflow/cli_components/step_cmd.py +189 -0
  9. metaflow/cli_components/utils.py +140 -0
  10. metaflow/cmd/develop/stub_generator.py +9 -2
  11. metaflow/decorators.py +54 -2
  12. metaflow/extension_support/plugins.py +41 -27
  13. metaflow/flowspec.py +156 -16
  14. metaflow/includefile.py +50 -22
  15. metaflow/metaflow_config.py +1 -1
  16. metaflow/package.py +17 -3
  17. metaflow/parameters.py +80 -23
  18. metaflow/plugins/__init__.py +4 -0
  19. metaflow/plugins/airflow/airflow_cli.py +1 -0
  20. metaflow/plugins/argo/argo_workflows.py +41 -1
  21. metaflow/plugins/argo/argo_workflows_cli.py +1 -0
  22. metaflow/plugins/aws/batch/batch_decorator.py +2 -2
  23. metaflow/plugins/aws/step_functions/step_functions.py +32 -0
  24. metaflow/plugins/aws/step_functions/step_functions_cli.py +1 -0
  25. metaflow/plugins/datatools/s3/s3op.py +3 -3
  26. metaflow/plugins/kubernetes/kubernetes_cli.py +1 -1
  27. metaflow/plugins/kubernetes/kubernetes_decorator.py +2 -2
  28. metaflow/plugins/pypi/conda_decorator.py +22 -0
  29. metaflow/plugins/pypi/pypi_decorator.py +1 -0
  30. metaflow/plugins/timeout_decorator.py +2 -2
  31. metaflow/runner/click_api.py +73 -19
  32. metaflow/runtime.py +111 -73
  33. metaflow/sidecar/sidecar_worker.py +1 -1
  34. metaflow/user_configs/__init__.py +0 -0
  35. metaflow/user_configs/config_decorators.py +563 -0
  36. metaflow/user_configs/config_options.py +495 -0
  37. metaflow/user_configs/config_parameters.py +386 -0
  38. metaflow/util.py +17 -0
  39. metaflow/version.py +1 -1
  40. {metaflow-2.12.36.dist-info → metaflow-2.12.37.dist-info}/METADATA +3 -2
  41. {metaflow-2.12.36.dist-info → metaflow-2.12.37.dist-info}/RECORD +45 -35
  42. {metaflow-2.12.36.dist-info → metaflow-2.12.37.dist-info}/LICENSE +0 -0
  43. {metaflow-2.12.36.dist-info → metaflow-2.12.37.dist-info}/WHEEL +0 -0
  44. {metaflow-2.12.36.dist-info → metaflow-2.12.37.dist-info}/entry_points.txt +0 -0
  45. {metaflow-2.12.36.dist-info → metaflow-2.12.37.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,563 @@
1
+ from functools import partial
2
+ from typing import Any, Callable, Generator, Optional, TYPE_CHECKING, Tuple, Union
3
+
4
+ from metaflow.debug import debug
5
+ from metaflow.exception import MetaflowException
6
+ from metaflow.user_configs.config_parameters import (
7
+ ConfigValue,
8
+ resolve_delayed_evaluator,
9
+ )
10
+
11
+ if TYPE_CHECKING:
12
+ import metaflow.decorators
13
+ import metaflow.flowspec
14
+ import metaflow.parameters
15
+
16
+
17
+ class MutableStep:
18
+ """
19
+ A MutableStep is a wrapper passed to the `CustomStepDecorator`'s `evaluate` method
20
+ to allow the decorator to interact with the step and providing easy methods to
21
+ modify the behavior of the step.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ flow_spec: "metaflow.flowspec.FlowSpec",
27
+ step: Union[
28
+ Callable[["metaflow.decorators.FlowSpecDerived"], None],
29
+ Callable[["metaflow.decorators.FlowSpecDerived", Any], None],
30
+ ],
31
+ ):
32
+ self._mutable_container = MutableFlow(flow_spec)
33
+ self._my_step = step
34
+
35
+ @property
36
+ def flow(self) -> "MutableFlow":
37
+ """
38
+ The flow that contains this step
39
+
40
+ Returns
41
+ -------
42
+ MutableFlow
43
+ The flow that contains this step
44
+ """
45
+ return self._mutable_container
46
+
47
+ @property
48
+ def decorators(self) -> Generator["metaflow.decorators.StepDecorator", None, None]:
49
+ """
50
+ Iterate over all the decorators of this step. Note that the same type of decorator
51
+ may be present multiple times and no order is guaranteed.
52
+
53
+ Yields
54
+ ------
55
+ metaflow.decorators.StepDecorator
56
+ A decorator of the step
57
+ """
58
+ for deco in self._my_step.decorators:
59
+ yield deco
60
+
61
+ def add_decorator(self, deco_type: partial, **kwargs) -> None:
62
+ """
63
+ Add a Metaflow decorator to a step.
64
+
65
+ Parameters
66
+ ----------
67
+ deco_type : partial
68
+ The decorator class to add to this step
69
+ """
70
+ # Prevent circular import
71
+ from metaflow.decorators import DuplicateStepDecoratorException, StepDecorator
72
+
73
+ # Validate deco_type
74
+ if (
75
+ not isinstance(deco_type, partial)
76
+ or len(deco_type.args) != 1
77
+ or not issubclass(deco_type.args[0], StepDecorator)
78
+ ):
79
+ raise TypeError("add_decorator takes a StepDecorator")
80
+
81
+ deco_type = deco_type.args[0]
82
+ if (
83
+ deco_type.name in [deco.name for deco in self._my_step.decorators]
84
+ and not deco_type.allow_multiple
85
+ ):
86
+ raise DuplicateStepDecoratorException(deco_type.name, self._my_step)
87
+
88
+ debug.userconf_exec(
89
+ "Mutable decorator adding step decorator %s to step %s"
90
+ % (deco_type.name, self._my_step.name)
91
+ )
92
+ self._my_step.decorators.append(
93
+ deco_type(attributes=kwargs, statically_defined=True)
94
+ )
95
+
96
+ def remove_decorator(self, deco_name: str, all: bool = True, **kwargs) -> bool:
97
+ """
98
+ Remove one or more Metaflow decorators from a step.
99
+
100
+ Some decorators can be applied multiple times to a step. This method allows you
101
+ to choose which decorator to remove or just remove all of them or one of them.
102
+
103
+ Parameters
104
+ ----------
105
+ deco_name : str
106
+ Name of the decorator to remove
107
+ all : bool, default True
108
+ If True, remove all instances of the decorator that match the filters
109
+ passed using kwargs (or all the instances of the decorator if no filters are
110
+ passed). If False, removes only the first found instance of the decorator.
111
+
112
+ Returns
113
+ -------
114
+ bool
115
+ Returns True if at least one decorator was removed.
116
+ """
117
+ new_deco_list = []
118
+ did_remove = False
119
+ for deco in self._my_step.decorators:
120
+ if deco.name == deco_name:
121
+ # Evaluate all the configuration values if any
122
+ deco.init()
123
+
124
+ # Check filters
125
+ match_ok = True
126
+ if kwargs:
127
+ for k, v in kwargs.items():
128
+ match_ok = k in deco.attributes and deco.attributes[k] == v
129
+ if match_ok is False:
130
+ break
131
+ if match_ok:
132
+ did_remove = True
133
+ debug.userconf_exec(
134
+ "Mutable decorator removing step decorator %s from step %s"
135
+ % (deco.name, self._my_step.name)
136
+ )
137
+ else:
138
+ new_deco_list.append(deco)
139
+ else:
140
+ new_deco_list.append(deco)
141
+ if did_remove and not all:
142
+ break
143
+
144
+ self._my_step.decorators = new_deco_list
145
+ return did_remove
146
+
147
+
148
+ class MutableFlow:
149
+ def __init__(self, flow_spec: "metaflow.flowspec.FlowSpec"):
150
+ self._flow_cls = flow_spec
151
+
152
+ @property
153
+ def decorators(self) -> Generator["metaflow.decorators.FlowDecorator", None, None]:
154
+ """
155
+ Iterate over all the decorators of this flow. Note that the same type of decorator
156
+ may be present multiple times and no order is guaranteed.
157
+
158
+ Yields
159
+ ------
160
+ metaflow.decorators.FlowDecorator
161
+ A decorator of the flow
162
+ """
163
+ for decos in self._flow_cls._flow_decorators.values():
164
+ for deco in decos:
165
+ yield deco
166
+
167
+ @property
168
+ def configs(self) -> Generator[Tuple[str, ConfigValue], None, None]:
169
+ """
170
+ Iterate over all user configurations in this flow
171
+
172
+ Use this to parameterize your flow based on configuration. As an example, the
173
+ `evaluate` method of your `CustomFlowDecorator` can use this to add an
174
+ environment decorator.
175
+ ```
176
+ class MyDecorator(CustomFlowDecorator):
177
+ def evaluate(flow: MutableFlow):
178
+ val = next(flow.configs)[1].steps.start.cpu
179
+ flow.start.add_decorator(environment, vars={'mycpu': val})
180
+ return flow
181
+
182
+ @MyDecorator()
183
+ class TestFlow(FlowSpec):
184
+ config = Config('myconfig.json')
185
+
186
+ @step
187
+ def start(self):
188
+ pass
189
+ ```
190
+ can be used to add an environment decorator to the `start` step.
191
+
192
+ Yields
193
+ ------
194
+ Tuple[str, ConfigValue]
195
+ Iterates over the configurations of the flow
196
+ """
197
+ from metaflow.flowspec import _FlowState
198
+
199
+ # When configs are parsed, they are loaded in _flow_state[_FlowState.CONFIGS]
200
+ for name, value in self._flow_cls._flow_state.get(
201
+ _FlowState.CONFIGS, {}
202
+ ).items():
203
+ yield name, ConfigValue(value)
204
+
205
+ @property
206
+ def parameters(self) -> Generator[Tuple[str, Any], None, None]:
207
+ for var, param in self._flow_cls._get_parameters():
208
+ if param.IS_CONFIG_PARAMETER:
209
+ continue
210
+ yield var, param
211
+
212
+ @property
213
+ def steps(self) -> Generator[Tuple[str, MutableStep], None, None]:
214
+ """
215
+ Iterate over all the steps in this flow. The order of the steps
216
+ returned is not guaranteed.
217
+
218
+ Yields
219
+ ------
220
+ Tuple[str, MutableStep]
221
+ A tuple with the step name and the step proxy
222
+ """
223
+ for var in dir(self._flow_cls):
224
+ potential_step = getattr(self._flow_cls, var)
225
+ if callable(potential_step) and hasattr(potential_step, "is_step"):
226
+ yield var, MutableStep(self._flow_cls, potential_step)
227
+
228
+ def add_parameter(
229
+ self, name: str, value: "metaflow.parameters.Parameter", overwrite: bool = False
230
+ ) -> None:
231
+ from metaflow.parameters import Parameter
232
+
233
+ if hasattr(self._flow_cls, name) and not overwrite:
234
+ raise MetaflowException(
235
+ "Flow '%s' already has a class member '%s' -- "
236
+ "set overwrite=True in add_parameter to overwrite it."
237
+ % (self._flow_cls.__name__, name)
238
+ )
239
+ if not isinstance(value, Parameter) or value.IS_CONFIG_PARAMETER:
240
+ raise MetaflowException(
241
+ "Only a Parameter or an IncludeFile can be added using `add_parameter`"
242
+ "; got %s" % type(value)
243
+ )
244
+ debug.userconf_exec("Mutable flow decorator adding parameter %s to flow" % name)
245
+ setattr(self._flow_cls, name, value)
246
+
247
+ def remove_parameter(self, parameter_name: str) -> bool:
248
+ """
249
+ Remove a parameter from the flow.
250
+
251
+ The name given should match the name of the parameter (can be different
252
+ from the name of the parameter in the flow. You can not remove config parameters.
253
+
254
+ Parameters
255
+ ----------
256
+ parameter_name : str
257
+ Name of the parameter
258
+
259
+ Returns
260
+ -------
261
+ bool
262
+ Returns True if the parameter was removed
263
+ """
264
+ from metaflow.flowspec import _FlowState
265
+
266
+ for var, param in self._flow_cls._get_parameters():
267
+ if param.IS_CONFIG_PARAMETER:
268
+ continue
269
+ if param.name == parameter_name:
270
+ delattr(self._flow_cls, var)
271
+ debug.userconf_exec(
272
+ "Mutable flow decorator removing parameter %s from flow" % var
273
+ )
274
+ # Reset so that we don't list it again
275
+ del self._flow_cls._flow_state[_FlowState.CACHED_PARAMETERS]
276
+ return True
277
+ return False
278
+
279
+ def add_decorator(self, deco_type: partial, **kwargs) -> None:
280
+ """
281
+ Add a Metaflow decorator to a flow.
282
+
283
+ Parameters
284
+ ----------
285
+ deco_type : partial
286
+ The decorator class to add to this flow
287
+ """
288
+ # Prevent circular import
289
+ from metaflow.decorators import DuplicateFlowDecoratorException, FlowDecorator
290
+
291
+ # Validate deco_type
292
+ if (
293
+ not isinstance(deco_type, partial)
294
+ or len(deco_type.args) != 1
295
+ or not issubclass(deco_type.args[0], FlowDecorator)
296
+ ):
297
+ raise TypeError("add_decorator takes a FlowDecorator")
298
+
299
+ deco_type = deco_type.args[0]
300
+ if (
301
+ deco_type.name in self._flow_cls._flow_decorators
302
+ and not deco_type.allow_multiple
303
+ ):
304
+ raise DuplicateFlowDecoratorException(deco_type.name)
305
+
306
+ self._flow_cls._flow_decorators.setdefault(deco_type.name, []).append(
307
+ deco_type(attributes=kwargs, statically_defined=True)
308
+ )
309
+ debug.userconf_exec(
310
+ "Mutable flow decorator adding decorator %s to flow" % deco_type.name
311
+ )
312
+
313
+ def remove_decorator(self, deco_name: str, all: bool = True, **kwargs) -> bool:
314
+ """
315
+ Remove one or more Metaflow decorators from a flow.
316
+
317
+ Some decorators can be applied multiple times to a flow. This method allows you
318
+ to choose which decorator to remove or just remove all of them or one of them.
319
+
320
+ Parameters
321
+ ----------
322
+ deco_name : str
323
+ Name of the decorator to remove
324
+ all : bool, default True
325
+ If True, remove all instances of the decorator that match the filters
326
+ passed using kwargs (or all the instances of the decorator if no filters are
327
+ passed). If False, removes only the first found instance of the decorator.
328
+
329
+ Returns
330
+ -------
331
+ bool
332
+ Returns True if at least one decorator was removed.
333
+ """
334
+ new_deco_list = []
335
+ old_deco_list = self._flow_cls._flow_decorators.get(deco_name)
336
+ if old_deco_list is None:
337
+ return False
338
+
339
+ did_remove = False
340
+ for deco in old_deco_list:
341
+ # Evaluate all the configuration values if any
342
+ deco.init()
343
+
344
+ # Check filters
345
+ match_ok = True
346
+ if kwargs:
347
+ for k, v in kwargs.items():
348
+ match_ok = k in deco.attributes and deco.attributes[k] == v
349
+ if match_ok is False:
350
+ break
351
+ if match_ok:
352
+ did_remove = True
353
+ debug.userconf_exec(
354
+ "Mutable flow decorator removing decorator %s from flow" % deco.name
355
+ )
356
+ else:
357
+ new_deco_list.append(deco)
358
+ if did_remove and not all:
359
+ break
360
+
361
+ if new_deco_list:
362
+ self._flow_cls._flow_decorators[deco_name] = new_deco_list
363
+ else:
364
+ del self._flow_cls._flow_decorators[deco_name]
365
+ return did_remove
366
+
367
+ def __getattr__(self, name):
368
+ # We allow direct access to the steps, configs and parameters but nothing else
369
+ from metaflow.parameters import Parameter
370
+
371
+ attr = getattr(self._flow_cls, name)
372
+ if attr:
373
+ # Steps
374
+ if callable(attr) and hasattr(attr, "is_step"):
375
+ return MutableStep(self._flow_cls, attr)
376
+ if name[0] == "_" or name in self._flow_cls._NON_PARAMETERS:
377
+ raise AttributeError(self, name)
378
+ if isinstance(attr, (Parameter, ConfigValue)):
379
+ return attr
380
+ raise AttributeError(self, name)
381
+
382
+
383
+ class CustomFlowDecorator:
384
+ def __init__(self, *args, **kwargs):
385
+ from ..flowspec import FlowSpecMeta
386
+
387
+ if args and isinstance(args[0], (CustomFlowDecorator, FlowSpecMeta)):
388
+ # This means the decorator is bare like @MyDecorator
389
+ # and the first argument is the FlowSpec or another decorator (they
390
+ # can be stacked)
391
+
392
+ # If we have a init function, we call it with no arguments -- this can
393
+ # happen if the user defines a function with default parameters for example
394
+ try:
395
+ self.init()
396
+ except NotImplementedError:
397
+ pass
398
+
399
+ # Now set the flow class we apply to
400
+ if isinstance(args[0], FlowSpecMeta):
401
+ self._set_flow_cls(args[0])
402
+ else:
403
+ self._set_flow_cls(args[0]._flow_cls)
404
+ else:
405
+ # The arguments are actually passed to the init function for this decorator
406
+ self._args = args
407
+ self._kwargs = kwargs
408
+
409
+ def __get__(self, instance, owner):
410
+ # Required so that we "present" as a FlowSpec when the flow decorator is
411
+ # of the form
412
+ # @MyFlowDecorator
413
+ # class MyFlow(FlowSpec):
414
+ # pass
415
+ #
416
+ # In that case, if we don't have __get__, the object is a CustomFlowDecorator
417
+ # and not a FlowSpec. This is more critical for steps (and CustomStepDecorator)
418
+ # because other parts of the code rely on steps having is_step. There are
419
+ # other ways to solve this but this allowed for minimal changes going forward.
420
+ return self()
421
+
422
+ def __call__(
423
+ self, flow_spec: Optional["metaflow.flowspec.FlowSpecMeta"] = None
424
+ ) -> "metaflow.flowspec.FlowSpecMeta":
425
+ if flow_spec:
426
+ # This is the case of a decorator @MyDecorator(foo=1, bar=2) and so
427
+ # we already called __init__ and saved foo and bar in self._args and
428
+ # self._kwargs and are now calling this on the flow itself.
429
+
430
+ # You can use config values in the arguments to a CustomFlowDecorator
431
+ # so we resolve those as well
432
+ new_args = [resolve_delayed_evaluator(arg) for arg in self._args]
433
+ new_kwargs = {
434
+ k: resolve_delayed_evaluator(v) for k, v in self._kwargs.items()
435
+ }
436
+ self.init(*new_args, **new_kwargs)
437
+ if hasattr(self, "_empty_init"):
438
+ raise MetaflowException(
439
+ "CustomFlowDecorator '%s' is used with arguments "
440
+ "but does not implement init" % str(self.__class__)
441
+ )
442
+
443
+ return self._set_flow_cls(flow_spec)
444
+ elif not self._flow_cls:
445
+ # This means that somehow the initialization did not happen properly
446
+ # so this may have been applied to a non flow
447
+ raise MetaflowException(
448
+ "A CustomFlowDecorator can only be applied to a FlowSpec"
449
+ )
450
+ return self._flow_cls()
451
+
452
+ def _set_flow_cls(
453
+ self, flow_spec: "metaflow.flowspec.FlowSpecMeta"
454
+ ) -> "metaflow.flowspec.FlowSpecMeta":
455
+ from ..flowspec import _FlowState
456
+
457
+ flow_spec._flow_state.setdefault(_FlowState.CONFIG_DECORATORS, []).append(self)
458
+ self._flow_cls = flow_spec
459
+ return flow_spec
460
+
461
+ def init(self, *args, **kwargs):
462
+ """
463
+ This method is intended to be optionally overridden if you need to
464
+ have an initializer.
465
+ """
466
+ self._empty_init = True
467
+
468
+ def evaluate(self, mutable_flow: MutableFlow) -> None:
469
+ """
470
+ Implement this method to act on the flow and modify it as needed.
471
+
472
+ Parameters
473
+ ----------
474
+ mutable_flow : MutableFlow
475
+ Flow
476
+
477
+ Raises
478
+ ------
479
+ NotImplementedError
480
+ _description_
481
+ """
482
+ raise NotImplementedError()
483
+
484
+
485
+ class CustomStepDecorator:
486
+ def __init__(self, *args, **kwargs):
487
+ arg = None
488
+ if args:
489
+ if isinstance(args[0], CustomStepDecorator):
490
+ arg = args[0]._my_step
491
+ else:
492
+ arg = args[0]
493
+ if arg and callable(arg) and hasattr(arg, "is_step"):
494
+ # This means the decorator is bare like @MyDecorator
495
+ # and the first argument is the step
496
+ self._set_my_step(arg)
497
+ else:
498
+ # The arguments are actually passed to the init function for this decorator
499
+ self._args = args
500
+ self._kwargs = kwargs
501
+
502
+ def __get__(self, instance, owner):
503
+ # See explanation in CustomFlowDecorator.__get__
504
+ return self()
505
+
506
+ def __call__(
507
+ self,
508
+ step: Optional[
509
+ Union[
510
+ Callable[["metaflow.decorators.FlowSpecDerived"], None],
511
+ Callable[["metaflow.decorators.FlowSpecDerived", Any], None],
512
+ ]
513
+ ] = None,
514
+ ) -> Union[
515
+ Callable[["metaflow.decorators.FlowSpecDerived"], None],
516
+ Callable[["metaflow.decorators.FlowSpecDerived", Any], None],
517
+ ]:
518
+ if step:
519
+ # You can use config values in the arguments to a CustomFlowDecorator
520
+ # so we resolve those as well
521
+ new_args = [resolve_delayed_evaluator(arg) for arg in self._args]
522
+ new_kwargs = {
523
+ k: resolve_delayed_evaluator(v) for k, v in self._kwargs.items()
524
+ }
525
+ self.init(*new_args, **new_kwargs)
526
+ if hasattr(self, "_empty_init"):
527
+ raise MetaflowException(
528
+ "CustomStepDecorator '%s' is used with arguments "
529
+ "but does not implement init" % str(self.__class__)
530
+ )
531
+ return self._set_my_step(step)
532
+ elif not self._my_step:
533
+ # This means that somehow the initialization did not happen properly
534
+ # so this may have been applied to a non step
535
+ raise MetaflowException(
536
+ "A CustomStepDecorator can only be applied to a step function"
537
+ )
538
+ return self._my_step
539
+
540
+ def _set_my_step(
541
+ self,
542
+ step: Union[
543
+ Callable[["metaflow.decorators.FlowSpecDerived"], None],
544
+ Callable[["metaflow.decorators.FlowSpecDerived", Any], None],
545
+ ],
546
+ ) -> Union[
547
+ Callable[["metaflow.decorators.FlowSpecDerived"], None],
548
+ Callable[["metaflow.decorators.FlowSpecDerived", Any], None],
549
+ ]:
550
+
551
+ self._my_step = step
552
+ self._my_step.config_decorators.append(self)
553
+ return self._my_step
554
+
555
+ def init(self, *args, **kwargs):
556
+ """
557
+ This method is intended to be optionally overridden if you need to
558
+ have an initializer.
559
+ """
560
+ self._empty_init = True
561
+
562
+ def evaluate(self, mutable_step: MutableStep) -> None:
563
+ raise NotImplementedError()