supervisely 6.73.419__py3-none-any.whl → 6.73.421__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 (77) hide show
  1. supervisely/api/api.py +10 -5
  2. supervisely/api/app_api.py +71 -4
  3. supervisely/api/module_api.py +4 -0
  4. supervisely/api/nn/deploy_api.py +15 -9
  5. supervisely/api/nn/ecosystem_models_api.py +201 -0
  6. supervisely/api/nn/neural_network_api.py +12 -3
  7. supervisely/api/project_api.py +35 -6
  8. supervisely/api/task_api.py +5 -1
  9. supervisely/app/widgets/__init__.py +8 -1
  10. supervisely/app/widgets/agent_selector/template.html +1 -0
  11. supervisely/app/widgets/deploy_model/__init__.py +0 -0
  12. supervisely/app/widgets/deploy_model/deploy_model.py +729 -0
  13. supervisely/app/widgets/dropdown_checkbox_selector/__init__.py +0 -0
  14. supervisely/app/widgets/dropdown_checkbox_selector/dropdown_checkbox_selector.py +87 -0
  15. supervisely/app/widgets/dropdown_checkbox_selector/template.html +12 -0
  16. supervisely/app/widgets/ecosystem_model_selector/__init__.py +0 -0
  17. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +190 -0
  18. supervisely/app/widgets/experiment_selector/experiment_selector.py +447 -264
  19. supervisely/app/widgets/fast_table/fast_table.py +402 -74
  20. supervisely/app/widgets/fast_table/script.js +364 -96
  21. supervisely/app/widgets/fast_table/style.css +24 -0
  22. supervisely/app/widgets/fast_table/template.html +43 -3
  23. supervisely/app/widgets/radio_table/radio_table.py +10 -2
  24. supervisely/app/widgets/select/select.py +6 -4
  25. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +18 -0
  26. supervisely/app/widgets/tabs/tabs.py +22 -6
  27. supervisely/app/widgets/tabs/template.html +5 -1
  28. supervisely/nn/artifacts/__init__.py +1 -1
  29. supervisely/nn/artifacts/artifacts.py +10 -2
  30. supervisely/nn/artifacts/detectron2.py +1 -0
  31. supervisely/nn/artifacts/hrda.py +1 -0
  32. supervisely/nn/artifacts/mmclassification.py +20 -0
  33. supervisely/nn/artifacts/mmdetection.py +5 -3
  34. supervisely/nn/artifacts/mmsegmentation.py +1 -0
  35. supervisely/nn/artifacts/ritm.py +1 -0
  36. supervisely/nn/artifacts/rtdetr.py +1 -0
  37. supervisely/nn/artifacts/unet.py +1 -0
  38. supervisely/nn/artifacts/utils.py +3 -0
  39. supervisely/nn/artifacts/yolov5.py +2 -0
  40. supervisely/nn/artifacts/yolov8.py +1 -0
  41. supervisely/nn/benchmark/semantic_segmentation/metric_provider.py +18 -18
  42. supervisely/nn/experiments.py +9 -0
  43. supervisely/nn/inference/gui/serving_gui_template.py +39 -13
  44. supervisely/nn/inference/inference.py +160 -94
  45. supervisely/nn/inference/predict_app/__init__.py +0 -0
  46. supervisely/nn/inference/predict_app/gui/__init__.py +0 -0
  47. supervisely/nn/inference/predict_app/gui/classes_selector.py +91 -0
  48. supervisely/nn/inference/predict_app/gui/gui.py +710 -0
  49. supervisely/nn/inference/predict_app/gui/input_selector.py +165 -0
  50. supervisely/nn/inference/predict_app/gui/model_selector.py +79 -0
  51. supervisely/nn/inference/predict_app/gui/output_selector.py +139 -0
  52. supervisely/nn/inference/predict_app/gui/preview.py +93 -0
  53. supervisely/nn/inference/predict_app/gui/settings_selector.py +184 -0
  54. supervisely/nn/inference/predict_app/gui/tags_selector.py +110 -0
  55. supervisely/nn/inference/predict_app/gui/utils.py +282 -0
  56. supervisely/nn/inference/predict_app/predict_app.py +184 -0
  57. supervisely/nn/inference/uploader.py +9 -5
  58. supervisely/nn/model/prediction.py +2 -0
  59. supervisely/nn/model/prediction_session.py +20 -3
  60. supervisely/nn/training/gui/gui.py +131 -44
  61. supervisely/nn/training/gui/model_selector.py +8 -6
  62. supervisely/nn/training/gui/train_val_splits_selector.py +122 -70
  63. supervisely/nn/training/gui/training_artifacts.py +0 -5
  64. supervisely/nn/training/train_app.py +161 -44
  65. supervisely/project/project.py +211 -73
  66. supervisely/template/experiment/experiment.html.jinja +74 -17
  67. supervisely/template/experiment/experiment_generator.py +258 -112
  68. supervisely/template/experiment/header.html.jinja +31 -13
  69. supervisely/template/experiment/sly-style.css +7 -2
  70. {supervisely-6.73.419.dist-info → supervisely-6.73.421.dist-info}/METADATA +3 -1
  71. {supervisely-6.73.419.dist-info → supervisely-6.73.421.dist-info}/RECORD +75 -57
  72. supervisely/app/widgets/experiment_selector/style.css +0 -27
  73. supervisely/app/widgets/experiment_selector/template.html +0 -61
  74. {supervisely-6.73.419.dist-info → supervisely-6.73.421.dist-info}/LICENSE +0 -0
  75. {supervisely-6.73.419.dist-info → supervisely-6.73.421.dist-info}/WHEEL +0 -0
  76. {supervisely-6.73.419.dist-info → supervisely-6.73.421.dist-info}/entry_points.txt +0 -0
  77. {supervisely-6.73.419.dist-info → supervisely-6.73.421.dist-info}/top_level.txt +0 -0
