digitalhub 0.13.3__py3-none-any.whl → 0.14.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 +3 -8
- digitalhub/context/api.py +1 -5
- digitalhub/context/builder.py +1 -5
- digitalhub/context/context.py +15 -9
- digitalhub/entities/_base/_base/entity.py +0 -15
- digitalhub/entities/_base/context/entity.py +1 -1
- digitalhub/entities/_base/entity/builder.py +5 -5
- digitalhub/entities/_base/entity/entity.py +0 -8
- digitalhub/entities/_base/executable/entity.py +169 -79
- digitalhub/entities/_base/material/entity.py +6 -22
- digitalhub/entities/_base/material/utils.py +1 -4
- digitalhub/entities/_base/runtime_entity/builder.py +53 -18
- digitalhub/entities/_base/unversioned/entity.py +1 -1
- digitalhub/entities/_base/versioned/entity.py +1 -1
- digitalhub/entities/_commons/enums.py +1 -31
- digitalhub/entities/_commons/utils.py +83 -21
- digitalhub/entities/_constructors/_resources.py +151 -0
- digitalhub/entities/{_base/entity/_constructors → _constructors}/name.py +18 -0
- digitalhub/entities/_processors/base/__init__.py +3 -0
- digitalhub/entities/_processors/{base.py → base/crud.py} +14 -226
- digitalhub/entities/_processors/base/import_export.py +123 -0
- digitalhub/entities/_processors/base/processor.py +302 -0
- digitalhub/entities/_processors/base/special_ops.py +108 -0
- digitalhub/entities/_processors/context/__init__.py +3 -0
- digitalhub/entities/_processors/context/crud.py +652 -0
- digitalhub/entities/_processors/context/import_export.py +242 -0
- digitalhub/entities/_processors/context/material.py +123 -0
- digitalhub/entities/_processors/context/processor.py +400 -0
- digitalhub/entities/_processors/context/special_ops.py +476 -0
- digitalhub/entities/_processors/processors.py +12 -0
- digitalhub/entities/_processors/utils.py +12 -11
- digitalhub/entities/artifact/crud.py +58 -22
- digitalhub/entities/artifact/utils.py +3 -3
- digitalhub/entities/dataitem/crud.py +63 -20
- digitalhub/entities/dataitem/table/entity.py +24 -22
- digitalhub/entities/dataitem/utils.py +15 -15
- digitalhub/entities/function/_base/entity.py +3 -3
- digitalhub/entities/function/crud.py +55 -24
- digitalhub/entities/model/_base/entity.py +62 -20
- digitalhub/entities/model/crud.py +58 -22
- digitalhub/entities/model/utils.py +3 -3
- digitalhub/entities/project/_base/entity.py +321 -152
- digitalhub/entities/project/crud.py +15 -23
- digitalhub/entities/run/_base/builder.py +0 -4
- digitalhub/entities/run/_base/entity.py +70 -63
- digitalhub/entities/run/crud.py +79 -26
- digitalhub/entities/secret/_base/entity.py +1 -5
- digitalhub/entities/secret/crud.py +29 -26
- digitalhub/entities/task/_base/builder.py +0 -4
- digitalhub/entities/task/_base/entity.py +5 -5
- digitalhub/entities/task/_base/models.py +13 -16
- digitalhub/entities/task/crud.py +61 -29
- digitalhub/entities/trigger/_base/entity.py +1 -5
- digitalhub/entities/trigger/crud.py +64 -24
- digitalhub/entities/workflow/_base/entity.py +3 -3
- digitalhub/entities/workflow/crud.py +55 -21
- digitalhub/factory/entity.py +283 -0
- digitalhub/factory/enums.py +18 -0
- digitalhub/factory/registry.py +197 -0
- digitalhub/factory/runtime.py +44 -0
- digitalhub/factory/utils.py +3 -54
- digitalhub/runtimes/_base.py +2 -2
- digitalhub/stores/client/_base/enums.py +39 -0
- digitalhub/stores/client/_base/key_builder.py +2 -2
- digitalhub/stores/client/_base/params_builder.py +48 -0
- digitalhub/stores/client/api.py +6 -10
- digitalhub/stores/client/builder.py +4 -4
- digitalhub/stores/client/dhcore/api_builder.py +2 -1
- digitalhub/stores/client/dhcore/client.py +85 -429
- digitalhub/stores/client/dhcore/configurator.py +109 -328
- digitalhub/stores/client/dhcore/enums.py +0 -16
- digitalhub/stores/client/dhcore/error_parser.py +0 -4
- digitalhub/stores/client/dhcore/header_manager.py +61 -0
- digitalhub/stores/client/dhcore/http_handler.py +133 -0
- digitalhub/stores/client/dhcore/params_builder.py +147 -134
- digitalhub/stores/client/dhcore/response_processor.py +102 -0
- digitalhub/stores/client/dhcore/utils.py +6 -72
- digitalhub/stores/client/local/api_builder.py +1 -1
- digitalhub/stores/client/local/client.py +79 -47
- digitalhub/stores/client/local/params_builder.py +18 -41
- digitalhub/stores/credentials/api.py +0 -4
- digitalhub/stores/credentials/configurator.py +2 -28
- digitalhub/stores/credentials/enums.py +3 -0
- digitalhub/stores/credentials/handler.py +0 -12
- digitalhub/stores/credentials/ini_module.py +0 -22
- digitalhub/stores/credentials/store.py +0 -4
- digitalhub/stores/data/_base/store.py +0 -16
- digitalhub/stores/data/builder.py +1 -5
- digitalhub/stores/data/local/store.py +0 -103
- digitalhub/stores/data/remote/store.py +0 -4
- digitalhub/stores/data/s3/configurator.py +60 -14
- digitalhub/stores/data/s3/store.py +49 -16
- digitalhub/stores/data/sql/configurator.py +0 -8
- digitalhub/stores/data/sql/store.py +21 -10
- digitalhub/stores/readers/data/factory.py +0 -8
- digitalhub/stores/readers/data/pandas/reader.py +0 -16
- digitalhub/utils/file_utils.py +0 -17
- digitalhub/utils/generic_utils.py +0 -12
- digitalhub/utils/git_utils.py +0 -8
- digitalhub/utils/io_utils.py +0 -12
- digitalhub/utils/store_utils.py +44 -0
- {digitalhub-0.13.3.dist-info → digitalhub-0.14.0.dist-info}/METADATA +3 -2
- {digitalhub-0.13.3.dist-info → digitalhub-0.14.0.dist-info}/RECORD +111 -95
- digitalhub/entities/_processors/context.py +0 -1450
- digitalhub/entities/task/_base/utils.py +0 -22
- digitalhub/factory/factory.py +0 -381
- digitalhub/stores/client/dhcore/models.py +0 -40
- digitalhub/stores/data/s3/utils.py +0 -78
- /digitalhub/entities/{_base/entity/_constructors → _constructors}/__init__.py +0 -0
- /digitalhub/entities/{_base/entity/_constructors → _constructors}/metadata.py +0 -0
- /digitalhub/entities/{_base/entity/_constructors → _constructors}/spec.py +0 -0
- /digitalhub/entities/{_base/entity/_constructors → _constructors}/status.py +0 -0
- /digitalhub/entities/{_base/entity/_constructors → _constructors}/uuid.py +0 -0
- {digitalhub-0.13.3.dist-info → digitalhub-0.14.0.dist-info}/WHEEL +0 -0
- {digitalhub-0.13.3.dist-info → digitalhub-0.14.0.dist-info}/licenses/AUTHORS +0 -0
- {digitalhub-0.13.3.dist-info → digitalhub-0.14.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2025 DSLab - Fondazione Bruno Kessler
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import typing
|
|
8
|
+
from warnings import warn
|
|
9
|
+
|
|
10
|
+
from requests.exceptions import JSONDecodeError
|
|
11
|
+
|
|
12
|
+
from digitalhub.stores.client.dhcore.error_parser import ErrorParser
|
|
13
|
+
from digitalhub.utils.exceptions import BackendError, ClientError
|
|
14
|
+
|
|
15
|
+
if typing.TYPE_CHECKING:
|
|
16
|
+
from requests import Response
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# API levels that are supported
|
|
20
|
+
MAX_API_LEVEL = 20
|
|
21
|
+
MIN_API_LEVEL = 14
|
|
22
|
+
LIB_VERSION = 14
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ResponseProcessor:
|
|
26
|
+
"""
|
|
27
|
+
Processes and validates HTTP responses from DHCore backend.
|
|
28
|
+
|
|
29
|
+
Handles API version validation, error parsing, and response body parsing
|
|
30
|
+
to dictionary. Supports API versions {MIN_API_LEVEL} to {MAX_API_LEVEL}.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self._error_parser = ErrorParser()
|
|
35
|
+
|
|
36
|
+
def process(self, response: Response) -> dict:
|
|
37
|
+
"""
|
|
38
|
+
Process HTTP response with validation and parsing.
|
|
39
|
+
|
|
40
|
+
Performs API version compatibility check, error parsing for failed
|
|
41
|
+
responses, and JSON deserialization.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
response : Response
|
|
46
|
+
HTTP response object from backend.
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
dict
|
|
51
|
+
Parsed response body as dictionary.
|
|
52
|
+
"""
|
|
53
|
+
self._check_api_version(response)
|
|
54
|
+
self._error_parser.parse(response)
|
|
55
|
+
return self._parse_json(response)
|
|
56
|
+
|
|
57
|
+
def _check_api_version(self, response: Response) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Validate DHCore API version compatibility.
|
|
60
|
+
|
|
61
|
+
Checks backend API version against supported range and warns if backend
|
|
62
|
+
version is newer than library. Supported: {MIN_API_LEVEL} to {MAX_API_LEVEL}.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
response : Response
|
|
67
|
+
HTTP response containing X-Api-Level header.
|
|
68
|
+
"""
|
|
69
|
+
if "X-Api-Level" not in response.headers:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
core_api_level = int(response.headers["X-Api-Level"])
|
|
73
|
+
if not (MIN_API_LEVEL <= core_api_level <= MAX_API_LEVEL):
|
|
74
|
+
raise ClientError("Backend API level not supported.")
|
|
75
|
+
|
|
76
|
+
if LIB_VERSION < core_api_level:
|
|
77
|
+
warn("Backend API level is higher than library version. You should consider updating the library.")
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def _parse_json(response: Response) -> dict:
|
|
81
|
+
"""
|
|
82
|
+
Parse HTTP response body to dictionary.
|
|
83
|
+
|
|
84
|
+
Converts JSON response to Python dictionary, treating empty responses
|
|
85
|
+
as valid and returning empty dict.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
response : Response
|
|
90
|
+
HTTP response object to parse.
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
dict
|
|
95
|
+
Parsed response body as dictionary, or empty dict if body is empty.
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
return response.json()
|
|
99
|
+
except JSONDecodeError:
|
|
100
|
+
if response.text == "":
|
|
101
|
+
return {}
|
|
102
|
+
raise BackendError("Backend response could not be parsed.")
|
|
@@ -4,91 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
import os
|
|
8
7
|
import typing
|
|
9
8
|
|
|
10
9
|
from digitalhub.stores.client.api import get_client
|
|
11
|
-
from digitalhub.stores.credentials.enums import CredsEnvVar
|
|
12
10
|
|
|
13
11
|
if typing.TYPE_CHECKING:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def set_dhcore_env(
|
|
18
|
-
endpoint: str | None = None,
|
|
19
|
-
user: str | None = None,
|
|
20
|
-
password: str | None = None,
|
|
21
|
-
access_token: str | None = None,
|
|
22
|
-
refresh_token: str | None = None,
|
|
23
|
-
client_id: str | None = None,
|
|
24
|
-
) -> None:
|
|
25
|
-
"""
|
|
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.
|
|
30
|
-
Note that if the environment variable is already set, it
|
|
31
|
-
will be overwritten.
|
|
32
|
-
|
|
33
|
-
Parameters
|
|
34
|
-
----------
|
|
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.
|
|
47
|
-
|
|
48
|
-
Returns
|
|
49
|
-
-------
|
|
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.
|
|
56
|
-
"""
|
|
57
|
-
if endpoint is not None:
|
|
58
|
-
os.environ[CredsEnvVar.DHCORE_ENDPOINT.value] = endpoint
|
|
59
|
-
if user is not None:
|
|
60
|
-
os.environ[CredsEnvVar.DHCORE_USER.value] = user
|
|
61
|
-
if password is not None:
|
|
62
|
-
os.environ[CredsEnvVar.DHCORE_PASSWORD.value] = password
|
|
63
|
-
if access_token is not None:
|
|
64
|
-
os.environ[CredsEnvVar.DHCORE_ACCESS_TOKEN.value] = access_token
|
|
65
|
-
if refresh_token is not None:
|
|
66
|
-
os.environ[CredsEnvVar.DHCORE_REFRESH_TOKEN.value] = refresh_token
|
|
67
|
-
if client_id is not None:
|
|
68
|
-
os.environ[CredsEnvVar.DHCORE_CLIENT_ID.value] = client_id
|
|
69
|
-
|
|
70
|
-
client: ClientDHCore = get_client(local=False)
|
|
71
|
-
client._configurator.load_env_vars()
|
|
12
|
+
pass
|
|
72
13
|
|
|
73
14
|
|
|
74
15
|
def refresh_token() -> None:
|
|
75
16
|
"""
|
|
76
|
-
|
|
17
|
+
Refresh the current OAuth2 access token.
|
|
77
18
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
requires that the client be configured with OAuth2 authentication.
|
|
19
|
+
Uses the refresh token stored in client configuration to obtain a new
|
|
20
|
+
access token. Requires OAuth2 authentication configuration.
|
|
81
21
|
|
|
82
|
-
Returns
|
|
83
|
-
-------
|
|
84
|
-
None
|
|
85
22
|
|
|
86
23
|
Raises
|
|
87
24
|
------
|
|
88
25
|
ClientError
|
|
89
|
-
If
|
|
90
|
-
refresh fails.
|
|
26
|
+
If client not properly configured or token refresh fails.
|
|
91
27
|
"""
|
|
92
|
-
|
|
93
|
-
client._configurator.check_config()
|
|
94
|
-
client._configurator.refresh_credentials()
|
|
28
|
+
get_client(local=False).refresh_token()
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
from digitalhub.entities._commons.enums import ApiCategories, BackendOperations
|
|
8
7
|
from digitalhub.stores.client._base.api_builder import ClientApiBuilder
|
|
8
|
+
from digitalhub.stores.client._base.enums import ApiCategories, BackendOperations
|
|
9
9
|
from digitalhub.stores.client.local.enums import LocalClientVar
|
|
10
10
|
from digitalhub.utils.exceptions import BackendError
|
|
11
11
|
|
|
@@ -207,7 +207,16 @@ class ClientLocal(Client):
|
|
|
207
207
|
|
|
208
208
|
else:
|
|
209
209
|
name = obj.get("name", entity_id)
|
|
210
|
-
self._db[entity_type][name]
|
|
210
|
+
container = self._db[entity_type][name]
|
|
211
|
+
container[entity_id] = obj
|
|
212
|
+
|
|
213
|
+
# Keep the "latest" pointer consistent when updating the latest entity
|
|
214
|
+
try:
|
|
215
|
+
if container.get("latest", {}).get("id") == entity_id:
|
|
216
|
+
container["latest"] = obj
|
|
217
|
+
except AttributeError:
|
|
218
|
+
# In case "latest" is malformed, ignore and continue
|
|
219
|
+
pass
|
|
211
220
|
|
|
212
221
|
except KeyError:
|
|
213
222
|
msg = self._format_msg(3, entity_type=entity_type, entity_id=entity_id)
|
|
@@ -253,55 +262,51 @@ class ClientLocal(Client):
|
|
|
253
262
|
|
|
254
263
|
# Name is optional and extracted from kwargs
|
|
255
264
|
# "params": {"name": <name>}
|
|
256
|
-
|
|
265
|
+
name_param = kwargs.get("params", {}).get("name")
|
|
257
266
|
|
|
258
|
-
# Delete by name
|
|
259
|
-
if entity_id is None and
|
|
260
|
-
self._db[entity_type].pop(
|
|
267
|
+
# Delete by name (remove the whole named container)
|
|
268
|
+
if entity_id is None and name_param is not None:
|
|
269
|
+
self._db[entity_type].pop(name_param, None)
|
|
261
270
|
return {"deleted": True}
|
|
262
271
|
|
|
263
272
|
# Delete by id
|
|
264
|
-
|
|
273
|
+
found_name: str | None = None
|
|
274
|
+
container: dict | None = None
|
|
275
|
+
for n, v in self._db[entity_type].items():
|
|
265
276
|
if entity_id in v:
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
# Handle latest
|
|
269
|
-
if v["latest"]["id"] == entity_id:
|
|
270
|
-
name = v["latest"].get("name", entity_id)
|
|
271
|
-
v.pop("latest")
|
|
272
|
-
reset_latest = True
|
|
277
|
+
found_name = n
|
|
278
|
+
container = v
|
|
273
279
|
break
|
|
274
280
|
else:
|
|
275
281
|
raise KeyError
|
|
276
282
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
self._db[entity_type][name]["latest"] = self._db[entity_type][name][latest_uuid]
|
|
283
|
+
# Remove the entity from the container
|
|
284
|
+
assert container is not None # for type checkers
|
|
285
|
+
container.pop(entity_id)
|
|
286
|
+
|
|
287
|
+
# Handle latest pointer if needed
|
|
288
|
+
if container.get("latest", {}).get("id") == entity_id:
|
|
289
|
+
# Remove stale latest
|
|
290
|
+
container.pop("latest", None)
|
|
291
|
+
reset_latest = True
|
|
292
|
+
|
|
293
|
+
# If container is now empty, drop it entirely
|
|
294
|
+
if not container:
|
|
295
|
+
assert found_name is not None
|
|
296
|
+
self._db[entity_type].pop(found_name, None)
|
|
297
|
+
# Otherwise, recompute latest if required
|
|
298
|
+
elif reset_latest:
|
|
299
|
+
latest_uuid = None
|
|
300
|
+
latest_date = None
|
|
301
|
+
for k, v in container.items():
|
|
302
|
+
# Parse creation time from metadata; tolerate various formats
|
|
303
|
+
current_created = self._safe_parse_created(v)
|
|
304
|
+
if latest_date is None or current_created > latest_date:
|
|
305
|
+
latest_uuid = k
|
|
306
|
+
latest_date = current_created
|
|
307
|
+
|
|
308
|
+
if latest_uuid is not None:
|
|
309
|
+
container["latest"] = container[latest_uuid]
|
|
305
310
|
|
|
306
311
|
except KeyError:
|
|
307
312
|
msg = self._format_msg(3, entity_type=entity_type, entity_id=entity_id)
|
|
@@ -330,7 +335,10 @@ class ClientLocal(Client):
|
|
|
330
335
|
# "params": {"name": <name>}
|
|
331
336
|
name = kwargs.get("params", {}).get("name")
|
|
332
337
|
if name is not None:
|
|
333
|
-
|
|
338
|
+
try:
|
|
339
|
+
return [self._db[entity_type][name]["latest"]]
|
|
340
|
+
except KeyError:
|
|
341
|
+
return []
|
|
334
342
|
|
|
335
343
|
try:
|
|
336
344
|
# If no name is provided, get latest objects
|
|
@@ -343,7 +351,7 @@ class ClientLocal(Client):
|
|
|
343
351
|
if kind is not None:
|
|
344
352
|
listed_objects = [obj for obj in listed_objects if obj["kind"] == kind]
|
|
345
353
|
|
|
346
|
-
# If function is provided, return objects by function
|
|
354
|
+
# If function/task is provided, return objects by function/task
|
|
347
355
|
spec_params = ["function", "task"]
|
|
348
356
|
for i in spec_params:
|
|
349
357
|
p = kwargs.get("params", {}).get(i)
|
|
@@ -489,14 +497,15 @@ class ClientLocal(Client):
|
|
|
489
497
|
"""
|
|
490
498
|
# Deepcopy to avoid modifying the original object
|
|
491
499
|
project = deepcopy(obj)
|
|
492
|
-
spec
|
|
500
|
+
# Ensure spec exists on the returned project
|
|
501
|
+
spec = project.setdefault("spec", {})
|
|
493
502
|
|
|
494
503
|
# Get all entities associated with the project specs
|
|
495
504
|
projects_entities = [k for k, _ in self._db.items() if k not in ["projects", "runs", "tasks"]]
|
|
496
505
|
|
|
497
506
|
for entity_type in projects_entities:
|
|
498
507
|
# Get all objects of the entity type for the project
|
|
499
|
-
objs = self._db
|
|
508
|
+
objs = self._db.get(entity_type, {})
|
|
500
509
|
|
|
501
510
|
# Set empty list
|
|
502
511
|
spec[entity_type] = []
|
|
@@ -521,6 +530,29 @@ class ClientLocal(Client):
|
|
|
521
530
|
|
|
522
531
|
return project
|
|
523
532
|
|
|
533
|
+
@staticmethod
|
|
534
|
+
def _safe_parse_created(obj: dict) -> datetime:
|
|
535
|
+
"""
|
|
536
|
+
Safely parse the creation datetime of an object.
|
|
537
|
+
|
|
538
|
+
- Accepts ISO format with optional 'Z'.
|
|
539
|
+
- If tzinfo is missing, assume UTC.
|
|
540
|
+
- Falls back to epoch if missing/invalid.
|
|
541
|
+
"""
|
|
542
|
+
created_raw = obj.get("metadata", {}).get("created")
|
|
543
|
+
fallback = datetime.fromtimestamp(0, timezone.utc)
|
|
544
|
+
if not created_raw or not isinstance(created_raw, str):
|
|
545
|
+
return fallback
|
|
546
|
+
try:
|
|
547
|
+
# Support trailing 'Z'
|
|
548
|
+
ts = created_raw.replace("Z", "+00:00")
|
|
549
|
+
dt = datetime.fromisoformat(ts)
|
|
550
|
+
if dt.tzinfo is None:
|
|
551
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
552
|
+
return dt
|
|
553
|
+
except Exception:
|
|
554
|
+
return fallback
|
|
555
|
+
|
|
524
556
|
##############################
|
|
525
557
|
# Utils
|
|
526
558
|
##############################
|
|
@@ -538,9 +570,9 @@ class ClientLocal(Client):
|
|
|
538
570
|
----------
|
|
539
571
|
error_code : int
|
|
540
572
|
Error code identifying the type of error.
|
|
541
|
-
entity_type : str
|
|
573
|
+
entity_type : str
|
|
542
574
|
Entity type that caused the error.
|
|
543
|
-
entity_id : str
|
|
575
|
+
entity_id : str
|
|
544
576
|
Entity ID that caused the error.
|
|
545
577
|
|
|
546
578
|
Returns
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
from digitalhub.
|
|
7
|
+
from digitalhub.stores.client._base.enums import ApiCategories, BackendOperations
|
|
8
8
|
from digitalhub.stores.client._base.params_builder import ClientParametersBuilder
|
|
9
9
|
|
|
10
10
|
|
|
@@ -51,10 +51,13 @@ class ClientLocalParametersBuilder(ClientParametersBuilder):
|
|
|
51
51
|
dict
|
|
52
52
|
Parameters formatted.
|
|
53
53
|
"""
|
|
54
|
-
kwargs = self.
|
|
54
|
+
kwargs = self._ensure_params(**kwargs)
|
|
55
|
+
|
|
56
|
+
# Handle delete
|
|
55
57
|
if operation == BackendOperations.DELETE.value:
|
|
56
58
|
if (cascade := kwargs.pop("cascade", None)) is not None:
|
|
57
|
-
kwargs
|
|
59
|
+
kwargs = self._add_param("cascade", str(cascade).lower(), **kwargs)
|
|
60
|
+
|
|
58
61
|
return kwargs
|
|
59
62
|
|
|
60
63
|
def build_parameters_context(self, operation: str, **kwargs) -> dict:
|
|
@@ -73,48 +76,22 @@ class ClientLocalParametersBuilder(ClientParametersBuilder):
|
|
|
73
76
|
dict
|
|
74
77
|
Parameters formatted.
|
|
75
78
|
"""
|
|
76
|
-
kwargs = self.
|
|
79
|
+
kwargs = self._ensure_params(**kwargs)
|
|
77
80
|
|
|
78
|
-
# Handle read
|
|
81
|
+
# Handle read all versions
|
|
79
82
|
if operation == BackendOperations.READ_ALL_VERSIONS.value:
|
|
80
|
-
kwargs
|
|
81
|
-
kwargs
|
|
83
|
+
kwargs = self._add_param("versions", "all", **kwargs)
|
|
84
|
+
kwargs = self._add_param("name", kwargs.pop("name"), **kwargs)
|
|
85
|
+
|
|
82
86
|
# Handle delete
|
|
83
87
|
elif operation == BackendOperations.DELETE.value:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
kwargs["params"]["cascade"] = str(cascade).lower()
|
|
87
|
-
|
|
88
|
-
# Handle delete all versions
|
|
89
|
-
entity_id = kwargs.pop("entity_id")
|
|
90
|
-
entity_name = kwargs.pop("entity_name")
|
|
91
|
-
if not kwargs.pop("delete_all_versions", False):
|
|
92
|
-
if entity_id is None:
|
|
93
|
-
raise ValueError(
|
|
94
|
-
"If `delete_all_versions` is False, `entity_id` must be provided,"
|
|
95
|
-
" either as an argument or in key `identifier`.",
|
|
96
|
-
)
|
|
97
|
-
else:
|
|
98
|
-
kwargs["params"]["name"] = entity_name
|
|
99
|
-
return kwargs
|
|
100
|
-
|
|
101
|
-
@staticmethod
|
|
102
|
-
def _set_params(**kwargs) -> dict:
|
|
103
|
-
"""
|
|
104
|
-
Format params parameter.
|
|
88
|
+
if (cascade := kwargs.pop("cascade", None)) is not None:
|
|
89
|
+
kwargs = self._add_param("cascade", str(cascade).lower(), **kwargs)
|
|
105
90
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
91
|
+
# Handle delete all versions
|
|
92
|
+
elif operation == BackendOperations.DELETE_ALL_VERSIONS.value:
|
|
93
|
+
if (cascade := kwargs.pop("cascade", None)) is not None:
|
|
94
|
+
kwargs = self._add_param("cascade", str(cascade).lower(), **kwargs)
|
|
95
|
+
kwargs = self._add_param("name", kwargs.pop("name"), **kwargs)
|
|
110
96
|
|
|
111
|
-
Returns
|
|
112
|
-
-------
|
|
113
|
-
dict
|
|
114
|
-
Parameters with initialized params.
|
|
115
|
-
"""
|
|
116
|
-
if not kwargs:
|
|
117
|
-
kwargs = {}
|
|
118
|
-
if "params" not in kwargs:
|
|
119
|
-
kwargs["params"] = {}
|
|
120
97
|
return kwargs
|
|
@@ -53,30 +53,20 @@ class Configurator:
|
|
|
53
53
|
def load_configs(self) -> None:
|
|
54
54
|
"""
|
|
55
55
|
Load the configuration from both environment and file sources.
|
|
56
|
-
|
|
57
|
-
Returns
|
|
58
|
-
-------
|
|
59
|
-
None
|
|
60
56
|
"""
|
|
61
57
|
self.load_env_vars()
|
|
62
58
|
self.load_file_vars()
|
|
63
59
|
|
|
64
60
|
@abstractmethod
|
|
65
|
-
def load_env_vars(self) -> None:
|
|
66
|
-
...
|
|
61
|
+
def load_env_vars(self) -> None: ...
|
|
67
62
|
|
|
68
63
|
@abstractmethod
|
|
69
|
-
def load_file_vars(self) -> None:
|
|
70
|
-
...
|
|
64
|
+
def load_file_vars(self) -> None: ...
|
|
71
65
|
|
|
72
66
|
def check_config(self) -> None:
|
|
73
67
|
"""
|
|
74
68
|
Check if the current profile has changed and reload
|
|
75
69
|
the file credentials if needed.
|
|
76
|
-
|
|
77
|
-
Returns
|
|
78
|
-
-------
|
|
79
|
-
None
|
|
80
70
|
"""
|
|
81
71
|
if (current := self._creds_handler.get_current_profile()) != self._current_profile:
|
|
82
72
|
self.load_file_vars()
|
|
@@ -121,10 +111,6 @@ class Configurator:
|
|
|
121
111
|
"""
|
|
122
112
|
Attempt to change the origin of credentials.
|
|
123
113
|
Raise error if already evaluated.
|
|
124
|
-
|
|
125
|
-
Returns
|
|
126
|
-
-------
|
|
127
|
-
None
|
|
128
114
|
"""
|
|
129
115
|
try:
|
|
130
116
|
self.change_origin()
|
|
@@ -134,10 +120,6 @@ class Configurator:
|
|
|
134
120
|
def change_origin(self) -> None:
|
|
135
121
|
"""
|
|
136
122
|
Change the origin of credentials from env to file or vice versa.
|
|
137
|
-
|
|
138
|
-
Returns
|
|
139
|
-
-------
|
|
140
|
-
None
|
|
141
123
|
"""
|
|
142
124
|
if self._changed_origin:
|
|
143
125
|
raise ConfigError("Origin has already been changed.")
|
|
@@ -149,10 +131,6 @@ class Configurator:
|
|
|
149
131
|
def change_to_file(self) -> None:
|
|
150
132
|
"""
|
|
151
133
|
Set the credentials origin to file.
|
|
152
|
-
|
|
153
|
-
Returns
|
|
154
|
-
-------
|
|
155
|
-
None
|
|
156
134
|
"""
|
|
157
135
|
if self._origin == self._env:
|
|
158
136
|
self._changed_origin = True
|
|
@@ -161,10 +139,6 @@ class Configurator:
|
|
|
161
139
|
def change_to_env(self) -> None:
|
|
162
140
|
"""
|
|
163
141
|
Set the credentials origin to environment.
|
|
164
|
-
|
|
165
|
-
Returns
|
|
166
|
-
-------
|
|
167
|
-
None
|
|
168
142
|
"""
|
|
169
143
|
if self._origin == self._file:
|
|
170
144
|
self._changed_origin = True
|
|
@@ -64,10 +64,6 @@ class CredentialHandler:
|
|
|
64
64
|
----------
|
|
65
65
|
creds_set : str
|
|
66
66
|
Name of the credentials profile to set.
|
|
67
|
-
|
|
68
|
-
Returns
|
|
69
|
-
-------
|
|
70
|
-
None
|
|
71
67
|
"""
|
|
72
68
|
self._profile = creds_set
|
|
73
69
|
set_current_profile(creds_set)
|
|
@@ -127,10 +123,6 @@ class CredentialHandler:
|
|
|
127
123
|
----------
|
|
128
124
|
creds : dict
|
|
129
125
|
Credentials to write.
|
|
130
|
-
|
|
131
|
-
Returns
|
|
132
|
-
-------
|
|
133
|
-
None
|
|
134
126
|
"""
|
|
135
127
|
write_config(creds, self._profile)
|
|
136
128
|
|
|
@@ -148,10 +140,6 @@ class CredentialHandler:
|
|
|
148
140
|
The origin of the credentials ('env' or 'file').
|
|
149
141
|
creds : dict
|
|
150
142
|
Credentials to set.
|
|
151
|
-
|
|
152
|
-
Returns
|
|
153
|
-
-------
|
|
154
|
-
None
|
|
155
143
|
"""
|
|
156
144
|
self._creds_store.set_credentials(self._profile, origin, creds)
|
|
157
145
|
|
|
@@ -92,9 +92,6 @@ def write_config(creds: dict, environment: str) -> None:
|
|
|
92
92
|
environment : str
|
|
93
93
|
Name of the credentials profile/environment.
|
|
94
94
|
|
|
95
|
-
Returns
|
|
96
|
-
-------
|
|
97
|
-
None
|
|
98
95
|
|
|
99
96
|
Raises
|
|
100
97
|
------
|
|
@@ -129,9 +126,6 @@ def set_current_profile(environment: str) -> None:
|
|
|
129
126
|
environment : str
|
|
130
127
|
Name of the credentials profile to set as current.
|
|
131
128
|
|
|
132
|
-
Returns
|
|
133
|
-
-------
|
|
134
|
-
None
|
|
135
129
|
|
|
136
130
|
Raises
|
|
137
131
|
------
|
|
@@ -146,19 +140,3 @@ def set_current_profile(environment: str) -> None:
|
|
|
146
140
|
|
|
147
141
|
except Exception as e:
|
|
148
142
|
raise ClientError(f"Failed to write env file: {e}")
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def read_env_from_file() -> str | None:
|
|
152
|
-
"""
|
|
153
|
-
Read the current credentials profile name from the .dhcore.ini file.
|
|
154
|
-
|
|
155
|
-
Returns
|
|
156
|
-
-------
|
|
157
|
-
str or None
|
|
158
|
-
Name of the current credentials profile, or None if not found.
|
|
159
|
-
"""
|
|
160
|
-
try:
|
|
161
|
-
cfg = load_file()
|
|
162
|
-
return cfg["DEFAULT"]["current_environment"]
|
|
163
|
-
except Exception:
|
|
164
|
-
return None
|
|
@@ -42,10 +42,6 @@ class CredentialsStore:
|
|
|
42
42
|
Origin of the credentials ('env' or 'file').
|
|
43
43
|
credentials : dict of str to Any
|
|
44
44
|
Dictionary of credentials to set.
|
|
45
|
-
|
|
46
|
-
Returns
|
|
47
|
-
-------
|
|
48
|
-
None
|
|
49
45
|
"""
|
|
50
46
|
if origin == CredsOrigin.ENV.value:
|
|
51
47
|
for key, value in credentials.items():
|