zenml-nightly 0.75.0.dev20250314__py3-none-any.whl → 0.75.0.dev20250316__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 (38) hide show
  1. zenml/VERSION +1 -1
  2. zenml/cli/login.py +21 -18
  3. zenml/cli/server.py +5 -5
  4. zenml/client.py +5 -1
  5. zenml/config/server_config.py +9 -9
  6. zenml/integrations/gcp/__init__.py +1 -0
  7. zenml/integrations/gcp/flavors/vertex_orchestrator_flavor.py +5 -0
  8. zenml/integrations/gcp/flavors/vertex_step_operator_flavor.py +5 -28
  9. zenml/integrations/gcp/orchestrators/vertex_orchestrator.py +125 -78
  10. zenml/integrations/gcp/vertex_custom_job_parameters.py +50 -0
  11. zenml/login/credentials.py +26 -27
  12. zenml/login/credentials_store.py +5 -5
  13. zenml/login/pro/client.py +9 -9
  14. zenml/login/pro/utils.py +8 -8
  15. zenml/login/pro/{tenant → workspace}/__init__.py +1 -1
  16. zenml/login/pro/{tenant → workspace}/client.py +25 -25
  17. zenml/login/pro/{tenant → workspace}/models.py +27 -28
  18. zenml/models/v2/core/model.py +9 -1
  19. zenml/models/v2/core/tag.py +96 -3
  20. zenml/models/v2/misc/server_models.py +6 -6
  21. zenml/orchestrators/step_run_utils.py +1 -1
  22. zenml/utils/dashboard_utils.py +1 -1
  23. zenml/utils/tag_utils.py +0 -12
  24. zenml/zen_server/cloud_utils.py +3 -3
  25. zenml/zen_server/feature_gate/endpoint_utils.py +1 -1
  26. zenml/zen_server/feature_gate/zenml_cloud_feature_gate.py +1 -1
  27. zenml/zen_server/rbac/models.py +30 -5
  28. zenml/zen_server/rbac/zenml_cloud_rbac.py +7 -70
  29. zenml/zen_server/routers/server_endpoints.py +2 -2
  30. zenml/zen_server/zen_server_api.py +3 -3
  31. zenml/zen_stores/base_zen_store.py +3 -3
  32. zenml/zen_stores/rest_zen_store.py +1 -1
  33. zenml/zen_stores/sql_zen_store.py +60 -7
  34. {zenml_nightly-0.75.0.dev20250314.dist-info → zenml_nightly-0.75.0.dev20250316.dist-info}/METADATA +1 -1
  35. {zenml_nightly-0.75.0.dev20250314.dist-info → zenml_nightly-0.75.0.dev20250316.dist-info}/RECORD +38 -37
  36. {zenml_nightly-0.75.0.dev20250314.dist-info → zenml_nightly-0.75.0.dev20250316.dist-info}/LICENSE +0 -0
  37. {zenml_nightly-0.75.0.dev20250314.dist-info → zenml_nightly-0.75.0.dev20250316.dist-info}/WHEEL +0 -0
  38. {zenml_nightly-0.75.0.dev20250314.dist-info → zenml_nightly-0.75.0.dev20250316.dist-info}/entry_points.txt +0 -0
@@ -14,12 +14,12 @@
14
14
  """Models representing tags."""
15
15
 
16
16
  import random
17
- from typing import Optional
17
+ from typing import TYPE_CHECKING, ClassVar, List, Optional, Type, TypeVar
18
18
 
19
- from pydantic import Field
19
+ from pydantic import Field, field_validator
20
20
 
21
21
  from zenml.constants import STR_FIELD_MAX_LENGTH
22
- from zenml.enums import ColorVariants
22
+ from zenml.enums import ColorVariants, TaggableResourceTypes
23
23
  from zenml.models.v2.base.base import BaseUpdate
