datalab-api 0.5.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datalab-api
3
- Version: 0.5.2
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("GET", url, follow_redirects=True) as response:
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
 
@@ -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
- def __init__(self, datalab_api_url: str, log_level: str = "WARNING"):
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.")
@@ -367,6 +386,7 @@ This is likely a server-side bug. Please report this issue to the datalab develo
367
386
  The JSON response data
368
387
  """
369
388
  timeout = kwargs.pop("timeout", None)
389
+ kwargs = self._apply_elevation(method, kwargs)
370
390
  try:
371
391
  response = self.session.request(
372
392
  method, url, follow_redirects=True, timeout=timeout, **kwargs
@@ -375,6 +395,22 @@ This is likely a server-side bug. Please report this issue to the datalab develo
375
395
  except (httpx.RequestError, httpx.HTTPStatusError) as e:
376
396
  raise DatalabAPIError(f"Request failed for {url}: {e}")
377
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
+
378
414
  def _get(self, url: str, **kwargs) -> dict[str, Any]:
379
415
  """Make a GET request with error handling."""
380
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.2
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.
@@ -123,3 +123,49 @@ def test_upload_file_not_modified(fake_api_url, mocked_api, tmp_path):
123
123
  assert result["file_id"] == "6810ab6bb2b13a2be6c1e37f"
124
124
  assert result["is_update"] is True
125
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"
File without changes
File without changes
File without changes