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,5 +1,6 @@
1
1
  import atexit
2
2
  import importlib
3
+ import importlib.util
3
4
  import itertools
4
5
  import pickle
5
6
  import re
@@ -41,6 +42,8 @@ class _WrappedModule(object):
41
42
  def __getattr__(self, name):
42
43
  if name == "__loader__":
43
44
  return self._loader
45
+ if name == "__spec__":
46
+ return importlib.util.spec_from_loader(self._prefix, self._loader)
44
47
  if name in ("__name__", "__package__"):
45
48
  return self._prefix
46
49
  if name in ("__file__", "__path__"):
@@ -71,7 +74,8 @@ class _WrappedModule(object):
71
74
  # Try to see if this is a submodule that we can load
72
75
  m = None
73
76
  try:
74
- m = self._loader.load_module(".".join([self._prefix, name]))
77
+ submodule_name = ".".join([self._prefix, name])
78
+ m = importlib.import_module(submodule_name)
75
79
  except ImportError:
76
80
  pass
77
81
  if m is None:
@@ -117,7 +121,28 @@ class _WrappedModule(object):
117
121
 
118
122
 
119
123
  class ModuleImporter(object):
120
- # This ModuleImporter implements the Importer Protocol defined in PEP 302
124
+ """
125
+ A custom import hook that proxies module imports to a different Python environment.
126
+
127
+ This class implements the MetaPathFinder and Loader protocols (PEP 451) to enable
128
+ "environment escape" - allowing the current Python process to import and use modules
129
+ from a different Python interpreter with potentially different versions or packages.
130
+
131
+ When a module is imported through this importer:
132
+ 1. A client spawns a server process in the target Python environment
133
+ 2. The module is loaded in the remote environment
134
+ 3. A _WrappedModule proxy is returned that forwards all operations (function calls,
135
+ attribute access, etc.) to the remote environment via RPC
136
+ 4. Data is serialized/deserialized using pickle for cross-environment communication
137
+
138
+ Args:
139
+ python_executable: Path to the Python interpreter for the remote environment
140
+ pythonpath: Python path to use in the remote environment
141
+ max_pickle_version: Maximum pickle protocol version supported by remote interpreter
142
+ config_dir: Directory containing configuration for the environment escape
143
+ module_prefixes: List of module name prefixes to handle
144
+ """
145
+
121
146
  def __init__(
122
147
  self,
123
148
  python_executable,
@@ -135,84 +160,89 @@ class ModuleImporter(object):
135
160
  self._handled_modules = None
136
161
  self._aliases = {}
137
162
 
138
- def find_module(self, fullname, path=None):
163
+ def find_spec(self, fullname, path=None, target=None):
139
164
  if self._handled_modules is not None:
140
165
  if get_canonical_name(fullname, self._aliases) in self._handled_modules:
141
- return self
166
+ return importlib.util.spec_from_loader(fullname, self)
142
167
  return None
143
168
  if any([fullname.startswith(prefix) for prefix in self._module_prefixes]):
144
169
  # We potentially handle this
145
- return self
170
+ return importlib.util.spec_from_loader(fullname, self)
146
171
  return None
147
172
 
148
- def load_module(self, fullname):
149
- if fullname in sys.modules:
150
- return sys.modules[fullname]
151
- if self._client is None:
152
- if sys.version_info[0] < 3:
153
- raise NotImplementedError(
154
- "Environment escape imports are not supported in Python 2"
155
- )
156
- # We initialize a client and query the modules we handle
157
- # The max_pickle_version is the pickle version that the server (so
158
- # the underlying interpreter we call into) supports; we determine
159
- # what version the current environment support and take the minimum
160
- # of those two
161
- max_pickle_version = min(self._max_pickle_version, pickle.HIGHEST_PROTOCOL)
162
-
163
- self._client = Client(
164
- self._module_prefixes,
165
- self._python_executable,
166
- self._pythonpath,
167
- max_pickle_version,
168
- self._config_dir,
169
- )
170
- atexit.register(_clean_client, self._client)
171
-
172
- # Get information about overrides and what the server knows about
173
- exports = self._client.get_exports()
174
-
175
- prefixes = set()
176
- export_classes = exports.get("classes", [])
177
- export_functions = exports.get("functions", [])
178
- export_values = exports.get("values", [])
179
- export_exceptions = exports.get("exceptions", [])
180
- self._aliases = exports.get("aliases", {})
181
- for name in itertools.chain(
182
- export_classes,
183
- export_functions,
184
- export_values,
185
- (e[0] for e in export_exceptions),
186
- ):
187
- splits = name.rsplit(".", 1)
188
- prefixes.add(splits[0])
189
- # We will make sure that we create modules even for "empty" prefixes
190
- # because packages are always loaded hierarchically so if we have
191
- # something in `a.b.c` but nothing directly in `a`, we still need to
192
- # create a module named `a`. There is probably a better way of doing this
193
- all_prefixes = list(prefixes)
194
- for prefix in all_prefixes:
195
- parts = prefix.split(".")
196
- cur = parts[0]
197
- for i in range(1, len(parts)):
198
- prefixes.add(cur)
199
- cur = ".".join([cur, parts[i]])
200
-
201
- # We now know all the modules that we can handle. We update
202
- # handled_module and return the module if we have it or raise ImportError
203
- self._handled_modules = {}
204
- for prefix in prefixes:
205
- self._handled_modules[prefix] = _WrappedModule(
206
- self, prefix, exports, self._client
207
- )
173
+ def create_module(self, spec):
174
+ # Return the pre-created wrapped module for this spec
175
+ self._initialize_client()
176
+
177
+ fullname = spec.name
208
178
  canonical_fullname = get_canonical_name(fullname, self._aliases)
209
- # Modules are created canonically but we need to return something for any
210
- # of the aliases.
211
- module = self._handled_modules.get(canonical_fullname)
212
- if module is None:
213
- raise ImportError
214
- sys.modules[fullname] = module
215
- return module
179
+ # Modules are created canonically but we need to handle any of the aliases.
180
+ wrapped_module = self._handled_modules.get(canonical_fullname)
181
+ if wrapped_module is None:
182
+ raise ImportError(f"No module named '{fullname}'")
183
+ return wrapped_module
184
+
185
+ def exec_module(self, module):
186
+ # No initialization needed since the wrapped module returned by
187
+ # create_module() is fully initialized
188
+ pass
189
+
190
+ def _initialize_client(self):
191
+ if self._client is not None:
192
+ return
193
+
194
+ # We initialize a client and query the modules we handle
195
+ # The max_pickle_version is the pickle version that the server (so
196
+ # the underlying interpreter we call into) supports; we determine
197
+ # what version the current environment support and take the minimum
198
+ # of those two
199
+ max_pickle_version = min(self._max_pickle_version, pickle.HIGHEST_PROTOCOL)
200
+
201
+ self._client = Client(
202
+ self._module_prefixes,
203
+ self._python_executable,
204
+ self._pythonpath,
205
+ max_pickle_version,
206
+ self._config_dir,
207
+ )
208
+ atexit.register(_clean_client, self._client)
209
+
210
+ # Get information about overrides and what the server knows about
211
+ exports = self._client.get_exports()
212
+
213
+ prefixes = set()
214
+ export_classes = exports.get("classes", [])
215
+ export_functions = exports.get("functions", [])
216
+ export_values = exports.get("values", [])
217
+ export_exceptions = exports.get("exceptions", [])
218
+ self._aliases = exports.get("aliases", {})
219
+ for name in itertools.chain(
220
+ export_classes,
221
+ export_functions,
222
+ export_values,
223
+ (e[0] for e in export_exceptions),
224
+ ):
225
+ splits = name.rsplit(".", 1)
226
+ prefixes.add(splits[0])
227
+ # We will make sure that we create modules even for "empty" prefixes
228
+ # because packages are always loaded hierarchically so if we have
229
+ # something in `a.b.c` but nothing directly in `a`, we still need to
230
+ # create a module named `a`. There is probably a better way of doing this
231
+ all_prefixes = list(prefixes)
232
+ for prefix in all_prefixes:
233
+ parts = prefix.split(".")
234
+ cur = parts[0]
235
+ for i in range(1, len(parts)):
236
+ prefixes.add(cur)
237
+ cur = ".".join([cur, parts[i]])
238
+
239
+ # We now know all the modules that we can handle. We update
240
+ # handled_module and return the module if we have it or raise ImportError
241
+ self._handled_modules = {}
242
+ for prefix in prefixes:
243
+ self._handled_modules[prefix] = _WrappedModule(
244
+ self, prefix, exports, self._client
245
+ )
216
246
 
217
247
 
218
248
  def create_modules(python_executable, pythonpath, max_pickle_version, path, prefixes):
@@ -70,7 +70,26 @@ class TriggerDecorator(FlowDecorator):
70
70
  "options": {},
71
71
  }
