supervisely 6.73.438__py3-none-any.whl → 6.73.513__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.
Files changed (203) hide show
  1. supervisely/__init__.py +137 -1
  2. supervisely/_utils.py +81 -0
  3. supervisely/annotation/annotation.py +8 -2
  4. supervisely/annotation/json_geometries_map.py +14 -11
  5. supervisely/annotation/label.py +80 -3
  6. supervisely/api/annotation_api.py +14 -11
  7. supervisely/api/api.py +59 -38
  8. supervisely/api/app_api.py +11 -2
  9. supervisely/api/dataset_api.py +74 -12
  10. supervisely/api/entities_collection_api.py +10 -0
  11. supervisely/api/entity_annotation/figure_api.py +52 -4
  12. supervisely/api/entity_annotation/object_api.py +3 -3
  13. supervisely/api/entity_annotation/tag_api.py +63 -12
  14. supervisely/api/guides_api.py +210 -0
  15. supervisely/api/image_api.py +72 -1
  16. supervisely/api/labeling_job_api.py +83 -1
  17. supervisely/api/labeling_queue_api.py +33 -7
  18. supervisely/api/module_api.py +9 -0
  19. supervisely/api/project_api.py +71 -26
  20. supervisely/api/storage_api.py +3 -1
  21. supervisely/api/task_api.py +13 -2
  22. supervisely/api/team_api.py +4 -3
  23. supervisely/api/video/video_annotation_api.py +119 -3
  24. supervisely/api/video/video_api.py +65 -14
  25. supervisely/api/video/video_figure_api.py +24 -11
  26. supervisely/app/__init__.py +1 -1
  27. supervisely/app/content.py +23 -7
  28. supervisely/app/development/development.py +18 -2
  29. supervisely/app/fastapi/__init__.py +1 -0
  30. supervisely/app/fastapi/custom_static_files.py +1 -1
  31. supervisely/app/fastapi/multi_user.py +105 -0
  32. supervisely/app/fastapi/subapp.py +88 -42
  33. supervisely/app/fastapi/websocket.py +77 -9
  34. supervisely/app/singleton.py +21 -0
  35. supervisely/app/v1/app_service.py +18 -2
  36. supervisely/app/v1/constants.py +7 -1
  37. supervisely/app/widgets/__init__.py +6 -0
  38. supervisely/app/widgets/activity_feed/__init__.py +0 -0
  39. supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
  40. supervisely/app/widgets/activity_feed/style.css +78 -0
  41. supervisely/app/widgets/activity_feed/template.html +22 -0
  42. supervisely/app/widgets/card/card.py +20 -0
  43. supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
  44. supervisely/app/widgets/classes_list_selector/template.html +60 -93
  45. supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
  46. supervisely/app/widgets/classes_table/classes_table.py +1 -0
  47. supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
  48. supervisely/app/widgets/dialog/dialog.py +12 -0
  49. supervisely/app/widgets/dialog/template.html +2 -1
  50. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
  51. supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
  52. supervisely/app/widgets/fast_table/fast_table.py +184 -60
  53. supervisely/app/widgets/fast_table/template.html +1 -1
  54. supervisely/app/widgets/heatmap/__init__.py +0 -0
  55. supervisely/app/widgets/heatmap/heatmap.py +564 -0
  56. supervisely/app/widgets/heatmap/script.js +533 -0
  57. supervisely/app/widgets/heatmap/style.css +233 -0
  58. supervisely/app/widgets/heatmap/template.html +21 -0
  59. supervisely/app/widgets/modal/__init__.py +0 -0
  60. supervisely/app/widgets/modal/modal.py +198 -0
  61. supervisely/app/widgets/modal/template.html +10 -0
  62. supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
  63. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  64. supervisely/app/widgets/radio_tabs/template.html +1 -0
  65. supervisely/app/widgets/select/select.py +6 -3
  66. supervisely/app/widgets/select_class/__init__.py +0 -0
  67. supervisely/app/widgets/select_class/select_class.py +363 -0
  68. supervisely/app/widgets/select_class/template.html +50 -0
  69. supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
  70. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
  71. supervisely/app/widgets/select_tag/__init__.py +0 -0
  72. supervisely/app/widgets/select_tag/select_tag.py +352 -0
  73. supervisely/app/widgets/select_tag/template.html +64 -0
  74. supervisely/app/widgets/select_team/select_team.py +37 -4
  75. supervisely/app/widgets/select_team/template.html +4 -5
  76. supervisely/app/widgets/select_user/__init__.py +0 -0
  77. supervisely/app/widgets/select_user/select_user.py +270 -0
  78. supervisely/app/widgets/select_user/template.html +13 -0
  79. supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
  80. supervisely/app/widgets/select_workspace/template.html +9 -12
  81. supervisely/app/widgets/table/table.py +68 -13
  82. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  83. supervisely/aug/aug.py +6 -2
  84. supervisely/convert/base_converter.py +1 -0
  85. supervisely/convert/converter.py +2 -2
  86. supervisely/convert/image/csv/csv_converter.py +24 -15
  87. supervisely/convert/image/image_converter.py +3 -1
  88. supervisely/convert/image/image_helper.py +48 -4
  89. supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
  90. supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
  91. supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
  92. supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
  93. supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
  94. supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
  95. supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
  96. supervisely/convert/pointcloud/las/las_converter.py +13 -1
  97. supervisely/convert/pointcloud/las/las_helper.py +110 -11
  98. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
  99. supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
  100. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
  101. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
  102. supervisely/convert/video/__init__.py +1 -0
  103. supervisely/convert/video/multi_view/__init__.py +0 -0
  104. supervisely/convert/video/multi_view/multi_view.py +543 -0
  105. supervisely/convert/video/sly/sly_video_converter.py +359 -3
  106. supervisely/convert/video/video_converter.py +24 -4
  107. supervisely/convert/volume/dicom/dicom_converter.py +13 -5
  108. supervisely/convert/volume/dicom/dicom_helper.py +30 -18
  109. supervisely/geometry/constants.py +1 -0
  110. supervisely/geometry/geometry.py +4 -0
  111. supervisely/geometry/helpers.py +5 -1
  112. supervisely/geometry/oriented_bbox.py +676 -0
  113. supervisely/geometry/polyline_3d.py +110 -0
  114. supervisely/geometry/rectangle.py +2 -1
  115. supervisely/io/env.py +76 -1
  116. supervisely/io/fs.py +21 -0
  117. supervisely/nn/benchmark/base_evaluator.py +104 -11
  118. supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
  119. supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
  120. supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
  121. supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
  122. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
  123. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
  124. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
  125. supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
  126. supervisely/nn/inference/cache.py +43 -18
  127. supervisely/nn/inference/gui/serving_gui_template.py +5 -2
  128. supervisely/nn/inference/inference.py +916 -222
  129. supervisely/nn/inference/inference_request.py +55 -10
  130. supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
  131. supervisely/nn/inference/predict_app/gui/gui.py +676 -488
  132. supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
  133. supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
  134. supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
  135. supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
  136. supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
  137. supervisely/nn/inference/predict_app/gui/utils.py +236 -119
  138. supervisely/nn/inference/predict_app/predict_app.py +2 -2
  139. supervisely/nn/inference/session.py +43 -35
  140. supervisely/nn/inference/tracking/bbox_tracking.py +118 -35
  141. supervisely/nn/inference/tracking/point_tracking.py +5 -1
  142. supervisely/nn/inference/tracking/tracker_interface.py +10 -1
  143. supervisely/nn/inference/uploader.py +139 -12
  144. supervisely/nn/live_training/__init__.py +7 -0
  145. supervisely/nn/live_training/api_server.py +111 -0
  146. supervisely/nn/live_training/artifacts_utils.py +243 -0
  147. supervisely/nn/live_training/checkpoint_utils.py +229 -0
  148. supervisely/nn/live_training/dynamic_sampler.py +44 -0
  149. supervisely/nn/live_training/helpers.py +14 -0
  150. supervisely/nn/live_training/incremental_dataset.py +146 -0
  151. supervisely/nn/live_training/live_training.py +497 -0
  152. supervisely/nn/live_training/loss_plateau_detector.py +111 -0
  153. supervisely/nn/live_training/request_queue.py +52 -0
  154. supervisely/nn/model/model_api.py +9 -0
  155. supervisely/nn/model/prediction.py +2 -1
  156. supervisely/nn/model/prediction_session.py +26 -14
  157. supervisely/nn/prediction_dto.py +19 -1
  158. supervisely/nn/tracker/base_tracker.py +11 -1
  159. supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
  160. supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
  161. supervisely/nn/tracker/botsort_tracker.py +94 -65
  162. supervisely/nn/tracker/utils.py +4 -5
  163. supervisely/nn/tracker/visualize.py +93 -93
  164. supervisely/nn/training/gui/classes_selector.py +16 -1
  165. supervisely/nn/training/gui/train_val_splits_selector.py +52 -31
  166. supervisely/nn/training/train_app.py +46 -31
  167. supervisely/project/data_version.py +115 -51
  168. supervisely/project/download.py +1 -1
  169. supervisely/project/pointcloud_episode_project.py +37 -8
  170. supervisely/project/pointcloud_project.py +30 -2
  171. supervisely/project/project.py +14 -2
  172. supervisely/project/project_meta.py +27 -1
  173. supervisely/project/project_settings.py +32 -18
  174. supervisely/project/versioning/__init__.py +1 -0
  175. supervisely/project/versioning/common.py +20 -0
  176. supervisely/project/versioning/schema_fields.py +35 -0
  177. supervisely/project/versioning/video_schema.py +221 -0
  178. supervisely/project/versioning/volume_schema.py +87 -0
  179. supervisely/project/video_project.py +717 -15
  180. supervisely/project/volume_project.py +623 -5
  181. supervisely/template/experiment/experiment.html.jinja +4 -4
  182. supervisely/template/experiment/experiment_generator.py +14 -21
  183. supervisely/template/live_training/__init__.py +0 -0
  184. supervisely/template/live_training/header.html.jinja +96 -0
  185. supervisely/template/live_training/live_training.html.jinja +51 -0
  186. supervisely/template/live_training/live_training_generator.py +464 -0
  187. supervisely/template/live_training/sly-style.css +402 -0
  188. supervisely/template/live_training/template.html.jinja +18 -0
  189. supervisely/versions.json +28 -26
  190. supervisely/video/sampling.py +39 -20
  191. supervisely/video/video.py +41 -12
  192. supervisely/video_annotation/video_figure.py +38 -4
  193. supervisely/video_annotation/video_object.py +29 -4
  194. supervisely/volume/stl_converter.py +2 -0
  195. supervisely/worker_api/agent_rpc.py +24 -1
  196. supervisely/worker_api/rpc_servicer.py +31 -7
  197. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/METADATA +58 -40
  198. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/RECORD +203 -155
  199. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
  200. supervisely_lib/__init__.py +6 -1
  201. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
  202. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
  203. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/top_level.txt +0 -0
