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.
- digitalhub/__init__.py +63 -93
- digitalhub/client/__init__.py +0 -0
- digitalhub/client/_base/__init__.py +0 -0
- digitalhub/client/_base/client.py +56 -0
- digitalhub/client/api.py +63 -0
- digitalhub/client/builder.py +50 -0
- digitalhub/client/dhcore/__init__.py +0 -0
- digitalhub/client/dhcore/client.py +669 -0
- digitalhub/client/dhcore/env.py +21 -0
- digitalhub/client/dhcore/models.py +46 -0
- digitalhub/client/dhcore/utils.py +111 -0
- digitalhub/client/local/__init__.py +0 -0
- digitalhub/client/local/client.py +533 -0
- digitalhub/context/__init__.py +0 -0
- digitalhub/context/api.py +93 -0
- digitalhub/context/builder.py +94 -0
- digitalhub/context/context.py +136 -0
- digitalhub/datastores/__init__.py +0 -0
- digitalhub/datastores/_base/__init__.py +0 -0
- digitalhub/datastores/_base/datastore.py +85 -0
- digitalhub/datastores/api.py +37 -0
- digitalhub/datastores/builder.py +110 -0
- digitalhub/datastores/local/__init__.py +0 -0
- digitalhub/datastores/local/datastore.py +50 -0
- digitalhub/datastores/remote/__init__.py +0 -0
- digitalhub/datastores/remote/datastore.py +31 -0
- digitalhub/datastores/s3/__init__.py +0 -0
- digitalhub/datastores/s3/datastore.py +46 -0
- digitalhub/datastores/sql/__init__.py +0 -0
- digitalhub/datastores/sql/datastore.py +68 -0
- digitalhub/entities/__init__.py +0 -0
- digitalhub/entities/_base/__init__.py +0 -0
- digitalhub/entities/_base/_base/__init__.py +0 -0
- digitalhub/entities/_base/_base/entity.py +82 -0
- digitalhub/entities/_base/api_utils.py +620 -0
- digitalhub/entities/_base/context/__init__.py +0 -0
- digitalhub/entities/_base/context/entity.py +118 -0
- digitalhub/entities/_base/crud.py +468 -0
- digitalhub/entities/_base/entity/__init__.py +0 -0
- digitalhub/entities/_base/entity/_constructors/__init__.py +0 -0
- digitalhub/entities/_base/entity/_constructors/metadata.py +44 -0
- digitalhub/entities/_base/entity/_constructors/name.py +31 -0
- digitalhub/entities/_base/entity/_constructors/spec.py +33 -0
- digitalhub/entities/_base/entity/_constructors/status.py +52 -0
- digitalhub/entities/_base/entity/_constructors/uuid.py +26 -0
- digitalhub/entities/_base/entity/builder.py +175 -0
- digitalhub/entities/_base/entity/entity.py +106 -0
- digitalhub/entities/_base/entity/metadata.py +59 -0
- digitalhub/entities/_base/entity/spec.py +58 -0
- digitalhub/entities/_base/entity/status.py +43 -0
- digitalhub/entities/_base/executable/__init__.py +0 -0
- digitalhub/entities/_base/executable/entity.py +405 -0
- digitalhub/entities/_base/material/__init__.py +0 -0
- digitalhub/entities/_base/material/entity.py +214 -0
- digitalhub/entities/_base/material/spec.py +22 -0
- digitalhub/entities/_base/material/status.py +49 -0
- digitalhub/entities/_base/runtime_entity/__init__.py +0 -0
- digitalhub/entities/_base/runtime_entity/builder.py +106 -0
- digitalhub/entities/_base/unversioned/__init__.py +0 -0
- digitalhub/entities/_base/unversioned/builder.py +66 -0
- digitalhub/entities/_base/unversioned/entity.py +49 -0
- digitalhub/entities/_base/versioned/__init__.py +0 -0
- digitalhub/entities/_base/versioned/builder.py +68 -0
- digitalhub/entities/_base/versioned/entity.py +53 -0
- digitalhub/entities/artifact/__init__.py +0 -0
- digitalhub/entities/artifact/_base/__init__.py +0 -0
- digitalhub/entities/artifact/_base/builder.py +86 -0
- digitalhub/entities/artifact/_base/entity.py +39 -0
- digitalhub/entities/artifact/_base/spec.py +15 -0
- digitalhub/entities/artifact/_base/status.py +9 -0
- digitalhub/entities/artifact/artifact/__init__.py +0 -0
- digitalhub/entities/artifact/artifact/builder.py +18 -0
- digitalhub/entities/artifact/artifact/entity.py +32 -0
- digitalhub/entities/artifact/artifact/spec.py +27 -0
- digitalhub/entities/artifact/artifact/status.py +15 -0
- digitalhub/entities/artifact/crud.py +332 -0
- digitalhub/entities/builders.py +63 -0
- digitalhub/entities/dataitem/__init__.py +0 -0
- digitalhub/entities/dataitem/_base/__init__.py +0 -0
- digitalhub/entities/dataitem/_base/builder.py +86 -0
- digitalhub/entities/dataitem/_base/entity.py +75 -0
- digitalhub/entities/dataitem/_base/spec.py +15 -0
- digitalhub/entities/dataitem/_base/status.py +20 -0
- digitalhub/entities/dataitem/crud.py +372 -0
- digitalhub/entities/dataitem/dataitem/__init__.py +0 -0
- digitalhub/entities/dataitem/dataitem/builder.py +18 -0
- digitalhub/entities/dataitem/dataitem/entity.py +32 -0
- digitalhub/entities/dataitem/dataitem/spec.py +15 -0
- digitalhub/entities/dataitem/dataitem/status.py +9 -0
- digitalhub/entities/dataitem/iceberg/__init__.py +0 -0
- digitalhub/entities/dataitem/iceberg/builder.py +18 -0
- digitalhub/entities/dataitem/iceberg/entity.py +32 -0
- digitalhub/entities/dataitem/iceberg/spec.py +15 -0
- digitalhub/entities/dataitem/iceberg/status.py +9 -0
- digitalhub/entities/dataitem/table/__init__.py +0 -0
- digitalhub/entities/dataitem/table/builder.py +18 -0
- digitalhub/entities/dataitem/table/entity.py +146 -0
- digitalhub/entities/dataitem/table/models.py +62 -0
- digitalhub/entities/dataitem/table/spec.py +25 -0
- digitalhub/entities/dataitem/table/status.py +9 -0
- digitalhub/entities/function/__init__.py +0 -0
- digitalhub/entities/function/_base/__init__.py +0 -0
- digitalhub/entities/function/_base/builder.py +79 -0
- digitalhub/entities/function/_base/entity.py +98 -0
- digitalhub/entities/function/_base/models.py +118 -0
- digitalhub/entities/function/_base/spec.py +15 -0
- digitalhub/entities/function/_base/status.py +9 -0
- digitalhub/entities/function/crud.py +279 -0
- digitalhub/entities/model/__init__.py +0 -0
- digitalhub/entities/model/_base/__init__.py +0 -0
- digitalhub/entities/model/_base/builder.py +86 -0
- digitalhub/entities/model/_base/entity.py +34 -0
- digitalhub/entities/model/_base/spec.py +49 -0
- digitalhub/entities/model/_base/status.py +9 -0
- digitalhub/entities/model/crud.py +331 -0
- digitalhub/entities/model/huggingface/__init__.py +0 -0
- digitalhub/entities/model/huggingface/builder.py +18 -0
- digitalhub/entities/model/huggingface/entity.py +32 -0
- digitalhub/entities/model/huggingface/spec.py +36 -0
- digitalhub/entities/model/huggingface/status.py +9 -0
- digitalhub/entities/model/mlflow/__init__.py +0 -0
- digitalhub/entities/model/mlflow/builder.py +18 -0
- digitalhub/entities/model/mlflow/entity.py +32 -0
- digitalhub/entities/model/mlflow/models.py +26 -0
- digitalhub/entities/model/mlflow/spec.py +44 -0
- digitalhub/entities/model/mlflow/status.py +9 -0
- digitalhub/entities/model/mlflow/utils.py +81 -0
- digitalhub/entities/model/model/__init__.py +0 -0
- digitalhub/entities/model/model/builder.py +18 -0
- digitalhub/entities/model/model/entity.py +32 -0
- digitalhub/entities/model/model/spec.py +15 -0
- digitalhub/entities/model/model/status.py +9 -0
- digitalhub/entities/model/sklearn/__init__.py +0 -0
- digitalhub/entities/model/sklearn/builder.py +18 -0
- digitalhub/entities/model/sklearn/entity.py +32 -0
- digitalhub/entities/model/sklearn/spec.py +15 -0
- digitalhub/entities/model/sklearn/status.py +9 -0
- digitalhub/entities/project/__init__.py +0 -0
- digitalhub/entities/project/_base/__init__.py +0 -0
- digitalhub/entities/project/_base/builder.py +128 -0
- digitalhub/entities/project/_base/entity.py +2078 -0
- digitalhub/entities/project/_base/spec.py +50 -0
- digitalhub/entities/project/_base/status.py +9 -0
- digitalhub/entities/project/crud.py +357 -0
- digitalhub/entities/run/__init__.py +0 -0
- digitalhub/entities/run/_base/__init__.py +0 -0
- digitalhub/entities/run/_base/builder.py +94 -0
- digitalhub/entities/run/_base/entity.py +307 -0
- digitalhub/entities/run/_base/spec.py +50 -0
- digitalhub/entities/run/_base/status.py +9 -0
- digitalhub/entities/run/crud.py +219 -0
- digitalhub/entities/secret/__init__.py +0 -0
- digitalhub/entities/secret/_base/__init__.py +0 -0
- digitalhub/entities/secret/_base/builder.py +81 -0
- digitalhub/entities/secret/_base/entity.py +74 -0
- digitalhub/entities/secret/_base/spec.py +35 -0
- digitalhub/entities/secret/_base/status.py +9 -0
- digitalhub/entities/secret/crud.py +290 -0
- digitalhub/entities/task/__init__.py +0 -0
- digitalhub/entities/task/_base/__init__.py +0 -0
- digitalhub/entities/task/_base/builder.py +91 -0
- digitalhub/entities/task/_base/entity.py +136 -0
- digitalhub/entities/task/_base/models.py +208 -0
- digitalhub/entities/task/_base/spec.py +53 -0
- digitalhub/entities/task/_base/status.py +9 -0
- digitalhub/entities/task/crud.py +228 -0
- digitalhub/entities/utils/__init__.py +0 -0
- digitalhub/entities/utils/api.py +346 -0
- digitalhub/entities/utils/entity_types.py +19 -0
- digitalhub/entities/utils/state.py +31 -0
- digitalhub/entities/utils/utils.py +202 -0
- digitalhub/entities/workflow/__init__.py +0 -0
- digitalhub/entities/workflow/_base/__init__.py +0 -0
- digitalhub/entities/workflow/_base/builder.py +79 -0
- digitalhub/entities/workflow/_base/entity.py +74 -0
- digitalhub/entities/workflow/_base/spec.py +15 -0
- digitalhub/entities/workflow/_base/status.py +9 -0
- digitalhub/entities/workflow/crud.py +278 -0
- digitalhub/factory/__init__.py +0 -0
- digitalhub/factory/api.py +277 -0
- digitalhub/factory/factory.py +268 -0
- digitalhub/factory/utils.py +90 -0
- digitalhub/readers/__init__.py +0 -0
- digitalhub/readers/_base/__init__.py +0 -0
- digitalhub/readers/_base/builder.py +26 -0
- digitalhub/readers/_base/reader.py +70 -0
- digitalhub/readers/api.py +80 -0
- digitalhub/readers/factory.py +133 -0
- digitalhub/readers/pandas/__init__.py +0 -0
- digitalhub/readers/pandas/builder.py +29 -0
- digitalhub/readers/pandas/reader.py +207 -0
- digitalhub/runtimes/__init__.py +0 -0
- digitalhub/runtimes/_base.py +102 -0
- digitalhub/runtimes/builder.py +32 -0
- digitalhub/stores/__init__.py +0 -0
- digitalhub/stores/_base/__init__.py +0 -0
- digitalhub/stores/_base/store.py +189 -0
- digitalhub/stores/api.py +54 -0
- digitalhub/stores/builder.py +211 -0
- digitalhub/stores/local/__init__.py +0 -0
- digitalhub/stores/local/store.py +230 -0
- digitalhub/stores/remote/__init__.py +0 -0
- digitalhub/stores/remote/store.py +143 -0
- digitalhub/stores/s3/__init__.py +0 -0
- digitalhub/stores/s3/store.py +563 -0
- digitalhub/stores/sql/__init__.py +0 -0
- digitalhub/stores/sql/store.py +328 -0
- digitalhub/utils/__init__.py +0 -0
- digitalhub/utils/data_utils.py +127 -0
- digitalhub/utils/exceptions.py +67 -0
- digitalhub/utils/file_utils.py +204 -0
- digitalhub/utils/generic_utils.py +183 -0
- digitalhub/utils/git_utils.py +148 -0
- digitalhub/utils/io_utils.py +116 -0
- digitalhub/utils/logger.py +17 -0
- digitalhub/utils/s3_utils.py +58 -0
- digitalhub/utils/uri_utils.py +56 -0
- {digitalhub-0.7.0b2.dist-info → digitalhub-0.8.0.dist-info}/METADATA +30 -13
- digitalhub-0.8.0.dist-info/RECORD +231 -0
- {digitalhub-0.7.0b2.dist-info → digitalhub-0.8.0.dist-info}/WHEEL +1 -1
- test/local/CRUD/test_artifacts.py +96 -0
- test/local/CRUD/test_dataitems.py +96 -0
- test/local/CRUD/test_models.py +95 -0
- test/test_crud_functions.py +1 -1
- test/test_crud_runs.py +1 -1
- test/test_crud_tasks.py +1 -1
- digitalhub-0.7.0b2.dist-info/RECORD +0 -14
- test/test_crud_artifacts.py +0 -96
- test/test_crud_dataitems.py +0 -96
- {digitalhub-0.7.0b2.dist-info → digitalhub-0.8.0.dist-info}/LICENSE.txt +0 -0
- {digitalhub-0.7.0b2.dist-info → digitalhub-0.8.0.dist-info}/top_level.txt +0 -0
- /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
|