datalab-api 0.2.1__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.1
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,6 +38,8 @@ 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
 
@@ -95,7 +105,7 @@ This package implements basic functionality for displaying and manipulating entr
95
105
  ```python
96
106
  from datalab_api import DatalabClient
97
107
 
98
- with DatalabClient("https://api.public.odbx.science") as client:
108
+ with DatalabClient("https://public.api.odbx.science") as client:
99
109
 
100
110
  # List all items of a given type
101
111
  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()
@@ -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
 
@@ -1,8 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: datalab-api
3
- Version: 0.2.1
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,6 +38,8 @@ 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
 
@@ -95,7 +105,7 @@ This package implements basic functionality for displaying and manipulating entr
95
105
  ```python
96
106
  from datalab_api import DatalabClient
97
107
 
98
- with DatalabClient("https://api.public.odbx.science") as client:
108
+ with DatalabClient("https://public.api.odbx.science") as client:
99
109
 
100
110
  # List all items of a given type
101
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