zenml-nightly 0.70.0.dev20241128__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 (41) 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/feast/__init__.py +1 -1
  11. zenml/integrations/feast/feature_stores/feast_feature_store.py +13 -9
  12. zenml/materializers/built_in_materializer.py +18 -1
  13. zenml/materializers/structured_string_materializer.py +8 -3
  14. zenml/model/model.py +6 -2
  15. zenml/models/v2/core/pipeline_run.py +4 -0
  16. zenml/models/v2/core/step_run.py +1 -1
  17. zenml/orchestrators/publish_utils.py +1 -1
  18. zenml/orchestrators/step_launcher.py +6 -2
  19. zenml/orchestrators/step_run_utils.py +15 -6
  20. zenml/orchestrators/step_runner.py +39 -1
  21. zenml/orchestrators/utils.py +5 -2
  22. zenml/pipelines/pipeline_decorator.py +4 -0
  23. zenml/pipelines/pipeline_definition.py +14 -3
  24. zenml/pipelines/run_utils.py +8 -3
  25. zenml/steps/base_step.py +11 -1
  26. zenml/steps/entrypoint_function_utils.py +4 -2
  27. zenml/steps/step_decorator.py +4 -0
  28. zenml/steps/utils.py +17 -5
  29. zenml/types.py +4 -0
  30. zenml/utils/string_utils.py +30 -12
  31. zenml/utils/visualization_utils.py +4 -1
  32. zenml/zen_server/template_execution/utils.py +1 -0
  33. zenml/zen_stores/schemas/artifact_schemas.py +2 -1
  34. zenml/zen_stores/schemas/pipeline_run_schemas.py +14 -3
  35. zenml/zen_stores/schemas/step_run_schemas.py +19 -0
  36. zenml/zen_stores/sql_zen_store.py +15 -11
  37. {zenml_nightly-0.70.0.dev20241128.dist-info → zenml_nightly-0.70.0.dev20241129.dist-info}/METADATA +1 -1
  38. {zenml_nightly-0.70.0.dev20241128.dist-info → zenml_nightly-0.70.0.dev20241129.dist-info}/RECORD +41 -41
  39. {zenml_nightly-0.70.0.dev20241128.dist-info → zenml_nightly-0.70.0.dev20241129.dist-info}/LICENSE +0 -0
  40. {zenml_nightly-0.70.0.dev20241128.dist-info → zenml_nightly-0.70.0.dev20241129.dist-info}/WHEEL +0 -0
  41. {zenml_nightly-0.70.0.dev20241128.dist-info → zenml_nightly-0.70.0.dev20241129.dist-info}/entry_points.txt +0 -0
zenml/VERSION CHANGED
@@ -1 +1 @@
1
- 0.70.0.dev20241128
1
+ 0.70.0.dev20241129
@@ -21,6 +21,7 @@ from zenml.enums import ArtifactType
21
21
  from zenml.logger import get_logger
22
22
  from zenml.metadata.metadata_types import MetadataType
23
23
  from zenml.utils.pydantic_utils import before_validator_handler
24
+ from zenml.utils.string_utils import format_name_template
24
25
 
25
26
  logger = get_logger(__name__)
26
27
 
