datalab-api 0.2.0__tar.gz → 0.2.3__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.0
3
+ Version: 0.2.3
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,13 +38,19 @@ 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: all
42
+ Requires-Dist: datalab-api[cli,dev,docs]; extra == "all"
33
43
 
34
44
  # <div align="center"><i>datalab</i> Python API</div>
35
45
 
36
46
  <div align="center">
37
- <a href="https://datalab-python-api.readthedocs.io/en/stable/?badge=stable">
38
- <img src="https://readthedocs.org/projects/datalab-python-api/badge/?version=stable"></img>
39
- </a>
47
+ <a href="https://pypi.org/datalab-api"><img src="https://img.shields.io/pypi/v/datalab-api?logo=pypi&label=PyPI" /></a>
48
+ <a href="https://github.com/datalab-org/datalab-api?tab=MIT-1-ov-file#MIT-1-ov-file"><img src="https://img.shields.io/github/license/datalab-org/datalab-api" /></a>
49
+ </div>
50
+
51
+ <div align="center">
52
+ <a href="https://datalab-python-api.readthedocs.io/en/stable/?badge=stable"><img src="https://img.shields.io/readthedocs/datalab-python-api?logo=readthedocs" /></a>
53
+ <a href="https://pypi.org/datalab-api"><img src="https://img.shields.io/pypi/pyversions/datalab-api?logo=python&label=Python%20versions" /></a>
40
54
  </div>
41
55
 
