digitalhub 0.13.0b2__py3-none-any.whl → 0.13.0b4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of digitalhub might be problematic. Click here for more details.

Files changed (61) hide show
  1. digitalhub/__init__.py +1 -1
  2. digitalhub/context/api.py +5 -5
  3. digitalhub/context/builder.py +3 -5
  4. digitalhub/context/context.py +9 -1
  5. digitalhub/entities/_base/material/entity.py +3 -3
  6. digitalhub/entities/_commons/metrics.py +64 -30
  7. digitalhub/entities/_commons/utils.py +36 -9
  8. digitalhub/entities/_processors/base.py +150 -79
  9. digitalhub/entities/_processors/context.py +363 -212
  10. digitalhub/entities/_processors/utils.py +74 -30
  11. digitalhub/entities/artifact/utils.py +28 -13
  12. digitalhub/entities/dataitem/crud.py +10 -2
  13. digitalhub/entities/dataitem/table/entity.py +3 -3
  14. digitalhub/entities/dataitem/utils.py +84 -35
  15. digitalhub/entities/model/utils.py +28 -13
  16. digitalhub/entities/task/_base/models.py +12 -3
  17. digitalhub/factory/factory.py +25 -3
  18. digitalhub/factory/utils.py +11 -3
  19. digitalhub/runtimes/_base.py +1 -1
  20. digitalhub/runtimes/builder.py +18 -1
  21. digitalhub/stores/client/__init__.py +12 -0
  22. digitalhub/stores/client/_base/api_builder.py +14 -0
  23. digitalhub/stores/client/_base/client.py +93 -0
  24. digitalhub/stores/client/_base/key_builder.py +28 -0
  25. digitalhub/stores/client/_base/params_builder.py +14 -0
  26. digitalhub/stores/client/api.py +10 -5
  27. digitalhub/stores/client/builder.py +3 -1
  28. digitalhub/stores/client/dhcore/api_builder.py +17 -0
  29. digitalhub/stores/client/dhcore/client.py +276 -58
  30. digitalhub/stores/client/dhcore/configurator.py +336 -141
  31. digitalhub/stores/client/dhcore/error_parser.py +35 -1
  32. digitalhub/stores/client/dhcore/params_builder.py +113 -17
  33. digitalhub/stores/client/dhcore/utils.py +32 -14
  34. digitalhub/stores/client/local/api_builder.py +17 -0
  35. digitalhub/stores/client/local/client.py +6 -8
  36. digitalhub/stores/credentials/api.py +8 -8
  37. digitalhub/stores/credentials/configurator.py +176 -3
  38. digitalhub/stores/credentials/enums.py +17 -3
  39. digitalhub/stores/credentials/handler.py +73 -45
  40. digitalhub/stores/credentials/ini_module.py +59 -27
  41. digitalhub/stores/credentials/store.py +33 -1
  42. digitalhub/stores/data/_base/store.py +8 -3
  43. digitalhub/stores/data/api.py +20 -16
  44. digitalhub/stores/data/builder.py +69 -13
  45. digitalhub/stores/data/s3/configurator.py +64 -23
  46. digitalhub/stores/data/s3/store.py +30 -27
  47. digitalhub/stores/data/s3/utils.py +9 -9
  48. digitalhub/stores/data/sql/configurator.py +76 -25
  49. digitalhub/stores/data/sql/store.py +180 -91
  50. digitalhub/utils/exceptions.py +6 -0
  51. digitalhub/utils/file_utils.py +53 -30
  52. digitalhub/utils/generic_utils.py +41 -33
  53. digitalhub/utils/git_utils.py +24 -14
  54. digitalhub/utils/io_utils.py +19 -18
  55. digitalhub/utils/uri_utils.py +31 -31
  56. {digitalhub-0.13.0b2.dist-info → digitalhub-0.13.0b4.dist-info}/METADATA +1 -1
  57. {digitalhub-0.13.0b2.dist-info → digitalhub-0.13.0b4.dist-info}/RECORD +60 -61
  58. digitalhub/entities/_commons/types.py +0 -9
  59. {digitalhub-0.13.0b2.dist-info → digitalhub-0.13.0b4.dist-info}/WHEEL +0 -0
  60. {digitalhub-0.13.0b2.dist-info → digitalhub-0.13.0b4.dist-info}/licenses/AUTHORS +0 -0
  61. {digitalhub-0.13.0b2.dist-info → digitalhub-0.13.0b4.dist-info}/licenses/LICENSE +0 -0
