datalab-api 0.3.1__tar.gz → 0.4.0__tar.gz
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.
- {datalab_api-0.3.1/src/datalab_api.egg-info → datalab_api-0.4.0}/PKG-INFO +3 -5
- {datalab_api-0.3.1 → datalab_api-0.4.0}/README.md +1 -1
- {datalab_api-0.3.1 → datalab_api-0.4.0}/pyproject.toml +1 -2
- {datalab_api-0.3.1 → datalab_api-0.4.0}/src/datalab_api/__init__.py +21 -4
- {datalab_api-0.3.1 → datalab_api-0.4.0}/src/datalab_api/_base.py +51 -10
- {datalab_api-0.3.1 → datalab_api-0.4.0/src/datalab_api.egg-info}/PKG-INFO +3 -5
- {datalab_api-0.3.1 → datalab_api-0.4.0}/src/datalab_api.egg-info/SOURCES.txt +0 -1
- {datalab_api-0.3.1 → datalab_api-0.4.0}/src/datalab_api.egg-info/requires.txt +0 -4
- datalab_api-0.3.1/src/datalab_api/helpers.py +0 -60
- {datalab_api-0.3.1 → datalab_api-0.4.0}/LICENSE +0 -0
- {datalab_api-0.3.1 → datalab_api-0.4.0}/setup.cfg +0 -0
- {datalab_api-0.3.1 → datalab_api-0.4.0}/src/datalab_api/cli.py +0 -0
- {datalab_api-0.3.1 → datalab_api-0.4.0}/src/datalab_api/utils.py +0 -0
- {datalab_api-0.3.1 → datalab_api-0.4.0}/src/datalab_api.egg-info/dependency_links.txt +0 -0
- {datalab_api-0.3.1 → datalab_api-0.4.0}/src/datalab_api.egg-info/entry_points.txt +0 -0
- {datalab_api-0.3.1 → datalab_api-0.4.0}/src/datalab_api.egg-info/top_level.txt +0 -0
- {datalab_api-0.3.1 → datalab_api-0.4.0}/tests/test_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datalab-api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A Python API for instances of the datalab data management platform (https://github.com/datalab-org/datalab).
|
|
5
5
|
Author-email: Matthew Evans <matthew@ml-evs.science>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
17
|
Classifier: Intended Audience :: Science/Research
|
|
17
18
|
Classifier: Intended Audience :: Information Technology
|
|
18
19
|
Classifier: Topic :: Other/Nonlisted Topic
|
|
@@ -38,9 +39,6 @@ Requires-Dist: mkdocs-jupyter; extra == "docs"
|
|
|
38
39
|
Provides-Extra: cli
|
|
39
40
|
Requires-Dist: typer~=0.9; extra == "cli"
|
|
40
41
|
Requires-Dist: click-shell~=2.1; extra == "cli"
|
|
41
|
-
Provides-Extra: cheminventory-helper
|
|
42
|
-
Requires-Dist: pandas~=2.2; extra == "cheminventory-helper"
|
|
43
|
-
Requires-Dist: openpyxl~=3.0; extra == "cheminventory-helper"
|
|
44
42
|
Provides-Extra: all
|
|
45
43
|
Requires-Dist: datalab-api[cli,dev,docs]; extra == "all"
|
|
46
44
|
Dynamic: license-file
|
|
@@ -61,7 +59,7 @@ A simple Python API that can interact with [*datalab*](https://github.com/datala
|
|
|
61
59
|
|
|
62
60
|
The idea here is to provide a set of utility functions and models for manipulating samples, cells, inventory, files, users and metadata associated with *datalab* entries in an automated way.
|
|
63
61
|
|
|
64
|
-
This API may not expose all the functionality available in a given *datalab* instance, and the idea would be that this package can support multiple versions of the underlying [*datalab* REST API](https://docs.datalab-org.io
|
|
62
|
+
This API may not expose all the functionality available in a given *datalab* instance, and the idea would be that this package can support multiple versions of the underlying [*datalab* REST API](https://docs.datalab-org.io).
|
|
65
63
|
This means that the API is primarily *functional* in nature, as opposed to object-oriented, since *datalab* instances are free to use their own custom data models.
|
|
66
64
|
The available schemas are reported as instance metadata and in the future object-oriented models may be able to be genereated directly in the client (so e.g., the returned data would be Python objects like `Sample` rather than JSON data).
|
|
67
65
|
|
|
@@ -14,7 +14,7 @@ A simple Python API that can interact with [*datalab*](https://github.com/datala
|
|
|
14
14
|
|
|
15
15
|
The idea here is to provide a set of utility functions and models for manipulating samples, cells, inventory, files, users and metadata associated with *datalab* entries in an automated way.
|
|
16
16
|
|
|
17
|
-
This API may not expose all the functionality available in a given *datalab* instance, and the idea would be that this package can support multiple versions of the underlying [*datalab* REST API](https://docs.datalab-org.io
|
|
17
|
+
This API may not expose all the functionality available in a given *datalab* instance, and the idea would be that this package can support multiple versions of the underlying [*datalab* REST API](https://docs.datalab-org.io).
|
|
18
18
|
This means that the API is primarily *functional* in nature, as opposed to object-oriented, since *datalab* instances are free to use their own custom data models.
|
|
19
19
|
The available schemas are reported as instance metadata and in the future object-oriented models may be able to be genereated directly in the client (so e.g., the returned data would be Python objects like `Sample` rather than JSON data).
|
|
20
20
|
|
|
@@ -16,6 +16,7 @@ classifiers = [
|
|
|
16
16
|
"Programming Language :: Python :: 3.10",
|
|
17
17
|
"Programming Language :: Python :: 3.11",
|
|
18
18
|
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
19
20
|
"Intended Audience :: Science/Research",
|
|
20
21
|
"Intended Audience :: Information Technology",
|
|
21
22
|
"Topic :: Other/Nonlisted Topic",
|
|
@@ -52,8 +53,6 @@ cli = [
|
|
|
52
53
|
"click-shell ~= 2.1", # REPL-like interface
|
|
53
54
|
]
|
|
54
55
|
|
|
55
|
-
cheminventory-helper = ["pandas ~= 2.2", "openpyxl ~= 3.0"]
|
|
56
|
-
|
|
57
56
|
all = ["datalab-api[dev,docs,cli]"]
|
|
58
57
|
|
|
59
58
|
[project.scripts]
|
|
@@ -5,7 +5,9 @@ from collections.abc import Iterable
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Any, Optional, Union
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from ._base import BaseDatalabClient, DatalabAPIError, DuplicateItemError, __version__
|
|
9
11
|
|
|
10
12
|
__all__ = ("DatalabClient", "DuplicateItemError", "__version__")
|
|
11
13
|
|
|
@@ -147,7 +149,7 @@ class DatalabClient(BaseDatalabClient):
|
|
|
147
149
|
if collection_id is not None:
|
|
148
150
|
try:
|
|
149
151
|
collection_immutable_id = self.get_collection(collection_id)[0]["immutable_id"]
|
|
150
|
-
except RuntimeError:
|
|
152
|
+
except (RuntimeError, DatalabAPIError):
|
|
151
153
|
self.create_collection(collection_id)
|
|
152
154
|
collection_immutable_id = self.get_collection(collection_id)[0]["immutable_id"]
|
|
153
155
|
new_item["collections"] = new_item.get("collections", [])
|
|
@@ -285,6 +287,10 @@ class DatalabClient(BaseDatalabClient):
|
|
|
285
287
|
if not file_path.exists():
|
|
286
288
|
raise FileNotFoundError(f"File {file_path=} does not exist.")
|
|
287
289
|
|
|
290
|
+
# Use a longer read timeout for uploads, as the server may do
|
|
291
|
+
# significant processing before responding
|
|
292
|
+
upload_timeout = httpx.Timeout(self._timeout.connect, read=600.0, pool=self._timeout.pool)
|
|
293
|
+
|
|
288
294
|
upload_url = f"{self.datalab_api_url}/upload-file/"
|
|
289
295
|
with open(file_path, "rb") as file:
|
|
290
296
|
files = {"file": (file_path.name, file)}
|
|
@@ -293,6 +299,7 @@ class DatalabClient(BaseDatalabClient):
|
|
|
293
299
|
files=files,
|
|
294
300
|
data={"item_id": item_id, "replace_file": None},
|
|
295
301
|
expected_status=201,
|
|
302
|
+
timeout=upload_timeout,
|
|
296
303
|
)
|
|
297
304
|
return upload
|
|
298
305
|
|
|
@@ -400,8 +407,18 @@ class DatalabClient(BaseDatalabClient):
|
|
|
400
407
|
"save_to_db": True,
|
|
401
408
|
}
|
|
402
409
|
|
|
403
|
-
resp = self._post(blocks_url, json=payload)
|
|
404
|
-
|
|
410
|
+
resp = self._post(blocks_url, expected_status=[200, 202], json=payload)
|
|
411
|
+
|
|
412
|
+
# If block is created synchronously, return the block data
|
|
413
|
+
if "new_block_data" in resp:
|
|
414
|
+
return resp["new_block_data"]
|
|
415
|
+
|
|
416
|
+
# Otherwise, we return the response which may contain information about the asynchronous block creation process
|
|
417
|
+
task_id = resp.get("task_id")
|
|
418
|
+
if task_id:
|
|
419
|
+
self.triggered_block_task_ids.add(task_id)
|
|
420
|
+
|
|
421
|
+
return resp
|
|
405
422
|
|
|
406
423
|
def get_collection(self, collection_id: str) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
407
424
|
"""Get a collection with a given ID.
|
|
@@ -2,8 +2,9 @@ import logging
|
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
4
|
import warnings
|
|
5
|
+
from getpass import getpass
|
|
5
6
|
from importlib.metadata import version
|
|
6
|
-
from typing import Any, Optional
|
|
7
|
+
from typing import Any, Optional, Union
|
|
7
8
|
|
|
8
9
|
import httpx
|
|
9
10
|
from rich.logging import RichHandler
|
|
@@ -37,6 +38,14 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
|
|
|
37
38
|
_headers: dict[str, str] = {}
|
|
38
39
|
_timeout: httpx.Timeout = httpx.Timeout(10.0, read=60.0)
|
|
39
40
|
|
|
41
|
+
interactive: bool = True
|
|
42
|
+
"""Whether the client is being used in an interactive context (e.g., CLI or notebook) or not.
|
|
43
|
+
Set to false if you want the script to error if no API key is found in the environment variables,
|
|
44
|
+
rather than prompting for input."""
|
|
45
|
+
|
|
46
|
+
triggered_block_task_ids: set[str] = set()
|
|
47
|
+
"""A set of block task IDs that have been triggered during the lifetime of this client instance."""
|
|
48
|
+
|
|
40
49
|
info: dict[str, Any] = {}
|
|
41
50
|
"""The `data` response from the `/info` endpoint of the Datalab API."""
|
|
42
51
|
|
|
@@ -189,9 +198,14 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
|
|
|
189
198
|
api_key = api_key.strip("'").strip('"')
|
|
190
199
|
|
|
191
200
|
if api_key is None:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
201
|
+
if self.interactive:
|
|
202
|
+
api_key = getpass(
|
|
203
|
+
f"Enter your API key for {self.datalab_api_url} (input will be hidden): "
|
|
204
|
+
)
|
|
205
|
+
else:
|
|
206
|
+
raise ValueError(
|
|
207
|
+
f"No API key found in environment variables {key_env_var}/<prefix>_{key_env_var}."
|
|
208
|
+
)
|
|
195
209
|
|
|
196
210
|
self._api_key = api_key
|
|
197
211
|
|
|
@@ -215,14 +229,14 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
|
|
|
215
229
|
self._session.close()
|
|
216
230
|
|
|
217
231
|
def _handle_response(
|
|
218
|
-
self, response: httpx.Response, url: str, expected_status: int = 200
|
|
232
|
+
self, response: httpx.Response, url: str, expected_status: Union[int, list[int]] = 200
|
|
219
233
|
) -> dict[str, Any]:
|
|
220
234
|
"""Handle HTTP response with consistent error handling.
|
|
221
235
|
|
|
222
236
|
Args:
|
|
223
237
|
response: The HTTP response object
|
|
224
238
|
url: The URL that was requested
|
|
225
|
-
expected_status: The expected HTTP status code (default: 200)
|
|
239
|
+
expected_status: The expected HTTP status code(s) (default: 200)
|
|
226
240
|
|
|
227
241
|
Returns:
|
|
228
242
|
The JSON response data
|
|
@@ -248,7 +262,10 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
|
|
|
248
262
|
)
|
|
249
263
|
|
|
250
264
|
# Handle other HTTP status code errors
|
|
251
|
-
if
|
|
265
|
+
if isinstance(expected_status, int):
|
|
266
|
+
expected_status = [expected_status]
|
|
267
|
+
|
|
268
|
+
if response.status_code not in expected_status:
|
|
252
269
|
try:
|
|
253
270
|
error_data = response.json()
|
|
254
271
|
error_message = error_data.get("message", str(response.content))
|
|
@@ -310,8 +327,27 @@ This is likely a server-side bug. Please report this issue to the datalab develo
|
|
|
310
327
|
|
|
311
328
|
return data
|
|
312
329
|
|
|
330
|
+
def check_tasks(self) -> tuple[set[str], set[str]]:
|
|
331
|
+
"""Check the status of any triggered tasks, returning any that have completed."""
|
|
332
|
+
completed_tasks = set()
|
|
333
|
+
error_tasks = set()
|
|
334
|
+
|
|
335
|
+
for task_id in self.triggered_block_task_ids:
|
|
336
|
+
try:
|
|
337
|
+
response = self._get(f"{self.datalab_api_url}/blocks/{task_id}/status")
|
|
338
|
+
task_status = response["status"]
|
|
339
|
+
if task_status == "ready":
|
|
340
|
+
completed_tasks.add(task_id)
|
|
341
|
+
except DatalabAPIError:
|
|
342
|
+
error_tasks.add(task_id)
|
|
343
|
+
|
|
344
|
+
self.triggered_block_task_ids -= completed_tasks
|
|
345
|
+
self.triggered_block_task_ids -= error_tasks
|
|
346
|
+
|
|
347
|
+
return completed_tasks, error_tasks
|
|
348
|
+
|
|
313
349
|
def _request(
|
|
314
|
-
self, method: str, url: str, expected_status: int = 200, **kwargs
|
|
350
|
+
self, method: str, url: str, expected_status: Union[int, list[int]] = 200, **kwargs
|
|
315
351
|
) -> dict[str, Any]:
|
|
316
352
|
"""Make an HTTP request with consistent error handling.
|
|
317
353
|
|
|
@@ -324,8 +360,11 @@ This is likely a server-side bug. Please report this issue to the datalab develo
|
|
|
324
360
|
Returns:
|
|
325
361
|
The JSON response data
|
|
326
362
|
"""
|
|
363
|
+
timeout = kwargs.pop("timeout", None)
|
|
327
364
|
try:
|
|
328
|
-
response = self.session.request(
|
|
365
|
+
response = self.session.request(
|
|
366
|
+
method, url, follow_redirects=True, timeout=timeout, **kwargs
|
|
367
|
+
)
|
|
329
368
|
return self._handle_response(response, url, expected_status)
|
|
330
369
|
except (httpx.RequestError, httpx.HTTPStatusError) as e:
|
|
331
370
|
raise DatalabAPIError(f"Request failed for {url}: {e}")
|
|
@@ -334,7 +373,9 @@ This is likely a server-side bug. Please report this issue to the datalab develo
|
|
|
334
373
|
"""Make a GET request with error handling."""
|
|
335
374
|
return self._request("GET", url, **kwargs)
|
|
336
375
|
|
|
337
|
-
def _post(
|
|
376
|
+
def _post(
|
|
377
|
+
self, url: str, expected_status: Union[int, list[int]] = 200, **kwargs
|
|
378
|
+
) -> dict[str, Any]:
|
|
338
379
|
"""Make a POST request with error handling."""
|
|
339
380
|
return self._request("POST", url, expected_status, **kwargs)
|
|
340
381
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datalab-api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A Python API for instances of the datalab data management platform (https://github.com/datalab-org/datalab).
|
|
5
5
|
Author-email: Matthew Evans <matthew@ml-evs.science>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
17
|
Classifier: Intended Audience :: Science/Research
|
|
17
18
|
Classifier: Intended Audience :: Information Technology
|
|
18
19
|
Classifier: Topic :: Other/Nonlisted Topic
|
|
@@ -38,9 +39,6 @@ Requires-Dist: mkdocs-jupyter; extra == "docs"
|
|
|
38
39
|
Provides-Extra: cli
|
|
39
40
|
Requires-Dist: typer~=0.9; extra == "cli"
|
|
40
41
|
Requires-Dist: click-shell~=2.1; extra == "cli"
|
|
41
|
-
Provides-Extra: cheminventory-helper
|
|
42
|
-
Requires-Dist: pandas~=2.2; extra == "cheminventory-helper"
|
|
43
|
-
Requires-Dist: openpyxl~=3.0; extra == "cheminventory-helper"
|
|
44
42
|
Provides-Extra: all
|
|
45
43
|
Requires-Dist: datalab-api[cli,dev,docs]; extra == "all"
|
|
46
44
|
Dynamic: license-file
|
|
@@ -61,7 +59,7 @@ A simple Python API that can interact with [*datalab*](https://github.com/datala
|
|
|
61
59
|
|
|
62
60
|
The idea here is to provide a set of utility functions and models for manipulating samples, cells, inventory, files, users and metadata associated with *datalab* entries in an automated way.
|
|
63
61
|
|
|
64
|
-
This API may not expose all the functionality available in a given *datalab* instance, and the idea would be that this package can support multiple versions of the underlying [*datalab* REST API](https://docs.datalab-org.io
|
|
62
|
+
This API may not expose all the functionality available in a given *datalab* instance, and the idea would be that this package can support multiple versions of the underlying [*datalab* REST API](https://docs.datalab-org.io).
|
|
65
63
|
This means that the API is primarily *functional* in nature, as opposed to object-oriented, since *datalab* instances are free to use their own custom data models.
|
|
66
64
|
The available schemas are reported as instance metadata and in the future object-oriented models may be able to be genereated directly in the client (so e.g., the returned data would be Python objects like `Sample` rather than JSON data).
|
|
67
65
|
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import random
|
|
2
|
-
import warnings
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
from datalab_api import DatalabClient, DuplicateItemError
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def import_cheminventory(filename: Path, client: DatalabClient):
|
|
9
|
-
"""Helper function to import a ChemInventory xlsx export into datalab, using the
|
|
10
|
-
'starting_materials' item type.
|
|
11
|
-
|
|
12
|
-
Migrated from the original version in the main datalab repository:
|
|
13
|
-
https://github.com/the-grey-group/datalab/blob/43764fb494c2cc1bf9f7dc90c25594aeb79d5767/pydatalab/tasks.py#L350-L413
|
|
14
|
-
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
warnings.warn(
|
|
18
|
-
DeprecationWarning(
|
|
19
|
-
"This helper is deprecated in favour of the full-fledged plugin at: https://github.com/datalab-industries/datalab-cheminventory-plugin"
|
|
20
|
-
)
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
def _generate_random_startingmaterial_id():
|
|
24
|
-
"""Generate 'XX' + a random 15-length string for use as an id for starting materials
|
|
25
|
-
that don't have a barcode.
|
|
26
|
-
"""
|
|
27
|
-
yield "".join(["XX"] + random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=15))
|
|
28
|
-
|
|
29
|
-
try:
|
|
30
|
-
import pandas as pd
|
|
31
|
-
|
|
32
|
-
inventory_df = pd.read_excel(filename)
|
|
33
|
-
except ImportError as exc:
|
|
34
|
-
raise ImportError(
|
|
35
|
-
"Please install pandas + openpyxl to use this helper function, via `pip install datalab-api[cheminventory-helper]`"
|
|
36
|
-
) from exc
|
|
37
|
-
inventory_df["type"] = "starting_materials"
|
|
38
|
-
inventory_df["item_id"] = inventory_df["Barcode"]
|
|
39
|
-
# Fill missing barcodes
|
|
40
|
-
inventory_df["item_id"] = inventory_df["item_id"].fillna(
|
|
41
|
-
inventory_df["item_id"].apply(lambda _: next(_generate_random_startingmaterial_id()))
|
|
42
|
-
)
|
|
43
|
-
inventory_df["Molecular Weight"] = inventory_df["Molecular Weight"].replace(" ", float("nan"))
|
|
44
|
-
|
|
45
|
-
counts = {"success": 0, "duplicate": 0, "failed": 0}
|
|
46
|
-
|
|
47
|
-
for item in inventory_df.to_dict(orient="records"):
|
|
48
|
-
try:
|
|
49
|
-
client.create_item(
|
|
50
|
-
item["item_id"], item["type"], item, collection_id="jul24-inventory-import"
|
|
51
|
-
)
|
|
52
|
-
counts["success"] += 1
|
|
53
|
-
except DuplicateItemError:
|
|
54
|
-
counts["duplicate"] += 1
|
|
55
|
-
except Exception as exc:
|
|
56
|
-
counts["failed"] += 1
|
|
57
|
-
print(f"Failed to import item: {item}. Error: {exc}")
|
|
58
|
-
continue
|
|
59
|
-
|
|
60
|
-
print(f"Done: {counts=}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|