ob-metaflow 2.12.30.2__py2.py3-none-any.whl → 2.13.6.1__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.

Potentially problematic release.


This version of ob-metaflow might be problematic. Click here for more details.

Files changed (96) hide show
  1. metaflow/__init__.py +3 -0
  2. metaflow/cards.py +1 -0
  3. metaflow/cli.py +185 -717
  4. metaflow/cli_args.py +17 -0
  5. metaflow/cli_components/__init__.py +0 -0
  6. metaflow/cli_components/dump_cmd.py +96 -0
  7. metaflow/cli_components/init_cmd.py +51 -0
  8. metaflow/cli_components/run_cmds.py +362 -0
  9. metaflow/cli_components/step_cmd.py +176 -0
  10. metaflow/cli_components/utils.py +140 -0
  11. metaflow/cmd/develop/stub_generator.py +9 -2
  12. metaflow/datastore/flow_datastore.py +2 -2
  13. metaflow/decorators.py +63 -2
  14. metaflow/exception.py +8 -2
  15. metaflow/extension_support/plugins.py +42 -27
  16. metaflow/flowspec.py +176 -23
  17. metaflow/graph.py +28 -27
  18. metaflow/includefile.py +50 -22
  19. metaflow/lint.py +35 -20
  20. metaflow/metadata_provider/heartbeat.py +23 -8
  21. metaflow/metaflow_config.py +10 -1
  22. metaflow/multicore_utils.py +31 -14
  23. metaflow/package.py +17 -3
  24. metaflow/parameters.py +97 -25
  25. metaflow/plugins/__init__.py +22 -0
  26. metaflow/plugins/airflow/airflow.py +18 -17
  27. metaflow/plugins/airflow/airflow_cli.py +1 -0
  28. metaflow/plugins/argo/argo_client.py +0 -2
  29. metaflow/plugins/argo/argo_workflows.py +195 -132
  30. metaflow/plugins/argo/argo_workflows_cli.py +1 -1
  31. metaflow/plugins/argo/argo_workflows_decorator.py +2 -4
  32. metaflow/plugins/argo/argo_workflows_deployer_objects.py +51 -9
  33. metaflow/plugins/argo/jobset_input_paths.py +0 -1
  34. metaflow/plugins/aws/aws_utils.py +6 -1
  35. metaflow/plugins/aws/batch/batch_client.py +1 -3
  36. metaflow/plugins/aws/batch/batch_decorator.py +13 -13
  37. metaflow/plugins/aws/secrets_manager/aws_secrets_manager_secrets_provider.py +13 -10
  38. metaflow/plugins/aws/step_functions/dynamo_db_client.py +0 -3
  39. metaflow/plugins/aws/step_functions/production_token.py +1 -1
  40. metaflow/plugins/aws/step_functions/step_functions.py +33 -1
  41. metaflow/plugins/aws/step_functions/step_functions_cli.py +1 -1
  42. metaflow/plugins/aws/step_functions/step_functions_decorator.py +0 -1
  43. metaflow/plugins/aws/step_functions/step_functions_deployer_objects.py +7 -9
  44. metaflow/plugins/cards/card_cli.py +7 -2
  45. metaflow/plugins/cards/card_creator.py +1 -0
  46. metaflow/plugins/cards/card_decorator.py +79 -8
  47. metaflow/plugins/cards/card_modules/basic.py +56 -5
  48. metaflow/plugins/cards/card_modules/card.py +16 -1
  49. metaflow/plugins/cards/card_modules/components.py +64 -16
  50. metaflow/plugins/cards/card_modules/main.js +27 -25
  51. metaflow/plugins/cards/card_modules/test_cards.py +4 -4
  52. metaflow/plugins/cards/component_serializer.py +1 -1
  53. metaflow/plugins/datatools/s3/s3.py +12 -4
  54. metaflow/plugins/datatools/s3/s3op.py +3 -3
  55. metaflow/plugins/events_decorator.py +338 -186
  56. metaflow/plugins/kubernetes/kube_utils.py +84 -1
  57. metaflow/plugins/kubernetes/kubernetes.py +40 -92
  58. metaflow/plugins/kubernetes/kubernetes_cli.py +32 -7
  59. metaflow/plugins/kubernetes/kubernetes_decorator.py +76 -4
  60. metaflow/plugins/kubernetes/kubernetes_job.py +23 -20
  61. metaflow/plugins/kubernetes/kubernetes_jobsets.py +41 -20
  62. metaflow/plugins/kubernetes/spot_metadata_cli.py +69 -0
  63. metaflow/plugins/kubernetes/spot_monitor_sidecar.py +109 -0
  64. metaflow/plugins/parallel_decorator.py +4 -1
  65. metaflow/plugins/project_decorator.py +33 -5
  66. metaflow/plugins/pypi/bootstrap.py +249 -81
  67. metaflow/plugins/pypi/conda_decorator.py +20 -10
  68. metaflow/plugins/pypi/conda_environment.py +83 -27
  69. metaflow/plugins/pypi/micromamba.py +82 -37
  70. metaflow/plugins/pypi/pip.py +9 -6
  71. metaflow/plugins/pypi/pypi_decorator.py +11 -9
  72. metaflow/plugins/pypi/utils.py +4 -2
  73. metaflow/plugins/timeout_decorator.py +2 -2
  74. metaflow/runner/click_api.py +240 -50
  75. metaflow/runner/deployer.py +1 -1
  76. metaflow/runner/deployer_impl.py +12 -11
  77. metaflow/runner/metaflow_runner.py +68 -34
  78. metaflow/runner/nbdeploy.py +2 -0
  79. metaflow/runner/nbrun.py +1 -1
  80. metaflow/runner/subprocess_manager.py +61 -10
  81. metaflow/runner/utils.py +208 -44
  82. metaflow/runtime.py +216 -112
  83. metaflow/sidecar/sidecar_worker.py +1 -1
  84. metaflow/tracing/tracing_modules.py +4 -1
  85. metaflow/user_configs/__init__.py +0 -0
  86. metaflow/user_configs/config_decorators.py +563 -0
  87. metaflow/user_configs/config_options.py +548 -0
  88. metaflow/user_configs/config_parameters.py +436 -0
  89. metaflow/util.py +22 -0
  90. metaflow/version.py +1 -1
  91. {ob_metaflow-2.12.30.2.dist-info → ob_metaflow-2.13.6.1.dist-info}/METADATA +12 -3
  92. {ob_metaflow-2.12.30.2.dist-info → ob_metaflow-2.13.6.1.dist-info}/RECORD +96 -84
  93. {ob_metaflow-2.12.30.2.dist-info → ob_metaflow-2.13.6.1.dist-info}/WHEEL +1 -1
  94. {ob_metaflow-2.12.30.2.dist-info → ob_metaflow-2.13.6.1.dist-info}/LICENSE +0 -0
  95. {ob_metaflow-2.12.30.2.dist-info → ob_metaflow-2.13.6.1.dist-info}/entry_points.txt +0 -0
  96. {ob_metaflow-2.12.30.2.dist-info → ob_metaflow-2.13.6.1.dist-info}/top_level.txt +0 -0