@@ -10,7 +10,7 @@ from requests import request
10
10
 
11
11
  from digitalhub.stores.client.dhcore.enums import AuthType
12
12
  from digitalhub.stores.credentials.configurator import Configurator
13
- from digitalhub.stores.credentials.enums import CredsEnvVar, CredsOrigin
13
+ from digitalhub.stores.credentials.enums import CredsEnvVar
14
14
  from digitalhub.stores.credentials.handler import creds_handler
15
15
  from digitalhub.utils.exceptions import ClientError
16
16
  from digitalhub.utils.generic_utils import list_enum
@@ -61,10 +61,17 @@ class ClientDHCoreConfigurator(Configurator):
61
61
  ]
62
62
 
63
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
+ """
64
74
  super().__init__()
65
- self.load_configs()
66
- self._origin = self.set_origin()
67
- self._current_profile = creds_handler.get_current_env()
68
75
  self._auth_type: str | None = None
69
76
  self.set_auth_type()
70
77
 
@@ -72,34 +79,53 @@ class ClientDHCoreConfigurator(Configurator):
72
79
  # Credentials methods
73
80
  ##############################
74
81
 
75
- def load_configs(self) -> str:
76
- """
77
- Load the configuration from the environment and from the file.
78
- """
79
- self.load_env_vars()
80
- self.load_file_vars()
81
-
82
82
  def load_env_vars(self) -> None:
83
83
  """
84
- Load the credentials from the environment.
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.
85
98
  """
86
- env_creds = {var: self._creds_handler.load_from_env(var) for var in self.keys}
99
+ env_creds = self._creds_handler.load_from_env(self.keys)
87
100
  env_creds = self._sanitize_env_vars(env_creds)
88
101
  self._creds_handler.set_credentials(self._env, env_creds)
89
102
 
90
103
  def _sanitize_env_vars(self, creds: dict) -> dict:
91
104
  """
92
- Sanitize the env vars. We expect issuer to have the
93
- form "DHCORE_ISSUER" in env.
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.
94
109
 
95
110
  Parameters
96
111
  ----------
97
112
  creds : dict
98
- Credentials dictionary.
113
+ Raw credentials dictionary loaded from environment variables.
99
114
 
100
115
  Returns
101
116
  -------
102
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.
103
129
  """
104
130
  creds[CredsEnvVar.DHCORE_ENDPOINT.value] = self._sanitize_endpoint(creds[CredsEnvVar.DHCORE_ENDPOINT.value])
105
131
  creds[CredsEnvVar.DHCORE_ISSUER.value] = self._sanitize_endpoint(creds[CredsEnvVar.DHCORE_ISSUER.value])
@@ -107,20 +133,35 @@ class ClientDHCoreConfigurator(Configurator):
107
133
 
108
134
  def load_file_vars(self) -> None:
109
135
  """
110
- Load the credentials from the file.
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
111
153
  """
112
154
  keys = [*self._remove_prefix_dhcore()]
113
- file_creds = {var: self._creds_handler.load_from_file(var) for var in keys}
155
+ file_creds = self._creds_handler.load_from_file(keys)
156
+ env_creds = self._creds_handler.load_from_env(self.keys)
114
157
 
115
158
  # Because in the response there is no endpoint
116
159
  if file_creds[CredsEnvVar.DHCORE_ENDPOINT.value] is None:
117
- file_creds[CredsEnvVar.DHCORE_ENDPOINT.value] = self._creds_handler.load_from_env(
118
- CredsEnvVar.DHCORE_ENDPOINT.value
119
- )
160
+ file_creds[CredsEnvVar.DHCORE_ENDPOINT.value] = env_creds.get(CredsEnvVar.DHCORE_ENDPOINT.value)
120
161
 
121
162
  # Because in the response there is no personal access token
122
163
  if file_creds[CredsEnvVar.DHCORE_PERSONAL_ACCESS_TOKEN.value] is None:
123
- file_creds[CredsEnvVar.DHCORE_PERSONAL_ACCESS_TOKEN.value] = self._creds_handler.load_from_env(
164
+ file_creds[CredsEnvVar.DHCORE_PERSONAL_ACCESS_TOKEN.value] = env_creds.get(
124
165
  CredsEnvVar.DHCORE_PERSONAL_ACCESS_TOKEN.value
125
166
  )
126
167
 
@@ -129,17 +170,33 @@ class ClientDHCoreConfigurator(Configurator):
129
170
 
130
171
  def _sanitize_file_vars(self, creds: dict) -> dict:
131
172
  """