@@ -72,6 +72,7 @@ class FastTable(Widget):
72
72
  """
73
73
 
74
74
  class Routes:
75
+ SELECTION_CHANGED = "selection_changed_cb"
75
76
  ROW_CLICKED = "row_clicked_cb"
76
77
  CELL_CLICKED = "cell_clicked_cb"
77
78
  UPDATE_DATA = "update_data_cb"
@@ -100,6 +101,29 @@ class FastTable(Widget):
100
101
  self.column_name = column_name
101
102
  self.column_value = column_value
102
103
 
104
+ class ColumnData:
105
+ def __init__(self, name, is_widget=False, widget: Widget = None):
106
+ self.name = name
107
+ self.is_widget = is_widget
108
+ self.widget = widget
109
+
110
+ @property
111
+ def widget_html(self):
112
+ html = self.widget.to_html()
113
+ html = html.replace(f".{self.widget.widget_id}", "[JSON.parse(cellValue).widget_id]")
114
+ html = html.replace(
115
+ f"/{self.widget.widget_id}", "/' + JSON.parse(cellValue).widget_id + '"
116
+ )
117
+ if hasattr(self.widget, "_widgets"):
118
+ for i, widget in enumerate(self.widget._widgets):
119
+ html = html.replace(
120
+ f".{widget.widget_id}", f"[JSON.parse(cellValue).widgets[{i}]]"
121
+ )
122
+ html = html.replace(
123
+ f"/{widget.widget_id}", f"/' + JSON.parse(cellValue).widgets[{i}] + '"
124
+ )
125
+ return html
126
+
103
127
  def __init__(
104
128
  self,
105
129
  data: Optional[Union[pd.DataFrame, List]] = None,
@@ -113,23 +137,48 @@ class FastTable(Widget):
113
137
  width: Optional[str] = "auto",
114
138
  widget_id: Optional[str] = None,
115
139
  show_header: bool = True,
140
+ is_radio: bool = False,
141
+ is_selectable: bool = False,
142
+ header_left_content: Optional[Widget] = None,
143
+ header_right_content: Optional[Widget] = None,
116
144
  ):
117
145
  self._supported_types = tuple([pd.DataFrame, list, type(None)])
118
146
  self._row_click_handled = False
119
147
  self._cell_click_handled = False
120
- self._columns_first_idx = columns
148
+ self._selection_changed_handled = False
149
+ self._columns = columns
150
+ self._columns_data = []
151
+ if columns is None:
152
+ self._columns_first_idx = None
153
+ else:
154
+ self._columns_first_idx = []
155
+ for col in columns:
156
+ if isinstance(col, str):
157
+ self._columns_first_idx.append(col)
158
+ self._columns_data.append(self.ColumnData(name=col))
159
+ elif isinstance(col, tuple):
160
+ self._columns_first_idx.append(col[0])
161
+ self._columns_data.append(
162
+ self.ColumnData(name=col[0], is_widget=True, widget=col[1])
163
+ )
164
+ else:
165
+ raise TypeError(f"Column name must be a string or a tuple, got {type(col)}")
166
+
121
167
  self._columns_options = columns_options
122
168
  self._sorted_data = None
123
169
  self._filtered_data = None
170
+ self._searched_data = None
124
171
  self._active_page = 1
125
172
  self._width = width
126
- self._selected_row = None
173
+ self._selected_rows = None
127
174
  self._selected_cell = None
128
- self._clickable_rows = False
129
- self._clickable_cells = False
175
+ self._is_row_clickable = False
176
+ self._is_cell_clickable = False
130
177
  self._search_str = ""
131
178
  self._show_header = show_header
132
179
  self._project_meta = self._unpack_project_meta(project_meta)
180
+ self._header_left_content = header_left_content
181
+ self._header_right_content = header_right_content
133
182
 
134
183
  # table_options
135
184
  self._page_size = page_size
@@ -137,6 +186,12 @@ class FastTable(Widget):
137
186
  self._sort_column_idx = sort_column_idx
138
187
  self._sort_order = sort_order
139
188
  self._validate_sort_attrs()
189
+ self._is_radio = is_radio
190
+ self._is_selectable = is_selectable
191
+ self._search_function = self._default_search_function
192
+ self._sort_function = self._default_sort_function
193
+ self._filter_function = self._default_filter_function
194
+ self._filter_value = None
140
195
 
141
196
  # to avoid errors with the duplicated names in columns
142
197
  self._multi_idx_columns = None
@@ -154,6 +209,9 @@ class FastTable(Widget):
154
209
 
155
210
  self._rows_total = len(self._parsed_source_data["data"])
156
211
 
212
+ if self._is_radio and self._rows_total > 0:
213
+ self._selected_rows = [self._parsed_source_data["data"][0]]
214
+
157
215
  super().__init__(widget_id=widget_id, file_path=__file__)
158
216
 
159
217
  script_path = "./sly/css/app/widgets/fast_table/script.js"
@@ -163,27 +221,32 @@ class FastTable(Widget):
163
221
  server = self._sly_app.get_server()
164
222
 
165
223
  @server.post(filter_changed_route_path)
166
- def _filter_changed():
167
- self._active_page = StateJson()[self.widget_id]["page"]
168
- self._sort_order = StateJson()[self.widget_id]["sort"]["order"]
169
- self._sort_column_idx = StateJson()[self.widget_id]["sort"]["column"]
170
- search_value = StateJson()[self.widget_id]["search"]
171
- self._filtered_data = self.search(search_value)
172
- self._rows_total = len(self._filtered_data)
173
-
174
- if self._rows_total > 0 and self._active_page == 0: # if previous filtered data was empty
175
- self._active_page = 1
176
- StateJson()[self.widget_id]["page"] = self._active_page
177
-
178
- self._sorted_data = self._sort_table_data(self._filtered_data)
179
- self._sliced_data = self._slice_table_data(
180
- self._sorted_data, actual_page=self._active_page
181
- )
182
- self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
183
- DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
184
- DataJson()[self.widget_id]["total"] = self._rows_total
185
- DataJson().send_changes()
186
- StateJson().send_changes()
224
+ def _filter_changed_handler():
225
+ self._refresh()
226
+
227
+ def _refresh(self):
228
+ # TODO sort widgets
229
+ self._active_page = StateJson()[self.widget_id]["page"]
230
+ self._sort_order = StateJson()[self.widget_id]["sort"]["order"]
231
+ self._sort_column_idx = StateJson()[self.widget_id]["sort"]["column"]
232
+ search_value = StateJson()[self.widget_id]["search"]
233
+ self._filtered_data = self._filter(self._filter_value)
234
+ self._searched_data = self._search(search_value)
235
+ self._rows_total = len(self._searched_data)
236
+
237
+ if self._rows_total > 0 and self._active_page == 0: # if previous filtered data was empty
238
+ self._active_page = 1
239
+ StateJson()[self.widget_id]["page"] = self._active_page
240
+
241
+ self._sorted_data = self._sort_table_data(self._searched_data)
242
+ self._sliced_data = self._slice_table_data(self._sorted_data, actual_page=self._active_page)
243
+ self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
244
+ StateJson().send_changes()
245
+ DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
246
+ DataJson()[self.widget_id]["total"] = self._rows_total
247
+ DataJson().send_changes()
248
+ StateJson()["reactToChanges"] = True
249
+ StateJson().send_changes()
187
250
 
188
251
  def get_json_data(self) -> Dict[str, Any]:
189
252
  """Returns dictionary with widget data, which defines the appearance and behavior of the widget.
