datalab-api 0.2.6__tar.gz → 0.2.8__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,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: datalab-api
3
- Version: 0.2.6
3
+ Version: 0.2.8
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
@@ -24,10 +24,11 @@ License-File: LICENSE
24
24
  Requires-Dist: httpx~=0.27
25
25
  Requires-Dist: bokeh~=2.4
26
26
  Requires-Dist: rich~=13.0
27
+ Requires-Dist: numpy<2
27
28
  Provides-Extra: dev
28
- Requires-Dist: pre-commit~=3.0; extra == "dev"
29
+ Requires-Dist: pre-commit<5,>=3; extra == "dev"
29
30
  Requires-Dist: pytest~=8.0; extra == "dev"
30
- Requires-Dist: pytest-cov~=5.0; extra == "dev"
31
+ Requires-Dist: pytest-cov<7,>=5; extra == "dev"
31
32
  Requires-Dist: respx~=0.21; extra == "dev"
32
33
  Provides-Extra: docs
33
34
  Requires-Dist: mkdocs; extra == "docs"
@@ -56,11 +57,11 @@ Requires-Dist: datalab-api[cli,dev,docs]; extra == "all"
56
57
  <a href="https://pypi.org/datalab-api"><img src="https://img.shields.io/pypi/pyversions/datalab-api?logo=python&label=Python%20versions" /></a>
57
58
  </div>
58
59
 
