metaflow 2.15.18__py2.py3-none-any.whl → 2.15.20__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 +49 -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.20.data}/data/share/metaflow/devtools/Tiltfile +27 -2
  27. {metaflow-2.15.18.dist-info → metaflow-2.15.20.dist-info}/METADATA +2 -2
  28. {metaflow-2.15.18.dist-info → metaflow-2.15.20.dist-info}/RECORD +34 -25
  29. {metaflow-2.15.18.data → metaflow-2.15.20.data}/data/share/metaflow/devtools/Makefile +0 -0
  30. {metaflow-2.15.18.data → metaflow-2.15.20.data}/data/share/metaflow/devtools/pick_services.sh +0 -0
  31. {metaflow-2.15.18.dist-info → metaflow-2.15.20.dist-info}/WHEEL +0 -0
  32. {metaflow-2.15.18.dist-info → metaflow-2.15.20.dist-info}/entry_points.txt +0 -0
  33. {metaflow-2.15.18.dist-info → metaflow-2.15.20.dist-info}/licenses/LICENSE +0 -0
  34. {metaflow-2.15.18.dist-info → metaflow-2.15.20.dist-info}/top_level.txt +0 -0
@@ -65,6 +65,7 @@ from metaflow.util import (
65
65
  )
66
66
 
67
67
  from .argo_client import ArgoClient
68
+ from .exit_hooks import ExitHookHack, HttpExitHook, ContainerHook
68
69
  from metaflow.util import resolve_identity
69
70
 
70
71
 
@@ -795,6 +796,7 @@ class ArgoWorkflows(object):
795
796
 
796
797
  dag_annotation = {"metaflow/dag": json.dumps(graph_info)}
797
798
 
