supervisely 6.73.221__py3-none-any.whl → 6.73.223__py3-none-any.whl

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.

Potentially problematic release.


This version of supervisely might be problematic. Click here for more details.

@@ -1,13 +1,26 @@
1
1
  # coding: utf-8
2
2
  from __future__ import annotations
3
3
 
4
+ import asyncio
4
5
  import datetime
5
6
  import json
6
7
  import os
7
8
  import urllib.parse
8
9
  from functools import partial
9
- from typing import Callable, Dict, Iterator, List, NamedTuple, Optional, Tuple, Union
10
+ from typing import (
11
+ AsyncGenerator,
12
+ Callable,
13
+ Dict,
14
+ Iterator,
15
+ List,
16
+ Literal,
17
+ NamedTuple,
18
+ Optional,
19
+ Tuple,
20
+ Union,
21
+ )
10
22
 
23
+ import aiofiles
11
24
  from numerize.numerize import numerize
12
25
  from requests import Response
13
26
  from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
@@ -31,6 +44,7 @@ from supervisely.io.fs import (
31
44
  ensure_base_path,
32
45
  get_file_ext,
33
46
  get_file_hash,
47
+ get_file_hash_async,
34
48
  get_file_name_with_ext,
35
49
  get_file_size,
36
50
  list_files,
@@ -399,7 +413,12 @@ class VideoApi(RemoveableBulkModuleApi):
399
413
  return_first_response=False,
400
414
  )
401
415
 
402
- def get_info_by_id(self, id: int, raise_error: Optional[bool] = False) -> VideoInfo:
416
+ def get_info_by_id(
417
+ self,
418
+ id: int,
419
+ raise_error: Optional[bool] = False,
420
+ force_metadata_for_links=True,
421
+ ) -> VideoInfo:
403
422
  """
404
423
  Get Video information by ID in VideoInfo<VideoInfo> format.
405
424
 
@@ -407,6 +426,8 @@ class VideoApi(RemoveableBulkModuleApi):
407
426
  :type id: int
408
427
  :param raise_error: Return an error if the video info was not received.
409
428
  :type raise_error: bool
429
+ :param force_metadata_for_links: Get video metadata from server (if the video is uploaded as a link)
430
+ :type force_metadata_for_links: bool
410
431
  :return: Information about Video. See :class:`info_sequence<info_sequence>`
411
432
  :rtype: :class:`VideoInfo`
412
433
 
@@ -463,7 +484,11 @@ class VideoApi(RemoveableBulkModuleApi):
463
484
  # )
464
485
  """
465
486
 
466
- info = self._get_info_by_id(id, "videos.info")
487
+ info = self._get_info_by_id(
488
+ id,
489
+ "videos.info",
490
+ fields={ApiField.FORCE_METADATA_FOR_LINKS: force_metadata_for_links},
491
+ )
467
492
  if info is None and raise_error is True:
468
493
  raise KeyError(f"Video with id={id} not found in your account")
469
494
  return info
@@ -532,6 +557,7 @@ class VideoApi(RemoveableBulkModuleApi):
532
557
  ApiField.DATASET_ID: dataset_id,
533
558
  ApiField.FILTER: filters,
534
559
  ApiField.FIELDS: fields,
560
+ ApiField.FORCE_METADATA_FOR_LINKS: force_metadata_for_links,
535
561
  },
536
562
  )
537
563
  )
@@ -541,7 +567,12 @@ class VideoApi(RemoveableBulkModuleApi):
541
567
  ordered_results = [temp_map[id] for id in ids]
542
568
  return ordered_results
543
569
 
544
- def get_json_info_by_id(self, id: int, raise_error: Optional[bool] = False) -> Dict:
570
+ def get_json_info_by_id(
571
+ self,
572
+ id: int,
573
+ raise_error: Optional[bool] = False,
574
+ force_metadata_for_links: Optional[bool] = True,
575
+ ) -> Dict:
545
576
  """
546
577
  Get Video information by ID in json format.
547
578
 
@@ -615,7 +646,12 @@ class VideoApi(RemoveableBulkModuleApi):
615
646
  """
