supervisely 6.73.410__py3-none-any.whl → 6.73.470__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.

Files changed (190) hide show
  1. supervisely/__init__.py +136 -1
  2. supervisely/_utils.py +81 -0
  3. supervisely/annotation/json_geometries_map.py +2 -0
  4. supervisely/annotation/label.py +80 -3
  5. supervisely/api/annotation_api.py +9 -9
  6. supervisely/api/api.py +67 -43
  7. supervisely/api/app_api.py +72 -5
  8. supervisely/api/dataset_api.py +108 -33
  9. supervisely/api/entity_annotation/figure_api.py +113 -49
  10. supervisely/api/image_api.py +82 -0
  11. supervisely/api/module_api.py +10 -0
  12. supervisely/api/nn/deploy_api.py +15 -9
  13. supervisely/api/nn/ecosystem_models_api.py +201 -0
  14. supervisely/api/nn/neural_network_api.py +12 -3
  15. supervisely/api/pointcloud/pointcloud_api.py +38 -0
  16. supervisely/api/pointcloud/pointcloud_episode_annotation_api.py +3 -0
  17. supervisely/api/project_api.py +213 -6
  18. supervisely/api/task_api.py +11 -1
  19. supervisely/api/video/video_annotation_api.py +4 -2
  20. supervisely/api/video/video_api.py +79 -1
  21. supervisely/api/video/video_figure_api.py +24 -11
  22. supervisely/api/volume/volume_api.py +38 -0
  23. supervisely/app/__init__.py +1 -1
  24. supervisely/app/content.py +14 -6
  25. supervisely/app/fastapi/__init__.py +1 -0
  26. supervisely/app/fastapi/custom_static_files.py +1 -1
  27. supervisely/app/fastapi/multi_user.py +88 -0
  28. supervisely/app/fastapi/subapp.py +175 -42
  29. supervisely/app/fastapi/templating.py +1 -1
  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 +11 -1
  35. supervisely/app/widgets/agent_selector/template.html +1 -0
  36. supervisely/app/widgets/card/card.py +20 -0
  37. supervisely/app/widgets/dataset_thumbnail/dataset_thumbnail.py +11 -2
  38. supervisely/app/widgets/dataset_thumbnail/template.html +3 -1
  39. supervisely/app/widgets/deploy_model/deploy_model.py +750 -0
  40. supervisely/app/widgets/dialog/dialog.py +12 -0
  41. supervisely/app/widgets/dialog/template.html +2 -1
  42. supervisely/app/widgets/dropdown_checkbox_selector/__init__.py +0 -0
  43. supervisely/app/widgets/dropdown_checkbox_selector/dropdown_checkbox_selector.py +87 -0
  44. supervisely/app/widgets/dropdown_checkbox_selector/template.html +12 -0
  45. supervisely/app/widgets/ecosystem_model_selector/__init__.py +0 -0
  46. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +195 -0
  47. supervisely/app/widgets/experiment_selector/experiment_selector.py +454 -263
  48. supervisely/app/widgets/fast_table/fast_table.py +713 -126
  49. supervisely/app/widgets/fast_table/script.js +492 -95
  50. supervisely/app/widgets/fast_table/style.css +54 -0
  51. supervisely/app/widgets/fast_table/template.html +45 -5
  52. supervisely/app/widgets/heatmap/__init__.py +0 -0
  53. supervisely/app/widgets/heatmap/heatmap.py +523 -0
  54. supervisely/app/widgets/heatmap/script.js +378 -0
  55. supervisely/app/widgets/heatmap/style.css +227 -0
  56. supervisely/app/widgets/heatmap/template.html +21 -0
  57. supervisely/app/widgets/input_tag/input_tag.py +102 -15
  58. supervisely/app/widgets/input_tag_list/__init__.py +0 -0
  59. supervisely/app/widgets/input_tag_list/input_tag_list.py +274 -0
  60. supervisely/app/widgets/input_tag_list/template.html +70 -0
  61. supervisely/app/widgets/radio_table/radio_table.py +10 -2
  62. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  63. supervisely/app/widgets/radio_tabs/template.html +1 -0
  64. supervisely/app/widgets/select/select.py +6 -4
  65. supervisely/app/widgets/select_dataset/select_dataset.py +6 -0
  66. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +83 -7
  67. supervisely/app/widgets/table/table.py +68 -13
  68. supervisely/app/widgets/tabs/tabs.py +22 -6
  69. supervisely/app/widgets/tabs/template.html +5 -1
  70. supervisely/app/widgets/transfer/style.css +3 -0
  71. supervisely/app/widgets/transfer/template.html +3 -1
  72. supervisely/app/widgets/transfer/transfer.py +48 -45
  73. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  74. supervisely/convert/image/csv/csv_converter.py +24 -15
  75. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +43 -41
  76. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +75 -51
  77. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +137 -124
  78. supervisely/convert/video/video_converter.py +2 -2
  79. supervisely/geometry/polyline_3d.py +110 -0
  80. supervisely/io/env.py +161 -1
  81. supervisely/nn/artifacts/__init__.py +1 -1
  82. supervisely/nn/artifacts/artifacts.py +10 -2
  83. supervisely/nn/artifacts/detectron2.py +1 -0
  84. supervisely/nn/artifacts/hrda.py +1 -0
  85. supervisely/nn/artifacts/mmclassification.py +20 -0
  86. supervisely/nn/artifacts/mmdetection.py +5 -3
  87. supervisely/nn/artifacts/mmsegmentation.py +1 -0
  88. supervisely/nn/artifacts/ritm.py +1 -0
  89. supervisely/nn/artifacts/rtdetr.py +1 -0
  90. supervisely/nn/artifacts/unet.py +1 -0
  91. supervisely/nn/artifacts/utils.py +3 -0
  92. supervisely/nn/artifacts/yolov5.py +2 -0
  93. supervisely/nn/artifacts/yolov8.py +1 -0
  94. supervisely/nn/benchmark/semantic_segmentation/metric_provider.py +18 -18
  95. supervisely/nn/experiments.py +9 -0
  96. supervisely/nn/inference/cache.py +37 -17
  97. supervisely/nn/inference/gui/serving_gui_template.py +39 -13
  98. supervisely/nn/inference/inference.py +953 -211
  99. supervisely/nn/inference/inference_request.py +15 -8
  100. supervisely/nn/inference/instance_segmentation/instance_segmentation.py +1 -0
  101. supervisely/nn/inference/object_detection/object_detection.py +1 -0
  102. supervisely/nn/inference/predict_app/__init__.py +0 -0
  103. supervisely/nn/inference/predict_app/gui/__init__.py +0 -0
  104. supervisely/nn/inference/predict_app/gui/classes_selector.py +160 -0
  105. supervisely/nn/inference/predict_app/gui/gui.py +915 -0
  106. supervisely/nn/inference/predict_app/gui/input_selector.py +344 -0
  107. supervisely/nn/inference/predict_app/gui/model_selector.py +77 -0
  108. supervisely/nn/inference/predict_app/gui/output_selector.py +179 -0
  109. supervisely/nn/inference/predict_app/gui/preview.py +93 -0
  110. supervisely/nn/inference/predict_app/gui/settings_selector.py +881 -0
  111. supervisely/nn/inference/predict_app/gui/tags_selector.py +110 -0
  112. supervisely/nn/inference/predict_app/gui/utils.py +399 -0
  113. supervisely/nn/inference/predict_app/predict_app.py +176 -0
  114. supervisely/nn/inference/session.py +47 -39
  115. supervisely/nn/inference/tracking/bbox_tracking.py +5 -1
  116. supervisely/nn/inference/tracking/point_tracking.py +5 -1
  117. supervisely/nn/inference/tracking/tracker_interface.py +4 -0
  118. supervisely/nn/inference/uploader.py +9 -5
  119. supervisely/nn/model/model_api.py +44 -22
  120. supervisely/nn/model/prediction.py +15 -1
  121. supervisely/nn/model/prediction_session.py +70 -14
  122. supervisely/nn/prediction_dto.py +7 -0
  123. supervisely/nn/tracker/__init__.py +6 -8
  124. supervisely/nn/tracker/base_tracker.py +54 -0
  125. supervisely/nn/tracker/botsort/__init__.py +1 -0
  126. supervisely/nn/tracker/botsort/botsort_config.yaml +30 -0
  127. supervisely/nn/tracker/botsort/osnet_reid/__init__.py +0 -0
  128. supervisely/nn/tracker/botsort/osnet_reid/osnet.py +566 -0
  129. supervisely/nn/tracker/botsort/osnet_reid/osnet_reid_interface.py +88 -0
  130. supervisely/nn/tracker/botsort/tracker/__init__.py +0 -0
  131. supervisely/nn/tracker/{bot_sort → botsort/tracker}/basetrack.py +1 -2
  132. supervisely/nn/tracker/{utils → botsort/tracker}/gmc.py +51 -59
  133. supervisely/nn/tracker/{deep_sort/deep_sort → botsort/tracker}/kalman_filter.py +71 -33
  134. supervisely/nn/tracker/botsort/tracker/matching.py +202 -0
  135. supervisely/nn/tracker/{bot_sort/bot_sort.py → botsort/tracker/mc_bot_sort.py} +68 -81
  136. supervisely/nn/tracker/botsort_tracker.py +273 -0
  137. supervisely/nn/tracker/calculate_metrics.py +264 -0
  138. supervisely/nn/tracker/utils.py +273 -0
  139. supervisely/nn/tracker/visualize.py +520 -0
  140. supervisely/nn/training/gui/gui.py +152 -49
  141. supervisely/nn/training/gui/hyperparameters_selector.py +1 -1
  142. supervisely/nn/training/gui/model_selector.py +8 -6
  143. supervisely/nn/training/gui/train_val_splits_selector.py +144 -71
  144. supervisely/nn/training/gui/training_artifacts.py +3 -1
  145. supervisely/nn/training/train_app.py +225 -46
  146. supervisely/project/pointcloud_episode_project.py +12 -8
  147. supervisely/project/pointcloud_project.py +12 -8
  148. supervisely/project/project.py +221 -75
  149. supervisely/template/experiment/experiment.html.jinja +105 -55
  150. supervisely/template/experiment/experiment_generator.py +258 -112
  151. supervisely/template/experiment/header.html.jinja +31 -13
  152. supervisely/template/experiment/sly-style.css +7 -2
  153. supervisely/versions.json +3 -1
  154. supervisely/video/sampling.py +42 -20
  155. supervisely/video/video.py +41 -12
  156. supervisely/video_annotation/video_figure.py +38 -4
  157. supervisely/volume/stl_converter.py +2 -0
  158. supervisely/worker_api/agent_rpc.py +24 -1
  159. supervisely/worker_api/rpc_servicer.py +31 -7
  160. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/METADATA +22 -14
  161. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/RECORD +167 -148
  162. supervisely_lib/__init__.py +6 -1
  163. supervisely/app/widgets/experiment_selector/style.css +0 -27
  164. supervisely/app/widgets/experiment_selector/template.html +0 -61
  165. supervisely/nn/tracker/bot_sort/__init__.py +0 -21
  166. supervisely/nn/tracker/bot_sort/fast_reid_interface.py +0 -152
  167. supervisely/nn/tracker/bot_sort/matching.py +0 -127
  168. supervisely/nn/tracker/bot_sort/sly_tracker.py +0 -401
  169. supervisely/nn/tracker/deep_sort/__init__.py +0 -6
  170. supervisely/nn/tracker/deep_sort/deep_sort/__init__.py +0 -1
  171. supervisely/nn/tracker/deep_sort/deep_sort/detection.py +0 -49
  172. supervisely/nn/tracker/deep_sort/deep_sort/iou_matching.py +0 -81
  173. supervisely/nn/tracker/deep_sort/deep_sort/linear_assignment.py +0 -202
  174. supervisely/nn/tracker/deep_sort/deep_sort/nn_matching.py +0 -176
  175. supervisely/nn/tracker/deep_sort/deep_sort/track.py +0 -166
  176. supervisely/nn/tracker/deep_sort/deep_sort/tracker.py +0 -145
  177. supervisely/nn/tracker/deep_sort/deep_sort.py +0 -301
  178. supervisely/nn/tracker/deep_sort/generate_clip_detections.py +0 -90
  179. supervisely/nn/tracker/deep_sort/preprocessing.py +0 -70
  180. supervisely/nn/tracker/deep_sort/sly_tracker.py +0 -273
  181. supervisely/nn/tracker/tracker.py +0 -285
  182. supervisely/nn/tracker/utils/kalman_filter.py +0 -492
  183. supervisely/nn/tracking/__init__.py +0 -1
  184. supervisely/nn/tracking/boxmot.py +0 -114
  185. supervisely/nn/tracking/tracking.py +0 -24
  186. /supervisely/{nn/tracker/utils → app/widgets/deploy_model}/__init__.py +0 -0
  187. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/LICENSE +0 -0
  188. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/WHEEL +0 -0
  189. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/entry_points.txt +0 -0
  190. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/top_level.txt +0 -0
