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.
- zenml/VERSION +1 -1
- zenml/cli/login.py +21 -18
- zenml/cli/server.py +5 -5
- zenml/client.py +5 -1
- zenml/config/server_config.py +9 -9
- zenml/integrations/gcp/__init__.py +1 -0
- zenml/integrations/gcp/flavors/vertex_orchestrator_flavor.py +5 -0
- zenml/integrations/gcp/flavors/vertex_step_operator_flavor.py +5 -28
- zenml/integrations/gcp/orchestrators/vertex_orchestrator.py +125 -78
- zenml/integrations/gcp/vertex_custom_job_parameters.py +50 -0
- zenml/login/credentials.py +26 -27
- zenml/login/credentials_store.py +5 -5
- zenml/login/pro/client.py +9 -9
- zenml/login/pro/utils.py +8 -8
- zenml/login/pro/{tenant → workspace}/__init__.py +1 -1
- zenml/login/pro/{tenant → workspace}/client.py +25 -25
- zenml/login/pro/{tenant → workspace}/models.py +27 -28
- zenml/models/v2/core/model.py +9 -1
- zenml/models/v2/core/tag.py +96 -3
- zenml/models/v2/misc/server_models.py +6 -6
- zenml/orchestrators/step_run_utils.py +1 -1
- zenml/utils/dashboard_utils.py +1 -1
- zenml/utils/tag_utils.py +0 -12
- zenml/zen_server/cloud_utils.py +3 -3
- zenml/zen_server/feature_gate/endpoint_utils.py +1 -1
- zenml/zen_server/feature_gate/zenml_cloud_feature_gate.py +1 -1
- zenml/zen_server/rbac/models.py +30 -5
- zenml/zen_server/rbac/zenml_cloud_rbac.py +7 -70
- zenml/zen_server/routers/server_endpoints.py +2 -2
- zenml/zen_server/zen_server_api.py +3 -3
- zenml/zen_stores/base_zen_store.py +3 -3
- zenml/zen_stores/rest_zen_store.py +1 -1
- zenml/zen_stores/sql_zen_store.py +60 -7
- {zenml_nightly-0.75.0.dev20250314.dist-info → zenml_nightly-0.75.0.dev20250316.dist-info}/METADATA +1 -1
- {zenml_nightly-0.75.0.dev20250314.dist-info → zenml_nightly-0.75.0.dev20250316.dist-info}/RECORD +38 -37
- {zenml_nightly-0.75.0.dev20250314.dist-info → zenml_nightly-0.75.0.dev20250316.dist-info}/LICENSE +0 -0
- {zenml_nightly-0.75.0.dev20250314.dist-info → zenml_nightly-0.75.0.dev20250316.dist-info}/WHEEL +0 -0
- {zenml_nightly-0.75.0.dev20250314.dist-info → zenml_nightly-0.75.0.dev20250316.dist-info}/entry_points.txt +0 -0
zenml/models/v2/core/tag.py
CHANGED
@@ -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
|
-
|
134
|
+
pro_workspace_id: Optional[UUID] = Field(
|
135
135
|
None,
|
136
|
-
title="The ID of the ZenML Pro
|
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
|
-
|
140
|
+
pro_workspace_name: Optional[str] = Field(
|
141
141
|
None,
|
142
|
-
title="The name of the ZenML Pro
|
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
|
347
|
+
If the current server is not a ZenML Pro workspace, a fallback message is
|
348
348
|
logged instead.
|
349
349
|
|
350
350
|
Args:
|
zenml/utils/dashboard_utils.py
CHANGED
@@ -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
|
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,
|
zenml/zen_server/cloud_utils.py
CHANGED
@@ -267,6 +267,6 @@ def cloud_connection() -> ZenMLCloudConnection:
|
|
267
267
|
return _cloud_connection
|
268
268
|
|
269
269
|
|
270
|
-
def
|
271
|
-
"""Send a
|
272
|
-
cloud_connection().patch("/
|
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
|
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
|
-
"
|
113
|
+
"workspace_id": str(server_config.get_external_server_id()),
|
114
114
|
"resource_id": str(resource_id),
|
115
115
|
},
|
116
116
|
).model_dump()
|
zenml/zen_server/rbac/models.py
CHANGED
@@ -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,
|
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
|
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 {
|
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":
|
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":
|
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
|
-
#
|
149
|
-
# For cloud
|
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
|
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
|
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
|
-
|
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.
|
409
|
-
if pro_config.
|
410
|
-
store_info.
|
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
|
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
|
903
|
-
# the
|
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
|
-
|
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
|
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
|
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
|
-
|
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 "
|