@@ -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
  """
@@ -246,11 +362,7 @@ class TriggerOnFinishDecorator(FlowDecorator):
246
362
  """
247
363
 
248
364
  name = "trigger_on_finish"
249
- defaults = {
250
- "flow": None, # flow_name or project_flow_name
251
- "flows": [], # flow_names or project_flow_names
252
- "options": {},
253
- }
365
+
254
366
  options = {
255
367
  "trigger": dict(
256
368
  multiple=True,
@@ -258,6 +370,14 @@ class TriggerOnFinishDecorator(FlowDecorator):
258
370
  help="Specify run pathspec for testing @trigger_on_finish locally.",
259
371
  ),
260
372
  }
373
+ defaults = {
374
+ "flow": None, # flow_name or project_flow_name
375
+ "flows": [], # flow_names or project_flow_names
376
+ "options": {},
377
+ # Re-enable if you want to support TL options directly in the decorator like
378
+ # for @project decorator
379
+ # **{k: v["default"] for k, v in options.items()},
380
+ }
261
381
 
262
382
  def flow_init(
263
383
  self,
@@ -278,97 +398,23 @@ class TriggerOnFinishDecorator(FlowDecorator):
278
398
  )
279
399
  elif self.attributes["flow"]:
280
400
  # flow supports the format @trigger_on_finish(flow='FooFlow')