59
- A simple Python API that can interact with [*datalab*](https://github.com/the-grey-group/datalab) instances.
60
+ A simple Python API that can interact with [*datalab*](https://github.com/datalab-org/datalab) instances.
60
61
 
61
62
  The idea here is to provide a set of utility functions and models for manipulating samples, cells, inventory, files, users and metadata associated with *datalab* entries in an automated way.
62
63
 
63
- This API may not expose all the functionality available in a given *datalab* instance, and the idea would be that this package can support multiple versions of the underlying [*datalab* REST API](https://the-datalab.readthedocs.io/en/latest/rest_api/).
64
+ This API may not expose all the functionality available in a given *datalab* instance, and the idea would be that this package can support multiple versions of the underlying [*datalab* REST API](https://docs.datalab-org.io/en/stable/rest_api/).
64
65
  This means that the API is primarily *functional* in nature, as opposed to object-oriented, since *datalab* instances are free to use their own custom data models.
65
66
  The available schemas are reported as instance metadata and in the future object-oriented models may be able to be genereated directly in the client (so e.g., the returned data would be Python objects like `Sample` rather than JSON data).
66
67
 
@@ -88,9 +89,9 @@ pip install .
88
89
  ## Usage
89
90
 
90
91
  Example usage as a Jupyter notebook can be found in the `examples` directory or
91
- in the [online documentation](https://datalab-api.readthedocs.io/), as
92
+ in the [online documentation](https://api-docs.datalab-org.io), as
92
93
  well as the full [API
93
- documentation](https://datalab-api.readthedocs.io/en/latest/reference/).
94
+ documentation](https://api-docs.datalab-org.io/en/stable/reference/).
94
95
 
95
96
  ### Authentication
96
97
 
@@ -98,7 +99,7 @@ Currently the only supported authentication method is via an API key.
98
99
  You can generate one for your account for a given *datalab* instance by visiting the `/get-api-key` endpoint of your chosen instance, or, if using a recent version of *datalab*, by visiting your account settings in the browser.
99
100
 
100
101
  This API key can be set via the environment variable `DATALAB_API_KEY`.
101
- To suport the use case of needing to interact with multiple datalab instances, the client will also check prefixed environment variables that use the [`IDENTIFIER_PREFIX` of the chosen datalab instance](https://the-datalab.readthedocs.io/en/latest/config/#mandatory-settings), e.g., `GREY_DATALAB_API_KEY` or `PUBLIC_DATALAB_API_KEY`.
102
+ To suport the use case of needing to interact with multiple datalab instances, the client will also check prefixed environment variables that use the [`IDENTIFIER_PREFIX` of the chosen datalab instance](https://docs.datalab-org.io/en/stable/config/#mandatory-settings), e.g., `GREY_DATALAB_API_KEY` or `PUBLIC_DATALAB_API_KEY`.
102
103
  Only keys that match will be read (e.g., other environment variables starting with `PUBLIC_` will be ignored, when connecting to the [public demo datalab](https://demo.datalab-org.io)).
103
104
 
104
105
  ### Python API
@@ -10,11 +10,11 @@
10
10
  <a href="https://pypi.org/datalab-api"><img src="https://img.shields.io/pypi/pyversions/datalab-api?logo=python&label=Python%20versions" /></a>
11
11
  </div>
12
12
 
13
- A simple Python API that can interact with [*datalab*](https://github.com/the-grey-group/datalab) instances.
13
+ A simple Python API that can interact with [*datalab*](https://github.com/datalab-org/datalab) instances.
14
14
 
15
15
  The idea here is to provide a set of utility functions and models for manipulating samples, cells, inventory, files, users and metadata associated with *datalab* entries in an automated way.
16
16
 
17
- This API may not expose all the functionality available in a given *datalab* instance, and the idea would be that this package can support multiple versions of the underlying [*datalab* REST API](https://the-datalab.readthedocs.io/en/latest/rest_api/).
17
+ This API may not expose all the functionality available in a given *datalab* instance, and the idea would be that this package can support multiple versions of the underlying [*datalab* REST API](https://docs.datalab-org.io/en/stable/rest_api/).
18
18
  This means that the API is primarily *functional* in nature, as opposed to object-oriented, since *datalab* instances are free to use their own custom data models.
19
19
  The available schemas are reported as instance metadata and in the future object-oriented models may be able to be genereated directly in the client (so e.g., the returned data would be Python objects like `Sample` rather than JSON data).
20
20
 
@@ -42,9 +42,9 @@ pip install .
42
42
  ## Usage
43
43
 
44
44
  Example usage as a Jupyter notebook can be found in the `examples` directory or
45
- in the [online documentation](https://datalab-api.readthedocs.io/), as
45
+ in the [online documentation](https://api-docs.datalab-org.io), as
46
46
  well as the full [API
47
- documentation](https://datalab-api.readthedocs.io/en/latest/reference/).
47
+ documentation](https://api-docs.datalab-org.io/en/stable/reference/).
48
48
 
49
49
  ### Authentication
50
50
 
@@ -52,7 +52,7 @@ Currently the only supported authentication method is via an API key.
52
52
  You can generate one for your account for a given *datalab* instance by visiting the `/get-api-key` endpoint of your chosen instance, or, if using a recent version of *datalab*, by visiting your account settings in the browser.
53
53
 
54
54
  This API key can be set via the environment variable `DATALAB_API_KEY`.
55
- To suport the use case of needing to interact with multiple datalab instances, the client will also check prefixed environment variables that use the [`IDENTIFIER_PREFIX` of the chosen datalab instance](https://the-datalab.readthedocs.io/en/latest/config/#mandatory-settings), e.g., `GREY_DATALAB_API_KEY` or `PUBLIC_DATALAB_API_KEY`.
55
+ To suport the use case of needing to interact with multiple datalab instances, the client will also check prefixed environment variables that use the [`IDENTIFIER_PREFIX` of the chosen datalab instance](https://docs.datalab-org.io/en/stable/config/#mandatory-settings), e.g., `GREY_DATALAB_API_KEY` or `PUBLIC_DATALAB_API_KEY`.
56
56
  Only keys that match will be read (e.g., other environment variables starting with `PUBLIC_` will be ignored, when connecting to the [public demo datalab](https://demo.datalab-org.io)).
57
57
 
58
58
  ### Python API
@@ -26,14 +26,15 @@ dependencies =[
26
26
  "httpx ~= 0.27", # better HTTP requests
27
27
  "bokeh ~= 2.4", # interactive plots from datalab directly
28
28
  "rich ~= 13.0", # nicer terminal output
29
+ "numpy < 2", # upper pin for numpy due to bad pins in bokeh
29
30
  ]
30
31
 
31
32
 
32
33
  [project.optional-dependencies]
33
34
  dev = [
34
- "pre-commit ~= 3.0",
35
+ "pre-commit >= 3,< 5",
35
36
  "pytest ~= 8.0",
36
- "pytest-cov ~= 5.0",
37
+ "pytest-cov >= 5,< 7",
37
38
  "respx ~= 0.21",
38
39
  ]
39
40
 
@@ -68,11 +69,13 @@ repository = "https://github.com/datalab-org/datalab-api"
68
69
  documentation = "https://datalab-python-api.readthedocs.io"
69
70
  changelog = "https://github.com/datalab-org/datalab-api/releases"
70
71
 
72
+ [tool.uv]
73
+ cache-keys = [{ git = true }]
74
+
71
75
  [tool.versioningit.vcs]
72
76
  method = "git"
73
77
  default-tag = "0.0.1"
74
78
 
75
-
76
79
  [tool.mypy]
77
80
  # plugins = "pydantic.mypy"
78
81
  ignore_missing_imports = true
@@ -30,13 +30,17 @@ class DatalabClient(BaseDatalabClient):
30
30
  def get_info(self) -> dict[str, Any]:
31
31
  """Fetch metadata associated with this datalab instance.
32
32
 
33
+ The server information is stored on the client and can be accessed
34
+ by the `info` attribute.
35
+
33
36
  Returns:
34
37
  dict: The JSON response from the `/info` endpoint of the Datalab API.
35
38
 
36
39
  """
37
40
  info_url = f"{self.datalab_api_url}/info"
38
41
  info_resp = self.session.get(info_url, follow_redirects=True)
39
- return info_resp.json()
42
+ self.info = info_resp.json()["data"]
43
+ return self.info
40
44
 
41
45
  def authenticate(self):
42
46
  """Tests authentication of the client with the Datalab API."""
@@ -48,6 +52,26 @@ class DatalabClient(BaseDatalabClient):
48
52
  )
49
53
  return user_resp.json()
50
54
 
55
+ def get_block_info(self) -> list[dict[str, Any]]:
56
+ """Return the list of available data blocks types for this instance,
57
+ including some details and descriptions of their usage.
58
+
59
+ The block information is stored on the client and can be accessed
60
+ by the `block_info` attribute.
61
+
62
+ Returns:
63
+ A list of dictionary of block types.
64
+
65
+ """
66
+ block_info_url = f"{self.datalab_api_url}/info/blocks"
67
+ block_info_resp = self.session.get(block_info_url, follow_redirects=True)
68
+ if block_info_resp.status_code != 200:
69
+ raise RuntimeError(
70
+ f"Failed to list block information at {block_info_url}: {block_info_resp.status_code=}."
71
+ )
72
+ self.block_info = block_info_resp.json()["data"]
73
+ return self.block_info
74
+
51
75
  def get_items(self, item_type: str | None = "samples") -> list[dict[str, Any]]:
52
76
  """List items of the given type available to the authenticated user.
53
77
 
@@ -131,7 +155,8 @@ class DatalabClient(BaseDatalabClient):
131
155
  try:
132
156
  collection_immutable_id = self.get_collection(collection_id)["immutable_id"]
133
157
  except RuntimeError:
134
- collection_immutable_id = self.create_collection(collection_id)["immutable_id"]
158
+ self.create_collection(collection_id)
159
+ collection_immutable_id = self.get_collection(collection_id)["immutable_id"]
135
160
  new_item["collections"] = new_item.get("collections", [])
136
161
  new_item["collections"].append({"immutable_id": collection_immutable_id})
137
162
 
@@ -252,8 +277,7 @@ class DatalabClient(BaseDatalabClient):
252
277
 
253
278
  item_data = self.get_item(item_id)
254
279
  for f in item_data.get("files", []):
255
- file_location = f["location"]
256
- url = file_location.replace("/app", self.datalab_api_url)
280
+ url = f"{self.datalab_api_url}/files/{f['immutable_id']}/{f['name']}"
257
281
  if Path(f["name"]).exists():
258
282
  warnings.warn(f"Will not overwrite existing file {f['name']}")
259
283
  continue
@@ -389,6 +413,33 @@ class DatalabClient(BaseDatalabClient):
389
413
 
390
414
  return block_data
391
415
 
416
+ def update_data_block(
417
+ self, item_id: str, block_id: str, block_type: str, block_data: dict
418
+ ) -> dict[str, Any]:
419
+ """Attempts to update a block with the given payload of data,
420
+ returning the updated block.
421
+
422
+ Parameters:
423
+ item_id: The ID of the item that the block is attached to.
424
+ block_id: The ID of existing block.
425
+ block_type: The type of block to update.
426
+ block_data: The payload of block data to update; will be used to selectively update
427
+ any fields present, with validation performed by the remote block code itself.
428
+
429
+ Returns:
430
+ If successful, the updated block data.
431
+
432
+ Raises:
433
+ RuntimeError: If the block update fails.
434
+
435
+ """
436
+ payload = {k: v for k, v in block_data.items()}
437
+ payload["block_id"] = block_id
438
+ payload["blocktype"] = block_type
439
+ payload["item_id"] = item_id
440
+
441
+ return self._update_data_block(block_type=block_type, block_data=payload)
442
+
392
443
  def _update_data_block(
393
444
  self, block_type: str, block_data: dict, file_ids: str | list[str] | None = None
394
445
  ) -> dict[str, Any]:
@@ -401,7 +452,6 @@ class DatalabClient(BaseDatalabClient):
401
452
  raise RuntimeError(
402
453
  "API does not currently support attaching multiple files in a block."
403
454
  )
404
-
405
455
  block_data["file_id"] = file_ids[0]
406
456
 
407
457
  blocks_url = f"{self.datalab_api_url}/update-block/"
@@ -43,21 +43,24 @@ def pretty_displayer(method):
43
43
  if result:
44
44
  pprint(result, max_length=None, max_string=100, max_depth=3)
45
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)
46
+ try:
47
+ table = Table(show_lines=True)
48
+ table.add_column("type", overflow="crop", justify="center")
49
+ table.add_column("ID", style="cyan", no_wrap=True)
50
+ table.add_column("refcode", style="magenta", no_wrap=True)
51
+ table.add_column("name", width=30, overflow="ellipsis", no_wrap=True)
52
+ for item in result[:page_limit]:
53
+ table.add_row(
54
+ item["type"][0].upper(),
55
+ item["item_id"],
56
+ item["refcode"],
57
+ item["name"],
58
+ end_section=True,
59
+ )
60
+ console = Console()
61
+ console.print(table)
62
+ except Exception:
63
+ pprint(result, max_length=None, max_string=100, max_depth=3)
61
64
 
62
65
  return result
63
66
 
@@ -95,6 +98,13 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
95
98
  _api_key: Optional[str] = None
96
99
  _session: Optional[httpx.Client] = None
97
100
  _headers: dict[str, str] = {}
101
+ _timeout: httpx.Timeout = httpx.Timeout(10.0, read=60.0)
102
+
103
+ info: dict[str, Any] = {}
104
+ """The `data` response from the `/info` endpoint of the Datalab API."""
105
+
106
+ block_info: list[dict[str, Any]] = []
107
+ """The `data` response from the `/info/blocks` endpoint of the Datalab API."""
98
108
 
99
109
  bad_server_versions: Optional[tuple[tuple[int, int, int]]] = ((0, 2, 0),)
100
110
  """Any known server versions that are not supported by this client."""
@@ -133,13 +143,12 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
133
143
 
134
144
  self._detect_api_url()
135
145
 
136
- info_json = self.get_info()
146
+ self.get_info()
147
+ self.get_block_info()
137
148
 
138
- self._datalab_api_versions: list[str] = info_json["data"]["attributes"][
139
- "available_api_versions"
140
- ]
141
- self._datalab_server_version: str = info_json["data"]["attributes"]["server_version"]
142
- self._datalab_instance_prefix: Optional[str] = info_json["data"]["attributes"].get(
149
+ self._datalab_api_versions: list[str] = self.info["attributes"]["available_api_versions"]
150
+ self._datalab_server_version: str = self.info["attributes"]["server_version"]
151
+ self._datalab_instance_prefix: Optional[str] = self.info["attributes"].get(
143
152
  "identifier_prefix"
144
153
  )
145
154
 
@@ -169,16 +178,25 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
169
178
  def get_info(self) -> dict[str, Any]:
170
179
  raise NotImplementedError
171
180
 
181
+ def get_block_info(self) -> list[dict[str, Any]]:
182
+ raise NotImplementedError
183
+
172
184
  @property
173
185
  def session(self) -> httpx.Client:
174
186
  if self._session is None:
175
- return self._http_client(headers=self.headers)
187
+ return self._http_client(headers=self.headers, timeout=self.timeout)
176
188
  return self._session
177
189
 
178
190
  @property
179
- def headers(self):
191
+ def headers(self) -> dict[str, str]:
192
+ """Any headers to send with each request to the datalab API."""
180
193
  return self._headers
181
194
 
195
+ @property
196
+ def timeout(self) -> httpx.Timeout:
197
+ """A timeout object to use for the datalab API session."""
198
+ return self._timeout
199
+
182
200
  def _version_negotiation(self):
183
201
  """Check whether this client is expected to work with this instance.
184
202
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: datalab-api
3
- Version: 0.2.6
3
+ Version: 0.2.8
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
@@ -24,10 +24,11 @@ License-File: LICENSE
24
24
  Requires-Dist: httpx~=0.27
25
25
  Requires-Dist: bokeh~=2.4
26
26
  Requires-Dist: rich~=13.0
27
+ Requires-Dist: numpy<2
27
28
  Provides-Extra: dev
28
- Requires-Dist: pre-commit~=3.0; extra == "dev"
29
+ Requires-Dist: pre-commit<5,>=3; extra == "dev"
29
30
  Requires-Dist: pytest~=8.0; extra == "dev"
30
- Requires-Dist: pytest-cov~=5.0; extra == "dev"
31
+ Requires-Dist: pytest-cov<7,>=5; extra == "dev"
31
32
  Requires-Dist: respx~=0.21; extra == "dev"
32
33
  Provides-Extra: docs
33
34
  Requires-Dist: mkdocs; extra == "docs"
@@ -56,11 +57,11 @@ Requires-Dist: datalab-api[cli,dev,docs]; extra == "all"
56
57
  <a href="https://pypi.org/datalab-api"><img src="https://img.shields.io/pypi/pyversions/datalab-api?logo=python&label=Python%20versions" /></a>
57
58
  </div>
58
59
 
59
- A simple Python API that can interact with [*datalab*](https://github.com/the-grey-group/datalab) instances.
60
+ A simple Python API that can interact with [*datalab*](https://github.com/datalab-org/datalab) instances.
60
61
 
61
62
  The idea here is to provide a set of utility functions and models for manipulating samples, cells, inventory, files, users and metadata associated with *datalab* entries in an automated way.
62
63
 
63
- This API may not expose all the functionality available in a given *datalab* instance, and the idea would be that this package can support multiple versions of the underlying [*datalab* REST API](https://the-datalab.readthedocs.io/en/latest/rest_api/).
64
+ This API may not expose all the functionality available in a given *datalab* instance, and the idea would be that this package can support multiple versions of the underlying [*datalab* REST API](https://docs.datalab-org.io/en/stable/rest_api/).
64
65
  This means that the API is primarily *functional* in nature, as opposed to object-oriented, since *datalab* instances are free to use their own custom data models.
65
66
  The available schemas are reported as instance metadata and in the future object-oriented models may be able to be genereated directly in the client (so e.g., the returned data would be Python objects like `Sample` rather than JSON data).
66
67
 
@@ -88,9 +89,9 @@ pip install .
88
89
  ## Usage
89
90
 
90
91
  Example usage as a Jupyter notebook can be found in the `examples` directory or
91
- in the [online documentation](https://datalab-api.readthedocs.io/), as
92
+ in the [online documentation](https://api-docs.datalab-org.io), as
92
93
  well as the full [API
93
- documentation](https://datalab-api.readthedocs.io/en/latest/reference/).
94
+ documentation](https://api-docs.datalab-org.io/en/stable/reference/).
94
95
 
95
96
  ### Authentication
96
97
 
@@ -98,7 +99,7 @@ Currently the only supported authentication method is via an API key.
98
99
  You can generate one for your account for a given *datalab* instance by visiting the `/get-api-key` endpoint of your chosen instance, or, if using a recent version of *datalab*, by visiting your account settings in the browser.
99
100
 
100
101
  This API key can be set via the environment variable `DATALAB_API_KEY`.
101
- To suport the use case of needing to interact with multiple datalab instances, the client will also check prefixed environment variables that use the [`IDENTIFIER_PREFIX` of the chosen datalab instance](https://the-datalab.readthedocs.io/en/latest/config/#mandatory-settings), e.g., `GREY_DATALAB_API_KEY` or `PUBLIC_DATALAB_API_KEY`.
102
+ To suport the use case of needing to interact with multiple datalab instances, the client will also check prefixed environment variables that use the [`IDENTIFIER_PREFIX` of the chosen datalab instance](https://docs.datalab-org.io/en/stable/config/#mandatory-settings), e.g., `GREY_DATALAB_API_KEY` or `PUBLIC_DATALAB_API_KEY`.
102
103
  Only keys that match will be read (e.g., other environment variables starting with `PUBLIC_` will be ignored, when connecting to the [public demo datalab](https://demo.datalab-org.io)).
103
104
 
104
105
  ### Python API
@@ -1,6 +1,7 @@
1
1
  httpx~=0.27
2
2
  bokeh~=2.4
3
3
  rich~=13.0
4
+ numpy<2
4
5
 
5
6
  [all]
6
7
  datalab-api[cli,dev,docs]
@@ -14,9 +15,9 @@ typer~=0.9
14
15
  click-shell~=2.1
15
16
 
16
17
  [dev]
17
- pre-commit~=3.0
18
+ pre-commit<5,>=3
18
19
  pytest~=8.0
19
- pytest-cov~=5.0
20
+ pytest-cov<7,>=5
20
21
  respx~=0.21
21
22
 
22
23
  [docs]
@@ -5,18 +5,22 @@ from httpx import Response
5
5
 
6
6
 
7
7
  @respx.mock
8
- def test_redirect_url(fake_ui_html, fake_info_json):
8
+ def test_redirect_url(fake_ui_html, fake_info_json, fake_block_info_json):
9
9
  fake_ui = respx.get("https://ui.datalab.industries").mock(
10
10
  return_value=Response(200, content=fake_ui_html)
11
11
  )
12
- fake_api = respx.get("https://api.datalab.industries/info").mock(
12
+ fake_info_api = respx.get("https://api.datalab.industries/info").mock(
13
13
  return_value=Response(200, json=fake_info_json)
14
14
  )
15
+ fake_block_info_api = respx.get("https://api.datalab.industries/info/blocks").mock(
16
+ return_value=Response(200, json=fake_block_info_json)
17
+ )
15
18
  with pytest.warns(
16
19
  UserWarning,
17
20
  match="^Found API URL https://api.datalab.industries in HTML meta tag. Creating client with this URL instead.$",
18
21
  ):
19
22
  client = DatalabClient("https://ui.datalab.industries")
20
23
  assert fake_ui.called
21
- assert fake_api.called
24
+ assert fake_info_api.called
25
+ assert fake_block_info_api.called
22
26
  assert client.datalab_api_url == "https://api.datalab.industries"
File without changes
File without changes