@@ -209,12 +272,14 @@ class FastTable(Widget):
209
272
  "columnsOptions": self._columns_options,
210
273
  "total": self._rows_total,
211
274
  "options": {
212
- "isRowClickable": self._clickable_rows,
213
- "isCellClickable": self._clickable_cells,
275
+ "isRowClickable": self._is_row_clickable,
276
+ "isCellClickable": self._is_cell_clickable,
214
277
  "fixColumns": self._fix_columns,
278
+ "isRadio": self._is_radio,
215
279
  },
216
280
  "pageSize": self._page_size,
217
281
  "showHeader": self._show_header,
282
+ "selectionChangedHandled": self._selection_changed_handled,
218
283
  }
219
284
 
220
285
  def get_json_state(self) -> Dict[str, Any]:
@@ -233,7 +298,7 @@ class FastTable(Widget):
233
298
  """
234
299
  return {
235
300
  "search": self._search_str,
236
- "selectedRow": self._selected_row,
301
+ "selectedRows": self._selected_rows,
237
302
  "selectedCell": self._selected_cell,
238
303
  "page": self._active_page,
239
304
  "sort": {
@@ -289,6 +354,35 @@ class FastTable(Widget):
289
354
  self._page_size = size
290
355
  DataJson()[self.widget_id]["pageSize"] = self._page_size
291
356
 
357
+ def set_sort(
358
+ self, func: Callable[[pd.DataFrame, int, Optional[Literal["asc", "desc"]]], pd.DataFrame]
359
+ ) -> None:
360
+ """Sets custom sort function for the table.
361
+
362
+ :param func: Custom sort function
363
+ :type func: Callable[[pd.DataFrame, int, Optional[Literal["asc", "desc"]]], pd.DataFrame]
364
+ """
365
+ self._sort_function = func
366
+
367
+ def set_search(self, func: Callable[[pd.DataFrame, str], pd.DataFrame]) -> None:
368
+ """Sets custom search function for the table.
369
+
370
+ :param func: Custom search function
371
+ :type func: Callable[[pd.DataFrame, str], pd.DataFrame]
372
+ """
373
+ self._search_function = func
374
+
375
+ def set_filter(self, filter_function: Callable[[pd.DataFrame, Any], pd.DataFrame]) -> None:
376
+ """Sets a custom filter function for the table.
377
+ first argument is a DataFrame, second argument is a filter value.
378
+
379
+ :param filter_function: Custom filter function
380
+ :type filter_function: Callable[[pd.DataFrame, Any], pd.DataFrame]
381
+ """
382
+ if filter_function is None:
383
+ filter_function = self._default_filter_function
384
+ self._filter_function = filter_function
385
+
292
386
  def read_json(self, data: Dict, meta: Dict = None) -> None:
293
387
  """Replace table data with options and project meta in the widget