72
72
 
73
- def process_event_name(self, event):
73
+ def process_event(self, event):
74
+ """
75
+ Process a single event and return a dictionary if static trigger and a function
76
+ if deploy-time trigger.
77
+
78
+ Parameters
79
+ ----------
80
+ event : Union[str, Dict[str, Any], Callable]
81
+ Event to process
82
+
83
+ Returns
84
+ -------
85
+ Union[Dict[str, Union[str, Callable]], Callable]
86
+ Processed event
87
+
88
+ Raises
89
+ ------
90
+ MetaflowException
91
+ If the event is not in the correct format
92
+ """
74
93
  if is_stringish(event):
75
94
  return {"name": str(event)}
76
95
  elif isinstance(event, dict):
@@ -82,12 +101,26 @@ class TriggerDecorator(FlowDecorator):
82
101
  event["name"], DeployTimeField
83
102
  ):
84
103
  event["name"] = DeployTimeField(
85
- "event_name", str, None, event["name"], False
104
+ "event_name",
105
+ str,
106
+ None,
107
+ event["name"],
108
+ False,
109
+ print_representation=str(event["name"]),
86
110
  )
87
- event["parameters"] = self.process_parameters(event.get("parameters", {}))
111
+ event["parameters"] = self.process_parameters(
112
+ event.get("parameters", {}), event["name"]
113
+ )
88
114
  return event
