zenml-nightly 0.70.0.dev20241127__py3-none-any.whl → 0.70.0.dev20241129__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 (53) hide show
  1. zenml/VERSION +1 -1
  2. zenml/artifacts/artifact_config.py +21 -1
  3. zenml/artifacts/utils.py +5 -1
  4. zenml/cli/pipeline.py +80 -0
  5. zenml/config/compiler.py +12 -3
  6. zenml/config/pipeline_configurations.py +20 -0
  7. zenml/config/pipeline_run_configuration.py +1 -0
  8. zenml/config/step_configurations.py +21 -0
  9. zenml/enums.py +1 -0
  10. zenml/integrations/__init__.py +1 -0
  11. zenml/integrations/constants.py +1 -0
  12. zenml/integrations/feast/__init__.py +1 -1
  13. zenml/integrations/feast/feature_stores/feast_feature_store.py +13 -9
  14. zenml/integrations/kubernetes/orchestrators/kube_utils.py +46 -2
  15. zenml/integrations/kubernetes/orchestrators/kubernetes_orchestrator.py +13 -2
  16. zenml/integrations/kubernetes/orchestrators/kubernetes_orchestrator_entrypoint.py +3 -1
  17. zenml/integrations/kubernetes/orchestrators/manifest_utils.py +3 -2
  18. zenml/integrations/kubernetes/step_operators/kubernetes_step_operator.py +3 -1
  19. zenml/integrations/modal/__init__.py +46 -0
  20. zenml/integrations/modal/flavors/__init__.py +26 -0
  21. zenml/integrations/modal/flavors/modal_step_operator_flavor.py +125 -0
  22. zenml/integrations/modal/step_operators/__init__.py +22 -0
  23. zenml/integrations/modal/step_operators/modal_step_operator.py +242 -0
  24. zenml/materializers/built_in_materializer.py +18 -1
  25. zenml/materializers/structured_string_materializer.py +8 -3
  26. zenml/model/model.py +6 -2
  27. zenml/models/v2/core/pipeline_run.py +4 -0
  28. zenml/models/v2/core/step_run.py +1 -1
  29. zenml/orchestrators/publish_utils.py +1 -1
  30. zenml/orchestrators/step_launcher.py +6 -2
  31. zenml/orchestrators/step_run_utils.py +15 -6
  32. zenml/orchestrators/step_runner.py +40 -2
  33. zenml/orchestrators/utils.py +29 -4
  34. zenml/pipelines/pipeline_decorator.py +4 -0
  35. zenml/pipelines/pipeline_definition.py +14 -3
  36. zenml/pipelines/run_utils.py +8 -3
  37. zenml/steps/base_step.py +11 -1
  38. zenml/steps/entrypoint_function_utils.py +4 -2
  39. zenml/steps/step_decorator.py +4 -0
  40. zenml/steps/utils.py +17 -5
  41. zenml/types.py +4 -0
  42. zenml/utils/string_utils.py +30 -12
  43. zenml/utils/visualization_utils.py +4 -1
  44. zenml/zen_server/template_execution/utils.py +1 -0
  45. zenml/zen_stores/schemas/artifact_schemas.py +2 -1
  46. zenml/zen_stores/schemas/pipeline_run_schemas.py +14 -3
  47. zenml/zen_stores/schemas/step_run_schemas.py +19 -0
  48. zenml/zen_stores/sql_zen_store.py +15 -11
  49. {zenml_nightly-0.70.0.dev20241127.dist-info → zenml_nightly-0.70.0.dev20241129.dist-info}/METADATA +1 -1
  50. {zenml_nightly-0.70.0.dev20241127.dist-info → zenml_nightly-0.70.0.dev20241129.dist-info}/RECORD +53 -48
  51. {zenml_nightly-0.70.0.dev20241127.dist-info → zenml_nightly-0.70.0.dev20241129.dist-info}/LICENSE +0 -0
  52. {zenml_nightly-0.70.0.dev20241127.dist-info → zenml_nightly-0.70.0.dev20241129.dist-info}/WHEEL +0 -0
  53. {zenml_nightly-0.70.0.dev20241127.dist-info → zenml_nightly-0.70.0.dev20241129.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,125 @@
1
+ # Copyright (c) ZenML GmbH 2024. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at:
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12
+ # or implied. See the License for the specific language governing
13
+ # permissions and limitations under the License.
14
+ """Modal step operator flavor."""
15
+
16
+ from typing import TYPE_CHECKING, Optional, Type
17
+
18
+ from zenml.config.base_settings import BaseSettings
19
+ from zenml.integrations.modal import MODAL_STEP_OPERATOR_FLAVOR
20
+ from zenml.step_operators import BaseStepOperatorConfig, BaseStepOperatorFlavor
21
+
22
+ if TYPE_CHECKING:
23
+ from zenml.integrations.modal.step_operators import ModalStepOperator
24
+
25
+
26
+ class ModalStepOperatorSettings(BaseSettings):
27
+ """Settings for the Modal step operator.
28
+
29
+ Specifying the region and cloud provider is only available for Enterprise
30
+ and Team plan customers.
31
+
32
+ Certain combinations of settings are not available. It is suggested to err
33
+ on the side of looser settings rather than more restrictive ones to avoid
34
+ pipeline execution failures. In the case of failures, however, Modal
35
+ provides detailed error messages that can help identify what is
36
+ incompatible. See more in the Modal docs at https://modal.com/docs/guide/region-selection.
37
+
38
+ Attributes:
39
+ gpu: The type of GPU to use for the step execution.
40
+ region: The region to use for the step execution.
41
+ cloud: The cloud provider to use for the step execution.
42
+ """
43
+
44
+ gpu: Optional[str] = None
45
+ region: Optional[str] = None
46
+ cloud: Optional[str] = None
47
+
48
+
49
+ class ModalStepOperatorConfig(
50
+ BaseStepOperatorConfig, ModalStepOperatorSettings
51
+ ):
52
+ """Configuration for the Modal step operator."""
53
+
54
+ @property
55
+ def is_remote(self) -> bool:
56
+ """Checks if this stack component is running remotely.
57
+
58
+ This designation is used to determine if the stack component can be
59
+ used with a local ZenML database or if it requires a remote ZenML
60
+ server.
61
+
62
+ Returns:
63
+ True if this config is for a remote component, False otherwise.
64
+ """
65
+ return True
66
+
67
+
68
+ class ModalStepOperatorFlavor(BaseStepOperatorFlavor):
69
+ """Modal step operator flavor."""
70
+
71
+ @property
72
+ def name(self) -> str:
73
+ """Name of the flavor.
74
+
75
+ Returns:
76
+ The name of the flavor.
77
+ """
78
+ return MODAL_STEP_OPERATOR_FLAVOR
79
+
80
+ @property
81
+ def docs_url(self) -> Optional[str]:
82
+ """A url to point at docs explaining this flavor.
83
+
84
+ Returns:
85
+ A flavor docs url.
86
+ """
87
+ return self.generate_default_docs_url()
88
+
89
+ @property
90
+ def sdk_docs_url(self) -> Optional[str]:
91
+ """A url to point at SDK docs explaining this flavor.
92
+
93
+ Returns:
94
+ A flavor SDK docs url.
95
+ """
96
+ return self.generate_default_sdk_docs_url()
97
+
98
+ @property
99
+ def logo_url(self) -> str:
100
+ """A url to represent the flavor in the dashboard.
101
+
102
+ Returns:
103
+ The flavor logo.
104
+ """
105
+ return "https://public-flavor-logos.s3.eu-central-1.amazonaws.com/step_operator/modal.png"
106
+
107
+ @property
108
+ def config_class(self) -> Type[ModalStepOperatorConfig]:
109
+ """Returns `ModalStepOperatorConfig` config class.
110
+
111
+ Returns:
112
+ The config class.
113
+ """
114
+ return ModalStepOperatorConfig
115
+
116
+ @property
117
+ def implementation_class(self) -> Type["ModalStepOperator"]:
118
+ """Implementation class for this flavor.
119
+
120
+ Returns:
121
+ The implementation class.
122
+ """
123
+ from zenml.integrations.modal.step_operators import ModalStepOperator
124
+
125
+ return ModalStepOperator
@@ -0,0 +1,22 @@
1
+ # Copyright (c) ZenML GmbH 2024. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at:
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12
+ # or implied. See the License for the specific language governing
13
+ # permissions and limitations under the License.
14
+ """Modal step operator."""
15
+
16
+ from zenml.integrations.modal.step_operators.modal_step_operator import (
17
+ ModalStepOperator,
18
+ )
19
+
20
+ __all__ = [
21
+ "ModalStepOperator",
22
+ ]
@@ -0,0 +1,242 @@
1
+ # Copyright (c) ZenML GmbH 2024. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at:
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12
+ # or implied. See the License for the specific language governing
13
+ # permissions and limitations under the License.
14
+ """Modal step operator implementation."""
15
+
16
+ import asyncio
17
+ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, cast
18
+
19
+ import modal
20
+ from modal_proto import api_pb2
21
+
22
+ from zenml.client import Client
23
+ from zenml.config.build_configuration import BuildConfiguration
24
+ from zenml.config.resource_settings import ByteUnit, ResourceSettings
25
+ from zenml.enums import StackComponentType
26
+ from zenml.integrations.modal.flavors import (
27
+ ModalStepOperatorConfig,
28
+ ModalStepOperatorSettings,
29
+ )
30
+ from zenml.logger import get_logger
31
+ from zenml.stack import Stack, StackValidator
32
+ from zenml.step_operators import BaseStepOperator
33
+
34
+ if TYPE_CHECKING:
35
+ from zenml.config.base_settings import BaseSettings
36
+ from zenml.config.step_run_info import StepRunInfo
37
+ from zenml.models import PipelineDeploymentBase
38
+
39
+ logger = get_logger(__name__)
40
+
41
+ MODAL_STEP_OPERATOR_DOCKER_IMAGE_KEY = "modal_step_operator"
42
+
43
+
44
+ def get_gpu_values(
45
+ settings: ModalStepOperatorSettings, resource_settings: ResourceSettings
46
+ ) -> Optional[str]:
47
+ """Get the GPU values for the Modal step operator.
48
+
49
+ Args:
50
+ settings: The Modal step operator settings.
51
+ resource_settings: The resource settings.
52
+
53
+ Returns:
54
+ The GPU string if a count is specified, otherwise the GPU type.
55
+ """
56
+ if not settings.gpu:
57
+ return None
58
+ gpu_count = resource_settings.gpu_count
59
+ return f"{settings.gpu}:{gpu_count}" if gpu_count else settings.gpu
60
+
61
+
62
+ class ModalStepOperator(BaseStepOperator):
63
+ """Step operator to run a step on Modal.
64
+
65
+ This class defines code that can set up a Modal environment and run
66
+ functions in it.
67
+ """
68
+
69
+ @property
70
+ def config(self) -> ModalStepOperatorConfig:
71
+ """Get the Modal step operator configuration.
72
+
73
+ Returns:
74
+ The Modal step operator configuration.
75
+ """
76
+ return cast(ModalStepOperatorConfig, self._config)
77
+
78
+ @property
79
+ def settings_class(self) -> Optional[Type["BaseSettings"]]:
80
+ """Get the settings class for the Modal step operator.
81
+
82
+ Returns:
83
+ The Modal step operator settings class.
84
+ """
85
+ return ModalStepOperatorSettings
86
+
87
+ @property
88
+ def validator(self) -> Optional[StackValidator]:
89
+ """Get the stack validator for the Modal step operator.
90
+
91
+ Returns:
92
+ The stack validator.
93
+ """
94
+
95
+ def _validate_remote_components(stack: "Stack") -> Tuple[bool, str]:
96
+ if stack.artifact_store.config.is_local:
97
+ return False, (
98
+ "The Modal step operator runs code remotely and "
99
+ "needs to write files into the artifact store, but the "
100
+ f"artifact store `{stack.artifact_store.name}` of the "
101
+ "active stack is local. Please ensure that your stack "
102
+ "contains a remote artifact store when using the Modal "
103
+ "step operator."
104
+ )
105
+
106
+ container_registry = stack.container_registry
107
+ assert container_registry is not None
108
+
109
+ if container_registry.config.is_local:
110
+ return False, (
111
+ "The Modal step operator runs code remotely and "
112
+ "needs to push/pull Docker images, but the "
113
+ f"container registry `{container_registry.name}` of the "
114
+ "active stack is local. Please ensure that your stack "
115
+ "contains a remote container registry when using the "
116
+ "Modal step operator."
117
+ )
118
+
119
+ return True, ""
120
+
121
+ return StackValidator(
122
+ required_components={
123
+ StackComponentType.CONTAINER_REGISTRY,
124
+ StackComponentType.IMAGE_BUILDER,
125
+ },
126
+ custom_validation_function=_validate_remote_components,
127
+ )
128
+
129
+ def get_docker_builds(
130
+ self, deployment: "PipelineDeploymentBase"
131
+ ) -> List["BuildConfiguration"]:
132
+ """Get the Docker build configurations for the Modal step operator.
133
+
134
+ Args:
135
+ deployment: The pipeline deployment.
136
+
137
+ Returns:
138
+ A list of Docker build configurations.
139
+ """
140
+ builds = []
141
+ for step_name, step in deployment.step_configurations.items():
142
+ if step.config.step_operator == self.name:
143
+ build = BuildConfiguration(
144
+ key=MODAL_STEP_OPERATOR_DOCKER_IMAGE_KEY,
145
+ settings=step.config.docker_settings,
146
+ step_name=step_name,
147
+ )
148
+ builds.append(build)
149
+
150
+ return builds
151
+
152
+ def launch(
153
+ self,
154
+ info: "StepRunInfo",
155
+ entrypoint_command: List[str],
156
+ environment: Dict[str, str],
157
+ ) -> None:
158
+ """Launch a step run on Modal.
159
+
160
+ Args:
161
+ info: The step run information.
162
+ entrypoint_command: The entrypoint command for the step.
163
+ environment: The environment variables for the step.
164
+
165
+ Raises:
166
+ RuntimeError: If no Docker credentials are found for the container registry.
167
+ ValueError: If no container registry is found in the stack.
168
+ """
169
+ settings = cast(ModalStepOperatorSettings, self.get_settings(info))
170
+ image_name = info.get_image(key=MODAL_STEP_OPERATOR_DOCKER_IMAGE_KEY)
171
+ zc = Client()
172
+ stack = zc.active_stack
173
+
174
+ if not stack.container_registry:
175
+ raise ValueError(
176
+ "No Container registry found in the stack. "
177
+ "Please add a container registry and ensure "
178
+ "it is correctly configured."
179
+ )
180
+
181
+ if docker_creds := stack.container_registry.credentials:
182
+ docker_username, docker_password = docker_creds
183
+ else:
184
+ raise RuntimeError(
185
+ "No Docker credentials found for the container registry."
186
+ )
187
+
188
+ my_secret = modal.secret._Secret.from_dict(
189
+ {
190
+ "REGISTRY_USERNAME": docker_username,
191
+ "REGISTRY_PASSWORD": docker_password,
192
+ }
193
+ )
194
+
195
+ spec = modal.image.DockerfileSpec(
196
+ commands=[f"FROM {image_name}"], context_files={}
197
+ )
198
+
199
+ zenml_image = modal.Image._from_args(
200
+ dockerfile_function=lambda *_, **__: spec,
201
+ force_build=False,
202
+ image_registry_config=modal.image._ImageRegistryConfig(
203
+ api_pb2.REGISTRY_AUTH_TYPE_STATIC_CREDS, my_secret
204
+ ),
205
+ ).env(environment)
206
+
207
+ resource_settings = info.config.resource_settings
208
+ gpu_values = get_gpu_values(settings, resource_settings)
209
+
210
+ app = modal.App(
211
+ f"zenml-{info.run_name}-{info.step_run_id}-{info.pipeline_step_name}"
212
+ )
213
+
214
+ async def run_sandbox() -> asyncio.Future[None]:
215
+ loop = asyncio.get_event_loop()
216
+ future = loop.create_future()
217
+ with modal.enable_output():
218
+ async with app.run():
219
+ memory_mb = resource_settings.get_memory(ByteUnit.MB)
220
+ memory_int = (
221
+ int(memory_mb) if memory_mb is not None else None
222
+ )
223
+ sb = await modal.Sandbox.create.aio(
224
+ "bash",
225
+ "-c",
226
+ " ".join(entrypoint_command),
227
+ image=zenml_image,
228
+ gpu=gpu_values,
229
+ cpu=resource_settings.cpu_count,
230
+ memory=memory_int,
231
+ cloud=settings.cloud,
232
+ region=settings.region,
233
+ app=app,
234
+ timeout=86400, # 24h, the max Modal allows
235
+ )
236
+
237
+ await sb.wait.aio()
238
+
239
+ future.set_result(None)
240
+ return future
241
+
242
+ asyncio.run(run_sandbox())
@@ -28,7 +28,7 @@ from typing import (
28
28
  )
29
29
 
30
30
  from zenml.artifact_stores.base_artifact_store import BaseArtifactStore
31
- from zenml.enums import ArtifactType
31
+ from zenml.enums import ArtifactType, VisualizationType
32
32
  from zenml.logger import get_logger
33
33
  from zenml.materializers.base_materializer import BaseMaterializer
34
34
  from zenml.materializers.materializer_registry import materializer_registry
@@ -415,6 +415,23 @@ class BuiltInContainerMaterializer(BaseMaterializer):
415
415
  self.artifact_store.rmtree(entry["path"])
416
416
  raise e
417
417
 
418
+ # save dict type objects to JSON file with JSON visualization type
419
+ def save_visualizations(self, data: Any) -> Dict[str, "VisualizationType"]:
420
+ """Save visualizations for the given data.
421
+
422
+ Args:
423
+ data: The data to save visualizations for.
424
+
425
+ Returns:
426
+ A dictionary of visualization URIs and their types.
427
+ """
428
+ # dict/list type objects are always saved as JSON files
429
+ # doesn't work for non-serializable types as they
430
+ # are saved as list of lists in different files
431
+ if _is_serializable(data):
432
+ return {self.data_path: VisualizationType.JSON}
433
+ return {}
434
+
418
435
  def extract_metadata(self, data: Any) -> Dict[str, "MetadataType"]:
419
436
  """Extract metadata from the given built-in container object.
420
437
 
@@ -19,22 +19,23 @@ from typing import Dict, Type, Union
19
19
  from zenml.enums import ArtifactType, VisualizationType
20
20
  from zenml.logger import get_logger
21
21
  from zenml.materializers.base_materializer import BaseMaterializer
22
- from zenml.types import CSVString, HTMLString, MarkdownString
22
+ from zenml.types import CSVString, HTMLString, JSONString, MarkdownString
23
23
 
24
24
  logger = get_logger(__name__)
25
25
 
26
26
 
27
- STRUCTURED_STRINGS = Union[CSVString, HTMLString, MarkdownString]
27
+ STRUCTURED_STRINGS = Union[CSVString, HTMLString, MarkdownString, JSONString]
28
28
 
29
29
  HTML_FILENAME = "output.html"
30
30
  MARKDOWN_FILENAME = "output.md"
31
31
  CSV_FILENAME = "output.csv"
32
+ JSON_FILENAME = "output.json"
32
33
 
33
34
 
34
35
  class StructuredStringMaterializer(BaseMaterializer):
35
36
  """Materializer for HTML or Markdown strings."""
36
37
 
37
- ASSOCIATED_TYPES = (CSVString, HTMLString, MarkdownString)
38
+ ASSOCIATED_TYPES = (CSVString, HTMLString, MarkdownString, JSONString)
38
39
  ASSOCIATED_ARTIFACT_TYPE = ArtifactType.DATA_ANALYSIS
39
40
 
40
41
  def load(self, data_type: Type[STRUCTURED_STRINGS]) -> STRUCTURED_STRINGS:
@@ -94,6 +95,8 @@ class StructuredStringMaterializer(BaseMaterializer):
94
95
  filename = HTML_FILENAME
95
96
  elif issubclass(data_type, MarkdownString):
96
97
  filename = MARKDOWN_FILENAME
98
+ elif issubclass(data_type, JSONString):
99
+ filename = JSON_FILENAME
97
100
  else:
98
101
  raise ValueError(
99
102
  f"Data type {data_type} is not supported by this materializer."
@@ -120,6 +123,8 @@ class StructuredStringMaterializer(BaseMaterializer):
120
123
  return VisualizationType.HTML
121
124
  elif issubclass(data_type, MarkdownString):
122
125
  return VisualizationType.MARKDOWN
126
+ elif issubclass(data_type, JSONString):
127
+ return VisualizationType.JSON
123
128
  else:
124
129
  raise ValueError(
125
130
  f"Data type {data_type} is not supported by this materializer."
zenml/model/model.py CHANGED
@@ -57,7 +57,9 @@ class Model(BaseModel):
57
57
  ethics: The ethical implications of the model.
58
58
  tags: Tags associated with the model.
59
59
  version: The version name, version number or stage is optional and points model context
60
- to a specific version/stage. If skipped new version will be created.
60
+ to a specific version/stage. If skipped new version will be created. `version`
61
+ also supports placeholders: standard `{date}` and `{time}` and any custom placeholders
62
+ that are passed as substitutions in the pipeline or step decorators.
61
63
  save_models_to_registry: Whether to save all ModelArtifacts to Model Registry,
62
64
  if available in active stack.
63
65
  """
@@ -534,6 +536,8 @@ class Model(BaseModel):
534
536
  from zenml.models import ModelRequest
535
537
 
536
538
  zenml_client = Client()
539
+ # backup logic, if the Model class is used directly from the code
540
+ self.name = format_name_template(self.name, substitutions={})
537
541
  if self.model_version_id:
538
542
  mv = zenml_client.get_model_version(
539
543
  model_version_name_or_number_or_id=self.model_version_id,
@@ -663,7 +667,7 @@ class Model(BaseModel):
663
667
 
664
668
  # backup logic, if the Model class is used directly from the code
665
669
  if isinstance(self.version, str):
666
- self.version = format_name_template(self.version)
670
+ self.version = format_name_template(self.version, substitutions={})
667
671
 
668
672
  try:
669
673
  if self.version or self.model_version_id:
@@ -237,6 +237,10 @@ class PipelineRunResponseMetadata(WorkspaceScopedResponseMetadata):
237
237
  default=False,
238
238
  description="Whether a template can be created from this run.",
239
239
  )
240
+ steps_substitutions: Dict[str, Dict[str, str]] = Field(
241
+ title="Substitutions used in the step runs of this pipeline run.",
242
+ default_factory=dict,
243
+ )
240
244
 
241
245
 
242
246
  class PipelineRunResponseResources(WorkspaceScopedResponseResources):
@@ -142,7 +142,7 @@ class StepRunRequest(WorkspaceScopedRequest):
142
142
  class StepRunUpdate(BaseModel):
143
143
  """Update model for step runs."""
144
144
 
145
- outputs: Dict[str, UUID] = Field(
145
+ outputs: Dict[str, List[UUID]] = Field(
146
146
  title="The IDs of the output artifact versions of the step run.",
147
147
  default={},
148
148
  )
@@ -32,7 +32,7 @@ if TYPE_CHECKING:
32
32
 
33
33
 
34
34
  def publish_successful_step_run(
35
- step_run_id: "UUID", output_artifact_ids: Dict[str, "UUID"]
35
+ step_run_id: "UUID", output_artifact_ids: Dict[str, List["UUID"]]
36
36
  ) -> "StepRunResponse":
37
37
  """Publishes a successful step run.
38
38
 
@@ -309,8 +309,12 @@ class StepLauncher:
309
309
  The created or existing pipeline run,
310
310
  and a boolean indicating whether the run was created or reused.
311
311
  """
312
+ start_time = datetime.utcnow()
312
313
  run_name = orchestrator_utils.get_run_name(
313
- run_name_template=self._deployment.run_name_template
314
+ run_name_template=self._deployment.run_name_template,
315
+ substitutions=self._deployment.pipeline_configuration._get_full_substitutions(
316
+ start_time
317
+ ),
314
318
  )
315
319
 
316
320
  logger.debug("Creating pipeline run %s", run_name)
@@ -329,7 +333,7 @@ class StepLauncher:
329
333
  ),
330
334
  status=ExecutionStatus.RUNNING,
331
335
  orchestrator_environment=get_run_environment_dict(),
332
- start_time=datetime.utcnow(),
336
+ start_time=start_time,
333
337
  tags=self._deployment.pipeline_configuration.tags,
334
338
  )