294
388
 
@@ -305,6 +399,7 @@ class FastTable(Widget):
305
399
  self._source_data = self._prepare_input_data(self._parsed_source_data)
306
400
  self._sliced_data = self._slice_table_data(self._source_data)
307
401
  self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
402
+ self._rows_total = len(self._parsed_source_data["data"])
308
403
  init_options = DataJson()[self.widget_id]["options"]
309
404
  init_options.update(self._table_options)
310
405
  sort = init_options.pop("sort", {"column": None, "order": None})
@@ -332,6 +427,7 @@ class FastTable(Widget):
332
427
  self._sliced_data = self._slice_table_data(self._sorted_data)
333
428
  self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
334
429
  self._parsed_source_data = self._unpack_pandas_table_data(self._source_data)
430
+ self._rows_total = len(self._parsed_source_data["data"])
335
431
  DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
336
432
  DataJson()[self.widget_id]["columns"] = self._parsed_active_data["columns"]
337
433
  DataJson()[self.widget_id]["total"] = len(self._source_data)
@@ -381,9 +477,10 @@ class FastTable(Widget):
381
477
 
382
478
  def clear_selection(self) -> None:
383
479
  """Clears the selection of the table."""
384
- StateJson()[self.widget_id]["selectedRow"] = None
480
+ StateJson()[self.widget_id]["selectedRows"] = None
385
481
  StateJson()[self.widget_id]["selectedCell"] = None
386
482
  StateJson().send_changes()
483
+ self._maybe_update_selected_row()
387
484
 
388
485
  def get_selected_row(self) -> ClickedRow:
389
486
  """Returns the selected row.
@@ -391,20 +488,56 @@ class FastTable(Widget):
391
488
  :return: Selected row
392
489
  :rtype: ClickedRow
393
490
  """
