datalab-api 0.3.2__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.2/src/datalab_api.egg-info → datalab_api-0.4.0}/PKG-INFO +2 -4
- {datalab_api-0.3.2 → datalab_api-0.4.0}/pyproject.toml +1 -2
- {datalab_api-0.3.2 → datalab_api-0.4.0}/src/datalab_api/__init__.py +20 -6
- {datalab_api-0.3.2 → datalab_api-0.4.0}/src/datalab_api/_base.py +37 -7
- {datalab_api-0.3.2 → datalab_api-0.4.0/src/datalab_api.egg-info}/PKG-INFO +2 -4
- {datalab_api-0.3.2 → datalab_api-0.4.0}/src/datalab_api.egg-info/SOURCES.txt +0 -1
- {datalab_api-0.3.2 → datalab_api-0.4.0}/src/datalab_api.egg-info/requires.txt +0 -4
- datalab_api-0.3.2/src/datalab_api/helpers.py +0 -60
- {datalab_api-0.3.2 → datalab_api-0.4.0}/LICENSE +0 -0
- {datalab_api-0.3.2 → datalab_api-0.4.0}/README.md +0 -0
- {datalab_api-0.3.2 → datalab_api-0.4.0}/setup.cfg +0 -0
- {datalab_api-0.3.2 → datalab_api-0.4.0}/src/datalab_api/cli.py +0 -0
- {datalab_api-0.3.2 → datalab_api-0.4.0}/src/datalab_api/utils.py +0 -0
- {datalab_api-0.3.2 → datalab_api-0.4.0}/src/datalab_api.egg-info/dependency_links.txt +0 -0
- {datalab_api-0.3.2 → datalab_api-0.4.0}/src/datalab_api.egg-info/entry_points.txt +0 -0
- {datalab_api-0.3.2 → datalab_api-0.4.0}/src/datalab_api.egg-info/top_level.txt +0 -0
- {datalab_api-0.3.2 → 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
|
|
@@ -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,6 +5,8 @@ from collections.abc import Iterable
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Any, Optional, Union
|
|
7
7
|
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
8
10
|
from ._base import BaseDatalabClient, DatalabAPIError, DuplicateItemError, __version__
|
|
9
11
|
|
|
10
12
|
__all__ = ("DatalabClient", "DuplicateItemError", "__version__")
|
|
@@ -147,10 +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:
|
|
151
|
-
self.create_collection(collection_id)
|
|
152
|
-
collection_immutable_id = self.get_collection(collection_id)[0]["immutable_id"]
|
|
153
|
-
except DatalabAPIError:
|
|
152
|
+
except (RuntimeError, DatalabAPIError):
|
|
154
153
|
self.create_collection(collection_id)
|
|
155
154
|
collection_immutable_id = self.get_collection(collection_id)[0]["immutable_id"]
|
|
156
155
|
new_item["collections"] = new_item.get("collections", [])
|
|
@@ -288,6 +287,10 @@ class DatalabClient(BaseDatalabClient):
|
|
|
288
287
|
if not file_path.exists():
|
|
289
288
|
raise FileNotFoundError(f"File {file_path=} does not exist.")
|
|
290
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
|
+
|
|
291
294
|
upload_url = f"{self.datalab_api_url}/upload-file/"
|
|
292
295
|
with open(file_path, "rb") as file:
|
|
293
296
|
files = {"file": (file_path.name, file)}
|
|
@@ -296,6 +299,7 @@ class DatalabClient(BaseDatalabClient):
|
|
|
296
299
|
files=files,
|
|
297
300
|
data={"item_id": item_id, "replace_file": None},
|
|
298
301
|
expected_status=201,
|
|
302
|
+
timeout=upload_timeout,
|
|
299
303
|
)
|
|
300
304
|
return upload
|
|
301
305
|
|
|
@@ -403,8 +407,18 @@ class DatalabClient(BaseDatalabClient):
|
|
|
403
407
|
"save_to_db": True,
|
|
404
408
|
}
|
|
405
409
|
|
|
406
|
-
resp = self._post(blocks_url, json=payload)
|
|
407
|
-
|
|
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
|
|
408
422
|
|
|
409
423
|
def get_collection(self, collection_id: str) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
410
424
|
"""Get a collection with a given ID.
|
|
@@ -4,7 +4,7 @@ import re
|
|
|
4
4
|
import warnings
|
|
5
5
|
from getpass import getpass
|
|
6
6
|
from importlib.metadata import version
|
|
7
|
-
from typing import Any, Optional
|
|
7
|
+
from typing import Any, Optional, Union
|
|
8
8
|
|
|
9
9
|
import httpx
|
|
10
10
|
from rich.logging import RichHandler
|
|
@@ -43,6 +43,9 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
|
|
|
43
43
|
Set to false if you want the script to error if no API key is found in the environment variables,
|
|
44
44
|
rather than prompting for input."""
|
|
45
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
|
+
|
|
46
49
|
info: dict[str, Any] = {}
|
|
47
50
|
"""The `data` response from the `/info` endpoint of the Datalab API."""
|
|
48
51
|
|
|
@@ -226,14 +229,14 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
|
|
|
226
229
|
self._session.close()
|
|
227
230
|
|
|
228
231
|
def _handle_response(
|
|
229
|
-
self, response: httpx.Response, url: str, expected_status: int = 200
|
|
232
|
+
self, response: httpx.Response, url: str, expected_status: Union[int, list[int]] = 200
|
|
230
233
|
) -> dict[str, Any]:
|
|
231
234
|
"""Handle HTTP response with consistent error handling.
|
|
232
235
|
|
|
233
236
|
Args:
|
|
234
237
|
response: The HTTP response object
|
|
235
238
|
url: The URL that was requested
|
|
236
|
-
expected_status: The expected HTTP status code (default: 200)
|
|
239
|
+
expected_status: The expected HTTP status code(s) (default: 200)
|
|
237
240
|
|
|
238
241
|
Returns:
|
|
239
242
|
The JSON response data
|
|
@@ -259,7 +262,10 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
|
|
|
259
262
|
)
|
|
260
263
|
|
|
261
264
|
# Handle other HTTP status code errors
|
|
262
|
-
if
|
|
265
|
+
if isinstance(expected_status, int):
|
|
266
|
+
expected_status = [expected_status]
|
|
267
|
+
|
|
268
|
+
if response.status_code not in expected_status:
|
|
263
269
|
try:
|
|
264
270
|
error_data = response.json()
|
|
265
271
|
error_message = error_data.get("message", str(response.content))
|
|
@@ -321,8 +327,27 @@ This is likely a server-side bug. Please report this issue to the datalab develo
|
|
|
321
327
|
|
|
322
328
|
return data
|
|
323
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
|
+
|
|
324
349
|
def _request(
|
|
325
|
-
self, method: str, url: str, expected_status: int = 200, **kwargs
|
|
350
|
+
self, method: str, url: str, expected_status: Union[int, list[int]] = 200, **kwargs
|
|
326
351
|
) -> dict[str, Any]:
|
|
327
352
|
"""Make an HTTP request with consistent error handling.
|
|
328
353
|
|
|
@@ -335,8 +360,11 @@ This is likely a server-side bug. Please report this issue to the datalab develo
|
|
|
335
360
|
Returns:
|
|
336
361
|
The JSON response data
|
|
337
362
|
"""
|
|
363
|
+
timeout = kwargs.pop("timeout", None)
|
|
338
364
|
try:
|
|
339
|
-
response = self.session.request(
|
|
365
|
+
response = self.session.request(
|
|
366
|
+
method, url, follow_redirects=True, timeout=timeout, **kwargs
|
|
367
|
+
)
|
|
340
368
|
return self._handle_response(response, url, expected_status)
|
|
341
369
|
except (httpx.RequestError, httpx.HTTPStatusError) as e:
|
|
342
370
|
raise DatalabAPIError(f"Request failed for {url}: {e}")
|
|
@@ -345,7 +373,9 @@ This is likely a server-side bug. Please report this issue to the datalab develo
|
|
|
345
373
|
"""Make a GET request with error handling."""
|
|
346
374
|
return self._request("GET", url, **kwargs)
|
|
347
375
|
|
|
348
|
-
def _post(
|
|
376
|
+
def _post(
|
|
377
|
+
self, url: str, expected_status: Union[int, list[int]] = 200, **kwargs
|
|
378
|
+
) -> dict[str, Any]:
|
|
349
379
|
"""Make a POST request with error handling."""
|
|
350
380
|
return self._request("POST", url, expected_status, **kwargs)
|
|
351
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
|
|
@@ -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
|
|
File without changes
|