datalab-api 0.2.3__tar.gz → 0.2.4__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.1
2
2
  Name: datalab-api
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: A Python API for instances of the datalab data management platform (https://github.com/the-grey-group/datalab).
5
5
  Author-email: Matthew Evans <matthew@ml-evs.science>
6
6
  License: MIT
@@ -38,6 +38,9 @@ Requires-Dist: mkdocs-jupyter; extra == "docs"
38
38
  Provides-Extra: cli
39
39
  Requires-Dist: typer~=0.9; extra == "cli"
40
40
  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"
41
44
  Provides-Extra: all
42
45
  Requires-Dist: datalab-api[cli,dev,docs]; extra == "all"
43
46
 
@@ -50,6 +50,11 @@ cli = [
50
50
  "click-shell ~= 2.1", # REPL-like interface
51
51
  ]
52
52
 
53
+ cheminventory-helper = [
54
+ "pandas ~= 2.2",
55
+ "openpyxl ~= 3.0"
56
+ ]
57
+
53
58
  all = [
54
59
  "datalab-api[dev,docs,cli]"
55
60
  ]
@@ -75,13 +80,96 @@ follow_imports = "skip"
75
80
 
76
81
  [tool.ruff]
77
82
  line-length = 100
78
- target-version = "py310"
83
+ target-version = "py39"
79
84
 
80
85
  [tool.ruff.lint]
81
- select = ["E", "F", "I", "W", "Q"]
82
- ignore = ["E501", "E402"]
86
+ select = ["ALL"]
87
+ ignore = [
88
+ "A001",
89
+ "A002",
90
+ "ANN", # TODO fix all ANN errors
91
+ "ARG", # TODO fix unused method argument
92
+ "B007",
93
+ "B028",
94
+ "B904",
95
+ "BLE001",
96
+ "C408", # Unnecessary (dict/list/tuple) call - remove call
97
+ "C416",
98
+ "C419",
99
+ "C901", # function too complex
100
+ "COM812", # trailing comma missing
101
+ "D",
102
+ "D205",
103
+ "DTZ", # datetime-tz-now
104
+ "E501", # TODO fix line too long
105
+ "EM", # exception message must not use f-string literal
106
+ "ERA001", # found commented out code
107
+ "FA100", # TODO fix FA errors
108
+ "FBT001",
109
+ "FBT002",
110
+ "FBT003",
111
+ "FIX002",
112
+ "G004", # logging uses fstring
113
+ "G201",
114
+ "INP001",
115
+ "ISC001",
116
+ "N802", # TODO maybe fix these
117
+ "N805",
118
+ "N818",
119
+ "PD011", # pandas-use-of-dot-values
120
+ "PERF102",
121
+ "PERF203", # try-except-in-loop
122
+ "PERF401",
123
+ "PGH003",
124
+ "PGH004",
125
+ "PLR", # pylint-refactor
126
+ "PLW0602",
127
+ "PLW0603",
128
+ "PLW2901",
129
+ "PT003",
130
+ "PT004", # pytest-missing-fixture-name-underscore
131
+ "PT006", # pytest-parametrize-names-wrong-type
132
+ "PT013", # pytest-incorrect-pytest-import
133
+ "PTH", # prefer Pathlib to os.path
134
+ "PYI024",
135
+ "RET",
136
+ "RET504",
137
+ "RUF005",
138
+ "RUF012",
139
+ "RUF013", # implicit-optional
140
+ "RUF015",
141
+ "S106",
142
+ "S110",
143
+ "S112",
144
+ "S311",
145
+ "S324", # use of insecure hash function
146
+ "S507", # paramiko auto trust
147
+ "S602",
148
+ "S603",
149
+ "S607",
150
+ "SIM102",
151
+ "SIM105",
152
+ "SIM108",
153
+ "SIM117",
154
+ "SLF", # private member accessed outside class
155
+ "SLOT000",
156
+ "T201",
157
+ "TCH",
158
+ "TD", # TODOs
159
+ "TRY", # long message outside exception class
160
+ "UP031",
161
+ ]
162
+ pydocstyle.convention = "numpy"
163
+ isort.known-first-party = ["jobflow_remote"]
164
+ isort.split-on-trailing-comma = false
83
165
  fixable = ["A", "B", "C", "D", "E", "F", "I"]
84
166
  unfixable = []
85
- per-file-ignores = {}
86
167
  # Allow unused variables when underscore-prefixed.
87
168
  dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
169
+
170
+ [tool.ruff.format]
171
+ docstring-code-format = true
172
+
173
+ [tool.ruff.lint.per-file-ignores]
174
+ "__init__.py" = ["F401"]
175
+ "**/tests/*" = ["INP001", "S101"]
@@ -1,4 +1,5 @@
1
1
  import warnings
2
+ from collections.abc import Iterable
2
3
  from pathlib import Path
3
4
  from typing import Any, Optional, Union
4
5
 
@@ -7,6 +8,10 @@ from ._base import BaseDatalabClient, __version__
7
8
  __all__ = ("__version__", "DatalabClient")
8
9
 
9
10
 
11
+ class DuplicateItemError(ValueError):
12
+ """Raised when the API operation would create a duplicate item."""
13
+
14
+
10
15
  class DatalabClient(BaseDatalabClient):
11
16
  """A client for the Datalab API.
12
17
 
@@ -66,7 +71,7 @@ class DatalabClient(BaseDatalabClient):
66
71
  return items[item_type]
67
72
 
68
73
  def search_items(
69
- self, query: str, item_types: Union[list[str], str] = ["samples", "cells"]
74
+ self, query: str, item_types: Union[Iterable[str], str] = ("samples", "cells")
70
75
  ) -> list[dict[str, Any]]:
71
76
  """Search for items of the given types that match the query.
72
77
 
@@ -96,7 +101,11 @@ class DatalabClient(BaseDatalabClient):
96
101
  return items["items"]
97
102
 
98
103
  def create_item(
99
- self, item_id: str, item_type: str, item_data: Optional[dict[str, Any]] = None
104
+ self,
105
+ item_id: str,
106
+ item_type: str,
107
+ item_data: Optional[dict[str, Any]] = None,
108
+ collection_id: Optional[str] = None,
100
109
  ) -> dict[str, Any]:
101
110
  """Create an item with a given ID and item data.
102
111
 
@@ -104,12 +113,22 @@ class DatalabClient(BaseDatalabClient):
104
113
  item_id: The ID of the item to create.
105
114
  item_type: The type of item to create, e.g., 'samples', 'cells'.
106
115
  item_data: The data for the item.
116
+ collection_id: The ID of the collection to add the item to (optional).
117
+ If such a collection does not exist, one will be made.
107
118
 
108
119
  """
109
120
  new_item = {}
110
121
  if item_data is not None:
111
122
  new_item = item_data
112
- new_item.update(**{"item_id": item_id, "type": item_type})
123
+ new_item.update({"item_id": item_id, "type": item_type})
124
+
125
+ if collection_id is not None:
126
+ try:
127
+ collection_immutable_id = self.get_collection(collection_id)["immutable_id"]
128
+ except RuntimeError:
129
+ collection_immutable_id = self.create_collection(collection_id)["immutable_id"]
130
+ new_item["collections"] = new_item.get("collections", [])
131
+ new_item["collections"].append({"immutable_id": collection_immutable_id})
113
132
 
114
133
  create_item_url = f"{self.datalab_api_url}/new-sample/"
115
134
  create_item_resp = self.session.post(
@@ -119,16 +138,18 @@ class DatalabClient(BaseDatalabClient):
119
138
  )
120
139
  try:
121
140
  created_item = create_item_resp.json()
122
- if created_item["status"] != "success":
123
- raise RuntimeError(
124
- f"Failed to create item at {create_item_url}: {created_item['status']!r}."
141
+ if create_item_resp.status_code == 409:
142
+ raise DuplicateItemError(
143
+ f"Item {item_id=} already exists at {create_item_url}: {created_item['status']!r}."
125
144
  )
145
+ if created_item["status"] != "success":
146
+ raise RuntimeError(f"Failed to create item at {create_item_url}: {created_item}.")
126
147
  return created_item["sample_list_entry"]
127
148
 
128
- except Exception:
129
- raise RuntimeError(
130
- f"Failed to create item {item_id=} with data {item_data=} at {create_item_url}: {create_item_resp.status_code=}. Check the item information is correct."
131
- )
149
+ except Exception as exc:
150
+ raise exc.__class__(
151
+ f"Failed to create item {item_id=} with data {item_data=} at {create_item_url}: {create_item_resp.status_code=}, {create_item_resp.content}. Check the item information is correct."
152
+ ) from exc
132
153
 
133
154
  def update_item(self, item_id: str, item_data: dict[str, Any]) -> dict[str, Any]:
134
155
  """Update an item with the given item data.
@@ -390,3 +411,57 @@ class DatalabClient(BaseDatalabClient):
390
411
  raise RuntimeError(f"Failed to update block {block_type=}:\n{resp.text}")
391
412
 
392
413
  return resp.json()["new_block_data"]
414
+
415
+ def get_collection(self, collection_id: str) -> dict[str, Any]:
416
+ """Get a collection with a given ID.
417
+
418
+ Parameters:
419
+ collection_id: The ID of the collection to search for.
420
+
421
+ Returns:
422
+ A dictionary of collection data for the collection with the given ID.
423
+
424
+ """
425
+ collection_url = f"{self.datalab_api_url}/collections/{collection_id}"
426
+ collection_resp = self.session.get(collection_url, follow_redirects=True)
427
+ if collection_resp.status_code != 200:
428
+ raise RuntimeError(
429
+ f"Failed to find collection {collection_id=} at {collection_url}: {collection_resp.status_code=}. Check the collection ID is correct."
430
+ )
431
+ collection = collection_resp.json()
432
+ if collection["status"] != "success":
433
+ raise RuntimeError(
434
+ f"Failed to get collection at {collection_url}: {collection['status']!r}."
435
+ )
436
+ return collection["data"]
437
+
438
+ def create_collection(
439
+ self, collection_id: str, collection_data: Optional[dict] = None
440
+ ) -> dict[str, Any]:
441
+ """Create a collection with a given ID and collection data.
442
+
443
+ Parameters:
444
+ collection_id: The ID of the collection to create.
445
+ collection_data: The data for the collection.
446
+
447
+ """
448
+ collection_url = f"{self.datalab_api_url}/collections"
449
+
450
+ new_collection = {}
451
+ if collection_data is not None:
452
+ new_collection = collection_data
453
+ new_collection.update({"collection_id": collection_id, "type": "collections"})
454
+
455
+ collection_resp = self.session.put(
456
+ collection_url,
457
+ json={"data": new_collection},
458
+ follow_redirects=True,
459
+ )
460
+
461
+ if collection_resp.status_code != 201 or collection_resp.json()["status"] != "success":
462
+ raise RuntimeError(
463
+ f"Failed to create collection {collection_id=} at {collection_url}: {collection_resp.status_code=}. Check the collection information is correct: {collection_resp.content}"
464
+ )
465
+
466
+ created_collection = collection_resp.json()["data"]
467
+ return created_collection
@@ -76,10 +76,7 @@ def bokeh_from_json(block_data, show=True):
76
76
  from bokeh.io import curdoc
77
77
  from bokeh.plotting import show as bokeh_show
78
78
 
79
- if "bokeh_plot_data" in block_data:
80
- bokeh_plot_data = block_data["bokeh_plot_data"]
81
- else:
82
- bokeh_plot_data = block_data
79
+ bokeh_plot_data = block_data.get("bokeh_plot_data", block_data)
83
80
  curdoc().replace_with_json(bokeh_plot_data["doc"])
84
81
  if show:
85
82
  bokeh_show(curdoc().roots[0])
@@ -0,0 +1,51 @@
1
+ import random
2
+ from pathlib import Path
3
+
4
+ from datalab_api import DatalabClient, DuplicateItemError
5
+
6
+
7
+ def import_cheminventory(filename: Path, client: DatalabClient):
8
+ """Helper function to import a ChemInventory xlsx export into datalab, using the
9
+ 'starting_materials' item type.
10
+
11
+ Migrated from the original version in the main datalab repository:
12
+ https://github.com/the-grey-group/datalab/blob/43764fb494c2cc1bf9f7dc90c25594aeb79d5767/pydatalab/tasks.py#L350-L413
13
+
14
+ """
15
+
16
+ def _generate_random_startingmaterial_id():
17
+ """Generate 'XX' + a random 15-length string for use as an id for starting materials
18
+ that don't have a barcode.
19
+ """
20
+ yield "".join(["XX"] + random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=15))
21
+
22
+ try:
23
+ import pandas as pd
24
+
25
+ inventory_df = pd.read_excel(filename)
26
+ except ImportError as exc:
27
+ raise ImportError(
28
+ "Please install pandas + openpyxl to use this helper function, via `pip install datalab-api[cheminventory-helper]`"
29
+ ) from exc
30
+ inventory_df["type"] = "starting_materials"
31
+ inventory_df["item_id"] = inventory_df["Barcode"]
32
+ # Fill missing barcodes
33
+ inventory_df["item_id"] = inventory_df["item_id"].fillna(
34
+ inventory_df["item_id"].apply(lambda _: _generate_random_startingmaterial_id())
35
+ )
36
+ inventory_df["Molecular Weight"] = inventory_df["Molecular Weight"].replace(" ", float("nan"))
37
+
38
+ inventory_df = inventory_df.dropna()
39
+
40
+ counts = {"success": 0, "duplicate": 0}
41
+
42
+ for item in inventory_df.to_dict(orient="records"):
43
+ try:
44
+ client.create_item(
45
+ item["item_id"], item["type"], item, collection_id="jul24-inventory-import"
46
+ )
47
+ counts["success"] += 1
48
+ except DuplicateItemError:
49
+ counts["duplicate"] += 1
50
+
51
+ print(f"Done: {counts=}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: datalab-api
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: A Python API for instances of the datalab data management platform (https://github.com/the-grey-group/datalab).
5
5
  Author-email: Matthew Evans <matthew@ml-evs.science>
6
6
  License: MIT
@@ -38,6 +38,9 @@ Requires-Dist: mkdocs-jupyter; extra == "docs"
38
38
  Provides-Extra: cli
39
39
  Requires-Dist: typer~=0.9; extra == "cli"
40
40
  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"
41
44
  Provides-Extra: all
42
45
  Requires-Dist: datalab-api[cli,dev,docs]; extra == "all"
43
46
 
@@ -4,6 +4,7 @@ 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
7
8
  src/datalab_api.egg-info/PKG-INFO
8
9
  src/datalab_api.egg-info/SOURCES.txt
9
10
  src/datalab_api.egg-info/dependency_links.txt
@@ -5,6 +5,10 @@ rich~=13.0
5
5
  [all]
6
6
  datalab-api[cli,dev,docs]
7
7
 
8
+ [cheminventory-helper]
9
+ pandas~=2.2
10
+ openpyxl~=3.0
11
+
8
12
  [cli]
9
13
  typer~=0.9
10
14
  click-shell~=2.1
File without changes
File without changes
File without changes