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,669 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import json
5
+ import os
6
+ import typing
7
+ from urllib.parse import urlparse
8
+
9
+ from dotenv import load_dotenv, set_key
10
+ from requests import request
11
+ from requests.exceptions import HTTPError, JSONDecodeError, RequestException
12
+
13
+ from digitalhub.client._base.client import Client
14
+ from digitalhub.client.dhcore.env import ENV_FILE, FALLBACK_USER, MAX_API_LEVEL, MIN_API_LEVEL
15
+ from digitalhub.client.dhcore.models import BasicAuth, OAuth2TokenAuth
16
+ from digitalhub.utils.exceptions import (
17
+ BackendError,
18
+ BadRequestError,
19
+ EntityAlreadyExistsError,
20
+ EntityNotExistsError,
21
+ ForbiddenError,
22
+ MissingSpecError,
23
+ UnauthorizedError,
24
+ )
25
+
26
+ if typing.TYPE_CHECKING:
27
+ from requests import Response
28
+
29
+
30
+ class ClientDHCore(Client):
31
+ """
32
+ DHCore client.
33
+
34
+ The DHCore client is used to communicate with the Digitalhub Core
35
+ backendAPI via REST. The client supports basic authentication and
36
+ OAuth2 token authentication with token refresh.
37
+ At creation, the client tries to get the endpoint and authentication
38
+ parameters from the .dhcore file and the environment variables. In
39
+ case the user incours into an authentication/endpoint error during
40
+ the client creation, the user has the possibility to update the
41
+ correct parameters using the `set_dhcore_env` function. If the DHCore
42
+ client is already initialized, this function will override the
43
+ configuration, otherwise it simply set the environment variables.
44
+ """
45
+
46
+ def __init__(self, config: dict | None = None) -> None:
47
+ super().__init__()
48
+
49
+ # Endpoints
50
+ self._endpoint_core: str | None = None
51
+ self._endpoint_issuer: str | None = None
52
+
53
+ # Authentication
54
+ self._auth_type: str | None = None
55
+
56
+ # Basic
57
+ self._user: str | None = None
58
+ self._password: str | None = None
59
+
60
+ # OAuth2
61
+ self._access_token: str | None = None
62
+ self._refresh_token: str | None = None
63
+
64
+ self._configure(config)
65
+
66
+ ##############################
67
+ # CRUD methods
68
+ ##############################
69
+
70
+ def create_object(self, api: str, obj: dict, **kwargs) -> dict:
71
+ """
72
+ Create an object in DHCore.
73
+
74
+ Parameters
75
+ ----------
76
+ api : str
77
+ Create API.
78
+ obj : dict
79
+ Object to create.
80
+ **kwargs : dict
81
+ Keyword arguments to pass to the request.
82
+
83
+ Returns
84
+ -------
85
+ dict
86
+ Response object.
87
+ """
88
+ if "headers" not in kwargs:
89
+ kwargs["headers"] = {}
90
+ kwargs["headers"]["Content-Type"] = "application/json"
91
+ kwargs["data"] = json.dumps(obj, default=ClientDHCore._json_serialize)
92
+ return self._prepare_call("POST", api, **kwargs)
93
+
94
+ def read_object(self, api: str, **kwargs) -> dict:
95
+ """
96
+ Get an object from DHCore.
97
+
98
+ Parameters
99
+ ----------
100
+ api : str
101
+ Read API.
102
+ **kwargs : dict
103
+ Keyword arguments to pass to the request.
104
+
105
+ Returns
106
+ -------
107
+ dict
108
+ Response object.
109
+ """
110
+ return self._prepare_call("GET", api, **kwargs)
111
+
112
+ def update_object(self, api: str, obj: dict, **kwargs) -> dict:
113
+ """
114
+ Update an object in DHCore.
115
+
116
+ Parameters
117
+ ----------
118
+ api : str
119
+ Update API.
120
+ obj : dict
121
+ Object to update.
122
+ **kwargs : dict
123
+ Keyword arguments to pass to the request.
124
+
125
+ Returns
126
+ -------
127
+ dict
128
+ Response object.
129
+ """
130
+ if "headers" not in kwargs:
131
+ kwargs["headers"] = {}
132
+ kwargs["headers"]["Content-Type"] = "application/json"
133
+ kwargs["data"] = json.dumps(obj, default=ClientDHCore._json_serialize)
134
+ return self._prepare_call("PUT", api, **kwargs)
135
+
136
+ def delete_object(self, api: str, **kwargs) -> dict:
137
+ """
138
+ Delete an object from DHCore.
139
+
140
+ Parameters
141
+ ----------
142
+ api : str
143
+ Delete API.
144
+ **kwargs : dict
145
+ Keyword arguments to pass to the request.
146
+
147
+ Returns
148
+ -------
149
+ dict
150
+ Response object.
151
+ """
152
+ resp = self._prepare_call("DELETE", api, **kwargs)
153
+ if isinstance(resp, bool):
154
+ resp = {"deleted": resp}
155
+ return resp
156
+
157
+ def list_objects(self, api: str, **kwargs) -> list[dict]:
158
+ """
159
+ List objects from DHCore.
160
+
161
+ Parameters
162
+ ----------
163
+ api : str
164
+ List API.
165
+ **kwargs : dict
166
+ Keyword arguments to pass to the request.
167
+
168
+ Returns
169
+ -------
170
+ list[dict]
171
+ Response objects.
172
+ """
173
+ if kwargs is None:
174
+ kwargs = {}
175
+
176
+ if "params" not in kwargs:
177
+ kwargs["params"] = {}
178
+
179
+ start_page = 0
180
+ if "page" not in kwargs["params"]:
181
+ kwargs["params"]["page"] = start_page
182
+
183
+ objects = []
184
+ while True:
185
+ resp = self._prepare_call("GET", api, **kwargs)
186
+ contents = resp["content"]
187
+ total_pages = resp["totalPages"]
188
+ if not contents or kwargs["params"]["page"] >= total_pages:
189
+ break
190
+ objects.extend(contents)
191
+ kwargs["params"]["page"] += 1
192
+
193
+ return objects
194
+
195
+ def list_first_object(self, api: str, **kwargs) -> dict:
196
+ """
197
+ List first objects.
198
+
199
+ Parameters
200
+ ----------
201
+ api : str
202
+ The api to list the objects with.
203
+ **kwargs : dict
204
+ Keyword arguments passed to the request.
205
+
206
+ Returns
207
+ -------
208
+ dict
209
+ The list of objects.
210
+ """
211
+ try:
212
+ return self.list_objects(api, **kwargs)[0]
213
+ except IndexError:
214
+ raise BackendError("No object found.")
215
+
216
+ ##############################
217
+ # Call methods
218
+ ##############################
219
+
220
+ @staticmethod
221
+ def _json_serialize(obj: dict) -> dict:
222
+ """
223
+ JSON datetime to ISO format serializer.
224
+
225
+ Parameters
226
+ ----------
227
+ obj : dict
228
+ The object to serialize.
229
+
230
+ Returns
231
+ -------
232
+ dict
233
+ The serialized object.
234
+ """
235
+ if isinstance(obj, (datetime.datetime, datetime.date)):
236
+ return obj.isoformat()
237
+ raise TypeError("Type %s not serializable" % type(obj))
238
+
239
+ def _prepare_call(self, call_type: str, api: str, **kwargs) -> dict:
240
+ """
241
+ Prepare a call to the DHCore API.
242
+
243
+ Parameters
244
+ ----------
245
+ call_type : str
246
+ The type of call to prepare.
247
+ api : str
248
+ The api to call.
249
+ **kwargs : dict
250
+ Keyword arguments to pass to the request.
251
+
252
+ Returns
253
+ -------
254
+ dict
255
+ Response object.
256
+ """
257
+ if kwargs is None:
258
+ kwargs = {}
259
+ url = self._endpoint_core + api
260
+ kwargs = self._set_auth(kwargs)
261
+ return self._make_call(call_type, url, **kwargs)
262
+
263
+ def _set_auth(self, kwargs: dict) -> dict:
264
+ """
265
+ Set the authentication type.
266
+
267
+ Parameters
268
+ ----------
269
+ kwargs : dict
270
+ Keyword arguments to pass to the request.
271
+
272
+ Returns
273
+ -------
274
+ dict
275
+ Keyword arguments with the authentication parameters.
276
+ """
277
+ if self._auth_type == "basic":
278
+ kwargs["auth"] = self._user, self._password
279
+ elif self._auth_type == "oauth2":
280
+ if "headers" not in kwargs:
281
+ kwargs["headers"] = {}
282
+ kwargs["headers"]["Authorization"] = f"Bearer {self._access_token}"
283
+ return kwargs
284
+
285
+ def _make_call(self, call_type: str, url: str, refresh_token: bool = True, **kwargs) -> dict:
286
+ """
287
+ Make a call to the DHCore API.
288
+
289
+ Parameters
290
+ ----------
291
+ call_type : str
292
+ The type of call to make.
293
+ url : str
294
+ The URL to call.
295
+ **kwargs : dict
296
+ Keyword arguments to pass to the request.
297
+
298
+ Returns
299
+ -------
300
+ dict
301
+ Response object.
302
+ """
303
+ # Call the API
304
+ response = request(call_type, url, timeout=60, **kwargs)
305
+
306
+ # Evaluate DHCore API version
307
+ self._check_core_version(response)
308
+
309
+ # Handle token refresh
310
+ if response.status_code in [401] and refresh_token:
311
+ self._get_new_access_token()
312
+ kwargs = self._set_auth(kwargs)
313
+ return self._make_call(call_type, url, refresh_token=False, **kwargs)
314
+
315
+ self._raise_for_error(response)
316
+ return self._parse_response(response)
317
+
318
+ def _check_core_version(self, response: Response) -> None:
319
+ """
320
+ Raise an exception if DHCore API version is not supported.
321
+
322
+ Parameters
323
+ ----------
324
+ response : Response
325
+ The response object.
326
+
327
+ Returns
328
+ -------
329
+ None
330
+ """
331
+ if "X-Api-Level" in response.headers:
332
+ core_api_level = int(response.headers["X-Api-Level"])
333
+ if not (MIN_API_LEVEL <= core_api_level <= MAX_API_LEVEL):
334
+ raise BackendError("Backend API level not supported.")
335
+
336
+ def _raise_for_error(self, response: Response) -> None:
337
+ """
338
+ Handle DHCore API errors.
339
+
340
+ Parameters
341
+ ----------
342
+ response : Response
343
+ The response object.
344
+
345
+ Returns
346
+ -------
347
+ None
348
+ """
349
+ try:
350
+ response.raise_for_status()
351
+
352
+ # Backend errors
353
+ except RequestException as e:
354
+ # Handle timeout
355
+ if isinstance(e, TimeoutError):
356
+ msg = "Request to DHCore backend timed out."
357
+ raise TimeoutError(msg)
358
+
359
+ # Handle connection error
360
+ elif isinstance(e, ConnectionError):
361
+ msg = "Unable to connect to DHCore backend."
362
+ raise ConnectionError(msg)
363
+
364
+ # Handle HTTP errors
365
+ elif isinstance(e, HTTPError):
366
+ txt_resp = f"Response: {response.text}."
367
+
368
+ # Bad request
369
+ if response.status_code == 400:
370
+ # Missing spec in backend
371
+ if "missing spec" in response.text:
372
+ msg = f"Missing spec in backend. {txt_resp}"
373
+ raise MissingSpecError(msg)
374
+
375
+ # Duplicated entity
376
+ elif "Duplicated entity" in response.text:
377
+ msg = f"Entity already exists. {txt_resp}"
378
+ raise EntityAlreadyExistsError(msg)
379
+
380
+ # Other errors
381
+ else:
382
+ msg = f"Bad request. {txt_resp}"
383
+ raise BadRequestError(msg)
384
+
385
+ # Unauthorized errors
386
+ elif response.status_code == 401:
387
+ msg = f"Unauthorized. {txt_resp}"
388
+ raise UnauthorizedError(msg)
389
+
390
+ # Forbidden errors
391
+ elif response.status_code == 403:
392
+ msg = f"Forbidden. {txt_resp}"
393
+ raise ForbiddenError(msg)
394
+
395
+ # Entity not found
396
+ elif response.status_code == 404:
397
+ # Put with entity not found
398
+ if "No such EntityName" in response.text:
399
+ msg = f"Entity does not exists. {txt_resp}"
400
+ raise EntityNotExistsError(msg)
401
+
402
+ # Other cases
403
+ else:
404
+ msg = f"Not found. {txt_resp}"
405
+ raise BackendError(msg)
406
+
407
+ # Other errors
408
+ else:
409
+ msg = f"Backend error. {txt_resp}"
410
+ raise BackendError(msg) from e
411
+
412
+ # Other requests errors
413
+ else:
414
+ msg = f"Some error occurred. {e}"
415
+ raise BackendError(msg) from e
416
+
417
+ # Other generic errors
418
+ except Exception as e:
419
+ msg = f"Some error occurred: {e}"
420
+ raise RuntimeError(msg) from e
421
+
422
+ def _parse_response(self, response: Response) -> dict:
423
+ """
424
+ Parse the response object.
425
+
426
+ Parameters
427
+ ----------
428
+ response : Response
429
+ The response object.
430
+
431
+ Returns
432
+ -------
433
+ dict
434
+ The parsed response object.
435
+ """
436
+ try:
437
+ return response.json()
438
+ except JSONDecodeError:
439
+ if response.text == "":
440
+ return {}
441
+ raise BackendError("Backend response could not be parsed.")
442
+
443
+ ##############################
444
+ # Configuration methods
445
+ ##############################
446
+
447
+ def _configure(self, config: dict | None = None) -> None:
448
+ """
449
+ Configure the client attributes with config (given or from
450
+ environment).
451
+ Regarding authentication parameters, the config parameter
452
+ takes precedence over the env variables, and the token
453
+ over the basic auth. Furthermore, the config parameter is
454
+ validated against the proper pydantic model.
455
+
456
+ Parameters
457
+ ----------
458
+ config : dict
459
+ Configuration dictionary.
460
+
461
+ Returns
462
+ -------
463
+ None
464
+ """
465
+ # Load env from file
466
+ self._load_env()
467
+
468
+ self._get_endpoints_from_env()
469
+
470
+ if config is not None:
471
+ if config.get("access_token") is not None:
472
+ config = OAuth2TokenAuth(**config)
473
+ self._user = config.user
474
+ self._access_token = config.access_token
475
+ self._refresh_token = config.refresh_token
476
+ self._client_id = config.client_id
477
+ self._auth_type = "oauth2"
478
+
479
+ elif config.get("user") is not None and config.get("password") is not None:
480
+ config = BasicAuth(**config)
481
+ self._user = config.user
482
+ self._password = config.password
483
+ self._auth_type = "basic"
484
+
485
+ return
486
+
487
+ self._get_auth_from_env()
488
+
489
+ # Propagate access and refresh token to env file
490
+ self._write_env()
491
+
492
+ def _get_endpoints_from_env(self) -> None:
493
+ """
494
+ Get the DHCore endpoint and token issuer endpoint from env.
495
+
496
+ Returns
497
+ -------
498
+ None
499
+
500
+ Raises
501
+ ------
502
+ Exception
503
+ If the endpoint of DHCore is not set in the env variables.
504
+ """
505
+ core_endpt = os.getenv("DHCORE_ENDPOINT")
506
+ if core_endpt is None:
507
+ raise BackendError("Endpoint not set as environment variables.")
508
+ self._endpoint_core = self._sanitize_endpoint(core_endpt)
509
+
510
+ issr_endpt = os.getenv("DHCORE_ISSUER")
511
+ if issr_endpt is not None:
512
+ self._endpoint_issuer = self._sanitize_endpoint(issr_endpt)
513
+
514
+ def _sanitize_endpoint(self, endpoint: str) -> str:
515
+ """
516
+ Sanitize the endpoint.
517
+
518
+ Returns
519
+ -------
520
+ None
521
+ """
522
+ parsed = urlparse(endpoint)
523
+ if parsed.scheme not in ["http", "https"]:
524
+ raise BackendError("Invalid endpoint scheme.")
525
+
526
+ endpoint = endpoint.strip()
527
+ return endpoint.removesuffix("/")
528
+
529
+ def _get_auth_from_env(self) -> None:
530
+ """
531
+ Get authentication parameters from the env.
532
+
533
+ Returns
534
+ -------
535
+ None
536
+ """
537
+ self._user = os.getenv("DHCORE_USER", FALLBACK_USER)
538
+ self._refresh_token = os.getenv("DHCORE_REFRESH_TOKEN")
539
+ self._client_id = os.getenv("DHCORE_CLIENT_ID")
540
+
541
+ token = os.getenv("DHCORE_ACCESS_TOKEN")
542
+ if token is not None and token != "":
543
+ self._auth_type = "oauth2"
544
+ self._access_token = token
545
+ return
546
+
547
+ password = os.getenv("DHCORE_PASSWORD")
548
+ if self._user is not None and password is not None:
549
+ self._auth_type = "basic"
550
+ self._password = password
551
+ return
552
+
553
+ def _get_new_access_token(self) -> None:
554
+ """
555
+ Get a new access token.
556
+
557
+ Returns
558
+ -------
559
+ None
560
+ """
561
+ # Call issuer and get endpoint for
562
+ # refreshing access token
563
+ url = self._get_refresh_endpoint()
564
+
565
+ # Call refresh token endpoint
566
+ response = self._call_refresh_token_endpoint(url)
567
+
568
+ # Read new access token and refresh token
569
+ self._access_token = response["access_token"]
570
+ self._refresh_token = response["refresh_token"]
571
+
572
+ # Propagate new access token to env
573
+ self._write_env()
574
+
575
+ def _get_refresh_endpoint(self) -> str:
576
+ """
577
+ Get the refresh endpoint.
578
+
579
+ Returns
580
+ -------
581
+ str
582
+ Refresh endpoint.
583
+ """
584
+ # Get issuer endpoint
585
+ if self._endpoint_issuer is None:
586
+ raise BackendError("Issuer endpoint not set.")
587
+
588
+ # Standard issuer endpoint path
589
+ url = self._endpoint_issuer + "/.well-known/openid-configuration"
590
+
591
+ # Call
592
+ r = request("GET", url, timeout=60)
593
+ self._raise_for_error(r)
594
+ return r.json().get("token_endpoint")
595
+
596
+ def _call_refresh_token_endpoint(self, url: str) -> dict:
597
+ """
598
+ Call the refresh token endpoint.
599
+
600
+ Parameters
601
+ ----------
602
+ url : str
603
+ Refresh token endpoint.
604
+
605
+ Returns
606
+ -------
607
+ dict
608
+ Response object.
609
+ """
610
+ # Get refersh token from .core file to avoid concurrency
611
+ # in a shared workspace
612
+ self._load_env()
613
+ refresh_token = os.getenv("DHCORE_REFRESH_TOKEN")
614
+
615
+ # Send request to get new access token
616
+ payload = {
617
+ "grant_type": "refresh_token",
618
+ "client_id": self._client_id,
619
+ "refresh_token": refresh_token,
620
+ }
621
+ headers = {"Content-Type": "application/x-www-form-urlencoded"}
622
+ r = request("POST", url, data=payload, headers=headers, timeout=60)
623
+ self._raise_for_error(r)
624
+ return r.json()
625
+
626
+ @staticmethod
627
+ def _load_env() -> None:
628
+ """
629
+ Load the env variables from the .dhcore file.
630
+
631
+ Returns
632
+ -------
633
+ None
634
+ """
635
+ load_dotenv(dotenv_path=ENV_FILE, override=True)
636
+
637
+ def _write_env(self) -> None:
638
+ """
639
+ Write the env variables to the .dhcore file.
640
+ It will overwrite any existing env variables.
641
+
642
+ Returns
643
+ -------
644
+ None
645
+ """
646
+ keys = {}
647
+ if self._access_token is not None:
648
+ keys["DHCORE_ACCESS_TOKEN"] = self._access_token
649
+ if self._refresh_token is not None:
650
+ keys["DHCORE_REFRESH_TOKEN"] = self._refresh_token
651
+
652
+ for k, v in keys.items():
653
+ set_key(dotenv_path=ENV_FILE, key_to_set=k, value_to_set=v)
654
+
655
+ ##############################
656
+ # Interface methods
657
+ ##############################
658
+
659
+ @staticmethod
660
+ def is_local() -> bool:
661
+ """
662
+ Declare if Client is local.
663
+
664
+ Returns
665
+ -------
666
+ bool
667
+ False
668
+ """
669
+ return False
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ # Use env user as fallback in the API calls
6
+ try:
7
+ FALLBACK_USER = os.getlogin()
8
+ except Exception:
9
+ FALLBACK_USER = None
10
+
11
+ # File where to write DHCORE_ACCESS_TOKEN and DHCORE_REFRESH_TOKEN
12
+ # It's used because we inject the variables in jupyter env,
13
+ # but refresh token is only available once. Is it's used, we cannot
14
+ # overwrite it with coder, so we need to store the new one in a file,
15
+ # preserved for jupyter restart
16
+ ENV_FILE = ".dhcore"
17
+
18
+
19
+ # API levels that are supported
20
+ MAX_API_LEVEL = 20
21
+ MIN_API_LEVEL = 8