zenml-nightly 0.84.2.dev20250826__py3-none-any.whl → 0.84.3.dev20250828__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 (121) hide show
  1. zenml/VERSION +1 -1
  2. zenml/cli/login.py +30 -19
  3. zenml/cli/server.py +6 -10
  4. zenml/login/credentials.py +27 -0
  5. zenml/login/credentials_store.py +80 -80
  6. zenml/login/pro/client.py +74 -54
  7. zenml/login/pro/utils.py +1 -3
  8. zenml/utils/package_utils.py +1 -1
  9. zenml/zen_server/dashboard/assets/{404-CMbwR10f.js → 404-JcbBcNWG.js} +1 -1
  10. zenml/zen_server/dashboard/assets/{AlertDialogDropdownItem-BpLj419i.js → AlertDialogDropdownItem-DuTp8JFP.js} +1 -1
  11. zenml/zen_server/dashboard/assets/{ButtonGroup-DVhzbQxV.js → ButtonGroup-B9_gfcC-.js} +1 -1
  12. zenml/zen_server/dashboard/assets/{CodeSnippet-B7Wd4R8u.js → CodeSnippet-D7OZMz7I.js} +1 -1
  13. zenml/zen_server/dashboard/assets/{CollapsibleCard-DSZzbjx5.js → CollapsibleCard-Aqd_AiGn.js} +1 -1
  14. zenml/zen_server/dashboard/assets/{ComponentBadge-MVT08kBV.js → ComponentBadge-BVRb7wgm.js} +1 -1
  15. zenml/zen_server/dashboard/assets/{ComponentIcon-C9AY8usJ.js → ComponentIcon-CWDJZO4Q.js} +1 -1
  16. zenml/zen_server/dashboard/assets/{DeleteAlertDialog-CzhTVQaC.js → DeleteAlertDialog-C5z9RupH.js} +1 -1
  17. zenml/zen_server/dashboard/assets/{DialogItem-DD_7v3UY.js → DialogItem-Ce3ugDgT.js} +1 -1
  18. zenml/zen_server/dashboard/assets/{Error-Cakgjexo.js → Error-BGej6F2q.js} +1 -1
  19. zenml/zen_server/dashboard/assets/{ExecutionStatus-CQbWovhV.js → ExecutionStatus-BhDlfndt.js} +1 -1
  20. zenml/zen_server/dashboard/assets/{Helpbox-7FZpmsHB.js → Helpbox-CQjyNo0n.js} +1 -1
  21. zenml/zen_server/dashboard/assets/{Infobox-DnduZjNU.js → Infobox-CXOIY92f.js} +1 -1
  22. zenml/zen_server/dashboard/assets/{LeftSideMenu-D5UEs4vB.js → LeftSideMenu-DGICatl7.js} +1 -1
  23. zenml/zen_server/dashboard/assets/{NestedCollapsible-0yviIfaA.js → NestedCollapsible-DmryIMwk.js} +1 -1
  24. zenml/zen_server/dashboard/assets/{NumberBox-C2Pju4PR.js → NumberBox-7OIA56xe.js} +1 -1
  25. zenml/zen_server/dashboard/assets/{Pagination-C1sE5Nzn.js → Pagination-CVYIgR9l.js} +1 -1
  26. zenml/zen_server/dashboard/assets/{Partials-DosysQMU.js → Partials-NicibWrh.js} +1 -1
  27. zenml/zen_server/dashboard/assets/{ProCta-CUkpV3PJ.js → ProCta-DspRnbtD.js} +1 -1
  28. zenml/zen_server/dashboard/assets/{ProviderIcon-aQjTuTRX.js → ProviderIcon-DwKsXziU.js} +1 -1
  29. zenml/zen_server/dashboard/assets/{ProviderRadio-C6pLVNxC.js → ProviderRadio-BixXi4GC.js} +1 -1
  30. zenml/zen_server/dashboard/assets/{RunsBody-CZAiSxYK.js → RunsBody-BSOSIxRG.js} +1 -1
  31. zenml/zen_server/dashboard/assets/{SearchField-Byv1PtST.js → SearchField-BbwrusMV.js} +1 -1
  32. zenml/zen_server/dashboard/assets/{SecretTooltip-CErfhVgF.js → SecretTooltip-BsFul9Oe.js} +1 -1
  33. zenml/zen_server/dashboard/assets/{SetPassword-C4OH-cn1.js → SetPassword-D-V7D0OR.js} +1 -1
  34. zenml/zen_server/dashboard/assets/{SheetHeader-Bb3v9rha.js → SheetHeader-xumE4haD.js} +1 -1
  35. zenml/zen_server/dashboard/assets/{StackComponentList-D85oab98.js → StackComponentList-CLnkE6-6.js} +1 -1
  36. zenml/zen_server/dashboard/assets/{StackList-DSAjbVb5.js → StackList-DhsbaNva.js} +1 -1
  37. zenml/zen_server/dashboard/assets/{Tabs-2uwqXsdL.js → Tabs-CkY3GwhK.js} +1 -1
  38. zenml/zen_server/dashboard/assets/{Tick-BQ7_xDBl.js → Tick-Bo7jwBjb.js} +1 -1
  39. zenml/zen_server/dashboard/assets/{UpdatePasswordSchemas-DNkYgzN6.js → UpdatePasswordSchemas-BP4hg95A.js} +1 -1
  40. zenml/zen_server/dashboard/assets/{Wizard-2FIi8JNY.js → Wizard-0nbnAM1R.js} +1 -1
  41. zenml/zen_server/dashboard/assets/{WizardFooter-DQEnlDmZ.js → WizardFooter-CLPi4VBo.js} +1 -1
  42. zenml/zen_server/dashboard/assets/{all-pipeline-runs-query-CdvWtiAY.js → all-pipeline-runs-query-mPYeJsKz.js} +1 -1
  43. zenml/zen_server/dashboard/assets/{bulk-delete-wdObjfps.js → bulk-delete-Bgf_MsSj.js} +1 -1
  44. zenml/zen_server/dashboard/assets/{configuration-form-BbcNBQTO.js → configuration-form-Bnyw4-V5.js} +1 -1
  45. zenml/zen_server/dashboard/assets/{constants-DfvsDtcH.js → constants-Ddhz1mpL.js} +1 -1
  46. zenml/zen_server/dashboard/assets/{create-stack-D4Sf7QpF.js → create-stack-BpTy7-E4.js} +1 -1
  47. zenml/zen_server/dashboard/assets/{delete-run-XSre8ru-.js → delete-run-MHYb1wuH.js} +1 -1
  48. zenml/zen_server/dashboard/assets/form-VRON3-kT.js +1 -0
  49. zenml/zen_server/dashboard/assets/{form-schemas-BjWDRvGd.js → form-schemas-CpTCEVoG.js} +1 -1
  50. zenml/zen_server/dashboard/assets/{index-BYHxFvoG.js → index-BYCBKUXk.js} +1 -1
  51. zenml/zen_server/dashboard/assets/{index-CO6UN3UX.js → index-Ci5z7q6x.js} +2 -2
  52. zenml/zen_server/dashboard/assets/{index-d_QrPL-8.js → index-D_-iUPTg.js} +1 -1
  53. zenml/zen_server/dashboard/assets/{index-BYzR7YrM.js → index-rnWXXblA.js} +1 -1
  54. zenml/zen_server/dashboard/assets/{layout-Cyc-V16c.js → layout-DhFllShI.js} +1 -1
  55. zenml/zen_server/dashboard/assets/{login-mutation-CDgTlQBD.js → login-mutation-DOxF97JV.js} +1 -1
  56. zenml/zen_server/dashboard/assets/{page-Bncp08FW.js → page-62MHYKGv.js} +1 -1
  57. zenml/zen_server/dashboard/assets/{page-CMvcoaSZ.js → page-6YPAQ0BU.js} +1 -1
  58. zenml/zen_server/dashboard/assets/{page-CEHZzQ1Q.js → page-8ZbMMHB-.js} +1 -1
  59. zenml/zen_server/dashboard/assets/{page-D654xHWd.js → page-AWVW_lra.js} +1 -1
  60. zenml/zen_server/dashboard/assets/{page-BnZEAhNn.js → page-B1rTgWkQ.js} +1 -1
  61. zenml/zen_server/dashboard/assets/{page-BbsvGfyi.js → page-B5YCjh4t.js} +1 -1
  62. zenml/zen_server/dashboard/assets/{page-DgOEl3Bc.js → page-BDawWKR9.js} +1 -1
  63. zenml/zen_server/dashboard/assets/{page-DOnAInjC.js → page-BFzVhfws.js} +1 -1
  64. zenml/zen_server/dashboard/assets/page-BaQw51KO.js +1 -0
  65. zenml/zen_server/dashboard/assets/{page-C4Jnp1qP.js → page-Bh4TFAum.js} +1 -1
  66. zenml/zen_server/dashboard/assets/{page-CGtll3Jk.js → page-BjuIRasU.js} +1 -1
  67. zenml/zen_server/dashboard/assets/{page-43SWdz4k.js → page-BoQ_Wheb.js} +1 -1
  68. zenml/zen_server/dashboard/assets/{page-Du6bFZTS.js → page-Bs6NTqHo.js} +1 -1
  69. zenml/zen_server/dashboard/assets/{page-B4QBxV0B.js → page-Bsiqb0ZN.js} +1 -1
  70. zenml/zen_server/dashboard/assets/{page-AOVMuHDc.js → page-C09_xoEs.js} +1 -1
  71. zenml/zen_server/dashboard/assets/{page-O5xnz_nW.js → page-CI0U7nE4.js} +1 -1
  72. zenml/zen_server/dashboard/assets/{page-C9IsnFid.js → page-CId8DuKX.js} +1 -1
  73. zenml/zen_server/dashboard/assets/{page-CzDjN1wE.js → page-CJSSCJ58.js} +1 -1
  74. zenml/zen_server/dashboard/assets/{page-B8ict_cR.js → page-CJXxkmGx.js} +1 -1
  75. zenml/zen_server/dashboard/assets/{page-DgblJuXC.js → page-CS3tgRFg.js} +1 -1
  76. zenml/zen_server/dashboard/assets/page-Cd8VCEYG.js +1 -0
  77. zenml/zen_server/dashboard/assets/{page-DsFXol8x.js → page-CicaAxfg.js} +1 -1
  78. zenml/zen_server/dashboard/assets/page-CkFIY_Cg.js +1 -0
  79. zenml/zen_server/dashboard/assets/{page-BnAMyQZb.js → page-CkX3R4er.js} +1 -1
  80. zenml/zen_server/dashboard/assets/{page-DerIbaqa.js → page-Cu-ZYOrj.js} +1 -1
  81. zenml/zen_server/dashboard/assets/page-CyID6Sp5.js +1 -0
  82. zenml/zen_server/dashboard/assets/{page-DZ5hcS1o.js → page-D0Z9wEBf.js} +1 -1
  83. zenml/zen_server/dashboard/assets/{page-hX7NkNdA.js → page-D83H0IPX.js} +1 -1
  84. zenml/zen_server/dashboard/assets/{page-DgSu4pTE.js → page-DAF5ySum.js} +1 -1
  85. zenml/zen_server/dashboard/assets/{page-ClqRvz_V.js → page-DMiWibIV.js} +1 -1
  86. zenml/zen_server/dashboard/assets/{page-Bi4I23Hs.js → page-DMtUDzEY.js} +1 -1
  87. zenml/zen_server/dashboard/assets/{page-BDre_qCO.js → page-DNLxVFH7.js} +1 -1
  88. zenml/zen_server/dashboard/assets/{page-DLrYW-Dn.js → page-Dfaj9bx6.js} +1 -1
  89. zenml/zen_server/dashboard/assets/{page-DznxxpKA.js → page-DhlhT_XT.js} +1 -1
  90. zenml/zen_server/dashboard/assets/{page-CKPkbOoE.js → page-DqXw1YhM.js} +1 -1
  91. zenml/zen_server/dashboard/assets/{page-E9Mx9_rn.js → page-DtjangP-.js} +1 -1
  92. zenml/zen_server/dashboard/assets/{page-DQncGJeH.js → page-GJ63jkFi.js} +1 -1
  93. zenml/zen_server/dashboard/assets/{page-DSn1Jcvs.js → page-RtFcujP8.js} +1 -1
  94. zenml/zen_server/dashboard/assets/{page-HrebZOAM.js → page-T1JEtrlC.js} +1 -1
  95. zenml/zen_server/dashboard/assets/{page-DQzNmXk3.js → page-cS5O9iLC.js} +1 -1
  96. zenml/zen_server/dashboard/assets/{page-Dpw-xP4q.js → page-s1bm6Xid.js} +1 -1
  97. zenml/zen_server/dashboard/assets/{persist-Dkbp9MFP.js → persist-C98EYq5I.js} +1 -1
  98. zenml/zen_server/dashboard/assets/{persist-B3Hb8HDW.js → persist-FuiGbvJD.js} +1 -1
  99. zenml/zen_server/dashboard/assets/{primary-role-CjkpP9fL.js → primary-role-Bsu7s76U.js} +1 -1
  100. zenml/zen_server/dashboard/assets/{resource-tyes-list-CugyWUAE.js → resource-tyes-list-C7I0VL8e.js} +1 -1
  101. zenml/zen_server/dashboard/assets/{resource-type-tooltip-V-zdiLKz.js → resource-type-tooltip-2vjL6Wh9.js} +1 -1
  102. zenml/zen_server/dashboard/assets/{service-FjqYTmBw.js → service-BPdLzxxT.js} +1 -1
  103. zenml/zen_server/dashboard/assets/{service-connectors-nF4-OXB9.js → service-connectors-BkE8Ol4i.js} +1 -1
  104. zenml/zen_server/dashboard/assets/{sharedSchema-Br1JPr8d.js → sharedSchema-CKFbjwmg.js} +1 -1
  105. zenml/zen_server/dashboard/assets/{stack-detail-query-DnyMCdVE.js → stack-detail-query-JsrqFo3U.js} +1 -1
  106. zenml/zen_server/dashboard/assets/{update-current-user-mutation-B3Dpv3u6.js → update-current-user-mutation-C4CPf7vG.js} +1 -1
  107. zenml/zen_server/dashboard/assets/{update-server-settings-mutation-s7tlHN8s.js → update-server-settings-mutation-BYfIU-xU.js} +1 -1
  108. zenml/zen_server/dashboard/index.html +2 -2
  109. zenml/zen_server/routers/service_accounts_endpoints.py +24 -0
  110. zenml/zen_stores/migrations/versions/0.84.3_release.py +23 -0
  111. zenml/zen_stores/rest_zen_store.py +16 -5
  112. {zenml_nightly-0.84.2.dev20250826.dist-info → zenml_nightly-0.84.3.dev20250828.dist-info}/METADATA +2 -2
  113. {zenml_nightly-0.84.2.dev20250826.dist-info → zenml_nightly-0.84.3.dev20250828.dist-info}/RECORD +116 -115
  114. zenml/zen_server/dashboard/assets/form-C1jJgRy1.js +0 -1
  115. zenml/zen_server/dashboard/assets/page-1DXG7hp8.js +0 -1
  116. zenml/zen_server/dashboard/assets/page-C7Z2sDHE.js +0 -1
  117. zenml/zen_server/dashboard/assets/page-C8t9qLdV.js +0 -1
  118. zenml/zen_server/dashboard/assets/page-CVwTAlzd.js +0 -1
  119. {zenml_nightly-0.84.2.dev20250826.dist-info → zenml_nightly-0.84.3.dev20250828.dist-info}/LICENSE +0 -0
  120. {zenml_nightly-0.84.2.dev20250826.dist-info → zenml_nightly-0.84.3.dev20250828.dist-info}/WHEEL +0 -0
  121. {zenml_nightly-0.84.2.dev20250826.dist-info → zenml_nightly-0.84.3.dev20250828.dist-info}/entry_points.txt +0 -0