supervisely/api/api.py CHANGED
@@ -10,6 +10,7 @@ import glob
10
10
  import json
11
11
  import os
12
12
  import shutil
13
+ import threading
13
14
  from logging import Logger
14
15
  from pathlib import Path
15
16
  from typing import (
@@ -41,6 +42,7 @@ import supervisely.api.dataset_api as dataset_api
41
42
  import supervisely.api.entities_collection_api as entities_collection_api
42
43
  import supervisely.api.file_api as file_api
43
44
  import supervisely.api.github_api as github_api
45
+ import supervisely.api.guides_api as guides_api
44
46
  import supervisely.api.image_annotation_tool_api as image_annotation_tool_api
45
47
  import supervisely.api.image_api as image_api
46
48
  import supervisely.api.import_storage_api as import_stoarge_api
@@ -357,6 +359,7 @@ class Api:
357
359
  self.user = user_api.UserApi(self)
358
360
  self.labeling_job = labeling_job_api.LabelingJobApi(self)
359
361
  self.labeling_queue = labeling_queue_api.LabelingQueueApi(self)
362
+ self.guides = guides_api.GuidesApi(self)
360
363
  self.video = video_api.VideoApi(self)
361
364
  # self.project_class = project_class_api.ProjectClassApi(self)
362
365
  self.object_class = object_class_api.ObjectClassApi(self)
@@ -392,13 +395,15 @@ class Api:
392
395
  else not self.server_address.startswith("https://")
393
396
  )
