supervisely 6.73.226__py3-none-any.whl → 6.73.228__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/__init__.py +2 -2
- supervisely/_utils.py +78 -1
- supervisely/api/annotation_api.py +184 -14
- supervisely/api/api.py +2 -2
- supervisely/api/dataset_api.py +90 -1
- supervisely/api/entity_annotation/figure_api.py +11 -2
- supervisely/api/file_api.py +144 -8
- supervisely/api/image_api.py +94 -13
- supervisely/api/pointcloud/pointcloud_api.py +4 -8
- supervisely/api/project_api.py +285 -1
- supervisely/api/video/video_annotation_api.py +45 -0
- supervisely/api/video/video_api.py +2 -4
- supervisely/api/volume/volume_api.py +2 -4
- supervisely/convert/base_converter.py +14 -10
- supervisely/io/fs.py +55 -8
- supervisely/io/json.py +32 -0
- supervisely/project/download.py +176 -64
- supervisely/project/project.py +676 -35
- supervisely/project/project_type.py +4 -1
- supervisely/project/video_project.py +293 -3
- {supervisely-6.73.226.dist-info → supervisely-6.73.228.dist-info}/METADATA +1 -1
- {supervisely-6.73.226.dist-info → supervisely-6.73.228.dist-info}/RECORD +26 -26
- {supervisely-6.73.226.dist-info → supervisely-6.73.228.dist-info}/LICENSE +0 -0
- {supervisely-6.73.226.dist-info → supervisely-6.73.228.dist-info}/WHEEL +0 -0
- {supervisely-6.73.226.dist-info → supervisely-6.73.228.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.226.dist-info → supervisely-6.73.228.dist-info}/top_level.txt +0 -0
supervisely/__init__.py
CHANGED
|
@@ -55,7 +55,7 @@ from supervisely.task.progress import (
|
|
|
55
55
|
|
|
56
56
|
import supervisely.project as project
|
|
57
57
|
from supervisely.project import read_project, get_project_class
|
|
58
|
-
from supervisely.project.download import download
|
|
58
|
+
from supervisely.project.download import download, download_async
|
|
59
59
|
from supervisely.project.upload import upload
|
|
60
60
|
from supervisely.project.project import (
|
|
61
61
|
Project,
|
|
@@ -309,4 +309,4 @@ except Exception as e:
|
|
|
309
309
|
# If new changes in Supervisely Python SDK require upgrade of the Supervisely instance
|
|
310
310
|
# set a new value for the environment variable MINIMUM_INSTANCE_VERSION_FOR_SDK, otherwise
|
|
311
311
|
# users can face compatibility issues, if the instance version is lower than the SDK version.
|
|
312
|
-
os.environ["MINIMUM_INSTANCE_VERSION_FOR_SDK"] = "6.
|
|
312
|
+
os.environ["MINIMUM_INSTANCE_VERSION_FOR_SDK"] = "6.12.5"
|
supervisely/_utils.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import base64
|
|
4
5
|
import copy
|
|
5
6
|
import hashlib
|
|
@@ -13,7 +14,7 @@ import urllib
|
|
|
13
14
|
from datetime import datetime
|
|
14
15
|
from functools import wraps
|
|
15
16
|
from tempfile import gettempdir
|
|
16
|
-
from typing import List, Literal, Optional
|
|
17
|
+
from typing import Any, Dict, List, Literal, Optional, Tuple
|
|
17
18
|
|
|
18
19
|
import numpy as np
|
|
19
20
|
from requests.utils import DEFAULT_CA_BUNDLE_PATH
|
|
@@ -315,6 +316,15 @@ def get_readable_datetime(value: str) -> str:
|
|
|
315
316
|
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
316
317
|
|
|
317
318
|
|
|
319
|
+
def get_unix_timestamp() -> int:
|
|
320
|
+
"""Return the current Unix timestamp.
|
|
321
|
+
|
|
322
|
+
:return: Current Unix timestamp.
|
|
323
|
+
:rtype: int
|
|
324
|
+
"""
|
|
325
|
+
return int(time.time())
|
|
326
|
+
|
|
327
|
+
|
|
318
328
|
def get_certificates_list(path: str = DEFAULT_CA_BUNDLE_PATH) -> List[str]:
|
|
319
329
|
with open(path, "r", encoding="ascii") as f:
|
|
320
330
|
content = f.read().strip()
|
|
@@ -382,3 +392,70 @@ def add_callback(func, callback):
|
|
|
382
392
|
return res
|
|
383
393
|
|
|
384
394
|
return wrapper
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def compare_dicts(
|
|
398
|
+
template: Dict[Any, Any], data: Dict[Any, Any], strict: bool = True
|
|
399
|
+
) -> Tuple[List[str], List[str]]:
|
|
400
|
+
"""Compare two dictionaries recursively (by keys only) and return lists of missing and extra fields.
|
|
401
|
+
If strict is True, the keys of the template and data dictionaries must match exactly.
|
|
402
|
+
Otherwise, the data dictionary may contain additional keys that are not in the template dictionary.
|
|
403
|
+
|
|
404
|
+
:param template: The template dictionary.
|
|
405
|
+
:type template: Dict[Any, Any]
|
|
406
|
+
:param data: The data dictionary.
|
|
407
|
+
:type data: Dict[Any, Any]
|
|
408
|
+
:param strict: If True, the keys of the template and data dictionaries must match exactly.
|
|
409
|
+
:type strict: bool, optional
|
|
410
|
+
:return: A tuple containing a list of missing fields and a list of extra fields.
|
|
411
|
+
:rtype: Tuple[List[str], List[str]]
|
|
412
|
+
"""
|
|
413
|
+
missing_fields = []
|
|
414
|
+
extra_fields = []
|
|
415
|
+
|
|
416
|
+
if not isinstance(template, dict) or not isinstance(data, dict):
|
|
417
|
+
return missing_fields, extra_fields
|
|
418
|
+
|
|
419
|
+
if strict:
|
|
420
|
+
template_keys = set(template.keys())
|
|
421
|
+
data_keys = set(data.keys())
|
|
422
|
+
|
|
423
|
+
missing_fields = list(template_keys - data_keys)
|
|
424
|
+
extra_fields = list(data_keys - template_keys)
|
|
425
|
+
|
|
426
|
+
for key in template_keys & data_keys:
|
|
427
|
+
sub_missing, sub_extra = compare_dicts(template[key], data[key], strict)
|
|
428
|
+
missing_fields.extend([f"{key}.{m}" for m in sub_missing])
|
|
429
|
+
extra_fields.extend([f"{key}.{e}" for e in sub_extra])
|
|
430
|
+
else:
|
|
431
|
+
for key in template:
|
|
432
|
+
if key not in data:
|
|
433
|
+
missing_fields.append(key)
|
|
434
|
+
else:
|
|
435
|
+
sub_missing, sub_extra = compare_dicts(template[key], data[key], strict)
|
|
436
|
+
missing_fields.extend([f"{key}.{m}" for m in sub_missing])
|
|
437
|
+
extra_fields.extend([f"{key}.{e}" for e in sub_extra])
|
|
438
|
+
|
|
439
|
+
return missing_fields, extra_fields
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def get_or_create_event_loop() -> asyncio.AbstractEventLoop:
|
|
443
|
+
"""
|
|
444
|
+
Get the current event loop or create a new one if it doesn't exist.
|
|
445
|
+
Works for different Python versions and contexts.
|
|
446
|
+
|
|
447
|
+
:return: Event loop
|
|
448
|
+
:rtype: asyncio.AbstractEventLoop
|
|
449
|
+
"""
|
|
450
|
+
try:
|
|
451
|
+
# Preferred method for asynchronous context (Python 3.7+)
|
|
452
|
+
return asyncio.get_running_loop()
|
|
453
|
+
except RuntimeError:
|
|
454
|
+
# If the loop is not running, get the current one or create a new one (Python 3.8 and 3.9)
|
|
455
|
+
try:
|
|
456
|
+
return asyncio.get_event_loop()
|
|
457
|
+
except RuntimeError:
|
|
458
|
+
# For Python 3.10+ or if the call occurs outside of an active loop context
|
|
459
|
+
loop = asyncio.new_event_loop()
|
|
460
|
+
asyncio.set_event_loop(loop)
|
|
461
|
+
return loop
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
# docs
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
import asyncio
|
|
7
8
|
import json
|
|
8
9
|
from collections import defaultdict
|
|
9
10
|
from copy import deepcopy
|
|
10
|
-
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Union
|
|
11
|
+
from typing import Any, Callable, Dict, List, Literal, NamedTuple, Optional, Union
|
|
11
12
|
|
|
12
13
|
from tqdm import tqdm
|
|
13
14
|
|
|
@@ -1307,36 +1308,36 @@ class AnnotationApi(ModuleApi):
|
|
|
1307
1308
|
Priority increases with the number: a higher number indicates a higher priority.
|
|
1308
1309
|
The higher priority means that the label will be displayed on top of the others.
|
|
1309
1310
|
The lower priority means that the label will be displayed below the others.
|
|
1310
|
-
|
|
1311
|
+
|
|
1311
1312
|
:param label_id: ID of the label to update
|
|
1312
1313
|
:type label_id: int
|
|
1313
1314
|
:param priority: New priority of the label
|
|
1314
1315
|
:type priority: int
|
|
1315
|
-
|
|
1316
|
-
:Usage example:
|
|
1317
|
-
|
|
1316
|
+
|
|
1317
|
+
:Usage example:
|
|
1318
|
+
|
|
1318
1319
|
.. code-block:: python
|
|
1319
|
-
|
|
1320
|
+
|
|
1320
1321
|
import os
|
|
1321
1322
|
from dotenv import load_dotenv
|
|
1322
|
-
|
|
1323
|
+
|
|
1323
1324
|
import supervisely as sly
|
|
1324
|
-
|
|
1325
|
+
|
|
1325
1326
|
# Load secrets and create API object from .env file (recommended)
|
|
1326
1327
|
# Learn more here: https://developer.supervisely.com/getting-started/basics-of-authentication
|
|
1327
1328
|
load_dotenv(os.path.expanduser("~/supervisely.env"))
|
|
1328
|
-
|
|
1329
|
+
|
|
1329
1330
|
api = sly.Api.from_env()
|
|
1330
|
-
|
|
1331
|
+
|
|
1331
1332
|
label_ids = [123, 456, 789]
|
|
1332
1333
|
priorities = [1, 2, 3]
|
|
1333
|
-
|
|
1334
|
+
|
|
1334
1335
|
for label_id, priority in zip(label_ids, priorities):
|
|
1335
1336
|
api.annotation.update_label_priority(label_id, priority)
|
|
1336
|
-
|
|
1337
|
+
|
|
1337
1338
|
# The label with ID 789 will be displayed on top of the others.
|
|
1338
1339
|
# The label with ID 123 will be displayed below the others.
|
|
1339
|
-
|
|
1340
|
+
|
|
1340
1341
|
"""
|
|
1341
1342
|
self._api.post(
|
|
1342
1343
|
"figures.priority.update",
|
|
@@ -1344,4 +1345,173 @@ class AnnotationApi(ModuleApi):
|
|
|
1344
1345
|
ApiField.ID: label_id,
|
|
1345
1346
|
ApiField.PRIORITY: priority,
|
|
1346
1347
|
},
|
|
1347
|
-
)
|
|
1348
|
+
)
|
|
1349
|
+
|
|
1350
|
+
async def download_async(
|
|
1351
|
+
self,
|
|
1352
|
+
image_id: int,
|
|
1353
|
+
semaphore: Optional[asyncio.Semaphore] = None,
|
|
1354
|
+
with_custom_data: Optional[bool] = False,
|
|
1355
|
+
force_metadata_for_links: Optional[bool] = True,
|
|
1356
|
+
progress_cb: Optional[Union[tqdm, Callable]] = None,
|
|
1357
|
+
progress_cb_type: Literal["number", "size"] = "number",
|
|
1358
|
+
) -> AnnotationInfo:
|
|
1359
|
+
"""
|
|
1360
|
+
Download AnnotationInfo by image ID from API.
|
|
1361
|
+
|
|
1362
|
+
:param image_id: Image ID in Supervisely.
|
|
1363
|
+
:type image_id: int
|
|
1364
|
+
:param semaphore: Semaphore for limiting the number of simultaneous downloads.
|
|
1365
|
+
:type semaphore: asyncio.Semaphore, optional
|
|
1366
|
+
:param with_custom_data: Include custom data in the response.
|
|
1367
|
+
:type with_custom_data: bool, optional
|
|
1368
|
+
:param force_metadata_for_links: Force metadata for links.
|
|
1369
|
+
:type force_metadata_for_links: bool, optional
|
|
1370
|
+
:param progress_cb: Function for tracking download progress.
|
|
1371
|
+
:type progress_cb: tqdm or callable, optional
|
|
1372
|
+
:param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "number".
|
|
1373
|
+
:type progress_cb_type: str, optional
|
|
1374
|
+
:return: Information about Annotation. See :class:`info_sequence<info_sequence>`
|
|
1375
|
+
:rtype: :class:`AnnotationInfo`
|
|
1376
|
+
:Usage example:
|
|
1377
|
+
|
|
1378
|
+
.. code-block:: python
|
|
1379
|
+
|
|
1380
|
+
import supervisely as sly
|
|
1381
|
+
|
|
1382
|
+
os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
|
|
1383
|
+
os.environ['API_TOKEN'] = 'Your Supervisely API Token'
|
|
1384
|
+
api = sly.Api.from_env()
|
|
1385
|
+
|
|
1386
|
+
image_id = 121236918
|
|
1387
|
+
loop = sly.utils.get_or_create_event_loop()
|
|
1388
|
+
ann_info = loop.run_until_complete(api.annotation.download_async(image_id))
|
|
1389
|
+
"""
|
|
1390
|
+
if semaphore is None:
|
|
1391
|
+
semaphore = self._api._get_default_semaphore()
|
|
1392
|
+
async with semaphore:
|
|
1393
|
+
response = await self._api.post_async(
|
|
1394
|
+
"annotations.info",
|
|
1395
|
+
{
|
|
1396
|
+
ApiField.IMAGE_ID: image_id,
|
|
1397
|
+
ApiField.WITH_CUSTOM_DATA: with_custom_data,
|
|
1398
|
+
ApiField.FORCE_METADATA_FOR_LINKS: force_metadata_for_links,
|
|
1399
|
+
ApiField.INTEGER_COORDS: False,
|
|
1400
|
+
},
|
|
1401
|
+
)
|
|
1402
|
+
if progress_cb is not None and progress_cb_type == "size":
|
|
1403
|
+
progress_cb(len(response.content))
|
|
1404
|
+
|
|
1405
|
+
result = response.json()
|
|
1406
|
+
# Convert annotation to pixel coordinate system
|
|
1407
|
+
result[ApiField.ANNOTATION] = Annotation._to_pixel_coordinate_system_json(
|
|
1408
|
+
result[ApiField.ANNOTATION]
|
|
1409
|
+
)
|
|
1410
|
+
# check if there are any AlphaMask geometries in the batch
|
|
1411
|
+
additonal_geometries = defaultdict(int)
|
|
1412
|
+
labels = result[ApiField.ANNOTATION][AnnotationJsonFields.LABELS]
|
|
1413
|
+
for idx, label in enumerate(labels):
|
|
1414
|
+
if label[LabelJsonFields.GEOMETRY_TYPE] == AlphaMask.geometry_name():
|
|
1415
|
+
figure_id = label[LabelJsonFields.ID]
|
|
1416
|
+
additonal_geometries[figure_id] = idx
|
|
1417
|
+
|
|
1418
|
+
# if so, download them separately and update the annotation
|
|
1419
|
+
if len(additonal_geometries) > 0:
|
|
1420
|
+
figure_ids = list(additonal_geometries.keys())
|
|
1421
|
+
figures = self._api.image.figure.download_geometries_batch(
|
|
1422
|
+
figure_ids,
|
|
1423
|
+
(
|
|
1424
|
+
progress_cb
|
|
1425
|
+
if progress_cb is not None and progress_cb_type == "size"
|
|
1426
|
+
else None
|
|
1427
|
+
),
|
|
1428
|
+
)
|
|
1429
|
+
for figure_id, geometry in zip(figure_ids, figures):
|
|
1430
|
+
label_idx = additonal_geometries[figure_id]
|
|
1431
|
+
labels[label_idx].update({BITMAP: geometry})
|
|
1432
|
+
ann_info = self._convert_json_info(result)
|
|
1433
|
+
if progress_cb is not None and progress_cb_type == "number":
|
|
1434
|
+
progress_cb(1)
|
|
1435
|
+
return ann_info
|
|
1436
|
+
|
|
1437
|
+
async def download_batch_async(
|
|
1438
|
+
self,
|
|
1439
|
+
dataset_id: int,
|
|
1440
|
+
image_ids: List[int],
|
|
1441
|
+
semaphore: Optional[asyncio.Semaphore] = None,
|
|
1442
|
+
with_custom_data: Optional[bool] = False,
|
|
1443
|
+
force_metadata_for_links: Optional[bool] = True,
|
|
1444
|
+
progress_cb: Optional[Union[tqdm, Callable]] = None,
|
|
1445
|
+
progress_cb_type: Literal["number", "size"] = "number",
|
|
1446
|
+
) -> List[AnnotationInfo]:
|
|
1447
|
+
"""
|
|
1448
|
+
Get list of AnnotationInfos for given dataset ID from API.
|
|
1449
|
+
|
|
1450
|
+
:param dataset_id: Dataset ID in Supervisely.
|
|
1451
|
+
:type dataset_id: int
|
|
1452
|
+
:param image_ids: List of integers.
|
|
1453
|
+
:type image_ids: List[int]
|
|
1454
|
+
:param semaphore: Semaphore for limiting the number of simultaneous downloads.
|
|
1455
|
+
:type semaphore: asyncio.Semaphore, optional
|
|
1456
|
+
:param with_custom_data: Include custom data in the response.
|
|
1457
|
+
:type with_custom_data: bool, optional
|
|
1458
|
+
:param force_metadata_for_links: Force metadata for links.
|
|
1459
|
+
:type force_metadata_for_links: bool, optional
|
|
1460
|
+
:param progress_cb: Function for tracking download progress. Total should be equal to len(image_ids) or None.
|
|
1461
|
+
:type progress_cb: tqdm or callable, optional
|
|
1462
|
+
:param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "number".
|
|
1463
|
+
:type progress_cb_type: str, optional
|
|
1464
|
+
:return: Information about Annotations. See :class:`info_sequence<info_sequence>`
|
|
1465
|
+
:rtype: :class:`List[AnnotationInfo]`
|
|
1466
|
+
|
|
1467
|
+
:Usage example:
|
|
1468
|
+
|
|
1469
|
+
.. code-block:: python
|
|
1470
|
+
|
|
1471
|
+
import supervisely as sly
|
|
1472
|
+
|
|
1473
|
+
os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
|
|
1474
|
+
os.environ['API_TOKEN'] = 'Your Supervisely API Token'
|
|
1475
|
+
api = sly.Api.from_env()
|
|
1476
|
+
|
|
1477
|
+
dataset_id = 254737
|
|
1478
|
+
image_ids = [121236918, 121236919]
|
|
1479
|
+
pbar = tqdm(desc="Download annotations", total=len(image_ids))
|
|
1480
|
+
|
|
1481
|
+
loop = sly.utils.get_or_create_event_loop()
|
|
1482
|
+
ann_infos = loop.run_until_complete(
|
|
1483
|
+
api.annotation.download_batch_async(dataset_id, image_ids, progress_cb=pbar)
|
|
1484
|
+
)
|
|
1485
|
+
"""
|
|
1486
|
+
|
|
1487
|
+
# use context to avoid redundant API calls
|
|
1488
|
+
context = self._api.optimization_context
|
|
1489
|
+
context_dataset_id = context.get("dataset_id")
|
|
1490
|
+
project_meta = context.get("project_meta")
|
|
1491
|
+
project_id = context.get("project_id")
|
|
1492
|
+
if dataset_id != context_dataset_id:
|
|
1493
|
+
context["dataset_id"] = dataset_id
|
|
1494
|
+
project_id, project_meta = None, None
|
|
1495
|
+
|
|
1496
|
+
if not isinstance(project_meta, ProjectMeta):
|
|
1497
|
+
if project_id is None:
|
|
1498
|
+
project_id = self._api.dataset.get_info_by_id(dataset_id).project_id
|
|
1499
|
+
context["project_id"] = project_id
|
|
1500
|
+
project_meta = ProjectMeta.from_json(self._api.project.get_meta(project_id))
|
|
1501
|
+
context["project_meta"] = project_meta
|
|
1502
|
+
|
|
1503
|
+
if semaphore is None:
|
|
1504
|
+
semaphore = self._api._get_default_semaphore()
|
|
1505
|
+
tasks = []
|
|
1506
|
+
for image in image_ids:
|
|
1507
|
+
task = self.download_async(
|
|
1508
|
+
image_id=image,
|
|
1509
|
+
semaphore=semaphore,
|
|
1510
|
+
with_custom_data=with_custom_data,
|
|
1511
|
+
force_metadata_for_links=force_metadata_for_links,
|
|
1512
|
+
progress_cb=progress_cb,
|
|
1513
|
+
progress_cb_type=progress_cb_type,
|
|
1514
|
+
)
|
|
1515
|
+
tasks.append(task)
|
|
1516
|
+
ann_infos = await asyncio.gather(*tasks)
|
|
1517
|
+
return ann_infos
|
supervisely/api/api.py
CHANGED
|
@@ -1248,7 +1248,7 @@ class Api:
|
|
|
1248
1248
|
raise ValueError(
|
|
1249
1249
|
f"Streamed size does not match the expected: {total_streamed} != {expected_size}"
|
|
1250
1250
|
)
|
|
1251
|
-
logger.
|
|
1251
|
+
logger.trace(f"Streamed size: {total_streamed}, expected size: {expected_size}")
|
|
1252
1252
|
return
|
|
1253
1253
|
except httpx.RequestError as e:
|
|
1254
1254
|
retry_range_start = total_streamed + (range_start or 0)
|
|
@@ -1478,7 +1478,7 @@ class Api:
|
|
|
1478
1478
|
raise ValueError(
|
|
1479
1479
|
f"Streamed size does not match the expected: {total_streamed} != {expected_size}"
|
|
1480
1480
|
)
|
|
1481
|
-
logger.
|
|
1481
|
+
logger.trace(f"Streamed size: {total_streamed}, expected size: {expected_size}")
|
|
1482
1482
|
return
|
|
1483
1483
|
except httpx.RequestError as e:
|
|
1484
1484
|
retry_range_start = total_streamed + (range_start or 0)
|
supervisely/api/dataset_api.py
CHANGED
|
@@ -4,7 +4,17 @@
|
|
|
4
4
|
# docs
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
from typing import
|
|
7
|
+
from typing import (
|
|
8
|
+
Any,
|
|
9
|
+
Dict,
|
|
10
|
+
Generator,
|
|
11
|
+
List,
|
|
12
|
+
Literal,
|
|
13
|
+
NamedTuple,
|
|
14
|
+
Optional,
|
|
15
|
+
Tuple,
|
|
16
|
+
Union,
|
|
17
|
+
)
|
|
8
18
|
|
|
9
19
|
from supervisely._utils import abs_url, compress_image_url, is_development
|
|
10
20
|
from supervisely.api.module_api import (
|
|
@@ -61,6 +71,7 @@ class DatasetInfo(NamedTuple):
|
|
|
61
71
|
team_id: int
|
|
62
72
|
workspace_id: int
|
|
63
73
|
parent_id: Union[int, None]
|
|
74
|
+
custom_data: dict
|
|
64
75
|
|
|
65
76
|
@property
|
|
66
77
|
def image_preview_url(self):
|
|
@@ -135,6 +146,7 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
|
|
|
135
146
|
ApiField.TEAM_ID,
|
|
136
147
|
ApiField.WORKSPACE_ID,
|
|
137
148
|
ApiField.PARENT_ID,
|
|
149
|
+
ApiField.CUSTOM_DATA,
|
|
138
150
|
]
|
|
139
151
|
|
|
140
152
|
@staticmethod
|
|
@@ -362,6 +374,83 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
|
|
|
362
374
|
)
|
|
363
375
|
return dataset_info
|
|
364
376
|
|
|
377
|
+
def update(
|
|
378
|
+
self,
|
|
379
|
+
id: int,
|
|
380
|
+
name: Optional[str] = None,
|
|
381
|
+
description: Optional[str] = None,
|
|
382
|
+
custom_data: Optional[Dict[Any, Any]] = None,
|
|
383
|
+
) -> DatasetInfo:
|
|
384
|
+
"""Update Dataset information by given ID.
|
|
385
|
+
|
|
386
|
+
:param id: Dataset ID in Supervisely.
|
|
387
|
+
:type id: int
|
|
388
|
+
:param name: New Dataset name.
|
|
389
|
+
:type name: str, optional
|
|
390
|
+
:param description: New Dataset description.
|
|
391
|
+
:type description: str, optional
|
|
392
|
+
:param custom_data: New custom data.
|
|
393
|
+
:type custom_data: Dict[Any, Any], optional
|
|
394
|
+
:return: Information about Dataset. See :class:`info_sequence<info_sequence>`
|
|
395
|
+
:rtype: :class:`DatasetInfo`
|
|
396
|
+
|
|
397
|
+
:Usage example:
|
|
398
|
+
|
|
399
|
+
.. code-block:: python
|
|
400
|
+
|
|
401
|
+
import supervisely as sly
|
|
402
|
+
|
|
403
|
+
dataset_id = 384126
|
|
404
|
+
|
|
405
|
+
os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
|
|
406
|
+
os.environ['API_TOKEN'] = 'Your Supervisely API Token'
|
|
407
|
+
api = sly.Api.from_env()
|
|
408
|
+
|
|
409
|
+
new_ds = api.dataset.update(dataset_id, name='new_ds', description='new description')
|
|
410
|
+
"""
|
|
411
|
+
fields = [name, description, custom_data] # Extend later if needed.
|
|
412
|
+
if all(f is None for f in fields):
|
|
413
|
+
raise ValueError(f"At least one of the fields must be specified: {fields}")
|
|
414
|
+
|
|
415
|
+
payload = {
|
|
416
|
+
ApiField.ID: id,
|
|
417
|
+
ApiField.NAME: name,
|
|
418
|
+
ApiField.DESCRIPTION: description,
|
|
419
|
+
ApiField.CUSTOM_DATA: custom_data,
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
payload = {k: v for k, v in payload.items() if v is not None}
|
|
423
|
+
|
|
424
|
+
response = self._api.post(self._get_update_method(), payload)
|
|
425
|
+
return self._convert_json_info(response.json())
|
|
426
|
+
|
|
427
|
+
def update_custom_data(self, id: int, custom_data: Dict[Any, Any]) -> DatasetInfo:
|
|
428
|
+
"""Update custom data for Dataset by given ID.
|
|
429
|
+
Custom data is a dictionary that can store any additional information about the Dataset.
|
|
430
|
+
|
|
431
|
+
:param id: Dataset ID in Supervisely.
|
|
432
|
+
:type id: int
|
|
433
|
+
:param custom_data: New custom data.
|
|
434
|
+
:type custom_data: Dict[Any, Any]
|
|
435
|
+
:return: Information about Dataset. See :class:`info_sequence<info_sequence>`
|
|
436
|
+
:rtype: :class:`DatasetInfo`
|
|
437
|
+
|
|
438
|
+
:Usage example:
|
|
439
|
+
|
|
440
|
+
.. code-block:: python
|
|
441
|
+
|
|
442
|
+
import supervisely as sly
|
|
443
|
+
|
|
444
|
+
dataset_id = 384126
|
|
445
|
+
|
|
446
|
+
os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
|
|
447
|
+
os.environ['API_TOKEN'] = 'Your Supervisely API Token'
|
|
448
|
+
api = sly.Api.from_env()
|
|
449
|
+
|
|
450
|
+
new_ds = api.dataset.update_custom_data(dataset_id, custom_data={'key': 'value'})
|
|
451
|
+
"""
|
|
452
|
+
return self.update(id, custom_data=custom_data)
|
|
453
|
+
|
|
365
454
|
def _get_update_method(self):
|
|
366
455
|
""" """
|
|
367
456
|
return "datasets.editInfo"
|
|
@@ -6,10 +6,11 @@ from __future__ import annotations
|
|
|
6
6
|
import json
|
|
7
7
|
import re
|
|
8
8
|
from collections import defaultdict
|
|
9
|
-
from typing import Dict, Generator, List, NamedTuple, Optional, Tuple
|
|
9
|
+
from typing import Callable, Dict, Generator, List, NamedTuple, Optional, Tuple, Union
|
|
10
10
|
|
|
11
11
|
import numpy as np
|
|
12
12
|
from requests_toolbelt import MultipartDecoder, MultipartEncoder
|
|
13
|
+
from tqdm import tqdm
|
|
13
14
|
|
|
14
15
|
from supervisely._utils import batched
|
|
15
16
|
from supervisely.api.module_api import ApiField, ModuleApi, RemoveableBulkModuleApi
|
|
@@ -540,17 +541,25 @@ class FigureApi(RemoveableBulkModuleApi):
|
|
|
540
541
|
"""
|
|
541
542
|
return self.download_geometries_batch([figure_id])
|
|
542
543
|
|
|
543
|
-
def download_geometries_batch(
|
|
544
|
+
def download_geometries_batch(
|
|
545
|
+
self,
|
|
546
|
+
ids: List[int],
|
|
547
|
+
progress_cb: Optional[Union[tqdm, Callable]] = None,
|
|
548
|
+
) -> List[dict]:
|
|
544
549
|
"""
|
|
545
550
|
Download figure geometries with given IDs from storage.
|
|
546
551
|
|
|
547
552
|
:param ids: List of figure IDs in Supervisely.
|
|
548
553
|
:type ids: List[int]
|
|
554
|
+
:param progress_cb: Progress bar to show the download progress. Shows the number of bytes downloaded.
|
|
555
|
+
:type progress_cb: Union[tqdm, Callable], optional
|
|
549
556
|
:return: List of figure geometries in Supervisely JSON format.
|
|
550
557
|
:rtype: List[dict]
|
|
551
558
|
"""
|
|
552
559
|
geometries = {}
|
|
553
560
|
for idx, part in self._download_geometries_generator(ids):
|
|
561
|
+
if progress_cb is not None:
|
|
562
|
+
progress_cb(len(part.content))
|
|
554
563
|
geometry_json = json.loads(part.content)
|
|
555
564
|
geometries[idx] = geometry_json
|
|
556
565
|
|