zenml-nightly 0.73.0.dev20250127__py3-none-any.whl → 0.73.0.dev20250129__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.
@@ -13,11 +13,11 @@
13
13
  # permissions and limitations under the License.
14
14
  """Models representing schedules."""
15
15
 
16
- import datetime
16
+ from datetime import datetime, timedelta, timezone
17
17
  from typing import Dict, Optional, Union
18
18
  from uuid import UUID
19
19
 
20
- from pydantic import Field, model_validator
20
+ from pydantic import Field, field_validator, model_validator
21
21
 
22
22
  from zenml.constants import STR_FIELD_MAX_LENGTH
23
23
  from zenml.logger import get_logger
@@ -46,15 +46,36 @@ class ScheduleRequest(WorkspaceScopedRequest):
46
46
  active: bool
47
47
 
48
48
  cron_expression: Optional[str] = None
49
- start_time: Optional[datetime.datetime] = None
50
- end_time: Optional[datetime.datetime] = None
51
- interval_second: Optional[datetime.timedelta] = None
49
+ start_time: Optional[datetime] = None
50
+ end_time: Optional[datetime] = None
51
+ interval_second: Optional[timedelta] = None
52
52
  catchup: bool = False
53
- run_once_start_time: Optional[datetime.datetime] = None
53
+ run_once_start_time: Optional[datetime] = None
54
54
 
55
55
  orchestrator_id: Optional[UUID]
56
56
  pipeline_id: Optional[UUID]
57
57
 
58
+ @field_validator(
59
+ "start_time", "end_time", "run_once_start_time", mode="after"
60
+ )
61
+ @classmethod
62
+ def _ensure_tzunaware_utc(
63
+ cls, value: Optional[datetime]
64
+ ) -> Optional[datetime]:
65
+ """Ensures that all datetimes are timezone unaware and in UTC time.
66
+
67
+ Args:
68
+ value: The datetime.
69
+
70
+ Returns:
71
+ The datetime in UTC time without timezone.
72
+ """
73
+ if value and value.tzinfo:
74
+ value = value.astimezone(timezone.utc)
75
+ value = value.replace(tzinfo=None)
76
+
77
+ return value
78
+
58
79
  @model_validator(mode="after")
