digitalhub 0.9.1__py3-none-any.whl → 0.10.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.
Potentially problematic release.
This version of digitalhub might be problematic. Click here for more details.
- digitalhub/__init__.py +2 -3
- digitalhub/client/_base/api_builder.py +1 -1
- digitalhub/client/_base/client.py +25 -2
- digitalhub/client/_base/params_builder.py +16 -0
- digitalhub/client/dhcore/api_builder.py +10 -4
- digitalhub/client/dhcore/client.py +30 -398
- digitalhub/client/dhcore/configurator.py +361 -0
- digitalhub/client/dhcore/error_parser.py +107 -0
- digitalhub/client/dhcore/models.py +13 -23
- digitalhub/client/dhcore/params_builder.py +178 -0
- digitalhub/client/dhcore/utils.py +4 -44
- digitalhub/client/local/api_builder.py +13 -18
- digitalhub/client/local/client.py +18 -2
- digitalhub/client/local/enums.py +11 -0
- digitalhub/client/local/params_builder.py +116 -0
- digitalhub/configurator/api.py +31 -0
- digitalhub/configurator/configurator.py +195 -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/context/entity.py +4 -4
- digitalhub/entities/_base/entity/builder.py +5 -5
- digitalhub/entities/_base/executable/entity.py +2 -2
- digitalhub/entities/_base/material/entity.py +12 -12
- digitalhub/entities/_base/material/status.py +1 -1
- digitalhub/entities/_base/material/utils.py +2 -2
- digitalhub/entities/_base/unversioned/entity.py +2 -2
- digitalhub/entities/_base/versioned/entity.py +2 -2
- digitalhub/entities/_commons/enums.py +2 -0
- digitalhub/entities/_commons/metrics.py +164 -0
- digitalhub/entities/_commons/types.py +5 -0
- digitalhub/entities/_commons/utils.py +2 -2
- digitalhub/entities/_processors/base.py +527 -0
- digitalhub/entities/{_operations/processor.py → _processors/context.py} +212 -837
- digitalhub/entities/_processors/utils.py +158 -0
- digitalhub/entities/artifact/artifact/spec.py +3 -1
- digitalhub/entities/artifact/crud.py +13 -12
- digitalhub/entities/artifact/utils.py +1 -1
- digitalhub/entities/builders.py +6 -18
- digitalhub/entities/dataitem/_base/entity.py +0 -41
- digitalhub/entities/dataitem/crud.py +27 -15
- digitalhub/entities/dataitem/table/entity.py +49 -35
- digitalhub/entities/dataitem/table/models.py +4 -3
- digitalhub/{utils/data_utils.py → entities/dataitem/table/utils.py} +46 -54
- digitalhub/entities/dataitem/utils.py +58 -10
- digitalhub/entities/function/crud.py +9 -9
- digitalhub/entities/model/_base/entity.py +120 -0
- digitalhub/entities/model/_base/spec.py +6 -17
- digitalhub/entities/model/_base/status.py +10 -0
- digitalhub/entities/model/crud.py +13 -12
- digitalhub/entities/model/huggingface/spec.py +9 -4
- digitalhub/entities/model/mlflow/models.py +2 -2
- digitalhub/entities/model/mlflow/spec.py +7 -7
- digitalhub/entities/model/mlflow/utils.py +44 -5
- digitalhub/entities/project/_base/entity.py +317 -9
- digitalhub/entities/project/_base/spec.py +8 -6
- digitalhub/entities/project/crud.py +12 -11
- digitalhub/entities/run/_base/entity.py +103 -6
- digitalhub/entities/run/_base/spec.py +4 -2
- digitalhub/entities/run/_base/status.py +12 -0
- digitalhub/entities/run/crud.py +8 -8
- digitalhub/entities/secret/_base/entity.py +3 -3
- digitalhub/entities/secret/_base/spec.py +4 -2
- digitalhub/entities/secret/crud.py +11 -9
- digitalhub/entities/task/_base/entity.py +4 -4
- digitalhub/entities/task/_base/models.py +51 -40
- digitalhub/entities/task/_base/spec.py +2 -0
- digitalhub/entities/task/_base/utils.py +2 -2
- digitalhub/entities/task/crud.py +12 -8
- digitalhub/entities/workflow/crud.py +9 -9
- digitalhub/factory/utils.py +9 -9
- digitalhub/readers/{_base → data/_base}/builder.py +1 -1
- digitalhub/readers/{_base → data/_base}/reader.py +16 -4
- digitalhub/readers/{api.py → data/api.py} +2 -2
- digitalhub/readers/{factory.py → data/factory.py} +3 -3
- digitalhub/readers/{pandas → data/pandas}/builder.py +2 -2
- digitalhub/readers/{pandas → data/pandas}/reader.py +110 -30
- digitalhub/readers/query/__init__.py +0 -0
- digitalhub/stores/_base/store.py +59 -69
- digitalhub/stores/api.py +8 -33
- digitalhub/stores/builder.py +44 -161
- digitalhub/stores/local/store.py +106 -89
- digitalhub/stores/remote/store.py +86 -11
- digitalhub/stores/s3/configurator.py +108 -0
- digitalhub/stores/s3/enums.py +17 -0
- digitalhub/stores/s3/models.py +21 -0
- digitalhub/stores/s3/store.py +154 -70
- 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 +106 -85
- digitalhub/{readers/_commons → utils}/enums.py +5 -1
- digitalhub/utils/exceptions.py +6 -0
- digitalhub/utils/file_utils.py +8 -7
- digitalhub/utils/generic_utils.py +28 -15
- digitalhub/utils/git_utils.py +16 -9
- digitalhub/utils/types.py +5 -0
- digitalhub/utils/uri_utils.py +2 -2
- {digitalhub-0.9.1.dist-info → digitalhub-0.10.0.dist-info}/METADATA +25 -31
- {digitalhub-0.9.1.dist-info → digitalhub-0.10.0.dist-info}/RECORD +108 -99
- {digitalhub-0.9.1.dist-info → digitalhub-0.10.0.dist-info}/WHEEL +1 -2
- digitalhub/client/dhcore/env.py +0 -23
- digitalhub/entities/_base/project/entity.py +0 -341
- digitalhub-0.9.1.dist-info/top_level.txt +0 -2
- test/local/CRUD/test_artifacts.py +0 -96
- test/local/CRUD/test_dataitems.py +0 -96
- test/local/CRUD/test_models.py +0 -95
- test/local/imports/test_imports.py +0 -66
- test/local/instances/test_validate.py +0 -55
- test/test_crud_functions.py +0 -109
- test/test_crud_runs.py +0 -86
- test/test_crud_tasks.py +0 -81
- test/testkfp.py +0 -37
- test/testkfp_pipeline.py +0 -22
- /digitalhub/{entities/_base/project → configurator}/__init__.py +0 -0
- /digitalhub/entities/{_operations → _processors}/__init__.py +0 -0
- /digitalhub/readers/{_base → data}/__init__.py +0 -0
- /digitalhub/readers/{_commons → data/_base}/__init__.py +0 -0
- /digitalhub/readers/{pandas → data/pandas}/__init__.py +0 -0
- {digitalhub-0.9.1.dist-info → digitalhub-0.10.0.dist-info/licenses}/LICENSE.txt +0 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import typing
|
|
5
|
+
from warnings import warn
|
|
6
|
+
|
|
7
|
+
from requests import request
|
|
8
|
+
|
|
9
|
+
from digitalhub.client.dhcore.enums import AuthType, DhcoreEnvVar
|
|
10
|
+
from digitalhub.client.dhcore.models import BasicAuth, OAuth2TokenAuth
|
|
11
|
+
from digitalhub.configurator.configurator import configurator
|
|
12
|
+
from digitalhub.stores.s3.enums import S3StoreEnv
|
|
13
|
+
from digitalhub.stores.sql.enums import SqlStoreEnv
|
|
14
|
+
from digitalhub.utils.exceptions import ClientError
|
|
15
|
+
from digitalhub.utils.uri_utils import has_remote_scheme
|
|
16
|
+
|
|
17
|
+
if typing.TYPE_CHECKING:
|
|
18
|
+
from requests import Response
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Default key used to store authentication information
|
|
22
|
+
AUTH_KEY = "_auth"
|
|
23
|
+
|
|
24
|
+
# API levels that are supported
|
|
25
|
+
MAX_API_LEVEL = 20
|
|
26
|
+
MIN_API_LEVEL = 10
|
|
27
|
+
LIB_VERSION = 10
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ClientDHCoreConfigurator:
|
|
31
|
+
"""
|
|
32
|
+
Configurator object used to configure the client.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
##############################
|
|
36
|
+
# Configuration methods
|
|
37
|
+
##############################
|
|
38
|
+
|
|
39
|
+
def configure(self, config: dict | None = None) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Configure the client attributes with config (given or from
|
|
42
|
+
environment).
|
|
43
|
+
Regarding authentication parameters, the config parameter
|
|
44
|
+
takes precedence over the env variables, and the token
|
|
45
|
+
over the basic auth. Furthermore, the config parameter is
|
|
46
|
+
validated against the proper pydantic model.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
config : dict
|
|
51
|
+
Configuration dictionary.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
None
|
|
56
|
+
"""
|
|
57
|
+
if config is None:
|
|
58
|
+
self._get_core_endpoint()
|
|
59
|
+
self._get_auth_vars()
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
# Read passed config
|
|
63
|
+
# Validate and save credentials
|
|
64
|
+
if config.get("access_token") is not None:
|
|
65
|
+
config = OAuth2TokenAuth(**config)
|
|
66
|
+
for pair in [
|
|
67
|
+
(AUTH_KEY, AuthType.OAUTH2.value),
|
|
68
|
+
(DhcoreEnvVar.ENDPOINT.value, config.endpoint),
|
|
69
|
+
(DhcoreEnvVar.ISSUER.value, config.issuer),
|
|
70
|
+
(DhcoreEnvVar.ACCESS_TOKEN.value, config.access_token),
|
|
71
|
+
(DhcoreEnvVar.REFRESH_TOKEN.value, config.refresh_token),
|
|
72
|
+
(DhcoreEnvVar.CLIENT_ID.value, config.client_id),
|
|
73
|
+
]:
|
|
74
|
+
configurator.set_credential(*pair)
|
|
75
|
+
|
|
76
|
+
elif config.get("user") is not None and config.get("password") is not None:
|
|
77
|
+
config = BasicAuth(**config)
|
|
78
|
+
for pair in [
|
|
79
|
+
(AUTH_KEY, AuthType.BASIC.value),
|
|
80
|
+
(DhcoreEnvVar.ENDPOINT.value, config.endpoint),
|
|
81
|
+
(DhcoreEnvVar.USER.value, config.user),
|
|
82
|
+
(DhcoreEnvVar.PASSWORD.value, config.password),
|
|
83
|
+
]:
|
|
84
|
+
configurator.set_credential(*pair)
|
|
85
|
+
|
|
86
|
+
else:
|
|
87
|
+
raise ClientError("Invalid credentials format.")
|
|
88
|
+
|
|
89
|
+
def check_core_version(self, response: Response) -> None:
|
|
90
|
+
"""
|
|
91
|
+
Raise an exception if DHCore API version is not supported.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
response : Response
|
|
96
|
+
The response object.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
None
|
|
101
|
+
"""
|
|
102
|
+
if "X-Api-Level" in response.headers:
|
|
103
|
+
core_api_level = int(response.headers["X-Api-Level"])
|
|
104
|
+
if not (MIN_API_LEVEL <= core_api_level <= MAX_API_LEVEL):
|
|
105
|
+
raise ClientError("Backend API level not supported.")
|
|
106
|
+
if LIB_VERSION < core_api_level:
|
|
107
|
+
warn("Backend API level is higher than library version. You should consider updating the library.")
|
|
108
|
+
|
|
109
|
+
def build_url(self, api: str) -> str:
|
|
110
|
+
"""
|
|
111
|
+
Build the url.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
api : str
|
|
116
|
+
The api to call.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
str
|
|
121
|
+
The url.
|
|
122
|
+
"""
|
|
123
|
+
api = api.removeprefix("/")
|
|
124
|
+
return f"{configurator.get_credential(DhcoreEnvVar.ENDPOINT.value)}/{api}"
|
|
125
|
+
|
|
126
|
+
##############################
|
|
127
|
+
# Private methods
|
|
128
|
+
##############################
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def _sanitize_endpoint(endpoint: str) -> str:
|
|
132
|
+
"""
|
|
133
|
+
Sanitize the endpoint.
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
None
|
|
138
|
+
"""
|
|
139
|
+
if not has_remote_scheme(endpoint):
|
|
140
|
+
raise ClientError("Invalid endpoint scheme. Must start with http:// or https://.")
|
|
141
|
+
|
|
142
|
+
endpoint = endpoint.strip()
|
|
143
|
+
return endpoint.removesuffix("/")
|
|
144
|
+
|
|
145
|
+
def _get_core_endpoint(self) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Get the DHCore endpoint from env.
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
None
|
|
152
|
+
|
|
153
|
+
Raises
|
|
154
|
+
------
|
|
155
|
+
Exception
|
|
156
|
+
If the endpoint of DHCore is not set in the env variables.
|
|
157
|
+
"""
|
|
158
|
+
endpoint = configurator.load_var(DhcoreEnvVar.ENDPOINT.value)
|
|
159
|
+
if endpoint is None:
|
|
160
|
+
raise ClientError("Endpoint not set as environment variables.")
|
|
161
|
+
endpoint = self._sanitize_endpoint(endpoint)
|
|
162
|
+
configurator.set_credential(DhcoreEnvVar.ENDPOINT.value, endpoint)
|
|
163
|
+
|
|
164
|
+
def _get_auth_vars(self) -> None:
|
|
165
|
+
"""
|
|
166
|
+
Get authentication parameters from the env.
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
None
|
|
171
|
+
"""
|
|
172
|
+
# Give priority to access token
|
|
173
|
+
access_token = configurator.load_var(DhcoreEnvVar.ACCESS_TOKEN.value)
|
|
174
|
+
if access_token is not None:
|
|
175
|
+
configurator.set_credential(AUTH_KEY, AuthType.OAUTH2.value)
|
|
176
|
+
configurator.set_credential(DhcoreEnvVar.ACCESS_TOKEN.value, access_token)
|
|
177
|
+
|
|
178
|
+
# Fallback to basic
|
|
179
|
+
else:
|
|
180
|
+
user = configurator.load_var(DhcoreEnvVar.USER.value)
|
|
181
|
+
password = configurator.load_var(DhcoreEnvVar.PASSWORD.value)
|
|
182
|
+
if user is not None and password is not None:
|
|
183
|
+
configurator.set_credential(AUTH_KEY, AuthType.BASIC.value)
|
|
184
|
+
configurator.set_credential(DhcoreEnvVar.USER.value, user)
|
|
185
|
+
configurator.set_credential(DhcoreEnvVar.PASSWORD.value, password)
|
|
186
|
+
|
|
187
|
+
##############################
|
|
188
|
+
# Auth methods
|
|
189
|
+
##############################
|
|
190
|
+
|
|
191
|
+
def basic_auth(self) -> bool:
|
|
192
|
+
"""
|
|
193
|
+
Get basic auth.
|
|
194
|
+
|
|
195
|
+
Returns
|
|
196
|
+
-------
|
|
197
|
+
bool
|
|
198
|
+
"""
|
|
199
|
+
auth_type = configurator.get_credential(AUTH_KEY)
|
|
200
|
+
return auth_type == AuthType.BASIC.value
|
|
201
|
+
|
|
202
|
+
def oauth2_auth(self) -> bool:
|
|
203
|
+
"""
|
|
204
|
+
Get oauth2 auth.
|
|
205
|
+
|
|
206
|
+
Returns
|
|
207
|
+
-------
|
|
208
|
+
bool
|
|
209
|
+
"""
|
|
210
|
+
auth_type = configurator.get_credential(AUTH_KEY)
|
|
211
|
+
return auth_type == AuthType.OAUTH2.value
|
|
212
|
+
|
|
213
|
+
def set_request_auth(self, kwargs: dict) -> dict:
|
|
214
|
+
"""
|
|
215
|
+
Get the authentication header.
|
|
216
|
+
|
|
217
|
+
Parameters
|
|
218
|
+
----------
|
|
219
|
+
kwargs : dict
|
|
220
|
+
Keyword arguments to pass to the request.
|
|
221
|
+
|
|
222
|
+
Returns
|
|
223
|
+
-------
|
|
224
|
+
dict
|
|
225
|
+
Authentication header.
|
|
226
|
+
"""
|
|
227
|
+
creds = configurator.get_all_credentials()
|
|
228
|
+
if AUTH_KEY not in creds:
|
|
229
|
+
return kwargs
|
|
230
|
+
if self.basic_auth():
|
|
231
|
+
user = creds[DhcoreEnvVar.USER.value]
|
|
232
|
+
password = creds[DhcoreEnvVar.PASSWORD.value]
|
|
233
|
+
kwargs["auth"] = (user, password)
|
|
234
|
+
elif self.oauth2_auth():
|
|
235
|
+
if "headers" not in kwargs:
|
|
236
|
+
kwargs["headers"] = {}
|
|
237
|
+
access_token = creds[DhcoreEnvVar.ACCESS_TOKEN.value]
|
|
238
|
+
kwargs["headers"]["Authorization"] = f"Bearer {access_token}"
|
|
239
|
+
return kwargs
|
|
240
|
+
|
|
241
|
+
def get_new_access_token(self) -> None:
|
|
242
|
+
"""
|
|
243
|
+
Get a new access token.
|
|
244
|
+
|
|
245
|
+
Returns
|
|
246
|
+
-------
|
|
247
|
+
None
|
|
248
|
+
"""
|
|
249
|
+
# Call issuer and get endpoint for
|
|
250
|
+
# refreshing access token
|
|
251
|
+
url = self._get_refresh_endpoint()
|
|
252
|
+
|
|
253
|
+
# Call refresh token endpoint
|
|
254
|
+
# Try token from env
|
|
255
|
+
refresh_token = configurator.load_from_env(DhcoreEnvVar.REFRESH_TOKEN.value)
|
|
256
|
+
response = self._call_refresh_token_endpoint(url, refresh_token)
|
|
257
|
+
|
|
258
|
+
# Otherwise try token from file
|
|
259
|
+
if response.status_code in (400, 401, 403):
|
|
260
|
+
refresh_token = configurator.load_from_config(DhcoreEnvVar.REFRESH_TOKEN.value)
|
|
261
|
+
response = self._call_refresh_token_endpoint(url, refresh_token)
|
|
262
|
+
|
|
263
|
+
response.raise_for_status()
|
|
264
|
+
dict_response = response.json()
|
|
265
|
+
|
|
266
|
+
# Read new access token and refresh token
|
|
267
|
+
configurator.set_credential(DhcoreEnvVar.ACCESS_TOKEN.value, dict_response["access_token"])
|
|
268
|
+
configurator.set_credential(DhcoreEnvVar.REFRESH_TOKEN.value, dict_response["refresh_token"])
|
|
269
|
+
|
|
270
|
+
# Set new credential in stores
|
|
271
|
+
if (access_key := dict_response.get("aws_access_key_id")) is not None:
|
|
272
|
+
configurator.set_credential(S3StoreEnv.ACCESS_KEY_ID.value, access_key)
|
|
273
|
+
os.environ[S3StoreEnv.ACCESS_KEY_ID.value] = access_key
|
|
274
|
+
if (secret_key := dict_response.get("aws_secret_access_key")) is not None:
|
|
275
|
+
configurator.set_credential(S3StoreEnv.SECRET_ACCESS_KEY.value, secret_key)
|
|
276
|
+
os.environ[S3StoreEnv.SECRET_ACCESS_KEY.value] = secret_key
|
|
277
|
+
if (db_username := dict_response.get("db_username")) is not None:
|
|
278
|
+
configurator.set_credential(SqlStoreEnv.USERNAME.value, db_username)
|
|
279
|
+
os.environ[SqlStoreEnv.USERNAME.value] = db_username
|
|
280
|
+
if (db_password := dict_response.get("db_password")) is not None:
|
|
281
|
+
configurator.set_credential(SqlStoreEnv.PASSWORD.value, db_password)
|
|
282
|
+
os.environ[SqlStoreEnv.PASSWORD.value] = db_password
|
|
283
|
+
|
|
284
|
+
# Propagate new access token to config file
|
|
285
|
+
self._write_env()
|
|
286
|
+
|
|
287
|
+
def _write_env(self) -> None:
|
|
288
|
+
"""
|
|
289
|
+
Write the env variables to the .dhcore.ini file.
|
|
290
|
+
It will overwrite any existing env variables.
|
|
291
|
+
|
|
292
|
+
Returns
|
|
293
|
+
-------
|
|
294
|
+
None
|
|
295
|
+
"""
|
|
296
|
+
configurator.write_env(
|
|
297
|
+
[
|
|
298
|
+
DhcoreEnvVar.ACCESS_TOKEN.value,
|
|
299
|
+
DhcoreEnvVar.REFRESH_TOKEN.value,
|
|
300
|
+
S3StoreEnv.ACCESS_KEY_ID.value,
|
|
301
|
+
S3StoreEnv.SECRET_ACCESS_KEY.value,
|
|
302
|
+
SqlStoreEnv.USERNAME.value,
|
|
303
|
+
SqlStoreEnv.PASSWORD.value,
|
|
304
|
+
]
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def _get_refresh_endpoint(self) -> str:
|
|
308
|
+
"""
|
|
309
|
+
Get the refresh endpoint.
|
|
310
|
+
|
|
311
|
+
Returns
|
|
312
|
+
-------
|
|
313
|
+
str
|
|
314
|
+
Refresh endpoint.
|
|
315
|
+
"""
|
|
316
|
+
# Get issuer endpoint
|
|
317
|
+
endpoint_issuer = configurator.load_var(DhcoreEnvVar.ISSUER.value)
|
|
318
|
+
if endpoint_issuer is not None:
|
|
319
|
+
endpoint_issuer = self._sanitize_endpoint(endpoint_issuer)
|
|
320
|
+
configurator.set_credential(DhcoreEnvVar.ISSUER.value, endpoint_issuer)
|
|
321
|
+
else:
|
|
322
|
+
raise ClientError("Issuer endpoint not set.")
|
|
323
|
+
|
|
324
|
+
# Standard issuer endpoint path
|
|
325
|
+
url = endpoint_issuer + "/.well-known/openid-configuration"
|
|
326
|
+
|
|
327
|
+
# Call issuer to get refresh endpoint
|
|
328
|
+
r = request("GET", url, timeout=60)
|
|
329
|
+
r.raise_for_status()
|
|
330
|
+
return r.json().get("token_endpoint")
|
|
331
|
+
|
|
332
|
+
def _call_refresh_token_endpoint(self, url: str, refresh_token: str) -> Response:
|
|
333
|
+
"""
|
|
334
|
+
Call the refresh token endpoint.
|
|
335
|
+
|
|
336
|
+
Parameters
|
|
337
|
+
----------
|
|
338
|
+
url : str
|
|
339
|
+
Refresh token endpoint.
|
|
340
|
+
refresh_token : str
|
|
341
|
+
Refresh token.
|
|
342
|
+
|
|
343
|
+
Returns
|
|
344
|
+
-------
|
|
345
|
+
Response
|
|
346
|
+
Response object.
|
|
347
|
+
"""
|
|
348
|
+
# Get client id
|
|
349
|
+
client_id = configurator.load_var(DhcoreEnvVar.CLIENT_ID.value)
|
|
350
|
+
if client_id is None:
|
|
351
|
+
raise ClientError("Client id not set.")
|
|
352
|
+
|
|
353
|
+
# Send request to get new access token
|
|
354
|
+
payload = {
|
|
355
|
+
"grant_type": "refresh_token",
|
|
356
|
+
"client_id": client_id,
|
|
357
|
+
"refresh_token": refresh_token,
|
|
358
|
+
"scope": "openid credentials offline_access",
|
|
359
|
+
}
|
|
360
|
+
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
361
|
+
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."""
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from digitalhub.client._base.params_builder import ClientParametersBuilder
|
|
4
|
+
from digitalhub.entities._commons.enums import ApiCategories, BackendOperations
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ClientDHCoreParametersBuilder(ClientParametersBuilder):
|
|
8
|
+
"""
|
|
9
|
+
This class is used to build the parameters for the DHCore client calls.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def build_parameters(self, category: str, operation: str, **kwargs) -> dict:
|
|
13
|
+
"""
|
|
14
|
+
Build the parameters for the client call.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
category : str
|
|
19
|
+
API category.
|
|
20
|
+
operation : str
|
|
21
|
+
API operation.
|
|
22
|
+
**kwargs : dict
|
|
23
|
+
Parameters to build.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
dict
|
|
28
|
+
Parameters formatted.
|
|
29
|
+
"""
|
|
30
|
+
if category == ApiCategories.BASE.value:
|
|
31
|
+
return self.build_parameters_base(operation, **kwargs)
|
|
32
|
+
return self.build_parameters_context(operation, **kwargs)
|
|
33
|
+
|
|
34
|
+
def build_parameters_base(self, operation: str, **kwargs) -> dict:
|
|
35
|
+
"""
|
|
36
|
+
Build the base parameters for the client call.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
operation : str
|
|
41
|
+
API operation.
|
|
42
|
+
**kwargs : dict
|
|
43
|
+
Parameters to build.
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
dict
|
|
48
|
+
Parameters formatted.
|
|
49
|
+
"""
|
|
50
|
+
kwargs = self._set_params(**kwargs)
|
|
51
|
+
if operation == BackendOperations.DELETE.value:
|
|
52
|
+
if (cascade := kwargs.pop("cascade", None)) is not None:
|
|
53
|
+
kwargs["params"]["cascade"] = str(cascade).lower()
|
|
54
|
+
elif operation == BackendOperations.SHARE.value:
|
|
55
|
+
kwargs["params"]["user"] = kwargs.pop("user")
|
|
56
|
+
if kwargs.pop("unshare", False):
|
|
57
|
+
kwargs["params"]["id"] = kwargs.pop("id")
|
|
58
|
+
|
|
59
|
+
return kwargs
|
|
60
|
+
|
|
61
|
+
def build_parameters_context(self, operation: str, **kwargs) -> dict:
|
|
62
|
+
"""
|
|
63
|
+
Build the context parameters for the client call.
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
operation : str
|
|
68
|
+
API operation.
|
|
69
|
+
**kwargs : dict
|
|
70
|
+
Parameters to build.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
dict
|
|
75
|
+
Parameters formatted.
|
|
76
|
+
"""
|
|
77
|
+
kwargs = self._set_params(**kwargs)
|
|
78
|
+
|
|
79
|
+
# Handle read
|
|
80
|
+
if operation == BackendOperations.READ.value:
|
|
81
|
+
name = kwargs.pop("entity_name", None)
|
|
82
|
+
if name is not None:
|
|
83
|
+
kwargs["params"]["name"] = name
|
|
84
|
+
elif operation == BackendOperations.READ_ALL_VERSIONS.value:
|
|
85
|
+
kwargs["params"]["versions"] = "all"
|
|
86
|
+
kwargs["params"]["name"] = kwargs.pop("entity_name")
|
|
87
|
+
# Handle delete
|
|
88
|
+
elif operation == BackendOperations.DELETE.value:
|
|
89
|
+
# Handle cascade
|
|
90
|
+
if (cascade := kwargs.pop("cascade", None)) is not None:
|
|
91
|
+
kwargs["params"]["cascade"] = str(cascade).lower()
|
|
92
|
+
|
|
93
|
+
# Handle delete all versions
|
|
94
|
+
entity_id = kwargs.pop("entity_id")
|
|
95
|
+
entity_name = kwargs.pop("entity_name")
|
|
96
|
+
if not kwargs.pop("delete_all_versions", False):
|
|
97
|
+
if entity_id is None:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
"If `delete_all_versions` is False, `entity_id` must be provided,"
|
|
100
|
+
" either as an argument or in key `identifier`.",
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
kwargs["params"]["name"] = entity_name
|
|
104
|
+
# Handle search
|
|
105
|
+
elif operation == BackendOperations.SEARCH.value:
|
|
106
|
+
# Handle fq
|
|
107
|
+
if (fq := kwargs.pop("fq", None)) is not None:
|
|
108
|
+
kwargs["params"]["fq"] = fq
|
|
109
|
+
|
|
110
|
+
# Add search query
|
|
111
|
+
if (query := kwargs.pop("query", None)) is not None:
|
|
112
|
+
kwargs["params"]["q"] = query
|
|
113
|
+
|
|
114
|
+
# Add search filters
|
|
115
|
+
fq = []
|
|
116
|
+
|
|
117
|
+
# Entity types
|
|
118
|
+
if (entity_types := kwargs.pop("entity_types", None)) is not None:
|
|
119
|
+
if not isinstance(entity_types, list):
|
|
120
|
+
entity_types = [entity_types]
|
|
121
|
+
if len(entity_types) == 1:
|
|
122
|
+
entity_types = entity_types[0]
|
|
123
|
+
else:
|
|
124
|
+
entity_types = " OR ".join(entity_types)
|
|
125
|
+
fq.append(f"type:({entity_types})")
|
|
126
|
+
|
|
127
|
+
# Name
|
|
128
|
+
if (name := kwargs.pop("name", None)) is not None:
|
|
129
|
+
fq.append(f'metadata.name:"{name}"')
|
|
130
|
+
|
|
131
|
+
# Kind
|
|
132
|
+
if (kind := kwargs.pop("kind", None)) is not None:
|
|
133
|
+
fq.append(f'kind:"{kind}"')
|
|
134
|
+
|
|
135
|
+
# Time
|
|
136
|
+
created = kwargs.pop("created", None)
|
|
137
|
+
updated = kwargs.pop("updated", None)
|
|
138
|
+
created = created if created is not None else "*"
|
|
139
|
+
updated = updated if updated is not None else "*"
|
|
140
|
+
fq.append(f"metadata.updated:[{created} TO {updated}]")
|
|
141
|
+
|
|
142
|
+
# Description
|
|
143
|
+
if (description := kwargs.pop("description", None)) is not None:
|
|
144
|
+
fq.append(f'metadata.description:"{description}"')
|
|
145
|
+
|
|
146
|
+
# Labels
|
|
147
|
+
if (labels := kwargs.pop("labels", None)) is not None:
|
|
148
|
+
if len(labels) == 1:
|
|
149
|
+
labels = labels[0]
|
|
150
|
+
else:
|
|
151
|
+
labels = " AND ".join(labels)
|
|
152
|
+
fq.append(f"metadata.labels:({labels})")
|
|
153
|
+
|
|
154
|
+
# Add filters
|
|
155
|
+
kwargs["params"]["fq"] = fq
|
|
156
|
+
|
|
157
|
+
return kwargs
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def _set_params(**kwargs) -> dict:
|
|
161
|
+
"""
|
|
162
|
+
Format params parameter.
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
**kwargs : dict
|
|
167
|
+
Keyword arguments.
|
|
168
|
+
|
|
169
|
+
Returns
|
|
170
|
+
-------
|
|
171
|
+
dict
|
|
172
|
+
Parameters with initialized params.
|
|
173
|
+
"""
|
|
174
|
+
if not kwargs:
|
|
175
|
+
kwargs = {}
|
|
176
|
+
if "params" not in kwargs:
|
|
177
|
+
kwargs["params"] = {}
|
|
178
|
+
return kwargs
|