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.
- {datalab_api-0.2.3/src/datalab_api.egg-info → datalab_api-0.2.4}/PKG-INFO +4 -1
- {datalab_api-0.2.3 → datalab_api-0.2.4}/pyproject.toml +92 -4
- {datalab_api-0.2.3 → datalab_api-0.2.4}/src/datalab_api/__init__.py +85 -10
- {datalab_api-0.2.3 → datalab_api-0.2.4}/src/datalab_api/_base.py +1 -4
- datalab_api-0.2.4/src/datalab_api/helpers.py +51 -0
- {datalab_api-0.2.3 → datalab_api-0.2.4/src/datalab_api.egg-info}/PKG-INFO +4 -1
- {datalab_api-0.2.3 → datalab_api-0.2.4}/src/datalab_api.egg-info/SOURCES.txt +1 -0
- {datalab_api-0.2.3 → datalab_api-0.2.4}/src/datalab_api.egg-info/requires.txt +4 -0
- {datalab_api-0.2.3 → datalab_api-0.2.4}/LICENSE +0 -0
- {datalab_api-0.2.3 → datalab_api-0.2.4}/README.md +0 -0
- {datalab_api-0.2.3 → datalab_api-0.2.4}/setup.cfg +0 -0
- {datalab_api-0.2.3 → datalab_api-0.2.4}/src/datalab_api/cli.py +0 -0
- {datalab_api-0.2.3 → datalab_api-0.2.4}/src/datalab_api.egg-info/dependency_links.txt +0 -0
- {datalab_api-0.2.3 → datalab_api-0.2.4}/src/datalab_api.egg-info/entry_points.txt +0 -0
- {datalab_api-0.2.3 → datalab_api-0.2.4}/src/datalab_api.egg-info/top_level.txt +0 -0
- {datalab_api-0.2.3 → datalab_api-0.2.4}/tests/test_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: datalab-api
|
|
3
|
-
Version: 0.2.
|
|
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 = "
|
|
83
|
+
target-version = "py39"
|
|
79
84
|
|
|
80
85
|
[tool.ruff.lint]
|
|
81
|
-
select = ["
|
|
82
|
-
ignore = [
|
|
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[
|
|
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,
|
|
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(
|
|
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
|
|
123
|
-
raise
|
|
124
|
-
f"
|
|
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
|
|
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
|
-
|
|
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
|
+
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|