394
397
 
395
- if check_instance_version:
396
- self._check_version(None if check_instance_version is True else check_instance_version)
397
-
398
398
  self.async_httpx_client: httpx.AsyncClient = None
399
399
  self.httpx_client: httpx.Client = None
400
400
  self._semaphore = None
401
401
  self._instance_version = None
402
+ self._version_check_completed = False
403
+ self._version_check_lock = threading.Lock()
404
+
405
+ if check_instance_version:
406
+ self._check_version(None if check_instance_version is True else check_instance_version)
402
407
 
403
408
  @classmethod
404
409
  def normalize_server_address(cls, server_address: str) -> str:
@@ -600,38 +605,49 @@ class Api:
600
605
  :type version: Optional[str], e.g. "6.9.13"
601
606
  """
602
607
 
603
- # Since it's a informational message, we don't raise an exception if the check fails
604
- # in any case, we don't want to interrupt the user's workflow.
605
- try:
606
- check_result = self.is_version_supported(version)
607
- if check_result is None:
608
+ # Thread-safe one-time check with double-checked locking pattern
609
+ if self._version_check_completed:
610
+ return
611
+
612
+ with self._version_check_lock:
613
+ # Double-check inside the lock
614
+ if self._version_check_completed:
615
+ return
616
+
617
+ self._version_check_completed = True
618
+
619
+ # Since it's a informational message, we don't raise an exception if the check fails
620
+ # in any case, we don't want to interrupt the user's workflow.
621
+ try:
622
+ check_result = self.is_version_supported(version)
623
+ if check_result is None:
624
+ logger.debug(
625
+ "Failed to check if the instance version meets the minimum requirements "
626
+ "of current SDK version. "
627
+ "Ensure that the MINIMUM_INSTANCE_VERSION_FOR_SDK environment variable is set. "
628
+ "Usually you can ignore this message, but if you're adding new features, "
629
+ "which will require upgrade of the Supervisely instance, you should update "
630
+ "it supervisely.__init__.py file."
631
+ )
632
+ if check_result is False:
633
+ message = (
634
+ "The current version of the Supervisely instance is not supported by the SDK. "
635
+ "Some features may not work correctly."
636
+ )
637
+ if not is_community():
638
+ message += (
639
+ " Please upgrade the Supervisely instance to the latest version (recommended) "
640
+ "or downgrade the SDK to the version that supports the current instance (not recommended). "
641
+ "Refer to this docs for more information: "
642
+ "https://docs.supervisely.com/enterprise-edition/get-supervisely/upgrade "
643
+ "Check out changelog for the latest version of Supervisely: "
644
+ "https://app.supervisely.com/changelog"
645
+ )
646
+ logger.warning(message)
647
+ except Exception as e:
608
648
  logger.debug(
609
- "Failed to check if the instance version meets the minimum requirements "
610
- "of current SDK version. "
611
- "Ensure that the MINIMUM_INSTANCE_VERSION_FOR_SDK environment variable is set. "
612
- "Usually you can ignore this message, but if you're adding new features, "
613
- "which will require upgrade of the Supervisely instance, you should update "
614
- "it supervisely.__init__.py file."
615
- )
616
- if check_result is False:
617
- message = (
618
- "The current version of the Supervisely instance is not supported by the SDK. "
619
- "Some features may not work correctly."
649
+ f"Tried to check version compatibility between SDK and instance, but failed: {e}"
620
650
  )
621
- if not is_community():
622
- message += (
623
- " Please upgrade the Supervisely instance to the latest version (recommended) "
624
- "or downgrade the SDK to the version that supports the current instance (not recommended). "
625
- "Refer to this docs for more information: "
626
- "https://docs.supervisely.com/enterprise-edition/get-supervisely/upgrade "
627
- "Check out changelog for the latest version of Supervisely: "
628
- "https://app.supervisely.com/changelog"
629
- )
630
- logger.warning(message)
631
- except Exception as e:
632
- logger.debug(
633
- f"Tried to check version compatibility between SDK and instance, but failed: {e}"
634
- )
635
651
 
636
652
  def post(
637
653
  self,
@@ -686,7 +702,8 @@ class Api:
686
702
  )
687
703
 
688
704
  if response.status_code != requests.codes.ok: # pylint: disable=no-member
689
- self._check_version()
705
+ if not self._version_check_completed:
706
+ self._check_version()
690
707
  Api._raise_for_status(response)
691
708
  return response
692
709
  except requests.RequestException as exc:
@@ -1103,7 +1120,8 @@ class Api:
1103
1120
  timeout=timeout,
1104
1121
  )
1105
1122
  if response.status_code != httpx.codes.OK:
1106
- self._check_version()
1123
+ if not self._version_check_completed:
1124
+ self._check_version()
1107
1125
  Api._raise_for_status_httpx(response)
1108
1126
  return response
1109
1127
  except (httpx.RequestError, httpx.HTTPStatusError) as exc:
@@ -1319,7 +1337,8 @@ class Api:
1319
1337
  httpx.codes.OK,
1320
1338
  httpx.codes.PARTIAL_CONTENT,
1321
1339
  ]:
1322
- self._check_version()
1340
+ if not self._version_check_completed:
1341
+ self._check_version()
1323
1342
  Api._raise_for_status_httpx(resp)
1324
1343
 
1325
1344
  hhash = resp.headers.get("x-content-checksum-sha256", None)
@@ -1433,7 +1452,8 @@ class Api:
1433
1452
  timeout=timeout,
1434
1453
  )
1435
1454
  if response.status_code != httpx.codes.OK:
1436
- self._check_version()
1455
+ if not self._version_check_completed:
1456
+ self._check_version()
1437
1457
  Api._raise_for_status_httpx(response)
1438
1458
  return response
1439
1459
  except (httpx.RequestError, httpx.HTTPStatusError) as exc:
@@ -1574,7 +1594,8 @@ class Api:
1574
1594
  httpx.codes.OK,
1575
1595
  httpx.codes.PARTIAL_CONTENT,
1576
1596
  ]:
1577
- self._check_version()
1597
+ if not self._version_check_completed:
1598
+ self._check_version()
1578
1599
  Api._raise_for_status_httpx(resp)
1579
1600
 
1580
1601
  # received hash of the content to check integrity of the data stream
@@ -140,7 +140,7 @@ def check_workflow_compatibility(api, min_instance_version: str) -> bool:
140
140
  "instance_version", api.instance_version
141
141
  )
142
142
 
143
- if instance_version == "unknown":
143
+ if instance_version is None or instance_version == "unknown":
144
144
  # to check again on the next call
145
145
  del _workflow_compatibility_version_cache["instance_version"]
146
146
  logger.info(
@@ -1750,6 +1750,7 @@ class AppApi(TaskApi):
1750
1750
  module_id: Optional[int] = None,
1751
1751
  redirect_requests: Dict[str, int] = {},
1752
1752
  kubernetes_settings: Optional[Union[KubernetesSettings, Dict[str, Any]]] = None,
1753
+ multi_user_session: bool = False,
1753
1754
  ) -> SessionInfo:
1754
1755
  """Start a new application session (task).
