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.

@@ -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, dataset_id: int, name: str, path: str, meta: Optional[Dict] = None
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(dataset_id, [name], [path], metas=metas)[0]
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, names, hashes, metas=metas, conflict_resolution=conflict_resolution
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._get_default_semaphore()
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._get_default_semaphore()
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._get_default_semaphore()
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._get_default_semaphore()
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._get_default_semaphore()
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._get_default_semaphore()
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._get_default_semaphore()
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._get_default_semaphore()
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._get_default_semaphore()
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._get_default_semaphore()
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(
@@ -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 abs_url, compress_image_url, is_development
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._get_default_semaphore()
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._get_default_semaphore()
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._get_default_semaphore()
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._get_default_semaphore()
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._get_default_semaphore()
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
+ )
@@ -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._get_default_semaphore()
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"
@@ -1594,7 +1594,7 @@ async def download_video_project_async(
1594
1594
  )
1595
1595
  """
1596
1596
  if semaphore is None:
1597
- semaphore = api._get_default_semaphore()
1597
+ semaphore = api.get_default_semaphore()
1598
1598
 
1599
1599
  key_id_map = KeyIdMap()
1600
1600