metaflow 2.12.33__py2.py3-none-any.whl → 2.12.34__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.
@@ -6,7 +6,18 @@ from tempfile import NamedTemporaryFile
6
6
  import time
7
7
  import metaflow.tracing as tracing
8
8
 
9
- from typing import Any, Callable, Iterable, Iterator, List, Optional
9
+ from typing import (
10
+ Any,
11
+ Callable,
12
+ Iterable,
13
+ Iterator,
14
+ List,
15
+ Optional,
16
+ NoReturn,
17
+ Tuple,
18
+ TypeVar,
19
+ Union,
20
+ )
10
21
 
11
22
  try:
12
23
  # Python 2
@@ -30,7 +41,13 @@ class MulticoreException(Exception):
30
41
  pass
31
42
 
32
43
 
33
- def _spawn(func, arg, dir):
44
+ _A = TypeVar("_A")
45
+ _R = TypeVar("_R")
46
+
47
+
48
+ def _spawn(
49
+ func: Callable[[_A], _R], arg: _A, dir: Optional[str]
50
+ ) -> Union[Tuple[int, str], NoReturn]:
34
51
  with NamedTemporaryFile(prefix="parallel_map_", dir=dir, delete=False) as tmpfile:
35
52
  output_file = tmpfile.name
36
53
 
@@ -63,11 +80,11 @@ def _spawn(func, arg, dir):
63
80
 
64
81
 
65
82
  def parallel_imap_unordered(
66
- func: Callable[[Any], Any],
67
- iterable: Iterable[Any],
83
+ func: Callable[[_A], _R],
84
+ iterable: Iterable[_A],
68
85
  max_parallel: Optional[int] = None,
69
86
  dir: Optional[str] = None,
70
- ) -> Iterator[Any]:
87
+ ) -> Iterator[_R]:
71
88
  """
72
89
  Parallelizes execution of a function using multiprocessing. The result
73
90
  order is not guaranteed.
@@ -79,9 +96,9 @@ def parallel_imap_unordered(
79
96
  iterable : Iterable[Any]
80
97
  Iterable over arguments to pass to fun
81
98
  max_parallel int, optional, default None
82
- Maximum parallelism. If not specified, uses the number of CPUs
99
+ Maximum parallelism. If not specified, it uses the number of CPUs
83
100
  dir : str, optional, default None
84
- If specified, directory where temporary files are created
101
+ If specified, it's the directory where temporary files are created
85
102
 
86
103
  Yields
87
104
  ------
@@ -121,14 +138,14 @@ def parallel_imap_unordered(
121
138
 
122
139
 
123
140
  def parallel_map(
124
- func: Callable[[Any], Any],
125
- iterable: Iterable[Any],
141
+ func: Callable[[_A], _R],
142
+ iterable: Iterable[_A],
126
143
  max_parallel: Optional[int] = None,
127
144
  dir: Optional[str] = None,
128
- ) -> List[Any]:
145
+ ) -> List[_R]:
129
146
  """
130
147
  Parallelizes execution of a function using multiprocessing. The result
131
- order is that of the arguments in `iterable`
148
+ order is that of the arguments in `iterable`.
132
149
 
133
150
  Parameters
134
151
  ----------