@@ -5,6 +5,7 @@ from supervisely.api.api import Api
5
5
  from supervisely.app.widgets import Widget
6
6
  from supervisely.app.widgets.checkbox.checkbox import Checkbox
7
7
  from supervisely.app.widgets.container.container import Container
8
+ from supervisely.app.widgets.field.field import Field
8
9
  from supervisely.app.widgets.select.select import Select
9
10
  from supervisely.app.widgets.tree_select.tree_select import TreeSelect
10
11
  from supervisely.project.project_type import ProjectType
@@ -97,6 +98,7 @@ class SelectDatasetTree(Widget):
97
98
  widget_id: Union[str, None] = None,
98
99
  show_select_all_datasets_checkbox: bool = True,
99
100
  width: int = 193,
101
+ show_selectors_labels: bool = False,
100
102
  ):
101
103
  self._api = Api.from_env()
102
104
 
@@ -114,11 +116,29 @@ class SelectDatasetTree(Widget):
114
116
  # Using environment variables to set the default values if they are not provided.
115
117
  self._project_id = project_id or env.project_id(raise_not_found=False)
116
118
  self._dataset_id = default_id or env.dataset_id(raise_not_found=False)
119
+ if self._project_id:
120
+ project_info = self._api.project.get_info_by_id(self._project_id)
121
+ if allowed_project_types is not None:
122
+ allowed_values = []
123
+ if not isinstance(allowed_project_types, list):
124
+ allowed_project_types = [allowed_project_types]
125
+
126
+ for pt in allowed_project_types:
127
+ if isinstance(pt, (ProjectType, str)):
128
+ allowed_values.append(str(pt))
129
+
130
+ if project_info.type not in allowed_values:
131
+ self._project_id = None
117
132
 