799
+ lifecycle_hooks = self._lifecycle_hooks()
798
800
  return (
799
801
  WorkflowTemplate()
800
802
  .metadata(
@@ -903,97 +905,20 @@ class ArgoWorkflows(object):
903
905
  if self.enable_error_msg_capture
904
906
  else None
905
907
  )
906
- # Set exit hook handlers if notifications are enabled
908
+ # Set lifecycle hooks if notifications are enabled
907
909
  .hooks(
908
910
  {
909
- **(
910
- {
911
- # workflow status maps to Completed
912
- "notify-slack-on-success": LifecycleHook()
913
- .expression("workflow.status == 'Succeeded'")
914
- .template("notify-slack-on-success"),
915
- }
916
- if self.notify_on_success and self.notify_slack_webhook_url
917
- else {}
918
- ),
919
- **(
920
- {
921
- # workflow status maps to Completed
922
- "notify-pager-duty-on-success": LifecycleHook()
923
- .expression("workflow.status == 'Succeeded'")
924
- .template("notify-pager-duty-on-success"),
925
- }
926
- if self.notify_on_success
927
- and self.notify_pager_duty_integration_key
928
- else {}
929
- ),
930
- **(
931
- {
932
- # workflow status maps to Completed
933
- "notify-incident-io-on-success": LifecycleHook()
934
- .expression("workflow.status == 'Succeeded'")
935
- .template("notify-incident-io-on-success"),
936
- }
937
- if self.notify_on_success
938
- and self.notify_incident_io_api_key
939
- else {}
940
- ),
941
- **(
942
- {
943
- # workflow status maps to Failed or Error
944
- "notify-slack-on-failure": LifecycleHook()
945
- .expression("workflow.status == 'Failed'")
946
- .template("notify-slack-on-error"),
947
- "notify-slack-on-error": LifecycleHook()
948
- .expression("workflow.status == 'Error'")
949
- .template("notify-slack-on-error"),
950
- }
951
- if self.notify_on_error and self.notify_slack_webhook_url
952
- else {}
953
- ),
954
- **(
955
- {
956
- # workflow status maps to Failed or Error
957
- "notify-pager-duty-on-failure": LifecycleHook()
958
- .expression("workflow.status == 'Failed'")
959
- .template("notify-pager-duty-on-error"),
960
- "notify-pager-duty-on-error": LifecycleHook()
961
- .expression("workflow.status == 'Error'")
962
- .template("notify-pager-duty-on-error"),
963
- }
964
- if self.notify_on_error
965
- and self.notify_pager_duty_integration_key
966
- else {}
967
- ),
968
- **(
969
- {
970
- # workflow status maps to Failed or Error
971
- "notify-incident-io-on-failure": LifecycleHook()
972
- .expression("workflow.status == 'Failed'")
973
- .template("notify-incident-io-on-error"),
974
- "notify-incident-io-on-error": LifecycleHook()
975
- .expression("workflow.status == 'Error'")
976
- .template("notify-incident-io-on-error"),
977
- }
978
- if self.notify_on_error and self.notify_incident_io_api_key
979
- else {}
980
- ),
981
- # Warning: terrible hack to workaround a bug in Argo Workflow
982
- # where the hooks listed above do not execute unless
983
- # there is an explicit exit hook. as and when this
984
- # bug is patched, we should remove this effectively
985
- # no-op hook.
986
- **(
987
- {"exit": LifecycleHook().template("exit-hook-hack")}
988
- if self.notify_on_error or self.notify_on_success
989
- else {}
990
- ),
911
+ lifecycle.name: lifecycle
912
+ for hook in lifecycle_hooks
913
+ for lifecycle in hook.lifecycle_hooks
991
914
  }
992
915
  )
993
916
  # Top-level DAG template(s)
994
917
  .templates(self._dag_templates())
995
918
  # Container templates
996
919
  .templates(self._container_templates())
920
+ # Lifecycle hook template(s)
921
+ .templates([hook.template for hook in lifecycle_hooks])
997
922
  # Exit hook template(s)
998
923
  .templates(self._exit_hook_templates())
999
924
  # Sidecar templates (Daemon Containers)
@@ -2336,40 +2261,186 @@ class ArgoWorkflows(object):
2336
2261
  templates.append(self._heartbeat_daemon_template())
2337
2262
  return templates
2338
2263
 
2339
- # Return exit hook templates for workflow execution notifications.
2340
- def _exit_hook_templates(self):
2341
- templates = []
2264
+ # Return lifecycle hooks for workflow execution notifications.
2265
+ def _lifecycle_hooks(self):
2266
+ hooks = []
2342
2267
  if self.notify_on_error:
2343
- templates.append(self._slack_error_template())
2344
- templates.append(self._pager_duty_alert_template())
2345
- templates.append(self._incident_io_alert_template())
2268
+ hooks.append(self._slack_error_template())
2269
+ hooks.append(self._pager_duty_alert_template())
2270
+ hooks.append(self._incident_io_alert_template())
2346
2271
  if self.notify_on_success:
2347
- templates.append(self._slack_success_template())
2348
- templates.append(self._pager_duty_change_template())
2349
- templates.append(self._incident_io_change_template())
2272
+ hooks.append(self._slack_success_template())
2273
+ hooks.append(self._pager_duty_change_template())
2274
+ hooks.append(self._incident_io_change_template())
2275
+
2276
+ exit_hook_decos = self.flow._flow_decorators.get("exit_hook", [])
2277
+
2278
+ for deco in exit_hook_decos:
2279
+ hooks.extend(self._lifecycle_hook_from_deco(deco))
2350
2280
 
2351
2281
  # Clean up None values from templates.
2352
- templates = list(filter(None, templates))
2353
-
2354
- if self.notify_on_error or self.notify_on_success:
2355
- # Warning: terrible hack to workaround a bug in Argo Workflow where the
2356
- # templates listed above do not execute unless there is an
2357
- # explicit exit hook. as and when this bug is patched, we should
2358
- # remove this effectively no-op template.
2359
- # Note: We use the Http template because changing this to an actual no-op container had the side-effect of
2360
- # leaving LifecycleHooks in a pending state even when they have finished execution.
2361
- templates.append(
2362
- Template("exit-hook-hack").http(
2363
- Http("GET")
2364
- .url(
2282
+ hooks = list(filter(None, hooks))
2283
+
2284
+ if hooks:
2285
+ hooks.append(
2286
+ ExitHookHack(
2287
+ url=(
2365
2288
  self.notify_slack_webhook_url
2366
2289
  or "https://events.pagerduty.com/v2/enqueue"
2367
2290
  )
2368
- .success_condition("true == true")
2369
2291
  )
2370
2292
  )
2293
+ return hooks
2294
+
2295
+ def _lifecycle_hook_from_deco(self, deco):
2296
+ from kubernetes import client as kubernetes_sdk
2297
+
2298
+ start_step = [step for step in self.graph if step.name == "start"][0]
2299
+ # We want to grab the base image used by the start step, as this is known to be pullable from within the cluster,
2300
+ # and it might contain the required libraries, allowing us to start up faster.
2301
+ resources = dict(
2302
+ [deco for deco in start_step.decorators if deco.name == "kubernetes"][
2303
+ 0
2304
+ ].attributes
2305
+ )
2306
+
2307
+ run_id_template = "argo-{{workflow.name}}"
2308
+ metaflow_version = self.environment.get_environment_info()
2309
+ metaflow_version["flow_name"] = self.graph.name
2310
+ metaflow_version["production_token"] = self.production_token
2311
+ env = {
2312
+ # These values are needed by Metaflow to set it's internal
2313
+ # state appropriately.
2314
+ "METAFLOW_CODE_URL": self.code_package_url,
2315
+ "METAFLOW_CODE_SHA": self.code_package_sha,
2316
+ "METAFLOW_CODE_DS": self.flow_datastore.TYPE,
2317
+ "METAFLOW_SERVICE_URL": SERVICE_INTERNAL_URL,
2318
+ "METAFLOW_SERVICE_HEADERS": json.dumps(SERVICE_HEADERS),
2319
+ "METAFLOW_USER": "argo-workflows",
2320
+ "METAFLOW_DEFAULT_DATASTORE": self.flow_datastore.TYPE,
2321
+ "METAFLOW_DEFAULT_METADATA": DEFAULT_METADATA,
2322
+ "METAFLOW_OWNER": self.username,
2323
+ }
2324
+ # pass on the Run pathspec for script
2325
+ env["RUN_PATHSPEC"] = f"{self.graph.name}/{run_id_template}"
2326
+
2327
+ # support Metaflow sandboxes
2328
+ env["METAFLOW_INIT_SCRIPT"] = KUBERNETES_SANDBOX_INIT_SCRIPT
2329
+
2330
+ env["METAFLOW_WORKFLOW_NAME"] = "{{workflow.name}}"
2331
+ env["METAFLOW_WORKFLOW_NAMESPACE"] = "{{workflow.namespace}}"
2332
+ env = {
2333
+ k: v
2334
+ for k, v in env.items()
2335
+ if v is not None
2336
+ and k not in set(ARGO_WORKFLOWS_ENV_VARS_TO_SKIP.split(","))
2337
+ }
2338
+
2339
+ def _cmd(fn_name):
2340
+ mflog_expr = export_mflog_env_vars(
2341
+ datastore_type=self.flow_datastore.TYPE,
2342
+ stdout_path="$PWD/.logs/mflog_stdout",
2343
+ stderr_path="$PWD/.logs/mflog_stderr",
2344
+ flow_name=self.flow.name,
2345
+ run_id=run_id_template,
2346
+ step_name=f"_hook_{fn_name}",
2347
+ task_id="1",
2348
+ retry_count="0",
2349
+ )
2350
+ cmds = " && ".join(
2351
+ [
2352
+ # For supporting sandboxes, ensure that a custom script is executed
2353
+ # before anything else is executed. The script is passed in as an
2354
+ # env var.
2355
+ '${METAFLOW_INIT_SCRIPT:+eval \\"${METAFLOW_INIT_SCRIPT}\\"}',
2356
+ "mkdir -p $PWD/.logs",
2357
+ mflog_expr,
2358
+ ]
2359
+ + self.environment.get_package_commands(
2360
+ self.code_package_url, self.flow_datastore.TYPE
2361
+ )[:-1]
2362
+ # Replace the line 'Task in starting'
2363
+ + [f"mflog 'Lifecycle hook {fn_name} is starting.'"]
2364
+ + [
2365
+ f"python -m metaflow.plugins.exit_hook.exit_hook_script {metaflow_version['script']} {fn_name} $RUN_PATHSPEC"
2366
+ ]
2367
+ )
2368
+
2369
+ cmds = shlex.split('bash -c "%s"' % cmds)
2370
+ return cmds
2371
+
2372
+ def _container(cmds):
2373
+ return to_camelcase(
2374
+ kubernetes_sdk.V1Container(
2375
+ name="main",
2376
+ command=cmds,
2377
+ image=deco.attributes["options"].get("image", None)
2378
+ or resources["image"],
2379
+ env=[
2380
+ kubernetes_sdk.V1EnvVar(name=k, value=str(v))
2381
+ for k, v in env.items()
2382
+ ],
2383
+ env_from=[
2384
+ kubernetes_sdk.V1EnvFromSource(
2385
+ secret_ref=kubernetes_sdk.V1SecretEnvSource(
2386
+ name=str(k),
2387
+ # optional=True
2388
+ )
2389
+ )
2390
+ for k in list(
2391
+ []
2392
+ if not resources.get("secrets")
2393
+ else (
2394
+ [resources.get("secrets")]
2395
+ if isinstance(resources.get("secrets"), str)
2396
+ else resources.get("secrets")
2397
+ )
2398
+ )
2399
+ + KUBERNETES_SECRETS.split(",")
2400
+ + ARGO_WORKFLOWS_KUBERNETES_SECRETS.split(",")
2401
+ if k
2402
+ ],
2403
+ resources=kubernetes_sdk.V1ResourceRequirements(
2404
+ # NOTE: base resources for this are kept to a minimum to save on running costs.
2405
+ requests={
2406
+ "cpu": "200m",
2407
+ "memory": "100Mi",
2408
+ },
2409
+ limits={
2410
+ "cpu": "200m",
2411
+ "memory": "500Mi",
2412
+ },
2413
+ ),
2414
+ ).to_dict()
2415
+ )
2416
+
2417
+ # create lifecycle hooks from deco
2418
+ hooks = []
2419
+ for success_fn_name in deco.success_hooks:
2420
+ hook = ContainerHook(
2421
+ name=f"success-{success_fn_name.replace('_', '-')}",
2422
+ container=_container(cmds=_cmd(success_fn_name)),
2423
+ service_account_name=resources["service_account"],
2424
+ on_success=True,
2425
+ )
2426
+ hooks.append(hook)
2427
+
2428
+ for error_fn_name in deco.error_hooks:
2429
+ hook = ContainerHook(
2430
+ name=f"error-{error_fn_name.replace('_', '-')}",
2431
+ service_account_name=resources["service_account"],
2432
+ container=_container(cmds=_cmd(error_fn_name)),
2433
+ on_error=True,
2434
+ )
2435
+ hooks.append(hook)
2436
+
2437
+ return hooks
2438
+
2439
+ def _exit_hook_templates(self):
2440
+ templates = []
2371
2441
  if self.enable_error_msg_capture:
2372
2442
  templates.extend(self._error_msg_capture_hook_templates())
2443
+
2373
2444
  return templates
2374
2445
 
2375
2446
  def _error_msg_capture_hook_templates(self):
@@ -2518,30 +2589,30 @@ class ArgoWorkflows(object):
2518
2589
  # https://developer.pagerduty.com/docs/ZG9jOjExMDI5NTgx-send-an-alert-event
2519
2590
  if self.notify_pager_duty_integration_key is None:
2520
2591
  return None
2521
- return Template("notify-pager-duty-on-error").http(
2522
- Http("POST")
2523
- .url("https://events.pagerduty.com/v2/enqueue")
2524
- .header("Content-Type", "application/json")
2525
- .body(
2526
- json.dumps(
2527
- {
2528
- "event_action": "trigger",
2529
- "routing_key": self.notify_pager_duty_integration_key,
2530
- # "dedup_key": self.flow.name, # TODO: Do we need deduplication?
2531
- "payload": {
2532
- "source": "{{workflow.name}}",
2533
- "severity": "info",
2534
- "summary": "Metaflow run %s/argo-{{workflow.name}} failed!"
2535
- % self.flow.name,
2536
- "custom_details": {
2537
- "Flow": self.flow.name,
2538
- "Run ID": "argo-{{workflow.name}}",
2539
- },
2592
+ return HttpExitHook(
2593
+ name="notify-pager-duty-on-error",
2594
+ method="POST",
2595
+ url="https://events.pagerduty.com/v2/enqueue",
2596
+ headers={"Content-Type": "application/json"},
2597
+ body=json.dumps(
2598
+ {
2599
+ "event_action": "trigger",
2600
+ "routing_key": self.notify_pager_duty_integration_key,
2601
+ # "dedup_key": self.flow.name, # TODO: Do we need deduplication?
2602
+ "payload": {
2603
+ "source": "{{workflow.name}}",
2604
+ "severity": "info",
2605
+ "summary": "Metaflow run %s/argo-{{workflow.name}} failed!"
2606
+ % self.flow.name,
2607
+ "custom_details": {
2608
+ "Flow": self.flow.name,
2609
+ "Run ID": "argo-{{workflow.name}}",
2540
2610
  },
2541
- "links": self._pager_duty_notification_links(),
2542
- }
2543
- )
2544
- )
2611
+ },
2612
+ "links": self._pager_duty_notification_links(),
2613
+ }
2614
+ ),
2615
+ on_error=True,
2545
2616
  )
2546
2617
 
2547
2618
  def _incident_io_alert_template(self):
@@ -2552,50 +2623,52 @@ class ArgoWorkflows(object):
2552
2623
  "Creating alerts for errors requires a alert source config ID."
2553
2624
  )
2554
2625
  ui_links = self._incident_io_ui_urls_for_run()
2555
- return Template("notify-incident-io-on-error").http(
2556
- Http("POST")
2557
- .url(
2626
+ return HttpExitHook(
2627
+ name="notify-incident-io-on-error",
2628
+ method="POST",
2629
+ url=(
2558
2630
  "https://api.incident.io/v2/alert_events/http/%s"
2559
2631
  % self.incident_io_alert_source_config_id
2560
- )
2561
- .header("Content-Type", "application/json")
2562
- .header("Authorization", "Bearer %s" % self.notify_incident_io_api_key)
2563
- .body(
2564
- json.dumps(
2565
- {
2566
- "idempotency_key": "argo-{{workflow.name}}", # use run id to deduplicate alerts.
2567
- "status": "firing",
2568
- "title": "Flow %s has failed." % self.flow.name,
2569
- "description": "Metaflow run {run_pathspec} failed!{urls}".format(
2570
- run_pathspec="%s/argo-{{workflow.name}}" % self.flow.name,
2571
- urls=(
2572
- "\n\nSee details for the run at:\n\n"
2573
- + "\n\n".join(ui_links)
2574
- if ui_links
2575
- else ""
2576
- ),
2577
- ),
2578
- "source_url": (
2579
- "%s/%s/%s"
2580
- % (
2581
- UI_URL.rstrip("/"),
2582
- self.flow.name,
2583
- "argo-{{workflow.name}}",
2584
- )
2585
- if UI_URL
2586
- else None
2632
+ ),
2633
+ headers={
2634
+ "Content-Type": "application/json",
2635
+ "Authorization": "Bearer %s" % self.notify_incident_io_api_key,
2636
+ },
2637
+ body=json.dumps(
2638
+ {
2639
+ "idempotency_key": "argo-{{workflow.name}}", # use run id to deduplicate alerts.
2640
+ "status": "firing",
2641
+ "title": "Flow %s has failed." % self.flow.name,
2642
+ "description": "Metaflow run {run_pathspec} failed!{urls}".format(
2643
+ run_pathspec="%s/argo-{{workflow.name}}" % self.flow.name,
2644
+ urls=(
2645
+ "\n\nSee details for the run at:\n\n"
2646
+ + "\n\n".join(ui_links)
2647
+ if ui_links
2648
+ else ""
2587
2649
  ),
2588
- "metadata": {
2589
- **(self.incident_io_metadata or {}),
2590
- **{
2591
- "run_status": "failed",
2592
- "flow_name": self.flow.name,
2593
- "run_id": "argo-{{workflow.name}}",
2594
- },
2650
+ ),
2651
+ "source_url": (
2652
+ "%s/%s/%s"
2653
+ % (
2654
+ UI_URL.rstrip("/"),
2655
+ self.flow.name,
2656
+ "argo-{{workflow.name}}",
2657
+ )
2658
+ if UI_URL
2659
+ else None
2660
+ ),
2661
+ "metadata": {
2662
+ **(self.incident_io_metadata or {}),
2663
+ **{
2664
+ "run_status": "failed",
2665
+ "flow_name": self.flow.name,
2666
+ "run_id": "argo-{{workflow.name}}",
2595
2667
  },
2596
- }
2597
- )
2598
- )
2668
+ },
2669
+ }
2670
+ ),
2671
+ on_error=True,
2599
2672
  )
2600
2673
 
2601
2674
  def _incident_io_change_template(self):
@@ -2606,50 +2679,52 @@ class ArgoWorkflows(object):
2606
2679
  "Creating alerts for successes requires an alert source config ID."
2607
2680
  )
2608
2681
  ui_links = self._incident_io_ui_urls_for_run()
2609
- return Template("notify-incident-io-on-success").http(
2610
- Http("POST")
2611
- .url(
2682
+ return HttpExitHook(
2683
+ name="notify-incident-io-on-success",
2684
+ method="POST",
2685
+ url=(
2612
2686
  "https://api.incident.io/v2/alert_events/http/%s"
2613
2687
  % self.incident_io_alert_source_config_id
2614
- )
2615
- .header("Content-Type", "application/json")
2616
- .header("Authorization", "Bearer %s" % self.notify_incident_io_api_key)
2617
- .body(
2618
- json.dumps(
2619
- {
2620
- "idempotency_key": "argo-{{workflow.name}}", # use run id to deduplicate alerts.
2621
- "status": "firing",
2622
- "title": "Flow %s has succeeded." % self.flow.name,
2623
- "description": "Metaflow run {run_pathspec} succeeded!{urls}".format(
2624
- run_pathspec="%s/argo-{{workflow.name}}" % self.flow.name,
2625
- urls=(
2626
- "\n\nSee details for the run at:\n\n"
2627
- + "\n\n".join(ui_links)
2628
- if ui_links
2629
- else ""
2630
- ),
2631
- ),
2632
- "source_url": (
2633
- "%s/%s/%s"
2634
- % (
2635
- UI_URL.rstrip("/"),
2636
- self.flow.name,
2637
- "argo-{{workflow.name}}",
2638
- )
2639
- if UI_URL
2640
- else None
2688
+ ),
2689
+ headers={
2690
+ "Content-Type": "application/json",
2691
+ "Authorization": "Bearer %s" % self.notify_incident_io_api_key,
2692
+ },
2693
+ body=json.dumps(
2694
+ {
2695
+ "idempotency_key": "argo-{{workflow.name}}", # use run id to deduplicate alerts.
2696
+ "status": "firing",
2697
+ "title": "Flow %s has succeeded." % self.flow.name,
2698
+ "description": "Metaflow run {run_pathspec} succeeded!{urls}".format(
2699
+ run_pathspec="%s/argo-{{workflow.name}}" % self.flow.name,
2700
+ urls=(
2701
+ "\n\nSee details for the run at:\n\n"
2702
+ + "\n\n".join(ui_links)
2703
+ if ui_links
2704
+ else ""
2641
2705
  ),
2642
- "metadata": {
2643
- **(self.incident_io_metadata or {}),
2644
- **{
2645
- "run_status": "succeeded",
2646
- "flow_name": self.flow.name,
2647
- "run_id": "argo-{{workflow.name}}",
2648
- },
2706
+ ),
2707
+ "source_url": (
2708
+ "%s/%s/%s"
2709
+ % (
2710
+ UI_URL.rstrip("/"),
2711
+ self.flow.name,
2712
+ "argo-{{workflow.name}}",
2713
+ )
2714
+ if UI_URL
2715
+ else None
2716
+ ),
2717
+ "metadata": {
2718
+ **(self.incident_io_metadata or {}),
2719
+ **{
2720
+ "run_status": "succeeded",
2721
+ "flow_name": self.flow.name,
2722
+ "run_id": "argo-{{workflow.name}}",
2649
2723
  },
2650
- }
2651
- )
2652
- )
2724
+ },
2725
+ }
2726
+ ),
2727
+ on_success=True,
2653
2728
  )
2654
2729
 
2655
2730
  def _incident_io_ui_urls_for_run(self):
@@ -2674,27 +2749,27 @@ class ArgoWorkflows(object):
2674
2749
  # https://developer.pagerduty.com/docs/ZG9jOjExMDI5NTgy-send-a-change-event
2675
2750
  if self.notify_pager_duty_integration_key is None:
2676
2751
  return None
2677
- return Template("notify-pager-duty-on-success").http(
2678
- Http("POST")
2679
- .url("https://events.pagerduty.com/v2/change/enqueue")
2680
- .header("Content-Type", "application/json")
2681
- .body(
2682
- json.dumps(
2683
- {
2684
- "routing_key": self.notify_pager_duty_integration_key,
2685
- "payload": {
2686
- "summary": "Metaflow run %s/argo-{{workflow.name}} Succeeded"
2687
- % self.flow.name,
2688
- "source": "{{workflow.name}}",
2689
- "custom_details": {
2690
- "Flow": self.flow.name,
2691
- "Run ID": "argo-{{workflow.name}}",
2692
- },
2752
+ return HttpExitHook(
2753
+ name="notify-pager-duty-on-success",
2754
+ method="POST",
2755
+ url="https://events.pagerduty.com/v2/change/enqueue",
2756
+ headers={"Content-Type": "application/json"},
2757
+ body=json.dumps(
2758
+ {
2759
+ "routing_key": self.notify_pager_duty_integration_key,
2760
+ "payload": {
2761
+ "summary": "Metaflow run %s/argo-{{workflow.name}} Succeeded"
2762
+ % self.flow.name,
2763
+ "source": "{{workflow.name}}",
2764
+ "custom_details": {
2765
+ "Flow": self.flow.name,
2766
+ "Run ID": "argo-{{workflow.name}}",
2693
2767
  },
2694
- "links": self._pager_duty_notification_links(),
2695
- }
2696
- )
2697
- )
2768
+ },
2769
+ "links": self._pager_duty_notification_links(),
2770
+ }
2771
+ ),
2772
+ on_success=True,
2698
2773
  )
2699
2774
 
2700
2775
  def _pager_duty_notification_links(self):
@@ -2798,8 +2873,12 @@ class ArgoWorkflows(object):
2798
2873
  blocks = self._get_slack_blocks(message)
2799
2874
  payload = {"text": message, "blocks": blocks}
2800
2875
 
2801
- return Template("notify-slack-on-error").http(
2802
- Http("POST").url(self.notify_slack_webhook_url).body(json.dumps(payload))
2876
+ return HttpExitHook(
2877
+ name="notify-slack-on-error",
2878
+ method="POST",
2879
+ url=self.notify_slack_webhook_url,
2880
+ body=json.dumps(payload),
2881
+ on_error=True,
2803
2882
  )
2804
2883
 
2805
2884
  def _slack_success_template(self):
@@ -2814,8 +2893,12 @@ class ArgoWorkflows(object):
2814
2893
  blocks = self._get_slack_blocks(message)
2815
2894
  payload = {"text": message, "blocks": blocks}
2816
2895
 
2817
- return Template("notify-slack-on-success").http(
2818
- Http("POST").url(self.notify_slack_webhook_url).body(json.dumps(payload))
2896
+ return HttpExitHook(
2897
+ name="notify-slack-on-success",
2898
+ method="POST",
2899
+ url=self.notify_slack_webhook_url,
2900
+ body=json.dumps(payload),
2901
+ on_success=True,
2819
2902
  )
2820
2903
 
2821
2904
  def _heartbeat_daemon_template(self):
@@ -4186,57 +4269,3 @@ class TriggerParameter(object):
4186
4269
 
4187
4270
  def __str__(self):
4188
4271
  return json.dumps(self.payload, indent=4)
4189
-
4190
-
4191
- class Http(object):
4192
- # https://argoproj.github.io/argo-workflows/fields/#http
4193
-
4194
- def __init__(self, method):
4195
- tree = lambda: defaultdict(tree)
4196
- self.payload = tree()
4197
- self.payload["method"] = method
4198
- self.payload["headers"] = []
4199
-
4200
- def header(self, header, value):
4201
- self.payload["headers"].append({"name": header, "value": value})
4202
- return self
4203
-
4204
- def body(self, body):
4205
- self.payload["body"] = str(body)
4206
- return self
4207
-
4208
- def url(self, url):
4209
- self.payload["url"] = url
4210
- return self
4211
-
4212
- def success_condition(self, success_condition):
4213
- self.payload["successCondition"] = success_condition
4214
- return self
4215
-
4216
- def to_json(self):
4217
- return self.payload
4218
-
4219
- def __str__(self):
4220
- return json.dumps(self.payload, indent=4)
4221
-
4222
-
4223
- class LifecycleHook(object):
4224
- # https://argoproj.github.io/argo-workflows/fields/#lifecyclehook
4225
-
4226
- def __init__(self):
4227
- tree = lambda: defaultdict(tree)
4228
- self.payload = tree()
4229
-
4230
- def expression(self, expression):
4231
- self.payload["expression"] = str(expression)
4232
- return self
4233
-
4234
- def template(self, template):
4235
- self.payload["template"] = template
4236
- return self
4237
-
4238
- def to_json(self):
4239
- return self.payload
4240
-
4241
- def __str__(self):
4242
- return json.dumps(self.payload, indent=4)