281
- if is_stringish(self.attributes["flow"]):
282
- self.triggers.append(
283
- {
284
- "fq_name": self.attributes["flow"],
285
- }
286
- )
287
- elif isinstance(self.attributes["flow"], dict):
288
- if "name" not in self.attributes["flow"]:
289
- raise MetaflowException(
290
- "The *flow* attribute for *@trigger_on_finish* is missing the "
291
- "*name* key."
292
- )
293
- flow_name = self.attributes["flow"]["name"]
294
-
295
- if not is_stringish(flow_name) or "." in flow_name:
296
- raise MetaflowException(
297
- "The *name* attribute of the *flow* is not a valid string"
298
- )
299
- result = {"fq_name": flow_name}
300
- if "project" in self.attributes["flow"]:
301
- if is_stringish(self.attributes["flow"]["project"]):
302
- result["project"] = self.attributes["flow"]["project"]
303
- else:
304
- raise MetaflowException(
305
- "The *project* attribute of the *flow* is not a string"
306
- )
307
- if "project_branch" in self.attributes["flow"]:
308
- if is_stringish(self.attributes["flow"]["project_branch"]):
309
- result["branch"] = self.attributes["flow"]["project_branch"]
310
- else:
311
- raise MetaflowException(
312
- "The *project_branch* attribute of the *flow* is not a string"
313
- )
314
- self.triggers.append(result)
401
+ flow = self.attributes["flow"]
402
+ if callable(flow) and not isinstance(
403
+ self.attributes["flow"], DeployTimeField
404
+ ):
405
+ trig = DeployTimeField("fq_name", [str, dict], None, flow, False)
406
+ self.triggers.append(trig)
315
407
  else:
316
- raise MetaflowException(
317
- "Incorrect type for *flow* attribute in *@trigger_on_finish* "
318
- " decorator. Supported type is string or Dict[str, str] - \n"
319
- "@trigger_on_finish(flow='FooFlow') or "
320
- "@trigger_on_finish(flow={'name':'FooFlow', 'project_branch': 'branch'})"
321
- )
408
+ self.triggers.extend(self._parse_static_triggers([flow]))
322
409
  elif self.attributes["flows"]:
323
410
  # flows attribute supports the following formats -
324
411
  # 1. flows=['FooFlow', 'BarFlow']
325
- if isinstance(self.attributes["flows"], list):
326
- for flow in self.attributes["flows"]:
327
- if is_stringish(flow):
328
- self.triggers.append(
329
- {
330
- "fq_name": flow,
331
- }
332
- )
333
- elif isinstance(flow, dict):
334
- if "name" not in flow:
335
- raise MetaflowException(
336
- "One or more flows in the *flows* attribute for "
337
- "*@trigger_on_finish* is missing the "
338
- "*name* key."
339
- )
340
- flow_name = flow["name"]
341
-
342
- if not is_stringish(flow_name) or "." in flow_name:
343
- raise MetaflowException(
344
- "The *name* attribute '%s' is not a valid string"
345
- % str(flow_name)
346
- )
347
- result = {"fq_name": flow_name}
348
- if "project" in flow:
349
- if is_stringish(flow["project"]):
350
- result["project"] = flow["project"]
351
- else:
352
- raise MetaflowException(
353
- "The *project* attribute of the *flow* '%s' is not "
354
- "a string" % flow_name
355
- )
356
- if "project_branch" in flow:
357
- if is_stringish(flow["project_branch"]):
358
- result["branch"] = flow["project_branch"]
359
- else:
360
- raise MetaflowException(
361
- "The *project_branch* attribute of the *flow* %s "
362
- "is not a string" % flow_name
363
- )
364
- self.triggers.append(result)
365
- else:
366
- raise MetaflowException(
367
- "One or more flows in *flows* attribute in "
368
- "*@trigger_on_finish* decorator have an incorrect type. "
369
- "Supported type is string or Dict[str, str]- \n"
370
- "@trigger_on_finish(flows=['FooFlow', 'BarFlow']"
371
- )
412
+ flows = self.attributes["flows"]
413
+ if callable(flows) and not isinstance(flows, DeployTimeField):
414
+ trig = DeployTimeField("flows", list, None, flows, False)
415
+ self.triggers.append(trig)
416
+ elif isinstance(flows, list):
417
+ self.triggers.extend(self._parse_static_triggers(flows))
372
418
  else:
