digitalhub 0.11.0b7__py3-none-any.whl → 0.13.0__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 +4 -1
- digitalhub/context/api.py +9 -5
- digitalhub/context/builder.py +7 -5
- digitalhub/context/context.py +13 -1
- digitalhub/entities/__init__.py +3 -0
- digitalhub/entities/_base/__init__.py +3 -0
- digitalhub/entities/_base/_base/__init__.py +3 -0
- digitalhub/entities/_base/_base/entity.py +4 -0
- digitalhub/entities/_base/context/__init__.py +3 -0
- digitalhub/entities/_base/context/entity.py +4 -0
- digitalhub/entities/_base/entity/__init__.py +3 -0
- digitalhub/entities/_base/entity/_constructors/__init__.py +3 -0
- digitalhub/entities/_base/entity/_constructors/metadata.py +4 -0
- digitalhub/entities/_base/entity/_constructors/name.py +4 -0
- digitalhub/entities/_base/entity/_constructors/spec.py +4 -0
- digitalhub/entities/_base/entity/_constructors/status.py +4 -0
- digitalhub/entities/_base/entity/_constructors/uuid.py +4 -0
- digitalhub/entities/_base/entity/builder.py +4 -0
- digitalhub/entities/_base/entity/entity.py +4 -0
- digitalhub/entities/_base/entity/metadata.py +4 -0
- digitalhub/entities/_base/entity/spec.py +4 -0
- digitalhub/entities/_base/entity/status.py +4 -0
- digitalhub/entities/_base/executable/__init__.py +3 -0
- digitalhub/entities/_base/executable/entity.py +109 -57
- digitalhub/entities/_base/material/__init__.py +3 -0
- digitalhub/entities/_base/material/entity.py +15 -18
- digitalhub/entities/_base/material/spec.py +4 -0
- digitalhub/entities/_base/material/status.py +4 -0
- digitalhub/entities/_base/material/utils.py +5 -1
- digitalhub/entities/_base/runtime_entity/__init__.py +3 -0
- digitalhub/entities/_base/runtime_entity/builder.py +4 -0
- digitalhub/entities/_base/unversioned/__init__.py +3 -0
- digitalhub/entities/_base/unversioned/builder.py +4 -0
- digitalhub/entities/_base/unversioned/entity.py +4 -0
- digitalhub/entities/_base/versioned/__init__.py +3 -0
- digitalhub/entities/_base/versioned/builder.py +4 -0
- digitalhub/entities/_base/versioned/entity.py +4 -0
- digitalhub/entities/_commons/__init__.py +3 -0
- digitalhub/entities/_commons/enums.py +4 -0
- digitalhub/entities/_commons/metrics.py +68 -30
- digitalhub/entities/_commons/utils.py +40 -9
- digitalhub/entities/_processors/__init__.py +3 -0
- digitalhub/entities/_processors/base.py +154 -79
- digitalhub/entities/_processors/context.py +370 -215
- digitalhub/entities/_processors/utils.py +78 -30
- digitalhub/entities/artifact/__init__.py +3 -0
- digitalhub/entities/artifact/_base/__init__.py +3 -0
- digitalhub/entities/artifact/_base/builder.py +4 -0
- digitalhub/entities/artifact/_base/entity.py +4 -0
- digitalhub/entities/artifact/_base/spec.py +4 -0
- digitalhub/entities/artifact/_base/status.py +4 -0
- digitalhub/entities/artifact/artifact/__init__.py +3 -0
- digitalhub/entities/artifact/artifact/builder.py +4 -0
- digitalhub/entities/artifact/artifact/entity.py +4 -0
- digitalhub/entities/artifact/artifact/spec.py +4 -0
- digitalhub/entities/artifact/artifact/status.py +4 -0
- digitalhub/entities/artifact/crud.py +8 -0
- digitalhub/entities/artifact/utils.py +32 -13
- digitalhub/entities/builders.py +4 -0
- digitalhub/entities/dataitem/__init__.py +3 -0
- digitalhub/entities/dataitem/_base/__init__.py +3 -0
- digitalhub/entities/dataitem/_base/builder.py +4 -0
- digitalhub/entities/dataitem/_base/entity.py +4 -0
- digitalhub/entities/dataitem/_base/spec.py +4 -0
- digitalhub/entities/dataitem/_base/status.py +4 -0
- digitalhub/entities/dataitem/crud.py +18 -2
- digitalhub/entities/dataitem/dataitem/__init__.py +3 -0
- digitalhub/entities/dataitem/dataitem/builder.py +4 -0
- digitalhub/entities/dataitem/dataitem/entity.py +4 -0
- digitalhub/entities/dataitem/dataitem/spec.py +4 -0
- digitalhub/entities/dataitem/dataitem/status.py +4 -0
- digitalhub/entities/dataitem/iceberg/__init__.py +3 -0
- digitalhub/entities/dataitem/iceberg/builder.py +4 -0
- digitalhub/entities/dataitem/iceberg/entity.py +4 -0
- digitalhub/entities/dataitem/iceberg/spec.py +4 -0
- digitalhub/entities/dataitem/iceberg/status.py +4 -0
- digitalhub/entities/dataitem/table/__init__.py +3 -0
- digitalhub/entities/dataitem/table/builder.py +4 -0
- digitalhub/entities/dataitem/table/entity.py +7 -3
- digitalhub/entities/dataitem/table/models.py +4 -0
- digitalhub/entities/dataitem/table/spec.py +4 -0
- digitalhub/entities/dataitem/table/status.py +4 -0
- digitalhub/entities/dataitem/table/utils.py +4 -0
- digitalhub/entities/dataitem/utils.py +88 -35
- digitalhub/entities/function/__init__.py +3 -0
- digitalhub/entities/function/_base/__init__.py +3 -0
- digitalhub/entities/function/_base/builder.py +4 -0
- digitalhub/entities/function/_base/entity.py +4 -0
- digitalhub/entities/function/_base/spec.py +4 -0
- digitalhub/entities/function/_base/status.py +4 -0
- digitalhub/entities/function/crud.py +4 -0
- digitalhub/entities/model/__init__.py +3 -0
- digitalhub/entities/model/_base/__init__.py +3 -0
- digitalhub/entities/model/_base/builder.py +4 -0
- digitalhub/entities/model/_base/entity.py +4 -0
- digitalhub/entities/model/_base/spec.py +4 -0
- digitalhub/entities/model/_base/status.py +4 -0
- digitalhub/entities/model/crud.py +8 -0
- digitalhub/entities/model/huggingface/__init__.py +3 -0
- digitalhub/entities/model/huggingface/builder.py +4 -0
- digitalhub/entities/model/huggingface/entity.py +4 -0
- digitalhub/entities/model/huggingface/spec.py +4 -0
- digitalhub/entities/model/huggingface/status.py +4 -0
- digitalhub/entities/model/mlflow/__init__.py +3 -0
- digitalhub/entities/model/mlflow/builder.py +4 -0
- digitalhub/entities/model/mlflow/entity.py +4 -0
- digitalhub/entities/model/mlflow/models.py +4 -0
- digitalhub/entities/model/mlflow/spec.py +4 -0
- digitalhub/entities/model/mlflow/status.py +4 -0
- digitalhub/entities/model/mlflow/utils.py +4 -0
- digitalhub/entities/model/model/__init__.py +3 -0
- digitalhub/entities/model/model/builder.py +4 -0
- digitalhub/entities/model/model/entity.py +4 -0
- digitalhub/entities/model/model/spec.py +4 -0
- digitalhub/entities/model/model/status.py +4 -0
- digitalhub/entities/model/sklearn/__init__.py +3 -0
- digitalhub/entities/model/sklearn/builder.py +4 -0
- digitalhub/entities/model/sklearn/entity.py +4 -0
- digitalhub/entities/model/sklearn/spec.py +4 -0
- digitalhub/entities/model/sklearn/status.py +4 -0
- digitalhub/entities/model/utils.py +32 -13
- digitalhub/entities/project/__init__.py +3 -0
- digitalhub/entities/project/_base/__init__.py +3 -0
- digitalhub/entities/project/_base/builder.py +4 -0
- digitalhub/entities/project/_base/entity.py +4 -2
- digitalhub/entities/project/_base/models.py +4 -0
- digitalhub/entities/project/_base/spec.py +4 -0
- digitalhub/entities/project/_base/status.py +4 -0
- digitalhub/entities/project/crud.py +4 -0
- digitalhub/entities/project/utils.py +4 -0
- digitalhub/entities/run/__init__.py +3 -0
- digitalhub/entities/run/_base/__init__.py +3 -0
- digitalhub/entities/run/_base/builder.py +4 -0
- digitalhub/entities/run/_base/entity.py +6 -2
- digitalhub/entities/run/_base/spec.py +4 -0
- digitalhub/entities/run/_base/status.py +4 -0
- digitalhub/entities/run/crud.py +4 -0
- digitalhub/entities/secret/__init__.py +3 -0
- digitalhub/entities/secret/_base/__init__.py +3 -0
- digitalhub/entities/secret/_base/builder.py +4 -0
- digitalhub/entities/secret/_base/entity.py +4 -0
- digitalhub/entities/secret/_base/spec.py +4 -0
- digitalhub/entities/secret/_base/status.py +4 -0
- digitalhub/entities/secret/crud.py +4 -0
- digitalhub/entities/task/__init__.py +3 -0
- digitalhub/entities/task/_base/__init__.py +3 -0
- digitalhub/entities/task/_base/builder.py +4 -0
- digitalhub/entities/task/_base/entity.py +4 -0
- digitalhub/entities/task/_base/models.py +16 -3
- digitalhub/entities/task/_base/spec.py +4 -0
- digitalhub/entities/task/_base/status.py +4 -0
- digitalhub/entities/task/_base/utils.py +4 -0
- digitalhub/entities/task/crud.py +4 -0
- digitalhub/entities/trigger/__init__.py +3 -0
- digitalhub/entities/trigger/_base/__init__.py +3 -0
- digitalhub/entities/trigger/_base/builder.py +4 -0
- digitalhub/entities/trigger/_base/entity.py +15 -0
- digitalhub/entities/trigger/_base/spec.py +4 -0
- digitalhub/entities/trigger/_base/status.py +4 -0
- digitalhub/entities/trigger/crud.py +4 -0
- digitalhub/entities/trigger/lifecycle/__init__.py +3 -0
- digitalhub/entities/trigger/lifecycle/builder.py +4 -0
- digitalhub/entities/trigger/lifecycle/entity.py +4 -0
- digitalhub/entities/trigger/lifecycle/spec.py +4 -0
- digitalhub/entities/trigger/lifecycle/status.py +4 -0
- digitalhub/entities/trigger/scheduler/__init__.py +3 -0
- digitalhub/entities/trigger/scheduler/builder.py +4 -0
- digitalhub/entities/trigger/scheduler/entity.py +4 -0
- digitalhub/entities/trigger/scheduler/spec.py +4 -0
- digitalhub/entities/trigger/scheduler/status.py +4 -0
- digitalhub/entities/workflow/__init__.py +3 -0
- digitalhub/entities/workflow/_base/__init__.py +3 -0
- digitalhub/entities/workflow/_base/builder.py +4 -0
- digitalhub/entities/workflow/_base/entity.py +4 -0
- digitalhub/entities/workflow/_base/spec.py +4 -0
- digitalhub/entities/workflow/_base/status.py +4 -0
- digitalhub/entities/workflow/crud.py +4 -0
- digitalhub/factory/__init__.py +3 -0
- digitalhub/factory/factory.py +29 -3
- digitalhub/factory/utils.py +15 -3
- digitalhub/runtimes/__init__.py +3 -0
- digitalhub/runtimes/_base.py +5 -1
- digitalhub/runtimes/builder.py +22 -1
- digitalhub/runtimes/enums.py +4 -0
- digitalhub/stores/__init__.py +3 -0
- digitalhub/stores/client/__init__.py +15 -0
- digitalhub/stores/client/_base/__init__.py +3 -0
- digitalhub/stores/client/_base/api_builder.py +18 -0
- digitalhub/stores/client/_base/client.py +97 -0
- digitalhub/stores/client/_base/key_builder.py +32 -0
- digitalhub/stores/client/_base/params_builder.py +18 -0
- digitalhub/stores/client/api.py +14 -5
- digitalhub/stores/client/builder.py +7 -1
- digitalhub/stores/client/dhcore/__init__.py +3 -0
- digitalhub/stores/client/dhcore/api_builder.py +21 -0
- digitalhub/stores/client/dhcore/client.py +329 -70
- digitalhub/stores/client/dhcore/configurator.py +489 -193
- digitalhub/stores/client/dhcore/enums.py +7 -0
- digitalhub/stores/client/dhcore/error_parser.py +39 -1
- digitalhub/stores/client/dhcore/key_builder.py +4 -0
- digitalhub/stores/client/dhcore/models.py +4 -0
- digitalhub/stores/client/dhcore/params_builder.py +117 -17
- digitalhub/stores/client/dhcore/utils.py +44 -22
- digitalhub/stores/client/local/__init__.py +3 -0
- digitalhub/stores/client/local/api_builder.py +21 -0
- digitalhub/stores/client/local/client.py +10 -8
- digitalhub/stores/client/local/enums.py +4 -0
- digitalhub/stores/client/local/key_builder.py +4 -0
- digitalhub/stores/client/local/params_builder.py +4 -0
- digitalhub/stores/credentials/__init__.py +3 -0
- digitalhub/stores/credentials/api.py +35 -0
- digitalhub/stores/credentials/configurator.py +210 -0
- digitalhub/stores/credentials/enums.py +68 -0
- digitalhub/stores/credentials/handler.py +176 -0
- digitalhub/stores/credentials/ini_module.py +164 -0
- digitalhub/stores/credentials/store.py +81 -0
- digitalhub/stores/data/__init__.py +3 -0
- digitalhub/stores/data/_base/__init__.py +3 -0
- digitalhub/stores/data/_base/store.py +31 -9
- digitalhub/stores/data/api.py +53 -9
- digitalhub/stores/data/builder.py +94 -41
- digitalhub/stores/data/enums.py +4 -0
- digitalhub/stores/data/local/__init__.py +3 -0
- digitalhub/stores/data/local/store.py +8 -7
- digitalhub/stores/data/remote/__init__.py +3 -0
- digitalhub/stores/data/remote/store.py +8 -7
- digitalhub/stores/data/s3/__init__.py +3 -0
- digitalhub/stores/data/s3/configurator.py +69 -80
- digitalhub/stores/data/s3/store.py +73 -81
- digitalhub/stores/data/s3/utils.py +14 -10
- digitalhub/stores/data/sql/__init__.py +3 -0
- digitalhub/stores/data/sql/configurator.py +80 -73
- digitalhub/stores/data/sql/store.py +195 -102
- digitalhub/stores/readers/__init__.py +3 -0
- digitalhub/stores/readers/data/__init__.py +3 -0
- digitalhub/stores/readers/data/_base/__init__.py +3 -0
- digitalhub/stores/readers/data/_base/builder.py +4 -0
- digitalhub/stores/readers/data/_base/reader.py +4 -0
- digitalhub/stores/readers/data/api.py +4 -0
- digitalhub/stores/readers/data/factory.py +4 -0
- digitalhub/stores/readers/data/pandas/__init__.py +3 -0
- digitalhub/stores/readers/data/pandas/builder.py +4 -0
- digitalhub/stores/readers/data/pandas/reader.py +4 -0
- digitalhub/stores/readers/query/__init__.py +3 -0
- digitalhub/utils/__init__.py +3 -0
- digitalhub/utils/enums.py +4 -0
- digitalhub/utils/exceptions.py +10 -0
- digitalhub/utils/file_utils.py +57 -30
- digitalhub/utils/generic_utils.py +45 -33
- digitalhub/utils/git_utils.py +28 -14
- digitalhub/utils/io_utils.py +23 -18
- digitalhub/utils/logger.py +4 -0
- digitalhub/utils/types.py +4 -0
- digitalhub/utils/uri_utils.py +35 -31
- digitalhub-0.13.0.dist-info/METADATA +301 -0
- digitalhub-0.13.0.dist-info/RECORD +259 -0
- digitalhub-0.13.0.dist-info/licenses/AUTHORS +5 -0
- digitalhub-0.13.0.dist-info/licenses/LICENSE +201 -0
- digitalhub/entities/_commons/types.py +0 -5
- digitalhub/stores/configurator/__init__.py +0 -0
- digitalhub/stores/configurator/api.py +0 -31
- digitalhub/stores/configurator/configurator.py +0 -198
- digitalhub/stores/configurator/credentials_store.py +0 -65
- digitalhub/stores/configurator/enums.py +0 -21
- digitalhub/stores/configurator/ini_module.py +0 -128
- digitalhub/stores/data/s3/enums.py +0 -16
- digitalhub/stores/data/sql/enums.py +0 -16
- digitalhub/stores/data/utils.py +0 -34
- digitalhub-0.11.0b7.dist-info/METADATA +0 -259
- digitalhub-0.11.0b7.dist-info/RECORD +0 -261
- digitalhub-0.11.0b7.dist-info/licenses/LICENSE.txt +0 -216
- {digitalhub-0.11.0b7.dist-info → digitalhub-0.13.0.dist-info}/WHEEL +0 -0
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2025 DSLab - Fondazione Bruno Kessler
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
1
5
|
from __future__ import annotations
|
|
2
6
|
|
|
3
7
|
import typing
|
|
4
|
-
from warnings import warn
|
|
5
8
|
|
|
6
9
|
from requests import request
|
|
7
10
|
|
|
8
|
-
from digitalhub.stores.client.dhcore.enums import AuthType
|
|
9
|
-
from digitalhub.stores.
|
|
10
|
-
from digitalhub.stores.
|
|
11
|
-
from digitalhub.stores.
|
|
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
|
|
12
15
|
from digitalhub.utils.exceptions import ClientError
|
|
13
16
|
from digitalhub.utils.generic_utils import list_enum
|
|
14
17
|
from digitalhub.utils.uri_utils import has_remote_scheme
|
|
@@ -17,288 +20,503 @@ if typing.TYPE_CHECKING:
|
|
|
17
20
|
from requests import Response
|
|
18
21
|
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
AUTH_KEY = "_auth"
|
|
22
|
-
|
|
23
|
-
# API levels that are supported
|
|
24
|
-
MAX_API_LEVEL = 20
|
|
25
|
-
MIN_API_LEVEL = 11
|
|
26
|
-
LIB_VERSION = 11
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class ClientDHCoreConfigurator:
|
|
23
|
+
class ClientDHCoreConfigurator(Configurator):
|
|
30
24
|
"""
|
|
31
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.
|
|
32
52
|
"""
|
|
33
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
|
+
|
|
34
63
|
def __init__(self) -> None:
|
|
35
|
-
|
|
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()
|
|
36
77
|
|
|
37
78
|
##############################
|
|
38
|
-
#
|
|
79
|
+
# Credentials methods
|
|
39
80
|
##############################
|
|
40
81
|
|
|
41
|
-
def
|
|
82
|
+
def load_env_vars(self) -> None:
|
|
42
83
|
"""
|
|
43
|
-
|
|
84
|
+
Load credentials from environment variables.
|
|
44
85
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
Configuration dictionary.
|
|
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.
|
|
49
89
|
|
|
50
90
|
Returns
|
|
51
91
|
-------
|
|
52
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.
|
|
53
98
|
"""
|
|
54
|
-
|
|
55
|
-
|
|
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)
|
|
56
102
|
|
|
57
|
-
def
|
|
103
|
+
def _sanitize_env_vars(self, creds: dict) -> dict:
|
|
58
104
|
"""
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
over the basic auth. Furthermore, the config parameter is
|
|
64
|
-
validated against the proper pydantic model.
|
|
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.
|
|
65
109
|
|
|
66
110
|
Parameters
|
|
67
111
|
----------
|
|
68
|
-
|
|
69
|
-
|
|
112
|
+
creds : dict
|
|
113
|
+
Raw credentials dictionary loaded from environment variables.
|
|
70
114
|
|
|
71
115
|
Returns
|
|
72
116
|
-------
|
|
73
|
-
|
|
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.
|
|
74
129
|
"""
|
|
75
|
-
self.
|
|
76
|
-
self.
|
|
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
|
|
77
133
|
|
|
78
|
-
def
|
|
134
|
+
def load_file_vars(self) -> None:
|
|
79
135
|
"""
|
|
80
|
-
|
|
136
|
+
Load credentials from configuration file.
|
|
81
137
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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.
|
|
86
142
|
|
|
87
143
|
Returns
|
|
88
144
|
-------
|
|
89
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
|
|
90
153
|
"""
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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)
|
|
97
161
|
|
|
98
|
-
|
|
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:
|
|
99
172
|
"""
|
|
100
|
-
|
|
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.
|
|
101
178
|
|
|
102
179
|
Parameters
|
|
103
180
|
----------
|
|
104
|
-
|
|
105
|
-
|
|
181
|
+
creds : dict
|
|
182
|
+
Raw credentials dictionary loaded from configuration file.
|
|
106
183
|
|
|
107
184
|
Returns
|
|
108
185
|
-------
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
api = api.removeprefix("/")
|
|
113
|
-
return f"{configurator.get_credential(DhcoreEnvVar.ENDPOINT.value)}/{api}"
|
|
186
|
+
dict
|
|
187
|
+
Sanitized credentials dictionary with standardized key names
|
|
188
|
+
and normalized URLs, filtered to include only valid keys.
|
|
114
189
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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}
|
|
118
213
|
|
|
119
214
|
@staticmethod
|
|
120
|
-
def _sanitize_endpoint(endpoint: str) -> str:
|
|
215
|
+
def _sanitize_endpoint(endpoint: str | None = None) -> str | None:
|
|
121
216
|
"""
|
|
122
|
-
Sanitize
|
|
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.
|
|
123
226
|
|
|
124
227
|
Returns
|
|
125
228
|
-------
|
|
126
|
-
None
|
|
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.
|
|
127
242
|
"""
|
|
243
|
+
if endpoint is None:
|
|
244
|
+
return
|
|
128
245
|
if not has_remote_scheme(endpoint):
|
|
129
246
|
raise ClientError("Invalid endpoint scheme. Must start with http:// or https://.")
|
|
130
247
|
|
|
131
248
|
endpoint = endpoint.strip()
|
|
132
249
|
return endpoint.removesuffix("/")
|
|
133
250
|
|
|
134
|
-
def
|
|
251
|
+
def get_endpoint(self) -> str:
|
|
135
252
|
"""
|
|
136
|
-
Get the DHCore endpoint
|
|
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).
|
|
137
257
|
|
|
138
258
|
Returns
|
|
139
259
|
-------
|
|
140
|
-
|
|
260
|
+
str
|
|
261
|
+
The DHCore backend endpoint URL.
|
|
141
262
|
|
|
142
263
|
Raises
|
|
143
264
|
------
|
|
144
|
-
|
|
145
|
-
If the endpoint
|
|
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.
|
|
146
272
|
"""
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
273
|
+
creds = self._creds_handler.get_credentials(self._origin)
|
|
274
|
+
return creds[CredsEnvVar.DHCORE_ENDPOINT.value]
|
|
275
|
+
|
|
276
|
+
##############################
|
|
277
|
+
# Origin methods
|
|
278
|
+
##############################
|
|
152
279
|
|
|
153
|
-
def
|
|
280
|
+
def change_origin(self) -> None:
|
|
154
281
|
"""
|
|
155
|
-
|
|
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.
|
|
156
288
|
|
|
157
289
|
Returns
|
|
158
290
|
-------
|
|
159
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.
|
|
160
298
|
"""
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
configurator.set_credential(DhcoreEnvVar.ACCESS_TOKEN.value.removeprefix("DHCORE_"), access_token)
|
|
166
|
-
|
|
167
|
-
# Fallback to basic
|
|
168
|
-
else:
|
|
169
|
-
user = configurator.load_var(DhcoreEnvVar.USER.value)
|
|
170
|
-
password = configurator.load_var(DhcoreEnvVar.PASSWORD.value)
|
|
171
|
-
if user is not None and password is not None:
|
|
172
|
-
configurator.set_credential(AUTH_KEY, AuthType.BASIC.value)
|
|
173
|
-
configurator.set_credential(DhcoreEnvVar.USER.value, user)
|
|
174
|
-
configurator.set_credential(DhcoreEnvVar.PASSWORD.value, password)
|
|
299
|
+
super().change_origin()
|
|
300
|
+
|
|
301
|
+
# Re-evaluate the auth type
|
|
302
|
+
self.set_auth_type()
|
|
175
303
|
|
|
176
304
|
##############################
|
|
177
305
|
# Auth methods
|
|
178
306
|
##############################
|
|
179
307
|
|
|
180
|
-
def
|
|
308
|
+
def set_auth_type(self) -> None:
|
|
181
309
|
"""
|
|
182
|
-
|
|
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.
|
|
183
322
|
|
|
184
323
|
Returns
|
|
185
324
|
-------
|
|
186
|
-
|
|
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
|
|
187
333
|
"""
|
|
188
|
-
|
|
189
|
-
|
|
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()
|
|
190
343
|
|
|
191
|
-
def
|
|
344
|
+
def refreshable_auth_types(self) -> bool:
|
|
192
345
|
"""
|
|
193
|
-
|
|
346
|
+
Check if the current authentication type supports token refresh.
|
|
347
|
+
|
|
348
|
+
Determines whether the current authentication method supports
|
|
349
|
+
automatic token refresh capabilities.
|
|
194
350
|
|
|
195
351
|
Returns
|
|
196
352
|
-------
|
|
197
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
|
|
198
363
|
"""
|
|
199
|
-
|
|
200
|
-
return auth_type == AuthType.OAUTH2.value
|
|
364
|
+
return self._auth_type in [AuthType.OAUTH2.value, AuthType.EXCHANGE.value]
|
|
201
365
|
|
|
202
|
-
def
|
|
366
|
+
def get_auth_parameters(self, kwargs: dict) -> dict:
|
|
203
367
|
"""
|
|
204
|
-
|
|
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.
|
|
205
373
|
|
|
206
374
|
Parameters
|
|
207
375
|
----------
|
|
208
376
|
kwargs : dict
|
|
209
|
-
|
|
377
|
+
HTTP request keyword arguments to be modified with authentication.
|
|
210
378
|
|
|
211
379
|
Returns
|
|
212
380
|
-------
|
|
213
381
|
dict
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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]
|
|
224
403
|
if "headers" not in kwargs:
|
|
225
404
|
kwargs["headers"] = {}
|
|
226
|
-
access_token = creds[DhcoreEnvVar.ACCESS_TOKEN.value.removeprefix("DHCORE_")]
|
|
227
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)
|
|
228
410
|
return kwargs
|
|
229
411
|
|
|
230
|
-
def
|
|
412
|
+
def refresh_credentials(self, change_origin: bool = False) -> None:
|
|
231
413
|
"""
|
|
232
|
-
|
|
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.
|
|
233
426
|
|
|
234
427
|
Returns
|
|
235
428
|
-------
|
|
236
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.
|
|
237
449
|
"""
|
|
238
|
-
|
|
239
|
-
|
|
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
|
|
240
454
|
url = self._get_refresh_endpoint()
|
|
241
455
|
|
|
242
|
-
#
|
|
243
|
-
|
|
244
|
-
refresh_token = configurator.load_from_env(DhcoreEnvVar.REFRESH_TOKEN.value)
|
|
245
|
-
response = self._call_refresh_token_endpoint(url, refresh_token)
|
|
456
|
+
# Get credentials
|
|
457
|
+
creds = self._creds_handler.get_credentials(self._origin)
|
|
246
458
|
|
|
247
|
-
#
|
|
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
|
|
248
483
|
if response.status_code in (400, 401, 403):
|
|
249
|
-
|
|
250
|
-
|
|
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)
|
|
251
488
|
|
|
252
489
|
response.raise_for_status()
|
|
253
490
|
|
|
254
491
|
# Read new credentials and propagate to config file
|
|
255
|
-
self.
|
|
492
|
+
self._export_new_creds(response.json())
|
|
256
493
|
|
|
257
|
-
def
|
|
494
|
+
def _remove_prefix_dhcore(self) -> list[str]:
|
|
258
495
|
"""
|
|
259
|
-
|
|
496
|
+
Remove DHCORE_ prefix from selected credential keys for CLI compatibility.
|
|
260
497
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
Response from refresh token endpoint.
|
|
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.
|
|
265
501
|
|
|
266
502
|
Returns
|
|
267
503
|
-------
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
keys = [
|
|
271
|
-
*self._remove_prefix_dhcore(list_enum(DhcoreEnvVar)),
|
|
272
|
-
*list_enum(S3StoreEnv),
|
|
273
|
-
*list_enum(SqlStoreEnv),
|
|
274
|
-
]
|
|
275
|
-
for key in keys:
|
|
276
|
-
if (value := response.get(key.lower())) is not None:
|
|
277
|
-
configurator.set_credential(key, value)
|
|
278
|
-
configurator.write_env(keys)
|
|
279
|
-
|
|
280
|
-
def _remove_prefix_dhcore(self, keys: list[str]) -> list[str]:
|
|
281
|
-
"""
|
|
282
|
-
Remove prefix from selected keys. (Compatibility with CLI)
|
|
504
|
+
list[str]
|
|
505
|
+
List of credential keys with selective prefix removal applied.
|
|
283
506
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
|
288
514
|
|
|
289
|
-
|
|
290
|
-
-------
|
|
291
|
-
list[str]
|
|
292
|
-
List of keys without prefix.
|
|
515
|
+
Other keys retain their full names for consistency.
|
|
293
516
|
"""
|
|
294
517
|
new_list = []
|
|
295
|
-
for key in keys:
|
|
296
|
-
if key in
|
|
297
|
-
DhcoreEnvVar.REFRESH_TOKEN.value,
|
|
298
|
-
DhcoreEnvVar.ACCESS_TOKEN.value,
|
|
299
|
-
DhcoreEnvVar.ISSUER.value,
|
|
300
|
-
DhcoreEnvVar.CLIENT_ID.value,
|
|
301
|
-
):
|
|
518
|
+
for key in self.keys:
|
|
519
|
+
if key in self.keys_to_unprefix:
|
|
302
520
|
new_list.append(key.removeprefix("DHCORE_"))
|
|
303
521
|
else:
|
|
304
522
|
new_list.append(key)
|
|
@@ -306,19 +524,37 @@ class ClientDHCoreConfigurator:
|
|
|
306
524
|
|
|
307
525
|
def _get_refresh_endpoint(self) -> str:
|
|
308
526
|
"""
|
|
309
|
-
|
|
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.
|
|
310
531
|
|
|
311
532
|
Returns
|
|
312
533
|
-------
|
|
313
534
|
str
|
|
314
|
-
|
|
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
|
|
315
552
|
"""
|
|
316
553
|
# Get issuer endpoint
|
|
317
|
-
|
|
554
|
+
creds = self._creds_handler.get_credentials(self._origin)
|
|
555
|
+
endpoint_issuer = creds.get(CredsEnvVar.DHCORE_ISSUER.value)
|
|
318
556
|
if endpoint_issuer is None:
|
|
319
557
|
raise ClientError("Issuer endpoint not set.")
|
|
320
|
-
endpoint_issuer = self._sanitize_endpoint(endpoint_issuer)
|
|
321
|
-
configurator.set_credential(DhcoreEnvVar.ISSUER.value.removeprefix("DHCORE_"), endpoint_issuer)
|
|
322
558
|
|
|
323
559
|
# Standard issuer endpoint path
|
|
324
560
|
url = endpoint_issuer + "/.well-known/openid-configuration"
|
|
@@ -328,52 +564,112 @@ class ClientDHCoreConfigurator:
|
|
|
328
564
|
r.raise_for_status()
|
|
329
565
|
return r.json().get("token_endpoint")
|
|
330
566
|
|
|
331
|
-
def
|
|
567
|
+
def _call_refresh_endpoint(
|
|
568
|
+
self,
|
|
569
|
+
url: str,
|
|
570
|
+
**kwargs,
|
|
571
|
+
) -> Response:
|
|
332
572
|
"""
|
|
333
|
-
|
|
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.
|
|
334
577
|
|
|
335
578
|
Parameters
|
|
336
579
|
----------
|
|
337
580
|
url : str
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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.
|
|
341
585
|
|
|
342
586
|
Returns
|
|
343
587
|
-------
|
|
344
588
|
Response
|
|
345
|
-
|
|
346
|
-
"""
|
|
347
|
-
# Get client id
|
|
348
|
-
client_id = self._load_dhcore_oauth_vars(DhcoreEnvVar.CLIENT_ID.value)
|
|
349
|
-
if client_id is None:
|
|
350
|
-
raise ClientError("Client id not set.")
|
|
589
|
+
The HTTP response object from the token endpoint.
|
|
351
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
|
+
"""
|
|
352
598
|
# Send request to get new access token
|
|
353
|
-
payload = {
|
|
354
|
-
"grant_type": "refresh_token",
|
|
355
|
-
"client_id": client_id,
|
|
356
|
-
"refresh_token": refresh_token,
|
|
357
|
-
"scope": "openid credentials offline_access",
|
|
358
|
-
}
|
|
599
|
+
payload = {**kwargs}
|
|
359
600
|
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
360
601
|
return request("POST", url, data=payload, headers=headers, timeout=60)
|
|
361
602
|
|
|
362
|
-
def
|
|
603
|
+
def _eval_auth_type(self, creds: dict) -> str | None:
|
|
363
604
|
"""
|
|
364
|
-
|
|
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.
|
|
365
609
|
|
|
366
610
|
Parameters
|
|
367
611
|
----------
|
|
368
|
-
|
|
369
|
-
|
|
612
|
+
creds : dict
|
|
613
|
+
Dictionary containing credential values.
|
|
370
614
|
|
|
371
615
|
Returns
|
|
372
616
|
-------
|
|
373
|
-
str
|
|
374
|
-
The
|
|
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.
|
|
375
670
|
"""
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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()
|