1755
1756
 
@@ -1783,13 +1784,20 @@ class AppApi(TaskApi):
1783
1784
  :type redirect_requests: dict
1784
1785
  :param kubernetes_settings: Kubernetes settings for the task. If not specified, default settings will be used.
1785
1786
  :type kubernetes_settings: Optional[Union[KubernetesSettings, Dict[str, Any]]]
1787
+ :param multi_user_session: If True, the application session will be created as multi-user.
1788
+ In this case, multiple users will be able to connect to the same application session.
1789
+ All users will have separate application states.
1790
+ Available only for applications that support multi-user sessions.
1791
+ :type multi_user_session: bool, default is False
1786
1792
  :return: SessionInfo object with information about the started task.
1787
1793
  :rtype: SessionInfo
1788
1794
  :raises ValueError: If both app_id and module_id are not provided.
1789
1795
  :raises ValueError: If both app_id and module_id are provided.
1790
1796
  """
1791
1797
  users_ids = None
1792
- if users_id is not None:
1798
+ if isinstance(users_id, list) and all(isinstance(u, int) for u in users_id):
1799
+ users_ids = users_id
1800
+ elif isinstance(users_id, int):
1793
1801
  users_ids = [users_id]
1794
1802
 
1795
1803
  new_params = {}
@@ -1818,6 +1826,7 @@ class AppApi(TaskApi):
1818
1826
  module_id=module_id,
1819
1827
  redirect_requests=redirect_requests,
1820
1828
  kubernetes_settings=kubernetes_settings,
1829
+ multi_user_session=multi_user_session,
1821
1830
  )
1822
1831
  if type(result) is not list:
1823
1832
  result = [result]
@@ -1021,13 +1021,66 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
1021
1021
 
1022
1022
  return dataset_tree
1023
1023
 
1024
- def tree(self, project_id: int) -> Generator[Tuple[List[str], DatasetInfo], None, None]:
1024
+ def _yield_tree(
1025
+ self, tree: Dict[DatasetInfo, Dict], path: List[str]
1026
+ ) -> Generator[Tuple[List[str], DatasetInfo], None, None]:
1027
+ """
1028
+ Helper method for recursive tree traversal.
1029
+ Yields tuples of (path, dataset) for all datasets in the tree. For each node (dataset) at the current level,
1030
+ yields its (path, dataset) before recursively traversing and yielding from its children.
1031
+
1032
+ :param tree: Tree structure to yield from.
1033
+ :type tree: Dict[DatasetInfo, Dict]
1034
+ :param path: Current path (used for recursion).
1035
+ :type path: List[str]
1036
+ :return: Generator of tuples of (path, dataset).
1037
+ :rtype: Generator[Tuple[List[str], DatasetInfo], None, None]
1038
+ """
1039
+ for dataset, children in tree.items():
1040
+ yield path, dataset
1041
+ new_path = path + [dataset.name]
1042
+ if children:
1043
+ yield from self._yield_tree(children, new_path)
1044
+
1045
+ def _find_dataset_in_tree(
1046
+ self, tree: Dict[DatasetInfo, Dict], target_id: int, path: List[str] = None
1047
+ ) -> Tuple[Optional[DatasetInfo], Optional[Dict], List[str]]:
1048
+ """Find a specific dataset in the tree and return its subtree and path.
1049
+
1050
+ :param tree: Tree structure to search in.
1051
+ :type tree: Dict[DatasetInfo, Dict]
1052
+ :param target_id: ID of the dataset to find.
1053
+ :type target_id: int
1054
+ :param path: Current path (used for recursion).
1055
+ :type path: List[str], optional
1056
+ :return: Tuple of (found_dataset, its_subtree, path_to_dataset).
1057
+ :rtype: Tuple[Optional[DatasetInfo], Optional[Dict], List[str]]
1058
+ """
1059
+ if path is None:
1060
+ path = []
1061
+
1062
+ for dataset, children in tree.items():
1063
+ if dataset.id == target_id:
1064
+ return dataset, children, path
1065
+ # Search in children
1066
+ if children:
1067
+ found_dataset, found_children, found_path = self._find_dataset_in_tree(
1068
+ children, target_id, path + [dataset.name]
1069
+ )
1070
+ if found_dataset is not None:
1071
+ return found_dataset, found_children, found_path
1072
+ return None, None, []
1073
+
1074
+ def tree(self, project_id: int, dataset_id: Optional[int] = None) -> Generator[Tuple[List[str], DatasetInfo], None, None]:
1025
1075
  """Yields tuples of (path, dataset) for all datasets in the project.
