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.
Files changed (139) hide show
  1. digitalhub/__init__.py +3 -8
  2. digitalhub/context/api.py +43 -6
  3. digitalhub/context/builder.py +1 -5
  4. digitalhub/context/context.py +28 -13
  5. digitalhub/entities/_base/_base/entity.py +0 -15
  6. digitalhub/entities/_base/context/entity.py +1 -4
  7. digitalhub/entities/_base/entity/builder.py +5 -5
  8. digitalhub/entities/_base/entity/entity.py +0 -8
  9. digitalhub/entities/_base/executable/entity.py +195 -87
  10. digitalhub/entities/_base/material/entity.py +11 -23
  11. digitalhub/entities/_base/material/utils.py +28 -4
  12. digitalhub/entities/_base/runtime_entity/builder.py +53 -18
  13. digitalhub/entities/_base/unversioned/entity.py +1 -1
  14. digitalhub/entities/_base/versioned/entity.py +1 -1
  15. digitalhub/entities/_commons/enums.py +1 -31
  16. digitalhub/entities/_commons/metrics.py +64 -30
  17. digitalhub/entities/_commons/utils.py +119 -30
  18. digitalhub/entities/_constructors/_resources.py +151 -0
  19. digitalhub/entities/{_base/entity/_constructors → _constructors}/name.py +18 -0
  20. digitalhub/entities/_processors/base/crud.py +381 -0
  21. digitalhub/entities/_processors/base/import_export.py +118 -0
  22. digitalhub/entities/_processors/base/processor.py +299 -0
  23. digitalhub/entities/_processors/base/special_ops.py +104 -0
  24. digitalhub/entities/_processors/context/crud.py +652 -0
  25. digitalhub/entities/_processors/context/import_export.py +242 -0
  26. digitalhub/entities/_processors/context/material.py +123 -0
  27. digitalhub/entities/_processors/context/processor.py +400 -0
  28. digitalhub/entities/_processors/context/special_ops.py +476 -0
  29. digitalhub/entities/_processors/processors.py +12 -0
  30. digitalhub/entities/_processors/utils.py +38 -102
  31. digitalhub/entities/artifact/crud.py +58 -22
  32. digitalhub/entities/artifact/utils.py +28 -13
  33. digitalhub/entities/builders.py +2 -0
  34. digitalhub/entities/dataitem/crud.py +63 -20
  35. digitalhub/entities/dataitem/table/entity.py +27 -22
  36. digitalhub/entities/dataitem/utils.py +82 -32
  37. digitalhub/entities/function/_base/entity.py +3 -6
  38. digitalhub/entities/function/crud.py +55 -24
  39. digitalhub/entities/model/_base/entity.py +62 -20
  40. digitalhub/entities/model/crud.py +59 -23
  41. digitalhub/entities/model/mlflow/utils.py +29 -20
  42. digitalhub/entities/model/utils.py +28 -13
  43. digitalhub/entities/project/_base/builder.py +0 -6
  44. digitalhub/entities/project/_base/entity.py +337 -164
  45. digitalhub/entities/project/_base/spec.py +4 -4
  46. digitalhub/entities/project/crud.py +28 -71
  47. digitalhub/entities/project/utils.py +7 -3
  48. digitalhub/entities/run/_base/builder.py +0 -4
  49. digitalhub/entities/run/_base/entity.py +70 -63
  50. digitalhub/entities/run/crud.py +79 -26
  51. digitalhub/entities/secret/_base/entity.py +1 -5
  52. digitalhub/entities/secret/crud.py +31 -28
  53. digitalhub/entities/task/_base/builder.py +0 -4
  54. digitalhub/entities/task/_base/entity.py +5 -5
  55. digitalhub/entities/task/_base/models.py +13 -16
  56. digitalhub/entities/task/crud.py +61 -29
  57. digitalhub/entities/trigger/_base/entity.py +1 -5
  58. digitalhub/entities/trigger/crud.py +89 -30
  59. digitalhub/entities/workflow/_base/entity.py +3 -8
  60. digitalhub/entities/workflow/crud.py +55 -24
  61. digitalhub/factory/entity.py +283 -0
  62. digitalhub/factory/enums.py +18 -0
  63. digitalhub/factory/registry.py +197 -0
  64. digitalhub/factory/runtime.py +44 -0
  65. digitalhub/factory/utils.py +3 -54
  66. digitalhub/runtimes/_base.py +2 -2
  67. digitalhub/stores/client/{dhcore/api_builder.py → api_builder.py} +3 -3
  68. digitalhub/stores/client/builder.py +19 -31
  69. digitalhub/stores/client/client.py +322 -0
  70. digitalhub/stores/client/configurator.py +408 -0
  71. digitalhub/stores/client/enums.py +50 -0
  72. digitalhub/stores/client/{dhcore/error_parser.py → error_parser.py} +0 -4
  73. digitalhub/stores/client/header_manager.py +61 -0
  74. digitalhub/stores/client/http_handler.py +152 -0
  75. digitalhub/stores/client/{_base/key_builder.py → key_builder.py} +14 -14
  76. digitalhub/stores/client/params_builder.py +330 -0
  77. digitalhub/stores/client/response_processor.py +102 -0
  78. digitalhub/stores/client/utils.py +35 -0
  79. digitalhub/stores/{credentials → configurator}/api.py +5 -9
  80. digitalhub/stores/configurator/configurator.py +123 -0
  81. digitalhub/stores/{credentials → configurator}/enums.py +27 -10
  82. digitalhub/stores/configurator/handler.py +213 -0
  83. digitalhub/stores/{credentials → configurator}/ini_module.py +31 -22
  84. digitalhub/stores/data/_base/store.py +0 -20
  85. digitalhub/stores/data/api.py +5 -7
  86. digitalhub/stores/data/builder.py +53 -27
  87. digitalhub/stores/data/local/store.py +0 -103
  88. digitalhub/stores/data/remote/store.py +0 -4
  89. digitalhub/stores/data/s3/configurator.py +39 -77
  90. digitalhub/stores/data/s3/store.py +57 -37
  91. digitalhub/stores/data/sql/configurator.py +66 -46
  92. digitalhub/stores/data/sql/store.py +171 -104
  93. digitalhub/stores/readers/data/factory.py +0 -8
  94. digitalhub/stores/readers/data/pandas/reader.py +9 -19
  95. digitalhub/utils/file_utils.py +0 -17
  96. digitalhub/utils/generic_utils.py +1 -14
  97. digitalhub/utils/git_utils.py +0 -8
  98. digitalhub/utils/io_utils.py +0 -12
  99. digitalhub/utils/store_utils.py +44 -0
  100. {digitalhub-0.13.0b3.dist-info → digitalhub-0.14.9.dist-info}/METADATA +5 -4
  101. {digitalhub-0.13.0b3.dist-info → digitalhub-0.14.9.dist-info}/RECORD +112 -113
  102. {digitalhub-0.13.0b3.dist-info → digitalhub-0.14.9.dist-info}/WHEEL +1 -1
  103. digitalhub/entities/_commons/types.py +0 -9
  104. digitalhub/entities/_processors/base.py +0 -531
  105. digitalhub/entities/_processors/context.py +0 -1299
  106. digitalhub/entities/task/_base/utils.py +0 -22
  107. digitalhub/factory/factory.py +0 -381
  108. digitalhub/stores/client/_base/api_builder.py +0 -34
  109. digitalhub/stores/client/_base/client.py +0 -243
  110. digitalhub/stores/client/_base/params_builder.py +0 -34
  111. digitalhub/stores/client/api.py +0 -36
  112. digitalhub/stores/client/dhcore/client.py +0 -613
  113. digitalhub/stores/client/dhcore/configurator.py +0 -675
  114. digitalhub/stores/client/dhcore/enums.py +0 -34
  115. digitalhub/stores/client/dhcore/key_builder.py +0 -62
  116. digitalhub/stores/client/dhcore/models.py +0 -40
  117. digitalhub/stores/client/dhcore/params_builder.py +0 -278
  118. digitalhub/stores/client/dhcore/utils.py +0 -94
  119. digitalhub/stores/client/local/api_builder.py +0 -116
  120. digitalhub/stores/client/local/client.py +0 -573
  121. digitalhub/stores/client/local/enums.py +0 -15
  122. digitalhub/stores/client/local/key_builder.py +0 -62
  123. digitalhub/stores/client/local/params_builder.py +0 -120
  124. digitalhub/stores/credentials/__init__.py +0 -3
  125. digitalhub/stores/credentials/configurator.py +0 -210
  126. digitalhub/stores/credentials/handler.py +0 -176
  127. digitalhub/stores/credentials/store.py +0 -81
  128. digitalhub/stores/data/enums.py +0 -15
  129. digitalhub/stores/data/s3/utils.py +0 -78
  130. /digitalhub/entities/{_base/entity/_constructors → _constructors}/__init__.py +0 -0
  131. /digitalhub/entities/{_base/entity/_constructors → _constructors}/metadata.py +0 -0
  132. /digitalhub/entities/{_base/entity/_constructors → _constructors}/spec.py +0 -0
  133. /digitalhub/entities/{_base/entity/_constructors → _constructors}/status.py +0 -0
  134. /digitalhub/entities/{_base/entity/_constructors → _constructors}/uuid.py +0 -0
  135. /digitalhub/{stores/client/_base → entities/_processors/base}/__init__.py +0 -0
  136. /digitalhub/{stores/client/dhcore → entities/_processors/context}/__init__.py +0 -0
  137. /digitalhub/stores/{client/local → configurator}/__init__.py +0 -0
  138. {digitalhub-0.13.0b3.dist-info → digitalhub-0.14.9.dist-info}/licenses/AUTHORS +0 -0
  139. {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"