zenml-nightly 0.72.0.dev20250121__py3-none-any.whl → 0.73.0.dev20250124__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 (167) hide show
  1. zenml/VERSION +1 -1
  2. zenml/cli/login.py +2 -0
  3. zenml/cli/server.py +1 -0
  4. zenml/cli/service_connectors.py +8 -4
  5. zenml/cli/stack.py +2 -2
  6. zenml/config/pipeline_configurations.py +2 -2
  7. zenml/config/server_config.py +20 -0
  8. zenml/enums.py +1 -0
  9. zenml/event_hub/base_event_hub.py +2 -2
  10. zenml/integrations/airflow/orchestrators/airflow_orchestrator.py +4 -2
  11. zenml/integrations/aws/__init__.py +2 -1
  12. zenml/integrations/aws/flavors/sagemaker_orchestrator_flavor.py +15 -0
  13. zenml/integrations/aws/orchestrators/sagemaker_orchestrator.py +308 -70
  14. zenml/integrations/gcp/__init__.py +3 -0
  15. zenml/integrations/gcp/experiment_trackers/__init__.py +18 -0
  16. zenml/integrations/gcp/experiment_trackers/vertex_experiment_tracker.py +214 -0
  17. zenml/integrations/gcp/flavors/__init__.py +6 -0
  18. zenml/integrations/gcp/flavors/vertex_experiment_tracker_flavor.py +199 -0
  19. zenml/integrations/huggingface/__init__.py +1 -6
  20. zenml/integrations/kubernetes/orchestrators/kube_utils.py +2 -2
  21. zenml/integrations/mlflow/experiment_trackers/mlflow_experiment_tracker.py +0 -1
  22. zenml/integrations/whylogs/data_validators/whylogs_data_validator.py +3 -1
  23. zenml/models/v2/core/api_key.py +2 -2
  24. zenml/models/v2/core/schedule.py +16 -1
  25. zenml/orchestrators/publish_utils.py +4 -4
  26. zenml/orchestrators/step_launcher.py +3 -3
  27. zenml/orchestrators/step_run_utils.py +2 -2
  28. zenml/pipelines/run_utils.py +2 -2
  29. zenml/service_connectors/service_connector.py +2 -2
  30. zenml/stack/stack.py +3 -3
  31. zenml/stack/stack_component.py +10 -2
  32. zenml/stack_deployments/stack_deployment.py +5 -0
  33. zenml/utils/git_utils.py +1 -1
  34. zenml/utils/string_utils.py +2 -2
  35. zenml/zen_server/auth.py +13 -6
  36. zenml/zen_server/dashboard/assets/{404-Dfq64Boz.js → 404-c8OuXDAT.js} +1 -1
  37. zenml/zen_server/dashboard/assets/{@reactflow-BUNIMFeC.js → @reactflow-6JPoencd.js} +1 -1
  38. zenml/zen_server/dashboard/assets/{AlertDialogDropdownItem-B73Vs10T.js → AlertDialogDropdownItem-8yPFDxEI.js} +1 -1
  39. zenml/zen_server/dashboard/assets/{CodeSnippet-DIJRT2NT.js → CodeSnippet-Qh1ae_DJ.js} +1 -1
  40. zenml/zen_server/dashboard/assets/{CollapsibleCard-BzUHGZOU.js → CollapsibleCard-TiI4lId1.js} +1 -1
  41. zenml/zen_server/dashboard/assets/{Commands-BEGyld4c.js → Commands-BcR2Arie.js} +1 -1
  42. zenml/zen_server/dashboard/assets/{ComponentBadge-xyKiek1s.js → ComponentBadge-BqQNUZgb.js} +1 -1
  43. zenml/zen_server/dashboard/assets/{CopyButton-DhW-mapu.js → CopyButton-DCiXO3JC.js} +1 -1
  44. zenml/zen_server/dashboard/assets/{CsvVizualization-D8oazBiE.js → CsvVizualization-O9cVIaL8.js} +1 -1
  45. zenml/zen_server/dashboard/assets/{DeleteAlertDialog-WkSIIgfy.js → DeleteAlertDialog-DrPjHtXX.js} +1 -1
  46. zenml/zen_server/dashboard/assets/{DialogItem-Bgroeg29.js → DialogItem-BYG7d_M2.js} +1 -1
  47. zenml/zen_server/dashboard/assets/{Error-CY5tlu17.js → Error-C1zbWr19.js} +1 -1
  48. zenml/zen_server/dashboard/assets/{ExecutionStatus-G8mjIaeA.js → ExecutionStatus-Ct9srgHC.js} +1 -1
  49. zenml/zen_server/dashboard/assets/{Helpbox-Bb1ed--O.js → Helpbox-Bm_1Zx9f.js} +1 -1
  50. zenml/zen_server/dashboard/assets/{Infobox-Da6-76M2.js → Infobox-OQdkCLSP.js} +1 -1
  51. zenml/zen_server/dashboard/assets/{InlineAvatar-DqnZaBNq.js → InlineAvatar-CQNjKoEQ.js} +1 -1
  52. zenml/zen_server/dashboard/assets/{NestedCollapsible-aK5ojKoF.js → NestedCollapsible-DDgd2SGb.js} +1 -1
  53. zenml/zen_server/dashboard/assets/Partials-MD3e95Dk.js +1 -0
  54. zenml/zen_server/dashboard/assets/{ProBadge-B4tRUYve.js → ProBadge-D784iVNC.js} +1 -1
  55. zenml/zen_server/dashboard/assets/{ProCta-CZuP29Qz.js → ProCta-W2PEvNow.js} +1 -1
  56. zenml/zen_server/dashboard/assets/{ProviderIcon-Bd7GUQ1_.js → ProviderIcon-DfDUOeAy.js} +1 -1
  57. zenml/zen_server/dashboard/assets/{ProviderRadio-mstdqzsS.js → ProviderRadio-B81Elxrc.js} +1 -1
  58. zenml/zen_server/dashboard/assets/{RunSelector-CsruSB4i.js → RunSelector-DOXgdry5.js} +1 -1
  59. zenml/zen_server/dashboard/assets/{RunsBody-DxxtWVYz.js → RunsBody-Bnx2fxub.js} +1 -1
  60. zenml/zen_server/dashboard/assets/SearchField-Yjv-KRW4.js +1 -0
  61. zenml/zen_server/dashboard/assets/{SecretTooltip-CLzJIYW_.js → SecretTooltip-EKpMlG2f.js} +1 -1
  62. zenml/zen_server/dashboard/assets/{SetPassword-Yn50ooBC.js → SetPassword-CDLy57PZ.js} +1 -1
  63. zenml/zen_server/dashboard/assets/StackList-DKQaLDo4.js +1 -0
  64. zenml/zen_server/dashboard/assets/{Tabs-CNv-eTYM.js → Tabs-B5E-o_h6.js} +1 -1
  65. zenml/zen_server/dashboard/assets/{Tick-jEIevzVf.js → Tick-DSYBiuXU.js} +1 -1
  66. zenml/zen_server/dashboard/assets/{UpdatePasswordSchemas-C16GW-kX.js → UpdatePasswordSchemas-HBNOeyoP.js} +1 -1
  67. zenml/zen_server/dashboard/assets/{UsageReason-Bf2tzhv1.js → UsageReason-DXtPS5nE.js} +1 -1
  68. zenml/zen_server/dashboard/assets/{WizardFooter-D6i-AP1K.js → WizardFooter-_1VSMZ_c.js} +1 -1
  69. zenml/zen_server/dashboard/assets/{all-pipeline-runs-query-DUti43aF.js → all-pipeline-runs-query-D0qDLdKB.js} +1 -1
  70. zenml/zen_server/dashboard/assets/{create-stack-Ch2WPs9U.js → create-stack-7JzgAYAm.js} +1 -1
  71. zenml/zen_server/dashboard/assets/{delete-run-Byf9hTjA.js → delete-run-CUdtYFLl.js} +1 -1
  72. zenml/zen_server/dashboard/assets/{form-schemas-BZqKBPBF.js → form-schemas-B6PCV3Y4.js} +1 -1
  73. zenml/zen_server/dashboard/assets/index-B6U0OkEN.css +1 -0
  74. zenml/zen_server/dashboard/assets/{index-CyBKZcpO.js → index-CJ5IfeAl.js} +1 -1
  75. zenml/zen_server/dashboard/assets/{index-CtdYkjUi.js → index-Ceyzb1yI.js} +1 -1
  76. zenml/zen_server/dashboard/assets/{index-CE0aQlv8.js → index-CxO6541P.js} +3 -3
  77. zenml/zen_server/dashboard/assets/{index-v6gQjDEo.js → index-D4yoZ_gH.js} +1 -1
  78. zenml/zen_server/dashboard/assets/{login-mutation-DNDVp_2H.js → login-mutation-BaeJ7MAg.js} +1 -1
  79. zenml/zen_server/dashboard/assets/{not-found-Bmup4ctE.js → not-found-MGptrNBk.js} +1 -1
  80. zenml/zen_server/dashboard/assets/{page-DGlm1RVc.js → page-Aeu3v0MQ.js} +1 -1
  81. zenml/zen_server/dashboard/assets/{page-CltCNL0T.js → page-BCgEdmhP.js} +1 -1
  82. zenml/zen_server/dashboard/assets/{page-Hn8q9iJZ.js → page-BKwwfTNy.js} +1 -1
  83. zenml/zen_server/dashboard/assets/{page-BNxYrN0q.js → page-BUjw8Tp1.js} +1 -1
  84. zenml/zen_server/dashboard/assets/{page-BYJfqgLN.js → page-BXgXP-Qj.js} +1 -1
  85. zenml/zen_server/dashboard/assets/{page-DN4BVIOL.js → page-BXrtxEbw.js} +1 -1
  86. zenml/zen_server/dashboard/assets/{page-CHRn1fQm.js → page-BaUDR9Ri.js} +1 -1
  87. zenml/zen_server/dashboard/assets/{page-DlIi5ThM.js → page-BbljjC-k.js} +1 -1
  88. zenml/zen_server/dashboard/assets/{page-BrmJp1Wt.js → page-BhOXn-s9.js} +1 -1
  89. zenml/zen_server/dashboard/assets/{page-Cc8ZEuj4.js → page-C37IDa-Q.js} +1 -1
  90. zenml/zen_server/dashboard/assets/{page-Dif8CWyZ.js → page-C4JpDeUM.js} +1 -1
  91. zenml/zen_server/dashboard/assets/{page-BC27C_OI.js → page-CB2_GdBA.js} +1 -1
  92. zenml/zen_server/dashboard/assets/{page-B5Sr8pib.js → page-CBiT2Ox9.js} +1 -1
  93. zenml/zen_server/dashboard/assets/{page-IhckKFnD.js → page-CXPc-HN1.js} +1 -1
  94. zenml/zen_server/dashboard/assets/{page-Dth9X1Ih.js → page-CbwI6emp.js} +1 -1
  95. zenml/zen_server/dashboard/assets/{page-DweqqCkF.js → page-CeNL9JWi.js} +1 -1
  96. zenml/zen_server/dashboard/assets/{page-LyZ_l8vR.js → page-CkPwPmLZ.js} +1 -1
  97. zenml/zen_server/dashboard/assets/{page-C70wZtV2.js → page-CmJU3Gqo.js} +1 -1
  98. zenml/zen_server/dashboard/assets/{page-D9Oh05fl.js → page-CoFVtzhG.js} +1 -1
  99. zenml/zen_server/dashboard/assets/{page-PamGpk0j.js → page-D-KPzeQb.js} +1 -1
  100. zenml/zen_server/dashboard/assets/{page-DoW7YxTu.js → page-DKQ3wZgr.js} +1 -1
  101. zenml/zen_server/dashboard/assets/page-DWWhxCoF.js +1 -0
  102. zenml/zen_server/dashboard/assets/{page-CmlYj7Nl.js → page-DbW8MfQ4.js} +1 -1
  103. zenml/zen_server/dashboard/assets/{page-CWr96ZKN.js → page-Dv5lN2w7.js} +1 -1
  104. zenml/zen_server/dashboard/assets/{page-ANYGfEUL.js → page-Dvbq1BoF.js} +1 -1
  105. zenml/zen_server/dashboard/assets/{page-D6Ev5P8V.js → page-DyAuja95.js} +1 -1
  106. zenml/zen_server/dashboard/assets/{page-DyOJ_pq3.js → page-DzrdL2v1.js} +1 -1
  107. zenml/zen_server/dashboard/assets/{page-CXAbSyp9.js → page-I2B4Ocv8.js} +1 -1
  108. zenml/zen_server/dashboard/assets/page-OdjGauvw.js +2 -0
  109. zenml/zen_server/dashboard/assets/{page-CaeI9ptC.js → page-Ox-eC1ik.js} +1 -1
  110. zenml/zen_server/dashboard/assets/{page-B_0XkV48.js → page-khp8QJ6b.js} +1 -1
  111. zenml/zen_server/dashboard/assets/{page--XLMzHrn.js → page-yNh6PQKt.js} +1 -1
  112. zenml/zen_server/dashboard/assets/{persist-vP0-Xl4f.js → persist-DBTFy--v.js} +1 -1
  113. zenml/zen_server/dashboard/assets/{persist-DeXRG61d.js → persist-K7AY0ju4.js} +1 -1
  114. zenml/zen_server/dashboard/assets/{service-DH_oUqQj.js → service-BvOYLH5b.js} +1 -1
  115. zenml/zen_server/dashboard/assets/{sharedSchema-Bw1_Wa7l.js → sharedSchema-xJDsJNgJ.js} +1 -1
  116. zenml/zen_server/dashboard/assets/{stack-detail-query-B_0R_fd6.js → stack-detail-query-DMJoxwgv.js} +1 -1
  117. zenml/zen_server/dashboard/assets/{update-server-settings-mutation-D9qYhfaN.js → update-server-settings-mutation-ATZDNNZk.js} +1 -1
  118. zenml/zen_server/dashboard/assets/{url-Dh93fvh0.js → url-BWJXzuI4.js} +1 -1
  119. zenml/zen_server/dashboard/index.html +4 -4
  120. zenml/zen_server/deploy/helm/Chart.yaml +1 -1
  121. zenml/zen_server/deploy/helm/README.md +2 -2
  122. zenml/zen_server/deploy/helm/templates/server-db-job.yaml +5 -3
  123. zenml/zen_server/deploy/helm/values.yaml +4 -0
  124. zenml/zen_server/routers/devices_endpoints.py +4 -2
  125. zenml/zen_server/routers/workspaces_endpoints.py +2 -0
  126. zenml/zen_stores/migrations/versions/0.73.0_release.py +23 -0
  127. zenml/zen_stores/migrations/versions/25155145c545_separate_actions_and_triggers.py +2 -2
  128. zenml/zen_stores/migrations/versions/46506f72f0ed_add_server_settings.py +2 -2
  129. zenml/zen_stores/migrations/versions/5994f9ad0489_introduce_role_permissions.py +6 -6
  130. zenml/zen_stores/migrations/versions/7500f434b71c_remove_shared_columns.py +2 -2
  131. zenml/zen_stores/migrations/versions/a91762e6be36_artifact_version_table.py +3 -3
  132. zenml/zen_stores/schemas/action_schemas.py +2 -2
  133. zenml/zen_stores/schemas/api_key_schemas.py +4 -4
  134. zenml/zen_stores/schemas/artifact_schemas.py +3 -3
  135. zenml/zen_stores/schemas/base_schemas.py +7 -3
  136. zenml/zen_stores/schemas/code_repository_schemas.py +2 -2
  137. zenml/zen_stores/schemas/component_schemas.py +2 -2
  138. zenml/zen_stores/schemas/device_schemas.py +4 -4
  139. zenml/zen_stores/schemas/event_source_schemas.py +2 -2
  140. zenml/zen_stores/schemas/flavor_schemas.py +2 -2
  141. zenml/zen_stores/schemas/model_schemas.py +3 -3
  142. zenml/zen_stores/schemas/pipeline_run_schemas.py +10 -3
  143. zenml/zen_stores/schemas/pipeline_schemas.py +2 -2
  144. zenml/zen_stores/schemas/run_template_schemas.py +2 -2
  145. zenml/zen_stores/schemas/schedule_schema.py +19 -4
  146. zenml/zen_stores/schemas/secret_schemas.py +2 -2
  147. zenml/zen_stores/schemas/server_settings_schemas.py +9 -5
  148. zenml/zen_stores/schemas/service_connector_schemas.py +2 -2
  149. zenml/zen_stores/schemas/service_schemas.py +2 -2
  150. zenml/zen_stores/schemas/stack_schemas.py +2 -2
  151. zenml/zen_stores/schemas/step_run_schemas.py +2 -2
  152. zenml/zen_stores/schemas/tag_schemas.py +2 -2
  153. zenml/zen_stores/schemas/trigger_schemas.py +2 -2
  154. zenml/zen_stores/schemas/user_schemas.py +3 -3
  155. zenml/zen_stores/schemas/workspace_schemas.py +2 -2
  156. zenml/zen_stores/sql_zen_store.py +10 -1
  157. {zenml_nightly-0.72.0.dev20250121.dist-info → zenml_nightly-0.73.0.dev20250124.dist-info}/METADATA +3 -3
  158. {zenml_nightly-0.72.0.dev20250121.dist-info → zenml_nightly-0.73.0.dev20250124.dist-info}/RECORD +161 -157
  159. zenml/zen_server/dashboard/assets/Partials-CqZp5NMX.js +0 -1
  160. zenml/zen_server/dashboard/assets/SearchField-D6tPxyqw.js +0 -1
  161. zenml/zen_server/dashboard/assets/StackList-U537qoYd.js +0 -1
  162. zenml/zen_server/dashboard/assets/index-DXvT1_Um.css +0 -1
  163. zenml/zen_server/dashboard/assets/page-C2nU3Gxn.js +0 -1
  164. zenml/zen_server/dashboard/assets/page-PxOWfKgF.js +0 -2
  165. {zenml_nightly-0.72.0.dev20250121.dist-info → zenml_nightly-0.73.0.dev20250124.dist-info}/LICENSE +0 -0
  166. {zenml_nightly-0.72.0.dev20250121.dist-info → zenml_nightly-0.73.0.dev20250124.dist-info}/WHEEL +0 -0
  167. {zenml_nightly-0.72.0.dev20250121.dist-info → zenml_nightly-0.73.0.dev20250124.dist-info}/entry_points.txt +0 -0