118
133
  self._multiselect = multiselect
119
134
  self._compact = compact
120
135
  self._append_to_body = append_to_body
121
136
 
137
+ # User-defined callbacks
138
+ self._team_changed_callbacks = []
139
+ self._workspace_changed_callbacks = []
140
+ self._project_changed_callbacks = []
141
+
122
142
  # Extract values from Enum to match the .type property of the ProjectInfo object.
123
143
  self._project_types = None
124
144
  if allowed_project_types is not None:
@@ -141,6 +161,10 @@ class SelectDatasetTree(Widget):
141
161
  self._select_dataset = None
142
162
  self._width = width
143
163
 
164
+ # Flags
165
+ self._team_is_selectable = team_is_selectable
166
+ self._workspace_is_selectable = workspace_is_selectable
167
+
144
168
  # List of widgets will be used to create a Container.
145
169
  self._widgets = []
146
170
 
@@ -156,6 +180,7 @@ class SelectDatasetTree(Widget):
156
180
  if show_select_all_datasets_checkbox:
157
181
  self._create_select_all_datasets_checkbox(select_all_datasets)
158
182
 
183
+ self._show_selectors_labels = show_selectors_labels
159
184
  # Group the selectors and the dataset selector into a container.
160
185
  self._content = Container(self._widgets)
