zenml-nightly 0.82.0.dev20250505__py3-none-any.whl → 0.82.0.dev20250506__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.
zenml/VERSION CHANGED
@@ -1 +1 @@
1
- 0.82.0.dev20250505
1
+ 0.82.0.dev20250506
@@ -20,12 +20,15 @@ from typing import TYPE_CHECKING, Dict, Optional
20
20
  from pydantic import BaseModel
21
21
 
22
22
  from zenml.config.docker_settings import DockerSettings
23
+ from zenml.logger import get_logger
23
24
  from zenml.utils import json_utils
24
25
 
25
26
  if TYPE_CHECKING:
26
27
  from zenml.code_repositories import BaseCodeRepository
27
28
  from zenml.stack import Stack
28
29
 
30
+ logger = get_logger(__name__)
31
+
29
32
 
30
33
  class BuildConfiguration(BaseModel):
31
34
  """Configuration of Docker builds.
@@ -99,6 +102,24 @@ class BuildConfiguration(BaseModel):
99
102
  with open(self.settings.dockerfile, "rb") as f:
100
103
  hash_.update(f.read())
101
104
 
105
+ if self.settings.parent_image and stack.container_registry:
106
+ digest = stack.container_registry.get_image_repo_digest(
107
+ self.settings.parent_image
108
+ )
109
+ if digest:
110
+ hash_.update(digest.encode())
111
+ else:
112
+ logger.warning(
113
+ "Unable to fetch parent image digest for image `%s`. "
114
+ "This may lead to ZenML reusing existing builds even "
115
+ "though a new version of the parent image has been "
116
+ "pushed. Most likely you can fix this error by making sure "
117
+ "the parent image is pushed to the container registry of "
118
+ "your active stack `%s`.",
119
+ self.settings.parent_image,
120
+ stack.name,
121
+ )
122
+
102
123
  return hash_.hexdigest()
103
124
 
104
125
  def should_include_files(
@@ -29,6 +29,7 @@ from pydantic import (
29
29
  )
30
30
 
31
31
  from zenml.constants import (
32
+ DEFAULT_HTTP_TIMEOUT,
32
33
  DEFAULT_REPORTABLE_RESOURCES,
33
34
  DEFAULT_ZENML_JWT_TOKEN_ALGORITHM,
34
35
  DEFAULT_ZENML_JWT_TOKEN_LEEWAY,
@@ -679,6 +680,7 @@ class ServerProConfiguration(BaseModel):
679
680
  organization_name: Optional[str] = None
680
681
  workspace_id: UUID
681
682
  workspace_name: Optional[str] = None
683
+ http_timeout: int = DEFAULT_HTTP_TIMEOUT
682
684
 
683
685
  @field_validator("api_url", "dashboard_url")
684
686
  @classmethod
@@ -160,6 +160,25 @@ class BaseContainerRegistry(AuthenticationMixin):
160
160
 
161
161
  return self._docker_client
162
162
 
163
+ def is_valid_image_name_for_registry(self, image_name: str) -> bool:
164
+ """Check if the image name is valid for the container registry.
165
+
166
+ Args:
167
+ image_name: The name of the image.
168
+
169
+ Returns:
170
+ `True` if the image name is valid for the container registry,
171
+ `False` otherwise.
172
+ """
173
+ # Remove prefixes to make sure this logic also works for DockerHub
174
+ image_name = image_name.removeprefix("index.docker.io/")
175
+ image_name = image_name.removeprefix("docker.io/")
176
+
177
+ registry_uri = self.config.uri.removeprefix("index.docker.io/")
178
+ registry_uri = registry_uri.removeprefix("docker.io/")
179
+
180
+ return image_name.startswith(registry_uri)
181
+
163
182
  def prepare_image_push(self, image_name: str) -> None:
164
183
  """Preparation before an image gets pushed.
165
184
 
@@ -183,7 +202,7 @@ class BaseContainerRegistry(AuthenticationMixin):
183
202
  ValueError: If the image name is not associated with this
184
203
  container registry.