132
- Sanitize the file vars. We expect issuer, client_id and access_token and
133
- refresh_token to not have the form "DHCORE_" in the file.
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.
134
178
 
135
179
  Parameters
136
180
  ----------
137
181
  creds : dict
138
- Credentials dictionary.
182
+ Raw credentials dictionary loaded from configuration file.
139
183
 
140
184
  Returns
141
185
  -------
142
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
143
200
  """
144
201
  creds[CredsEnvVar.DHCORE_ENDPOINT.value] = self._sanitize_endpoint(creds[CredsEnvVar.DHCORE_ENDPOINT.value])
145
202
  creds[CredsEnvVar.DHCORE_ISSUER.value] = self._sanitize_endpoint(
@@ -157,12 +214,31 @@ class ClientDHCoreConfigurator(Configurator):
157
214
  @staticmethod
158
215
  def _sanitize_endpoint(endpoint: str | None = None) -> str | None:
159
216
  """
160
- Sanitize the endpoint.
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.
161
226
 
162
227
  Returns
163
228
  -------
164
- str | None
165
- The sanitized endpoint.
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.
166
242
  """
167
243
  if endpoint is None:
168
244
  return
@@ -172,31 +248,27 @@ class ClientDHCoreConfigurator(Configurator):
172
248
  endpoint = endpoint.strip()
173
249
  return endpoint.removesuffix("/")
174
250
 
175
- def check_config(self) -> None:
251
+ def get_endpoint(self) -> str:
176
252
  """
177
- Check if the config is valid.
253
+ Get the configured DHCore backend endpoint.
178
254
 
179
- Parameters
180
- ----------
181
- config : dict
182
- Configuration dictionary.
255
+ Retrieves the DHCore endpoint URL from the current credential source
256
+ (environment or file based on current origin).
183
257
 
184
258
  Returns
185
259
  -------
186
- None
187
- """
188
- if (current := creds_handler.get_current_env()) != self._current_profile:
189
- self.load_file_vars()
190
- self._current_profile = current
260
+ str
261
+ The DHCore backend endpoint URL.
191
262
 
192
- def get_endpoint(self) -> str:
193
- """
194
- Get the DHCore endpoint.
263
+ Raises
264
+ ------
265
+ KeyError
266
+ If the endpoint is not configured in the current credential source.
195
267
 
196
- Returns
197
- -------
198
- str
199
- The endpoint.
268
+ Notes
269
+ -----
270
+ The endpoint returned is already sanitized and validated during
271
+ the credential loading process.
200
272
  """
201
273
  creds = self._creds_handler.get_credentials(self._origin)
202
274
  return creds[CredsEnvVar.DHCORE_ENDPOINT.value]
@@ -205,70 +277,59 @@ class ClientDHCoreConfigurator(Configurator):
205
277
  # Origin methods
206
278
  ##############################
207
279
 
208
- def set_origin(self) -> str:
280
+ def change_origin(self) -> None:
209
281
  """
210
- Evaluate the default origin from the credentials.
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.
211
288
 
212
289
  Returns
213
290
  -------
214
- str
215
- The origin.
216
- """
217
- origin = CredsOrigin.ENV.value
218
-
219
- env_creds = self._creds_handler.get_credentials(self._env)
220
- missing_env = self._check_credentials(env_creds)
221
-
222
- file_creds = self._creds_handler.get_credentials(self._file)
223
- missing_file = self._check_credentials(file_creds)
224
-
225
- msg = ""
226
- if missing_env:
227
- msg = f"Missing required vars in env: {', '.join(missing_env)}"
228
- origin = CredsOrigin.FILE.value
229
- elif missing_file:
230
- msg += f"Missing required vars in .dhcore.ini file: {', '.join(missing_file)}"
231
-
232
- if missing_env and missing_file:
233
- raise ClientError(msg)
234
-
235
- return origin
291
+ None
236
292
 
237
- def change_origin(self) -> None:
238
- """
239
- Change the origin of the credentials.
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.
240
298
  """
241
- if self._origin == CredsOrigin.ENV.value:
242
- self.change_to_file()
243
- else:
244
- self.change_to_env()
299
+ super().change_origin()
245
300
 
246
301
  # Re-evaluate the auth type
247
302
  self.set_auth_type()
248
303
 
249
- def change_to_file(self) -> None:
250
- """
251
- Change the origin to file. Re-evaluate the auth type.
252
- """
253
- self._origin = CredsOrigin.FILE.value
254
-
255
- def change_to_env(self) -> None:
256
- """
257
- Change the origin to env. Re-evaluate the auth type.
258
- """
259
- self._origin = CredsOrigin.ENV.value
260
-
261
304
  ##############################
262
305
  # Auth methods
263
306
  ##############################
264
307
 
265
308
  def set_auth_type(self) -> None:
266
309
  """
