digitalhub 0.9.1__py3-none-any.whl → 0.10.0b0__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.
- digitalhub/__init__.py +2 -3
- digitalhub/client/_base/client.py +3 -2
- digitalhub/client/dhcore/api_builder.py +6 -1
- digitalhub/client/dhcore/client.py +27 -399
- digitalhub/client/dhcore/configurator.py +339 -0
- digitalhub/client/dhcore/error_parser.py +107 -0
- digitalhub/client/dhcore/models.py +13 -23
- digitalhub/client/dhcore/utils.py +4 -44
- digitalhub/client/local/api_builder.py +9 -17
- digitalhub/client/local/client.py +12 -2
- digitalhub/client/local/enums.py +11 -0
- digitalhub/configurator/api.py +31 -0
- digitalhub/configurator/configurator.py +194 -0
- digitalhub/configurator/credentials_store.py +65 -0
- digitalhub/configurator/ini_module.py +74 -0
- digitalhub/entities/_base/_base/entity.py +2 -2
- digitalhub/entities/_base/material/entity.py +19 -6
- digitalhub/entities/_base/material/utils.py +2 -2
- digitalhub/entities/_commons/enums.py +1 -0
- digitalhub/entities/_commons/models.py +9 -0
- digitalhub/entities/_commons/utils.py +25 -0
- digitalhub/entities/_operations/processor.py +103 -107
- digitalhub/entities/artifact/crud.py +3 -3
- digitalhub/entities/dataitem/_base/entity.py +2 -2
- digitalhub/entities/dataitem/crud.py +3 -3
- digitalhub/entities/dataitem/table/entity.py +2 -2
- digitalhub/{utils/data_utils.py → entities/dataitem/table/utils.py} +43 -51
- digitalhub/entities/dataitem/utils.py +6 -3
- digitalhub/entities/model/_base/entity.py +172 -0
- digitalhub/entities/model/_base/spec.py +0 -10
- digitalhub/entities/model/_base/status.py +10 -0
- digitalhub/entities/model/crud.py +3 -3
- digitalhub/entities/model/huggingface/spec.py +6 -3
- digitalhub/entities/model/mlflow/models.py +2 -2
- digitalhub/entities/model/mlflow/spec.py +1 -3
- digitalhub/entities/model/mlflow/utils.py +44 -5
- digitalhub/entities/run/_base/entity.py +149 -0
- digitalhub/entities/run/_base/status.py +12 -0
- digitalhub/entities/task/_base/spec.py +2 -0
- digitalhub/entities/task/crud.py +4 -0
- digitalhub/readers/{_commons → pandas}/enums.py +4 -0
- digitalhub/readers/pandas/reader.py +58 -10
- digitalhub/stores/_base/store.py +1 -49
- digitalhub/stores/api.py +8 -33
- digitalhub/stores/builder.py +44 -161
- digitalhub/stores/local/store.py +4 -18
- digitalhub/stores/remote/store.py +3 -10
- digitalhub/stores/s3/configurator.py +107 -0
- digitalhub/stores/s3/enums.py +17 -0
- digitalhub/stores/s3/models.py +21 -0
- digitalhub/stores/s3/store.py +8 -28
- digitalhub/{utils/s3_utils.py → stores/s3/utils.py} +7 -3
- digitalhub/stores/sql/configurator.py +88 -0
- digitalhub/stores/sql/enums.py +16 -0
- digitalhub/stores/sql/models.py +24 -0
- digitalhub/stores/sql/store.py +14 -57
- digitalhub/utils/exceptions.py +6 -0
- digitalhub/utils/generic_utils.py +9 -8
- digitalhub/utils/uri_utils.py +1 -1
- {digitalhub-0.9.1.dist-info → digitalhub-0.10.0b0.dist-info}/METADATA +5 -6
- {digitalhub-0.9.1.dist-info → digitalhub-0.10.0b0.dist-info}/RECORD +66 -53
- test/local/imports/test_imports.py +0 -1
- digitalhub/client/dhcore/env.py +0 -23
- /digitalhub/{readers/_commons → configurator}/__init__.py +0 -0
- {digitalhub-0.9.1.dist-info → digitalhub-0.10.0b0.dist-info}/LICENSE.txt +0 -0
- {digitalhub-0.9.1.dist-info → digitalhub-0.10.0b0.dist-info}/WHEEL +0 -0
- {digitalhub-0.9.1.dist-info → digitalhub-0.10.0b0.dist-info}/top_level.txt +0 -0
digitalhub/__init__.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
__version__ = "0.9.0b0"
|
|
2
1
|
from digitalhub.entities.artifact.crud import (
|
|
3
2
|
delete_artifact,
|
|
4
3
|
get_artifact,
|
|
@@ -85,15 +84,15 @@ from digitalhub.entities.workflow.crud import (
|
|
|
85
84
|
)
|
|
86
85
|
|
|
87
86
|
try:
|
|
88
|
-
from digitalhub.entities.model.mlflow.utils import from_mlflow_run
|
|
87
|
+
from digitalhub.entities.model.mlflow.utils import from_mlflow_run, get_mlflow_model_metrics
|
|
89
88
|
except ImportError:
|
|
90
89
|
...
|
|
91
90
|
|
|
92
91
|
from digitalhub.client.dhcore.utils import refresh_token, set_dhcore_env
|
|
92
|
+
from digitalhub.configurator.api import get_current_env, set_current_env
|
|
93
93
|
|
|
94
94
|
# Register entities into registry
|
|
95
95
|
from digitalhub.factory.utils import register_entities, register_runtimes_entities
|
|
96
|
-
from digitalhub.stores.api import set_store
|
|
97
96
|
|
|
98
97
|
register_entities()
|
|
99
98
|
register_runtimes_entities()
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import typing
|
|
4
4
|
from abc import abstractmethod
|
|
5
|
+
from typing import Any
|
|
5
6
|
|
|
6
7
|
if typing.TYPE_CHECKING:
|
|
7
8
|
from digitalhub.client._base.api_builder import ClientApiBuilder
|
|
@@ -26,7 +27,7 @@ class Client:
|
|
|
26
27
|
##############################
|
|
27
28
|
|
|
28
29
|
@abstractmethod
|
|
29
|
-
def create_object(self, api: str, obj:
|
|
30
|
+
def create_object(self, api: str, obj: Any, **kwargs) -> dict:
|
|
30
31
|
"""
|
|
31
32
|
Create object method.
|
|
32
33
|
"""
|
|
@@ -38,7 +39,7 @@ class Client:
|
|
|
38
39
|
"""
|
|
39
40
|
|
|
40
41
|
@abstractmethod
|
|
41
|
-
def update_object(self, api: str, obj:
|
|
42
|
+
def update_object(self, api: str, obj: Any, **kwargs) -> dict:
|
|
42
43
|
"""
|
|
43
44
|
Update object method.
|
|
44
45
|
"""
|
|
@@ -91,10 +91,15 @@ class ClientDHCoreApiBuilder(ClientApiBuilder):
|
|
|
91
91
|
elif operation == BackendOperations.RESUME.value:
|
|
92
92
|
return f"{API_CONTEXT}/{project}/{entity_type}/{kwargs['entity_id']}/resume"
|
|
93
93
|
elif operation == BackendOperations.DATA.value:
|
|
94
|
-
return f"{API_CONTEXT}/{project}/{entity_type}/
|
|
94
|
+
return f"{API_CONTEXT}/{project}/{entity_type}/data"
|
|
95
95
|
elif operation == BackendOperations.FILES.value:
|
|
96
96
|
return f"{API_CONTEXT}/{project}/{entity_type}/{kwargs['entity_id']}/files/info"
|
|
97
97
|
elif operation == BackendOperations.SEARCH.value:
|
|
98
98
|
return f"{API_CONTEXT}/{project}/solr/search/item"
|
|
99
|
+
elif operation == BackendOperations.METRICS.value:
|
|
100
|
+
if kwargs["metric_name"] is not None:
|
|
101
|
+
return f"{API_CONTEXT}/{project}/{entity_type}/{kwargs['entity_id']}/metrics/{kwargs['metric_name']}"
|
|
102
|
+
else:
|
|
103
|
+
return f"{API_CONTEXT}/{project}/{entity_type}/{kwargs['entity_id']}/metrics"
|
|
99
104
|
|
|
100
105
|
raise BackendError(f"Invalid operation '{operation}' for entity type '{entity_type}' in DHCore.")
|
|
@@ -1,31 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import datetime
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
3
|
import typing
|
|
7
|
-
from
|
|
4
|
+
from typing import Any
|
|
8
5
|
|
|
9
|
-
from dotenv import get_key, set_key
|
|
10
6
|
from requests import request
|
|
11
|
-
from requests.exceptions import
|
|
7
|
+
from requests.exceptions import JSONDecodeError
|
|
12
8
|
|
|
13
9
|
from digitalhub.client._base.client import Client
|
|
14
10
|
from digitalhub.client.dhcore.api_builder import ClientDHCoreApiBuilder
|
|
15
|
-
from digitalhub.client.dhcore.
|
|
16
|
-
from digitalhub.client.dhcore.
|
|
11
|
+
from digitalhub.client.dhcore.configurator import ClientDHCoreConfigurator
|
|
12
|
+
from digitalhub.client.dhcore.error_parser import ErrorParser
|
|
17
13
|
from digitalhub.client.dhcore.key_builder import ClientDHCoreKeyBuilder
|
|
18
|
-
from digitalhub.
|
|
19
|
-
from digitalhub.utils.
|
|
20
|
-
BackendError,
|
|
21
|
-
BadRequestError,
|
|
22
|
-
EntityAlreadyExistsError,
|
|
23
|
-
EntityNotExistsError,
|
|
24
|
-
ForbiddenError,
|
|
25
|
-
MissingSpecError,
|
|
26
|
-
UnauthorizedError,
|
|
27
|
-
)
|
|
28
|
-
from digitalhub.utils.uri_utils import has_remote_scheme
|
|
14
|
+
from digitalhub.utils.exceptions import BackendError
|
|
15
|
+
from digitalhub.utils.generic_utils import dump_json
|
|
29
16
|
|
|
30
17
|
if typing.TYPE_CHECKING:
|
|
31
18
|
from requests import Response
|
|
@@ -56,28 +43,18 @@ class ClientDHCore(Client):
|
|
|
56
43
|
# Key builder
|
|
57
44
|
self._key_builder = ClientDHCoreKeyBuilder()
|
|
58
45
|
|
|
59
|
-
#
|
|
60
|
-
self.
|
|
61
|
-
self._endpoint_issuer: str | None = None
|
|
46
|
+
# Error parser
|
|
47
|
+
self._error_parser = ErrorParser()
|
|
62
48
|
|
|
63
|
-
#
|
|
64
|
-
self.
|
|
65
|
-
|
|
66
|
-
# Basic
|
|
67
|
-
self._user: str | None = None
|
|
68
|
-
self._password: str | None = None
|
|
69
|
-
|
|
70
|
-
# OAuth2
|
|
71
|
-
self._access_token: str | None = None
|
|
72
|
-
self._refresh_token: str | None = None
|
|
73
|
-
|
|
74
|
-
self._configure(config)
|
|
49
|
+
# Client Configurator
|
|
50
|
+
self._configurator = ClientDHCoreConfigurator()
|
|
51
|
+
self._configurator.configure(config)
|
|
75
52
|
|
|
76
53
|
##############################
|
|
77
54
|
# CRUD methods
|
|
78
55
|
##############################
|
|
79
56
|
|
|
80
|
-
def create_object(self, api: str, obj:
|
|
57
|
+
def create_object(self, api: str, obj: Any, **kwargs) -> dict:
|
|
81
58
|
"""
|
|
82
59
|
Create an object in DHCore.
|
|
83
60
|
|
|
@@ -85,7 +62,7 @@ class ClientDHCore(Client):
|
|
|
85
62
|
----------
|
|
86
63
|
api : str
|
|
87
64
|
Create API.
|
|
88
|
-
obj :
|
|
65
|
+
obj : Any
|
|
89
66
|
Object to create.
|
|
90
67
|
**kwargs : dict
|
|
91
68
|
Keyword arguments to pass to the request.
|
|
@@ -98,7 +75,7 @@ class ClientDHCore(Client):
|
|
|
98
75
|
if "headers" not in kwargs:
|
|
99
76
|
kwargs["headers"] = {}
|
|
100
77
|
kwargs["headers"]["Content-Type"] = "application/json"
|
|
101
|
-
kwargs["data"] =
|
|
78
|
+
kwargs["data"] = dump_json(obj)
|
|
102
79
|
return self._prepare_call("POST", api, **kwargs)
|
|
103
80
|
|
|
104
81
|
def read_object(self, api: str, **kwargs) -> dict:
|
|
@@ -119,7 +96,7 @@ class ClientDHCore(Client):
|
|
|
119
96
|
"""
|
|
120
97
|
return self._prepare_call("GET", api, **kwargs)
|
|
121
98
|
|
|
122
|
-
def update_object(self, api: str, obj:
|
|
99
|
+
def update_object(self, api: str, obj: Any, **kwargs) -> dict:
|
|
123
100
|
"""
|
|
124
101
|
Update an object in DHCore.
|
|
125
102
|
|
|
@@ -140,7 +117,7 @@ class ClientDHCore(Client):
|
|
|
140
117
|
if "headers" not in kwargs:
|
|
141
118
|
kwargs["headers"] = {}
|
|
142
119
|
kwargs["headers"]["Content-Type"] = "application/json"
|
|
143
|
-
kwargs["data"] =
|
|
120
|
+
kwargs["data"] = dump_json(obj)
|
|
144
121
|
return self._prepare_call("PUT", api, **kwargs)
|
|
145
122
|
|
|
146
123
|
def delete_object(self, api: str, **kwargs) -> dict:
|
|
@@ -256,7 +233,7 @@ class ClientDHCore(Client):
|
|
|
256
233
|
if "sort" not in kwargs["params"]:
|
|
257
234
|
kwargs["params"]["sort"] = "metadata.updated,DESC"
|
|
258
235
|
|
|
259
|
-
objects_with_highlights = []
|
|
236
|
+
objects_with_highlights: list[dict] = []
|
|
260
237
|
while True:
|
|
261
238
|
resp = self._prepare_call("GET", api, **kwargs)
|
|
262
239
|
contents = resp["content"]
|
|
@@ -277,25 +254,6 @@ class ClientDHCore(Client):
|
|
|
277
254
|
# Call methods
|
|
278
255
|
##############################
|
|
279
256
|
|
|
280
|
-
@staticmethod
|
|
281
|
-
def _json_serialize(obj: dict) -> dict:
|
|
282
|
-
"""
|
|
283
|
-
JSON datetime to ISO format serializer.
|
|
284
|
-
|
|
285
|
-
Parameters
|
|
286
|
-
----------
|
|
287
|
-
obj : dict
|
|
288
|
-
The object to serialize.
|
|
289
|
-
|
|
290
|
-
Returns
|
|
291
|
-
-------
|
|
292
|
-
dict
|
|
293
|
-
The serialized object.
|
|
294
|
-
"""
|
|
295
|
-
if isinstance(obj, (datetime.datetime, datetime.date)):
|
|
296
|
-
return obj.isoformat()
|
|
297
|
-
raise TypeError("Type %s not serializable" % type(obj))
|
|
298
|
-
|
|
299
257
|
def _prepare_call(self, call_type: str, api: str, **kwargs) -> dict:
|
|
300
258
|
"""
|
|
301
259
|
Prepare a call to the DHCore API.
|
|
@@ -316,32 +274,10 @@ class ClientDHCore(Client):
|
|
|
316
274
|
"""
|
|
317
275
|
if kwargs is None:
|
|
318
276
|
kwargs = {}
|
|
319
|
-
url = self.
|
|
320
|
-
kwargs = self.
|
|
277
|
+
url = self._configurator.build_url(api)
|
|
278
|
+
kwargs = self._configurator.set_request_auth(kwargs)
|
|
321
279
|
return self._make_call(call_type, url, **kwargs)
|
|
322
280
|
|
|
323
|
-
def _set_auth(self, kwargs: dict) -> dict:
|
|
324
|
-
"""
|
|
325
|
-
Set the authentication type.
|
|
326
|
-
|
|
327
|
-
Parameters
|
|
328
|
-
----------
|
|
329
|
-
kwargs : dict
|
|
330
|
-
Keyword arguments to pass to the request.
|
|
331
|
-
|
|
332
|
-
Returns
|
|
333
|
-
-------
|
|
334
|
-
dict
|
|
335
|
-
Keyword arguments with the authentication parameters.
|
|
336
|
-
"""
|
|
337
|
-
if self._auth_type == AuthType.BASIC.value:
|
|
338
|
-
kwargs["auth"] = self._user, self._password
|
|
339
|
-
elif self._auth_type == AuthType.OAUTH2.value:
|
|
340
|
-
if "headers" not in kwargs:
|
|
341
|
-
kwargs["headers"] = {}
|
|
342
|
-
kwargs["headers"]["Authorization"] = f"Bearer {self._access_token}"
|
|
343
|
-
return kwargs
|
|
344
|
-
|
|
345
281
|
def _make_call(self, call_type: str, url: str, refresh_token: bool = True, **kwargs) -> dict:
|
|
346
282
|
"""
|
|
347
283
|
Make a call to the DHCore API.
|
|
@@ -364,126 +300,20 @@ class ClientDHCore(Client):
|
|
|
364
300
|
response = request(call_type, url, timeout=60, **kwargs)
|
|
365
301
|
|
|
366
302
|
# Evaluate DHCore API version
|
|
367
|
-
self.
|
|
303
|
+
self._configurator.check_core_version(response)
|
|
368
304
|
|
|
369
305
|
# Handle token refresh
|
|
370
|
-
if response.status_code in [401] and refresh_token:
|
|
371
|
-
self.
|
|
372
|
-
kwargs = self.
|
|
306
|
+
if response.status_code in [401] and refresh_token and self._configurator.oauth2_auth():
|
|
307
|
+
self._configurator.get_new_access_token()
|
|
308
|
+
kwargs = self._configurator.set_request_auth(kwargs)
|
|
373
309
|
return self._make_call(call_type, url, refresh_token=False, **kwargs)
|
|
374
310
|
|
|
375
|
-
self.
|
|
376
|
-
return self.
|
|
377
|
-
|
|
378
|
-
def _check_core_version(self, response: Response) -> None:
|
|
379
|
-
"""
|
|
380
|
-
Raise an exception if DHCore API version is not supported.
|
|
381
|
-
|
|
382
|
-
Parameters
|
|
383
|
-
----------
|
|
384
|
-
response : Response
|
|
385
|
-
The response object.
|
|
311
|
+
self._error_parser.parse(response)
|
|
312
|
+
return self._dictify_response(response)
|
|
386
313
|
|
|
387
|
-
|
|
388
|
-
-------
|
|
389
|
-
None
|
|
390
|
-
"""
|
|
391
|
-
if "X-Api-Level" in response.headers:
|
|
392
|
-
core_api_level = int(response.headers["X-Api-Level"])
|
|
393
|
-
if not (MIN_API_LEVEL <= core_api_level <= MAX_API_LEVEL):
|
|
394
|
-
raise BackendError("Backend API level not supported.")
|
|
395
|
-
if LIB_VERSION < core_api_level:
|
|
396
|
-
warn("Backend API level is higher than library version. You should consider updating the library.")
|
|
397
|
-
|
|
398
|
-
def _raise_for_error(self, response: Response) -> None:
|
|
399
|
-
"""
|
|
400
|
-
Handle DHCore API errors.
|
|
401
|
-
|
|
402
|
-
Parameters
|
|
403
|
-
----------
|
|
404
|
-
response : Response
|
|
405
|
-
The response object.
|
|
406
|
-
|
|
407
|
-
Returns
|
|
408
|
-
-------
|
|
409
|
-
None
|
|
410
|
-
"""
|
|
411
|
-
try:
|
|
412
|
-
response.raise_for_status()
|
|
413
|
-
|
|
414
|
-
# Backend errors
|
|
415
|
-
except RequestException as e:
|
|
416
|
-
# Handle timeout
|
|
417
|
-
if isinstance(e, TimeoutError):
|
|
418
|
-
msg = "Request to DHCore backend timed out."
|
|
419
|
-
raise TimeoutError(msg)
|
|
420
|
-
|
|
421
|
-
# Handle connection error
|
|
422
|
-
elif isinstance(e, ConnectionError):
|
|
423
|
-
msg = "Unable to connect to DHCore backend."
|
|
424
|
-
raise ConnectionError(msg)
|
|
425
|
-
|
|
426
|
-
# Handle HTTP errors
|
|
427
|
-
elif isinstance(e, HTTPError):
|
|
428
|
-
txt_resp = f"Response: {response.text}."
|
|
429
|
-
|
|
430
|
-
# Bad request
|
|
431
|
-
if response.status_code == 400:
|
|
432
|
-
# Missing spec in backend
|
|
433
|
-
if "missing spec" in response.text:
|
|
434
|
-
msg = f"Missing spec in backend. {txt_resp}"
|
|
435
|
-
raise MissingSpecError(msg)
|
|
436
|
-
|
|
437
|
-
# Duplicated entity
|
|
438
|
-
elif "Duplicated entity" in response.text:
|
|
439
|
-
msg = f"Entity already exists. {txt_resp}"
|
|
440
|
-
raise EntityAlreadyExistsError(msg)
|
|
441
|
-
|
|
442
|
-
# Other errors
|
|
443
|
-
else:
|
|
444
|
-
msg = f"Bad request. {txt_resp}"
|
|
445
|
-
raise BadRequestError(msg)
|
|
446
|
-
|
|
447
|
-
# Unauthorized errors
|
|
448
|
-
elif response.status_code == 401:
|
|
449
|
-
msg = f"Unauthorized. {txt_resp}"
|
|
450
|
-
raise UnauthorizedError(msg)
|
|
451
|
-
|
|
452
|
-
# Forbidden errors
|
|
453
|
-
elif response.status_code == 403:
|
|
454
|
-
msg = f"Forbidden. {txt_resp}"
|
|
455
|
-
raise ForbiddenError(msg)
|
|
456
|
-
|
|
457
|
-
# Entity not found
|
|
458
|
-
elif response.status_code == 404:
|
|
459
|
-
# Put with entity not found
|
|
460
|
-
if "No such EntityName" in response.text:
|
|
461
|
-
msg = f"Entity does not exists. {txt_resp}"
|
|
462
|
-
raise EntityNotExistsError(msg)
|
|
463
|
-
|
|
464
|
-
# Other cases
|
|
465
|
-
else:
|
|
466
|
-
msg = f"Not found. {txt_resp}"
|
|
467
|
-
raise BackendError(msg)
|
|
468
|
-
|
|
469
|
-
# Other errors
|
|
470
|
-
else:
|
|
471
|
-
msg = f"Backend error. {txt_resp}"
|
|
472
|
-
raise BackendError(msg) from e
|
|
473
|
-
|
|
474
|
-
# Other requests errors
|
|
475
|
-
else:
|
|
476
|
-
msg = f"Some error occurred. {e}"
|
|
477
|
-
raise BackendError(msg) from e
|
|
478
|
-
|
|
479
|
-
# Other generic errors
|
|
480
|
-
except Exception as e:
|
|
481
|
-
msg = f"Some error occurred: {e}"
|
|
482
|
-
raise RuntimeError(msg) from e
|
|
483
|
-
|
|
484
|
-
def _parse_response(self, response: Response) -> dict:
|
|
314
|
+
def _dictify_response(self, response: Response) -> dict:
|
|
485
315
|
"""
|
|
486
|
-
|
|
316
|
+
Return dict from response.
|
|
487
317
|
|
|
488
318
|
Parameters
|
|
489
319
|
----------
|
|
@@ -502,208 +332,6 @@ class ClientDHCore(Client):
|
|
|
502
332
|
return {}
|
|
503
333
|
raise BackendError("Backend response could not be parsed.")
|
|
504
334
|
|
|
505
|
-
##############################
|
|
506
|
-
# Configuration methods
|
|
507
|
-
##############################
|
|
508
|
-
|
|
509
|
-
def _configure(self, config: dict | None = None) -> None:
|
|
510
|
-
"""
|
|
511
|
-
Configure the client attributes with config (given or from
|
|
512
|
-
environment).
|
|
513
|
-
Regarding authentication parameters, the config parameter
|
|
514
|
-
takes precedence over the env variables, and the token
|
|
515
|
-
over the basic auth. Furthermore, the config parameter is
|
|
516
|
-
validated against the proper pydantic model.
|
|
517
|
-
|
|
518
|
-
Parameters
|
|
519
|
-
----------
|
|
520
|
-
config : dict
|
|
521
|
-
Configuration dictionary.
|
|
522
|
-
|
|
523
|
-
Returns
|
|
524
|
-
-------
|
|
525
|
-
None
|
|
526
|
-
"""
|
|
527
|
-
self._get_endpoints_from_env()
|
|
528
|
-
|
|
529
|
-
if config is not None:
|
|
530
|
-
if config.get("access_token") is not None:
|
|
531
|
-
config = OAuth2TokenAuth(**config)
|
|
532
|
-
self._user = config.user
|
|
533
|
-
self._access_token = config.access_token
|
|
534
|
-
self._refresh_token = config.refresh_token
|
|
535
|
-
self._client_id = config.client_id
|
|
536
|
-
self._auth_type = AuthType.OAUTH2.value
|
|
537
|
-
|
|
538
|
-
elif config.get("user") is not None and config.get("password") is not None:
|
|
539
|
-
config = BasicAuth(**config)
|
|
540
|
-
self._user = config.user
|
|
541
|
-
self._password = config.password
|
|
542
|
-
self._auth_type = AuthType.BASIC.value
|
|
543
|
-
|
|
544
|
-
return
|
|
545
|
-
|
|
546
|
-
self._get_auth_from_env()
|
|
547
|
-
|
|
548
|
-
# Propagate access and refresh token to env file
|
|
549
|
-
self._write_env()
|
|
550
|
-
|
|
551
|
-
def _get_endpoints_from_env(self) -> None:
|
|
552
|
-
"""
|
|
553
|
-
Get the DHCore endpoint and token issuer endpoint from env.
|
|
554
|
-
|
|
555
|
-
Returns
|
|
556
|
-
-------
|
|
557
|
-
None
|
|
558
|
-
|
|
559
|
-
Raises
|
|
560
|
-
------
|
|
561
|
-
Exception
|
|
562
|
-
If the endpoint of DHCore is not set in the env variables.
|
|
563
|
-
"""
|
|
564
|
-
core_endpt = os.getenv(DhcoreEnvVar.ENDPOINT.value)
|
|
565
|
-
if core_endpt is None:
|
|
566
|
-
raise BackendError("Endpoint not set as environment variables.")
|
|
567
|
-
self._endpoint_core = self._sanitize_endpoint(core_endpt)
|
|
568
|
-
|
|
569
|
-
issr_endpt = os.getenv(DhcoreEnvVar.ISSUER.value)
|
|
570
|
-
if issr_endpt is not None:
|
|
571
|
-
self._endpoint_issuer = self._sanitize_endpoint(issr_endpt)
|
|
572
|
-
|
|
573
|
-
def _sanitize_endpoint(self, endpoint: str) -> str:
|
|
574
|
-
"""
|
|
575
|
-
Sanitize the endpoint.
|
|
576
|
-
|
|
577
|
-
Returns
|
|
578
|
-
-------
|
|
579
|
-
None
|
|
580
|
-
"""
|
|
581
|
-
if not has_remote_scheme(endpoint):
|
|
582
|
-
raise BackendError("Invalid endpoint scheme. Must start with http:// or https://.")
|
|
583
|
-
|
|
584
|
-
endpoint = endpoint.strip()
|
|
585
|
-
return endpoint.removesuffix("/")
|
|
586
|
-
|
|
587
|
-
def _get_auth_from_env(self) -> None:
|
|
588
|
-
"""
|
|
589
|
-
Get authentication parameters from the env.
|
|
590
|
-
|
|
591
|
-
Returns
|
|
592
|
-
-------
|
|
593
|
-
None
|
|
594
|
-
"""
|
|
595
|
-
self._user = os.getenv(DhcoreEnvVar.USER.value, FALLBACK_USER)
|
|
596
|
-
self._refresh_token = os.getenv(DhcoreEnvVar.REFRESH_TOKEN.value)
|
|
597
|
-
self._client_id = os.getenv(DhcoreEnvVar.CLIENT_ID.value)
|
|
598
|
-
|
|
599
|
-
token = os.getenv(DhcoreEnvVar.ACCESS_TOKEN.value)
|
|
600
|
-
if token is not None and token != "":
|
|
601
|
-
self._auth_type = AuthType.OAUTH2.value
|
|
602
|
-
self._access_token = token
|
|
603
|
-
return
|
|
604
|
-
|
|
605
|
-
password = os.getenv(DhcoreEnvVar.PASSWORD.value)
|
|
606
|
-
if self._user is not None and password is not None:
|
|
607
|
-
self._auth_type = AuthType.BASIC.value
|
|
608
|
-
self._password = password
|
|
609
|
-
return
|
|
610
|
-
|
|
611
|
-
def _get_new_access_token(self) -> None:
|
|
612
|
-
"""
|
|
613
|
-
Get a new access token.
|
|
614
|
-
|
|
615
|
-
Returns
|
|
616
|
-
-------
|
|
617
|
-
None
|
|
618
|
-
"""
|
|
619
|
-
# Call issuer and get endpoint for
|
|
620
|
-
# refreshing access token
|
|
621
|
-
url = self._get_refresh_endpoint()
|
|
622
|
-
|
|
623
|
-
# Call refresh token endpoint
|
|
624
|
-
# Try token from env
|
|
625
|
-
refresh_token = os.getenv(DhcoreEnvVar.REFRESH_TOKEN.value)
|
|
626
|
-
response = self._call_refresh_token_endpoint(url, refresh_token)
|
|
627
|
-
|
|
628
|
-
# Otherwise try token from file
|
|
629
|
-
if response.status_code in (400, 401, 403):
|
|
630
|
-
refresh_token = get_key(ENV_FILE, DhcoreEnvVar.REFRESH_TOKEN.value)
|
|
631
|
-
response = self._call_refresh_token_endpoint(url, refresh_token)
|
|
632
|
-
|
|
633
|
-
response.raise_for_status()
|
|
634
|
-
dict_response = response.json()
|
|
635
|
-
|
|
636
|
-
# Read new access token and refresh token
|
|
637
|
-
self._access_token = dict_response["access_token"]
|
|
638
|
-
self._refresh_token = dict_response["refresh_token"]
|
|
639
|
-
|
|
640
|
-
# Propagate new access token to env
|
|
641
|
-
self._write_env()
|
|
642
|
-
|
|
643
|
-
def _get_refresh_endpoint(self) -> str:
|
|
644
|
-
"""
|
|
645
|
-
Get the refresh endpoint.
|
|
646
|
-
|
|
647
|
-
Returns
|
|
648
|
-
-------
|
|
649
|
-
str
|
|
650
|
-
Refresh endpoint.
|
|
651
|
-
"""
|
|
652
|
-
# Get issuer endpoint
|
|
653
|
-
if self._endpoint_issuer is None:
|
|
654
|
-
raise BackendError("Issuer endpoint not set.")
|
|
655
|
-
|
|
656
|
-
# Standard issuer endpoint path
|
|
657
|
-
url = self._endpoint_issuer + "/.well-known/openid-configuration"
|
|
658
|
-
|
|
659
|
-
# Call
|
|
660
|
-
r = request("GET", url, timeout=60)
|
|
661
|
-
self._raise_for_error(r)
|
|
662
|
-
return r.json().get("token_endpoint")
|
|
663
|
-
|
|
664
|
-
def _call_refresh_token_endpoint(self, url: str, refresh_token: str) -> Response:
|
|
665
|
-
"""
|
|
666
|
-
Call the refresh token endpoint.
|
|
667
|
-
|
|
668
|
-
Parameters
|
|
669
|
-
----------
|
|
670
|
-
url : str
|
|
671
|
-
Refresh token endpoint.
|
|
672
|
-
refresh_token : str
|
|
673
|
-
Refresh token.
|
|
674
|
-
|
|
675
|
-
Returns
|
|
676
|
-
-------
|
|
677
|
-
Response
|
|
678
|
-
Response object.
|
|
679
|
-
"""
|
|
680
|
-
# Send request to get new access token
|
|
681
|
-
payload = {
|
|
682
|
-
"grant_type": "refresh_token",
|
|
683
|
-
"client_id": self._client_id,
|
|
684
|
-
"refresh_token": refresh_token,
|
|
685
|
-
}
|
|
686
|
-
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
687
|
-
return request("POST", url, data=payload, headers=headers, timeout=60)
|
|
688
|
-
|
|
689
|
-
def _write_env(self) -> None:
|
|
690
|
-
"""
|
|
691
|
-
Write the env variables to the .dhcore file.
|
|
692
|
-
It will overwrite any existing env variables.
|
|
693
|
-
|
|
694
|
-
Returns
|
|
695
|
-
-------
|
|
696
|
-
None
|
|
697
|
-
"""
|
|
698
|
-
keys = {}
|
|
699
|
-
if self._access_token is not None:
|
|
700
|
-
keys[DhcoreEnvVar.ACCESS_TOKEN.value] = self._access_token
|
|
701
|
-
if self._refresh_token is not None:
|
|
702
|
-
keys[DhcoreEnvVar.REFRESH_TOKEN.value] = self._refresh_token
|
|
703
|
-
|
|
704
|
-
for k, v in keys.items():
|
|
705
|
-
set_key(dotenv_path=ENV_FILE, key_to_set=k, value_to_set=v)
|
|
706
|
-
|
|
707
335
|
##############################
|
|
708
336
|
# Interface methods
|
|
709
337
|
##############################
|