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