datalab-api 0.2.1__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,8 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: datalab-api
3
- Version: 0.2.1
3
+ Version: 0.2.4
4
+ Summary: A Python API for instances of the datalab data management platform (https://github.com/the-grey-group/datalab).
4
5
  Author-email: Matthew Evans <matthew@ml-evs.science>
5
6
  License: MIT
7
+ Project-URL: homepage, https://github.com/datalab-org/datalab-api
8
+ Project-URL: repository, https://github.com/datalab-org/datalab-api
9
+ Project-URL: documentation, https://datalab-python-api.readthedocs.io
10
+ Project-URL: changelog, https://github.com/datalab-org/datalab-api/releases
6
11
  Classifier: Programming Language :: Python :: 3
7
12
  Classifier: Programming Language :: Python :: 3.9
8
13
  Classifier: Programming Language :: Python :: 3
@@ -21,6 +26,9 @@ Requires-Dist: bokeh~=2.4
21
26
  Requires-Dist: rich~=13.0
22
27
  Provides-Extra: dev
23
28
  Requires-Dist: pre-commit~=3.0; extra == "dev"
29
+ Requires-Dist: pytest~=8.0; extra == "dev"
30
+ Requires-Dist: pytest-cov~=5.0; extra == "dev"
31
+ Requires-Dist: respx~=0.21; extra == "dev"
24
32
  Provides-Extra: docs
25
33
  Requires-Dist: mkdocs; extra == "docs"
26
34
  Requires-Dist: mkdocs-material; extra == "docs"
@@ -30,6 +38,11 @@ Requires-Dist: mkdocs-jupyter; extra == "docs"
30
38
  Provides-Extra: cli
31
39
  Requires-Dist: typer~=0.9; extra == "cli"
32
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"
44
+ Provides-Extra: all
45
+ Requires-Dist: datalab-api[cli,dev,docs]; extra == "all"
33
46
 
34
47
  # <div align="center"><i>datalab</i> Python API</div>
35
48
 
@@ -95,7 +108,7 @@ This package implements basic functionality for displaying and manipulating entr
95
108
  ```python
96
109
  from datalab_api import DatalabClient
97
110
 
98
- with DatalabClient("https://api.public.odbx.science") as client:
111
+ with DatalabClient("https://public.api.odbx.science") as client:
99
112
 
100
113
  # List all items of a given type
101
114
  items = client.get_items()
@@ -62,7 +62,7 @@ This package implements basic functionality for displaying and manipulating entr
62
62
  ```python
63
63
  from datalab_api import DatalabClient
64
64
 
65
- with DatalabClient("https://api.public.odbx.science") as client:
65
+ with DatalabClient("https://public.api.odbx.science") as client:
66
66
 
67
67
  # List all items of a given type
68
68
  items = client.get_items()
@@ -0,0 +1,175 @@
1
+ [build-system]
2
+ requires = ["setuptools >= 62", "versioningit ~= 3.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "datalab-api"
7
+ readme = "README.md"
8
+ description = "A Python API for instances of the datalab data management platform (https://github.com/the-grey-group/datalab)."
9
+ keywords = []
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Matthew Evans", email = "matthew@ml-evs.science" }]
12
+ dynamic = ["version"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9",
15
+ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Intended Audience :: Science/Research",
19
+ "Intended Audience :: Information Technology",
20
+ "Topic :: Other/Nonlisted Topic",
21
+ "Topic :: Scientific/Engineering",
22
+ ]
23
+
24
+ requires-python = ">=3.9"
25
+ dependencies =[
26
+ "httpx ~= 0.27", # better HTTP requests
27
+ "bokeh ~= 2.4", # interactive plots from datalab directly
28
+ "rich ~= 13.0", # nicer terminal output
29
+ ]
30
+
31
+
32
+ [project.optional-dependencies]
33
+ dev = [
34
+ "pre-commit ~= 3.0",
35
+ "pytest ~= 8.0",
36
+ "pytest-cov ~= 5.0",
37
+ "respx ~= 0.21",
38
+ ]
39
+
40
+ docs = [
41
+ "mkdocs",
42
+ "mkdocs-material",
43
+ "mkdocstrings[python-legacy]",
44
+ "mkdocs-awesome-pages-plugin",
45
+ "mkdocs-jupyter",
46
+ ]
47
+
48
+ cli = [
49
+ "typer ~= 0.9", # command line interface
50
+ "click-shell ~= 2.1", # REPL-like interface
51
+ ]
52
+
53
+ cheminventory-helper = [
54
+ "pandas ~= 2.2",
55
+ "openpyxl ~= 3.0"
56
+ ]
57
+
58
+ all = [
59
+ "datalab-api[dev,docs,cli]"
60
+ ]
61
+
62
+ [project.scripts]
63
+ datalab = "datalab_api.cli:app"
64
+
65
+ [project.urls]
66
+ homepage = "https://github.com/datalab-org/datalab-api"
67
+ repository = "https://github.com/datalab-org/datalab-api"
68
+ documentation = "https://datalab-python-api.readthedocs.io"
69
+ changelog = "https://github.com/datalab-org/datalab-api/releases"
70
+
71
+ [tool.versioningit.vcs]
72
+ method = "git"
73
+ default-tag = "0.0.1"
74
+
75
+
76
+ [tool.mypy]
77
+ # plugins = "pydantic.mypy"
78
+ ignore_missing_imports = true
79
+ follow_imports = "skip"
80
+
81
+ [tool.ruff]
82
+ line-length = 100
83
+ target-version = "py39"
84
+
85
+ [tool.ruff.lint]
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
165
+ fixable = ["A", "B", "C", "D", "E", "F", "I"]
166
+ unfixable = []
167
+ # Allow unused variables when underscore-prefixed.
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,3 +1,5 @@
1
+ import warnings
2
+ from collections.abc import Iterable
1
3
  from pathlib import Path
2
4
  from typing import Any, Optional, Union
3
5
 
@@ -6,6 +8,10 @@ from ._base import BaseDatalabClient, __version__
6
8
  __all__ = ("__version__", "DatalabClient")
7
9
 
8
10
 
11
+ class DuplicateItemError(ValueError):
12
+ """Raised when the API operation would create a duplicate item."""
13
+
14
+
9
15
  class DatalabClient(BaseDatalabClient):
10
16
  """A client for the Datalab API.
11
17
 
@@ -65,7 +71,7 @@ class DatalabClient(BaseDatalabClient):
65
71
  return items[item_type]
66
72
 
67
73
  def search_items(
68
- self, query: str, item_types: Union[list[str], str] = ["samples", "cells"]
74
+ self, query: str, item_types: Union[Iterable[str], str] = ("samples", "cells")
69
75
  ) -> list[dict[str, Any]]:
70
76
  """Search for items of the given types that match the query.
71
77
 
@@ -95,7 +101,11 @@ class DatalabClient(BaseDatalabClient):
95
101
  return items["items"]
96
102
 
97
103
  def create_item(
98
- 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,
99
109
  ) -> dict[str, Any]:
100
110
  """Create an item with a given ID and item data.
101
111
 
@@ -103,12 +113,22 @@ class DatalabClient(BaseDatalabClient):
103
113
  item_id: The ID of the item to create.
104
114
  item_type: The type of item to create, e.g., 'samples', 'cells'.
105
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.
106
118
 
107
119
  """
108
120
  new_item = {}
109
121
  if item_data is not None:
110
122
  new_item = item_data
111
- 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})
112
132
 