@@ -15,6 +15,7 @@
15
15
 
16
16
  import os
17
17
  import re
18
+ from datetime import datetime, timezone
18
19
  from typing import (
19
20
  TYPE_CHECKING,
20
21
  Any,
@@ -35,14 +36,20 @@ from sagemaker.processing import ProcessingInput, ProcessingOutput
35
36
  from sagemaker.workflow.execution_variables import ExecutionVariables
36
37
  from sagemaker.workflow.pipeline import Pipeline
37
38
  from sagemaker.workflow.steps import ProcessingStep, TrainingStep
39
+ from sagemaker.workflow.triggers import PipelineSchedule
38
40
 
41
+ from zenml.client import Client
39
42
  from zenml.config.base_settings import BaseSettings
40
43
  from zenml.constants import (
41
44
  METADATA_ORCHESTRATOR_LOGS_URL,
42
45
  METADATA_ORCHESTRATOR_RUN_ID,
43
46
  METADATA_ORCHESTRATOR_URL,
44
47
  )
45
- from zenml.enums import ExecutionStatus, StackComponentType
48
+ from zenml.enums import (
49
+ ExecutionStatus,
50
+ MetadataResourceTypes,
51
+ StackComponentType,
52
+ )
46
53
  from zenml.integrations.aws.flavors.sagemaker_orchestrator_flavor import (
47
54
  SagemakerOrchestratorConfig,
48
55
  SagemakerOrchestratorSettings,
@@ -69,6 +76,36 @@ POLLING_DELAY = 30
69
76
  logger = get_logger(__name__)
70
77
 
71
78
 
79
+ def dissect_schedule_arn(
80
+ schedule_arn: str,
81
+ ) -> Tuple[Optional[str], Optional[str]]:
82
+ """Extracts the region and the name from an EventBridge schedule ARN.
83
+
84
+ Args:
85
+ schedule_arn: The ARN of the EventBridge schedule.
86
+
87
+ Returns:
88
+ Region Name, Schedule Name (including the group name)
89
+
90
+ Raises:
91
+ ValueError: If the input is not a properly formatted ARN.
92
+ """
93
+ # Split the ARN into parts
94
+ arn_parts = schedule_arn.split(":")
95
+
96
+ # Validate ARN structure
97
+ if len(arn_parts) < 6 or not arn_parts[5].startswith("schedule/"):
98
+ raise ValueError("Invalid EventBridge schedule ARN format.")
99
+
100
+ # Extract the region
101
+ region = arn_parts[3]
102
+
103
+ # Extract the group name and schedule name
104
+ name = arn_parts[5].split("schedule/")[1]
105
+
106
+ return region, name
107
+
108
+
72
109
  def dissect_pipeline_execution_arn(
73
110
  pipeline_execution_arn: str,
74
111
  ) -> Tuple[Optional[str], Optional[str], Optional[str]]:
@@ -237,21 +274,15 @@ class SagemakerOrchestrator(ContainerizedOrchestrator):
237
274
  environment.
238
275
 
239
276
  Raises:
240
- RuntimeError: If a connector is used that does not return a
241
- `boto3.Session` object.
277
+ RuntimeError: If there is an error creating or scheduling the
278
+ pipeline.
242
279
  TypeError: If the network_config passed is not compatible with the
243
280
  AWS SageMaker NetworkConfig class.
281
+ ValueError: If the schedule is not valid.
244
282
 
245
283
  Yields:
246
284
  A dictionary of metadata related to the pipeline run.
247
285
  """
248
- if deployment.schedule:
249
- logger.warning(
250
- "The Sagemaker Orchestrator currently does not support the "
251
- "use of schedules. The `schedule` will be ignored "
252
- "and the pipeline will be run immediately."
253
- )
254
-
255
286
  # sagemaker requires pipelineName to use alphanum and hyphens only
256
287
  unsanitized_orchestrator_run_name = get_orchestrator_run_name(
257
288
  pipeline_name=deployment.pipeline_configuration.name
@@ -459,7 +490,7 @@ class SagemakerOrchestrator(ContainerizedOrchestrator):
459
490
 
460
491
  sagemaker_steps.append(sagemaker_step)
461
492
 
462
- # construct the pipeline from the sagemaker_steps
493
+ # Create the pipeline
463
494
  pipeline = Pipeline(
464
495
  name=orchestrator_run_name,
465
496
  steps=sagemaker_steps,
@@ -479,39 +510,207 @@ class SagemakerOrchestrator(ContainerizedOrchestrator):
479
510
  if settings.pipeline_tags
480
511
  else None,
481
512
  )
482
- execution = pipeline.start()
483
- logger.warning(
484
- "Steps can take 5-15 minutes to start running "
485
- "when using the Sagemaker Orchestrator."
486
- )
487
513
 
488
- # Yield metadata based on the generated execution object
489
- yield from self.compute_metadata(
490
- execution=execution, settings=settings
491
- )
514
+ # Handle scheduling if specified
515
+ if deployment.schedule:
516
+ if settings.synchronous:
517
+ logger.warning(
518
+ "The 'synchronous' setting is ignored for scheduled "
519
+ "pipelines since they run independently of the "
520
+ "deployment process."
521
+ )
492
522
 
493
- # mainly for testing purposes, we wait for the pipeline to finish
494
- if settings.synchronous:
495
- logger.info(
496
- "Executing synchronously. Waiting for pipeline to finish... \n"
497
- "At this point you can `Ctrl-C` out without cancelling the "
498
- "execution."
523
+ schedule_name = orchestrator_run_name
524
+ next_execution = None
525
+
526
+ # Create PipelineSchedule based on schedule type
527
+ if deployment.schedule.cron_expression:
528
+ cron_exp = self._validate_cron_expression(
529
+ deployment.schedule.cron_expression
530
+ )
531
+ schedule = PipelineSchedule(
532
+ name=schedule_name,
533
+ cron=cron_exp,
534
+ start_date=deployment.schedule.start_time,
535
+ enabled=True,
536
+ )
537
+ elif deployment.schedule.interval_second:
538
+ # This is necessary because SageMaker's PipelineSchedule rate
539
+ # expressions require minutes as the minimum time unit.
540
+ # Even if a user specifies an interval of less than 60 seconds,
541
+ # it will be rounded up to 1 minute.
542
+ minutes = max(
543
+ 1,
544
+ int(
545
+ deployment.schedule.interval_second.total_seconds()
546
+ / 60
547
+ ),
548
+ )
549
+ schedule = PipelineSchedule(
550
+ name=schedule_name,
551
+ rate=(minutes, "minutes"),
552
+ start_date=deployment.schedule.start_time,
553
+ enabled=True,
554
+ )
555
+ next_execution = (
556
+ deployment.schedule.start_time
557
+ or datetime.now(timezone.utc)
558
+ ) + deployment.schedule.interval_second
559
+ else:
560
+ # One-time schedule
561
+ execution_time = (
562
+ deployment.schedule.run_once_start_time
563
+ or deployment.schedule.start_time
564
+ )
565
+ if not execution_time:
566
+ raise ValueError(
567
+ "A start time must be specified for one-time "
568
+ "schedule execution"
569
+ )
570
+ schedule = PipelineSchedule(
571
+ name=schedule_name,
572
+ at=execution_time.astimezone(timezone.utc),
573
+ enabled=True,
574
+ )
575
+ next_execution = execution_time
576
+
577
+ # Get the current role ARN if not explicitly configured
578
+ if self.config.scheduler_role is None:
579
+ logger.info(
580
+ "No scheduler_role configured. Trying to extract it from "
581
+ "the client side authentication."
582
+ )
583
+ sts = session.boto_session.client("sts")
584
+ try:
585
+ scheduler_role_arn = sts.get_caller_identity()["Arn"]
586
+ # If this is a user ARN, try to get the role ARN
587
+ if ":user/" in scheduler_role_arn:
588
+ logger.warning(
589
+ f"Using IAM user credentials "
590
+ f"({scheduler_role_arn}). For production "
591
+ "environments, it's recommended to use IAM roles "
592
+ "instead."
593
+ )
594
+ # If this is an assumed role, extract the role ARN
595
+ elif ":assumed-role/" in scheduler_role_arn:
596
+ # Convert assumed-role ARN format to role ARN format
597
+ # From: arn:aws:sts::123456789012:assumed-role/role-name/session-name
598
+ # To: arn:aws:iam::123456789012:role/role-name
599
+ scheduler_role_arn = re.sub(
600
+ r"arn:aws:sts::(\d+):assumed-role/([^/]+)/.*",
601
+ r"arn:aws:iam::\1:role/\2",
602
+ scheduler_role_arn,
603
+ )
604
+ elif ":role/" not in scheduler_role_arn:
605
+ raise RuntimeError(
606
+ f"Unexpected credential type "
607
+ f"({scheduler_role_arn}). Please use IAM "
608
+ f"roles for SageMaker pipeline scheduling."
609
+ )
610
+ else:
611
+ raise RuntimeError(
612
+ "The ARN of the caller identity "
613
+ f"`{scheduler_role_arn}` does not "
614
+ "include a user or a proper role."
615
+ )
616
+ except Exception:
617
+ raise RuntimeError(
618
+ "Failed to get current role ARN. This means the "
619
+ "your client side credentials that you are "
620
+ "is not configured correctly to schedule sagemaker "
621
+ "pipelines. For more information, please check:"
622
+ "https://docs.zenml.io/stack-components/orchestrators/sagemaker#required-iam-permissions-for-schedules"
623
+ )
624
+ else:
625
+ scheduler_role_arn = self.config.scheduler_role
626
+
627
+ # Attach schedule to pipeline
628
+ triggers = pipeline.put_triggers(
629
+ triggers=[schedule],
630
+ role_arn=scheduler_role_arn,
499
631
  )
632
+ logger.info(f"The schedule ARN is: {triggers[0]}")
633
+
500
634
  try:
501
- execution.wait(
502
- delay=POLLING_DELAY, max_attempts=MAX_POLLING_ATTEMPTS
635
+ from zenml.models import RunMetadataResource
636
+
637
+ schedule_metadata = self.generate_schedule_metadata(
638
+ schedule_arn=triggers[0]
503
639
  )
504
- logger.info("Pipeline completed successfully.")
505
- except WaiterError:
506
- raise RuntimeError(
507
- "Timed out while waiting for pipeline execution to "
508
- "finish. For long-running pipelines we recommend "
509
- "configuring your orchestrator for asynchronous execution. "
510
- "The following command does this for you: \n"
511
- f"`zenml orchestrator update {self.name} "
512
- f"--synchronous=False`"
640
+
641
+ Client().create_run_metadata(
642
+ metadata=schedule_metadata, # type: ignore[arg-type]
643
+ resources=[
644
+ RunMetadataResource(
645
+ id=deployment.schedule.id,
646
+ type=MetadataResourceTypes.SCHEDULE,
647
+ )
648
+ ],
649
+ )
650
+ except Exception as e:
651
+ logger.debug(
652
+ "There was an error attaching metadata to the "
653
+ f"schedule: {e}"
513
654
  )
514
655
 
656
+ logger.info(
657
+ f"Successfully scheduled pipeline with name: {schedule_name}\n"
658
+ + (
659
+ f"First execution will occur at: "
660
+ f"{next_execution.strftime('%Y-%m-%d %H:%M:%S UTC')}"
661
+ if next_execution
662
+ else f"Using cron expression: "
663
+ f"{deployment.schedule.cron_expression}"
664
+ )
665
+ + (
666
+ f" (and every {minutes} minutes after)"
667
+ if deployment.schedule.interval_second
668
+ else ""
669
+ )
670
+ )
671
+ logger.info(
672
+ "\n\nIn order to cancel the schedule, you can use execute "
673
+ "the following command:\n"
674
+ )
675
+ logger.info(
676
+ f"`aws scheduler delete-schedule --name {schedule_name}`"
677
+ )
678
+ else:
679
+ # Execute the pipeline immediately if no schedule is specified
680
+ execution = pipeline.start()
681
+ logger.warning(
682
+ "Steps can take 5-15 minutes to start running "
683
+ "when using the Sagemaker Orchestrator."
684
+ )
685
+
686
+ # Yield metadata based on the generated execution object
687
+ yield from self.compute_metadata(
688
+ execution_arn=execution.arn, settings=settings
689
+ )
690
+
691
+ # mainly for testing purposes, we wait for the pipeline to finish
692
+ if settings.synchronous:
693
+ logger.info(
694
+ "Executing synchronously. Waiting for pipeline to "
695
+ "finish... \n"
696
+ "At this point you can `Ctrl-C` out without cancelling the "
697
+ "execution."
698
+ )
699
+ try:
700
+ execution.wait(
701
+ delay=POLLING_DELAY, max_attempts=MAX_POLLING_ATTEMPTS
702
+ )
703
+ logger.info("Pipeline completed successfully.")
704
+ except WaiterError:
705
+ raise RuntimeError(
706
+ "Timed out while waiting for pipeline execution to "
707
+ "finish. For long-running pipelines we recommend "
708
+ "configuring your orchestrator for asynchronous "
709
+ "execution. The following command does this for you: \n"
710
+ f"`zenml orchestrator update {self.name} "
711
+ f"--synchronous=False`"
712
+ )
713
+
515
714
  def get_pipeline_run_metadata(
516
715
  self, run_id: UUID
517
716
  ) -> Dict[str, "MetadataType"]:
@@ -523,10 +722,22 @@ class SagemakerOrchestrator(ContainerizedOrchestrator):
523
722
  Returns:
524
723
  A dictionary of metadata.
525
724
  """
526
- pipeline_execution_arn = os.environ[ENV_ZENML_SAGEMAKER_RUN_ID]
527
- run_metadata: Dict[str, "MetadataType"] = {
528
- "pipeline_execution_arn": pipeline_execution_arn,
529
- }
725
+ from zenml import get_step_context
726
+
727
+ execution_arn = os.environ[ENV_ZENML_SAGEMAKER_RUN_ID]
728
+
729
+ run_metadata: Dict[str, "MetadataType"] = {}
730
+
731
+ settings = cast(
732
+ SagemakerOrchestratorSettings,
733
+ self.get_settings(get_step_context().pipeline_run),
734
+ )
735
+
736
+ for metadata in self.compute_metadata(
737
+ execution_arn=execution_arn,
738
+ settings=settings,
739
+ ):
740
+ run_metadata.update(metadata)
530
741
 
531
742
  return run_metadata
532
743
 
@@ -588,56 +799,57 @@ class SagemakerOrchestrator(ContainerizedOrchestrator):
588
799
 
589
800
  def compute_metadata(
590
801
  self,
591
- execution: Any,
802
+ execution_arn: str,
592
803
  settings: SagemakerOrchestratorSettings,
593
804
  ) -> Iterator[Dict[str, MetadataType]]:
594
805
  """Generate run metadata based on the generated Sagemaker Execution.
595
806
 
596
807
  Args:
597
- execution: The corresponding _PipelineExecution object.
808
+ execution_arn: The ARN of the pipeline execution.
598
809
  settings: The Sagemaker orchestrator settings.
599
810
 
600
811
  Yields:
601
812
  A dictionary of metadata related to the pipeline run.
602
813
  """
603
- # Metadata
604
- metadata: Dict[str, MetadataType] = {}
605
-
606
814
  # Orchestrator Run ID
607
- if run_id := self._compute_orchestrator_run_id(execution):
608
- metadata[METADATA_ORCHESTRATOR_RUN_ID] = run_id
815
+ metadata: Dict[str, MetadataType] = {
816
+ "pipeline_execution_arn": execution_arn,
817
+ METADATA_ORCHESTRATOR_RUN_ID: execution_arn,
818
+ }
609
819
 
610
820
  # URL to the Sagemaker's pipeline view
611
- if orchestrator_url := self._compute_orchestrator_url(execution):
821
+ if orchestrator_url := self._compute_orchestrator_url(
822
+ execution_arn=execution_arn
823
+ ):
612
824
  metadata[METADATA_ORCHESTRATOR_URL] = Uri(orchestrator_url)
613
825
 
614
826
  # URL to the corresponding CloudWatch page
615
827
  if logs_url := self._compute_orchestrator_logs_url(
616
- execution, settings
828
+ execution_arn=execution_arn, settings=settings
617
829
  ):
618
830
  metadata[METADATA_ORCHESTRATOR_LOGS_URL] = Uri(logs_url)
619
831
 
620
832
  yield metadata
621
833
 
622
- @staticmethod
623
834
  def _compute_orchestrator_url(
624
- pipeline_execution: Any,
835
+ self,
836
+ execution_arn: Any,
625
837
  ) -> Optional[str]:
626
838
  """Generate the Orchestrator Dashboard URL upon pipeline execution.
627
839
 
628
840
  Args:
629
- pipeline_execution: The corresponding _PipelineExecution object.
841
+ execution_arn: The ARN of the pipeline execution.
630
842
 
631
843
  Returns:
632
844
  the URL to the dashboard view in SageMaker.
633
845
  """
634
846
  try:
635
847
  region_name, pipeline_name, execution_id = (
636
- dissect_pipeline_execution_arn(pipeline_execution.arn)
848
+ dissect_pipeline_execution_arn(execution_arn)
637
849
  )
638
850
 
639
851
  # Get the Sagemaker session
640
- session = pipeline_execution.sagemaker_session
852
+ session = self._get_sagemaker_session()
641
853
 
642
854
  # List the Studio domains and get the Studio Domain ID
643
855
  domains_response = session.sagemaker_client.list_domains()
@@ -657,13 +869,13 @@ class SagemakerOrchestrator(ContainerizedOrchestrator):
657
869
 
658
870
  @staticmethod
659
871
  def _compute_orchestrator_logs_url(
660
- pipeline_execution: Any,
872
+ execution_arn: Any,
661
873
  settings: SagemakerOrchestratorSettings,
662
874
  ) -> Optional[str]:
663
875
  """Generate the CloudWatch URL upon pipeline execution.
664
876
 
665
877
  Args:
666
- pipeline_execution: The corresponding _PipelineExecution object.
878
+ execution_arn: The ARN of the pipeline execution.
667
879
  settings: The Sagemaker orchestrator settings.
668
880
 
669
881
  Returns:
@@ -671,7 +883,7 @@ class SagemakerOrchestrator(ContainerizedOrchestrator):
671
883
  """
672
884
  try:
673
885
  region_name, _, execution_id = dissect_pipeline_execution_arn(
674
- pipeline_execution.arn
886
+ execution_arn
675
887
  )
676
888
 
677
889
  use_training_jobs = True
@@ -693,22 +905,48 @@ class SagemakerOrchestrator(ContainerizedOrchestrator):
693
905
  return None
694
906
 
695
907
  @staticmethod
696
- def _compute_orchestrator_run_id(
697
- pipeline_execution: Any,
698
- ) -> Optional[str]:
699
- """Fetch the Orchestrator Run ID upon pipeline execution.
908
+ def generate_schedule_metadata(schedule_arn: str) -> Dict[str, str]:
909
+ """Attaches metadata to the ZenML Schedules.
700
910
 
701
911
  Args:
702
- pipeline_execution: The corresponding _PipelineExecution object.
912
+ schedule_arn: The trigger ARNs that is generated on the AWS side.
703
913
 
704
914
  Returns:
705
- the Execution ID of the run in SageMaker.
915
+ a dictionary containing metadata related to the schedule.
706
916
  """
707
- try:
708
- return str(pipeline_execution.arn)
917
+ region, name = dissect_schedule_arn(schedule_arn=schedule_arn)
709
918
 
710
- except Exception as e:
711
- logger.warning(
712
- f"There was an issue while extracting the pipeline run ID: {e}"
919
+ return {
920
+ "trigger_url": (
921
+ f"https://{region}.console.aws.amazon.com/scheduler/home"
922
+ f"?region={region}#schedules/{name}"
923
+ ),
924
+ }
925
+
926
+ @staticmethod
927
+ def _validate_cron_expression(cron_expression: str) -> str:
928
+ """Validates and formats a cron expression for SageMaker schedules.
929
+
930
+ Args:
931
+ cron_expression: The cron expression to validate
932
+
933
+ Returns:
934
+ The formatted cron expression
935
+
936
+ Raises:
937
+ ValueError: If the cron expression is invalid
938
+ """
939
+ # Strip any "cron(" prefix if it exists
940
+ cron_exp = cron_expression.replace("cron(", "").replace(")", "")
941
+
942
+ # Split into components
943
+ parts = cron_exp.split()
944
+ if len(parts) not in [6, 7]: # AWS cron requires 6 or 7 fields
945
+ raise ValueError(
946
+ f"Invalid cron expression: {cron_expression}. AWS cron "
947
+ "expressions must have 6 or 7 fields: minute hour day-of-month "
948
+ "month day-of-week year(optional). Example: '15 10 ? * 6L "
949
+ "2022-2023'"
713
950
  )
714
- return None
951
+
952
+ return cron_exp
@@ -30,6 +30,7 @@ from zenml.stack import Flavor
30
30
 
31
31
  GCP_ARTIFACT_STORE_FLAVOR = "gcp"
32
32
  GCP_IMAGE_BUILDER_FLAVOR = "gcp"
33
+ GCP_VERTEX_EXPERIMENT_TRACKER_FLAVOR = "vertex"
33
34
  GCP_VERTEX_ORCHESTRATOR_FLAVOR = "vertex"
34
35
  GCP_VERTEX_STEP_OPERATOR_FLAVOR = "vertex"
35
36
 
@@ -70,6 +71,7 @@ class GcpIntegration(Integration):
70
71
  from zenml.integrations.gcp.flavors import (
71
72
  GCPArtifactStoreFlavor,
72
73
  GCPImageBuilderFlavor,
74
+ VertexExperimentTrackerFlavor,
73
75
  VertexOrchestratorFlavor,
74
76
  VertexStepOperatorFlavor,
75
77
  )
@@ -77,6 +79,7 @@ class GcpIntegration(Integration):
77
79
  return [
78
80
  GCPArtifactStoreFlavor,
79
81
  GCPImageBuilderFlavor,
82
+ VertexExperimentTrackerFlavor,
80
83
  VertexOrchestratorFlavor,
81
84
  VertexStepOperatorFlavor,
82
85
  ]
@@ -0,0 +1,18 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at:
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
10
+ # or implied. See the License for the specific language governing
11
+ # permissions and limitations under the License.
12
+ """Initialization for the VertexAI experiment tracker."""
13
+
14
+ from zenml.integrations.gcp.experiment_trackers.vertex_experiment_tracker import ( # noqa
15
+ VertexExperimentTracker,
16
+ )
17
+
18
+ __all__ = ["VertexExperimentTracker"]