373
419
  raise MetaflowException(
374
420
  "Incorrect type for *flows* attribute in *@trigger_on_finish* "
@@ -383,37 +429,50 @@ class TriggerOnFinishDecorator(FlowDecorator):
383
429
 
384
430
  # Make triggers @project aware
385
431
  for trigger in self.triggers:
386
- if trigger["fq_name"].count(".") == 0:
387
- # fully qualified name is just the flow name
388
- trigger["flow"] = trigger["fq_name"]
389
- elif trigger["fq_name"].count(".") >= 2:
390
- # fully qualified name is of the format - project.branch.flow_name
391
- trigger["project"], tail = trigger["fq_name"].split(".", maxsplit=1)
392
- trigger["branch"], trigger["flow"] = tail.rsplit(".", maxsplit=1)
393
- else:
394
- raise MetaflowException(
395
- "Incorrect format for *flow* in *@trigger_on_finish* "
396
- "decorator. Specify either just the *flow_name* or a fully "
397
- "qualified name like *project_name.branch_name.flow_name*."
398
- )
399
- # TODO: Also sanity check project and branch names
400
- if not re.match(r"^[A-Za-z0-9_]+$", trigger["flow"]):
401
- raise MetaflowException(
402
- "Invalid flow name *%s* in *@trigger_on_finish* "
403
- "decorator. Only alphanumeric characters and "
404
- "underscores(_) are allowed." % trigger["flow"]
405
- )
432
+ if isinstance(trigger, DeployTimeField):
433
+ continue
434
+ self._parse_fq_name(trigger)
406
435
 
407
436
  self.options = self.attributes["options"]
408
437
 
409
438
  # Handle scenario for local testing using --trigger.
439
+
440
+ # Re-enable this code if you want to support passing trigger directly in the
441
+ # decorator in a way similar to how production and branch are passed in the
442
+ # project decorator.
443
+
444
+ # # This is overkill since default is None for all options but adding this code
445
+ # # to make it safe if other non None-default options are added in the future.
446
+ # for op in options:
447
+ # if (
448
+ # op in self._user_defined_attributes
449
+ # and options[op] != self.defaults[op]
450
+ # and self.attributes[op] != options[op]
451
+ # ):
452
+ # # Exception if:
453
+ # # - the user provides a value in the attributes field
454
+ # # - AND the user provided a value in the command line (non default)
455
+ # # - AND the values are different
456
+ # # Note that this won't raise an error if the user provided the default
457
+ # # value in the command line and provided one in attribute but although
458
+ # # slightly inconsistent, it is not incorrect.
459
+ # raise MetaflowException(
460
+ # "You cannot pass %s as both a command-line argument and an attribute "
461
+ # "of the @trigger_on_finish decorator." % op
462
+ # )
463
+
464
+ # if "trigger" in self._user_defined_attributes:
465
+ # trigger_option = self.attributes["trigger"]
466
+ # else:
467
+ trigger_option = options["trigger"]
468
+
410
469
  self._option_values = options
411
- if options["trigger"]:
470
+ if trigger_option:
412
471
  from metaflow import Run
413
472
  from metaflow.events import Trigger
414
473
 
415
474
  run_objs = []
416
- for run_pathspec in options["trigger"]:
475
+ for run_pathspec in trigger_option:
417
476
  if len(run_pathspec.split("/")) != 2:
418
477
  raise MetaflowException(
419
478
  "Incorrect format for run pathspec for *--trigger*. "
@@ -427,5 +486,98 @@ class TriggerOnFinishDecorator(FlowDecorator):
427
486
  run_objs.append(run_obj)
428
487
  current._update_env({"trigger": Trigger.from_runs(run_objs)})
429
488
 
489
+ @staticmethod
490
+ def _parse_static_triggers(flows):
491
+ results = []
492
+ for flow in flows:
493
+ if is_stringish(flow):
494
+ results.append(
495
+ {
496
+ "fq_name": flow,
497
+ }
498
+ )
499
+ elif isinstance(flow, dict):
500
+ if "name" not in flow:
501
+ if len(flows) > 1:
502
+ raise MetaflowException(
503
+ "One or more flows in the *flows* attribute for "
504
+ "*@trigger_on_finish* is missing the "
505
+ "*name* key."
506
+ )
507
+ raise MetaflowException(
508
+ "The *flow* attribute for *@trigger_on_finish* is missing the "
509
+ "*name* key."
510
+ )
511
+ flow_name = flow["name"]
512
+
513
+ if not is_stringish(flow_name) or "." in flow_name:
514
+ raise MetaflowException(
515
+ f"The *name* attribute of the *flow* {flow_name} is not a valid string"
516
+ )
517
+ result = {"fq_name": flow_name}
518
+ if "project" in flow:
519
+ if is_stringish(flow["project"]):
520
+ result["project"] = flow["project"]
521
+ else:
522
+ raise MetaflowException(
523
+ f"The *project* attribute of the *flow* {flow_name} is not a string"
524
+ )
525
+ if "project_branch" in flow:
526
+ if is_stringish(flow["project_branch"]):
527
+ result["branch"] = flow["project_branch"]
528
+ else:
529
+ raise MetaflowException(
530
+ f"The *project_branch* attribute of the *flow* {flow_name} is not a string"
531
+ )
532
+ results.append(result)
533
+ else:
534
+ if len(flows) > 1:
535
+ raise MetaflowException(
536
+ "One or more flows in the *flows* attribute for "
537
+ "*@trigger_on_finish* decorator have an incorrect type. "
538
+ "Supported type is string or Dict[str, str]- \n"
539
+ "@trigger_on_finish(flows=['FooFlow', 'BarFlow']"
540
+ )
541
+ raise MetaflowException(
542
+ "Incorrect type for *flow* attribute in *@trigger_on_finish* "
543
+ " decorator. Supported type is string or Dict[str, str] - \n"
544
+ "@trigger_on_finish(flow='FooFlow') or "
545
+ "@trigger_on_finish(flow={'name':'FooFlow', 'project_branch': 'branch'})"
546
+ )
547
+ return results
548
+
549
+ def _parse_fq_name(self, trigger):
550
+ if trigger["fq_name"].count(".") == 0:
551
+ # fully qualified name is just the flow name
552
+ trigger["flow"] = trigger["fq_name"]
553
+ elif trigger["fq_name"].count(".") >= 2:
554
+ # fully qualified name is of the format - project.branch.flow_name
555
+ trigger["project"], tail = trigger["fq_name"].split(".", maxsplit=1)
556
+ trigger["branch"], trigger["flow"] = tail.rsplit(".", maxsplit=1)
557
+ else:
558
+ raise MetaflowException(
559
+ "Incorrect format for *flow* in *@trigger_on_finish* "
560
+ "decorator. Specify either just the *flow_name* or a fully "
561
+ "qualified name like *project_name.branch_name.flow_name*."
562
+ )
563
+ if not re.match(r"^[A-Za-z0-9_]+$", trigger["flow"]):
564
+ raise MetaflowException(
565
+ "Invalid flow name *%s* in *@trigger_on_finish* "
566
+ "decorator. Only alphanumeric characters and "
567
+ "underscores(_) are allowed." % trigger["flow"]
568
+ )
569
+
570
+ def format_deploytime_value(self):
571
+ if len(self.triggers) == 1 and isinstance(self.triggers[0], DeployTimeField):
572
+ deploy_value = deploy_time_eval(self.triggers[0])
573
+ if isinstance(deploy_value, list):
574
+ self.triggers = deploy_value
575
+ else:
576
+ self.triggers = [deploy_value]
577
+ triggers = self._parse_static_triggers(self.triggers)
578
+ for trigger in triggers:
579
+ self._parse_fq_name(trigger)
580
+ self.triggers = triggers
581
+
430
582
  def get_top_level_options(self):
431
583
  return list(self._option_values.items())