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
|
@@ -1,675 +0,0 @@
|
|
|
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
|
-
|
|
9
|
-
from requests import request
|
|
10
|
-
|
|
11
|
-
from digitalhub.stores.client.dhcore.enums import AuthType
|
|
12
|
-
from digitalhub.stores.credentials.configurator import Configurator
|
|
13
|
-
from digitalhub.stores.credentials.enums import CredsEnvVar
|
|
14
|
-
from digitalhub.stores.credentials.handler import creds_handler
|
|
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
|
-
class ClientDHCoreConfigurator(Configurator):
|
|
24
|
-
"""
|
|
25
|
-
Configurator object used to configure the client.
|
|
26
|
-
|
|
27
|
-
The configurator starts reading the credentials from the
|
|
28
|
-
environment and from the ini file and stores them into the
|
|
29
|
-
creds_handler object.
|
|
30
|
-
|
|
31
|
-
While reading the credentials from the two sources (environment and file),
|
|
32
|
-
the configurator evaluate if the required keys are present in both sources.
|
|
33
|
-
If the required keys are not present in both sources, the configurator
|
|
34
|
-
will rise an error, otherwise decide which source to use.
|
|
35
|
-
|
|
36
|
-
Once the credentials are read, the configurator check the current profile
|
|
37
|
-
name from the ini file, and set it. The default one is __default. The
|
|
38
|
-
profile is used to discriminate a set of credentials inside the ini file.
|
|
39
|
-
|
|
40
|
-
The configurator finally set the authentication type based on the credentials.
|
|
41
|
-
The logic is the following:
|
|
42
|
-
|
|
43
|
-
1. Check for a personal access token. Use it immediately to
|
|
44
|
-
require a timed access token in an exchange endpoint.
|
|
45
|
-
Switche then the origin to file and .
|
|
46
|
-
Set the auth type to EXCHANGE.
|
|
47
|
-
2. Check for an access token and a refresh token.
|
|
48
|
-
Set the auth type to OAUTH2.
|
|
49
|
-
3. Check for username and password.
|
|
50
|
-
Set the auth type to BASIC.
|
|
51
|
-
4. If none of the above is true, leave the auth type to None.
|
|
52
|
-
"""
|
|
53
|
-
|
|
54
|
-
keys = [*list_enum(CredsEnvVar)]
|
|
55
|
-
required_keys = [CredsEnvVar.DHCORE_ENDPOINT.value]
|
|
56
|
-
keys_to_unprefix = [
|
|
57
|
-
CredsEnvVar.DHCORE_REFRESH_TOKEN.value,
|
|
58
|
-
CredsEnvVar.DHCORE_ACCESS_TOKEN.value,
|
|
59
|
-
CredsEnvVar.DHCORE_ISSUER.value,
|
|
60
|
-
CredsEnvVar.DHCORE_CLIENT_ID.value,
|
|
61
|
-
]
|
|
62
|
-
|
|
63
|
-
def __init__(self) -> None:
|
|
64
|
-
"""
|
|
65
|
-
Initialize the DHCore configurator.
|
|
66
|
-
|
|
67
|
-
Sets up the configurator by calling the parent constructor and
|
|
68
|
-
initializing the authentication type evaluation process.
|
|
69
|
-
|
|
70
|
-
Returns
|
|
71
|
-
-------
|
|
72
|
-
None
|
|
73
|
-
"""
|
|
74
|
-
super().__init__()
|
|
75
|
-
self._auth_type: str | None = None
|
|
76
|
-
self.set_auth_type()
|
|
77
|
-
|
|
78
|
-
##############################
|
|
79
|
-
# Credentials methods
|
|
80
|
-
##############################
|
|
81
|
-
|
|
82
|
-
def load_env_vars(self) -> None:
|
|
83
|
-
"""
|
|
84
|
-
Load credentials from environment variables.
|
|
85
|
-
|
|
86
|
-
Retrieves DHCore credentials from environment variables, sanitizes
|
|
87
|
-
them (particularly endpoint URLs), and stores them in the credentials
|
|
88
|
-
handler for the environment origin.
|
|
89
|
-
|
|
90
|
-
Returns
|
|
91
|
-
-------
|
|
92
|
-
None
|
|
93
|
-
|
|
94
|
-
Notes
|
|
95
|
-
-----
|
|
96
|
-
This method sanitizes endpoint and issuer URLs to ensure they have
|
|
97
|
-
proper schemes and removes trailing slashes.
|
|
98
|
-
"""
|
|
99
|
-
env_creds = self._creds_handler.load_from_env(self.keys)
|
|
100
|
-
env_creds = self._sanitize_env_vars(env_creds)
|
|
101
|
-
self._creds_handler.set_credentials(self._env, env_creds)
|
|
102
|
-
|
|
103
|
-
def _sanitize_env_vars(self, creds: dict) -> dict:
|
|
104
|
-
"""
|
|
105
|
-
Sanitize credentials loaded from environment variables.
|
|
106
|
-
|
|
107
|
-
Validates and normalizes endpoint and issuer URLs from environment
|
|
108
|
-
variables. Ensures URLs have proper schemes and removes trailing slashes.
|
|
109
|
-
|
|
110
|
-
Parameters
|
|
111
|
-
----------
|
|
112
|
-
creds : dict
|
|
113
|
-
Raw credentials dictionary loaded from environment variables.
|
|
114
|
-
|
|
115
|
-
Returns
|
|
116
|
-
-------
|
|
117
|
-
dict
|
|
118
|
-
Sanitized credentials dictionary with normalized URLs.
|
|
119
|
-
|
|
120
|
-
Raises
|
|
121
|
-
------
|
|
122
|
-
ClientError
|
|
123
|
-
If endpoint or issuer URLs have invalid schemes.
|
|
124
|
-
|
|
125
|
-
Notes
|
|
126
|
-
-----
|
|
127
|
-
Environment variables are expected to have the full "DHCORE_" prefix
|
|
128
|
-
for issuer endpoints.
|
|
129
|
-
"""
|
|
130
|
-
creds[CredsEnvVar.DHCORE_ENDPOINT.value] = self._sanitize_endpoint(creds[CredsEnvVar.DHCORE_ENDPOINT.value])
|
|
131
|
-
creds[CredsEnvVar.DHCORE_ISSUER.value] = self._sanitize_endpoint(creds[CredsEnvVar.DHCORE_ISSUER.value])
|
|
132
|
-
return creds
|
|
133
|
-
|
|
134
|
-
def load_file_vars(self) -> None:
|
|
135
|
-
"""
|
|
136
|
-
Load credentials from configuration file.
|
|
137
|
-
|
|
138
|
-
Retrieves DHCore credentials from the .dhcore.ini file, handles
|
|
139
|
-
compatibility with CLI format (keys without DHCORE_ prefix), and
|
|
140
|
-
falls back to environment variables for missing endpoint and
|
|
141
|
-
personal access token values.
|
|
142
|
-
|
|
143
|
-
Returns
|
|
144
|
-
-------
|
|
145
|
-
None
|
|
146
|
-
|
|
147
|
-
Notes
|
|
148
|
-
-----
|
|
149
|
-
This method handles the case where:
|
|
150
|
-
- Endpoint might not be present in file response, falls back to env
|
|
151
|
-
- Personal access token might not be present, falls back to env
|
|
152
|
-
- File format uses keys without "DHCORE_" prefix for compatibility
|
|
153
|
-
"""
|
|
154
|
-
keys = [*self._remove_prefix_dhcore()]
|
|
155
|
-
file_creds = self._creds_handler.load_from_file(keys)
|
|
156
|
-
env_creds = self._creds_handler.load_from_env(self.keys)
|
|
157
|
-
|
|
158
|
-
# Because in the response there is no endpoint
|
|
159
|
-
if file_creds[CredsEnvVar.DHCORE_ENDPOINT.value] is None:
|
|
160
|
-
file_creds[CredsEnvVar.DHCORE_ENDPOINT.value] = env_creds.get(CredsEnvVar.DHCORE_ENDPOINT.value)
|
|
161
|
-
|
|
162
|
-
# Because in the response there is no personal access token
|
|
163
|
-
if file_creds[CredsEnvVar.DHCORE_PERSONAL_ACCESS_TOKEN.value] is None:
|
|
164
|
-
file_creds[CredsEnvVar.DHCORE_PERSONAL_ACCESS_TOKEN.value] = env_creds.get(
|
|
165
|
-
CredsEnvVar.DHCORE_PERSONAL_ACCESS_TOKEN.value
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
file_creds = self._sanitize_file_vars(file_creds)
|
|
169
|
-
self._creds_handler.set_credentials(self._file, file_creds)
|
|
170
|
-
|
|
171
|
-
def _sanitize_file_vars(self, creds: dict) -> dict:
|
|
172
|
-
"""
|
|
173
|
-
Sanitize credentials loaded from configuration file.
|
|
174
|
-
|
|
175
|
-
Handles the different key formats used in configuration files compared
|
|
176
|
-
to environment variables. File format omits "DHCORE_" prefix for
|
|
177
|
-
certain keys for CLI compatibility.
|
|
178
|
-
|
|
179
|
-
Parameters
|
|
180
|
-
----------
|
|
181
|
-
creds : dict
|
|
182
|
-
Raw credentials dictionary loaded from configuration file.
|
|
183
|
-
|
|
184
|
-
Returns
|
|
185
|
-
-------
|
|
186
|
-
dict
|
|
187
|
-
Sanitized credentials dictionary with standardized key names
|
|
188
|
-
and normalized URLs, filtered to include only valid keys.
|
|
189
|
-
|
|
190
|
-
Raises
|
|
191
|
-
------
|
|
192
|
-
ClientError
|
|
193
|
-
If endpoint or issuer URLs have invalid schemes.
|
|
194
|
-
|
|
195
|
-
Notes
|
|
196
|
-
-----
|
|
197
|
-
File format expects these keys without "DHCORE_" prefix:
|
|
198
|
-
- issuer, client_id, access_token, refresh_token
|
|
199
|
-
But uses full names for: endpoint, user, password, personal_access_token
|
|
200
|
-
"""
|
|
201
|
-
creds[CredsEnvVar.DHCORE_ENDPOINT.value] = self._sanitize_endpoint(creds[CredsEnvVar.DHCORE_ENDPOINT.value])
|
|
202
|
-
creds[CredsEnvVar.DHCORE_ISSUER.value] = self._sanitize_endpoint(
|
|
203
|
-
creds[CredsEnvVar.DHCORE_ISSUER.value.removeprefix("DHCORE_")]
|
|
204
|
-
)
|
|
205
|
-
creds[CredsEnvVar.DHCORE_REFRESH_TOKEN.value] = creds[
|
|
206
|
-
CredsEnvVar.DHCORE_REFRESH_TOKEN.value.removeprefix("DHCORE_")
|
|
207
|
-
]
|
|
208
|
-
creds[CredsEnvVar.DHCORE_ACCESS_TOKEN.value] = creds[
|
|
209
|
-
CredsEnvVar.DHCORE_ACCESS_TOKEN.value.removeprefix("DHCORE_")
|
|
210
|
-
]
|
|
211
|
-
creds[CredsEnvVar.DHCORE_CLIENT_ID.value] = creds[CredsEnvVar.DHCORE_CLIENT_ID.value.removeprefix("DHCORE_")]
|
|
212
|
-
return {k: v for k, v in creds.items() if k in self.keys}
|
|
213
|
-
|
|
214
|
-
@staticmethod
|
|
215
|
-
def _sanitize_endpoint(endpoint: str | None = None) -> str | None:
|
|
216
|
-
"""
|
|
217
|
-
Sanitize and validate endpoint URL.
|
|
218
|
-
|
|
219
|
-
Validates that the endpoint URL has a proper HTTP/HTTPS scheme,
|
|
220
|
-
trims whitespace, and removes trailing slashes for consistency.
|
|
221
|
-
|
|
222
|
-
Parameters
|
|
223
|
-
----------
|
|
224
|
-
endpoint : str, optional
|
|
225
|
-
The endpoint URL to sanitize. If None, returns None.
|
|
226
|
-
|
|
227
|
-
Returns
|
|
228
|
-
-------
|
|
229
|
-
str or None
|
|
230
|
-
The sanitized endpoint URL with trailing slash removed,
|
|
231
|
-
or None if input was None.
|
|
232
|
-
|
|
233
|
-
Raises
|
|
234
|
-
------
|
|
235
|
-
ClientError
|
|
236
|
-
If the endpoint does not start with http:// or https://.
|
|
237
|
-
|
|
238
|
-
Notes
|
|
239
|
-
-----
|
|
240
|
-
This method ensures endpoint URLs are properly formatted for
|
|
241
|
-
HTTP requests and prevents common URL formatting issues.
|
|
242
|
-
"""
|
|
243
|
-
if endpoint is None:
|
|
244
|
-
return
|
|
245
|
-
if not has_remote_scheme(endpoint):
|
|
246
|
-
raise ClientError("Invalid endpoint scheme. Must start with http:// or https://.")
|
|
247
|
-
|
|
248
|
-
endpoint = endpoint.strip()
|
|
249
|
-
return endpoint.removesuffix("/")
|
|
250
|
-
|
|
251
|
-
def get_endpoint(self) -> str:
|
|
252
|
-
"""
|
|
253
|
-
Get the configured DHCore backend endpoint.
|
|
254
|
-
|
|
255
|
-
Retrieves the DHCore endpoint URL from the current credential source
|
|
256
|
-
(environment or file based on current origin).
|
|
257
|
-
|
|
258
|
-
Returns
|
|
259
|
-
-------
|
|
260
|
-
str
|
|
261
|
-
The DHCore backend endpoint URL.
|
|
262
|
-
|
|
263
|
-
Raises
|
|
264
|
-
------
|
|
265
|
-
KeyError
|
|
266
|
-
If the endpoint is not configured in the current credential source.
|
|
267
|
-
|
|
268
|
-
Notes
|
|
269
|
-
-----
|
|
270
|
-
The endpoint returned is already sanitized and validated during
|
|
271
|
-
the credential loading process.
|
|
272
|
-
"""
|
|
273
|
-
creds = self._creds_handler.get_credentials(self._origin)
|
|
274
|
-
return creds[CredsEnvVar.DHCORE_ENDPOINT.value]
|
|
275
|
-
|
|
276
|
-
##############################
|
|
277
|
-
# Origin methods
|
|
278
|
-
##############################
|
|
279
|
-
|
|
280
|
-
def change_origin(self) -> None:
|
|
281
|
-
"""
|
|
282
|
-
Change the credentials origin and re-evaluate authentication type.
|
|
283
|
-
|
|
284
|
-
Switches the credential source (between environment and file) and
|
|
285
|
-
re-evaluates the authentication type based on the new credential set.
|
|
286
|
-
This is typically called when the current credential source fails
|
|
287
|
-
or when switching contexts.
|
|
288
|
-
|
|
289
|
-
Returns
|
|
290
|
-
-------
|
|
291
|
-
None
|
|
292
|
-
|
|
293
|
-
Notes
|
|
294
|
-
-----
|
|
295
|
-
This method extends the parent class behavior by also re-evaluating
|
|
296
|
-
the authentication type, which may change based on different
|
|
297
|
-
credentials available in the new source.
|
|
298
|
-
"""
|
|
299
|
-
super().change_origin()
|
|
300
|
-
|
|
301
|
-
# Re-evaluate the auth type
|
|
302
|
-
self.set_auth_type()
|
|
303
|
-
|
|
304
|
-
##############################
|
|
305
|
-
# Auth methods
|
|
306
|
-
##############################
|
|
307
|
-
|
|
308
|
-
def set_auth_type(self) -> None:
|
|
309
|
-
"""
|
|
310
|
-
Evaluate and set the authentication type from available credentials.
|
|
311
|
-
|
|
312
|
-
Analyzes the available credentials and determines the appropriate
|
|
313
|
-
authentication method based on the following priority:
|
|
314
|
-
1. EXCHANGE - Personal access token available
|
|
315
|
-
2. OAUTH2 - Access token and refresh token available
|
|
316
|
-
3. ACCESS_TOKEN - Only access token available
|
|
317
|
-
4. BASIC - Username and password available
|
|
318
|
-
5. None - No valid credentials found
|
|
319
|
-
|
|
320
|
-
For EXCHANGE authentication, automatically performs token exchange
|
|
321
|
-
and switches to file-based credential storage.
|
|
322
|
-
|
|
323
|
-
Returns
|
|
324
|
-
-------
|
|
325
|
-
None
|
|
326
|
-
|
|
327
|
-
Notes
|
|
328
|
-
-----
|
|
329
|
-
When EXCHANGE authentication is detected, this method automatically:
|
|
330
|
-
- Performs credential refresh to exchange the personal access token
|
|
331
|
-
- Changes origin to file-based storage for the new tokens
|
|
332
|
-
- Updates the authentication type accordingly
|
|
333
|
-
"""
|
|
334
|
-
creds = creds_handler.get_credentials(self._origin)
|
|
335
|
-
self._auth_type = self._eval_auth_type(creds)
|
|
336
|
-
# If we have an exchange token, we need to get a new access token.
|
|
337
|
-
# Therefore, we change the origin to file, where the refresh token is written.
|
|
338
|
-
# We also try to fetch the PAT from both env and file
|
|
339
|
-
if self._auth_type == AuthType.EXCHANGE.value:
|
|
340
|
-
self.refresh_credentials(change_origin=True)
|
|
341
|
-
# Just to ensure we get the right source from file
|
|
342
|
-
self.change_to_file()
|
|
343
|
-
|
|
344
|
-
def refreshable_auth_types(self) -> bool:
|
|
345
|
-
"""
|
|
346
|
-
Check if the current authentication type supports token refresh.
|
|
347
|
-
|
|
348
|
-
Determines whether the current authentication method supports
|
|
349
|
-
automatic token refresh capabilities.
|
|
350
|
-
|
|
351
|
-
Returns
|
|
352
|
-
-------
|
|
353
|
-
bool
|
|
354
|
-
True if the authentication type supports refresh (OAUTH2 or EXCHANGE),
|
|
355
|
-
False otherwise (BASIC or ACCESS_TOKEN).
|
|
356
|
-
|
|
357
|
-
Notes
|
|
358
|
-
-----
|
|
359
|
-
Only OAUTH2 and EXCHANGE authentication types support refresh:
|
|
360
|
-
- OAUTH2: Uses refresh token to get new access tokens
|
|
361
|
-
- EXCHANGE: Uses personal access token for token exchange
|
|
362
|
-
- BASIC and ACCESS_TOKEN do not support refresh
|
|
363
|
-
"""
|
|
364
|
-
return self._auth_type in [AuthType.OAUTH2.value, AuthType.EXCHANGE.value]
|
|
365
|
-
|
|
366
|
-
def get_auth_parameters(self, kwargs: dict) -> dict:
|
|
367
|
-
"""
|
|
368
|
-
Add authentication parameters to HTTP request arguments.
|
|
369
|
-
|
|
370
|
-
Modifies the provided kwargs dictionary to include the appropriate
|
|
371
|
-
authentication headers or parameters based on the current authentication
|
|
372
|
-
type and available credentials.
|
|
373
|
-
|
|
374
|
-
Parameters
|
|
375
|
-
----------
|
|
376
|
-
kwargs : dict
|
|
377
|
-
HTTP request keyword arguments to be modified with authentication.
|
|
378
|
-
|
|
379
|
-
Returns
|
|
380
|
-
-------
|
|
381
|
-
dict
|
|
382
|
-
The modified kwargs dictionary with authentication parameters added.
|
|
383
|
-
|
|
384
|
-
Notes
|
|
385
|
-
-----
|
|
386
|
-
Authentication is added based on auth type:
|
|
387
|
-
- OAUTH2/EXCHANGE/ACCESS_TOKEN: Adds Authorization Bearer header
|
|
388
|
-
- BASIC: Adds auth tuple with username/password
|
|
389
|
-
- None: No authentication added
|
|
390
|
-
|
|
391
|
-
The method assumes that:
|
|
392
|
-
- Authentication type has been properly set
|
|
393
|
-
- For EXCHANGE type, refresh token has been obtained
|
|
394
|
-
- Required credentials are available for the current auth type
|
|
395
|
-
"""
|
|
396
|
-
creds = creds_handler.get_credentials(self._origin)
|
|
397
|
-
if self._auth_type in (
|
|
398
|
-
AuthType.EXCHANGE.value,
|
|
399
|
-
AuthType.OAUTH2.value,
|
|
400
|
-
AuthType.ACCESS_TOKEN.value,
|
|
401
|
-
):
|
|
402
|
-
access_token = creds[CredsEnvVar.DHCORE_ACCESS_TOKEN.value]
|
|
403
|
-
if "headers" not in kwargs:
|
|
404
|
-
kwargs["headers"] = {}
|
|
405
|
-
kwargs["headers"]["Authorization"] = f"Bearer {access_token}"
|
|
406
|
-
elif self._auth_type == AuthType.BASIC.value:
|
|
407
|
-
user = creds[CredsEnvVar.DHCORE_USER.value]
|
|
408
|
-
password = creds[CredsEnvVar.DHCORE_PASSWORD.value]
|
|
409
|
-
kwargs["auth"] = (user, password)
|
|
410
|
-
return kwargs
|
|
411
|
-
|
|
412
|
-
def refresh_credentials(self, change_origin: bool = False) -> None:
|
|
413
|
-
"""
|
|
414
|
-
Refresh authentication credentials by obtaining new access tokens.
|
|
415
|
-
|
|
416
|
-
Performs credential refresh using either OAuth2 refresh token flow
|
|
417
|
-
or personal access token exchange, depending on the current
|
|
418
|
-
authentication type. Updates stored credentials with new tokens.
|
|
419
|
-
|
|
420
|
-
Parameters
|
|
421
|
-
----------
|
|
422
|
-
change_origin : bool, default False
|
|
423
|
-
Whether to allow changing credential source if refresh fails.
|
|
424
|
-
If True and refresh fails, attempts to switch credential sources
|
|
425
|
-
and retry once.
|
|
426
|
-
|
|
427
|
-
Returns
|
|
428
|
-
-------
|
|
429
|
-
None
|
|
430
|
-
|
|
431
|
-
Raises
|
|
432
|
-
------
|
|
433
|
-
ClientError
|
|
434
|
-
If the authentication type doesn't support refresh, if required
|
|
435
|
-
credentials are missing, or if refresh fails and change_origin
|
|
436
|
-
is False.
|
|
437
|
-
|
|
438
|
-
Notes
|
|
439
|
-
-----
|
|
440
|
-
Refresh behavior by authentication type:
|
|
441
|
-
- OAUTH2: Uses refresh_token grant to get new access/refresh tokens
|
|
442
|
-
- EXCHANGE: Uses token exchange with personal access token
|
|
443
|
-
|
|
444
|
-
If refresh fails with 400/401/403 status and change_origin=True,
|
|
445
|
-
attempts to switch credential sources and retry once.
|
|
446
|
-
|
|
447
|
-
New credentials are automatically saved to the configuration file
|
|
448
|
-
and the origin is switched to file-based storage.
|
|
449
|
-
"""
|
|
450
|
-
if not self.refreshable_auth_types():
|
|
451
|
-
raise ClientError(f"Auth type {self._auth_type} does not support refresh.")
|
|
452
|
-
|
|
453
|
-
# Get refresh endpoint
|
|
454
|
-
url = self._get_refresh_endpoint()
|
|
455
|
-
|
|
456
|
-
# Get credentials
|
|
457
|
-
creds = self._creds_handler.get_credentials(self._origin)
|
|
458
|
-
|
|
459
|
-
# Get client id
|
|
460
|
-
if (client_id := creds.get(CredsEnvVar.DHCORE_CLIENT_ID.value)) is None:
|
|
461
|
-
raise ClientError("Client id not set.")
|
|
462
|
-
|
|
463
|
-
# Handling of token exchange or refresh
|
|
464
|
-
if self._auth_type == AuthType.OAUTH2.value:
|
|
465
|
-
response = self._call_refresh_endpoint(
|
|
466
|
-
url,
|
|
467
|
-
client_id=client_id,
|
|
468
|
-
refresh_token=creds.get(CredsEnvVar.DHCORE_REFRESH_TOKEN.value),
|
|
469
|
-
grant_type="refresh_token",
|
|
470
|
-
scope="credentials",
|
|
471
|
-
)
|
|
472
|
-
elif self._auth_type == AuthType.EXCHANGE.value:
|
|
473
|
-
response = self._call_refresh_endpoint(
|
|
474
|
-
url,
|
|
475
|
-
client_id=client_id,
|
|
476
|
-
subject_token=creds.get(CredsEnvVar.DHCORE_PERSONAL_ACCESS_TOKEN.value),
|
|
477
|
-
subject_token_type="urn:ietf:params:oauth:token-type:pat",
|
|
478
|
-
grant_type="urn:ietf:params:oauth:grant-type:token-exchange",
|
|
479
|
-
scope="credentials",
|
|
480
|
-
)
|
|
481
|
-
|
|
482
|
-
# Change origin of creds if needed
|
|
483
|
-
if response.status_code in (400, 401, 403):
|
|
484
|
-
if not change_origin:
|
|
485
|
-
raise ClientError("Unable to refresh credentials. Please check your credentials.")
|
|
486
|
-
self.eval_change_origin()
|
|
487
|
-
self.refresh_credentials(change_origin=False)
|
|
488
|
-
|
|
489
|
-
response.raise_for_status()
|
|
490
|
-
|
|
491
|
-
# Read new credentials and propagate to config file
|
|
492
|
-
self._export_new_creds(response.json())
|
|
493
|
-
|
|
494
|
-
def _remove_prefix_dhcore(self) -> list[str]:
|
|
495
|
-
"""
|
|
496
|
-
Remove DHCORE_ prefix from selected credential keys for CLI compatibility.
|
|
497
|
-
|
|
498
|
-
Creates a list of credential key names with "DHCORE_" prefix removed
|
|
499
|
-
from specific keys that are stored without the prefix in configuration
|
|
500
|
-
files for compatibility with CLI tools.
|
|
501
|
-
|
|
502
|
-
Returns
|
|
503
|
-
-------
|
|
504
|
-
list[str]
|
|
505
|
-
List of credential keys with selective prefix removal applied.
|
|
506
|
-
|
|
507
|
-
Notes
|
|
508
|
-
-----
|
|
509
|
-
Keys that have prefix removed (defined in keys_to_unprefix):
|
|
510
|
-
- DHCORE_REFRESH_TOKEN -> refresh_token
|
|
511
|
-
- DHCORE_ACCESS_TOKEN -> access_token
|
|
512
|
-
- DHCORE_ISSUER -> issuer
|
|
513
|
-
- DHCORE_CLIENT_ID -> client_id
|
|
514
|
-
|
|
515
|
-
Other keys retain their full names for consistency.
|
|
516
|
-
"""
|
|
517
|
-
new_list = []
|
|
518
|
-
for key in self.keys:
|
|
519
|
-
if key in self.keys_to_unprefix:
|
|
520
|
-
new_list.append(key.removeprefix("DHCORE_"))
|
|
521
|
-
else:
|
|
522
|
-
new_list.append(key)
|
|
523
|
-
return new_list
|
|
524
|
-
|
|
525
|
-
def _get_refresh_endpoint(self) -> str:
|
|
526
|
-
"""
|
|
527
|
-
Discover the OAuth2 token refresh endpoint from the issuer.
|
|
528
|
-
|
|
529
|
-
Queries the OAuth2 issuer's well-known configuration endpoint to
|
|
530
|
-
discover the token endpoint used for credential refresh operations.
|
|
531
|
-
|
|
532
|
-
Returns
|
|
533
|
-
-------
|
|
534
|
-
str
|
|
535
|
-
The token endpoint URL for credential refresh.
|
|
536
|
-
|
|
537
|
-
Raises
|
|
538
|
-
------
|
|
539
|
-
ClientError
|
|
540
|
-
If the issuer endpoint is not configured.
|
|
541
|
-
HTTPError
|
|
542
|
-
If the well-known configuration endpoint is not accessible.
|
|
543
|
-
KeyError
|
|
544
|
-
If the token_endpoint is not found in the issuer configuration.
|
|
545
|
-
|
|
546
|
-
Notes
|
|
547
|
-
-----
|
|
548
|
-
This method follows the OAuth2/OpenID Connect discovery standard by:
|
|
549
|
-
1. Accessing the issuer's /.well-known/openid-configuration endpoint
|
|
550
|
-
2. Extracting the token_endpoint from the configuration
|
|
551
|
-
3. Using this endpoint for subsequent token refresh operations
|
|
552
|
-
"""
|
|
553
|
-
# Get issuer endpoint
|
|
554
|
-
creds = self._creds_handler.get_credentials(self._origin)
|
|
555
|
-
endpoint_issuer = creds.get(CredsEnvVar.DHCORE_ISSUER.value)
|
|
556
|
-
if endpoint_issuer is None:
|
|
557
|
-
raise ClientError("Issuer endpoint not set.")
|
|
558
|
-
|
|
559
|
-
# Standard issuer endpoint path
|
|
560
|
-
url = endpoint_issuer + "/.well-known/openid-configuration"
|
|
561
|
-
|
|
562
|
-
# Call issuer to get refresh endpoint
|
|
563
|
-
r = request("GET", url, timeout=60)
|
|
564
|
-
r.raise_for_status()
|
|
565
|
-
return r.json().get("token_endpoint")
|
|
566
|
-
|
|
567
|
-
def _call_refresh_endpoint(
|
|
568
|
-
self,
|
|
569
|
-
url: str,
|
|
570
|
-
**kwargs,
|
|
571
|
-
) -> Response:
|
|
572
|
-
"""
|
|
573
|
-
Make HTTP request to OAuth2 token refresh endpoint.
|
|
574
|
-
|
|
575
|
-
Performs a POST request to the OAuth2 token endpoint with the
|
|
576
|
-
appropriate form-encoded payload for token refresh or exchange.
|
|
577
|
-
|
|
578
|
-
Parameters
|
|
579
|
-
----------
|
|
580
|
-
url : str
|
|
581
|
-
The token endpoint URL to call.
|
|
582
|
-
**kwargs : dict
|
|
583
|
-
Token request parameters such as grant_type, client_id,
|
|
584
|
-
refresh_token, subject_token, etc.
|
|
585
|
-
|
|
586
|
-
Returns
|
|
587
|
-
-------
|
|
588
|
-
Response
|
|
589
|
-
The HTTP response object from the token endpoint.
|
|
590
|
-
|
|
591
|
-
Notes
|
|
592
|
-
-----
|
|
593
|
-
This method:
|
|
594
|
-
- Uses application/x-www-form-urlencoded content type as required by OAuth2
|
|
595
|
-
- Sets a 60-second timeout for the request
|
|
596
|
-
- Returns the raw response for caller to handle status and parsing
|
|
597
|
-
"""
|
|
598
|
-
# Send request to get new access token
|
|
599
|
-
payload = {**kwargs}
|
|
600
|
-
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
601
|
-
return request("POST", url, data=payload, headers=headers, timeout=60)
|
|
602
|
-
|
|
603
|
-
def _eval_auth_type(self, creds: dict) -> str | None:
|
|
604
|
-
"""
|
|
605
|
-
Evaluate authentication type based on available credentials.
|
|
606
|
-
|
|
607
|
-
Analyzes the provided credentials and determines the most appropriate
|
|
608
|
-
authentication method based on which credential types are available.
|
|
609
|
-
|
|
610
|
-
Parameters
|
|
611
|
-
----------
|
|
612
|
-
creds : dict
|
|
613
|
-
Dictionary containing credential values.
|
|
614
|
-
|
|
615
|
-
Returns
|
|
616
|
-
-------
|
|
617
|
-
str or None
|
|
618
|
-
The determined authentication type from AuthType enum, or None
|
|
619
|
-
if no valid authentication method can be determined.
|
|
620
|
-
|
|
621
|
-
Notes
|
|
622
|
-
-----
|
|
623
|
-
Authentication type priority (checked in order):
|
|
624
|
-
1. EXCHANGE - Personal access token is available
|
|
625
|
-
2. OAUTH2 - Both access token and refresh token are available
|
|
626
|
-
3. ACCESS_TOKEN - Only access token is available
|
|
627
|
-
4. BASIC - Both username and password are available
|
|
628
|
-
5. None - No valid credential combination found
|
|
629
|
-
"""
|
|
630
|
-
if creds[CredsEnvVar.DHCORE_PERSONAL_ACCESS_TOKEN.value] is not None:
|
|
631
|
-
return AuthType.EXCHANGE.value
|
|
632
|
-
if (
|
|
633
|
-
creds[CredsEnvVar.DHCORE_ACCESS_TOKEN.value] is not None
|
|
634
|
-
and creds[CredsEnvVar.DHCORE_REFRESH_TOKEN.value] is not None
|
|
635
|
-
):
|
|
636
|
-
return AuthType.OAUTH2.value
|
|
637
|
-
if creds[CredsEnvVar.DHCORE_ACCESS_TOKEN.value] is not None:
|
|
638
|
-
return AuthType.ACCESS_TOKEN.value
|
|
639
|
-
if creds[CredsEnvVar.DHCORE_USER.value] is not None and creds[CredsEnvVar.DHCORE_PASSWORD.value] is not None:
|
|
640
|
-
return AuthType.BASIC.value
|
|
641
|
-
return None
|
|
642
|
-
|
|
643
|
-
def _export_new_creds(self, response: dict) -> None:
|
|
644
|
-
"""
|
|
645
|
-
Save new credentials from token refresh response.
|
|
646
|
-
|
|
647
|
-
Takes the response from a successful token refresh operation and
|
|
648
|
-
persists the new credentials to the configuration file, then
|
|
649
|
-
reloads file-based credentials and switches to file origin.
|
|
650
|
-
|
|
651
|
-
Parameters
|
|
652
|
-
----------
|
|
653
|
-
response : dict
|
|
654
|
-
Token response containing new access_token, refresh_token,
|
|
655
|
-
and other credential information.
|
|
656
|
-
|
|
657
|
-
Returns
|
|
658
|
-
-------
|
|
659
|
-
None
|
|
660
|
-
|
|
661
|
-
Notes
|
|
662
|
-
-----
|
|
663
|
-
This method:
|
|
664
|
-
1. Writes new credentials to the configuration file
|
|
665
|
-
2. Reloads file-based credentials to ensure consistency
|
|
666
|
-
3. Changes current origin to file since new tokens are file-based
|
|
667
|
-
|
|
668
|
-
The response typically contains access_token, refresh_token,
|
|
669
|
-
token_type, expires_in, and other OAuth2 standard fields.
|
|
670
|
-
"""
|
|
671
|
-
creds_handler.write_env(response)
|
|
672
|
-
self.load_file_vars()
|
|
673
|
-
|
|
674
|
-
# Change current origin to file because of refresh
|
|
675
|
-
self.change_to_file()
|
|
@@ -1,34 +0,0 @@
|
|
|
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 DhcoreEnvVar(Enum):
|
|
11
|
-
"""
|
|
12
|
-
Environment variables.
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
ENDPOINT = "DHCORE_ENDPOINT"
|
|
16
|
-
ISSUER = "DHCORE_ISSUER"
|
|
17
|
-
USER = "DHCORE_USER"
|
|
18
|
-
PASSWORD = "DHCORE_PASSWORD"
|
|
19
|
-
CLIENT_ID = "DHCORE_CLIENT_ID"
|
|
20
|
-
ACCESS_TOKEN = "DHCORE_ACCESS_TOKEN"
|
|
21
|
-
REFRESH_TOKEN = "DHCORE_REFRESH_TOKEN"
|
|
22
|
-
PERSONAL_ACCESS_TOKEN = "DHCORE_PERSONAL_ACCESS_TOKEN"
|
|
23
|
-
WORKFLOW_IMAGE = "DHCORE_WORKFLOW_IMAGE"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class AuthType(Enum):
|
|
27
|
-
"""
|
|
28
|
-
Authentication types.
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
BASIC = "basic"
|
|
32
|
-
OAUTH2 = "oauth2"
|
|
33
|
-
EXCHANGE = "exchange"
|
|
34
|
-
ACCESS_TOKEN = "access_token_only"
|