24
24
  from zenml.models.v2.base.scoped import (
25
25
  UserScopedFilter,
@@ -29,6 +29,14 @@ from zenml.models.v2.base.scoped import (
29
29
  UserScopedResponseMetadata,
30
30
  UserScopedResponseResources,
31
31
  )
32
+ from zenml.utils.uuid_utils import is_valid_uuid
33
+
34
+ if TYPE_CHECKING:
35
+ from sqlalchemy.sql.elements import ColumnElement
36
+
37
+ from zenml.zen_stores.schemas import BaseSchema
38
+
39
+ AnySchema = TypeVar("AnySchema", bound="BaseSchema")
32
40
 
33
41
  # ------------------ Request Model ------------------
34
42
 
@@ -49,6 +57,28 @@ class TagRequest(UserScopedRequest):
49
57
  default_factory=lambda: random.choice(list(ColorVariants)),
50
58
  )
51
59
 
60
+ @field_validator("name")
61
+ @classmethod
62
+ def validate_name_not_uuid(cls, value: str) -> str:
63
+ """Validates that the tag name is not a UUID.
64
+
65
+ Args:
66
+ value: The tag name to validate.
67
+
68
+ Returns:
69
+ The validated tag name.
70
+
71
+ Raises:
72
+ ValueError: If the tag name can be converted
73
+ to a UUID.
74
+ """
75
+ if is_valid_uuid(value):
76
+ raise ValueError(
77
+ "Tag names cannot be UUIDs or strings that "
78
+ "can be converted to UUIDs."
79
+ )
80
+ return value
81
+
52
82
 
53
83
  # ------------------ Update Model ------------------
54
84
 
@@ -60,6 +90,27 @@ class TagUpdate(BaseUpdate):
60
90
  exclusive: Optional[bool] = None
61
91
  color: Optional[ColorVariants] = None
62
92
 
93
+ @field_validator("name")
94
+ @classmethod
95
+ def validate_name_not_uuid(cls, value: Optional[str]) -> Optional[str]:
96
+ """Validates that the tag name is not a UUID.
97
+
98
+ Args:
99
+ value: The tag name to validate.
100
+
101
+ Returns:
102
+ The validated tag name.
103
+
104
+ Raises:
105
+ ValueError: If the tag name can be converted to a UUID.
106
+ """
107
+ if value is not None and is_valid_uuid(value):
108
+ raise ValueError(
109
+ "Tag names cannot be UUIDs or strings that "
110
+ "can be converted to UUIDs."
111
+ )
112
+ return value
113
+
63
114
 
64
115
  # ------------------ Response Model ------------------
65
116
 
@@ -143,6 +194,11 @@ class TagResponse(
143
194
  class TagFilter(UserScopedFilter):
144
195
  """Model to enable advanced filtering of all tags."""
145
196
 
197
+ FILTER_EXCLUDE_FIELDS: ClassVar[List[str]] = [
198
+ *UserScopedFilter.FILTER_EXCLUDE_FIELDS,
199
+ "resource_type",
200
+ ]
201
+
146
202
  name: Optional[str] = Field(
147
203
  description="The unique title of the tag.", default=None
148
204
  )
@@ -153,3 +209,40 @@ class TagFilter(UserScopedFilter):
153
209
  description="The flag signifying whether the tag is an exclusive tag.",
154
210
  default=None,
155
211
  )
212
+ resource_type: Optional[TaggableResourceTypes] = Field(
213
+ description="Filter tags associated with a specific resource type.",
214
+ default=None,
215
+ )
216
+
217
+ def get_custom_filters(
218
+ self, table: Type["AnySchema"]
219
+ ) -> List["ColumnElement[bool]"]:
220
+ """Get custom filters.
221
+
222
+ Args:
223
+ table: The query table.
224
+
225
+ Returns:
226
+ A list of custom filters.
227
+ """
228
+ custom_filters = super().get_custom_filters(table)
229
+
230
+ from sqlmodel import exists, select
231
+
232
+ from zenml.zen_stores.schemas import (
233
+ TagResourceSchema,
234
+ TagSchema,
235
+ )
236
+
237
+ if self.resource_type:
238
+ # Filter for tags that have at least one association with the specified resource type
239
+ resource_type_filter = exists(
240
+ select(TagResourceSchema).where(
241
+ TagResourceSchema.tag_id == TagSchema.id,
242
+ TagResourceSchema.resource_type
243
+ == self.resource_type.value,
244
+ )
245
+ )
246
+ custom_filters.append(resource_type_filter)
247
+
248
+ return custom_filters
@@ -131,16 +131,16 @@ class ServerModel(BaseModel):
131
131
  "connected. Only set if the server is a ZenML Pro server.",