59
80
  def _ensure_cron_or_periodic_schedule_configured(
60
81
  self,
@@ -108,11 +129,11 @@ class ScheduleUpdate(BaseUpdate):
108
129
  name: Optional[str] = None
109
130
  active: Optional[bool] = None
110
131
  cron_expression: Optional[str] = None
111
- start_time: Optional[datetime.datetime] = None
112
- end_time: Optional[datetime.datetime] = None
113
- interval_second: Optional[datetime.timedelta] = None
132
+ start_time: Optional[datetime] = None
133
+ end_time: Optional[datetime] = None
134
+ interval_second: Optional[timedelta] = None
114
135
  catchup: Optional[bool] = None
115
- run_once_start_time: Optional[datetime.datetime] = None
136
+ run_once_start_time: Optional[datetime] = None
116
137
  orchestrator_id: Optional[UUID] = None
117
138
  pipeline_id: Optional[UUID] = None
118
139
 
@@ -125,11 +146,11 @@ class ScheduleResponseBody(WorkspaceScopedResponseBody):
125
146
 
126
147
  active: bool
127
148
  cron_expression: Optional[str] = None
128
- start_time: Optional[datetime.datetime] = None
129
- end_time: Optional[datetime.datetime] = None
130
- interval_second: Optional[datetime.timedelta] = None
149
+ start_time: Optional[datetime] = None
150
+ end_time: Optional[datetime] = None
151
+ interval_second: Optional[timedelta] = None
131
152
  catchup: bool = False
132
- run_once_start_time: Optional[datetime.datetime] = None
153
+ run_once_start_time: Optional[datetime] = None
133
154
 
134
155
 
135
156
  class ScheduleResponseMetadata(WorkspaceScopedResponseMetadata):
@@ -217,7 +238,7 @@ class ScheduleResponse(
217
238
  return self.get_body().cron_expression
218
239
 
219
240
  @property
220
- def start_time(self) -> Optional[datetime.datetime]:
241
+ def start_time(self) -> Optional[datetime]:
221
242
  """The `start_time` property.
222
243
 
223
244
  Returns:
@@ -226,7 +247,7 @@ class ScheduleResponse(
226
247
  return self.get_body().start_time
227
248
 
228
249
  @property
229
- def end_time(self) -> Optional[datetime.datetime]:
250
+ def end_time(self) -> Optional[datetime]:
230
251
  """The `end_time` property.
231
252
 
232
253
  Returns:
@@ -235,7 +256,7 @@ class ScheduleResponse(
235
256
  return self.get_body().end_time
236
257
 
237
258
  @property
238
- def run_once_start_time(self) -> Optional[datetime.datetime]:
259
+ def run_once_start_time(self) -> Optional[datetime]:
239
260
  """The `run_once_start_time` property.
240
261
 
241
262
  Returns:
@@ -244,7 +265,7 @@ class ScheduleResponse(
244
265
  return self.get_body().run_once_start_time
245
266
 
246
267
  @property
247
- def interval_second(self) -> Optional[datetime.timedelta]:
268
+ def interval_second(self) -> Optional[timedelta]:
248
269
  """The `interval_second` property.
249
270
 
250
271
  Returns:
@@ -313,10 +334,10 @@ class ScheduleFilter(WorkspaceScopedFilter):
313
334
  default=None,
314
335
  description="The cron expression, describing the schedule",
315
336
  )
316
- start_time: Optional[Union[datetime.datetime, str]] = Field(
337
+ start_time: Optional[Union[datetime, str]] = Field(
317
338
  default=None, description="Start time", union_mode="left_to_right"
318
339
  )
319
- end_time: Optional[Union[datetime.datetime, str]] = Field(
340
+ end_time: Optional[Union[datetime, str]] = Field(
320
341
  default=None, description="End time", union_mode="left_to_right"
321
342
  )
322
343
  interval_second: Optional[Optional[float]] = Field(
@@ -332,7 +353,7 @@ class ScheduleFilter(WorkspaceScopedFilter):
332
353
  default=None,
333
354
  description="Name of the schedule",
334
355
  )
335
- run_once_start_time: Optional[Union[datetime.datetime, str]] = Field(
356
+ run_once_start_time: Optional[Union[datetime, str]] = Field(
336
357
  default=None,
337
358
  description="The time at which the schedule should run once",
338
359
  union_mode="left_to_right",
@@ -30,7 +30,6 @@ from zenml.code_repositories import BaseCodeRepository
30
30
  from zenml.logger import get_logger
31
31
  from zenml.models import (
32
32
  BuildItem,
33
- CodeReferenceRequest,
34
33
  PipelineBuildBase,
35
34
  PipelineBuildRequest,
36
35
  PipelineBuildResponse,
@@ -362,6 +361,7 @@ def create_pipeline_build(
362
361
  item_key = checksums[checksum]
363
362
  image_name_or_digest = images[item_key].image
364
363
  contains_code = images[item_key].contains_code
364
+ requires_code_download = images[item_key].requires_code_download
365
365
  dockerfile = images[item_key].dockerfile
366
366
  requirements = images[item_key].requirements
367
367
  else:
@@ -373,7 +373,7 @@ def create_pipeline_build(
373
373
  include_files = build_config.should_include_files(
374
374
  code_repository=code_repository,
375
375
  )
376
- download_files = build_config.should_download_files(
376
+ requires_code_download = build_config.should_download_files(
377
377
  code_repository=code_repository,
378
378
  )
379
379
  pass_code_repo = (
@@ -391,7 +391,6 @@ def create_pipeline_build(
391
391
  tag=tag,
392
392
  stack=stack,
393
393
  include_files=include_files,
394
- download_files=download_files,
395
394
  entrypoint=build_config.entrypoint,
396
395
  extra_files=build_config.extra_files,
397
396
  code_repository=code_repository if pass_code_repo else None,
@@ -404,7 +403,7 @@ def create_pipeline_build(
404
403
  requirements=requirements,
405
404
  settings_checksum=checksum,
406
405
  contains_code=contains_code,
407
- requires_code_download=download_files,
406
+ requires_code_download=requires_code_download,
408
407
  )
409
408
  checksums[checksum] = combined_key
410
409
 
@@ -537,6 +536,14 @@ def verify_local_repository_context(
537
536
  )
538
537
  code_repository = BaseCodeRepository.from_model(model)
539
538
 
539
+ if will_download_from_code_repository(
540
+ deployment=deployment, local_repo_context=local_repo_context
541
+ ):
542
+ logger.info(
543
+ "Using code repository `%s` to download code for this run.",
544
+ model.name,
545
+ )
546
+
540
547
  return code_repository
541
548
 
542
549
 
@@ -695,14 +702,15 @@ def compute_stack_checksum(stack: StackResponse) -> str:
695
702
  def should_upload_code(
696
703
  deployment: PipelineDeploymentBase,
697
704
  build: Optional[PipelineBuildResponse],
698
- code_reference: Optional[CodeReferenceRequest],
705
+ can_download_from_code_repository: bool,
699
706
  ) -> bool:
700
707
  """Checks whether the current code should be uploaded for the deployment.
701
708
 
702
709
  Args:
703
710
  deployment: The deployment.
704
711
  build: The build for the deployment.
705
- code_reference: The code reference for the deployment.
712
+ can_download_from_code_repository: Whether the code can be downloaded
713
+ from a code repository.
706
714
 
707
715
  Returns:
708
716
  Whether the current code should be uploaded for the deployment.
@@ -718,7 +726,7 @@ def should_upload_code(
718
726
  docker_settings = step.config.docker_settings
719
727
 
720
728
  if (
721
- code_reference
729
+ can_download_from_code_repository
722
730
  and docker_settings.allow_download_from_code_repository
723
731
  ):
724
732
  # No upload needed for this step
@@ -728,3 +736,31 @@ def should_upload_code(
728
736
  return True
729
737
 
730
738
  return False
739
+
740
+
741
+ def will_download_from_code_repository(
742
+ deployment: PipelineDeploymentBase,
743
+ local_repo_context: "LocalRepositoryContext",
744
+ ) -> bool:
745
+ """Checks whether a code repository will be used to download code.
746
+
747
+ Args:
748
+ deployment: The deployment.
749
+ local_repo_context: The local repository context.
750
+
751
+ Returns:
752
+ Whether a code repository will be used to download code.
753
+ """
754
+ if not build_required(deployment=deployment):
755
+ return False
756
+
757
+ if local_repo_context.has_local_changes:
758
+ return False
759
+
760
+ for step in deployment.step_configurations.values():
761
+ docker_settings = step.config.docker_settings
762
+
763
+ if docker_settings.allow_download_from_code_repository:
764
+ return True
765
+
766
+ return False
@@ -701,6 +701,7 @@ To avoid this consider setting pipeline parameters only in one place (config or
701
701
  code_repository = build_utils.verify_local_repository_context(
702
702
  deployment=deployment, local_repo_context=local_repo_context
703
703
  )
704
+ can_download_from_code_repository = code_repository is not None
704
705
 
705
706
  if prevent_build_reuse:
706
707
  logger.warning(
@@ -737,7 +738,7 @@ To avoid this consider setting pipeline parameters only in one place (config or
737
738
  if build_utils.should_upload_code(
738
739
  deployment=deployment,
739
740
  build=build_model,
740
- code_reference=code_reference,
741
+ can_download_from_code_repository=can_download_from_code_repository,
741
742
  ):
742
743
  code_archive = code_utils.CodeArchive(
743
744
  root=source_utils.get_source_root()
zenml/stack/flavor.py CHANGED
@@ -13,11 +13,13 @@
13
13
  # permissions and limitations under the License.
14
14
  """Base ZenML Flavor implementation."""
15
15
 
16
+ import os
16
17
  from abc import abstractmethod
17
18
  from typing import Any, Dict, Optional, Type, cast
18
19
 
19
20
  from zenml.client import Client
20
21
  from zenml.enums import StackComponentType
22
+ from zenml.exceptions import CustomFlavorImportError
21
23
  from zenml.models import (
22
24
  FlavorRequest,
23
25
  FlavorResponse,
@@ -126,10 +128,34 @@ class Flavor:
126
128
  Args:
127
129
  flavor_model: The model to load from.
128
130
 
131
+ Raises:
132
+ CustomFlavorImportError: If the custom flavor can't be imported.
133
+ ImportError: If the flavor can't be imported.
134
+
129
135
  Returns:
130
136
  The loaded flavor.
131
137
  """
132
- flavor = source_utils.load(flavor_model.source)()
138
+ try:
139
+ flavor = source_utils.load(flavor_model.source)()
140
+ except (ModuleNotFoundError, ImportError, NotImplementedError) as err:
141
+ if flavor_model.is_custom:
142
+ flavor_module, _ = flavor_model.source.rsplit(".")
143
+ expected_file_path = os.path.join(
144
+ source_utils.get_source_root(),
145
+ flavor_module.replace(".", os.path.sep),
146
+ )
147
+ raise CustomFlavorImportError(
148
+ f"Couldn't import custom flavor {flavor_model.name}: "
149
+ f"{err}. Make sure the custom flavor class "
150
+ f"`{flavor_model.source}` is importable. If it is part of "
151
+ "a library, make sure it is installed. If "
152
+ "it is a local code file, make sure it exists at "
153
+ f"`{expected_file_path}.py`."
154
+ )
155
+ else:
156
+ raise ImportError(
157
+ f"Couldn't import flavor {flavor_model.name}: {err}"
158
+ )
133
159
  return cast(Flavor, flavor)
134
160
 
135
161
  def to_model(
@@ -402,16 +402,10 @@ class StackComponent:
402
402
  Raises:
403
403
  ImportError: If the flavor can't be imported.
404
404
  """
405
- flavor_model = component_model.flavor
406
-
407
- try:
408
- from zenml.stack import Flavor
405
+ from zenml.stack import Flavor
409
406
 
410
- flavor = Flavor.from_model(flavor_model)
411
- except (ModuleNotFoundError, ImportError, NotImplementedError) as err:
412
- raise ImportError(
413
- f"Couldn't import flavor {flavor_model.name}: {err}"
414
- )
407
+ flavor_model = component_model.flavor
408
+ flavor = Flavor.from_model(flavor_model)
415
409
 
416
410
  configuration = flavor.config_class(**component_model.configuration)
417
411
 
@@ -109,9 +109,16 @@ def find_active_code_repository(
109
109
  for model in depaginate(list_method=Client().list_code_repositories):
110
110
  try:
111
111
  repo = BaseCodeRepository.from_model(model)
112
- except Exception:
112
+ except ImportError:
113
113
  logger.debug(
114
- "Failed to instantiate code repository class.", exc_info=True
114
+ "Failed to import code repository class.", exc_info=True
115
+ )
116
+ continue
117
+ except Exception as e:
118
+ logger.warning(
119
+ "Failed to instantiate or login to code repository `%s`: %s",
120
+ model.name,
121
+ e,
115
122
  )
116
123
  continue
117
124
 
zenml/utils/code_utils.py CHANGED
@@ -249,16 +249,7 @@ def download_and_extract_code(code_path: str, extract_dir: str) -> None:
249
249
  Args:
250
250
  code_path: Path where the code is uploaded.
251
251
  extract_dir: Directory where to code should be extracted to.
252
-
253
- Raises:
254
- RuntimeError: If the code is stored in an artifact store which is
255
- not active.
256
252
  """
257
- artifact_store = Client().active_stack.artifact_store
258
-
259
- if not code_path.startswith(artifact_store.path):
260
- raise RuntimeError("Code stored in different artifact store.")
261
-
262
253
  download_path = os.path.basename(code_path)
263
254
  fileio.copy(code_path, download_path)
264
255
 
@@ -266,17 +257,30 @@ def download_and_extract_code(code_path: str, extract_dir: str) -> None:
266
257
  os.remove(download_path)
267
258
 
268
259
 
269
- def download_code_from_artifact_store(code_path: str) -> None:
260
+ def download_code_from_artifact_store(
261
+ code_path: str, artifact_store: "BaseArtifactStore"
262
+ ) -> None:
270
263
  """Download code from the artifact store.
271
264
 
272
265
  Args:
273
266
  code_path: Path where the code is stored.
267
+ artifact_store: The artifact store to use for the download.
268
+
269
+ Raises:
270
+ RuntimeError: If the code is stored in an artifact store which is
271
+ not active.
274
272
  """
275
273
  logger.info("Downloading code from artifact store path `%s`.", code_path)
276
274
 
277
- # Do not remove this line, we need to instantiate the artifact store to
278
- # register the filesystem needed for the file download
279
- _ = Client().active_stack.artifact_store
275
+ if not code_path.startswith(artifact_store.path):
276
+ raise RuntimeError(
277
+ "The code is not stored in the artifact store "
278
+ f"{artifact_store.name} that was passed to download it."
279
+ )
280
+
281
+ # Make sure we register the artifact store filesystem here so the
282
+ # fileio.copy call will pick up the right credentials
283
+ artifact_store._register()
280
284
 
281
285
  extract_dir = os.path.abspath("code")
282
286
  os.makedirs(extract_dir)
@@ -13,6 +13,7 @@
13
13
  # permissions and limitations under the License.
14
14
  """Utility class to help with interacting with the dashboard."""
15
15
 
16
+ import os
16
17
  from typing import Optional
17
18
  from uuid import UUID
18
19
 
@@ -156,9 +157,27 @@ def show_dashboard(url: str) -> None:
156
157
  display(IFrame(src=url, width="100%", height=720))
157
158
 
158
159
  elif environment in (EnvironmentType.NATIVE, EnvironmentType.WSL):
159
- if constants.handle_bool_env_var(
160
+ open_dashboard = True
161
+
162
+ if constants.ENV_AUTO_OPEN_DASHBOARD in os.environ:
163
+ logger.warning(
164
+ "The `%s` environment variable is deprecated, use the `%s` "
165
+ "environment variable instead.",
166
+ constants.ENV_AUTO_OPEN_DASHBOARD,
167
+ constants.ENV_ZENML_AUTO_OPEN_DASHBOARD,
168
+ )
169
+
170
+ if not constants.handle_bool_env_var(
160
171
  constants.ENV_AUTO_OPEN_DASHBOARD, default=True
161
172
  ):
173
+ open_dashboard = False
174
+
175
+ if not constants.handle_bool_env_var(
176
+ constants.ENV_ZENML_AUTO_OPEN_DASHBOARD, default=True
177
+ ):
178
+ open_dashboard = False
179
+
180
+ if open_dashboard:
162
181
  try:
163
182
  import webbrowser
164
183
 
@@ -169,14 +188,16 @@ def show_dashboard(url: str) -> None:
169
188
  logger.info(
170
189
  "Automatically opening the dashboard in your "
171
190
  "browser. To disable this, set the env variable "
172
- "AUTO_OPEN_DASHBOARD=false."
191
+ "`%s=false`.",
192
+ constants.ENV_ZENML_AUTO_OPEN_DASHBOARD,
173
193
  )
174
194
  except Exception as e:
175
195
  logger.error(e)
176
196
  else:
177
197
  logger.info(
178
198
  "To open the dashboard in a browser automatically, "
179
- "set the env variable AUTO_OPEN_DASHBOARD=true."
199
+ "set the env variable `%s=true`.",
200
+ constants.ENV_ZENML_AUTO_OPEN_DASHBOARD,
180
201
  )
181
202
 
182
203
  else:
@@ -132,14 +132,16 @@ def deprecate_pydantic_attributes(
132
132
  if replacement_attribute is None:
133
133
  _warn(
134
134
  message=f"The attribute `{deprecated_attribute}` of class "
135
- f"`{cls.__name__}` will be deprecated soon.",
135
+ f"`{cls.__name__}` is deprecated and will be removed in "
136
+ "the future.",
136
137
  attribute=deprecated_attribute,
137
138
  )
138
139
  continue
139
140
 
140
141
  _warn(
141
142
  message=f"The attribute `{deprecated_attribute}` of class "
142
- f"`{cls.__name__}` will be deprecated soon. Use the "
143
+ f"`{cls.__name__}` is deprecated and will be removed in the "
144
+ "future. Use the "
143
145
  f"attribute `{replacement_attribute}` instead.",
144
146
  attribute=deprecated_attribute,
145
147
  )
@@ -78,7 +78,6 @@ class PipelineDockerImageBuilder:
78
78
  tag: str,
79
79
  stack: "Stack",
80
80
  include_files: bool,
81
- download_files: bool,
82
81
  entrypoint: Optional[str] = None,
83
82
  extra_files: Optional[Dict[str, str]] = None,
84
83
  code_repository: Optional["BaseCodeRepository"] = None,
@@ -93,7 +92,6 @@ class PipelineDockerImageBuilder:
93
92
  tag: The tag to use for the image.
94
93
  stack: The stack on which the pipeline will be deployed.
95
94
  include_files: Whether to include files in the build context.
96
- download_files: Whether to download files in the build context.
97
95
  entrypoint: Entrypoint to use for the final image. If left empty,
98
96
  no entrypoint will be included in the image.
99
97
  extra_files: Extra files to add to the build context. Keys are the
@@ -165,7 +163,6 @@ class PipelineDockerImageBuilder:
165
163
  docker_settings.apt_packages,
166
164
  docker_settings.environment,
167
165
  include_files,
168
- download_files,
169
166
  entrypoint,
170
167
  extra_files,
171
168
  ]
@@ -590,7 +587,7 @@ class PipelineDockerImageBuilder:
590
587
  f"ENV {ENV_ZENML_LOGGING_COLORS_DISABLED}={str(handle_bool_env_var(ENV_ZENML_LOGGING_COLORS_DISABLED, False))}"
591
588
  )
592
589
  for key, value in docker_settings.environment.items():
593
- lines.append(f"ENV {key.upper()}={value}")
590
+ lines.append(f"ENV {key.upper()}='{value}'")
594
591
 
595
592
  if apt_packages:
596
593
  apt_packages = " ".join(f"'{p}'" for p in apt_packages)
@@ -151,6 +151,9 @@ class CodeRepositorySchema(NamedSchema, table=True):
151
151
  if update.logo_url:
152
152
  self.logo_url = update.logo_url
153
153
 
154
+ if update.config:
155
+ self.config = json.dumps(update.config)
156
+
154
157
  self.updated = utc_now()
155
158
  return self
156
159
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: zenml-nightly
3
- Version: 0.73.0.dev20250127
3
+ Version: 0.73.0.dev20250129
4
4
  Summary: ZenML: Write production-ready ML code.
5
5
  License: Apache-2.0
6
6
  Keywords: machine learning,production,pipeline,mlops,devops