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.

@@ -3,18 +3,19 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
+ import asyncio
6
7
  import mimetypes
7
8
  import os
8
9
  import shutil
9
10
  import tarfile
10
11
  import tempfile
11
- import urllib
12
12
  from pathlib import Path
13
13
  from time import time
14
14
  from typing import Callable, Dict, List, NamedTuple, Optional, Union
15
15
 
16
- from dotenv import load_dotenv
16
+ import aiofiles
17
17
  import requests
18
+ from dotenv import load_dotenv
18
19
  from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
19
20
  from tqdm import tqdm
20
21
  from typing_extensions import Literal
@@ -27,6 +28,7 @@ from supervisely.io.fs import (
27
28
  ensure_base_path,
28
29
  get_file_ext,
29
30
  get_file_hash,
31
+ get_file_hash_async,
30
32
  get_file_name,
31
33
  get_file_name_with_ext,
32
34
  get_file_size,
@@ -36,7 +38,7 @@ from supervisely.io.fs import (
36
38
  from supervisely.io.fs_cache import FileCache
37
39
  from supervisely.io.json import load_json_file
38
40
  from supervisely.sly_logger import logger
39
- from supervisely.task.progress import Progress
41
+ from supervisely.task.progress import Progress, tqdm_sly
40
42
 
41
43
 
42
44
  class FileInfo(NamedTuple):
@@ -414,13 +416,30 @@ class FileApi(ModuleApiBase):
414
416
  return dir_size
415
417
 
416
418
  def _download(
417
- self, team_id, remote_path, local_save_path, progress_cb=None
418
- ): # TODO: progress bar
419
+ self,
420
+ team_id,
421
+ remote_path,
422
+ local_save_path,
423
+ progress_cb=None,
424
+ log_progress: bool = False,
425
+ ):
419
426
  response = self._api.post(
420
427
  "file-storage.download",
421
428
  {ApiField.TEAM_ID: team_id, ApiField.PATH: remote_path},
422
429
  stream=True,
423
430
  )
431
+ if progress_cb is not None:
432
+ log_progress = False
433
+
434
+ if log_progress and progress_cb is None:
435
+ total_size = int(response.headers.get("Content-Length", 0))
436
+ progress_cb = tqdm_sly(
437
+ total=total_size,
438
+ unit="B",
439
+ unit_scale=True,
440
+ desc="Downloading file",
441
+ leave=True,
442
+ )
424
443
  # print(response.headers)
425
444
  # print(response.headers['Content-Length'])
426
445
  ensure_base_path(local_save_path)
@@ -500,6 +519,7 @@ class FileApi(ModuleApiBase):
500
519
  def parse_agent_id_and_path(self, remote_path: str) -> int:
501
520
  return sly_fs.parse_agent_id_and_path(remote_path)
502
521
 
522
+ # TODO replace with download_async
503
523
  def download_from_agent(
504
524
  self,
505
525
  remote_path: str,
@@ -530,6 +550,7 @@ class FileApi(ModuleApiBase):
530
550
  if progress_cb is not None:
531
551
  progress_cb(len(chunk))
532
552
 
553
+ # TODO replace with download_directory_async
533
554
  def download_directory(
534
555
  self,
535
556
  team_id: int,
@@ -603,7 +624,7 @@ class FileApi(ModuleApiBase):
603
624
  log_progress: bool = False,
604
625
  ) -> None:
605
626
  """Downloads data for application from input using environment variables.
606
- Automatically detects is data is a file or a directory and saves it to the specified directory.
627
+ Automatically detects if data is a file or a directory and saves it to the specified directory.
607
628
  If data is an archive, it will be unpacked to the specified directory if unpack_if_archive is True.
608
629
 
609
630
  :param save_path: path to a directory where data will be saved
@@ -616,9 +637,11 @@ class FileApi(ModuleApiBase):
616
637
  :type force: Optional[bool]
617
638
  :param log_progress: if True, progress bar will be displayed
618
639
  :type log_progress: bool
619
- :raises RuntimeError: if both file and folder paths not found in environment variables
620
- :raises RuntimeError: if both file and folder paths found in environment variables (debug)
621
- :raises RuntimeError: if team id not found in environment variables
640
+ :raises RuntimeError:
641
+ - if both file and folder paths not found in environment variables \n
642
+ - if both file and folder paths found in environment variables (debug)
643
+ - if team id not found in environment variables
644
+
622
645
  :Usage example:
623
646
 
624
647
  .. code-block:: python
@@ -1494,3 +1517,494 @@ class FileApi(ModuleApiBase):
1494
1517
  return content
1495
1518
  else:
1496
1519
  raise FileNotFoundError(f"File not found: {remote_path}")
1520
+
1521
+ async def _download_async(
1522
+ self,
1523
+ team_id: int,
1524
+ remote_path: str,
1525
+ local_save_path: str,
1526
+ range_start: Optional[int] = None,
1527
+ range_end: Optional[int] = None,
1528
+ headers: dict = None,
1529
+ check_hash: bool = True,
1530
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
1531
+ progress_cb_type: Literal["number", "size"] = "size",
1532
+ ):
1533
+ """
1534
+ Download file from Team Files or connected Cloud Storage.
1535
+
1536
+ :param team_id: Team ID in Supervisely.
1537
+ :type team_id: int
1538
+ :param remote_path: Path to File in Team Files.
1539
+ :type remote_path: str
1540
+ :param local_save_path: Local save path.
1541
+ :type local_save_path: str
1542
+ :param range_start: Start byte position for partial download.
1543
+ :type range_start: int, optional
1544
+ :param range_end: End byte position for partial download.
1545
+ :type range_end: int, optional
1546
+ :param headers: Additional headers for request.
1547
+ :type headers: dict, optional
1548
+ :param check_hash: If True, checks hash of downloaded file.
1549
+ Check is not supported for partial downloads.
1550
+ When range is set, hash check is disabled.
1551
+ :type check_hash: bool
1552
+ :param progress_cb: Function for tracking download progress.
1553
+ :type progress_cb: tqdm or callable, optional
1554
+ :param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "size".
1555
+ :type progress_cb_type: Literal["number", "size"], optional
1556
+ :return: None
1557
+ :rtype: :class:`NoneType`
1558
+ """
1559
+ api_method = "file-storage.download"
1560
+
1561
+ if range_start is not None or range_end is not None:
1562
+ check_hash = False
1563
+ headers = headers or {}
1564
+ headers["Range"] = f"bytes={range_start or ''}-{range_end or ''}"
1565
+ logger.debug(f"File: {remote_path}. Setting Range header: {headers['Range']}")
1566
+
1567
+ json_body = {
1568
+ ApiField.TEAM_ID: team_id,
1569
+ ApiField.PATH: remote_path,
1570
+ **self._api.additional_fields,
1571
+ }
1572
+
1573
+ writing_method = "ab" if range_start not in [0, None] else "wb"
1574
+
1575
+ ensure_base_path(local_save_path)
1576
+ hash_to_check = None
1577
+ async with aiofiles.open(local_save_path, writing_method) as fd:
1578
+ async for chunk, hhash in self._api.stream_async(
1579
+ method=api_method,
1580
+ method_type="POST",
1581
+ data=json_body,
1582
+ headers=headers,
1583
+ range_start=range_start,
1584
+ range_end=range_end,
1585
+ ):
1586
+ await fd.write(chunk)
1587
+ hash_to_check = hhash
1588
+ if progress_cb is not None and progress_cb_type == "size":
1589
+ progress_cb(len(chunk))
1590
+ await fd.flush()
1591
+
1592
+ if check_hash:
1593
+ if hash_to_check is not None:
1594
+ downloaded_file_hash = await get_file_hash_async(local_save_path)
1595
+ if hash_to_check != downloaded_file_hash:
1596
+ raise RuntimeError(
1597
+ f"Downloaded hash of image with ID:{id} does not match the expected hash: {downloaded_file_hash} != {hash_to_check}"
1598
+ )
1599
+ if progress_cb is not None and progress_cb_type == "number":
1600
+ progress_cb(1)
1601
+
1602
+ async def download_async(
1603
+ self,
1604
+ team_id: int,
1605
+ remote_path: str,
1606
+ local_save_path: str,
1607
+ semaphore: Optional[asyncio.Semaphore] = None,
1608
+ cache: Optional[FileCache] = None,
1609
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
1610
+ progress_cb_type: Literal["number", "size"] = "size",
1611
+ ) -> None:
1612
+ """
1613
+ Download File from Team Files.
1614
+
1615
+ :param team_id: Team ID in Supervisely.
1616
+ :type team_id: int
1617
+ :param remote_path: Path to File in Team Files.
1618
+ :type remote_path: str
1619
+ :param local_save_path: Local save path.
1620
+ :type local_save_path: str
1621
+ :param semaphore: Semaphore for limiting the number of simultaneous downloads.
1622
+ :type semaphore: asyncio.Semaphore
1623
+ :param cache: Cache object for storing files.
1624
+ :type cache: FileCache, optional
1625
+ :param progress_cb: Function for tracking download progress.
1626
+ :type progress_cb: tqdm or callable, optional
1627
+ :param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "size".
1628
+ :type progress_cb_type: Literal["number", "size"], optional
1629
+ :return: None
1630
+ :rtype: :class:`NoneType`
1631
+ :Usage example:
1632
+
1633
+ .. code-block:: python
1634
+
1635
+ import supervisely as sly
1636
+ import asyncio
1637
+
1638
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
1639
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
1640
+ api = sly.Api.from_env()
1641
+
1642
+ path_to_file = "/999_App_Test/ds1/01587.json"
1643
+ local_save_path = "/path/to/save/999_App_Test/ds1/01587.json"
1644
+ loop = asyncio.new_event_loop()
1645
+ asyncio.set_event_loop(loop)
1646
+ loop.run_until_complete(api.file.download_async(8, path_to_file, local_save_path))
1647
+ """
1648
+ if semaphore is None:
1649
+ semaphore = self._api._get_default_semaphore()
1650
+ async with semaphore:
1651
+ if self.is_on_agent(remote_path):
1652
+ # for optimized download from agent
1653
+ # in other agent cases download will be performed as usual
1654
+ agent_id, path_in_agent_folder = self.parse_agent_id_and_path(remote_path)
1655
+ if (
1656
+ agent_id == env.agent_id(raise_not_found=False)
1657
+ and env.agent_storage(raise_not_found=False) is not None
1658
+ ):
1659
+ path_on_agent = os.path.normpath(env.agent_storage() + path_in_agent_folder)
1660
+ logger.info(f"Optimized download from agent: {path_on_agent}")
1661
+ await sly_fs.copy_file_async(
1662
+ path_on_agent, local_save_path, progress_cb, progress_cb_type
1663
+ )
1664
+ return
1665
+
1666
+ if cache is None:
1667
+ await self._download_async(
1668
+ team_id,
1669
+ remote_path,
1670
+ local_save_path,
1671
+ progress_cb=progress_cb,
1672
+ progress_cb_type=progress_cb_type,
1673
+ )
1674
+ else:
1675
+ file_info = self.get_info_by_path(team_id, remote_path)
1676
+ if file_info.hash is None:
1677
+ await self._download_async(
1678
+ team_id,
1679
+ remote_path,
1680
+ local_save_path,
1681
+ progress_cb=progress_cb,
1682
+ progress_cb_type=progress_cb_type,
1683
+ )
1684
+ else:
1685
+ cache_path = cache.check_storage_object(
1686
+ file_info.hash, get_file_ext(remote_path)
1687
+ )
1688
+ if cache_path is None:
1689
+ # file not in cache
1690
+ await self._download_async(
1691
+ team_id,
1692
+ remote_path,
1693
+ local_save_path,
1694
+ progress_cb=progress_cb,
1695
+ progress_cb_type=progress_cb_type,
1696
+ )
1697
+ if file_info.hash != await get_file_hash_async(local_save_path):
1698
+ raise KeyError(
1699
+ f"Remote and local hashes are different (team id: {team_id}, file: {remote_path})"
1700
+ )
1701
+ await cache.write_object_async(local_save_path, file_info.hash)
1702
+ else:
1703
+ await cache.read_object_async(file_info.hash, local_save_path)
1704
+ if progress_cb is not None and progress_cb_type == "size":
1705
+ progress_cb(get_file_size(local_save_path))
1706
+ if progress_cb is not None and progress_cb_type == "number":
1707
+ progress_cb(1)
1708
+
1709
+ async def download_bulk_async(
1710
+ self,
1711
+ team_id: int,
1712
+ remote_paths: List[str],
1713
+ local_save_paths: List[str],
1714
+ semaphore: Optional[asyncio.Semaphore] = None,
1715
+ caches: Optional[List[FileCache]] = None,
1716
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
1717
+ progress_cb_type: Literal["number", "size"] = "size",
1718
+ ):
1719
+ """
1720
+ Download multiple Files from Team Files.
1721
+
1722
+ :param team_id: Team ID in Supervisely.
1723
+ :type team_id: int
1724
+ :param remote_paths: List of paths to Files in Team Files.
1725
+ :type remote_paths: List[str]
1726
+ :param local_save_paths: List of local save paths.
1727
+ :type local_save_paths: List[str]
1728
+ :param semaphore: Semaphore for limiting the number of simultaneous downloads.
1729
+ :type semaphore: asyncio.Semaphore
1730
+ :param caches: List of cache objects for storing files.
1731
+ :type caches: List[FileCache], optional
1732
+ :param progress_cb: Function for tracking download progress.
1733
+ :type progress_cb: tqdm or callable, optional
1734
+ :param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "size".
1735
+ :type progress_cb_type: Literal["number", "size"], optional
1736
+ :return: None
1737
+ :rtype: :class:`NoneType`
1738
+ :Usage example:
1739
+
1740
+ .. code-block:: python
1741
+
1742
+ import supervisely as sly
1743
+ import asyncio
1744
+
1745
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
1746
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
1747
+ api = sly.Api.from_env()
1748
+
1749
+ paths_to_files = [
1750
+ "/999_App_Test/ds1/01587.json",
1751
+ "/999_App_Test/ds1/01588.json",
1752
+ "/999_App_Test/ds1/01587.json"
1753
+ ]
1754
+ local_paths = [
1755
+ "/path/to/save/999_App_Test/ds1/01587.json",
1756
+ "/path/to/save/999_App_Test/ds1/01588.json",
1757
+ "/path/to/save/999_App_Test/ds1/01587.json"
1758
+ ]
1759
+ loop = asyncio.new_event_loop()
1760
+ asyncio.set_event_loop(loop)
1761
+ loop.run_until_complete(
1762
+ api.file.download_bulk_async(8, paths_to_files, local_paths)
1763
+ )
1764
+ """
1765
+ if len(remote_paths) == 0:
1766
+ return
1767
+
1768
+ if len(remote_paths) != len(local_save_paths):
1769
+ raise ValueError(
1770
+ f"Length of remote_paths and local_save_paths must be equal: {len(remote_paths)} != {len(local_save_paths)}"
1771
+ )
1772
+ elif caches is not None and len(remote_paths) != len(caches):
1773
+ raise ValueError(
1774
+ f"Length of remote_paths and caches must be equal: {len(remote_paths)} != {len(caches)}"
1775
+ )
1776
+
1777
+ if semaphore is None:
1778
+ semaphore = self._api._get_default_semaphore()
1779
+
1780
+ tasks = []
1781
+ for remote_path, local_path, cache in zip(
1782
+ remote_paths, local_save_paths, caches or [None] * len(remote_paths)
1783
+ ):
1784
+ task = self.download_async(
1785
+ team_id,
1786
+ remote_path,
1787
+ local_path,
1788
+ semaphore=semaphore,
1789
+ cache=cache,
1790
+ progress_cb=progress_cb,
1791
+ progress_cb_type=progress_cb_type,
1792
+ )
1793
+ tasks.append(task)
1794
+ await asyncio.gather(*tasks)
1795
+
1796
+ async def download_directory_async(
1797
+ self,
1798
+ team_id: int,
1799
+ remote_path: str,
1800
+ local_save_path: str,
1801
+ semaphore: Optional[asyncio.Semaphore] = None,
1802
+ show_progress: bool = True,
1803
+ ) -> None:
1804
+ """
1805
+ Download Directory from Team Files to local path asynchronously.
1806
+
1807
+ :param team_id: Team ID in Supervisely.
1808
+ :type team_id: int
1809
+ :param remote_path: Path to Directory in Team Files.
1810
+ :type remote_path: str
1811
+ :param local_save_path: Local save path.
1812
+ :type local_save_path: str
1813
+ :param semaphore: Semaphore for limiting the number of simultaneous downloads.
1814
+ :type semaphore: asyncio.Semaphore
1815
+ :param show_progress: If True show download progress.
1816
+ :type show_progress: bool
1817
+ :return: None
1818
+ :rtype: :class:`NoneType`
1819
+ :Usage example:
1820
+
1821
+ .. code-block:: python
1822
+
1823
+ import supervisely as sly
1824
+ import asyncio
1825
+
1826
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
1827
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
1828
+ api = sly.Api.from_env()
1829
+
1830
+ path_to_dir = "/files/folder"
1831
+ local_path = "path/to/local/folder"
1832
+
1833
+ loop = asyncio.new_event_loop()
1834
+ asyncio.set_event_loop(loop)
1835
+ loop.run_until_complete(
1836
+ api.file.download_directory_async(9, path_to_dir, local_path)
1837
+ )
1838
+ """
1839
+
1840
+ if semaphore is None:
1841
+ semaphore = self._api._get_default_semaphore()
1842
+
1843
+ if not remote_path.endswith("/"):
1844
+ remote_path += "/"
1845
+
1846
+ tasks = []
1847
+ files = self._api.storage.list( # to avoid method duplication in storage api
1848
+ team_id,
1849
+ remote_path,
1850
+ recursive=True,
1851
+ include_folders=False,
1852
+ with_metadata=False,
1853
+ )
1854
+ sizeb = sum([file.sizeb for file in files])
1855
+ if show_progress:
1856
+ progress_cb = tqdm_sly(
1857
+ total=sizeb, desc=f"Downloading files from directory", unit="B", unit_scale=True
1858
+ )
1859
+ else:
1860
+ progress_cb = None
1861
+
1862
+ for file in files:
1863
+ task = self.download_async(
1864
+ team_id,
1865
+ file.path,
1866
+ os.path.join(local_save_path, file.path[len(remote_path) :]),
1867
+ semaphore=semaphore,
1868
+ progress_cb=progress_cb,
1869
+ )
1870
+ tasks.append(task)
1871
+ await asyncio.gather(*tasks)
1872
+
1873
+ async def download_input_async(
1874
+ self,
1875
+ save_path: str,
1876
+ semaphore: Optional[asyncio.Semaphore] = None,
1877
+ unpack_if_archive: Optional[bool] = True,
1878
+ remove_archive: Optional[bool] = True,
1879
+ force: Optional[bool] = False,
1880
+ show_progress: bool = False,
1881
+ ) -> None:
1882
+ """Asynchronously downloads data for the application, using a path from file/folder selector.
1883
+ The application adds this path to environment variables, which the method then reads.
1884
+ Automatically detects if data is a file or a directory and saves it to the specified directory.
1885
+ If data is an archive, it will be unpacked to the specified directory if unpack_if_archive is True.
1886
+
1887
+ :param save_path: path to a directory where data will be saved
1888
+ :type save_path: str
1889
+ :param semaphore: Semaphore for limiting the number of simultaneous downloads
1890
+ :type semaphore: asyncio.Semaphore
1891
+ :param unpack_if_archive: if True, archive will be unpacked to the specified directory
1892
+ :type unpack_if_archive: Optional[bool]
1893
+ :param remove_archive: if True, archive will be removed after unpacking
1894
+ :type remove_archive: Optional[bool]
1895
+ :param force: if True, data will be downloaded even if it already exists in the specified directory
1896
+ :type force: Optional[bool]
1897
+ :param show_progress: if True, progress bar will be displayed
1898
+ :type show_progress: bool
1899
+ :raises RuntimeError:
1900
+ - if both file and folder paths not found in environment variables \n
1901
+ - if both file and folder paths found in environment variables (debug)
1902
+ - if team id not found in environment variables
1903
+
1904
+ :Usage example:
1905
+
1906
+ .. code-block:: python
1907
+
1908
+ import os
1909
+ from dotenv import load_dotenv
1910
+
1911
+ import supervisely as sly
1912
+ import asyncio
1913
+
1914
+ # Load secrets and create API object from .env file (recommended)
1915
+ # Learn more here: https://developer.supervisely.com/getting-started/basics-of-authentication
1916
+ load_dotenv(os.path.expanduser("~/supervisely.env"))
1917
+ api = sly.Api.from_env()
1918
+
1919
+ # Application is started...
1920
+ save_path = "/my_app_data"
1921
+ loop = asyncio.new_event_loop()
1922
+ asyncio.set_event_loop(loop)
1923
+ loop.run_until_complete(
1924
+ api.file.download_input_async(save_path)
1925
+ )
1926
+
1927
+ # The data is downloaded to the specified directory.
1928
+ """
1929
+
1930
+ if semaphore is None:
1931
+ semaphore = self._api._get_default_semaphore()
1932
+
1933
+ remote_file_path = env.file(raise_not_found=False)
1934
+ remote_folder_path = env.folder(raise_not_found=False)
1935
+ team_id = env.team_id()
1936
+
1937
+ sly_fs.mkdir(save_path)
1938
+
1939
+ if remote_file_path is None and remote_folder_path is None:
1940
+ raise RuntimeError(
1941
+ "Both file and folder paths not found in environment variables. "
1942
+ "Please, specify one of them."
1943
+ )
1944
+ elif remote_file_path is not None and remote_folder_path is not None:
1945
+ raise RuntimeError(
1946
+ "Both file and folder paths found in environment variables. "
1947
+ "Please, specify only one of them."
1948
+ )
1949
+ if team_id is None:
1950
+ raise RuntimeError("Team id not found in environment variables.")
1951
+
1952
+ if remote_file_path is not None:
1953
+ file_name = sly_fs.get_file_name_with_ext(remote_file_path)
1954
+ local_file_path = os.path.join(save_path, file_name)
1955
+
1956
+ if os.path.isfile(local_file_path) and not force:
1957
+ logger.info(
1958
+ f"The file {local_file_path} already exists. "
1959
+ "Download is skipped, if you want to download it again, "
1960
+ "use force=True."
1961
+ )
1962
+ return
1963
+
1964
+ sly_fs.silent_remove(local_file_path)
1965
+
1966
+ progress_cb = None
1967
+ file_info = self.get_info_by_path(team_id, remote_file_path)
1968
+ if show_progress is True and file_info is not None:
1969
+ progress_cb = tqdm_sly(
1970
+ desc=f"Downloading {remote_file_path}",
1971
+ total=file_info.sizeb,
1972
+ unit="B",
1973
+ unit_scale=True,
1974
+ )
1975
+ await self.download_async(
1976
+ team_id,
1977
+ remote_file_path,
1978
+ local_file_path,
1979
+ semaphore=semaphore,
1980
+ progress_cb=progress_cb,
1981
+ )
1982
+ if unpack_if_archive and sly_fs.is_archive(local_file_path):
1983
+ await sly_fs.unpack_archive_async(local_file_path, save_path)
1984
+ if remove_archive:
1985
+ sly_fs.silent_remove(local_file_path)
1986
+ else:
1987
+ logger.info(
1988
+ f"Achive {local_file_path} was unpacked, but not removed. "
1989
+ "If you want to remove it, use remove_archive=True."
1990
+ )
1991
+ elif remote_folder_path is not None:
1992
+ folder_name = os.path.basename(os.path.normpath(remote_folder_path))
1993
+ local_folder_path = os.path.join(save_path, folder_name)
1994
+ if os.path.isdir(local_folder_path) and not force:
1995
+ logger.info(
1996
+ f"The folder {folder_name} already exists. "
1997
+ "Download is skipped, if you want to download it again, "
1998
+ "use force=True."
1999
+ )
2000
+ return
2001
+
2002
+ sly_fs.remove_dir(local_folder_path)
2003
+
2004
+ await self.download_directory_async(
2005
+ team_id,
2006
+ remote_folder_path,
2007
+ local_folder_path,
2008
+ semaphore=semaphore,
2009
+ show_progress=show_progress,
2010
+ )