supervisely 6.73.452__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 (189) hide show
  1. supervisely/__init__.py +25 -1
  2. supervisely/annotation/annotation.py +8 -2
  3. supervisely/annotation/json_geometries_map.py +13 -12
  4. supervisely/api/annotation_api.py +6 -3
  5. supervisely/api/api.py +2 -0
  6. supervisely/api/app_api.py +10 -1
  7. supervisely/api/dataset_api.py +74 -12
  8. supervisely/api/entities_collection_api.py +10 -0
  9. supervisely/api/entity_annotation/figure_api.py +28 -0
  10. supervisely/api/entity_annotation/object_api.py +3 -3
  11. supervisely/api/entity_annotation/tag_api.py +63 -12
  12. supervisely/api/guides_api.py +210 -0
  13. supervisely/api/image_api.py +4 -0
  14. supervisely/api/labeling_job_api.py +83 -1
  15. supervisely/api/labeling_queue_api.py +33 -7
  16. supervisely/api/module_api.py +5 -0
  17. supervisely/api/project_api.py +71 -26
  18. supervisely/api/storage_api.py +3 -1
  19. supervisely/api/task_api.py +13 -2
  20. supervisely/api/team_api.py +4 -3
  21. supervisely/api/video/video_annotation_api.py +119 -3
  22. supervisely/api/video/video_api.py +65 -14
  23. supervisely/app/__init__.py +1 -1
  24. supervisely/app/content.py +23 -7
  25. supervisely/app/development/development.py +18 -2
  26. supervisely/app/fastapi/__init__.py +1 -0
  27. supervisely/app/fastapi/custom_static_files.py +1 -1
  28. supervisely/app/fastapi/multi_user.py +105 -0
  29. supervisely/app/fastapi/subapp.py +88 -42
  30. supervisely/app/fastapi/websocket.py +77 -9
  31. supervisely/app/singleton.py +21 -0
  32. supervisely/app/v1/app_service.py +18 -2
  33. supervisely/app/v1/constants.py +7 -1
  34. supervisely/app/widgets/__init__.py +6 -0
  35. supervisely/app/widgets/activity_feed/__init__.py +0 -0
  36. supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
  37. supervisely/app/widgets/activity_feed/style.css +78 -0
  38. supervisely/app/widgets/activity_feed/template.html +22 -0
  39. supervisely/app/widgets/card/card.py +20 -0
  40. supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
  41. supervisely/app/widgets/classes_list_selector/template.html +60 -93
  42. supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
  43. supervisely/app/widgets/classes_table/classes_table.py +1 -0
  44. supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
  45. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
  46. supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
  47. supervisely/app/widgets/fast_table/fast_table.py +184 -60
  48. supervisely/app/widgets/fast_table/template.html +1 -1
  49. supervisely/app/widgets/heatmap/__init__.py +0 -0
  50. supervisely/app/widgets/heatmap/heatmap.py +564 -0
  51. supervisely/app/widgets/heatmap/script.js +533 -0
  52. supervisely/app/widgets/heatmap/style.css +233 -0
  53. supervisely/app/widgets/heatmap/template.html +21 -0
  54. supervisely/app/widgets/modal/__init__.py +0 -0
  55. supervisely/app/widgets/modal/modal.py +198 -0
  56. supervisely/app/widgets/modal/template.html +10 -0
  57. supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
  58. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  59. supervisely/app/widgets/radio_tabs/template.html +1 -0
  60. supervisely/app/widgets/select/select.py +6 -3
  61. supervisely/app/widgets/select_class/__init__.py +0 -0
  62. supervisely/app/widgets/select_class/select_class.py +363 -0
  63. supervisely/app/widgets/select_class/template.html +50 -0
  64. supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
  65. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
  66. supervisely/app/widgets/select_tag/__init__.py +0 -0
  67. supervisely/app/widgets/select_tag/select_tag.py +352 -0
  68. supervisely/app/widgets/select_tag/template.html +64 -0
  69. supervisely/app/widgets/select_team/select_team.py +37 -4
  70. supervisely/app/widgets/select_team/template.html +4 -5
  71. supervisely/app/widgets/select_user/__init__.py +0 -0
  72. supervisely/app/widgets/select_user/select_user.py +270 -0
  73. supervisely/app/widgets/select_user/template.html +13 -0
  74. supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
  75. supervisely/app/widgets/select_workspace/template.html +9 -12
  76. supervisely/app/widgets/table/table.py +68 -13
  77. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  78. supervisely/aug/aug.py +6 -2
  79. supervisely/convert/base_converter.py +1 -0
  80. supervisely/convert/converter.py +2 -2
  81. supervisely/convert/image/image_converter.py +3 -1
  82. supervisely/convert/image/image_helper.py +48 -4
  83. supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
  84. supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
  85. supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
  86. supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
  87. supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
  88. supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
  89. supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
  90. supervisely/convert/pointcloud/las/las_converter.py +13 -1
  91. supervisely/convert/pointcloud/las/las_helper.py +110 -11
  92. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
  93. supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
  94. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
  95. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
  96. supervisely/convert/video/__init__.py +1 -0
  97. supervisely/convert/video/multi_view/__init__.py +0 -0
  98. supervisely/convert/video/multi_view/multi_view.py +543 -0
  99. supervisely/convert/video/sly/sly_video_converter.py +359 -3
  100. supervisely/convert/video/video_converter.py +22 -2
  101. supervisely/convert/volume/dicom/dicom_converter.py +13 -5
  102. supervisely/convert/volume/dicom/dicom_helper.py +30 -18
  103. supervisely/geometry/constants.py +1 -0
  104. supervisely/geometry/geometry.py +4 -0
  105. supervisely/geometry/helpers.py +5 -1
  106. supervisely/geometry/oriented_bbox.py +676 -0
  107. supervisely/geometry/rectangle.py +2 -1
  108. supervisely/io/env.py +76 -1
  109. supervisely/io/fs.py +21 -0
  110. supervisely/nn/benchmark/base_evaluator.py +104 -11
  111. supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
  112. supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
  113. supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
  114. supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
  115. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
  116. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
  117. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
  118. supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
  119. supervisely/nn/inference/cache.py +43 -18
  120. supervisely/nn/inference/gui/serving_gui_template.py +5 -2
  121. supervisely/nn/inference/inference.py +795 -199
  122. supervisely/nn/inference/inference_request.py +42 -9
  123. supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
  124. supervisely/nn/inference/predict_app/gui/gui.py +676 -488
  125. supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
  126. supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
  127. supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
  128. supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
  129. supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
  130. supervisely/nn/inference/predict_app/gui/utils.py +236 -119
  131. supervisely/nn/inference/predict_app/predict_app.py +2 -2
  132. supervisely/nn/inference/session.py +43 -35
  133. supervisely/nn/inference/tracking/bbox_tracking.py +113 -34
  134. supervisely/nn/inference/tracking/tracker_interface.py +7 -2
  135. supervisely/nn/inference/uploader.py +139 -12
  136. supervisely/nn/live_training/__init__.py +7 -0
  137. supervisely/nn/live_training/api_server.py +111 -0
  138. supervisely/nn/live_training/artifacts_utils.py +243 -0
  139. supervisely/nn/live_training/checkpoint_utils.py +229 -0
  140. supervisely/nn/live_training/dynamic_sampler.py +44 -0
  141. supervisely/nn/live_training/helpers.py +14 -0
  142. supervisely/nn/live_training/incremental_dataset.py +146 -0
  143. supervisely/nn/live_training/live_training.py +497 -0
  144. supervisely/nn/live_training/loss_plateau_detector.py +111 -0
  145. supervisely/nn/live_training/request_queue.py +52 -0
  146. supervisely/nn/model/model_api.py +9 -0
  147. supervisely/nn/prediction_dto.py +12 -1
  148. supervisely/nn/tracker/base_tracker.py +11 -1
  149. supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
  150. supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
  151. supervisely/nn/tracker/botsort_tracker.py +94 -65
  152. supervisely/nn/tracker/visualize.py +87 -90
  153. supervisely/nn/training/gui/classes_selector.py +16 -1
  154. supervisely/nn/training/train_app.py +28 -29
  155. supervisely/project/data_version.py +115 -51
  156. supervisely/project/download.py +1 -1
  157. supervisely/project/pointcloud_episode_project.py +37 -8
  158. supervisely/project/pointcloud_project.py +30 -2
  159. supervisely/project/project.py +14 -2
  160. supervisely/project/project_meta.py +27 -1
  161. supervisely/project/project_settings.py +32 -18
  162. supervisely/project/versioning/__init__.py +1 -0
  163. supervisely/project/versioning/common.py +20 -0
  164. supervisely/project/versioning/schema_fields.py +35 -0
  165. supervisely/project/versioning/video_schema.py +221 -0
  166. supervisely/project/versioning/volume_schema.py +87 -0
  167. supervisely/project/video_project.py +717 -15
  168. supervisely/project/volume_project.py +623 -5
  169. supervisely/template/experiment/experiment.html.jinja +4 -4
  170. supervisely/template/experiment/experiment_generator.py +14 -21
  171. supervisely/template/live_training/__init__.py +0 -0
  172. supervisely/template/live_training/header.html.jinja +96 -0
  173. supervisely/template/live_training/live_training.html.jinja +51 -0
  174. supervisely/template/live_training/live_training_generator.py +464 -0
  175. supervisely/template/live_training/sly-style.css +402 -0
  176. supervisely/template/live_training/template.html.jinja +18 -0
  177. supervisely/versions.json +28 -26
  178. supervisely/video/sampling.py +39 -20
  179. supervisely/video/video.py +40 -11
  180. supervisely/video_annotation/video_object.py +29 -4
  181. supervisely/volume/stl_converter.py +2 -0
  182. supervisely/worker_api/agent_rpc.py +24 -1
  183. supervisely/worker_api/rpc_servicer.py +31 -7
  184. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/METADATA +56 -39
  185. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/RECORD +189 -142
  186. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
  187. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
  188. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
  189. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/top_level.txt +0 -0
