datalab-api 0.5.3__tar.gz → 0.6.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.5.3/src/datalab_api.egg-info → datalab_api-0.6.0}/PKG-INFO +9 -11
- {datalab_api-0.5.3 → datalab_api-0.6.0}/README.md +1 -5
- {datalab_api-0.5.3 → datalab_api-0.6.0}/pyproject.toml +15 -7
- {datalab_api-0.5.3 → datalab_api-0.6.0}/src/datalab_api/__init__.py +59 -3
- {datalab_api-0.5.3 → datalab_api-0.6.0}/src/datalab_api/_base.py +9 -13
- {datalab_api-0.5.3 → datalab_api-0.6.0}/src/datalab_api/cli.py +6 -7
- {datalab_api-0.5.3 → datalab_api-0.6.0}/src/datalab_api/utils.py +37 -1
- {datalab_api-0.5.3 → datalab_api-0.6.0/src/datalab_api.egg-info}/PKG-INFO +9 -11
- {datalab_api-0.5.3 → datalab_api-0.6.0}/src/datalab_api.egg-info/requires.txt +10 -5
- {datalab_api-0.5.3 → datalab_api-0.6.0}/tests/test_client.py +35 -2
- {datalab_api-0.5.3 → datalab_api-0.6.0}/LICENSE +0 -0
- {datalab_api-0.5.3 → datalab_api-0.6.0}/setup.cfg +0 -0
- {datalab_api-0.5.3 → datalab_api-0.6.0}/src/datalab_api.egg-info/SOURCES.txt +0 -0
- {datalab_api-0.5.3 → datalab_api-0.6.0}/src/datalab_api.egg-info/dependency_links.txt +0 -0
- {datalab_api-0.5.3 → datalab_api-0.6.0}/src/datalab_api.egg-info/entry_points.txt +0 -0
- {datalab_api-0.5.3 → datalab_api-0.6.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.6.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
|
|
@@ -9,7 +9,6 @@ Project-URL: repository, https://github.com/datalab-org/datalab-api
|
|
|
9
9
|
Project-URL: documentation, https://datalab-python-api.readthedocs.io
|
|
10
10
|
Project-URL: changelog, https://github.com/datalab-org/datalab-api/releases
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -18,13 +17,16 @@ Classifier: Intended Audience :: Science/Research
|
|
|
18
17
|
Classifier: Intended Audience :: Information Technology
|
|
19
18
|
Classifier: Topic :: Other/Nonlisted Topic
|
|
20
19
|
Classifier: Topic :: Scientific/Engineering
|
|
21
|
-
Requires-Python: >=3.
|
|
20
|
+
Requires-Python: >=3.10
|
|
22
21
|
Description-Content-Type: text/markdown
|
|
23
22
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist:
|
|
25
|
-
Requires-Dist: bokeh~=2.4; platform_system != "Emscripten"
|
|
23
|
+
Requires-Dist: httpx2~=2.13
|
|
26
24
|
Requires-Dist: rich<15,>=13
|
|
27
|
-
|
|
25
|
+
Provides-Extra: plotting
|
|
26
|
+
Requires-Dist: bokeh~=2.4; platform_system != "Emscripten" and extra == "plotting"
|
|
27
|
+
Requires-Dist: numpy<2; extra == "plotting"
|
|
28
|
+
Provides-Extra: networkx
|
|
29
|
+
Requires-Dist: networkx>=3; extra == "networkx"
|
|
28
30
|
Provides-Extra: dev
|
|
29
31
|
Requires-Dist: pre-commit<5,>=3; extra == "dev"
|
|
30
32
|
Requires-Dist: pytest~=8.0; extra == "dev"
|
|
@@ -108,7 +110,6 @@ This package implements basic functionality for displaying and manipulating entr
|
|
|
108
110
|
from datalab_api import DatalabClient
|
|
109
111
|
|
|
110
112
|
with DatalabClient("https://demo-api.datalab-org.io") as client:
|
|
111
|
-
|
|
112
113
|
# List all items of a given type
|
|
113
114
|
items = client.get_items()
|
|
114
115
|
|
|
@@ -117,7 +118,6 @@ with DatalabClient("https://demo-api.datalab-org.io") as client:
|
|
|
117
118
|
|
|
118
119
|
# Upload a file to an item
|
|
119
120
|
file_response = client.upload_file(filepath="my_echem_data.mpr", item_id="test")
|
|
120
|
-
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
### Elevated (admin) permissions
|
|
@@ -126,9 +126,7 @@ By default a client can only see items its account owns or has been shared on.
|
|
|
126
126
|
Admins can opt in to datalab's super-user mode to read items belonging to other users, by passing `elevate_permissions`:
|
|
127
127
|
|
|
128
128
|
```python
|
|
129
|
-
with DatalabClient(
|
|
130
|
-
"https://demo-api.datalab-org.io", elevate_permissions=True
|
|
131
|
-
) as client:
|
|
129
|
+
with DatalabClient("https://demo-api.datalab-org.io", elevate_permissions=True) as client:
|
|
132
130
|
# Finds the item even if it belongs to another user
|
|
133
131
|
item = client.get_item(item_id="someone-elses-sample")
|
|
134
132
|
```
|
|
@@ -63,7 +63,6 @@ This package implements basic functionality for displaying and manipulating entr
|
|
|
63
63
|
from datalab_api import DatalabClient
|
|
64
64
|
|
|
65
65
|
with DatalabClient("https://demo-api.datalab-org.io") as client:
|
|
66
|
-
|
|
67
66
|
# List all items of a given type
|
|
68
67
|
items = client.get_items()
|
|
69
68
|
|
|
@@ -72,7 +71,6 @@ with DatalabClient("https://demo-api.datalab-org.io") as client:
|
|
|
72
71
|
|
|
73
72
|
# Upload a file to an item
|
|
74
73
|
file_response = client.upload_file(filepath="my_echem_data.mpr", item_id="test")
|
|
75
|
-
|
|
76
74
|
```
|
|
77
75
|
|
|
78
76
|
### Elevated (admin) permissions
|
|
@@ -81,9 +79,7 @@ By default a client can only see items its account owns or has been shared on.
|
|
|
81
79
|
Admins can opt in to datalab's super-user mode to read items belonging to other users, by passing `elevate_permissions`:
|
|
82
80
|
|
|
83
81
|
```python
|
|
84
|
-
with DatalabClient(
|
|
85
|
-
"https://demo-api.datalab-org.io", elevate_permissions=True
|
|
86
|
-
) as client:
|
|
82
|
+
with DatalabClient("https://demo-api.datalab-org.io", elevate_permissions=True) as client:
|
|
87
83
|
# Finds the item even if it belongs to another user
|
|
88
84
|
item = client.get_item(item_id="someone-elses-sample")
|
|
89
85
|
```
|
|
@@ -12,7 +12,6 @@ authors = [{ name = "Matthew Evans", email = "matthew@ml-evs.science" }]
|
|
|
12
12
|
dynamic = ["version"]
|
|
13
13
|
classifiers = [
|
|
14
14
|
"Programming Language :: Python :: 3",
|
|
15
|
-
"Programming Language :: Python :: 3.9",
|
|
16
15
|
"Programming Language :: Python :: 3.10",
|
|
17
16
|
"Programming Language :: Python :: 3.11",
|
|
18
17
|
"Programming Language :: Python :: 3.12",
|
|
@@ -23,16 +22,23 @@ classifiers = [
|
|
|
23
22
|
"Topic :: Scientific/Engineering",
|
|
24
23
|
]
|
|
25
24
|
|
|
26
|
-
requires-python = ">=3.
|
|
25
|
+
requires-python = ">=3.10"
|
|
27
26
|
dependencies = [
|
|
28
|
-
"
|
|
29
|
-
'bokeh ~= 2.4; platform_system != "Emscripten"', # interactive plots from datalab directly
|
|
27
|
+
"httpx2 ~= 2.13", # better HTTP requests
|
|
30
28
|
"rich >= 13,< 15", # nicer terminal output
|
|
31
|
-
"numpy < 2", # upper pin for numpy due to bad pins in bokeh
|
|
32
29
|
]
|
|
33
30
|
|
|
34
31
|
|
|
35
32
|
[project.optional-dependencies]
|
|
33
|
+
plotting = [
|
|
34
|
+
"bokeh ~= 2.4; platform_system != 'Emscripten'", # interactive plots from datalab directly
|
|
35
|
+
"numpy < 2", # upper pin for numpy due to bad pins in bokeh
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
networkx = [
|
|
39
|
+
"networkx >= 3", # build graphs of item relationships
|
|
40
|
+
]
|
|
41
|
+
|
|
36
42
|
dev = [
|
|
37
43
|
"pre-commit >= 3,< 5",
|
|
38
44
|
"pytest ~= 8.0",
|
|
@@ -78,7 +84,7 @@ follow_imports = "skip"
|
|
|
78
84
|
|
|
79
85
|
[tool.ruff]
|
|
80
86
|
line-length = 100
|
|
81
|
-
target-version = "
|
|
87
|
+
target-version = "py310"
|
|
82
88
|
|
|
83
89
|
[tool.ruff.lint]
|
|
84
90
|
select = ["ALL"]
|
|
@@ -96,6 +102,7 @@ ignore = [
|
|
|
96
102
|
"C419",
|
|
97
103
|
"C901", # function too complex
|
|
98
104
|
"COM812", # trailing comma missing
|
|
105
|
+
"CPY001", # missing copyright notice
|
|
99
106
|
"D",
|
|
100
107
|
"D205",
|
|
101
108
|
"DTZ", # datetime-tz-now
|
|
@@ -120,6 +127,7 @@ ignore = [
|
|
|
120
127
|
"PERF401",
|
|
121
128
|
"PGH003",
|
|
122
129
|
"PGH004",
|
|
130
|
+
"PLC0415", # import not at top-level (used for lazy optional deps)
|
|
123
131
|
"PLR", # pylint-refactor
|
|
124
132
|
"PLW0602",
|
|
125
133
|
"PLW0603",
|
|
@@ -157,7 +165,7 @@ ignore = [
|
|
|
157
165
|
"UP031",
|
|
158
166
|
]
|
|
159
167
|
pydocstyle.convention = "numpy"
|
|
160
|
-
isort.known-first-party = ["
|
|
168
|
+
isort.known-first-party = ["datalab_api"]
|
|
161
169
|
isort.split-on-trailing-comma = false
|
|
162
170
|
fixable = ["A", "B", "C", "D", "E", "F", "I", "PT", "RUF"]
|
|
163
171
|
unfixable = []
|
|
@@ -3,11 +3,15 @@ from __future__ import annotations
|
|
|
3
3
|
import warnings
|
|
4
4
|
from collections.abc import Iterable
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Any, Optional, Union
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Optional, Union
|
|
7
7
|
|
|
8
8
|
import httpx
|
|
9
9
|
|
|
10
10
|
from ._base import BaseDatalabClient, DatalabAPIError, DuplicateItemError, __version__
|
|
11
|
+
from .utils import networkx_from_item_graph
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
import networkx as nx
|
|
11
15
|
|
|
12
16
|
__all__ = ("DatalabClient", "DuplicateItemError", "__version__")
|
|
13
17
|
|
|
@@ -253,6 +257,59 @@ class DatalabClient(BaseDatalabClient):
|
|
|
253
257
|
|
|
254
258
|
return item["item_data"]
|
|
255
259
|
|
|
260
|
+
def get_item_graph(
|
|
261
|
+
self,
|
|
262
|
+
item_id: str | None = None,
|
|
263
|
+
collection_id: str | None = None,
|
|
264
|
+
hide_collections: bool | None = None,
|
|
265
|
+
max_depth: int | None = None,
|
|
266
|
+
as_networkx: bool = False,
|
|
267
|
+
) -> dict[str, list[dict[str, Any]]] | nx.DiGraph:
|
|
268
|
+
"""Get the graph of relationships between items.
|
|
269
|
+
|
|
270
|
+
With no arguments, the global item graph for all items visible to the
|
|
271
|
+
authenticated user is returned. If `item_id` is provided, only the
|
|
272
|
+
neighbourhood of that item is returned.
|
|
273
|
+
|
|
274
|
+
Parameters:
|
|
275
|
+
item_id: The ID of the item to centre the graph on (optional).
|
|
276
|
+
If not provided, the global graph is returned.
|
|
277
|
+
collection_id: Restrict the global graph to members of this
|
|
278
|
+
collection (optional; ignored when `item_id` is provided).
|
|
279
|
+
hide_collections: Whether to omit collections as nodes in the graph.
|
|
280
|
+
If `None`, the server default is used (hidden, for recent versions).
|
|
281
|
+
max_depth: The number of relationship hops to follow from `item_id`.
|
|
282
|
+
If `None`, the server default is used (1). Requires a recent
|
|
283
|
+
version of datalab.
|
|
284
|
+
as_networkx: Whether to return the graph as a `networkx.DiGraph`
|
|
285
|
+
(requires the optional `networkx` dependency).
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
A dictionary with `nodes` and `edges` keys, in the Cytoscape.js
|
|
289
|
+
format used by the datalab UI, or a `networkx.DiGraph` if
|
|
290
|
+
`as_networkx` is `True`.
|
|
291
|
+
|
|
292
|
+
"""
|
|
293
|
+
graph_url = f"{self.datalab_api_url}/item-graph"
|
|
294
|
+
if item_id is not None:
|
|
295
|
+
graph_url += f"/{item_id}"
|
|
296
|
+
|
|
297
|
+
params: dict[str, Any] = {}
|
|
298
|
+
if collection_id is not None:
|
|
299
|
+
params["collection_id"] = collection_id
|
|
300
|
+
if hide_collections is not None:
|
|
301
|
+
params["hide_collections"] = str(hide_collections).lower()
|
|
302
|
+
if max_depth is not None:
|
|
303
|
+
params["max_depth"] = max_depth
|
|
304
|
+
|
|
305
|
+
graph = self._get(graph_url, params=params)
|
|
306
|
+
graph = {"nodes": graph.get("nodes", []), "edges": graph.get("edges", [])}
|
|
307
|
+
|
|
308
|
+
if as_networkx:
|
|
309
|
+
return networkx_from_item_graph(graph)
|
|
310
|
+
|
|
311
|
+
return graph
|
|
312
|
+
|
|
256
313
|
def get_item_files(self, item_id: str) -> None:
|
|
257
314
|
"""Download all the files for a given item and save them locally
|
|
258
315
|
in the current working directory.
|
|
@@ -275,8 +332,7 @@ class DatalabClient(BaseDatalabClient):
|
|
|
275
332
|
with self.session.stream(
|
|
276
333
|
"GET", url, follow_redirects=True, **stream_kwargs
|
|
277
334
|
) as response:
|
|
278
|
-
|
|
279
|
-
file.write(chunk)
|
|
335
|
+
file.writelines(response.iter_bytes(chunk_size=1024))
|
|
280
336
|
|
|
281
337
|
def get_block(
|
|
282
338
|
self,
|
|
@@ -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
|
|
7
|
+
from typing import Any
|
|
8
8
|
|
|
9
9
|
import httpx
|
|
10
10
|
from rich.logging import RichHandler
|
|
@@ -33,8 +33,8 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
|
|
|
33
33
|
readable by users.
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
|
-
_api_key:
|
|
37
|
-
_session:
|
|
36
|
+
_api_key: str | None = None
|
|
37
|
+
_session: httpx.Client | None = None
|
|
38
38
|
_headers: dict[str, str]
|
|
39
39
|
_timeout: httpx.Timeout = httpx.Timeout(10.0, read=60.0)
|
|
40
40
|
|
|
@@ -52,7 +52,7 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
|
|
|
52
52
|
block_info: list[dict[str, Any]] = []
|
|
53
53
|
"""The `data` response from the `/info/blocks` endpoint of the Datalab API."""
|
|
54
54
|
|
|
55
|
-
bad_server_versions:
|
|
55
|
+
bad_server_versions: tuple[tuple[int, int, int]] | None = ((0, 2, 0),)
|
|
56
56
|
"""Any known server versions that are not supported by this client."""
|
|
57
57
|
|
|
58
58
|
min_server_version: tuple[int, int, int] = (0, 1, 0)
|
|
@@ -113,9 +113,7 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
|
|
|
113
113
|
|
|
114
114
|
self._datalab_api_versions: list[str] = self.info["attributes"]["available_api_versions"]
|
|
115
115
|
self._datalab_server_version: str = self.info["attributes"]["server_version"]
|
|
116
|
-
self._datalab_instance_prefix:
|
|
117
|
-
"identifier_prefix"
|
|
118
|
-
)
|
|
116
|
+
self._datalab_instance_prefix: str | None = self.info["attributes"].get("identifier_prefix")
|
|
119
117
|
|
|
120
118
|
self._find_api_key()
|
|
121
119
|
|
|
@@ -203,7 +201,7 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
|
|
|
203
201
|
if self._api_key is None:
|
|
204
202
|
key_env_var = "DATALAB_API_KEY"
|
|
205
203
|
|
|
206
|
-
api_key:
|
|
204
|
+
api_key: str | None = None
|
|
207
205
|
|
|
208
206
|
# probe the prefixed environment variable first
|
|
209
207
|
if self._datalab_instance_prefix is not None:
|
|
@@ -248,7 +246,7 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
|
|
|
248
246
|
self._session.close()
|
|
249
247
|
|
|
250
248
|
def _handle_response(
|
|
251
|
-
self, response: httpx.Response, url: str, expected_status:
|
|
249
|
+
self, response: httpx.Response, url: str, expected_status: int | list[int] = 200
|
|
252
250
|
) -> dict[str, Any]:
|
|
253
251
|
"""Handle HTTP response with consistent error handling.
|
|
254
252
|
|
|
@@ -372,7 +370,7 @@ This is likely a server-side bug. Please report this issue to the datalab develo
|
|
|
372
370
|
return completed_tasks, error_tasks
|
|
373
371
|
|
|
374
372
|
def _request(
|
|
375
|
-
self, method: str, url: str, expected_status:
|
|
373
|
+
self, method: str, url: str, expected_status: int | list[int] = 200, **kwargs
|
|
376
374
|
) -> dict[str, Any]:
|
|
377
375
|
"""Make an HTTP request with consistent error handling.
|
|
378
376
|
|
|
@@ -415,9 +413,7 @@ This is likely a server-side bug. Please report this issue to the datalab develo
|
|
|
415
413
|
"""Make a GET request with error handling."""
|
|
416
414
|
return self._request("GET", url, **kwargs)
|
|
417
415
|
|
|
418
|
-
def _post(
|
|
419
|
-
self, url: str, expected_status: Union[int, list[int]] = 200, **kwargs
|
|
420
|
-
) -> dict[str, Any]:
|
|
416
|
+
def _post(self, url: str, expected_status: int | list[int] = 200, **kwargs) -> dict[str, Any]:
|
|
421
417
|
"""Make a POST request with error handling."""
|
|
422
418
|
return self._request("POST", url, expected_status, **kwargs)
|
|
423
419
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import random
|
|
1
2
|
import time
|
|
2
|
-
from typing import Annotated
|
|
3
|
+
from typing import Annotated
|
|
3
4
|
|
|
4
5
|
import typer
|
|
5
6
|
from click_shell import make_click_shell
|
|
@@ -23,7 +24,7 @@ console = Console()
|
|
|
23
24
|
@app.callback(invoke_without_command=True)
|
|
24
25
|
def launch(
|
|
25
26
|
ctx: typer.Context,
|
|
26
|
-
instance_url: Annotated[
|
|
27
|
+
instance_url: Annotated[str | None, typer.Argument()] = None,
|
|
27
28
|
animate_intro: bool = True,
|
|
28
29
|
):
|
|
29
30
|
"""Makes an interactive REPL-style interface using the subcommands below."""
|
|
@@ -68,7 +69,7 @@ def launch(
|
|
|
68
69
|
|
|
69
70
|
def _get_client(
|
|
70
71
|
ctx: typer.Context,
|
|
71
|
-
instance_url:
|
|
72
|
+
instance_url: str | None = None,
|
|
72
73
|
log_level: str = "WARNING",
|
|
73
74
|
):
|
|
74
75
|
client = getattr(ctx, "client", None)
|
|
@@ -91,7 +92,7 @@ def _get_instance_url(ctx: typer.Context):
|
|
|
91
92
|
@app.command()
|
|
92
93
|
def authenticate(
|
|
93
94
|
ctx: typer.Context,
|
|
94
|
-
instance_url: Annotated[
|
|
95
|
+
instance_url: Annotated[str | None, typer.Argument()] = None,
|
|
95
96
|
log_level: str = "WARNING",
|
|
96
97
|
):
|
|
97
98
|
client = _get_client(ctx, instance_url, log_level)
|
|
@@ -105,7 +106,7 @@ def authenticate(
|
|
|
105
106
|
def get(
|
|
106
107
|
ctx: typer.Context,
|
|
107
108
|
item_type: str,
|
|
108
|
-
instance_url: Annotated[
|
|
109
|
+
instance_url: Annotated[str | None, typer.Argument()] = None,
|
|
109
110
|
page_limit: int = 10,
|
|
110
111
|
log_level: str = "WARNING",
|
|
111
112
|
):
|
|
@@ -143,8 +144,6 @@ def info(ctx: typer.Context, instance_url: str, log_level: str = "WARNING"):
|
|
|
143
144
|
|
|
144
145
|
def _make_fancy_intro(animate=True):
|
|
145
146
|
"""Bit of fun, make an animated datalab logo intro to the CLI."""
|
|
146
|
-
import random
|
|
147
|
-
|
|
148
147
|
intro_ascii = """
|
|
149
148
|
oooo o8 o888 oooo
|
|
150
149
|
ooooo888 ooooooo o888oo ooooooo 888 ooooooo 888ooooo
|
|
@@ -4,7 +4,7 @@ from rich.console import Console
|
|
|
4
4
|
from rich.pretty import pprint
|
|
5
5
|
from rich.table import Table
|
|
6
6
|
|
|
7
|
-
__all__ = ("AutoPrettyPrint", "bokeh_from_json", "pretty_displayer")
|
|
7
|
+
__all__ = ("AutoPrettyPrint", "bokeh_from_json", "networkx_from_item_graph", "pretty_displayer")
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def pretty_displayer(method):
|
|
@@ -77,3 +77,39 @@ def bokeh_from_json(block_data, show=True):
|
|
|
77
77
|
bokeh_show(curdoc().roots[0])
|
|
78
78
|
|
|
79
79
|
return curdoc()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def networkx_from_item_graph(graph):
|
|
83
|
+
"""Convert an item graph, as returned by the `/item-graph` endpoint,
|
|
84
|
+
into a `networkx.DiGraph`.
|
|
85
|
+
|
|
86
|
+
Nodes are keyed by their ID (item ID, or `"Collection: <collection_id>"`
|
|
87
|
+
for collections), with all other node data stored as attributes.
|
|
88
|
+
Edges point from parent to child.
|
|
89
|
+
|
|
90
|
+
Parameters:
|
|
91
|
+
graph: A dictionary with `nodes` and `edges` keys in Cytoscape.js format.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
A `networkx.DiGraph` of the item relationships.
|
|
95
|
+
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
import networkx as nx
|
|
99
|
+
except ImportError as exc:
|
|
100
|
+
raise ImportError(
|
|
101
|
+
"The `networkx` package is required to create a graph; "
|
|
102
|
+
"install it with `pip install datalab-api[networkx]`."
|
|
103
|
+
) from exc
|
|
104
|
+
|
|
105
|
+
nx_graph = nx.DiGraph()
|
|
106
|
+
for node in graph.get("nodes", []):
|
|
107
|
+
data = dict(node["data"])
|
|
108
|
+
nx_graph.add_node(data.pop("id"), **data)
|
|
109
|
+
for edge in graph.get("edges", []):
|
|
110
|
+
data = dict(edge["data"])
|
|
111
|
+
source, target = data.pop("source"), data.pop("target")
|
|
112
|
+
data.pop("id", None)
|
|
113
|
+
nx_graph.add_edge(source, target, **data)
|
|
114
|
+
|
|
115
|
+
return nx_graph
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datalab-api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -9,7 +9,6 @@ Project-URL: repository, https://github.com/datalab-org/datalab-api
|
|
|
9
9
|
Project-URL: documentation, https://datalab-python-api.readthedocs.io
|
|
10
10
|
Project-URL: changelog, https://github.com/datalab-org/datalab-api/releases
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -18,13 +17,16 @@ Classifier: Intended Audience :: Science/Research
|
|
|
18
17
|
Classifier: Intended Audience :: Information Technology
|
|
19
18
|
Classifier: Topic :: Other/Nonlisted Topic
|
|
20
19
|
Classifier: Topic :: Scientific/Engineering
|
|
21
|
-
Requires-Python: >=3.
|
|
20
|
+
Requires-Python: >=3.10
|
|
22
21
|
Description-Content-Type: text/markdown
|
|
23
22
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist:
|
|
25
|
-
Requires-Dist: bokeh~=2.4; platform_system != "Emscripten"
|
|
23
|
+
Requires-Dist: httpx2~=2.13
|
|
26
24
|
Requires-Dist: rich<15,>=13
|
|
27
|
-
|
|
25
|
+
Provides-Extra: plotting
|
|
26
|
+
Requires-Dist: bokeh~=2.4; platform_system != "Emscripten" and extra == "plotting"
|
|
27
|
+
Requires-Dist: numpy<2; extra == "plotting"
|
|
28
|
+
Provides-Extra: networkx
|
|
29
|
+
Requires-Dist: networkx>=3; extra == "networkx"
|
|
28
30
|
Provides-Extra: dev
|
|
29
31
|
Requires-Dist: pre-commit<5,>=3; extra == "dev"
|
|
30
32
|
Requires-Dist: pytest~=8.0; extra == "dev"
|
|
@@ -108,7 +110,6 @@ This package implements basic functionality for displaying and manipulating entr
|
|
|
108
110
|
from datalab_api import DatalabClient
|
|
109
111
|
|
|
110
112
|
with DatalabClient("https://demo-api.datalab-org.io") as client:
|
|
111
|
-
|
|
112
113
|
# List all items of a given type
|
|
113
114
|
items = client.get_items()
|
|
114
115
|
|
|
@@ -117,7 +118,6 @@ with DatalabClient("https://demo-api.datalab-org.io") as client:
|
|
|
117
118
|
|
|
118
119
|
# Upload a file to an item
|
|
119
120
|
file_response = client.upload_file(filepath="my_echem_data.mpr", item_id="test")
|
|
120
|
-
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
### Elevated (admin) permissions
|
|
@@ -126,9 +126,7 @@ By default a client can only see items its account owns or has been shared on.
|
|
|
126
126
|
Admins can opt in to datalab's super-user mode to read items belonging to other users, by passing `elevate_permissions`:
|
|
127
127
|
|
|
128
128
|
```python
|
|
129
|
-
with DatalabClient(
|
|
130
|
-
"https://demo-api.datalab-org.io", elevate_permissions=True
|
|
131
|
-
) as client:
|
|
129
|
+
with DatalabClient("https://demo-api.datalab-org.io", elevate_permissions=True) as client:
|
|
132
130
|
# Finds the item even if it belongs to another user
|
|
133
131
|
item = client.get_item(item_id="someone-elses-sample")
|
|
134
132
|
```
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
httpx2~=2.13
|
|
2
2
|
rich<15,>=13
|
|
3
|
-
numpy<2
|
|
4
|
-
|
|
5
|
-
[:platform_system != "Emscripten"]
|
|
6
|
-
bokeh~=2.4
|
|
7
3
|
|
|
8
4
|
[all]
|
|
9
5
|
datalab-api[cli,dev,docs]
|
|
@@ -24,3 +20,12 @@ mkdocs-material
|
|
|
24
20
|
mkdocstrings[python-legacy]
|
|
25
21
|
mkdocs-awesome-pages-plugin
|
|
26
22
|
mkdocs-jupyter
|
|
23
|
+
|
|
24
|
+
[networkx]
|
|
25
|
+
networkx>=3
|
|
26
|
+
|
|
27
|
+
[plotting]
|
|
28
|
+
numpy<2
|
|
29
|
+
|
|
30
|
+
[plotting:platform_system != "Emscripten"]
|
|
31
|
+
bokeh~=2.4
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
1
3
|
import pytest
|
|
2
4
|
import respx
|
|
3
5
|
from httpx import Response
|
|
@@ -40,7 +42,9 @@ def test_sample_operations(mocked_api, fake_api_url):
|
|
|
40
42
|
# Check that the full server error message is shown for a missing item
|
|
41
43
|
with pytest.raises(
|
|
42
44
|
DatalabAPIError,
|
|
43
|
-
match=
|
|
45
|
+
match=re.escape(
|
|
46
|
+
"No matching items for match={'item_id': 'TEST'} with current authorization"
|
|
47
|
+
),
|
|
44
48
|
):
|
|
45
49
|
client.get_item("TEST", display=True)
|
|
46
50
|
assert mocked_api["sample-missing-TEST"].called
|
|
@@ -57,7 +61,7 @@ def test_collection_get(fake_api_url, mocked_api):
|
|
|
57
61
|
assert mocked_api["info"].called
|
|
58
62
|
assert mocked_api["info-blocks"].called
|
|
59
63
|
|
|
60
|
-
|
|
64
|
+
_collection, _children = client.get_collection("test_collection", display=True)
|
|
61
65
|
assert mocked_api["collection-test_collection"].called
|
|
62
66
|
|
|
63
67
|
|
|
@@ -169,3 +173,32 @@ def test_elevate_permissions_preserves_existing_params(mocked_api, fake_api_url)
|
|
|
169
173
|
request = mocked_api["sample-KUVEKJ"].calls.last.request
|
|
170
174
|
assert request.url.params.get("load_blocks") == "false"
|
|
171
175
|
assert request.url.params.get("sudo") == "1"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_get_item_graph(mocked_api, fake_api_url):
|
|
179
|
+
with DatalabClient(fake_api_url) as client:
|
|
180
|
+
graph = client.get_item_graph(display=True)
|
|
181
|
+
assert mocked_api["item-graph"].called
|
|
182
|
+
assert not mocked_api["item-graph"].calls.last.request.url.params
|
|
183
|
+
assert len(graph["nodes"]) == 3
|
|
184
|
+
assert len(graph["edges"]) == 2
|
|
185
|
+
|
|
186
|
+
graph = client.get_item_graph(
|
|
187
|
+
"KUVEKJ", hide_collections=False, max_depth=2, collection_id="test_collection"
|
|
188
|
+
)
|
|
189
|
+
params = mocked_api["item-graph-KUVEKJ"].calls.last.request.url.params
|
|
190
|
+
assert params["hide_collections"] == "false"
|
|
191
|
+
assert params["max_depth"] == "2"
|
|
192
|
+
assert params["collection_id"] == "test_collection"
|
|
193
|
+
assert set(graph) == {"nodes", "edges"}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_get_item_graph_networkx(mocked_api, fake_api_url):
|
|
197
|
+
nx = pytest.importorskip("networkx")
|
|
198
|
+
with DatalabClient(fake_api_url) as client:
|
|
199
|
+
graph = client.get_item_graph("KUVEKJ", as_networkx=True)
|
|
200
|
+
assert isinstance(graph, nx.DiGraph)
|
|
201
|
+
assert set(graph.nodes) == {"parent", "KUVEKJ", "sibling"}
|
|
202
|
+
assert set(graph.successors("parent")) == {"KUVEKJ", "sibling"}
|
|
203
|
+
assert graph.nodes["KUVEKJ"]["name"] == "Child"
|
|
204
|
+
assert graph.nodes["KUVEKJ"]["special"] is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|