digitalhub 0.7.0b2__py3-none-any.whl → 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of digitalhub might be problematic. Click here for more details.

Files changed (232) hide show
  1. digitalhub/__init__.py +63 -93
  2. digitalhub/client/__init__.py +0 -0
  3. digitalhub/client/_base/__init__.py +0 -0
  4. digitalhub/client/_base/client.py +56 -0
  5. digitalhub/client/api.py +63 -0
  6. digitalhub/client/builder.py +50 -0
  7. digitalhub/client/dhcore/__init__.py +0 -0
  8. digitalhub/client/dhcore/client.py +669 -0
  9. digitalhub/client/dhcore/env.py +21 -0
  10. digitalhub/client/dhcore/models.py +46 -0
  11. digitalhub/client/dhcore/utils.py +111 -0
  12. digitalhub/client/local/__init__.py +0 -0
  13. digitalhub/client/local/client.py +533 -0
  14. digitalhub/context/__init__.py +0 -0
  15. digitalhub/context/api.py +93 -0
  16. digitalhub/context/builder.py +94 -0
  17. digitalhub/context/context.py +136 -0
  18. digitalhub/datastores/__init__.py +0 -0
  19. digitalhub/datastores/_base/__init__.py +0 -0
  20. digitalhub/datastores/_base/datastore.py +85 -0
  21. digitalhub/datastores/api.py +37 -0
  22. digitalhub/datastores/builder.py +110 -0
  23. digitalhub/datastores/local/__init__.py +0 -0
  24. digitalhub/datastores/local/datastore.py +50 -0
  25. digitalhub/datastores/remote/__init__.py +0 -0
  26. digitalhub/datastores/remote/datastore.py +31 -0
  27. digitalhub/datastores/s3/__init__.py +0 -0
  28. digitalhub/datastores/s3/datastore.py +46 -0
  29. digitalhub/datastores/sql/__init__.py +0 -0
  30. digitalhub/datastores/sql/datastore.py +68 -0
  31. digitalhub/entities/__init__.py +0 -0
  32. digitalhub/entities/_base/__init__.py +0 -0
  33. digitalhub/entities/_base/_base/__init__.py +0 -0
  34. digitalhub/entities/_base/_base/entity.py +82 -0
  35. digitalhub/entities/_base/api_utils.py +620 -0
  36. digitalhub/entities/_base/context/__init__.py +0 -0
  37. digitalhub/entities/_base/context/entity.py +118 -0
  38. digitalhub/entities/_base/crud.py +468 -0
  39. digitalhub/entities/_base/entity/__init__.py +0 -0
  40. digitalhub/entities/_base/entity/_constructors/__init__.py +0 -0
  41. digitalhub/entities/_base/entity/_constructors/metadata.py +44 -0
  42. digitalhub/entities/_base/entity/_constructors/name.py +31 -0
  43. digitalhub/entities/_base/entity/_constructors/spec.py +33 -0
  44. digitalhub/entities/_base/entity/_constructors/status.py +52 -0
  45. digitalhub/entities/_base/entity/_constructors/uuid.py +26 -0
  46. digitalhub/entities/_base/entity/builder.py +175 -0
  47. digitalhub/entities/_base/entity/entity.py +106 -0
  48. digitalhub/entities/_base/entity/metadata.py +59 -0
  49. digitalhub/entities/_base/entity/spec.py +58 -0
  50. digitalhub/entities/_base/entity/status.py +43 -0
  51. digitalhub/entities/_base/executable/__init__.py +0 -0
  52. digitalhub/entities/_base/executable/entity.py +405 -0
  53. digitalhub/entities/_base/material/__init__.py +0 -0
  54. digitalhub/entities/_base/material/entity.py +214 -0
  55. digitalhub/entities/_base/material/spec.py +22 -0
  56. digitalhub/entities/_base/material/status.py +49 -0
  57. digitalhub/entities/_base/runtime_entity/__init__.py +0 -0
  58. digitalhub/entities/_base/runtime_entity/builder.py +106 -0
  59. digitalhub/entities/_base/unversioned/__init__.py +0 -0
  60. digitalhub/entities/_base/unversioned/builder.py +66 -0
  61. digitalhub/entities/_base/unversioned/entity.py +49 -0
  62. digitalhub/entities/_base/versioned/__init__.py +0 -0
  63. digitalhub/entities/_base/versioned/builder.py +68 -0
  64. digitalhub/entities/_base/versioned/entity.py +53 -0
  65. digitalhub/entities/artifact/__init__.py +0 -0
  66. digitalhub/entities/artifact/_base/__init__.py +0 -0
  67. digitalhub/entities/artifact/_base/builder.py +86 -0
  68. digitalhub/entities/artifact/_base/entity.py +39 -0
  69. digitalhub/entities/artifact/_base/spec.py +15 -0
  70. digitalhub/entities/artifact/_base/status.py +9 -0
  71. digitalhub/entities/artifact/artifact/__init__.py +0 -0
  72. digitalhub/entities/artifact/artifact/builder.py +18 -0
  73. digitalhub/entities/artifact/artifact/entity.py +32 -0
  74. digitalhub/entities/artifact/artifact/spec.py +27 -0
  75. digitalhub/entities/artifact/artifact/status.py +15 -0
  76. digitalhub/entities/artifact/crud.py +332 -0
  77. digitalhub/entities/builders.py +63 -0
  78. digitalhub/entities/dataitem/__init__.py +0 -0
  79. digitalhub/entities/dataitem/_base/__init__.py +0 -0
  80. digitalhub/entities/dataitem/_base/builder.py +86 -0
  81. digitalhub/entities/dataitem/_base/entity.py +75 -0
  82. digitalhub/entities/dataitem/_base/spec.py +15 -0
  83. digitalhub/entities/dataitem/_base/status.py +20 -0
  84. digitalhub/entities/dataitem/crud.py +372 -0
  85. digitalhub/entities/dataitem/dataitem/__init__.py +0 -0
  86. digitalhub/entities/dataitem/dataitem/builder.py +18 -0
  87. digitalhub/entities/dataitem/dataitem/entity.py +32 -0
  88. digitalhub/entities/dataitem/dataitem/spec.py +15 -0
  89. digitalhub/entities/dataitem/dataitem/status.py +9 -0
  90. digitalhub/entities/dataitem/iceberg/__init__.py +0 -0
  91. digitalhub/entities/dataitem/iceberg/builder.py +18 -0
  92. digitalhub/entities/dataitem/iceberg/entity.py +32 -0
  93. digitalhub/entities/dataitem/iceberg/spec.py +15 -0
  94. digitalhub/entities/dataitem/iceberg/status.py +9 -0
  95. digitalhub/entities/dataitem/table/__init__.py +0 -0
  96. digitalhub/entities/dataitem/table/builder.py +18 -0
  97. digitalhub/entities/dataitem/table/entity.py +146 -0
  98. digitalhub/entities/dataitem/table/models.py +62 -0
  99. digitalhub/entities/dataitem/table/spec.py +25 -0
  100. digitalhub/entities/dataitem/table/status.py +9 -0
  101. digitalhub/entities/function/__init__.py +0 -0
  102. digitalhub/entities/function/_base/__init__.py +0 -0
  103. digitalhub/entities/function/_base/builder.py +79 -0
  104. digitalhub/entities/function/_base/entity.py +98 -0
  105. digitalhub/entities/function/_base/models.py +118 -0
  106. digitalhub/entities/function/_base/spec.py +15 -0
  107. digitalhub/entities/function/_base/status.py +9 -0
  108. digitalhub/entities/function/crud.py +279 -0
  109. digitalhub/entities/model/__init__.py +0 -0
  110. digitalhub/entities/model/_base/__init__.py +0 -0
  111. digitalhub/entities/model/_base/builder.py +86 -0
  112. digitalhub/entities/model/_base/entity.py +34 -0
  113. digitalhub/entities/model/_base/spec.py +49 -0
  114. digitalhub/entities/model/_base/status.py +9 -0
  115. digitalhub/entities/model/crud.py +331 -0
  116. digitalhub/entities/model/huggingface/__init__.py +0 -0
  117. digitalhub/entities/model/huggingface/builder.py +18 -0
  118. digitalhub/entities/model/huggingface/entity.py +32 -0
  119. digitalhub/entities/model/huggingface/spec.py +36 -0
  120. digitalhub/entities/model/huggingface/status.py +9 -0
  121. digitalhub/entities/model/mlflow/__init__.py +0 -0
  122. digitalhub/entities/model/mlflow/builder.py +18 -0
  123. digitalhub/entities/model/mlflow/entity.py +32 -0
  124. digitalhub/entities/model/mlflow/models.py +26 -0
  125. digitalhub/entities/model/mlflow/spec.py +44 -0
  126. digitalhub/entities/model/mlflow/status.py +9 -0
  127. digitalhub/entities/model/mlflow/utils.py +81 -0
  128. digitalhub/entities/model/model/__init__.py +0 -0
  129. digitalhub/entities/model/model/builder.py +18 -0
  130. digitalhub/entities/model/model/entity.py +32 -0
  131. digitalhub/entities/model/model/spec.py +15 -0
  132. digitalhub/entities/model/model/status.py +9 -0
  133. digitalhub/entities/model/sklearn/__init__.py +0 -0
  134. digitalhub/entities/model/sklearn/builder.py +18 -0
  135. digitalhub/entities/model/sklearn/entity.py +32 -0
  136. digitalhub/entities/model/sklearn/spec.py +15 -0
  137. digitalhub/entities/model/sklearn/status.py +9 -0
  138. digitalhub/entities/project/__init__.py +0 -0
  139. digitalhub/entities/project/_base/__init__.py +0 -0
  140. digitalhub/entities/project/_base/builder.py +128 -0
  141. digitalhub/entities/project/_base/entity.py +2078 -0
  142. digitalhub/entities/project/_base/spec.py +50 -0
  143. digitalhub/entities/project/_base/status.py +9 -0
  144. digitalhub/entities/project/crud.py +357 -0
  145. digitalhub/entities/run/__init__.py +0 -0
  146. digitalhub/entities/run/_base/__init__.py +0 -0
  147. digitalhub/entities/run/_base/builder.py +94 -0
  148. digitalhub/entities/run/_base/entity.py +307 -0
  149. digitalhub/entities/run/_base/spec.py +50 -0
  150. digitalhub/entities/run/_base/status.py +9 -0
  151. digitalhub/entities/run/crud.py +219 -0
  152. digitalhub/entities/secret/__init__.py +0 -0
  153. digitalhub/entities/secret/_base/__init__.py +0 -0
  154. digitalhub/entities/secret/_base/builder.py +81 -0
  155. digitalhub/entities/secret/_base/entity.py +74 -0
  156. digitalhub/entities/secret/_base/spec.py +35 -0
  157. digitalhub/entities/secret/_base/status.py +9 -0
  158. digitalhub/entities/secret/crud.py +290 -0
  159. digitalhub/entities/task/__init__.py +0 -0
  160. digitalhub/entities/task/_base/__init__.py +0 -0
  161. digitalhub/entities/task/_base/builder.py +91 -0
  162. digitalhub/entities/task/_base/entity.py +136 -0
  163. digitalhub/entities/task/_base/models.py +208 -0
  164. digitalhub/entities/task/_base/spec.py +53 -0
  165. digitalhub/entities/task/_base/status.py +9 -0
  166. digitalhub/entities/task/crud.py +228 -0
  167. digitalhub/entities/utils/__init__.py +0 -0
  168. digitalhub/entities/utils/api.py +346 -0
  169. digitalhub/entities/utils/entity_types.py +19 -0
  170. digitalhub/entities/utils/state.py +31 -0
  171. digitalhub/entities/utils/utils.py +202 -0
  172. digitalhub/entities/workflow/__init__.py +0 -0
  173. digitalhub/entities/workflow/_base/__init__.py +0 -0
  174. digitalhub/entities/workflow/_base/builder.py +79 -0
  175. digitalhub/entities/workflow/_base/entity.py +74 -0
  176. digitalhub/entities/workflow/_base/spec.py +15 -0
  177. digitalhub/entities/workflow/_base/status.py +9 -0
  178. digitalhub/entities/workflow/crud.py +278 -0
  179. digitalhub/factory/__init__.py +0 -0
  180. digitalhub/factory/api.py +277 -0
  181. digitalhub/factory/factory.py +268 -0
  182. digitalhub/factory/utils.py +90 -0
  183. digitalhub/readers/__init__.py +0 -0
  184. digitalhub/readers/_base/__init__.py +0 -0
  185. digitalhub/readers/_base/builder.py +26 -0
  186. digitalhub/readers/_base/reader.py +70 -0
  187. digitalhub/readers/api.py +80 -0
  188. digitalhub/readers/factory.py +133 -0
  189. digitalhub/readers/pandas/__init__.py +0 -0
  190. digitalhub/readers/pandas/builder.py +29 -0
  191. digitalhub/readers/pandas/reader.py +207 -0
  192. digitalhub/runtimes/__init__.py +0 -0
  193. digitalhub/runtimes/_base.py +102 -0
  194. digitalhub/runtimes/builder.py +32 -0
  195. digitalhub/stores/__init__.py +0 -0
  196. digitalhub/stores/_base/__init__.py +0 -0
  197. digitalhub/stores/_base/store.py +189 -0
  198. digitalhub/stores/api.py +54 -0
  199. digitalhub/stores/builder.py +211 -0
  200. digitalhub/stores/local/__init__.py +0 -0
  201. digitalhub/stores/local/store.py +230 -0
  202. digitalhub/stores/remote/__init__.py +0 -0
  203. digitalhub/stores/remote/store.py +143 -0
  204. digitalhub/stores/s3/__init__.py +0 -0
  205. digitalhub/stores/s3/store.py +563 -0
  206. digitalhub/stores/sql/__init__.py +0 -0
  207. digitalhub/stores/sql/store.py +328 -0
  208. digitalhub/utils/__init__.py +0 -0
  209. digitalhub/utils/data_utils.py +127 -0
  210. digitalhub/utils/exceptions.py +67 -0
  211. digitalhub/utils/file_utils.py +204 -0
  212. digitalhub/utils/generic_utils.py +183 -0
  213. digitalhub/utils/git_utils.py +148 -0
  214. digitalhub/utils/io_utils.py +116 -0
  215. digitalhub/utils/logger.py +17 -0
  216. digitalhub/utils/s3_utils.py +58 -0
  217. digitalhub/utils/uri_utils.py +56 -0
  218. {digitalhub-0.7.0b2.dist-info → digitalhub-0.8.0.dist-info}/METADATA +30 -13
  219. digitalhub-0.8.0.dist-info/RECORD +231 -0
  220. {digitalhub-0.7.0b2.dist-info → digitalhub-0.8.0.dist-info}/WHEEL +1 -1
  221. test/local/CRUD/test_artifacts.py +96 -0
  222. test/local/CRUD/test_dataitems.py +96 -0
  223. test/local/CRUD/test_models.py +95 -0
  224. test/test_crud_functions.py +1 -1
  225. test/test_crud_runs.py +1 -1
  226. test/test_crud_tasks.py +1 -1
  227. digitalhub-0.7.0b2.dist-info/RECORD +0 -14
  228. test/test_crud_artifacts.py +0 -96
  229. test/test_crud_dataitems.py +0 -96
  230. {digitalhub-0.7.0b2.dist-info → digitalhub-0.8.0.dist-info}/LICENSE.txt +0 -0
  231. {digitalhub-0.7.0b2.dist-info → digitalhub-0.8.0.dist-info}/top_level.txt +0 -0
  232. /test/{test_imports.py → local/imports/test_imports.py} +0 -0
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from digitalhub.client.dhcore.env import FALLBACK_USER
6
+
7
+
8
+ class AuthConfig(BaseModel):
9
+ """Client configuration model."""
10
+
11
+ user: str = FALLBACK_USER
12
+ """Username."""
13
+
14
+
15
+ class BasicAuth(AuthConfig):
16
+ """Basic authentication model."""
17
+
18
+ password: str
19
+ """Basic authentication password."""
20
+
21
+
22
+ class ClientParams(AuthConfig):
23
+ """Client id authentication model."""
24
+
25
+ client_id: str = None
26
+ """OAuth2 client id."""
27
+
28
+ client_scecret: str = None
29
+ """OAuth2 client secret."""
30
+
31
+
32
+ class OAuth2TokenAuth(ClientParams):
33
+ """OAuth2 token authentication model."""
34
+
35
+ access_token: str
36
+ """OAuth2 token."""
37
+
38
+ refresh_token: str = None
39
+ """OAuth2 refresh token."""
40
+
41
+
42
+ class TokenExchangeAuth(ClientParams):
43
+ """Token exchange authentication model."""
44
+
45
+ exchange_token: str
46
+ """Exchange token."""
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import typing
5
+
6
+ from digitalhub.client.api import check_client_exists, get_client
7
+
8
+ if typing.TYPE_CHECKING:
9
+ from digitalhub.client.dhcore.client import ClientDHCore
10
+
11
+
12
+ def set_dhcore_env(
13
+ endpoint: str | None = None,
14
+ user: str | None = None,
15
+ password: str | None = None,
16
+ access_token: str | None = None,
17
+ refresh_token: str | None = None,
18
+ client_id: str | None = None,
19
+ ) -> None:
20
+ """
21
+ Function to set environment variables for DHCore config.
22
+ Note that if the environment variable is already set, it
23
+ will be overwritten. It also ovverides the remote client
24
+ configuration.
25
+
26
+ Parameters
27
+ ----------
28
+ endpoint : str
29
+ The endpoint of DHCore.
30
+ user : str
31
+ The user of DHCore.
32
+ password : str
33
+ The password of DHCore.
34
+ access_token : str
35
+ The access token of DHCore.
36
+ refresh_token : str
37
+ The refresh token of DHCore.
38
+ client_id : str
39
+ The client id of DHCore.
40
+
41
+ Returns
42
+ -------
43
+ None
44
+ """
45
+ if endpoint is not None:
46
+ os.environ["DHCORE_ENDPOINT"] = endpoint
47
+ if user is not None:
48
+ os.environ["DHCORE_USER"] = user
49
+ if password is not None:
50
+ os.environ["DHCORE_PASSWORD"] = password
51
+ if access_token is not None:
52
+ os.environ["DHCORE_ACCESS_TOKEN"] = access_token
53
+ if refresh_token is not None:
54
+ os.environ["DHCORE_REFRESH_TOKEN"] = refresh_token
55
+ if client_id is not None:
56
+ os.environ["DHCORE_CLIENT_ID"] = client_id
57
+
58
+ if check_client_exists(local=False):
59
+ update_client_from_env()
60
+
61
+
62
+ def update_client_from_env() -> None:
63
+ """
64
+ Function to update client from environment variables.
65
+
66
+ Returns
67
+ -------
68
+ None
69
+ """
70
+ client: ClientDHCore = get_client(local=False)
71
+
72
+ # Update endpoint
73
+ endpoint = os.getenv("DHCORE_ENDPOINT")
74
+ if endpoint is not None:
75
+ client._endpoint_core = endpoint
76
+
77
+ # Update auth
78
+
79
+ # If token is set, it will override the other auth options
80
+ access_token = os.getenv("DHCORE_ACCESS_TOKEN")
81
+ refresh_token = os.getenv("DHCORE_REFRESH_TOKEN")
82
+ client_id = os.getenv("DHCORE_CLIENT_ID")
83
+
84
+ if access_token is not None:
85
+ if refresh_token is not None:
86
+ client._refresh_token = refresh_token
87
+ if client_id is not None:
88
+ client._client_id = client_id
89
+ client._access_token = access_token
90
+ client._auth_type = "oauth2"
91
+ return
92
+
93
+ # Otherwise, if user and password are set, basic auth will be used
94
+ username = os.getenv("DHCORE_USER")
95
+ password = os.getenv("DHCORE_PASSWORD")
96
+ if username is not None and password is not None:
97
+ client._user = username
98
+ client._password = password
99
+ client._auth_type = "basic"
100
+
101
+
102
+ def refresh_token() -> None:
103
+ """
104
+ Function to refresh token.
105
+
106
+ Returns
107
+ -------
108
+ None
109
+ """
110
+ client: ClientDHCore = get_client(local=False)
111
+ client._get_new_access_token()
File without changes
@@ -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._base.client 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", False):
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