616
647
 
617
648
  data = None
618
- response = self._get_response_by_id(id, "videos.info", id_field=ApiField.ID)
649
+ response = self._get_response_by_id(
650
+ id,
651
+ "videos.info",
652
+ id_field=ApiField.ID,
653
+ fields={ApiField.FORCE_METADATA_FOR_LINKS: force_metadata_for_links},
654
+ )
619
655
  if response is None:
620
656
  if raise_error is True:
621
657
  raise KeyError(f"Video with id={id} not found in your account")
@@ -1036,7 +1072,14 @@ class VideoApi(RemoveableBulkModuleApi):
1036
1072
  return new_videos
1037
1073
 
1038
1074
  def _upload_bulk_add(
1039
- self, func_item_to_kv, dataset_id, names, items, metas=None, progress_cb=None
1075
+ self,
1076
+ func_item_to_kv,
1077
+ dataset_id,
1078
+ names,
1079
+ items,
1080
+ metas=None,
1081
+ progress_cb=None,
1082
+ force_metadata_for_links=True,
1040
1083
  ):
1041
1084
  if metas is None:
1042
1085
  metas = [{}] * len(items)
@@ -1063,7 +1106,11 @@ class VideoApi(RemoveableBulkModuleApi):
1063
1106
  )
1064
1107
  response = self._api.post(
1065
1108
  "videos.bulk.add",
1066
- {ApiField.DATASET_ID: dataset_id, ApiField.VIDEOS: images},
1109
+ {
1110
+ ApiField.DATASET_ID: dataset_id,
1111
+ ApiField.VIDEOS: images,
1112
+ ApiField.FORCE_METADATA_FOR_LINKS: force_metadata_for_links,
1113
+ },
1067
1114
  )
1068
1115
  if progress_cb is not None:
1069
1116
  progress_cb(len(images))
@@ -1777,6 +1824,8 @@ class VideoApi(RemoveableBulkModuleApi):
1777
1824
  hashes: List[str] = None,
1778
1825
  metas: Optional[List[Dict]] = None,
1779
1826
  skip_download: Optional[bool] = False,
1827
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
1828
+ force_metadata_for_links: Optional[bool] = True,
1780
1829
  ) -> List[VideoInfo]:
1781
1830
  """
1782
1831
  Upload Videos from given links to Dataset.
@@ -1795,6 +1844,10 @@ class VideoApi(RemoveableBulkModuleApi):
1795
1844
  :type metas: List[dict], optional
1796
1845
  :param skip_download: Skip download videos to local storage.
1797
1846
  :type skip_download: Optional[bool]
1847
+ :param progress_cb: Function for tracking the progress of copying.
1848
+ :type progress_cb: tqdm or callable, optional
1849
+ :param force_metadata_for_links: Specify if metadata should be forced. Default is True.
1850
+ :type force_metadata_for_links: Optional[bool]
1798
1851
  :return: List with information about Videos. See :class:`info_sequence<info_sequence>`
1799
1852
  :rtype: :class:`List[VideoInfo]`
1800
1853
  :Usage example:
@@ -1829,7 +1882,13 @@ class VideoApi(RemoveableBulkModuleApi):
1829
1882
  # if infos is not None and hashes is not None and not skip_download:
1830
1883
  # self.upsert_infos(hashes, infos, links)
1831
1884
  return self._upload_bulk_add(
1832
- lambda item: (ApiField.LINK, item), dataset_id, names, links, metas
1885
+ lambda item: (ApiField.LINK, item),
1886
+ dataset_id,
1887
+ names,
1888
+ links,
1889
+ metas,
1890
+ progress_cb=progress_cb,
1891
+ force_metadata_for_links=force_metadata_for_links,
1833
1892
  )
1834
1893
 
1835
1894
  def update_custom_data(self, id: int, data: dict):
@@ -2305,3 +2364,219 @@ class VideoApi(RemoveableBulkModuleApi):
2305
2364
  data = {ApiField.VIDEOS: videos_list, ApiField.CLEAR_LOCAL_DATA_SOURCE: True}
2306
2365
  r = self._api.post("videos.update.links", data)
2307
2366
  return r.json()
2367
+
2368
+ async def _download_async(
2369
+ self,
2370
+ id: int,
2371
+ is_stream: bool = False,
2372
+ range_start: Optional[int] = None,
2373
+ range_end: Optional[int] = None,
2374
+ headers: Optional[dict] = None,
2375
+ chunk_size: int = 1024 * 1024,
2376
+ ) -> AsyncGenerator:
2377
+ """
2378
+ Download Video with given ID asynchronously.
2379
+
2380
+ :param id: Video ID in Supervisely.
2381
+ :type id: int
2382
+ :param is_stream: If True, returns stream of bytes, otherwise returns response object.
2383
+ :type is_stream: bool, optional
2384
+ :param range_start: Start byte of range for partial download.
2385
+ :type range_start: int, optional
2386
+ :param range_end: End byte of range for partial download.
2387
+ :type range_end: int, optional
2388
+ :param headers: Headers for request.
2389
+ :type headers: dict, optional
2390
+ :param chunk_size: Size of chunk for partial download. Default is 1MB.
2391
+ :type chunk_size: int, optional
2392
+ :return: Stream of bytes or response object.
2393
+ :rtype: AsyncGenerator
2394
+ """
2395
+ api_method_name = "videos.download"
2396
+
2397
+ json_body = {ApiField.ID: id}
2398
+
2399
+ if is_stream:
2400
+ async for chunk, hhash in self._api.stream_async(
2401
+ api_method_name,
2402
+ "POST",
2403
+ json_body,
2404
+ headers=headers,
2405
+ range_start=range_start,
2406
+ range_end=range_end,
2407
+ chunk_size=chunk_size,
2408
+ ):
2409
+ yield chunk, hhash
2410
+ else:
2411
+ response = await self._api.post_async(api_method_name, json_body, headers=headers)
2412
+ yield response
2413
+
2414
+ async def download_path_async(
2415
+ self,
2416
+ id: int,
2417
+ path: str,
2418
+ semaphore: Optional[asyncio.Semaphore] = None,
2419
+ range_start: Optional[int] = None,
2420
+ range_end: Optional[int] = None,
2421
+ headers: Optional[dict] = None,
2422
+ chunk_size: int = 1024 * 1024,
2423
+ check_hash: bool = True,
2424
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
2425
+ progress_cb_type: Literal["number", "size"] = "number",
2426
+ ) -> None:
2427
+ """
2428
+ Downloads Video with given ID to local path.
2429
+
2430
+ :param id: Video ID in Supervisely.
2431
+ :type id: int
2432
+ :param path: Local save path for Video.
2433
+ :type path: str
2434
+ :param semaphore: Semaphore for limiting the number of simultaneous downloads.
2435
+ :type semaphore: :class:`asyncio.Semaphore`, optional
2436
+ :param range_start: Start byte of range for partial download.
2437
+ :type range_start: int, optional
2438
+ :param range_end: End byte of range for partial download.
2439
+ :type range_end: int, optional
2440
+ :param headers: Headers for request.
2441
+ :type headers: dict, optional
2442
+ :param chunk_size: Size of chunk for partial download. Default is 1MB.
2443
+ :type chunk_size: int, optional
2444
+ :param check_hash: If True, checks hash of downloaded file.
2445
+ Check is not supported for partial downloads.
2446
+ When range is set, hash check is disabled.
2447
+ :type check_hash: bool, optional
2448
+ :param progress_cb: Function for tracking download progress.
2449
+ :type progress_cb: Optional[Union[tqdm, Callable]]
2450
+ :param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "number".
2451
+ :type progress_cb_type: Literal["number", "size"], optional
2452
+ :return: None
2453
+ :rtype: :class:`NoneType`
2454
+ :Usage example:
2455
+
2456
+ .. code-block:: python
2457
+
2458
+ import supervisely as sly
2459
+ import asyncio
2460
+
2461
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
2462
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
2463
+ api = sly.Api.from_env()
2464
+
2465
+ video_info = api.video.get_info_by_id(770918)
2466
+ save_path = os.path.join("/path/to/save/", video_info.name)
2467
+
2468
+ semaphore = asyncio.Semaphore(100)
2469
+ loop = asyncio.new_event_loop()
2470
+ asyncio.set_event_loop(loop)
2471
+ loop.run_until_complete(
2472
+ api.video.download_path_async(video_info.id, save_path, semaphore)
2473
+ )
2474
+ """
2475
+ if range_start is not None or range_end is not None:
2476
+ check_hash = False # hash check is not supported for partial downloads
2477
+ headers = headers or {}
2478
+ headers["Range"] = f"bytes={range_start or ''}-{range_end or ''}"
2479
+ logger.debug(f"Image ID: {id}. Setting Range header: {headers['Range']}")
2480
+
2481
+ writing_method = "ab" if range_start not in [0, None] else "wb"
2482
+
2483
+ ensure_base_path(path)
2484
+ hash_to_check = None
2485
+ if semaphore is None:
2486
+ semaphore = self._api._get_default_semaphore()
2487
+ async with semaphore:
2488
+ async with aiofiles.open(path, writing_method) as fd:
2489
+ async for chunk, hhash in self._download_async(
2490
+ id,
2491
+ is_stream=True,
2492
+ range_start=range_start,
2493
+ range_end=range_end,
2494
+ headers=headers,
2495
+ chunk_size=chunk_size,
2496
+ ):
2497
+ await fd.write(chunk)
2498
+ hash_to_check = hhash
2499
+ if progress_cb is not None and progress_cb_type == "size":
2500
+ progress_cb(len(chunk))
2501
+ if check_hash:
2502
+ if hash_to_check is not None:
2503
+ downloaded_file_hash = await get_file_hash_async(path)
2504
+ if hash_to_check != downloaded_file_hash:
2505
+ raise RuntimeError(
2506
+ f"Downloaded hash of video with ID:{id} does not match the expected hash: {downloaded_file_hash} != {hash_to_check}"
2507
+ )
2508
+ if progress_cb is not None and progress_cb_type == "number":
2509
+ progress_cb(1)
2510
+
2511
+ async def download_paths_async(
2512
+ self,
2513
+ ids: List[int],
2514
+ paths: List[str],
2515
+ semaphore: Optional[asyncio.Semaphore] = None,
2516
+ headers: Optional[dict] = None,
2517
+ chunk_size: int = 1024 * 1024,
2518
+ check_hash: bool = True,
2519
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
2520
+ progress_cb_type: Literal["number", "size"] = "number",
2521
+ ) -> None:
2522
+ """
2523
+ Download Videos with given IDs and saves them to given local paths asynchronously.
2524
+
2525
+ :param ids: List of Video IDs in Supervisely.
2526
+ :type ids: :class:`List[int]`
2527
+ :param paths: Local save paths for Videos.
2528
+ :type paths: :class:`List[str]`
2529
+ :param semaphore: Semaphore
2530
+ :type semaphore: :class:`asyncio.Semaphore`, optional
2531
+ :param headers: Headers for request.
2532
+ :type headers: dict, optional
2533
+ :param chunk_size: Size of chunk for partial download. Default is 1MB.
2534
+ :type chunk_size: int, optional
2535
+ :param check_hash: If True, checks hash of downloaded files.
2536
+ :type check_hash: bool, optional
2537
+ :param progress_cb: Function for tracking download progress.
2538
+ :type progress_cb: Optional[Union[tqdm, Callable]]
2539
+ :param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "number".
2540
+ :type progress_cb_type: Literal["number", "size"], optional
2541
+ :raises: :class:`ValueError` if len(ids) != len(paths)
2542
+ :return: None
2543
+ :rtype: :class:`NoneType`
2544
+
2545
+ :Usage example:
2546
+
2547
+ .. code-block:: python
2548
+
2549
+ import supervisely as sly
2550
+ import asyncio
2551
+
2552
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
2553
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
2554
+ api = sly.Api.from_env()
2555
+
2556
+ ids = [770914, 770915]
2557
+ paths = ["/path/to/save/video1.mp4", "/path/to/save/video2.mp4"]
2558
+ loop = asyncio.new_event_loop()
2559
+ asyncio.set_event_loop(loop)
2560
+ loop.run_until_complete(api.video.download_paths_async(ids, paths))
2561
+ """
2562
+ if len(ids) == 0:
2563
+ return
2564
+ if len(ids) != len(paths):
2565
+ raise ValueError('Can not match "ids" and "paths" lists, len(ids) != len(paths)')
2566
+ if semaphore is None:
2567
+ semaphore = self._api._get_default_semaphore()
2568
+ tasks = []
2569
+ for img_id, img_path in zip(ids, paths):
2570
+ task = self.download_path_async(
2571
+ img_id,
2572
+ img_path,
2573
+ semaphore=semaphore,
2574
+ headers=headers,
2575
+ chunk_size=chunk_size,
2576
+ check_hash=check_hash,
2577
+ progress_cb=progress_cb,
2578
+ progress_cb_type=progress_cb_type,
2579
+ )
2580
+
2581
+ tasks.append(task)
2582
+ await asyncio.gather(*tasks)
@@ -1,6 +1,8 @@
1
+ import asyncio
1
2
  import os
2
- from typing import Callable, List, NamedTuple, Optional, Union
3
+ from typing import AsyncGenerator, Callable, List, NamedTuple, Optional, Union
3
4
 
5
+ import aiofiles
4
6
  from tqdm import tqdm
5
7
  from tqdm.contrib.logging import logging_redirect_tqdm
6
8
 
@@ -19,6 +21,7 @@ from supervisely.io.fs import (
19
21
  ensure_base_path,
20
22
  get_bytes_hash,
21
23
  get_file_ext,
24
+ get_file_hash_async,
22
25
  get_file_name,
23
26
  get_file_name_with_ext,
24
27
  )
@@ -852,7 +855,7 @@ class VolumeApi(RemoveableBulkModuleApi):
852
855
 
853
856
  def _download(self, id: int, is_stream: bool = False):
854
857
  """