185
204
  """
186
- if not image_name.startswith(self.config.uri):
205
+ if not self.is_valid_image_name_for_registry(image_name):
187
206
  raise ValueError(
188
207
  f"Docker image `{image_name}` does not belong to container "
189
208
  f"registry `{self.config.uri}`."
@@ -194,6 +213,25 @@ class BaseContainerRegistry(AuthenticationMixin):
194
213
  image_name, docker_client=self.docker_client
195
214
  )
196
215
 
216
+ def get_image_repo_digest(self, image_name: str) -> Optional[str]:
217
+ """Get the repository digest of an image.
218
+
219
+ Args:
220
+ image_name: The name of the image.
221
+
222
+ Returns:
223
+ The repository digest of the image.
224
+ """
225
+ if not self.is_valid_image_name_for_registry(image_name):
226
+ return None
227
+
228
+ try:
229
+ metadata = self.docker_client.images.get_registry_data(image_name)
230
+ except Exception:
231
+ return None
232
+
233
+ return cast(str, metadata.id.split(":")[-1])
234
+
197
235
 
198
236
  class BaseContainerRegistryFlavor(Flavor):
199
237
  """Base flavor for container registries."""
@@ -1,6 +1,7 @@
1
1
  """Utils concerning anything concerning the cloud control plane backend."""
2
2
 
3
3
  from datetime import datetime, timedelta
4
+ from threading import RLock
4
5
  from typing import Any, Dict, Optional
5
6
 
6
7
  import requests
@@ -26,6 +27,7 @@ class ZenMLCloudConnection:
26
27
  self._session: Optional[requests.Session] = None
27
28
  self._token: Optional[str] = None
28
29
  self._token_expires_at: Optional[datetime] = None
30
+ self._lock = RLock()
29
31
 
30
32
  def request(
31
33
  self,
@@ -55,13 +57,21 @@ class ZenMLCloudConnection:
55
57
  url = self._config.api_url + endpoint
56
58
 
57
59
  response = self.session.request(
58
- method=method, url=url, params=params, json=data, timeout=7
60
+ method=method,
61
+ url=url,
62
+ params=params,
63
+ json=data,
64
+ timeout=self._config.http_timeout,
59
65
  )
60
66
  if response.status_code == 401:
61
67
  # Refresh the auth token and try again
62
- self._clear_session()
68
+ self._reset_login()
63
69
  response = self.session.request(
64
- method=method, url=url, params=params, json=data, timeout=7
70
+ method=method,
71
+ url=url,
72
+ params=params,
73
+ json=data,
74
+ timeout=self._config.http_timeout,
65
75
  )
66
76
 
67
77
  try:
@@ -164,57 +174,90 @@ class ZenMLCloudConnection:
164
174
  Returns:
165
175
  A requests session with the authentication token.
166
176
  """
167
- if self._session is None:
168
- # Set up the session's connection pool size to match the server's
169
- # thread pool size. This allows the server to cache one connection
170
- # per thread, which means we can keep connections open for longer
171
- # and avoid the overhead of setting up a new connection for each
172
- # request.
173
- conn_pool_size = server_config().thread_pool_size
174
-
175
- self._session = requests.Session()
176
- token = self._fetch_auth_token()
177
- self._session.headers.update({"Authorization": "Bearer " + token})
178
- # Add the ZenML specific headers
179
- self._session.headers.update(get_zenml_headers())
180
-
181
- retries = Retry(
182
- total=5, backoff_factor=0.1, status_forcelist=[502, 504]
183
- )
184
- self._session.mount(
185
- "https://",
186
- HTTPAdapter(
187
- max_retries=retries,
188
- # We only use one connection pool to be cached because we
189
- # only communicate with one remote server (the control
190
- # plane)
191
- pool_connections=1,
192
- pool_maxsize=conn_pool_size,
193
- ),
177
+ with self._lock:
178
+ if self._session is None:
179
+ # Set up the session's connection pool size to match the server's
180
+ # thread pool size. This allows the server to cache one connection
181
+ # per thread, which means we can keep connections open for longer
182
+ # and avoid the overhead of setting up a new connection for each
183
+ # request.
184
+ conn_pool_size = server_config().thread_pool_size
185
+
186
+ self._session = requests.Session()
187
+ # Add the ZenML specific headers
188
+ self._session.headers.update(get_zenml_headers())
189
+
190
+ retries = Retry(
191
+ connect=5,
192
+ read=8,
193
+ redirect=3,
194
+ status=10,
195
+ allowed_methods=[
196
+ "HEAD",
197
+ "GET",
198
+ "PUT",
199
+ "PATCH",
200
+ "POST",
201
+ "DELETE",
202
+ "OPTIONS",
203
+ ],
204
+ status_forcelist=[
205
+ 408, # Request Timeout
206
+ 429, # Too Many Requests
207
+ 502, # Bad Gateway
208
+ 503, # Service Unavailable
209
+ 504, # Gateway Timeout
210
+ ],
211
+ other=3,
212
+ backoff_factor=0.5,
213
+ )
214
+
215
+ self._session.mount(
216
+ "https://",
217
+ HTTPAdapter(
218
+ max_retries=retries,
219
+ # We only use one connection pool to be cached because we
220
+ # only communicate with one remote server (the control
221
+ # plane)
222
+ pool_connections=1,
223
+ pool_maxsize=conn_pool_size,
224
+ ),
225
+ )
226
+
227
+ # Login to the ZenML Pro Management Plane. Calling this will fetch
228
+ # the active session token. It will also refresh the token if it is
229
+ # going to expire soon or if it is already expired.
230
+ access_token = self._fetch_auth_token(session=self._session)
231
+ self._session.headers.update(
232
+ {"Authorization": "Bearer " + access_token}
194
233
  )
195
234
 
196
- return self._session
235
+ return self._session
197
236
 
198
- def _clear_session(self) -> None:
199
- """Clear the authentication session."""
200
- self._session = None
201
- self._token = None
202
- self._token_expires_at = None
237
+ def _reset_login(self) -> None:
238
+ """Force a new login to the ZenML Pro Management Plane."""
239
+ with self._lock:
240
+ self._token = None
241
+ self._token_expires_at = None
203
242
 
204
- def _fetch_auth_token(self) -> str:
243
+ def _fetch_auth_token(self, session: requests.Session) -> str:
205
244
  """Fetch an auth token from the Cloud API.
206
245
 
207
- Raises:
208
- RuntimeError: If the auth token can't be fetched.
246
+ Args:
247
+ session: The session to use to fetch the auth token.
209
248
 
210
249
  Returns:
211
- Auth token.
250
+ The auth token.
251
+
252
+ Raises:
253
+ RuntimeError: If the auth token can't be fetched.
212
254
  """
213
255
  if (
214
256
  self._token is not None
215
257
  and self._token_expires_at is not None
216
258
  and utc_now() + timedelta(minutes=5) < self._token_expires_at
217
259
  ):
260
+ # Already logged in and token is still valid
218
261
  return self._token
219
262
 
220
263
  # Get an auth token from the Cloud API
@@ -229,9 +272,13 @@ class ZenMLCloudConnection:
229
272
  "audience": self._config.oauth2_audience,
230
273
  "grant_type": "client_credentials",
231
274
  }