267
- Evaluate the auth type from the credentials.
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.
268
322
 
269
323
  Returns
270
324
  -------
271
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
272
333
  """
273
334
  creds = creds_handler.get_credentials(self._origin)
274
335
  self._auth_type = self._eval_auth_type(creds)
@@ -276,39 +337,68 @@ class ClientDHCoreConfigurator(Configurator):
276
337
  # Therefore, we change the origin to file, where the refresh token is written.
277
338
  # We also try to fetch the PAT from both env and file
278
339
  if self._auth_type == AuthType.EXCHANGE.value:
279
- self.get_new_access_token(change_origin=True)
340
+ self.refresh_credentials(change_origin=True)
280
341
  # Just to ensure we get the right source from file
281
342
  self.change_to_file()
282
343
 
283
344
  def refreshable_auth_types(self) -> bool:
284
345
  """
285
- Check if the auth type is refreshable.
346
+ Check if the current authentication type supports token refresh.
347
+
348
+ Determines whether the current authentication method supports
349
+ automatic token refresh capabilities.
286
350
 
287
351
  Returns
288
352
  -------
289
353
  bool
290
- True if the auth type is refreshable, False otherwise.
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
291
363
  """
292
364
  return self._auth_type in [AuthType.OAUTH2.value, AuthType.EXCHANGE.value]
293
365
 
294
366
  def get_auth_parameters(self, kwargs: dict) -> dict:
295
367
  """
296
- Get the authentication header for the request.
297
- It is given for granted that the auth type is set and that,
298
- if the auth type is EXCHANGE, the refresh token is set.
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.
299
373
 
300
374
  Parameters
301
375
  ----------
302
376
  kwargs : dict
303
- Keyword arguments to pass to the request.
377
+ HTTP request keyword arguments to be modified with authentication.
304
378
 
305
379
  Returns
306
380
  -------
307
381
  dict
308
- Authentication parameters.
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
309
395
  """
310
396
  creds = creds_handler.get_credentials(self._origin)
311
- if self._auth_type in (AuthType.EXCHANGE.value, AuthType.OAUTH2.value, AuthType.ACCESS_TOKEN.value):
397
+ if self._auth_type in (
398
+ AuthType.EXCHANGE.value,
399
+ AuthType.OAUTH2.value,
400
+ AuthType.ACCESS_TOKEN.value,
401
+ ):
312
402
  access_token = creds[CredsEnvVar.DHCORE_ACCESS_TOKEN.value]
313
403
  if "headers" not in kwargs:
314
404
  kwargs["headers"] = {}
@@ -319,18 +409,43 @@ class ClientDHCoreConfigurator(Configurator):
319
409
  kwargs["auth"] = (user, password)
320
410
  return kwargs
321
411
 
322
- def get_new_access_token(self, change_origin: bool = False) -> None:
412
+ def refresh_credentials(self, change_origin: bool = False) -> None:
323
413
  """
324
- Get a new access token.
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.
325
419
 
326
420
  Parameters
327
421
  ----------
328
- change_origin : bool, optional
329
- Whether to change the origin of the credentials, by default False
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.
330
426
 
331
427
  Returns
332
428
  -------
