digitalhub 0.14.0b7__py3-none-any.whl → 0.14.1b0__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 -2
- digitalhub/context/api.py +42 -1
- digitalhub/context/context.py +3 -6
- digitalhub/entities/_base/context/entity.py +0 -3
- digitalhub/entities/_base/material/entity.py +2 -2
- digitalhub/entities/_processors/base/crud.py +14 -23
- digitalhub/entities/_processors/base/import_export.py +0 -5
- digitalhub/entities/_processors/base/processor.py +1 -4
- digitalhub/entities/_processors/base/special_ops.py +4 -8
- digitalhub/entities/_processors/context/crud.py +5 -5
- digitalhub/entities/_processors/context/import_export.py +5 -5
- digitalhub/entities/_processors/context/material.py +2 -2
- digitalhub/entities/_processors/context/special_ops.py +13 -13
- digitalhub/entities/_processors/utils.py +2 -111
- digitalhub/entities/function/_base/entity.py +0 -3
- digitalhub/entities/project/_base/builder.py +0 -6
- digitalhub/entities/project/_base/entity.py +4 -12
- digitalhub/entities/project/_base/spec.py +4 -4
- digitalhub/entities/project/crud.py +9 -44
- digitalhub/entities/project/utils.py +7 -3
- digitalhub/entities/workflow/_base/entity.py +0 -5
- digitalhub/stores/client/{dhcore/api_builder.py → api_builder.py} +2 -3
- digitalhub/stores/client/builder.py +20 -32
- digitalhub/stores/client/{dhcore/client.py → client.py} +64 -23
- digitalhub/stores/client/{dhcore/configurator.py → configurator.py} +122 -176
- digitalhub/stores/client/{_base/enums.py → enums.py} +11 -0
- digitalhub/stores/client/{dhcore/http_handler.py → http_handler.py} +4 -5
- digitalhub/stores/client/{_base/key_builder.py → key_builder.py} +13 -13
- digitalhub/stores/client/{dhcore/params_builder.py → params_builder.py} +51 -12
- digitalhub/stores/client/{dhcore/response_processor.py → response_processor.py} +1 -1
- digitalhub/stores/client/{dhcore/utils.py → utils.py} +2 -7
- digitalhub/stores/{credentials → configurator}/api.py +5 -5
- digitalhub/stores/configurator/configurator.py +123 -0
- digitalhub/stores/{credentials → configurator}/enums.py +25 -10
- digitalhub/stores/configurator/handler.py +213 -0
- digitalhub/stores/{credentials → configurator}/ini_module.py +31 -0
- digitalhub/stores/data/_base/store.py +0 -4
- digitalhub/stores/data/api.py +2 -4
- digitalhub/stores/data/builder.py +5 -37
- digitalhub/stores/data/s3/configurator.py +30 -114
- digitalhub/stores/data/s3/store.py +9 -22
- digitalhub/stores/data/sql/configurator.py +49 -71
- digitalhub/stores/data/sql/store.py +20 -55
- {digitalhub-0.14.0b7.dist-info → digitalhub-0.14.1b0.dist-info}/METADATA +1 -1
- {digitalhub-0.14.0b7.dist-info → digitalhub-0.14.1b0.dist-info}/RECORD +51 -66
- digitalhub/stores/client/_base/api_builder.py +0 -34
- digitalhub/stores/client/_base/client.py +0 -243
- digitalhub/stores/client/_base/params_builder.py +0 -82
- digitalhub/stores/client/api.py +0 -32
- digitalhub/stores/client/dhcore/__init__.py +0 -3
- digitalhub/stores/client/dhcore/enums.py +0 -18
- digitalhub/stores/client/dhcore/key_builder.py +0 -62
- digitalhub/stores/client/local/__init__.py +0 -3
- digitalhub/stores/client/local/api_builder.py +0 -116
- digitalhub/stores/client/local/client.py +0 -605
- digitalhub/stores/client/local/enums.py +0 -15
- digitalhub/stores/client/local/key_builder.py +0 -62
- digitalhub/stores/client/local/params_builder.py +0 -97
- digitalhub/stores/credentials/__init__.py +0 -3
- digitalhub/stores/credentials/configurator.py +0 -185
- digitalhub/stores/credentials/handler.py +0 -164
- digitalhub/stores/credentials/store.py +0 -77
- /digitalhub/stores/client/{dhcore/error_parser.py → error_parser.py} +0 -0
- /digitalhub/stores/client/{dhcore/header_manager.py → header_manager.py} +0 -0
- /digitalhub/stores/{client/_base → configurator}/__init__.py +0 -0
- {digitalhub-0.14.0b7.dist-info → digitalhub-0.14.1b0.dist-info}/WHEEL +0 -0
- {digitalhub-0.14.0b7.dist-info → digitalhub-0.14.1b0.dist-info}/licenses/AUTHORS +0 -0
- {digitalhub-0.14.0b7.dist-info → digitalhub-0.14.1b0.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,17 +6,16 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
-
from digitalhub.stores.client.
|
|
10
|
-
from digitalhub.stores.client.
|
|
11
|
-
from digitalhub.stores.client.
|
|
12
|
-
from digitalhub.stores.client.
|
|
13
|
-
from digitalhub.stores.client.
|
|
14
|
-
from digitalhub.stores.client.dhcore.params_builder import ClientDHCoreParametersBuilder
|
|
9
|
+
from digitalhub.stores.client.api_builder import ClientApiBuilder
|
|
10
|
+
from digitalhub.stores.client.header_manager import HeaderManager
|
|
11
|
+
from digitalhub.stores.client.http_handler import HttpRequestHandler
|
|
12
|
+
from digitalhub.stores.client.key_builder import ClientKeyBuilder
|
|
13
|
+
from digitalhub.stores.client.params_builder import ClientParametersBuilder
|
|
15
14
|
from digitalhub.utils.exceptions import BackendError
|
|
16
15
|
from digitalhub.utils.generic_utils import dump_json
|
|
17
16
|
|
|
18
17
|
|
|
19
|
-
class
|
|
18
|
+
class Client:
|
|
20
19
|
"""
|
|
21
20
|
DHCore client for remote DigitalHub Core backend communication.
|
|
22
21
|
|
|
@@ -27,13 +26,11 @@ class ClientDHCore(Client):
|
|
|
27
26
|
JSON serialization.
|
|
28
27
|
"""
|
|
29
28
|
|
|
30
|
-
def __init__(self
|
|
31
|
-
super().__init__()
|
|
32
|
-
|
|
29
|
+
def __init__(self) -> None:
|
|
33
30
|
# API, key and parameters builders
|
|
34
|
-
self._api_builder:
|
|
35
|
-
self._key_builder:
|
|
36
|
-
self._params_builder:
|
|
31
|
+
self._api_builder: ClientApiBuilder = ClientApiBuilder()
|
|
32
|
+
self._key_builder: ClientKeyBuilder = ClientKeyBuilder()
|
|
33
|
+
self._params_builder: ClientParametersBuilder = ClientParametersBuilder()
|
|
37
34
|
|
|
38
35
|
# HTTP request handling
|
|
39
36
|
self._http_handler = HttpRequestHandler()
|
|
@@ -239,23 +236,68 @@ class ClientDHCore(Client):
|
|
|
239
236
|
return objects
|
|
240
237
|
|
|
241
238
|
##############################
|
|
242
|
-
#
|
|
239
|
+
# Build methods
|
|
243
240
|
##############################
|
|
244
241
|
|
|
245
|
-
|
|
246
|
-
def is_local() -> bool:
|
|
242
|
+
def build_api(self, category: str, operation: str, **kwargs) -> str:
|
|
247
243
|
"""
|
|
248
|
-
|
|
244
|
+
Build the API for the client.
|
|
249
245
|
|
|
250
|
-
|
|
251
|
-
|
|
246
|
+
Parameters
|
|
247
|
+
----------
|
|
248
|
+
category : str
|
|
249
|
+
API category.
|
|
250
|
+
operation : str
|
|
251
|
+
API operation.
|
|
252
|
+
**kwargs : dict
|
|
253
|
+
Additional parameters.
|
|
252
254
|
|
|
253
255
|
Returns
|
|
254
256
|
-------
|
|
255
|
-
|
|
256
|
-
|
|
257
|
+
str
|
|
258
|
+
API formatted.
|
|
259
|
+
"""
|
|
260
|
+
return self._api_builder.build_api(category, operation, **kwargs)
|
|
261
|
+
|
|
262
|
+
def build_key(self, category: str, *args, **kwargs) -> str:
|
|
263
|
+
"""
|
|
264
|
+
Build the key for the client.
|
|
265
|
+
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
category : str
|
|
269
|
+
Key category.
|
|
270
|
+
*args : tuple
|
|
271
|
+
Additional arguments.
|
|
272
|
+
**kwargs : dict
|
|
273
|
+
Additional parameters.
|
|
274
|
+
|
|
275
|
+
Returns
|
|
276
|
+
-------
|
|
277
|
+
str
|
|
278
|
+
Key formatted.
|
|
279
|
+
"""
|
|
280
|
+
return self._key_builder.build_key(category, *args, **kwargs)
|
|
281
|
+
|
|
282
|
+
def build_parameters(self, category: str, operation: str, **kwargs) -> dict:
|
|
283
|
+
"""
|
|
284
|
+
Build the parameters for the client call.
|
|
285
|
+
|
|
286
|
+
Parameters
|
|
287
|
+
----------
|
|
288
|
+
category : str
|
|
289
|
+
API category.
|
|
290
|
+
operation : str
|
|
291
|
+
API operation.
|
|
292
|
+
**kwargs : dict
|
|
293
|
+
Parameters to build.
|
|
294
|
+
|
|
295
|
+
Returns
|
|
296
|
+
-------
|
|
297
|
+
dict
|
|
298
|
+
Parameters formatted.
|
|
257
299
|
"""
|
|
258
|
-
return
|
|
300
|
+
return self._params_builder.build_parameters(category, operation, **kwargs)
|
|
259
301
|
|
|
260
302
|
##############################
|
|
261
303
|
# Utility methods
|
|
@@ -265,5 +307,4 @@ class ClientDHCore(Client):
|
|
|
265
307
|
"""
|
|
266
308
|
Manually trigger OAuth2 token refresh.
|
|
267
309
|
"""
|
|
268
|
-
self._http_handler._configurator.check_config()
|
|
269
310
|
self._http_handler._configurator.refresh_credentials()
|
|
@@ -8,10 +8,9 @@ import typing
|
|
|
8
8
|
|
|
9
9
|
from requests import request
|
|
10
10
|
|
|
11
|
-
from digitalhub.stores.client.
|
|
12
|
-
from digitalhub.stores.
|
|
13
|
-
from digitalhub.stores.
|
|
14
|
-
from digitalhub.stores.credentials.handler import creds_handler
|
|
11
|
+
from digitalhub.stores.client.enums import AuthType
|
|
12
|
+
from digitalhub.stores.configurator.configurator import configurator
|
|
13
|
+
from digitalhub.stores.configurator.enums import ConfigurationVars, CredentialsVars
|
|
15
14
|
from digitalhub.utils.exceptions import ClientError
|
|
16
15
|
from digitalhub.utils.generic_utils import list_enum
|
|
17
16
|
from digitalhub.utils.uri_utils import has_remote_scheme
|
|
@@ -20,7 +19,10 @@ if typing.TYPE_CHECKING:
|
|
|
20
19
|
from requests import Response
|
|
21
20
|
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
DEFAULT_TIMEOUT = 60
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ClientConfigurator:
|
|
24
26
|
"""
|
|
25
27
|
DHCore client configurator for credential management and authentication.
|
|
26
28
|
|
|
@@ -35,20 +37,13 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
35
37
|
credential storage.
|
|
36
38
|
"""
|
|
37
39
|
|
|
38
|
-
keys = [*list_enum(
|
|
39
|
-
required_keys = [CredsEnvVar.DHCORE_ENDPOINT.value]
|
|
40
|
-
keys_to_prefix = [
|
|
41
|
-
CredsEnvVar.DHCORE_REFRESH_TOKEN.value,
|
|
42
|
-
CredsEnvVar.DHCORE_ACCESS_TOKEN.value,
|
|
43
|
-
CredsEnvVar.DHCORE_ISSUER.value,
|
|
44
|
-
CredsEnvVar.DHCORE_CLIENT_ID.value,
|
|
45
|
-
]
|
|
40
|
+
keys = [*list_enum(ConfigurationVars), *list_enum(CredentialsVars)]
|
|
46
41
|
|
|
47
42
|
def __init__(self) -> None:
|
|
48
43
|
"""
|
|
49
44
|
Initialize DHCore configurator and evaluate authentication type.
|
|
50
45
|
"""
|
|
51
|
-
|
|
46
|
+
self._validate()
|
|
52
47
|
self._auth_type: str | None = None
|
|
53
48
|
self.set_auth_type()
|
|
54
49
|
|
|
@@ -56,92 +51,6 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
56
51
|
# Credentials methods
|
|
57
52
|
##############################
|
|
58
53
|
|
|
59
|
-
def load_env_vars(self) -> None:
|
|
60
|
-
"""
|
|
61
|
-
Load and sanitize credentials from environment variables.
|
|
62
|
-
|
|
63
|
-
Sanitizes endpoint and issuer URLs to ensure proper HTTP/HTTPS schemes
|
|
64
|
-
and removes trailing slashes.
|
|
65
|
-
"""
|
|
66
|
-
env_creds = self._creds_handler.load_from_env(self.keys)
|
|
67
|
-
env_creds = self._sanitize_env_vars(env_creds)
|
|
68
|
-
self._creds_handler.set_credentials(self._env, env_creds)
|
|
69
|
-
|
|
70
|
-
def _sanitize_env_vars(self, creds: dict) -> dict:
|
|
71
|
-
"""
|
|
72
|
-
Sanitize environment variable credentials.
|
|
73
|
-
|
|
74
|
-
Validates and normalizes endpoint and issuer URLs. Environment variables
|
|
75
|
-
use full "DHCORE_" prefixes.
|
|
76
|
-
|
|
77
|
-
Parameters
|
|
78
|
-
----------
|
|
79
|
-
creds : dict
|
|
80
|
-
Raw credentials from environment variables.
|
|
81
|
-
|
|
82
|
-
Returns
|
|
83
|
-
-------
|
|
84
|
-
dict
|
|
85
|
-
Sanitized credentials with normalized URLs.
|
|
86
|
-
|
|
87
|
-
Raises
|
|
88
|
-
------
|
|
89
|
-
ClientError
|
|
90
|
-
If endpoint or issuer URLs have invalid schemes.
|
|
91
|
-
"""
|
|
92
|
-
creds[CredsEnvVar.DHCORE_ENDPOINT.value] = self._sanitize_endpoint(creds[CredsEnvVar.DHCORE_ENDPOINT.value])
|
|
93
|
-
creds[CredsEnvVar.DHCORE_ISSUER.value] = self._sanitize_endpoint(creds[CredsEnvVar.DHCORE_ISSUER.value])
|
|
94
|
-
return creds
|
|
95
|
-
|
|
96
|
-
def load_file_vars(self) -> None:
|
|
97
|
-
"""
|
|
98
|
-
Load credentials from configuration file with CLI compatibility.
|
|
99
|
-
|
|
100
|
-
Handles keys without "DHCORE_" prefix for CLI compatibility. Falls back
|
|
101
|
-
to environment variables for missing endpoint and personal access token values.
|
|
102
|
-
"""
|
|
103
|
-
file_creds = self._creds_handler.load_from_file(self.keys)
|
|
104
|
-
|
|
105
|
-
# Because in the response there is no personal access token
|
|
106
|
-
pat = CredsEnvVar.DHCORE_PERSONAL_ACCESS_TOKEN.value
|
|
107
|
-
if file_creds[pat] is None:
|
|
108
|
-
file_creds[pat] = self._creds_handler.load_from_env([pat]).get(pat)
|
|
109
|
-
|
|
110
|
-
# Because in the response there is no endpoint
|
|
111
|
-
endpoint = CredsEnvVar.DHCORE_ENDPOINT.value
|
|
112
|
-
if file_creds[endpoint] is None:
|
|
113
|
-
file_creds[endpoint] = self._creds_handler.load_from_env([endpoint]).get(endpoint)
|
|
114
|
-
|
|
115
|
-
file_creds = self._sanitize_file_vars(file_creds)
|
|
116
|
-
self._creds_handler.set_credentials(self._file, file_creds)
|
|
117
|
-
|
|
118
|
-
def _sanitize_file_vars(self, creds: dict) -> dict:
|
|
119
|
-
"""
|
|
120
|
-
Sanitize configuration file credentials.
|
|
121
|
-
|
|
122
|
-
Handles different key formats between file and environment variables.
|
|
123
|
-
File format omits "DHCORE_" prefix for: issuer, client_id, access_token,
|
|
124
|
-
refresh_token. Full names used for: endpoint, user, password, personal_access_token.
|
|
125
|
-
|
|
126
|
-
Parameters
|
|
127
|
-
----------
|
|
128
|
-
creds : dict
|
|
129
|
-
Raw credentials from configuration file.
|
|
130
|
-
|
|
131
|
-
Returns
|
|
132
|
-
-------
|
|
133
|
-
dict
|
|
134
|
-
Sanitized credentials with standardized keys and normalized URLs.
|
|
135
|
-
|
|
136
|
-
Raises
|
|
137
|
-
------
|
|
138
|
-
ClientError
|
|
139
|
-
If endpoint or issuer URLs have invalid schemes.
|
|
140
|
-
"""
|
|
141
|
-
creds[CredsEnvVar.DHCORE_ENDPOINT.value] = self._sanitize_endpoint(creds[CredsEnvVar.DHCORE_ENDPOINT.value])
|
|
142
|
-
creds[CredsEnvVar.DHCORE_ISSUER.value] = self._sanitize_endpoint(creds[CredsEnvVar.DHCORE_ISSUER.value])
|
|
143
|
-
return {k: v for k, v in creds.items() if k in self.keys}
|
|
144
|
-
|
|
145
54
|
@staticmethod
|
|
146
55
|
def _sanitize_endpoint(endpoint: str | None = None) -> str | None:
|
|
147
56
|
"""
|
|
@@ -188,8 +97,9 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
188
97
|
KeyError
|
|
189
98
|
If endpoint not configured in current credential source.
|
|
190
99
|
"""
|
|
191
|
-
|
|
192
|
-
|
|
100
|
+
config = configurator.get_configuration()
|
|
101
|
+
endpoint = config[ConfigurationVars.DHCORE_ENDPOINT.value]
|
|
102
|
+
return self._sanitize_endpoint(endpoint)
|
|
193
103
|
|
|
194
104
|
##############################
|
|
195
105
|
# Origin methods
|
|
@@ -220,15 +130,13 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
220
130
|
(username + password). For EXCHANGE type, automatically exchanges the
|
|
221
131
|
personal access token and switches to file-based credentials storage.
|
|
222
132
|
"""
|
|
223
|
-
creds =
|
|
133
|
+
creds = configurator.get_credentials()
|
|
224
134
|
self._auth_type = self._eval_auth_type(creds)
|
|
225
135
|
# If we have an exchange token, we need to get a new access token.
|
|
226
136
|
# Therefore, we change the origin to file, where the refresh token is written.
|
|
227
137
|
# We also try to fetch the PAT from both env and file
|
|
228
138
|
if self._auth_type == AuthType.EXCHANGE.value:
|
|
229
|
-
self.refresh_credentials(
|
|
230
|
-
# Just to ensure we get the right source from file
|
|
231
|
-
self.change_to_file()
|
|
139
|
+
self.refresh_credentials(retry=True)
|
|
232
140
|
|
|
233
141
|
def refreshable_auth_types(self) -> bool:
|
|
234
142
|
"""
|
|
@@ -261,83 +169,97 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
261
169
|
dict
|
|
262
170
|
Modified kwargs with authentication parameters.
|
|
263
171
|
"""
|
|
264
|
-
creds =
|
|
172
|
+
creds = configurator.get_credentials()
|
|
265
173
|
if self._auth_type in (
|
|
266
174
|
AuthType.EXCHANGE.value,
|
|
267
175
|
AuthType.OAUTH2.value,
|
|
268
176
|
AuthType.ACCESS_TOKEN.value,
|
|
269
177
|
):
|
|
270
|
-
access_token = creds[
|
|
178
|
+
access_token = creds[CredentialsVars.DHCORE_ACCESS_TOKEN.value]
|
|
271
179
|
if "headers" not in kwargs:
|
|
272
180
|
kwargs["headers"] = {}
|
|
273
181
|
kwargs["headers"]["Authorization"] = f"Bearer {access_token}"
|
|
274
182
|
elif self._auth_type == AuthType.BASIC.value:
|
|
275
|
-
user = creds[
|
|
276
|
-
password = creds[
|
|
183
|
+
user = creds[CredentialsVars.DHCORE_USER.value]
|
|
184
|
+
password = creds[CredentialsVars.DHCORE_PASSWORD.value]
|
|
277
185
|
kwargs["auth"] = (user, password)
|
|
278
186
|
return kwargs
|
|
279
187
|
|
|
280
|
-
def refresh_credentials(self
|
|
188
|
+
def refresh_credentials(self) -> None:
|
|
281
189
|
"""
|
|
282
190
|
Refresh authentication tokens using OAuth2 flows.
|
|
191
|
+
"""
|
|
192
|
+
# Get credentials and configuration
|
|
193
|
+
creds = configurator.get_config_creds()
|
|
194
|
+
|
|
195
|
+
# Get token refresh from creds
|
|
196
|
+
if (url := creds.get(ConfigurationVars.OAUTH2_TOKEN_ENDPOINT.value)) is None:
|
|
197
|
+
url = self._get_refresh_endpoint()
|
|
198
|
+
url = self._sanitize_endpoint(url)
|
|
199
|
+
|
|
200
|
+
# Execute the appropriate auth flow
|
|
201
|
+
response = self._evaluate_auth_flow(url, creds)
|
|
283
202
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
203
|
+
# Evaluate a retry
|
|
204
|
+
self._evaluate_retry(response)
|
|
205
|
+
|
|
206
|
+
# Raise an error if the response indicates failure
|
|
207
|
+
response.raise_for_status()
|
|
208
|
+
|
|
209
|
+
# Export new credentials to file
|
|
210
|
+
self._export_new_creds(response.json())
|
|
211
|
+
|
|
212
|
+
def _evaluate_auth_flow(self, url: str, creds: dict) -> Response:
|
|
213
|
+
"""
|
|
214
|
+
Evaluate the auth flow to execute.
|
|
287
215
|
|
|
288
216
|
Parameters
|
|
289
217
|
----------
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
------
|
|
295
|
-
ClientError
|
|
296
|
-
If auth type doesn't support refresh or credentials missing.
|
|
218
|
+
url : str
|
|
219
|
+
Token endpoint URL.
|
|
220
|
+
creds : dict
|
|
221
|
+
Available credential values.
|
|
297
222
|
"""
|
|
298
223
|
if not self.refreshable_auth_types():
|
|
299
224
|
raise ClientError(f"Auth type {self._auth_type} does not support refresh.")
|
|
300
225
|
|
|
301
|
-
|
|
302
|
-
url = self._get_refresh_endpoint()
|
|
303
|
-
|
|
304
|
-
# Get credentials
|
|
305
|
-
creds = self._creds_handler.get_credentials(self._origin)
|
|
306
|
-
|
|
307
|
-
# Get client id
|
|
308
|
-
if (client_id := creds.get(CredsEnvVar.DHCORE_CLIENT_ID.value)) is None:
|
|
226
|
+
if (client_id := creds.get(ConfigurationVars.DHCORE_CLIENT_ID.value)) is None:
|
|
309
227
|
raise ClientError("Client id not set.")
|
|
310
228
|
|
|
311
|
-
# Handling of token
|
|
229
|
+
# Handling of token refresh
|
|
312
230
|
if self._auth_type == AuthType.OAUTH2.value:
|
|
313
|
-
|
|
231
|
+
return self._call_refresh_endpoint(
|
|
314
232
|
url,
|
|
315
233
|
client_id=client_id,
|
|
316
|
-
refresh_token=creds.get(
|
|
234
|
+
refresh_token=creds.get(CredentialsVars.DHCORE_REFRESH_TOKEN.value),
|
|
317
235
|
grant_type="refresh_token",
|
|
318
236
|
scope="credentials",
|
|
319
237
|
)
|
|
320
|
-
elif self._auth_type == AuthType.EXCHANGE.value:
|
|
321
|
-
response = self._call_refresh_endpoint(
|
|
322
|
-
url,
|
|
323
|
-
client_id=client_id,
|
|
324
|
-
subject_token=creds.get(CredsEnvVar.DHCORE_PERSONAL_ACCESS_TOKEN.value),
|
|
325
|
-
subject_token_type="urn:ietf:params:oauth:token-type:pat",
|
|
326
|
-
grant_type="urn:ietf:params:oauth:grant-type:token-exchange",
|
|
327
|
-
scope="credentials",
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
# Change origin of creds if needed
|
|
331
|
-
if response.status_code in (400, 401, 403):
|
|
332
|
-
if not change_origin:
|
|
333
|
-
raise ClientError("Unable to refresh credentials. Please check your credentials.")
|
|
334
|
-
self.eval_change_origin()
|
|
335
|
-
self.refresh_credentials(change_origin=False)
|
|
336
238
|
|
|
337
|
-
|
|
239
|
+
## Handling of token exchange
|
|
240
|
+
return self._call_refresh_endpoint(
|
|
241
|
+
url,
|
|
242
|
+
client_id=client_id,
|
|
243
|
+
subject_token=creds.get(CredentialsVars.DHCORE_PERSONAL_ACCESS_TOKEN.value),
|
|
244
|
+
subject_token_type="urn:ietf:params:oauth:token-type:pat",
|
|
245
|
+
grant_type="urn:ietf:params:oauth:grant-type:token-exchange",
|
|
246
|
+
scope="credentials",
|
|
247
|
+
)
|
|
338
248
|
|
|
339
|
-
|
|
340
|
-
|
|
249
|
+
def _evaluate_retry(self, response: Response) -> None:
|
|
250
|
+
"""
|
|
251
|
+
Evaluate the status of retry lifecycle.
|
|
252
|
+
"""
|
|
253
|
+
if response.status_code not in (400, 401, 403):
|
|
254
|
+
return
|
|
255
|
+
if configurator.eval_retry():
|
|
256
|
+
self.refresh_credentials()
|
|
257
|
+
raise ClientError(
|
|
258
|
+
"Failed to refresh credentials after retry"
|
|
259
|
+
" (checked credentials from file and env)."
|
|
260
|
+
" Please check your credentials"
|
|
261
|
+
" (refresh tokens, password, etc.)."
|
|
262
|
+
)
|
|
341
263
|
|
|
342
264
|
def _get_refresh_endpoint(self) -> str:
|
|
343
265
|
"""
|
|
@@ -346,28 +268,25 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
346
268
|
Queries /.well-known/openid-configuration to extract token_endpoint for
|
|
347
269
|
credential refresh operations.
|
|
348
270
|
|
|
271
|
+
Parameters
|
|
272
|
+
----------
|
|
273
|
+
creds : dict
|
|
274
|
+
Available credential values.
|
|
275
|
+
|
|
349
276
|
Returns
|
|
350
277
|
-------
|
|
351
278
|
str
|
|
352
279
|
Token endpoint URL for credential refresh.
|
|
353
|
-
|
|
354
|
-
Raises
|
|
355
|
-
------
|
|
356
|
-
ClientError
|
|
357
|
-
If issuer endpoint not configured.
|
|
358
|
-
HTTPError
|
|
359
|
-
If well-known configuration endpoint inaccessible.
|
|
360
|
-
KeyError
|
|
361
|
-
If token_endpoint not found in issuer configuration.
|
|
362
280
|
"""
|
|
281
|
+
config = configurator.get_configuration()
|
|
282
|
+
|
|
363
283
|
# Get issuer endpoint
|
|
364
|
-
|
|
365
|
-
endpoint_issuer = creds.get(CredsEnvVar.DHCORE_ISSUER.value)
|
|
366
|
-
if endpoint_issuer is None:
|
|
284
|
+
if (endpoint_issuer := config.get(ConfigurationVars.DHCORE_ISSUER.value)) is None:
|
|
367
285
|
raise ClientError("Issuer endpoint not set.")
|
|
368
286
|
|
|
369
287
|
# Standard issuer endpoint path
|
|
370
288
|
url = endpoint_issuer + "/.well-known/openid-configuration"
|
|
289
|
+
url = self._sanitize_endpoint(url)
|
|
371
290
|
|
|
372
291
|
# Call issuer to get refresh endpoint
|
|
373
292
|
r = request("GET", url, timeout=60)
|
|
@@ -400,7 +319,13 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
400
319
|
# Send request to get new access token
|
|
401
320
|
payload = {**kwargs}
|
|
402
321
|
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
403
|
-
return request(
|
|
322
|
+
return request(
|
|
323
|
+
"POST",
|
|
324
|
+
url,
|
|
325
|
+
data=payload,
|
|
326
|
+
headers=headers,
|
|
327
|
+
timeout=DEFAULT_TIMEOUT,
|
|
328
|
+
)
|
|
404
329
|
|
|
405
330
|
def _eval_auth_type(self, creds: dict) -> str | None:
|
|
406
331
|
"""
|
|
@@ -419,16 +344,19 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
419
344
|
str or None
|
|
420
345
|
Authentication type from AuthType enum, or None if no valid credentials.
|
|
421
346
|
"""
|
|
422
|
-
if creds[
|
|
347
|
+
if creds[CredentialsVars.DHCORE_PERSONAL_ACCESS_TOKEN.value] is not None:
|
|
423
348
|
return AuthType.EXCHANGE.value
|
|
424
349
|
if (
|
|
425
|
-
creds[
|
|
426
|
-
and creds[
|
|
350
|
+
creds[CredentialsVars.DHCORE_ACCESS_TOKEN.value] is not None
|
|
351
|
+
and creds[CredentialsVars.DHCORE_REFRESH_TOKEN.value] is not None
|
|
427
352
|
):
|
|
428
353
|
return AuthType.OAUTH2.value
|
|
429
|
-
if creds[
|
|
354
|
+
if creds[CredentialsVars.DHCORE_ACCESS_TOKEN.value] is not None:
|
|
430
355
|
return AuthType.ACCESS_TOKEN.value
|
|
431
|
-
if
|
|
356
|
+
if (
|
|
357
|
+
creds[CredentialsVars.DHCORE_USER.value] is not None
|
|
358
|
+
and creds[CredentialsVars.DHCORE_PASSWORD.value] is not None
|
|
359
|
+
):
|
|
432
360
|
return AuthType.BASIC.value
|
|
433
361
|
return None
|
|
434
362
|
|
|
@@ -444,12 +372,30 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
444
372
|
response : dict
|
|
445
373
|
OAuth2 token response with new credentials.
|
|
446
374
|
"""
|
|
447
|
-
|
|
375
|
+
keys_to_prefix = [
|
|
376
|
+
CredentialsVars.DHCORE_REFRESH_TOKEN.value,
|
|
377
|
+
CredentialsVars.DHCORE_ACCESS_TOKEN.value,
|
|
378
|
+
ConfigurationVars.DHCORE_CLIENT_ID.value,
|
|
379
|
+
ConfigurationVars.DHCORE_ISSUER.value,
|
|
380
|
+
ConfigurationVars.OAUTH2_TOKEN_ENDPOINT.value,
|
|
381
|
+
]
|
|
382
|
+
for key in keys_to_prefix:
|
|
383
|
+
if key == ConfigurationVars.OAUTH2_TOKEN_ENDPOINT.value:
|
|
384
|
+
prefix = "oauth2_"
|
|
385
|
+
else:
|
|
386
|
+
prefix = "dhcore_"
|
|
448
387
|
key = key.lower()
|
|
449
|
-
if key.removeprefix(
|
|
450
|
-
response[key] = response.pop(key.removeprefix(
|
|
451
|
-
|
|
452
|
-
|
|
388
|
+
if key.removeprefix(prefix) in response:
|
|
389
|
+
response[key] = response.pop(key.removeprefix(prefix))
|
|
390
|
+
configurator.write_file(response)
|
|
391
|
+
configurator.reload_credentials()
|
|
453
392
|
|
|
454
|
-
|
|
455
|
-
|
|
393
|
+
def _validate(self) -> None:
|
|
394
|
+
"""
|
|
395
|
+
Validate if all required keys are present in the configuration.
|
|
396
|
+
"""
|
|
397
|
+
required_keys = [ConfigurationVars.DHCORE_ENDPOINT.value]
|
|
398
|
+
current_keys = configurator.get_config_creds()
|
|
399
|
+
for key in required_keys:
|
|
400
|
+
if current_keys.get(key) is None:
|
|
401
|
+
raise ClientError(f"Required configuration key '{key}' is missing.")
|
|
@@ -7,6 +7,17 @@ from __future__ import annotations
|
|
|
7
7
|
from enum import Enum
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
class AuthType(Enum):
|
|
11
|
+
"""
|
|
12
|
+
Authentication types.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
BASIC = "basic"
|
|
16
|
+
OAUTH2 = "oauth2"
|
|
17
|
+
EXCHANGE = "exchange"
|
|
18
|
+
ACCESS_TOKEN = "access_token_only"
|
|
19
|
+
|
|
20
|
+
|
|
10
21
|
class ApiCategories(Enum):
|
|
11
22
|
"""
|
|
12
23
|
API categories.
|
|
@@ -6,8 +6,8 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
from requests import request
|
|
8
8
|
|
|
9
|
-
from digitalhub.stores.client.
|
|
10
|
-
from digitalhub.stores.client.
|
|
9
|
+
from digitalhub.stores.client.configurator import ClientConfigurator
|
|
10
|
+
from digitalhub.stores.client.response_processor import ResponseProcessor
|
|
11
11
|
from digitalhub.utils.exceptions import BackendError
|
|
12
12
|
|
|
13
13
|
# Default timeout for requests (in seconds)
|
|
@@ -25,7 +25,7 @@ class HttpRequestHandler:
|
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
27
|
def __init__(self) -> None:
|
|
28
|
-
self._configurator =
|
|
28
|
+
self._configurator = ClientConfigurator()
|
|
29
29
|
self._response_processor = ResponseProcessor()
|
|
30
30
|
|
|
31
31
|
def prepare_request(self, method: str, api: str, **kwargs) -> dict:
|
|
@@ -90,7 +90,7 @@ class HttpRequestHandler:
|
|
|
90
90
|
except BackendError as e:
|
|
91
91
|
# Handle authentication errors with token refresh
|
|
92
92
|
if response.status_code == 401 and refresh and self._configurator.refreshable_auth_types():
|
|
93
|
-
self._configurator.refresh_credentials(
|
|
93
|
+
self._configurator.refresh_credentials(retry=True)
|
|
94
94
|
kwargs = self._configurator.get_auth_parameters(kwargs)
|
|
95
95
|
return self._execute_request(method, url, refresh=False, **kwargs)
|
|
96
96
|
raise e
|
|
@@ -109,7 +109,6 @@ class HttpRequestHandler:
|
|
|
109
109
|
dict
|
|
110
110
|
kwargs enhanced with authentication parameters.
|
|
111
111
|
"""
|
|
112
|
-
self._configurator.check_config()
|
|
113
112
|
return self._configurator.get_auth_parameters(kwargs)
|
|
114
113
|
|
|
115
114
|
def _build_url(self, api: str) -> str:
|
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
from
|
|
8
|
-
|
|
9
|
-
from digitalhub.stores.client._base.enums import ApiCategories
|
|
7
|
+
from digitalhub.stores.client.enums import ApiCategories
|
|
10
8
|
|
|
11
9
|
|
|
12
10
|
class ClientKeyBuilder:
|
|
@@ -36,7 +34,6 @@ class ClientKeyBuilder:
|
|
|
36
34
|
return self.base_entity_key(*args, **kwargs)
|
|
37
35
|
return self.context_entity_key(*args, **kwargs)
|
|
38
36
|
|
|
39
|
-
@abstractmethod
|
|
40
37
|
def base_entity_key(self, entity_id: str) -> str:
|
|
41
38
|
"""
|
|
42
39
|
Build for base entity key.
|
|
@@ -44,15 +41,15 @@ class ClientKeyBuilder:
|
|
|
44
41
|
Parameters
|
|
45
42
|
----------
|
|
46
43
|
entity_id : str
|
|
47
|
-
|
|
44
|
+
Entity id.
|
|
48
45
|
|
|
49
46
|
Returns
|
|
50
47
|
-------
|
|
51
48
|
str
|
|
52
|
-
|
|
49
|
+
Key.
|
|
53
50
|
"""
|
|
51
|
+
return f"store://{entity_id}"
|
|
54
52
|
|
|
55
|
-
@abstractmethod
|
|
56
53
|
def context_entity_key(
|
|
57
54
|
self,
|
|
58
55
|
project: str,
|
|
@@ -67,18 +64,21 @@ class ClientKeyBuilder:
|
|
|
67
64
|
Parameters
|
|
68
65
|
----------
|
|
69
66
|
project : str
|
|
70
|
-
|
|
67
|
+
Project name.
|
|
71
68
|
entity_type : str
|
|
72
|
-
|
|
69
|
+
Entity type.
|
|
73
70
|
entity_kind : str
|
|
74
|
-
|
|
71
|
+
Entity kind.
|
|
75
72
|
entity_name : str
|
|
76
|
-
|
|
73
|
+
Entity name.
|
|
77
74
|
entity_id : str
|
|
78
|
-
|
|
75
|
+
Entity ID.
|
|
79
76
|
|
|
80
77
|
Returns
|
|
81
78
|
-------
|
|
82
79
|
str
|
|
83
|
-
|
|
80
|
+
Key.
|
|
84
81
|
"""
|
|
82
|
+
if entity_id is None:
|
|
83
|
+
return f"store://{project}/{entity_type}/{entity_kind}/{entity_name}"
|
|
84
|
+
return f"store://{project}/{entity_type}/{entity_kind}/{entity_name}:{entity_id}"
|