digitalhub 0.14.0b5__py3-none-any.whl → 0.14.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- digitalhub/__init__.py +2 -2
- digitalhub/context/api.py +43 -6
- digitalhub/context/builder.py +1 -1
- digitalhub/context/context.py +3 -6
- digitalhub/entities/_base/context/entity.py +0 -3
- digitalhub/entities/_base/executable/entity.py +29 -11
- digitalhub/entities/_base/material/entity.py +2 -2
- digitalhub/entities/_base/material/utils.py +0 -4
- digitalhub/entities/_commons/enums.py +1 -0
- digitalhub/entities/_commons/utils.py +19 -0
- digitalhub/entities/_processors/base/crud.py +15 -24
- digitalhub/entities/_processors/base/import_export.py +3 -7
- digitalhub/entities/_processors/base/processor.py +4 -7
- digitalhub/entities/_processors/base/special_ops.py +4 -8
- digitalhub/entities/_processors/context/crud.py +27 -29
- digitalhub/entities/_processors/context/import_export.py +7 -7
- digitalhub/entities/_processors/context/material.py +2 -2
- digitalhub/entities/_processors/context/special_ops.py +25 -25
- digitalhub/entities/_processors/utils.py +7 -116
- digitalhub/entities/artifact/crud.py +3 -3
- digitalhub/entities/artifact/utils.py +2 -2
- digitalhub/entities/builders.py +2 -0
- digitalhub/entities/dataitem/crud.py +3 -3
- digitalhub/entities/dataitem/utils.py +10 -14
- digitalhub/entities/function/_base/entity.py +0 -3
- digitalhub/entities/function/crud.py +3 -3
- digitalhub/entities/model/crud.py +3 -3
- digitalhub/entities/model/mlflow/utils.py +29 -20
- digitalhub/entities/model/utils.py +2 -2
- digitalhub/entities/project/_base/builder.py +0 -6
- digitalhub/entities/project/_base/entity.py +264 -114
- digitalhub/entities/project/_base/spec.py +4 -4
- digitalhub/entities/project/crud.py +16 -51
- digitalhub/entities/project/utils.py +7 -3
- digitalhub/entities/secret/crud.py +2 -2
- digitalhub/entities/task/_base/models.py +13 -16
- digitalhub/entities/trigger/crud.py +28 -9
- digitalhub/entities/workflow/_base/entity.py +0 -5
- digitalhub/entities/workflow/crud.py +3 -6
- digitalhub/stores/client/{dhcore/api_builder.py → api_builder.py} +2 -3
- digitalhub/stores/client/builder.py +20 -32
- digitalhub/stores/client/client.py +322 -0
- digitalhub/stores/client/{dhcore/configurator.py → configurator.py} +148 -195
- digitalhub/stores/client/{_base/enums.py → enums.py} +11 -0
- digitalhub/stores/client/header_manager.py +61 -0
- digitalhub/stores/client/http_handler.py +152 -0
- digitalhub/stores/client/{_base/key_builder.py → key_builder.py} +14 -14
- digitalhub/stores/client/{dhcore/params_builder.py → params_builder.py} +51 -12
- digitalhub/stores/client/response_processor.py +102 -0
- digitalhub/stores/client/utils.py +35 -0
- digitalhub/stores/{credentials → configurator}/api.py +5 -9
- digitalhub/stores/configurator/configurator.py +123 -0
- digitalhub/stores/{credentials → configurator}/enums.py +26 -10
- digitalhub/stores/configurator/handler.py +213 -0
- digitalhub/stores/{credentials → configurator}/ini_module.py +31 -6
- digitalhub/stores/data/_base/store.py +0 -4
- digitalhub/stores/data/api.py +4 -6
- digitalhub/stores/data/builder.py +6 -38
- digitalhub/stores/data/s3/configurator.py +30 -114
- digitalhub/stores/data/s3/store.py +9 -22
- digitalhub/stores/data/sql/configurator.py +49 -71
- digitalhub/stores/data/sql/store.py +26 -61
- digitalhub/utils/generic_utils.py +0 -12
- digitalhub/utils/git_utils.py +0 -8
- digitalhub/utils/io_utils.py +0 -8
- digitalhub/utils/store_utils.py +1 -1
- {digitalhub-0.14.0b5.dist-info → digitalhub-0.14.9.dist-info}/METADATA +3 -3
- {digitalhub-0.14.0b5.dist-info → digitalhub-0.14.9.dist-info}/RECORD +73 -86
- {digitalhub-0.14.0b5.dist-info → digitalhub-0.14.9.dist-info}/WHEEL +1 -1
- digitalhub/stores/client/_base/api_builder.py +0 -34
- digitalhub/stores/client/_base/client.py +0 -243
- digitalhub/stores/client/_base/params_builder.py +0 -82
- digitalhub/stores/client/api.py +0 -32
- digitalhub/stores/client/dhcore/__init__.py +0 -3
- digitalhub/stores/client/dhcore/client.py +0 -553
- digitalhub/stores/client/dhcore/enums.py +0 -18
- digitalhub/stores/client/dhcore/key_builder.py +0 -62
- digitalhub/stores/client/dhcore/utils.py +0 -86
- digitalhub/stores/client/local/__init__.py +0 -3
- digitalhub/stores/client/local/api_builder.py +0 -116
- digitalhub/stores/client/local/client.py +0 -605
- digitalhub/stores/client/local/enums.py +0 -15
- digitalhub/stores/client/local/key_builder.py +0 -62
- digitalhub/stores/client/local/params_builder.py +0 -97
- digitalhub/stores/credentials/__init__.py +0 -3
- digitalhub/stores/credentials/configurator.py +0 -185
- digitalhub/stores/credentials/handler.py +0 -164
- digitalhub/stores/credentials/store.py +0 -77
- digitalhub/stores/data/enums.py +0 -15
- /digitalhub/stores/client/{dhcore/error_parser.py → error_parser.py} +0 -0
- /digitalhub/stores/{client/_base → configurator}/__init__.py +0 -0
- {digitalhub-0.14.0b5.dist-info → digitalhub-0.14.9.dist-info}/licenses/AUTHORS +0 -0
- {digitalhub-0.14.0b5.dist-info → digitalhub-0.14.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,605 +0,0 @@
|
|
|
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 copy import deepcopy
|
|
8
|
-
from datetime import datetime, timezone
|
|
9
|
-
from typing import Any
|
|
10
|
-
|
|
11
|
-
from digitalhub.stores.client._base.client import Client
|
|
12
|
-
from digitalhub.stores.client.local.api_builder import ClientLocalApiBuilder
|
|
13
|
-
from digitalhub.stores.client.local.enums import LocalClientVar
|
|
14
|
-
from digitalhub.stores.client.local.key_builder import ClientLocalKeyBuilder
|
|
15
|
-
from digitalhub.stores.client.local.params_builder import ClientLocalParametersBuilder
|
|
16
|
-
from digitalhub.utils.exceptions import BackendError
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class ClientLocal(Client):
|
|
20
|
-
"""
|
|
21
|
-
Local client.
|
|
22
|
-
|
|
23
|
-
The Local client can be used when a remote Digitalhub backend is not available.
|
|
24
|
-
It handles the creation, reading, updating and deleting of objects in memory,
|
|
25
|
-
storing them in a local dictionary.
|
|
26
|
-
The functionality of the Local client is almost the same as the DHCore client.
|
|
27
|
-
Main differences are:
|
|
28
|
-
- Local client does delete objects on cascade.
|
|
29
|
-
- The run execution are forced to be local.
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
def __init__(self) -> None:
|
|
33
|
-
super().__init__()
|
|
34
|
-
self._api_builder = ClientLocalApiBuilder()
|
|
35
|
-
self._key_builder = ClientLocalKeyBuilder()
|
|
36
|
-
self._params_builder = ClientLocalParametersBuilder()
|
|
37
|
-
self._db: dict[str, dict[str, dict]] = {}
|
|
38
|
-
|
|
39
|
-
##############################
|
|
40
|
-
# CRUD
|
|
41
|
-
##############################
|
|
42
|
-
|
|
43
|
-
def create_object(self, api: str, obj: Any, **kwargs) -> dict:
|
|
44
|
-
"""
|
|
45
|
-
Create an object in local.
|
|
46
|
-
|
|
47
|
-
Parameters
|
|
48
|
-
----------
|
|
49
|
-
api : str
|
|
50
|
-
Create API.
|
|
51
|
-
obj : dict
|
|
52
|
-
Object to create.
|
|
53
|
-
|
|
54
|
-
Returns
|
|
55
|
-
-------
|
|
56
|
-
dict
|
|
57
|
-
The created object.
|
|
58
|
-
"""
|
|
59
|
-
if api == LocalClientVar.EMPTY.value:
|
|
60
|
-
return {}
|
|
61
|
-
if not isinstance(obj, dict):
|
|
62
|
-
raise TypeError("Object must be a dictionary")
|
|
63
|
-
|
|
64
|
-
entity_type, _, context_api = self._parse_api(api)
|
|
65
|
-
try:
|
|
66
|
-
# Check if entity_type is valid
|
|
67
|
-
if entity_type is None:
|
|
68
|
-
raise TypeError
|
|
69
|
-
|
|
70
|
-
# Check if entity_type exists, if not, create a mapping
|
|
71
|
-
self._db.setdefault(entity_type, {})
|
|
72
|
-
|
|
73
|
-
# Base API
|
|
74
|
-
#
|
|
75
|
-
# POST /api/v1/projects
|
|
76
|
-
#
|
|
77
|
-
# Project are not versioned, everything is stored on "entity_id" key
|
|
78
|
-
if not context_api:
|
|
79
|
-
if entity_type == "projects":
|
|
80
|
-
entity_id = obj["name"]
|
|
81
|
-
if entity_id in self._db[entity_type]:
|
|
82
|
-
raise ValueError
|
|
83
|
-
self._db[entity_type][entity_id] = obj
|
|
84
|
-
|
|
85
|
-
# Context API
|
|
86
|
-
#
|
|
87
|
-
# POST /api/v1/-/<project-name>/artifacts
|
|
88
|
-
# POST /api/v1/-/<project-name>/functions
|
|
89
|
-
# POST /api/v1/-/<project-name>/runs
|
|
90
|
-
#
|
|
91
|
-
# Runs and tasks are not versioned, so we keep name as entity_id.
|
|
92
|
-
# We have both "name" and "id" attributes for versioned objects so we use them as storage keys.
|
|
93
|
-
# The "latest" key is used to store the latest version of the object.
|
|
94
|
-
else:
|
|
95
|
-
entity_id = obj["id"]
|
|
96
|
-
name = obj.get("name", entity_id)
|
|
97
|
-
self._db[entity_type].setdefault(name, {})
|
|
98
|
-
if entity_id in self._db[entity_type][name]:
|
|
99
|
-
raise ValueError
|
|
100
|
-
self._db[entity_type][name][entity_id] = obj
|
|
101
|
-
self._db[entity_type][name]["latest"] = obj
|
|
102
|
-
|
|
103
|
-
# Return the created object
|
|
104
|
-
return obj
|
|
105
|
-
|
|
106
|
-
# Key error are possibly raised by accessing invalid objects
|
|
107
|
-
except (KeyError, TypeError):
|
|
108
|
-
msg = self._format_msg(1, entity_type=entity_type)
|
|
109
|
-
raise BackendError(msg)
|
|
110
|
-
|
|
111
|
-
# If try to create already existing object
|
|
112
|
-
except ValueError:
|
|
113
|
-
msg = self._format_msg(2, entity_type=entity_type, entity_id=entity_id)
|
|
114
|
-
raise BackendError(msg)
|
|
115
|
-
|
|
116
|
-
def read_object(self, api: str, **kwargs) -> dict:
|
|
117
|
-
"""
|
|
118
|
-
Get an object from local.
|
|
119
|
-
|
|
120
|
-
Parameters
|
|
121
|
-
----------
|
|
122
|
-
api : str
|
|
123
|
-
Read API.
|
|
124
|
-
|
|
125
|
-
Returns
|
|
126
|
-
-------
|
|
127
|
-
dict
|
|
128
|
-
The read object.
|
|
129
|
-
"""
|
|
130
|
-
if api == LocalClientVar.EMPTY.value:
|
|
131
|
-
return {}
|
|
132
|
-
entity_type, entity_id, context_api = self._parse_api(api)
|
|
133
|
-
if entity_id is None:
|
|
134
|
-
msg = self._format_msg(4)
|
|
135
|
-
raise BackendError(msg)
|
|
136
|
-
try:
|
|
137
|
-
# Base API
|
|
138
|
-
#
|
|
139
|
-
# GET /api/v1/projects/<entity_id>
|
|
140
|
-
#
|
|
141
|
-
# self._parse_api() should return only entity_type
|
|
142
|
-
|
|
143
|
-
if not context_api:
|
|
144
|
-
obj = self._db[entity_type][entity_id]
|
|
145
|
-
|
|
146
|
-
# If the object is a project, we need to add the project spec,
|
|
147
|
-
# for example artifacts, functions, workflows, etc.
|
|
148
|
-
# Technically we have only projects that access base apis,
|
|
149
|
-
# we check entity_type just in case we add something else.
|
|
150
|
-
if entity_type == "projects":
|
|
151
|
-
obj = self._get_project_spec(obj, entity_id)
|
|
152
|
-
return obj
|
|
153
|
-
|
|
154
|
-
# Context API
|
|
155
|
-
#
|
|
156
|
-
# GET /api/v1/-/<project-name>/runs/<entity_id>
|
|
157
|
-
# GET /api/v1/-/<project-name>/artifacts/<entity_id>
|
|
158
|
-
# GET /api/v1/-/<project-name>/functions/<entity_id>
|
|
159
|
-
#
|
|
160
|
-
# self._parse_api() should return entity_type and entity_id/version
|
|
161
|
-
|
|
162
|
-
else:
|
|
163
|
-
for _, v in self._db[entity_type].items():
|
|
164
|
-
if entity_id in v:
|
|
165
|
-
return v[entity_id]
|
|
166
|
-
else:
|
|
167
|
-
raise KeyError
|
|
168
|
-
|
|
169
|
-
except KeyError:
|
|
170
|
-
msg = self._format_msg(3, entity_type=entity_type, entity_id=entity_id)
|
|
171
|
-
raise BackendError(msg)
|
|
172
|
-
|
|
173
|
-
def update_object(self, api: str, obj: Any, **kwargs) -> dict:
|
|
174
|
-
"""
|
|
175
|
-
Update an object in local.
|
|
176
|
-
|
|
177
|
-
Parameters
|
|
178
|
-
----------
|
|
179
|
-
api : str
|
|
180
|
-
Update API.
|
|
181
|
-
obj : dict
|
|
182
|
-
Object to update.
|
|
183
|
-
|
|
184
|
-
Returns
|
|
185
|
-
-------
|
|
186
|
-
dict
|
|
187
|
-
The updated object.
|
|
188
|
-
"""
|
|
189
|
-
if api == LocalClientVar.EMPTY.value:
|
|
190
|
-
return {}
|
|
191
|
-
if not isinstance(obj, dict):
|
|
192
|
-
raise TypeError("Object must be a dictionary")
|
|
193
|
-
|
|
194
|
-
entity_type, entity_id, context_api = self._parse_api(api)
|
|
195
|
-
try:
|
|
196
|
-
# API example
|
|
197
|
-
#
|
|
198
|
-
# PUT /api/v1/projects/<entity_id>
|
|
199
|
-
|
|
200
|
-
if not context_api:
|
|
201
|
-
self._db[entity_type][entity_id] = obj
|
|
202
|
-
|
|
203
|
-
# Context API
|
|
204
|
-
#
|
|
205
|
-
# PUT /api/v1/-/<project-name>/runs/<entity_id>
|
|
206
|
-
# PUT /api/v1/-/<project-name>/artifacts/<entity_id>
|
|
207
|
-
|
|
208
|
-
else:
|
|
209
|
-
name = obj.get("name", entity_id)
|
|
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
|
|
220
|
-
|
|
221
|
-
except KeyError:
|
|
222
|
-
msg = self._format_msg(3, entity_type=entity_type, entity_id=entity_id)
|
|
223
|
-
raise BackendError(msg)
|
|
224
|
-
|
|
225
|
-
return obj
|
|
226
|
-
|
|
227
|
-
def delete_object(self, api: str, **kwargs) -> dict:
|
|
228
|
-
"""
|
|
229
|
-
Delete an object from local.
|
|
230
|
-
|
|
231
|
-
Parameters
|
|
232
|
-
----------
|
|
233
|
-
api : str
|
|
234
|
-
Delete API.
|
|
235
|
-
**kwargs : dict
|
|
236
|
-
Keyword arguments parsed from request.
|
|
237
|
-
|
|
238
|
-
Returns
|
|
239
|
-
-------
|
|
240
|
-
dict
|
|
241
|
-
Response object.
|
|
242
|
-
"""
|
|
243
|
-
entity_type, entity_id, context_api = self._parse_api(api)
|
|
244
|
-
try:
|
|
245
|
-
# Base API
|
|
246
|
-
#
|
|
247
|
-
# DELETE /api/v1/projects/<entity_id>
|
|
248
|
-
|
|
249
|
-
if not context_api:
|
|
250
|
-
self._db[entity_type].pop(entity_id)
|
|
251
|
-
|
|
252
|
-
# Context API
|
|
253
|
-
#
|
|
254
|
-
# DELETE /api/v1/-/<project-name>/artifacts/<entity_id>
|
|
255
|
-
#
|
|
256
|
-
# We do not handle cascade in local client and
|
|
257
|
-
# in the sdk we selectively delete objects by id,
|
|
258
|
-
# not by name nor entity_type.
|
|
259
|
-
|
|
260
|
-
else:
|
|
261
|
-
reset_latest = False
|
|
262
|
-
|
|
263
|
-
# Name is optional and extracted from kwargs
|
|
264
|
-
# "params": {"name": <name>}
|
|
265
|
-
name_param = kwargs.get("params", {}).get("name")
|
|
266
|
-
|
|
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)
|
|
270
|
-
return {"deleted": True}
|
|
271
|
-
|
|
272
|
-
# Delete by id
|
|
273
|
-
found_name: str | None = None
|
|
274
|
-
container: dict | None = None
|
|
275
|
-
for n, v in self._db[entity_type].items():
|
|
276
|
-
if entity_id in v:
|
|
277
|
-
found_name = n
|
|
278
|
-
container = v
|
|
279
|
-
break
|
|
280
|
-
else:
|
|
281
|
-
raise KeyError
|
|
282
|
-
|
|
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]
|
|
310
|
-
|
|
311
|
-
except KeyError:
|
|
312
|
-
msg = self._format_msg(3, entity_type=entity_type, entity_id=entity_id)
|
|
313
|
-
raise BackendError(msg)
|
|
314
|
-
return {"deleted": True}
|
|
315
|
-
|
|
316
|
-
def list_objects(self, api: str, **kwargs) -> list:
|
|
317
|
-
"""
|
|
318
|
-
List objects.
|
|
319
|
-
|
|
320
|
-
Parameters
|
|
321
|
-
----------
|
|
322
|
-
api : str
|
|
323
|
-
List API.
|
|
324
|
-
**kwargs : dict
|
|
325
|
-
Keyword arguments parsed from request.
|
|
326
|
-
|
|
327
|
-
Returns
|
|
328
|
-
-------
|
|
329
|
-
list | None
|
|
330
|
-
The list of objects.
|
|
331
|
-
"""
|
|
332
|
-
entity_type, _, _ = self._parse_api(api)
|
|
333
|
-
|
|
334
|
-
# Name is optional and extracted from kwargs
|
|
335
|
-
# "params": {"name": <name>}
|
|
336
|
-
name = kwargs.get("params", {}).get("name")
|
|
337
|
-
if name is not None:
|
|
338
|
-
try:
|
|
339
|
-
return [self._db[entity_type][name]["latest"]]
|
|
340
|
-
except KeyError:
|
|
341
|
-
return []
|
|
342
|
-
|
|
343
|
-
try:
|
|
344
|
-
# If no name is provided, get latest objects
|
|
345
|
-
listed_objects = [v["latest"] for _, v in self._db[entity_type].items()]
|
|
346
|
-
except KeyError:
|
|
347
|
-
listed_objects = []
|
|
348
|
-
|
|
349
|
-
# If kind is provided, return objects by kind
|
|
350
|
-
kind = kwargs.get("params", {}).get("kind")
|
|
351
|
-
if kind is not None:
|
|
352
|
-
listed_objects = [obj for obj in listed_objects if obj["kind"] == kind]
|
|
353
|
-
|
|
354
|
-
# If function/task is provided, return objects by function/task
|
|
355
|
-
spec_params = ["function", "task"]
|
|
356
|
-
for i in spec_params:
|
|
357
|
-
p = kwargs.get("params", {}).get(i)
|
|
358
|
-
if p is not None:
|
|
359
|
-
listed_objects = [obj for obj in listed_objects if obj["spec"][i] == p]
|
|
360
|
-
|
|
361
|
-
return listed_objects
|
|
362
|
-
|
|
363
|
-
def list_first_object(self, api: str, **kwargs) -> dict:
|
|
364
|
-
"""
|
|
365
|
-
List first objects.
|
|
366
|
-
|
|
367
|
-
Parameters
|
|
368
|
-
----------
|
|
369
|
-
api : str
|
|
370
|
-
The api to list the objects with.
|
|
371
|
-
**kwargs : dict
|
|
372
|
-
Keyword arguments passed to the request.
|
|
373
|
-
|
|
374
|
-
Returns
|
|
375
|
-
-------
|
|
376
|
-
dict
|
|
377
|
-
The list of objects.
|
|
378
|
-
"""
|
|
379
|
-
try:
|
|
380
|
-
return self.list_objects(api, **kwargs)[0]
|
|
381
|
-
except IndexError:
|
|
382
|
-
raise IndexError("No objects found")
|
|
383
|
-
|
|
384
|
-
def search_objects(self, api: str, **kwargs) -> dict:
|
|
385
|
-
"""
|
|
386
|
-
Search objects from Local.
|
|
387
|
-
|
|
388
|
-
Parameters
|
|
389
|
-
----------
|
|
390
|
-
api : str
|
|
391
|
-
Search API.
|
|
392
|
-
**kwargs : dict
|
|
393
|
-
Keyword arguments to pass to the request.
|
|
394
|
-
|
|
395
|
-
Returns
|
|
396
|
-
-------
|
|
397
|
-
dict
|
|
398
|
-
Response objects.
|
|
399
|
-
"""
|
|
400
|
-
raise NotImplementedError("Local client does not support search_objects.")
|
|
401
|
-
|
|
402
|
-
##############################
|
|
403
|
-
# Helpers
|
|
404
|
-
##############################
|
|
405
|
-
|
|
406
|
-
def _parse_api(self, api: str) -> tuple:
|
|
407
|
-
"""
|
|
408
|
-
Parse the given API to extract the entity_type, entity_id
|
|
409
|
-
and if its a context API.
|
|
410
|
-
|
|
411
|
-
Parameters
|
|
412
|
-
----------
|
|
413
|
-
api : str
|
|
414
|
-
API to parse.
|
|
415
|
-
|
|
416
|
-
Returns
|
|
417
|
-
-------
|
|
418
|
-
tuple
|
|
419
|
-
Parsed elements.
|
|
420
|
-
"""
|
|
421
|
-
# Remove prefix from API
|
|
422
|
-
api = api.removeprefix("/api/v1/")
|
|
423
|
-
|
|
424
|
-
# Set context flag by default to False
|
|
425
|
-
context_api = False
|
|
426
|
-
|
|
427
|
-
# Remove context prefix from API and set context flag to True
|
|
428
|
-
if api.startswith("-/"):
|
|
429
|
-
context_api = True
|
|
430
|
-
api = api[2:]
|
|
431
|
-
|
|
432
|
-
# Return parsed elements
|
|
433
|
-
return self._parse_api_elements(api, context_api)
|
|
434
|
-
|
|
435
|
-
@staticmethod
|
|
436
|
-
def _parse_api_elements(api: str, context_api: bool) -> tuple:
|
|
437
|
-
"""
|
|
438
|
-
Parse the elements from the given API.
|
|
439
|
-
Elements returned are: entity_type, entity_id, context_api.
|
|
440
|
-
|
|
441
|
-
Parameters
|
|
442
|
-
----------
|
|
443
|
-
api : str
|
|
444
|
-
Parsed API.
|
|
445
|
-
context_api : bool
|
|
446
|
-
True if the API is a context API.
|
|
447
|
-
|
|
448
|
-
Returns
|
|
449
|
-
-------
|
|
450
|
-
tuple
|
|
451
|
-
Parsed elements from the API.
|
|
452
|
-
"""
|
|
453
|
-
# Split API path
|
|
454
|
-
parsed = api.split("/")
|
|
455
|
-
|
|
456
|
-
# Base API for versioned objects
|
|
457
|
-
|
|
458
|
-
# POST /api/v1/<entity_type>
|
|
459
|
-
# Returns entity_type, None, False
|
|
460
|
-
if len(parsed) == 1 and not context_api:
|
|
461
|
-
return parsed[0], None, context_api
|
|
462
|
-
|
|
463
|
-
# GET/DELETE/UPDATE /api/v1/<entity_type>/<entity_id>
|
|
464
|
-
# Return entity_type, entity_id, False
|
|
465
|
-
if len(parsed) == 2 and not context_api:
|
|
466
|
-
return parsed[0], parsed[1], context_api
|
|
467
|
-
|
|
468
|
-
# Context API for versioned objects
|
|
469
|
-
|
|
470
|
-
# POST /api/v1/-/<project>/<entity_type>
|
|
471
|
-
# Returns entity_type, None, True
|
|
472
|
-
if len(parsed) == 2 and context_api:
|
|
473
|
-
return parsed[1], None, context_api
|
|
474
|
-
|
|
475
|
-
# GET/DELETE/UPDATE /api/v1/-/<project>/<entity_type>/<entity_id>
|
|
476
|
-
# Return entity_type, entity_id, True
|
|
477
|
-
if len(parsed) == 3 and context_api:
|
|
478
|
-
return parsed[1], parsed[2], context_api
|
|
479
|
-
|
|
480
|
-
raise ValueError(f"Invalid API: {api}")
|
|
481
|
-
|
|
482
|
-
def _get_project_spec(self, obj: dict, name: str) -> dict:
|
|
483
|
-
"""
|
|
484
|
-
Enrich project object with spec (artifacts, functions, etc.).
|
|
485
|
-
|
|
486
|
-
Parameters
|
|
487
|
-
----------
|
|
488
|
-
obj : dict
|
|
489
|
-
The project object.
|
|
490
|
-
name : str
|
|
491
|
-
The project name.
|
|
492
|
-
|
|
493
|
-
Returns
|
|
494
|
-
-------
|
|
495
|
-
dict
|
|
496
|
-
The project object with the spec.
|
|
497
|
-
"""
|
|
498
|
-
# Deepcopy to avoid modifying the original object
|
|
499
|
-
project = deepcopy(obj)
|
|
500
|
-
# Ensure spec exists on the returned project
|
|
501
|
-
spec = project.setdefault("spec", {})
|
|
502
|
-
|
|
503
|
-
# Get all entities associated with the project specs
|
|
504
|
-
projects_entities = [k for k, _ in self._db.items() if k not in ["projects", "runs", "tasks"]]
|
|
505
|
-
|
|
506
|
-
for entity_type in projects_entities:
|
|
507
|
-
# Get all objects of the entity type for the project
|
|
508
|
-
objs = self._db.get(entity_type, {})
|
|
509
|
-
|
|
510
|
-
# Set empty list
|
|
511
|
-
spec[entity_type] = []
|
|
512
|
-
|
|
513
|
-
# Cycle through named objects
|
|
514
|
-
for _, named_entities in objs.items():
|
|
515
|
-
# Get latest version
|
|
516
|
-
for version, entity in named_entities.items():
|
|
517
|
-
if version != "latest":
|
|
518
|
-
continue
|
|
519
|
-
|
|
520
|
-
# Deepcopy to avoid modifying the original object
|
|
521
|
-
copied = deepcopy(entity)
|
|
522
|
-
|
|
523
|
-
# Remove spec if not embedded
|
|
524
|
-
if not copied.get("metadata", {}).get("embedded", False):
|
|
525
|
-
copied.pop("spec", None)
|
|
526
|
-
|
|
527
|
-
# Add to project spec
|
|
528
|
-
if copied["project"] == name:
|
|
529
|
-
spec[entity_type].append(copied)
|
|
530
|
-
|
|
531
|
-
return project
|
|
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
|
-
|
|
556
|
-
##############################
|
|
557
|
-
# Utils
|
|
558
|
-
##############################
|
|
559
|
-
|
|
560
|
-
@staticmethod
|
|
561
|
-
def _format_msg(
|
|
562
|
-
error_code: int,
|
|
563
|
-
entity_type: str | None = None,
|
|
564
|
-
entity_id: str | None = None,
|
|
565
|
-
) -> str:
|
|
566
|
-
"""
|
|
567
|
-
Format a message.
|
|
568
|
-
|
|
569
|
-
Parameters
|
|
570
|
-
----------
|
|
571
|
-
error_code : int
|
|
572
|
-
Error code identifying the type of error.
|
|
573
|
-
entity_type : str, optional
|
|
574
|
-
Entity type that caused the error.
|
|
575
|
-
entity_id : str, optional
|
|
576
|
-
Entity ID that caused the error.
|
|
577
|
-
|
|
578
|
-
Returns
|
|
579
|
-
-------
|
|
580
|
-
str
|
|
581
|
-
The formatted error message.
|
|
582
|
-
"""
|
|
583
|
-
msg = {
|
|
584
|
-
1: f"Object '{entity_type}' to create is not valid",
|
|
585
|
-
2: f"Object '{entity_type}' with id '{entity_id}' already exists",
|
|
586
|
-
3: f"Object '{entity_type}' with id '{entity_id}' not found",
|
|
587
|
-
4: "Must provide entity_id to read an object",
|
|
588
|
-
}
|
|
589
|
-
return msg[error_code]
|
|
590
|
-
|
|
591
|
-
##############################
|
|
592
|
-
# Interface methods
|
|
593
|
-
##############################
|
|
594
|
-
|
|
595
|
-
@staticmethod
|
|
596
|
-
def is_local() -> bool:
|
|
597
|
-
"""
|
|
598
|
-
Declare if Client is local.
|
|
599
|
-
|
|
600
|
-
Returns
|
|
601
|
-
-------
|
|
602
|
-
bool
|
|
603
|
-
True
|
|
604
|
-
"""
|
|
605
|
-
return True
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# SPDX-FileCopyrightText: © 2025 DSLab - Fondazione Bruno Kessler
|
|
2
|
-
#
|
|
3
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
from enum import Enum
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class LocalClientVar(Enum):
|
|
11
|
-
"""
|
|
12
|
-
Variables for Local.
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
EMPTY = "EMPTY"
|
|
@@ -1,62 +0,0 @@
|
|
|
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.client._base.key_builder import ClientKeyBuilder
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class ClientLocalKeyBuilder(ClientKeyBuilder):
|
|
11
|
-
"""
|
|
12
|
-
Class that build the key of entities.
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
def base_entity_key(self, entity_id: str) -> str:
|
|
16
|
-
"""
|
|
17
|
-
Build for base entity key.
|
|
18
|
-
|
|
19
|
-
Parameters
|
|
20
|
-
----------
|
|
21
|
-
entity_id : str
|
|
22
|
-
Entity id.
|
|
23
|
-
|
|
24
|
-
Returns
|
|
25
|
-
-------
|
|
26
|
-
str
|
|
27
|
-
Key.
|
|
28
|
-
"""
|
|
29
|
-
return f"store://{entity_id}"
|
|
30
|
-
|
|
31
|
-
def context_entity_key(
|
|
32
|
-
self,
|
|
33
|
-
project: str,
|
|
34
|
-
entity_type: str,
|
|
35
|
-
entity_kind: str,
|
|
36
|
-
entity_name: str,
|
|
37
|
-
entity_id: str | None = None,
|
|
38
|
-
) -> str:
|
|
39
|
-
"""
|
|
40
|
-
Build for context entity key.
|
|
41
|
-
|
|
42
|
-
Parameters
|
|
43
|
-
----------
|
|
44
|
-
project : str
|
|
45
|
-
Project name.
|
|
46
|
-
entity_type : str
|
|
47
|
-
Entity type.
|
|
48
|
-
entity_kind : str
|
|
49
|
-
Entity kind.
|
|
50
|
-
entity_name : str
|
|
51
|
-
Entity name.
|
|
52
|
-
entity_id : str
|
|
53
|
-
Entity ID.
|
|
54
|
-
|
|
55
|
-
Returns
|
|
56
|
-
-------
|
|
57
|
-
str
|
|
58
|
-
Key.
|
|
59
|
-
"""
|
|
60
|
-
if entity_id is None:
|
|
61
|
-
return f"store://{project}/{entity_type}/{entity_kind}/{entity_name}"
|
|
62
|
-
return f"store://{project}/{entity_type}/{entity_kind}/{entity_name}:{entity_id}"
|