275
+
232
276
  try:
233
- response = requests.post(
234
- login_url, headers=headers, data=payload, timeout=7
277
+ response = session.post(
278
+ login_url,
279
+ headers=headers,
280
+ data=payload,
281
+ timeout=self._config.http_timeout,
235
282
  )
236
283
  response.raise_for_status()
237
284
  except Exception as e:
@@ -253,8 +300,8 @@ class ZenMLCloudConnection:
253
300
  "Could not fetch auth token from the Cloud API."
254
301
  )
255
302
 
256
- self._token = access_token
257
303
  self._token_expires_at = utc_now() + timedelta(seconds=expires_in)
304
+ self._token = access_token
258
305
 
259
306
  assert self._token is not None
260
307
  return self._token
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: zenml-nightly
3
- Version: 0.82.0.dev20250505
3
+ Version: 0.82.0.dev20250506
4
4
  Summary: ZenML: Write production-ready ML code.
5
5
  License: Apache-2.0
6
6
  Keywords: machine learning,production,pipeline,mlops,devops
@@ -112,7 +112,7 @@ Requires-Dist: ruff (>=0.1.7) ; extra == "templates" or extra == "dev"
112
112
  Requires-Dist: s3fs (>=2022.11.0,!=2025.3.1) ; extra == "s3fs"
113
113
  Requires-Dist: sagemaker (>=2.237.3) ; extra == "sagemaker"
114
114
  Requires-Dist: secure (>=0.3.0,<0.4.0) ; extra == "server"
115
- Requires-Dist: setuptools
115
+ Requires-Dist: setuptools (>=70.0.0)
116
116
  Requires-Dist: sqlalchemy (>=2.0.0,<3.0.0)
117
117
  Requires-Dist: sqlalchemy_utils
118
118
  Requires-Dist: sqlmodel (==0.0.18)
@@ -1,5 +1,5 @@
1
1
  zenml/README.md,sha256=827dekbOWAs1BpW7VF1a4d7EbwPbjwccX-2zdXBENZo,1777
2
- zenml/VERSION,sha256=xIFP7SS2Bk0B5oVwe5sVgUD_kvbgpml_jBENg4ew1rE,19
2
+ zenml/VERSION,sha256=ppUfeGUhs-NasvRcl6HFrQq-3Bkc5Mvd-jli7V03Ufs,19
3
3
  zenml/__init__.py,sha256=CKEyepFK-7akXYiMrNVh92Nb01Cjs23w4_YyI6sgdc8,2242