394
- row_data = StateJson()[self.widget_id]["selectedRow"]
395
- row_index = row_data["idx"]
396
- row = row_data["row"]
491
+ if self._is_radio or self._is_selectable:
492
+ selected_rows = StateJson()[self.widget_id]["selectedRows"]
493
+ if selected_rows is None:
494
+ return None
495
+ if len(selected_rows) == 0:
496
+ return None
497
+ if len(selected_rows) > 1:
498
+ raise ValueError(
499
+ "Multiple rows selected. Use get_selected_rows() method to get all selected rows."
500
+ )
501
+ row = selected_rows[0]
502
+ row_index = row["idx"]
503
+ row = row.get("row", row.get("items", None))
504
+ if row_index is None or row is None:
505
+ return None
506
+ return self.ClickedRow(row, row_index)
507
+ return self.get_clicked_row()
508
+
509
+ def get_selected_rows(self) -> List[ClickedRow]:
510
+ if self._is_radio or self._is_selectable:
511
+ selected_rows = StateJson()[self.widget_id]["selectedRows"]
512
+ rows = []
513
+ for row in selected_rows:
514
+ row_index = row["idx"]
515
+ row_data = row.get("row", row.get("items", None))
516
+ if row_index is None or row_data is None:
517
+ continue
518
+ rows.append(self.ClickedRow(row_data, row_index))
519
+ return rows
520
+ return [self.get_clicked_row()]
521
+
522
+ def get_clicked_row(self) -> ClickedRow:
523
+ clicked_row = StateJson()[self.widget_id]["clickedRow"]
524
+ if clicked_row is None:
525
+ return None
526
+ row_index = clicked_row["idx"]
527
+ row = clicked_row["row"]
397
528
  if row_index is None or row is None:
398
529
  return None
399
530
  return self.ClickedRow(row, row_index)
400
531
 
401
- def get_selected_cell(self) -> ClickedCell:
532
+ def get_clicked_cell(self) -> ClickedCell:
402
533
  """Returns the selected cell.
403
534
 
404
535
  :return: Selected cell
405
536
  :rtype: ClickedCell
406
537
  """
407
- cell_data = StateJson()[self.widget_id]["selectedCell"]
538
+ cell_data = StateJson()[self.widget_id]["clickedCell"]
539
+ if cell_data is None:
540
+ return None
408
541
  row_index = cell_data["idx"]
409
542
  row = cell_data["row"]
410
543
  column_index = cell_data["column"]
@@ -414,6 +547,40 @@ class FastTable(Widget):
414
547
  return None
415
548
  return self.ClickedCell(row, column_index, row_index, column_name, column_value)
416
549
 
550
+ def get_selected_cell(self) -> ClickedCell:
551
+ """Alias for get_clicked_cell method.
552
+ Will be removed in future versions.
553
+ """
554
+ return self.get_clicked_cell()
555
+
556
+ def _maybe_update_selected_row(self) -> None:
557
+ if self._is_radio:
558
+ if self._rows_total != 0:
559
+ self.select_row(0)
560
+ else:
561
+ self._selected_rows = None
562
+ StateJson()[self.widget_id]["selectedRows"] = None
563
+ StateJson().send_changes()
564
+ return
565
+ if not self._selected_rows:
566
+ return
567
+ if self._rows_total == 0:
568
+ self._selected_rows = None
569
+ StateJson()[self.widget_id]["selectedRows"] = None
570
+ StateJson().send_changes()
571
+ return
572
+ if self._is_selectable:
573
+ updated_selected_rows = []
574
+ for row in self._parsed_source_data["data"]:
575
+ items = row.get("items", row.get("row", None))
576
+ if items is not None:
577
+ for selected_row in self._selected_rows:
578
+ if selected_row.get("row", selected_row.get("items", None)) == items:
579
+ updated_selected_rows.append(row)
580
+ self._selected_rows = updated_selected_rows
581
+ StateJson()[self.widget_id]["selectedRows"] = self._selected_rows
582
+ StateJson().send_changes()
583
+
417
584
  def insert_row(self, row: List, index: Optional[int] = -1) -> None:
418
585
  """Inserts a row into the table to the specified position.
419
586
 
@@ -424,8 +591,7 @@ class FastTable(Widget):
424
591
  """
425
592
  self._validate_table_sizes(row)
426
593
  self._validate_row_values_types(row)
427
- table_data = self._parsed_source_data
428
- index = len(table_data) if index > len(table_data) or index < 0 else index
594
+ index = len(self._source_data) if index > len(self._source_data) or index < 0 else index
429
595
 
