datalab-api 0.4.0__tar.gz → 0.5.1__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.4.0
3
+ Version: 0.5.1
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
@@ -127,6 +127,8 @@ class DatalabClient(BaseDatalabClient):
127
127
  item_type: str = "samples",
128
128
  item_data: dict[str, Any] | None = None,
129
129
  collection_id: str | None = None,
130
+ collection_ids: str | Iterable[str] | None = None, # alias for collection_id
131
+ group_ids: str | Iterable[str] | None = None,
130
132
  ) -> dict[str, Any]:
131
133
  """Create an item with a given ID and item data.
132
134
 
@@ -135,7 +137,9 @@ class DatalabClient(BaseDatalabClient):
135
137
  item_type: The type of item to create, e.g., 'samples', 'cells'.
136
138
  item_data: The data for the item.
137
139
  collection_id: The ID of the collection to add the item to (optional).
138
- If such a collection does not exist, one will be made.
140
+ If such a collection does not exist, one will be made. Deprecated in favor of `collection_ids`.
141
+ collection_ids: The ID or IDs of the collection(s) to add the item to (optional).
142
+ group_ids: The ID or IDs of the group(s) to share the item with (optional).
139
143
 
140
144
  """
141
145
  new_item = {}
@@ -147,13 +151,34 @@ class DatalabClient(BaseDatalabClient):
147
151
  new_item.pop("item_id")
148
152
 
149
153
  if collection_id is not None:
150
- try:
151
- collection_immutable_id = self.get_collection(collection_id)[0]["immutable_id"]
152
- except (RuntimeError, DatalabAPIError):
153
- self.create_collection(collection_id)
154
- collection_immutable_id = self.get_collection(collection_id)[0]["immutable_id"]
154
+ warnings.warn(
155
+ DeprecationWarning(
156
+ "`collection_id` is deprecated, please use `collection_ids` instead."
157
+ )
158
+ )
159
+ if collection_ids is None:
160
+ collection_ids = [collection_id]
161
+
162
+ if collection_ids:
155
163
  new_item["collections"] = new_item.get("collections", [])
156
- new_item["collections"].append({"immutable_id": collection_immutable_id})
164
+ for collection_id in collection_ids:
165
+ try:
166
+ collection_immutable_id = self.get_collection(collection_id)[0]["immutable_id"]
167
+ except (RuntimeError, DatalabAPIError):
168
+ self.create_collection(collection_id)
169
+ collection_immutable_id = self.get_collection(collection_id)[0]["immutable_id"]
170
+ new_item["collections"].append({"immutable_id": collection_immutable_id})
171
+
172
+ if group_ids:
173
+ # For now, we have to use the search groups endpoint
174
+ # until we add a get group by id endpoint that is locked down per user
175
+ new_item["groups"] = new_item.get("groups", [])
176
+ for group_id in group_ids:
177
+ try:
178
+ group_immutable_id = self.get_group(group_id)["immutable_id"]
179
+ new_item["groups"].append({"immutable_id": group_immutable_id})
180
+ except (RuntimeError, DatalabAPIError):
181
+ pass
157
182
 
158
183
  create_item_url = f"{self.datalab_api_url}/new-sample/"
159
184
  created_item = self._post(
@@ -248,7 +273,13 @@ class DatalabClient(BaseDatalabClient):
248
273
  for chunk in response.iter_bytes(chunk_size=1024):
249
274
  file.write(chunk)
250
275
 
251
- 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]:
252
283
  """Get a block with a given ID and block data.
253
284
  Should be used in conjunction with `get_item` to load an existing block.
254
285
 
@@ -256,27 +287,39 @@ class DatalabClient(BaseDatalabClient):
256
287
  item_id: The ID of the item to search for.
257
288
  block_id: The ID of the block to search for.
258
289
  block_data: Any other block data required by the request.
290
+ event_data: Any block events to trigger.
259
291
 
260
292
  Returns:
261
293
  A dictionary of block data for the block with the given ID.
262
294
 
263
295
  """
264
- 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
+
265
306
  block_request = {
266
307
  "block_data": block_data,
267
- "item_id": item_id,
268
- "block_id": block_id,
269
- "save_to_db": False,
308
+ "event_data": event_data,
270
309
  }
271
310
  block = self._post(block_url, json=block_request)