@@ -137,9 +154,9 @@ def parallel_map(
137
154
  iterable : Iterable[Any]
138
155
  Iterable over arguments to pass to fun
139
156
  max_parallel int, optional, default None
140
- Maximum parallelism. If not specified, uses the number of CPUs
157
+ Maximum parallelism. If not specified, it uses the number of CPUs
141
158
  dir : str, optional, default None
142
- If specified, directory where temporary files are created
159
+ If specified, it's the directory where temporary files are created
143
160
 
144
161
  Returns
145
162
  -------
@@ -155,4 +172,4 @@ def parallel_map(
155
172
  res = parallel_imap_unordered(
156
173
  wrapper, enumerate(iterable), max_parallel=max_parallel, dir=dir
157
174
  )
158
- return [r for idx, r in sorted(res)]
175
+ return [r for _, r in sorted(res)]
metaflow/parameters.py CHANGED
@@ -151,6 +151,7 @@ class DeployTimeField(object):
151
151
  return self._check_type(val, deploy_time)
152
152
 
153
153
  def _check_type(self, val, deploy_time):
154
+
154
155
  # it is easy to introduce a deploy-time function that accidentally
155
156
  # returns a value whose type is not compatible with what is defined
156
157
  # in Parameter. Let's catch those mistakes early here, instead of
@@ -158,7 +159,7 @@ class DeployTimeField(object):
158
159
 
159
160
  # note: this doesn't work with long in Python2 or types defined as
160
161
  # click types, e.g. click.INT
161
- TYPES = {bool: "bool", int: "int", float: "float", list: "list"}
162
+ TYPES = {bool: "bool", int: "int", float: "float", list: "list", dict: "dict"}
162
163
 
163
164
  msg = (
164
165
  "The value returned by the deploy-time function for "
@@ -166,7 +167,12 @@ class DeployTimeField(object):
166
167
  % (self.parameter_name, self.field)
167
168
  )
168
169
 
169
- if self.parameter_type in TYPES:
170
+ if isinstance(self.parameter_type, list):
171
+ if not any(isinstance(val, x) for x in self.parameter_type):
172
+ msg += "Expected one of the following %s." % TYPES[self.parameter_type]
173
+ raise ParameterFieldTypeMismatch(msg)
174
+ return str(val) if self.return_str else val
175
+ elif self.parameter_type in TYPES:
170
176
  if type(val) != self.parameter_type:
171
177
  msg += "Expected a %s." % TYPES[self.parameter_type]
172
178
  raise ParameterFieldTypeMismatch(msg)
@@ -522,7 +522,9 @@ class ArgoWorkflows(object):
522
522
  params = set(
523
523
  [param.name.lower() for var, param in self.flow._get_parameters()]
524
524
  )
525
- for event in self.flow._flow_decorators.get("trigger")[0].triggers:
525
+ trigger_deco = self.flow._flow_decorators.get("trigger")[0]
526
+ trigger_deco.format_deploytime_value()
527
+ for event in trigger_deco.triggers:
526
528
  parameters = {}
527
529
  # TODO: Add a check to guard against names starting with numerals(?)
528
530
  if not re.match(r"^[A-Za-z0-9_.-]+$", event["name"]):
@@ -562,9 +564,11 @@ class ArgoWorkflows(object):
562
564
 
563
565
  # @trigger_on_finish decorator
564
566
  if self.flow._flow_decorators.get("trigger_on_finish"):
565
- for event in self.flow._flow_decorators.get("trigger_on_finish")[
566
- 0
567
- ].triggers:
567
+ trigger_on_finish_deco = self.flow._flow_decorators.get(
568
+ "trigger_on_finish"
569
+ )[0]
570
+ trigger_on_finish_deco.format_deploytime_value()
571
+ for event in trigger_on_finish_deco.triggers:
568
572
  # Actual filters are deduced here since we don't have access to
569
573
  # the current object in the @trigger_on_finish decorator.
570
574
  triggers.append(
@@ -10,7 +10,7 @@ from metaflow.metaflow_config import KUBERNETES_NAMESPACE
10
10
  from metaflow.plugins.argo.argo_workflows import ArgoWorkflows
11
11
  from metaflow.runner.deployer import Deployer, DeployedFlow, TriggeredRun
12
12
 
13
- from metaflow.runner.utils import get_lower_level_group, handle_timeout
13
+ from metaflow.runner.utils import get_lower_level_group, handle_timeout, temporary_fifo
14
14
 
15
15
 
16
16
  def generate_fake_flow_file_contents(
@@ -341,18 +341,14 @@ class ArgoWorkflowsDeployedFlow(DeployedFlow):
341
341
  Exception
342
342
  If there is an error during the trigger process.
343
343
  """
344
- with tempfile.TemporaryDirectory() as temp_dir:
345
- tfp_runner_attribute = tempfile.NamedTemporaryFile(
346
- dir=temp_dir, delete=False
347
- )
348
-
344
+ with temporary_fifo() as (attribute_file_path, attribute_file_fd):
349
345
  # every subclass needs to have `self.deployer_kwargs`
350
346
  command = get_lower_level_group(
351
347
  self.deployer.api,
352
348
  self.deployer.top_level_kwargs,
353
349
  self.deployer.TYPE,
354
350
  self.deployer.deployer_kwargs,
355
- ).trigger(deployer_attribute_file=tfp_runner_attribute.name, **kwargs)
351
+ ).trigger(deployer_attribute_file=attribute_file_path, **kwargs)
356
352
 
357
353
  pid = self.deployer.spm.run_command(
358
354
  [sys.executable, *command],
@@ -363,7 +359,7 @@ class ArgoWorkflowsDeployedFlow(DeployedFlow):
363
359
 
364
360
  command_obj = self.deployer.spm.get(pid)
365
361
  content = handle_timeout(
366
- tfp_runner_attribute, command_obj, self.deployer.file_read_timeout
362
+ attribute_file_fd, command_obj, self.deployer.file_read_timeout
367
363
  )
368
364
 
369
365
  if command_obj.process.returncode == 0:
@@ -6,7 +6,7 @@ from typing import ClassVar, Optional, List
6
6
  from metaflow.plugins.aws.step_functions.step_functions import StepFunctions
7
7
  from metaflow.runner.deployer import DeployedFlow, TriggeredRun
8
8
 
9
- from metaflow.runner.utils import get_lower_level_group, handle_timeout
9
+ from metaflow.runner.utils import get_lower_level_group, handle_timeout, temporary_fifo
10
10
 
11
11
 
12
12
  class StepFunctionsTriggeredRun(TriggeredRun):
@@ -196,18 +196,14 @@ class StepFunctionsDeployedFlow(DeployedFlow):
196
196
  Exception
197
197
  If there is an error during the trigger process.
198
198
  """
199
- with tempfile.TemporaryDirectory() as temp_dir:
200
- tfp_runner_attribute = tempfile.NamedTemporaryFile(
201
- dir=temp_dir, delete=False
202
- )
203
-
199
+ with temporary_fifo() as (attribute_file_path, attribute_file_fd):
204
200
  # every subclass needs to have `self.deployer_kwargs`
205
201
  command = get_lower_level_group(
206
202
  self.deployer.api,
207
203
  self.deployer.top_level_kwargs,
208
204
  self.deployer.TYPE,
209
205
  self.deployer.deployer_kwargs,
210
- ).trigger(deployer_attribute_file=tfp_runner_attribute.name, **kwargs)
206
+ ).trigger(deployer_attribute_file=attribute_file_path, **kwargs)
211
207
 
212
208
  pid = self.deployer.spm.run_command(
213
209
  [sys.executable, *command],
@@ -218,7 +214,7 @@ class StepFunctionsDeployedFlow(DeployedFlow):
218
214
 
219
215
  command_obj = self.deployer.spm.get(pid)
220
216
  content = handle_timeout(
221
- tfp_runner_attribute, command_obj, self.deployer.file_read_timeout
217
+ attribute_file_fd, command_obj, self.deployer.file_read_timeout
222
218
  )
223
219
 
224
220
  if command_obj.process.returncode == 0:
@@ -600,7 +600,9 @@ class S3(object):
600
600
  # returned are Unicode.
601
601
  key = getattr(key_value, "key", key_value)
602
602
  if self._s3root is None:
603
- parsed = urlparse(to_unicode(key))
603
+ # NOTE: S3 allows fragments as part of object names, e.g. /dataset #1/data.txt
604
+ # Without allow_fragments=False the parsed.path for an object name with fragments is incomplete.
605
+ parsed = urlparse(to_unicode(key), allow_fragments=False)
604
606
  if parsed.scheme == "s3" and parsed.path:
605
607
  return key
606
608
  else:
@@ -765,7 +767,9 @@ class S3(object):
765
767
  """
766
768
 
767
769
  url = self._url(key)
768
- src = urlparse(url)
770
+ # NOTE: S3 allows fragments as part of object names, e.g. /dataset #1/data.txt
771
+ # Without allow_fragments=False the parsed src.path for an object name with fragments is incomplete.
772
+ src = urlparse(url, allow_fragments=False)
769
773
 
770
774
  def _info(s3, tmp):
771
775
  resp = s3.head_object(Bucket=src.netloc, Key=src.path.lstrip('/"'))
@@ -891,7 +895,9 @@ class S3(object):
891
895
  DOWNLOAD_MAX_CHUNK = 2 * 1024 * 1024 * 1024 - 1
892
896
 
893
897
  url, r = self._url_and_range(key)
894
- src = urlparse(url)
898
+ # NOTE: S3 allows fragments as part of object names, e.g. /dataset #1/data.txt
899
+ # Without allow_fragments=False the parsed src.path for an object name with fragments is incomplete.
900
+ src = urlparse(url, allow_fragments=False)
895
901
 
896
902
  def _download(s3, tmp):
897
903
  if r:
@@ -1173,7 +1179,9 @@ class S3(object):
1173
1179
  blob.close = lambda: None
1174
1180
 
1175
1181
  url = self._url(key)
1176
- src = urlparse(url)
1182
+ # NOTE: S3 allows fragments as part of object names, e.g. /dataset #1/data.txt
1183
+ # Without allow_fragments=False the parsed src.path for an object name with fragments is incomplete.
1184
+ src = urlparse(url, allow_fragments=False)
1177
1185
  extra_args = None
1178
1186
  if content_type or metadata or self._encryption:
1179
1187
  extra_args = {}
@@ -1,9 +1,11 @@
1
1
  import re
2
+ import json
2
3
 
3
4
  from metaflow import current
4
5
  from metaflow.decorators import FlowDecorator
5
6
  from metaflow.exception import MetaflowException
6
7
  from metaflow.util import is_stringish
8
+ from metaflow.parameters import DeployTimeField, deploy_time_eval
7
9
 
8
10
  # TODO: Support dynamic parameter mapping through a context object that exposes
9
11
  # flow name and user name similar to parameter context
@@ -68,6 +70,75 @@ class TriggerDecorator(FlowDecorator):
68
70
  "options": {},
69
71
  }
70
72
 
73
+ def process_event_name(self, event):
74
+ if is_stringish(event):
75
+ return {"name": str(event)}
76
+ elif isinstance(event, dict):
77
+ if "name" not in event:
78
+ raise MetaflowException(
79
+ "The *event* attribute for *@trigger* is missing the *name* key."
80
+ )
81
+ if callable(event["name"]) and not isinstance(
82
+ event["name"], DeployTimeField
83
+ ):
84
+ event["name"] = DeployTimeField(
85
+ "event_name", str, None, event["name"], False
86
+ )
87
+ event["parameters"] = self.process_parameters(event.get("parameters", {}))
88
+ return event
89
+ elif callable(event) and not isinstance(event, DeployTimeField):
90
+ return DeployTimeField("event", [str, dict], None, event, False)
91
+ else:
92
+ raise MetaflowException(
93
+ "Incorrect format for *event* attribute in *@trigger* decorator. "
94
+ "Supported formats are string and dictionary - \n"
95
+ "@trigger(event='foo') or @trigger(event={'name': 'foo', "
96
+ "'parameters': {'alpha': 'beta'}})"
97
+ )
98
+
99
+ def process_parameters(self, parameters):
100
+ new_param_values = {}
101
+ if isinstance(parameters, (list, tuple)):
102
+ for mapping in parameters:
103
+ if is_stringish(mapping):
104
+ new_param_values[mapping] = mapping
105
+ elif callable(mapping) and not isinstance(mapping, DeployTimeField):
106
+ mapping = DeployTimeField(
107
+ "parameter_val", str, None, mapping, False
108
+ )
109
+ new_param_values[mapping] = mapping
110
+ elif isinstance(mapping, (list, tuple)) and len(mapping) == 2:
111
+ if callable(mapping[0]) and not isinstance(
112
+ mapping[0], DeployTimeField
113
+ ):
114
+ mapping[0] = DeployTimeField(
115
+ "parameter_val", str, None, mapping[0], False
116
+ )
117
+ if callable(mapping[1]) and not isinstance(
118
+ mapping[1], DeployTimeField
119
+ ):
120
+ mapping[1] = DeployTimeField(
121
+ "parameter_val", str, None, mapping[1], False
122
+ )
123
+ new_param_values[mapping[0]] = mapping[1]
124
+ else:
125
+ raise MetaflowException(
126
+ "The *parameters* attribute for event is invalid. "
127
+ "It should be a list/tuple of strings and lists/tuples of size 2"
128
+ )
129
+ elif callable(parameters) and not isinstance(parameters, DeployTimeField):
130
+ return DeployTimeField(
131
+ "parameters", [list, dict, tuple], None, parameters, False
132
+ )
133
+ elif isinstance(parameters, dict):
134
+ for key, value in parameters.items():
135
+ if callable(key) and not isinstance(key, DeployTimeField):
136
+ key = DeployTimeField("flow_parameter", str, None, key, False)
137
+ if callable(value) and not isinstance(value, DeployTimeField):
138
+ value = DeployTimeField("signal_parameter", str, None, value, False)
139
+ new_param_values[key] = value
140
+ return new_param_values
141
+
71
142
  def flow_init(
72
143
  self,
73
144
  flow_name,
@@ -86,41 +157,9 @@ class TriggerDecorator(FlowDecorator):
86
157
  "attributes in *@trigger* decorator."
87
158
  )
88
159
  elif self.attributes["event"]:
89
- # event attribute supports the following formats -
90
- # 1. event='table.prod_db.members'
91
- # 2. event={'name': 'table.prod_db.members',
92
- # 'parameters': {'alpha': 'member_weight'}}
93
- if is_stringish(self.attributes["event"]):
94
- self.triggers.append({"name": str(self.attributes["event"])})
95
- elif isinstance(self.attributes["event"], dict):
96
- if "name" not in self.attributes["event"]:
97
- raise MetaflowException(
98
- "The *event* attribute for *@trigger* is missing the "
99
- "*name* key."
100
- )
101
- param_value = self.attributes["event"].get("parameters", {})
102
- if isinstance(param_value, (list, tuple)):
103
- new_param_value = {}
104
- for mapping in param_value:
105
- if is_stringish(mapping):
106
- new_param_value[mapping] = mapping
107
- elif isinstance(mapping, (list, tuple)) and len(mapping) == 2:
108
- new_param_value[mapping[0]] = mapping[1]
109
- else:
110
- raise MetaflowException(
111
- "The *parameters* attribute for event '%s' is invalid. "
112
- "It should be a list/tuple of strings and lists/tuples "
113
- "of size 2" % self.attributes["event"]["name"]
114
- )
115
- self.attributes["event"]["parameters"] = new_param_value
116
- self.triggers.append(self.attributes["event"])
117
- else:
118
- raise MetaflowException(
119
- "Incorrect format for *event* attribute in *@trigger* decorator. "
120
- "Supported formats are string and dictionary - \n"
121
- "@trigger(event='foo') or @trigger(event={'name': 'foo', "
122
- "'parameters': {'alpha': 'beta'}})"
123
- )
160
+ event = self.attributes["event"]
161
+ processed_event = self.process_event_name(event)
162
+ self.triggers.append(processed_event)
124
163
  elif self.attributes["events"]:
125
164
  # events attribute supports the following formats -
126
165
  # 1. events=[{'name': 'table.prod_db.members',
@@ -128,43 +167,17 @@ class TriggerDecorator(FlowDecorator):
128
167
  # {'name': 'table.prod_db.metadata',
129
168
  # 'parameters': {'beta': 'grade'}}]
130
169
  if isinstance(self.attributes["events"], list):
170
+ # process every event in events
131
171
  for event in self.attributes["events"]:
132
- if is_stringish(event):
133
- self.triggers.append({"name": str(event)})
134
- elif isinstance(event, dict):
135
- if "name" not in event:
136
- raise MetaflowException(
137
- "One or more events in *events* attribute for "
138
- "*@trigger* are missing the *name* key."
139
- )
140
- param_value = event.get("parameters", {})
141
- if isinstance(param_value, (list, tuple)):
142
- new_param_value = {}
143
- for mapping in param_value:
144
- if is_stringish(mapping):
145
- new_param_value[mapping] = mapping
146
- elif (
147
- isinstance(mapping, (list, tuple))
148
- and len(mapping) == 2
149
- ):
150
- new_param_value[mapping[0]] = mapping[1]
151
- else:
152
- raise MetaflowException(
153
- "The *parameters* attribute for event '%s' is "
154
- "invalid. It should be a list/tuple of strings "
155
- "and lists/tuples of size 2" % event["name"]
156
- )
157
- event["parameters"] = new_param_value
158
- self.triggers.append(event)
159
- else:
160
- raise MetaflowException(
161
- "One or more events in *events* attribute in *@trigger* "
162
- "decorator have an incorrect format. Supported format "
163
- "is dictionary - \n"
164
- "@trigger(events=[{'name': 'foo', 'parameters': {'alpha': "
165
- "'beta'}}, {'name': 'bar', 'parameters': "
166
- "{'gamma': 'kappa'}}])"
167
- )
172
+ processed_event = self.process_event_name(event)
173
+ self.triggers.append(processed_event)
174
+ elif callable(self.attributes["events"]) and not isinstance(
175
+ self.attributes["events"], DeployTimeField
176
+ ):
177
+ trig = DeployTimeField(
178
+ "events", list, None, self.attributes["events"], False
179
+ )
180
+ self.triggers.append(trig)
168
181
  else:
169
182
  raise MetaflowException(
170
183
  "Incorrect format for *events* attribute in *@trigger* decorator. "
@@ -178,7 +191,12 @@ class TriggerDecorator(FlowDecorator):
178
191
  raise MetaflowException("No event(s) specified in *@trigger* decorator.")
179
192
 
180
193
  # same event shouldn't occur more than once
181
- names = [x["name"] for x in self.triggers]
194
+ names = [
195
+ x["name"]
196
+ for x in self.triggers
197
+ if not isinstance(x, DeployTimeField)
198
+ and not isinstance(x["name"], DeployTimeField)
199
+ ]
182
200
  if len(names) != len(set(names)):
183
201
  raise MetaflowException(
184
202
  "Duplicate event names defined in *@trigger* decorator."
@@ -188,6 +206,104 @@ class TriggerDecorator(FlowDecorator):
188
206
 
189
207
  # TODO: Handle scenario for local testing using --trigger.
190
208
 
209
+ def format_deploytime_value(self):
210
+ new_triggers = []
211
+ for trigger in self.triggers:
212
+ # Case where trigger is a function that returns a list of events
213
+ # Need to do this bc we need to iterate over list later
214
+ if isinstance(trigger, DeployTimeField):
215
+ 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
+ if isinstance(evaluated_trigger, list):
221
+ for trig in evaluated_trigger:
222
+ if is_stringish(trig):
223
+ new_triggers.append({"name": trig})
224
+ else: # dict or another deploytimefield
225
+ new_triggers.append(trig)
226
+ else:
227
+ new_triggers.append(trigger)
228
+ else:
229
+ new_triggers.append(trigger)
230
+
231
+ 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
+
191
307
 
192
308
  class TriggerOnFinishDecorator(FlowDecorator):
193
309
  """
@@ -312,6 +428,13 @@ class TriggerOnFinishDecorator(FlowDecorator):
312
428
  "The *project_branch* attribute of the *flow* is not a string"
313
429
  )
314
430
  self.triggers.append(result)
431
+ elif callable(self.attributes["flow"]) and not isinstance(
432
+ self.attributes["flow"], DeployTimeField
433
+ ):
434
+ trig = DeployTimeField(
435
+ "fq_name", [str, dict], None, self.attributes["flow"], False
436
+ )
437
+ self.triggers.append(trig)
315
438
  else:
316
439
  raise MetaflowException(
317
440
  "Incorrect type for *flow* attribute in *@trigger_on_finish* "
@@ -369,6 +492,13 @@ class TriggerOnFinishDecorator(FlowDecorator):
369
492
  "Supported type is string or Dict[str, str]- \n"
370
493
  "@trigger_on_finish(flows=['FooFlow', 'BarFlow']"
371
494
  )
495
+ elif callable(self.attributes["flows"]) and not isinstance(
496
+ self.attributes["flows"], DeployTimeField
497
+ ):
498
+ trig = DeployTimeField(
499
+ "flows", list, None, self.attributes["flows"], False
500
+ )
501
+ self.triggers.append(trig)
372
502
  else:
373
503
  raise MetaflowException(
374
504
  "Incorrect type for *flows* attribute in *@trigger_on_finish* "
@@ -383,6 +513,8 @@ class TriggerOnFinishDecorator(FlowDecorator):
383
513
 
384
514
  # Make triggers @project aware
385
515
  for trigger in self.triggers:
516
+ if isinstance(trigger, DeployTimeField):
517
+ continue
386
518
  if trigger["fq_name"].count(".") == 0:
387
519
  # fully qualified name is just the flow name
388
520
  trigger["flow"] = trigger["fq_name"]
@@ -427,5 +559,54 @@ class TriggerOnFinishDecorator(FlowDecorator):
427
559
  run_objs.append(run_obj)
428
560
  current._update_env({"trigger": Trigger.from_runs(run_objs)})
429
561
 
562
+ def _parse_fq_name(self, trigger):
563
+ if isinstance(trigger, DeployTimeField):
564
+ trigger["fq_name"] = deploy_time_eval(trigger["fq_name"])
565
+ if trigger["fq_name"].count(".") == 0:
566
+ # fully qualified name is just the flow name
567
+ trigger["flow"] = trigger["fq_name"]
568
+ elif trigger["fq_name"].count(".") >= 2:
569
+ # fully qualified name is of the format - project.branch.flow_name
570
+ trigger["project"], tail = trigger["fq_name"].split(".", maxsplit=1)
571
+ trigger["branch"], trigger["flow"] = tail.rsplit(".", maxsplit=1)
572
+ else:
573
+ raise MetaflowException(
574
+ "Incorrect format for *flow* in *@trigger_on_finish* "
575
+ "decorator. Specify either just the *flow_name* or a fully "
576
+ "qualified name like *project_name.branch_name.flow_name*."
577
+ )
578
+ if not re.match(r"^[A-Za-z0-9_]+$", trigger["flow"]):
579
+ raise MetaflowException(
580
+ "Invalid flow name *%s* in *@trigger_on_finish* "
581
+ "decorator. Only alphanumeric characters and "
582
+ "underscores(_) are allowed." % trigger["flow"]
583
+ )
584
+ return trigger
585
+
586
+ def format_deploytime_value(self):
587
+ for trigger in self.triggers:
588
+ # Case were trigger is a function that returns a list
589
+ # Need to do this bc we need to iterate over list and process
590
+ if isinstance(trigger, DeployTimeField):
591
+ deploy_value = deploy_time_eval(trigger)
592
+ if isinstance(deploy_value, list):
593
+ self.triggers = deploy_value
594
+ else:
595
+ break
596
+ for trigger in self.triggers:
597
+ # Entire trigger is a function (returns either string or dict)
598
+ old_trig = trigger
599
+ if isinstance(trigger, DeployTimeField):
600
+ trigger = deploy_time_eval(trigger)
601
+ if isinstance(trigger, dict):
602
+ trigger["fq_name"] = trigger.get("name")
603
+ trigger["project"] = trigger.get("project")
604
+ trigger["branch"] = trigger.get("project_branch")
605
+ # We also added this bc it won't be formatted yet
606
+ if isinstance(trigger, str):
607
+ trigger = {"fq_name": trigger}
608
+ trigger = self._parse_fq_name(trigger)
609
+ self.triggers[self.triggers.index(old_trig)] = trigger
610
+
430
611
  def get_top_level_options(self):
431
612
  return list(self._option_values.items())