430
596
  self._source_data = pd.concat(
431
597
  [
@@ -443,6 +609,7 @@ class FastTable(Widget):
443
609
  DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
444
610
  DataJson()[self.widget_id]["total"] = self._rows_total
445
611
  DataJson().send_changes()
612
+ self._maybe_update_selected_row()
446
613
 
447
614
  def pop_row(self, index: Optional[int] = -1) -> List:
448
615
  """Removes a row from the table at the specified position and returns it.
@@ -469,7 +636,7 @@ class FastTable(Widget):
469
636
  self._rows_total = len(self._parsed_source_data["data"])
470
637
  DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
471
638
  DataJson()[self.widget_id]["total"] = self._rows_total
472
- DataJson().send_changes()
639
+ self._maybe_update_selected_row()
473
640
  return popped_row
474
641
 
475
642
  def clear(self) -> None:
@@ -482,6 +649,7 @@ class FastTable(Widget):
482
649
  DataJson()[self.widget_id]["data"] = []
483
650
  DataJson()[self.widget_id]["total"] = 0
484
651
  DataJson().send_changes()
652
+ self._maybe_update_selected_row()
485
653
 
486
654
  def row_click(self, func: Callable[[ClickedRow], Any]) -> Callable[[], None]:
487
655
  """Decorator for function that handles row click event.
@@ -495,8 +663,8 @@ class FastTable(Widget):
495
663
  server = self._sly_app.get_server()
496
664
 
497
665
  self._row_click_handled = True
498
- self._clickable_rows = True
499
- DataJson()[self.widget_id]["options"]["isRowClickable"] = self._clickable_rows
666
+ self._is_row_clickable = True
667
+ DataJson()[self.widget_id]["options"]["isRowClickable"] = self._is_row_clickable
500
668
  DataJson().send_changes()
501
669
 
502
670
  if self._cell_click_handled is True:
@@ -528,8 +696,8 @@ class FastTable(Widget):
528
696
  server = self._sly_app.get_server()
529
697
 
530
698
  self._cell_click_handled = True
531
- self._clickable_cells = True
532
- DataJson()[self.widget_id]["options"]["isCellClickable"] = self._clickable_cells
699
+ self._is_cell_clickable = True
700
+ DataJson()[self.widget_id]["options"]["isCellClickable"] = self._is_cell_clickable
533
701
  DataJson().send_changes()
534
702
 
535
703
  if self._row_click_handled is True:
@@ -549,7 +717,40 @@ class FastTable(Widget):
549
717
 
550
718
  return _click
551
719
 
552
- def search(self, search_value: str) -> pd.DataFrame:
720
+ def _default_filter_function(self, data: pd.DataFrame, filter_value: Any) -> pd.DataFrame:
721
+ return data
722
+
723
+ def _filter_table_data(self, data: pd.DataFrame) -> pd.DataFrame:
724
+ """Filter source data using a self._filter_function as filter function.
725
+ To apply a custom filter function, use the set_filter method.
726
+
727
+ :return: Filtered data
728
+ :rtype: pd.DataFrame
729
+ """
730
+ filtered_data = self._filter_function(data, self._filter_value)
731
+ return filtered_data
732
+
733
+ def _filter(self, filter_value: Any) -> pd.DataFrame:
734
+ filtered_data = self._source_data.copy()
735
+ if filter_value is None:
736
+ return filtered_data
737
+ if self._filter_value != filter_value:
738
+ self._active_page = 1
739
+ StateJson()[self.widget_id]["page"] = self._active_page
740
+ StateJson().send_changes()
741
+ self._filter_value = filter_value
742
+ filtered_data = self._filter_table_data(filtered_data)
743
+ return filtered_data
744
+
745
+ def filter(self, filter_value) -> None:
746
+ self._filter_value = filter_value
747
+ self._refresh()
748
+
749
+ def _default_search_function(self, data: pd.DataFrame, search_value: str) -> pd.DataFrame:
750
+ data = data[data.applymap(lambda x: search_value in str(x)).any(axis=1)]
751
+ return data
752
+
753
+ def _search(self, search_value: str) -> pd.DataFrame:
553
754
  """Search source data for search value.
554
755
 
555
756
  :param search_value: Search value
@@ -557,20 +758,41 @@ class FastTable(Widget):
557
758
  :return: Filtered data
558
759
  :rtype: pd.DataFrame
559
760
  """
560
- filtered_data = self._source_data.copy()
761
+ filtered_data = self._filtered_data.copy()
561
762
  if search_value == "":
562
763
  return filtered_data
563
- else:
564
- if self._search_str != search_value:
565
- self._active_page = 1
566
- StateJson()[self.widget_id]["page"] = self._active_page
567
- StateJson().send_changes()
568
- filtered_data = filtered_data[
569
- filtered_data.applymap(lambda x: search_value in str(x)).any(axis=1)
570
- ]
571
- self._search_str = search_value
764
+ if self._search_str != search_value:
765
+ self._active_page = 1
766
+ StateJson()[self.widget_id]["page"] = self._active_page
767
+ StateJson().send_changes()
768
+ filtered_data = self._search_function(filtered_data, search_value)
769
+ self._search_str = search_value
572
770
  return filtered_data
573
771
 
772
+ def search(self, search_value: str) -> None:
773
+ StateJson()[self.widget_id]["search"] = search_value
774
+ StateJson().send_changes()
775
+ self._refresh()
776
+
777
+ def _default_sort_function(
778
+ self,
779
+ data: pd.DataFrame,
780
+ column_idx: Optional[int],
781
+ order: Optional[Literal["asc", "desc"]],
782
+ ) -> pd.DataFrame:
783
+ if order == "asc":
784
+ ascending = True
785
+ else:
786
+ ascending = False
787
+ try:
788
+ data = data.sort_values(by=data.columns[column_idx], ascending=ascending)
789
+ except IndexError as e:
790
+ e.args = (
791
+ f"Sorting by column idx = {column_idx} is not possible, your table has only {len(data.columns)} columns with idx from 0 to {len(data.columns) - 1}",
792
+ )
793
+ raise e
794
+ return data
795
+
574
796
  def sort(
575
797
  self, column_idx: Optional[int] = None, order: Optional[Literal["asc", "desc"]] = None
576
798
  ) -> None:
@@ -588,13 +810,15 @@ class FastTable(Widget):
588
810
  StateJson()[self.widget_id]["sort"]["column"] = self._sort_column_idx
589
811
  if self._sort_order is not None:
590
812
  StateJson()[self.widget_id]["sort"]["order"] = self._sort_order
591
- self._filtered_data = self.search(self._search_str)
592
- self._rows_total = len(self._filtered_data)
593
- self._sorted_data = self._sort_table_data(self._filtered_data)
813
+ self._filtered_data = self._filter(self._filter_value)
814
+ self._searched_data = self._search(self._search_str)
815
+ self._rows_total = len(self._searched_data)
816
+ self._sorted_data = self._sort_table_data(self._searched_data)
594
817
  self._sliced_data = self._slice_table_data(self._sorted_data, actual_page=self._active_page)
595
818
  self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
596
819
  DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
597
820
  DataJson()[self.widget_id]["total"] = self._rows_total
821
+ self._maybe_update_selected_row()
598
822
  StateJson().send_changes()
599
823
 
600
824
  def _prepare_json_data(self, data: dict, key: str):
@@ -746,28 +970,29 @@ class FastTable(Widget):
746
970
  data = data.iloc[start_idx:end_idx]
747
971
  return data
748
972
 
749
- def _sort_table_data(self, input_data: pd.DataFrame) -> pd.DataFrame:
973
+ def _sort_table_data(
974
+ self,
975
+ input_data: pd.DataFrame,
976
+ column_index: Optional[int] = None,
977
+ sort_order: Optional[Literal["asc", "desc"]] = None,
978
+ ) -> pd.DataFrame:
750
979
  """
751
980
  Apply sorting to received data
752
981
 
753
982
  """
754
- if self._sort_order is None or self._sort_column_idx is None:
983
+ if column_index is None:
984
+ column_index = self._sort_column_idx
985
+ if sort_order is None:
986
+ sort_order = self._sort_order
987
+
988
+ if sort_order is None or column_index is None:
755
989
  return input_data # unsorted
756
990
 
757
- if input_data is not None:
758
- if self._sort_order == "asc":
759
- ascending = True
760
- else:
761
- ascending = False
762
- data: pd.DataFrame = copy.deepcopy(input_data)
763
- try:
764
- data = data.sort_values(by=data.columns[self._sort_column_idx], ascending=ascending)
765
- except IndexError as e:
766
- e.args = (
767
- f"Sorting by column idx = {self._sort_column_idx} is not possible, your table has only {len(data.columns)} columns with idx from 0 to {len(data.columns) - 1}",
768
- )
769
- raise e
991
+ data = copy.deepcopy(input_data)
992
+ if input_data is None:
993
+ return data
770
994
 
995
+ data = self._sort_function(data=input_data, column_idx=column_index, order=sort_order)
771
996
  return data
772
997
 
773
998
  def _unpack_project_meta(self, project_meta: Union[ProjectMeta, dict]) -> dict:
@@ -857,13 +1082,16 @@ class FastTable(Widget):
857
1082
  self._sort_column_idx = StateJson()[self.widget_id]["sort"]["column"]
858
1083
  self._sort_order = StateJson()[self.widget_id]["sort"]["order"]
859
1084
  self._validate_sort_attrs()
860
- self._filtered_data = self.search(self._search_str)
861
- self._rows_total = len(self._filtered_data)
862
- self._sorted_data = self._sort_table_data(self._filtered_data)
1085
+ self._filtered_data = self._filter(self._filter_value)
1086
+ self._searched_data = self._search(self._search_str)
1087
+ self._rows_total = len(self._searched_data)
1088
+ self._sorted_data = self._sort_table_data(self._searched_data)
863
1089
 
864
1090
  increment = 0 if self._rows_total % self._page_size == 0 else 1
865
1091
  max_page = self._rows_total // self._page_size + increment
866
- if self._active_page > max_page: # active page is out of range (in case of the filtered data)
1092
+ if (
1093
+ self._active_page > max_page
1094
+ ): # active page is out of range (in case of the filtered data)
867
1095
  self._active_page = max_page
868
1096
  StateJson()[self.widget_id]["page"] = self._active_page
869
1097
 
@@ -873,3 +1101,103 @@ class FastTable(Widget):
873
1101
  DataJson()[self.widget_id]["total"] = self._rows_total
874
1102
  DataJson().send_changes()
875
1103
  StateJson().send_changes()
1104
+
1105
+ def selection_changed(self, func):
1106
+ """Decorator for function that handles selection change event.
1107
+
1108
+ :param func: Function that handles selection change event
1109
+ :type func: Callable[[], Any]
1110
+ :return: Decorated function
1111
+ :rtype: Callable[[], None]
1112
+ """
1113
+ selection_changed_route_path = self.get_route_path(FastTable.Routes.SELECTION_CHANGED)
1114
+ server = self._sly_app.get_server()
1115
+
1116
+ @server.post(selection_changed_route_path)
1117
+ def _selection_changed():
1118
+ selected_row = self.get_selected_row()
1119
+ func(selected_row)
1120
+
1121
+ self._selection_changed_handled = True
1122
+ DataJson()[self.widget_id]["selectionChangedHandled"] = True
1123
+ DataJson().send_changes()
1124
+ return _selection_changed
1125
+
1126
+ def select_row(self, idx: int):
1127
+ if not self._is_selectable and not self._is_radio:
1128
+ raise ValueError(
1129
+ "Table is not selectable. Set 'is_selectable' or 'is_radio' to True to use this method."
1130
+ )
1131
+ if idx < 0 or idx >= len(self._parsed_source_data["data"]):
1132
+ raise IndexError(
1133
+ f"Row index {idx} is out of range. Valid range is 0 to {len(self._parsed_source_data['data']) - 1}."
1134
+ )
1135
+ selected_row = self._parsed_source_data["data"][idx]
1136
+ self._selected_rows = [
1137
+ {"idx": idx, "row": selected_row.get("items", selected_row.get("row", None))}
1138
+ ]
1139
+ StateJson()[self.widget_id]["selectedRows"] = self._selected_rows
1140
+ page = idx // self._page_size + 1
1141
+ if self._active_page != page:
1142
+ self._active_page = page
1143
+ StateJson()[self.widget_id]["page"] = self._active_page
1144
+ self._refresh()
1145
+
1146
+ def select_rows(self, idxs: List[int]):
1147
+ if not self._is_selectable:
1148
+ raise ValueError(
1149
+ "Table is not selectable. Set 'is_selectable' to True to use this method."
1150
+ )
1151
+ selected_rows = [
1152
+ self._parsed_source_data["data"][idx]
1153
+ for idx in idxs
1154
+ if 0 <= idx < len(self._parsed_source_data["data"])
1155
+ ]
1156
+ self._selected_rows = [
1157
+ {"idx": row["idx"], "row": row.get("items", row.get("row", None))}
1158
+ for row in selected_rows
1159
+ ]
1160
+ StateJson()[self.widget_id]["selectedRows"] = self._selected_rows
1161
+ StateJson().send_changes()
1162
+
1163
+ def select_row_by_value(self, column, value: Any):
1164
+ """Selects a row by value in a specific column.
1165
+
1166
+ :param column: Column name to filter by
1167
+ :type column: str
1168
+ :param value: Value to select row by
1169
+ :type value: Any
1170
+ """
1171
+ if not self._is_selectable and not self._is_radio:
1172
+ raise ValueError(
1173
+ "Table is not selectable. Set 'is_selectable' to True to use this method."
1174
+ )
1175
+ if column not in self._columns_first_idx:
1176
+ raise ValueError(f"Column '{column}' does not exist in the table.")
1177
+
1178
+ idx = self._source_data[self._source_data[column] == value].index.tolist()
1179
+ if not idx:
1180
+ raise ValueError(f"No rows found with {column} = {value}.")
1181
+ if len(idx) > 1:
1182
+ raise ValueError(
1183
+ f"Multiple rows found with {column} = {value}. Please use select_rows_by_value method."
1184
+ )
1185
+ self.select_row(idx[0])
1186
+
1187
+ def select_rows_by_value(self, column, values: List):
1188
+ """Selects rows by value in a specific column.
1189
+
1190
+ :param column: Column name to filter by
1191
+ :type column: str
1192
+ :param values: List of values to select rows by
1193
+ :type values: List
1194
+ """
1195
+ if not self._is_selectable:
1196
+ raise ValueError(
1197
+ "Table is not selectable. Set 'is_selectable' to True to use this method."
1198
+ )
1199
+ if column not in self._columns_first_idx:
1200
+ raise ValueError(f"Column '{column}' does not exist in the table.")
1201
+
1202
+ idxs = self._source_data[self._source_data[column].isin(values)].index.tolist()
1203
+ self.select_rows(idxs)