kalbio 0.2.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.
- kalbio/__init__.py +1 -0
- kalbio/_kaleidoscope_model.py +111 -0
- kalbio/activities.py +1202 -0
- kalbio/client.py +463 -0
- kalbio/dashboards.py +276 -0
- kalbio/entity_fields.py +474 -0
- kalbio/entity_types.py +188 -0
- kalbio/exports.py +126 -0
- kalbio/helpers.py +52 -0
- kalbio/imports.py +89 -0
- kalbio/labels.py +88 -0
- kalbio/programs.py +96 -0
- kalbio/property_fields.py +81 -0
- kalbio/record_views.py +191 -0
- kalbio/records.py +1173 -0
- kalbio/workspace.py +315 -0
- kalbio-0.2.0.dist-info/METADATA +289 -0
- kalbio-0.2.0.dist-info/RECORD +19 -0
- kalbio-0.2.0.dist-info/WHEEL +4 -0
kalbio/client.py
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"""Kaleidoscope API Client Module.
|
|
2
|
+
|
|
3
|
+
This module provides the main client class for interacting with the Kaleidoscope API.
|
|
4
|
+
|
|
5
|
+
The KaleidoscopeClient provides access to various service endpoints including:
|
|
6
|
+
|
|
7
|
+
- activities: Manage activities
|
|
8
|
+
- imports: Import data into Kaleidoscope
|
|
9
|
+
- programs: Manage programs
|
|
10
|
+
- entity_types: Manage entity types
|
|
11
|
+
- records: Manage records
|
|
12
|
+
- fields: Manage fields
|
|
13
|
+
- experiments: Manage experiments
|
|
14
|
+
- record_views: Manage record views
|
|
15
|
+
- exports: Export data from Kaleidoscope
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
PROD_API_URL (str): The production URL for the Kaleidoscope API.
|
|
19
|
+
VALID_CONTENT_TYPES (list): List of acceptable content types for file downloads.
|
|
20
|
+
TIMEOUT_MAXIMUM (int): Maximum timeout for API requests in seconds.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
```python
|
|
24
|
+
# instantiate client object
|
|
25
|
+
client = KaleidoscopeClient(
|
|
26
|
+
client_id="your_client_id",
|
|
27
|
+
client_secret="your_client_secret"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# retrieve activities
|
|
31
|
+
programs = client.activities.get_activities()
|
|
32
|
+
```
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import os
|
|
36
|
+
from datetime import datetime, timedelta
|
|
37
|
+
import json
|
|
38
|
+
from json import JSONDecodeError
|
|
39
|
+
from pydantic import BaseModel
|
|
40
|
+
import requests
|
|
41
|
+
import urllib
|
|
42
|
+
from typing import Any, BinaryIO, Dict, Optional
|
|
43
|
+
|
|
44
|
+
PROD_API_URL = "https://api.kaleidoscope.bio"
|
|
45
|
+
"""The production URL for the Kaleidoscope API.
|
|
46
|
+
|
|
47
|
+
This is the default url used for the `KaleidoscopeClient`, in the event
|
|
48
|
+
no url is provided in the `KaleidoscopeClient`'s initialization"""
|
|
49
|
+
|
|
50
|
+
VALID_CONTENT_TYPES = [
|
|
51
|
+
"text/csv",
|
|
52
|
+
"chemical/x-mdl-sdfile",
|
|
53
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
54
|
+
]
|
|
55
|
+
"""List of acceptable content types for file downloads.
|
|
56
|
+
|
|
57
|
+
Any file retrieved by the `KaleidoscopeClient` must be of one the above types"""
|
|
58
|
+
|
|
59
|
+
TIMEOUT_MAXIMUM = 10
|
|
60
|
+
"""Maximum timeout for API requests in seconds."""
|
|
61
|
+
|
|
62
|
+
_api_key = os.getenv("KALEIDOSCOPE_API_CLIENT_ID")
|
|
63
|
+
_api_secret = os.getenv("KALEIDOSCOPE_API_CLIENT_SECRET")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _TokenResponse(BaseModel):
|
|
67
|
+
"""OAuth token response payload returned by Kaleidoscope auth endpoints.
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
access_token (str): Bearer token used for authenticated API calls.
|
|
71
|
+
refresh_token (str): Token used to obtain a new access token when the current one expires.
|
|
72
|
+
expires_in (int): Lifetime of the access token in seconds.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
access_token: str
|
|
76
|
+
refresh_token: str
|
|
77
|
+
expires_in: int
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class KaleidoscopeClient:
|
|
81
|
+
"""A client for interacting with the Kaleidoscope API.
|
|
82
|
+
|
|
83
|
+
This client provides a high-level interface to various Kaleidoscope services including
|
|
84
|
+
imports, programs, entity types, records, fields, tasks, experiments, record views, and exports.
|
|
85
|
+
It handles authentication using API key credentials and provides methods for making HTTP requests
|
|
86
|
+
(GET, POST, PUT) to the API endpoints.
|
|
87
|
+
|
|
88
|
+
Attributes:
|
|
89
|
+
activities (ActivitiesService): Service for managing activities.
|
|
90
|
+
dashboards (DashboardsService): Service for managing dashboards.
|
|
91
|
+
workspace (WorkspaceService): Service for workspace-related operations.
|
|
92
|
+
programs (ProgramsService): Service for managing programs.
|
|
93
|
+
labels (LabelsService): Service for managing labels.
|
|
94
|
+
entity_types (EntityTypesService): Service for managing entity types.
|
|
95
|
+
entity_fields (EntityFieldsService): Service for managing entity fields.
|
|
96
|
+
records (RecordsService): Service for managing records.
|
|
97
|
+
record_views (RecordViewsService): Service for managing record views.
|
|
98
|
+
imports (ImportsService): Service for managing data imports.
|
|
99
|
+
exports (ExportsService): Service for managing data exports.
|
|
100
|
+
property_fields (PropertyFieldsService): Service for managing property fields.
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
```python
|
|
104
|
+
client = KaleidoscopeClient(
|
|
105
|
+
client_id="your_api_client_id",
|
|
106
|
+
client_secret="your_api_client_secret"
|
|
107
|
+
)
|
|
108
|
+
# Use the client to interact with various services
|
|
109
|
+
programs = client.activities.get_activities()
|
|
110
|
+
```
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
_client_id: str
|
|
114
|
+
_client_secret: str
|
|
115
|
+
|
|
116
|
+
_refresh_token: str
|
|
117
|
+
_access_token: str
|
|
118
|
+
_refresh_before: datetime
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
client_id: Optional[str] = _api_key,
|
|
123
|
+
client_secret: Optional[str] = _api_secret,
|
|
124
|
+
url: str = PROD_API_URL,
|
|
125
|
+
):
|
|
126
|
+
"""Initialize the Kaleidoscope API client.
|
|
127
|
+
|
|
128
|
+
Sets up the client with API credentials and optional API URL, and initializes
|
|
129
|
+
service interfaces for interacting with different API endpoints.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
client_id (str): The API client ID for authentication.
|
|
133
|
+
client_secret (str): The API client secret for authentication.
|
|
134
|
+
url (Optional[str]): The base URL for the API. Defaults to the production
|
|
135
|
+
API URL if not provided.
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
```python
|
|
139
|
+
# Using explicit credentials
|
|
140
|
+
client = KaleidoscopeClient(client_id="id", client_secret="secret")
|
|
141
|
+
|
|
142
|
+
# Or rely on environment variables KALEIDOSCOPE_API_CLIENT_ID/SECRET
|
|
143
|
+
client = KaleidoscopeClient()
|
|
144
|
+
```
|
|
145
|
+
"""
|
|
146
|
+
if client_id is None:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
'No client_id provided and "KALEIDOSCOPE_API_CLIENT_ID" was not found in the environment.'
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if client_secret is None:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
'No client_secret provided and "KALEIDOSCOPE_API_CLIENT_SECRET" was not found in the environment.'
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
from kalbio.activities import ActivitiesService
|
|
157
|
+
from kalbio.dashboards import DashboardsService
|
|
158
|
+
from kalbio.entity_fields import EntityFieldsService
|
|
159
|
+
from kalbio.entity_types import EntityTypesService
|
|
160
|
+
from kalbio.exports import ExportsService
|
|
161
|
+
from kalbio.imports import ImportsService
|
|
162
|
+
from kalbio.labels import LabelsService
|
|
163
|
+
from kalbio.programs import ProgramsService
|
|
164
|
+
from kalbio.property_fields import PropertyFieldsService
|
|
165
|
+
from kalbio.record_views import RecordViewsService
|
|
166
|
+
from kalbio.records import RecordsService
|
|
167
|
+
from kalbio.workspace import WorkspaceService
|
|
168
|
+
|
|
169
|
+
self._api_url = url
|
|
170
|
+
|
|
171
|
+
self.activities = ActivitiesService(self)
|
|
172
|
+
self.dashboards = DashboardsService(self)
|
|
173
|
+
self.entity_fields = EntityFieldsService(self)
|
|
174
|
+
self.entity_types = EntityTypesService(self)
|
|
175
|
+
self.exports = ExportsService(self)
|
|
176
|
+
self.imports = ImportsService(self)
|
|
177
|
+
self.labels = LabelsService(self)
|
|
178
|
+
self.property_fields = PropertyFieldsService(self)
|
|
179
|
+
self.programs = ProgramsService(self)
|
|
180
|
+
self.record_views = RecordViewsService(self)
|
|
181
|
+
self.records = RecordsService(self)
|
|
182
|
+
self.workspace = WorkspaceService(self)
|
|
183
|
+
|
|
184
|
+
self._client_id = client_id
|
|
185
|
+
self._client_secret = client_secret
|
|
186
|
+
self._get_auth_token()
|
|
187
|
+
|
|
188
|
+
def _update_tokens(self, resp: _TokenResponse):
|
|
189
|
+
"""Persist access and refresh tokens and compute the next refresh time.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
resp: Token payload returned from the auth endpoint.
|
|
193
|
+
"""
|
|
194
|
+
self._access_token = resp.access_token
|
|
195
|
+
self._refresh_token = resp.refresh_token
|
|
196
|
+
self._last_refreshed_at = datetime.now() + timedelta(
|
|
197
|
+
seconds=resp.expires_in - (60 * 10) # add a 10 minute buffer
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def _get_auth_token(self):
|
|
201
|
+
"""Fetch an access token using client credentials.
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
RuntimeError: If the auth endpoint responds with an error status code.
|
|
205
|
+
"""
|
|
206
|
+
auth_resp = requests.post(
|
|
207
|
+
self._api_url + "/auth/oauth/token",
|
|
208
|
+
data={
|
|
209
|
+
"grant_type": "client_credentials",
|
|
210
|
+
"client_id": self._client_id,
|
|
211
|
+
"client_secret": self._client_secret,
|
|
212
|
+
},
|
|
213
|
+
timeout=TIMEOUT_MAXIMUM,
|
|
214
|
+
)
|
|
215
|
+
if auth_resp.status_code >= 400:
|
|
216
|
+
raise RuntimeError(
|
|
217
|
+
f"Could not connect to server with client_id {self._client_id}: {auth_resp.content}"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
self._update_tokens(_TokenResponse.model_validate(auth_resp.json()))
|
|
221
|
+
|
|
222
|
+
def _refresh_auth_token(self):
|
|
223
|
+
"""Refresh the access token using the stored refresh token.
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
RuntimeError: If the auth endpoint responds with an error status code.
|
|
227
|
+
"""
|
|
228
|
+
if self._refresh_token is None:
|
|
229
|
+
return self._get_auth_token()
|
|
230
|
+
|
|
231
|
+
auth_resp = requests.post(
|
|
232
|
+
self._api_url + "/auth/oauth/token",
|
|
233
|
+
data={
|
|
234
|
+
"grant_type": "refresh_token",
|
|
235
|
+
"refresh_token": self._refresh_token,
|
|
236
|
+
},
|
|
237
|
+
timeout=TIMEOUT_MAXIMUM,
|
|
238
|
+
)
|
|
239
|
+
if auth_resp.status_code >= 400:
|
|
240
|
+
raise RuntimeError(f"Could not refresh access token: {auth_resp.content}")
|
|
241
|
+
|
|
242
|
+
self._update_tokens(_TokenResponse.model_validate(auth_resp.json()))
|
|
243
|
+
|
|
244
|
+
def _get_headers(self) -> dict:
|
|
245
|
+
"""Build authorization headers, refreshing tokens if needed.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
dict: HTTP headers including `Authorization` and `Content-Type`.
|
|
249
|
+
"""
|
|
250
|
+
if datetime.now() > self._last_refreshed_at:
|
|
251
|
+
self._refresh_auth_token()
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
"Content-Type": "application/json",
|
|
255
|
+
"Authorization": f"Bearer {self._access_token}",
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
def _post(self, url: str, payload: dict) -> Any:
|
|
259
|
+
"""Send a POST request to the specified URL with the given payload.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
url (str): The endpoint URL (relative to the API base URL) to send the
|
|
263
|
+
POST request to.
|
|
264
|
+
payload (dict): The data to be sent in the body of the POST request.
|
|
265
|
+
Should be serializable to JSON.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Any: The JSON response from the server if the request is successful
|
|
269
|
+
and the response is valid JSON. Returns None if the response cannot be decoded.
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
Exception: Any exception that may be raised `requests.post`
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
resp = requests.post(
|
|
276
|
+
self._api_url + url,
|
|
277
|
+
data=json.dumps(payload),
|
|
278
|
+
headers=self._get_headers(),
|
|
279
|
+
timeout=TIMEOUT_MAXIMUM,
|
|
280
|
+
)
|
|
281
|
+
if resp.status_code >= 400:
|
|
282
|
+
print(f"POST {url} received {resp.status_code}: ", resp.content)
|
|
283
|
+
return None
|
|
284
|
+
try:
|
|
285
|
+
return resp.json()
|
|
286
|
+
except JSONDecodeError:
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
def _post_file(
|
|
290
|
+
self, url: str, file_data: tuple[str, BinaryIO, str], body: Any = None
|
|
291
|
+
) -> Any:
|
|
292
|
+
"""Send a POST request with a file and optional JSON body.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
url (str): The endpoint URL (relative to the API base URL).
|
|
296
|
+
file_data (tuple[str, BinaryIO, str]): A tuple containing the file name,
|
|
297
|
+
file object, and MIME type.
|
|
298
|
+
body (Any): Optional data to be sent as JSON in the
|
|
299
|
+
form data. Defaults to None.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Any: The JSON response from the server if the request is successful.
|
|
303
|
+
Returns None if the response cannot be decoded.
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
Exception: Any exception that may be raised `requests.post`
|
|
307
|
+
"""
|
|
308
|
+
files = {"file": file_data}
|
|
309
|
+
|
|
310
|
+
form_data = {}
|
|
311
|
+
if body:
|
|
312
|
+
form_data["body"] = json.dumps(body)
|
|
313
|
+
|
|
314
|
+
resp = requests.post(
|
|
315
|
+
self._api_url + url,
|
|
316
|
+
files=files,
|
|
317
|
+
data=form_data,
|
|
318
|
+
headers=self._get_headers(),
|
|
319
|
+
timeout=TIMEOUT_MAXIMUM,
|
|
320
|
+
)
|
|
321
|
+
if resp.status_code >= 400:
|
|
322
|
+
print(f"POST {url} received {resp.status_code}: ", resp.content)
|
|
323
|
+
return None
|
|
324
|
+
try:
|
|
325
|
+
return resp.json()
|
|
326
|
+
except JSONDecodeError:
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
def _put(self, url: str, payload: dict) -> Any:
|
|
330
|
+
"""Send a PUT request to the specified URL with the provided payload.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
url (str): The endpoint URL (relative to the base API URL).
|
|
334
|
+
payload (dict): The data to be sent in the PUT request body.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Any: The JSON response from the server if the request is successful.
|
|
338
|
+
Returns None if the response cannot be decoded.
|
|
339
|
+
|
|
340
|
+
Raises:
|
|
341
|
+
Exception: Any exception that may be raised `requests.put`
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
resp = requests.put(
|
|
345
|
+
self._api_url + url,
|
|
346
|
+
data=json.dumps(payload),
|
|
347
|
+
headers=self._get_headers(),
|
|
348
|
+
timeout=TIMEOUT_MAXIMUM,
|
|
349
|
+
)
|
|
350
|
+
if resp.status_code >= 400:
|
|
351
|
+
print(f"PUT {url} received {resp.status_code}: ", resp.content)
|
|
352
|
+
return None
|
|
353
|
+
try:
|
|
354
|
+
return resp.json()
|
|
355
|
+
except JSONDecodeError:
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
def _get(self, url: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
359
|
+
"""Send a GET request to the specified API endpoint with optional query parameters.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
url (str): The API endpoint path to append to the base URL.
|
|
363
|
+
params (Optional[Dict[str, Any]]): Dictionary of query parameters to
|
|
364
|
+
include in the request. Defaults to None.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Any: The JSON response from the server if the request is successful.
|
|
368
|
+
Returns None if the response cannot be decoded.
|
|
369
|
+
|
|
370
|
+
Raises:
|
|
371
|
+
Exception: Any exception that may be raised `requests.get`
|
|
372
|
+
"""
|
|
373
|
+
url = self._api_url + url
|
|
374
|
+
if params:
|
|
375
|
+
url += "?" + urllib.parse.urlencode(params)
|
|
376
|
+
|
|
377
|
+
resp = requests.get(url, headers=self._get_headers(), timeout=TIMEOUT_MAXIMUM)
|
|
378
|
+
if resp.status_code >= 400:
|
|
379
|
+
print(f"GET {url} received {resp.status_code}", resp.content)
|
|
380
|
+
return None
|
|
381
|
+
try:
|
|
382
|
+
return resp.json()
|
|
383
|
+
except JSONDecodeError:
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
def _get_file(
|
|
387
|
+
self, url: str, download_path: str, params: Optional[Dict[str, Any]] = None
|
|
388
|
+
) -> str | None:
|
|
389
|
+
"""Download a file from the specified URL and save it to the given path.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
url (str): The endpoint URL (relative to the API base URL) to download
|
|
393
|
+
the file from.
|
|
394
|
+
download_path (str): The local file path where the downloaded file
|
|
395
|
+
will be saved.
|
|
396
|
+
params (Optional[Dict[str, Any]]): Dictionary of query parameters to
|
|
397
|
+
include in the request. Defaults to None.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
(str | None): The path to the downloaded file if successful. Returns None
|
|
401
|
+
if the request fails or the response does not contain valid file data.
|
|
402
|
+
|
|
403
|
+
Raises:
|
|
404
|
+
Exception: Any exception that may be raised `requests.get`
|
|
405
|
+
|
|
406
|
+
Note:
|
|
407
|
+
Only responses with valid content types (as defined in VALID_CONTENT_TYPES)
|
|
408
|
+
are saved.
|
|
409
|
+
"""
|
|
410
|
+
url = self._api_url + url
|
|
411
|
+
if params:
|
|
412
|
+
url += "?" + urllib.parse.urlencode(params)
|
|
413
|
+
|
|
414
|
+
resp = requests.get(
|
|
415
|
+
url, headers=self._get_headers(), stream=True, timeout=TIMEOUT_MAXIMUM
|
|
416
|
+
)
|
|
417
|
+
if resp.status_code >= 400:
|
|
418
|
+
print(f"GET {url} received {resp.status_code}", resp.content)
|
|
419
|
+
return None
|
|
420
|
+
|
|
421
|
+
content_type = resp.headers.get("Content-Type", "")
|
|
422
|
+
if content_type not in VALID_CONTENT_TYPES:
|
|
423
|
+
print(
|
|
424
|
+
f"Invalid Content-Type: {content_type}. Response does not contain valid file data."
|
|
425
|
+
)
|
|
426
|
+
return None
|
|
427
|
+
|
|
428
|
+
with open(download_path, "wb") as f_download:
|
|
429
|
+
for chunk in resp.iter_content(chunk_size=8192):
|
|
430
|
+
if chunk:
|
|
431
|
+
f_download.write(chunk)
|
|
432
|
+
|
|
433
|
+
return download_path
|
|
434
|
+
|
|
435
|
+
def _delete(self, url: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
436
|
+
"""Send a DELETE request to the specified API endpoint with optional query parameters.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
url (str): The API endpoint path to append to the base URL.
|
|
440
|
+
params (Optional[Dict[str, Any]]): Dictionary of query parameters to
|
|
441
|
+
include in the request. Defaults to None.
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
Any: The JSON response from the server if the request is successful.
|
|
445
|
+
Returns None if the response cannot be decoded.
|
|
446
|
+
|
|
447
|
+
Raises:
|
|
448
|
+
Exception: Any exception that may be raised `requests.delete`
|
|
449
|
+
"""
|
|
450
|
+
url = self._api_url + url
|
|
451
|
+
if params:
|
|
452
|
+
url += "?" + urllib.parse.urlencode(params)
|
|
453
|
+
|
|
454
|
+
resp = requests.delete(
|
|
455
|
+
url, headers=self._get_headers(), timeout=TIMEOUT_MAXIMUM
|
|
456
|
+
)
|
|
457
|
+
if resp.status_code >= 400:
|
|
458
|
+
print(f"DELETE {url} received {resp.status_code}", resp.content)
|
|
459
|
+
return None
|
|
460
|
+
try:
|
|
461
|
+
return resp.json()
|
|
462
|
+
except JSONDecodeError:
|
|
463
|
+
return None
|