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.
- supervisely/api/api.py +609 -3
- supervisely/api/file_api.py +574 -14
- supervisely/api/image_api.py +469 -0
- supervisely/api/pointcloud/pointcloud_api.py +390 -1
- supervisely/api/video/video_api.py +231 -1
- supervisely/api/volume/volume_api.py +223 -2
- supervisely/app/development/__init__.py +1 -0
- supervisely/app/development/development.py +96 -2
- supervisely/app/fastapi/subapp.py +19 -4
- supervisely/convert/base_converter.py +53 -4
- supervisely/convert/converter.py +6 -5
- supervisely/convert/image/image_converter.py +26 -13
- supervisely/convert/image/sly/fast_sly_image_converter.py +4 -0
- supervisely/convert/image/sly/sly_image_converter.py +9 -4
- supervisely/convert/pointcloud_episodes/sly/sly_pointcloud_episodes_converter.py +7 -1
- supervisely/convert/video/sly/sly_video_converter.py +9 -1
- supervisely/convert/video/video_converter.py +44 -23
- 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.220.dist-info → supervisely-6.73.222.dist-info}/METADATA +3 -1
- {supervisely-6.73.220.dist-info → supervisely-6.73.222.dist-info}/RECORD +27 -27
- {supervisely-6.73.220.dist-info → supervisely-6.73.222.dist-info}/LICENSE +0 -0
- {supervisely-6.73.220.dist-info → supervisely-6.73.222.dist-info}/WHEEL +0 -0
- {supervisely-6.73.220.dist-info → supervisely-6.73.222.dist-info}/entry_points.txt +0 -0
- {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
|
|
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)
|
|
@@ -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
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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.
|
|
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[
|
|
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
|
-
|
|
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")
|
supervisely/convert/converter.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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(
|
|
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
|