@@ -45,7 +46,13 @@ class ArtifactConfig(BaseModel):
45
46
  ```
46
47
 
47
48
  Attributes:
48
- name: The name of the artifact.
49
+ name: The name of the artifact:
50
+ - static string e.g. "name"
51
+ - dynamic string e.g. "name_{date}_{time}_{custom_placeholder}"
52
+ If you use any placeholders besides `date` and `time`,
53
+ you need to provide the values for them in the `substitutions`
54
+ argument of the step decorator or the `substitutions` argument
55
+ of `with_options` of the step.
49
56
  version: The version of the artifact.
50
57
  tags: The tags of the artifact.
51
58
  run_metadata: Metadata to add to the artifact.
@@ -111,3 +118,16 @@ class ArtifactConfig(BaseModel):
111
118
  data.setdefault("artifact_type", ArtifactType.SERVICE)
112
119
 
113
120
  return data
121
+
122
+ def _evaluated_name(self, substitutions: Dict[str, str]) -> Optional[str]:
123
+ """Evaluated name of the artifact.
124
+
125
+ Args:
126
+ substitutions: Extra placeholders to use in the name template.
127
+
128
+ Returns:
129
+ The evaluated name of the artifact.
130
+ """
131
+ if self.name:
132
+ return format_name_template(self.name, substitutions=substitutions)
133
+ return self.name
zenml/artifacts/utils.py CHANGED
@@ -689,7 +689,11 @@ def _link_artifact_version_to_the_step_and_model(
689
689
  client.zen_store.update_run_step(
690
690
  step_run_id=step_run.id,
691
691
  step_run_update=StepRunUpdate(
692
- outputs={artifact_version.artifact.name: artifact_version.id}
692
+ outputs={
693
+ artifact_version.artifact.name: [
694
+ artifact_version.id,
695
+ ]
696
+ }
693
697
  ),
694
698
  )
695
699
  error_message = "model"
zenml/cli/pipeline.py CHANGED
@@ -315,6 +315,86 @@ def run_pipeline(
315
315
  pipeline_instance()
316
316
 
317
317
 
318
+ @pipeline.command(
319
+ "create-run-template",
320
+ help="Create a run template for a pipeline. The SOURCE argument needs to "
321
+ "be an importable source path resolving to a ZenML pipeline instance, e.g. "
322
+ "`my_module.my_pipeline_instance`.",
323
+ )
324
+ @click.argument("source")
325
+ @click.option(
326
+ "--name",
327
+ "-n",
328
+ type=str,
329
+ required=True,
330
+ help="Name for the template",
331
+ )
332
+ @click.option(
333
+ "--config",
334
+ "-c",
335
+ "config_path",
336
+ type=click.Path(exists=True, dir_okay=False),
337
+ required=False,
338
+ help="Path to configuration file for the build.",
339
+ )
340
+ @click.option(
341
+ "--stack",
342
+ "-s",
343
+ "stack_name_or_id",
344
+ type=str,
345
+ required=False,
346
+ help="Name or ID of the stack to use for the build.",
347
+ )
348
+ def create_run_template(
349
+ source: str,
350
+ name: str,
351
+ config_path: Optional[str] = None,
352
+ stack_name_or_id: Optional[str] = None,
353
+ ) -> None:
354
+ """Create a run template for a pipeline.
355
+
356
+ Args:
357
+ source: Importable source resolving to a pipeline instance.
358
+ name: Name of the run template.
359
+ config_path: Path to pipeline configuration file.
360
+ stack_name_or_id: Name or ID of the stack for which the template should
361
+ be created.
362
+ """
363
+ if not Client().root:
364
+ cli_utils.warning(
365
+ "You're running the `zenml pipeline create-run-template` command "
366
+ "without a ZenML repository. Your current working directory will "
367
+ "be used as the source root relative to which the registered step "
368
+ "classes will be resolved. To silence this warning, run `zenml "
369
+ "init` at your source code root."
370
+ )
371
+
372
+ try:
373
+ pipeline_instance = source_utils.load(source)
374
+ except ModuleNotFoundError as e:
375
+ source_root = source_utils.get_source_root()
376
+ cli_utils.error(
377
+ f"Unable to import module `{e.name}`. Make sure the source path is "
378
+ f"relative to your source root `{source_root}`."
379
+ )
380
+ except AttributeError as e:
381
+ cli_utils.error("Unable to load attribute from module: " + str(e))
382
+
383
+ if not isinstance(pipeline_instance, Pipeline):
384
+ cli_utils.error(
385
+ f"The given source path `{source}` does not resolve to a pipeline "
386
+ "object."
387
+ )
388
+
389
+ with cli_utils.temporary_active_stack(stack_name_or_id=stack_name_or_id):
390
+ pipeline_instance = pipeline_instance.with_options(
391
+ config_path=config_path
392
+ )
393
+ template = pipeline_instance.create_run_template(name=name)
394
+
395
+ cli_utils.declare(f"Created run template `{template.id}`.")
396
+
397
+
318
398
  @pipeline.command("list", help="List all registered pipelines.")
319
399
  @list_options(PipelineFilter)
320
400
  def list_pipelines(**kwargs: Any) -> None:
zenml/config/compiler.py CHANGED
@@ -99,7 +99,10 @@ class Compiler:
99
99
 
100
100
  self._apply_stack_default_settings(pipeline=pipeline, stack=stack)
101
101
  if run_configuration.run_name:
102
- self._verify_run_name(run_configuration.run_name)
102
+ self._verify_run_name(
103
+ run_configuration.run_name,
104
+ pipeline.configuration.substitutions,
105
+ )
103
106
 
