digitalhub 0.9.1__py3-none-any.whl → 0.10.0b0__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.
Potentially problematic release.
This version of digitalhub might be problematic. Click here for more details.
- digitalhub/__init__.py +2 -3
- digitalhub/client/_base/client.py +3 -2
- digitalhub/client/dhcore/api_builder.py +6 -1
- digitalhub/client/dhcore/client.py +27 -399
- digitalhub/client/dhcore/configurator.py +339 -0
- digitalhub/client/dhcore/error_parser.py +107 -0
- digitalhub/client/dhcore/models.py +13 -23
- digitalhub/client/dhcore/utils.py +4 -44
- digitalhub/client/local/api_builder.py +9 -17
- digitalhub/client/local/client.py +12 -2
- digitalhub/client/local/enums.py +11 -0
- digitalhub/configurator/api.py +31 -0
- digitalhub/configurator/configurator.py +194 -0
- digitalhub/configurator/credentials_store.py +65 -0
- digitalhub/configurator/ini_module.py +74 -0
- digitalhub/entities/_base/_base/entity.py +2 -2
- digitalhub/entities/_base/material/entity.py +19 -6
- digitalhub/entities/_base/material/utils.py +2 -2
- digitalhub/entities/_commons/enums.py +1 -0
- digitalhub/entities/_commons/models.py +9 -0
- digitalhub/entities/_commons/utils.py +25 -0
- digitalhub/entities/_operations/processor.py +103 -107
- digitalhub/entities/artifact/crud.py +3 -3
- digitalhub/entities/dataitem/_base/entity.py +2 -2
- digitalhub/entities/dataitem/crud.py +3 -3
- digitalhub/entities/dataitem/table/entity.py +2 -2
- digitalhub/{utils/data_utils.py → entities/dataitem/table/utils.py} +43 -51
- digitalhub/entities/dataitem/utils.py +6 -3
- digitalhub/entities/model/_base/entity.py +172 -0
- digitalhub/entities/model/_base/spec.py +0 -10
- digitalhub/entities/model/_base/status.py +10 -0
- digitalhub/entities/model/crud.py +3 -3
- digitalhub/entities/model/huggingface/spec.py +6 -3
- digitalhub/entities/model/mlflow/models.py +2 -2
- digitalhub/entities/model/mlflow/spec.py +1 -3
- digitalhub/entities/model/mlflow/utils.py +44 -5
- digitalhub/entities/run/_base/entity.py +149 -0
- digitalhub/entities/run/_base/status.py +12 -0
- digitalhub/entities/task/_base/spec.py +2 -0
- digitalhub/entities/task/crud.py +4 -0
- digitalhub/readers/{_commons → pandas}/enums.py +4 -0
- digitalhub/readers/pandas/reader.py +58 -10
- digitalhub/stores/_base/store.py +1 -49
- digitalhub/stores/api.py +8 -33
- digitalhub/stores/builder.py +44 -161
- digitalhub/stores/local/store.py +4 -18
- digitalhub/stores/remote/store.py +3 -10
- digitalhub/stores/s3/configurator.py +107 -0
- digitalhub/stores/s3/enums.py +17 -0
- digitalhub/stores/s3/models.py +21 -0
- digitalhub/stores/s3/store.py +8 -28
- digitalhub/{utils/s3_utils.py → stores/s3/utils.py} +7 -3
- digitalhub/stores/sql/configurator.py +88 -0
- digitalhub/stores/sql/enums.py +16 -0
- digitalhub/stores/sql/models.py +24 -0
- digitalhub/stores/sql/store.py +14 -57
- digitalhub/utils/exceptions.py +6 -0
- digitalhub/utils/generic_utils.py +9 -8
- digitalhub/utils/uri_utils.py +1 -1
- {digitalhub-0.9.1.dist-info → digitalhub-0.10.0b0.dist-info}/METADATA +5 -6
- {digitalhub-0.9.1.dist-info → digitalhub-0.10.0b0.dist-info}/RECORD +66 -53
- test/local/imports/test_imports.py +0 -1
- digitalhub/client/dhcore/env.py +0 -23
- /digitalhub/{readers/_commons → configurator}/__init__.py +0 -0
- {digitalhub-0.9.1.dist-info → digitalhub-0.10.0b0.dist-info}/LICENSE.txt +0 -0
- {digitalhub-0.9.1.dist-info → digitalhub-0.10.0b0.dist-info}/WHEEL +0 -0
- {digitalhub-0.9.1.dist-info → digitalhub-0.10.0b0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
from warnings import warn
|
|
5
|
+
|
|
6
|
+
from requests import request
|
|
7
|
+
|
|
8
|
+
from digitalhub.client.dhcore.enums import AuthType, DhcoreEnvVar
|
|
9
|
+
from digitalhub.client.dhcore.models import BasicAuth, OAuth2TokenAuth
|
|
10
|
+
from digitalhub.configurator.configurator import configurator
|
|
11
|
+
from digitalhub.utils.exceptions import ClientError
|
|
12
|
+
from digitalhub.utils.uri_utils import has_remote_scheme
|
|
13
|
+
|
|
14
|
+
if typing.TYPE_CHECKING:
|
|
15
|
+
from requests import Response
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Default key used to store authentication information
|
|
19
|
+
AUTH_KEY = "_auth"
|
|
20
|
+
|
|
21
|
+
# API levels that are supported
|
|
22
|
+
MAX_API_LEVEL = 20
|
|
23
|
+
MIN_API_LEVEL = 9
|
|
24
|
+
LIB_VERSION = 9
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ClientDHCoreConfigurator:
|
|
28
|
+
"""
|
|
29
|
+
Configurator object used to configure the client.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
##############################
|
|
33
|
+
# Configuration methods
|
|
34
|
+
##############################
|
|
35
|
+
|
|
36
|
+
def configure(self, config: dict | None = None) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Configure the client attributes with config (given or from
|
|
39
|
+
environment).
|
|
40
|
+
Regarding authentication parameters, the config parameter
|
|
41
|
+
takes precedence over the env variables, and the token
|
|
42
|
+
over the basic auth. Furthermore, the config parameter is
|
|
43
|
+
validated against the proper pydantic model.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
config : dict
|
|
48
|
+
Configuration dictionary.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
None
|
|
53
|
+
"""
|
|
54
|
+
if config is None:
|
|
55
|
+
self._get_core_endpoint()
|
|
56
|
+
self._get_auth_vars()
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
# Read passed config
|
|
60
|
+
# Validate and save credentials
|
|
61
|
+
if config.get("access_token") is not None:
|
|
62
|
+
config = OAuth2TokenAuth(**config)
|
|
63
|
+
for pair in [
|
|
64
|
+
(AUTH_KEY, AuthType.OAUTH2.value),
|
|
65
|
+
(DhcoreEnvVar.ENDPOINT.value, config.endpoint),
|
|
66
|
+
(DhcoreEnvVar.ISSUER.value, config.issuer),
|
|
67
|
+
(DhcoreEnvVar.ACCESS_TOKEN.value, config.access_token),
|
|
68
|
+
(DhcoreEnvVar.REFRESH_TOKEN.value, config.refresh_token),
|
|
69
|
+
(DhcoreEnvVar.CLIENT_ID.value, config.client_id),
|
|
70
|
+
]:
|
|
71
|
+
configurator.set_credential(*pair)
|
|
72
|
+
|
|
73
|
+
elif config.get("user") is not None and config.get("password") is not None:
|
|
74
|
+
config = BasicAuth(**config)
|
|
75
|
+
for pair in [
|
|
76
|
+
(AUTH_KEY, AuthType.BASIC.value),
|
|
77
|
+
(DhcoreEnvVar.ENDPOINT.value, config.endpoint),
|
|
78
|
+
(DhcoreEnvVar.USER.value, config.user),
|
|
79
|
+
(DhcoreEnvVar.PASSWORD.value, config.password),
|
|
80
|
+
]:
|
|
81
|
+
configurator.set_credential(*pair)
|
|
82
|
+
|
|
83
|
+
else:
|
|
84
|
+
raise ClientError("Invalid credentials format.")
|
|
85
|
+
|
|
86
|
+
def check_core_version(self, response: Response) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Raise an exception if DHCore API version is not supported.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
response : Response
|
|
93
|
+
The response object.
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
None
|
|
98
|
+
"""
|
|
99
|
+
if "X-Api-Level" in response.headers:
|
|
100
|
+
core_api_level = int(response.headers["X-Api-Level"])
|
|
101
|
+
if not (MIN_API_LEVEL <= core_api_level <= MAX_API_LEVEL):
|
|
102
|
+
raise ClientError("Backend API level not supported.")
|
|
103
|
+
if LIB_VERSION < core_api_level:
|
|
104
|
+
warn("Backend API level is higher than library version. You should consider updating the library.")
|
|
105
|
+
|
|
106
|
+
def build_url(self, api: str) -> str:
|
|
107
|
+
"""
|
|
108
|
+
Build the url.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
api : str
|
|
113
|
+
The api to call.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
str
|
|
118
|
+
The url.
|
|
119
|
+
"""
|
|
120
|
+
api = api.removeprefix("/")
|
|
121
|
+
return f"{configurator.get_credentials(DhcoreEnvVar.ENDPOINT.value)}/{api}"
|
|
122
|
+
|
|
123
|
+
##############################
|
|
124
|
+
# Private methods
|
|
125
|
+
##############################
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _sanitize_endpoint(endpoint: str) -> str:
|
|
129
|
+
"""
|
|
130
|
+
Sanitize the endpoint.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
None
|
|
135
|
+
"""
|
|
136
|
+
if not has_remote_scheme(endpoint):
|
|
137
|
+
raise ClientError("Invalid endpoint scheme. Must start with http:// or https://.")
|
|
138
|
+
|
|
139
|
+
endpoint = endpoint.strip()
|
|
140
|
+
return endpoint.removesuffix("/")
|
|
141
|
+
|
|
142
|
+
def _get_core_endpoint(self) -> None:
|
|
143
|
+
"""
|
|
144
|
+
Get the DHCore endpoint from env.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
None
|
|
149
|
+
|
|
150
|
+
Raises
|
|
151
|
+
------
|
|
152
|
+
Exception
|
|
153
|
+
If the endpoint of DHCore is not set in the env variables.
|
|
154
|
+
"""
|
|
155
|
+
endpoint = configurator.load_var(DhcoreEnvVar.ENDPOINT.value)
|
|
156
|
+
if endpoint is None:
|
|
157
|
+
raise ClientError("Endpoint not set as environment variables.")
|
|
158
|
+
endpoint = self._sanitize_endpoint(endpoint)
|
|
159
|
+
configurator.set_credential(DhcoreEnvVar.ENDPOINT.value, endpoint)
|
|
160
|
+
|
|
161
|
+
def _get_auth_vars(self) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Get authentication parameters from the env.
|
|
164
|
+
|
|
165
|
+
Returns
|
|
166
|
+
-------
|
|
167
|
+
None
|
|
168
|
+
"""
|
|
169
|
+
# Give priority to access token
|
|
170
|
+
access_token = configurator.load_var(DhcoreEnvVar.ACCESS_TOKEN.value)
|
|
171
|
+
if access_token is not None:
|
|
172
|
+
configurator.set_credential(AUTH_KEY, AuthType.OAUTH2.value)
|
|
173
|
+
configurator.set_credential(DhcoreEnvVar.ACCESS_TOKEN.value, access_token)
|
|
174
|
+
|
|
175
|
+
# Fallback to basic
|
|
176
|
+
else:
|
|
177
|
+
user = configurator.load_var(DhcoreEnvVar.USER.value)
|
|
178
|
+
password = configurator.load_var(DhcoreEnvVar.PASSWORD.value)
|
|
179
|
+
if user is not None and password is not None:
|
|
180
|
+
configurator.set_credential(AUTH_KEY, AuthType.BASIC.value)
|
|
181
|
+
configurator.set_credential(DhcoreEnvVar.USER.value, user)
|
|
182
|
+
configurator.set_credential(DhcoreEnvVar.PASSWORD.value, password)
|
|
183
|
+
|
|
184
|
+
##############################
|
|
185
|
+
# Auth methods
|
|
186
|
+
##############################
|
|
187
|
+
|
|
188
|
+
def basic_auth(self) -> bool:
|
|
189
|
+
"""
|
|
190
|
+
Get basic auth.
|
|
191
|
+
|
|
192
|
+
Returns
|
|
193
|
+
-------
|
|
194
|
+
bool
|
|
195
|
+
"""
|
|
196
|
+
auth_type = configurator.get_credentials(AUTH_KEY)
|
|
197
|
+
return auth_type == AuthType.BASIC.value
|
|
198
|
+
|
|
199
|
+
def oauth2_auth(self) -> bool:
|
|
200
|
+
"""
|
|
201
|
+
Get oauth2 auth.
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
bool
|
|
206
|
+
"""
|
|
207
|
+
auth_type = configurator.get_credentials(AUTH_KEY)
|
|
208
|
+
return auth_type == AuthType.OAUTH2.value
|
|
209
|
+
|
|
210
|
+
def set_request_auth(self, kwargs: dict) -> dict:
|
|
211
|
+
"""
|
|
212
|
+
Get the authentication header.
|
|
213
|
+
|
|
214
|
+
Parameters
|
|
215
|
+
----------
|
|
216
|
+
kwargs : dict
|
|
217
|
+
Keyword arguments to pass to the request.
|
|
218
|
+
|
|
219
|
+
Returns
|
|
220
|
+
-------
|
|
221
|
+
dict
|
|
222
|
+
Authentication header.
|
|
223
|
+
"""
|
|
224
|
+
creds = configurator.get_all_cred()
|
|
225
|
+
if AUTH_KEY not in creds:
|
|
226
|
+
return kwargs
|
|
227
|
+
if self.basic_auth():
|
|
228
|
+
user = creds[DhcoreEnvVar.USER.value]
|
|
229
|
+
password = creds[DhcoreEnvVar.PASSWORD.value]
|
|
230
|
+
kwargs["auth"] = (user, password)
|
|
231
|
+
elif self.oauth2_auth():
|
|
232
|
+
if "headers" not in kwargs:
|
|
233
|
+
kwargs["headers"] = {}
|
|
234
|
+
access_token = creds[DhcoreEnvVar.ACCESS_TOKEN.value]
|
|
235
|
+
kwargs["headers"]["Authorization"] = f"Bearer {access_token}"
|
|
236
|
+
return kwargs
|
|
237
|
+
|
|
238
|
+
def get_new_access_token(self) -> None:
|
|
239
|
+
"""
|
|
240
|
+
Get a new access token.
|
|
241
|
+
|
|
242
|
+
Returns
|
|
243
|
+
-------
|
|
244
|
+
None
|
|
245
|
+
"""
|
|
246
|
+
# Call issuer and get endpoint for
|
|
247
|
+
# refreshing access token
|
|
248
|
+
url = self._get_refresh_endpoint()
|
|
249
|
+
|
|
250
|
+
# Call refresh token endpoint
|
|
251
|
+
# Try token from env
|
|
252
|
+
refresh_token = configurator.load_from_env(DhcoreEnvVar.REFRESH_TOKEN.value)
|
|
253
|
+
response = self._call_refresh_token_endpoint(url, refresh_token)
|
|
254
|
+
|
|
255
|
+
# Otherwise try token from file
|
|
256
|
+
if response.status_code in (400, 401, 403):
|
|
257
|
+
refresh_token = configurator.load_from_config(DhcoreEnvVar.REFRESH_TOKEN.value)
|
|
258
|
+
response = self._call_refresh_token_endpoint(url, refresh_token)
|
|
259
|
+
|
|
260
|
+
response.raise_for_status()
|
|
261
|
+
dict_response = response.json()
|
|
262
|
+
|
|
263
|
+
# Read new access token and refresh token
|
|
264
|
+
configurator.set_credential(DhcoreEnvVar.ACCESS_TOKEN.value, dict_response["access_token"])
|
|
265
|
+
configurator.set_credential(DhcoreEnvVar.REFRESH_TOKEN.value, dict_response["refresh_token"])
|
|
266
|
+
|
|
267
|
+
# Propagate new access token to config file
|
|
268
|
+
self._write_env()
|
|
269
|
+
|
|
270
|
+
def _write_env(self) -> None:
|
|
271
|
+
"""
|
|
272
|
+
Write the env variables to the .dhcore.ini file.
|
|
273
|
+
It will overwrite any existing env variables.
|
|
274
|
+
|
|
275
|
+
Returns
|
|
276
|
+
-------
|
|
277
|
+
None
|
|
278
|
+
"""
|
|
279
|
+
configurator.write_env(
|
|
280
|
+
[
|
|
281
|
+
DhcoreEnvVar.ACCESS_TOKEN.value,
|
|
282
|
+
DhcoreEnvVar.REFRESH_TOKEN.value,
|
|
283
|
+
]
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def _get_refresh_endpoint(self) -> str:
|
|
287
|
+
"""
|
|
288
|
+
Get the refresh endpoint.
|
|
289
|
+
|
|
290
|
+
Returns
|
|
291
|
+
-------
|
|
292
|
+
str
|
|
293
|
+
Refresh endpoint.
|
|
294
|
+
"""
|
|
295
|
+
# Get issuer endpoint
|
|
296
|
+
endpoint_issuer = configurator.load_var(DhcoreEnvVar.ISSUER.value)
|
|
297
|
+
if endpoint_issuer is not None:
|
|
298
|
+
endpoint_issuer = self._sanitize_endpoint(endpoint_issuer)
|
|
299
|
+
configurator.set_credential(DhcoreEnvVar.ISSUER.value, endpoint_issuer)
|
|
300
|
+
else:
|
|
301
|
+
raise ClientError("Issuer endpoint not set.")
|
|
302
|
+
|
|
303
|
+
# Standard issuer endpoint path
|
|
304
|
+
url = endpoint_issuer + "/.well-known/openid-configuration"
|
|
305
|
+
|
|
306
|
+
# Call issuer to get refresh endpoint
|
|
307
|
+
r = request("GET", url, timeout=60)
|
|
308
|
+
r.raise_for_status()
|
|
309
|
+
return r.json().get("token_endpoint")
|
|
310
|
+
|
|
311
|
+
def _call_refresh_token_endpoint(self, url: str, refresh_token: str) -> Response:
|
|
312
|
+
"""
|
|
313
|
+
Call the refresh token endpoint.
|
|
314
|
+
|
|
315
|
+
Parameters
|
|
316
|
+
----------
|
|
317
|
+
url : str
|
|
318
|
+
Refresh token endpoint.
|
|
319
|
+
refresh_token : str
|
|
320
|
+
Refresh token.
|
|
321
|
+
|
|
322
|
+
Returns
|
|
323
|
+
-------
|
|
324
|
+
Response
|
|
325
|
+
Response object.
|
|
326
|
+
"""
|
|
327
|
+
# Get client id
|
|
328
|
+
client_id = configurator.load_var(DhcoreEnvVar.CLIENT_ID.value)
|
|
329
|
+
if client_id is None:
|
|
330
|
+
raise ClientError("Client id not set.")
|
|
331
|
+
|
|
332
|
+
# Send request to get new access token
|
|
333
|
+
payload = {
|
|
334
|
+
"grant_type": "refresh_token",
|
|
335
|
+
"client_id": client_id,
|
|
336
|
+
"refresh_token": refresh_token,
|
|
337
|
+
}
|
|
338
|
+
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
339
|
+
return request("POST", url, data=payload, headers=headers, timeout=60)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from requests.exceptions import HTTPError, RequestException
|
|
6
|
+
|
|
7
|
+
from digitalhub.utils.exceptions import (
|
|
8
|
+
BackendError,
|
|
9
|
+
BadRequestError,
|
|
10
|
+
EntityAlreadyExistsError,
|
|
11
|
+
EntityNotExistsError,
|
|
12
|
+
ForbiddenError,
|
|
13
|
+
MissingSpecError,
|
|
14
|
+
UnauthorizedError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if typing.TYPE_CHECKING:
|
|
18
|
+
from requests import Response
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ErrorParser:
|
|
22
|
+
@staticmethod
|
|
23
|
+
def parse(response: Response) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Handle DHCore API errors.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
response : Response
|
|
30
|
+
The response object.
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
None
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
response.raise_for_status()
|
|
38
|
+
|
|
39
|
+
# Backend errors
|
|
40
|
+
except RequestException as e:
|
|
41
|
+
# Handle timeout
|
|
42
|
+
if isinstance(e, TimeoutError):
|
|
43
|
+
msg = "Request to DHCore backend timed out."
|
|
44
|
+
raise TimeoutError(msg)
|
|
45
|
+
|
|
46
|
+
# Handle connection error
|
|
47
|
+
elif isinstance(e, ConnectionError):
|
|
48
|
+
msg = "Unable to connect to DHCore backend."
|
|
49
|
+
raise ConnectionError(msg)
|
|
50
|
+
|
|
51
|
+
# Handle HTTP errors
|
|
52
|
+
elif isinstance(e, HTTPError):
|
|
53
|
+
txt_resp = f"Response: {response.text}."
|
|
54
|
+
|
|
55
|
+
# Bad request
|
|
56
|
+
if response.status_code == 400:
|
|
57
|
+
# Missing spec in backend
|
|
58
|
+
if "missing spec" in response.text:
|
|
59
|
+
msg = f"Missing spec in backend. {txt_resp}"
|
|
60
|
+
raise MissingSpecError(msg)
|
|
61
|
+
|
|
62
|
+
# Duplicated entity
|
|
63
|
+
elif "Duplicated entity" in response.text:
|
|
64
|
+
msg = f"Entity already exists. {txt_resp}"
|
|
65
|
+
raise EntityAlreadyExistsError(msg)
|
|
66
|
+
|
|
67
|
+
# Other errors
|
|
68
|
+
else:
|
|
69
|
+
msg = f"Bad request. {txt_resp}"
|
|
70
|
+
raise BadRequestError(msg)
|
|
71
|
+
|
|
72
|
+
# Unauthorized errors
|
|
73
|
+
elif response.status_code == 401:
|
|
74
|
+
msg = f"Unauthorized. {txt_resp}"
|
|
75
|
+
raise UnauthorizedError(msg)
|
|
76
|
+
|
|
77
|
+
# Forbidden errors
|
|
78
|
+
elif response.status_code == 403:
|
|
79
|
+
msg = f"Forbidden. {txt_resp}"
|
|
80
|
+
raise ForbiddenError(msg)
|
|
81
|
+
|
|
82
|
+
# Entity not found
|
|
83
|
+
elif response.status_code == 404:
|
|
84
|
+
# Put with entity not found
|
|
85
|
+
if "No such EntityName" in response.text:
|
|
86
|
+
msg = f"Entity does not exists. {txt_resp}"
|
|
87
|
+
raise EntityNotExistsError(msg)
|
|
88
|
+
|
|
89
|
+
# Other cases
|
|
90
|
+
else:
|
|
91
|
+
msg = f"Not found. {txt_resp}"
|
|
92
|
+
raise BackendError(msg)
|
|
93
|
+
|
|
94
|
+
# Other errors
|
|
95
|
+
else:
|
|
96
|
+
msg = f"Backend error. {txt_resp}"
|
|
97
|
+
raise BackendError(msg) from e
|
|
98
|
+
|
|
99
|
+
# Other requests errors
|
|
100
|
+
else:
|
|
101
|
+
msg = f"Some error occurred. {e}"
|
|
102
|
+
raise BackendError(msg) from e
|
|
103
|
+
|
|
104
|
+
# Other generic errors
|
|
105
|
+
except Exception as e:
|
|
106
|
+
msg = f"Some error occurred: {e}"
|
|
107
|
+
raise RuntimeError(msg) from e
|
|
@@ -2,45 +2,35 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
5
|
-
from digitalhub.client.dhcore.env import FALLBACK_USER
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
class AuthConfig(BaseModel):
|
|
6
|
+
class ClientConfig(BaseModel):
|
|
9
7
|
"""Client configuration model."""
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
"""
|
|
9
|
+
endpoint: str
|
|
10
|
+
"""API endpoint."""
|
|
13
11
|
|
|
14
12
|
|
|
15
|
-
class BasicAuth(
|
|
13
|
+
class BasicAuth(ClientConfig):
|
|
16
14
|
"""Basic authentication model."""
|
|
17
15
|
|
|
16
|
+
user: str
|
|
17
|
+
"""Username."""
|
|
18
|
+
|
|
18
19
|
password: str
|
|
19
20
|
"""Basic authentication password."""
|
|
20
21
|
|
|
21
22
|
|
|
22
|
-
class
|
|
23
|
-
"""Client id authentication model."""
|
|
24
|
-
|
|
25
|
-
client_id: str = None
|
|
26
|
-
"""OAuth2 client id."""
|
|
27
|
-
|
|
28
|
-
client_scecret: str = None
|
|
29
|
-
"""OAuth2 client secret."""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class OAuth2TokenAuth(ClientParams):
|
|
23
|
+
class OAuth2TokenAuth(ClientConfig):
|
|
33
24
|
"""OAuth2 token authentication model."""
|
|
34
25
|
|
|
35
26
|
access_token: str
|
|
36
27
|
"""OAuth2 token."""
|
|
37
28
|
|
|
38
|
-
refresh_token: str
|
|
29
|
+
refresh_token: str
|
|
39
30
|
"""OAuth2 refresh token."""
|
|
40
31
|
|
|
32
|
+
client_id: str
|
|
33
|
+
"""OAuth2 client id."""
|
|
41
34
|
|
|
42
|
-
|
|
43
|
-
"""
|
|
44
|
-
|
|
45
|
-
exchange_token: str
|
|
46
|
-
"""Exchange token."""
|
|
35
|
+
issuer_endpoint: str
|
|
36
|
+
"""OAuth2 issuer endpoint."""
|
|
@@ -4,7 +4,7 @@ import os
|
|
|
4
4
|
import typing
|
|
5
5
|
|
|
6
6
|
from digitalhub.client.api import get_client
|
|
7
|
-
from digitalhub.client.dhcore.enums import
|
|
7
|
+
from digitalhub.client.dhcore.enums import DhcoreEnvVar
|
|
8
8
|
|
|
9
9
|
if typing.TYPE_CHECKING:
|
|
10
10
|
from digitalhub.client.dhcore.client import ClientDHCore
|
|
@@ -21,8 +21,7 @@ def set_dhcore_env(
|
|
|
21
21
|
"""
|
|
22
22
|
Function to set environment variables for DHCore config.
|
|
23
23
|
Note that if the environment variable is already set, it
|
|
24
|
-
will be overwritten.
|
|
25
|
-
configuration.
|
|
24
|
+
will be overwritten.
|
|
26
25
|
|
|
27
26
|
Parameters
|
|
28
27
|
----------
|
|
@@ -56,47 +55,8 @@ def set_dhcore_env(
|
|
|
56
55
|
if client_id is not None:
|
|
57
56
|
os.environ[DhcoreEnvVar.CLIENT_ID.value] = client_id
|
|
58
57
|
|
|
59
|
-
update_client_from_env()
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def update_client_from_env() -> None:
|
|
63
|
-
"""
|
|
64
|
-
Function to update client from environment variables.
|
|
65
|
-
|
|
66
|
-
Returns
|
|
67
|
-
-------
|
|
68
|
-
None
|
|
69
|
-
"""
|
|
70
58
|
client: ClientDHCore = get_client(local=False)
|
|
71
|
-
|
|
72
|
-
# Update endpoint
|
|
73
|
-
endpoint = os.getenv(DhcoreEnvVar.ENDPOINT.value)
|
|
74
|
-
if endpoint is not None:
|
|
75
|
-
client._endpoint_core = endpoint
|
|
76
|
-
|
|
77
|
-
# Update auth
|
|
78
|
-
|
|
79
|
-
# If token is set, it will override the other auth options
|
|
80
|
-
access_token = os.getenv(DhcoreEnvVar.ACCESS_TOKEN.value)
|
|
81
|
-
refresh_token = os.getenv(DhcoreEnvVar.REFRESH_TOKEN.value)
|
|
82
|
-
client_id = os.getenv(DhcoreEnvVar.CLIENT_ID.value)
|
|
83
|
-
|
|
84
|
-
if access_token is not None:
|
|
85
|
-
if refresh_token is not None:
|
|
86
|
-
client._refresh_token = refresh_token
|
|
87
|
-
if client_id is not None:
|
|
88
|
-
client._client_id = client_id
|
|
89
|
-
client._access_token = access_token
|
|
90
|
-
client._auth_type = AuthType.OAUTH2.value
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
# Otherwise, if user and password are set, basic auth will be used
|
|
94
|
-
username = os.getenv(DhcoreEnvVar.USER.value)
|
|
95
|
-
password = os.getenv(DhcoreEnvVar.PASSWORD.value)
|
|
96
|
-
if username is not None and password is not None:
|
|
97
|
-
client._user = username
|
|
98
|
-
client._password = password
|
|
99
|
-
client._auth_type = AuthType.BASIC.value
|
|
59
|
+
client._configurator.configure()
|
|
100
60
|
|
|
101
61
|
|
|
102
62
|
def refresh_token() -> None:
|
|
@@ -108,4 +68,4 @@ def refresh_token() -> None:
|
|
|
108
68
|
None
|
|
109
69
|
"""
|
|
110
70
|
client: ClientDHCore = get_client(local=False)
|
|
111
|
-
client.
|
|
71
|
+
client._configurator.get_new_access_token()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from digitalhub.client._base.api_builder import ClientApiBuilder
|
|
4
|
+
from digitalhub.client.local.enums import LocalClientVar
|
|
4
5
|
from digitalhub.entities._commons.enums import ApiCategories, BackendOperations
|
|
5
6
|
from digitalhub.utils.exceptions import BackendError
|
|
6
7
|
|
|
@@ -63,9 +64,7 @@ class ClientLocalApiBuilder(ClientApiBuilder):
|
|
|
63
64
|
BackendOperations.DELETE.value,
|
|
64
65
|
):
|
|
65
66
|
return f"{API_BASE}/{entity_type}/{kwargs['entity_name']}"
|
|
66
|
-
|
|
67
|
-
raise BackendError("Share API not implemented for Local.")
|
|
68
|
-
raise BackendError(f"Invalid operation '{operation}' for entity type '{entity_type}' in Local.")
|
|
67
|
+
raise BackendError(f"API for operation '{operation}' for entity type '{entity_type}' not implemented in Local.")
|
|
69
68
|
|
|
70
69
|
def build_api_context(self, operation: str, **kwargs) -> str:
|
|
71
70
|
"""
|
|
@@ -84,17 +83,10 @@ class ClientLocalApiBuilder(ClientApiBuilder):
|
|
|
84
83
|
BackendOperations.DELETE.value,
|
|
85
84
|
):
|
|
86
85
|
return f"{API_CONTEXT}/{project}/{entity_type}/{kwargs['entity_id']}"
|
|
87
|
-
elif operation
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
raise BackendError("Secret API (read/set value) not implemented for Local.")
|
|
95
|
-
elif operation == BackendOperations.FILES.value:
|
|
96
|
-
raise BackendError("Files API not implemented for Local.")
|
|
97
|
-
elif operation == BackendOperations.SEARCH.value:
|
|
98
|
-
raise BackendError("Search API not implemented for Local.")
|
|
99
|
-
|
|
100
|
-
raise BackendError(f"Invalid operation '{operation}' for entity type '{entity_type}' in Local.")
|
|
86
|
+
elif operation in (
|
|
87
|
+
BackendOperations.LOGS.value,
|
|
88
|
+
BackendOperations.FILES.value,
|
|
89
|
+
BackendOperations.METRICS.value,
|
|
90
|
+
):
|
|
91
|
+
return LocalClientVar.EMPTY.value
|
|
92
|
+
raise BackendError(f"API for operation '{operation}' for entity type '{entity_type}' not implemented in Local.")
|
|
@@ -2,9 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from copy import deepcopy
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any
|
|
5
6
|
|
|
6
7
|
from digitalhub.client._base.client import Client
|
|
7
8
|
from digitalhub.client.local.api_builder import ClientLocalApiBuilder
|
|
9
|
+
from digitalhub.client.local.enums import LocalClientVar
|
|
8
10
|
from digitalhub.client.local.key_builder import ClientLocalKeyBuilder
|
|
9
11
|
from digitalhub.utils.exceptions import BackendError
|
|
10
12
|
|
|
@@ -32,7 +34,7 @@ class ClientLocal(Client):
|
|
|
32
34
|
# CRUD
|
|
33
35
|
##############################
|
|
34
36
|
|
|
35
|
-
def create_object(self, api: str, obj:
|
|
37
|
+
def create_object(self, api: str, obj: Any, **kwargs) -> dict:
|
|
36
38
|
"""
|
|
37
39
|
Create an object in local.
|
|
38
40
|
|
|
@@ -48,6 +50,9 @@ class ClientLocal(Client):
|
|
|
48
50
|
dict
|
|
49
51
|
The created object.
|
|
50
52
|
"""
|
|
53
|
+
if not isinstance(obj, dict):
|
|
54
|
+
raise TypeError("Object must be a dictionary")
|
|
55
|
+
|
|
51
56
|
entity_type, _, context_api = self._parse_api(api)
|
|
52
57
|
try:
|
|
53
58
|
# Check if entity_type is valid
|
|
@@ -114,6 +119,8 @@ class ClientLocal(Client):
|
|
|
114
119
|
dict
|
|
115
120
|
The read object.
|
|
116
121
|
"""
|
|
122
|
+
if api == LocalClientVar.EMPTY.value:
|
|
123
|
+
return {}
|
|
117
124
|
entity_type, entity_id, context_api = self._parse_api(api)
|
|
118
125
|
if entity_id is None:
|
|
119
126
|
msg = self._format_msg(4)
|
|
@@ -155,7 +162,7 @@ class ClientLocal(Client):
|
|
|
155
162
|
msg = self._format_msg(3, entity_type=entity_type, entity_id=entity_id)
|
|
156
163
|
raise BackendError(msg)
|
|
157
164
|
|
|
158
|
-
def update_object(self, api: str, obj:
|
|
165
|
+
def update_object(self, api: str, obj: Any, **kwargs) -> dict:
|
|
159
166
|
"""
|
|
160
167
|
Update an object in local.
|
|
161
168
|
|
|
@@ -171,6 +178,9 @@ class ClientLocal(Client):
|
|
|
171
178
|
dict
|
|
172
179
|
The updated object.
|
|
173
180
|
"""
|
|
181
|
+
if not isinstance(obj, dict):
|
|
182
|
+
raise TypeError("Object must be a dictionary")
|
|
183
|
+
|
|
174
184
|
entity_type, entity_id, context_api = self._parse_api(api)
|
|
175
185
|
try:
|
|
176
186
|
# API example
|