333
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.
334
449
  """
335
450
  if not self.refreshable_auth_types():
336
451
  raise ClientError(f"Auth type {self._auth_type} does not support refresh.")
@@ -347,7 +462,7 @@ class ClientDHCoreConfigurator(Configurator):
347
462
 
348
463
  # Handling of token exchange or refresh
349
464
  if self._auth_type == AuthType.OAUTH2.value:
350
- response = self._call_refresh_token_endpoint(
465
+ response = self._call_refresh_endpoint(
351
466
  url,
352
467
  client_id=client_id,
353
468
  refresh_token=creds.get(CredsEnvVar.DHCORE_REFRESH_TOKEN.value),
@@ -355,7 +470,7 @@ class ClientDHCoreConfigurator(Configurator):
355
470
  scope="credentials",
356
471
  )
357
472
  elif self._auth_type == AuthType.EXCHANGE.value:
358
- response = self._call_refresh_token_endpoint(
473
+ response = self._call_refresh_endpoint(
359
474
  url,
360
475
  client_id=client_id,
361
476
  subject_token=creds.get(CredsEnvVar.DHCORE_PERSONAL_ACCESS_TOKEN.value),
@@ -367,47 +482,37 @@ class ClientDHCoreConfigurator(Configurator):
367
482
  # Change origin of creds if needed
368
483
  if response.status_code in (400, 401, 403):
369
484
  if not change_origin:
370
- raise ClientError("Unable to refresh token. Please check your credentials.")
371
- self.change_origin()
372
- self.get_new_access_token(change_origin=False)
485
+ raise ClientError("Unable to refresh credentials. Please check your credentials.")
486
+ self.eval_change_origin()
487
+ self.refresh_credentials(change_origin=False)
373
488
 
374
489
  response.raise_for_status()
375
490
 
376
491
  # Read new credentials and propagate to config file
377
492
  self._export_new_creds(response.json())
378
493
 
379
- def _export_new_creds(self, response: dict) -> None:
380
- """
381
- Set new credentials.
382
-
383
- Parameters
384
- ----------
385
- response : dict
386
- Response from refresh token endpoint.
387
-
388
- Returns
389
- -------
390
- None
391
- """
392
- creds_handler.write_env(response)
393
- self.load_file_vars()
394
-
395
- # Change current origin to file because of refresh
396
- self._origin = CredsOrigin.FILE.value
397
-
398
494
  def _remove_prefix_dhcore(self) -> list[str]:
399
495
  """
400
- Remove prefix from selected keys. (Compatibility with CLI)
496
+ Remove DHCORE_ prefix from selected credential keys for CLI compatibility.
401
497
 
402
- Parameters
403
- ----------
404
- keys : list[str]
405
- List of keys.
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.
406
501
 
407
502
  Returns
408
503
  -------
409
504
  list[str]
410
- List of keys without prefix.
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.
411
516
  """
412
517
  new_list = []
413
518
  for key in self.keys:
@@ -419,12 +524,31 @@ class ClientDHCoreConfigurator(Configurator):
419
524
 
420
525
  def _get_refresh_endpoint(self) -> str:
421
526
  """
422
- Get the refresh endpoint.
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.
423
531
 
424
532
  Returns
425
533
  -------
426
534
  str
427
- Refresh endpoint.
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
428
552
  """
429
553
  # Get issuer endpoint
430
554
  creds = self._creds_handler.get_credentials(self._origin)
@@ -440,25 +564,36 @@ class ClientDHCoreConfigurator(Configurator):
440
564
  r.raise_for_status()
441
565
  return r.json().get("token_endpoint")
442
566
 
443
- def _call_refresh_token_endpoint(
567
+ def _call_refresh_endpoint(
444
568
  self,
445
569
  url: str,
446
570
  **kwargs,
447
571
  ) -> Response:
448
572
  """
449
- Call the refresh token endpoint.
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.
450
577
 
451
578
  Parameters
452
579
  ----------
453
580
  url : str
454
- Refresh token endpoint.
455
- kwargs : dict
456
- Keyword arguments to pass to the request.
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.
457
585
 
458
586
  Returns
459
587
  -------
460
588
  Response
461
- Response object.
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
462
597
  """
463
598
  # Send request to get new access token
464
599
  payload = {**kwargs}
@@ -466,6 +601,32 @@ class ClientDHCoreConfigurator(Configurator):
466
601
  return request("POST", url, data=payload, headers=headers, timeout=60)
467
602
 
468
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
+ """
469
630
  if creds[CredsEnvVar.DHCORE_PERSONAL_ACCESS_TOKEN.value] is not None:
470
631
  return AuthType.EXCHANGE.value
471
632
  if (
@@ -478,3 +639,37 @@ class ClientDHCoreConfigurator(Configurator):
478
639
  if creds[CredsEnvVar.DHCORE_USER.value] is not None and creds[CredsEnvVar.DHCORE_PASSWORD.value] is not None:
479
640
  return AuthType.BASIC.value
480
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()