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.

Files changed (159) hide show
  1. digitalhub/__init__.py +62 -94
  2. digitalhub/client/__init__.py +0 -0
  3. digitalhub/client/builder.py +105 -0
  4. digitalhub/client/objects/__init__.py +0 -0
  5. digitalhub/client/objects/base.py +56 -0
  6. digitalhub/client/objects/dhcore.py +681 -0
  7. digitalhub/client/objects/local.py +533 -0
  8. digitalhub/context/__init__.py +0 -0
  9. digitalhub/context/builder.py +178 -0
  10. digitalhub/context/context.py +136 -0
  11. digitalhub/datastores/__init__.py +0 -0
  12. digitalhub/datastores/builder.py +134 -0
  13. digitalhub/datastores/objects/__init__.py +0 -0
  14. digitalhub/datastores/objects/base.py +85 -0
  15. digitalhub/datastores/objects/local.py +42 -0
  16. digitalhub/datastores/objects/remote.py +23 -0
  17. digitalhub/datastores/objects/s3.py +38 -0
  18. digitalhub/datastores/objects/sql.py +60 -0
  19. digitalhub/entities/__init__.py +0 -0
  20. digitalhub/entities/_base/__init__.py +0 -0
  21. digitalhub/entities/_base/api.py +346 -0
  22. digitalhub/entities/_base/base.py +82 -0
  23. digitalhub/entities/_base/crud.py +610 -0
  24. digitalhub/entities/_base/entity/__init__.py +0 -0
  25. digitalhub/entities/_base/entity/base.py +132 -0
  26. digitalhub/entities/_base/entity/context.py +118 -0
  27. digitalhub/entities/_base/entity/executable.py +380 -0
  28. digitalhub/entities/_base/entity/material.py +214 -0
  29. digitalhub/entities/_base/entity/unversioned.py +87 -0
  30. digitalhub/entities/_base/entity/versioned.py +94 -0
  31. digitalhub/entities/_base/metadata.py +59 -0
  32. digitalhub/entities/_base/spec/__init__.py +0 -0
  33. digitalhub/entities/_base/spec/base.py +58 -0
  34. digitalhub/entities/_base/spec/material.py +22 -0
  35. digitalhub/entities/_base/state.py +31 -0
  36. digitalhub/entities/_base/status/__init__.py +0 -0
  37. digitalhub/entities/_base/status/base.py +32 -0
  38. digitalhub/entities/_base/status/material.py +49 -0
  39. digitalhub/entities/_builders/__init__.py +0 -0
  40. digitalhub/entities/_builders/metadata.py +60 -0
  41. digitalhub/entities/_builders/name.py +31 -0
  42. digitalhub/entities/_builders/spec.py +43 -0
  43. digitalhub/entities/_builders/status.py +62 -0
  44. digitalhub/entities/_builders/uuid.py +33 -0
  45. digitalhub/entities/artifact/__init__.py +0 -0
  46. digitalhub/entities/artifact/builder.py +133 -0
  47. digitalhub/entities/artifact/crud.py +358 -0
  48. digitalhub/entities/artifact/entity/__init__.py +0 -0
  49. digitalhub/entities/artifact/entity/_base.py +39 -0
  50. digitalhub/entities/artifact/entity/artifact.py +9 -0
  51. digitalhub/entities/artifact/spec.py +39 -0
  52. digitalhub/entities/artifact/status.py +15 -0
  53. digitalhub/entities/dataitem/__init__.py +0 -0
  54. digitalhub/entities/dataitem/builder.py +144 -0
  55. digitalhub/entities/dataitem/crud.py +395 -0
  56. digitalhub/entities/dataitem/entity/__init__.py +0 -0
  57. digitalhub/entities/dataitem/entity/_base.py +75 -0
  58. digitalhub/entities/dataitem/entity/dataitem.py +9 -0
  59. digitalhub/entities/dataitem/entity/iceberg.py +7 -0
  60. digitalhub/entities/dataitem/entity/table.py +125 -0
  61. digitalhub/entities/dataitem/models.py +62 -0
  62. digitalhub/entities/dataitem/spec.py +61 -0
  63. digitalhub/entities/dataitem/status.py +38 -0
  64. digitalhub/entities/entity_types.py +19 -0
  65. digitalhub/entities/function/__init__.py +0 -0
  66. digitalhub/entities/function/builder.py +86 -0
  67. digitalhub/entities/function/crud.py +305 -0
  68. digitalhub/entities/function/entity.py +101 -0
  69. digitalhub/entities/function/models.py +118 -0
  70. digitalhub/entities/function/spec.py +81 -0
  71. digitalhub/entities/function/status.py +9 -0
  72. digitalhub/entities/model/__init__.py +0 -0
  73. digitalhub/entities/model/builder.py +152 -0
  74. digitalhub/entities/model/crud.py +358 -0
  75. digitalhub/entities/model/entity/__init__.py +0 -0
  76. digitalhub/entities/model/entity/_base.py +34 -0
  77. digitalhub/entities/model/entity/huggingface.py +9 -0
  78. digitalhub/entities/model/entity/mlflow.py +90 -0
  79. digitalhub/entities/model/entity/model.py +9 -0
  80. digitalhub/entities/model/entity/sklearn.py +9 -0
  81. digitalhub/entities/model/models.py +26 -0
  82. digitalhub/entities/model/spec.py +146 -0
  83. digitalhub/entities/model/status.py +33 -0
  84. digitalhub/entities/project/__init__.py +0 -0
  85. digitalhub/entities/project/builder.py +82 -0
  86. digitalhub/entities/project/crud.py +350 -0
  87. digitalhub/entities/project/entity.py +2060 -0
  88. digitalhub/entities/project/spec.py +50 -0
  89. digitalhub/entities/project/status.py +9 -0
  90. digitalhub/entities/registries.py +48 -0
  91. digitalhub/entities/run/__init__.py +0 -0
  92. digitalhub/entities/run/builder.py +77 -0
  93. digitalhub/entities/run/crud.py +232 -0
  94. digitalhub/entities/run/entity.py +461 -0
  95. digitalhub/entities/run/spec.py +153 -0
  96. digitalhub/entities/run/status.py +114 -0
  97. digitalhub/entities/secret/__init__.py +0 -0
  98. digitalhub/entities/secret/builder.py +93 -0
  99. digitalhub/entities/secret/crud.py +294 -0
  100. digitalhub/entities/secret/entity.py +73 -0
  101. digitalhub/entities/secret/spec.py +35 -0
  102. digitalhub/entities/secret/status.py +9 -0
  103. digitalhub/entities/task/__init__.py +0 -0
  104. digitalhub/entities/task/builder.py +74 -0
  105. digitalhub/entities/task/crud.py +241 -0
  106. digitalhub/entities/task/entity.py +135 -0
  107. digitalhub/entities/task/models.py +199 -0
  108. digitalhub/entities/task/spec.py +51 -0
  109. digitalhub/entities/task/status.py +9 -0
  110. digitalhub/entities/utils.py +184 -0
  111. digitalhub/entities/workflow/__init__.py +0 -0
  112. digitalhub/entities/workflow/builder.py +91 -0
  113. digitalhub/entities/workflow/crud.py +304 -0
  114. digitalhub/entities/workflow/entity.py +77 -0
  115. digitalhub/entities/workflow/spec.py +15 -0
  116. digitalhub/entities/workflow/status.py +9 -0
  117. digitalhub/readers/__init__.py +0 -0
  118. digitalhub/readers/builder.py +54 -0
  119. digitalhub/readers/objects/__init__.py +0 -0
  120. digitalhub/readers/objects/base.py +70 -0
  121. digitalhub/readers/objects/pandas.py +207 -0
  122. digitalhub/readers/registry.py +15 -0
  123. digitalhub/registry/__init__.py +0 -0
  124. digitalhub/registry/models.py +87 -0
  125. digitalhub/registry/registry.py +74 -0
  126. digitalhub/registry/utils.py +150 -0
  127. digitalhub/runtimes/__init__.py +0 -0
  128. digitalhub/runtimes/base.py +164 -0
  129. digitalhub/runtimes/builder.py +53 -0
  130. digitalhub/runtimes/kind_registry.py +170 -0
  131. digitalhub/stores/__init__.py +0 -0
  132. digitalhub/stores/builder.py +257 -0
  133. digitalhub/stores/objects/__init__.py +0 -0
  134. digitalhub/stores/objects/base.py +189 -0
  135. digitalhub/stores/objects/local.py +230 -0
  136. digitalhub/stores/objects/remote.py +143 -0
  137. digitalhub/stores/objects/s3.py +563 -0
  138. digitalhub/stores/objects/sql.py +328 -0
  139. digitalhub/utils/__init__.py +0 -0
  140. digitalhub/utils/data_utils.py +127 -0
  141. digitalhub/utils/env_utils.py +123 -0
  142. digitalhub/utils/exceptions.py +55 -0
  143. digitalhub/utils/file_utils.py +204 -0
  144. digitalhub/utils/generic_utils.py +207 -0
  145. digitalhub/utils/git_utils.py +148 -0
  146. digitalhub/utils/io_utils.py +79 -0
  147. digitalhub/utils/logger.py +17 -0
  148. digitalhub/utils/uri_utils.py +56 -0
  149. {digitalhub-0.8.0b0.dist-info → digitalhub-0.8.0b2.dist-info}/METADATA +27 -12
  150. digitalhub-0.8.0b2.dist-info/RECORD +161 -0
  151. test/test_crud_artifacts.py +1 -1
  152. test/test_crud_dataitems.py +1 -1
  153. test/test_crud_functions.py +1 -1
  154. test/test_crud_runs.py +1 -1
  155. test/test_crud_tasks.py +1 -1
  156. digitalhub-0.8.0b0.dist-info/RECORD +0 -14
  157. {digitalhub-0.8.0b0.dist-info → digitalhub-0.8.0b2.dist-info}/LICENSE.txt +0 -0
  158. {digitalhub-0.8.0b0.dist-info → digitalhub-0.8.0b2.dist-info}/WHEEL +0 -0
  159. {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()