272
311
  return block["new_block_data"]
273
312
 
274
- def upload_file(self, item_id: str, file_path: Path | str) -> dict[str, Any]:
313
+ def upload_file(
314
+ self, item_id: str, file_path: Path | str, replace_file_id: str | None = None
315
+ ) -> dict[str, Any]:
275
316
  """Upload a file to an item with a given ID.
276
317
 
277
318
  Parameters:
278
319
  item_id: The ID of the item to upload the file to.
279
320
  file_path: The path to the file to upload.
321
+ replace_file_id: The ID of an existing file to replace with the uploaded file, if any.
322
+ If provided, the new file will take the place of the old file in any blocks it was attached to.
280
323
 
281
324
  Returns:
282
325
  A dictionary of the uploaded file data.
@@ -297,7 +340,7 @@ class DatalabClient(BaseDatalabClient):
297
340
  upload = self._post(
298
341
  upload_url,
299
342
  files=files,
300
- data={"item_id": item_id, "replace_file": None},
343
+ data={"item_id": item_id, "replace_file": replace_file_id},
301
344
  expected_status=201,
302
345
  timeout=upload_timeout,
303
346
  )
@@ -395,10 +438,10 @@ class DatalabClient(BaseDatalabClient):
395
438
 
396
439
  if file_ids:
397
440
  if len(file_ids) > 1:
398
- raise RuntimeError(
399
- "API does not currently support attaching multiple files in a block."
400
- )
401
- block_data["file_id"] = file_ids[0]
441
+ block_data["file_ids"] = file_ids
442
+ else:
443
+ block_data.pop("file_ids", None)
444
+ block_data["file_id"] = file_ids[0]
402
445
 
403
446
  blocks_url = f"{self.datalab_api_url}/update-block/"
404
447
  payload = {
@@ -435,6 +478,32 @@ class DatalabClient(BaseDatalabClient):
435
478
  collection = self._get(collection_url)
436
479
  return collection["data"], collection["child_items"]
437
480
 
481
+ def get_group(self, group_id: str) -> dict[str, Any]:
482
+ """Get a group with a given ID.
483
+
484
+ Uses the search groups endpoint under the hood,
485
+ so will only find groups that the user has access
486
+ to and that match the query.
487
+
488
+ Parameters:
489
+ group_id: The human-readable ID of the group to search for.
490
+
491
+ Returns:
492
+ A dictionary of group data for the group with the given ID.
493
+
494
+ Raises:
495
+ DatalabAPIError: If no group with the given ID is found.
496
+
497
+ """
498
+ group_url = f"{self.datalab_api_url}/search/groups?query={group_id}"
499
+ group_resp = self._get(group_url)
500
+ matching_groups = group_resp["data"]
501
+ if matching_groups:
502
+ matching_groups = [g for g in matching_groups if g.get("group_id") == group_id]
503
+ return matching_groups[0]
504
+
505
+ raise DatalabAPIError(f"No group found with ID {group_id!r}")
506
+
438
507
  def create_collection(
439
508
  self, collection_id: str, collection_data: dict | None = None
440
509
  ) -> dict[str, Any]:
@@ -35,7 +35,7 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
35
35
 
36
36
  _api_key: Optional[str] = None
37
37
  _session: Optional[httpx.Client] = None
38
- _headers: dict[str, str] = {}
38
+ _headers: dict[str, str]
39
39
  _timeout: httpx.Timeout = httpx.Timeout(10.0, read=60.0)
40
40
 
41
41
  interactive: bool = True
@@ -85,7 +85,7 @@ class BaseDatalabClient(metaclass=AutoPrettyPrint):
85
85
  self.log = logging.getLogger(__name__)
86
86
 
87
87
  self._http_client = httpx.Client
88
- self._headers["User-Agent"] = f"Datalab Python API/{__version__}"
88
+ self._headers = {"User-Agent": f"Datalab Python API/{__version__}"}
89
89
 
90
90
  self._detect_api_url()
91
91
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datalab-api
3
- Version: 0.4.0
3
+ Version: 0.5.1
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
@@ -58,3 +58,16 @@ def test_collection_get(fake_api_url, mocked_api):
58
58
 
59
59
  collection, children = client.get_collection("test_collection", display=True)
60
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