132
132
  )
133
133
 
134
- pro_tenant_id: Optional[UUID] = Field(
134
+ pro_workspace_id: Optional[UUID] = Field(
135
135
  None,
136
- title="The ID of the ZenML Pro tenant to which the server is connected. "
137
- "Only set if the server is a ZenML Pro server.",
136
+ title="The ID of the ZenML Pro workspace to which the server is "
137
+ "connected. Only set if the server is a ZenML Pro server.",
138
138
  )
139
139
 
140
- pro_tenant_name: Optional[str] = Field(
140
+ pro_workspace_name: Optional[str] = Field(
141
141
  None,
142
- title="The name of the ZenML Pro tenant to which the server is connected. "
143
- "Only set if the server is a ZenML Pro server.",
142
+ title="The name of the ZenML Pro workspace to which the server is "
143
+ "connected. Only set if the server is a ZenML Pro server.",
144
144
  )
145
145
 
146
146
  def is_local(self) -> bool:
@@ -344,7 +344,7 @@ def log_model_version_dashboard_url(
344
344
  ) -> None:
345
345
  """Log the dashboard URL for a model version.
346
346
 
347
- If the current server is not a ZenML Pro tenant, a fallback message is
347
+ If the current server is not a ZenML Pro workspace, a fallback message is
348
348
  logged instead.
349
349
 
350
350
  Args:
@@ -33,7 +33,7 @@ logger = get_logger(__name__)
33
33
 
34
34
 
35
35
  def get_cloud_dashboard_url() -> Optional[str]:
36
- """Get the base url of the cloud dashboard if the server is a cloud tenant.
36
+ """Get the base url of the cloud dashboard if the server is a ZenML Pro workspace.
37
37
 
38
38
  Returns:
39
39
  The base url of the cloud dashboard.
zenml/utils/tag_utils.py CHANGED
@@ -379,18 +379,6 @@ def add_tags(
379
379
  else:
380
380
  tag_model = client.create_tag(name=tag)
381
381
 
382
- if tag_model.exclusive and resource_type not in [
383
- TaggableResourceTypes.PIPELINE_RUN,
384
- TaggableResourceTypes.ARTIFACT_VERSION,
385
- TaggableResourceTypes.RUN_TEMPLATE,
386
- ]:
387
- logger.warning(
388
- "The tag will be added, however, please keep in mind that "
389
- "the functionality of having exclusive tags is only "
390
- "applicable for pipeline runs, artifact versions and run "
391
- f"templates, not {resource_type.value}s."
392
- )
393
-
394
382
  if resource_id:
395
383
  client.attach_tag(
396
384
  tag_name_or_id=tag_model.name,
@@ -267,6 +267,6 @@ def cloud_connection() -> ZenMLCloudConnection:
267
267
  return _cloud_connection
268
268
 
269
269
 
270
- def send_pro_tenant_status_update() -> None:
271
- """Send a tenant status update to the Cloud API."""
272
- cloud_connection().patch("/tenant_status")
270
+ def send_pro_workspace_status_update() -> None:
271
+ """Send a workspace status update to the Cloud API."""
272
+ cloud_connection().patch("/workspace_status")
@@ -20,7 +20,7 @@ from zenml.zen_server.utils import feature_gate, server_config
20
20
 
21
21
 
22
22
  def check_entitlement(resource_type: ResourceType) -> None:
23
- """Queries the feature gate to see if the operation falls within the tenants entitlements.
23
+ """Queries the feature gate to see if the operation falls within the Pro workspaces entitlements.
24
24
 
25
25
  Raises an exception if the user is not entitled to create an instance of the
26
26
  resource. Otherwise, simply returns.
@@ -110,7 +110,7 @@ class ZenMLCloudFeatureGateInterface(FeatureGateInterface):
110
110
  feature=resource,
111
111
  total=1 if not is_decrement else -1,
112
112
  metadata={
113
- "tenant_id": str(server_config.get_external_server_id()),
113
+ "workspace_id": str(server_config.get_external_server_id()),
114
114
  "resource_id": str(resource_id),
115
115
  },
116
116
  ).model_dump()
@@ -111,11 +111,6 @@ class Resource(BaseModel):
111
111
  Resource string representation.
112
112
  """
113
113
  project_id = self.project_id
114
- if self.type == ResourceType.PROJECT and self.id:
115
- # TODO: For now, we duplicate the project ID in the string
116
- # representation when describing a project instance, because
117
- # this is what is expected by the RBAC implementation.
118
- project_id = self.id
119
114
 
120
115
  if project_id:
121
116
  representation = f"{project_id}:"
@@ -127,6 +122,36 @@ class Resource(BaseModel):
127
122
 
128
123
  return representation
129
124
 
125
+ @classmethod
126
+ def parse(cls, resource: str) -> "Resource":
127
+ """Parse an RBAC resource string into a Resource object.
128
+
129
+ Args:
130
+ resource: The resource to convert.
131
+
132
+ Returns:
133
+ The converted resource.
134
+ """
135
+ project_id: Optional[str] = None
136
+ if ":" in resource:
137
+ (
138
+ project_id,
139
+ resource_type_and_id,
140
+ ) = resource.split(":", maxsplit=1)
141
+ else:
142
+ project_id = None
143
+ resource_type_and_id = resource
144
+
145
+ resource_id: Optional[str] = None
146
+ if "/" in resource_type_and_id:
147
+ resource_type, resource_id = resource_type_and_id.split("/")
148
+ else:
149
+ resource_type = resource_type_and_id
150
+
151
+ return Resource(
152
+ type=resource_type, id=resource_id, project_id=project_id
153
+ )
154
+
130
155
  @model_validator(mode="after")
131
156
  def validate_project_id(self) -> "Resource":
132
157
  """Validate that project_id is set in combination with project-scoped resource types.
@@ -13,12 +13,11 @@
13
13
  # permissions and limitations under the License.
14
14
  """Cloud RBAC implementation."""
15
15
 
16
- from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
16
+ from typing import TYPE_CHECKING, Dict, List, Set, Tuple
17
17
 
18
18
  from zenml.zen_server.cloud_utils import cloud_connection
19
- from zenml.zen_server.rbac.models import Action, Resource, ResourceType
19
+ from zenml.zen_server.rbac.models import Action, Resource
20
20
  from zenml.zen_server.rbac.rbac_interface import RBACInterface
21
- from zenml.zen_server.utils import server_config
22
21
 
23
22
  if TYPE_CHECKING:
24
23
  from zenml.models import UserResponse
@@ -29,64 +28,6 @@ ALLOWED_RESOURCE_IDS_ENDPOINT = "/rbac/allowed_resource_ids"
29
28
  RESOURCE_MEMBERSHIP_ENDPOINT = "/rbac/resource_members"
30
29
  RESOURCES_ENDPOINT = "/rbac/resources"
31
30
 
32
- SERVER_SCOPE_IDENTIFIER = "server"
33
-
34
- SERVER_ID = server_config().external_server_id
35
-
36
-
37
- def _convert_to_cloud_resource(resource: Resource) -> str:
38
- """Convert a resource to a ZenML Pro Management Plane resource.
39
-
40
- Args:
41
- resource: The resource to convert.
42
-
43
- Returns:
44
- The converted resource.
45
- """
46
- return f"{SERVER_ID}@{SERVER_SCOPE_IDENTIFIER}:{resource}"
47
-
48
-
49
- def _convert_from_cloud_resource(cloud_resource: str) -> Resource:
50
- """Convert a cloud resource to a ZenML server resource.
51
-
52
- Args:
53
- cloud_resource: The cloud resource to convert.
54
-
55
- Raises:
56
- ValueError: If the cloud resource is invalid for this server.
57
-
58
- Returns:
59
- The converted resource.
60
- """
61
- scope, project_resource_type_and_id = cloud_resource.split(":", maxsplit=1)
62
-
63
- if scope != f"{SERVER_ID}@{SERVER_SCOPE_IDENTIFIER}":
64
- raise ValueError("Invalid scope for server resource.")
65
-
66
- project_id: Optional[str] = None
67
- if ":" in project_resource_type_and_id:
68
- (
69
- project_id,
70
- resource_type_and_id,
71
- ) = project_resource_type_and_id.split(":", maxsplit=1)
72
- else:
73
- project_id = None
74
- resource_type_and_id = project_resource_type_and_id
75
-
76
- resource_id: Optional[str] = None
77
- if "/" in resource_type_and_id:
78
- resource_type, resource_id = resource_type_and_id.split("/")
79
- else:
80
- resource_type = resource_type_and_id
81
-
82
- if resource_type == ResourceType.PROJECT and project_id is not None:
83
- # TODO: For now, we duplicate the project ID in the string
84
- # representation when describing a project instance, because
85
- # this is what is expected by the RBAC implementation.
86
- project_id = None
87
-
88
- return Resource(type=resource_type, id=resource_id, project_id=project_id)
89
-
90
31
 
91
32
  class ZenMLCloudRBAC(RBACInterface):
92
33
  """RBAC implementation that uses the ZenML Pro Management Plane as a backend."""
@@ -123,9 +64,7 @@ class ZenMLCloudRBAC(RBACInterface):
123
64
 
124
65
  params = {
125
66
  "user_id": str(user.external_user_id),
126
- "resources": [
127
- _convert_to_cloud_resource(resource) for resource in resources
128
- ],
67
+ "resources": resources,
129
68
  "action": str(action),
130
69
  }
131
70
  response = self._connection.get(
@@ -134,7 +73,7 @@ class ZenMLCloudRBAC(RBACInterface):
134
73
  value = response.json()
135
74
 
136
75
  assert isinstance(value, dict)
137
- return {_convert_from_cloud_resource(k): v for k, v in value.items()}
76
+ return {Resource.parse(k): v for k, v in value.items()}
138
77
 
139
78
  def list_allowed_resource_ids(
140
79
  self, user: "UserResponse", resource: Resource, action: Action
@@ -164,7 +103,7 @@ class ZenMLCloudRBAC(RBACInterface):
164
103
  assert user.external_user_id
165
104
  params = {
166
105
  "user_id": str(user.external_user_id),
167
- "resource": _convert_to_cloud_resource(resource),
106
+ "resource": str(resource),
168
107
  "action": str(action),
169
108
  }
170
109
  response = self._connection.get(
@@ -194,7 +133,7 @@ class ZenMLCloudRBAC(RBACInterface):
194
133
 
195
134
  data = {
196
135
  "user_id": str(user.external_user_id),
197
- "resource": _convert_to_cloud_resource(resource),
136
+ "resource": str(resource),
198
137
  "actions": [str(action) for action in actions],
199
138
  }
200
139
  self._connection.post(endpoint=RESOURCE_MEMBERSHIP_ENDPOINT, data=data)
@@ -207,8 +146,6 @@ class ZenMLCloudRBAC(RBACInterface):
207
146
  information.
208
147
  """
209
148
  params = {
210
- "resources": [
211
- _convert_to_cloud_resource(resource) for resource in resources
212
- ],
149
+ "resources": [str(resource) for resource in resources],
213
150
  }
214
151
  self._connection.delete(endpoint=RESOURCES_ENDPOINT, params=params)
@@ -145,8 +145,8 @@ def get_onboarding_state(
145
145
 
146
146
 
147
147
  # We don't have any concrete value that tells us whether a server is a cloud
148
- # tenant, so we use `external_server_id` as the best proxy option.
149
- # For cloud tenants, we don't add these endpoints as the server settings don't
148
+ # workspace, so we use `external_server_id` as the best proxy option.
149
+ # For cloud workspaces, we don't add these endpoints as the server settings don't
150
150
  # have any effect and even allow users to disable functionality that is
151
151
  # necessary for the cloud onboarding to work.
152
152
  if server_config().external_server_id is None:
@@ -55,7 +55,7 @@ from zenml.constants import (
55
55
  from zenml.enums import AuthScheme, SourceContextTypes
56
56
  from zenml.models import ServerDeploymentType
57
57
  from zenml.utils.time_utils import utc_now
58
- from zenml.zen_server.cloud_utils import send_pro_tenant_status_update
58
+ from zenml.zen_server.cloud_utils import send_pro_workspace_status_update
59
59
  from zenml.zen_server.exceptions import error_detail
60
60
  from zenml.zen_server.routers import (
61
61
  actions_endpoints,
@@ -394,9 +394,9 @@ def initialize() -> None:
394
394
  initialize_secure_headers()
395
395
  initialize_memcache(cfg.memcache_max_capacity, cfg.memcache_default_expiry)
396
396
  if cfg.deployment_type == ServerDeploymentType.CLOUD:
397
- # Send a tenant status update to the Cloud API to indicate that the
397
+ # Send a workspace status update to the Cloud API to indicate that the
398
398
  # ZenML server is running or to update the version and server URL.
399
- send_pro_tenant_status_update()
399
+ send_pro_workspace_status_update()
400
400
 
401
401
 
402
402
  DASHBOARD_REDIRECT_URL = None
@@ -405,9 +405,9 @@ class BaseZenStore(
405
405
  store_info.pro_api_url = pro_config.api_url
406
406
  store_info.pro_dashboard_url = pro_config.dashboard_url
407
407
  store_info.pro_organization_id = pro_config.organization_id
408
- store_info.pro_tenant_id = pro_config.tenant_id
409
- if pro_config.tenant_name:
410
- store_info.pro_tenant_name = pro_config.tenant_name
408
+ store_info.pro_workspace_id = pro_config.workspace_id
409
+ if pro_config.workspace_name:
410
+ store_info.pro_workspace_name = pro_config.workspace_name
411
411
  if pro_config.organization_name:
412
412
  store_info.pro_organization_name = pro_config.organization_name
413
413
 
@@ -4098,7 +4098,7 @@ class RestZenStore(BaseZenStore):
4098
4098
  "password": password,
4099
4099
  }
4100
4100
  elif self.server_info.is_pro_server():
4101
- # ZenML Pro tenants use a proprietary authorization grant
4101
+ # ZenML Pro workspaces use a proprietary authorization grant
4102
4102
  # where the ZenML Pro API session token is exchanged for a
4103
4103
  # regular ZenML server access token.
4104
4104
 
@@ -899,8 +899,8 @@ class SqlZenStore(BaseZenStore):
899
899
  server_config = ServerConfiguration.get_server_config()
900
900
 
901
901
  if server_config.deployment_type == ServerDeploymentType.CLOUD:
902
- # Do not send events for cloud tenants where the event comes from
903
- # the cloud API
902
+ # Do not send events for Pro workspaces where the event comes from
903
+ # the Pro API
904
904
  return
905
905
 
906
906
  query = select(UserSchema).where(
@@ -11296,6 +11296,22 @@ class SqlZenStore(BaseZenStore):
11296
11296
 
11297
11297
  tag_schemas = []
11298
11298
  for tag in tags:
11299
+ # Check if the tag is a string that can be converted to a UUID
11300
+ if isinstance(tag, str):
11301
+ try:
11302
+ tag_uuid = UUID(tag)
11303
+ except ValueError:
11304
+ # Not a valid UUID string, proceed normally
11305
+ pass
11306
+ else:
11307
+ tag_schema = self._get_schema_by_id(
11308
+ resource_id=tag_uuid,
11309
+ schema_class=TagSchema,
11310
+ session=session,
11311
+ )
11312
+ tag_schemas.append(tag_schema)
11313
+ continue
11314
+
11299
11315
  try:
11300
11316
  if isinstance(tag, tag_utils.Tag):
11301
11317
  tag_request = tag.to_request()
@@ -11523,7 +11539,7 @@ class SqlZenStore(BaseZenStore):
11523
11539
  An updated tag.
11524
11540
 
11525
11541
  Raises:
11526
- IllegalOperationError: If the tag can not be converted to an exclusive tag due
11542
+ ValueError: If the tag can not be converted to an exclusive tag due
11527
11543
  to it being associated to multiple entities.
11528
11544
  """
11529
11545
  with Session(self.engine) as session:
@@ -11539,6 +11555,41 @@ class SqlZenStore(BaseZenStore):
11539
11555
 
11540
11556
  if tag_update_model.exclusive is True:
11541
11557
  error_messages = []
11558
+
11559
+ # Define allowed resource types for exclusive tags
11560
+ allowed_resource_types = [
11561
+ TaggableResourceTypes.PIPELINE_RUN.value,
11562
+ TaggableResourceTypes.ARTIFACT_VERSION.value,
11563
+ TaggableResourceTypes.RUN_TEMPLATE.value,
11564
+ ]
11565
+
11566
+ # Check if tag is associated with any non-allowed resource types
11567
+ non_allowed_resources_query = (
11568
+ select(TagResourceSchema.resource_type)
11569
+ .where(
11570
+ TagResourceSchema.tag_id == tag.id,
11571
+ TagResourceSchema.resource_type.not_in( # type: ignore[attr-defined]
11572
+ allowed_resource_types
11573
+ ),
11574
+ )
11575
+ .distinct()
11576
+ )
11577
+
11578
+ non_allowed_resources = session.exec(
11579
+ non_allowed_resources_query
11580
+ ).all()
11581
+ if non_allowed_resources:
11582
+ error_message = (
11583
+ f"The tag `{tag.name}` cannot be made "
11584
+ "exclusive because it is associated with "
11585
+ "non-allowed resource types: "
11586
+ f"{', '.join(non_allowed_resources)}. "
11587
+ "Exclusive tags can only be applied "
11588
+ "to pipeline runs, artifact versions, "
11589
+ "and run templates."
11590
+ )
11591
+ error_messages.append(error_message)
11592
+
11542
11593
  for resource_type, resource_id, scope_id in [
11543
11594
  (
11544
11595
  TaggableResourceTypes.PIPELINE_RUN,
@@ -11603,7 +11654,7 @@ class SqlZenStore(BaseZenStore):
11603
11654
  error_messages.append(error)
11604
11655
 
11605
11656
  if error_messages:
11606
- raise IllegalOperationError(
11657
+ raise ValueError(
11607
11658
  "\n".join(error_messages)
11608
11659
  + "\nYou can only convert a tag into an exclusive tag "
11609
11660
  "if the conflicts mentioned above are resolved."
@@ -11705,8 +11756,8 @@ class SqlZenStore(BaseZenStore):
11705
11756
  The newly created tag resource relationships.
11706
11757
 
11707
11758
  Raises:
11708
- ValueError: If an exclusive tag is being attached to multiple resources
11709
- of the same type within the same scope.
11759
+ ValueError: If an exclusive tag is being attached
11760
+ to multiple resources of the same type within the same scope.
11710
11761
  EntityExistsError: If a tag resource already exists.
11711
11762
  """
11712
11763
  max_retries = 10
@@ -11837,7 +11888,9 @@ class SqlZenStore(BaseZenStore):
11837
11888
  )
11838
11889
  )
11839
11890
  else:
11840
- logger.debug(
11891
+ raise ValueError(
11892
+ "Can not attach exclusive tag to resource of type "
11893
+ f"{resource_type.value} with ID: `{resource.id}`. "
11841
11894
  "Exclusive tag functionality only works for "
11842
11895
  "templates, for pipeline runs (within the scope of "
11843
11896
  "pipelines) and for artifact versions (within the "
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: zenml-nightly
3
- Version: 0.75.0.dev20250314
3
+ Version: 0.75.0.dev20250316
4
4
  Summary: ZenML: Write production-ready ML code.
5
5
  License: Apache-2.0
6
6
  Keywords: machine learning,production,pipeline,mlops,devops