89
115
  elif callable(event) and not isinstance(event, DeployTimeField):
90
- return DeployTimeField("event", [str, dict], None, event, False)
116
+ return DeployTimeField(
117
+ "event",
118
+ [str, dict],
119
+ None,
120
+ event,
121
+ False,
122
+ print_representation=str(event),
123
+ )
91
124
  else:
92
125
  raise MetaflowException(
93
126
  "Incorrect format for *event* attribute in *@trigger* decorator. "
@@ -96,47 +129,67 @@ class TriggerDecorator(FlowDecorator):
96
129
  "'parameters': {'alpha': 'beta'}})"
97
130
  )
98
131
 
99
- def process_parameters(self, parameters):
132
+ def process_parameters(self, parameters, event_name):
133
+ """
134
+ Process the parameters for an event and return a dictionary of parameter mappings if
135
+ parameters was statically defined or a function if deploy-time trigger.
136
+
137
+ Parameters
138
+ ----------
139
+ Parameters : Union[Dict[str, str], List[Union[str, Tuple[str, str]]], Callable]
140
+ Parameters to process
141
+
142
+ event_name : Union[str, callable]
143
+ Name of the event
144
+
145
+ Returns
146
+ -------
147
+ Union[Dict[str, str], Callable]
148
+ Processed parameters
149
+
150
+ Raises
151
+ ------
152
+ MetaflowException
153
+ If the parameters are not in the correct format
154
+ """
100
155
  new_param_values = {}
101
- if isinstance(parameters, (list, tuple)):
156
+ if isinstance(parameters, list):
102
157
  for mapping in parameters:
103
158
  if is_stringish(mapping):
159
+ # param_name
104
160
  new_param_values[mapping] = mapping
