datalab-api 0.5.1__tar.gz → 0.5.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.
- {datalab_api-0.5.1/src/datalab_api.egg-info → datalab_api-0.5.3}/PKG-INFO +26 -1
- {datalab_api-0.5.1 → datalab_api-0.5.3}/README.md +25 -0
- {datalab_api-0.5.1 → datalab_api-0.5.3}/src/datalab_api/__init__.py +27 -4
- {datalab_api-0.5.1 → datalab_api-0.5.3}/src/datalab_api/_base.py +43 -1
- {datalab_api-0.5.1 → datalab_api-0.5.3/src/datalab_api.egg-info}/PKG-INFO +26 -1
- datalab_api-0.5.3/tests/test_client.py +171 -0
- datalab_api-0.5.1/tests/test_client.py +0 -73
- {datalab_api-0.5.1 → datalab_api-0.5.3}/LICENSE +0 -0
- {datalab_api-0.5.1 → datalab_api-0.5.3}/pyproject.toml +0 -0
- {datalab_api-0.5.1 → datalab_api-0.5.3}/setup.cfg +0 -0
- {datalab_api-0.5.1 → datalab_api-0.5.3}/src/datalab_api/cli.py +0 -0
- {datalab_api-0.5.1 → datalab_api-0.5.3}/src/datalab_api/utils.py +0 -0
- {datalab_api-0.5.1 → datalab_api-0.5.3}/src/datalab_api.egg-info/SOURCES.txt +0 -0
- {datalab_api-0.5.1 → datalab_api-0.5.3}/src/datalab_api.egg-info/dependency_links.txt +0 -0
- {datalab_api-0.5.1 → datalab_api-0.5.3}/src/datalab_api.egg-info/entry_points.txt +0 -0
- {datalab_api-0.5.1 → datalab_api-0.5.3}/src/datalab_api.egg-info/requires.txt +0 -0
- {datalab_api-0.5.1 → datalab_api-0.5.3}/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.5.
|
|
3
|
+
Version: 0.5.3
|
|
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
|
|
@@ -120,6 +120,31 @@ with DatalabClient("https://demo-api.datalab-org.io") as client:
|
|
|
120
120
|
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
### Elevated (admin) permissions
|
|
124
|
+
|
|
125
|
+
By default a client can only see items its account owns or has been shared on.
|
|
126
|
+
Admins can opt in to datalab's super-user mode to read items belonging to other users, by passing `elevate_permissions`:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
with DatalabClient(
|
|
130
|
+
"https://demo-api.datalab-org.io", elevate_permissions=True
|
|
131
|
+
) as client:
|
|
132
|
+
# Finds the item even if it belongs to another user
|
|
133
|
+
item = client.get_item(item_id="someone-elses-sample")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This sends datalab's `sudo=1` flag on read requests.
|
|
137
|
+
It requires the API key in use to belong to an **admin** account; for any other account the server ignores
|
|
138
|
+
it and normal permissions apply.
|
|
139
|
+
|
|
140
|
+
The attribute can also be toggled at any point in the client's lifetime:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
client.elevate_permissions = True
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Note that objects created while elevated (e.g. uploaded files) are still recorded as belonging to the admin's own account, not the item owner's.
|
|
147
|
+
|
|
123
148
|
### Command-line interface (CLI)
|
|
124
149
|
|
|
125
150
|
There is also an EXPERIMENTAL CLI, exposed via the `datalab` command.
|
|
@@ -75,6 +75,31 @@ with DatalabClient("https://demo-api.datalab-org.io") as client:
|
|
|
75
75
|
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
### Elevated (admin) permissions
|
|
79
|
+
|
|
80
|
+
By default a client can only see items its account owns or has been shared on.
|
|
81
|
+
Admins can opt in to datalab's super-user mode to read items belonging to other users, by passing `elevate_permissions`:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
with DatalabClient(
|
|
85
|
+
"https://demo-api.datalab-org.io", elevate_permissions=True
|
|
86
|
+
) as client:
|
|
87
|
+
# Finds the item even if it belongs to another user
|
|
88
|
+
item = client.get_item(item_id="someone-elses-sample")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
This sends datalab's `sudo=1` flag on read requests.
|
|
92
|
+
It requires the API key in use to belong to an **admin** account; for any other account the server ignores
|
|
93
|
+
it and normal permissions apply.
|
|
94
|
+
|
|
95
|
+
The attribute can also be toggled at any point in the client's lifetime:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
client.elevate_permissions = True
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Note that objects created while elevated (e.g. uploaded files) are still recorded as belonging to the admin's own account, not the item owner's.
|
|
102
|
+
|
|
78
103
|
### Command-line interface (CLI)
|
|
79
104
|
|
|
80
105
|
There is also an EXPERIMENTAL CLI, exposed via the `datalab` command.
|
|
@@ -268,8 +268,13 @@ class DatalabClient(BaseDatalabClient):
|
|
|
268
268
|
if Path(f["name"]).exists():
|
|
269
269
|
warnings.warn(f"Will not overwrite existing file {f['name']}")
|
|
270
270
|
continue
|
|
271
|
+
# This streams directly from the session rather than going through
|
|
272
|
+
# `_request`, so the elevation flag has to be applied by hand.
|
|
273
|
+
stream_kwargs = self._apply_elevation("GET", {})
|
|
271
274
|
with open(f["name"], "wb") as file:
|
|
272
|
-
with self.session.stream(
|
|
275
|
+
with self.session.stream(
|
|
276
|
+
"GET", url, follow_redirects=True, **stream_kwargs
|
|
277
|
+
) as response:
|
|
273
278
|
for chunk in response.iter_bytes(chunk_size=1024):
|
|
274
279
|
file.write(chunk)
|
|
275
280
|
|
|
@@ -322,7 +327,12 @@ class DatalabClient(BaseDatalabClient):
|
|
|
322
327
|
If provided, the new file will take the place of the old file in any blocks it was attached to.
|
|
323
328
|
|
|
324
329
|
Returns:
|
|
325
|
-
A dictionary of the uploaded file data
|
|
330
|
+
A dictionary of the uploaded file data, with an additional
|
|
331
|
+
`not_modified` key. If `replace_file_id` was provided and the
|
|
332
|
+
server already holds identical content (compared by hash), the
|
|
333
|
+
update is a no-op: the server replies `304 Not Modified` and
|
|
334
|
+
`not_modified` is `True`. Servers older than datalab#1744 never
|
|
335
|
+
report this, so `not_modified` is always `False` against them.
|
|
326
336
|
|
|
327
337
|
"""
|
|
328
338
|
if isinstance(file_path, str):
|
|
@@ -341,10 +351,23 @@ class DatalabClient(BaseDatalabClient):
|
|
|
341
351
|
upload_url,
|
|
342
352
|
files=files,
|
|
343
353
|
data={"item_id": item_id, "replace_file": replace_file_id},
|
|
344
|
-
expected_status=201,
|
|
354
|
+
expected_status=[201, 304],
|
|
345
355
|
timeout=upload_timeout,
|
|
346
356
|
)
|
|
347
|
-
|
|
357
|
+
|
|
358
|
+
# A 201 always carries a body describing the stored file, so an
|
|
359
|
+
# empty dict here can only be the 304: the server compared hashes,
|
|
360
|
+
# found it already holds this content, and made no update. That is
|
|
361
|
+
# a success, so report it rather than raising.
|
|
362
|
+
if not upload:
|
|
363
|
+
return {
|
|
364
|
+
"status": "success",
|
|
365
|
+
"file_id": replace_file_id,
|
|
366
|
+
"is_update": True,
|
|
367
|
+
"not_modified": True,
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return {**upload, "not_modified": False}
|
|
348
371
|
|
|
349
372
|
def create_data_block(
|
|
350
373
|
self,
|
|
@@ -58,7 +58,22 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
|
|
|
58
58
|
min_server_version: tuple[int, int, int] = (0, 1, 0)
|
|
59
59
|
"""The minimum supported server version that this client supports."""
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
elevate_permissions: bool = False
|
|
62
|
+
"""Whether to make requests in datalab's admin super-user mode, i.e., with
|
|
63
|
+
access to items belonging to other users.
|
|
64
|
+
|
|
65
|
+
This only has an effect if the API key in use belongs to an admin account;
|
|
66
|
+
for any other account the server ignores the request and normal permissions
|
|
67
|
+
apply. Can be set at any point in the client's lifetime to toggle the
|
|
68
|
+
behaviour.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
datalab_api_url: str,
|
|
74
|
+
log_level: str = "WARNING",
|
|
75
|
+
elevate_permissions: bool = False,
|
|
76
|
+
):
|
|
62
77
|
"""Creates an authenticated client.
|
|
63
78
|
|
|
64
79
|
An API key is required to authenticate requests. The client will attempt to load it from a
|
|
@@ -72,10 +87,14 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
|
|
|
72
87
|
to resolve the underlying API URL (e.g., `https://demo-api.datalab-org.io`
|
|
73
88
|
will 'redirect' to `https://demo-api.datalab-org.io`).
|
|
74
89
|
log_level: The logging level to use for the client. Defaults to "WARNING".
|
|
90
|
+
elevate_permissions: Whether to read items belonging to other users, using
|
|
91
|
+
datalab's admin super-user mode. Requires the API key in use to belong
|
|
92
|
+
to an admin account; ignored by the server otherwise. Defaults to False.
|
|
75
93
|
|
|
76
94
|
|
|
77
95
|
"""
|
|
78
96
|
|
|
97
|
+
self.elevate_permissions = elevate_permissions
|
|
79
98
|
self.datalab_api_url = datalab_api_url
|
|
80
99
|
if not self.datalab_api_url:
|
|
81
100
|
raise ValueError("No Datalab API URL provided.")
|
|
@@ -317,6 +336,12 @@ This is likely a server-side bug. Please report this issue to the datalab develo
|
|
|
317
336
|
f"HTTP {response.status_code} error at {url}: {error_message}"
|
|
318
337
|
)
|
|
319
338
|
|
|
339
|
+
# 204 and 304 carry no entity body by definition
|
|
340
|
+
# Where such a status is expected, an empty body
|
|
341
|
+
# is the success case rather than a parse failure.
|
|
342
|
+
if response.status_code in (204, 304):
|
|
343
|
+
return {}
|
|
344
|
+
|
|
320
345
|
# Parse JSON response
|
|
321
346
|
try:
|
|
322
347
|
data = response.json()
|
|
@@ -361,6 +386,7 @@ This is likely a server-side bug. Please report this issue to the datalab develo
|
|
|
361
386
|
The JSON response data
|
|
362
387
|
"""
|
|
363
388
|
timeout = kwargs.pop("timeout", None)
|
|
389
|
+
kwargs = self._apply_elevation(method, kwargs)
|
|
364
390
|
try:
|
|
365
391
|
response = self.session.request(
|
|
366
392
|
method, url, follow_redirects=True, timeout=timeout, **kwargs
|
|
@@ -369,6 +395,22 @@ This is likely a server-side bug. Please report this issue to the datalab develo
|
|
|
369
395
|
except (httpx.RequestError, httpx.HTTPStatusError) as e:
|
|
370
396
|
raise DatalabAPIError(f"Request failed for {url}: {e}")
|
|
371
397
|
|
|
398
|
+
def _apply_elevation(self, method: str, kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
399
|
+
"""Add datalab's super-user flag to a read when running elevated.
|
|
400
|
+
|
|
401
|
+
datalab treats an admin as an ordinary user on GET requests unless the
|
|
402
|
+
request opts in with `sudo=1`, but grants them full access on writes
|
|
403
|
+
with no opt-in at all — so only reads need the flag. Any `params` the
|
|
404
|
+
caller supplied are preserved.
|
|
405
|
+
"""
|
|
406
|
+
if not self.elevate_permissions or method.upper() != "GET":
|
|
407
|
+
return kwargs
|
|
408
|
+
|
|
409
|
+
params = dict(kwargs.pop("params", None) or {})
|
|
410
|
+
params.setdefault("sudo", "1")
|
|
411
|
+
kwargs["params"] = params
|
|
412
|
+
return kwargs
|
|
413
|
+
|
|
372
414
|
def _get(self, url: str, **kwargs) -> dict[str, Any]:
|
|
373
415
|
"""Make a GET request with error handling."""
|
|
374
416
|
return self._request("GET", url, **kwargs)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datalab-api
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
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
|
|
@@ -120,6 +120,31 @@ with DatalabClient("https://demo-api.datalab-org.io") as client:
|
|
|
120
120
|
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
### Elevated (admin) permissions
|
|
124
|
+
|
|
125
|
+
By default a client can only see items its account owns or has been shared on.
|
|
126
|
+
Admins can opt in to datalab's super-user mode to read items belonging to other users, by passing `elevate_permissions`:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
with DatalabClient(
|
|
130
|
+
"https://demo-api.datalab-org.io", elevate_permissions=True
|
|
131
|
+
) as client:
|
|
132
|
+
# Finds the item even if it belongs to another user
|
|
133
|
+
item = client.get_item(item_id="someone-elses-sample")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This sends datalab's `sudo=1` flag on read requests.
|
|
137
|
+
It requires the API key in use to belong to an **admin** account; for any other account the server ignores
|
|
138
|
+
it and normal permissions apply.
|
|
139
|
+
|
|
140
|
+
The attribute can also be toggled at any point in the client's lifetime:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
client.elevate_permissions = True
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Note that objects created while elevated (e.g. uploaded files) are still recorded as belonging to the admin's own account, not the item owner's.
|
|
147
|
+
|
|
123
148
|
### Command-line interface (CLI)
|
|
124
149
|
|
|
125
150
|
There is also an EXPERIMENTAL CLI, exposed via the `datalab` command.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import respx
|
|
3
|
+
from httpx import Response
|
|
4
|
+
|
|
5
|
+
from datalab_api import DatalabClient
|
|
6
|
+
from datalab_api._base import DatalabAPIError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_redirect_url(mocked_api, mocked_ui, fake_api_url, fake_ui_url):
|
|
10
|
+
with pytest.warns(
|
|
11
|
+
UserWarning,
|
|
12
|
+
match="^Found API URL https://api.datalab.industries in HTML meta tag. Creating client with this URL instead.$",
|
|
13
|
+
):
|
|
14
|
+
client = DatalabClient(fake_ui_url)
|
|
15
|
+
|
|
16
|
+
assert mocked_ui["ui-redirect"].called
|
|
17
|
+
assert mocked_api["info"].called
|
|
18
|
+
assert mocked_api["info-blocks"].called
|
|
19
|
+
assert client.datalab_api_url == fake_api_url
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_sample_operations(mocked_api, fake_api_url):
|
|
23
|
+
with DatalabClient(fake_api_url) as client:
|
|
24
|
+
assert mocked_api["api"].called
|
|
25
|
+
assert mocked_api["info"].called
|
|
26
|
+
assert mocked_api["info-blocks"].called
|
|
27
|
+
|
|
28
|
+
client.authenticate()
|
|
29
|
+
assert mocked_api["current_user"].called
|
|
30
|
+
|
|
31
|
+
samples = client.get_items(display=True)
|
|
32
|
+
assert mocked_api["samples"].called
|
|
33
|
+
assert len(samples)
|
|
34
|
+
assert samples[0]["item_id"] == "test"
|
|
35
|
+
|
|
36
|
+
sample = client.get_item("KUVEKJ", display=True)
|
|
37
|
+
assert mocked_api["sample-KUVEKJ"].called
|
|
38
|
+
assert sample["item_id"] == "KUVEKJ"
|
|
39
|
+
|
|
40
|
+
# Check that the full server error message is shown for a missing item
|
|
41
|
+
with pytest.raises(
|
|
42
|
+
DatalabAPIError,
|
|
43
|
+
match="No matching items for match={'item_id': 'TEST'} with current authorization",
|
|
44
|
+
):
|
|
45
|
+
client.get_item("TEST", display=True)
|
|
46
|
+
assert mocked_api["sample-missing-TEST"].called
|
|
47
|
+
|
|
48
|
+
with pytest.raises(DatalabAPIError, match="KeyError"):
|
|
49
|
+
client.update_item(item_id="TEST", item_data={"name": "test"})
|
|
50
|
+
assert mocked_api["bad-save"].called
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@respx.mock
|
|
54
|
+
def test_collection_get(fake_api_url, mocked_api):
|
|
55
|
+
with DatalabClient("https://api.datalab.industries") as client:
|
|
56
|
+
assert mocked_api["api"].called
|
|
57
|
+
assert mocked_api["info"].called
|
|
58
|
+
assert mocked_api["info-blocks"].called
|
|
59
|
+
|
|
60
|
+
collection, children = client.get_collection("test_collection", display=True)
|
|
61
|
+
assert mocked_api["collection-test_collection"].called
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@respx.mock
|
|
65
|
+
def test_group_get(fake_api_url, mocked_api):
|
|
66
|
+
with DatalabClient("https://api.datalab.industries") as client:
|
|
67
|
+
assert mocked_api["api"].called
|
|
68
|
+
assert mocked_api["info"].called
|
|
69
|
+
assert mocked_api["info-blocks"].called
|
|
70
|
+
|
|
71
|
+
group = client.get_group("test_group", display=True)
|
|
72
|
+
assert mocked_api["search-groups"].called
|
|
73
|
+
assert group["immutable_id"] == "67d0b01e03000cdc75134dbd"
|
|
74
|
+
assert group["group_id"] == "test_group"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@respx.mock
|
|
78
|
+
def test_upload_file(fake_api_url, mocked_api, tmp_path):
|
|
79
|
+
"""A new upload is stored by the server and reported as modified."""
|
|
80
|
+
fake_upload = mocked_api.post("/upload-file/", name="upload")
|
|
81
|
+
fake_upload.return_value = Response(
|
|
82
|
+
201,
|
|
83
|
+
json={
|
|
84
|
+
"status": "success",
|
|
85
|
+
"file_id": "6810ab6bb2b13a2be6c1e37f",
|
|
86
|
+
"file_information": {"name": "data.csv"},
|
|
87
|
+
"is_update": False,
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
test_file = tmp_path / "data.csv"
|
|
92
|
+
test_file.write_text("a,b\n1,2\n")
|
|
93
|
+
|
|
94
|
+
with DatalabClient(fake_api_url) as client:
|
|
95
|
+
result = client.upload_file(item_id="KUVEKJ", file_path=test_file)
|
|
96
|
+
|
|
97
|
+
assert mocked_api["upload"].called
|
|
98
|
+
assert result["file_id"] == "6810ab6bb2b13a2be6c1e37f"
|
|
99
|
+
assert result["not_modified"] is False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@respx.mock
|
|
103
|
+
def test_upload_file_not_modified(fake_api_url, mocked_api, tmp_path):
|
|
104
|
+
"""When replacing a file whose content the server already holds, it
|
|
105
|
+
replies 304 with an empty body. That is a successful no-op, so the
|
|
106
|
+
existing file ID is returned with `not_modified` set, rather than
|
|
107
|
+
the empty body being treated as an error."""
|
|
108
|
+
fake_upload = mocked_api.post("/upload-file/", name="upload")
|
|
109
|
+
fake_upload.return_value = Response(304, content=b"")
|
|
110
|
+
|
|
111
|
+
test_file = tmp_path / "data.csv"
|
|
112
|
+
test_file.write_text("a,b\n1,2\n")
|
|
113
|
+
|
|
114
|
+
with DatalabClient(fake_api_url) as client:
|
|
115
|
+
result = client.upload_file(
|
|
116
|
+
item_id="KUVEKJ",
|
|
117
|
+
file_path=test_file,
|
|
118
|
+
replace_file_id="6810ab6bb2b13a2be6c1e37f",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
assert mocked_api["upload"].called
|
|
122
|
+
assert result["status"] == "success"
|
|
123
|
+
assert result["file_id"] == "6810ab6bb2b13a2be6c1e37f"
|
|
124
|
+
assert result["is_update"] is True
|
|
125
|
+
assert result["not_modified"] is True
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_elevate_permissions_adds_sudo_to_reads(mocked_api, fake_api_url):
|
|
129
|
+
"""An elevated client sends `sudo=1` on GETs (and nothing else)."""
|
|
130
|
+
with DatalabClient(fake_api_url, elevate_permissions=True) as client:
|
|
131
|
+
client.get_item("KUVEKJ")
|
|
132
|
+
|
|
133
|
+
request = mocked_api["sample-KUVEKJ"].calls.last.request
|
|
134
|
+
assert request.url.params.get("sudo") == "1"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_elevate_permissions_is_toggleable(mocked_api, fake_api_url):
|
|
138
|
+
"""`elevate_permissions` can be flipped after construction."""
|
|
139
|
+
with DatalabClient(fake_api_url) as client:
|
|
140
|
+
client.get_item("KUVEKJ")
|
|
141
|
+
assert "sudo" not in mocked_api["sample-KUVEKJ"].calls.last.request.url.params
|
|
142
|
+
|
|
143
|
+
client.elevate_permissions = True
|
|
144
|
+
client.get_item("KUVEKJ")
|
|
145
|
+
assert mocked_api["sample-KUVEKJ"].calls.last.request.url.params.get("sudo") == "1"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_elevate_permissions_not_sent_on_writes(mocked_api, fake_api_url):
|
|
149
|
+
"""datalab grants admins full write access with no opt-in, so the flag
|
|
150
|
+
must not leak onto non-GET requests."""
|
|
151
|
+
with DatalabClient(fake_api_url, elevate_permissions=True) as client:
|
|
152
|
+
with pytest.raises(DatalabAPIError):
|
|
153
|
+
# `/save-item/` is mocked as a 500; we only care about the request
|
|
154
|
+
client.update_item("KUVEKJ", {"description": "test"})
|
|
155
|
+
|
|
156
|
+
request = mocked_api["bad-save"].calls.last.request
|
|
157
|
+
assert request.method == "POST"
|
|
158
|
+
assert "sudo" not in request.url.params
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_elevate_permissions_preserves_existing_params(mocked_api, fake_api_url):
|
|
162
|
+
"""A caller-supplied query string must survive elevation."""
|
|
163
|
+
with DatalabClient(fake_api_url, elevate_permissions=True) as client:
|
|
164
|
+
client._get(
|
|
165
|
+
f"{client.datalab_api_url}/get-item-data/KUVEKJ",
|
|
166
|
+
params={"load_blocks": "false"},
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
request = mocked_api["sample-KUVEKJ"].calls.last.request
|
|
170
|
+
assert request.url.params.get("load_blocks") == "false"
|
|
171
|
+
assert request.url.params.get("sudo") == "1"
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
import respx
|
|
3
|
-
|
|
4
|
-
from datalab_api import DatalabClient
|
|
5
|
-
from datalab_api._base import DatalabAPIError
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def test_redirect_url(mocked_api, mocked_ui, fake_api_url, fake_ui_url):
|
|
9
|
-
with pytest.warns(
|
|
10
|
-
UserWarning,
|
|
11
|
-
match="^Found API URL https://api.datalab.industries in HTML meta tag. Creating client with this URL instead.$",
|
|
12
|
-
):
|
|
13
|
-
client = DatalabClient(fake_ui_url)
|
|
14
|
-
|
|
15
|
-
assert mocked_ui["ui-redirect"].called
|
|
16
|
-
assert mocked_api["info"].called
|
|
17
|
-
assert mocked_api["info-blocks"].called
|
|
18
|
-
assert client.datalab_api_url == fake_api_url
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def test_sample_operations(mocked_api, fake_api_url):
|
|
22
|
-
with DatalabClient(fake_api_url) as client:
|
|
23
|
-
assert mocked_api["api"].called
|
|
24
|
-
assert mocked_api["info"].called
|
|
25
|
-
assert mocked_api["info-blocks"].called
|
|
26
|
-
|
|
27
|
-
client.authenticate()
|
|
28
|
-
assert mocked_api["current_user"].called
|
|
29
|
-
|
|
30
|
-
samples = client.get_items(display=True)
|
|
31
|
-
assert mocked_api["samples"].called
|
|
32
|
-
assert len(samples)
|
|
33
|
-
assert samples[0]["item_id"] == "test"
|
|
34
|
-
|
|
35
|
-
sample = client.get_item("KUVEKJ", display=True)
|
|
36
|
-
assert mocked_api["sample-KUVEKJ"].called
|
|
37
|
-
assert sample["item_id"] == "KUVEKJ"
|
|
38
|
-
|
|
39
|
-
# Check that the full server error message is shown for a missing item
|
|
40
|
-
with pytest.raises(
|
|
41
|
-
DatalabAPIError,
|
|
42
|
-
match="No matching items for match={'item_id': 'TEST'} with current authorization",
|
|
43
|
-
):
|
|
44
|
-
client.get_item("TEST", display=True)
|
|
45
|
-
assert mocked_api["sample-missing-TEST"].called
|
|
46
|
-
|
|
47
|
-
with pytest.raises(DatalabAPIError, match="KeyError"):
|
|
48
|
-
client.update_item(item_id="TEST", item_data={"name": "test"})
|
|
49
|
-
assert mocked_api["bad-save"].called
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@respx.mock
|
|
53
|
-
def test_collection_get(fake_api_url, mocked_api):
|
|
54
|
-
with DatalabClient("https://api.datalab.industries") as client:
|
|
55
|
-
assert mocked_api["api"].called
|
|
56
|
-
assert mocked_api["info"].called
|
|
57
|
-
assert mocked_api["info-blocks"].called
|
|
58
|
-
|
|
59
|
-
collection, children = client.get_collection("test_collection", display=True)
|
|
60
|
-
assert mocked_api["collection-test_collection"].called
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@respx.mock
|
|
64
|
-
def test_group_get(fake_api_url, mocked_api):
|
|
65
|
-
with DatalabClient("https://api.datalab.industries") as client:
|
|
66
|
-
assert mocked_api["api"].called
|
|
67
|
-
assert mocked_api["info"].called
|
|
68
|
-
assert mocked_api["info-blocks"].called
|
|
69
|
-
|
|
70
|
-
group = client.get_group("test_group", display=True)
|
|
71
|
-
assert mocked_api["search-groups"].called
|
|
72
|
-
assert group["immutable_id"] == "67d0b01e03000cdc75134dbd"
|
|
73
|
-
assert group["group_id"] == "test_group"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|