datalab-api 0.5.0__tar.gz → 0.5.2__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.0
3
+ Version: 0.5.2
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
@@ -273,7 +273,13 @@ class DatalabClient(BaseDatalabClient):
273
273
  for chunk in response.iter_bytes(chunk_size=1024):
274
274
  file.write(chunk)
275
275
 
276
- def get_block(self, item_id: str, block_id: str, block_data: dict[str, Any]) -> dict[str, Any]:
276
+ def get_block(
277
+ self,
278
+ item_id: str,
279
+ block_id: str,
280
+ block_data: dict[str, Any] | None = None,
281
+ event_data: dict[str, Any] | None = None,
282
+ ) -> dict[str, Any]:
277
283
  """Get a block with a given ID and block data.
278
284
  Should be used in conjunction with `get_item` to load an existing block.
279
285
 
@@ -281,17 +287,25 @@ class DatalabClient(BaseDatalabClient):
281
287
  item_id: The ID of the item to search for.
282
288
  block_id: The ID of the block to search for.
283
289
  block_data: Any other block data required by the request.
290
+ event_data: Any block events to trigger.
284
291
 
285
292
  Returns:
286
293
  A dictionary of block data for the block with the given ID.
287
294
 
288
295
  """
289
- block_url = f"{self.datalab_api_url}/update-block/"
296
+ block_url = f"{self.datalab_api_url}/blocks/"
297
+
298
+ if not block_data:
299
+ block_data = {}
300
+
301
+ block_data["item_id"] = item_id
302
+ block_data["block_id"] = block_id
303
+ if "blocktype" not in block_data:
304
+ raise ValueError("block_data must contain a 'blocktype' key.")
305
+
290
306
  block_request = {
291
307
  "block_data": block_data,
292
- "item_id": item_id,
293
- "block_id": block_id,
294
- "save_to_db": False,
308
+ "event_data": event_data,
295
309
  }
296
310
  block = self._post(block_url, json=block_request)
297
311
  return block["new_block_data"]
@@ -308,7 +322,12 @@ class DatalabClient(BaseDatalabClient):
308
322
  If provided, the new file will take the place of the old file in any blocks it was attached to.
309
323
 
310
324
  Returns:
311
- A dictionary of the uploaded file data.
325
+ A dictionary of the uploaded file data, with an additional
326
+ `not_modified` key. If `replace_file_id` was provided and the
327
+ server already holds identical content (compared by hash), the
328
+ update is a no-op: the server replies `304 Not Modified` and
329
+ `not_modified` is `True`. Servers older than datalab#1744 never
330
+ report this, so `not_modified` is always `False` against them.
312
331
 
313
332
  """
314
333
  if isinstance(file_path, str):
@@ -327,10 +346,23 @@ class DatalabClient(BaseDatalabClient):
327
346
  upload_url,
328
347
  files=files,
329
348
  data={"item_id": item_id, "replace_file": replace_file_id},
330
- expected_status=201,
349
+ expected_status=[201, 304],
331
350
  timeout=upload_timeout,
332
351
  )
333
- return upload
352
+
353
+ # A 201 always carries a body describing the stored file, so an
354
+ # empty dict here can only be the 304: the server compared hashes,
355
+ # found it already holds this content, and made no update. That is
356
+ # a success, so report it rather than raising.
357
+ if not upload:
358
+ return {
359
+ "status": "success",
360
+ "file_id": replace_file_id,
361
+ "is_update": True,
362
+ "not_modified": True,
363
+ }
364
+
365
+ return {**upload, "not_modified": False}
334
366
 
335
367
  def create_data_block(
336
368
  self,
@@ -424,10 +456,10 @@ class DatalabClient(BaseDatalabClient):
424
456
 
425
457
  if file_ids:
426
458
  if len(file_ids) > 1:
427
- raise RuntimeError(
428
- "API does not currently support attaching multiple files in a block."
429
- )
430
- block_data["file_id"] = file_ids[0]
459
+ block_data["file_ids"] = file_ids
460
+ else:
461
+ block_data.pop("file_ids", None)
462
+ block_data["file_id"] = file_ids[0]
431
463
 
432
464
  blocks_url = f"{self.datalab_api_url}/update-block/"
433
465
  payload = {
@@ -317,6 +317,12 @@ This is likely a server-side bug. Please report this issue to the datalab develo
317
317
  f"HTTP {response.status_code} error at {url}: {error_message}"
318
318
  )
319
319
 
320
+ # 204 and 304 carry no entity body by definition
321
+ # Where such a status is expected, an empty body
322
+ # is the success case rather than a parse failure.
323
+ if response.status_code in (204, 304):
324
+ return {}
325
+
320
326
  # Parse JSON response
321
327
  try:
322
328
  data = response.json()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datalab-api
3
- Version: 0.5.0
3
+ Version: 0.5.2
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
@@ -1,5 +1,6 @@
1
1
  import pytest
2
2
  import respx
3
+ from httpx import Response
3
4
 
4
5
  from datalab_api import DatalabClient
5
6
  from datalab_api._base import DatalabAPIError
@@ -71,3 +72,54 @@ def test_group_get(fake_api_url, mocked_api):
71
72
  assert mocked_api["search-groups"].called
72
73
  assert group["immutable_id"] == "67d0b01e03000cdc75134dbd"
73
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
File without changes
File without changes
File without changes
File without changes