supervisely 6.73.456__py3-none-any.whl → 6.73.458__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 (41) hide show
  1. supervisely/__init__.py +24 -1
  2. supervisely/api/image_api.py +4 -0
  3. supervisely/api/video/video_annotation_api.py +4 -2
  4. supervisely/api/video/video_api.py +41 -1
  5. supervisely/app/v1/app_service.py +18 -2
  6. supervisely/app/v1/constants.py +7 -1
  7. supervisely/app/widgets/card/card.py +20 -0
  8. supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
  9. supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
  10. supervisely/app/widgets/fast_table/fast_table.py +45 -11
  11. supervisely/app/widgets/fast_table/template.html +1 -1
  12. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  13. supervisely/app/widgets/radio_tabs/template.html +1 -0
  14. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +63 -7
  15. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  16. supervisely/nn/inference/cache.py +2 -2
  17. supervisely/nn/inference/inference.py +364 -73
  18. supervisely/nn/inference/inference_request.py +3 -2
  19. supervisely/nn/inference/predict_app/gui/classes_selector.py +81 -12
  20. supervisely/nn/inference/predict_app/gui/gui.py +676 -488
  21. supervisely/nn/inference/predict_app/gui/input_selector.py +178 -25
  22. supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
  23. supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
  24. supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
  25. supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
  26. supervisely/nn/inference/predict_app/gui/utils.py +236 -119
  27. supervisely/nn/inference/predict_app/predict_app.py +2 -2
  28. supervisely/nn/model/model_api.py +9 -0
  29. supervisely/nn/tracker/base_tracker.py +11 -1
  30. supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
  31. supervisely/nn/tracker/botsort_tracker.py +14 -7
  32. supervisely/nn/tracker/visualize.py +70 -72
  33. supervisely/video/video.py +15 -1
  34. supervisely/worker_api/agent_rpc.py +24 -1
  35. supervisely/worker_api/rpc_servicer.py +31 -7
  36. {supervisely-6.73.456.dist-info → supervisely-6.73.458.dist-info}/METADATA +3 -2
  37. {supervisely-6.73.456.dist-info → supervisely-6.73.458.dist-info}/RECORD +41 -41
  38. {supervisely-6.73.456.dist-info → supervisely-6.73.458.dist-info}/LICENSE +0 -0
  39. {supervisely-6.73.456.dist-info → supervisely-6.73.458.dist-info}/WHEEL +0 -0
  40. {supervisely-6.73.456.dist-info → supervisely-6.73.458.dist-info}/entry_points.txt +0 -0
  41. {supervisely-6.73.456.dist-info → supervisely-6.73.458.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,
@@ -112,7 +128,14 @@ from supervisely.worker_api.chunking import (
112
128
  ChunkedFileWriter,
113
129
  ChunkedFileReader,
114
130
  )
115
- import supervisely.worker_proto.worker_api_pb2 as api_proto
131
+
132
+ # Global import of api_proto works only if protobuf is installed and compatible
133
+ # Otherwise, we use a placeholder that raises an error when accessed
134
+ try:
135
+ import supervisely.worker_proto.worker_api_pb2 as api_proto
136
+ except Exception:
137
+ api_proto = _ApiProtoNotAvailable()
138
+
116
139
 
117
140
  from supervisely.api.api import Api, UserSession, ApiContext
118
141
  from supervisely.api import api
@@ -397,6 +397,9 @@ class ImageInfo(NamedTuple):
397
397
  #: Format: "YYYY-MM-DDTHH:MM:SS.sssZ"
398
398
  embeddings_updated_at: Optional[str] = None
399
399
 
400
+ #: :class:`int`: :class:`Dataset<supervisely.project.project.Project>` ID in Supervisely.
401
+ project_id: int = None
402
+
400
403
  # DO NOT DELETE THIS COMMENT
401
404
  #! New fields must be added with default values to keep backward compatibility.
402
405
 
@@ -476,6 +479,7 @@ class ImageApi(RemoveableBulkModuleApi):
476
479
  ApiField.OFFSET_END,
477
480
  ApiField.AI_SEARCH_META,
478
481
  ApiField.EMBEDDINGS_UPDATED_AT,
482
+ ApiField.PROJECT_ID,
479
483
  ]