supervisely/__init__.py CHANGED
@@ -8,6 +8,22 @@ try:
8
8
  except TypeError as e:
9
9
  __version__ = "development"
10
10
 
11
+
12
+ class _ApiProtoNotAvailable:
13
+ """Placeholder class that raises an error when accessing any attribute"""
14
+
15
+ def __getattr__(self, name):
16
+ from supervisely.app.v1.constants import PROTOBUF_REQUIRED_ERROR
17
+
18
+ raise ImportError(f"Cannot access `api_proto.{name}` : " + PROTOBUF_REQUIRED_ERROR)
19
+
20
+ def __bool__(self):
21
+ return False
22
+
23
+ def __repr__(self):
24
+ return "<api_proto: not available - install supervisely[agent] to enable>"
25
+
26
+
11
27
  from supervisely.sly_logger import (
12
28
  logger,
13
29
  ServiceType,
@@ -90,6 +106,7 @@ from supervisely.geometry.graph import GraphNodes, Node
90
106
  from supervisely.geometry.multichannel_bitmap import MultichannelBitmap
91
107
  from supervisely.geometry.alpha_mask import AlphaMask
92
108
  from supervisely.geometry.cuboid_2d import Cuboid2d
109
+ from supervisely.geometry.oriented_bbox import OrientedBBox
93
110
 
94
111
  from supervisely.geometry.helpers import geometry_to_bitmap
95
112
  from supervisely.geometry.helpers import deserialize_geometry
@@ -112,7 +129,14 @@ from supervisely.worker_api.chunking import (
112
129
  ChunkedFileWriter,
113
130
  ChunkedFileReader,
114
131
  )
115
- import supervisely.worker_proto.worker_api_pb2 as api_proto
132
+
133
+ # Global import of api_proto works only if protobuf is installed and compatible
134
+ # Otherwise, we use a placeholder that raises an error when accessed
135
+ try:
136
+ import supervisely.worker_proto.worker_api_pb2 as api_proto
137
+ except Exception:
138
+ api_proto = _ApiProtoNotAvailable()
139
+
116
140
 
117
141
  from supervisely.api.api import Api, UserSession, ApiContext
118
142
  from supervisely.api import api
@@ -26,6 +26,7 @@ from supervisely.geometry.bitmap import Bitmap
26
26
  from supervisely.geometry.geometry import Geometry
27
27
  from supervisely.geometry.image_rotator import ImageRotator
28
28
  from supervisely.geometry.multichannel_bitmap import MultichannelBitmap
29
+ from supervisely.geometry.oriented_bbox import OrientedBBox
29
30
  from supervisely.geometry.polygon import Polygon
30
31
  from supervisely.geometry.rectangle import Rectangle
31
32
  from supervisely.imaging import font as sly_font
@@ -551,7 +552,7 @@ class Annotation:
551
552
  :return: list of the Label class objects
552
553
  """
553
554
  for label in labels:
554
- if self.img_size.count(None) == 0:
555
+ if self.img_size.count(None) == 0 and not isinstance(label.geometry, OrientedBBox):
555
556
  # image has resolution in DB
556
557
  canvas_rect = Rectangle.from_size(self.img_size)
557
558
  try:
@@ -566,6 +567,7 @@ class Annotation:
566
567
  else:
567
568
  # image was uploaded by link and does not have resolution in DB
568
569
  # add label without normalization and validation
570
+ # OrientedBBox geometries can be outside of image bounds
569
571
  dest.append(label)
570
572
 
571
573
  def add_label(self, label: Label) -> Annotation:
@@ -1417,7 +1419,7 @@ class Annotation:
1417
1419
  if draw_tags is True:
1418
1420
  tags_font = self._get_font()
1419
1421
  for label in self._labels:
1420
- if not fill_rectangles and isinstance(label.geometry, Rectangle):
1422
+ if not fill_rectangles and isinstance(label.geometry, (Rectangle, OrientedBBox)):
1421
1423
  label.draw_contour(
1422
1424
  bitmap,
1423
1425
  color=color,
@@ -2962,6 +2964,8 @@ class Annotation:
2962
2964
  for label in data[AnnotationJsonFields.LABELS]:
2963
2965
  if label[LabelJsonFields.GEOMETRY_TYPE] == Rectangle.geometry_name():
2964
2966
  label = Rectangle._to_pixel_coordinate_system_json(label, image_size)
2967
+ elif label[LabelJsonFields.GEOMETRY_TYPE] == OrientedBBox.geometry_name():
2968
+ label = OrientedBBox._to_pixel_coordinate_system_json(label, image_size)
2965
2969
  else:
2966
2970
  label = Geometry._to_pixel_coordinate_system_json(label, image_size)
2967
2971
  new_labels.append(label)
@@ -2988,6 +2992,8 @@ class Annotation:
2988
2992
  for label in data[AnnotationJsonFields.LABELS]:
2989
2993
  if label[LabelJsonFields.GEOMETRY_TYPE] == Rectangle.geometry_name():
2990
2994
  label = Rectangle._to_subpixel_coordinate_system_json(label)
2995
+ elif label[LabelJsonFields.GEOMETRY_TYPE] == OrientedBBox.geometry_name():
2996
+ label = OrientedBBox._to_subpixel_coordinate_system_json(label)
2991
2997
  else:
2992
2998
  label = Geometry._to_subpixel_coordinate_system_json(label)
2993
2999
  new_labels.append(label)
@@ -1,22 +1,22 @@
1
1
  # coding: utf-8
2
+ from supervisely.geometry.alpha_mask import AlphaMask
3
+ from supervisely.geometry.any_geometry import AnyGeometry
2
4
  from supervisely.geometry.bitmap import Bitmap
3
- from supervisely.geometry.mask_3d import Mask3D
5
+ from supervisely.geometry.closed_surface_mesh import ClosedSurfaceMesh
4
6
  from supervisely.geometry.cuboid import Cuboid
7
+ from supervisely.geometry.cuboid_2d import Cuboid2d
8
+ from supervisely.geometry.cuboid_3d import Cuboid3d
9
+ from supervisely.geometry.graph import GraphNodes
10
+ from supervisely.geometry.mask_3d import Mask3D
11
+ from supervisely.geometry.multichannel_bitmap import MultichannelBitmap
12
+ from supervisely.geometry.oriented_bbox import OrientedBBox
5
13
  from supervisely.geometry.point import Point
14
+ from supervisely.geometry.point_3d import Point3d
15
+ from supervisely.geometry.pointcloud import Pointcloud
6
16
  from supervisely.geometry.polygon import Polygon
7
17
  from supervisely.geometry.polyline import Polyline
8
- from supervisely.geometry.rectangle import Rectangle
9
- from supervisely.geometry.graph import GraphNodes
10
- from supervisely.geometry.any_geometry import AnyGeometry
11
- from supervisely.geometry.cuboid_3d import Cuboid3d
12
- from supervisely.geometry.pointcloud import Pointcloud
13
- from supervisely.geometry.point_3d import Point3d
14
- from supervisely.geometry.multichannel_bitmap import MultichannelBitmap
15
- from supervisely.geometry.closed_surface_mesh import ClosedSurfaceMesh
16
- from supervisely.geometry.alpha_mask import AlphaMask
17
- from supervisely.geometry.cuboid_2d import Cuboid2d
18
18
  from supervisely.geometry.polyline_3d import Polyline3D
19
-
19
+ from supervisely.geometry.rectangle import Rectangle
20
20
 
21
21
  _INPUT_GEOMETRIES = [
22
22
  Bitmap,
@@ -36,6 +36,7 @@ _INPUT_GEOMETRIES = [
36
36
  AlphaMask,
37
37
  Cuboid2d,
38
38
  Polyline3D,
39
+ OrientedBBox,
39
40
  ]
40
41
  _JSON_SHAPE_TO_GEOMETRY_TYPE = {
41
42
  geometry.geometry_name(): geometry for geometry in _INPUT_GEOMETRIES
@@ -869,9 +869,12 @@ class AnnotationApi(ModuleApi):
869
869
  )
870
870
 
871
871
  # use context to avoid redundant API calls
872
- dataset_id = self._api.image.get_info_by_id(
873
- img_ids[0], force_metadata_for_links=False
874
- ).dataset_id
872
+ image_info = self._api.image.get_info_by_id(img_ids[0], force_metadata_for_links=False)
873
+ if image_info is None:
874
+ raise RuntimeError(
875
+ f"Cannot get dataset ID from image info. Image with ID={img_ids[0]} not found."
876
+ )
877
+ dataset_id = image_info.dataset_id
875
878
  context = self._api.optimization_context
876
879
  context_dataset_id = context.get("dataset_id")
877
880
  project_id = context.get("project_id")
supervisely/api/api.py CHANGED
@@ -42,6 +42,7 @@ import supervisely.api.dataset_api as dataset_api
42
42
  import supervisely.api.entities_collection_api as entities_collection_api
43
43
  import supervisely.api.file_api as file_api
44
44
  import supervisely.api.github_api as github_api
45
+ import supervisely.api.guides_api as guides_api
45
46
  import supervisely.api.image_annotation_tool_api as image_annotation_tool_api
46
47
  import supervisely.api.image_api as image_api
47
48
  import supervisely.api.import_storage_api as import_stoarge_api
@@ -358,6 +359,7 @@ class Api:
358
359
  self.user = user_api.UserApi(self)
359
360
  self.labeling_job = labeling_job_api.LabelingJobApi(self)
360
361
  self.labeling_queue = labeling_queue_api.LabelingQueueApi(self)
362
+ self.guides = guides_api.GuidesApi(self)
361
363
  self.video = video_api.VideoApi(self)
362
364
  # self.project_class = project_class_api.ProjectClassApi(self)
363
365
  self.object_class = object_class_api.ObjectClassApi(self)
@@ -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,
@@ -1046,3 +1046,31 @@ class FigureApi(RemoveableBulkModuleApi):
1046
1046
  image_ids=image_ids,
1047
1047
  skip_geometry=skip_geometry,
1048
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
+ )