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.
Files changed (34) hide show
  1. metaflow/_vendor/imghdr/__init__.py +180 -0
  2. metaflow/cmd/develop/stub_generator.py +19 -2
  3. metaflow/plugins/__init__.py +3 -0
  4. metaflow/plugins/airflow/airflow.py +6 -0
  5. metaflow/plugins/argo/argo_workflows.py +316 -287
  6. metaflow/plugins/argo/exit_hooks.py +209 -0
  7. metaflow/plugins/aws/aws_utils.py +1 -1
  8. metaflow/plugins/aws/step_functions/step_functions.py +6 -0
  9. metaflow/plugins/cards/card_cli.py +20 -1
  10. metaflow/plugins/cards/card_creator.py +24 -1
  11. metaflow/plugins/cards/card_decorator.py +57 -1
  12. metaflow/plugins/cards/card_modules/convert_to_native_type.py +5 -2
  13. metaflow/plugins/cards/card_modules/test_cards.py +16 -0
  14. metaflow/plugins/cards/metadata.py +22 -0
  15. metaflow/plugins/exit_hook/__init__.py +0 -0
  16. metaflow/plugins/exit_hook/exit_hook_decorator.py +46 -0
  17. metaflow/plugins/exit_hook/exit_hook_script.py +52 -0
  18. metaflow/plugins/secrets/__init__.py +3 -0
  19. metaflow/plugins/secrets/secrets_decorator.py +9 -173
  20. metaflow/plugins/secrets/secrets_func.py +60 -0
  21. metaflow/plugins/secrets/secrets_spec.py +101 -0
  22. metaflow/plugins/secrets/utils.py +74 -0
  23. metaflow/runner/metaflow_runner.py +16 -1
  24. metaflow/runtime.py +45 -0
  25. metaflow/version.py +1 -1
  26. {metaflow-2.15.18.data → metaflow-2.15.19.data}/data/share/metaflow/devtools/Tiltfile +27 -2
  27. {metaflow-2.15.18.dist-info → metaflow-2.15.19.dist-info}/METADATA +2 -2
  28. {metaflow-2.15.18.dist-info → metaflow-2.15.19.dist-info}/RECORD +34 -25
  29. {metaflow-2.15.18.data → metaflow-2.15.19.data}/data/share/metaflow/devtools/Makefile +0 -0
  30. {metaflow-2.15.18.data → metaflow-2.15.19.data}/data/share/metaflow/devtools/pick_services.sh +0 -0
  31. {metaflow-2.15.18.dist-info → metaflow-2.15.19.dist-info}/WHEEL +0 -0
  32. {metaflow-2.15.18.dist-info → metaflow-2.15.19.dist-info}/entry_points.txt +0 -0
  33. {metaflow-2.15.18.dist-info → metaflow-2.15.19.dist-info}/licenses/LICENSE +0 -0
  34. {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__(self, top_level_options):
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(CardCreator(self._create_top_level_args(flow)))
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
- import imghdr
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)
@@ -9,3 +9,6 @@ class SecretsProvider(abc.ABC):
9
9
  def get_secret_as_dict(self, secret_id, options={}, role=None) -> Dict[str, str]:
10
10
  """Retrieve the secret from secrets backend, and return a dictionary of
11
11
  environment variables."""
12
+
13
+
14
+ from .secrets_func import get_secrets