supervisely 6.73.420__py3-none-any.whl → 6.73.422__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.
- supervisely/api/api.py +10 -5
- supervisely/api/app_api.py +71 -4
- supervisely/api/module_api.py +4 -0
- supervisely/api/nn/deploy_api.py +15 -9
- supervisely/api/nn/ecosystem_models_api.py +201 -0
- supervisely/api/nn/neural_network_api.py +12 -3
- supervisely/api/project_api.py +35 -6
- supervisely/api/task_api.py +5 -1
- supervisely/app/widgets/__init__.py +8 -1
- supervisely/app/widgets/agent_selector/template.html +1 -0
- supervisely/app/widgets/deploy_model/__init__.py +0 -0
- supervisely/app/widgets/deploy_model/deploy_model.py +729 -0
- supervisely/app/widgets/dropdown_checkbox_selector/__init__.py +0 -0
- supervisely/app/widgets/dropdown_checkbox_selector/dropdown_checkbox_selector.py +87 -0
- supervisely/app/widgets/dropdown_checkbox_selector/template.html +12 -0
- supervisely/app/widgets/ecosystem_model_selector/__init__.py +0 -0
- supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +190 -0
- supervisely/app/widgets/experiment_selector/experiment_selector.py +447 -264
- supervisely/app/widgets/fast_table/fast_table.py +402 -74
- supervisely/app/widgets/fast_table/script.js +364 -96
- supervisely/app/widgets/fast_table/style.css +24 -0
- supervisely/app/widgets/fast_table/template.html +43 -3
- supervisely/app/widgets/radio_table/radio_table.py +10 -2
- supervisely/app/widgets/select/select.py +6 -4
- supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +18 -0
- supervisely/app/widgets/tabs/tabs.py +22 -6
- supervisely/app/widgets/tabs/template.html +5 -1
- supervisely/nn/artifacts/__init__.py +1 -1
- supervisely/nn/artifacts/artifacts.py +10 -2
- supervisely/nn/artifacts/detectron2.py +1 -0
- supervisely/nn/artifacts/hrda.py +1 -0
- supervisely/nn/artifacts/mmclassification.py +20 -0
- supervisely/nn/artifacts/mmdetection.py +5 -3
- supervisely/nn/artifacts/mmsegmentation.py +1 -0
- supervisely/nn/artifacts/ritm.py +1 -0
- supervisely/nn/artifacts/rtdetr.py +1 -0
- supervisely/nn/artifacts/unet.py +1 -0
- supervisely/nn/artifacts/utils.py +3 -0
- supervisely/nn/artifacts/yolov5.py +2 -0
- supervisely/nn/artifacts/yolov8.py +1 -0
- supervisely/nn/benchmark/semantic_segmentation/metric_provider.py +18 -18
- supervisely/nn/experiments.py +9 -0
- supervisely/nn/inference/gui/serving_gui_template.py +39 -13
- supervisely/nn/inference/inference.py +160 -94
- supervisely/nn/inference/predict_app/__init__.py +0 -0
- supervisely/nn/inference/predict_app/gui/__init__.py +0 -0
- supervisely/nn/inference/predict_app/gui/classes_selector.py +91 -0
- supervisely/nn/inference/predict_app/gui/gui.py +710 -0
- supervisely/nn/inference/predict_app/gui/input_selector.py +165 -0
- supervisely/nn/inference/predict_app/gui/model_selector.py +79 -0
- supervisely/nn/inference/predict_app/gui/output_selector.py +139 -0
- supervisely/nn/inference/predict_app/gui/preview.py +93 -0
- supervisely/nn/inference/predict_app/gui/settings_selector.py +184 -0
- supervisely/nn/inference/predict_app/gui/tags_selector.py +110 -0
- supervisely/nn/inference/predict_app/gui/utils.py +282 -0
- supervisely/nn/inference/predict_app/predict_app.py +184 -0
- supervisely/nn/inference/uploader.py +9 -5
- supervisely/nn/model/prediction.py +2 -0
- supervisely/nn/model/prediction_session.py +20 -3
- supervisely/nn/training/gui/gui.py +131 -44
- supervisely/nn/training/gui/model_selector.py +8 -6
- supervisely/nn/training/gui/train_val_splits_selector.py +122 -70
- supervisely/nn/training/gui/training_artifacts.py +0 -5
- supervisely/nn/training/train_app.py +161 -44
- supervisely/template/experiment/experiment.html.jinja +74 -17
- supervisely/template/experiment/experiment_generator.py +258 -112
- supervisely/template/experiment/header.html.jinja +31 -13
- supervisely/template/experiment/sly-style.css +7 -2
- {supervisely-6.73.420.dist-info → supervisely-6.73.422.dist-info}/METADATA +3 -1
- {supervisely-6.73.420.dist-info → supervisely-6.73.422.dist-info}/RECORD +74 -56
- supervisely/app/widgets/experiment_selector/style.css +0 -27
- supervisely/app/widgets/experiment_selector/template.html +0 -61
- {supervisely-6.73.420.dist-info → supervisely-6.73.422.dist-info}/LICENSE +0 -0
- {supervisely-6.73.420.dist-info → supervisely-6.73.422.dist-info}/WHEEL +0 -0
- {supervisely-6.73.420.dist-info → supervisely-6.73.422.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.420.dist-info → supervisely-6.73.422.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.
|
|
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.
|
|
173
|
+
self._selected_rows = None
|
|
127
174
|
self._selected_cell = None
|
|
128
|
-
self.
|
|
129
|
-
self.
|
|
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
|
|
167
|
-
self.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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.
|
|
213
|
-
"isCellClickable": self.
|
|
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
|
-
"
|
|
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]["
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
|
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]["
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
499
|
-
DataJson()[self.widget_id]["options"]["isRowClickable"] = self.
|
|
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.
|
|
532
|
-
DataJson()[self.widget_id]["options"]["isCellClickable"] = self.
|
|
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
|
|
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.
|
|
761
|
+
filtered_data = self._filtered_data.copy()
|
|
561
762
|
if search_value == "":
|
|
562
763
|
return filtered_data
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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.
|
|
592
|
-
self.
|
|
593
|
-
self.
|
|
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(
|
|
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
|
|
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
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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.
|
|
861
|
-
self.
|
|
862
|
-
self.
|
|
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
|
|
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)
|