metaflow 2.15.18__py2.py3-none-any.whl → 2.15.19__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/_vendor/imghdr/__init__.py +180 -0
- metaflow/cmd/develop/stub_generator.py +19 -2
- metaflow/plugins/__init__.py +3 -0
- metaflow/plugins/airflow/airflow.py +6 -0
- metaflow/plugins/argo/argo_workflows.py +316 -287
- metaflow/plugins/argo/exit_hooks.py +209 -0
- metaflow/plugins/aws/aws_utils.py +1 -1
- metaflow/plugins/aws/step_functions/step_functions.py +6 -0
- metaflow/plugins/cards/card_cli.py +20 -1
- metaflow/plugins/cards/card_creator.py +24 -1
- metaflow/plugins/cards/card_decorator.py +57 -1
- metaflow/plugins/cards/card_modules/convert_to_native_type.py +5 -2
- metaflow/plugins/cards/card_modules/test_cards.py +16 -0
- metaflow/plugins/cards/metadata.py +22 -0
- 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/secrets/__init__.py +3 -0
- metaflow/plugins/secrets/secrets_decorator.py +9 -173
- metaflow/plugins/secrets/secrets_func.py +60 -0
- metaflow/plugins/secrets/secrets_spec.py +101 -0
- metaflow/plugins/secrets/utils.py +74 -0
- metaflow/runner/metaflow_runner.py +16 -1
- metaflow/runtime.py +45 -0
- metaflow/version.py +1 -1
- {metaflow-2.15.18.data → metaflow-2.15.19.data}/data/share/metaflow/devtools/Tiltfile +27 -2
- {metaflow-2.15.18.dist-info → metaflow-2.15.19.dist-info}/METADATA +2 -2
- {metaflow-2.15.18.dist-info → metaflow-2.15.19.dist-info}/RECORD +34 -25
- {metaflow-2.15.18.data → metaflow-2.15.19.data}/data/share/metaflow/devtools/Makefile +0 -0
- {metaflow-2.15.18.data → metaflow-2.15.19.data}/data/share/metaflow/devtools/pick_services.sh +0 -0
- {metaflow-2.15.18.dist-info → metaflow-2.15.19.dist-info}/WHEEL +0 -0
- {metaflow-2.15.18.dist-info → metaflow-2.15.19.dist-info}/entry_points.txt +0 -0
- {metaflow-2.15.18.dist-info → metaflow-2.15.19.dist-info}/licenses/LICENSE +0 -0
- {metaflow-2.15.18.dist-info → metaflow-2.15.19.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,209 @@
|
|
1
|
+
from collections import defaultdict
|
2
|
+
import json
|
3
|
+
from typing import Dict, List
|
4
|
+
|
5
|
+
|
6
|
+
class JsonSerializable(object):
|
7
|
+
def to_json(self):
|
8
|
+
return self.payload
|
9
|
+
|
10
|
+
def __str__(self):
|
11
|
+
return json.dumps(self.payload, indent=4)
|
12
|
+
|
13
|
+
|
14
|
+
class _LifecycleHook(JsonSerializable):
|
15
|
+
# https://argoproj.github.io/argo-workflows/fields/#lifecyclehook
|
16
|
+
|
17
|
+
def __init__(self, name):
|
18
|
+
tree = lambda: defaultdict(tree)
|
19
|
+
self.name = name
|
20
|
+
self.payload = tree()
|
21
|
+
|
22
|
+
def expression(self, expression):
|
23
|
+
self.payload["expression"] = str(expression)
|
24
|
+
return self
|
25
|
+
|
26
|
+
def template(self, template):
|
27
|
+
self.payload["template"] = template
|
28
|
+
return self
|
29
|
+
|
30
|
+
|
31
|
+
class _Template(JsonSerializable):
|
32
|
+
# https://argoproj.github.io/argo-workflows/fields/#template
|
33
|
+
|
34
|
+
def __init__(self, name):
|
35
|
+
tree = lambda: defaultdict(tree)
|
36
|
+
self.name = name
|
37
|
+
self.payload = tree()
|
38
|
+
self.payload["name"] = name
|
39
|
+
|
40
|
+
def http(self, http):
|
41
|
+
self.payload["http"] = http.to_json()
|
42
|
+
return self
|
43
|
+
|
44
|
+
def script(self, script):
|
45
|
+
self.payload["script"] = script.to_json()
|
46
|
+
return self
|
47
|
+
|
48
|
+
def container(self, container):
|
49
|
+
self.payload["container"] = container
|
50
|
+
return self
|
51
|
+
|
52
|
+
def service_account_name(self, service_account_name):
|
53
|
+
self.payload["serviceAccountName"] = service_account_name
|
54
|
+
return self
|
55
|
+
|
56
|
+
|
57
|
+
class Hook(object):
|
58
|
+
"""
|
59
|
+
Abstraction for Argo Workflows exit hooks.
|
60
|
+
A hook consists of a Template, and one or more LifecycleHooks that trigger the template
|
61
|
+
"""
|
62
|
+
|
63
|
+
template: "_Template"
|
64
|
+
lifecycle_hooks: List["_LifecycleHook"]
|
65
|
+
|
66
|
+
|
67
|
+
class _HttpSpec(JsonSerializable):
|
68
|
+
# https://argoproj.github.io/argo-workflows/fields/#http
|
69
|
+
|
70
|
+
def __init__(self, method):
|
71
|
+
tree = lambda: defaultdict(tree)
|
72
|
+
self.payload = tree()
|
73
|
+
self.payload["method"] = method
|
74
|
+
self.payload["headers"] = []
|
75
|
+
|
76
|
+
def header(self, header, value):
|
77
|
+
self.payload["headers"].append({"name": header, "value": value})
|
78
|
+
return self
|
79
|
+
|
80
|
+
def body(self, body):
|
81
|
+
self.payload["body"] = str(body)
|
82
|
+
return self
|
83
|
+
|
84
|
+
def url(self, url):
|
85
|
+
self.payload["url"] = url
|
86
|
+
return self
|
87
|
+
|
88
|
+
def success_condition(self, success_condition):
|
89
|
+
self.payload["successCondition"] = success_condition
|
90
|
+
return self
|
91
|
+
|
92
|
+
|
93
|
+
# HTTP hook
|
94
|
+
class HttpExitHook(Hook):
|
95
|
+
def __init__(
|
96
|
+
self,
|
97
|
+
name,
|
98
|
+
url,
|
99
|
+
method="GET",
|
100
|
+
headers=None,
|
101
|
+
body=None,
|
102
|
+
on_success=False,
|
103
|
+
on_error=False,
|
104
|
+
):
|
105
|
+
self.template = _Template(name)
|
106
|
+
http = _HttpSpec(method).url(url)
|
107
|
+
if headers is not None:
|
108
|
+
for header, value in headers.items():
|
109
|
+
http.header(header, value)
|
110
|
+
|
111
|
+
if body is not None:
|
112
|
+
http.body(json.dumps(body))
|
113
|
+
|
114
|
+
self.template.http(http)
|
115
|
+
|
116
|
+
self.lifecycle_hooks = []
|
117
|
+
|
118
|
+
if on_success and on_error:
|
119
|
+
raise Exception("Set only one of the on_success/on_error at a time.")
|
120
|
+
|
121
|
+
if on_success:
|
122
|
+
self.lifecycle_hooks.append(
|
123
|
+
_LifecycleHook(name)
|
124
|
+
.expression("workflow.status == 'Succeeded'")
|
125
|
+
.template(self.template.name)
|
126
|
+
)
|
127
|
+
|
128
|
+
if on_error:
|
129
|
+
self.lifecycle_hooks.append(
|
130
|
+
_LifecycleHook(name)
|
131
|
+
.expression("workflow.status == 'Error' || workflow.status == 'Failed'")
|
132
|
+
.template(self.template.name)
|
133
|
+
)
|
134
|
+
|
135
|
+
if not on_success and not on_error:
|
136
|
+
# add an expressionless lifecycle hook
|
137
|
+
self.lifecycle_hooks.append(_LifecycleHook(name).template(name))
|
138
|
+
|
139
|
+
|
140
|
+
class ExitHookHack(Hook):
|
141
|
+
# Warning: terrible hack to workaround a bug in Argo Workflow where the
|
142
|
+
# templates listed above do not execute unless there is an
|
143
|
+
# explicit exit hook. as and when this bug is patched, we should
|
144
|
+
# remove this effectively no-op template.
|
145
|
+
# Note: We use the Http template because changing this to an actual no-op container had the side-effect of
|
146
|
+
# leaving LifecycleHooks in a pending state even when they have finished execution.
|
147
|
+
def __init__(
|
148
|
+
self,
|
149
|
+
url,
|
150
|
+
headers=None,
|
151
|
+
body=None,
|
152
|
+
):
|
153
|
+
self.template = _Template("exit-hook-hack")
|
154
|
+
http = _HttpSpec("GET").url(url)
|
155
|
+
if headers is not None:
|
156
|
+
for header, value in headers.items():
|
157
|
+
http.header(header, value)
|
158
|
+
|
159
|
+
if body is not None:
|
160
|
+
http.body(json.dumps(body))
|
161
|
+
|
162
|
+
http.success_condition("true == true")
|
163
|
+
|
164
|
+
self.template.http(http)
|
165
|
+
|
166
|
+
self.lifecycle_hooks = []
|
167
|
+
|
168
|
+
# add an expressionless lifecycle hook
|
169
|
+
self.lifecycle_hooks.append(_LifecycleHook("exit").template("exit-hook-hack"))
|
170
|
+
|
171
|
+
|
172
|
+
class ContainerHook(Hook):
|
173
|
+
def __init__(
|
174
|
+
self,
|
175
|
+
name: str,
|
176
|
+
container: Dict,
|
177
|
+
service_account_name: str = None,
|
178
|
+
on_success: bool = False,
|
179
|
+
on_error: bool = False,
|
180
|
+
):
|
181
|
+
self.template = _Template(name)
|
182
|
+
|
183
|
+
if service_account_name is not None:
|
184
|
+
self.template.service_account_name(service_account_name)
|
185
|
+
|
186
|
+
self.template.container(container)
|
187
|
+
|
188
|
+
self.lifecycle_hooks = []
|
189
|
+
|
190
|
+
if on_success and on_error:
|
191
|
+
raise Exception("Set only one of the on_success/on_error at a time.")
|
192
|
+
|
193
|
+
if on_success:
|
194
|
+
self.lifecycle_hooks.append(
|
195
|
+
_LifecycleHook(name)
|
196
|
+
.expression("workflow.status == 'Succeeded'")
|
197
|
+
.template(self.template.name)
|
198
|
+
)
|
199
|
+
|
200
|
+
if on_error:
|
201
|
+
self.lifecycle_hooks.append(
|
202
|
+
_LifecycleHook(name)
|
203
|
+
.expression("workflow.status == 'Error' || workflow.status == 'Failed'")
|
204
|
+
.template(self.template.name)
|
205
|
+
)
|
206
|
+
|
207
|
+
if not on_success and not on_error:
|
208
|
+
# add an expressionless lifecycle hook
|
209
|
+
self.lifecycle_hooks.append(_LifecycleHook(name).template(name))
|
@@ -48,7 +48,7 @@ def get_ec2_instance_metadata():
|
|
48
48
|
# Try to get an IMDSv2 token.
|
49
49
|
token = requests.put(
|
50
50
|
url="http://169.254.169.254/latest/api/token",
|
51
|
-
headers={"X-aws-ec2-metadata-token-ttl-seconds": 100},
|
51
|
+
headers={"X-aws-ec2-metadata-token-ttl-seconds": "100"},
|
52
52
|
timeout=timeout,
|
53
53
|
).text
|
54
54
|
except:
|
@@ -301,6 +301,12 @@ class StepFunctions(object):
|
|
301
301
|
"to AWS Step Functions is not supported currently."
|
302
302
|
)
|
303
303
|
|
304
|
+
if self.flow._flow_decorators.get("exit_hook"):
|
305
|
+
raise StepFunctionsException(
|
306
|
+
"Deploying flows with the @exit_hook decorator "
|
307
|
+
"to AWS Step Functions is not currently supported."
|
308
|
+
)
|
309
|
+
|
304
310
|
# Visit every node of the flow and recursively build the state machine.
|
305
311
|
def _visit(node, workflow, exit_node=None):
|
306
312
|
if node.parallel_foreach:
|
@@ -30,7 +30,7 @@ from .exception import (
|
|
30
30
|
)
|
31
31
|
import traceback
|
32
32
|
from collections import namedtuple
|
33
|
-
|
33
|
+
from .metadata import _save_metadata
|
34
34
|
from .card_resolver import resolve_paths_from_task, resumed_info
|
35
35
|
|
36
36
|
id_func = id
|
@@ -613,6 +613,14 @@ def update_card(mf_card, mode, task, data, timeout_value=None):
|
|
613
613
|
hidden=True,
|
614
614
|
help="Delete data-file and component-file after reading. (internal)",
|
615
615
|
)
|
616
|
+
@click.option(
|
617
|
+
"--save-metadata",
|
618
|
+
default=None,
|
619
|
+
show_default=True,
|
620
|
+
type=JSONTypeClass(),
|
621
|
+
hidden=True,
|
622
|
+
help="JSON string containing metadata to be saved. (internal)",
|
623
|
+
)
|
616
624
|
@click.pass_context
|
617
625
|
def create(
|
618
626
|
ctx,
|
@@ -627,6 +635,7 @@ def create(
|
|
627
635
|
card_uuid=None,
|
628
636
|
delete_input_files=None,
|
629
637
|
id=None,
|
638
|
+
save_metadata=None,
|
630
639
|
):
|
631
640
|
card_id = id
|
632
641
|
rendered_info = None # Variable holding all the information which will be rendered
|
@@ -824,6 +833,16 @@ def create(
|
|
824
833
|
% (card_info.type, card_info.hash[:NUM_SHORT_HASH_CHARS]),
|
825
834
|
fg="green",
|
826
835
|
)
|
836
|
+
if save_metadata:
|
837
|
+
_save_metadata(
|
838
|
+
ctx.obj.metadata,
|
839
|
+
task.parent.parent.id,
|
840
|
+
task.parent.id,
|
841
|
+
task.id,
|
842
|
+
task.current_attempt,
|
843
|
+
card_uuid,
|
844
|
+
save_metadata,
|
845
|
+
)
|
827
846
|
|
828
847
|
|
829
848
|
@card.command()
|
@@ -5,6 +5,8 @@ import json
|
|
5
5
|
import sys
|
6
6
|
import os
|
7
7
|
from metaflow import current
|
8
|
+
from typing import Callable, Tuple, Dict
|
9
|
+
|
8
10
|
|
9
11
|
ASYNC_TIMEOUT = 30
|
10
12
|
|
@@ -44,8 +46,18 @@ class CardProcessManager:
|
|
44
46
|
|
45
47
|
|
46
48
|
class CardCreator:
|
47
|
-
def __init__(
|
49
|
+
def __init__(
|
50
|
+
self,
|
51
|
+
top_level_options,
|
52
|
+
should_save_metadata_lambda: Callable[[str], Tuple[bool, Dict]],
|
53
|
+
):
|
54
|
+
# should_save_metadata_lambda is a lambda that provides a flag to indicate if
|
55
|
+
# card metadata should be written to the metadata store.
|
56
|
+
# It gets called only once when the card is created inside the subprocess.
|
57
|
+
# The intent is that this is a stateful lambda that will ensure that we only end
|
58
|
+
# up writing to the metadata store once.
|
48
59
|
self._top_level_options = top_level_options
|
60
|
+
self._should_save_metadata = should_save_metadata_lambda
|
49
61
|
|
50
62
|
def create(
|
51
63
|
self,
|
@@ -62,6 +74,8 @@ class CardCreator:
|
|
62
74
|
# Setting `final` will affect the Reload token set during the card refresh
|
63
75
|
# data creation along with synchronous execution of subprocess.
|
64
76
|
# Setting `sync` will only cause synchronous execution of subprocess.
|
77
|
+
save_metadata = False
|
78
|
+
metadata_dict = {}
|
65
79
|
if mode != "render" and not runtime_card:
|
66
80
|
# silently ignore runtime updates for cards that don't support them
|
67
81
|
return
|
@@ -71,6 +85,8 @@ class CardCreator:
|
|
71
85
|
component_strings = []
|
72
86
|
else:
|
73
87
|
component_strings = current.card._serialize_components(card_uuid)
|
88
|
+
# Since the mode is a render, we can check if we need to write to the metadata store.
|
89
|
+
save_metadata, metadata_dict = self._should_save_metadata(card_uuid)
|
74
90
|
data = current.card._get_latest_data(card_uuid, final=final, mode=mode)
|
75
91
|
runspec = "/".join([current.run_id, current.step_name, current.task_id])
|
76
92
|
self._run_cards_subprocess(
|
@@ -85,6 +101,8 @@ class CardCreator:
|
|
85
101
|
data,
|
86
102
|
final=final,
|
87
103
|
sync=sync,
|
104
|
+
save_metadata=save_metadata,
|
105
|
+
metadata_dict=metadata_dict,
|
88
106
|
)
|
89
107
|
|
90
108
|
def _run_cards_subprocess(
|
@@ -100,6 +118,8 @@ class CardCreator:
|
|
100
118
|
data=None,
|
101
119
|
final=False,
|
102
120
|
sync=False,
|
121
|
+
save_metadata=False,
|
122
|
+
metadata_dict=None,
|
103
123
|
):
|
104
124
|
components_file = data_file = None
|
105
125
|
wait = final or sync
|
@@ -156,6 +176,9 @@ class CardCreator:
|
|
156
176
|
if data_file is not None:
|
157
177
|
cmd += ["--data-file", data_file.name]
|
158
178
|
|
179
|
+
if save_metadata:
|
180
|
+
cmd += ["--save-metadata", json.dumps(metadata_dict)]
|
181
|
+
|
159
182
|
response, fail = self._run_command(
|
160
183
|
cmd,
|
161
184
|
card_uuid,
|
@@ -2,8 +2,10 @@ import json
|
|
2
2
|
import os
|
3
3
|
import re
|
4
4
|
import tempfile
|
5
|
+
from typing import Tuple, Dict
|
5
6
|
|
6
7
|
from metaflow.decorators import StepDecorator
|
8
|
+
from metaflow.metadata_provider import MetaDatum
|
7
9
|
from metaflow.metaflow_current import current
|
8
10
|
from metaflow.user_configs.config_options import ConfigInput
|
9
11
|
from metaflow.user_configs.config_parameters import dump_config_values
|
@@ -22,6 +24,24 @@ def warning_message(message, logger=None, ts=False):
|
|
22
24
|
logger(msg, timestamp=ts, bad=True)
|
23
25
|
|
24
26
|
|
27
|
+
class MetadataStateManager(object):
|
28
|
+
def __init__(self, info_func):
|
29
|
+
self._info_func = info_func
|
30
|
+
self._metadata_registered = {}
|
31
|
+
|
32
|
+
def register_metadata(self, card_uuid) -> Tuple[bool, Dict]:
|
33
|
+
info = self._info_func()
|
34
|
+
# Check that metadata was not written yet. We only want to write once.
|
35
|
+
if (
|
36
|
+
info is None
|
37
|
+
or info.get(card_uuid) is None
|
38
|
+
or self._metadata_registered.get(card_uuid)
|
39
|
+
):
|
40
|
+
return False, {}
|
41
|
+
self._metadata_registered[card_uuid] = True
|
42
|
+
return True, info.get(card_uuid)
|
43
|
+
|
44
|
+
|
25
45
|
class CardDecorator(StepDecorator):
|
26
46
|
"""
|
27
47
|
Creates a human-readable report, a Metaflow Card, after this step completes.
|
@@ -55,11 +75,14 @@ class CardDecorator(StepDecorator):
|
|
55
75
|
The or one of the cards attached to this step.
|
56
76
|
"""
|
57
77
|
|
78
|
+
_GLOBAL_CARD_INFO = {}
|
79
|
+
|
58
80
|
name = "card"
|
59
81
|
defaults = {
|
60
82
|
"type": "default",
|
61
83
|
"options": {},
|
62
84
|
"scope": "task",
|
85
|
+
"rank": None, # Can be one of "high", "medium", "low". Can help derive ordering on the UI.
|
63
86
|
"timeout": 45,
|
64
87
|
"id": None,
|
65
88
|
"save_errors": True,
|
@@ -91,6 +114,7 @@ class CardDecorator(StepDecorator):
|
|
91
114
|
self._is_editable = False
|
92
115
|
self._card_uuid = None
|
93
116
|
self._user_set_card_id = None
|
117
|
+
self._metadata_registered = False
|
94
118
|
|
95
119
|
@classmethod
|
96
120
|
def _set_card_creator(cls, card_creator):
|
@@ -131,6 +155,16 @@ class CardDecorator(StepDecorator):
|
|
131
155
|
json.dump(config_value, config_file)
|
132
156
|
cls._config_file_name = config_file.name
|
133
157
|
|
158
|
+
@classmethod
|
159
|
+
def _register_card_info(cls, **kwargs):
|
160
|
+
if not kwargs.get("card_uuid"):
|
161
|
+
raise ValueError("card_uuid is required")
|
162
|
+
cls._GLOBAL_CARD_INFO[kwargs["card_uuid"]] = kwargs
|
163
|
+
|
164
|
+
@classmethod
|
165
|
+
def all_cards_info(cls):
|
166
|
+
return cls._GLOBAL_CARD_INFO.copy()
|
167
|
+
|
134
168
|
def step_init(
|
135
169
|
self, flow, graph, step_name, decorators, environment, flow_datastore, logger
|
136
170
|
):
|
@@ -191,6 +225,11 @@ class CardDecorator(StepDecorator):
|
|
191
225
|
# we need to ensure that a single config file is being referenced for all card create commands.
|
192
226
|
# This config file will be removed when the last card decorator has finished creating its card.
|
193
227
|
self._set_config_file_name(flow)
|
228
|
+
# The MetadataStateManager is used to track the state of the metadata registration.
|
229
|
+
# It is there to ensure that we only register metadata for the card once. This is so that we
|
230
|
+
# avoid any un-necessary metadata writes because the create command can be called multiple times during the
|
231
|
+
# card creation process.
|
232
|
+
self._metadata_state_manager = MetadataStateManager(self.all_cards_info)
|
194
233
|
|
195
234
|
card_type = self.attributes["type"]
|
196
235
|
card_class = get_card_class(card_type)
|
@@ -225,7 +264,12 @@ class CardDecorator(StepDecorator):
|
|
225
264
|
# we need to ensure that `current.card` has `CardComponentCollector` instantiated only once.
|
226
265
|
if not self._is_event_registered("pre-step"):
|
227
266
|
self._register_event("pre-step")
|
228
|
-
self._set_card_creator(
|
267
|
+
self._set_card_creator(
|
268
|
+
CardCreator(
|
269
|
+
self._create_top_level_args(flow),
|
270
|
+
self._metadata_state_manager.register_metadata,
|
271
|
+
)
|
272
|
+
)
|
229
273
|
|
230
274
|
current._update_env(
|
231
275
|
{"card": CardComponentCollector(self._logger, self.card_creator)}
|
@@ -248,6 +292,18 @@ class CardDecorator(StepDecorator):
|
|
248
292
|
)
|
249
293
|
self._card_uuid = card_metadata["uuid"]
|
250
294
|
|
295
|
+
self._register_card_info(
|
296
|
+
card_uuid=self._card_uuid,
|
297
|
+
rank=self.attributes["rank"],
|
298
|
+
type=self.attributes["type"],
|
299
|
+
options=self.card_options,
|
300
|
+
is_editable=self._is_editable,
|
301
|
+
is_runtime_card=self._is_runtime_card,
|
302
|
+
refresh_interval=self.attributes["refresh_interval"],
|
303
|
+
customize=customize,
|
304
|
+
id=self._user_set_card_id,
|
305
|
+
)
|
306
|
+
|
251
307
|
# This means that we are calling `task_pre_step` on the last card decorator.
|
252
308
|
# We can now `finalize` method in the CardComponentCollector object.
|
253
309
|
# This will set up the `current.card` object for usage inside `@step` code.
|
@@ -143,7 +143,10 @@ class TaskToDict:
|
|
143
143
|
obj_type_name = self._get_object_type(data_object)
|
144
144
|
if obj_type_name == "bytes":
|
145
145
|
# Works for python 3.1+
|
146
|
-
|
146
|
+
# Python 3.13 removes the standard ``imghdr`` module. Metaflow
|
147
|
+
# vendors a copy so we can keep using ``what`` to detect image
|
148
|
+
# formats irrespective of the Python version.
|
149
|
+
from metaflow._vendor import imghdr
|
147
150
|
|
148
151
|
resp = imghdr.what(None, h=data_object)
|
149
152
|
# Only accept types supported on the web
|
@@ -157,7 +160,7 @@ class TaskToDict:
|
|
157
160
|
obj_type_name = self._get_object_type(data_object)
|
158
161
|
if obj_type_name == "bytes":
|
159
162
|
# Works for python 3.1+
|
160
|
-
import imghdr
|
163
|
+
from metaflow._vendor import imghdr
|
161
164
|
|
162
165
|
resp = imghdr.what(None, h=data_object)
|
163
166
|
# Only accept types supported on the web
|
@@ -213,3 +213,19 @@ class TestRefreshComponentCard(MetaflowCard):
|
|
213
213
|
if task.finished:
|
214
214
|
return "final"
|
215
215
|
return "runtime-%s" % _component_values_to_hash(data["components"])
|
216
|
+
|
217
|
+
|
218
|
+
class TestImageCard(MetaflowCard):
|
219
|
+
"""Card that renders a tiny PNG using ``TaskToDict.parse_image``."""
|
220
|
+
|
221
|
+
type = "test_image_card"
|
222
|
+
|
223
|
+
def render(self, task):
|
224
|
+
from .convert_to_native_type import TaskToDict
|
225
|
+
import base64
|
226
|
+
|
227
|
+
png_bytes = base64.b64decode(
|
228
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgYGBgAAAABQABRDE8UwAAAABJRU5ErkJggg=="
|
229
|
+
)
|
230
|
+
img_src = TaskToDict().parse_image(png_bytes)
|
231
|
+
return f"<html><img src='{img_src}' /></html>"
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import json
|
2
|
+
from metaflow.metadata_provider import MetaDatum
|
3
|
+
|
4
|
+
|
5
|
+
def _save_metadata(
|
6
|
+
metadata_provider,
|
7
|
+
run_id,
|
8
|
+
step_name,
|
9
|
+
task_id,
|
10
|
+
attempt_id,
|
11
|
+
card_uuid,
|
12
|
+
save_metadata,
|
13
|
+
):
|
14
|
+
entries = [
|
15
|
+
MetaDatum(
|
16
|
+
field=card_uuid,
|
17
|
+
value=json.dumps(save_metadata),
|
18
|
+
type="card-info",
|
19
|
+
tags=["attempt_id:{0}".format(attempt_id)],
|
20
|
+
)
|
21
|
+
]
|
22
|
+
metadata_provider.register_metadata(run_id, step_name, task_id, entries)
|
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)
|