supervisely 6.73.220__py3-none-any.whl → 6.73.222__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.

Files changed (27) hide show
  1. supervisely/api/api.py +609 -3
  2. supervisely/api/file_api.py +574 -14
  3. supervisely/api/image_api.py +469 -0
  4. supervisely/api/pointcloud/pointcloud_api.py +390 -1
  5. supervisely/api/video/video_api.py +231 -1
  6. supervisely/api/volume/volume_api.py +223 -2
  7. supervisely/app/development/__init__.py +1 -0
  8. supervisely/app/development/development.py +96 -2
  9. supervisely/app/fastapi/subapp.py +19 -4
  10. supervisely/convert/base_converter.py +53 -4
  11. supervisely/convert/converter.py +6 -5
  12. supervisely/convert/image/image_converter.py +26 -13
  13. supervisely/convert/image/sly/fast_sly_image_converter.py +4 -0
  14. supervisely/convert/image/sly/sly_image_converter.py +9 -4
  15. supervisely/convert/pointcloud_episodes/sly/sly_pointcloud_episodes_converter.py +7 -1
  16. supervisely/convert/video/sly/sly_video_converter.py +9 -1
  17. supervisely/convert/video/video_converter.py +44 -23
  18. supervisely/io/fs.py +125 -0
  19. supervisely/io/fs_cache.py +19 -1
  20. supervisely/io/network_exceptions.py +20 -3
  21. supervisely/task/progress.py +1 -1
  22. {supervisely-6.73.220.dist-info → supervisely-6.73.222.dist-info}/METADATA +3 -1
  23. {supervisely-6.73.220.dist-info → supervisely-6.73.222.dist-info}/RECORD +27 -27
  24. {supervisely-6.73.220.dist-info → supervisely-6.73.222.dist-info}/LICENSE +0 -0
  25. {supervisely-6.73.220.dist-info → supervisely-6.73.222.dist-info}/WHEEL +0 -0
  26. {supervisely-6.73.220.dist-info → supervisely-6.73.222.dist-info}/entry_points.txt +0 -0
  27. {supervisely-6.73.220.dist-info → supervisely-6.73.222.dist-info}/top_level.txt +0 -0
@@ -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)
@@ -1,4 +1,5 @@
1
1
  from supervisely.app.development.development import (
2
2
  supervisely_vpn_network,
3
3
  create_debug_task,
4
+ enable_advanced_debug,
4
5
  )
@@ -6,6 +6,8 @@ from typing import Any, Dict, Optional
6
6
 
7
7
  import requests
8
8
 
9
+ import supervisely.io.env as sly_env
10
+ from supervisely._utils import is_development
9
11
  from supervisely.api.api import Api
10
12
  from supervisely.io.fs import mkdir
11
13
  from supervisely.sly_logger import logger