zenml/VERSION CHANGED
@@ -1 +1 @@
1
- 0.84.2.dev20250826
1
+ 0.84.3.dev20250828
zenml/cli/login.py CHANGED
@@ -254,7 +254,7 @@ def connect_to_server(
254
254
  # flow.
255
255
  cli_utils.declare(f"Authenticating to ZenML server '{url}'...")
256
256
  else:
257
- if refresh or not credentials_store.has_valid_authentication(url):
257
+ if refresh or not credentials_store.has_valid_credentials(url):
258
258
  cli_utils.declare(
259
259
  f"Authenticating to ZenML server '{url}' using the web "
260
260
  "login..."
@@ -331,6 +331,17 @@ def connect_to_pro_server(
331
331
 
332
332
  server_id, server_url, server_name = None, None, None
333
333
  login = False
334
+ credentials_store = get_credentials_store()
335
+
336
+ if api_key and api_key.startswith("ZENPROKEY_"):
337
+ if not pro_server:
338
+ raise ValueError(
339
+ "You must provide the name of the ZenML Pro server when "
340
+ "connecting with a ZenML Pro API key."
341
+ )
342
+ credentials_store.set_api_key(pro_api_url, api_key, is_zenml_pro=True)
343
+ api_key = None
344
+
334
345
  if not pro_server:
335
346
  login = True
336
347
  if api_key:
@@ -351,8 +362,7 @@ def connect_to_pro_server(
351
362
  else:
352
363
  server_url = pro_server
353
364
 
354
- credentials_store = get_credentials_store()
355
- if not credentials_store.has_valid_pro_authentication(pro_api_url):
365
+ if not credentials_store.has_valid_pro_credentials(pro_api_url):
356
366
  # Without valid ZenML Pro credentials, we can only connect to a ZenML
357
367
  # Pro server with an API key and we also need to know the URL of the
358
368
  # server to connect to.
@@ -375,10 +385,9 @@ def connect_to_pro_server(
375
385
 
376
386
  if login or refresh:
377
387
  # If we reached this point, then we need to start a new login flow.
378
- # We also need to remove all existing API tokens associated with the
379
- # target ZenML Pro API, otherwise they will continue to be used after
380
- # the re-login flow.
381
- credentials_store.clear_all_pro_tokens()
388
+ # We also need to remove all existing credentials, otherwise they will
389
+ # continue to be used after the re-login flow.
390
+ credentials_store.clear_all_credentials()
382
391
  try:
383
392
  token = web_login(
384
393
  pro_api_url=pro_api_url,
@@ -858,10 +867,21 @@ def login(
858
867
  )
859
868
  return
860
869
 
870
+ api_key_value: Optional[str] = None
871
+ if api_key:
872
+ # Read the API key from the user
873
+ api_key_value = click.prompt(
874
+ "Please enter the API key for the ZenML server",
875
+ type=str,
876
+ hide_input=True,
877
+ )
878
+ if api_key_value and api_key_value.startswith("ZENPROKEY_"):
879
+ pro = True
880
+
861
881
  if pro:
862
882
  connect_to_pro_server(
863
883
  pro_server=server,
864
- refresh=True,
884
+ api_key=api_key_value,
865
885
  pro_api_url=pro_api_url,
866
886
  verify_ssl=ssl_ca_cert
867
887
  if ssl_ca_cert is not None
@@ -877,15 +897,6 @@ def login(
877
897
  if not connected_to_local_server():
878
898
  current_non_local_server = store_cfg.url
879
899
 
880
- api_key_value: Optional[str] = None
881
- if api_key:
882
- # Read the API key from the user
883
- api_key_value = click.prompt(
884
- "Please enter the API key for the ZenML server",
885
- type=str,
886
- hide_input=True,
887
- )
888
-
889
900
  verify_ssl: Union[str, bool] = (
890
901
  ssl_ca_cert if ssl_ca_cert is not None else not no_verify_ssl
891
902
  )
@@ -1126,7 +1137,7 @@ def logout(
1126
1137
 
1127
1138
  pro_api_url = pro_api_url or ZENML_PRO_API_URL
1128
1139
  pro_api_url = pro_api_url.rstrip("/")
1129
- if credentials_store.has_valid_pro_authentication(pro_api_url):
1140
+ if credentials_store.get_pro_credentials(pro_api_url):
1130
1141
  credentials_store.clear_pro_credentials(pro_api_url)
1131
1142
  cli_utils.declare("Logged out from ZenML Pro.")
1132
1143
  else:
@@ -1141,7 +1152,7 @@ def logout(
1141
1152
  if credentials and credentials.pro_api_url == pro_api_url:
1142
1153
  gc.set_default_store()
1143
1154
 
1144
- credentials_store.clear_all_pro_tokens(pro_api_url)
1155
+ credentials_store.clear_all_credentials()
1145
1156
  cli_utils.declare("Logged out from all ZenML Pro servers.")
1146
1157
  return
1147
1158
 
zenml/cli/server.py CHANGED
@@ -217,12 +217,12 @@ def status() -> None:
217
217
  if server:
218
218
  if server.type == ServerType.PRO:
219
219
  # If connected to a ZenML Pro server, refresh the server info
220
- pro_credentials = credentials_store.get_pro_credentials(
221
- pro_api_url=server.pro_api_url or ZENML_PRO_API_URL,
222
- allow_expired=False,
223
- )
224
- if pro_credentials:
225
- pro_client = ZenMLProClient(pro_credentials.url)
220
+ if credentials_store.can_login(
221
+ server_url=server.pro_api_url or ZENML_PRO_API_URL,
222
+ ):
223
+ pro_client = ZenMLProClient(
224
+ server.pro_api_url or ZENML_PRO_API_URL
225
+ )
226
226
  pro_servers = pro_client.workspace.list(
227
227
  url=store_cfg.url, member_only=True
228
228
  )
@@ -240,10 +240,6 @@ def status() -> None:
240
240
  cli_utils.declare(
241
241
  f" ZenML Pro Organization: {server.organization_hyperlink}"
242
242
  )
243
- if pro_credentials:
244
- cli_utils.declare(
245
- f" ZenML Pro authentication: {pro_credentials.auth_status}"
246
- )
247
243
  else:
248
244
  cli_utils.declare(
249
245
  f"Connected to a remote ZenML server: `{server.dashboard_hyperlink}`"
@@ -138,6 +138,33 @@ class ServerCredentials(BaseModel):
138
138
  return ServerType.LOCAL
139
139
  return ServerType.REMOTE
140
140
 
141
+ @property
142
+ def can_refresh_token(self) -> bool:
143
+ """Check if the credentials can be used to refresh the API token.
144
+
145
+ A token refresh is only possible if long-lived credentials exist that
146
+ can be used to login and generate a new API token.
147
+
148
+ Returns:
149
+ True if the server credentials can be used to refresh the API token,
150
+ False otherwise.
151
+ """
152
+ return (
153
+ self.username is not None
154
+ and self.password is not None
155
+ or self.api_key is not None
156
+ or self.type == ServerType.LOCAL
157
+ )
158
+
159
+ @property
160
+ def has_valid_token(self) -> bool:
161
+ """Check if the API token is valid.
162
+
163
+ Returns:
164
+ True if the API token is valid, False otherwise.
165
+ """
166
+ return self.api_token is not None and not self.api_token.expired
167
+
141
168
  def update_server_info(
142
169
  self, server_info: Union[ServerModel, WorkspaceRead]
143
170
  ) -> None:
@@ -54,12 +54,15 @@ class CredentialsStore(metaclass=SingletonMetaClass):
54
54
  1. when the user runs `zenml login` to authenticate to a ZenML Pro
55
55
  server, the ZenML Pro API token fetched from the web login flow is
56
56
  stored in the Credentials Store.
57
- 2. when the user runs `zenml login` to authenticate to a regular ZenML
57
+ 2. when the user runs `zenml login` to authenticate to a ZenML Pro
58
+ server using an API key, the ZenML Pro API key is stored in the
59
+ Credentials Store.
60
+ 3. when the user runs `zenml login` to authenticate to a regular ZenML
58
61
  server with the web login flow, the ZenML server API token fetched
59
62
  through the web login flow is stored in the Credentials Store
60
- 3. when the user runs `zenml login` to authenticate to any ZenML server
63
+ 4. when the user runs `zenml login` to authenticate to any ZenML server
61
64
  using an API key, the API key is stored in the Credentials Store
62
- 4. when the REST zen store is initialized, it starts up not yet
65
+ 5. when the REST zen store is initialized, it starts up not yet
63
66
  authenticated. Then, if/when it needs to authenticate or re-authenticate
64
67
  to the remote server, it will use whatever form of credentials it finds
65
68
  in the Credentials Store, in order of priority:
@@ -71,8 +74,12 @@ class CredentialsStore(metaclass=SingletonMetaClass):
71
74
  * for ZenML servers that use an API key to authenticate, it will use
72
75
  that to fetch a short-lived ZenML Pro server API token that it also
73
76
  stores in the Credentials Store
74
- * for ZenML Pro servers, it exchanges the longer-lived ZenML Pro API
75
- token into a short lived ZenML Pro server API token
77
+ * for ZenML Pro servers:
78
+ * if a valid ZenML Pro API token is found, it exchanges it into
79
+ a short lived ZenML Pro server API token
80
+ * otherwise, if a ZenML Pro API key is found, it will use that
81
+ to fetch a short-lived ZenML Pro server API token that it also
82
+ stores in the Credentials Store
76
83
 
77
84
  Alongside credentials, the Credentials Store is also used to store
78
85
  additional server information:
@@ -290,6 +297,37 @@ class CredentialsStore(metaclass=SingletonMetaClass):
290
297
  self.check_and_reload_from_file()
291
298
  return self.credentials.get(server_url)
292
299
 
300
+ def has_valid_credentials(self, server_url: str) -> bool:
301
+ """Check if valid credentials exist for a specific server URL.
302
+
303
+ Args:
304
+ server_url: The server URL for which to check the authentication.
305
+
306
+ Returns:
307
+ True if the credentials store contains credentials that can be used
308
+ to login to the given server URL, False otherwise.
309
+ """
310
+ credentials = self.get_credentials(server_url)
311
+ if credentials and (
312
+ credentials.has_valid_token or credentials.can_refresh_token
313
+ ):
314
+ return True
315
+ return False
316
+
317
+ def get_pro_api_key(self, pro_api_url: str) -> Optional[str]:
318
+ """Retrieve an API key from the credentials store for a ZenML Pro API server.
319
+
320
+ Args:
321
+ pro_api_url: The URL of the ZenML Pro API server.
322
+
323
+ Returns:
324
+ The stored API key if it exists, None otherwise.
325
+ """
326
+ credential = self.get_pro_credentials(pro_api_url)
327
+ if credential and credential.api_key:
328
+ return credential.api_key
329
+ return None
330
+
293
331
  def get_pro_token(
294
332
  self, pro_api_url: str, allow_expired: bool = False
295
333
  ) -> Optional[APIToken]:
@@ -304,98 +342,58 @@ class CredentialsStore(metaclass=SingletonMetaClass):
304
342
  Returns:
305
343
  The stored token if it exists and is not expired, None otherwise.
306
344
  """
307
- credential = self.get_pro_credentials(pro_api_url, allow_expired)
345
+ credential = self.get_pro_credentials(pro_api_url)
308
346
  if credential:
309
- return credential.api_token
347
+ token = credential.api_token
348
+ if token and (not token.expired or allow_expired):
349
+ return token
310
350
  return None
311
351
 
312
352
  def get_pro_credentials(
313
- self, pro_api_url: str, allow_expired: bool = False
353
+ self, pro_api_url: str
314
354
  ) -> Optional[ServerCredentials]:
315
- """Retrieve a valid token from the credentials store for a ZenML Pro API server.
355
+ """Retrieve valid credentials from the credentials store for a ZenML Pro API server.
316
356
 
317
357
  Args:
318
358
  pro_api_url: The URL of the ZenML Pro API server.
319
- allow_expired: Whether to allow expired tokens to be returned. The
320
- default behavior is to return None if a token does exist but is
321
- expired.
322
359
 
323
360
  Returns:
324
- The stored credentials if they exist and are not expired, None otherwise.
361
+ The stored credentials if they exist, None otherwise.
325
362
  """
326
363
  credential = self.get_credentials(pro_api_url)
327
- if (
328
- credential
329
- and credential.type == ServerType.PRO_API
330
- and credential.api_token
331
- and (not credential.api_token.expired or allow_expired)
332
- ):
364
+ if credential and credential.type == ServerType.PRO_API:
333
365
  return credential
334
366
  return None
335
367
 
336
- def clear_pro_credentials(self, pro_api_url: str) -> None:
337
- """Delete the token from the store for a ZenML Pro API server.
338
-
339
- Args:
340
- pro_api_url: The URL of the ZenML Pro API server.
341
- """
342
- self.clear_token(pro_api_url)
343
-
344
- def clear_all_pro_tokens(
345
- self, pro_api_url: Optional[str] = None
346
- ) -> List[ServerCredentials]:
347
- """Delete all tokens from the store for ZenML Pro servers connected to a given API server.
368
+ def has_valid_pro_credentials(self, pro_api_url: str) -> bool:
369
+ """Check if valid credentials exist for a ZenML Pro API server.
348
370
 
349
371
  Args:
350
372
  pro_api_url: The URL of the ZenML Pro API server.
351
373
 
352
374
  Returns:
353
- A list of the credentials that were cleared.
354
- """
355
- credentials_to_clear = []
356
- for server_url, server in self.credentials.copy().items():
357
- if (
358
- server.type == ServerType.PRO
359
- and server.pro_api_url
360
- and (pro_api_url is None or server.pro_api_url == pro_api_url)
361
- ):
362
- if server.api_key:
363
- continue
364
- self.clear_token(server_url)
365
- credentials_to_clear.append(server)
366
- return credentials_to_clear
367
-
368
- def has_valid_authentication(self, url: str) -> bool:
369
- """Check if a valid authentication credential for the given server URL is stored.
370
-
371
- Args:
372
- url: The server URL for which to check the authentication.
373
-
374
- Returns:
375
- bool: True if a valid token or API key is stored, False otherwise.
375
+ True if the credentials store contains credentials that can be used
376
+ to login to the given ZenML Pro API server, False otherwise.
376
377
  """
377
- self.check_and_reload_from_file()
378
- credential = self.credentials.get(url)
379
-
380
- if not credential:
381
- return False
382
- if credential.api_key or (
383
- credential.username and credential.password is not None
378
+ credentials = self.get_pro_credentials(pro_api_url)
379
+ if credentials and (
380
+ credentials.has_valid_token or credentials.can_refresh_token
384
381
  ):
385
382
  return True
386
- token = credential.api_token
387
- return token is not None and not token.expired
383
+ return False
388
384
 
389
- def has_valid_pro_authentication(self, pro_api_url: str) -> bool:
390
- """Check if a valid token for a ZenML Pro API server is stored.
385
+ def clear_pro_credentials(self, pro_api_url: str) -> None:
386
+ """Delete the token from the store for a ZenML Pro API server.
391
387
 
392
388
  Args:
393
389
  pro_api_url: The URL of the ZenML Pro API server.
394
-
395
- Returns:
396
- bool: True if a valid token is stored, False otherwise.
397
390
  """
398
- return self.get_pro_token(pro_api_url) is not None
391
+ self.clear_token(pro_api_url)
392
+
393
+ def clear_all_credentials(self) -> None:
394
+ """Delete all stored credentials."""
395
+ self.credentials = {}
396
+ self._save_credentials()
399
397
 
400
398
  def can_login(self, server_url: str) -> bool:
401
399
  """Check if credentials to login to the given server exist.
@@ -407,22 +405,20 @@ class CredentialsStore(metaclass=SingletonMetaClass):
407
405
  True if the credentials store contains credentials that can be used
408
406
  to login to the given server URL, False otherwise.
409
407
  """
410
- self.check_and_reload_from_file()
411
408
  credentials = self.get_credentials(server_url)
412
409
  if not credentials:
413
410
  return False
414
411
 
415
- if credentials.api_key is not None:
416
- return True
417
- elif (
418
- credentials.username is not None
419
- and credentials.password is not None
420
- ):
412
+ if credentials.can_refresh_token:
421
413
  return True
422
- elif credentials.type == ServerType.PRO:
414
+
415
+ if credentials.type == ServerType.PRO:
423
416
  pro_api_url = credentials.pro_api_url or ZENML_PRO_API_URL
424
- pro_token = self.get_pro_token(pro_api_url, allow_expired=False)
425
- if pro_token:
417
+ pro_credentials = self.get_pro_credentials(pro_api_url)
418
+ if pro_credentials and (
419
+ pro_credentials.has_valid_token
420
+ or pro_credentials.can_refresh_token
421
+ ):
426
422
  return True
427
423
 
428
424
  return False
@@ -431,6 +427,7 @@ class CredentialsStore(metaclass=SingletonMetaClass):
431
427
  self,
432
428
  server_url: str,
433
429
  api_key: str,
430
+ is_zenml_pro: bool = False,
434
431
  ) -> None:
435
432
  """Store an API key in the credentials store for a specific server URL.
436
433
 
@@ -440,6 +437,7 @@ class CredentialsStore(metaclass=SingletonMetaClass):
440
437
  Args:
441
438
  server_url: The server URL for which the token is to be stored.
442
439
  api_key: The API key to store.
440
+ is_zenml_pro: Whether the API key is for the ZenML Pro API.
443
441
  """
444
442
  self.check_and_reload_from_file()
445
443
  credential = self.credentials.get(server_url)
@@ -450,9 +448,11 @@ class CredentialsStore(metaclass=SingletonMetaClass):
450
448
  credential.api_key = api_key
451
449
  credential.username = None
452
450
  credential.password = None
451
+ if is_zenml_pro:
452
+ credential.pro_api_url = server_url
453
453
  else:
454
454
  self.credentials[server_url] = ServerCredentials(
455
- url=server_url, api_key=api_key
455
+ url=server_url, api_key=api_key, pro_api_url=server_url
456
456
  )
457
457
 
458
458
  self._save_credentials()
zenml/login/pro/client.py CHANGED
@@ -34,6 +34,7 @@ from zenml.logger import get_logger
34
34
  from zenml.login.credentials import APIToken
35
35
  from zenml.login.credentials_store import get_credentials_store
36
36
  from zenml.login.pro.models import BaseRestAPIModel
37
+ from zenml.models.v2.misc.auth_models import OAuthTokenResponse
37
38
  from zenml.utils.singleton import SingletonMetaClass
38
39
  from zenml.zen_server.exceptions import exception_from_response
39
40
 
@@ -54,39 +55,54 @@ class ZenMLProClient(metaclass=SingletonMetaClass):
54
55
  """ZenML Pro client."""
55
56
 
56
57
  _url: str
57
- _api_token: APIToken
58
+ _api_token: Optional[APIToken] = None
58
59
  _session: Optional[requests.Session] = None
59
60
  _workspace: Optional["WorkspaceClient"] = None
60
61
  _organization: Optional["OrganizationClient"] = None
61
62
 
62
- def __init__(self, url: str, api_token: Optional[APIToken] = None) -> None:
63
+ def __init__(self, url: str) -> None:
63
64
  """Initialize the ZenML Pro client.
64
65
 
65
66
  Args:
66
67
  url: The URL of the ZenML Pro API server.
67
- api_token: The API token to use for authentication. If not provided,
68
- the token is fetched from the credentials store.
68
+ """
69
+ self._url = url
70
+
71
+ def authenticate(self) -> APIToken:
72
+ """Authenticate to ZenML Pro and return the API token.
73
+
74
+ Returns:
75
+ The API token.
69
76
 
70
77
  Raises:
71
- AuthorizationException: If no API token is provided and no token
72
- is found in the credentials store.
78
+ AuthorizationException: If the login fails.
73
79
  """
74
- self._url = url
75
- if api_token is None:
76
- logger.debug(
77
- "No ZenML Pro API token provided. Fetching from credentials "
78
- "store."
80
+ credentials_store = get_credentials_store()
81
+ pro_credentials = credentials_store.get_pro_credentials(self._url)
82
+ if pro_credentials is None:
83
+ raise AuthorizationException(
84
+ "No ZenML Pro credentials found. Please run 'zenml login' to "
85
+ "login to ZenML Pro."
79
86
  )
80
- api_token = get_credentials_store().get_token(
81
- server_url=self._url, allow_expired=True
87
+ if pro_credentials.has_valid_token:
88
+ assert pro_credentials.api_token is not None
89
+ return pro_credentials.api_token
90
+
91
+ if pro_credentials.can_refresh_token:
92
+ assert pro_credentials.api_key is not None
93
+ api_token = self.fetch_api_token(
94
+ self._url, pro_credentials.api_key
95
+ )
96
+ return credentials_store.set_token(
97
+ self._url,
98
+ api_token,
99
+ is_zenml_pro=True,
100
+ )
101
+ else:
102
+ raise AuthorizationException(
103
+ "Your ZenML Pro authentication has expired. Please run "
104
+ "'zenml login' to login again."
82
105
  )
83
- if api_token is None:
84
- raise AuthorizationException(
85
- "No ZenML Pro API token found. Please run 'zenml login' to "
86
- "login to ZenML Pro."
87
- )
88
-
89
- self._api_token = api_token
90
106
 
91
107
  @property
92
108
  def workspace(self) -> "WorkspaceClient":
@@ -121,20 +137,10 @@ class ZenMLProClient(metaclass=SingletonMetaClass):
121
137
  Returns:
122
138
  The API token.
123
139
  """
140
+ if self._api_token is None or self._api_token.expired:
141
+ self._api_token = self.authenticate()
124
142
  return self._api_token.access_token
125
143
 
126
- def raise_on_expired_api_token(self) -> None:
127
- """Raise an exception if the API token has expired.
128
-
129
- Raises:
130
- AuthorizationException: If the API token has expired.
131
- """
132
- if self._api_token and self._api_token.expired:
133
- raise AuthorizationException(
134
- "Your ZenML Pro authentication has expired. Please run "
135
- "'zenml login' to login again."
136
- )
137
-
138
144
  @property
139
145
  def session(self) -> requests.Session:
140
146
  """Authenticate to the ZenML Pro API server.
@@ -142,10 +148,6 @@ class ZenMLProClient(metaclass=SingletonMetaClass):
142
148
  Returns:
143
149
  A requests session with the authentication token.
144
150
  """
145
- # Check if the API token has expired before every call to the server.
146
- # This prevents unwanted authorization errors from being raised during
147
- # the call itself.
148
- self.raise_on_expired_api_token()
149
151
  if self._session is None:
150
152
  self._session = requests.Session()
151
153
  retries = Retry(backoff_factor=0.1, connect=5)
@@ -157,6 +159,35 @@ class ZenMLProClient(metaclass=SingletonMetaClass):
157
159
  logger.debug("Authenticated to ZenML Pro server.")
158
160
  return self._session
159
161
 
162
+ @classmethod
163
+ def fetch_api_token(cls, url: str, api_key: str) -> OAuthTokenResponse:
164
+ """Fetch the API token for a given ZenML Pro server URL.
165
+
166
+ Args:
167
+ url: ZenML Pro server URL
168
+ api_key: API key
169
+
170
+ Returns:
171
+ The API token.
172
+
173
+ Raises:
174
+ ValueError: if the response is not in the right format.
175
+ """
176
+ headers = {"Authorization": "Bearer " + api_key}
177
+ response = requests.post(
178
+ url + "/auth/login",
179
+ data={"password": api_key},
180
+ headers=headers,
181
+ timeout=10,
182
+ )
183
+ json_response = cls._handle_response(response)
184
+ if not isinstance(json_response, dict):
185
+ raise ValueError(
186
+ f"Bad response from API. Expected dict, got\n{json_response}"
187
+ )
188
+
189
+ return OAuthTokenResponse.model_validate(json_response)
190
+
160
191
  @staticmethod
161
192
  def _handle_response(response: requests.Response) -> Json:
162
193
  """Handle API response, translating http status codes to Exception.
@@ -215,10 +246,6 @@ class ZenMLProClient(metaclass=SingletonMetaClass):
215
246
 
216
247
  Returns:
217
248
  The parsed response.
218
-
219
- Raises:
220
- AuthorizationException: if the request fails due to an expired
221
- authentication token.
222
249
  """
223
250
  params = {k: str(v) for k, v in params.items()} if params else {}
224
251
 
@@ -226,21 +253,14 @@ class ZenMLProClient(metaclass=SingletonMetaClass):
226
253
  {source_context.name: source_context.get().value}
227
254
  )
228
255
 
229
- try:
230
- return self._handle_response(
231
- self.session.request(
232
- method,
233
- url,
234
- params=params,
235
- **kwargs,
236
- )
256
+ return self._handle_response(
257
+ self.session.request(
258
+ method,
259
+ url,
260
+ params=params,
261
+ **kwargs,
237
262
  )
238
- except AuthorizationException:
239
- # Check if this is caused by an expired API token.
240
- self.raise_on_expired_api_token()
241
-
242
- # If not, raise the exception.
243
- raise
263
+ )
244
264
 
245
265
  def get(
246
266
  self,
zenml/login/pro/utils.py CHANGED
@@ -39,9 +39,7 @@ def get_troubleshooting_instructions(url: str) -> str:
39
39
  if credentials and credentials.type == ServerType.PRO:
40
40
  pro_api_url = credentials.pro_api_url or ZENML_PRO_API_URL
41
41
 
42
- if pro_api_url and credentials_store.has_valid_pro_authentication(
43
- pro_api_url
44
- ):
42
+ if pro_api_url and credentials_store.can_login(pro_api_url):
45
43
  client = ZenMLProClient(pro_api_url)
46
44
 
47
45
  try:
@@ -42,7 +42,7 @@ def is_latest_zenml_version() -> bool:
42
42
  True in case the current running zenml code is the latest available version on PYPI, otherwise False.
43
43
 
44
44
  Raises:
45
- RuntimeError: In case something goe wrong
45
+ RuntimeError: In case something goes wrong
46
46
  """
47
47
  from zenml import __version__
48
48
 
@@ -1 +1 @@
1
- import{j as e}from"./@radix-B1sy0Lhr.js";import{B as s,r as t}from"./index-CO6UN3UX.js";import{E as r}from"./EmptyState-BMA34iVR.js";import{S as a}from"./help-CRPfbPTO.js";import{L as o}from"./@react-router-CHjLNlgw.js";import"./@tanstack-Dwlisomv.js";import"./@reactflow-gbyyU3kq.js";function d(){return e.jsx("div",{className:"flex min-h-screen w-full flex-col",children:e.jsx(r,{icon:e.jsx(a,{className:"h-[120px] w-[120px] fill-neutral-300"}),children:e.jsxs("div",{className:"text-center",children:[e.jsx("h1",{className:"mb-2 text-display-xs font-semibold",children:"We can't find the page you are looking for"}),e.jsx("p",{className:"text-lg text-theme-text-secondary",children:"You can try typing a different URL or we can bring you back to your Homepage."}),e.jsx("div",{className:"mt-5 flex justify-center",children:e.jsx(s,{size:"md",asChild:!0,children:e.jsx(o,{className:"w-min self-center whitespace-nowrap",to:t.projects.overview,children:e.jsx("span",{className:"px-0.5",children:"Go to Home"})})})})]})})})}export{d as default};
1
+ import{j as e}from"./@radix-B1sy0Lhr.js";import{B as s,r as t}from"./index-Ci5z7q6x.js";import{E as r}from"./EmptyState-BMA34iVR.js";import{S as a}from"./help-CRPfbPTO.js";import{L as o}from"./@react-router-CHjLNlgw.js";import"./@tanstack-Dwlisomv.js";import"./@reactflow-gbyyU3kq.js";function d(){return e.jsx("div",{className:"flex min-h-screen w-full flex-col",children:e.jsx(r,{icon:e.jsx(a,{className:"h-[120px] w-[120px] fill-neutral-300"}),children:e.jsxs("div",{className:"text-center",children:[e.jsx("h1",{className:"mb-2 text-display-xs font-semibold",children:"We can't find the page you are looking for"}),e.jsx("p",{className:"text-lg text-theme-text-secondary",children:"You can try typing a different URL or we can bring you back to your Homepage."}),e.jsx("div",{className:"mt-5 flex justify-center",children:e.jsx(s,{size:"md",asChild:!0,children:e.jsx(o,{className:"w-min self-center whitespace-nowrap",to:t.projects.overview,children:e.jsx("span",{className:"px-0.5",children:"Go to Home"})})})})]})})})}export{d as default};
@@ -1 +1 @@
1
- import{r as m,j as e}from"./@radix-B1sy0Lhr.js";import{J as g,K as p,aw as d,br as h}from"./index-CO6UN3UX.js";const x=m.forwardRef((r,t)=>{const{triggerChildren:a,children:i,onSelect:o,onOpenChange:l,icon:D,open:n,...s}=r;return e.jsxs(g,{open:n,onOpenChange:l,children:[e.jsx(p,{asChild:!0,children:e.jsx(d,{...s,className:"hover:cursor-pointer",icon:r.icon,ref:t,onSelect:c=>{c.preventDefault(),o&&o()},children:a})}),e.jsx(h,{children:i})]})});x.displayName="AlertDialogItem";export{x as A};
1
+ import{r as m,j as e}from"./@radix-B1sy0Lhr.js";import{J as g,K as p,aw as d,br as h}from"./index-Ci5z7q6x.js";const x=m.forwardRef((r,t)=>{const{triggerChildren:a,children:i,onSelect:o,onOpenChange:l,icon:D,open:n,...s}=r;return e.jsxs(g,{open:n,onOpenChange:l,children:[e.jsx(p,{asChild:!0,children:e.jsx(d,{...s,className:"hover:cursor-pointer",icon:r.icon,ref:t,onSelect:c=>{c.preventDefault(),o&&o()},children:a})}),e.jsx(h,{children:i})]})});x.displayName="AlertDialogItem";export{x as A};
@@ -1 +1 @@
1
- import{r as l,j as e}from"./@radix-B1sy0Lhr.js";import{L as j,M as R,N as w,O as C,J as b,K as y,B as O}from"./index-CO6UN3UX.js";import{S as h}from"./trash-D9LA4VFD.js";import{A as v}from"./AlertDialogDropdownItem-BpLj419i.js";import{D as f,a as p}from"./DeleteAlertDialog-CzhTVQaC.js";import{c as A,u as S}from"./bulk-delete-wdObjfps.js";import{u as T}from"./delete-run-XSre8ru-.js";const{ContextProvider:F,useContext:c}=A();function m(){const{setRowSelection:n}=c(),{mutateAsync:t}=T();async function s(r){await t({runId:r})}return S({deleteFn:s,queryKeyToInvalidate:["runs"],setRowSelection:n})}function H({id:n}){const[t,s]=l.useState(!1),[r,a]=l.useState(!1),i=l.useRef(null),u=l.useRef(null),{bulkDelete:x}=m();async function D(){await x([n]),d(!1)}function g(){u.current=i.current}function d(o){if(o===!1){a(!1),setTimeout(()=>{s(o)},200);return}s(o)}return e.jsxs(j,{onOpenChange:a,open:r,children:[e.jsx(R,{ref:i,children:e.jsx(w,{className:"h-4 w-4 fill-theme-text-tertiary"})}),e.jsx(C,{hidden:t,onCloseAutoFocus:o=>{u.current&&(u.current.focus(),u.current=null,o.preventDefault())},align:"end",sideOffset:7,children:e.jsx(v,{onSelect:g,open:t,onOpenChange:d,triggerChildren:"Delete",icon:e.jsx(h,{fill:"red"}),children:e.jsx(f,{title:"Delete Run",handleDelete:D,children:e.jsxs(p,{children:[e.jsx("p",{children:"Are you sure?"}),e.jsx("p",{children:"This action cannot be undone."})]})})})})]})}function I(){const[n,t]=l.useState(!1),{selectedRowCount:s,selectedRowIDs:r}=c(),{bulkDelete:a}=m();async function i(){await a(r),t(!1)}return e.jsxs(b,{open:n,onOpenChange:t,children:[e.jsx(y,{asChild:!0,children:e.jsxs(O,{className:"rounded-sharp border-y-0 bg-white",size:"md",emphasis:"subtle",intent:"secondary",children:[e.jsx(h,{className:"h-5 w-5 shrink-0 gap-1 fill-neutral-400"}),"Delete"]})}),e.jsx(f,{title:`Delete Run${s>=2?"s":""}`,handleDelete:i,children:e.jsxs(p,{children:[e.jsx("p",{children:"Are you sure?"}),e.jsx("p",{children:"This action cannot be undone."})]})})]})}function K(){const{selectedRowCount:n}=c();return e.jsxs("div",{className:"flex items-center divide-x divide-theme-border-moderate overflow-hidden rounded-md border border-theme-border-moderate",children:[e.jsx("div",{className:"bg-primary-25 px-2 py-1 font-semibold text-theme-text-brand",children:`${n} Run${n>1?"s":""} selected`}),e.jsx(I,{})]})}export{H as R,K as a,F as b,c as u};
1
+ import{r as l,j as e}from"./@radix-B1sy0Lhr.js";import{L as j,M as R,N as w,O as C,J as b,K as y,B as O}from"./index-Ci5z7q6x.js";import{S as h}from"./trash-D9LA4VFD.js";import{A as v}from"./AlertDialogDropdownItem-DuTp8JFP.js";import{D as f,a as p}from"./DeleteAlertDialog-C5z9RupH.js";import{c as A,u as S}from"./bulk-delete-Bgf_MsSj.js";import{u as T}from"./delete-run-MHYb1wuH.js";const{ContextProvider:F,useContext:c}=A();function m(){const{setRowSelection:n}=c(),{mutateAsync:t}=T();async function s(r){await t({runId:r})}return S({deleteFn:s,queryKeyToInvalidate:["runs"],setRowSelection:n})}function H({id:n}){const[t,s]=l.useState(!1),[r,a]=l.useState(!1),i=l.useRef(null),u=l.useRef(null),{bulkDelete:x}=m();async function D(){await x([n]),d(!1)}function g(){u.current=i.current}function d(o){if(o===!1){a(!1),setTimeout(()=>{s(o)},200);return}s(o)}return e.jsxs(j,{onOpenChange:a,open:r,children:[e.jsx(R,{ref:i,children:e.jsx(w,{className:"h-4 w-4 fill-theme-text-tertiary"})}),e.jsx(C,{hidden:t,onCloseAutoFocus:o=>{u.current&&(u.current.focus(),u.current=null,o.preventDefault())},align:"end",sideOffset:7,children:e.jsx(v,{onSelect:g,open:t,onOpenChange:d,triggerChildren:"Delete",icon:e.jsx(h,{fill:"red"}),children:e.jsx(f,{title:"Delete Run",handleDelete:D,children:e.jsxs(p,{children:[e.jsx("p",{children:"Are you sure?"}),e.jsx("p",{children:"This action cannot be undone."})]})})})})]})}function I(){const[n,t]=l.useState(!1),{selectedRowCount:s,selectedRowIDs:r}=c(),{bulkDelete:a}=m();async function i(){await a(r),t(!1)}return e.jsxs(b,{open:n,onOpenChange:t,children:[e.jsx(y,{asChild:!0,children:e.jsxs(O,{className:"rounded-sharp border-y-0 bg-white",size:"md",emphasis:"subtle",intent:"secondary",children:[e.jsx(h,{className:"h-5 w-5 shrink-0 gap-1 fill-neutral-400"}),"Delete"]})}),e.jsx(f,{title:`Delete Run${s>=2?"s":""}`,handleDelete:i,children:e.jsxs(p,{children:[e.jsx("p",{children:"Are you sure?"}),e.jsx("p",{children:"This action cannot be undone."})]})})]})}function K(){const{selectedRowCount:n}=c();return e.jsxs("div",{className:"flex items-center divide-x divide-theme-border-moderate overflow-hidden rounded-md border border-theme-border-moderate",children:[e.jsx("div",{className:"bg-primary-25 px-2 py-1 font-semibold text-theme-text-brand",children:`${n} Run${n>1?"s":""} selected`}),e.jsx(I,{})]})}export{H as R,K as a,F as b,c as u};