4
4
  zenml/actions/__init__.py,sha256=mrt6wPo73iKRxK754_NqsGyJ3buW7RnVeIGXr1xEw8Y,681
5
5
  zenml/actions/base_action.py,sha256=UcaHev6BTuLDwuswnyaPjdA8AgUqB5xPZ-lRtuvf2FU,25553
@@ -64,7 +64,7 @@ zenml/code_repositories/git/local_git_repository_context.py,sha256=hfX7zVDQ27Le0
64
64
  zenml/code_repositories/local_repository_context.py,sha256=1VyiYkJBDVg0iGusgRQDToGRPJuu9lx7jTBDpplukDg,2816
65
65
  zenml/config/__init__.py,sha256=DZEic7euSbwI9Yb3FMRQhTgfhqz-C6OdAiYmOb0-opI,1519
66
66
  zenml/config/base_settings.py,sha256=itoLqc1cOwEYhgSGdZmSKSaBevQkvYH7NQh7PUamazc,1700
67
- zenml/config/build_configuration.py,sha256=vpea4cXTWfxRyijMFMo3U6jtHpC7wgL01NAIzp721RI,5263
67
+ zenml/config/build_configuration.py,sha256=tEAIXwAGS8Flxo4mK5BGHsBk7LPWW16MOa6oRDymZ7k,6172
68
68
  zenml/config/compiler.py,sha256=jzyyTmM8nTxtpIVTSjZ-jrVOBJ1roVoJ3fs1A1nwv9M,24130
69
69
  zenml/config/constants.py,sha256=QvSgMwXWxtspcJ45CrFDP1ZY3w6gS3bIhXLOtIDAbZA,713
70
70
  zenml/config/docker_settings.py,sha256=w62SDW2Qo5jFxcikHr8p-Lk6ymC2ieh3MnEfkte9Lf0,13322
@@ -77,7 +77,7 @@ zenml/config/retry_config.py,sha256=4UH1xqw0G6fSEbXSNKfmiFEkwadxQef9BGMe3JBm6NI,
77
77
  zenml/config/schedule.py,sha256=qtMWa-mEo7jIKvDzQUstMwe57gdbvyWAQ7ggsoddbCA,5349
78
78
  zenml/config/secret_reference_mixin.py,sha256=YvY68MTd1gE23IVprf0BLkNn62hoxcvb5nqGgc8jMkU,5871
79
79
  zenml/config/secrets_store_config.py,sha256=y05zqyQhr_DGrs3IfBGa_FRoZ043hSYRT5wzrx-zHTU,2818
80
- zenml/config/server_config.py,sha256=Kns2ul12Zt4Y4ByKDJ4tgBrmUMvSrnxrc3bXRr5iQnw,31487
80
+ zenml/config/server_config.py,sha256=xLBOs_fWUM_Imk2IDzLPC1aVUSpwYYRobYAYC9hd2qE,31558
81
81
  zenml/config/settings_resolver.py,sha256=PR9BRm_x1dy7nVKa9UqpeFdck8IEATSW6aWT8FKd-DI,4278
82
82
  zenml/config/source.py,sha256=RzUw8lin8QztUjz-AdoCzVM5Om_cSSPuroaPx-qAO4w,8226
83
83
  zenml/config/step_configurations.py,sha256=mngjobhHRj88f3klMdz6iw2mOj9wzYUPIV8Rp_2hV3g,10433
@@ -88,7 +88,7 @@ zenml/console.py,sha256=hj_KerPQKwnyKACj0ehSqUQX0mGVCJBKE1QvCt6ik3A,1160
88
88
  zenml/constants.py,sha256=ZVwWVxE2h54cNbJDyujJmhAc8w3J7I9MkWHOfP4HN9A,16431
89
89
  zenml/container_registries/__init__.py,sha256=ZSPbBIOnzhg88kQSpYgKe_POLuru14m629665-kAVAA,2200
90
90
  zenml/container_registries/azure_container_registry.py,sha256=t1sfDa94Vzbyqtb1iPFNutJ2EXV5_p9CUNITasoiQ70,2667
91
- zenml/container_registries/base_container_registry.py,sha256=6c2e32wuqxYHJXm5OV2LY1MtX9yopB7WZtes9fmTAz0,7625
91
+ zenml/container_registries/base_container_registry.py,sha256=-9RIkD6oXNPaU59R3PB_PtyCqsFoLPLSn5xYZmEmzbc,8915
92
92
  zenml/container_registries/default_container_registry.py,sha256=_7lh2tQvoRgbQlSATc87Wzzl9dQzdTj8XvaTByTWKD4,1890
