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.
- metaflow/__init__.py +10 -3
- metaflow/_vendor/imghdr/__init__.py +186 -0
- metaflow/_vendor/yaml/__init__.py +427 -0
- metaflow/_vendor/yaml/composer.py +139 -0
- metaflow/_vendor/yaml/constructor.py +748 -0
- metaflow/_vendor/yaml/cyaml.py +101 -0
- metaflow/_vendor/yaml/dumper.py +62 -0
- metaflow/_vendor/yaml/emitter.py +1137 -0
- metaflow/_vendor/yaml/error.py +75 -0
- metaflow/_vendor/yaml/events.py +86 -0
- metaflow/_vendor/yaml/loader.py +63 -0
- metaflow/_vendor/yaml/nodes.py +49 -0
- metaflow/_vendor/yaml/parser.py +589 -0
- metaflow/_vendor/yaml/reader.py +185 -0
- metaflow/_vendor/yaml/representer.py +389 -0
- metaflow/_vendor/yaml/resolver.py +227 -0
- metaflow/_vendor/yaml/scanner.py +1435 -0
- metaflow/_vendor/yaml/serializer.py +111 -0
- metaflow/_vendor/yaml/tokens.py +104 -0
- metaflow/cards.py +4 -0
- metaflow/cli.py +125 -21
- metaflow/cli_components/init_cmd.py +1 -0
- metaflow/cli_components/run_cmds.py +204 -40
- metaflow/cli_components/step_cmd.py +160 -4
- metaflow/client/__init__.py +1 -0
- metaflow/client/core.py +198 -130
- metaflow/client/filecache.py +59 -32
- metaflow/cmd/code/__init__.py +2 -1
- metaflow/cmd/develop/stub_generator.py +49 -18
- metaflow/cmd/develop/stubs.py +9 -27
- metaflow/cmd/make_wrapper.py +30 -0
- metaflow/datastore/__init__.py +1 -0
- metaflow/datastore/content_addressed_store.py +40 -9
- metaflow/datastore/datastore_set.py +10 -1
- metaflow/datastore/flow_datastore.py +124 -4
- metaflow/datastore/spin_datastore.py +91 -0
- metaflow/datastore/task_datastore.py +92 -6
- metaflow/debug.py +5 -0
- metaflow/decorators.py +331 -82
- metaflow/extension_support/__init__.py +414 -356
- metaflow/extension_support/_empty_file.py +2 -2
- metaflow/flowspec.py +322 -82
- metaflow/graph.py +178 -15
- metaflow/includefile.py +25 -3
- metaflow/lint.py +94 -3
- metaflow/meta_files.py +13 -0
- metaflow/metadata_provider/metadata.py +13 -2
- metaflow/metaflow_config.py +66 -4
- metaflow/metaflow_environment.py +91 -25
- metaflow/metaflow_profile.py +18 -0
- metaflow/metaflow_version.py +16 -1
- metaflow/package/__init__.py +673 -0
- metaflow/packaging_sys/__init__.py +880 -0
- metaflow/packaging_sys/backend.py +128 -0
- metaflow/packaging_sys/distribution_support.py +153 -0
- metaflow/packaging_sys/tar_backend.py +99 -0
- metaflow/packaging_sys/utils.py +54 -0
- metaflow/packaging_sys/v1.py +527 -0
- metaflow/parameters.py +6 -2
- metaflow/plugins/__init__.py +6 -0
- metaflow/plugins/airflow/airflow.py +11 -1
- metaflow/plugins/airflow/airflow_cli.py +16 -5
- metaflow/plugins/argo/argo_client.py +42 -20
- metaflow/plugins/argo/argo_events.py +6 -6
- metaflow/plugins/argo/argo_workflows.py +1023 -344
- metaflow/plugins/argo/argo_workflows_cli.py +396 -94
- metaflow/plugins/argo/argo_workflows_decorator.py +9 -0
- metaflow/plugins/argo/argo_workflows_deployer_objects.py +75 -49
- metaflow/plugins/argo/capture_error.py +5 -2
- metaflow/plugins/argo/conditional_input_paths.py +35 -0
- metaflow/plugins/argo/exit_hooks.py +209 -0
- metaflow/plugins/argo/param_val.py +19 -0
- metaflow/plugins/aws/aws_client.py +6 -0
- metaflow/plugins/aws/aws_utils.py +33 -1
- metaflow/plugins/aws/batch/batch.py +72 -5
- metaflow/plugins/aws/batch/batch_cli.py +24 -3
- metaflow/plugins/aws/batch/batch_decorator.py +57 -6
- metaflow/plugins/aws/step_functions/step_functions.py +28 -3
- metaflow/plugins/aws/step_functions/step_functions_cli.py +49 -4
- metaflow/plugins/aws/step_functions/step_functions_deployer.py +3 -0
- metaflow/plugins/aws/step_functions/step_functions_deployer_objects.py +30 -0
- metaflow/plugins/cards/card_cli.py +20 -1
- metaflow/plugins/cards/card_creator.py +24 -1
- metaflow/plugins/cards/card_datastore.py +21 -49
- metaflow/plugins/cards/card_decorator.py +58 -6
- metaflow/plugins/cards/card_modules/basic.py +38 -9
- metaflow/plugins/cards/card_modules/bundle.css +1 -1
- metaflow/plugins/cards/card_modules/chevron/renderer.py +1 -1
- metaflow/plugins/cards/card_modules/components.py +592 -3
- metaflow/plugins/cards/card_modules/convert_to_native_type.py +34 -5
- metaflow/plugins/cards/card_modules/json_viewer.py +232 -0
- metaflow/plugins/cards/card_modules/main.css +1 -0
- metaflow/plugins/cards/card_modules/main.js +56 -41
- metaflow/plugins/cards/card_modules/test_cards.py +22 -6
- metaflow/plugins/cards/component_serializer.py +1 -8
- metaflow/plugins/cards/metadata.py +22 -0
- metaflow/plugins/catch_decorator.py +9 -0
- metaflow/plugins/datastores/local_storage.py +12 -6
- metaflow/plugins/datastores/spin_storage.py +12 -0
- metaflow/plugins/datatools/s3/s3.py +49 -17
- metaflow/plugins/datatools/s3/s3op.py +113 -66
- metaflow/plugins/env_escape/client_modules.py +102 -72
- metaflow/plugins/events_decorator.py +127 -121
- metaflow/plugins/exit_hook/__init__.py +0 -0
- metaflow/plugins/exit_hook/exit_hook_decorator.py +46 -0
- metaflow/plugins/exit_hook/exit_hook_script.py +52 -0
- metaflow/plugins/kubernetes/kubernetes.py +12 -1
- metaflow/plugins/kubernetes/kubernetes_cli.py +11 -0
- metaflow/plugins/kubernetes/kubernetes_decorator.py +25 -6
- metaflow/plugins/kubernetes/kubernetes_job.py +12 -4
- metaflow/plugins/kubernetes/kubernetes_jobsets.py +31 -30
- metaflow/plugins/metadata_providers/local.py +76 -82
- metaflow/plugins/metadata_providers/service.py +13 -9
- metaflow/plugins/metadata_providers/spin.py +16 -0
- metaflow/plugins/package_cli.py +36 -24
- metaflow/plugins/parallel_decorator.py +11 -2
- metaflow/plugins/parsers.py +16 -0
- metaflow/plugins/pypi/bootstrap.py +7 -1
- metaflow/plugins/pypi/conda_decorator.py +41 -82
- metaflow/plugins/pypi/conda_environment.py +14 -6
- metaflow/plugins/pypi/micromamba.py +9 -1
- metaflow/plugins/pypi/pip.py +41 -5
- metaflow/plugins/pypi/pypi_decorator.py +4 -4
- metaflow/plugins/pypi/utils.py +22 -0
- metaflow/plugins/secrets/__init__.py +3 -0
- metaflow/plugins/secrets/secrets_decorator.py +14 -178
- metaflow/plugins/secrets/secrets_func.py +49 -0
- metaflow/plugins/secrets/secrets_spec.py +101 -0
- metaflow/plugins/secrets/utils.py +74 -0
- metaflow/plugins/test_unbounded_foreach_decorator.py +2 -2
- metaflow/plugins/timeout_decorator.py +0 -1
- metaflow/plugins/uv/bootstrap.py +29 -1
- metaflow/plugins/uv/uv_environment.py +5 -3
- metaflow/pylint_wrapper.py +5 -1
- metaflow/runner/click_api.py +79 -26
- metaflow/runner/deployer.py +208 -6
- metaflow/runner/deployer_impl.py +32 -12
- metaflow/runner/metaflow_runner.py +266 -33
- metaflow/runner/subprocess_manager.py +21 -1
- metaflow/runner/utils.py +27 -16
- metaflow/runtime.py +660 -66
- metaflow/task.py +255 -26
- metaflow/user_configs/config_options.py +33 -21
- metaflow/user_configs/config_parameters.py +220 -58
- metaflow/user_decorators/__init__.py +0 -0
- metaflow/user_decorators/common.py +144 -0
- metaflow/user_decorators/mutable_flow.py +512 -0
- metaflow/user_decorators/mutable_step.py +424 -0
- metaflow/user_decorators/user_flow_decorator.py +264 -0
- metaflow/user_decorators/user_step_decorator.py +749 -0
- metaflow/util.py +197 -7
- metaflow/vendor.py +23 -7
- metaflow/version.py +1 -1
- {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/Makefile +13 -2
- {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/Tiltfile +107 -7
- {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/pick_services.sh +1 -0
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/METADATA +2 -3
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/RECORD +162 -121
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/WHEEL +1 -1
- metaflow/_vendor/v3_5/__init__.py +0 -1
- metaflow/_vendor/v3_5/importlib_metadata/__init__.py +0 -644
- metaflow/_vendor/v3_5/importlib_metadata/_compat.py +0 -152
- metaflow/_vendor/v3_5/zipp.py +0 -329
- metaflow/info_file.py +0 -25
- metaflow/package.py +0 -203
- metaflow/user_configs/config_decorators.py +0 -568
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/entry_points.txt +0 -0
- {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
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",
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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[
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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.
|
|
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.
|
|
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",
|
|
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
|
|
222
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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)
|