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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datalab-api
3
- Version: 0.3.1
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/en/stable/rest_api/).
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/en/stable/rest_api/).
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
- from ._base import BaseDatalabClient, DuplicateItemError, __version__
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
- 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
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
- raise ValueError(
193
- f"No API key found in environment variables {key_env_var}/<prefix>_{key_env_var}."
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 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:
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(method, url, follow_redirects=True, **kwargs)
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(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]:
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.1
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/en/stable/rest_api/).
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
 
@@ -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