93
93
  zenml/container_registries/dockerhub_container_registry.py,sha256=RtdP-xqqTCX5-s9g3bYI_asTqs6BHBsTWeg3tS_Rg00,2684
94
94
  zenml/container_registries/gcp_container_registry.py,sha256=PNypXzqQ6RWDYP9_cPQUZDTf3NP6fxlsZWFD1Xu3jt4,2654
@@ -805,7 +805,7 @@ zenml/utils/yaml_utils.py,sha256=747M_BTf3lcHFgJDF8RJxnnGIbCU8e9YxBBMmoQ5O_U,583
805
805
  zenml/zen_server/__init__.py,sha256=WyltI9TzFW2mEHZVOs6alLWMCQrrZaFALtrQXs83STA,1355
806
806
  zenml/zen_server/auth.py,sha256=5EDqBb0HZJ8zr6OhwiTUcWkqoBFIEl3vfyit232UoOo,40259
807
807
  zenml/zen_server/cache.py,sha256=Tc4TSugmsU1bhThxlYfE8rv0KmltIX1CcVHgzrJ0Eus,6633
808
- zenml/zen_server/cloud_utils.py,sha256=nHCJjO4jRaiZiVIMbN339xG5_uLKzkRPk-FoId0hhx8,9307
808
+ zenml/zen_server/cloud_utils.py,sha256=qKMtHjnkVfAk_yxF2K9iC0TkMCjp8s0gdz_cEsbB4DE,10908
809
809
  zenml/zen_server/csrf.py,sha256=Jsbi_IKriWCOuquSYTOEWqSXiGORJATIhR5Wrkm4XzQ,2684
810
810
  zenml/zen_server/dashboard/assets/404-D4aYbspS.js,sha256=w7_KToWud6ST-NPC7zVqs4lFHLr8_q95w3pUYc-W0xo,1033
811
811
  zenml/zen_server/dashboard/assets/@radix-C7hRs6Kx.js,sha256=cNA9UX8LGrKUQubGrls3E3Wq1fCrlK51W14yh22FE_U,296700
@@ -1318,8 +1318,8 @@ zenml/zen_stores/secrets_stores/sql_secrets_store.py,sha256=LPFW757WCJLP1S8vrvjs
1318
1318
  zenml/zen_stores/sql_zen_store.py,sha256=biOoDb2_zYmpsN-J-FSlKICYdwM9KDIe-_KN_yDf_mA,441414
1319
1319
  zenml/zen_stores/template_utils.py,sha256=GWBP5QEOyvhzndS_MLPmvh28sQaOPpPoZFXCIX9CRL4,9065
1320
1320
  zenml/zen_stores/zen_store_interface.py,sha256=fF_uL_FplnvGvM5o3jOQ8i1zHXhuhKLL2n4nvIKSR7E,92090
1321
- zenml_nightly-0.82.0.dev20250505.dist-info/LICENSE,sha256=wbnfEnXnafPbqwANHkV6LUsPKOtdpsd-SNw37rogLtc,11359
1322
- zenml_nightly-0.82.0.dev20250505.dist-info/METADATA,sha256=cnfux-P5M0Zllz1JYwFHgtBslGfFDkFSzvpzATb5wAQ,24304
1323
- zenml_nightly-0.82.0.dev20250505.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
1324
- zenml_nightly-0.82.0.dev20250505.dist-info/entry_points.txt,sha256=QK3ETQE0YswAM2mWypNMOv8TLtr7EjnqAFq1br_jEFE,43
1325
- zenml_nightly-0.82.0.dev20250505.dist-info/RECORD,,
1321
+ zenml_nightly-0.82.0.dev20250506.dist-info/LICENSE,sha256=wbnfEnXnafPbqwANHkV6LUsPKOtdpsd-SNw37rogLtc,11359
1322
+ zenml_nightly-0.82.0.dev20250506.dist-info/METADATA,sha256=_sWp9WjyqeWPdnHJ3RUVzjNOID0vYkZhrN-yKVh7tuM,24315
1323
+ zenml_nightly-0.82.0.dev20250506.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
1324
+ zenml_nightly-0.82.0.dev20250506.dist-info/entry_points.txt,sha256=QK3ETQE0YswAM2mWypNMOv8TLtr7EjnqAFq1br_jEFE,43
1325
+ zenml_nightly-0.82.0.dev20250506.dist-info/RECORD,,