105
- elif callable(mapping) and not isinstance(mapping, DeployTimeField):
106
- mapping = DeployTimeField(
107
- "parameter_val", str, None, mapping, False
108
- )
109
- new_param_values[mapping] = mapping
110
- elif isinstance(mapping, (list, tuple)) and len(mapping) == 2:
111
- if callable(mapping[0]) and not isinstance(
112
- mapping[0], DeployTimeField
113
- ):
114
- mapping[0] = DeployTimeField(
115
- "parameter_val", str, None, mapping[0], False
116
- )
117
- if callable(mapping[1]) and not isinstance(
118
- mapping[1], DeployTimeField
119
- ):
120
- mapping[1] = DeployTimeField(
121
- "parameter_val", str, None, mapping[1], False
161
+ elif isinstance(mapping, tuple) and len(mapping) == 2:
162
+ # (param_name, field_name)
163
+ param_name, field_name = mapping
164
+ if not is_stringish(param_name) or not is_stringish(field_name):
165
+ raise MetaflowException(
166
+ f"The *parameters* attribute for event {event_name} is invalid. "
167
+ "It should be a list/tuple of strings and lists/tuples of size 2."
122
168
  )
123
- new_param_values[mapping[0]] = mapping[1]
169
+ new_param_values[param_name] = field_name
124
170
  else:
125
171
  raise MetaflowException(
126
172
  "The *parameters* attribute for event is invalid. "
127
173
  "It should be a list/tuple of strings and lists/tuples of size 2"
128
174
  )
129
- elif callable(parameters) and not isinstance(parameters, DeployTimeField):
130
- return DeployTimeField(
131
- "parameters", [list, dict, tuple], None, parameters, False
132
- )
133
175
  elif isinstance(parameters, dict):
134
176
  for key, value in parameters.items():
135
- if callable(key) and not isinstance(key, DeployTimeField):
136
- key = DeployTimeField("flow_parameter", str, None, key, False)
137
- if callable(value) and not isinstance(value, DeployTimeField):
138
- value = DeployTimeField("signal_parameter", str, None, value, False)
177
+ if not is_stringish(key) or not is_stringish(value):
178
+ raise MetaflowException(
179
+ f"The *parameters* attribute for event {event_name} is invalid. "
180
+ "It should be a dictionary of string keys and string values."
181
+ )
139
182
  new_param_values[key] = value
183
+ elif callable(parameters) and not isinstance(parameters, DeployTimeField):
184
+ # func
185
+ return DeployTimeField(
186
+ "parameters",
187
+ [list, dict, tuple],
188
+ None,
189
+ parameters,
190
+ False,
191
+ print_representation=str(parameters),
192
+ )
140
193
  return new_param_values
141
194
 
142
195
  def flow_init(
@@ -158,7 +211,7 @@ class TriggerDecorator(FlowDecorator):
158
211
  )
159
212
  elif self.attributes["event"]:
160
213
  event = self.attributes["event"]
161
- processed_event = self.process_event_name(event)
214
+ processed_event = self.process_event(event)
162
215
  self.triggers.append(processed_event)
163
216
  elif self.attributes["events"]:
164
217
  # events attribute supports the following formats -
@@ -169,13 +222,18 @@ class TriggerDecorator(FlowDecorator):
169
222
  if isinstance(self.attributes["events"], list):
170
223
  # process every event in events
171
224
  for event in self.attributes["events"]:
172
- processed_event = self.process_event_name(event)
225
+ processed_event = self.process_event(event)
173
226
  self.triggers.append(processed_event)
174
227
  elif callable(self.attributes["events"]) and not isinstance(
175
228
  self.attributes["events"], DeployTimeField
176
229
  ):
177
230
  trig = DeployTimeField(
178
- "events", list, None, self.attributes["events"], False
231
+ "events",
232
+ list,
233
+ None,
234
+ self.attributes["events"],
235
+ False,
236
+ print_representation=str(self.attributes["events"]),
179
237
  )
180
238
  self.triggers.append(trig)
181
239
  else:
@@ -208,101 +266,40 @@ class TriggerDecorator(FlowDecorator):
208
266
 
209
267
  def format_deploytime_value(self):
210
268
  new_triggers = []
269
+
270
+ # First pass to evaluate DeployTimeFields
211
271
  for trigger in self.triggers:
212
272
  # Case where trigger is a function that returns a list of events
213
273
  # Need to do this bc we need to iterate over list later
214
274
  if isinstance(trigger, DeployTimeField):
215
275
  evaluated_trigger = deploy_time_eval(trigger)
216
- if isinstance(evaluated_trigger, dict):
217
- trigger = evaluated_trigger
218
- elif isinstance(evaluated_trigger, str):
219
- trigger = {"name": evaluated_trigger}
220
276
  if isinstance(evaluated_trigger, list):
221
- for trig in evaluated_trigger:
222
- if is_stringish(trig):
223
- new_triggers.append({"name": trig})
224
- else: # dict or another deploytimefield
225
- new_triggers.append(trig)
277
+ for event in evaluated_trigger:
278
+ new_triggers.append(self.process_event(event))
226
279
  else:
227
- new_triggers.append(trigger)
280
+ new_triggers.append(self.process_event(evaluated_trigger))
228
281
  else:
229
282
  new_triggers.append(trigger)
230
283
 
284
+ # Second pass to evaluate names
285
+ for trigger in new_triggers:
286
+ name = trigger.get("name")
287
+ if isinstance(name, DeployTimeField):
288
+ trigger["name"] = deploy_time_eval(name)
289
+ if not is_stringish(trigger["name"]):
290
+ raise MetaflowException(
291
+ f"The *name* attribute for event {trigger} is not a valid string"
292
+ )
293
+
294
+ # third pass to evaluate parameters
295
+ for trigger in new_triggers:
296
+ parameters = trigger.get("parameters", {})
297
+ if isinstance(parameters, DeployTimeField):
298
+ parameters_eval = deploy_time_eval(parameters)
299
+ parameters = self.process_parameters(parameters_eval, trigger["name"])
300
+ trigger["parameters"] = parameters
301
+
231
302
  self.triggers = new_triggers
232
- for trigger in self.triggers:
233
- old_trigger = trigger
234
- trigger_params = trigger.get("parameters", {})
235
- # Case where param is a function (can return list or dict)
236
- if isinstance(trigger_params, DeployTimeField):
237
- trigger_params = deploy_time_eval(trigger_params)
238
- # If params is a list of strings, convert to dict with same key and value
239
- if isinstance(trigger_params, (list, tuple)):
240
- new_trigger_params = {}
241
- for mapping in trigger_params:
242
- if is_stringish(mapping) or callable(mapping):
243
- new_trigger_params[mapping] = mapping
244
- elif callable(mapping) and not isinstance(mapping, DeployTimeField):
245
- mapping = DeployTimeField(
246
- "parameter_val", str, None, mapping, False
247
- )
248
- new_trigger_params[mapping] = mapping
249
- elif isinstance(mapping, (list, tuple)) and len(mapping) == 2:
250
- if callable(mapping[0]) and not isinstance(
251
- mapping[0], DeployTimeField
252
- ):
253
- mapping[0] = DeployTimeField(
254
- "parameter_val",
255
- str,
256
- None,
257
- mapping[1],
258
- False,
259
- )
260
- if callable(mapping[1]) and not isinstance(
261
- mapping[1], DeployTimeField
262
- ):
263
- mapping[1] = DeployTimeField(
264
- "parameter_val",
265
- str,
266
- None,
267
- mapping[1],
268
- False,
269
- )
270
-
271
- new_trigger_params[mapping[0]] = mapping[1]
272
- else:
273
- raise MetaflowException(
274
- "The *parameters* attribute for event '%s' is invalid. "
275
- "It should be a list/tuple of strings and lists/tuples "
276
- "of size 2" % self.attributes["event"]["name"]
277
- )
278
- trigger_params = new_trigger_params
279
- trigger["parameters"] = trigger_params
280
-
281
- trigger_name = trigger.get("name")
282
- # Case where just the name is a function (always a str)
283
- if isinstance(trigger_name, DeployTimeField):
284
- trigger_name = deploy_time_eval(trigger_name)
285
- trigger["name"] = trigger_name
286
-
287
- # Third layer
288
- # {name:, parameters:[func, ..., ...]}
289
- # {name:, parameters:{func : func2}}
290
- for trigger in self.triggers:
291
- old_trigger = trigger
292
- trigger_params = trigger.get("parameters", {})
293
- new_trigger_params = {}
294
- for key, value in trigger_params.items():
295
- if isinstance(value, DeployTimeField) and key is value:
296
- evaluated_param = deploy_time_eval(value)
297
- new_trigger_params[evaluated_param] = evaluated_param
298
- elif isinstance(value, DeployTimeField):
299
- new_trigger_params[key] = deploy_time_eval(value)
300
- elif isinstance(key, DeployTimeField):
301
- new_trigger_params[deploy_time_eval(key)] = value
302
- else:
303
- new_trigger_params[key] = value
304
- trigger["parameters"] = new_trigger_params
305
- self.triggers[self.triggers.index(old_trigger)] = trigger
306
303
 
307
304
 
308
305
  class TriggerOnFinishDecorator(FlowDecorator):
@@ -402,7 +399,14 @@ class TriggerOnFinishDecorator(FlowDecorator):
402
399
  if callable(flow) and not isinstance(
403
400
  self.attributes["flow"], DeployTimeField
404
401
  ):
405
- trig = DeployTimeField("fq_name", [str, dict], None, flow, False)
402
+ trig = DeployTimeField(
403
+ "fq_name",
404
+ [str, dict],
405
+ None,
406
+ flow,
407
+ False,
408
+ print_representation=str(flow),
409
+ )
406
410
  self.triggers.append(trig)
407
411
  else:
408
412
  self.triggers.extend(self._parse_static_triggers([flow]))
@@ -411,7 +415,9 @@ class TriggerOnFinishDecorator(FlowDecorator):
411
415
  # 1. flows=['FooFlow', 'BarFlow']
412
416
  flows = self.attributes["flows"]
413
417
  if callable(flows) and not isinstance(flows, DeployTimeField):
414
- trig = DeployTimeField("flows", list, None, flows, False)
418
+ trig = DeployTimeField(
419
+ "flows", list, None, flows, False, print_representation=str(flows)
420
+ )
415
421
  self.triggers.append(trig)
416
422
  elif isinstance(flows, list):
417
423
  self.triggers.extend(self._parse_static_triggers(flows))
File without changes
@@ -0,0 +1,46 @@
1
+ from metaflow.decorators import FlowDecorator
2
+ from metaflow.exception import MetaflowException
3
+
4
+
5
+ class ExitHookDecorator(FlowDecorator):
6
+ name = "exit_hook"
7
+ allow_multiple = True
8
+
9
+ defaults = {
10
+ "on_success": [],
11
+ "on_error": [],
12
+ "options": {},
13
+ }
14
+
15
+ def flow_init(
16
+ self, flow, graph, environment, flow_datastore, metadata, logger, echo, options
17
+ ):
18
+ on_success = self.attributes["on_success"]
19
+ on_error = self.attributes["on_error"]
20
+
21
+ if not on_success and not on_error:
22
+ raise MetaflowException(
23
+ "Choose at least one of the options on_success/on_error"
24
+ )
25
+
26
+ self.success_hooks = []
27
+ self.error_hooks = []
28
+ for success_fn in on_success:
29
+ if isinstance(success_fn, str):
30
+ self.success_hooks.append(success_fn)
31
+ elif callable(success_fn):
32
+ self.success_hooks.append(success_fn.__name__)
33
+ else:
34
+ raise ValueError(
35
+ "Exit hooks inside 'on_success' must be a function or a string referring to the function"
36
+ )
37
+
38
+ for error_fn in on_error:
39
+ if isinstance(error_fn, str):
40
+ self.error_hooks.append(error_fn)
41
+ elif callable(error_fn):
42
+ self.error_hooks.append(error_fn.__name__)
43
+ else:
44
+ raise ValueError(
45
+ "Exit hooks inside 'on_error' must be a function or a string referring to the function"
46
+ )
@@ -0,0 +1,52 @@
1
+ import os
2
+ import inspect
3
+ import importlib
4
+ import sys
5
+
6
+
7
+ def main(flow_file, fn_name_or_path, run_pathspec):
8
+ hook_fn = None
9
+
10
+ try:
11
+ module_path, function_name = fn_name_or_path.rsplit(".", 1)
12
+ module = importlib.import_module(module_path)
13
+ hook_fn = getattr(module, function_name)
14
+ except (ImportError, AttributeError, ValueError):
15
+ try:
16
+ module_name = os.path.splitext(os.path.basename(flow_file))[0]
17
+ spec = importlib.util.spec_from_file_location(module_name, flow_file)
18
+ module = importlib.util.module_from_spec(spec)
19
+ spec.loader.exec_module(module)
20
+ hook_fn = getattr(module, fn_name_or_path)
21
+ except (AttributeError, IOError) as e:
22
+ print(
23
+ f"[exit_hook] Could not load function '{fn_name_or_path}' "
24
+ f"as an import path or from '{flow_file}': {e}"
25
+ )
26
+ sys.exit(1)
27
+
28
+ argspec = inspect.getfullargspec(hook_fn)
29
+
30
+ # Check if fn expects a run object as an arg.
31
+ if "run" in argspec.args or argspec.varkw is not None:
32
+ from metaflow import Run
33
+
34
+ try:
35
+ _run = Run(run_pathspec, _namespace_check=False)
36
+ except Exception as ex:
37
+ print(ex)
38
+ _run = None
39
+
40
+ hook_fn(run=_run)
41
+ else:
42
+ hook_fn()
43
+
44
+
45
+ if __name__ == "__main__":
46
+ try:
47
+ flow_file, fn_name, run_pathspec = sys.argv[1:4]
48
+ except Exception:
49
+ print("Usage: exit_hook_script.py <flow_file> <function_name> <run_pathspec>")
50
+ sys.exit(1)
51
+
52
+ main(flow_file, fn_name, run_pathspec)