42
56
  A simple Python API that can interact with [*datalab*](https://github.com/the-grey-group/datalab) instances.
@@ -91,7 +105,7 @@ This package implements basic functionality for displaying and manipulating entr
91
105
  ```python
92
106
  from datalab_api import DatalabClient
93
107
 
94
- with DatalabClient("https://api.public.odbx.science") as client:
108
+ with DatalabClient("https://public.api.odbx.science") as client:
95
109
 
96
110
  # List all items of a given type
97
111
  items = client.get_items()
@@ -1,9 +1,13 @@
1
1
  # <div align="center"><i>datalab</i> Python API</div>
2
2
 
3
3
  <div align="center">
4
- <a href="https://datalab-python-api.readthedocs.io/en/stable/?badge=stable">
5
- <img src="https://readthedocs.org/projects/datalab-python-api/badge/?version=stable"></img>
6
- </a>
4
+ <a href="https://pypi.org/datalab-api"><img src="https://img.shields.io/pypi/v/datalab-api?logo=pypi&label=PyPI" /></a>
5
+ <a href="https://github.com/datalab-org/datalab-api?tab=MIT-1-ov-file#MIT-1-ov-file"><img src="https://img.shields.io/github/license/datalab-org/datalab-api" /></a>
6
+ </div>
7
+
8
+ <div align="center">
9
+ <a href="https://datalab-python-api.readthedocs.io/en/stable/?badge=stable"><img src="https://img.shields.io/readthedocs/datalab-python-api?logo=readthedocs" /></a>
10
+ <a href="https://pypi.org/datalab-api"><img src="https://img.shields.io/pypi/pyversions/datalab-api?logo=python&label=Python%20versions" /></a>
7
11
  </div>
8
12
 
9
13
  A simple Python API that can interact with [*datalab*](https://github.com/the-grey-group/datalab) instances.
@@ -58,7 +62,7 @@ This package implements basic functionality for displaying and manipulating entr
58
62
  ```python
59
63
  from datalab_api import DatalabClient
60
64
 
61
- with DatalabClient("https://api.public.odbx.science") as client:
65
+ with DatalabClient("https://public.api.odbx.science") as client:
62
66
 
63
67
  # List all items of a given type
64
68
  items = client.get_items()
@@ -5,6 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "datalab-api"
7
7
  readme = "README.md"
8
+ description = "A Python API for instances of the datalab data management platform (https://github.com/the-grey-group/datalab)."
8
9
  keywords = []
9
10
  license = { text = "MIT" }
10
11
  authors = [{ name = "Matthew Evans", email = "matthew@ml-evs.science" }]
@@ -31,6 +32,9 @@ dependencies =[
31
32
  [project.optional-dependencies]
32
33
  dev = [
33
34
  "pre-commit ~= 3.0",
35
+ "pytest ~= 8.0",
36
+ "pytest-cov ~= 5.0",
37
+ "respx ~= 0.21",
34
38
  ]
35
39
 
36
40
  docs = [
@@ -46,14 +50,18 @@ cli = [
46
50
  "click-shell ~= 2.1", # REPL-like interface
47
51
  ]
48
52
 
53
+ all = [
54
+ "datalab-api[dev,docs,cli]"
55
+ ]
56
+
49
57
  [project.scripts]
50
58
  datalab = "datalab_api.cli:app"
51
59
 
52
60
  [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"
61
+ homepage = "https://github.com/datalab-org/datalab-api"
62
+ repository = "https://github.com/datalab-org/datalab-api"
63
+ documentation = "https://datalab-python-api.readthedocs.io"
64
+ changelog = "https://github.com/datalab-org/datalab-api/releases"
57
65
 
58
66
  [tool.versioningit.vcs]
59
67
  method = "git"
@@ -1,3 +1,4 @@
1
+ import warnings
1
2
  from pathlib import Path
2
3
  from typing import Any, Optional, Union
3
4
 
@@ -214,6 +215,26 @@ class DatalabClient(BaseDatalabClient):
214
215
 
215
216
  return item["item_data"]
216
217
 
218
+ def get_item_files(self, item_id: str) -> None:
219
+ """Download all the files for a given item and save them locally
220
+ in the current working directory.
221
+
222
+ Parameters:
223
+ item_id: The ID of the item to search for.
224
+
225
+ """
226
+
227
+ item_data = self.get_item(item_id)
228
+ for f in item_data.get("files", []):
229
+ file_location = f["location"]
230
+ url = file_location.replace("/app", self.datalab_api_url)
231
+ if Path(f["name"]).exists():
232
+ warnings.warn(f"Will not overwrite existing file {f['name']}")
233
+ continue
234
+ with open(f["name"], "wb") as file:
235
+ response = self.session.get(url, follow_redirects=True)
236
+ file.write(response.content)
237
+
217
238
  def get_block(self, item_id: str, block_id: str, block_data: dict[str, Any]) -> dict[str, Any]:
218
239
  """Get a block with a given ID and block data.
219
240
  Should be used in conjunction with `get_item` to load an existing block.
@@ -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,19 @@ 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
79
  if "bokeh_plot_data" in block_data:
47
80
  bokeh_plot_data = block_data["bokeh_plot_data"]
48
81
  else:
49
82
  bokeh_plot_data = block_data
50
83
  curdoc().replace_with_json(bokeh_plot_data["doc"])
51
- show(curdoc().roots[0])
84
+ if show:
85
+ bokeh_show(curdoc().roots[0])
86
+
87
+ return curdoc()
52
88
 
53
89
 
54
90
  class BaseDatalabClient(metaclass=AutoPrettyPrint):
@@ -98,6 +134,8 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
98
134
  self._http_client = httpx.Client
99
135
  self._headers["User-Agent"] = f"Datalab Python API/{__version__}"
100
136
 
137
+ self._detect_api_url()
138
+
101
139
  info_json = self.get_info()
102
140
 
103
141
  self._datalab_api_versions: list[str] = info_json["data"]["attributes"][
@@ -110,6 +148,27 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
110
148
 
111
149
  self._find_api_key()
112
150
 
151
+ def _detect_api_url(self) -> None:
152
+ """Perform a handshake with the chosen URL to ascertain the correct API URL.
153
+
154
+ If a datalab UI URL is passed, the client will attempt to resolve the API URL by
155
+ inspecting the HTML meta tags.
156
+
157
+ Do not use the session for this, so we are not passing the API key to arbitrary URLs.
158
+
159
+ """
160
+ response = httpx.get(self.datalab_api_url)
161
+ match = re.search(
162
+ r'<meta name="x_datalab_api_url" content="(.*?)">',
163
+ response.text,
164
+ re.IGNORECASE,
165
+ )
166
+ if match:
167
+ self.datalab_api_url = match.group(1)
168
+ warnings.warn(
169
+ f"Found API URL {self.datalab_api_url} in HTML meta tag. Creating client with this URL instead."
170
+ )
171
+
113
172
  def get_info(self) -> dict[str, Any]:
114
173
  raise NotImplementedError
115
174
 
@@ -173,6 +232,10 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
173
232
  if api_key is None:
174
233
  api_key = os.getenv("DATALAB_API_KEY")
175
234
 
235
+ # Remove single and double quotes around API key if present
236
+ if api_key is not None:
237
+ api_key = api_key.strip("'").strip('"')
238
+
176
239
  if api_key is None:
177
240
  raise ValueError(
178
241
  f"No API key found in environment variables {key_env_var}/<prefix>_{key_env_var}."
@@ -1,8 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: datalab-api
3
- Version: 0.2.0
3
+ Version: 0.2.3
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,13 +38,19 @@ 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: all
42
+ Requires-Dist: datalab-api[cli,dev,docs]; extra == "all"
33
43
 
34
44
  # <div align="center"><i>datalab</i> Python API</div>
35
45
 
36
46
  <div align="center">
37
- <a href="https://datalab-python-api.readthedocs.io/en/stable/?badge=stable">
38
- <img src="https://readthedocs.org/projects/datalab-python-api/badge/?version=stable"></img>
39
- </a>
47
+ <a href="https://pypi.org/datalab-api"><img src="https://img.shields.io/pypi/v/datalab-api?logo=pypi&label=PyPI" /></a>
48
+ <a href="https://github.com/datalab-org/datalab-api?tab=MIT-1-ov-file#MIT-1-ov-file"><img src="https://img.shields.io/github/license/datalab-org/datalab-api" /></a>
49
+ </div>
50
+
51
+ <div align="center">
52
+ <a href="https://datalab-python-api.readthedocs.io/en/stable/?badge=stable"><img src="https://img.shields.io/readthedocs/datalab-python-api?logo=readthedocs" /></a>
53
+ <a href="https://pypi.org/datalab-api"><img src="https://img.shields.io/pypi/pyversions/datalab-api?logo=python&label=Python%20versions" /></a>
40
54
  </div>
41
55
 
42
56
  A simple Python API that can interact with [*datalab*](https://github.com/the-grey-group/datalab) instances.
@@ -91,7 +105,7 @@ This package implements basic functionality for displaying and manipulating entr
91
105
  ```python
92
106
  from datalab_api import DatalabClient
93
107
 
94
- with DatalabClient("https://api.public.odbx.science") as client:
108
+ with DatalabClient("https://public.api.odbx.science") as client:
95
109
 
96
110
  # List all items of a given type
97
111
  items = client.get_items()
@@ -9,4 +9,5 @@ src/datalab_api.egg-info/SOURCES.txt
9
9
  src/datalab_api.egg-info/dependency_links.txt
10
10
  src/datalab_api.egg-info/entry_points.txt
11
11
  src/datalab_api.egg-info/requires.txt
12
- src/datalab_api.egg-info/top_level.txt
12
+ src/datalab_api.egg-info/top_level.txt
13
+ tests/test_client.py
@@ -2,12 +2,18 @@ httpx~=0.27
2
2
  bokeh~=2.4
3
3
  rich~=13.0
4
4
 
5
+ [all]
6
+ datalab-api[cli,dev,docs]
7
+
5
8
  [cli]
6
9
  typer~=0.9
7
10
  click-shell~=2.1
8
11
 
9
12
  [dev]
10
13
  pre-commit~=3.0
14
+ pytest~=8.0
15
+ pytest-cov~=5.0
16
+ respx~=0.21
11
17
 
12
18
  [docs]
13
19
  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"
File without changes
File without changes