113
133
  create_item_url = f"{self.datalab_api_url}/new-sample/"
114
134
  create_item_resp = self.session.post(
@@ -118,16 +138,18 @@ class DatalabClient(BaseDatalabClient):
118
138
  )
119
139
  try:
120
140
  created_item = create_item_resp.json()
121
- if created_item["status"] != "success":
122
- raise RuntimeError(
123
- 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}."
124
144
  )
145
+ if created_item["status"] != "success":
146
+ raise RuntimeError(f"Failed to create item at {create_item_url}: {created_item}.")
125
147
  return created_item["sample_list_entry"]
126
148
 
127
- except Exception:
128
- raise RuntimeError(
129
- 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."
130
- )
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
131
153
 
132
154
  def update_item(self, item_id: str, item_data: dict[str, Any]) -> dict[str, Any]:
133
155
  """Update an item with the given item data.
@@ -214,6 +236,26 @@ class DatalabClient(BaseDatalabClient):
214
236
 
215
237
  return item["item_data"]
216
238
 
239
+ def get_item_files(self, item_id: str) -> None:
240
+ """Download all the files for a given item and save them locally
241
+ in the current working directory.
242
+
243
+ Parameters:
244
+ item_id: The ID of the item to search for.
245
+
246
+ """
247
+
248
+ item_data = self.get_item(item_id)
249
+ for f in item_data.get("files", []):
250
+ file_location = f["location"]
251
+ url = file_location.replace("/app", self.datalab_api_url)
252
+ if Path(f["name"]).exists():
253
+ warnings.warn(f"Will not overwrite existing file {f['name']}")
254
+ continue
255
+ with open(f["name"], "wb") as file:
256
+ response = self.session.get(url, follow_redirects=True)
257
+ file.write(response.content)
258
+
217
259
  def get_block(self, item_id: str, block_id: str, block_data: dict[str, Any]) -> dict[str, Any]:
218
260
  """Get a block with a given ID and block data.