161
186
  super().__init__(widget_id=widget_id, file_path=__file__)
@@ -165,11 +190,25 @@ class SelectDatasetTree(Widget):
165
190
  for widget in self._widgets:
166
191
  widget.disable()
167
192
 
193
+ if self._select_team is not None:
194
+ if not self._team_is_selectable:
195
+ self._select_team.disable()
196
+ if self._select_workspace is not None:
197
+ if not self._workspace_is_selectable:
198
+ self._select_workspace.disable()
199
+
168
200
  def enable(self) -> None:
169
201
  """Enable the widget in the UI."""
170
202
  for widget in self._widgets:
171
203
  widget.enable()
172
204
 
205
+ if self._select_team is not None:
206
+ if not self._team_is_selectable:
207
+ self._select_team.disable()
208
+ if self._select_workspace is not None:
209
+ if not self._workspace_is_selectable:
210
+ self._select_workspace.disable()
211
+
173
212
  @property
174
213
  def team_id(self) -> int:
175
214
  """The ID of the team selected in the widget.
@@ -290,8 +329,30 @@ class SelectDatasetTree(Widget):
290
329
  """
291
330
  if not self._multiselect:
292
331
  raise ValueError("This method can only be called when multiselect is enabled.")
332
+ self._select_all_datasets_checkbox.uncheck()
293
333
  self._select_dataset.set_selected_by_id(dataset_ids)