1026
1076
  Path of the dataset is a list of parents, e.g. ["ds1", "ds2", "ds3"].
1027
1077
  For root datasets, the path is an empty list.
1028
1078
 
1029
1079
  :param project_id: Project ID in which the Dataset is located.
1030
1080
  :type project_id: int
1081
+ :param dataset_id: Optional Dataset ID to start the tree from. If provided, only yields
1082
+ the subtree starting from this dataset (including the dataset itself and all its children).
1083
+ :type dataset_id: Optional[int]
1031
1084
  :return: Generator of tuples of (path, dataset).
1032
1085
  :rtype: Generator[Tuple[List[str], DatasetInfo], None, None]
1033
1086
  :Usage example:
@@ -1040,11 +1093,17 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
1040
1093
 
1041
1094
  project_id = 123
1042
1095
 
1096
+ # Get all datasets in the project
1043
1097
  for parents, dataset in api.dataset.tree(project_id):
1044
1098
  parents: List[str]
1045
1099
  dataset: sly.DatasetInfo
1046
1100
  print(parents, dataset.name)
1047
1101
 
1102
+ # Get only a specific branch starting from dataset_id = 456
1103
+ for parents, dataset in api.dataset.tree(project_id, dataset_id=456):
1104
+ parents: List[str]
1105
+ dataset: sly.DatasetInfo
1106
+ print(parents, dataset.name)
1048
1107
 
1049
1108
  # Output:
1050
1109
  # [] ds1
@@ -1052,17 +1111,20 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
1052
1111
  # ["ds1", "ds2"] ds3
1053
1112
  """
1054
1113
 
1055
- def yield_tree(
1056
- tree: Dict[DatasetInfo, Dict], path: List[str]
1057
- ) -> Generator[Tuple[List[str], DatasetInfo], None, None]:
1058
- """Yields tuples of (path, dataset) for all datasets in the tree."""
1059
- for dataset, children in tree.items():
1060
- yield path, dataset
1061
- new_path = path + [dataset.name]
1062
- if children:
1063
- yield from yield_tree(children, new_path)
1064
-
1065
- yield from yield_tree(self.get_tree(project_id), [])
1114
+ full_tree = self.get_tree(project_id)
1115
+
1116
+ if dataset_id is None:
1117
+ # Return the full tree
1118
+ yield from self._yield_tree(full_tree, [])
1119
+ else:
1120
+ # Find the specific dataset and return only its subtree
1121
+ target_dataset, subtree, dataset_path = self._find_dataset_in_tree(full_tree, dataset_id)
1122
+ if target_dataset is not None:
1123
+ # Yield the target dataset first, then its children
1124
+ yield dataset_path, target_dataset
1125
+ if subtree:
1126
+ new_path = dataset_path + [target_dataset.name]
1127
+ yield from self._yield_tree(subtree, new_path)
1066
1128
 
1067
1129
  def get_nested(self, project_id: int, dataset_id: int) -> List[DatasetInfo]:
1068
1130
  """Returns a list of all nested datasets in the specified dataset.
@@ -281,6 +281,7 @@ class EntitiesCollectionApi(UpdateableModule, RemoveableModuleApi):
281
281
  description: Optional[str] = None,
282
282
  type: str = CollectionType.DEFAULT,
283
283
  ai_search_key: Optional[str] = None,
284
+ change_name_if_conflict=False,
284
285
  ) -> EntitiesCollectionInfo:
