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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datalab-api
3
- Version: 0.3.2
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
- return resp["new_block_data"]
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 response.status_code != expected_status:
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(method, url, follow_redirects=True, **kwargs)
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(self, url: str, expected_status: int = 200, **kwargs) -> dict[str, Any]:
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.2
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
@@ -4,7 +4,6 @@ pyproject.toml
4
4
  src/datalab_api/__init__.py
5
5
  src/datalab_api/_base.py
6
6
  src/datalab_api/cli.py
7
- src/datalab_api/helpers.py
8
7
  src/datalab_api/utils.py
9
8
  src/datalab_api.egg-info/PKG-INFO
10
9
  src/datalab_api.egg-info/SOURCES.txt
@@ -8,10 +8,6 @@ bokeh~=2.4
8
8
  [all]
9
9
  datalab-api[cli,dev,docs]
10
10
 
11
- [cheminventory-helper]
12
- pandas~=2.2
13
- openpyxl~=3.0
14
-
15
11
  [cli]
16
12
  typer~=0.9
17
13
  click-shell~=2.1
@@ -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