ob-metaflow 2.15.13.1__py2.py3-none-any.whl → 2.19.7.1rc0__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 (169) hide show
  1. metaflow/__init__.py +10 -3
  2. metaflow/_vendor/imghdr/__init__.py +186 -0
  3. metaflow/_vendor/yaml/__init__.py +427 -0
  4. metaflow/_vendor/yaml/composer.py +139 -0
  5. metaflow/_vendor/yaml/constructor.py +748 -0
  6. metaflow/_vendor/yaml/cyaml.py +101 -0
  7. metaflow/_vendor/yaml/dumper.py +62 -0
  8. metaflow/_vendor/yaml/emitter.py +1137 -0
  9. metaflow/_vendor/yaml/error.py +75 -0
  10. metaflow/_vendor/yaml/events.py +86 -0
  11. metaflow/_vendor/yaml/loader.py +63 -0
  12. metaflow/_vendor/yaml/nodes.py +49 -0
  13. metaflow/_vendor/yaml/parser.py +589 -0
  14. metaflow/_vendor/yaml/reader.py +185 -0
  15. metaflow/_vendor/yaml/representer.py +389 -0
  16. metaflow/_vendor/yaml/resolver.py +227 -0
  17. metaflow/_vendor/yaml/scanner.py +1435 -0
  18. metaflow/_vendor/yaml/serializer.py +111 -0
  19. metaflow/_vendor/yaml/tokens.py +104 -0
  20. metaflow/cards.py +4 -0
  21. metaflow/cli.py +125 -21
  22. metaflow/cli_components/init_cmd.py +1 -0
  23. metaflow/cli_components/run_cmds.py +204 -40
  24. metaflow/cli_components/step_cmd.py +160 -4
  25. metaflow/client/__init__.py +1 -0
  26. metaflow/client/core.py +198 -130
  27. metaflow/client/filecache.py +59 -32
  28. metaflow/cmd/code/__init__.py +2 -1
  29. metaflow/cmd/develop/stub_generator.py +49 -18
  30. metaflow/cmd/develop/stubs.py +9 -27
  31. metaflow/cmd/make_wrapper.py +30 -0
  32. metaflow/datastore/__init__.py +1 -0
  33. metaflow/datastore/content_addressed_store.py +40 -9
  34. metaflow/datastore/datastore_set.py +10 -1
  35. metaflow/datastore/flow_datastore.py +124 -4
  36. metaflow/datastore/spin_datastore.py +91 -0
  37. metaflow/datastore/task_datastore.py +92 -6
  38. metaflow/debug.py +5 -0
  39. metaflow/decorators.py +331 -82
  40. metaflow/extension_support/__init__.py +414 -356
  41. metaflow/extension_support/_empty_file.py +2 -2
  42. metaflow/flowspec.py +322 -82
  43. metaflow/graph.py +178 -15
  44. metaflow/includefile.py +25 -3
  45. metaflow/lint.py +94 -3
  46. metaflow/meta_files.py +13 -0
  47. metaflow/metadata_provider/metadata.py +13 -2
  48. metaflow/metaflow_config.py +66 -4
  49. metaflow/metaflow_environment.py +91 -25
  50. metaflow/metaflow_profile.py +18 -0
  51. metaflow/metaflow_version.py +16 -1
  52. metaflow/package/__init__.py +673 -0
  53. metaflow/packaging_sys/__init__.py +880 -0
  54. metaflow/packaging_sys/backend.py +128 -0
  55. metaflow/packaging_sys/distribution_support.py +153 -0
  56. metaflow/packaging_sys/tar_backend.py +99 -0
  57. metaflow/packaging_sys/utils.py +54 -0
  58. metaflow/packaging_sys/v1.py +527 -0
  59. metaflow/parameters.py +6 -2
  60. metaflow/plugins/__init__.py +6 -0
  61. metaflow/plugins/airflow/airflow.py +11 -1
  62. metaflow/plugins/airflow/airflow_cli.py +16 -5
  63. metaflow/plugins/argo/argo_client.py +42 -20
  64. metaflow/plugins/argo/argo_events.py +6 -6
  65. metaflow/plugins/argo/argo_workflows.py +1023 -344
  66. metaflow/plugins/argo/argo_workflows_cli.py +396 -94
  67. metaflow/plugins/argo/argo_workflows_decorator.py +9 -0
  68. metaflow/plugins/argo/argo_workflows_deployer_objects.py +75 -49
  69. metaflow/plugins/argo/capture_error.py +5 -2
  70. metaflow/plugins/argo/conditional_input_paths.py +35 -0
  71. metaflow/plugins/argo/exit_hooks.py +209 -0
  72. metaflow/plugins/argo/param_val.py +19 -0
  73. metaflow/plugins/aws/aws_client.py +6 -0
  74. metaflow/plugins/aws/aws_utils.py +33 -1
  75. metaflow/plugins/aws/batch/batch.py +72 -5
  76. metaflow/plugins/aws/batch/batch_cli.py +24 -3
  77. metaflow/plugins/aws/batch/batch_decorator.py +57 -6
  78. metaflow/plugins/aws/step_functions/step_functions.py +28 -3
  79. metaflow/plugins/aws/step_functions/step_functions_cli.py +49 -4
  80. metaflow/plugins/aws/step_functions/step_functions_deployer.py +3 -0
  81. metaflow/plugins/aws/step_functions/step_functions_deployer_objects.py +30 -0
  82. metaflow/plugins/cards/card_cli.py +20 -1
  83. metaflow/plugins/cards/card_creator.py +24 -1
  84. metaflow/plugins/cards/card_datastore.py +21 -49
  85. metaflow/plugins/cards/card_decorator.py +58 -6
  86. metaflow/plugins/cards/card_modules/basic.py +38 -9
  87. metaflow/plugins/cards/card_modules/bundle.css +1 -1
  88. metaflow/plugins/cards/card_modules/chevron/renderer.py +1 -1
  89. metaflow/plugins/cards/card_modules/components.py +592 -3
  90. metaflow/plugins/cards/card_modules/convert_to_native_type.py +34 -5
  91. metaflow/plugins/cards/card_modules/json_viewer.py +232 -0
  92. metaflow/plugins/cards/card_modules/main.css +1 -0
  93. metaflow/plugins/cards/card_modules/main.js +56 -41
  94. metaflow/plugins/cards/card_modules/test_cards.py +22 -6
  95. metaflow/plugins/cards/component_serializer.py +1 -8
  96. metaflow/plugins/cards/metadata.py +22 -0
  97. metaflow/plugins/catch_decorator.py +9 -0
  98. metaflow/plugins/datastores/local_storage.py +12 -6
  99. metaflow/plugins/datastores/spin_storage.py +12 -0
  100. metaflow/plugins/datatools/s3/s3.py +49 -17
  101. metaflow/plugins/datatools/s3/s3op.py +113 -66
  102. metaflow/plugins/env_escape/client_modules.py +102 -72
  103. metaflow/plugins/events_decorator.py +127 -121
  104. metaflow/plugins/exit_hook/__init__.py +0 -0
  105. metaflow/plugins/exit_hook/exit_hook_decorator.py +46 -0
  106. metaflow/plugins/exit_hook/exit_hook_script.py +52 -0
  107. metaflow/plugins/kubernetes/kubernetes.py +12 -1
  108. metaflow/plugins/kubernetes/kubernetes_cli.py +11 -0
  109. metaflow/plugins/kubernetes/kubernetes_decorator.py +25 -6
  110. metaflow/plugins/kubernetes/kubernetes_job.py +12 -4
  111. metaflow/plugins/kubernetes/kubernetes_jobsets.py +31 -30
  112. metaflow/plugins/metadata_providers/local.py +76 -82
  113. metaflow/plugins/metadata_providers/service.py +13 -9
  114. metaflow/plugins/metadata_providers/spin.py +16 -0
  115. metaflow/plugins/package_cli.py +36 -24
  116. metaflow/plugins/parallel_decorator.py +11 -2
  117. metaflow/plugins/parsers.py +16 -0
  118. metaflow/plugins/pypi/bootstrap.py +7 -1
  119. metaflow/plugins/pypi/conda_decorator.py +41 -82
  120. metaflow/plugins/pypi/conda_environment.py +14 -6
  121. metaflow/plugins/pypi/micromamba.py +9 -1
  122. metaflow/plugins/pypi/pip.py +41 -5
  123. metaflow/plugins/pypi/pypi_decorator.py +4 -4
  124. metaflow/plugins/pypi/utils.py +22 -0
  125. metaflow/plugins/secrets/__init__.py +3 -0
  126. metaflow/plugins/secrets/secrets_decorator.py +14 -178
  127. metaflow/plugins/secrets/secrets_func.py +49 -0
  128. metaflow/plugins/secrets/secrets_spec.py +101 -0
  129. metaflow/plugins/secrets/utils.py +74 -0
  130. metaflow/plugins/test_unbounded_foreach_decorator.py +2 -2
  131. metaflow/plugins/timeout_decorator.py +0 -1
  132. metaflow/plugins/uv/bootstrap.py +29 -1
  133. metaflow/plugins/uv/uv_environment.py +5 -3
  134. metaflow/pylint_wrapper.py +5 -1
  135. metaflow/runner/click_api.py +79 -26
  136. metaflow/runner/deployer.py +208 -6
  137. metaflow/runner/deployer_impl.py +32 -12
  138. metaflow/runner/metaflow_runner.py +266 -33
  139. metaflow/runner/subprocess_manager.py +21 -1
  140. metaflow/runner/utils.py +27 -16
  141. metaflow/runtime.py +660 -66
  142. metaflow/task.py +255 -26
  143. metaflow/user_configs/config_options.py +33 -21
  144. metaflow/user_configs/config_parameters.py +220 -58
  145. metaflow/user_decorators/__init__.py +0 -0
  146. metaflow/user_decorators/common.py +144 -0
  147. metaflow/user_decorators/mutable_flow.py +512 -0
  148. metaflow/user_decorators/mutable_step.py +424 -0
  149. metaflow/user_decorators/user_flow_decorator.py +264 -0
  150. metaflow/user_decorators/user_step_decorator.py +749 -0
  151. metaflow/util.py +197 -7
  152. metaflow/vendor.py +23 -7
  153. metaflow/version.py +1 -1
  154. {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/Makefile +13 -2
  155. {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/Tiltfile +107 -7
  156. {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/pick_services.sh +1 -0
  157. {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/METADATA +2 -3
  158. {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/RECORD +162 -121
  159. {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/WHEEL +1 -1
  160. metaflow/_vendor/v3_5/__init__.py +0 -1
  161. metaflow/_vendor/v3_5/importlib_metadata/__init__.py +0 -644
  162. metaflow/_vendor/v3_5/importlib_metadata/_compat.py +0 -152
  163. metaflow/_vendor/v3_5/zipp.py +0 -329
  164. metaflow/info_file.py +0 -25
  165. metaflow/package.py +0 -203
  166. metaflow/user_configs/config_decorators.py +0 -568
  167. {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/entry_points.txt +0 -0
  168. {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/licenses/LICENSE +0 -0
  169. {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/top_level.txt +0 -0
@@ -1,2 +1,2 @@
1
- # This file serves as a __init__.py for metaflow_extensions when it is packaged
2
- # and needs to remain empty.
1
+ # This file serves as a __init__.py for metaflow_extensions or metaflow
2
+ # packages when they are packaged and needs to remain empty.
metaflow/flowspec.py CHANGED
@@ -4,10 +4,11 @@ import sys
4
4
  import traceback
5
5
  import reprlib
6
6
 
7
+ from collections.abc import MutableMapping
7
8
  from enum import Enum
8
9
  from itertools import islice
9
10
  from types import FunctionType, MethodType
10
- from typing import TYPE_CHECKING, Any, Callable, Generator, List, Optional, Tuple
11
+ from typing import Any, Callable, List, Optional, Tuple
11
12
 
12
13
  from . import cmd_with_io, parameters
13
14
  from .debug import debug
@@ -23,13 +24,14 @@ from .extension_support import extension_info
23
24
 
24
25
  from .graph import FlowGraph
25
26
  from .unbounded_foreach import UnboundedForeachInput
26
- from .user_configs.config_decorators import (
27
- ConfigValue,
28
- CustomFlowDecorator,
29
- CustomStepDecorator,
30
- MutableFlow,
31
- MutableStep,
32
- )
27
+ from .user_configs.config_parameters import ConfigValue
28
+
29
+ from .user_decorators.mutable_flow import MutableFlow
30
+ from .user_decorators.mutable_step import MutableStep
31
+ from .user_decorators.user_flow_decorator import FlowMutator
32
+ from .user_decorators.user_step_decorator import StepMutator
33
+
34
+
33
35
  from .util import to_pod
34
36
  from .metaflow_config import INCLUDE_FOREACH_STACK, MAXIMUM_FOREACH_VALUE_CHARS
35
37
 
@@ -75,10 +77,84 @@ class ParallelUBF(UnboundedForeachInput):
75
77
  return item or 0 # item is None for the control task, but it is also split 0
76
78
 
77
79
 
78
- class _FlowState(Enum):
80
+ class FlowStateItems(Enum):
79
81
  CONFIGS = 1
80
- CONFIG_DECORATORS = 2
82
+ FLOW_MUTATORS = 2
81
83
  CACHED_PARAMETERS = 3
84
+ SET_CONFIG_PARAMETERS = 4 # Parameters that now have a ConfigValue (converted)
85
+ FLOW_DECORATORS = 5
86
+
87
+
88
+ class _FlowState(MutableMapping):
89
+ # Dict like structure to hold state information about the flow but it holds
90
+ # the key/values in two sub dictionaries: the ones that are specific to the flow
91
+ # and the ones that are inherited from parent classes.
92
+ # This is NOT a general purpose class and is meant to only work with FlowSpec.
93
+ # For example, it assumes that items are only list, dicts or None and assumes that
94
+ # self._self_data has all keys properly initialized.
95
+
96
+ def __init__(self, *args, **kwargs):
97
+ self._self_data = dict(*args, **kwargs)
98
+ self._merged_data = {}
99
+ self._inherited = {}
100
+
101
+ def __getitem__(self, key):
102
+ # ORDER IS IMPORTANT: we use inherited first and extend by whatever is in
103
+ # the flowspec
104
+ if key in self._merged_data:
105
+ return self._merged_data[key]
106
+
107
+ # We haven't accessed this yet so compute it for the first time
108
+ self_value = self._self_data.get(key)
109
+ inherited_value = self._inherited.get(key)
110
+
111
+ if self_value is not None:
112
+ self._merged_data[key] = self._merge_value(inherited_value, self_value)
113
+ return self._merged_data[key]
114
+ elif key in self._self_data:
115
+ # Case of CACHED_PARAMETERS; a valid value is None. It is never inherited
116
+ self._merged_data[key] = None
117
+ return None
118
+ raise KeyError(key)
119
+
120
+ def __setitem__(self, key, value):
121
+ self._self_data[key] = value
122
+
123
+ def __delitem__(self, key):
124
+ if key in self._merged_data:
125
+ del self._merged_data[key]
126
+ else:
127
+ raise KeyError(key)
128
+
129
+ def __iter__(self):
130
+ # All keys are in self._self_data
131
+ for key in self._self_data:
132
+ yield self[key]
133
+
134
+ def __len__(self):
135
+ return len(self._self_data)
136
+
137
+ @property
138
+ def self_data(self):
139
+ self._merged_data.clear()
140
+ return self._self_data
141
+
142
+ @property
143
+ def inherited_data(self):
144
+ return self._inherited
145
+
146
+ def _merge_value(self, inherited_value, self_value):
147
+ if self_value is None:
148
+ return None
149
+ inherited_value = inherited_value or type(self_value)()
150
+ if isinstance(self_value, dict):
151
+ return {**inherited_value, **self_value}
152
+ elif isinstance(self_value, list):
153
+ return inherited_value + self_value
154
+ raise RuntimeError(
155
+ f"Cannot merge values of type {type(inherited_value)} and {type(self_value)} -- "
156
+ "please report this as a bug"
157
+ )
82
158
 
83
159
 
84
160
  class FlowSpecMeta(type):
@@ -87,6 +163,9 @@ class FlowSpecMeta(type):
87
163
  if name == "FlowSpec":
88
164
  return
89
165
 
166
+ cls._init_attrs()
167
+
168
+ def _init_attrs(cls):
90
169
  from .decorators import (
91
170
  DuplicateFlowDecoratorException,
92
171
  ) # Prevent circular import
@@ -96,16 +175,26 @@ class FlowSpecMeta(type):
96
175
  # Runner/NBRunner. This is also created here in the meta class to avoid it being
97
176
  # shared between different children classes.
98
177
 
99
- # We should move _flow_decorators into this structure as well but keeping it
100
- # out to limit the changes for now.
101
- cls._flow_decorators = {}
178
+ # Keys are FlowStateItems enum values
179
+ cls._flow_state = _FlowState(
180
+ {
181
+ FlowStateItems.CONFIGS: {},
182
+ FlowStateItems.FLOW_MUTATORS: [],
183
+ FlowStateItems.CACHED_PARAMETERS: None,
184
+ FlowStateItems.SET_CONFIG_PARAMETERS: [],
185
+ FlowStateItems.FLOW_DECORATORS: {},
186
+ }
187
+ )
102
188
 
103
- # Keys are _FlowState enum values
104
- cls._flow_state = {}
189
+ # Keep track if configs have been processed -- this is particularly applicable
190
+ # for the Runner/Deployer where calling multiple APIs on the same flow could
191
+ # cause the configs to be processed multiple times. For a given flow, once
192
+ # the configs have been processed, we do not process them again.
193
+ cls._configs_processed = False
105
194
 
106
195
  # We inherit stuff from our parent classes as well -- we need to be careful
107
196
  # in terms of the order; we will follow the MRO with the following rules:
108
- # - decorators (cls._flow_decorators) will cause an error if they do not
197
+ # - decorators will cause an error if they do not
109
198
  # support multiple and we see multiple instances of the same
110
199
  # - config decorators will be joined
111
200
  # - configs will be added later directly by the class; base class configs will
@@ -113,24 +202,55 @@ class FlowSpecMeta(type):
113
202
 
114
203
  # We only need to do this for the base classes since the current class will
115
204
  # get updated as decorators are parsed.
205
+
206
+ # We also need to be sure to not duplicate things. Consider something like
207
+ # class A(FlowSpec):
208
+ # pass
209
+ #
210
+ # class B(A):
211
+ # pass
212
+ #
213
+ # class C(B):
214
+ # pass
215
+ #
216
+ # C inherits from both B and A but we need to duplicate things from A only
217
+ # ONCE. To do this, we only propagate the self data from each class.
218
+
116
219
  for base in cls.__mro__:
117
220
  if base != cls and base != FlowSpec and issubclass(base, FlowSpec):
118
221
  # Take care of decorators
119
- for deco_name, deco in base._flow_decorators.items():
120
- if deco_name in cls._flow_decorators and not deco.allow_multiple:
121
- raise DuplicateFlowDecoratorException(deco_name)
122
- cls._flow_decorators.setdefault(deco_name, []).extend(deco)
222
+ base_flow_decorators = base._flow_state.self_data[
223
+ FlowStateItems.FLOW_DECORATORS
224
+ ]
123
225
 
124
- # Take care of configs and config decorators
125
- base_configs = base._flow_state.get(_FlowState.CONFIG_DECORATORS)
126
- if base_configs:
127
- cls._flow_state.setdefault(_FlowState.CONFIG_DECORATORS, []).extend(
128
- base_configs
226
+ inherited_cls_flow_decorators = (
227
+ cls._flow_state.inherited_data.setdefault(
228
+ FlowStateItems.FLOW_DECORATORS, {}
129
229
  )
230
+ )
231
+ for deco_name, deco in base_flow_decorators.items():
232
+ if not deco:
233
+ continue
234
+ deco_allow_multiple = deco[0].allow_multiple
235
+ if (
236
+ deco_name in inherited_cls_flow_decorators
237
+ and not deco_allow_multiple
238
+ ):
239
+ raise DuplicateFlowDecoratorException(deco_name)
240
+ inherited_cls_flow_decorators.setdefault(deco_name, []).extend(deco)
130
241
 
131
- cls._init_attrs()
242
+ # Take care of flow mutators -- configs are just objects in the class
243
+ # so they are naturally inherited. We do not need to do anything special
244
+ # for them.
245
+ base_mutators = base._flow_state.self_data[FlowStateItems.FLOW_MUTATORS]
246
+ if base_mutators:
247
+ cls._flow_state.inherited_data.setdefault(
248
+ FlowStateItems.FLOW_MUTATORS, []
249
+ ).extend(base_mutators)
132
250
 
133
- def _init_attrs(cls):
251
+ cls._init_graph()
252
+
253
+ def _init_graph(cls):
134
254
  # Graph and steps are specific to the class -- store here so we can access
135
255
  # in class method _process_config_decorators
136
256
  cls._graph = FlowGraph(cls)
@@ -156,7 +276,6 @@ class FlowSpec(metaclass=FlowSpecMeta):
156
276
  "_datastore",
157
277
  "_cached_input",
158
278
  "_graph",
159
- "_flow_decorators",
160
279
  "_flow_state",
161
280
  "_steps",
162
281
  "index",
@@ -207,6 +326,11 @@ class FlowSpec(metaclass=FlowSpecMeta):
207
326
  fname = fname[:-1]
208
327
  return os.path.basename(fname)
209
328
 
329
+ @property
330
+ def _flow_decorators(self):
331
+ # Backward compatible method to access flow decorators
332
+ return self._flow_state[FlowStateItems.FLOW_DECORATORS]
333
+
210
334
  @classmethod
211
335
  def _check_parameters(cls, config_parameters=False):
212
336
  seen = set()
@@ -224,26 +348,32 @@ class FlowSpec(metaclass=FlowSpecMeta):
224
348
 
225
349
  @classmethod
226
350
  def _process_config_decorators(cls, config_options, process_configs=True):
351
+ if cls._configs_processed:
352
+ debug.userconf_exec("Mutating step/flow decorators already processed")
353
+ return None
354
+ cls._configs_processed = True
227
355
 
228
356
  # Fast path for no user configurations
229
357
  if not process_configs or (
230
- not cls._flow_state.get(_FlowState.CONFIG_DECORATORS)
358
+ not cls._flow_state[FlowStateItems.FLOW_MUTATORS]
231
359
  and all(len(step.config_decorators) == 0 for step in cls._steps)
232
360
  ):
233
361
  # Process parameters to allow them to also use config values easily
234
362
  for var, param in cls._get_parameters():
235
- if param.IS_CONFIG_PARAMETER:
363
+ if isinstance(param, ConfigValue) or param.IS_CONFIG_PARAMETER:
236
364
  continue
237
365
  param.init(not process_configs)
238
366
  return None
239
367
 
240
368
  debug.userconf_exec("Processing mutating step/flow decorators")
241
369
  # We need to convert all the user configurations from DelayedEvaluationParameters
242
- # to actual values so they can be used as is in the config decorators.
370
+ # to actual values so they can be used as is in the mutators.
243
371
 
244
- # We then reset them to be proper configs so they can be re-evaluated in
245
- # _set_constants
246
- to_reset_configs = []
372
+ # We, however, need to make sure _get_parameters still works properly so
373
+ # we store what was a config and has been set to a specific value.
374
+ # This is safe to do for now because all other uses of _get_parameters typically
375
+ # do not rely on the variable itself but just the parameter.
376
+ to_save_configs = []
247
377
  cls._check_parameters(config_parameters=True)
248
378
  for var, param in cls._get_parameters():
249
379
  if not param.IS_CONFIG_PARAMETER:
@@ -255,77 +385,79 @@ class FlowSpec(metaclass=FlowSpecMeta):
255
385
  # We store the value as well so that in _set_constants, we don't try
256
386
  # to recompute (no guarantee that it is stable)
257
387
  param._store_value(val)
258
- to_reset_configs.append((var, param))
388
+ to_save_configs.append((var, param))
259
389
  debug.userconf_exec("Setting config %s to %s" % (var, str(val)))
260
390
  setattr(cls, var, val)
261
391
 
262
- # Reset cached parameters since we have replaced configs already with ConfigValue
263
- # so they are not parameters anymore to be re-evaluated when we do _get_parameters
264
- if _FlowState.CACHED_PARAMETERS in cls._flow_state:
265
- del cls._flow_state[_FlowState.CACHED_PARAMETERS]
266
-
267
- # Run all the decorators. Step decorators are directly in the step and
268
- # we will run those first and *then* we run all the flow level decorators
269
- for step in cls._steps:
270
- for deco in step.config_decorators:
271
- if isinstance(deco, CustomStepDecorator):
272
- debug.userconf_exec(
273
- "Evaluating step level decorator %s for %s"
274
- % (deco.__class__.__name__, step.name)
275
- )
276
- deco.evaluate(MutableStep(cls, step))
277
- else:
278
- raise MetaflowInternalError(
279
- "A non CustomFlowDecorator found in step custom decorators"
280
- )
281
- if step.config_decorators:
282
- # We remove all mention of the custom step decorator
283
- setattr(cls, step.name, step)
284
-
285
- mutable_flow = MutableFlow(cls)
286
- for deco in cls._flow_state.get(_FlowState.CONFIG_DECORATORS, []):
287
- if isinstance(deco, CustomFlowDecorator):
392
+ cls._flow_state[FlowStateItems.SET_CONFIG_PARAMETERS] = to_save_configs
393
+ # Run all the decorators. We first run the flow-level decorators
394
+ # and then the step level ones to maintain a consistent order with how
395
+ # other decorators are run.
396
+
397
+ for deco in cls._flow_state[FlowStateItems.FLOW_MUTATORS]:
398
+ if isinstance(deco, FlowMutator):
399
+ inserted_by_value = [deco.decorator_name] + (deco.inserted_by or [])
400
+ mutable_flow = MutableFlow(
401
+ cls,
402
+ pre_mutate=True,
403
+ statically_defined=deco.statically_defined,
404
+ inserted_by=inserted_by_value,
405
+ )
288
406
  # Sanity check to make sure we are applying the decorator to the right
289
407
  # class
290
408
  if not deco._flow_cls == cls and not issubclass(cls, deco._flow_cls):
291
409
  raise MetaflowInternalError(
292
- "CustomFlowDecorator registered on the wrong flow -- "
410
+ "FlowMutator registered on the wrong flow -- "
293
411
  "expected %s but got %s"
294
412
  % (deco._flow_cls.__name__, cls.__name__)
295
413
  )
296
414
  debug.userconf_exec(
297
- "Evaluating flow level decorator %s" % deco.__class__.__name__
415
+ "Evaluating flow level decorator %s (pre-mutate)"
416
+ % deco.__class__.__name__
298
417
  )
299
- deco.evaluate(mutable_flow)
418
+ deco.pre_mutate(mutable_flow)
300
419
  # We reset cached_parameters on the very off chance that the user added
301
420
  # more configurations based on the configuration
302
- if _FlowState.CACHED_PARAMETERS in cls._flow_state:
303
- del cls._flow_state[_FlowState.CACHED_PARAMETERS]
421
+ if cls._flow_state[FlowStateItems.CACHED_PARAMETERS] is not None:
422
+ cls._flow_state[FlowStateItems.CACHED_PARAMETERS] = None
304
423
  else:
305
424
  raise MetaflowInternalError(
306
- "A non CustomFlowDecorator found in flow custom decorators"
425
+ "A non FlowMutator found in flow custom decorators"
307
426
  )
308
427
 
428
+ for step in cls._steps:
429
+ for deco in step.config_decorators:
430
+ if isinstance(deco, StepMutator):
431
+ inserted_by_value = [deco.decorator_name] + (deco.inserted_by or [])
432
+ debug.userconf_exec(
433
+ "Evaluating step level decorator %s for %s (pre-mutate)"
434
+ % (deco.__class__.__name__, step.name)
435
+ )
436
+ deco.pre_mutate(
437
+ MutableStep(
438
+ cls,
439
+ step,
440
+ pre_mutate=True,
441
+ statically_defined=deco.statically_defined,
442
+ inserted_by=inserted_by_value,
443
+ )
444
+ )
445
+ else:
446
+ raise MetaflowInternalError(
447
+ "A non StepMutator found in step custom decorators"
448
+ )
449
+
309
450
  # Process parameters to allow them to also use config values easily
310
451
  for var, param in cls._get_parameters():
311
452
  if param.IS_CONFIG_PARAMETER:
312
453
  continue
313
454
  param.init()
314
- # Reset all configs that were already present in the class.
315
- # TODO: This means that users can't override configs directly. Not sure if this
316
- # is a pattern we want to support
317
- for var, param in to_reset_configs:
318
- setattr(cls, var, param)
319
-
320
- # Reset cached parameters again since we added back the config parameters
321
- if _FlowState.CACHED_PARAMETERS in cls._flow_state:
322
- del cls._flow_state[_FlowState.CACHED_PARAMETERS]
323
455
 
324
456
  # Set the current flow class we are in (the one we just created)
325
457
  parameters.replace_flow_context(cls)
326
458
 
327
459
  # Re-calculate class level attributes after modifying the class
328
- cls._init_attrs()
460
+ cls._init_graph()
329
461
  return cls
330
462
 
331
463
  def _set_constants(self, graph, kwargs, config_options):
@@ -390,9 +522,19 @@ class FlowSpec(metaclass=FlowSpecMeta):
390
522
  "name": deco.name,
391
523
  "attributes": to_pod(deco.attributes),
392
524
  "statically_defined": deco.statically_defined,
525
+ "inserted_by": deco.inserted_by,
393
526
  }
394
527
  for deco in flow_decorators(self)
395
528
  if not deco.name.startswith("_")
529
+ ]
530
+ + [
531
+ {
532
+ "name": deco.__class__.__name__,
533
+ "attributes": {},
534
+ "statically_defined": deco.statically_defined,
535
+ "inserted_by": deco.inserted_by,
536
+ }
537
+ for deco in self._flow_state[FlowStateItems.FLOW_MUTATORS]
396
538
  ],
397
539
  "extensions": extension_info(),
398
540
  }
@@ -400,12 +542,20 @@ class FlowSpec(metaclass=FlowSpecMeta):
400
542
 
401
543
  @classmethod
402
544
  def _get_parameters(cls):
403
- cached = cls._flow_state.get(_FlowState.CACHED_PARAMETERS)
545
+ cached = cls._flow_state[FlowStateItems.CACHED_PARAMETERS]
546
+ returned = set()
404
547
  if cached is not None:
548
+ for set_config in cls._flow_state[FlowStateItems.SET_CONFIG_PARAMETERS]:
549
+ returned.add(set_config[0])
550
+ yield set_config[0], set_config[1]
405
551
  for var in cached:
406
- yield var, getattr(cls, var)
552
+ if var not in returned:
553
+ yield var, getattr(cls, var)
407
554
  return
408
555
  build_list = []
556
+ for set_config in cls._flow_state[FlowStateItems.SET_CONFIG_PARAMETERS]:
557
+ returned.add(set_config[0])
558
+ yield set_config[0], set_config[1]
409
559
  for var in dir(cls):
410
560
  if var[0] == "_" or var in cls._NON_PARAMETERS:
411
561
  continue
@@ -413,10 +563,10 @@ class FlowSpec(metaclass=FlowSpecMeta):
413
563
  val = getattr(cls, var)
414
564
  except:
415
565
  continue
416
- if isinstance(val, Parameter):
566
+ if isinstance(val, Parameter) and var not in returned:
417
567
  build_list.append(var)
418
568
  yield var, val
419
- cls._flow_state[_FlowState.CACHED_PARAMETERS] = build_list
569
+ cls._flow_state[FlowStateItems.CACHED_PARAMETERS] = build_list
420
570
 
421
571
  def _set_datastore(self, datastore):
422
572
  self._datastore = datastore
@@ -768,6 +918,15 @@ class FlowSpec(metaclass=FlowSpecMeta):
768
918
  evaluates to an iterator. A task will be launched for each value in the iterator and
769
919
  each task will execute the code specified by the step `foreach_step`.
770
920
 
921
+ - Switch statement:
922
+ ```
923
+ self.next({"case1": self.step_a, "case2": self.step_b}, condition='condition_variable')
924
+ ```
925
+ In this situation, `step_a` and `step_b` are methods in the current class decorated
926
+ with the `@step` decorator and `condition_variable` is a variable name in the current
927
+ class. The value of the condition variable determines which step to execute. If the
928
+ value doesn't match any of the dictionary keys, a RuntimeError is raised.
929
+
771
930
  Parameters
772
931
  ----------
773
932
  dsts : Callable[..., None]
@@ -783,6 +942,7 @@ class FlowSpec(metaclass=FlowSpecMeta):
783
942
 
784
943
  foreach = kwargs.pop("foreach", None)
785
944
  num_parallel = kwargs.pop("num_parallel", None)
945
+ condition = kwargs.pop("condition", None)
786
946
  if kwargs:
787
947
  kw = next(iter(kwargs))
788
948
  msg = (
@@ -799,6 +959,86 @@ class FlowSpec(metaclass=FlowSpecMeta):
799
959
  )
800
960
  raise InvalidNextException(msg)
801
961
 
962
+ # check: switch case using condition
963
+ if condition is not None:
964
+ if len(dsts) != 1 or not isinstance(dsts[0], dict) or not dsts[0]:
965
+ msg = (
966
+ "Step *{step}* has an invalid self.next() transition. "
967
+ "When using 'condition', the transition must be to a single, "
968
+ "non-empty dictionary mapping condition values to step methods.".format(
969
+ step=step
970
+ )
971
+ )
972
+ raise InvalidNextException(msg)
973
+
974
+ if not isinstance(condition, basestring):
975
+ msg = (
976
+ "Step *{step}* has an invalid self.next() transition. "
977
+ "The argument to 'condition' must be a string.".format(step=step)
978
+ )
979
+ raise InvalidNextException(msg)
980
+
981
+ if foreach is not None or num_parallel is not None:
982
+ msg = (
983
+ "Step *{step}* has an invalid self.next() transition. "
984
+ "Switch statements cannot be combined with foreach or num_parallel.".format(
985
+ step=step
986
+ )
987
+ )
988
+ raise InvalidNextException(msg)
989
+
990
+ switch_cases = dsts[0]
991
+
992
+ # Validate that condition variable exists
993
+ try:
994
+ condition_value = getattr(self, condition)
995
+ except AttributeError:
996
+ msg = (
997
+ "Condition variable *self.{var}* in step *{step}* "
998
+ "does not exist. Make sure you set self.{var} in this step.".format(
999
+ step=step, var=condition
1000
+ )
1001
+ )
1002
+ raise InvalidNextException(msg)
1003
+
1004
+ if condition_value not in switch_cases:
1005
+ available_cases = list(switch_cases.keys())
1006
+ raise RuntimeError(
1007
+ f"Switch condition variable '{condition}' has value '{condition_value}' "
1008
+ f"which is not in the available cases: {available_cases}"
1009
+ )
1010
+
1011
+ # Get the chosen step and set transition directly
1012
+ chosen_step_func = switch_cases[condition_value]
1013
+
1014
+ # Validate that the chosen step exists
1015
+ try:
1016
+ name = chosen_step_func.__func__.__name__
1017
+ except:
1018
+ msg = (
1019
+ "Step *{step}* specifies a switch transition that is not a function. "
1020
+ "Make sure the value in the dictionary is a method "
1021
+ "of the Flow class.".format(step=step)
1022
+ )
1023
+ raise InvalidNextException(msg)
1024
+ if not hasattr(self, name):
1025
+ msg = (
1026
+ "Step *{step}* specifies a switch transition to an "
1027
+ "unknown step, *{name}*.".format(step=step, name=name)
1028
+ )
1029
+ raise InvalidNextException(msg)
1030
+
1031
+ self._transition = ([name], None)
1032
+ return
1033
+
1034
+ # Check for an invalid transition: a dictionary used without a 'condition' parameter.
1035
+ if len(dsts) == 1 and isinstance(dsts[0], dict):
1036
+ msg = (
1037
+ "Step *{step}* has an invalid self.next() transition. "
1038
+ "Dictionary argument requires 'condition' parameter.".format(step=step)
1039
+ )
1040
+ raise InvalidNextException(msg)
1041
+
802
1042
  # check: all destinations are methods of this object
803
1043
  funcs = []
804
1044
  for i, dst in enumerate(dsts):
@@ -889,7 +1129,7 @@ class FlowSpec(metaclass=FlowSpecMeta):
889
1129
  self._foreach_var = foreach
890
1130
 
891
1131
  # check: non-keyword transitions are valid
892
- if foreach is None:
1132
+ if foreach is None and condition is None:
893
1133
  if len(dsts) < 1:
894
1134
  msg = (
895
1135
  "Step *{step}* has an invalid self.next() transition. "