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.
- supervisely/api/api.py +609 -3
- supervisely/api/file_api.py +523 -9
- supervisely/api/image_api.py +481 -5
- supervisely/api/pointcloud/pointcloud_api.py +390 -1
- supervisely/api/video/video_api.py +283 -8
- supervisely/api/volume/volume_api.py +223 -2
- supervisely/convert/base_converter.py +94 -3
- supervisely/convert/converter.py +11 -5
- supervisely/convert/image/image_converter.py +40 -19
- supervisely/convert/image/sly/fast_sly_image_converter.py +4 -0
- supervisely/convert/image/sly/sly_image_converter.py +24 -8
- supervisely/convert/video/sly/sly_video_converter.py +9 -1
- supervisely/convert/video/video_converter.py +53 -29
- supervisely/io/fs.py +125 -0
- supervisely/io/fs_cache.py +19 -1
- supervisely/io/network_exceptions.py +20 -3
- supervisely/task/progress.py +1 -1
- {supervisely-6.73.221.dist-info → supervisely-6.73.223.dist-info}/METADATA +3 -1
- {supervisely-6.73.221.dist-info → supervisely-6.73.223.dist-info}/RECORD +23 -23
- {supervisely-6.73.221.dist-info → supervisely-6.73.223.dist-info}/LICENSE +0 -0
- {supervisely-6.73.221.dist-info → supervisely-6.73.223.dist-info}/WHEEL +0 -0
- {supervisely-6.73.221.dist-info → supervisely-6.73.223.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.221.dist-info → supervisely-6.73.223.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
-
{
|
|
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),
|
|
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
|
|
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)
|