supervisely 6.73.227__py3-none-any.whl → 6.73.229__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 +1 -1
- supervisely/_utils.py +55 -1
- supervisely/api/annotation_api.py +2 -2
- supervisely/api/api.py +90 -43
- supervisely/api/app_api.py +3 -3
- supervisely/api/dataset_api.py +90 -1
- supervisely/api/file_api.py +6 -6
- supervisely/api/image_api.py +95 -9
- supervisely/api/pointcloud/pointcloud_api.py +4 -4
- supervisely/api/project_api.py +285 -1
- supervisely/api/video/video_annotation_api.py +1 -1
- supervisely/api/video/video_api.py +2 -2
- supervisely/api/volume/volume_api.py +2 -2
- supervisely/io/env.py +15 -0
- supervisely/project/project.py +1 -1
- supervisely/project/project_type.py +4 -1
- supervisely/project/video_project.py +1 -1
- {supervisely-6.73.227.dist-info → supervisely-6.73.229.dist-info}/METADATA +1 -1
- {supervisely-6.73.227.dist-info → supervisely-6.73.229.dist-info}/RECORD +23 -23
- {supervisely-6.73.227.dist-info → supervisely-6.73.229.dist-info}/LICENSE +0 -0
- {supervisely-6.73.227.dist-info → supervisely-6.73.229.dist-info}/WHEEL +0 -0
- {supervisely-6.73.227.dist-info → supervisely-6.73.229.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.227.dist-info → supervisely-6.73.229.dist-info}/top_level.txt +0 -0
supervisely/api/image_api.py
CHANGED
|
@@ -40,6 +40,7 @@ from tqdm import tqdm
|
|
|
40
40
|
|
|
41
41
|
from supervisely._utils import (
|
|
42
42
|
batched,
|
|
43
|
+
compare_dicts,
|
|
43
44
|
generate_free_name,
|
|
44
45
|
get_bytes_hash,
|
|
45
46
|
resize_image_url,
|
|
@@ -1135,7 +1136,14 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1135
1136
|
)
|
|
1136
1137
|
|
|
1137
1138
|
def upload_path(
|
|
1138
|
-
self,
|
|
1139
|
+
self,
|
|
1140
|
+
dataset_id: int,
|
|
1141
|
+
name: str,
|
|
1142
|
+
path: str,
|
|
1143
|
+
meta: Optional[Dict] = None,
|
|
1144
|
+
validate_meta: Optional[bool] = False,
|
|
1145
|
+
use_strict_validation: Optional[bool] = False,
|
|
1146
|
+
use_caching_for_validation: Optional[bool] = False,
|
|
1139
1147
|
) -> ImageInfo:
|
|
1140
1148
|
"""
|
|
1141
1149
|
Uploads Image with given name from given local path to Dataset.
|
|
@@ -1148,6 +1156,12 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1148
1156
|
:type path: str
|
|
1149
1157
|
:param meta: Image metadata.
|
|
1150
1158
|
:type meta: dict, optional
|
|
1159
|
+
:param validate_meta: If True, validates provided meta with saved JSON schema.
|
|
1160
|
+
:type validate_meta: bool, optional
|
|
1161
|
+
:param use_strict_validation: If True, uses strict validation.
|
|
1162
|
+
:type use_strict_validation: bool, optional
|
|
1163
|
+
:param use_caching_for_validation: If True, uses caching for validation.
|
|
1164
|
+
:type use_caching_for_validation: bool, optional
|
|
1151
1165
|
:return: Information about Image. See :class:`info_sequence<info_sequence>`
|
|
1152
1166
|
:rtype: :class:`ImageInfo`
|
|
1153
1167
|
:Usage example:
|
|
@@ -1163,7 +1177,15 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1163
1177
|
img_info = api.image.upload_path(dataset_id, name="7777.jpeg", path="/home/admin/Downloads/7777.jpeg")
|
|
1164
1178
|
"""
|
|
1165
1179
|
metas = None if meta is None else [meta]
|
|
1166
|
-
return self.upload_paths(
|
|
1180
|
+
return self.upload_paths(
|
|
1181
|
+
dataset_id,
|
|
1182
|
+
[name],
|
|
1183
|
+
[path],
|
|
1184
|
+
metas=metas,
|
|
1185
|
+
validate_meta=validate_meta,
|
|
1186
|
+
use_strict_validation=use_strict_validation,
|
|
1187
|
+
use_caching_for_validation=use_caching_for_validation,
|
|
1188
|
+
)[0]
|
|
1167
1189
|
|
|
1168
1190
|
def upload_paths(
|
|
1169
1191
|
self,
|
|
@@ -1173,6 +1195,9 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1173
1195
|
progress_cb: Optional[Union[tqdm, Callable]] = None,
|
|
1174
1196
|
metas: Optional[List[Dict]] = None,
|
|
1175
1197
|
conflict_resolution: Optional[Literal["rename", "skip", "replace"]] = None,
|
|
1198
|
+
validate_meta: Optional[bool] = False,
|
|
1199
|
+
use_strict_validation: Optional[bool] = False,
|
|
1200
|
+
use_caching_for_validation: Optional[bool] = False,
|
|
1176
1201
|
) -> List[ImageInfo]:
|
|
1177
1202
|
"""
|
|
1178
1203
|
Uploads Images with given names from given local path to Dataset.
|
|
@@ -1189,6 +1214,12 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1189
1214
|
:type metas: List[dict], optional
|
|
1190
1215
|
:param conflict_resolution: The strategy to resolve upload conflicts. 'Replace' option will replace the existing images in the dataset with the new images. The images that are being deleted are logged. 'Skip' option will ignore the upload of new images that would result in a conflict. An original image's ImageInfo list will be returned instead. 'Rename' option will rename the new images to prevent any conflict.
|
|
1191
1216
|
:type conflict_resolution: Optional[Literal["rename", "skip", "replace"]]
|
|
1217
|
+
:param validate_meta: If True, validates provided meta with saved JSON schema.
|
|
1218
|
+
:type validate_meta: bool, optional
|
|
1219
|
+
:param use_strict_validation: If True, uses strict validation.
|
|
1220
|
+
:type use_strict_validation: bool, optional
|
|
1221
|
+
:param use_caching_for_validation: If True, uses caching for validation.
|
|
1222
|
+
:type use_caching_for_validation: bool, optional
|
|
1192
1223
|
:raises: :class:`ValueError` if len(names) != len(paths)
|
|
1193
1224
|
:return: List with information about Images. See :class:`info_sequence<info_sequence>`
|
|
1194
1225
|
:rtype: :class:`List[ImageInfo]`
|
|
@@ -1214,7 +1245,14 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1214
1245
|
self._upload_data_bulk(path_to_bytes_stream, zip(paths, hashes), progress_cb=progress_cb)
|
|
1215
1246
|
|
|
1216
1247
|
return self.upload_hashes(
|
|
1217
|
-
dataset_id,
|
|
1248
|
+
dataset_id,
|
|
1249
|
+
names,
|
|
1250
|
+
hashes,
|
|
1251
|
+
metas=metas,
|
|
1252
|
+
conflict_resolution=conflict_resolution,
|
|
1253
|
+
validate_meta=validate_meta,
|
|
1254
|
+
use_strict_validation=use_strict_validation,
|
|
1255
|
+
use_caching_for_validation=use_caching_for_validation,
|
|
1218
1256
|
)
|
|
1219
1257
|
|
|
1220
1258
|
def upload_np(
|
|
@@ -1492,6 +1530,9 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1492
1530
|
batch_size: Optional[int] = 50,
|
|
1493
1531
|
skip_validation: Optional[bool] = False,
|
|
1494
1532
|
conflict_resolution: Optional[Literal["rename", "skip", "replace"]] = None,
|
|
1533
|
+
validate_meta: Optional[bool] = False,
|
|
1534
|
+
use_strict_validation: Optional[bool] = False,
|
|
1535
|
+
use_caching_for_validation: Optional[bool] = False,
|
|
1495
1536
|
) -> List[ImageInfo]:
|
|
1496
1537
|
"""
|
|
1497
1538
|
Upload images from given hashes to Dataset.
|
|
@@ -1512,6 +1553,12 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1512
1553
|
:type skip_validation: bool, optional
|
|
1513
1554
|
:param conflict_resolution: The strategy to resolve upload conflicts. 'Replace' option will replace the existing images in the dataset with the new images. The images that are being deleted are logged. 'Skip' option will ignore the upload of new images that would result in a conflict. An original image's ImageInfo list will be returned instead. 'Rename' option will rename the new images to prevent any conflict.
|
|
1514
1555
|
:type conflict_resolution: Optional[Literal["rename", "skip", "replace"]]
|
|
1556
|
+
:param validate_meta: If True, validates provided meta with saved JSON schema.
|
|
1557
|
+
:type validate_meta: bool, optional
|
|
1558
|
+
:param use_strict_validation: If True, uses strict validation.
|
|
1559
|
+
:type use_strict_validation: bool, optional
|
|
1560
|
+
:param use_caching_for_validation: If True, uses caching for validation.
|
|
1561
|
+
:type use_caching_for_validation: bool, optional
|
|
1515
1562
|
:return: List with information about Images. See :class:`info_sequence<info_sequence>`
|
|
1516
1563
|
:rtype: :class:`List[ImageInfo]`
|
|
1517
1564
|
:Usage example:
|
|
@@ -1553,6 +1600,9 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1553
1600
|
batch_size=batch_size,
|
|
1554
1601
|
skip_validation=skip_validation,
|
|
1555
1602
|
conflict_resolution=conflict_resolution,
|
|
1603
|
+
validate_meta=validate_meta,
|
|
1604
|
+
use_strict_validation=use_strict_validation,
|
|
1605
|
+
use_caching_for_validation=use_caching_for_validation,
|
|
1556
1606
|
)
|
|
1557
1607
|
|
|
1558
1608
|
def upload_id(
|
|
@@ -1750,8 +1800,44 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1750
1800
|
force_metadata_for_links=True,
|
|
1751
1801
|
skip_validation=False,
|
|
1752
1802
|
conflict_resolution: Optional[Literal["rename", "skip", "replace"]] = None,
|
|
1803
|
+
validate_meta: Optional[bool] = False,
|
|
1804
|
+
use_strict_validation: Optional[bool] = False,
|
|
1805
|
+
use_caching_for_validation: Optional[bool] = False,
|
|
1753
1806
|
):
|
|
1754
1807
|
""" """
|
|
1808
|
+
if use_strict_validation and not validate_meta:
|
|
1809
|
+
raise ValueError(
|
|
1810
|
+
"use_strict_validation is set to True, while validate_meta is set to False. "
|
|
1811
|
+
"Please set validate_meta to True to use strict validation "
|
|
1812
|
+
"or disable strict validation by setting use_strict_validation to False."
|
|
1813
|
+
)
|
|
1814
|
+
if validate_meta:
|
|
1815
|
+
dataset_info = self._api.dataset.get_info_by_id(dataset_id)
|
|
1816
|
+
|
|
1817
|
+
validation_schema = self._api.project.get_validation_schema(
|
|
1818
|
+
dataset_info.project_id, use_caching=use_caching_for_validation
|
|
1819
|
+
)
|
|
1820
|
+
|
|
1821
|
+
if validation_schema is None:
|
|
1822
|
+
raise ValueError(
|
|
1823
|
+
"Validation schema is not set for the project, while "
|
|
1824
|
+
"validate_meta is set to True. Either disable the validation "
|
|
1825
|
+
"or set the validation schema for the project using the "
|
|
1826
|
+
"api.project.set_validation_schema method."
|
|
1827
|
+
)
|
|
1828
|
+
|
|
1829
|
+
for idx, meta in enumerate(metas):
|
|
1830
|
+
missing_fields, extra_fields = compare_dicts(
|
|
1831
|
+
validation_schema, meta, strict=use_strict_validation
|
|
1832
|
+
)
|
|
1833
|
+
|
|
1834
|
+
if missing_fields or extra_fields:
|
|
1835
|
+
raise ValueError(
|
|
1836
|
+
f"Validation failed for the metadata of the image with index {idx} and name {names[idx]}. "
|
|
1837
|
+
"Please check the metadata and try again. "
|
|
1838
|
+
f"Missing fields: {missing_fields}, Extra fields: {extra_fields}"
|
|
1839
|
+
)
|
|
1840
|
+
|
|
1755
1841
|
if (
|
|
1756
1842
|
conflict_resolution is not None
|
|
1757
1843
|
and conflict_resolution not in SUPPORTED_CONFLICT_RESOLUTIONS
|
|
@@ -3553,7 +3639,7 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
3553
3639
|
results = await asyncio.gather(*tasks)
|
|
3554
3640
|
"""
|
|
3555
3641
|
if semaphore is None:
|
|
3556
|
-
semaphore = self._api.
|
|
3642
|
+
semaphore = self._api.get_default_semaphore()
|
|
3557
3643
|
|
|
3558
3644
|
async with semaphore:
|
|
3559
3645
|
async for response in self._download_async(id):
|
|
@@ -3611,7 +3697,7 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
3611
3697
|
|
|
3612
3698
|
"""
|
|
3613
3699
|
if semaphore is None:
|
|
3614
|
-
semaphore = self._api.
|
|
3700
|
+
semaphore = self._api.get_default_semaphore()
|
|
3615
3701
|
tasks = [
|
|
3616
3702
|
self.download_np_async(id, semaphore, keep_alpha, progress_cb, progress_cb_type)
|
|
3617
3703
|
for id in ids
|
|
@@ -3686,7 +3772,7 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
3686
3772
|
ensure_base_path(path)
|
|
3687
3773
|
hash_to_check = None
|
|
3688
3774
|
if semaphore is None:
|
|
3689
|
-
semaphore = self._api.
|
|
3775
|
+
semaphore = self._api.get_default_semaphore()
|
|
3690
3776
|
async with semaphore:
|
|
3691
3777
|
async with aiofiles.open(path, writing_method) as fd:
|
|
3692
3778
|
async for chunk, hhash in self._download_async(
|
|
@@ -3764,7 +3850,7 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
3764
3850
|
f'Length of "ids" and "paths" should be equal. {len(ids)} != {len(paths)}'
|
|
3765
3851
|
)
|
|
3766
3852
|
if semaphore is None:
|
|
3767
|
-
semaphore = self._api.
|
|
3853
|
+
semaphore = self._api.get_default_semaphore()
|
|
3768
3854
|
tasks = []
|
|
3769
3855
|
for img_id, img_path in zip(ids, paths):
|
|
3770
3856
|
task = self.download_path_async(
|
|
@@ -3838,7 +3924,7 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
3838
3924
|
hash_to_check = None
|
|
3839
3925
|
|
|
3840
3926
|
if semaphore is None:
|
|
3841
|
-
semaphore = self._api.
|
|
3927
|
+
semaphore = self._api.get_default_semaphore()
|
|
3842
3928
|
async with semaphore:
|
|
3843
3929
|
content = b""
|
|
3844
3930
|
async for chunk, hhash in self._download_async(
|
|
@@ -3907,7 +3993,7 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
3907
3993
|
img_bytes_list = loop.run_until_complete(api.image.download_bytes_imgs_async(ids, semaphore))
|
|
3908
3994
|
"""
|
|
3909
3995
|
if semaphore is None:
|
|
3910
|
-
semaphore = self._api.
|
|
3996
|
+
semaphore = self._api.get_default_semaphore()
|
|
3911
3997
|
tasks = []
|
|
3912
3998
|
for id in ids:
|
|
3913
3999
|
task = self.download_bytes_single_async(
|
|
@@ -1166,7 +1166,7 @@ class PointcloudApi(RemoveableBulkModuleApi):
|
|
|
1166
1166
|
ensure_base_path(path)
|
|
1167
1167
|
hash_to_check = None
|
|
1168
1168
|
if semaphore is None:
|
|
1169
|
-
semaphore = self._api.
|
|
1169
|
+
semaphore = self._api.get_default_semaphore()
|
|
1170
1170
|
async with semaphore:
|
|
1171
1171
|
async with aiofiles.open(path, writing_method) as fd:
|
|
1172
1172
|
async for chunk, hhash in self._download_async(
|
|
@@ -1248,7 +1248,7 @@ class PointcloudApi(RemoveableBulkModuleApi):
|
|
|
1248
1248
|
if len(ids) != len(paths):
|
|
1249
1249
|
raise ValueError('Can not match "ids" and "paths" lists, len(ids) != len(paths)')
|
|
1250
1250
|
if semaphore is None:
|
|
1251
|
-
semaphore = self._api.
|
|
1251
|
+
semaphore = self._api.get_default_semaphore()
|
|
1252
1252
|
tasks = []
|
|
1253
1253
|
for img_id, img_path in zip(ids, paths):
|
|
1254
1254
|
task = self.download_path_async(
|
|
@@ -1322,7 +1322,7 @@ class PointcloudApi(RemoveableBulkModuleApi):
|
|
|
1322
1322
|
ensure_base_path(path)
|
|
1323
1323
|
hash_to_check = None
|
|
1324
1324
|
if semaphore is None:
|
|
1325
|
-
semaphore = self._api.
|
|
1325
|
+
semaphore = self._api.get_default_semaphore()
|
|
1326
1326
|
async with semaphore:
|
|
1327
1327
|
async with aiofiles.open(path, "wb") as fd:
|
|
1328
1328
|
async for chunk, hhash in self._api.stream_async(
|
|
@@ -1403,7 +1403,7 @@ class PointcloudApi(RemoveableBulkModuleApi):
|
|
|
1403
1403
|
if len(ids) != len(paths):
|
|
1404
1404
|
raise ValueError('Can not match "ids" and "paths" lists, len(ids) != len(paths)')
|
|
1405
1405
|
if semaphore is None:
|
|
1406
|
-
semaphore = self._api.
|
|
1406
|
+
semaphore = self._api.get_default_semaphore()
|
|
1407
1407
|
tasks = []
|
|
1408
1408
|
for img_id, img_path in zip(ids, paths):
|
|
1409
1409
|
task = self.download_related_image_async(
|
supervisely/api/project_api.py
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
# docs
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
import os
|
|
7
8
|
from collections import defaultdict
|
|
9
|
+
from copy import deepcopy
|
|
8
10
|
from typing import (
|
|
9
11
|
TYPE_CHECKING,
|
|
10
12
|
Any,
|
|
@@ -25,7 +27,13 @@ if TYPE_CHECKING:
|
|
|
25
27
|
from datetime import datetime, timedelta
|
|
26
28
|
|
|
27
29
|
from supervisely import logger
|
|
28
|
-
from supervisely._utils import
|
|
30
|
+
from supervisely._utils import (
|
|
31
|
+
abs_url,
|
|
32
|
+
compare_dicts,
|
|
33
|
+
compress_image_url,
|
|
34
|
+
get_unix_timestamp,
|
|
35
|
+
is_development,
|
|
36
|
+
)
|
|
29
37
|
from supervisely.annotation.annotation import TagCollection
|
|
30
38
|
from supervisely.annotation.obj_class import ObjClass
|
|
31
39
|
from supervisely.annotation.obj_class_collection import ObjClassCollection
|
|
@@ -36,6 +44,7 @@ from supervisely.api.module_api import (
|
|
|
36
44
|
RemoveableModuleApi,
|
|
37
45
|
UpdateableModule,
|
|
38
46
|
)
|
|
47
|
+
from supervisely.io.json import dump_json_file, load_json_file
|
|
39
48
|
from supervisely.project.project_meta import ProjectMeta
|
|
40
49
|
from supervisely.project.project_meta import ProjectMetaJsonFields as MetaJsonF
|
|
41
50
|
from supervisely.project.project_settings import (
|
|
@@ -43,6 +52,9 @@ from supervisely.project.project_settings import (
|
|
|
43
52
|
ProjectSettingsJsonFields,
|
|
44
53
|
)
|
|
45
54
|
from supervisely.project.project_type import (
|
|
55
|
+
_METADATA_SYSTEM_KEY,
|
|
56
|
+
_METADATA_TIMESTAMP_KEY,
|
|
57
|
+
_METADATA_VALIDATION_SCHEMA_KEY,
|
|
46
58
|
_MULTISPECTRAL_TAG_NAME,
|
|
47
59
|
_MULTIVIEW_TAG_NAME,
|
|
48
60
|
ProjectType,
|
|
@@ -969,6 +981,278 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
|
|
|
969
981
|
)
|
|
970
982
|
return response.json()
|
|
971
983
|
|
|
984
|
+
def get_custom_data(self, id: int) -> Dict[Any, Any]:
|
|
985
|
+
"""Returns custom data of the Project by ID.
|
|
986
|
+
Custom data is a dictionary that can be used to store any additional information.
|
|
987
|
+
|
|
988
|
+
:param id: Project ID in Supervisely.
|
|
989
|
+
:type id: int
|
|
990
|
+
:return: Custom data of the Project
|
|
991
|
+
:rtype: :class:`dict`
|
|
992
|
+
|
|
993
|
+
:Usage example:
|
|
994
|
+
|
|
995
|
+
.. code-block:: python
|
|
996
|
+
|
|
997
|
+
import supervisely as sly
|
|
998
|
+
|
|
999
|
+
api = sly.Api.from_env()
|
|
1000
|
+
|
|
1001
|
+
project_id = 123456
|
|
1002
|
+
|
|
1003
|
+
custom_data = api.project.get_custom_data(project_id)
|
|
1004
|
+
|
|
1005
|
+
print(custom_data) # Output: {'key': 'value'}
|
|
1006
|
+
"""
|
|
1007
|
+
return self.get_info_by_id(id).custom_data
|
|
1008
|
+
|
|
1009
|
+
def _get_system_custom_data(self, id: int) -> Dict[Any, Any]:
|
|
1010
|
+
"""Returns system custom data of the Project by ID.
|
|
1011
|
+
System custom data is just a part of custom data that is used to store system information
|
|
1012
|
+
and obtained by the key `_METADATA_SYSTEM_KEY`.
|
|
1013
|
+
|
|
1014
|
+
:param id: Project ID in Supervisely.
|
|
1015
|
+
:type id: int
|
|
1016
|
+
:return: System custom data of the Project
|
|
1017
|
+
:rtype: :class:`dict`
|
|
1018
|
+
|
|
1019
|
+
:Usage example:
|
|
1020
|
+
|
|
1021
|
+
.. code-block:: python
|
|
1022
|
+
|
|
1023
|
+
import supervisely as sly
|
|
1024
|
+
|
|
1025
|
+
api = sly.Api.from_env()
|
|
1026
|
+
|
|
1027
|
+
project_id = 123456
|
|
1028
|
+
|
|
1029
|
+
system_custom_data = api.project._get_system_custom_data(project_id)
|
|
1030
|
+
|
|
1031
|
+
print(system_custom_data)
|
|
1032
|
+
"""
|
|
1033
|
+
return self.get_info_by_id(id).custom_data.get(_METADATA_SYSTEM_KEY, {})
|
|
1034
|
+
|
|
1035
|
+
def get_validation_schema(self, id: int, use_caching: bool = False) -> Optional[Dict[Any, Any]]:
|
|
1036
|
+
"""Returns validation schema of the Project by ID.
|
|
1037
|
+
Validation schema is a dictionary that can be used to validate metadata of each entity in the project
|
|
1038
|
+
if corresnpoding schema is provided.
|
|
1039
|
+
If using caching, the schema will be loaded from the cache if available.
|
|
1040
|
+
Use cached version only in scenarios when the schema is not expected to change,
|
|
1041
|
+
otherwise it may lead to checks with outdated schema.
|
|
1042
|
+
|
|
1043
|
+
:param id: Project ID in Supervisely.
|
|
1044
|
+
:type id: int
|
|
1045
|
+
:param use_caching: If True, uses cached version of the schema if available.
|
|
1046
|
+
NOTE: This may lead to checks with outdated schema. Use with caution.
|
|
1047
|
+
And only in scenarios when the schema is not expected to change.
|
|
1048
|
+
:return: Validation schema of the Project
|
|
1049
|
+
:rtype: :class:`dict`
|
|
1050
|
+
|
|
1051
|
+
:Usage example:
|
|
1052
|
+
|
|
1053
|
+
.. code-block:: python
|
|
1054
|
+
|
|
1055
|
+
import supervisely as sly
|
|
1056
|
+
|
|
1057
|
+
api = sly.Api.from_env()
|
|
1058
|
+
|
|
1059
|
+
project_id = 123456
|
|
1060
|
+
|
|
1061
|
+
validation_schema = api.project.get_validation_schema(project_id)
|
|
1062
|
+
|
|
1063
|
+
print(validation_schema) # Output: {'key': 'Description of the field'}
|
|
1064
|
+
"""
|
|
1065
|
+
SCHEMA_DIFF_THRESHOLD = 60 * 60 # 1 hour
|
|
1066
|
+
json_cache_filename = os.path.join(os.getcwd(), f"{id}_validation_schema.json")
|
|
1067
|
+
|
|
1068
|
+
if use_caching:
|
|
1069
|
+
if os.path.isfile(json_cache_filename):
|
|
1070
|
+
try:
|
|
1071
|
+
schema = load_json_file(json_cache_filename)
|
|
1072
|
+
timestamp = schema.pop(_METADATA_TIMESTAMP_KEY, 0)
|
|
1073
|
+
|
|
1074
|
+
if get_unix_timestamp() - timestamp < SCHEMA_DIFF_THRESHOLD:
|
|
1075
|
+
return schema
|
|
1076
|
+
except RuntimeError:
|
|
1077
|
+
pass
|
|
1078
|
+
|
|
1079
|
+
schema = self._get_system_custom_data(id).get(_METADATA_VALIDATION_SCHEMA_KEY)
|
|
1080
|
+
if schema and use_caching:
|
|
1081
|
+
schema_with_timestamp = deepcopy(schema)
|
|
1082
|
+
schema_with_timestamp[_METADATA_TIMESTAMP_KEY] = get_unix_timestamp()
|
|
1083
|
+
dump_json_file(schema_with_timestamp, json_cache_filename)
|
|
1084
|
+
|
|
1085
|
+
return schema
|
|
1086
|
+
|
|
1087
|
+
def _edit_validation_schema(
|
|
1088
|
+
self, id: int, schema: Optional[Dict[Any, Any]] = None
|
|
1089
|
+
) -> Dict[Any, Any]:
|
|
1090
|
+
"""Edits validation schema of the Project by ID.
|
|
1091
|
+
Do not use this method directly, use `set_validation_schema` or `remove_validation_schema` instead.
|
|
1092
|
+
|
|
1093
|
+
:param id: Project ID in Supervisely.
|
|
1094
|
+
:type id: int
|
|
1095
|
+
:param schema: Validation schema to set. If None, removes validation schema.
|
|
1096
|
+
:type schema: dict, optional
|
|
1097
|
+
:return: Project information in dict format
|
|
1098
|
+
:rtype: :class:`dict`
|
|
1099
|
+
|
|
1100
|
+
:Usage example:
|
|
1101
|
+
|
|
1102
|
+
.. code-block:: python
|
|
1103
|
+
|
|
1104
|
+
import supervisely as sly
|
|
1105
|
+
|
|
1106
|
+
api = sly.Api.from_env()
|
|
1107
|
+
|
|
1108
|
+
project_id = 123456
|
|
1109
|
+
|
|
1110
|
+
schema = {'key': 'Description of the field'}
|
|
1111
|
+
|
|
1112
|
+
api.project._edit_validation_schema(project_id, schema) #Set new validation schema.
|
|
1113
|
+
api.project._edit_validation_schema(project_id) #Remove validation schema.
|
|
1114
|
+
"""
|
|
1115
|
+
custom_data = self.get_custom_data(id)
|
|
1116
|
+
system_data = custom_data.setdefault(_METADATA_SYSTEM_KEY, {})
|
|
1117
|
+
|
|
1118
|
+
if not schema:
|
|
1119
|
+
system_data.pop(_METADATA_VALIDATION_SCHEMA_KEY, None)
|
|
1120
|
+
else:
|
|
1121
|
+
system_data[_METADATA_VALIDATION_SCHEMA_KEY] = schema
|
|
1122
|
+
return self.update_custom_data(id, custom_data)
|
|
1123
|
+
|
|
1124
|
+
def set_validation_schema(self, id: int, schema: Dict[Any, Any]) -> Dict[Any, Any]:
|
|
1125
|
+
"""Sets validation schema of the Project by ID.
|
|
1126
|
+
NOTE: This method will overwrite existing validation schema. To extend existing schema,
|
|
1127
|
+
use `get_validation_schema` first to get current schema, then update it and use this method to set new schema.
|
|
1128
|
+
|
|
1129
|
+
:param id: Project ID in Supervisely.
|
|
1130
|
+
:type id: int
|
|
1131
|
+
:param schema: Validation schema to set.
|
|
1132
|
+
:type schema: dict
|
|
1133
|
+
:return: Project information in dict format
|
|
1134
|
+
:rtype: :class:`dict`
|
|
1135
|
+
|
|
1136
|
+
:Usage example:
|
|
1137
|
+
|
|
1138
|
+
.. code-block:: python
|
|
1139
|
+
|
|
1140
|
+
import supervisely as sly
|
|
1141
|
+
|
|
1142
|
+
api = sly.Api.from_env()
|
|
1143
|
+
|
|
1144
|
+
project_id = 123456
|
|
1145
|
+
|
|
1146
|
+
schema = {'key': 'Description of the field'}
|
|
1147
|
+
|
|
1148
|
+
api.project.set_validation_schema(project_id, schema)
|
|
1149
|
+
"""
|
|
1150
|
+
return self._edit_validation_schema(id, schema)
|
|
1151
|
+
|
|
1152
|
+
def remove_validation_schema(self, id: int) -> Dict[Any, Any]:
|
|
1153
|
+
"""Removes validation schema of the Project by ID.
|
|
1154
|
+
|
|
1155
|
+
:param id: Project ID in Supervisely.
|
|
1156
|
+
:type id: int
|
|
1157
|
+
:return: Project information in dict format
|
|
1158
|
+
:rtype: :class:`dict`
|
|
1159
|
+
|
|
1160
|
+
:Usage example:
|
|
1161
|
+
|
|
1162
|
+
.. code-block:: python
|
|
1163
|
+
|
|
1164
|
+
import supervisely as sly
|
|
1165
|
+
|
|
1166
|
+
api = sly.Api.from_env()
|
|
1167
|
+
|
|
1168
|
+
project_id = 123456
|
|
1169
|
+
|
|
1170
|
+
api.project.remove_validation_schema(project_id)
|
|
1171
|
+
"""
|
|
1172
|
+
return self._edit_validation_schema(id)
|
|
1173
|
+
|
|
1174
|
+
def validate_entities_schema(
|
|
1175
|
+
self, id: int, strict: bool = False
|
|
1176
|
+
) -> List[Dict[str, Union[id, str, List[str], List[Any]]]]:
|
|
1177
|
+
"""Validates entities of the Project by ID using validation schema.
|
|
1178
|
+
Returns list of entities that do not match the schema.
|
|
1179
|
+
|
|
1180
|
+
Example of the returned list:
|
|
1181
|
+
|
|
1182
|
+
[
|
|
1183
|
+
{
|
|
1184
|
+
"entity_id": 123456,
|
|
1185
|
+
"entity_name": "image.jpg",
|
|
1186
|
+
"missing_fields": ["location"],
|
|
1187
|
+
"extra_fields": ["city.name"] <- Nested field (field "name" of the field "city")
|
|
1188
|
+
}
|
|
1189
|
+
]
|
|
1190
|
+
|
|
1191
|
+
:param id: Project ID in Supervisely.
|
|
1192
|
+
:type id: int
|
|
1193
|
+
:param strict: If strict is disabled, only checks if the entity has all the fields from the schema.
|
|
1194
|
+
Any extra fields in the entity will be ignored and will not be considered as an error.
|
|
1195
|
+
If strict is enabled, checks that the entity custom data is an exact match to the schema.
|
|
1196
|
+
:type strict: bool, optional
|
|
1197
|
+
:return: List of dictionaries with information about entities that do not match the schema.
|
|
1198
|
+
:rtype: :class:`List[Dict[str, Union[id, str, List[str], List[Any]]]`
|
|
1199
|
+
|
|
1200
|
+
:Usage example:
|
|
1201
|
+
|
|
1202
|
+
.. code-block:: python
|
|
1203
|
+
|
|
1204
|
+
import supervisely as sly
|
|
1205
|
+
|
|
1206
|
+
api = sly.Api.from_env()
|
|
1207
|
+
|
|
1208
|
+
project_id = 123456
|
|
1209
|
+
|
|
1210
|
+
incorrect_entities = api.project.validate_entities_schema(project_id)
|
|
1211
|
+
|
|
1212
|
+
for entity in incorrect_entities:
|
|
1213
|
+
print(entity.id, entity.name) # Output: 123456, 'image.jpg'
|
|
1214
|
+
"""
|
|
1215
|
+
validation_schema = self.get_validation_schema(id)
|
|
1216
|
+
if not validation_schema:
|
|
1217
|
+
raise ValueError("Validation schema is not set for this project.")
|
|
1218
|
+
|
|
1219
|
+
info = self.get_info_by_id(id)
|
|
1220
|
+
listing_method = {
|
|
1221
|
+
# Mapping of project type to listing method.
|
|
1222
|
+
ProjectType.IMAGES.value: self._api.image.get_list,
|
|
1223
|
+
}
|
|
1224
|
+
custom_data_properties = {
|
|
1225
|
+
# Mapping of project type to custom data property name.
|
|
1226
|
+
# Can be different for different project types.
|
|
1227
|
+
ProjectType.IMAGES.value: "meta"
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
listing_method = listing_method.get(info.type)
|
|
1231
|
+
custom_data_property = custom_data_properties.get(info.type)
|
|
1232
|
+
|
|
1233
|
+
if not listing_method or not custom_data_property:
|
|
1234
|
+
# TODO: Add support for other project types.
|
|
1235
|
+
raise NotImplementedError("Validation schema is not supported for this project type.")
|
|
1236
|
+
|
|
1237
|
+
entities = listing_method(project_id=id)
|
|
1238
|
+
incorrect_entities = []
|
|
1239
|
+
|
|
1240
|
+
for entity in entities:
|
|
1241
|
+
custom_data = getattr(entity, custom_data_property)
|
|
1242
|
+
missing_fields, extra_fields = compare_dicts(
|
|
1243
|
+
validation_schema, custom_data, strict=strict
|
|
1244
|
+
)
|
|
1245
|
+
if missing_fields or extra_fields:
|
|
1246
|
+
entry = {
|
|
1247
|
+
"entity_id": entity.id,
|
|
1248
|
+
"entity_name": entity.name,
|
|
1249
|
+
"missing_fields": missing_fields,
|
|
1250
|
+
"extra_fields": extra_fields,
|
|
1251
|
+
}
|
|
1252
|
+
incorrect_entities.append(entry)
|
|
1253
|
+
|
|
1254
|
+
return incorrect_entities
|
|
1255
|
+
|
|
972
1256
|
def get_settings(self, id: int) -> Dict[str, str]:
|
|
973
1257
|
info = self._get_info_by_id(id, "projects.info")
|
|
974
1258
|
return info.settings
|
|
@@ -284,7 +284,7 @@ class VideoAnnotationAPI(EntityAnnotationAPI):
|
|
|
284
284
|
video_info = self._api.video.get_info_by_id(video_id)
|
|
285
285
|
|
|
286
286
|
if semaphore is None:
|
|
287
|
-
semaphore = self._api.
|
|
287
|
+
semaphore = self._api.get_default_semaphore()
|
|
288
288
|
|
|
289
289
|
async with semaphore:
|
|
290
290
|
response = await self._api.post_async(
|
|
@@ -2482,7 +2482,7 @@ class VideoApi(RemoveableBulkModuleApi):
|
|
|
2482
2482
|
ensure_base_path(path)
|
|
2483
2483
|
hash_to_check = None
|
|
2484
2484
|
if semaphore is None:
|
|
2485
|
-
semaphore = self._api.
|
|
2485
|
+
semaphore = self._api.get_default_semaphore()
|
|
2486
2486
|
async with semaphore:
|
|
2487
2487
|
async with aiofiles.open(path, writing_method) as fd:
|
|
2488
2488
|
async for chunk, hhash in self._download_async(
|
|
@@ -2562,7 +2562,7 @@ class VideoApi(RemoveableBulkModuleApi):
|
|
|
2562
2562
|
if len(ids) != len(paths):
|
|
2563
2563
|
raise ValueError('Can not match "ids" and "paths" lists, len(ids) != len(paths)')
|
|
2564
2564
|
if semaphore is None:
|
|
2565
|
-
semaphore = self._api.
|
|
2565
|
+
semaphore = self._api.get_default_semaphore()
|
|
2566
2566
|
tasks = []
|
|
2567
2567
|
for img_id, img_path in zip(ids, paths):
|
|
2568
2568
|
task = self.download_path_async(
|
|
@@ -1360,7 +1360,7 @@ class VolumeApi(RemoveableBulkModuleApi):
|
|
|
1360
1360
|
ensure_base_path(path)
|
|
1361
1361
|
hash_to_check = None
|
|
1362
1362
|
if semaphore is None:
|
|
1363
|
-
semaphore = self._api.
|
|
1363
|
+
semaphore = self._api.get_default_semaphore()
|
|
1364
1364
|
async with semaphore:
|
|
1365
1365
|
async with aiofiles.open(path, writing_method) as fd:
|
|
1366
1366
|
async for chunk, hhash in self._download_async(
|
|
@@ -1440,7 +1440,7 @@ class VolumeApi(RemoveableBulkModuleApi):
|
|
|
1440
1440
|
if len(ids) != len(paths):
|
|
1441
1441
|
raise ValueError(f'Can not match "ids" and "paths" lists, {len(ids)} != {len(paths)}')
|
|
1442
1442
|
if semaphore is None:
|
|
1443
|
-
semaphore = self._api.
|
|
1443
|
+
semaphore = self._api.get_default_semaphore()
|
|
1444
1444
|
tasks = []
|
|
1445
1445
|
for img_id, img_path in zip(ids, paths):
|
|
1446
1446
|
task = self.download_path_async(
|
supervisely/io/env.py
CHANGED
|
@@ -539,3 +539,18 @@ def mininum_instance_version_for_sdk() -> str:
|
|
|
539
539
|
postprocess_fn=lambda x: x,
|
|
540
540
|
raise_not_found=False,
|
|
541
541
|
)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def semaphore_size() -> int:
|
|
545
|
+
"""Returns semaphore size from environment variable using following
|
|
546
|
+
- SUPERVISELY_ASYNC_SEMAPHORE
|
|
547
|
+
|
|
548
|
+
:return: semaphore size
|
|
549
|
+
:rtype: int
|
|
550
|
+
"""
|
|
551
|
+
return _parse_from_env(
|
|
552
|
+
name="semaphore_size",
|
|
553
|
+
keys=["SUPERVISELY_ASYNC_SEMAPHORE"],
|
|
554
|
+
postprocess_fn=lambda x: int(x),
|
|
555
|
+
raise_not_found=False,
|
|
556
|
+
)
|
supervisely/project/project.py
CHANGED
|
@@ -4387,7 +4387,7 @@ async def _download_project_async(
|
|
|
4387
4387
|
resume_download: Optional[bool] = False,
|
|
4388
4388
|
):
|
|
4389
4389
|
if semaphore is None:
|
|
4390
|
-
semaphore = api.
|
|
4390
|
+
semaphore = api.get_default_semaphore()
|
|
4391
4391
|
|
|
4392
4392
|
dataset_ids = set(dataset_ids) if (dataset_ids is not None) else None
|
|
4393
4393
|
project_fs = None
|
|
@@ -11,7 +11,10 @@ class ProjectType(StrEnum):
|
|
|
11
11
|
POINT_CLOUD_EPISODES = "point_cloud_episodes"
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
|
|
15
14
|
# Constants for multispectral and multiview projects.
|
|
16
15
|
_MULTISPECTRAL_TAG_NAME = "multispectral"
|
|
17
16
|
_MULTIVIEW_TAG_NAME = "multiview"
|
|
17
|
+
|
|
18
|
+
_METADATA_SYSTEM_KEY = "sly_system"
|
|
19
|
+
_METADATA_VALIDATION_SCHEMA_KEY = "validation_schema"
|
|
20
|
+
_METADATA_TIMESTAMP_KEY = "validation_schema_timestamp"
|