digitalhub 0.12.0__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.

Files changed (79) hide show
  1. digitalhub/__init__.py +1 -1
  2. digitalhub/context/api.py +5 -5
  3. digitalhub/context/builder.py +3 -5
  4. digitalhub/context/context.py +9 -1
  5. digitalhub/entities/_base/executable/entity.py +105 -57
  6. digitalhub/entities/_base/material/entity.py +11 -18
  7. digitalhub/entities/_base/material/utils.py +1 -1
  8. digitalhub/entities/_commons/metrics.py +64 -30
  9. digitalhub/entities/_commons/utils.py +36 -9
  10. digitalhub/entities/_processors/base.py +150 -79
  11. digitalhub/entities/_processors/context.py +366 -215
  12. digitalhub/entities/_processors/utils.py +74 -30
  13. digitalhub/entities/artifact/crud.py +4 -0
  14. digitalhub/entities/artifact/utils.py +28 -13
  15. digitalhub/entities/dataitem/crud.py +14 -2
  16. digitalhub/entities/dataitem/table/entity.py +3 -3
  17. digitalhub/entities/dataitem/utils.py +84 -35
  18. digitalhub/entities/model/crud.py +4 -0
  19. digitalhub/entities/model/utils.py +28 -13
  20. digitalhub/entities/project/_base/entity.py +0 -2
  21. digitalhub/entities/run/_base/entity.py +2 -2
  22. digitalhub/entities/task/_base/models.py +12 -3
  23. digitalhub/entities/trigger/_base/entity.py +11 -0
  24. digitalhub/factory/factory.py +25 -3
  25. digitalhub/factory/utils.py +11 -3
  26. digitalhub/runtimes/_base.py +1 -1
  27. digitalhub/runtimes/builder.py +18 -1
  28. digitalhub/stores/client/__init__.py +12 -0
  29. digitalhub/stores/client/_base/api_builder.py +14 -0
  30. digitalhub/stores/client/_base/client.py +93 -0
  31. digitalhub/stores/client/_base/key_builder.py +28 -0
  32. digitalhub/stores/client/_base/params_builder.py +14 -0
  33. digitalhub/stores/client/api.py +10 -5
  34. digitalhub/stores/client/builder.py +3 -1
  35. digitalhub/stores/client/dhcore/api_builder.py +17 -0
  36. digitalhub/stores/client/dhcore/client.py +325 -70
  37. digitalhub/stores/client/dhcore/configurator.py +485 -193
  38. digitalhub/stores/client/dhcore/enums.py +3 -0
  39. digitalhub/stores/client/dhcore/error_parser.py +35 -1
  40. digitalhub/stores/client/dhcore/params_builder.py +113 -17
  41. digitalhub/stores/client/dhcore/utils.py +40 -22
  42. digitalhub/stores/client/local/api_builder.py +17 -0
  43. digitalhub/stores/client/local/client.py +6 -8
  44. digitalhub/stores/credentials/api.py +35 -0
  45. digitalhub/stores/credentials/configurator.py +210 -0
  46. digitalhub/stores/credentials/enums.py +68 -0
  47. digitalhub/stores/credentials/handler.py +176 -0
  48. digitalhub/stores/{configurator → credentials}/ini_module.py +60 -28
  49. digitalhub/stores/credentials/store.py +81 -0
  50. digitalhub/stores/data/_base/store.py +27 -9
  51. digitalhub/stores/data/api.py +49 -9
  52. digitalhub/stores/data/builder.py +90 -41
  53. digitalhub/stores/data/local/store.py +4 -7
  54. digitalhub/stores/data/remote/store.py +4 -7
  55. digitalhub/stores/data/s3/configurator.py +65 -80
  56. digitalhub/stores/data/s3/store.py +69 -81
  57. digitalhub/stores/data/s3/utils.py +10 -10
  58. digitalhub/stores/data/sql/configurator.py +76 -73
  59. digitalhub/stores/data/sql/store.py +191 -102
  60. digitalhub/utils/exceptions.py +6 -0
  61. digitalhub/utils/file_utils.py +53 -30
  62. digitalhub/utils/generic_utils.py +41 -33
  63. digitalhub/utils/git_utils.py +24 -14
  64. digitalhub/utils/io_utils.py +19 -18
  65. digitalhub/utils/uri_utils.py +31 -31
  66. {digitalhub-0.12.0.dist-info → digitalhub-0.13.0.dist-info}/METADATA +1 -1
  67. {digitalhub-0.12.0.dist-info → digitalhub-0.13.0.dist-info}/RECORD +71 -74
  68. digitalhub/entities/_commons/types.py +0 -9
  69. digitalhub/stores/configurator/api.py +0 -35
  70. digitalhub/stores/configurator/configurator.py +0 -202
  71. digitalhub/stores/configurator/credentials_store.py +0 -69
  72. digitalhub/stores/configurator/enums.py +0 -25
  73. digitalhub/stores/data/s3/enums.py +0 -20
  74. digitalhub/stores/data/sql/enums.py +0 -20
  75. digitalhub/stores/data/utils.py +0 -38
  76. /digitalhub/stores/{configurator → credentials}/__init__.py +0 -0
  77. {digitalhub-0.12.0.dist-info → digitalhub-0.13.0.dist-info}/WHEEL +0 -0
  78. {digitalhub-0.12.0.dist-info → digitalhub-0.13.0.dist-info}/licenses/AUTHORS +0 -0
  79. {digitalhub-0.12.0.dist-info → digitalhub-0.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -19,6 +19,7 @@ class DhcoreEnvVar(Enum):
19
19
  CLIENT_ID = "DHCORE_CLIENT_ID"
20
20
  ACCESS_TOKEN = "DHCORE_ACCESS_TOKEN"
21
21
  REFRESH_TOKEN = "DHCORE_REFRESH_TOKEN"
22
+ PERSONAL_ACCESS_TOKEN = "DHCORE_PERSONAL_ACCESS_TOKEN"
22
23
  WORKFLOW_IMAGE = "DHCORE_WORKFLOW_IMAGE"
23
24
 
24
25
 
@@ -29,3 +30,5 @@ class AuthType(Enum):
29
30
 
30
31
  BASIC = "basic"
31
32
  OAUTH2 = "oauth2"
33
+ EXCHANGE = "exchange"
34
+ ACCESS_TOKEN = "access_token_only"
@@ -23,19 +23,53 @@ if typing.TYPE_CHECKING:
23
23
 
24
24
 
25
25
  class ErrorParser:
26
+ """
27
+ Parser for DHCore API errors.
28
+
29
+ This class handles the parsing and translation of HTTP responses
30
+ from the DHCore backend into appropriate Python exceptions.
31
+ """
32
+
26
33
  @staticmethod
27
34
  def parse(response: Response) -> None:
28
35
  """
29
36
  Handle DHCore API errors.
30
37
 
38
+ Parses the HTTP response and raises appropriate exceptions based on
39
+ the status code and response content. Maps backend errors to specific
40
+ exception types for better error handling in the client code.
41
+
31
42
  Parameters
32
43
  ----------
33
44
  response : Response
34
- The response object.
45
+ The HTTP response object from requests.
35
46
 
36
47
  Returns
37
48
  -------
38
49
  None
50
+
51
+ Raises
52
+ ------
53
+ TimeoutError
54
+ If the request timed out.
55
+ ConnectionError
56
+ If unable to connect to the backend.
57
+ MissingSpecError
58
+ If the backend reports a missing spec (400 status).
59
+ EntityAlreadyExistsError
60
+ If the entity already exists (400 status with specific message).
61
+ BadRequestError
62
+ For other 400 status codes.
63
+ UnauthorizedError
64
+ For 401 status codes.
65
+ ForbiddenError
66
+ For 403 status codes.
67
+ EntityNotExistsError
68
+ For 404 status codes with specific message.
69
+ BackendError
70
+ For other 404 status codes and general backend errors.
71
+ RuntimeError
72
+ For unexpected exceptions.
39
73
  """
40
74
  try:
41
75
  response.raise_for_status()
@@ -10,26 +10,65 @@ from digitalhub.stores.client._base.params_builder import ClientParametersBuilde
10
10
 
11
11
  class ClientDHCoreParametersBuilder(ClientParametersBuilder):
12
12
  """
13
- This class is used to build the parameters for the DHCore client calls.
13
+ Parameter builder for DHCore client API calls.
14
+
15
+ This class constructs HTTP request parameters for different DHCore API
16
+ operations, handling the specific parameter formats and query structures
17
+ required by the DigitalHub Core backend. It supports both base-level
18
+ operations (like project management) and context-level operations
19
+ (entity operations within projects).
20
+
21
+ The builder handles various parameter transformations including:
22
+ - Query parameter formatting for different operations
23
+ - Search filter construction for Solr-based searches
24
+ - Cascade deletion options
25
+ - Versioning parameters
26
+ - Entity sharing parameters
27
+
28
+ Methods
29
+ -------
30
+ build_parameters(category, operation, **kwargs)
31
+ Main entry point for parameter building based on API category.
32
+ build_parameters_base(operation, **kwargs)
33
+ Builds parameters for base-level API operations.
34
+ build_parameters_context(operation, **kwargs)
35
+ Builds parameters for context-level API operations.
14
36
  """
15
37
 
16
38
  def build_parameters(self, category: str, operation: str, **kwargs) -> dict:
17
39
  """
18
- Build the parameters for the client call.
40
+ Build HTTP request parameters for DHCore API calls.
41
+
42
+ Routes parameter building to the appropriate method based on the
43
+ API category (base or context operations) and applies operation-specific
44
+ parameter transformations.
19
45
 
20
46
  Parameters
21
47
  ----------
22
48
  category : str
23
- API category.
49
+ The API category, either 'base' for project-level operations
50
+ or 'context' for entity operations within projects.
24
51
  operation : str
25
- API operation.
52
+ The specific API operation being performed (create, read, update,
53
+ delete, list, search, etc.).
26
54
  **kwargs : dict
27
- Parameters to build.
55
+ Raw parameters to be transformed into proper HTTP request format.
56
+ May include entity identifiers, filter criteria, pagination
57
+ options, etc.
28
58
 
29
59
  Returns
30
60
  -------
31
61
  dict
32
- Parameters formatted.
62
+ Formatted parameters dictionary ready for HTTP request, typically
63
+ containing a 'params' key with query parameters and other
64
+ request-specific parameters.
65
+
66
+ Notes
67
+ -----
68
+ This method acts as a dispatcher, routing to either base or context
69
+ parameter building based on the category. All parameter dictionaries
70
+ are initialized with a 'params' key for query parameters if not
71
+ already present.
33
72
  """
34
73
  if category == ApiCategories.BASE.value:
35
74
  return self.build_parameters_base(operation, **kwargs)
@@ -37,19 +76,37 @@ class ClientDHCoreParametersBuilder(ClientParametersBuilder):
37
76
 
38
77
  def build_parameters_base(self, operation: str, **kwargs) -> dict:
39
78
  """
40
- Build the base parameters for the client call.
79
+ Build parameters for base-level API operations.
80
+
81
+ Constructs HTTP request parameters for operations that work at the
82
+ base level of the API, typically project-level operations and
83
+ entity sharing functionality.
41
84
 
42
85
  Parameters
43
86
  ----------
44
87
  operation : str
45
- API operation.
88
+ The API operation being performed. Supported operations:
89
+ - DELETE: Project deletion with optional cascade
90
+ - SHARE: Entity sharing/unsharing with users
46
91
  **kwargs : dict
47
- Parameters to build.
92
+ Operation-specific parameters including:
93
+ - cascade (bool): For DELETE, whether to cascade delete
94
+ - user (str): For SHARE, target user for sharing
95
+ - unshare (bool): For SHARE, whether to unshare instead
96
+ - id (str): For SHARE unshare, entity ID to unshare
48
97
 
49
98
  Returns
50
99
  -------
51
100
  dict
52
- Parameters formatted.
101
+ Formatted parameters with 'params' containing query parameters
102
+ and other request-specific parameters.
103
+
104
+ Notes
105
+ -----
106
+ Parameter transformations:
107
+ - CASCADE: Boolean values are converted to lowercase strings
108
+ - SHARE: User parameter is moved to query params
109
+ - UNSHARE: Requires both unshare=True and entity id
53
110
  """
54
111
  kwargs = self._set_params(**kwargs)
55
112
  if operation == BackendOperations.DELETE.value:
@@ -64,19 +121,46 @@ class ClientDHCoreParametersBuilder(ClientParametersBuilder):
64
121
 
65
122
  def build_parameters_context(self, operation: str, **kwargs) -> dict:
66
123
  """
67
- Build the context parameters for the client call.
124
+ Build parameters for context-level API operations.
125
+
126
+ Constructs HTTP request parameters for operations that work within
127
+ a specific context (project), including entity management and
128
+ search functionality.
68
129
 
69
130
  Parameters
70
131
  ----------
71
132
  operation : str
72
- API operation.
133
+ The API operation being performed. Supported operations:
134
+ - SEARCH: Search entities with filtering and pagination
135
+ - READ_MANY: Retrieve multiple entities with pagination
136
+ - DELETE: Delete specific entity by ID
137
+ - READ: Read specific entity by ID (with optional embedded)
73
138
  **kwargs : dict
74
- Parameters to build.
139
+ Operation-specific parameters including:
140
+ - params (dict): Search filters and conditions
141
+ - page (int): Page number for pagination (default: 0)
142
+ - size (int): Number of items per page (default: 20)
143
+ - order_by (str): Field to order results by
144
+ - order (str): Order direction ('asc' or 'desc')
145
+ - embedded (bool): For READ, whether to include embedded entities
146
+ - id (str): For READ/DELETE, entity identifier
75
147
 
76
148
  Returns
77
149
  -------
78
150
  dict
79
- Parameters formatted.
151
+ Formatted parameters with 'params' containing query parameters
152
+ and other request-specific parameters like 'id' for entity operations.
153
+
154
+ Notes
155
+ -----
156
+ Search and pagination:
157
+ - Filters are applied via 'filter' parameter in query string
158
+ - Pagination uses 'page' and 'size' parameters
159
+ - Results can be ordered using 'sort' parameter format
160
+
161
+ Entity operations:
162
+ - READ: Supports embedded entity inclusion via 'embedded' param
163
+ - DELETE: Requires entity 'id' parameter
80
164
  """
81
165
  kwargs = self._set_params(**kwargs)
82
166
 
@@ -163,17 +247,29 @@ class ClientDHCoreParametersBuilder(ClientParametersBuilder):
163
247
  @staticmethod
164
248
  def _set_params(**kwargs) -> dict:
165
249
  """
166
- Format params parameter.
250
+ Initialize parameter dictionary with query parameters structure.
251
+
252
+ Ensures that the parameter dictionary has a 'params' key for
253
+ HTTP query parameters. This is a utility method used by all
254
+ parameter building methods to guarantee consistent structure.
167
255
 
168
256
  Parameters
169
257
  ----------
170
258
  **kwargs : dict
171
- Keyword arguments.
259
+ Keyword arguments to be formatted. May be empty or contain
260
+ various parameters for API operations.
172
261
 
173
262
  Returns
174
263
  -------
175
264
  dict
176
- Parameters with initialized params.
265
+ Parameters dictionary with guaranteed 'params' key containing
266
+ an empty dict if not already present.
267
+
268
+ Notes
269
+ -----
270
+ This method is called at the beginning of all parameter building
271
+ methods to ensure consistent dictionary structure for query
272
+ parameter handling.
177
273
  """
178
274
  if not kwargs:
179
275
  kwargs = {}
@@ -8,7 +8,7 @@ import os
8
8
  import typing
9
9
 
10
10
  from digitalhub.stores.client.api import get_client
11
- from digitalhub.stores.client.dhcore.enums import DhcoreEnvVar
11
+ from digitalhub.stores.credentials.enums import CredsEnvVar
12
12
 
13
13
  if typing.TYPE_CHECKING:
14
14
  from digitalhub.stores.client.dhcore.client import ClientDHCore
@@ -24,53 +24,71 @@ def set_dhcore_env(
24
24
  ) -> None:
25
25
  """
26
26
  Function to set environment variables for DHCore config.
27
+
28
+ Sets the environment variables for DHCore configuration and
29
+ reloads the client configurator to use the new settings.
27
30
  Note that if the environment variable is already set, it
28
31
  will be overwritten.
29
32
 
30
33
  Parameters
31
34
  ----------
32
- endpoint : str
33
- The endpoint of DHCore.
34
- user : str
35
- The user of DHCore.
36
- password : str
37
- The password of DHCore.
38
- access_token : str
39
- The access token of DHCore.
40
- refresh_token : str
41
- The refresh token of DHCore.
42
- client_id : str
43
- The client id of DHCore.
35
+ endpoint : str, optional
36
+ The endpoint URL of the DHCore backend.
37
+ user : str, optional
38
+ The username for basic authentication.
39
+ password : str, optional
40
+ The password for basic authentication.
41
+ access_token : str, optional
42
+ The OAuth2 access token.
43
+ refresh_token : str, optional
44
+ The OAuth2 refresh token.
45
+ client_id : str, optional
46
+ The OAuth2 client identifier.
44
47
 
45
48
  Returns
46
49
  -------
47
50
  None
51
+
52
+ Notes
53
+ -----
54
+ After setting the environment variables, this function automatically
55
+ reloads the client configurator to apply the new configuration.
48
56
  """
49
57
  if endpoint is not None:
50
- os.environ[DhcoreEnvVar.ENDPOINT.value] = endpoint
58
+ os.environ[CredsEnvVar.DHCORE_ENDPOINT.value] = endpoint
51
59
  if user is not None:
52
- os.environ[DhcoreEnvVar.USER.value] = user
60
+ os.environ[CredsEnvVar.DHCORE_USER.value] = user
53
61
  if password is not None:
54
- os.environ[DhcoreEnvVar.PASSWORD.value] = password
62
+ os.environ[CredsEnvVar.DHCORE_PASSWORD.value] = password
55
63
  if access_token is not None:
56
- os.environ[DhcoreEnvVar.ACCESS_TOKEN.value] = access_token
64
+ os.environ[CredsEnvVar.DHCORE_ACCESS_TOKEN.value] = access_token
57
65
  if refresh_token is not None:
58
- os.environ[DhcoreEnvVar.REFRESH_TOKEN.value] = refresh_token
66
+ os.environ[CredsEnvVar.DHCORE_REFRESH_TOKEN.value] = refresh_token
59
67
  if client_id is not None:
60
- os.environ[DhcoreEnvVar.CLIENT_ID.value] = client_id
68
+ os.environ[CredsEnvVar.DHCORE_CLIENT_ID.value] = client_id
61
69
 
62
70
  client: ClientDHCore = get_client(local=False)
63
- client._configurator.configure()
71
+ client._configurator.load_env_vars()
64
72
 
65
73
 
66
74
  def refresh_token() -> None:
67
75
  """
68
- Function to refresh token.
76
+ Function to refresh the OAuth2 access token.
77
+
78
+ Attempts to refresh the current OAuth2 access token using the
79
+ refresh token stored in the client configuration. This function
80
+ requires that the client be configured with OAuth2 authentication.
69
81
 
70
82
  Returns
71
83
  -------
72
84
  None
85
+
86
+ Raises
87
+ ------
88
+ ClientError
89
+ If the client is not properly configured or if the token
90
+ refresh fails.
73
91
  """
74
92
  client: ClientDHCore = get_client(local=False)
75
93
  client._configurator.check_config()
76
- client._configurator.get_new_access_token()
94
+ client._configurator.refresh_credentials()
@@ -73,6 +73,23 @@ class ClientLocalApiBuilder(ClientApiBuilder):
73
73
  def build_api_context(self, operation: str, **kwargs) -> str:
74
74
  """
75
75
  Build the context API for the client.
76
+
77
+ Parameters
78
+ ----------
79
+ operation : str
80
+ The API operation.
81
+ **kwargs : dict
82
+ Additional parameters including project, entity_type, entity_id, etc.
83
+
84
+ Returns
85
+ -------
86
+ str
87
+ The formatted context API endpoint.
88
+
89
+ Raises
90
+ ------
91
+ BackendError
92
+ If the operation is not supported for the entity type.
76
93
  """
77
94
  try:
78
95
  entity_type = kwargs["entity_type"] + "s"
@@ -537,18 +537,16 @@ class ClientLocal(Client):
537
537
  Parameters
538
538
  ----------
539
539
  error_code : int
540
- Error code.
541
- project : str
542
- Project name.
543
- entity_type : str
544
- Entity type.
545
- entity_id : str
546
- Entity ID.
540
+ Error code identifying the type of error.
541
+ entity_type : str, optional
542
+ Entity type that caused the error.
543
+ entity_id : str, optional
544
+ Entity ID that caused the error.
547
545
 
548
546
  Returns
549
547
  -------
550
548
  str
551
- The formatted message.
549
+ The formatted error message.
552
550
  """
553
551
  msg = {
554
552
  1: f"Object '{entity_type}' to create is not valid",
@@ -0,0 +1,35 @@
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 digitalhub.stores.credentials.handler import creds_handler
8
+
9
+
10
+ def set_current_profile(environment: str) -> None:
11
+ """
12
+ Set the current credentials profile.
13
+
14
+ Parameters
15
+ ----------
16
+ environment : str
17
+ Name of the credentials profile to set.
18
+
19
+ Returns
20
+ -------
21
+ None
22
+ """
23
+ creds_handler.set_current_profile(environment)
24
+
25
+
26
+ def get_current_profile() -> str:
27
+ """
28
+ Get the name of the current credentials profile.
29
+
30
+ Returns
31
+ -------
32
+ str
33
+ Name of the current credentials profile.
34
+ """
35
+ return creds_handler.get_current_profile()
@@ -0,0 +1,210 @@
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 abc import abstractmethod
8
+
9
+ from digitalhub.stores.credentials.enums import CredsOrigin
10
+ from digitalhub.stores.credentials.handler import creds_handler
11
+ from digitalhub.utils.exceptions import ConfigError
12
+
13
+
14
+ class Configurator:
15
+ """
16
+ Base configurator for credentials management.
17
+
18
+ Attributes
19
+ ----------
20
+ keys : list of str
21
+ List of credential keys to manage.
22
+ required_keys : list of str
23
+ List of required credential keys.
24
+ _env : str
25
+ Environment origin identifier.
26
+ _file : str
27
+ File origin identifier.
28
+ _creds_handler : object
29
+ Credentials handler instance.
30
+ """
31
+
32
+ # Must be set in implementing class
33
+ keys: list[str] = []
34
+ required_keys: list[str] = []
35
+
36
+ # Origin of the credentials
37
+ _env = CredsOrigin.ENV.value
38
+ _file = CredsOrigin.FILE.value
39
+
40
+ # Credentials handler
41
+ _creds_handler = creds_handler
42
+
43
+ def __init__(self):
44
+ self._current_profile = self._creds_handler.get_current_profile()
45
+ self.load_configs()
46
+ self._changed_origin = False
47
+ self._origin = self.set_origin()
48
+
49
+ ##############################
50
+ # Configuration
51
+ ##############################
52
+
53
+ def load_configs(self) -> None:
54
+ """
55
+ Load the configuration from both environment and file sources.
56
+
57
+ Returns
58
+ -------
59
+ None
60
+ """
61
+ self.load_env_vars()
62
+ self.load_file_vars()
63
+
64
+ @abstractmethod
65
+ def load_env_vars(self) -> None:
66
+ ...
67
+
68
+ @abstractmethod
69
+ def load_file_vars(self) -> None:
70
+ ...
71
+
72
+ def check_config(self) -> None:
73
+ """
74
+ Check if the current profile has changed and reload
75
+ the file credentials if needed.
76
+
77
+ Returns
78
+ -------
79
+ None
80
+ """
81
+ if (current := self._creds_handler.get_current_profile()) != self._current_profile:
82
+ self.load_file_vars()
83
+ self._current_profile = current
84
+
85
+ def set_origin(self) -> str:
86
+ """
87
+ Determine the default origin for credentials (env or file).
88
+
89
+ Returns
90
+ -------
91
+ str
92
+ The selected origin ('env' or 'file').
93
+
94
+ Raises
95
+ ------
96
+ ConfigError
97
+ If required credentials are missing in both sources.
98
+ """
99
+ origin = self._env
100
+
101
+ env_creds = self._creds_handler.get_credentials(self._env)
102
+ missing_env = self._check_credentials(env_creds)
103
+
104
+ file_creds = self._creds_handler.get_credentials(self._file)
105
+ missing_file = self._check_credentials(file_creds)
106
+
107
+ msg = ""
108
+ if missing_env:
109
+ msg = f"Missing required vars in env: {', '.join(missing_env)}"
110
+ origin = self._file
111
+ self._changed_origin = True
112
+ elif missing_file:
113
+ msg += f"Missing required vars in .dhcore.ini file: {', '.join(missing_file)}"
114
+
115
+ if missing_env and missing_file:
116
+ raise ConfigError(msg)
117
+
118
+ return origin
119
+
120
+ def eval_change_origin(self) -> None:
121
+ """
122
+ Attempt to change the origin of credentials.
123
+ Raise error if already evaluated.
124
+
125
+ Returns
126
+ -------
127
+ None
128
+ """
129
+ try:
130
+ self.change_origin()
131
+ except ConfigError:
132
+ raise ConfigError("Credentials origin already evaluated. Please check your credentials.")
133
+
134
+ def change_origin(self) -> None:
135
+ """
136
+ Change the origin of credentials from env to file or vice versa.
137
+
138
+ Returns
139
+ -------
140
+ None
141
+ """
142
+ if self._changed_origin:
143
+ raise ConfigError("Origin has already been changed.")
144
+ if self._origin == self._env:
145
+ self.change_to_file()
146
+ self.change_to_env()
147
+
148
+ def change_to_file(self) -> None:
149
+ """
150
+ Set the credentials origin to file.
151
+
152
+ Returns
153
+ -------
154
+ None
155
+ """
156
+ if self._origin == self._env:
157
+ self._changed_origin = True
158
+ self._origin = CredsOrigin.FILE.value
159
+
160
+ def change_to_env(self) -> None:
161
+ """
162
+ Set the credentials origin to environment.
163
+
164
+ Returns
165
+ -------
166
+ None
167
+ """
168
+ if self._origin == self._file:
169
+ self._changed_origin = True
170
+ self._origin = CredsOrigin.ENV.value
171
+
172
+ ##############################
173
+ # Credentials
174
+ ##############################
175
+
176
+ def get_credentials(self, origin: str) -> dict:
177
+ """
178
+ Retrieve credentials for the specified origin.
179
+
180
+ Parameters
181
+ ----------
182
+ origin : str
183
+ The origin to retrieve credentials from ('env' or 'file').
184
+
185
+ Returns
186
+ -------
187
+ dict
188
+ Dictionary of credentials.
189
+ """
190
+ return self._creds_handler.get_credentials(origin)
191
+
192
+ def _check_credentials(self, creds: dict) -> list[str]:
193
+ """
194
+ Check for missing required credentials in a dictionary.
195
+
196
+ Parameters
197
+ ----------
198
+ creds : dict
199
+ Dictionary of credentials to check.
200
+
201
+ Returns
202
+ -------
203
+ list of str
204
+ List of missing required credential keys.
205
+ """
206
+ missing_keys = []
207
+ for k, v in creds.items():
208
+ if v is None and k in self.required_keys:
209
+ missing_keys.append(k)
210
+ return missing_keys