480
484
 
481
485
  @staticmethod
@@ -236,11 +236,13 @@ class VideoAnnotationAPI(EntityAnnotationAPI):
236
236
  dst_project_meta = ProjectMeta.from_json(
237
237
  self._api.project.get_meta(dst_dataset_info.project_id)
238
238
  )
239
- for src_ids_batch, dst_ids_batch in batched(list(zip(src_video_ids, dst_video_ids))):
239
+ for src_ids_batch, dst_ids_batch in zip(batched(src_video_ids), batched(dst_video_ids)):
240
240
  ann_jsons = self.download_bulk(src_dataset_id, src_ids_batch)
241
241
  for dst_id, ann_json in zip(dst_ids_batch, ann_jsons):
242
242
  try:
243
- ann = VideoAnnotation.from_json(ann_json, dst_project_meta)
243
+ ann = VideoAnnotation.from_json(
244
+ ann_json, dst_project_meta, key_id_map=KeyIdMap()
245
+ )
244
246
  except Exception as e:
245
247
  raise RuntimeError("Failed to validate Annotation") from e
246
248
  self.append(dst_id, ann)
@@ -5,6 +5,7 @@ import asyncio
5
5
  import datetime
6
6
  import json
7
7
  import os
8
+ import re
8
9
  import urllib.parse
9
10
  from functools import partial
