metaflow 2.15.17__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/cli.py +12 -0
- metaflow/cmd/develop/stub_generator.py +19 -2
- metaflow/metaflow_config.py +0 -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_datastore.py +8 -36
- 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/pypi/conda_environment.py +8 -4
- metaflow/plugins/pypi/micromamba.py +9 -1
- 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.17.data → metaflow-2.15.19.data}/data/share/metaflow/devtools/Tiltfile +27 -2
- {metaflow-2.15.17.dist-info → metaflow-2.15.19.dist-info}/METADATA +2 -2
- {metaflow-2.15.17.dist-info → metaflow-2.15.19.dist-info}/RECORD +39 -30
- {metaflow-2.15.17.data → metaflow-2.15.19.data}/data/share/metaflow/devtools/Makefile +0 -0
- {metaflow-2.15.17.data → metaflow-2.15.19.data}/data/share/metaflow/devtools/pick_services.sh +0 -0
- {metaflow-2.15.17.dist-info → metaflow-2.15.19.dist-info}/WHEEL +0 -0
- {metaflow-2.15.17.dist-info → metaflow-2.15.19.dist-info}/entry_points.txt +0 -0
- {metaflow-2.15.17.dist-info → metaflow-2.15.19.dist-info}/licenses/LICENSE +0 -0
- {metaflow-2.15.17.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,
|
@@ -16,7 +16,6 @@ from metaflow.metaflow_config import (
|
|
16
16
|
CARD_SUFFIX,
|
17
17
|
CARD_AZUREROOT,
|
18
18
|
CARD_GSROOT,
|
19
|
-
SKIP_CARD_DUALWRITE,
|
20
19
|
)
|
21
20
|
import metaflow.metaflow_config as metaflow_config
|
22
21
|
|
@@ -231,23 +230,6 @@ class CardDatastore(object):
|
|
231
230
|
|
232
231
|
def save_card(self, uuid, card_type, card_html, card_id=None, overwrite=True):
|
233
232
|
card_file_name = card_type
|
234
|
-
# TEMPORARY_WORKAROUND: FIXME (LATER) : Fix the duplication of below block in a few months.
|
235
|
-
# Check file blame to understand the age of this temporary workaround.
|
236
|
-
|
237
|
-
# This function will end up saving cards at two locations.
|
238
|
-
# Thereby doubling the number of cards. (Which is a temporary fix)
|
239
|
-
# Why do this ? :
|
240
|
-
# When cards were introduced there was an assumption made about task-ids being unique.
|
241
|
-
# This assumption was incorrect.
|
242
|
-
# Only the pathspec needs to be unique but there is no such guarantees about task-ids.
|
243
|
-
# When task-ids are non-unique, card read would result in finding incorrect cards.
|
244
|
-
# This happens because cards were stored based on task-ids.
|
245
|
-
# If we immediately switch from storing based on task-ids to a step-name abstraction folder,
|
246
|
-
# then card reading will crash for many users.
|
247
|
-
# It would especially happen for users who are accessing cards created by a newer
|
248
|
-
# MF client from an older version of MF client.
|
249
|
-
# It will also easily end up breaking the metaflow-ui (which maybe using a client from an older version).
|
250
|
-
# Hence, we are writing cards to both paths so that we can introduce breaking changes later in the future.
|
251
233
|
card_path_with_steps = self.get_card_location(
|
252
234
|
self._get_card_write_path(),
|
253
235
|
card_file_name,
|
@@ -255,24 +237,10 @@ class CardDatastore(object):
|
|
255
237
|
card_id=card_id,
|
256
238
|
suffix=CardNameSuffix.CARD,
|
257
239
|
)
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
)
|
263
|
-
else:
|
264
|
-
card_path_without_steps = self.get_card_location(
|
265
|
-
self._get_card_read_path(with_steps=False),
|
266
|
-
card_file_name,
|
267
|
-
uuid,
|
268
|
-
card_id=card_id,
|
269
|
-
suffix=CardNameSuffix.CARD,
|
270
|
-
)
|
271
|
-
for cp in [card_path_with_steps, card_path_without_steps]:
|
272
|
-
self._backend.save_bytes(
|
273
|
-
[(cp, BytesIO(bytes(card_html, "utf-8")))], overwrite=overwrite
|
274
|
-
)
|
275
|
-
|
240
|
+
self._backend.save_bytes(
|
241
|
+
[(card_path_with_steps, BytesIO(bytes(card_html, "utf-8")))],
|
242
|
+
overwrite=overwrite,
|
243
|
+
)
|
276
244
|
return self.info_from_path(card_path_with_steps, suffix=CardNameSuffix.CARD)
|
277
245
|
|
278
246
|
def _list_card_paths(self, card_type=None, card_hash=None, card_id=None):
|
@@ -283,6 +251,10 @@ class CardDatastore(object):
|
|
283
251
|
)
|
284
252
|
|
285
253
|
if len(card_paths_with_steps) == 0:
|
254
|
+
# The listing logic is reading the cards with steps and without steps
|
255
|
+
# because earlier versions of clients (ones that wrote cards before June 2022),
|
256
|
+
# would have written cards without steps. So as a fallback we will try to check for the
|
257
|
+
# cards without steps.
|
286
258
|
card_paths_without_steps = self._backend.list_content(
|
287
259
|
[self._get_card_read_path(with_steps=False)]
|
288
260
|
)
|
@@ -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
|
+
)
|