viettelcloud-aiplatform 0.3.0__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.
- viettelcloud/__init__.py +1 -0
- viettelcloud/aiplatform/__init__.py +15 -0
- viettelcloud/aiplatform/common/__init__.py +0 -0
- viettelcloud/aiplatform/common/constants.py +22 -0
- viettelcloud/aiplatform/common/types.py +28 -0
- viettelcloud/aiplatform/common/utils.py +40 -0
- viettelcloud/aiplatform/hub/OWNERS +14 -0
- viettelcloud/aiplatform/hub/__init__.py +25 -0
- viettelcloud/aiplatform/hub/api/__init__.py +13 -0
- viettelcloud/aiplatform/hub/api/_proxy_client.py +355 -0
- viettelcloud/aiplatform/hub/api/model_registry_client.py +561 -0
- viettelcloud/aiplatform/hub/api/model_registry_client_test.py +462 -0
- viettelcloud/aiplatform/optimizer/__init__.py +45 -0
- viettelcloud/aiplatform/optimizer/api/__init__.py +0 -0
- viettelcloud/aiplatform/optimizer/api/optimizer_client.py +248 -0
- viettelcloud/aiplatform/optimizer/backends/__init__.py +13 -0
- viettelcloud/aiplatform/optimizer/backends/base.py +77 -0
- viettelcloud/aiplatform/optimizer/backends/kubernetes/__init__.py +13 -0
- viettelcloud/aiplatform/optimizer/backends/kubernetes/backend.py +563 -0
- viettelcloud/aiplatform/optimizer/backends/kubernetes/utils.py +112 -0
- viettelcloud/aiplatform/optimizer/constants/__init__.py +13 -0
- viettelcloud/aiplatform/optimizer/constants/constants.py +59 -0
- viettelcloud/aiplatform/optimizer/types/__init__.py +13 -0
- viettelcloud/aiplatform/optimizer/types/algorithm_types.py +87 -0
- viettelcloud/aiplatform/optimizer/types/optimization_types.py +135 -0
- viettelcloud/aiplatform/optimizer/types/search_types.py +95 -0
- viettelcloud/aiplatform/py.typed +0 -0
- viettelcloud/aiplatform/trainer/__init__.py +82 -0
- viettelcloud/aiplatform/trainer/api/__init__.py +3 -0
- viettelcloud/aiplatform/trainer/api/trainer_client.py +277 -0
- viettelcloud/aiplatform/trainer/api/trainer_client_test.py +72 -0
- viettelcloud/aiplatform/trainer/backends/__init__.py +0 -0
- viettelcloud/aiplatform/trainer/backends/base.py +94 -0
- viettelcloud/aiplatform/trainer/backends/container/adapters/base.py +195 -0
- viettelcloud/aiplatform/trainer/backends/container/adapters/docker.py +231 -0
- viettelcloud/aiplatform/trainer/backends/container/adapters/podman.py +258 -0
- viettelcloud/aiplatform/trainer/backends/container/backend.py +668 -0
- viettelcloud/aiplatform/trainer/backends/container/backend_test.py +867 -0
- viettelcloud/aiplatform/trainer/backends/container/runtime_loader.py +631 -0
- viettelcloud/aiplatform/trainer/backends/container/runtime_loader_test.py +637 -0
- viettelcloud/aiplatform/trainer/backends/container/types.py +67 -0
- viettelcloud/aiplatform/trainer/backends/container/utils.py +213 -0
- viettelcloud/aiplatform/trainer/backends/kubernetes/__init__.py +0 -0
- viettelcloud/aiplatform/trainer/backends/kubernetes/backend.py +710 -0
- viettelcloud/aiplatform/trainer/backends/kubernetes/backend_test.py +1344 -0
- viettelcloud/aiplatform/trainer/backends/kubernetes/constants.py +15 -0
- viettelcloud/aiplatform/trainer/backends/kubernetes/utils.py +636 -0
- viettelcloud/aiplatform/trainer/backends/kubernetes/utils_test.py +582 -0
- viettelcloud/aiplatform/trainer/backends/localprocess/__init__.py +0 -0
- viettelcloud/aiplatform/trainer/backends/localprocess/backend.py +306 -0
- viettelcloud/aiplatform/trainer/backends/localprocess/backend_test.py +501 -0
- viettelcloud/aiplatform/trainer/backends/localprocess/constants.py +90 -0
- viettelcloud/aiplatform/trainer/backends/localprocess/job.py +184 -0
- viettelcloud/aiplatform/trainer/backends/localprocess/types.py +52 -0
- viettelcloud/aiplatform/trainer/backends/localprocess/utils.py +302 -0
- viettelcloud/aiplatform/trainer/constants/__init__.py +0 -0
- viettelcloud/aiplatform/trainer/constants/constants.py +179 -0
- viettelcloud/aiplatform/trainer/options/__init__.py +52 -0
- viettelcloud/aiplatform/trainer/options/common.py +55 -0
- viettelcloud/aiplatform/trainer/options/kubernetes.py +502 -0
- viettelcloud/aiplatform/trainer/options/kubernetes_test.py +259 -0
- viettelcloud/aiplatform/trainer/options/localprocess.py +20 -0
- viettelcloud/aiplatform/trainer/test/common.py +22 -0
- viettelcloud/aiplatform/trainer/types/__init__.py +0 -0
- viettelcloud/aiplatform/trainer/types/types.py +517 -0
- viettelcloud/aiplatform/trainer/types/types_test.py +115 -0
- viettelcloud_aiplatform-0.3.0.dist-info/METADATA +226 -0
- viettelcloud_aiplatform-0.3.0.dist-info/RECORD +71 -0
- viettelcloud_aiplatform-0.3.0.dist-info/WHEEL +4 -0
- viettelcloud_aiplatform-0.3.0.dist-info/licenses/LICENSE +201 -0
- viettelcloud_aiplatform-0.3.0.dist-info/licenses/NOTICE +36 -0
viettelcloud/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Namespace package for viettelcloud
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright 2025 The Kubeflow Authors.
|
|
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
|
+
# http://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 or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
__version__ = "0.3.0"
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Copyright 2025 The Kubeflow Authors.
|
|
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
|
+
# http://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 or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# The default Kubernetes namespace.
|
|
16
|
+
DEFAULT_NAMESPACE = "default"
|
|
17
|
+
|
|
18
|
+
# How long to wait in seconds for requests to the Kubernetes API Server.
|
|
19
|
+
DEFAULT_TIMEOUT = 120
|
|
20
|
+
|
|
21
|
+
# Unknown indicates that the value can't be identified.
|
|
22
|
+
UNKNOWN = "Unknown"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Copyright 2025 The Kubeflow Authors.
|
|
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
|
+
# http://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 or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from kubernetes import client
|
|
18
|
+
from pydantic import BaseModel
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class KubernetesBackendConfig(BaseModel):
|
|
22
|
+
namespace: Optional[str] = None
|
|
23
|
+
config_file: Optional[str] = None
|
|
24
|
+
context: Optional[str] = None
|
|
25
|
+
client_configuration: Optional[client.Configuration] = None
|
|
26
|
+
|
|
27
|
+
class Config:
|
|
28
|
+
arbitrary_types_allowed = True
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Copyright 2025 The Kubeflow Authors.
|
|
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
|
+
# http://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 or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
import os
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from kubernetes import config
|
|
18
|
+
|
|
19
|
+
from viettelcloud.aiplatform.common import constants
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_running_in_k8s() -> bool:
|
|
23
|
+
return os.path.isdir("/var/run/secrets/kubernetes.io/")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_default_target_namespace(context: Optional[str] = None) -> str:
|
|
27
|
+
if not is_running_in_k8s():
|
|
28
|
+
try:
|
|
29
|
+
all_contexts, current_context = config.list_kube_config_contexts()
|
|
30
|
+
# If context is set, we should get namespace from it.
|
|
31
|
+
if context:
|
|
32
|
+
for c in all_contexts:
|
|
33
|
+
if isinstance(c, dict) and c.get("name") == context:
|
|
34
|
+
return c["context"]["namespace"]
|
|
35
|
+
# Otherwise, try to get namespace from the current context.
|
|
36
|
+
return current_context["context"]["namespace"]
|
|
37
|
+
except Exception:
|
|
38
|
+
return constants.DEFAULT_NAMESPACE
|
|
39
|
+
with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace") as f:
|
|
40
|
+
return f.readline()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Copyright 2025 The Kubeflow Authors.
|
|
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
|
+
# http://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 or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from viettelcloud.aiplatform.hub.api.model_registry_client import ModelRegistryClient
|
|
16
|
+
from viettelcloud.aiplatform.hub.api._proxy_client import (
|
|
17
|
+
AuthenticationError,
|
|
18
|
+
PermissionDeniedError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"ModelRegistryClient",
|
|
23
|
+
"AuthenticationError",
|
|
24
|
+
"PermissionDeniedError",
|
|
25
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright 2025 The Kubeflow Authors.
|
|
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
|
+
# http://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 or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# Copyright 2025 The Kubeflow Authors.
|
|
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
|
+
# http://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 or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Internal HTTP client for cmp-backend proxy mode."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from typing import Any, Iterator, Optional
|
|
21
|
+
|
|
22
|
+
import requests
|
|
23
|
+
from requests.adapters import HTTPAdapter
|
|
24
|
+
from urllib3.util.retry import Retry
|
|
25
|
+
|
|
26
|
+
LOG = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AuthenticationError(Exception):
|
|
30
|
+
"""Raised when PAT token is invalid or expired."""
|
|
31
|
+
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PermissionDeniedError(Exception):
|
|
36
|
+
"""Raised when user lacks permission for the operation."""
|
|
37
|
+
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ProxyHTTPClient:
|
|
42
|
+
"""
|
|
43
|
+
HTTP client for cmp-backend Model Registry proxy.
|
|
44
|
+
|
|
45
|
+
This client handles communication with cmp-backend which:
|
|
46
|
+
- Validates PAT tokens
|
|
47
|
+
- Enforces RBAC permissions
|
|
48
|
+
- Routes to the correct MR instance based on project and region
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
cmp_backend_url: str,
|
|
54
|
+
pat_token: str,
|
|
55
|
+
project_id: str,
|
|
56
|
+
region: str,
|
|
57
|
+
timeout: float = 30,
|
|
58
|
+
):
|
|
59
|
+
"""
|
|
60
|
+
Initialize proxy client.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
cmp_backend_url: Base URL of cmp-backend (e.g., https://api.viettelcloud.vn)
|
|
64
|
+
pat_token: Personal Access Token for authentication
|
|
65
|
+
project_id: Project UUID or slug
|
|
66
|
+
region: Region UUID or name (e.g., "HN", "HCM")
|
|
67
|
+
timeout: Request timeout in seconds
|
|
68
|
+
"""
|
|
69
|
+
self._base_url = f"{cmp_backend_url.rstrip('/')}/v2/model-registry-proxy/proxy"
|
|
70
|
+
self._project_id = project_id
|
|
71
|
+
self._region = region
|
|
72
|
+
self._timeout = timeout
|
|
73
|
+
|
|
74
|
+
self._session = requests.Session()
|
|
75
|
+
self._session.headers.update({
|
|
76
|
+
"Authorization": f"Token {pat_token}",
|
|
77
|
+
"X-Project-ID": project_id,
|
|
78
|
+
"X-Region": region,
|
|
79
|
+
"Content-Type": "application/json",
|
|
80
|
+
"Accept": "application/json",
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
# Retry configuration
|
|
84
|
+
retries = Retry(
|
|
85
|
+
total=3,
|
|
86
|
+
backoff_factor=0.5,
|
|
87
|
+
status_forcelist=[502, 503, 504],
|
|
88
|
+
)
|
|
89
|
+
adapter = HTTPAdapter(
|
|
90
|
+
pool_connections=10,
|
|
91
|
+
pool_maxsize=10,
|
|
92
|
+
max_retries=retries,
|
|
93
|
+
)
|
|
94
|
+
self._session.mount("http://", adapter)
|
|
95
|
+
self._session.mount("https://", adapter)
|
|
96
|
+
|
|
97
|
+
def __del__(self):
|
|
98
|
+
if hasattr(self, "_session") and self._session:
|
|
99
|
+
try:
|
|
100
|
+
self._session.close()
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
def _request(
|
|
105
|
+
self,
|
|
106
|
+
method: str,
|
|
107
|
+
path: str,
|
|
108
|
+
body: Optional[dict] = None,
|
|
109
|
+
params: Optional[dict] = None,
|
|
110
|
+
) -> requests.Response:
|
|
111
|
+
"""
|
|
112
|
+
Send request to cmp-backend proxy.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
method: HTTP method
|
|
116
|
+
path: API path
|
|
117
|
+
body: Request body (JSON)
|
|
118
|
+
params: Query parameters
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Response object
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
AuthenticationError: If PAT token is invalid/expired
|
|
125
|
+
PermissionDeniedError: If user lacks permission
|
|
126
|
+
requests.HTTPError: For other HTTP errors
|
|
127
|
+
"""
|
|
128
|
+
url = f"{self._base_url}{path}"
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
LOG.debug(f"Proxy request: {method} {url}")
|
|
132
|
+
resp = self._session.request(
|
|
133
|
+
method,
|
|
134
|
+
url,
|
|
135
|
+
json=body,
|
|
136
|
+
params=params,
|
|
137
|
+
timeout=self._timeout,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Handle auth errors
|
|
141
|
+
if resp.status_code == 401:
|
|
142
|
+
raise AuthenticationError("PAT token is invalid or expired")
|
|
143
|
+
if resp.status_code == 403:
|
|
144
|
+
error_msg = "Permission denied"
|
|
145
|
+
try:
|
|
146
|
+
error_msg = resp.json().get("detail", error_msg)
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
raise PermissionDeniedError(error_msg)
|
|
150
|
+
|
|
151
|
+
resp.raise_for_status()
|
|
152
|
+
return resp
|
|
153
|
+
|
|
154
|
+
except requests.exceptions.ConnectionError as e:
|
|
155
|
+
LOG.error(f"Connection error: {url} - {e}")
|
|
156
|
+
raise ConnectionError(f"Failed to connect to cmp-backend: {e}") from e
|
|
157
|
+
|
|
158
|
+
except requests.exceptions.Timeout as e:
|
|
159
|
+
LOG.error(f"Timeout: {url} - {e}")
|
|
160
|
+
raise TimeoutError(f"Request to cmp-backend timed out: {e}") from e
|
|
161
|
+
|
|
162
|
+
def _get(self, path: str, params: Optional[dict] = None) -> Any:
|
|
163
|
+
"""GET request, returns JSON."""
|
|
164
|
+
resp = self._request("GET", path, params=params)
|
|
165
|
+
return resp.json()
|
|
166
|
+
|
|
167
|
+
def _post(self, path: str, body: Optional[dict] = None) -> Any:
|
|
168
|
+
"""POST request, returns JSON."""
|
|
169
|
+
resp = self._request("POST", path, body=body)
|
|
170
|
+
return resp.json()
|
|
171
|
+
|
|
172
|
+
def _patch(self, path: str, body: Optional[dict] = None) -> Any:
|
|
173
|
+
"""PATCH request, returns JSON."""
|
|
174
|
+
resp = self._request("PATCH", path, body=body)
|
|
175
|
+
return resp.json()
|
|
176
|
+
|
|
177
|
+
def _delete(self, path: str) -> None:
|
|
178
|
+
"""DELETE request."""
|
|
179
|
+
self._request("DELETE", path)
|
|
180
|
+
|
|
181
|
+
# =========================================================================
|
|
182
|
+
# Registered Models
|
|
183
|
+
# =========================================================================
|
|
184
|
+
|
|
185
|
+
def list_registered_models(
|
|
186
|
+
self,
|
|
187
|
+
page_size: Optional[int] = None,
|
|
188
|
+
order_by: Optional[str] = None,
|
|
189
|
+
next_page_token: Optional[str] = None,
|
|
190
|
+
) -> dict:
|
|
191
|
+
"""
|
|
192
|
+
List registered models.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
page_size: Maximum number of items to return
|
|
196
|
+
order_by: Sort order (e.g., "name", "-createTime")
|
|
197
|
+
next_page_token: Token for pagination
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Dict with 'items' and optional 'nextPageToken'
|
|
201
|
+
"""
|
|
202
|
+
params = {}
|
|
203
|
+
if page_size:
|
|
204
|
+
params["pageSize"] = page_size
|
|
205
|
+
if order_by:
|
|
206
|
+
params["orderBy"] = order_by
|
|
207
|
+
if next_page_token:
|
|
208
|
+
params["nextPageToken"] = next_page_token
|
|
209
|
+
return self._get("/registered_models/", params=params)
|
|
210
|
+
|
|
211
|
+
def get_registered_model(self, model_id: str) -> dict:
|
|
212
|
+
"""Get a registered model by ID or name."""
|
|
213
|
+
return self._get(f"/registered_models/{model_id}/")
|
|
214
|
+
|
|
215
|
+
def create_registered_model(
|
|
216
|
+
self,
|
|
217
|
+
name: str,
|
|
218
|
+
description: Optional[str] = None,
|
|
219
|
+
owner: Optional[str] = None,
|
|
220
|
+
custom_properties: Optional[dict] = None,
|
|
221
|
+
) -> dict:
|
|
222
|
+
"""Create a registered model."""
|
|
223
|
+
data = {"name": name}
|
|
224
|
+
if description:
|
|
225
|
+
data["description"] = description
|
|
226
|
+
if owner:
|
|
227
|
+
data["owner"] = owner
|
|
228
|
+
if custom_properties:
|
|
229
|
+
data["customProperties"] = custom_properties
|
|
230
|
+
return self._post("/registered_models/", body=data)
|
|
231
|
+
|
|
232
|
+
def update_registered_model(
|
|
233
|
+
self,
|
|
234
|
+
model_id: str,
|
|
235
|
+
description: Optional[str] = None,
|
|
236
|
+
custom_properties: Optional[dict] = None,
|
|
237
|
+
) -> dict:
|
|
238
|
+
"""Update a registered model."""
|
|
239
|
+
data = {}
|
|
240
|
+
if description is not None:
|
|
241
|
+
data["description"] = description
|
|
242
|
+
if custom_properties is not None:
|
|
243
|
+
data["customProperties"] = custom_properties
|
|
244
|
+
return self._patch(f"/registered_models/{model_id}/", body=data)
|
|
245
|
+
|
|
246
|
+
def delete_registered_model(self, model_id: str) -> None:
|
|
247
|
+
"""Delete a registered model."""
|
|
248
|
+
self._delete(f"/registered_models/{model_id}/")
|
|
249
|
+
|
|
250
|
+
# =========================================================================
|
|
251
|
+
# Model Versions
|
|
252
|
+
# =========================================================================
|
|
253
|
+
|
|
254
|
+
def list_model_versions(
|
|
255
|
+
self,
|
|
256
|
+
model_name: str,
|
|
257
|
+
page_size: Optional[int] = None,
|
|
258
|
+
order_by: Optional[str] = None,
|
|
259
|
+
next_page_token: Optional[str] = None,
|
|
260
|
+
) -> dict:
|
|
261
|
+
"""List versions of a model."""
|
|
262
|
+
params = {}
|
|
263
|
+
if page_size:
|
|
264
|
+
params["pageSize"] = page_size
|
|
265
|
+
if order_by:
|
|
266
|
+
params["orderBy"] = order_by
|
|
267
|
+
if next_page_token:
|
|
268
|
+
params["nextPageToken"] = next_page_token
|
|
269
|
+
return self._get(f"/registered_models/{model_name}/versions/", params=params)
|
|
270
|
+
|
|
271
|
+
def get_model_version(self, model_name: str, version: str) -> dict:
|
|
272
|
+
"""Get a specific model version."""
|
|
273
|
+
return self._get(f"/registered_models/{model_name}/versions/{version}/")
|
|
274
|
+
|
|
275
|
+
def create_model_version(
|
|
276
|
+
self,
|
|
277
|
+
model_name: str,
|
|
278
|
+
version_name: str,
|
|
279
|
+
description: Optional[str] = None,
|
|
280
|
+
author: Optional[str] = None,
|
|
281
|
+
custom_properties: Optional[dict] = None,
|
|
282
|
+
) -> dict:
|
|
283
|
+
"""Create a model version."""
|
|
284
|
+
data = {"name": version_name}
|
|
285
|
+
if description:
|
|
286
|
+
data["description"] = description
|
|
287
|
+
if author:
|
|
288
|
+
data["author"] = author
|
|
289
|
+
if custom_properties:
|
|
290
|
+
data["customProperties"] = custom_properties
|
|
291
|
+
return self._post(f"/registered_models/{model_name}/versions/", body=data)
|
|
292
|
+
|
|
293
|
+
def update_model_version(
|
|
294
|
+
self,
|
|
295
|
+
model_name: str,
|
|
296
|
+
version: str,
|
|
297
|
+
description: Optional[str] = None,
|
|
298
|
+
custom_properties: Optional[dict] = None,
|
|
299
|
+
) -> dict:
|
|
300
|
+
"""Update a model version."""
|
|
301
|
+
data = {}
|
|
302
|
+
if description is not None:
|
|
303
|
+
data["description"] = description
|
|
304
|
+
if custom_properties is not None:
|
|
305
|
+
data["customProperties"] = custom_properties
|
|
306
|
+
return self._patch(f"/registered_models/{model_name}/versions/{version}/", body=data)
|
|
307
|
+
|
|
308
|
+
# =========================================================================
|
|
309
|
+
# Model Artifacts
|
|
310
|
+
# =========================================================================
|
|
311
|
+
|
|
312
|
+
def get_model_artifact(self, artifact_id: str) -> dict:
|
|
313
|
+
"""Get a model artifact."""
|
|
314
|
+
return self._get(f"/model_artifacts/{artifact_id}/")
|
|
315
|
+
|
|
316
|
+
def create_model_artifact(
|
|
317
|
+
self,
|
|
318
|
+
name: str,
|
|
319
|
+
uri: str,
|
|
320
|
+
description: Optional[str] = None,
|
|
321
|
+
model_format_name: Optional[str] = None,
|
|
322
|
+
model_format_version: Optional[str] = None,
|
|
323
|
+
storage_key: Optional[str] = None,
|
|
324
|
+
storage_path: Optional[str] = None,
|
|
325
|
+
custom_properties: Optional[dict] = None,
|
|
326
|
+
) -> dict:
|
|
327
|
+
"""Create a model artifact."""
|
|
328
|
+
data = {"name": name, "uri": uri}
|
|
329
|
+
if description:
|
|
330
|
+
data["description"] = description
|
|
331
|
+
if model_format_name:
|
|
332
|
+
data["modelFormatName"] = model_format_name
|
|
333
|
+
if model_format_version:
|
|
334
|
+
data["modelFormatVersion"] = model_format_version
|
|
335
|
+
if storage_key:
|
|
336
|
+
data["storageKey"] = storage_key
|
|
337
|
+
if storage_path:
|
|
338
|
+
data["storagePath"] = storage_path
|
|
339
|
+
if custom_properties:
|
|
340
|
+
data["customProperties"] = custom_properties
|
|
341
|
+
return self._post("/model_artifacts/", body=data)
|
|
342
|
+
|
|
343
|
+
def update_model_artifact(
|
|
344
|
+
self,
|
|
345
|
+
artifact_id: str,
|
|
346
|
+
description: Optional[str] = None,
|
|
347
|
+
custom_properties: Optional[dict] = None,
|
|
348
|
+
) -> dict:
|
|
349
|
+
"""Update a model artifact."""
|
|
350
|
+
data = {}
|
|
351
|
+
if description is not None:
|
|
352
|
+
data["description"] = description
|
|
353
|
+
if custom_properties is not None:
|
|
354
|
+
data["customProperties"] = custom_properties
|
|
355
|
+
return self._patch(f"/model_artifacts/{artifact_id}/", body=data)
|