219
261
  Should be used in conjunction with `get_item` to load an existing block.
@@ -369,3 +411,57 @@ class DatalabClient(BaseDatalabClient):
369
411
  raise RuntimeError(f"Failed to update block {block_type=}:\n{resp.text}")
370
412
 
371
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
@@ -1,12 +1,16 @@
1
1
  import functools
2
2
  import logging
3
3
  import os
4
+ import re
5
+ import warnings
4
6
  from importlib.metadata import version
5
7
  from typing import Any, Optional
6
8
 
7
9
  import httpx
10
+ from rich.console import Console
8
11
  from rich.logging import RichHandler
9
12
  from rich.pretty import pprint
13
+ from rich.table import Table
10
14
 
11
15
  __version__ = version("datalab-api")
12
16
 
@@ -14,17 +18,46 @@ __all__ = ("__version__", "BaseDatalabClient")
14
18
 
15
19
 
16
20
  def pretty_displayer(method):
21
+ """A decorator which wraps a method with a 'display' kwarg, which will
22
+ either pretty print a JSON response, or display a Rich table.
23
+
24
+ """
25
+
17
26
  @functools.wraps(method)
18
27
  def rich_wrapper(self, *args, **kwargs):
19
28
  display = kwargs.pop("display", False)
29
+ page_limit = kwargs.pop("page_limit", 10)
20
30
  result = method(self, *args, **kwargs)
21
31
  if display:
22
- if isinstance(result, dict) and "blocks_obj" in result:
23
- blocks: dict[str, dict] = result["blocks_obj"]
24
- for block in blocks.values():
25
- if "bokeh_plot_data" in block:
26
- bokeh_from_json(block)
27
- pprint(result, max_length=None, max_string=100, max_depth=3)
32
+ blocks = None
33
+ if isinstance(result, dict):
34
+ if "blocks_obj" in result:
35
+ blocks = result["blocks_obj"]
36
+ elif "blocktype" in result:
37
+ blocks = {result["block_id"]: result}
38
+ result = None
39
+ if blocks:
40
+ for block in blocks.values():
41
+ if "bokeh_plot_data" in block:
42
+ bokeh_from_json(block)
43
+ if result:
44
+ pprint(result, max_length=None, max_string=100, max_depth=3)
45
+ elif isinstance(result, list):
46
+ table = Table(show_lines=True)
47
+ table.add_column("type", overflow="crop", justify="center")
48
+ table.add_column("ID", style="cyan", no_wrap=True)
49
+ table.add_column("refcode", style="magenta", no_wrap=True)
50
+ table.add_column("name", width=30, overflow="ellipsis", no_wrap=True)
51
+ for item in result[:page_limit]:
52
+ table.add_row(
53
+ item["type"][0].upper(),
54
+ item["item_id"],
55
+ item["refcode"],
56
+ item["name"],
57
+ end_section=True,
58
+ )
59
+ console = Console()
60
+ console.print(table)
28
61
 
29
62
  return result
30
63
 
@@ -39,16 +72,16 @@ class AutoPrettyPrint(type):
39
72
  return super().__new__(cls, name, bases, dct)
40
73
 
41
74
 
42
- def bokeh_from_json(block_data):
75
+ def bokeh_from_json(block_data, show=True):
43
76
  from bokeh.io import curdoc
44
- from bokeh.plotting import show
77
+ from bokeh.plotting import show as bokeh_show
45
78
 
46
- if "bokeh_plot_data" in block_data:
47
- bokeh_plot_data = block_data["bokeh_plot_data"]
48
- else:
49
- bokeh_plot_data = block_data
79
+ bokeh_plot_data = block_data.get("bokeh_plot_data", block_data)
50
80
  curdoc().replace_with_json(bokeh_plot_data["doc"])