855
- Private method for volume volume downloading.
858
+ Private method for volume downloading.
856
859
 
857
860
  :param id: Volume ID in Supervisely.
858
861
  :type id: int
@@ -1236,3 +1239,221 @@ class VolumeApi(RemoveableBulkModuleApi):
1236
1239
  )
1237
1240
  )
1238
1241
  return volume_infos
1242
+
1243
+ async def _download_async(
1244
+ self,
1245
+ id: int,
1246
+ is_stream: bool = False,
1247
+ range_start: Optional[int] = None,
1248
+ range_end: Optional[int] = None,
1249
+ headers: Optional[dict] = None,
1250
+ chunk_size: int = 1024 * 1024,
1251
+ ) -> AsyncGenerator:
1252
+ """
1253
+ Download Volume with given ID asynchronously.
1254
+ If is_stream is True, returns stream of bytes, otherwise returns response object.
1255
+ For streaming, returns tuple of chunk and hash.
1256
+
1257
+ :param id: Volume ID in Supervisely.
1258
+ :type id: int
1259
+ :param is_stream: If True, returns stream of bytes, otherwise returns response object.
1260
+ :type is_stream: bool, optional
1261
+ :param range_start: Start byte of range for partial download.
1262
+ :type range_start: int, optional
1263
+ :param range_end: End byte of range for partial download.
1264
+ :type range_end: int, optional
1265
+ :param headers: Headers for request.
1266
+ :type headers: dict, optional
1267
+ :param chunk_size: Size of chunk for downloading. Default is 1MB.
1268
+ :type chunk_size: int, optional
1269
+ :return: Stream of bytes or response object.
1270
+ :rtype: AsyncGenerator
1271
+ """
1272
+ api_method_name = "volumes.download"
1273
+
1274
+ json_body = {ApiField.ID: id}
1275
+
1276
+ if is_stream:
1277
+ async for chunk, hhash in self._api.stream_async(
1278
+ api_method_name,
1279
+ "POST",
1280
+ json_body,
1281
+ headers=headers,
1282
+ range_start=range_start,
1283
+ range_end=range_end,
1284
+ chunk_size=chunk_size,
1285
+ ):
1286
+ yield chunk, hhash
1287
+ else:
1288
+ response = await self._api.post_async(api_method_name, json_body, headers=headers)
1289
+ yield response
1290
+
1291
+ async def download_path_async(
1292
+ self,
1293
+ id: int,
1294
+ path: str,
1295
+ semaphore: Optional[asyncio.Semaphore] = None,
1296
+ range_start: Optional[int] = None,
1297
+ range_end: Optional[int] = None,
1298
+ headers: Optional[dict] = None,
1299
+ chunk_size: int = 1024 * 1024,
1300
+ check_hash: bool = True,
1301
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
1302
+ progress_cb_type: Literal["number", "size"] = "number",
1303
+ ) -> None:
1304
+ """
1305
+ Downloads Volume with given ID to local path.
1306
+
1307
+ :param id: Volume ID in Supervisely.
1308
+ :type id: int
1309
+ :param path: Local save path for Volume.
1310
+ :type path: str
1311
+ :param semaphore: Semaphore for limiting the number of simultaneous downloads.
1312
+ :type semaphore: :class:`asyncio.Semaphore`, optional
1313
+ :param range_start: Start byte of range for partial download.
1314
+ :type range_start: int, optional
1315
+ :param range_end: End byte of range for partial download.
1316
+ :type range_end: int, optional
1317
+ :param headers: Headers for request.
1318
+ :type headers: dict, optional
1319
+ :param chunk_size: Size of chunk for downloading. Default is 1MB.
1320
+ :type chunk_size: int, optional
1321
+ :param check_hash: If True, checks hash of downloaded file.
1322
+ Check is not supported for partial downloads.
1323
+ When range is set, hash check is disabled.
1324
+ :type check_hash: bool, optional
1325
+ :param progress_cb: Function for tracking download progress.
1326
+ :type progress_cb: tqdm or callable, optional
1327
+ :param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "number".
1328
+ :type progress_cb_type: str, optional
1329
+ :return: None
1330
+ :rtype: :class:`NoneType`
1331
+ :Usage example:
1332
+
1333
+ .. code-block:: python
1334
+
1335
+ import supervisely as sly
1336
+ import asyncio
1337
+
1338
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
1339
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
1340
+ api = sly.Api.from_env()
1341
+
1342
+ volume_info = api.volume.get_info_by_id(770918)
1343
+ save_path = os.path.join("/path/to/save/", volume_info.name)
1344
+
1345
+ semaphore = asyncio.Semaphore(100)
1346
+ loop = asyncio.new_event_loop()
1347
+ asyncio.set_event_loop(loop)
1348
+ loop.run_until_complete(
1349
+ api.volume.download_path_async(volume_info.id, save_path, semaphore)
1350
+ )
1351
+ """
1352
+
1353
+ if range_start is not None or range_end is not None:
1354
+ check_hash = False # Hash check is not supported for partial downloads
1355
+ headers = headers or {}
1356
+ headers["Range"] = f"bytes={range_start or ''}-{range_end or ''}"
1357
+ logger.debug(f"Image ID: {id}. Setting Range header: {headers['Range']}")
1358
+
1359
+ writing_method = "ab" if range_start not in [0, None] else "wb"
1360
+
1361
+ ensure_base_path(path)
1362
+ hash_to_check = None
1363
+ if semaphore is None:
1364
+ semaphore = self._api._get_default_semaphore()
1365
+ async with semaphore:
1366
+ async with aiofiles.open(path, writing_method) as fd:
1367
+ async for chunk, hhash in self._download_async(
1368
+ id,
1369
+ is_stream=True,
1370
+ headers=headers,
1371
+ range_start=range_start,
1372
+ range_end=range_end,
1373
+ chunk_size=chunk_size,
1374
+ ):
1375
+ await fd.write(chunk)
1376
+ hash_to_check = hhash
1377
+ if progress_cb is not None and progress_cb_type == "size":
1378
+ progress_cb(len(chunk))
1379
+ if check_hash:
1380
+ if hash_to_check is not None:
1381
+ downloaded_file_hash = await get_file_hash_async(path)
1382
+ if hash_to_check != downloaded_file_hash:
1383
+ raise RuntimeError(
1384
+ f"Downloaded hash of volume with ID:{id} does not match the expected hash: {downloaded_file_hash} != {hash_to_check}"
1385
+ )
1386
+ if progress_cb is not None and progress_cb_type == "number":
1387
+ progress_cb(1)
1388
+
1389
+ async def download_paths_async(
1390
+ self,
1391
+ ids: List[int],
1392
+ paths: List[str],
1393
+ semaphore: Optional[asyncio.Semaphore] = None,
1394
+ headers: Optional[dict] = None,
1395
+ chunk_size: int = 1024 * 1024,
1396
+ check_hash: bool = True,
1397
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
1398
+ progress_cb_type: Literal["number", "size"] = "number",
1399
+ ) -> None:
1400
+ """
1401
+ Download Volumes with given IDs and saves them to given local paths asynchronously.
1402
+
1403
+ :param ids: List of Volume IDs in Supervisely.
1404
+ :type ids: :class:`List[int]`
1405
+ :param paths: Local save paths for Volumes.
1406
+ :type paths: :class:`List[str]`
1407
+ :param semaphore: Semaphore for limiting the number of simultaneous downloads.
1408
+ :type semaphore: :class:`asyncio.Semaphore`, optional
1409
+ :param headers: Headers for request.
1410
+ :type headers: dict, optional
1411
+ :param chunk_size: Size of chunk for downloading. Default is 1MB.
1412
+ :type chunk_size: int, optional
1413
+ :param check_hash: If True, checks hash of downloaded file.
1414
+ :type check_hash: bool, optional
1415
+ :param progress_cb: Function for tracking download progress.
1416
+ :type progress_cb: tqdm or callable, optional
1417
+ :param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "number".
1418
+ :type progress_cb_type: str, optional
1419
+ :raises: :class:`ValueError` if len(ids) != len(paths)
1420
+ :return: None
1421
+ :rtype: :class:`NoneType`
1422
+
1423
+ :Usage example:
1424
+
1425
+ .. code-block:: python
1426
+
1427
+ import supervisely as sly
1428
+ import asyncio
1429
+
1430
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
1431
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
1432
+ api = sly.Api.from_env()
1433
+
1434
+ ids = [770914, 770915]
1435
+ paths = ["/path/to/save/volume1.nrrd", "/path/to/save/volume2.nrrd"]
1436
+ loop = asyncio.new_event_loop()
1437
+ asyncio.set_event_loop(loop)
1438
+ loop.run_until_complete(api.volume.download_paths_async(ids, paths))
1439
+ """
1440
+ if len(ids) == 0:
1441
+ return
1442
+ if len(ids) != len(paths):
1443
+ raise ValueError(f'Can not match "ids" and "paths" lists, {len(ids)} != {len(paths)}')
1444
+ if semaphore is None:
1445
+ semaphore = self._api._get_default_semaphore()
1446
+ tasks = []
1447
+ for img_id, img_path in zip(ids, paths):
1448
+ task = self.download_path_async(
1449
+ img_id,
1450
+ img_path,
1451
+ semaphore,
1452
+ headers=headers,
1453
+ chunk_size=chunk_size,
1454
+ check_hash=check_hash,
1455
+ progress_cb=progress_cb,
1456
+ progress_cb_type=progress_cb_type,
1457
+ )
1458
+ tasks.append(task)
1459
+ await asyncio.gather(*tasks)