digitalhub 0.8.0b0__py3-none-any.whl → 0.8.0b2__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 +62 -94
- digitalhub/client/__init__.py +0 -0
- digitalhub/client/builder.py +105 -0
- digitalhub/client/objects/__init__.py +0 -0
- digitalhub/client/objects/base.py +56 -0
- digitalhub/client/objects/dhcore.py +681 -0
- digitalhub/client/objects/local.py +533 -0
- digitalhub/context/__init__.py +0 -0
- digitalhub/context/builder.py +178 -0
- digitalhub/context/context.py +136 -0
- digitalhub/datastores/__init__.py +0 -0
- digitalhub/datastores/builder.py +134 -0
- digitalhub/datastores/objects/__init__.py +0 -0
- digitalhub/datastores/objects/base.py +85 -0
- digitalhub/datastores/objects/local.py +42 -0
- digitalhub/datastores/objects/remote.py +23 -0
- digitalhub/datastores/objects/s3.py +38 -0
- digitalhub/datastores/objects/sql.py +60 -0
- digitalhub/entities/__init__.py +0 -0
- digitalhub/entities/_base/__init__.py +0 -0
- digitalhub/entities/_base/api.py +346 -0
- digitalhub/entities/_base/base.py +82 -0
- digitalhub/entities/_base/crud.py +610 -0
- digitalhub/entities/_base/entity/__init__.py +0 -0
- digitalhub/entities/_base/entity/base.py +132 -0
- digitalhub/entities/_base/entity/context.py +118 -0
- digitalhub/entities/_base/entity/executable.py +380 -0
- digitalhub/entities/_base/entity/material.py +214 -0
- digitalhub/entities/_base/entity/unversioned.py +87 -0
- digitalhub/entities/_base/entity/versioned.py +94 -0
- digitalhub/entities/_base/metadata.py +59 -0
- digitalhub/entities/_base/spec/__init__.py +0 -0
- digitalhub/entities/_base/spec/base.py +58 -0
- digitalhub/entities/_base/spec/material.py +22 -0
- digitalhub/entities/_base/state.py +31 -0
- digitalhub/entities/_base/status/__init__.py +0 -0
- digitalhub/entities/_base/status/base.py +32 -0
- digitalhub/entities/_base/status/material.py +49 -0
- digitalhub/entities/_builders/__init__.py +0 -0
- digitalhub/entities/_builders/metadata.py +60 -0
- digitalhub/entities/_builders/name.py +31 -0
- digitalhub/entities/_builders/spec.py +43 -0
- digitalhub/entities/_builders/status.py +62 -0
- digitalhub/entities/_builders/uuid.py +33 -0
- digitalhub/entities/artifact/__init__.py +0 -0
- digitalhub/entities/artifact/builder.py +133 -0
- digitalhub/entities/artifact/crud.py +358 -0
- digitalhub/entities/artifact/entity/__init__.py +0 -0
- digitalhub/entities/artifact/entity/_base.py +39 -0
- digitalhub/entities/artifact/entity/artifact.py +9 -0
- digitalhub/entities/artifact/spec.py +39 -0
- digitalhub/entities/artifact/status.py +15 -0
- digitalhub/entities/dataitem/__init__.py +0 -0
- digitalhub/entities/dataitem/builder.py +144 -0
- digitalhub/entities/dataitem/crud.py +395 -0
- digitalhub/entities/dataitem/entity/__init__.py +0 -0
- digitalhub/entities/dataitem/entity/_base.py +75 -0
- digitalhub/entities/dataitem/entity/dataitem.py +9 -0
- digitalhub/entities/dataitem/entity/iceberg.py +7 -0
- digitalhub/entities/dataitem/entity/table.py +125 -0
- digitalhub/entities/dataitem/models.py +62 -0
- digitalhub/entities/dataitem/spec.py +61 -0
- digitalhub/entities/dataitem/status.py +38 -0
- digitalhub/entities/entity_types.py +19 -0
- digitalhub/entities/function/__init__.py +0 -0
- digitalhub/entities/function/builder.py +86 -0
- digitalhub/entities/function/crud.py +305 -0
- digitalhub/entities/function/entity.py +101 -0
- digitalhub/entities/function/models.py +118 -0
- digitalhub/entities/function/spec.py +81 -0
- digitalhub/entities/function/status.py +9 -0
- digitalhub/entities/model/__init__.py +0 -0
- digitalhub/entities/model/builder.py +152 -0
- digitalhub/entities/model/crud.py +358 -0
- digitalhub/entities/model/entity/__init__.py +0 -0
- digitalhub/entities/model/entity/_base.py +34 -0
- digitalhub/entities/model/entity/huggingface.py +9 -0
- digitalhub/entities/model/entity/mlflow.py +90 -0
- digitalhub/entities/model/entity/model.py +9 -0
- digitalhub/entities/model/entity/sklearn.py +9 -0
- digitalhub/entities/model/models.py +26 -0
- digitalhub/entities/model/spec.py +146 -0
- digitalhub/entities/model/status.py +33 -0
- digitalhub/entities/project/__init__.py +0 -0
- digitalhub/entities/project/builder.py +82 -0
- digitalhub/entities/project/crud.py +350 -0
- digitalhub/entities/project/entity.py +2060 -0
- digitalhub/entities/project/spec.py +50 -0
- digitalhub/entities/project/status.py +9 -0
- digitalhub/entities/registries.py +48 -0
- digitalhub/entities/run/__init__.py +0 -0
- digitalhub/entities/run/builder.py +77 -0
- digitalhub/entities/run/crud.py +232 -0
- digitalhub/entities/run/entity.py +461 -0
- digitalhub/entities/run/spec.py +153 -0
- digitalhub/entities/run/status.py +114 -0
- digitalhub/entities/secret/__init__.py +0 -0
- digitalhub/entities/secret/builder.py +93 -0
- digitalhub/entities/secret/crud.py +294 -0
- digitalhub/entities/secret/entity.py +73 -0
- digitalhub/entities/secret/spec.py +35 -0
- digitalhub/entities/secret/status.py +9 -0
- digitalhub/entities/task/__init__.py +0 -0
- digitalhub/entities/task/builder.py +74 -0
- digitalhub/entities/task/crud.py +241 -0
- digitalhub/entities/task/entity.py +135 -0
- digitalhub/entities/task/models.py +199 -0
- digitalhub/entities/task/spec.py +51 -0
- digitalhub/entities/task/status.py +9 -0
- digitalhub/entities/utils.py +184 -0
- digitalhub/entities/workflow/__init__.py +0 -0
- digitalhub/entities/workflow/builder.py +91 -0
- digitalhub/entities/workflow/crud.py +304 -0
- digitalhub/entities/workflow/entity.py +77 -0
- digitalhub/entities/workflow/spec.py +15 -0
- digitalhub/entities/workflow/status.py +9 -0
- digitalhub/readers/__init__.py +0 -0
- digitalhub/readers/builder.py +54 -0
- digitalhub/readers/objects/__init__.py +0 -0
- digitalhub/readers/objects/base.py +70 -0
- digitalhub/readers/objects/pandas.py +207 -0
- digitalhub/readers/registry.py +15 -0
- digitalhub/registry/__init__.py +0 -0
- digitalhub/registry/models.py +87 -0
- digitalhub/registry/registry.py +74 -0
- digitalhub/registry/utils.py +150 -0
- digitalhub/runtimes/__init__.py +0 -0
- digitalhub/runtimes/base.py +164 -0
- digitalhub/runtimes/builder.py +53 -0
- digitalhub/runtimes/kind_registry.py +170 -0
- digitalhub/stores/__init__.py +0 -0
- digitalhub/stores/builder.py +257 -0
- digitalhub/stores/objects/__init__.py +0 -0
- digitalhub/stores/objects/base.py +189 -0
- digitalhub/stores/objects/local.py +230 -0
- digitalhub/stores/objects/remote.py +143 -0
- digitalhub/stores/objects/s3.py +563 -0
- digitalhub/stores/objects/sql.py +328 -0
- digitalhub/utils/__init__.py +0 -0
- digitalhub/utils/data_utils.py +127 -0
- digitalhub/utils/env_utils.py +123 -0
- digitalhub/utils/exceptions.py +55 -0
- digitalhub/utils/file_utils.py +204 -0
- digitalhub/utils/generic_utils.py +207 -0
- digitalhub/utils/git_utils.py +148 -0
- digitalhub/utils/io_utils.py +79 -0
- digitalhub/utils/logger.py +17 -0
- digitalhub/utils/uri_utils.py +56 -0
- {digitalhub-0.8.0b0.dist-info → digitalhub-0.8.0b2.dist-info}/METADATA +27 -12
- digitalhub-0.8.0b2.dist-info/RECORD +161 -0
- test/test_crud_artifacts.py +1 -1
- test/test_crud_dataitems.py +1 -1
- test/test_crud_functions.py +1 -1
- test/test_crud_runs.py +1 -1
- test/test_crud_tasks.py +1 -1
- digitalhub-0.8.0b0.dist-info/RECORD +0 -14
- {digitalhub-0.8.0b0.dist-info → digitalhub-0.8.0b2.dist-info}/LICENSE.txt +0 -0
- {digitalhub-0.8.0b0.dist-info → digitalhub-0.8.0b2.dist-info}/WHEEL +0 -0
- {digitalhub-0.8.0b0.dist-info → digitalhub-0.8.0b2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
|
|
6
|
+
from digitalhub.client.objects.base import Client
|
|
7
|
+
from digitalhub.utils.exceptions import BackendError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ClientLocal(Client):
|
|
11
|
+
"""
|
|
12
|
+
Local client.
|
|
13
|
+
|
|
14
|
+
The Local client can be used when a remote Digitalhub backend is not available.
|
|
15
|
+
It handles the creation, reading, updating and deleting of objects in memory,
|
|
16
|
+
storing them in a local dictionary.
|
|
17
|
+
The functionality of the Local client is almost the same as the DHCore client.
|
|
18
|
+
Main differences are:
|
|
19
|
+
- Local client does delete objects on cascade.
|
|
20
|
+
- The run execution are forced to be local.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
super().__init__()
|
|
25
|
+
self._db: dict[str, dict[str, dict]] = {}
|
|
26
|
+
|
|
27
|
+
##############################
|
|
28
|
+
# CRUD
|
|
29
|
+
##############################
|
|
30
|
+
|
|
31
|
+
def create_object(self, api: str, obj: dict, **kwargs) -> dict:
|
|
32
|
+
"""
|
|
33
|
+
Create an object in local.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
api : str
|
|
38
|
+
Create API.
|
|
39
|
+
obj : dict
|
|
40
|
+
Object to create.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
dict
|
|
45
|
+
The created object.
|
|
46
|
+
"""
|
|
47
|
+
entity_type, _, context_api = self._parse_api(api)
|
|
48
|
+
try:
|
|
49
|
+
# Check if entity_type is valid
|
|
50
|
+
if entity_type is None:
|
|
51
|
+
raise TypeError
|
|
52
|
+
|
|
53
|
+
# Check if entity_type exists, if not, create a mapping
|
|
54
|
+
self._db.setdefault(entity_type, {})
|
|
55
|
+
|
|
56
|
+
# Base API
|
|
57
|
+
#
|
|
58
|
+
# POST /api/v1/projects
|
|
59
|
+
#
|
|
60
|
+
# Project are not versioned, everything is stored on "entity_id" key
|
|
61
|
+
if not context_api:
|
|
62
|
+
if entity_type == "projects":
|
|
63
|
+
entity_id = obj["name"]
|
|
64
|
+
if entity_id in self._db[entity_type]:
|
|
65
|
+
raise ValueError
|
|
66
|
+
self._db[entity_type][entity_id] = obj
|
|
67
|
+
|
|
68
|
+
# Context API
|
|
69
|
+
#
|
|
70
|
+
# POST /api/v1/-/<project-name>/artifacts
|
|
71
|
+
# POST /api/v1/-/<project-name>/functions
|
|
72
|
+
# POST /api/v1/-/<project-name>/runs
|
|
73
|
+
#
|
|
74
|
+
# Runs and tasks are not versioned, so we keep name as entity_id.
|
|
75
|
+
# We have both "name" and "id" attributes for versioned objects so we use them as storage keys.
|
|
76
|
+
# The "latest" key is used to store the latest version of the object.
|
|
77
|
+
else:
|
|
78
|
+
entity_id = obj["id"]
|
|
79
|
+
name = obj.get("name", entity_id)
|
|
80
|
+
self._db[entity_type].setdefault(name, {})
|
|
81
|
+
if entity_id in self._db[entity_type][name]:
|
|
82
|
+
raise ValueError
|
|
83
|
+
self._db[entity_type][name][entity_id] = obj
|
|
84
|
+
self._db[entity_type][name]["latest"] = obj
|
|
85
|
+
|
|
86
|
+
# Return the created object
|
|
87
|
+
return obj
|
|
88
|
+
|
|
89
|
+
# Key error are possibly raised by accessing invalid objects
|
|
90
|
+
except (KeyError, TypeError):
|
|
91
|
+
msg = self._format_msg(1, entity_type=entity_type)
|
|
92
|
+
raise BackendError(msg)
|
|
93
|
+
|
|
94
|
+
# If try to create already existing object
|
|
95
|
+
except ValueError:
|
|
96
|
+
msg = self._format_msg(2, entity_type=entity_type, entity_id=entity_id)
|
|
97
|
+
raise BackendError(msg)
|
|
98
|
+
|
|
99
|
+
def read_object(self, api: str, **kwargs) -> dict:
|
|
100
|
+
"""
|
|
101
|
+
Get an object from local.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
api : str
|
|
106
|
+
Read API.
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
dict
|
|
111
|
+
The read object.
|
|
112
|
+
"""
|
|
113
|
+
entity_type, entity_id, context_api = self._parse_api(api)
|
|
114
|
+
if entity_id is None:
|
|
115
|
+
msg = self._format_msg(4)
|
|
116
|
+
raise BackendError(msg)
|
|
117
|
+
try:
|
|
118
|
+
# Base API
|
|
119
|
+
#
|
|
120
|
+
# GET /api/v1/projects/<entity_id>
|
|
121
|
+
#
|
|
122
|
+
# self._parse_api() should return only entity_type
|
|
123
|
+
|
|
124
|
+
if not context_api:
|
|
125
|
+
obj = self._db[entity_type][entity_id]
|
|
126
|
+
|
|
127
|
+
# If the object is a project, we need to add the project spec,
|
|
128
|
+
# for example artifacts, functions, workflows, etc.
|
|
129
|
+
# Technically we have only projects that access base apis,
|
|
130
|
+
# we check entity_type just in case we add something else.
|
|
131
|
+
if entity_type == "projects":
|
|
132
|
+
obj = self._get_project_spec(obj, entity_id)
|
|
133
|
+
return obj
|
|
134
|
+
|
|
135
|
+
# Context API
|
|
136
|
+
#
|
|
137
|
+
# GET /api/v1/-/<project-name>/runs/<entity_id>
|
|
138
|
+
# GET /api/v1/-/<project-name>/artifacts/<entity_id>
|
|
139
|
+
# GET /api/v1/-/<project-name>/functions/<entity_id>
|
|
140
|
+
#
|
|
141
|
+
# self._parse_api() should return entity_type and entity_id/version
|
|
142
|
+
|
|
143
|
+
else:
|
|
144
|
+
for _, v in self._db[entity_type].items():
|
|
145
|
+
if entity_id in v:
|
|
146
|
+
return v[entity_id]
|
|
147
|
+
else:
|
|
148
|
+
raise KeyError
|
|
149
|
+
|
|
150
|
+
except KeyError:
|
|
151
|
+
msg = self._format_msg(3, entity_type=entity_type, entity_id=entity_id)
|
|
152
|
+
raise BackendError(msg)
|
|
153
|
+
|
|
154
|
+
def update_object(self, api: str, obj: dict, **kwargs) -> dict:
|
|
155
|
+
"""
|
|
156
|
+
Update an object in local.
|
|
157
|
+
|
|
158
|
+
Parameters
|
|
159
|
+
----------
|
|
160
|
+
api : str
|
|
161
|
+
Update API.
|
|
162
|
+
obj : dict
|
|
163
|
+
Object to update.
|
|
164
|
+
|
|
165
|
+
Returns
|
|
166
|
+
-------
|
|
167
|
+
dict
|
|
168
|
+
The updated object.
|
|
169
|
+
"""
|
|
170
|
+
entity_type, entity_id, context_api = self._parse_api(api)
|
|
171
|
+
try:
|
|
172
|
+
# API example
|
|
173
|
+
#
|
|
174
|
+
# PUT /api/v1/projects/<entity_id>
|
|
175
|
+
|
|
176
|
+
if not context_api:
|
|
177
|
+
self._db[entity_type][entity_id] = obj
|
|
178
|
+
|
|
179
|
+
# Context API
|
|
180
|
+
#
|
|
181
|
+
# PUT /api/v1/-/<project-name>/runs/<entity_id>
|
|
182
|
+
# PUT /api/v1/-/<project-name>/artifacts/<entity_id>
|
|
183
|
+
|
|
184
|
+
else:
|
|
185
|
+
name = obj.get("name", entity_id)
|
|
186
|
+
self._db[entity_type][name][entity_id] = obj
|
|
187
|
+
|
|
188
|
+
except KeyError:
|
|
189
|
+
msg = self._format_msg(3, entity_type=entity_type, entity_id=entity_id)
|
|
190
|
+
raise BackendError(msg)
|
|
191
|
+
|
|
192
|
+
return obj
|
|
193
|
+
|
|
194
|
+
def delete_object(self, api: str, **kwargs) -> dict:
|
|
195
|
+
"""
|
|
196
|
+
Delete an object from local.
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
api : str
|
|
201
|
+
Delete API.
|
|
202
|
+
**kwargs : dict
|
|
203
|
+
Keyword arguments parsed from request.
|
|
204
|
+
|
|
205
|
+
Returns
|
|
206
|
+
-------
|
|
207
|
+
dict
|
|
208
|
+
Response object.
|
|
209
|
+
"""
|
|
210
|
+
entity_type, entity_id, context_api = self._parse_api(api)
|
|
211
|
+
try:
|
|
212
|
+
# Base API
|
|
213
|
+
#
|
|
214
|
+
# DELETE /api/v1/projects/<entity_id>
|
|
215
|
+
|
|
216
|
+
if not context_api:
|
|
217
|
+
self._db[entity_type].pop(entity_id)
|
|
218
|
+
|
|
219
|
+
# Context API
|
|
220
|
+
#
|
|
221
|
+
# DELETE /api/v1/-/<project-name>/artifacts/<entity_id>
|
|
222
|
+
#
|
|
223
|
+
# We do not handle cascade in local client and
|
|
224
|
+
# in the sdk we selectively delete objects by id,
|
|
225
|
+
# not by name nor entity_type.
|
|
226
|
+
|
|
227
|
+
else:
|
|
228
|
+
reset_latest = False
|
|
229
|
+
|
|
230
|
+
# Name is optional and extracted from kwargs
|
|
231
|
+
# "params": {"name": <name>}
|
|
232
|
+
name = kwargs.get("params", {}).get("name")
|
|
233
|
+
|
|
234
|
+
# Delete by name
|
|
235
|
+
if entity_id is None and name is not None:
|
|
236
|
+
self._db[entity_type].pop(name, None)
|
|
237
|
+
return {"deleted": True}
|
|
238
|
+
|
|
239
|
+
# Delete by id
|
|
240
|
+
for _, v in self._db[entity_type].items():
|
|
241
|
+
if entity_id in v:
|
|
242
|
+
v.pop(entity_id)
|
|
243
|
+
|
|
244
|
+
# Handle latest
|
|
245
|
+
if v["latest"]["id"] == entity_id:
|
|
246
|
+
name = v["latest"].get("name", entity_id)
|
|
247
|
+
v.pop("latest")
|
|
248
|
+
reset_latest = True
|
|
249
|
+
break
|
|
250
|
+
else:
|
|
251
|
+
raise KeyError
|
|
252
|
+
|
|
253
|
+
if name is not None:
|
|
254
|
+
# Pop name if empty
|
|
255
|
+
if not self._db[entity_type][name]:
|
|
256
|
+
self._db[entity_type].pop(name)
|
|
257
|
+
|
|
258
|
+
# Handle latest
|
|
259
|
+
elif reset_latest:
|
|
260
|
+
latest_uuid = None
|
|
261
|
+
latest_date = None
|
|
262
|
+
for k, v in self._db[entity_type][name].items():
|
|
263
|
+
# Get created from metadata. If tzinfo is None, set it to UTC
|
|
264
|
+
# If created is not in ISO format, use fallback
|
|
265
|
+
fallback = datetime.fromtimestamp(0, timezone.utc)
|
|
266
|
+
try:
|
|
267
|
+
current_created = datetime.fromisoformat(v.get("metadata", {}).get("created"))
|
|
268
|
+
if current_created.tzinfo is None:
|
|
269
|
+
current_created = current_created.replace(tzinfo=timezone.utc)
|
|
270
|
+
except ValueError:
|
|
271
|
+
current_created = fallback
|
|
272
|
+
|
|
273
|
+
# Update latest date and uuid
|
|
274
|
+
if latest_date is None or current_created > latest_date:
|
|
275
|
+
latest_uuid = k
|
|
276
|
+
latest_date = current_created
|
|
277
|
+
|
|
278
|
+
# Set new latest
|
|
279
|
+
if latest_uuid is not None:
|
|
280
|
+
self._db[entity_type][name]["latest"] = self._db[entity_type][name][latest_uuid]
|
|
281
|
+
|
|
282
|
+
except KeyError:
|
|
283
|
+
msg = self._format_msg(3, entity_type=entity_type, entity_id=entity_id)
|
|
284
|
+
raise BackendError(msg)
|
|
285
|
+
return {"deleted": True}
|
|
286
|
+
|
|
287
|
+
def list_objects(self, api: str, **kwargs) -> list:
|
|
288
|
+
"""
|
|
289
|
+
List objects.
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
api : str
|
|
294
|
+
List API.
|
|
295
|
+
**kwargs : dict
|
|
296
|
+
Keyword arguments parsed from request.
|
|
297
|
+
|
|
298
|
+
Returns
|
|
299
|
+
-------
|
|
300
|
+
list | None
|
|
301
|
+
The list of objects.
|
|
302
|
+
"""
|
|
303
|
+
entity_type, _, _ = self._parse_api(api)
|
|
304
|
+
|
|
305
|
+
# Name is optional and extracted from kwargs
|
|
306
|
+
# "params": {"name": <name>}
|
|
307
|
+
name = kwargs.get("params", {}).get("name")
|
|
308
|
+
if name is not None:
|
|
309
|
+
return [self._db[entity_type][name]["latest"]]
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
# If no name is provided, get latest objects
|
|
313
|
+
listed_objects = [v["latest"] for _, v in self._db[entity_type].items()]
|
|
314
|
+
except KeyError:
|
|
315
|
+
listed_objects = []
|
|
316
|
+
|
|
317
|
+
# If kind is provided, return objects by kind
|
|
318
|
+
kind = kwargs.get("params", {}).get("kind")
|
|
319
|
+
if kind is not None:
|
|
320
|
+
listed_objects = [obj for obj in listed_objects if obj["kind"] == kind]
|
|
321
|
+
|
|
322
|
+
# If function is provided, return objects by function
|
|
323
|
+
spec_params = ["function", "task"]
|
|
324
|
+
for i in spec_params:
|
|
325
|
+
p = kwargs.get("params", {}).get(i)
|
|
326
|
+
if p is not None:
|
|
327
|
+
listed_objects = [obj for obj in listed_objects if obj["spec"][i] == p]
|
|
328
|
+
|
|
329
|
+
return listed_objects
|
|
330
|
+
|
|
331
|
+
def list_first_object(self, api: str, **kwargs) -> dict:
|
|
332
|
+
"""
|
|
333
|
+
List first objects.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
api : str
|
|
338
|
+
The api to list the objects with.
|
|
339
|
+
**kwargs : dict
|
|
340
|
+
Keyword arguments passed to the request.
|
|
341
|
+
|
|
342
|
+
Returns
|
|
343
|
+
-------
|
|
344
|
+
dict
|
|
345
|
+
The list of objects.
|
|
346
|
+
"""
|
|
347
|
+
try:
|
|
348
|
+
return self.list_objects(api, **kwargs)[0]
|
|
349
|
+
except IndexError:
|
|
350
|
+
raise IndexError("No objects found")
|
|
351
|
+
|
|
352
|
+
##############################
|
|
353
|
+
# Helpers
|
|
354
|
+
##############################
|
|
355
|
+
|
|
356
|
+
def _parse_api(self, api: str) -> tuple:
|
|
357
|
+
"""
|
|
358
|
+
Parse the given API to extract the entity_type, entity_id
|
|
359
|
+
and if its a context API.
|
|
360
|
+
|
|
361
|
+
Parameters
|
|
362
|
+
----------
|
|
363
|
+
api : str
|
|
364
|
+
API to parse.
|
|
365
|
+
|
|
366
|
+
Returns
|
|
367
|
+
-------
|
|
368
|
+
tuple
|
|
369
|
+
Parsed elements.
|
|
370
|
+
"""
|
|
371
|
+
# Remove prefix from API
|
|
372
|
+
api = api.removeprefix("/api/v1/")
|
|
373
|
+
|
|
374
|
+
# Set context flag by default to False
|
|
375
|
+
context_api = False
|
|
376
|
+
|
|
377
|
+
# Remove context prefix from API and set context flag to True
|
|
378
|
+
if api.startswith("-/"):
|
|
379
|
+
context_api = True
|
|
380
|
+
api = api[2:]
|
|
381
|
+
|
|
382
|
+
# Return parsed elements
|
|
383
|
+
return self._parse_api_elements(api, context_api)
|
|
384
|
+
|
|
385
|
+
@staticmethod
|
|
386
|
+
def _parse_api_elements(api: str, context_api: bool) -> tuple:
|
|
387
|
+
"""
|
|
388
|
+
Parse the elements from the given API.
|
|
389
|
+
Elements returned are: entity_type, entity_id, context_api.
|
|
390
|
+
|
|
391
|
+
Parameters
|
|
392
|
+
----------
|
|
393
|
+
api : str
|
|
394
|
+
Parsed API.
|
|
395
|
+
context_api : bool
|
|
396
|
+
True if the API is a context API.
|
|
397
|
+
|
|
398
|
+
Returns
|
|
399
|
+
-------
|
|
400
|
+
tuple
|
|
401
|
+
Parsed elements from the API.
|
|
402
|
+
"""
|
|
403
|
+
# Split API path
|
|
404
|
+
parsed = api.split("/")
|
|
405
|
+
|
|
406
|
+
# Base API for versioned objects
|
|
407
|
+
|
|
408
|
+
# POST /api/v1/<entity_type>
|
|
409
|
+
# Returns entity_type, None, False
|
|
410
|
+
if len(parsed) == 1 and not context_api:
|
|
411
|
+
return parsed[0], None, context_api
|
|
412
|
+
|
|
413
|
+
# GET/DELETE/UPDATE /api/v1/<entity_type>/<entity_id>
|
|
414
|
+
# Return entity_type, entity_id, False
|
|
415
|
+
if len(parsed) == 2 and not context_api:
|
|
416
|
+
return parsed[0], parsed[1], context_api
|
|
417
|
+
|
|
418
|
+
# Context API for versioned objects
|
|
419
|
+
|
|
420
|
+
# POST /api/v1/-/<project>/<entity_type>
|
|
421
|
+
# Returns entity_type, None, True
|
|
422
|
+
if len(parsed) == 2 and context_api:
|
|
423
|
+
return parsed[1], None, context_api
|
|
424
|
+
|
|
425
|
+
# GET/DELETE/UPDATE /api/v1/-/<project>/<entity_type>/<entity_id>
|
|
426
|
+
# Return entity_type, entity_id, True
|
|
427
|
+
if len(parsed) == 3 and context_api:
|
|
428
|
+
return parsed[1], parsed[2], context_api
|
|
429
|
+
|
|
430
|
+
raise ValueError(f"Invalid API: {api}")
|
|
431
|
+
|
|
432
|
+
def _get_project_spec(self, obj: dict, name: str) -> dict:
|
|
433
|
+
"""
|
|
434
|
+
Enrich project object with spec (artifacts, functions, etc.).
|
|
435
|
+
|
|
436
|
+
Parameters
|
|
437
|
+
----------
|
|
438
|
+
obj : dict
|
|
439
|
+
The project object.
|
|
440
|
+
name : str
|
|
441
|
+
The project name.
|
|
442
|
+
|
|
443
|
+
Returns
|
|
444
|
+
-------
|
|
445
|
+
dict
|
|
446
|
+
The project object with the spec.
|
|
447
|
+
"""
|
|
448
|
+
# Deepcopy to avoid modifying the original object
|
|
449
|
+
project = deepcopy(obj)
|
|
450
|
+
spec = project.get("spec", {})
|
|
451
|
+
|
|
452
|
+
# Get all entities associated with the project specs
|
|
453
|
+
projects_entities = [k for k, _ in self._db.items() if k not in ["projects", "runs", "tasks"]]
|
|
454
|
+
|
|
455
|
+
for entity_type in projects_entities:
|
|
456
|
+
# Get all objects of the entity type for the project
|
|
457
|
+
objs = self._db[entity_type]
|
|
458
|
+
|
|
459
|
+
# Set empty list
|
|
460
|
+
spec[entity_type] = []
|
|
461
|
+
|
|
462
|
+
# Cycle through named objects
|
|
463
|
+
for _, named_entities in objs.items():
|
|
464
|
+
# Get latest version
|
|
465
|
+
for version, entity in named_entities.items():
|
|
466
|
+
if version != "latest":
|
|
467
|
+
continue
|
|
468
|
+
|
|
469
|
+
# Deepcopy to avoid modifying the original object
|
|
470
|
+
copied = deepcopy(entity)
|
|
471
|
+
|
|
472
|
+
# Remove spec if not embedded
|
|
473
|
+
if not copied.get("metadata", {}).get("embedded", True):
|
|
474
|
+
copied.pop("spec", None)
|
|
475
|
+
|
|
476
|
+
# Add to project spec
|
|
477
|
+
if copied["project"] == name:
|
|
478
|
+
spec[entity_type].append(copied)
|
|
479
|
+
|
|
480
|
+
return project
|
|
481
|
+
|
|
482
|
+
##############################
|
|
483
|
+
# Utils
|
|
484
|
+
##############################
|
|
485
|
+
|
|
486
|
+
@staticmethod
|
|
487
|
+
def _format_msg(
|
|
488
|
+
error_code: int,
|
|
489
|
+
entity_type: str | None = None,
|
|
490
|
+
entity_id: str | None = None,
|
|
491
|
+
) -> str:
|
|
492
|
+
"""
|
|
493
|
+
Format a message.
|
|
494
|
+
|
|
495
|
+
Parameters
|
|
496
|
+
----------
|
|
497
|
+
error_code : int
|
|
498
|
+
Error code.
|
|
499
|
+
project : str
|
|
500
|
+
Project name.
|
|
501
|
+
entity_type : str
|
|
502
|
+
Entity type.
|
|
503
|
+
entity_id : str
|
|
504
|
+
Entity ID.
|
|
505
|
+
|
|
506
|
+
Returns
|
|
507
|
+
-------
|
|
508
|
+
str
|
|
509
|
+
The formatted message.
|
|
510
|
+
"""
|
|
511
|
+
msg = {
|
|
512
|
+
1: f"Object '{entity_type}' to create is not valid",
|
|
513
|
+
2: f"Object '{entity_type}' with id '{entity_id}' already exists",
|
|
514
|
+
3: f"Object '{entity_type}' with id '{entity_id}' not found",
|
|
515
|
+
4: "Must provide entity_id to read an object",
|
|
516
|
+
}
|
|
517
|
+
return msg[error_code]
|
|
518
|
+
|
|
519
|
+
##############################
|
|
520
|
+
# Interface methods
|
|
521
|
+
##############################
|
|
522
|
+
|
|
523
|
+
@staticmethod
|
|
524
|
+
def is_local() -> bool:
|
|
525
|
+
"""
|
|
526
|
+
Declare if Client is local.
|
|
527
|
+
|
|
528
|
+
Returns
|
|
529
|
+
-------
|
|
530
|
+
bool
|
|
531
|
+
True
|
|
532
|
+
"""
|
|
533
|
+
return True
|
|
File without changes
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from digitalhub.context.context import Context
|
|
6
|
+
|
|
7
|
+
if typing.TYPE_CHECKING:
|
|
8
|
+
from digitalhub.entities.project.entity._base import Project
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ContextBuilder:
|
|
12
|
+
"""
|
|
13
|
+
ContextBuilder class.
|
|
14
|
+
It implements the builder pattern to create a context instance.
|
|
15
|
+
It allows to use multiple projects as context at the same time
|
|
16
|
+
by adding them to the _instances registry with their name.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self._instances: dict[str, Context] = {}
|
|
21
|
+
|
|
22
|
+
def build(self, project_object: Project) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Add a project as context.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
project_object : Project
|
|
29
|
+
The project to add.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
None
|
|
34
|
+
"""
|
|
35
|
+
self._instances[project_object.name] = Context(project_object)
|
|
36
|
+
|
|
37
|
+
def get(self, project: str) -> Context:
|
|
38
|
+
"""
|
|
39
|
+
Get a context from project name if it exists.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
project : str
|
|
44
|
+
The project name.
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
Context
|
|
49
|
+
The project context.
|
|
50
|
+
|
|
51
|
+
Raises
|
|
52
|
+
------
|
|
53
|
+
ValueError
|
|
54
|
+
If the project is not in the context.
|
|
55
|
+
"""
|
|
56
|
+
ctx = self._instances.get(project)
|
|
57
|
+
if ctx is None:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"Context '{project}' not found. Please get or create a project named '{project}' to access its objects."
|
|
60
|
+
)
|
|
61
|
+
return ctx
|
|
62
|
+
|
|
63
|
+
def remove(self, project: str) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Remove a project from the context.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
project : str
|
|
70
|
+
The project name.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
None
|
|
75
|
+
"""
|
|
76
|
+
self._instances.pop(project, None)
|
|
77
|
+
|
|
78
|
+
def set(self, context: Context) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Set the context.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
context : Context
|
|
85
|
+
The context to set.
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
None
|
|
90
|
+
"""
|
|
91
|
+
self._instances[context.name] = context
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def set_context(project: Project) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Wrapper for ContextBuilder.build().
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
project : Project
|
|
101
|
+
The project object used to set the current context.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
None
|
|
106
|
+
"""
|
|
107
|
+
context_builder.build(project)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def set_context_object(context: Context) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Wrapper for ContextBuilder.set().
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
context : Context
|
|
117
|
+
The context to set.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
None
|
|
122
|
+
"""
|
|
123
|
+
context_builder.set(context)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_context(project: str) -> Context:
|
|
127
|
+
"""
|
|
128
|
+
Wrapper for ContextBuilder.get().
|
|
129
|
+
|
|
130
|
+
Parameters
|
|
131
|
+
----------
|
|
132
|
+
project : str
|
|
133
|
+
Project name.
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
Context
|
|
138
|
+
The context for the given project name.
|
|
139
|
+
"""
|
|
140
|
+
return context_builder.get(project)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def delete_context(project: str) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Wrapper for ContextBuilder.remove().
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
project : str
|
|
150
|
+
Project name.
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
None
|
|
155
|
+
"""
|
|
156
|
+
context_builder.remove(project)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def check_context(project: str) -> None:
|
|
160
|
+
"""
|
|
161
|
+
Check if the given project is in the context.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
project : str
|
|
166
|
+
Project name.
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
bool
|
|
171
|
+
True if the project is in the context, False otherwise.
|
|
172
|
+
"""
|
|
173
|
+
if project not in context_builder._instances:
|
|
174
|
+
msg = f"Context missing. Set context by creating or importing a project named '{project}'."
|
|
175
|
+
raise RuntimeError(msg)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
context_builder = ContextBuilder()
|