51
- show(curdoc().roots[0])
81
+ if show:
82
+ bokeh_show(curdoc().roots[0])
83
+
84
+ return curdoc()
52
85
 
53
86
 
54
87
  class BaseDatalabClient(metaclass=AutoPrettyPrint):
@@ -98,6 +131,8 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
98
131
  self._http_client = httpx.Client
99
132
  self._headers["User-Agent"] = f"Datalab Python API/{__version__}"
100
133
 
134
+ self._detect_api_url()
135
+
101
136
  info_json = self.get_info()
102
137
 
103
138
  self._datalab_api_versions: list[str] = info_json["data"]["attributes"][
@@ -110,6 +145,27 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
110
145
 
111
146
  self._find_api_key()
112
147
 
148
+ def _detect_api_url(self) -> None:
149
+ """Perform a handshake with the chosen URL to ascertain the correct API URL.
150
+
151
+ If a datalab UI URL is passed, the client will attempt to resolve the API URL by
152
+ inspecting the HTML meta tags.
153
+
154
+ Do not use the session for this, so we are not passing the API key to arbitrary URLs.
155
+
156
+ """
157
+ response = httpx.get(self.datalab_api_url)
158
+ match = re.search(
159
+ r'<meta name="x_datalab_api_url" content="(.*?)">',
160
+ response.text,
161
+ re.IGNORECASE,
162
+ )
163
+ if match:
164
+ self.datalab_api_url = match.group(1)
165
+ warnings.warn(
166
+ f"Found API URL {self.datalab_api_url} in HTML meta tag. Creating client with this URL instead."
167
+ )
168
+
113
169
  def get_info(self) -> dict[str, Any]:
114
170
  raise NotImplementedError
115
171
 
@@ -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,8 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: datalab-api
3
- Version: 0.2.1
3
+ Version: 0.2.4
4
+ Summary: A Python API for instances of the datalab data management platform (https://github.com/the-grey-group/datalab).
4
5
  Author-email: Matthew Evans <matthew@ml-evs.science>
5
6
  License: MIT
7
+ Project-URL: homepage, https://github.com/datalab-org/datalab-api
8
+ Project-URL: repository, https://github.com/datalab-org/datalab-api
9
+ Project-URL: documentation, https://datalab-python-api.readthedocs.io
10
+ Project-URL: changelog, https://github.com/datalab-org/datalab-api/releases
6
11
  Classifier: Programming Language :: Python :: 3
7
12
  Classifier: Programming Language :: Python :: 3.9
8
13
  Classifier: Programming Language :: Python :: 3
@@ -21,6 +26,9 @@ Requires-Dist: bokeh~=2.4
21
26
  Requires-Dist: rich~=13.0
22
27
  Provides-Extra: dev
23
28
  Requires-Dist: pre-commit~=3.0; extra == "dev"
29
+ Requires-Dist: pytest~=8.0; extra == "dev"
30
+ Requires-Dist: pytest-cov~=5.0; extra == "dev"
31
+ Requires-Dist: respx~=0.21; extra == "dev"
24
32
  Provides-Extra: docs
25
33
  Requires-Dist: mkdocs; extra == "docs"
26
34
  Requires-Dist: mkdocs-material; extra == "docs"
@@ -30,6 +38,11 @@ Requires-Dist: mkdocs-jupyter; extra == "docs"
30
38
  Provides-Extra: cli
31
39
  Requires-Dist: typer~=0.9; extra == "cli"
32
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"
44
+ Provides-Extra: all
45
+ Requires-Dist: datalab-api[cli,dev,docs]; extra == "all"
33
46
 
34
47
  # <div align="center"><i>datalab</i> Python API</div>
35
48
 
@@ -95,7 +108,7 @@ This package implements basic functionality for displaying and manipulating entr
95
108
  ```python
96
109
  from datalab_api import DatalabClient
97
110
 
98
- with DatalabClient("https://api.public.odbx.science") as client:
111
+ with DatalabClient("https://public.api.odbx.science") as client:
99
112
 
100
113
  # List all items of a given type
101
114
  items = client.get_items()
@@ -4,9 +4,11 @@ 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
10
11
  src/datalab_api.egg-info/entry_points.txt
11
12
  src/datalab_api.egg-info/requires.txt
12
- src/datalab_api.egg-info/top_level.txt
13
+ src/datalab_api.egg-info/top_level.txt
14
+ tests/test_client.py
@@ -2,12 +2,22 @@ httpx~=0.27
2
2
  bokeh~=2.4
3
3
  rich~=13.0
4
4
 
5
+ [all]
6
+ datalab-api[cli,dev,docs]
7
+
8
+ [cheminventory-helper]
9
+ pandas~=2.2
10
+ openpyxl~=3.0
11
+
5
12
  [cli]
6
13
  typer~=0.9
7
14
  click-shell~=2.1
8
15
 
9
16
  [dev]
10
17
  pre-commit~=3.0
18
+ pytest~=8.0
19
+ pytest-cov~=5.0
20
+ respx~=0.21
11
21
 
12
22
  [docs]
13
23
  mkdocs
@@ -0,0 +1,22 @@
1
+ import pytest
2
+ import respx
3
+ from datalab_api import DatalabClient
4
+ from httpx import Response
5
+
6
+
7
+ @respx.mock
8
+ def test_redirect_url(fake_ui_html, fake_info_json):
9
+ fake_ui = respx.get("https://ui.datalab.industries").mock(
10
+ return_value=Response(200, content=fake_ui_html)
11
+ )
12
+ fake_api = respx.get("https://api.datalab.industries/info").mock(
13
+ return_value=Response(200, json=fake_info_json)
14
+ )
15
+ with pytest.warns(
16
+ UserWarning,
17
+ match="^Found API URL https://api.datalab.industries in HTML meta tag. Creating client with this URL instead.$",
18
+ ):
19
+ client = DatalabClient("https://ui.datalab.industries")
20
+ assert fake_ui.called
21
+ assert fake_api.called
22
+ assert client.datalab_api_url == "https://api.datalab.industries"
@@ -1,79 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools >= 62", "versioningit ~= 3.0", "wheel"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "datalab-api"
7
- readme = "README.md"
8
- keywords = []
9
- license = { text = "MIT" }
10
- authors = [{ name = "Matthew Evans", email = "matthew@ml-evs.science" }]
11
- dynamic = ["version"]
12
- classifiers = [
13
- "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9",
14
- "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10",
15
- "Programming Language :: Python :: 3.11",
16
- "Programming Language :: Python :: 3.12",
17
- "Intended Audience :: Science/Research",
18
- "Intended Audience :: Information Technology",
19
- "Topic :: Other/Nonlisted Topic",
20
- "Topic :: Scientific/Engineering",
21
- ]
22
-
23
- requires-python = ">=3.9"
24
- dependencies =[
25
- "httpx ~= 0.27", # better HTTP requests
26
- "bokeh ~= 2.4", # interactive plots from datalab directly
27
- "rich ~= 13.0", # nicer terminal output
28
- ]
29
-
30
-
31
- [project.optional-dependencies]
32
- dev = [
33
- "pre-commit ~= 3.0",
34
- ]
35
-
36
- docs = [
37
- "mkdocs",
38
- "mkdocs-material",
39
- "mkdocstrings[python-legacy]",
40
- "mkdocs-awesome-pages-plugin",
41
- "mkdocs-jupyter",
42
- ]
43
-
44
- cli = [
45
- "typer ~= 0.9", # command line interface
46
- "click-shell ~= 2.1", # REPL-like interface
47
- ]
48
-
49
- [project.scripts]
50
- datalab = "datalab_api.cli:app"
51
-
52
- [project.urls]
53
- # homepage = "https://github.com/the-grey-group/datalab"
54
- # repository = "https://github.com/the-grey-group/datalab"
55
- # documentation = "https://the-datalab.readthedocs.io"
56
- # changelog = "https://github.com/the-grey-group/datalab/releases"
57
-
58
- [tool.versioningit.vcs]
59
- method = "git"
60
- default-tag = "0.0.1"
61
-
62
-
63
- [tool.mypy]
64
- # plugins = "pydantic.mypy"
65
- ignore_missing_imports = true
66
- follow_imports = "skip"
67
-
68
- [tool.ruff]
69
- line-length = 100
70
- target-version = "py310"
71
-
72
- [tool.ruff.lint]
73
- select = ["E", "F", "I", "W", "Q"]
74
- ignore = ["E501", "E402"]
75
- fixable = ["A", "B", "C", "D", "E", "F", "I"]
76
- unfixable = []
77
- per-file-ignores = {}
78
- # Allow unused variables when underscore-prefixed.
79
- dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
File without changes
File without changes