294
334
 
335
+ def team_changed(self, func: Callable) -> Callable:
336
+ """Decorator to set the callback function for the team changed event."""
337
+ if self._compact:
338
+ raise ValueError("callback 'team_changed' is not available in compact mode.")
339
+ self._team_changed_callbacks.append(func)
340
+ return func
341
+
342
+ def workspace_changed(self, func: Callable) -> Callable:
343
+ """Decorator to set the callback function for the workspace changed event."""
344
+ if self._compact:
345
+ raise ValueError("callback 'workspace_changed' is not available in compact mode.")
346
+ self._workspace_changed_callbacks.append(func)
347
+ return func
348
+
349
+ def project_changed(self, func: Callable) -> Callable:
350
+ """Decorator to set the callback function for the project changed event."""
351
+ if self._compact:
352
+ raise ValueError("callback 'project_changed' is not available in compact mode.")
353
+ self._project_changed_callbacks.append(func)
354
+ return func
355
+
295
356
  def value_changed(self, func: Callable) -> Callable:
296
357
  """Decorator to set the callback function for the value changed event.
297
358
 
@@ -335,13 +396,13 @@ class SelectDatasetTree(Widget):
335
396
 
336
397
  if checked:
337
398
  self._select_dataset.select_all()
338
- self._select_dataset.hide()
399
+ self._select_dataset_field.hide()
339
400
  else:
340
401
  self._select_dataset.clear_selected()
341
- self._select_dataset.show()
402
+ self._select_dataset_field.show()
342
403
 
343
404
  if select_all_datasets:
344
- self._select_dataset.hide()
405
+ self._select_dataset_field.hide()
345
406
  select_all_datasets_checkbox.check()
346
407
 
347
408
  self._widgets.append(select_all_datasets_checkbox)
@@ -372,9 +433,10 @@ class SelectDatasetTree(Widget):
372
433
  self._select_dataset.set_selected_by_id(self._dataset_id)
373
434
  if select_all_datasets:
374
435
  self._select_dataset.select_all()
436
+ self._select_dataset_field = Field(self._select_dataset, title="Dataset")
375
437
 
376
438
  # Adding the dataset selector to the list of widgets to be added to the container.
377
- self._widgets.append(self._select_dataset)
439
+ self._widgets.append(self._select_dataset_field)
378
440
 
379
441
  def _create_selectors(self, team_is_selectable: bool, workspace_is_selectable: bool):
380
442
  """Create the team, workspace, and project selectors.
@@ -394,6 +456,9 @@ class SelectDatasetTree(Widget):
394
456
  self._select_workspace.set(items=self._get_select_items(team_id=team_id))
395
457
  self._team_id = team_id
396
458
 
459
+ for callback in self._team_changed_callbacks:
460
+ callback(team_id)
461
+
397
462
  def workspace_selector_handler(workspace_id: int):
398
463
  """Handler function for the event when the workspace selector value changes.
399
464
 
@@ -403,6 +468,9 @@ class SelectDatasetTree(Widget):
403
468
  self._select_project.set(items=self._get_select_items(workspace_id=workspace_id))
404
469
  self._workspace_id = workspace_id
405
470
 
471
+ for callback in self._workspace_changed_callbacks:
472
+ callback(workspace_id)
473
+
406
474
  def project_selector_handler(project_id: int):
407
475
  """Handler function for the event when the project selector value changes.
408
476
 
@@ -417,7 +485,10 @@ class SelectDatasetTree(Widget):
417
485
  and self._select_all_datasets_checkbox.is_checked()
418
486
  ):
419
487
  self._select_dataset.select_all()
420
- self._select_dataset.hide()
488
+ self._select_dataset_field.hide()
489
+
490
+ for callback in self._project_changed_callbacks:
491
+ callback(project_id)
421
492
 