285
286
  """
286
287
  Creates Entities Collections.
@@ -295,6 +296,8 @@ class EntitiesCollectionApi(UpdateableModule, RemoveableModuleApi):
295
296
  :type type: str
296
297
  :param ai_search_key: AI search key for the collection. Defaults to None.
297
298
  :type ai_search_key: Optional[str]
299
+ :param change_name_if_conflict: Checks if given name already exists and adds suffix to the end of the name. Defaults to False.
300
+ :type change_name_if_conflict: bool
298
301
  :return: Information about new Entities Collection
299
302
  :rtype: :class:`EntitiesCollectionInfo`
300
303
  :Usage example:
@@ -316,6 +319,13 @@ class EntitiesCollectionApi(UpdateableModule, RemoveableModuleApi):
316
319
  new_collection = api.entities_collection.create(project_id, name, description, type, ai_search_key)
317
320
  print(new_collection)
318
321
  """
322
+
323
+ name = self._get_effective_new_name(
324
+ parent_id=project_id,
325
+ name=name,
326
+ change_name_if_conflict=change_name_if_conflict,
327
+ )
328
+
319
329
  method = "entities-collections.add"
320
330
  data = {
321
331
  ApiField.PROJECT_ID: project_id,
@@ -24,6 +24,7 @@ from requests_toolbelt import MultipartDecoder, MultipartEncoder
24
24
  from tqdm import tqdm
25
25
 
26
26
  from supervisely._utils import batched, logger, run_coroutine
27
+ from supervisely.annotation.label import LabelingStatus
27
28
  from supervisely.api.module_api import ApiField, ModuleApi, RemoveableBulkModuleApi
28
29
  from supervisely.geometry.rectangle import Rectangle
29
30
  from supervisely.video_annotation.key_id_map import KeyIdMap
@@ -221,6 +222,8 @@ class FigureApi(RemoveableBulkModuleApi):
221
222
  "meta",
222
223
  "area",
223
224
  "priority",
225
+ "nnCreated",
226
+ "nnUpdated",
224
227
  ]
225
228
  return self._get_info_by_id(id, "figures.info", {ApiField.FIELDS: fields})
226
229
 
@@ -233,6 +236,7 @@ class FigureApi(RemoveableBulkModuleApi):
233
236
  geometry_type: str,
234
237
  track_id: Optional[int] = None,
235
238
  custom_data: Optional[dict] = None,
239
+ status: Optional[LabelingStatus] = None,
236
240
  ) -> int:
237
241
  """"""
238
242
  input_figure = {
@@ -242,6 +246,13 @@ class FigureApi(RemoveableBulkModuleApi):
242
246
  ApiField.GEOMETRY: geometry_json,
243
247
  }
244
248
 
249
+ if status is None:
250
+ status = LabelingStatus.MANUAL
251
+
252
+ nn_created, nn_updated = LabelingStatus.to_flags(status)
253
+ input_figure[ApiField.NN_CREATED] = nn_created
254
+ input_figure[ApiField.NN_UPDATED] = nn_updated
255
+
245
256
  if track_id is not None:
246
257
  input_figure[ApiField.TRACK_ID] = track_id
247
258
 
@@ -376,6 +387,8 @@ class FigureApi(RemoveableBulkModuleApi):
376
387
  ApiField.AREA,
377
388
  ApiField.PRIORITY,
378
389
  ApiField.CUSTOM_DATA,
390
+ ApiField.NN_CREATED,
391
+ ApiField.NN_UPDATED,
379
392
  ]
380
393
  figures_infos = self.get_list_all_pages(
381
394
  "figures.list",
@@ -496,6 +509,8 @@ class FigureApi(RemoveableBulkModuleApi):
496
509
  ApiField.AREA,
497
510
  ApiField.PRIORITY,
498
511
  ApiField.CUSTOM_DATA,
512
+ ApiField.NN_CREATED,
513
+ ApiField.NN_UPDATED,
499
514
  ]
500
515
  if skip_geometry is True:
501
516
  fields = [x for x in fields if x != ApiField.GEOMETRY]
