supervisely 6.73.227__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 CHANGED
@@ -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.11.16"
312
+ os.environ["MINIMUM_INSTANCE_VERSION_FOR_SDK"] = "6.12.5"
supervisely/_utils.py CHANGED
@@ -14,7 +14,7 @@ import urllib
14
14
  from datetime import datetime
15
15
  from functools import wraps
16
16
  from tempfile import gettempdir
17
- from typing import List, Literal, Optional
17
+ from typing import Any, Dict, List, Literal, Optional, Tuple
18
18
 
19
19
  import numpy as np
20
20
  from requests.utils import DEFAULT_CA_BUNDLE_PATH
@@ -316,6 +316,15 @@ def get_readable_datetime(value: str) -> str:
316
316
  return dt.strftime("%Y-%m-%d %H:%M:%S")
317
317
 
318
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
+
319
328
  def get_certificates_list(path: str = DEFAULT_CA_BUNDLE_PATH) -> List[str]:
320
329
  with open(path, "r", encoding="ascii") as f:
321
330
  content = f.read().strip()
@@ -385,6 +394,51 @@ def add_callback(func, callback):
385
394
  return wrapper
386
395
 
387
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
+
388
442
  def get_or_create_event_loop() -> asyncio.AbstractEventLoop:
389
443
  """
390
444
  Get the current event loop or create a new one if it doesn't exist.
@@ -4,7 +4,17 @@
4
4
  # docs
5
5
  from __future__ import annotations
6
6
 
7
- from typing import Dict, Generator, List, Literal, NamedTuple, Optional, Tuple, Union
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"
@@ -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
@@ -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
@@ -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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: supervisely
3
- Version: 6.73.227
3
+ Version: 6.73.228
4
4
  Summary: Supervisely Python SDK.
5
5
  Home-page: https://github.com/supervisely/supervisely
6
6
  Author: Supervisely
@@ -1,6 +1,6 @@
1
1
  supervisely/README.md,sha256=XM-DiMC6To3I9RjQZ0c61905EFRR_jnCUx2q3uNR-X8,3331
2
- supervisely/__init__.py,sha256=mpj1kUpy8jURvMixE8b1QDccHDao-C-RSkYNM-2UL2k,10800
3
- supervisely/_utils.py,sha256=HV3PhGMbTIu5r95p8PqT7tzx84xEYb2uYI78OSy6xEA,12987
2
+ supervisely/__init__.py,sha256=Ghm5UsSQGjTr7cWetpSxewX0Rys9yC3rb3PQJQJD0_E,10799
3
+ supervisely/_utils.py,sha256=2l9e8L7-p9twlwsBTFKZPM71bCU0aeP3rvs31wy0P-A,15059
4
4
  supervisely/function_wrapper.py,sha256=R5YajTQ0GnRp2vtjwfC9hINkzQc0JiyGsu8TER373xY,1912
5
5
  supervisely/sly_logger.py,sha256=LG1wTyyctyEKuCuKM2IKf_SMPH7BzkTsFdO-0tnorzg,6225
6
6
  supervisely/tiny_timer.py,sha256=hkpe_7FE6bsKL79blSs7WBaktuPavEVu67IpEPrfmjE,183
@@ -24,11 +24,11 @@ supervisely/api/agent_api.py,sha256=ShWAIlXcWXcyI9fqVuP5GZVCigCMJmjnvdGUfLspD6Y,
24
24
  supervisely/api/annotation_api.py,sha256=kgbxlAmvwLX7nMFnQ23hZDZryD3tVSehfqRkLEGXQXA,58828
25
25
  supervisely/api/api.py,sha256=BdN99Znu_nLjnt--i6xoN9W4v0Am3kYlkqZZeIy4UuE,60633
26
26
  supervisely/api/app_api.py,sha256=hjoL24Nc1POlIqz5bkpMBij5D-cwCHvkznkaGpODyzA,67086
27
- supervisely/api/dataset_api.py,sha256=7iwAyz3pmzFG2i072gLdXjczfBGbyj-V_rRl7Tx-V30,37944
27
+ supervisely/api/dataset_api.py,sha256=2-SQBlgEnIN-0uvDbtPlSXr6ztBeZ3WPryhkOtpBmk4,40786
28
28
  supervisely/api/file_api.py,sha256=RM3clcdcfj0MtSqGdu6afCqUieYj_2q5gcEch_36tCE,82421
29
29
  supervisely/api/github_api.py,sha256=NIexNjEer9H5rf5sw2LEZd7C1WR-tK4t6IZzsgeAAwQ,623
30
30
  supervisely/api/image_annotation_tool_api.py,sha256=YcUo78jRDBJYvIjrd-Y6FJAasLta54nnxhyaGyanovA,5237
31
- supervisely/api/image_api.py,sha256=N2JotH62h-3NRiCbgzPvXOjLavkygPYlEtqFw8rgvIg,158305
31
+ supervisely/api/image_api.py,sha256=BMhlJZwfbxFx8eHBQaKNmGI9a7UbOpJrIEmC6Eci-Ck,162395
32
32
  supervisely/api/import_storage_api.py,sha256=BDCgmR0Hv6OoiRHLCVPKt3iDxSVlQp1WrnKhAK_Zl84,460
33
33
  supervisely/api/issues_api.py,sha256=BqDJXmNoTzwc3xe6_-mA7FDFC5QQ-ahGbXk_HmpkSeQ,17925
34
34
  supervisely/api/labeling_job_api.py,sha256=odnzZjp29yM16Gq-FYkv-OA4WFMNJCLFo4qSikW2A7c,56280
@@ -36,7 +36,7 @@ supervisely/api/module_api.py,sha256=nFMvROP7XB6_BVSsP9W_eEKiTlcGYxezyMCV4pdqk8k
36
36
  supervisely/api/neural_network_api.py,sha256=ktPVRO4Jeulougio8F0mioJJHwRJcX250Djp1wBoQ9c,7620
37
37
  supervisely/api/object_class_api.py,sha256=-rQcKwhBw3iL9KNH9c1ROgoimgWM1ls6Wi_tb1R-MzY,7683
38
38
  supervisely/api/plugin_api.py,sha256=TlfrosdRuYG4NUxk92QiQoVaOdztFspPpygyVa3M3zk,5283
39
- supervisely/api/project_api.py,sha256=CNB3kIh_RfW_yOEy58eNpQaE8u6NMOuBmk70QfVni4s,68654
39
+ supervisely/api/project_api.py,sha256=3_4wP3RqLHhbdftwu2LF2iKUMmFxrhk3FryL-OpplZo,78633
40
40
  supervisely/api/project_class_api.py,sha256=5cyjdGPPb2tpttu5WmYoOxUNiDxqiojschkhZumF0KM,1426
41
41
  supervisely/api/remote_storage_api.py,sha256=xy9-j5hSftVcAILyqF_mQdQ1DUywt9msq2QYuSE-PVY,15032
42
42
  supervisely/api/report_api.py,sha256=Om7CGulUbQ4BuJ16eDtz7luLe0JQNqab-LoLpUXu7YE,7123
@@ -938,7 +938,7 @@ supervisely/project/pointcloud_project.py,sha256=Y8Xhi6Hg-KyztwFncezuDfKTt2FILss
938
938
  supervisely/project/project.py,sha256=8ghaq9OCFRdhTNuKo_morBgVxgFOS1KKpmU8Kyl1vdo,180328
939
939
  supervisely/project/project_meta.py,sha256=26s8IiHC5Pg8B1AQi6_CrsWteioJP2in00cRNe8QlW0,51423
940
940
  supervisely/project/project_settings.py,sha256=NLThzU_DCynOK6hkHhVdFyezwprn9UqlnrLDe_3qhkY,9347
941
- supervisely/project/project_type.py,sha256=XRcZG5zI4RR8-JSlG-n0VVjGA4gJSfLIU-hbqiaNOcc,384
941
+ supervisely/project/project_type.py,sha256=_3RqW2CnDBKFOvSIrQT1RJQaiHirs34_jiQS8CkwCpo,530
942
942
  supervisely/project/readme_template.md,sha256=rGmSLRVUSGjvorjpzl0sZ7YA4sKfDexl95NFtMISj3I,9128
943
943
  supervisely/project/upload.py,sha256=AjgHYgVZwUE25ygC5pqvFjdAladbyB8T78mlet5Qpho,3750
944
944
  supervisely/project/video_project.py,sha256=AoZDIF3TAPke3WVZAOSWnuyIs12sFZsY5CxARSNQ5N8,63740
@@ -997,9 +997,9 @@ supervisely/worker_proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
997
997
  supervisely/worker_proto/worker_api_pb2.py,sha256=VQfi5JRBHs2pFCK1snec3JECgGnua3Xjqw_-b3aFxuM,59142
998
998
  supervisely/worker_proto/worker_api_pb2_grpc.py,sha256=3BwQXOaP9qpdi0Dt9EKG--Lm8KGN0C5AgmUfRv77_Jk,28940
999
999
  supervisely_lib/__init__.py,sha256=7-3QnN8Zf0wj8NCr2oJmqoQWMKKPKTECvjH9pd2S5vY,159
1000
- supervisely-6.73.227.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1001
- supervisely-6.73.227.dist-info/METADATA,sha256=vUAUlDnXTIsqkAF3lL4hg0HMNuKe8CIa4xnEyNREKo0,33150
1002
- supervisely-6.73.227.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
1003
- supervisely-6.73.227.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1004
- supervisely-6.73.227.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1005
- supervisely-6.73.227.dist-info/RECORD,,
1000
+ supervisely-6.73.228.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1001
+ supervisely-6.73.228.dist-info/METADATA,sha256=gEzztrS-yBBzf4a8JM9h_HWYtonbH_CrRzFgNqGr1Fs,33150
1002
+ supervisely-6.73.228.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
1003
+ supervisely-6.73.228.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1004
+ supervisely-6.73.228.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1005
+ supervisely-6.73.228.dist-info/RECORD,,