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.

@@ -4,6 +4,7 @@
4
4
  # docs
5
5
  from __future__ import annotations
6
6
 
7
+ import asyncio
7
8
  import io
8
9
  import json
9
10
  import re
@@ -16,6 +17,7 @@ from pathlib import Path
16
17
  from time import sleep
17
18
  from typing import (
18
19
  Any,
20
+ AsyncGenerator,
19
21
  Callable,
20
22
  Dict,
21
23
  Generator,
@@ -29,6 +31,7 @@ from typing import (
29
31
  )
30
32
  from uuid import uuid4
31
33
 
34
+ import aiofiles
32
35
  import numpy as np
33
36
  import requests
34
37
  from requests.exceptions import HTTPError
@@ -57,6 +60,7 @@ from supervisely.io.fs import (
57
60
  ensure_base_path,
58
61
  get_file_ext,
59
62
  get_file_hash,
63
+ get_file_hash_async,
60
64
  get_file_name,
61
65
  get_file_name_with_ext,
62
66
  list_files,
@@ -601,9 +605,13 @@ class ImageApi(RemoveableBulkModuleApi):
601
605
  infos_dict = {}
602
606
  ids_set = set(ids)
603
607
  while any(ids_set):
604
- dataset_id = self.get_info_by_id(
605
- ids_set.pop(), force_metadata_for_links=False
606
- ).dataset_id
608
+ img_id = ids_set.pop()
609
+ image_info = self.get_info_by_id(img_id, force_metadata_for_links=False)
610
+ if image_info is None:
611
+ raise KeyError(
612
+ f"Image (id: {img_id}) is either archived, doesn't exist or you don't have enough permissions to access it"
613
+ )
614
+ dataset_id = image_info.dataset_id
607
615
  for batch in batched(ids):
608
616
  filters = [{"field": ApiField.ID, "operator": "in", "value": batch}]
609
617
  temp_results = self.get_list_all_pages(
@@ -1751,12 +1759,13 @@ class ImageApi(RemoveableBulkModuleApi):
1751
1759
  raise ValueError(
1752
1760
  f"Conflict resolution should be one of the following: {SUPPORTED_CONFLICT_RESOLUTIONS}"
1753
1761
  )
1762
+ if len(set(names)) != len(names):
1763
+ raise ValueError("Some image names are duplicated, only unique images can be uploaded.")
1764
+
1754
1765
  results = []
1755
1766
 
1756
1767
  def _add_timestamp(name: str) -> str:
1757
-
1758
1768
  now = datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
1759
-
1760
1769
  return f"{get_file_name(name)}_{now}{get_file_ext(name)}"
1761
1770
 
1762
1771
  def _pack_for_request(names: List[str], items: List[Any], metas: List[Dict]) -> List[Any]:
@@ -1812,6 +1821,8 @@ class ImageApi(RemoveableBulkModuleApi):
1812
1821
  break
1813
1822
  except HTTPError as e:
1814
1823
  error_details = e.response.json().get("details", {})
1824
+ if isinstance(error_details, list):
1825
+ error_details = error_details[0]
1815
1826
  if (
1816
1827
  conflict_resolution is not None
1817
1828
  and e.response.status_code == 400
@@ -3450,3 +3461,468 @@ class ImageApi(RemoveableBulkModuleApi):
3450
3461
  data = {ApiField.IMAGES: images_list, ApiField.CLEAR_LOCAL_DATA_SOURCE: True}
3451
3462
  r = self._api.post("images.update.links", data)
3452
3463
  return r.json()
3464
+
3465
+ async def _download_async(
3466
+ self,
3467
+ id: int,
3468
+ is_stream: bool = False,
3469
+ range_start: Optional[int] = None,
3470
+ range_end: Optional[int] = None,
3471
+ headers: Optional[dict] = None,
3472
+ ) -> AsyncGenerator:
3473
+ """
3474
+ Download Image with given ID asynchronously.
3475
+ If is_stream is True, returns stream of bytes, otherwise returns response object.
3476
+ For streaming, returns tuple of chunk and hash.
3477
+
3478
+ :param id: Image ID in Supervisely.
3479
+ :type id: int
3480
+ :param is_stream: If True, returns stream of bytes, otherwise returns response object.
3481
+ :type is_stream: bool, optional
3482
+ :param range_start: Start byte of range for partial download.
3483
+ :type range_start: int, optional
3484
+ :param range_end: End byte of range for partial download.
3485
+ :type range_end: int, optional
3486
+ :param headers: Headers for request.
3487
+ :type headers: dict, optional
3488
+ :return: Stream of bytes or response object.
3489
+ :rtype: AsyncGenerator
3490
+ """
3491
+ api_method_name = "images.download"
3492
+
3493
+ json_body = {ApiField.ID: id}
3494
+
3495
+ if is_stream:
3496
+ async for chunk, hhash in self._api.stream_async(
3497
+ api_method_name,
3498
+ "POST",
3499
+ json_body,
3500
+ headers=headers,
3501
+ range_start=range_start,
3502
+ range_end=range_end,
3503
+ ):
3504
+ yield chunk, hhash
3505
+ else:
3506
+ response = await self._api.post_async(api_method_name, json_body, headers=headers)
3507
+ yield response
3508
+
3509
+ async def download_np_async(
3510
+ self,
3511
+ id: int,
3512
+ semaphore: Optional[asyncio.Semaphore] = None,
3513
+ keep_alpha: Optional[bool] = False,
3514
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
3515
+ progress_cb_type: Literal["number", "size"] = "number",
3516
+ ) -> np.ndarray:
3517
+ """
3518
+ Downloads Image with given ID in NumPy format asynchronously.
3519
+
3520
+ :param id: Image ID in Supervisely.
3521
+ :type id: int
3522
+ :param semaphore: Semaphore for limiting the number of simultaneous downloads.
3523
+ :type semaphore: :class:`asyncio.Semaphore`, optional
3524
+ :param keep_alpha: If True keeps alpha mask for image, otherwise don't.
3525
+ :type keep_alpha: bool, optional
3526
+ :param progress_cb: Function for tracking download progress.
3527
+ :type progress_cb: tqdm or callable, optional
3528
+ :param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "number".
3529
+ :type progress_cb_type: Literal["number", "size"], optional
3530
+ :return: Image in RGB numpy matrix format
3531
+ :rtype: :class:`np.ndarray`
3532
+
3533
+ :Usage example:
3534
+
3535
+ .. code-block:: python
3536
+
3537
+ import supervisely as sly
3538
+ import asyncio
3539
+ from tqdm.asyncio import tqdm
3540
+
3541
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
3542
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
3543
+ api = sly.Api.from_env()
3544
+
3545
+ DATASET_ID = 98357
3546
+ semaphore = asyncio.Semaphore(100)
3547
+ images = api.image.get_list(DATASET_ID)
3548
+ tasks = []
3549
+ pbar = tqdm(total=len(images), desc="Downloading images", unit="image")
3550
+ for image in images:
3551
+ task = api.image.download_np_async(image.id, semaphore, progress_cb=pbar)
3552
+ tasks.append(task)
3553
+ results = await asyncio.gather(*tasks)
3554
+ """
3555
+ if semaphore is None:
3556
+ semaphore = self._api._get_default_semaphore()
3557
+
3558
+ async with semaphore:
3559
+ async for response in self._download_async(id):
3560
+ img = sly_image.read_bytes(response.content, keep_alpha)
3561
+ if progress_cb is not None:
3562
+ if progress_cb_type == "number":
3563
+ progress_cb(1)
3564
+ elif progress_cb_type == "size":
3565
+ progress_cb(len(response.content))
3566
+ return img
3567
+
3568
+ async def download_nps_async(
3569
+ self,
3570
+ ids: List[int],
3571
+ semaphore: Optional[asyncio.Semaphore] = None,
3572
+ keep_alpha: Optional[bool] = False,
3573
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
3574
+ progress_cb_type: Literal["number", "size"] = "number",
3575
+ ) -> List[np.ndarray]:
3576
+ """
3577
+ Downloads Images with given IDs in NumPy format asynchronously.
3578
+
3579
+ :param ids: List of Image IDs in Supervisely.
3580
+ :type ids: :class:`List[int]`
3581
+ :param semaphore: Semaphore for limiting the number of simultaneous downloads.
3582
+ :type semaphore: :class:`asyncio.Semaphore`, optional
3583
+ :param keep_alpha: If True keeps alpha mask for images, otherwise don't.
3584
+ :type keep_alpha: bool, optional
3585
+ :param progress_cb: Function for tracking download progress.
3586
+ :type progress_cb: tqdm or callable, optional
3587
+ :param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "number".
3588
+ :type progress_cb_type: Literal["number", "size"], optional
3589
+ :return: List of Images in RGB numpy matrix format
3590
+ :rtype: :class:`List[np.ndarray]`
3591
+
3592
+ :Usage example:
3593
+
3594
+ .. code-block:: python
3595
+
3596
+ import supervisely as sly
3597
+ import asyncio
3598
+
3599
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
3600
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
3601
+ api = sly.Api.from_env()
3602
+
3603
+ DATASET_ID = 98357
3604
+ semaphore = asyncio.Semaphore(100)
3605
+ images = api.image.get_list(DATASET_ID)
3606
+ img_ids = [image.id for image in images]
3607
+ loop = asyncio.new_event_loop()
3608
+ asyncio.set_event_loop(loop)
3609
+ results = loop.run_until_complete(
3610
+ api.image.download_nps_async(img_ids, semaphore)
3611
+ )
3612
+
3613
+ """
3614
+ if semaphore is None:
3615
+ semaphore = self._api._get_default_semaphore()
3616
+ tasks = [
3617
+ self.download_np_async(id, semaphore, keep_alpha, progress_cb, progress_cb_type)
3618
+ for id in ids
3619
+ ]
3620
+ return await asyncio.gather(*tasks)
3621
+
3622
+ async def download_path_async(
3623
+ self,
3624
+ id: int,
3625
+ path: str,
3626
+ semaphore: Optional[asyncio.Semaphore] = None,
3627
+ range_start: Optional[int] = None,
3628
+ range_end: Optional[int] = None,
3629
+ headers: Optional[dict] = None,
3630
+ check_hash: bool = True,
3631
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
3632
+ progress_cb_type: Literal["number", "size"] = "number",
3633
+ ) -> None:
3634
+ """
3635
+ Downloads Image with given ID to local path.
3636
+
3637
+ :param id: Image ID in Supervisely.
3638
+ :type id: int
3639
+ :param path: Local save path for Image.
3640
+ :type path: str
3641
+ :param semaphore: Semaphore for limiting the number of simultaneous downloads.
3642
+ :type semaphore: :class:`asyncio.Semaphore`, optional
3643
+ :param range_start: Start byte of range for partial download.
3644
+ :type range_start: int, optional
3645
+ :param range_end: End byte of range for partial download.
3646
+ :type range_end: int, optional
3647
+ :param headers: Headers for request.
3648
+ :type headers: dict, optional
3649
+ :param check_hash: If True, checks hash of downloaded file.
3650
+ Check is not supported for partial downloads.
3651
+ When range is set, hash check is disabled.
3652
+ :type check_hash: bool, optional
3653
+ :param progress_cb: Function for tracking download progress.
3654
+ :type progress_cb: tqdm or callable, optional
3655
+ :param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "number".
3656
+ :type progress_cb_type: Literal["number", "size"], optional
3657
+ :return: None
3658
+ :rtype: :class:`NoneType`
3659
+ :Usage example:
3660
+
3661
+ .. code-block:: python
3662
+
3663
+ import supervisely as sly
3664
+ import asyncio
3665
+
3666
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
3667
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
3668
+ api = sly.Api.from_env()
3669
+
3670
+ img_info = api.image.get_info_by_id(770918)
3671
+ save_path = os.path.join("/path/to/save/", img_info.name)
3672
+
3673
+ semaphore = asyncio.Semaphore(100)
3674
+ loop = asyncio.new_event_loop()
3675
+ asyncio.set_event_loop(loop)
3676
+ loop.run_until_complete(
3677
+ api.image.download_path_async(img_info.id, save_path, semaphore)
3678
+ )
3679
+ """
3680
+ if range_start is not None or range_end is not None:
3681
+ check_hash = False # hash check is not supported for partial downloads
3682
+ headers = headers or {}
3683
+ headers["Range"] = f"bytes={range_start or ''}-{range_end or ''}"
3684
+ logger.debug(f"Image ID: {id}. Setting Range header: {headers['Range']}")
3685
+
3686
+ writing_method = "ab" if range_start not in [0, None] else "wb"
3687
+
3688
+ ensure_base_path(path)
3689
+ hash_to_check = None
3690
+ if semaphore is None:
3691
+ semaphore = self._api._get_default_semaphore()
3692
+ async with semaphore:
3693
+ async with aiofiles.open(path, writing_method) as fd:
3694
+ async for chunk, hhash in self._download_async(
3695
+ id,
3696
+ is_stream=True,
3697
+ range_start=range_start,
3698
+ range_end=range_end,
3699
+ headers=headers,
3700
+ ):
3701
+ await fd.write(chunk)
3702
+ hash_to_check = hhash
3703
+ if progress_cb is not None and progress_cb_type == "size":
3704
+ progress_cb(len(chunk))
3705
+ if progress_cb is not None and progress_cb_type == "number":
3706
+ progress_cb(1)
3707
+ if check_hash:
3708
+ if hash_to_check is not None:
3709
+ downloaded_file_hash = await get_file_hash_async(path)
3710
+ if hash_to_check != downloaded_file_hash:
3711
+ raise RuntimeError(
3712
+ f"Downloaded hash of image with ID:{id} does not match the expected hash: {downloaded_file_hash} != {hash_to_check}"
3713
+ )
3714
+
3715
+ async def download_paths_async(
3716
+ self,
3717
+ ids: List[int],
3718
+ paths: List[str],
3719
+ semaphore: Optional[asyncio.Semaphore] = None,
3720
+ headers: Optional[dict] = None,
3721
+ check_hash: bool = True,
3722
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
3723
+ progress_cb_type: Literal["number", "size"] = "number",
3724
+ ) -> None:
3725
+ """
3726
+ Download Images with given IDs and saves them to given local paths asynchronously.
3727
+
3728
+ :param ids: List of Image IDs in Supervisely.
3729
+ :type ids: :class:`List[int]`
3730
+ :param paths: Local save paths for Images.
3731
+ :type paths: :class:`List[str]`
3732
+ :param semaphore: Semaphore for limiting the number of simultaneous downloads.
3733
+ :type semaphore: :class:`asyncio.Semaphore`, optional
3734
+ :param headers: Headers for request.
3735
+ :type headers: dict, optional
3736
+ :param check_hash: If True, checks hash of downloaded images.
3737
+ :type check_hash: bool, optional
3738
+ :param progress_cb: Function for tracking download progress.
3739
+ :type progress_cb: tqdm or callable, optional
3740
+ :param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "number".
3741
+ :type progress_cb_type: Literal["number", "size"], optional
3742
+ :raises: :class:`ValueError` if len(ids) != len(paths)
3743
+ :return: None
3744
+ :rtype: :class:`NoneType`
3745
+
3746
+ :Usage example:
3747
+
3748
+ .. code-block:: python
3749
+
3750
+ import supervisely as sly
3751
+ import asyncio
3752
+
3753
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
3754
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
3755
+ api = sly.Api.from_env()
3756
+
3757
+ ids = [770918, 770919]
3758
+ paths = ["/path/to/save/image1.png", "/path/to/save/image2.png"]
3759
+ loop = asyncio.new_event_loop()
3760
+ asyncio.set_event_loop(loop)
3761
+ loop.run_until_complete(api.image.download_paths_async(ids, paths))
3762
+ """
3763
+ if len(ids) == 0:
3764
+ return
3765
+ if len(ids) != len(paths):
3766
+ raise ValueError(
3767
+ f'Length of "ids" and "paths" should be equal. {len(ids)} != {len(paths)}'
3768
+ )
3769
+ if semaphore is None:
3770
+ semaphore = self._api._get_default_semaphore()
3771
+ tasks = []
3772
+ for img_id, img_path in zip(ids, paths):
3773
+ task = self.download_path_async(
3774
+ img_id,
3775
+ img_path,
3776
+ semaphore,
3777
+ headers=headers,
3778
+ check_hash=check_hash,
3779
+ progress_cb=progress_cb,
3780
+ progress_cb_type=progress_cb_type,
3781
+ )
3782
+ tasks.append(task)
3783
+ await asyncio.gather(*tasks)
3784
+
3785
+ async def download_bytes_single_async(
3786
+ self,
3787
+ id: int,
3788
+ semaphore: Optional[asyncio.Semaphore] = None,
3789
+ range_start: Optional[int] = None,
3790
+ range_end: Optional[int] = None,
3791
+ headers: Optional[dict] = None,
3792
+ check_hash: bool = True,
3793
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
3794
+ progress_cb_type: Literal["number", "size"] = "number",
3795
+ ) -> bytes:
3796
+ """
3797
+ Downloads Image bytes with given ID.
3798
+
3799
+ :param id: Image ID in Supervisely.
3800
+ :type id: int
3801
+ :param semaphore: Semaphore for limiting the number of simultaneous downloads.
3802
+ :type semaphore: :class:`asyncio.Semaphore`, optional
3803
+ :param range_start: Start byte of range for partial download.
3804
+ :type range_start: int, optional
3805
+ :param range_end: End byte of range for partial download.
3806
+ :type range_end: int, optional
3807
+ :param headers: Headers for request.
3808
+ :type headers: dict, optional
3809
+ :param check_hash: If True, checks hash of downloaded bytes.
3810
+ Check is not supported for partial downloads.
3811
+ When range is set, hash check is disabled.
3812
+ :type check_hash: bool, optional
3813
+ :param progress_cb: Function for tracking download progress.
3814
+ :type progress_cb: Optional[Union[tqdm, Callable]]
3815
+ :param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "number".
3816
+ :type progress_cb_type: Literal["number", "size"], optional
3817
+ :return: Bytes of downloaded image.
3818
+ :rtype: :class:`bytes`
3819
+ :Usage example:
3820
+
3821
+ .. code-block:: python
3822
+
3823
+ import supervisely as sly
3824
+ import asyncio
3825
+
3826
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
3827
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
3828
+ api = sly.Api.from_env()
3829
+
3830
+ img_id = 770918
3831
+ loop = asyncio.new_event_loop()
3832
+ asyncio.set_event_loop(loop)
3833
+ img_bytes = loop.run_until_complete(api.image.download_bytes_async(img_id))
3834
+
3835
+ """
3836
+ if range_start is not None or range_end is not None:
3837
+ check_hash = False # hash check is not supported for partial downloads
3838
+ headers = headers or {}
3839
+ headers["Range"] = f"bytes={range_start or ''}-{range_end or ''}"
3840
+ logger.debug(f"Image ID: {id}. Setting Range header: {headers['Range']}")
3841
+
3842
+ hash_to_check = None
3843
+
3844
+ if semaphore is None:
3845
+ semaphore = self._api._get_default_semaphore()
3846
+ async with semaphore:
3847
+ content = b""
3848
+ async for chunk, hhash in self._download_async(
3849
+ id,
3850
+ is_stream=True,
3851
+ headers=headers,
3852
+ range_start=range_start,
3853
+ range_end=range_end,
3854
+ ):
3855
+ content += chunk
3856
+ hash_to_check = hhash
3857
+ if progress_cb is not None and progress_cb_type == "size":
3858
+ progress_cb(len(chunk))
3859
+ if check_hash:
3860
+ if hash_to_check is not None:
3861
+ downloaded_bytes_hash = get_bytes_hash(content)
3862
+ if hash_to_check != downloaded_bytes_hash:
3863
+ raise RuntimeError(
3864
+ f"Downloaded hash of image with ID:{id} does not match the expected hash: {downloaded_bytes_hash} != {hash_to_check}"
3865
+ )
3866
+ if progress_cb is not None and progress_cb_type == "number":
3867
+ progress_cb(1)
3868
+ return content
3869
+
3870
+ async def download_bytes_many_async(
3871
+ self,
3872
+ ids: List[int],
3873
+ semaphore: Optional[asyncio.Semaphore] = None,
3874
+ headers: Optional[dict] = None,
3875
+ check_hash: bool = True,
3876
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
3877
+ progress_cb_type: Literal["number", "size"] = "number",
3878
+ ) -> List[bytes]:
3879
+ """
3880
+ Downloads Images bytes with given IDs asynchronously
3881
+ and returns reults in the same order as in the input list.
3882
+
3883
+ :param ids: List of Image IDs in Supervisely.
3884
+ :type ids: :class:`List[int]`
3885
+ :param semaphore: Semaphore for limiting the number of simultaneous downloads.
3886
+ :type semaphore: :class:`asyncio.Semaphore`, optional
3887
+ :param headers: Headers for every request.
3888
+ :type headers: dict, optional
3889
+ :param check_hash: If True, checks hash of downloaded images.
3890
+ :type check_hash: bool, optional
3891
+ :param progress_cb: Function for tracking download progress.
3892
+ :type progress_cb: Optional[Union[tqdm, Callable]]
3893
+ :param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "number".
3894
+ :type progress_cb_type: Literal["number", "size"], optional
3895
+ :return: List of bytes of downloaded images.
3896
+ :rtype: :class:`List[bytes]`
3897
+
3898
+ :Usage example:
3899
+
3900
+ .. code-block:: python
3901
+
3902
+ import supervisely as sly
3903
+ import asyncio
3904
+
3905
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
3906
+ os.environ['API_TOKEN
3907
+ api = sly.Api.from_env()
3908
+
3909
+ loop = asyncio.new_event_loop()
3910
+ asyncio.set_event_loop(loop)
3911
+ semaphore = asyncio.Semaphore(100)
3912
+ img_bytes_list = loop.run_until_complete(api.image.download_bytes_imgs_async(ids, semaphore))
3913
+ """
3914
+ if semaphore is None:
3915
+ semaphore = self._api._get_default_semaphore()
3916
+ tasks = []
3917
+ for id in ids:
3918
+ task = self.download_bytes_single_async(
3919
+ id,
3920
+ semaphore,
3921
+ headers=headers,
3922
+ check_hash=check_hash,
3923
+ progress_cb=progress_cb,
3924
+ progress_cb_type=progress_cb_type,
3925
+ )
3926
+ tasks.append(task)
3927
+ results = await asyncio.gather(*tasks)
3928
+ return results