335
339
  return client.zen_store.get_or_create_run(pipeline_run)
@@ -354,13 +354,16 @@ def create_cached_step_runs(
354
354
 
355
355
 
356
356
  def get_or_create_model_version_for_pipeline_run(
357
- model: "Model", pipeline_run: PipelineRunResponse
357
+ model: "Model",
358
+ pipeline_run: PipelineRunResponse,
359
+ substitutions: Dict[str, str],
358
360
  ) -> Tuple[ModelVersionResponse, bool]:
359
361
  """Get or create a model version as part of a pipeline run.
360
362
 
361
363
  Args:
362
364
  model: The model to get or create.
363
365
  pipeline_run: The pipeline run for which the model should be created.
366
+ substitutions: Substitutions to apply to the model version name.
364
367
 
365
368
  Returns:
366
369
  The model version and a boolean indicating whether it was newly created
@@ -374,12 +377,14 @@ def get_or_create_model_version_for_pipeline_run(
374
377
  return model._get_model_version(), False
375
378
  elif model.version:
376
379
  if isinstance(model.version, str):
377
- start_time = pipeline_run.start_time or datetime.utcnow()
378
380
  model.version = string_utils.format_name_template(
379
381
  model.version,
380
- date=start_time.strftime("%Y_%m_%d"),
381
- time=start_time.strftime("%H_%M_%S_%f"),
382
+ substitutions=substitutions,
382
383
  )
384
+ model.name = string_utils.format_name_template(
385
+ model.name,
386
+ substitutions=substitutions,
387
+ )
383
388
 
384
389
  return (
385
390
  model._get_or_create_model_version(),
@@ -460,7 +465,9 @@ def prepare_pipeline_run_model_version(
460
465
  model_version = pipeline_run.model_version
461
466
  elif config_model := pipeline_run.config.model:
462
467
  model_version, _ = get_or_create_model_version_for_pipeline_run(
463
- model=config_model, pipeline_run=pipeline_run
468
+ model=config_model,
469
+ pipeline_run=pipeline_run,
470
+ substitutions=pipeline_run.config.substitutions,
464
471
  )
465
472
  pipeline_run = Client().zen_store.update_run(
466
473
  run_id=pipeline_run.id,
@@ -492,7 +499,9 @@ def prepare_step_run_model_version(
492
499
  model_version = step_run.model_version
493
500
  elif config_model := step_run.config.model:
494
501
  model_version, created = get_or_create_model_version_for_pipeline_run(
495
- model=config_model, pipeline_run=pipeline_run
502
+ model=config_model,
503
+ pipeline_run=pipeline_run,
504
+ substitutions=step_run.config.substitutions,
496
505
  )
497
506
  step_run = Client().zen_store.update_run_step(
498
507
  step_run_id=step_run.id,
@@ -152,6 +152,15 @@ class StepRunner:
152
152
  func=step_instance.entrypoint
153
153
  )
154
154
 
155
+ self._evaluate_artifact_names_in_collections(
156
+ step_run,
157
+ output_annotations,
158
+ [
159
+ output_artifact_uris,
160
+ output_materializers,
161
+ ],
162
+ )
163
+
155
164
  self._stack.prepare_step_run(info=step_run_info)
156
165
 
157
166
  # Initialize the step context singleton
@@ -257,7 +266,9 @@ class StepRunner:
257
266
 
258
267
  # Update the status and output artifacts of the step run.
259
268
  output_artifact_ids = {
260
- output_name: artifact.id
269
+ output_name: [
270
+ artifact.id,
271
+ ]
261
272
  for output_name, artifact in output_artifacts.items()
262
273
  }
263
274
  publish_successful_step_run(
@@ -265,6 +276,33 @@ class StepRunner:
265
276
  output_artifact_ids=output_artifact_ids,
266
277
  )
267
278
 
279
+ def _evaluate_artifact_names_in_collections(
280
+ self,
281
+ step_run: "StepRunResponse",
282
+ output_annotations: Dict[str, OutputSignature],
283
+ collections: List[Dict[str, Any]],
284
+ ) -> None:
285
+ """Evaluates the artifact names in the collections.
286
+
287
+ Args:
288
+ step_run: The step run.
289
+ output_annotations: The output annotations of the step function
290
+ (also evaluated).
291
+ collections: The collections to evaluate.
292
+ """
293
+ collections.append(output_annotations)
294
+ for k, v in list(output_annotations.items()):
295
+ _evaluated_name = None
296
+ if v.artifact_config:
297
+ _evaluated_name = v.artifact_config._evaluated_name(
298
+ step_run.config.substitutions
299
+ )
300
+ if _evaluated_name is None:
301
+ _evaluated_name = k
302
+
303
+ for d in collections:
304
+ d[_evaluated_name] = d.pop(k)
305
+
268
306
  def _load_step(self) -> "BaseStep":
269
307
  """Load the step instance.
270
308
 
@@ -400,7 +438,7 @@ class StepRunner:
400
438
  **artifact.get_hydrated_version().model_dump()
401
439
  )
402
440
 
403
- if data_type is Any or is_union(get_origin(data_type)):
441
+ if data_type in (None, Any) or is_union(get_origin(data_type)):
404
442
  # Entrypoint function does not define a specific type for the input,
405
443
  # we use the datatype of the stored artifact
406
444
  data_type = source_utils.load(artifact.data_type)