104
107
  pipeline_settings = self._filter_and_validate_settings(
105
108
  settings=pipeline.configuration.settings,
@@ -305,16 +308,22 @@ class Compiler:
305
308
  return default_settings
306
309
 
307
310
  @staticmethod
308
- def _verify_run_name(run_name: str) -> None:
311
+ def _verify_run_name(
312
+ run_name: str,
313
+ substitutions: Dict[str, str],
314
+ ) -> None:
309
315
  """Verifies that the run name contains only valid placeholders.
310
316
 
311
317
  Args:
312
318
  run_name: The run name to verify.
319
+ substitutions: The substitutions to be used in the run name.
313
320
 
314
321
  Raises:
315
322
  ValueError: If the run name contains invalid placeholders.
316
323
  """
317
- valid_placeholder_names = {"date", "time"}
324
+ valid_placeholder_names = {"date", "time"}.union(
325
+ set(substitutions.keys())
326
+ )
318
327
  placeholders = {
319
328
  v[1] for v in string.Formatter().parse(run_name) if v[1]
320
329
  }
@@ -13,6 +13,7 @@
13
13
  # permissions and limitations under the License.
14
14
  """Pipeline configuration classes."""
15
15
 
16
+ from datetime import datetime
16
17
  from typing import TYPE_CHECKING, Any, Dict, List, Optional
17
18
 
18
19
  from pydantic import SerializeAsAny, field_validator
@@ -46,6 +47,25 @@ class PipelineConfigurationUpdate(StrictBaseModel):
46
47
  model: Optional[Model] = None
47
48
  parameters: Optional[Dict[str, Any]] = None
48
49
  retry: Optional[StepRetryConfig] = None
50
+ substitutions: Dict[str, str] = {}
51
+
52
+ def _get_full_substitutions(
53
+ self, start_time: Optional[datetime]
54
+ ) -> Dict[str, str]:
55
+ """Returns the full substitutions dict.
56
+
57
+ Args:
58
+ start_time: Start time of the pipeline run.
59
+
60
+ Returns:
61
+ The full substitutions dict including date and time.
62
+ """
63
+ if start_time is None:
64
+ start_time = datetime.utcnow()
65
+ ret = self.substitutions.copy()
66
+ ret.setdefault("date", start_time.strftime("%Y_%m_%d"))
67
+ ret.setdefault("time", start_time.strftime("%H_%M_%S_%f"))
68
+ return ret
49
69
 
50
70
 
51
71
  class PipelineConfiguration(PipelineConfigurationUpdate):
@@ -52,3 +52,4 @@ class PipelineRunConfiguration(
52
52
  retry: Optional[StepRetryConfig] = None
53
53
  failure_hook_source: Optional[SourceWithValidator] = None
54
54
  success_hook_source: Optional[SourceWithValidator] = None
55
+ substitutions: Dict[str, str] = {}
@@ -13,6 +13,7 @@
13
13
  # permissions and limitations under the License.
14
14
  """Pipeline configuration classes."""
15
15
 
16
+ from datetime import datetime
16
17
  from typing import (
17
18
  TYPE_CHECKING,
18
19
  Any,
@@ -49,6 +50,7 @@ from zenml.utils.pydantic_utils import before_validator_handler
49
50
 
50
51
  if TYPE_CHECKING:
51
52
  from zenml.config import DockerSettings, ResourceSettings
53
+ from zenml.config.pipeline_configurations import PipelineConfiguration
52
54
 
53
55
  logger = get_logger(__name__)
54
56
 
@@ -152,6 +154,7 @@ class StepConfigurationUpdate(StrictBaseModel):
152
154
  success_hook_source: Optional[SourceWithValidator] = None
153
155
  model: Optional[Model] = None
154
156
  retry: Optional[StepRetryConfig] = None
157
+ substitutions: Dict[str, str] = {}
155
158
 
156
159
  outputs: Mapping[str, PartialArtifactConfiguration] = {}
157
160
 
@@ -237,6 +240,24 @@ class StepConfiguration(PartialStepConfiguration):
237
240
  model_or_dict = model_or_dict.model_dump()
238
241
  return DockerSettings.model_validate(model_or_dict)
239
242
 
243
+ def _get_full_substitutions(
244
+ self,
245
+ pipeline_config: "PipelineConfiguration",
246
+ start_time: Optional[datetime],
247
+ ) -> Dict[str, str]:
248
+ """Get the full set of substitutions for this step configuration.
249
+
250
+ Args:
251
+ pipeline_config: The pipeline configuration.
252
+ start_time: The start time of the pipeline run.
253
+
254
+ Returns:
255
+ The full set of substitutions for this step configuration.
256
+ """
257
+ ret = pipeline_config._get_full_substitutions(start_time)
258
+ ret.update(self.substitutions)
259
+ return ret
260
+
240
261
 
241
262
  class InputSpec(StrictBaseModel):
242
263
  """Step input specification."""
zenml/enums.py CHANGED
@@ -60,6 +60,7 @@ class VisualizationType(StrEnum):
60
60
  HTML = "html"
61
61
  IMAGE = "image"
62
62
  MARKDOWN = "markdown"
63
+ JSON = "json"
63
64
 
64
65
 
65
66
  class ZenMLServiceType(StrEnum):
@@ -31,7 +31,7 @@ class FeastIntegration(Integration):
31
31
 
32
32
  NAME = FEAST
33
33
  # click is added to keep the feast click version in sync with ZenML's click
34
- REQUIREMENTS = ["feast", "click>=8.0.1,<8.1.4"]
34
+ REQUIREMENTS = ["feast>=0.12.0", "click>=8.0.1,<8.1.4"]
35
35
  REQUIREMENTS_IGNORED_ON_UNINSTALL = ["click", "pandas"]
36
36
 
37
37
  @classmethod
@@ -16,7 +16,7 @@
16
16
  from typing import Any, Dict, List, Union, cast
17
17
 
18
18
  import pandas as pd
19
- from feast import FeatureStore # type: ignore
19
+ from feast import FeatureService, FeatureStore # type: ignore
20
20
  from feast.infra.registry.base_registry import BaseRegistry # type: ignore
21
21
 
22
22
  from zenml.feature_stores.base_feature_store import BaseFeatureStore
@@ -43,14 +43,14 @@ class FeastFeatureStore(BaseFeatureStore):
43
43
  def get_historical_features(
44
44
  self,
45
45
  entity_df: Union[pd.DataFrame, str],
46
- features: List[str],
46
+ features: Union[List[str], FeatureService],
47
47
  full_feature_names: bool = False,
48
48
  ) -> pd.DataFrame:
49
49
  """Returns the historical features for training or batch scoring.
50
50
 
51
51
  Args:
52
52
  entity_df: The entity DataFrame or entity name.
53
- features: The features to retrieve.
53
+ features: The features to retrieve or a FeatureService.
54
54
  full_feature_names: Whether to return the full feature names.
55
55
 
56
56
  Raise:
@@ -70,14 +70,14 @@ class FeastFeatureStore(BaseFeatureStore):
70
70
  def get_online_features(
71
71
  self,
72
72
  entity_rows: List[Dict[str, Any]],
73
- features: List[str],
73
+ features: Union[List[str], FeatureService],
74
74
  full_feature_names: bool = False,
75
75
  ) -> Dict[str, Any]:
76
76
  """Returns the latest online feature data.
77
77
 
78
78
  Args:
79
79
  entity_rows: The entity rows to retrieve.
80
- features: The features to retrieve.
80
+ features: The features to retrieve or a FeatureService.
81
81
  full_feature_names: Whether to return the full feature names.
82
82
 
83
83
  Raise:
@@ -118,17 +118,21 @@ class FeastFeatureStore(BaseFeatureStore):
118
118
  fs = FeatureStore(repo_path=self.config.feast_repo)
119
119
  return [ds.name for ds in fs.list_entities()]
120
120
 
121
- def get_feature_services(self) -> List[str]:
122
- """Returns the feature service names.
121
+ def get_feature_services(self) -> List[FeatureService]:
122
+ """Returns the feature services.
123
123
 
124
124
  Raise:
125
125
  ConnectionError: If the online component (Redis) is not available.
126
126
 
127
127
  Returns:
128
- The feature service names.
128
+ The feature services.
129
129
  """
130
130
  fs = FeatureStore(repo_path=self.config.feast_repo)
131
- return [ds.name for ds in fs.list_feature_services()]
131
+ feature_services: List[FeatureService] = list(
132
+ fs.list_feature_services()
133
+ )
134
+
135
+ return feature_services
132
136
 
133
137
  def get_feature_views(self) -> List[str]:
134
138
  """Returns the feature view names.
@@ -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
 
@@ -196,11 +196,12 @@ def get_config_environment_vars(
196
196
  return environment_vars
197
197
 
198
198
 
199
- def get_run_name(run_name_template: str) -> str:
199
+ def get_run_name(run_name_template: str, substitutions: Dict[str, str]) -> str:
200
200
  """Fill out the run name template to get a complete run name.
201
201
 
202
202
  Args:
203
203
  run_name_template: The run name template to fill out.
204
+ substitutions: The substitutions to use in the template.
204
205
 
205
206
  Raises:
206
207
  ValueError: If the run name is empty.
@@ -208,7 +209,9 @@ def get_run_name(run_name_template: str) -> str:
208
209
  Returns:
209
210
  The run name derived from the template.
210
211
  """
211
- run_name = format_name_template(run_name_template)
212
+ run_name = format_name_template(
213
+ run_name_template, substitutions=substitutions
214
+ )
212
215
 
213
216
  if run_name == "":
214
217
  raise ValueError("Empty run names are not allowed.")