422
493
  self._select_team = Select(
423
494
  items=self._get_select_items(),
@@ -428,6 +499,7 @@ class SelectDatasetTree(Widget):
428
499
  self._select_team.set_value(self._team_id)
429
500
  if not team_is_selectable:
430
501
  self._select_team.disable()
502
+ self._select_team_field = Field(self._select_team, title="Team")
431
503
 
432
504
  self._select_workspace = Select(
433
505
  items=self._get_select_items(team_id=self._team_id),
@@ -438,6 +510,7 @@ class SelectDatasetTree(Widget):
438
510
  self._select_workspace.set_value(self._workspace_id)
439
511
  if not workspace_is_selectable:
440
512
  self._select_workspace.disable()
513
+ self._select_workspace_field = Field(self._select_workspace, title="Workspace")
441
514
 
442
515
  self._select_project = Select(
443
516
  items=self._get_select_items(workspace_id=self._workspace_id),
@@ -446,14 +519,17 @@ class SelectDatasetTree(Widget):
446
519
  width_px=self._width,
447
520
  )
448
521
  self._select_project.set_value(self._project_id)
522
+ self._select_project_field = Field(self._select_project, title="Project")
449
523
 
450
- # Register the event handlers.
524
+ # Register the event handlers._select_project
451
525
  self._select_team.value_changed(team_selector_handler)
452
526
  self._select_workspace.value_changed(workspace_selector_handler)
453
527
  self._select_project.value_changed(project_selector_handler)
454
528
 
455
529
  # Adding widgets to the list, so they can be added to the container.
456
- self._widgets.extend([self._select_team, self._select_workspace, self._select_project])
530
+ self._widgets.extend(
531
+ [self._select_team_field, self._select_workspace_field, self._select_project_field]
532
+ )
457
533
 
458
534
  def _get_select_items(self, **kwargs) -> List[Select.Item]:
459
535
  """Get the list of items for the team, workspace, and project selectors.
@@ -1,3 +1,5 @@
1
+ # isort: skip_file
2
+
1
3
  import copy
2
4
  import io
3
5
 
@@ -54,9 +56,8 @@ class PackerUnpacker:
54
56
 
55
57
  @staticmethod
56
58
  def pandas_unpacker(data: pd.DataFrame):
57
- data = data.replace({np.nan: None})
58
- # data = data.astype(object).replace(np.nan, "-") # TODO: replace None later
59
-
59
+ # Keep None/NaN values in source data, don't replace them
60
+ # They will be converted to "" only when sending to frontend
60
61
  unpacked_data = {
61
62
  "columns": data.columns.to_list(),
62
63
  "data": data.values.tolist(),
@@ -169,9 +170,35 @@ class Table(Widget):
169
170
 
170
171
  super().__init__(widget_id=widget_id, file_path=__file__)
171
172
 
173
+ def _prepare_data_for_frontend(self, data_dict):
174
+ """Convert None and NaN values to empty strings for frontend display.
175
+ This preserves the original None/NaN values in _parsed_data.
176
+ """
177
+ import math
178
+
179
+ display_data = copy.deepcopy(data_dict)
180
+
181
+ # Convert None/NaN in data rows
182
+ for row in display_data.get("data", []):
183
+ for i in range(len(row)):
184
+ value = row[i]
185
+ # Check for None or NaN (NaN is a float that doesn't equal itself)
186
+ if value is None or (isinstance(value, float) and math.isnan(value)):
187
+ row[i] = ""
188
+
189
+ # Convert None/NaN in summary row if present
190
+ if "summaryRow" in display_data and display_data["summaryRow"] is not None:
191
+ summary_row = display_data["summaryRow"]
192
+ for i in range(len(summary_row)):
193
+ value = summary_row[i]
194
+ if value is None or (isinstance(value, float) and math.isnan(value)):
195
+ summary_row[i] = ""
196
+
197
+ return display_data
198
+
172
199
  def get_json_data(self):
173
200
  return {
174
- "table_data": self._parsed_data,
201
+ "table_data": self._prepare_data_for_frontend(self._parsed_data),
175
202
  "table_options": {
176
203
  "perPage": self._per_page,
177
204
  "pageSizes": self._page_sizes,
@@ -255,13 +282,17 @@ class Table(Widget):
255
282
 
256
283
  def read_json(self, value: dict) -> None:
257
284
  self._update_table_data(input_data=value)
258
- DataJson()[self.widget_id]["table_data"] = self._parsed_data
285
+ DataJson()[self.widget_id]["table_data"] = self._prepare_data_for_frontend(
286
+ self._parsed_data
287
+ )
259
288
  DataJson().send_changes()
260
289
  self.clear_selection()
261
290
 
262
291
  def read_pandas(self, value: pd.DataFrame) -> None:
263
292
  self._update_table_data(input_data=value)
264
- DataJson()[self.widget_id]["table_data"] = self._parsed_data
293
+ DataJson()[self.widget_id]["table_data"] = self._prepare_data_for_frontend(
294
+ self._parsed_data
295
+ )
265
296
  DataJson().send_changes()
266
297
  self.clear_selection()
267
298
 
@@ -272,7 +303,9 @@ class Table(Widget):
272
303
  index = len(table_data) if index > len(table_data) or index < 0 else index
273
304
 
274
305
  self._parsed_data["data"].insert(index, data)
275
- DataJson()[self.widget_id]["table_data"] = self._parsed_data
306
+ DataJson()[self.widget_id]["table_data"] = self._prepare_data_for_frontend(
307
+ self._parsed_data
308
+ )
276
309
  DataJson().send_changes()
277
310
 
278
311
  def pop_row(self, index=-1):
@@ -284,7 +317,9 @@ class Table(Widget):
284
317
 
285
318
  if len(self._parsed_data["data"]) != 0:
286
319
  popped_row = self._parsed_data["data"].pop(index)
287
- DataJson()[self.widget_id]["table_data"] = self._parsed_data
320
+ DataJson()[self.widget_id]["table_data"] = self._prepare_data_for_frontend(
321
+ self._parsed_data
322
+ )
288
323
  DataJson().send_changes()
289
324
  return popped_row
290
325
 
@@ -382,11 +417,27 @@ class Table(Widget):
382
417
  StateJson()[self.widget_id]["selected_row"] = {}
383
418
  StateJson().send_changes()
384
419
 
420
+ @staticmethod
421
+ def _values_equal(val1, val2):
422
+ """Compare two values, handling NaN specially."""
423
+ import math
424
+
425
+ # Check if both are NaN
426
+ is_nan1 = isinstance(val1, float) and math.isnan(val1)
427
+ is_nan2 = isinstance(val2, float) and math.isnan(val2)
428
+ if is_nan1 and is_nan2:
429
+ return True
430
+ # Check if both are None
431
+ if val1 is None and val2 is None:
432
+ return True
433
+ # Regular comparison
434
+ return val1 == val2
435
+
385
436
  def delete_row(self, key_column_name, key_cell_value):
386
437
  col_index = self._parsed_data["columns"].index(key_column_name)
387
438
  row_indices = []
388
439
  for idx, row in enumerate(self._parsed_data["data"]):
389
- if row[col_index] == key_cell_value:
440
+ if self._values_equal(row[col_index], key_cell_value):
390
441
  row_indices.append(idx)
391
442
  if len(row_indices) == 0:
392
443
  raise ValueError('Column "{key_column_name}" does not have value "{key_cell_value}"')
@@ -400,7 +451,7 @@ class Table(Widget):
400
451
  key_col_index = self._parsed_data["columns"].index(key_column_name)
401
452
  row_indices = []
402
453
  for idx, row in enumerate(self._parsed_data["data"]):
403
- if row[key_col_index] == key_cell_value:
454
+ if self._values_equal(row[key_col_index], key_cell_value):
404
455
  row_indices.append(idx)
405
456
  if len(row_indices) == 0:
406
457
  raise ValueError('Column "{key_column_name}" does not have value "{key_cell_value}"')
@@ -411,20 +462,24 @@ class Table(Widget):
411
462
 
412
463
  col_index = self._parsed_data["columns"].index(column_name)
413
464
  self._parsed_data["data"][row_indices[0]][col_index] = new_value
414
- DataJson()[self.widget_id]["table_data"] = self._parsed_data
465
+ DataJson()[self.widget_id]["table_data"] = self._prepare_data_for_frontend(
466
+ self._parsed_data
467
+ )
415
468
  DataJson().send_changes()
416
469
 
417
470
  def update_matching_cells(self, key_column_name, key_cell_value, column_name, new_value):
418
471
  key_col_index = self._parsed_data["columns"].index(key_column_name)
419
472
  row_indices = []
420
473
  for idx, row in enumerate(self._parsed_data["data"]):
421
- if row[key_col_index] == key_cell_value:
474
+ if self._values_equal(row[key_col_index], key_cell_value):
422
475
  row_indices.append(idx)
423
476
 
424
477
  col_index = self._parsed_data["columns"].index(column_name)
425
478
  for row_idx in row_indices:
426
479
  self._parsed_data["data"][row_idx][col_index] = new_value
427
- DataJson()[self.widget_id]["table_data"] = self._parsed_data
480
+ DataJson()[self.widget_id]["table_data"] = self._prepare_data_for_frontend(
481
+ self._parsed_data
482
+ )
428
483
  DataJson().send_changes()
429
484
 
430
485
  def sort(self, column_id: int = None, direction: Optional[Literal["asc", "desc"]] = None):
@@ -1,9 +1,10 @@
1
- from typing import List, Optional, Dict
2
- from supervisely.app import StateJson
3
- from supervisely.app.widgets import Widget
4
1
  import traceback
5
- from supervisely import logger
2
+ from typing import Dict, List, Optional
6
3
 
4
+ from supervisely import logger
5
+ from supervisely.app import StateJson
6
+ from supervisely.app.content import DataJson
7
+ from supervisely.app.widgets import Widget
7
8
 
8
9
  try:
9
10
  from typing import Literal
@@ -34,7 +35,7 @@ class Tabs(Widget):
34
35
  raise ValueError("You can specify up to 10 tabs.")
35
36
  if len(set(labels)) != len(labels):
36
37
  raise ValueError("All of tab labels should be unique.")
37
- self._items = []
38
+ self._items: List[Tabs.TabPane] = []
38
39
  for label, widget in zip(labels, contents):
39
40
  self._items.append(Tabs.TabPane(label=label, content=widget))
40
41
  self._value = labels[0]
@@ -43,7 +44,10 @@ class Tabs(Widget):
43
44
  super().__init__(widget_id=widget_id, file_path=__file__)
44
45
 
45
46
  def get_json_data(self) -> Dict:
46
- return {"type": self._type}
47
+ return {
48
+ "type": self._type,
49
+ "tabsOptions": {item.name: {"disabled": False} for item in self._items},
50
+ }
47
51
 
48
52
  def get_json_state(self) -> Dict:
49
53
  return {"value": self._value}
@@ -56,6 +60,18 @@ class Tabs(Widget):
56
60
  def get_active_tab(self) -> str:
57
61
  return StateJson()[self.widget_id]["value"]
58
62
 
63
+ def disable_tab(self, tab_name: str):
64
+ if tab_name not in [item.name for item in self._items]:
65
+ raise ValueError(f"Tab with name '{tab_name}' does not exist.")
66
+ DataJson()[self.widget_id]["tabsOptions"][tab_name]["disabled"] = True
67
+ DataJson().send_changes()
68
+
69
+ def enable_tab(self, tab_name: str):
70
+ if tab_name not in [item.name for item in self._items]:
71
+ raise ValueError(f"Tab with name '{tab_name}' does not exist.")
72
+ DataJson()[self.widget_id]["tabsOptions"][tab_name]["disabled"] = False
73
+ DataJson().send_changes()
74
+
59
75
  def click(self, func):
60
76
  route_path = self.get_route_path(Tabs.Routes.CLICK)
61
77
  server = self._sly_app.get_server()
@@ -11,7 +11,11 @@
11
11
  %}
12
12
  >
13
13
  {% for tab_pane in widget._items %}
14
- <el-tab-pane label="{{{tab_pane.label}}}" name="{{{tab_pane.name}}}">
14
+ <el-tab-pane
15
+ label="{{{tab_pane.label}}}"
16
+ name="{{{tab_pane.name}}}"
17
+ :disabled="data.{{{widget.widget_id}}}.tabsOptions['{{{tab_pane.name}}}'].disabled"
18
+ >
15
19
  {{{ tab_pane.content }}}
16
20
  </el-tab-pane>
17
21
  {% endfor %}
@@ -0,0 +1,3 @@
1
+ .wide-transfer .el-transfer-panel {
2
+ width: var(--panel-width, 150px);
3
+ }
@@ -1,4 +1,6 @@
1
- <div>
1
+ <link rel="stylesheet" href="./sly/css/app/widgets/transfer/style.css" />
2
+
3
+ <div class="wide-transfer" style="--panel-width: {{{widget._width}}}px;">
2
4
  <el-transfer
3
5
  {% if widget._filterable is true %}
4
6
  filterable