10
11
  from typing import (
@@ -23,7 +24,11 @@ from typing import (
23
24
  import aiofiles
24
25
  from numerize.numerize import numerize
25
26
  from requests import Response
26
- from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
27
+ from requests_toolbelt import (
28
+ MultipartDecoder,
29
+ MultipartEncoder,
30
+ MultipartEncoderMonitor,
31
+ )
27
32
  from tqdm import tqdm
28
33
 
29
34
  import supervisely.io.fs as sly_fs
@@ -1186,6 +1191,41 @@ class VideoApi(RemoveableBulkModuleApi):
1186
1191
  if progress_cb is not None:
1187
1192
  progress_cb(len(chunk))
1188
1193
 
1194
+ def download_frames(
1195
+ self, video_id: int, frames: List[int], paths: List[str], progress_cb=None
1196
+ ) -> None:
1197
+ endpoint = "videos.bulk.download-frame"
1198
+ response: Response = self._api.get(
1199
+ endpoint,
1200
+ params={},
1201
+ data={ApiField.VIDEO_ID: video_id, ApiField.FRAMES: frames},
1202
+ stream=True,
1203
+ )
1204
+ response.raise_for_status()
1205
+
1206
+ files = {frame_n: None for frame_n in frames}
1207
+ file_paths = {frame_n: path for frame_n, path in zip(frames, paths)}
1208
+
1209
+ try:
1210
+ decoder = MultipartDecoder.from_response(response)
1211
+ for part in decoder.parts:
1212
+ content_utf8 = part.headers[b"Content-Disposition"].decode("utf-8")
1213
+ # Find name="1245" preceded by a whitespace, semicolon or beginning of line.
1214
+ # The regex has 2 capture group: one for the prefix and one for the actual name value.
1215
+ frame_n = int(re.findall(r'(^|[\s;])name="(\d*)"', content_utf8)[0][1])
1216
+ if files[frame_n] is None:
1217
+ file_path = file_paths[frame_n]
1218
+ files[frame_n] = open(file_path, "wb")
1219
+ if progress_cb is not None:
1220
+ progress_cb(1)
1221
+ f = files[frame_n]
1222
+ f.write(part.content)
1223
+
1224
+ finally:
1225
+ for f in files.values():
1226
+ if f is not None:
1227
+ f.close()
1228
+
1189
1229
  def download_range_by_id(
1190
1230
  self,
1191
1231
  id: int,
@@ -1,3 +1,5 @@
1
+ # isort: skip_file
2
+
1
3
  import json
2
4
  import os
3
5
  import time
@@ -12,7 +14,8 @@ import queue
12
14
  import re
13
15
 
14
16
  from supervisely.worker_api.agent_api import AgentAPI
15
- from supervisely.worker_proto import worker_api_pb2 as api_proto
17
+
18
+ # from supervisely.worker_proto import worker_api_pb2 as api_proto # Import moved to methods where needed
16
19
  from supervisely.function_wrapper import function_wrapper
17
20
  from supervisely._utils import take_with_default
18
21
  from supervisely.sly_logger import logger as default_logger
@@ -30,7 +33,6 @@ from supervisely._utils import _remove_sensitive_information
30
33
  from supervisely.worker_api.agent_rpc import send_from_memory_generator
31
34
  from supervisely.io.fs_cache import FileCache
32
35
 
33
-
34
36
  # https://www.roguelynn.com/words/asyncio-we-did-it-wrong/
35
37
 
36
38
 
@@ -390,6 +392,13 @@ class AppService:
390
392
  )
391
393
 
392
394
  def publish_sync(self, initial_events=None):
395
+ try:
396
+ from supervisely.worker_proto import worker_api_pb2 as api_proto
397
+ except Exception as e:
398
+ from supervisely.app.v1.constants import PROTOBUF_REQUIRED_ERROR
399
+
400
+ raise ImportError(PROTOBUF_REQUIRED_ERROR) from e
401
+
393
402
  if initial_events is not None:
394
403
  for event_obj in initial_events:
395
404
  event_obj["api_token"] = os.environ[API_TOKEN]
@@ -507,6 +516,13 @@ class AppService:
507
516
  self._error = error
508
517
 
509
518
  def send_response(self, request_id, data):
519
+ try:
520
+ from supervisely.worker_proto import worker_api_pb2 as api_proto
521
+ except Exception as e:
522
+ from supervisely.app.v1.constants import PROTOBUF_REQUIRED_ERROR
523
+
524
+ raise ImportError(PROTOBUF_REQUIRED_ERROR) from e
525
+
510
526
  out_bytes = json.dumps(data).encode("utf-8")
511
527
  self.api.put_stream_with_data(
512
528
  "SendGeneralEventData",
@@ -7,4 +7,10 @@ SHARED_DATA = '/sessions'
7
7
 
8
8
  STOP_COMMAND = "stop"
9
9
 
10
- IMAGE_ANNOTATION_EVENTS = ["manual_selected_figure_changed"]
10
+ IMAGE_ANNOTATION_EVENTS = ["manual_selected_figure_changed"]
11
+
12
+ # Error message for missing or incompatible protobuf dependencies
13
+ PROTOBUF_REQUIRED_ERROR = (
14
+ "protobuf is required for agent/worker/app_v1 functionality. "
15
+ "Please install supervisely with agent extras: pip install 'supervisely[agent]'"
16
+ )
@@ -157,3 +157,23 @@ class Card(Widget):
157
157
  :rtype: bool
158
158
  """
159
159
  return self._disabled["disabled"]
160
+
161
+ @property
162
+ def description(self) -> Optional[str]:
163
+ """Description of the card.
164
+
165
+ :return: Description of the card.
166
+ :rtype: Optional[str]
167
+ """
168
+ return self._description
169
+
170
+ @description.setter
171
+ def description(self, value: str) -> None:
172
+ """Sets the description of the card.
173
+
174
+ :param value: Description of the card.
175
+ :type value: str
176
+ """
177
+ self._description = value
178
+ StateJson()[self.widget_id]["description"] = self._description
179
+ StateJson().send_changes()
@@ -1,10 +1,10 @@
1
1
  import datetime
2
2
  import tempfile
3
+ import threading
3
4
  from pathlib import Path
4
5
  from typing import Any, Dict, List, Literal
5
6
 
6
7
  import pandas as pd
7
- import yaml
8
8
 
9
9
  from supervisely._utils import logger
10
10
  from supervisely.api.api import Api
@@ -12,8 +12,6 @@ from supervisely.api.app_api import ModuleInfo
12
12
  from supervisely.app.widgets.agent_selector.agent_selector import AgentSelector
13
13
  from supervisely.app.widgets.button.button import Button
14
14
  from supervisely.app.widgets.container.container import Container
15
- from supervisely.app.widgets.card.card import Card
16
- from supervisely.app.widgets.model_info.model_info import ModelInfo
17
15
  from supervisely.app.widgets.ecosystem_model_selector.ecosystem_model_selector import (
18
16
  EcosystemModelSelector,
19
17
  )
@@ -23,7 +21,8 @@ from supervisely.app.widgets.experiment_selector.experiment_selector import (
23
21
  from supervisely.app.widgets.fast_table.fast_table import FastTable
24
22
  from supervisely.app.widgets.field.field import Field
25
23
  from supervisely.app.widgets.flexbox.flexbox import Flexbox
26
- from supervisely.app.widgets.tabs.tabs import Tabs
24
+ from supervisely.app.widgets.model_info.model_info import ModelInfo
25
+ from supervisely.app.widgets.radio_tabs.radio_tabs import RadioTabs
27
26
  from supervisely.app.widgets.text.text import Text
28
27
  from supervisely.app.widgets.widget import Widget
29
28
  from supervisely.io import env
@@ -211,24 +210,31 @@ class DeployModel(Widget):
211
210
  return self._layout
212
211
 
213
212
  def _create_layout(self) -> Container:
214
- frameworks = self.deploy_model.get_frameworks()
215
- experiment_infos = []
216
- for framework_name in frameworks:
217
- experiment_infos.extend(
218
- get_experiment_infos(self.api, self.team_id, framework_name=framework_name)
219
- )
220
213
  self.experiment_table = ExperimentSelector(
221
- experiment_infos=experiment_infos,
222
- team_id=self.team_id,
223
214
  api=self.api,
215
+ team_id=self.team_id,
224
216
  )
225
217
 
226
218
  @self.experiment_table.checkpoint_changed
227
219
  def _checkpoint_changed(row: ExperimentSelector.ModelRow, checkpoint_value: str):
228
220
  print(f"Checkpoint changed for {row._experiment_info.task_id}: {checkpoint_value}")
229
221
 
222
+ threading.Thread(target=self.refresh_experiments, daemon=True).start()
223
+
230
224
  return self.experiment_table
231
225
 
226
+ def refresh_experiments(self):
227
+ self.experiment_table.loading = True
228
+ frameworks = self.deploy_model.get_frameworks()
229
+ experiment_infos = []
230
+ for framework_name in frameworks:
231
+ experiment_infos.extend(
232
+ get_experiment_infos(self.api, self.team_id, framework_name=framework_name)
233
+ )
234
+
235
+ self.experiment_table.set_experiment_infos(experiment_infos)
236
+ self.experiment_table.loading = False
237
+
232
238
  def get_deploy_parameters(self) -> Dict[str, Any]:
233
239
  experiment_info = self.experiment_table.get_selected_experiment_info()
234
240
  return {
@@ -267,8 +273,8 @@ class DeployModel(Widget):
267
273
  MODES = [str(MODE.CONNECT), str(MODE.PRETRAINED), str(MODE.CUSTOM)]
268
274
  MODE_TO_CLASS = {
269
275
  str(MODE.CONNECT): Connect,
270
- str(MODE.PRETRAINED): Pretrained,
271
276
  str(MODE.CUSTOM): Custom,
277
+ str(MODE.PRETRAINED): Pretrained,
272
278
  }
273
279
 
274
280
  def __init__(
@@ -295,6 +301,11 @@ class DeployModel(Widget):
295
301
  self.MODE.PRETRAINED: "Pretrained",
296
302
  self.MODE.CUSTOM: "Custom",
297
303
  }
304
+ self.modes_descriptions = {
305
+ self.MODE.CONNECT: "Connect to an already deployed model",
306
+ self.MODE.PRETRAINED: "Deploy a pretrained model from the ecosystem",
307
+ self.MODE.CUSTOM: "Deploy a custom model from your experiments",
308
+ }
298
309
 
299
310
  # GUI
300
311
  self.layout: Widget = None
@@ -444,31 +455,41 @@ class DeployModel(Widget):
444
455
 
445
456
  self._init_modes(modes)
446
457
  _labels = []
458
+ _descriptions = []
447
459
  _contents = []
460
+ self.statuses_widgets = Container(
461
+ widgets=[
462
+ self.sesson_link,
463
+ self._model_info_container,
464
+ ],
465
+ gap=20,
466
+ )
467
+ self.statuses_widgets.hide()
448
468
  for mode_name, mode in self.modes.items():
449
469
  label = self.modes_labels[mode_name]
470
+ description = self.modes_descriptions[mode_name]
450
471
  if mode_name == str(self.MODE.CONNECT):
451
472
  widgets = [
452
473
  mode.layout,
453
- self._model_info_card,
454
- self.connect_stop_buttons,
455
474
  self.status,
456
- self.sesson_link,
475
+ self.statuses_widgets,
476
+ self.connect_stop_buttons,
457
477
  ]
458
478
  else:
459
479
  widgets = [
460
480
  mode.layout,
461
- self._model_info_card,
462
481
  self.select_agent_field,
463
- self.deploy_stop_buttons,
464
482
  self.status,
465
- self.sesson_link,
483
+ self.statuses_widgets,
484
+ self.deploy_stop_buttons,
466
485
  ]
486
+
467
487
  content = Container(widgets=widgets, gap=20)
468
488
  _labels.append(label)
489
+ _descriptions.append(description)
469
490
  _contents.append(content)
470
491
 
471
- self.tabs = Tabs(labels=_labels, contents=_contents)
492
+ self.tabs = RadioTabs(titles=_labels, descriptions=_descriptions, contents=_contents)
472
493
  if len(self.modes) == 1:
473
494
  self.layout = _contents[0]
474
495
  else:
@@ -490,7 +511,7 @@ class DeployModel(Widget):
490
511
  def _disconnect_button_clicked():
491
512
  self.disconnect()
492
513
 
493
- @self.tabs.click
514
+ @self.tabs.value_changed
494
515
  def _active_tab_changed(tab_name: str):
495
516
  self.set_model_message_by_tab(tab_name)
496
517
 
@@ -573,6 +594,7 @@ class DeployModel(Widget):
573
594
  f"Model {framework}: {model_name} deployed with session ID {model_api.task_id}."
574
595
  )
575
596
  self.model_api = model_api
597
+ self.statuses_widgets.show()
576
598
  self.set_model_status("connected")
577
599
  self.set_session_info(task_info)
578
600
  self.set_model_info(model_api.task_id)
@@ -603,12 +625,14 @@ class DeployModel(Widget):
603
625
  self.set_session_info(task_info)
604
626
  self.set_model_info(model_api.task_id)
605
627
  self.show_stop()
628
+ self.statuses_widgets.show()
606
629
  except Exception as e:
607
630
  logger.error(f"Failed to deploy model: {e}", exc_info=True)
608
631
  self.set_model_status("error", str(e))
609
632
  self.set_session_info(None)
610
633
  self.reset_model_info()
611
634
  self.show_deploy_button()
635
+ self.statuses_widgets.hide()
612
636
  self.enable_modes()
613
637
  else:
614
638
  if str(self.MODE.CONNECT) in self.modes:
@@ -634,6 +658,7 @@ class DeployModel(Widget):
634
658
  self.enable_modes()
635
659
  self.reset_model_info()
636
660
  self.show_deploy_button()
661
+ self.statuses_widgets.hide()
637
662
  if str(self.MODE.CONNECT) in self.modes:
638
663
  self.modes[str(self.MODE.CONNECT)]._update_sessions()
639
664
 
@@ -645,6 +670,7 @@ class DeployModel(Widget):
645
670
  self.set_session_info(None)
646
671
  self.reset_model_info()
647
672
  self.show_deploy_button()
673
+ self.statuses_widgets.hide()
648
674
  self.enable_modes()
649
675
 
650
676
  def load_from_json(self, data: Dict[str, Any]) -> None:
@@ -690,29 +716,24 @@ class DeployModel(Widget):
690
716
  title="Model Info",
691
717
  description="Information about the deployed model",
692
718
  )
693
-
694
- self._model_info_container = Container([self._model_info_widget_field])
695
- self._model_info_container.hide()
696
719
  self._model_info_message = Text("Connect to model to see the session information.")
697
-
698
- self._model_info_card = Card(
699
- title="Session Info",
700
- description="Model parameters and classes",
701
- collapsable=True,
702
- content=Container([self._model_info_container, self._model_info_message]),
720
+ self._model_info_container = Container(
721
+ [self._model_info_widget_field, self._model_info_message], gap=0
703
722
  )
704
- self._model_info_card.collapse()
723
+ self._model_info_widget_field.hide()
724
+
725
+ self._model_info_container.hide()
705
726
 
706
727
  def set_model_info(self, session_id):
707
- self._model_info_widget.set_model_info(session_id)
728
+ self._model_info_widget.set_session_id(session_id)
708
729
 
709
730
  self._model_info_message.hide()
731
+ self._model_info_widget_field.show()
710
732
  self._model_info_container.show()
711
- self._model_info_card.uncollapse()
712
733
 
713
734
  def reset_model_info(self):
714
- self._model_info_card.collapse()
715
735
  self._model_info_container.hide()
736
+ self._model_info_widget_field.hide()
716
737
  self._model_info_message.show()
717
738
 
718
739
  def set_model_message_by_tab(self, tab_name: str):
@@ -724,6 +745,6 @@ class DeployModel(Widget):
724
745
  self._model_info_message.set(
725
746
  "Deploy model to see the session information.", status="text"
726
747
  )
727
- self._model_info_card.collapse()
748
+ self._model_info_widget_field.hide()
728
749
 
729
750
  # ------------------------------------------------------------ #
@@ -721,6 +721,14 @@ class ExperimentSelector(Widget):
721
721
  def enable(self):
722
722
  return self.table.enable()
723
723
 
724
+ @property
725
+ def loading(self):
726
+ return self.table.loading
727
+
728
+ @loading.setter
729
+ def loading(self, value: bool):
730
+ self.table.loading = value
731
+
724
732
  def get_json_data(self):
725
733
  return {}
726
734
 
@@ -265,7 +265,9 @@ class FastTable(Widget):
265
265
  self._sliced_data = self._slice_table_data(self._sorted_data, actual_page=self._active_page)
266
266
  self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
267
267
  StateJson().send_changes()
268
- DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
268
+ DataJson()[self.widget_id]["data"] = {
269
+ i: row for i, row in enumerate(self._parsed_active_data["data"])
270
+ }
269
271
  DataJson()[self.widget_id]["total"] = self._rows_total
270
272
  DataJson().send_changes()
271
273
  StateJson()["reactToChanges"] = True
@@ -295,7 +297,7 @@ class FastTable(Widget):
295
297
  :rtype: Dict[str, Any]
296
298
  """
297
299
  return {
298
- "data": self._parsed_active_data["data"],
300
+ "data": {i: row for i, row in enumerate(self._parsed_active_data["data"])},
299
301
  "columns": self._parsed_source_data["columns"],
300
302
  "projectMeta": self._project_meta,
301
303
  "columnsOptions": self._columns_options,
@@ -307,7 +309,7 @@ class FastTable(Widget):
307
309
  "isRadio": self._is_radio,
308
310
  "isRowSelectable": self._is_selectable,
309
311
  "maxSelectedRows": self._max_selected_rows,
310
- "searchPosition": self._search_position
312
+ "searchPosition": self._search_position,
311
313
  },
312
314
  "pageSize": self._page_size,
313
315
  "showHeader": self._show_header,
@@ -490,7 +492,9 @@ class FastTable(Widget):
490
492
  self._sort_column_idx = None
491
493
  self._sort_order = sort.get("order", None)
492
494
  self._page_size = init_options.pop("pageSize", 10)
493
- DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
495
+ DataJson()[self.widget_id]["data"] = {
496
+ i: row for i, row in enumerate(self._parsed_active_data["data"])
497
+ }
494
498
  DataJson()[self.widget_id]["columns"] = self._parsed_active_data["columns"]
495
499
  DataJson()[self.widget_id]["columnsOptions"] = self._columns_options
496
500
  DataJson()[self.widget_id]["options"] = init_options
@@ -519,7 +523,9 @@ class FastTable(Widget):
519
523
  self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
520
524
  self._parsed_source_data = self._unpack_pandas_table_data(self._source_data)
521
525
  self._rows_total = len(self._parsed_source_data["data"])
522
- DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
526
+ DataJson()[self.widget_id]["data"] = {
527
+ i: row for i, row in enumerate(self._parsed_active_data["data"])
528
+ }
523
529
  DataJson()[self.widget_id]["columns"] = self._parsed_active_data["columns"]
524
530
  DataJson()[self.widget_id]["total"] = len(self._source_data)
525
531
  DataJson().send_changes()
@@ -715,7 +721,29 @@ class FastTable(Widget):
715
721
  self._parsed_active_data,
716
722
  ) = self._prepare_working_data()
717
723
  self._rows_total = len(self._parsed_source_data["data"])
718
- DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
724
+ DataJson()[self.widget_id]["data"] = {
725
+ i: row for i, row in enumerate(self._parsed_active_data["data"])
726
+ }
727
+ DataJson()[self.widget_id]["total"] = self._rows_total
728
+ DataJson().send_changes()
729
+ self._maybe_update_selected_row()
730
+
731
+ def add_rows(self, rows: List):
732
+ for row in rows:
733
+ self._validate_table_sizes(row)
734
+ self._validate_row_values_types(row)
735
+ self._source_data = pd.concat(
736
+ [self._source_data, pd.DataFrame(rows, columns=self._source_data.columns)]
737
+ ).reset_index(drop=True)
738
+ (
739
+ self._parsed_source_data,
740
+ self._sliced_data,
741
+ self._parsed_active_data,
742
+ ) = self._prepare_working_data()
743
+ self._rows_total = len(self._parsed_source_data["data"])
744
+ DataJson()[self.widget_id]["data"] = {
745
+ i: row for i, row in enumerate(self._parsed_active_data["data"])
746
+ }
719
747
  DataJson()[self.widget_id]["total"] = self._rows_total
720
748
  DataJson().send_changes()
721
749
  self._maybe_update_selected_row()
@@ -743,7 +771,9 @@ class FastTable(Widget):
743
771
  self._parsed_active_data,
744
772
  ) = self._prepare_working_data()
745
773
  self._rows_total = len(self._parsed_source_data["data"])
746
- DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
774
+ DataJson()[self.widget_id]["data"] = {
775
+ i: row for i, row in enumerate(self._parsed_active_data["data"])
776
+ }
747
777
  DataJson()[self.widget_id]["total"] = self._rows_total
748
778
  self._maybe_update_selected_row()
749
779
  return popped_row
@@ -755,7 +785,7 @@ class FastTable(Widget):
755
785
  self._sliced_data = pd.DataFrame(columns=self._columns_first_idx)
756
786
  self._parsed_active_data = {"data": [], "columns": []}
757
787
  self._rows_total = 0
758
- DataJson()[self.widget_id]["data"] = []
788
+ DataJson()[self.widget_id]["data"] = {}
759
789
  DataJson()[self.widget_id]["total"] = 0
760
790
  DataJson().send_changes()
761
791
  self._maybe_update_selected_row()
@@ -925,7 +955,9 @@ class FastTable(Widget):
925
955
  self._sorted_data = self._sort_table_data(self._searched_data)
926
956
  self._sliced_data = self._slice_table_data(self._sorted_data, actual_page=self._active_page)
927
957
  self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
928
- DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
958
+ DataJson()[self.widget_id]["data"] = {
959
+ i: row for i, row in enumerate(self._parsed_active_data["data"])
960
+ }
929
961
  DataJson()[self.widget_id]["total"] = self._rows_total
930
962
  self._maybe_update_selected_row()
931
963
  StateJson().send_changes()
@@ -1206,7 +1238,9 @@ class FastTable(Widget):
1206
1238
 
1207
1239
  self._sliced_data = self._slice_table_data(self._sorted_data, actual_page=self._active_page)
1208
1240
  self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
1209
- DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
1241
+ DataJson()[self.widget_id]["data"] = {
1242
+ i: row for i, row in enumerate(self._parsed_active_data["data"])
1243
+ }
1210
1244
  DataJson()[self.widget_id]["total"] = self._rows_total
1211
1245
  DataJson().send_changes()
1212
1246
  StateJson().send_changes()
@@ -1335,4 +1369,4 @@ class FastTable(Widget):
1335
1369
  else:
1336
1370
  raise TypeError(f"Column name must be a string or a tuple, got {type(col)}")
1337
1371
 
1338
- self._validate_sort_attrs()
1372
+ self._validate_sort_attrs()
@@ -11,7 +11,7 @@
11
11
  :project-meta="data.{{{widget.widget_id}}}.projectMeta"
12
12
  :sort.sync="state.{{{widget.widget_id}}}.sort"
13
13
  :search.sync="state.{{{widget.widget_id}}}.search"
14
- :data="data.{{{widget.widget_id}}}.data"
14
+ :data="Object.values(data.{{{widget.widget_id}}}.data || [])"
15
15
  :show-header="data.{{{widget.widget_id}}}.showHeader"
16
16
  :selected-rows="state.{{{widget.widget_id}}}.selectedRows"
17
17
  :selected-radio-idx="state.{{{widget.widget_id}}}.selectedRows && state.{{{widget.widget_id}}}.selectedRows.length > 0 ? state.{{{widget.widget_id}}}.selectedRows[0].idx : null"
@@ -1,5 +1,9 @@
1
- from typing import List, Optional, Dict
1
+ import traceback
2
+ from typing import Dict, List, Optional
3
+
4
+ from supervisely._utils import logger
2
5
  from supervisely.app import StateJson
6
+ from supervisely.app.content import DataJson
3
7
  from supervisely.app.widgets import Widget
4
8
 
5
9
 
@@ -65,7 +69,7 @@ class RadioTabs(Widget):
65
69
  return _value_changed
66
70
 
67
71
  def get_json_data(self) -> Dict:
68
- return {}
72
+ return {"tabsOptions": {item.name: {"disabled": False} for item in self._items}}
69
73
 
70
74
  def get_json_state(self) -> Dict:
71
75
  return {"value": self._value}
@@ -77,3 +81,15 @@ class RadioTabs(Widget):
77
81
 
78
82
  def get_active_tab(self) -> str:
79
83
  return StateJson()[self.widget_id]["value"]
84
+
85
+ def disable_tab(self, tab_name: str):
86
+ if tab_name not in [item.name for item in self._items]:
87
+ raise ValueError(f"Tab with name '{tab_name}' does not exist.")
88
+ DataJson()[self.widget_id]["tabsOptions"][tab_name]["disabled"] = True
89
+ DataJson().send_changes()
90
+
91
+ def enable_tab(self, tab_name: str):
92
+ if tab_name not in [item.name for item in self._items]:
93
+ raise ValueError(f"Tab with name '{tab_name}' does not exist.")
94
+ DataJson()[self.widget_id]["tabsOptions"][tab_name]["disabled"] = False
95
+ DataJson().send_changes()
@@ -20,6 +20,7 @@
20
20
  {% for tab_pane in widget._items %}
21
21
  <el-tab-pane
22
22
  name="{{{tab_pane.name}}}"
23
+ :disabled="data.{{{widget.widget_id}}}.tabsOptions['{{{tab_pane.name}}}'].disabled"
23
24
  >
24
25
  <el-radio
25
26
  slot="label"