@@ -154,11 +156,12 @@ def supervisely_vpn_network(
154
156
 
155
157
 
156
158
  def create_debug_task(
157
- team_id: int, port: int = 8000, update_status: bool = True
159
+ team_id: int = None, port: int = 8000, update_status: bool = True
158
160
  ) -> Dict[str, Any]:
159
161
  """Gets or creates a debug task for the current user.
160
162
 
161
- :param team_id: The ID of the team to create the task in.
163
+ :param team_id: The ID of the team to create the task in, if not provided, the function
164
+ will try to obtain it from environment variables. Default is None.
162
165
  :type team_id: int
163
166
  :param port: The port to redirect the requests to. Default is 8000.
164
167
  :type port: int
@@ -167,6 +170,14 @@ def create_debug_task(
167
170
  :return: The task details.
168
171
  :rtype: Dict[str, Any]
169
172
  """
173
+ team_id = team_id or sly_env.team_id()
174
+ if not team_id:
175
+ raise ValueError(
176
+ "Team ID is not provided and cannot be obtained from environment variables."
177
+ "The debug task cannot be created. Please provide the team ID as an argument "
178
+ "or set the TEAM_ID environment variable."
179
+ )
180
+
170
181
  api = Api()
171
182
  me = api.user.get_my_info()
172
183
  session_name = me.login + "-development"
@@ -200,4 +211,87 @@ def create_debug_task(
200
211
  if update_status:
201
212
  logger.info(f"Task status will be updated to STARTED for task with ID: {task['id']}")
202
213
  api.task.update_status(task["id"], api.task.Status.STARTED)
214
+
203
215
  return task
216
+
217
+
218
+ def enable_advanced_debug(
219
+ team_id: int = None,
220
+ port: int = 8000,
221
+ update_status: bool = True,
222
+ vpn_action: Literal["up", "down"] = "up",
223
+ vpn_raise_on_error: bool = True,
224
+ only_for_development: bool = True,
225
+ ) -> Optional[int]:
226
+ """Enables advanced debugging for the app.
227
+ At first, it establishes a WireGuard VPN connection to the Supervisely network.
228
+ And then creates a debug task to redirect requests to the local machine.
229
+
230
+ Please, ensure that the Team ID was provided, or set as TEAM_ID environment variable.
231
+ All other parameters can be omitted if using the default instance settings.
232
+
233
+ :param team_id: The ID of the team to create the task in, if not provided, the function
234
+ will try to obtain it from environment variables. Default is None.
235
+ :type team_id: int
236
+ :param port: The port to redirect the requests to. Default is 8000.
237
+ :type port: int
238
+ :param update_status: If True, the task status will be updated to STARTED.
239
+ :type update_status: bool
240
+ :param vpn_action: The action to perform with the VPN connection, either "up" or "down". Default is "up".
241
+ :type vpn_action: Literal["up", "down"]
242
+ :param vpn_raise_on_error: If True, an exception will be raised if an error occurs while connecting to VPN.
243
+ :type vpn_raise_on_error: bool
244
+ :param only_for_development: If True, the debugging will be started only if the app is running in development mode.
245
+ It's not recommended to set this parameter to False in production environments.
246
+ :type only_for_development: bool
247
+ :return: The task ID of the debug task or None if the debugging was not started.
248
+ :rtype: Optional[int]
249
+
250
+ :Usage example:
251
+
252
+ .. code-block:: python
253
+
254
+ import supervisely as sly
255
+
256
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
257
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
258
+ api = sly.Api.from_env()
259
+
260
+ # Ensure that the TEAM_ID environment variable is set.
261
+ # Or provide the team ID as an argument to the function.
262
+ os.environ['TEAM_ID'] = 123456
263
+
264
+ # The task ID can be used to make requests to the app.
265
+ task_id = sly.app.development.enable_advanced_debug()
266
+
267
+ # An example of how to send a request to the app using the task ID.
268
+ data = {"project_id": 789, "force": True}
269
+ api.task.send_request(task_id, "endpoint-name-here", data, skip_response=True)
270
+ """
271
+
272
+ if only_for_development and not is_development():
273
+ logger.debug(
274
+ "Advanced debugging was not started because the app is not running in development mode. "
275
+ "If you need to force the debugging, set the only_for_development argument to False. "
276
+ "Use this parameter with caution, and do not set to False in production environments."
277
+ )
278
+ return None
279
+
280
+ logger.debug(
281
+ "Starting advanced debugging, will create a wireguard VPN connection and create "
282
+ "or use an existing debug task to redirect requests to the local machine. "
283
+ "Learn more about this feature in Supervisely Developer Portal: "
284
+ "https://developer.supervisely.com/app-development/advanced/advanced-debugging"
285
+ )
286
+
287
+ supervisely_vpn_network(action=vpn_action, raise_on_error=vpn_raise_on_error)
288
+ task = create_debug_task(team_id=team_id, port=port, update_status=update_status)
289
+ task_id = task.get("id", None)
290
+
291
+ logger.debug(
292
+ f"Advanced debugging has been started. "
293
+ f"VPN connection has been established and debug task has been create. Task ID: {task_id}. "
294
+ "The metod will return the task ID, you can use it to make requests to the app."
295
+ )
296
+
297
+ return task_id
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import inspect
2
3
  import signal
3
4
  import sys
4
5
  from contextlib import suppress
@@ -1012,11 +1013,15 @@ class Application(metaclass=Singleton):
1012
1013
  self._graceful_stop_event.set()
1013
1014
  return suppress(self.StopException)
1014
1015
 
1015
- def event(self, event: Event) -> Callable:
1016
+ def event(self, event: Event, use_state: bool = False) -> Callable:
1016
1017
  """Decorator to register posts to specific endpoints.
1018
+ Supports both async and sync functions.
1017
1019
 
1018
1020
  :param event: event to register (e.g. `Event.Brush.LeftMouseReleased`)
1019
1021
  :type event: Event
1022
+ :param use_state: if set to True, data will be extracted from request.state.state,
1023
+ otherwise from request.state.context, defaults to False
1024
+ :type use_state: bool, optional
1020
1025
  :return: decorator
1021
1026
  :rtype: Callable
1022
1027
 
@@ -1037,9 +1042,19 @@ class Application(metaclass=Singleton):
1037
1042
  def inner(func: Callable) -> Callable:
1038
1043
  server = self.get_server()
1039
1044
 
1040
- @server.post(event.endpoint)
1041
- def wrapper(request: Request):
1042
- return func(request.state.api, event.from_json(request.state.context))
1045
+ if inspect.iscoroutinefunction(func):
1046
+
1047
+ @server.post(event.endpoint)
1048
+ async def wrapper(request: Request):
1049
+ data = request.state.state if use_state else request.state.context
1050
+ return await func(request.state.api, event.from_json(data))
1051
+
1052
+ else:
1053
+
1054
+ @server.post(event.endpoint)
1055
+ def wrapper(request: Request):
1056
+ data = request.state.state if use_state else request.state.context
1057
+ return func(request.state.api, event.from_json(data))
1043
1058
 
1044
1059
  return wrapper
1045
1060
 
@@ -5,11 +5,12 @@ from typing import Dict, List, Optional, Tuple, Union
5
5
 
6
6
  from tqdm import tqdm
7
7
 
8
- from supervisely._utils import is_production
8
+ from supervisely._utils import batched, is_production
9
9
  from supervisely.annotation.annotation import Annotation
10
10
  from supervisely.annotation.tag_meta import TagValueType
11
11
  from supervisely.api.api import Api
12
- from supervisely.io.fs import get_file_ext, get_file_name_with_ext
12
+ from supervisely.io.env import team_id
13
+ from supervisely.io.fs import get_file_ext, get_file_name_with_ext, silent_remove
13
14
  from supervisely.project.project_meta import ProjectMeta
14
15
  from supervisely.project.project_settings import LabelingInterface
15
16
  from supervisely.sly_logger import logger
@@ -145,12 +146,16 @@ class BaseConverter:
145
146
  remote_files_map: Optional[Dict[str, str]] = None,
146
147
  ):
147
148
  self._input_data: str = input_data
148
- self._items: List[self.BaseItem] = []
149
+ self._items: List[BaseConverter.BaseItem] = []
149
150
  self._meta: ProjectMeta = None
150
151
  self._labeling_interface = labeling_interface or LabelingInterface.DEFAULT
152
+
153
+ # import as links settings
151
154
  self._upload_as_links: bool = upload_as_links
152
155
  self._remote_files_map: Optional[Dict[str, str]] = remote_files_map
153
156
  self._supports_links = False # if converter supports uploading by links
157
+ self._api = Api.from_env() if self._upload_as_links else None
158
+ self._team_id = team_id() if self._upload_as_links else None
154
159
  self._converter = None
155
160
 
156
161
  if self._labeling_interface not in LabelingInterface.values():
@@ -286,15 +291,23 @@ class BaseConverter:
286
291
  return found_formats[0]
287
292
 
288
293
  def _collect_items_if_format_not_detected(self):
294
+ from supervisely.convert.pointcloud_episodes.pointcloud_episodes_converter import (
295
+ PointcloudEpisodeConverter,
296
+ )
297
+
289
298
  only_modality_items = True
290
299
  unsupported_exts = set()
291
300
  items = []
301
+ is_episode = isinstance(self, PointcloudEpisodeConverter)
292
302
  for root, _, files in os.walk(self._input_data):
293
303
  for file in files:
294
304
  full_path = os.path.join(root, file)
295
305
  ext = get_file_ext(full_path)
296
306
  if ext.lower() in self.allowed_exts: # pylint: disable=no-member
297
- items.append(self.Item(full_path)) # pylint: disable=no-member
307
+ if is_episode:
308
+ items.append(self.Item(full_path, len(items))) # pylint: disable=no-member
309
+ else:
310
+ items.append(self.Item(full_path)) # pylint: disable=no-member
298
311
  continue
299
312
  only_modality_items = False
300
313
  if ext.lower() in self.unsupported_exts:
@@ -411,3 +424,39 @@ class BaseConverter:
411
424
 
412
425
  return meta1.clone(project_settings=new_settings)
413
426
  return meta1
427
+
428
+ def _download_remote_ann_files(self) -> None:
429
+ """
430
+ Download all annotation files from Cloud Storage to the local storage.
431
+ Needed to detect annotation format if "upload_as_links" is enabled.
432
+ """
433
+ if not self.upload_as_links:
434
+ return
435
+
436
+ files_to_download = {
437
+ l: r for l, r in self._remote_files_map.items() if get_file_ext(l) == self.ann_ext
438
+ }
439
+ if not files_to_download:
440
+ return
441
+
442
+ import asyncio
443
+
444
+ loop = asyncio.get_event_loop()
445
+ _, progress_cb = self.get_progress(
446
+ len(files_to_download),
447
+ "Downloading annotation files from remote storage",
448
+ )
449
+
450
+ for local_path in files_to_download.keys():
451
+ silent_remove(local_path)
452
+
453
+ logger.info("Downloading annotation files from remote storage...")
454
+ loop.run_until_complete(
455
+ self._api.storage.download_bulk_async(
456
+ team_id=self._team_id,
457
+ remote_paths=list(files_to_download.values()),
458
+ local_save_paths=list(files_to_download.keys()),
459
+ progress_cb=progress_cb,
460
+ )
461
+ )
462
+ logger.info("Annotation files downloaded successfully")
@@ -1,15 +1,16 @@
1
1
  import os
2
2
  from pathlib import Path
3
+ from typing import Optional
3
4
 
4
5
  from tqdm import tqdm
5
6
 
6
- from typing import Literal, Optional
7
-
8
7
  from supervisely._utils import is_production
9
8
  from supervisely.api.api import Api
10
9
  from supervisely.app import get_data_dir
11
10
  from supervisely.convert.image.csv.csv_converter import CSVConverter
12
- from supervisely.convert.image.high_color.high_color_depth import HighColorDepthImageConverter
11
+ from supervisely.convert.image.high_color.high_color_depth import (
12
+ HighColorDepthImageConverter,
13
+ )
13
14
  from supervisely.convert.image.image_converter import ImageConverter
14
15
  from supervisely.convert.pointcloud.pointcloud_converter import PointcloudConverter
15
16
  from supervisely.convert.pointcloud_episodes.pointcloud_episodes_converter import (
@@ -28,10 +29,10 @@ from supervisely.io.fs import (
28
29
  touch,
29
30
  unpack_archive,
30
31
  )
32
+ from supervisely.project.project_settings import LabelingInterface
31
33
  from supervisely.project.project_type import ProjectType
32
34
  from supervisely.sly_logger import logger
33
35
  from supervisely.task.progress import Progress
34
- from supervisely.project.project_settings import LabelingInterface
35
36
 
36
37
 
37
38
  class ImportManager:
@@ -164,7 +165,7 @@ class ImportManager:
164
165
  dir_path = remote_path.rstrip("/") if is_dir else os.path.dirname(remote_path)
165
166
  dir_name = os.path.basename(dir_path)
166
167
 
167
- local_path = os.path.join(get_data_dir(), dir_name)
168
+ local_path = os.path.abspath(os.path.join(get_data_dir(), dir_name))
168
169
  mkdir(local_path, remove_content_if_exists=True)
169
170
 
170
171
  if is_dir:
@@ -137,8 +137,8 @@ class ImageConverter(BaseConverter):
137
137
  if item.path is None:
138
138
  continue # image has failed validation
139
139
  item.name = f"{get_file_name(item.path)}{get_file_ext(item.path).lower()}"
140
- if self.upload_as_links:
141
- ann = None # TODO: implement
140
+ if self.upload_as_links and not self.supports_links:
141
+ ann = None
142
142
  else:
143
143
  ann = self.to_supervisely(item, meta, renamed_classes, renamed_tags)
144
144
  name = generate_free_name(
@@ -160,19 +160,29 @@ class ImageConverter(BaseConverter):
160
160
  with ApiContext(
161
161
  api=api, project_id=project_id, dataset_id=dataset_id, project_meta=meta
162
162
  ):
163
- upload_method = (
164
- api.image.upload_links if self.upload_as_links else api.image.upload_paths
165
- )
166
- img_infos = upload_method(
167
- dataset_id,
168
- item_names,
169
- item_paths,
170
- metas=item_metas,
171
- conflict_resolution="rename",
172
- )
163
+ if self.upload_as_links:
164
+ img_infos = api.image.upload_links(
165
+ dataset_id,
166
+ item_names,
167
+ item_paths,
168
+ metas=item_metas,
169
+ conflict_resolution="rename",
170
+ force_metadata_for_links=False,
171
+ )
172
+ else:
173
+ img_infos = api.image.upload_paths(
174
+ dataset_id,
175
+ item_names,
176
+ item_paths,
177
+ metas=item_metas,
178
+ conflict_resolution="rename",
179
+ )
180
+
173
181
  img_ids = [img_info.id for img_info in img_infos]
174
182
  if len(anns) == len(img_ids):
175
- api.annotation.upload_anns(img_ids, anns)
183
+ api.annotation.upload_anns(
184
+ img_ids, anns, skip_bounds_validation=self.upload_as_links
185
+ )
176
186
 
177
187
  if log_progress:
178
188
  progress_cb(len(batch))
@@ -188,6 +198,9 @@ class ImageConverter(BaseConverter):
188
198
  return image_helper.validate_image(path)
189
199
 
190
200
  def is_image(self, path: str) -> bool:
201
+ if self._upload_as_links and self.supports_links:
202
+ ext = get_file_ext(path)
203
+ return ext.lower() in self.allowed_exts
191
204
  mimetypes.add_type("image/heic", ".heic") # to extend types_map
192
205
  mimetypes.add_type("image/heif", ".heif") # to extend types_map
193
206
  mimetypes.add_type("image/jpeg", ".jfif") # to extend types_map
@@ -20,6 +20,10 @@ from supervisely.convert.image.image_helper import validate_image_bounds
20
20
 
21
21
  class FastSlyImageConverter(SLYImageConverter, ImageConverter):
22
22
 
23
+ def __init__(self, *args, **kwargs):
24
+ super().__init__(*args, **kwargs)
25
+ self._supports_links = False
26
+
23
27
  def validate_format(self) -> bool:
24
28
 
25
29
  detected_ann_cnt = 0