digitalhub 0.13.0b3__py3-none-any.whl → 0.14.9__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.
- digitalhub/__init__.py +3 -8
- digitalhub/context/api.py +43 -6
- digitalhub/context/builder.py +1 -5
- digitalhub/context/context.py +28 -13
- digitalhub/entities/_base/_base/entity.py +0 -15
- digitalhub/entities/_base/context/entity.py +1 -4
- digitalhub/entities/_base/entity/builder.py +5 -5
- digitalhub/entities/_base/entity/entity.py +0 -8
- digitalhub/entities/_base/executable/entity.py +195 -87
- digitalhub/entities/_base/material/entity.py +11 -23
- digitalhub/entities/_base/material/utils.py +28 -4
- digitalhub/entities/_base/runtime_entity/builder.py +53 -18
- digitalhub/entities/_base/unversioned/entity.py +1 -1
- digitalhub/entities/_base/versioned/entity.py +1 -1
- digitalhub/entities/_commons/enums.py +1 -31
- digitalhub/entities/_commons/metrics.py +64 -30
- digitalhub/entities/_commons/utils.py +119 -30
- digitalhub/entities/_constructors/_resources.py +151 -0
- digitalhub/entities/{_base/entity/_constructors → _constructors}/name.py +18 -0
- digitalhub/entities/_processors/base/crud.py +381 -0
- digitalhub/entities/_processors/base/import_export.py +118 -0
- digitalhub/entities/_processors/base/processor.py +299 -0
- digitalhub/entities/_processors/base/special_ops.py +104 -0
- digitalhub/entities/_processors/context/crud.py +652 -0
- digitalhub/entities/_processors/context/import_export.py +242 -0
- digitalhub/entities/_processors/context/material.py +123 -0
- digitalhub/entities/_processors/context/processor.py +400 -0
- digitalhub/entities/_processors/context/special_ops.py +476 -0
- digitalhub/entities/_processors/processors.py +12 -0
- digitalhub/entities/_processors/utils.py +38 -102
- digitalhub/entities/artifact/crud.py +58 -22
- digitalhub/entities/artifact/utils.py +28 -13
- digitalhub/entities/builders.py +2 -0
- digitalhub/entities/dataitem/crud.py +63 -20
- digitalhub/entities/dataitem/table/entity.py +27 -22
- digitalhub/entities/dataitem/utils.py +82 -32
- digitalhub/entities/function/_base/entity.py +3 -6
- digitalhub/entities/function/crud.py +55 -24
- digitalhub/entities/model/_base/entity.py +62 -20
- digitalhub/entities/model/crud.py +59 -23
- digitalhub/entities/model/mlflow/utils.py +29 -20
- digitalhub/entities/model/utils.py +28 -13
- digitalhub/entities/project/_base/builder.py +0 -6
- digitalhub/entities/project/_base/entity.py +337 -164
- digitalhub/entities/project/_base/spec.py +4 -4
- digitalhub/entities/project/crud.py +28 -71
- digitalhub/entities/project/utils.py +7 -3
- digitalhub/entities/run/_base/builder.py +0 -4
- digitalhub/entities/run/_base/entity.py +70 -63
- digitalhub/entities/run/crud.py +79 -26
- digitalhub/entities/secret/_base/entity.py +1 -5
- digitalhub/entities/secret/crud.py +31 -28
- digitalhub/entities/task/_base/builder.py +0 -4
- digitalhub/entities/task/_base/entity.py +5 -5
- digitalhub/entities/task/_base/models.py +13 -16
- digitalhub/entities/task/crud.py +61 -29
- digitalhub/entities/trigger/_base/entity.py +1 -5
- digitalhub/entities/trigger/crud.py +89 -30
- digitalhub/entities/workflow/_base/entity.py +3 -8
- digitalhub/entities/workflow/crud.py +55 -24
- digitalhub/factory/entity.py +283 -0
- digitalhub/factory/enums.py +18 -0
- digitalhub/factory/registry.py +197 -0
- digitalhub/factory/runtime.py +44 -0
- digitalhub/factory/utils.py +3 -54
- digitalhub/runtimes/_base.py +2 -2
- digitalhub/stores/client/{dhcore/api_builder.py → api_builder.py} +3 -3
- digitalhub/stores/client/builder.py +19 -31
- digitalhub/stores/client/client.py +322 -0
- digitalhub/stores/client/configurator.py +408 -0
- digitalhub/stores/client/enums.py +50 -0
- digitalhub/stores/client/{dhcore/error_parser.py → error_parser.py} +0 -4
- digitalhub/stores/client/header_manager.py +61 -0
- digitalhub/stores/client/http_handler.py +152 -0
- digitalhub/stores/client/{_base/key_builder.py → key_builder.py} +14 -14
- digitalhub/stores/client/params_builder.py +330 -0
- digitalhub/stores/client/response_processor.py +102 -0
- digitalhub/stores/client/utils.py +35 -0
- digitalhub/stores/{credentials → configurator}/api.py +5 -9
- digitalhub/stores/configurator/configurator.py +123 -0
- digitalhub/stores/{credentials → configurator}/enums.py +27 -10
- digitalhub/stores/configurator/handler.py +213 -0
- digitalhub/stores/{credentials → configurator}/ini_module.py +31 -22
- digitalhub/stores/data/_base/store.py +0 -20
- digitalhub/stores/data/api.py +5 -7
- digitalhub/stores/data/builder.py +53 -27
- digitalhub/stores/data/local/store.py +0 -103
- digitalhub/stores/data/remote/store.py +0 -4
- digitalhub/stores/data/s3/configurator.py +39 -77
- digitalhub/stores/data/s3/store.py +57 -37
- digitalhub/stores/data/sql/configurator.py +66 -46
- digitalhub/stores/data/sql/store.py +171 -104
- digitalhub/stores/readers/data/factory.py +0 -8
- digitalhub/stores/readers/data/pandas/reader.py +9 -19
- digitalhub/utils/file_utils.py +0 -17
- digitalhub/utils/generic_utils.py +1 -14
- digitalhub/utils/git_utils.py +0 -8
- digitalhub/utils/io_utils.py +0 -12
- digitalhub/utils/store_utils.py +44 -0
- {digitalhub-0.13.0b3.dist-info → digitalhub-0.14.9.dist-info}/METADATA +5 -4
- {digitalhub-0.13.0b3.dist-info → digitalhub-0.14.9.dist-info}/RECORD +112 -113
- {digitalhub-0.13.0b3.dist-info → digitalhub-0.14.9.dist-info}/WHEEL +1 -1
- digitalhub/entities/_commons/types.py +0 -9
- digitalhub/entities/_processors/base.py +0 -531
- digitalhub/entities/_processors/context.py +0 -1299
- digitalhub/entities/task/_base/utils.py +0 -22
- digitalhub/factory/factory.py +0 -381
- 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 -34
- digitalhub/stores/client/api.py +0 -36
- digitalhub/stores/client/dhcore/client.py +0 -613
- digitalhub/stores/client/dhcore/configurator.py +0 -675
- digitalhub/stores/client/dhcore/enums.py +0 -34
- digitalhub/stores/client/dhcore/key_builder.py +0 -62
- digitalhub/stores/client/dhcore/models.py +0 -40
- digitalhub/stores/client/dhcore/params_builder.py +0 -278
- digitalhub/stores/client/dhcore/utils.py +0 -94
- digitalhub/stores/client/local/api_builder.py +0 -116
- digitalhub/stores/client/local/client.py +0 -573
- 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 -120
- digitalhub/stores/credentials/__init__.py +0 -3
- digitalhub/stores/credentials/configurator.py +0 -210
- digitalhub/stores/credentials/handler.py +0 -176
- digitalhub/stores/credentials/store.py +0 -81
- digitalhub/stores/data/enums.py +0 -15
- digitalhub/stores/data/s3/utils.py +0 -78
- /digitalhub/entities/{_base/entity/_constructors → _constructors}/__init__.py +0 -0
- /digitalhub/entities/{_base/entity/_constructors → _constructors}/metadata.py +0 -0
- /digitalhub/entities/{_base/entity/_constructors → _constructors}/spec.py +0 -0
- /digitalhub/entities/{_base/entity/_constructors → _constructors}/status.py +0 -0
- /digitalhub/entities/{_base/entity/_constructors → _constructors}/uuid.py +0 -0
- /digitalhub/{stores/client/_base → entities/_processors/base}/__init__.py +0 -0
- /digitalhub/{stores/client/dhcore → entities/_processors/context}/__init__.py +0 -0
- /digitalhub/stores/{client/local → configurator}/__init__.py +0 -0
- {digitalhub-0.13.0b3.dist-info → digitalhub-0.14.9.dist-info}/licenses/AUTHORS +0 -0
- {digitalhub-0.13.0b3.dist-info → digitalhub-0.14.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2025 DSLab - Fondazione Bruno Kessler
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import typing
|
|
8
|
+
from warnings import warn
|
|
9
|
+
|
|
10
|
+
from requests import request
|
|
11
|
+
|
|
12
|
+
from digitalhub.stores.client.enums import AuthType
|
|
13
|
+
from digitalhub.stores.configurator.configurator import configurator
|
|
14
|
+
from digitalhub.stores.configurator.enums import ConfigurationVars, CredentialsVars
|
|
15
|
+
from digitalhub.utils.exceptions import ClientError
|
|
16
|
+
from digitalhub.utils.generic_utils import list_enum
|
|
17
|
+
from digitalhub.utils.uri_utils import has_remote_scheme
|
|
18
|
+
|
|
19
|
+
if typing.TYPE_CHECKING:
|
|
20
|
+
from requests import Response
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
DEFAULT_TIMEOUT = 60
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ClientConfigurator:
|
|
27
|
+
"""
|
|
28
|
+
DHCore client configurator for credential management and authentication.
|
|
29
|
+
|
|
30
|
+
Handles loading credentials from environment variables and configuration files,
|
|
31
|
+
evaluates authentication types, and manages token refresh operations. Supports
|
|
32
|
+
multiple authentication methods: EXCHANGE (personal access token), OAUTH2
|
|
33
|
+
(access + refresh tokens), ACCESS_TOKEN (access token only), and BASIC
|
|
34
|
+
(username + password).
|
|
35
|
+
|
|
36
|
+
The configurator automatically determines the best authentication method and
|
|
37
|
+
handles token exchange for personal access tokens by switching to file-based
|
|
38
|
+
credential storage.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
keys = [*list_enum(ConfigurationVars), *list_enum(CredentialsVars)]
|
|
42
|
+
|
|
43
|
+
def __init__(self) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Initialize DHCore configurator and evaluate authentication type.
|
|
46
|
+
"""
|
|
47
|
+
self._validate()
|
|
48
|
+
self._auth_type: str | None = None
|
|
49
|
+
self.set_auth_type()
|
|
50
|
+
|
|
51
|
+
##############################
|
|
52
|
+
# Credentials methods
|
|
53
|
+
##############################
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _sanitize_endpoint(endpoint: str | None = None) -> str | None:
|
|
57
|
+
"""
|
|
58
|
+
Validate and normalize endpoint URL.
|
|
59
|
+
|
|
60
|
+
Ensures proper HTTP/HTTPS scheme, trims whitespace, and removes trailing slashes.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
endpoint : str
|
|
65
|
+
Endpoint URL to sanitize.
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
str or None
|
|
70
|
+
Sanitized URL or None if input was None.
|
|
71
|
+
|
|
72
|
+
Raises
|
|
73
|
+
------
|
|
74
|
+
ClientError
|
|
75
|
+
If endpoint lacks http:// or https:// scheme.
|
|
76
|
+
"""
|
|
77
|
+
if endpoint is None:
|
|
78
|
+
return
|
|
79
|
+
if not has_remote_scheme(endpoint):
|
|
80
|
+
raise ClientError("Invalid endpoint scheme. Must start with http:// or https://.")
|
|
81
|
+
|
|
82
|
+
endpoint = endpoint.strip()
|
|
83
|
+
return endpoint.removesuffix("/")
|
|
84
|
+
|
|
85
|
+
def get_endpoint(self) -> str:
|
|
86
|
+
"""
|
|
87
|
+
Get the configured DHCore backend endpoint.
|
|
88
|
+
|
|
89
|
+
Returns the sanitized and validated endpoint URL from current credential source.
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
str
|
|
94
|
+
DHCore backend endpoint URL.
|
|
95
|
+
|
|
96
|
+
Raises
|
|
97
|
+
------
|
|
98
|
+
KeyError
|
|
99
|
+
If endpoint not configured in current credential source.
|
|
100
|
+
"""
|
|
101
|
+
config = configurator.get_configuration()
|
|
102
|
+
endpoint = config[ConfigurationVars.DHCORE_ENDPOINT.value]
|
|
103
|
+
return self._sanitize_endpoint(endpoint)
|
|
104
|
+
|
|
105
|
+
##############################
|
|
106
|
+
# Auth methods
|
|
107
|
+
##############################
|
|
108
|
+
|
|
109
|
+
def set_auth_type(self) -> None:
|
|
110
|
+
"""
|
|
111
|
+
Determine authentication type from available credentials.
|
|
112
|
+
|
|
113
|
+
Evaluates credentials in priority order: EXCHANGE (personal access token),
|
|
114
|
+
OAUTH2 (access + refresh tokens), ACCESS_TOKEN (access only), BASIC
|
|
115
|
+
(username + password). For EXCHANGE type, automatically exchanges the
|
|
116
|
+
personal access token and switches to file-based credentials storage.
|
|
117
|
+
"""
|
|
118
|
+
creds = configurator.get_credentials()
|
|
119
|
+
self._auth_type = self._eval_auth_type(creds)
|
|
120
|
+
# If we have an exchange token, we need to get a new access token.
|
|
121
|
+
# Therefore, we change the origin to file, where the refresh token is written.
|
|
122
|
+
# We also try to fetch the PAT from both env and file
|
|
123
|
+
if self._auth_type == AuthType.EXCHANGE.value:
|
|
124
|
+
self.refresh_credentials()
|
|
125
|
+
|
|
126
|
+
def refreshable_auth_types(self) -> bool:
|
|
127
|
+
"""
|
|
128
|
+
Check if current authentication supports token refresh.
|
|
129
|
+
|
|
130
|
+
Returns True for OAUTH2 (refresh token) and EXCHANGE (personal access token),
|
|
131
|
+
False for BASIC and ACCESS_TOKEN.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
bool
|
|
136
|
+
Whether authentication type supports refresh.
|
|
137
|
+
"""
|
|
138
|
+
return self._auth_type in [AuthType.OAUTH2.value, AuthType.EXCHANGE.value]
|
|
139
|
+
|
|
140
|
+
def get_auth_parameters(self, kwargs: dict) -> dict:
|
|
141
|
+
"""
|
|
142
|
+
Add authentication headers/parameters to HTTP request kwargs.
|
|
143
|
+
|
|
144
|
+
Adds Authorization Bearer header for token-based auth or auth tuple
|
|
145
|
+
for basic authentication.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
kwargs : dict
|
|
150
|
+
HTTP request arguments to modify.
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
dict
|
|
155
|
+
Modified kwargs with authentication parameters.
|
|
156
|
+
"""
|
|
157
|
+
creds = configurator.get_credentials()
|
|
158
|
+
if self._auth_type in (
|
|
159
|
+
AuthType.EXCHANGE.value,
|
|
160
|
+
AuthType.OAUTH2.value,
|
|
161
|
+
AuthType.ACCESS_TOKEN.value,
|
|
162
|
+
):
|
|
163
|
+
access_token = creds[CredentialsVars.DHCORE_ACCESS_TOKEN.value]
|
|
164
|
+
if "headers" not in kwargs:
|
|
165
|
+
kwargs["headers"] = {}
|
|
166
|
+
kwargs["headers"]["Authorization"] = f"Bearer {access_token}"
|
|
167
|
+
elif self._auth_type == AuthType.BASIC.value:
|
|
168
|
+
user = creds[CredentialsVars.DHCORE_USER.value]
|
|
169
|
+
password = creds[CredentialsVars.DHCORE_PASSWORD.value]
|
|
170
|
+
kwargs["auth"] = (user, password)
|
|
171
|
+
return kwargs
|
|
172
|
+
|
|
173
|
+
def _evaluate_auth_flow(self, url: str, creds: dict) -> Response:
|
|
174
|
+
"""
|
|
175
|
+
Evaluate the auth flow to execute.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
url : str
|
|
180
|
+
Token endpoint URL.
|
|
181
|
+
creds : dict
|
|
182
|
+
Available credential values.
|
|
183
|
+
"""
|
|
184
|
+
if (client_id := creds.get(ConfigurationVars.DHCORE_CLIENT_ID.value)) is None:
|
|
185
|
+
raise ClientError("Client id not set.")
|
|
186
|
+
|
|
187
|
+
# Handling of token refresh
|
|
188
|
+
if self._auth_type == AuthType.OAUTH2.value:
|
|
189
|
+
return self._call_refresh_endpoint(
|
|
190
|
+
url,
|
|
191
|
+
client_id=client_id,
|
|
192
|
+
refresh_token=creds.get(CredentialsVars.DHCORE_REFRESH_TOKEN.value),
|
|
193
|
+
grant_type="refresh_token",
|
|
194
|
+
scope="credentials",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
## Handling of token exchange
|
|
198
|
+
return self._call_refresh_endpoint(
|
|
199
|
+
url,
|
|
200
|
+
client_id=client_id,
|
|
201
|
+
subject_token=creds.get(CredentialsVars.DHCORE_PERSONAL_ACCESS_TOKEN.value),
|
|
202
|
+
subject_token_type="urn:ietf:params:oauth:token-type:pat",
|
|
203
|
+
grant_type="urn:ietf:params:oauth:grant-type:token-exchange",
|
|
204
|
+
scope="credentials",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def refresh_credentials(self) -> None:
|
|
208
|
+
"""
|
|
209
|
+
Refresh authentication tokens using OAuth2 flows.
|
|
210
|
+
"""
|
|
211
|
+
if not self.refreshable_auth_types():
|
|
212
|
+
raise ClientError(f"Auth type {self._auth_type} does not support refresh.")
|
|
213
|
+
|
|
214
|
+
# Get credentials and configuration
|
|
215
|
+
creds = configurator.get_config_creds()
|
|
216
|
+
|
|
217
|
+
# Get token refresh from creds
|
|
218
|
+
if (url := creds.get(ConfigurationVars.OAUTH2_TOKEN_ENDPOINT.value)) is None:
|
|
219
|
+
url = self._get_refresh_endpoint()
|
|
220
|
+
url = self._sanitize_endpoint(url)
|
|
221
|
+
|
|
222
|
+
# Execute the appropriate auth flow
|
|
223
|
+
response = self._evaluate_auth_flow(url, creds)
|
|
224
|
+
|
|
225
|
+
# Raise an error if the response indicates failure
|
|
226
|
+
response.raise_for_status()
|
|
227
|
+
|
|
228
|
+
# Export new credentials to file
|
|
229
|
+
self._export_new_creds(response.json())
|
|
230
|
+
|
|
231
|
+
configurator.reload_credentials()
|
|
232
|
+
|
|
233
|
+
def evaluate_refresh(self) -> bool:
|
|
234
|
+
"""
|
|
235
|
+
Check if token refresh should be attempted.
|
|
236
|
+
|
|
237
|
+
Returns
|
|
238
|
+
-------
|
|
239
|
+
bool
|
|
240
|
+
True if token refresh is applicable, otherwise False.
|
|
241
|
+
"""
|
|
242
|
+
try:
|
|
243
|
+
self.refresh_credentials()
|
|
244
|
+
return True
|
|
245
|
+
except Exception:
|
|
246
|
+
if not configurator.eval_retry():
|
|
247
|
+
warn(
|
|
248
|
+
"Failed to refresh credentials after retry"
|
|
249
|
+
" (checked credentials from file and env)."
|
|
250
|
+
" Please check your credentials"
|
|
251
|
+
" and make sure they are up to date."
|
|
252
|
+
" (refresh tokens, password, etc.)."
|
|
253
|
+
)
|
|
254
|
+
return False
|
|
255
|
+
return self.evaluate_refresh()
|
|
256
|
+
|
|
257
|
+
def _get_refresh_endpoint(self) -> str:
|
|
258
|
+
"""
|
|
259
|
+
Discover OAuth2 token endpoint from issuer well-known configuration.
|
|
260
|
+
|
|
261
|
+
Queries /.well-known/openid-configuration to extract token_endpoint for
|
|
262
|
+
credential refresh operations.
|
|
263
|
+
|
|
264
|
+
Parameters
|
|
265
|
+
----------
|
|
266
|
+
creds : dict
|
|
267
|
+
Available credential values.
|
|
268
|
+
|
|
269
|
+
Returns
|
|
270
|
+
-------
|
|
271
|
+
str
|
|
272
|
+
Token endpoint URL for credential refresh.
|
|
273
|
+
"""
|
|
274
|
+
config = configurator.get_configuration()
|
|
275
|
+
|
|
276
|
+
# Get issuer endpoint
|
|
277
|
+
if (endpoint_issuer := config.get(ConfigurationVars.DHCORE_ISSUER.value)) is None:
|
|
278
|
+
raise ClientError("Issuer endpoint not set.")
|
|
279
|
+
|
|
280
|
+
# Standard issuer endpoint path
|
|
281
|
+
url = endpoint_issuer + "/.well-known/openid-configuration"
|
|
282
|
+
url = self._sanitize_endpoint(url)
|
|
283
|
+
|
|
284
|
+
# Call issuer to get refresh endpoint
|
|
285
|
+
r = request("GET", url, timeout=60)
|
|
286
|
+
r.raise_for_status()
|
|
287
|
+
return r.json().get("token_endpoint")
|
|
288
|
+
|
|
289
|
+
def _call_refresh_endpoint(
|
|
290
|
+
self,
|
|
291
|
+
url: str,
|
|
292
|
+
**kwargs,
|
|
293
|
+
) -> Response:
|
|
294
|
+
"""
|
|
295
|
+
Make OAuth2 token refresh request.
|
|
296
|
+
|
|
297
|
+
Sends POST request with form-encoded payload using required OAuth2
|
|
298
|
+
content type and 60-second timeout.
|
|
299
|
+
|
|
300
|
+
Parameters
|
|
301
|
+
----------
|
|
302
|
+
url : str
|
|
303
|
+
Token endpoint URL.
|
|
304
|
+
**kwargs : dict
|
|
305
|
+
Token request parameters (grant_type, client_id, etc.).
|
|
306
|
+
|
|
307
|
+
Returns
|
|
308
|
+
-------
|
|
309
|
+
Response
|
|
310
|
+
Raw HTTP response for caller handling.
|
|
311
|
+
"""
|
|
312
|
+
# Send request to get new access token
|
|
313
|
+
payload = {**kwargs}
|
|
314
|
+
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
315
|
+
return request(
|
|
316
|
+
"POST",
|
|
317
|
+
url,
|
|
318
|
+
data=payload,
|
|
319
|
+
headers=headers,
|
|
320
|
+
timeout=DEFAULT_TIMEOUT,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
def _eval_auth_type(self, creds: dict) -> str | None:
|
|
324
|
+
"""
|
|
325
|
+
Determine authentication type from available credentials.
|
|
326
|
+
|
|
327
|
+
Evaluates in priority order: EXCHANGE (personal access token), OAUTH2
|
|
328
|
+
(access + refresh tokens), ACCESS_TOKEN (access only), BASIC (username + password).
|
|
329
|
+
|
|
330
|
+
Parameters
|
|
331
|
+
----------
|
|
332
|
+
creds : dict
|
|
333
|
+
Available credential values.
|
|
334
|
+
|
|
335
|
+
Returns
|
|
336
|
+
-------
|
|
337
|
+
str or None
|
|
338
|
+
Authentication type from AuthType enum, or None if no valid credentials.
|
|
339
|
+
"""
|
|
340
|
+
if creds[CredentialsVars.DHCORE_PERSONAL_ACCESS_TOKEN.value] is not None:
|
|
341
|
+
return AuthType.EXCHANGE.value
|
|
342
|
+
if (
|
|
343
|
+
creds[CredentialsVars.DHCORE_ACCESS_TOKEN.value] is not None
|
|
344
|
+
and creds[CredentialsVars.DHCORE_REFRESH_TOKEN.value] is not None
|
|
345
|
+
):
|
|
346
|
+
return AuthType.OAUTH2.value
|
|
347
|
+
if creds[CredentialsVars.DHCORE_ACCESS_TOKEN.value] is not None:
|
|
348
|
+
return AuthType.ACCESS_TOKEN.value
|
|
349
|
+
if (
|
|
350
|
+
creds[CredentialsVars.DHCORE_USER.value] is not None
|
|
351
|
+
and creds[CredentialsVars.DHCORE_PASSWORD.value] is not None
|
|
352
|
+
):
|
|
353
|
+
return AuthType.BASIC.value
|
|
354
|
+
return None
|
|
355
|
+
|
|
356
|
+
def _export_new_creds(self, response: dict) -> None:
|
|
357
|
+
"""
|
|
358
|
+
Save refreshed credentials and switch to file-based storage.
|
|
359
|
+
|
|
360
|
+
Persists new tokens (access_token, refresh_token, etc.) to configuration
|
|
361
|
+
file and switches credential origin to file storage.
|
|
362
|
+
|
|
363
|
+
Parameters
|
|
364
|
+
----------
|
|
365
|
+
response : dict
|
|
366
|
+
OAuth2 token response with new credentials.
|
|
367
|
+
"""
|
|
368
|
+
keys_to_prefix = [
|
|
369
|
+
CredentialsVars.DHCORE_REFRESH_TOKEN.value,
|
|
370
|
+
CredentialsVars.DHCORE_ACCESS_TOKEN.value,
|
|
371
|
+
ConfigurationVars.DHCORE_CLIENT_ID.value,
|
|
372
|
+
ConfigurationVars.DHCORE_ISSUER.value,
|
|
373
|
+
ConfigurationVars.OAUTH2_TOKEN_ENDPOINT.value,
|
|
374
|
+
]
|
|
375
|
+
for key in keys_to_prefix:
|
|
376
|
+
if key == ConfigurationVars.OAUTH2_TOKEN_ENDPOINT.value:
|
|
377
|
+
prefix = "oauth2_"
|
|
378
|
+
else:
|
|
379
|
+
prefix = "dhcore_"
|
|
380
|
+
key = key.lower()
|
|
381
|
+
if key.removeprefix(prefix) in response:
|
|
382
|
+
response[key] = response.pop(key.removeprefix(prefix))
|
|
383
|
+
configurator.write_file(response)
|
|
384
|
+
|
|
385
|
+
def _validate(self) -> None:
|
|
386
|
+
"""
|
|
387
|
+
Validate if all required keys are present in the configuration.
|
|
388
|
+
"""
|
|
389
|
+
required_keys = [ConfigurationVars.DHCORE_ENDPOINT.value]
|
|
390
|
+
current_keys = configurator.get_config_creds()
|
|
391
|
+
for key in required_keys:
|
|
392
|
+
if current_keys.get(key) is None:
|
|
393
|
+
raise ClientError(f"Required configuration key '{key}' is missing.")
|
|
394
|
+
|
|
395
|
+
###############################
|
|
396
|
+
# Utility methods
|
|
397
|
+
###############################
|
|
398
|
+
|
|
399
|
+
def get_credentials_and_config(self) -> dict:
|
|
400
|
+
"""
|
|
401
|
+
Get current authentication credentials and configuration.
|
|
402
|
+
|
|
403
|
+
Returns
|
|
404
|
+
-------
|
|
405
|
+
dict
|
|
406
|
+
Current authentication credentials and configuration.
|
|
407
|
+
"""
|
|
408
|
+
return configurator.get_config_creds()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2025 DSLab - Fondazione Bruno Kessler
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
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
|
+
|
|
21
|
+
class ApiCategories(Enum):
|
|
22
|
+
"""
|
|
23
|
+
API categories.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
BASE = "base"
|
|
27
|
+
CONTEXT = "context"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BackendOperations(Enum):
|
|
31
|
+
"""
|
|
32
|
+
Backend operations.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
CREATE = "create"
|
|
36
|
+
READ = "read"
|
|
37
|
+
READ_ALL_VERSIONS = "read_all_versions"
|
|
38
|
+
UPDATE = "update"
|
|
39
|
+
DELETE = "delete"
|
|
40
|
+
DELETE_ALL_VERSIONS = "delete_all_versions"
|
|
41
|
+
LIST = "list"
|
|
42
|
+
LIST_FIRST = "list_first"
|
|
43
|
+
STOP = "stop"
|
|
44
|
+
RESUME = "resume"
|
|
45
|
+
DATA = "data"
|
|
46
|
+
FILES = "files"
|
|
47
|
+
LOGS = "logs"
|
|
48
|
+
SEARCH = "search"
|
|
49
|
+
SHARE = "share"
|
|
50
|
+
METRICS = "metrics"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2025 DSLab - Fondazione Bruno Kessler
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HeaderManager:
|
|
9
|
+
"""
|
|
10
|
+
Manages HTTP headers for DHCore client requests.
|
|
11
|
+
|
|
12
|
+
Provides utilities for setting and managing common HTTP headers
|
|
13
|
+
like Content-Type for JSON requests.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def ensure_headers(**kwargs) -> dict:
|
|
18
|
+
"""
|
|
19
|
+
Initialize headers dictionary in kwargs.
|
|
20
|
+
|
|
21
|
+
Ensures parameter dictionary has 'headers' key for HTTP headers,
|
|
22
|
+
guaranteeing consistent structure for all parameter building methods.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
**kwargs : dict
|
|
27
|
+
Keyword arguments to format. May be empty or contain various
|
|
28
|
+
parameters for API operations.
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
dict
|
|
33
|
+
Dictionary with guaranteed 'headers' key containing
|
|
34
|
+
empty dict if not already present.
|
|
35
|
+
"""
|
|
36
|
+
if "headers" not in kwargs:
|
|
37
|
+
kwargs["headers"] = {}
|
|
38
|
+
return kwargs
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def set_json_content_type(**kwargs) -> dict:
|
|
42
|
+
"""
|
|
43
|
+
Set Content-Type header to application/json.
|
|
44
|
+
|
|
45
|
+
Ensures that the 'Content-Type' header is set to 'application/json'
|
|
46
|
+
for requests that require JSON payloads.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
**kwargs : dict
|
|
51
|
+
Keyword arguments to format. May be empty or contain various
|
|
52
|
+
parameters for API operations.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
dict
|
|
57
|
+
Dictionary with 'Content-Type' header set to 'application/json'.
|
|
58
|
+
"""
|
|
59
|
+
kwargs = HeaderManager.ensure_headers(**kwargs)
|
|
60
|
+
kwargs["headers"]["Content-Type"] = "application/json"
|
|
61
|
+
return kwargs
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2025 DSLab - Fondazione Bruno Kessler
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from requests import request
|
|
8
|
+
|
|
9
|
+
from digitalhub.stores.client.configurator import ClientConfigurator
|
|
10
|
+
from digitalhub.stores.client.response_processor import ResponseProcessor
|
|
11
|
+
from digitalhub.utils.exceptions import BackendError
|
|
12
|
+
|
|
13
|
+
# Default timeout for requests (in seconds)
|
|
14
|
+
DEFAULT_TIMEOUT = 60
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HttpRequestHandler:
|
|
18
|
+
"""
|
|
19
|
+
Handles HTTP request execution for DHCore client.
|
|
20
|
+
|
|
21
|
+
Encapsulates all HTTP communication logic including request execution,
|
|
22
|
+
automatic token refresh on authentication failures, and response processing.
|
|
23
|
+
Works in coordination with configurator for authentication and response
|
|
24
|
+
processor for parsing.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self._configurator = ClientConfigurator()
|
|
29
|
+
self._response_processor = ResponseProcessor()
|
|
30
|
+
|
|
31
|
+
def prepare_request(self, method: str, api: str, **kwargs) -> dict:
|
|
32
|
+
"""
|
|
33
|
+
Execute API call with full URL construction and authentication.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
method : str
|
|
38
|
+
HTTP method type (GET, POST, PUT, DELETE, etc.).
|
|
39
|
+
api : str
|
|
40
|
+
API endpoint path to call.
|
|
41
|
+
**kwargs : dict
|
|
42
|
+
Additional HTTP request arguments.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
dict
|
|
47
|
+
Response from the API call.
|
|
48
|
+
"""
|
|
49
|
+
full_kwargs = self._set_auth(**kwargs)
|
|
50
|
+
url = self._build_url(api)
|
|
51
|
+
return self._execute_request(method, url, **full_kwargs)
|
|
52
|
+
|
|
53
|
+
def _execute_request(
|
|
54
|
+
self,
|
|
55
|
+
method: str,
|
|
56
|
+
url: str,
|
|
57
|
+
**kwargs,
|
|
58
|
+
) -> dict:
|
|
59
|
+
"""
|
|
60
|
+
Execute HTTP request with automatic handling.
|
|
61
|
+
|
|
62
|
+
Sends HTTP request with authentication, handles token refresh on 401 errors,
|
|
63
|
+
validates API version compatibility, and parses response. Uses 60-second
|
|
64
|
+
timeout by default.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
method : str
|
|
69
|
+
HTTP method (GET, POST, PUT, DELETE, etc.).
|
|
70
|
+
url : str
|
|
71
|
+
Complete URL to request.
|
|
72
|
+
**kwargs : dict
|
|
73
|
+
Additional HTTP request arguments (headers, params, data, etc.).
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
dict
|
|
78
|
+
Parsed response body as dictionary.
|
|
79
|
+
"""
|
|
80
|
+
# Execute HTTP request
|
|
81
|
+
response = request(method, url, timeout=DEFAULT_TIMEOUT, **kwargs)
|
|
82
|
+
|
|
83
|
+
# Process response (version check, error parsing, dictify)
|
|
84
|
+
try:
|
|
85
|
+
return self._response_processor.process(response)
|
|
86
|
+
except BackendError as e:
|
|
87
|
+
# Handle authentication errors with token refresh
|
|
88
|
+
if response.status_code == 401 and self._configurator.evaluate_refresh():
|
|
89
|
+
kwargs = self._configurator.get_auth_parameters(kwargs)
|
|
90
|
+
return self._execute_request(method, url, **kwargs)
|
|
91
|
+
raise e
|
|
92
|
+
|
|
93
|
+
def _set_auth(self, **kwargs) -> dict:
|
|
94
|
+
"""
|
|
95
|
+
Prepare kwargs with authentication parameters.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
**kwargs : dict
|
|
100
|
+
Request parameters to augment with authentication.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
dict
|
|
105
|
+
kwargs enhanced with authentication parameters.
|
|
106
|
+
"""
|
|
107
|
+
return self._configurator.get_auth_parameters(kwargs)
|
|
108
|
+
|
|
109
|
+
def _build_url(self, api: str) -> str:
|
|
110
|
+
"""
|
|
111
|
+
Build complete URL for API call.
|
|
112
|
+
|
|
113
|
+
Combines configured endpoint with API path, automatically removing
|
|
114
|
+
leading slashes for proper URL construction.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
api : str
|
|
119
|
+
API endpoint path. Leading slashes are automatically handled.
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
str
|
|
124
|
+
Complete URL for the API call.
|
|
125
|
+
"""
|
|
126
|
+
endpoint = self._configurator.get_endpoint()
|
|
127
|
+
return f"{endpoint}/{api.removeprefix('/')}"
|
|
128
|
+
|
|
129
|
+
###############################
|
|
130
|
+
# Utility methods
|
|
131
|
+
###############################
|
|
132
|
+
|
|
133
|
+
def refresh_token(self) -> None:
|
|
134
|
+
"""
|
|
135
|
+
Manually trigger OAuth2 token refresh.
|
|
136
|
+
"""
|
|
137
|
+
self._configurator.evaluate_refresh()
|
|
138
|
+
|
|
139
|
+
def get_credentials_and_config(self) -> dict:
|
|
140
|
+
"""
|
|
141
|
+
Get current authentication credentials and configuration.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
dict
|
|
146
|
+
Current authentication credentials and configuration.
|
|
147
|
+
"""
|
|
148
|
+
creds = self._configurator.get_credentials_and_config()
|
|
149
|
+
|
|
150
|
+
# Test connection to ensure validity
|
|
151
|
+
self.prepare_request("GET", "/api/auth")
|
|
152
|
+
return creds
|