@@ -580,10 +595,13 @@ class FigureApi(RemoveableBulkModuleApi):
580
595
  """
581
596
  geometries = {}
582
597
  for idx, part in self._download_geometries_generator(ids):
583
- if progress_cb is not None:
584
- progress_cb(len(part.content))
585
- geometry_json = json.loads(part.content)
586
- geometries[idx] = geometry_json
598
+ try:
599
+ if progress_cb is not None:
600
+ progress_cb(len(part.content))
601
+ geometry_json = json.loads(part.content)
602
+ geometries[idx] = geometry_json
603
+ except Exception as e:
604
+ raise RuntimeError(f"Failed to decode geometry for figure ID {idx}") from e
587
605
 
588
606
  if len(geometries) != len(ids):
589
607
  raise RuntimeError("Not all geometries were downloaded")
@@ -854,6 +872,8 @@ class FigureApi(RemoveableBulkModuleApi):
854
872
  ApiField.AREA,
855
873
  ApiField.PRIORITY,
856
874
  ApiField.CUSTOM_DATA,
875
+ ApiField.NN_CREATED,
876
+ ApiField.NN_UPDATED,
857
877
  ]
858
878
  if skip_geometry is True:
859
879
  fields = [x for x in fields if x != ApiField.GEOMETRY]
@@ -1026,3 +1046,31 @@ class FigureApi(RemoveableBulkModuleApi):
1026
1046
  image_ids=image_ids,
1027
1047
  skip_geometry=skip_geometry,
1028
1048
  )
1049
+
1050
+ def restore_batch(self, ids: List[int], progress_cb: Optional[Callable] = None, batch_size: int = 50):
1051
+ """
1052
+ Restore archived figures in batches from the Supervisely server.
1053
+
1054
+ :param ids: IDs of figures in Supervisely.
1055
+ :type ids: List[int]
1056
+ :param progress_cb: Optional callback to track restore progress. Receives number of restored figures in the current batch.
1057
+ :type progress_cb: Optional[Callable]
1058
+ :param batch_size: Number of figure IDs to send in a single request.
1059
+ :type batch_size: int
1060
+ """
1061
+ for ids_batch in batched(ids, batch_size=batch_size):
1062
+ self._api.post(
1063
+ "figures.bulk.restore",
1064
+ {ApiField.FIGURE_IDS: ids_batch},
1065
+ )
1066
+ if progress_cb is not None:
1067
+ progress_cb(len(ids_batch))
1068
+
1069
+ def restore(self, id: int):
1070
+ """
1071
+ Restore a single archived figure with the specified ID from the Supervisely server.
1072
+
1073
+ :param id: Figure ID in Supervisely.
1074
+ :type id: int
1075
+ """
1076
+ self.restore_batch([id])
@@ -214,6 +214,7 @@ class ObjectApi(RemoveableBulkModuleApi):
214
214
  objects,
215
215
  key_id_map: KeyIdMap = None,
216
216
  is_pointcloud=False,
217
+ is_video_multi_view: bool = False,
217
218
  ):
218
219
  """"""
219
220
  if len(objects) == 0:
@@ -225,8 +226,7 @@ class ObjectApi(RemoveableBulkModuleApi):
225
226
  for obj in objects:
226
227
  new_obj = {ApiField.CLASS_ID: objcls_name_id_map[obj.obj_class.name]}
227
228
 
228
- if not is_pointcloud:
229
- # if entity_id is not None:
229
+ if not is_video_multi_view and not is_pointcloud:
230
230
  new_obj[ApiField.ENTITY_ID] = entity_id
231
231
  items.append(new_obj)
232
232
 
@@ -238,7 +238,7 @@ class ObjectApi(RemoveableBulkModuleApi):
238
238
  KeyIdMap.add_objects_to(key_id_map, [obj.key() for obj in objects], ids)
239
239
 
240
240
  # add tags to objects
241
- tag_api.append_to_objects(entity_id, project_id, objects, key_id_map)
241
+ tag_api.append_to_objects(entity_id, project_id, objects, key_id_map, is_video_multi_view)
242
242
 
243
243
  return ids
244
244
 
@@ -5,6 +5,8 @@ from typing import Any, Dict, List, Optional, Union
5
5
  from supervisely._utils import batched
6
6
  from supervisely.api.module_api import ApiField, ModuleApi
7
7
  from supervisely.collection.key_indexed_collection import KeyIndexedCollection
8
+ from supervisely.project.project_meta import ProjectMeta
9
+ from supervisely.project.project_settings import LabelingInterface
8
10
  from supervisely.task.progress import tqdm_sly
9
11
  from supervisely.video_annotation.key_id_map import KeyIdMap
10
12
 
@@ -157,7 +159,12 @@ class TagApi(ModuleApi):
157
159
  return ids
158
160
 
159
161
  def append_to_objects(
160
- self, entity_id: int, project_id: int, objects: KeyIndexedCollection, key_id_map: KeyIdMap
162
+ self,
163
+ entity_id: int,
164
+ project_id: int,
165
+ objects: KeyIndexedCollection,
166
+ key_id_map: KeyIdMap,
167
+ is_video_multi_view: bool = False,
161
168
  ):
162
169
  """
163
170
  Add Tags to Annotation Objects for a specific entity (image etc.).
@@ -170,6 +177,8 @@ class TagApi(ModuleApi):
170
177
  :type objects: KeyIndexedCollection
171
178
  :param key_id_map: KeyIdMap object.
172
179
  :type key_id_map: KeyIdMap
180
+ :param is_video_multi_view: If True, indicates that the entity is a multi-view video.
181
+ :type is_video_multi_view: bool
173
182
  :return: List of tags IDs
174
183
  :rtype: list
175
184
  :Usage example:
@@ -210,12 +219,16 @@ class TagApi(ModuleApi):
210
219
  raise RuntimeError("SDK error: len(tags_keys) != len(tags_to_add)")
211
220
  if len(tags_keys) == 0:
212
221
  return
213
- ids = self.append_to_objects_json(entity_id, tags_to_add, project_id)
222
+ ids = self.append_to_objects_json(entity_id, tags_to_add, project_id, is_video_multi_view)
214
223
  KeyIdMap.add_tags_to(key_id_map, tags_keys, ids)
215
224
  return ids
216
225
 
217
226
  def append_to_objects_json(
218
- self, entity_id: int, tags_json: List[Dict], project_id: Optional[int] = None
227
+ self,
228
+ entity_id: int,
229
+ tags_json: List[Dict],
230
+ project_id: Optional[int] = None,
231
+ is_video_multi_view: bool = False,
219
232
  ) -> List[int]:
220
233
  """
221
234
  Add Tags to Annotation Objects for specific entity (image etc.).
@@ -224,6 +237,11 @@ class TagApi(ModuleApi):
224
237
  :type entity_id: int
225
238
  :param tags_json: Collection of tags in JSON format
226
239
  :type tags_json: dict
240
+ :param project_id: Project ID in Supervisely. Uses to get tag name to tag ID mapping.
241
+ Not required if `multi_view` is True.
242
+ :type project_id: int, optional
243
+ :param is_video_multi_view: If True, indicates that the entity is a multi-view video.
244
+ :type is_video_multi_view: bool
227
245
  :return: List of tags IDs
228
246
  :rtype: list
229
247
 
@@ -262,10 +280,15 @@ class TagApi(ModuleApi):
262
280
  # 80421103
263
281
  # ]
264
282
  """
