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