datalab-api 0.3.2__tar.gz → 0.5.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.5.0}/PKG-INFO +2 -4
- {datalab_api-0.3.2 → datalab_api-0.5.0}/pyproject.toml +1 -2
- {datalab_api-0.3.2 → datalab_api-0.5.0}/src/datalab_api/__init__.py +83 -14
- {datalab_api-0.3.2 → datalab_api-0.5.0}/src/datalab_api/_base.py +39 -9
- {datalab_api-0.3.2 → datalab_api-0.5.0/src/datalab_api.egg-info}/PKG-INFO +2 -4
- {datalab_api-0.3.2 → datalab_api-0.5.0}/src/datalab_api.egg-info/SOURCES.txt +0 -1
- {datalab_api-0.3.2 → datalab_api-0.5.0}/src/datalab_api.egg-info/requires.txt +0 -4
- {datalab_api-0.3.2 → datalab_api-0.5.0}/tests/test_client.py +13 -0
- datalab_api-0.3.2/src/datalab_api/helpers.py +0 -60
- {datalab_api-0.3.2 → datalab_api-0.5.0}/LICENSE +0 -0
- {datalab_api-0.3.2 → datalab_api-0.5.0}/README.md +0 -0
- {datalab_api-0.3.2 → datalab_api-0.5.0}/setup.cfg +0 -0
- {datalab_api-0.3.2 → datalab_api-0.5.0}/src/datalab_api/cli.py +0 -0
- {datalab_api-0.3.2 → datalab_api-0.5.0}/src/datalab_api/utils.py +0 -0
- {datalab_api-0.3.2 → datalab_api-0.5.0}/src/datalab_api.egg-info/dependency_links.txt +0 -0
- {datalab_api-0.3.2 → datalab_api-0.5.0}/src/datalab_api.egg-info/entry_points.txt +0 -0
- {datalab_api-0.3.2 → datalab_api-0.5.0}/src/datalab_api.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datalab-api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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__")
|
|
@@ -125,6 +127,8 @@ class DatalabClient(BaseDatalabClient):
|
|
|
125
127
|
item_type: str = "samples",
|
|
126
128
|
item_data: dict[str, Any] | None = None,
|
|
127
129
|
collection_id: str | None = None,
|
|
130
|
+
collection_ids: str | Iterable[str] | None = None, # alias for collection_id
|
|
131
|
+
group_ids: str | Iterable[str] | None = None,
|
|
128
132
|
) -> dict[str, Any]:
|
|
129
133
|
"""Create an item with a given ID and item data.
|
|
130
134
|
|
|
@@ -133,7 +137,9 @@ class DatalabClient(BaseDatalabClient):
|
|
|
133
137
|
item_type: The type of item to create, e.g., 'samples', 'cells'.
|
|
134
138
|
item_data: The data for the item.
|
|
135
139
|
collection_id: The ID of the collection to add the item to (optional).
|
|
136
|
-
If such a collection does not exist, one will be made.
|
|
140
|
+
If such a collection does not exist, one will be made. Deprecated in favor of `collection_ids`.
|
|
141
|
+
collection_ids: The ID or IDs of the collection(s) to add the item to (optional).
|
|
142
|
+
group_ids: The ID or IDs of the group(s) to share the item with (optional).
|
|
137
143
|
|
|
138
144
|
"""
|
|
139
145
|
new_item = {}
|
|
@@ -145,16 +151,34 @@ class DatalabClient(BaseDatalabClient):
|
|
|
145
151
|
new_item.pop("item_id")
|
|
146
152
|
|
|
147
153
|
if collection_id is not None:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
warnings.warn(
|
|
155
|
+
DeprecationWarning(
|
|
156
|
+
"`collection_id` is deprecated, please use `collection_ids` instead."
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
if collection_ids is None:
|
|
160
|
+
collection_ids = [collection_id]
|
|
161
|
+
|
|
162
|
+
if collection_ids:
|
|
156
163
|
new_item["collections"] = new_item.get("collections", [])
|
|
157
|
-
|
|
164
|
+
for collection_id in collection_ids:
|
|
165
|
+
try:
|
|
166
|
+
collection_immutable_id = self.get_collection(collection_id)[0]["immutable_id"]
|
|
167
|
+
except (RuntimeError, DatalabAPIError):
|
|
168
|
+
self.create_collection(collection_id)
|
|
169
|
+
collection_immutable_id = self.get_collection(collection_id)[0]["immutable_id"]
|
|
170
|
+
new_item["collections"].append({"immutable_id": collection_immutable_id})
|
|
171
|
+
|
|
172
|
+
if group_ids:
|
|
173
|
+
# For now, we have to use the search groups endpoint
|
|
174
|
+
# until we add a get group by id endpoint that is locked down per user
|
|
175
|
+
new_item["groups"] = new_item.get("groups", [])
|
|
176
|
+
for group_id in group_ids:
|
|
177
|
+
try:
|
|
178
|
+
group_immutable_id = self.get_group(group_id)["immutable_id"]
|
|
179
|
+
new_item["groups"].append({"immutable_id": group_immutable_id})
|
|
180
|
+
except (RuntimeError, DatalabAPIError):
|
|
181
|
+
pass
|
|
158
182
|
|
|
159
183
|
create_item_url = f"{self.datalab_api_url}/new-sample/"
|
|
160
184
|
created_item = self._post(
|
|
@@ -272,12 +296,16 @@ class DatalabClient(BaseDatalabClient):
|
|
|
272
296
|
block = self._post(block_url, json=block_request)
|
|
273
297
|
return block["new_block_data"]
|
|
274
298
|
|
|
275
|
-
def upload_file(
|
|
299
|
+
def upload_file(
|
|
300
|
+
self, item_id: str, file_path: Path | str, replace_file_id: str | None = None
|
|
301
|
+
) -> dict[str, Any]:
|
|
276
302
|
"""Upload a file to an item with a given ID.
|
|
277
303
|
|
|
278
304
|
Parameters:
|
|
279
305
|
item_id: The ID of the item to upload the file to.
|
|
280
306
|
file_path: The path to the file to upload.
|
|
307
|
+
replace_file_id: The ID of an existing file to replace with the uploaded file, if any.
|
|
308
|
+
If provided, the new file will take the place of the old file in any blocks it was attached to.
|
|
281
309
|
|
|
282
310
|
Returns:
|
|
283
311
|
A dictionary of the uploaded file data.
|
|
@@ -288,14 +316,19 @@ class DatalabClient(BaseDatalabClient):
|
|
|
288
316
|
if not file_path.exists():
|
|
289
317
|
raise FileNotFoundError(f"File {file_path=} does not exist.")
|
|
290
318
|
|
|
319
|
+
# Use a longer read timeout for uploads, as the server may do
|
|
320
|
+
# significant processing before responding
|
|
321
|
+
upload_timeout = httpx.Timeout(self._timeout.connect, read=600.0, pool=self._timeout.pool)
|
|
322
|
+
|
|
291
323
|
upload_url = f"{self.datalab_api_url}/upload-file/"
|
|
292
324
|
with open(file_path, "rb") as file:
|
|
293
325
|
files = {"file": (file_path.name, file)}
|
|
294
326
|
upload = self._post(
|
|
295
327
|
upload_url,
|
|
296
328
|
files=files,
|
|
297
|
-
data={"item_id": item_id, "replace_file":
|
|
329
|
+
data={"item_id": item_id, "replace_file": replace_file_id},
|
|
298
330
|
expected_status=201,
|
|
331
|
+
timeout=upload_timeout,
|
|
299
332
|
)
|
|
300
333
|
return upload
|
|
301
334
|
|
|
@@ -403,8 +436,18 @@ class DatalabClient(BaseDatalabClient):
|
|
|
403
436
|
"save_to_db": True,
|
|
404
437
|
}
|
|
405
438
|
|
|
406
|
-
resp = self._post(blocks_url, json=payload)
|
|
407
|
-
|
|
439
|
+
resp = self._post(blocks_url, expected_status=[200, 202], json=payload)
|
|
440
|
+
|
|
441
|
+
# If block is created synchronously, return the block data
|
|
442
|
+
if "new_block_data" in resp:
|
|
443
|
+
return resp["new_block_data"]
|
|
444
|
+
|
|
445
|
+
# Otherwise, we return the response which may contain information about the asynchronous block creation process
|
|
446
|
+
task_id = resp.get("task_id")
|
|
447
|
+
if task_id:
|
|
448
|
+
self.triggered_block_task_ids.add(task_id)
|
|
449
|
+
|
|
450
|
+
return resp
|
|
408
451
|
|
|
409
452
|
def get_collection(self, collection_id: str) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
410
453
|
"""Get a collection with a given ID.
|
|
@@ -421,6 +464,32 @@ class DatalabClient(BaseDatalabClient):
|
|
|
421
464
|
collection = self._get(collection_url)
|
|
422
465
|
return collection["data"], collection["child_items"]
|
|
423
466
|
|
|
467
|
+
def get_group(self, group_id: str) -> dict[str, Any]:
|
|
468
|
+
"""Get a group with a given ID.
|
|
469
|
+
|
|
470
|
+
Uses the search groups endpoint under the hood,
|
|
471
|
+
so will only find groups that the user has access
|
|
472
|
+
to and that match the query.
|
|
473
|
+
|
|
474
|
+
Parameters:
|
|
475
|
+
group_id: The human-readable ID of the group to search for.
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
A dictionary of group data for the group with the given ID.
|
|
479
|
+
|
|
480
|
+
Raises:
|
|
481
|
+
DatalabAPIError: If no group with the given ID is found.
|
|
482
|
+
|
|
483
|
+
"""
|
|
484
|
+
group_url = f"{self.datalab_api_url}/search/groups?query={group_id}"
|
|
485
|
+
group_resp = self._get(group_url)
|
|
486
|
+
matching_groups = group_resp["data"]
|
|
487
|
+
if matching_groups:
|
|
488
|
+
matching_groups = [g for g in matching_groups if g.get("group_id") == group_id]
|
|
489
|
+
return matching_groups[0]
|
|
490
|
+
|
|
491
|
+
raise DatalabAPIError(f"No group found with ID {group_id!r}")
|
|
492
|
+
|
|
424
493
|
def create_collection(
|
|
425
494
|
self, collection_id: str, collection_data: dict | None = None
|
|
426
495
|
) -> dict[str, Any]:
|
|
@@ -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
|
|
@@ -35,7 +35,7 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
|
|
|
35
35
|
|
|
36
36
|
_api_key: Optional[str] = None
|
|
37
37
|
_session: Optional[httpx.Client] = None
|
|
38
|
-
_headers: dict[str, str]
|
|
38
|
+
_headers: dict[str, str]
|
|
39
39
|
_timeout: httpx.Timeout = httpx.Timeout(10.0, read=60.0)
|
|
40
40
|
|
|
41
41
|
interactive: bool = True
|
|
@@ -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
|
|
|
@@ -82,7 +85,7 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
|
|
|
82
85
|
self.log = logging.getLogger(__name__)
|
|
83
86
|
|
|
84
87
|
self._http_client = httpx.Client
|
|
85
|
-
self._headers
|
|
88
|
+
self._headers = {"User-Agent": f"Datalab Python API/{__version__}"}
|
|
86
89
|
|
|
87
90
|
self._detect_api_url()
|
|
88
91
|
|
|
@@ -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.5.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
|
|
@@ -58,3 +58,16 @@ def test_collection_get(fake_api_url, mocked_api):
|
|
|
58
58
|
|
|
59
59
|
collection, children = client.get_collection("test_collection", display=True)
|
|
60
60
|
assert mocked_api["collection-test_collection"].called
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@respx.mock
|
|
64
|
+
def test_group_get(fake_api_url, mocked_api):
|
|
65
|
+
with DatalabClient("https://api.datalab.industries") as client:
|
|
66
|
+
assert mocked_api["api"].called
|
|
67
|
+
assert mocked_api["info"].called
|
|
68
|
+
assert mocked_api["info-blocks"].called
|
|
69
|
+
|
|
70
|
+
group = client.get_group("test_group", display=True)
|
|
71
|
+
assert mocked_api["search-groups"].called
|
|
72
|
+
assert group["immutable_id"] == "67d0b01e03000cdc75134dbd"
|
|
73
|
+
assert group["group_id"] == "test_group"
|
|
@@ -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
|