283
+ project_meta = self._api.optimization_context.get("project_meta")
284
+
285
+ if isinstance(project_meta, ProjectMeta):
286
+ if project_meta.labeling_interface == LabelingInterface.MULTIVIEW:
287
+ is_video_multi_view = True
265
288
 
266
289
  if len(tags_json) == 0:
267
290
  return []
268
- if project_id is not None:
291
+ if project_id is not None and not is_video_multi_view:
269
292
  json_data = {ApiField.PROJECT_ID: project_id, ApiField.TAGS: tags_json}
270
293
  else:
271
294
  json_data = {ApiField.ENTITY_ID: entity_id, ApiField.TAGS: tags_json}
@@ -280,6 +303,8 @@ class TagApi(ModuleApi):
280
303
  batch_size: int = 100,
281
304
  log_progress: bool = False,
282
305
  progress: Optional[tqdm_sly] = None,
306
+ is_video_multi_view: bool = False,
307
+ entity_id: Optional[int] = None,
283
308
  ) -> List[Dict[str, Union[str, int, None]]]:
284
309
  """
285
310
  For images project:
@@ -306,6 +331,11 @@ class TagApi(ModuleApi):
306
331
  :type log_progress: bool
307
332
  :param progress: Progress bar object to display progress.
308
333
  :type progress: Optional[tqdm_sly]
334
+ :param is_video_multi_view: If True, indicates that the entity is a multi-view video.
335
+ :type is_video_multi_view: bool
336
+ :param entity_id: ID of the entity in Supervisely to add a tag to its objects.
337
+ Required if `is_video_multi_view` is True.
338
+ :type entity_id: Optional[int]
309
339
  :return: List of tags infos as dictionaries.
310
340
  :rtype: List[Dict[str, Union[str, int, None]]]
311
341
 
@@ -363,6 +393,12 @@ class TagApi(ModuleApi):
363
393
  if progress is not None:
364
394
  log_progress = False
365
395
 
396
+ project_meta = self._api.optimization_context.get("project_meta")
397
+
398
+ if isinstance(project_meta, ProjectMeta):
399
+ if project_meta.labeling_interface == LabelingInterface.MULTIVIEW:
400
+ is_video_multi_view = True
401
+
366
402
  result = []
367
403
 
368
404
  if len(tags_list) == 0:
@@ -373,7 +409,12 @@ class TagApi(ModuleApi):
373
409
  total=len(tags_list),
374
410
  )
375
411
  for batch in batched(tags_list, batch_size):
376
- data = {ApiField.PROJECT_ID: project_id, ApiField.TAGS: batch}
412
+ if is_video_multi_view:
413
+ if entity_id is None:
414
+ raise ValueError("entity_id must be provided when is_video_multi_view is True")
415
+ data = {ApiField.ENTITY_ID: entity_id, ApiField.TAGS: batch}
416
+ else:
417
+ data = {ApiField.PROJECT_ID: project_id, ApiField.TAGS: batch}
377
418
  if type(self) is TagApi:
378
419
  response = self._api.post("figures.tags.bulk.add", data)
379
420
  else:
@@ -463,6 +504,8 @@ class TagApi(ModuleApi):
463
504
  tags_map: Dict[int, Any],
464
505
  batch_size: int = 100,
465
506
  log_progress: bool = False,
507
+ is_video_multi_view: bool = False,
508
+ entity_id: Optional[int] = None,
466
509
  ) -> List[Dict[str, Union[str, int, None]]]:
467
510
  """
468
511
  For images project:
@@ -483,8 +526,13 @@ class TagApi(ModuleApi):
483
526
  :type batch_size: int
484
527
  :param log_progress: If True, will display a progress bar.
485
528
  :type log_progress: bool
529
+ :param is_video_multi_view: If True, indicates that the entity is a multi-view video.
530
+ :type is_video_multi_view: bool
531
+ :param entity_id: ID of the entity in Supervisely to add a tag to its objects.
532
+ Required if `is_video_multi_view` is True.
533
+ :type entity_id: Optional[int]
486
534
  :return: List of tags infos as dictionaries.
487
- :rtype: List[Dit[str, Union[str, int, None]]]
535
+ :rtype: List[Dict[str, Union[str, int, None]]]
488
536
 
489
537
  Usage example:
490
538
  .. code-block:: python
@@ -527,11 +575,14 @@ class TagApi(ModuleApi):
527
575
  raise ValueError(f"Tag {tag.name} meta has no sly_id")
528
576
 
529
577
  data.append(
530
- {
531
- ApiField.TAG_ID: tag.meta.sly_id,
532
- OBJ_ID_FIELD: obj_id,
533
- **tag.to_json()
534
- }
578
+ {ApiField.TAG_ID: tag.meta.sly_id, OBJ_ID_FIELD: obj_id, **tag.to_json()}
535
579
  )
536
580
 
537
- return self.add_to_objects(project_id, data, batch_size, log_progress)
581
+ return self.add_to_objects(
582
+ project_id,
583
+ data,
584
+ batch_size,
585
+ log_progress,
586
+ is_video_multi_view=is_video_multi_view,
587
+ entity_id=entity_id,
588
+ )