supervisely 6.73.444__py3-none-any.whl → 6.73.468__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of supervisely might be problematic. Click here for more details.
- supervisely/__init__.py +24 -1
- supervisely/_utils.py +81 -0
- supervisely/annotation/json_geometries_map.py +2 -0
- supervisely/api/dataset_api.py +74 -12
- supervisely/api/entity_annotation/figure_api.py +8 -5
- supervisely/api/image_api.py +4 -0
- supervisely/api/video/video_annotation_api.py +4 -2
- supervisely/api/video/video_api.py +41 -1
- supervisely/app/__init__.py +1 -1
- supervisely/app/content.py +14 -6
- supervisely/app/fastapi/__init__.py +1 -0
- supervisely/app/fastapi/custom_static_files.py +1 -1
- supervisely/app/fastapi/multi_user.py +88 -0
- supervisely/app/fastapi/subapp.py +88 -42
- supervisely/app/fastapi/websocket.py +77 -9
- supervisely/app/singleton.py +21 -0
- supervisely/app/v1/app_service.py +18 -2
- supervisely/app/v1/constants.py +7 -1
- supervisely/app/widgets/card/card.py +20 -0
- supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
- supervisely/app/widgets/dialog/dialog.py +12 -0
- supervisely/app/widgets/dialog/template.html +2 -1
- supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
- supervisely/app/widgets/fast_table/fast_table.py +121 -31
- supervisely/app/widgets/fast_table/template.html +1 -1
- supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
- supervisely/app/widgets/radio_tabs/template.html +1 -0
- supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
- supervisely/app/widgets/table/table.py +68 -13
- supervisely/app/widgets/tree_select/tree_select.py +2 -0
- supervisely/convert/image/csv/csv_converter.py +24 -15
- supervisely/convert/video/video_converter.py +2 -2
- supervisely/geometry/polyline_3d.py +110 -0
- supervisely/io/env.py +76 -1
- supervisely/nn/inference/cache.py +37 -17
- supervisely/nn/inference/inference.py +667 -114
- supervisely/nn/inference/inference_request.py +15 -8
- supervisely/nn/inference/predict_app/gui/classes_selector.py +81 -12
- supervisely/nn/inference/predict_app/gui/gui.py +676 -488
- supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
- supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
- supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
- supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
- supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
- supervisely/nn/inference/predict_app/gui/utils.py +236 -119
- supervisely/nn/inference/predict_app/predict_app.py +2 -2
- supervisely/nn/inference/session.py +43 -35
- supervisely/nn/model/model_api.py +9 -0
- supervisely/nn/model/prediction_session.py +8 -7
- supervisely/nn/prediction_dto.py +7 -0
- supervisely/nn/tracker/base_tracker.py +11 -1
- supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
- supervisely/nn/tracker/botsort_tracker.py +14 -7
- supervisely/nn/tracker/visualize.py +70 -72
- supervisely/nn/training/gui/train_val_splits_selector.py +52 -31
- supervisely/nn/training/train_app.py +10 -5
- supervisely/project/project.py +9 -1
- supervisely/video/sampling.py +39 -20
- supervisely/video/video.py +41 -12
- supervisely/volume/stl_converter.py +2 -0
- supervisely/worker_api/agent_rpc.py +24 -1
- supervisely/worker_api/rpc_servicer.py +31 -7
- {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/METADATA +14 -11
- {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/RECORD +68 -66
- {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/LICENSE +0 -0
- {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/WHEEL +0 -0
- {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/top_level.txt +0 -0
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
:title=data.{{{widget.widget_id}}}.title
|
|
3
3
|
:size=data.{{{widget.widget_id}}}.size
|
|
4
4
|
:visible.sync="state.{{{widget.widget_id}}}.visible"
|
|
5
|
+
@close="post('/{{{widget.widget_id}}}/close_cb');"
|
|
5
6
|
>
|
|
6
7
|
<div>
|
|
7
8
|
{{{widget._content}}}
|
|
8
9
|
</div>
|
|
9
|
-
</el-dialog>
|
|
10
|
+
</el-dialog>
|
|
@@ -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
|
|
|
@@ -221,6 +221,11 @@ class FastTable(Widget):
|
|
|
221
221
|
self._validate_input_data(data)
|
|
222
222
|
self._source_data = self._prepare_input_data(data)
|
|
223
223
|
|
|
224
|
+
# Initialize filtered and searched data for proper initialization
|
|
225
|
+
self._filtered_data = self._filter(self._filter_value)
|
|
226
|
+
self._searched_data = self._search(self._search_str)
|
|
227
|
+
self._sorted_data = self._sort_table_data(self._searched_data)
|
|
228
|
+
|
|
224
229
|
# prepare parsed_source_data, sliced_data, parsed_active_data
|
|
225
230
|
(
|
|
226
231
|
self._parsed_source_data,
|
|
@@ -265,7 +270,7 @@ class FastTable(Widget):
|
|
|
265
270
|
self._sliced_data = self._slice_table_data(self._sorted_data, actual_page=self._active_page)
|
|
266
271
|
self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
|
|
267
272
|
StateJson().send_changes()
|
|
268
|
-
DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
|
|
273
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
269
274
|
DataJson()[self.widget_id]["total"] = self._rows_total
|
|
270
275
|
DataJson().send_changes()
|
|
271
276
|
StateJson()["reactToChanges"] = True
|
|
@@ -295,7 +300,7 @@ class FastTable(Widget):
|
|
|
295
300
|
:rtype: Dict[str, Any]
|
|
296
301
|
"""
|
|
297
302
|
return {
|
|
298
|
-
"data": self._parsed_active_data["data"],
|
|
303
|
+
"data": list(self._parsed_active_data["data"]),
|
|
299
304
|
"columns": self._parsed_source_data["columns"],
|
|
300
305
|
"projectMeta": self._project_meta,
|
|
301
306
|
"columnsOptions": self._columns_options,
|
|
@@ -307,7 +312,7 @@ class FastTable(Widget):
|
|
|
307
312
|
"isRadio": self._is_radio,
|
|
308
313
|
"isRowSelectable": self._is_selectable,
|
|
309
314
|
"maxSelectedRows": self._max_selected_rows,
|
|
310
|
-
"searchPosition": self._search_position
|
|
315
|
+
"searchPosition": self._search_position,
|
|
311
316
|
},
|
|
312
317
|
"pageSize": self._page_size,
|
|
313
318
|
"showHeader": self._show_header,
|
|
@@ -490,7 +495,7 @@ class FastTable(Widget):
|
|
|
490
495
|
self._sort_column_idx = None
|
|
491
496
|
self._sort_order = sort.get("order", None)
|
|
492
497
|
self._page_size = init_options.pop("pageSize", 10)
|
|
493
|
-
DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
|
|
498
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
494
499
|
DataJson()[self.widget_id]["columns"] = self._parsed_active_data["columns"]
|
|
495
500
|
DataJson()[self.widget_id]["columnsOptions"] = self._columns_options
|
|
496
501
|
DataJson()[self.widget_id]["options"] = init_options
|
|
@@ -519,7 +524,7 @@ class FastTable(Widget):
|
|
|
519
524
|
self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
|
|
520
525
|
self._parsed_source_data = self._unpack_pandas_table_data(self._source_data)
|
|
521
526
|
self._rows_total = len(self._parsed_source_data["data"])
|
|
522
|
-
DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
|
|
527
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
523
528
|
DataJson()[self.widget_id]["columns"] = self._parsed_active_data["columns"]
|
|
524
529
|
DataJson()[self.widget_id]["total"] = len(self._source_data)
|
|
525
530
|
DataJson().send_changes()
|
|
@@ -578,10 +583,17 @@ class FastTable(Widget):
|
|
|
578
583
|
:rtype: pd.DataFrame
|
|
579
584
|
"""
|
|
580
585
|
if active_page is True:
|
|
581
|
-
|
|
586
|
+
# Return sliced data directly from source to preserve None/NaN values
|
|
587
|
+
packed_data = self._sliced_data.copy()
|
|
588
|
+
# Reset column names to first level only
|
|
589
|
+
if isinstance(packed_data.columns, pd.MultiIndex):
|
|
590
|
+
packed_data.columns = packed_data.columns.get_level_values("first")
|
|
582
591
|
else:
|
|
583
|
-
|
|
584
|
-
|
|
592
|
+
# Return source data directly to preserve None/NaN values
|
|
593
|
+
packed_data = self._source_data.copy()
|
|
594
|
+
# Reset column names to first level only
|
|
595
|
+
if isinstance(packed_data.columns, pd.MultiIndex):
|
|
596
|
+
packed_data.columns = packed_data.columns.get_level_values("first")
|
|
585
597
|
return packed_data
|
|
586
598
|
|
|
587
599
|
def clear_selection(self) -> None:
|
|
@@ -621,8 +633,12 @@ class FastTable(Widget):
|
|
|
621
633
|
rows = []
|
|
622
634
|
for row in selected_rows:
|
|
623
635
|
row_index = row["idx"]
|
|
624
|
-
|
|
625
|
-
|
|
636
|
+
if row_index is None:
|
|
637
|
+
continue
|
|
638
|
+
# Get original data from source_data to preserve None/NaN values
|
|
639
|
+
try:
|
|
640
|
+
row_data = self._source_data.loc[row_index].values.tolist()
|
|
641
|
+
except (KeyError, IndexError):
|
|
626
642
|
continue
|
|
627
643
|
rows.append(self.ClickedRow(row_data, row_index))
|
|
628
644
|
return rows
|
|
@@ -633,8 +649,12 @@ class FastTable(Widget):
|
|
|
633
649
|
if clicked_row is None:
|
|
634
650
|
return None
|
|
635
651
|
row_index = clicked_row["idx"]
|
|
636
|
-
|
|
637
|
-
|
|
652
|
+
if row_index is None:
|
|
653
|
+
return None
|
|
654
|
+
# Get original data from source_data to preserve None/NaN values
|
|
655
|
+
try:
|
|
656
|
+
row = self._source_data.loc[row_index].values.tolist()
|
|
657
|
+
except (KeyError, IndexError):
|
|
638
658
|
return None
|
|
639
659
|
return self.ClickedRow(row, row_index)
|
|
640
660
|
|
|
@@ -644,15 +664,19 @@ class FastTable(Widget):
|
|
|
644
664
|
:return: Selected cell
|
|
645
665
|
:rtype: ClickedCell
|
|
646
666
|
"""
|
|
647
|
-
cell_data = StateJson()[self.widget_id]["
|
|
667
|
+
cell_data = StateJson()[self.widget_id]["selectedCell"]
|
|
648
668
|
if cell_data is None:
|
|
649
669
|
return None
|
|
650
670
|
row_index = cell_data["idx"]
|
|
651
|
-
row = cell_data["row"]
|
|
652
671
|
column_index = cell_data["column"]
|
|
672
|
+
if column_index is None or row_index is None:
|
|
673
|
+
return None
|
|
653
674
|
column_name = self._columns_first_idx[column_index]
|
|
654
|
-
|
|
655
|
-
|
|
675
|
+
# Get original data from source_data to preserve None/NaN values
|
|
676
|
+
try:
|
|
677
|
+
row = self._source_data.loc[row_index].values.tolist()
|
|
678
|
+
column_value = row[column_index]
|
|
679
|
+
except (KeyError, IndexError):
|
|
656
680
|
return None
|
|
657
681
|
return self.ClickedCell(row, column_index, row_index, column_name, column_value)
|
|
658
682
|
|
|
@@ -715,7 +739,25 @@ class FastTable(Widget):
|
|
|
715
739
|
self._parsed_active_data,
|
|
716
740
|
) = self._prepare_working_data()
|
|
717
741
|
self._rows_total = len(self._parsed_source_data["data"])
|
|
718
|
-
DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
|
|
742
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
743
|
+
DataJson()[self.widget_id]["total"] = self._rows_total
|
|
744
|
+
DataJson().send_changes()
|
|
745
|
+
self._maybe_update_selected_row()
|
|
746
|
+
|
|
747
|
+
def add_rows(self, rows: List):
|
|
748
|
+
for row in rows:
|
|
749
|
+
self._validate_table_sizes(row)
|
|
750
|
+
self._validate_row_values_types(row)
|
|
751
|
+
self._source_data = pd.concat(
|
|
752
|
+
[self._source_data, pd.DataFrame(rows, columns=self._source_data.columns)]
|
|
753
|
+
).reset_index(drop=True)
|
|
754
|
+
(
|
|
755
|
+
self._parsed_source_data,
|
|
756
|
+
self._sliced_data,
|
|
757
|
+
self._parsed_active_data,
|
|
758
|
+
) = self._prepare_working_data()
|
|
759
|
+
self._rows_total = len(self._parsed_source_data["data"])
|
|
760
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
719
761
|
DataJson()[self.widget_id]["total"] = self._rows_total
|
|
720
762
|
DataJson().send_changes()
|
|
721
763
|
self._maybe_update_selected_row()
|
|
@@ -743,7 +785,7 @@ class FastTable(Widget):
|
|
|
743
785
|
self._parsed_active_data,
|
|
744
786
|
) = self._prepare_working_data()
|
|
745
787
|
self._rows_total = len(self._parsed_source_data["data"])
|
|
746
|
-
DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
|
|
788
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
747
789
|
DataJson()[self.widget_id]["total"] = self._rows_total
|
|
748
790
|
self._maybe_update_selected_row()
|
|
749
791
|
return popped_row
|
|
@@ -755,7 +797,7 @@ class FastTable(Widget):
|
|
|
755
797
|
self._sliced_data = pd.DataFrame(columns=self._columns_first_idx)
|
|
756
798
|
self._parsed_active_data = {"data": [], "columns": []}
|
|
757
799
|
self._rows_total = 0
|
|
758
|
-
DataJson()[self.widget_id]["data"] =
|
|
800
|
+
DataJson()[self.widget_id]["data"] = {}
|
|
759
801
|
DataJson()[self.widget_id]["total"] = 0
|
|
760
802
|
DataJson().send_changes()
|
|
761
803
|
self._maybe_update_selected_row()
|
|
@@ -856,7 +898,11 @@ class FastTable(Widget):
|
|
|
856
898
|
self._refresh()
|
|
857
899
|
|
|
858
900
|
def _default_search_function(self, data: pd.DataFrame, search_value: str) -> pd.DataFrame:
|
|
859
|
-
|
|
901
|
+
# Use map() for pandas >= 2.1.0, fallback to applymap() for older versions
|
|
902
|
+
if hasattr(pd.DataFrame, "map"):
|
|
903
|
+
data = data[data.map(lambda x: search_value in str(x)).any(axis=1)]
|
|
904
|
+
else:
|
|
905
|
+
data = data[data.applymap(lambda x: search_value in str(x)).any(axis=1)]
|
|
860
906
|
return data
|
|
861
907
|
|
|
862
908
|
def _search(self, search_value: str) -> pd.DataFrame:
|
|
@@ -867,8 +913,14 @@ class FastTable(Widget):
|
|
|
867
913
|
:return: Filtered data
|
|
868
914
|
:rtype: pd.DataFrame
|
|
869
915
|
"""
|
|
870
|
-
filtered_data
|
|
916
|
+
# Use filtered_data if available, otherwise use source_data directly
|
|
917
|
+
if self._filtered_data is not None:
|
|
918
|
+
filtered_data = self._filtered_data.copy()
|
|
919
|
+
else:
|
|
920
|
+
filtered_data = self._source_data.copy()
|
|
921
|
+
|
|
871
922
|
if search_value == "":
|
|
923
|
+
self._search_str = search_value
|
|
872
924
|
return filtered_data
|
|
873
925
|
if self._search_str != search_value:
|
|
874
926
|
self._active_page = 1
|
|
@@ -894,7 +946,24 @@ class FastTable(Widget):
|
|
|
894
946
|
else:
|
|
895
947
|
ascending = False
|
|
896
948
|
try:
|
|
897
|
-
|
|
949
|
+
column = data.columns[column_idx]
|
|
950
|
+
# Try to convert to numeric for proper sorting
|
|
951
|
+
numeric_column = pd.to_numeric(data[column], errors="coerce")
|
|
952
|
+
|
|
953
|
+
# Check if column contains numeric data (has at least one non-NaN numeric value)
|
|
954
|
+
if numeric_column.notna().sum() > 0:
|
|
955
|
+
# Create temporary column for sorting
|
|
956
|
+
data_copy = data.copy()
|
|
957
|
+
data_copy["_sort_key"] = numeric_column
|
|
958
|
+
# Sort by numeric values with NaN at the end
|
|
959
|
+
data_copy = data_copy.sort_values(
|
|
960
|
+
by="_sort_key", ascending=ascending, na_position="last"
|
|
961
|
+
)
|
|
962
|
+
# Remove temporary column and return original data in sorted order
|
|
963
|
+
data = data.loc[data_copy.index]
|
|
964
|
+
else:
|
|
965
|
+
# Sort as strings with NaN values at the end
|
|
966
|
+
data = data.sort_values(by=column, ascending=ascending, na_position="last")
|
|
898
967
|
except IndexError as e:
|
|
899
968
|
e.args = (
|
|
900
969
|
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}",
|
|
@@ -925,7 +994,7 @@ class FastTable(Widget):
|
|
|
925
994
|
self._sorted_data = self._sort_table_data(self._searched_data)
|
|
926
995
|
self._sliced_data = self._slice_table_data(self._sorted_data, actual_page=self._active_page)
|
|
927
996
|
self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
|
|
928
|
-
DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
|
|
997
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
929
998
|
DataJson()[self.widget_id]["total"] = self._rows_total
|
|
930
999
|
self._maybe_update_selected_row()
|
|
931
1000
|
StateJson().send_changes()
|
|
@@ -1030,12 +1099,21 @@ class FastTable(Widget):
|
|
|
1030
1099
|
def _get_pandas_unpacked_data(self, data: pd.DataFrame) -> dict:
|
|
1031
1100
|
if not isinstance(data, pd.DataFrame):
|
|
1032
1101
|
raise TypeError("Cannot parse input data, please use Pandas Dataframe as input data")
|
|
1033
|
-
|
|
1034
|
-
#
|
|
1102
|
+
|
|
1103
|
+
# Create a copy for frontend display to avoid modifying source data
|
|
1104
|
+
display_data = data.copy()
|
|
1105
|
+
# Replace NaN and None with empty string only for display
|
|
1106
|
+
display_data = display_data.replace({np.nan: "", None: ""})
|
|
1107
|
+
|
|
1108
|
+
# Handle MultiIndex columns - extract only the first level
|
|
1109
|
+
if isinstance(display_data.columns, pd.MultiIndex):
|
|
1110
|
+
columns = display_data.columns.get_level_values("first").tolist()
|
|
1111
|
+
else:
|
|
1112
|
+
columns = display_data.columns.to_list()
|
|
1035
1113
|
|
|
1036
1114
|
unpacked_data = {
|
|
1037
|
-
"columns":
|
|
1038
|
-
"data":
|
|
1115
|
+
"columns": columns,
|
|
1116
|
+
"data": display_data.values.tolist(),
|
|
1039
1117
|
}
|
|
1040
1118
|
return unpacked_data
|
|
1041
1119
|
|
|
@@ -1206,7 +1284,7 @@ class FastTable(Widget):
|
|
|
1206
1284
|
|
|
1207
1285
|
self._sliced_data = self._slice_table_data(self._sorted_data, actual_page=self._active_page)
|
|
1208
1286
|
self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
|
|
1209
|
-
DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
|
|
1287
|
+
DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
|
|
1210
1288
|
DataJson()[self.widget_id]["total"] = self._rows_total
|
|
1211
1289
|
DataJson().send_changes()
|
|
1212
1290
|
StateJson().send_changes()
|
|
@@ -1275,6 +1353,7 @@ class FastTable(Widget):
|
|
|
1275
1353
|
|
|
1276
1354
|
def select_row_by_value(self, column, value: Any):
|
|
1277
1355
|
"""Selects a row by value in a specific column.
|
|
1356
|
+
The first column with the given name is used in case of duplicate column names.
|
|
1278
1357
|
|
|
1279
1358
|
:param column: Column name to filter by
|
|
1280
1359
|
:type column: str
|
|
@@ -1288,7 +1367,12 @@ class FastTable(Widget):
|
|
|
1288
1367
|
if column not in self._columns_first_idx:
|
|
1289
1368
|
raise ValueError(f"Column '{column}' does not exist in the table.")
|
|
1290
1369
|
|
|
1291
|
-
|
|
1370
|
+
# Find the first column index with this name (in case of duplicates)
|
|
1371
|
+
column_idx = self._columns_first_idx.index(column)
|
|
1372
|
+
column_tuple = self._source_data.columns[column_idx]
|
|
1373
|
+
|
|
1374
|
+
# Use column tuple to access the specific column
|
|
1375
|
+
idx = self._source_data[self._source_data[column_tuple] == value].index.tolist()
|
|
1292
1376
|
if not idx:
|
|
1293
1377
|
raise ValueError(f"No rows found with {column} = {value}.")
|
|
1294
1378
|
if len(idx) > 1:
|
|
@@ -1299,6 +1383,7 @@ class FastTable(Widget):
|
|
|
1299
1383
|
|
|
1300
1384
|
def select_rows_by_value(self, column, values: List):
|
|
1301
1385
|
"""Selects rows by value in a specific column.
|
|
1386
|
+
The first column with the given name is used in case of duplicate column names.
|
|
1302
1387
|
|
|
1303
1388
|
:param column: Column name to filter by
|
|
1304
1389
|
:type column: str
|
|
@@ -1312,7 +1397,12 @@ class FastTable(Widget):
|
|
|
1312
1397
|
if column not in self._columns_first_idx:
|
|
1313
1398
|
raise ValueError(f"Column '{column}' does not exist in the table.")
|
|
1314
1399
|
|
|
1315
|
-
|
|
1400
|
+
# Find the first column index with this name (in case of duplicates)
|
|
1401
|
+
column_idx = self._columns_first_idx.index(column)
|
|
1402
|
+
column_tuple = self._source_data.columns[column_idx]
|
|
1403
|
+
|
|
1404
|
+
# Use column tuple to access the specific column
|
|
1405
|
+
idxs = self._source_data[self._source_data[column_tuple].isin(values)].index.tolist()
|
|
1316
1406
|
self.select_rows(idxs)
|
|
1317
1407
|
|
|
1318
1408
|
def _read_custom_columns(self, columns: List[Union[str, tuple]]) -> None:
|
|
@@ -1335,4 +1425,4 @@ class FastTable(Widget):
|
|
|
1335
1425
|
else:
|
|
1336
1426
|
raise TypeError(f"Column name must be a string or a tuple, got {type(col)}")
|
|
1337
1427
|
|
|
1338
|
-
self._validate_sort_attrs()
|
|
1428
|
+
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
|
-
|
|
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()
|
|
@@ -5,6 +5,7 @@ from supervisely.api.api import Api
|
|
|
5
5
|
from supervisely.app.widgets import Widget
|
|
6
6
|
from supervisely.app.widgets.checkbox.checkbox import Checkbox
|
|
7
7
|
from supervisely.app.widgets.container.container import Container
|
|
8
|
+
from supervisely.app.widgets.field.field import Field
|
|
8
9
|
from supervisely.app.widgets.select.select import Select
|
|
9
10
|
from supervisely.app.widgets.tree_select.tree_select import TreeSelect
|
|
10
11
|
from supervisely.project.project_type import ProjectType
|
|
@@ -97,6 +98,7 @@ class SelectDatasetTree(Widget):
|
|
|
97
98
|
widget_id: Union[str, None] = None,
|
|
98
99
|
show_select_all_datasets_checkbox: bool = True,
|
|
99
100
|
width: int = 193,
|
|
101
|
+
show_selectors_labels: bool = False,
|
|
100
102
|
):
|
|
101
103
|
self._api = Api.from_env()
|
|
102
104
|
|
|
@@ -114,11 +116,29 @@ class SelectDatasetTree(Widget):
|
|
|
114
116
|
# Using environment variables to set the default values if they are not provided.
|
|
115
117
|
self._project_id = project_id or env.project_id(raise_not_found=False)
|
|
116
118
|
self._dataset_id = default_id or env.dataset_id(raise_not_found=False)
|
|
119
|
+
if self._project_id:
|
|
120
|
+
project_info = self._api.project.get_info_by_id(self._project_id)
|
|
121
|
+
if allowed_project_types is not None:
|
|
122
|
+
allowed_values = []
|
|
123
|
+
if not isinstance(allowed_project_types, list):
|
|
124
|
+
allowed_project_types = [allowed_project_types]
|
|
125
|
+
|
|
126
|
+
for pt in allowed_project_types:
|
|
127
|
+
if isinstance(pt, (ProjectType, str)):
|
|
128
|
+
allowed_values.append(str(pt))
|
|
129
|
+
|
|
130
|
+
if project_info.type not in allowed_values:
|
|
131
|
+
self._project_id = None
|
|
117
132
|
|
|
118
133
|
self._multiselect = multiselect
|
|
119
134
|
self._compact = compact
|
|
120
135
|
self._append_to_body = append_to_body
|
|
121
136
|
|
|
137
|
+
# User-defined callbacks
|
|
138
|
+
self._team_changed_callbacks = []
|
|
139
|
+
self._workspace_changed_callbacks = []
|
|
140
|
+
self._project_changed_callbacks = []
|
|
141
|
+
|
|
122
142
|
# Extract values from Enum to match the .type property of the ProjectInfo object.
|
|
123
143
|
self._project_types = None
|
|
124
144
|
if allowed_project_types is not None:
|
|
@@ -160,6 +180,7 @@ class SelectDatasetTree(Widget):
|
|
|
160
180
|
if show_select_all_datasets_checkbox:
|
|
161
181
|
self._create_select_all_datasets_checkbox(select_all_datasets)
|
|
162
182
|
|
|
183
|
+
self._show_selectors_labels = show_selectors_labels
|
|
163
184
|
# Group the selectors and the dataset selector into a container.
|
|
164
185
|
self._content = Container(self._widgets)
|
|
165
186
|
super().__init__(widget_id=widget_id, file_path=__file__)
|
|
@@ -308,8 +329,30 @@ class SelectDatasetTree(Widget):
|
|
|
308
329
|
"""
|
|
309
330
|
if not self._multiselect:
|
|
310
331
|
raise ValueError("This method can only be called when multiselect is enabled.")
|
|
332
|
+
self._select_all_datasets_checkbox.uncheck()
|
|
311
333
|
self._select_dataset.set_selected_by_id(dataset_ids)
|
|
312
334
|
|
|
335
|
+
def team_changed(self, func: Callable) -> Callable:
|
|
336
|
+
"""Decorator to set the callback function for the team changed event."""
|
|
337
|
+
if self._compact:
|
|
338
|
+
raise ValueError("callback 'team_changed' is not available in compact mode.")
|
|
339
|
+
self._team_changed_callbacks.append(func)
|
|
340
|
+
return func
|
|
341
|
+
|
|
342
|
+
def workspace_changed(self, func: Callable) -> Callable:
|
|
343
|
+
"""Decorator to set the callback function for the workspace changed event."""
|
|
344
|
+
if self._compact:
|
|
345
|
+
raise ValueError("callback 'workspace_changed' is not available in compact mode.")
|
|
346
|
+
self._workspace_changed_callbacks.append(func)
|
|
347
|
+
return func
|
|
348
|
+
|
|
349
|
+
def project_changed(self, func: Callable) -> Callable:
|
|
350
|
+
"""Decorator to set the callback function for the project changed event."""
|
|
351
|
+
if self._compact:
|
|
352
|
+
raise ValueError("callback 'project_changed' is not available in compact mode.")
|
|
353
|
+
self._project_changed_callbacks.append(func)
|
|
354
|
+
return func
|
|
355
|
+
|
|
313
356
|
def value_changed(self, func: Callable) -> Callable:
|
|
314
357
|
"""Decorator to set the callback function for the value changed event.
|
|
315
358
|
|
|
@@ -353,13 +396,13 @@ class SelectDatasetTree(Widget):
|
|
|
353
396
|
|
|
354
397
|
if checked:
|
|
355
398
|
self._select_dataset.select_all()
|
|
356
|
-
self.
|
|
399
|
+
self._select_dataset_field.hide()
|
|
357
400
|
else:
|
|
358
401
|
self._select_dataset.clear_selected()
|
|
359
|
-
self.
|
|
402
|
+
self._select_dataset_field.show()
|
|
360
403
|
|
|
361
404
|
if select_all_datasets:
|
|
362
|
-
self.
|
|
405
|
+
self._select_dataset_field.hide()
|
|
363
406
|
select_all_datasets_checkbox.check()
|
|
364
407
|
|
|
365
408
|
self._widgets.append(select_all_datasets_checkbox)
|
|
@@ -390,9 +433,10 @@ class SelectDatasetTree(Widget):
|
|
|
390
433
|
self._select_dataset.set_selected_by_id(self._dataset_id)
|
|
391
434
|
if select_all_datasets:
|
|
392
435
|
self._select_dataset.select_all()
|
|
436
|
+
self._select_dataset_field = Field(self._select_dataset, title="Dataset")
|
|
393
437
|
|
|
394
438
|
# Adding the dataset selector to the list of widgets to be added to the container.
|
|
395
|
-
self._widgets.append(self.
|
|
439
|
+
self._widgets.append(self._select_dataset_field)
|
|
396
440
|
|
|
397
441
|
def _create_selectors(self, team_is_selectable: bool, workspace_is_selectable: bool):
|
|
398
442
|
"""Create the team, workspace, and project selectors.
|
|
@@ -412,6 +456,9 @@ class SelectDatasetTree(Widget):
|
|
|
412
456
|
self._select_workspace.set(items=self._get_select_items(team_id=team_id))
|
|
413
457
|
self._team_id = team_id
|
|
414
458
|
|
|
459
|
+
for callback in self._team_changed_callbacks:
|
|
460
|
+
callback(team_id)
|
|
461
|
+
|
|
415
462
|
def workspace_selector_handler(workspace_id: int):
|
|
416
463
|
"""Handler function for the event when the workspace selector value changes.
|
|
417
464
|
|
|
@@ -421,6 +468,9 @@ class SelectDatasetTree(Widget):
|
|
|
421
468
|
self._select_project.set(items=self._get_select_items(workspace_id=workspace_id))
|
|
422
469
|
self._workspace_id = workspace_id
|
|
423
470
|
|
|
471
|
+
for callback in self._workspace_changed_callbacks:
|
|
472
|
+
callback(workspace_id)
|
|
473
|
+
|
|
424
474
|
def project_selector_handler(project_id: int):
|
|
425
475
|
"""Handler function for the event when the project selector value changes.
|
|
426
476
|
|
|
@@ -435,7 +485,10 @@ class SelectDatasetTree(Widget):
|
|
|
435
485
|
and self._select_all_datasets_checkbox.is_checked()
|
|
436
486
|
):
|
|
437
487
|
self._select_dataset.select_all()
|
|
438
|
-
self.
|
|
488
|
+
self._select_dataset_field.hide()
|
|
489
|
+
|
|
490
|
+
for callback in self._project_changed_callbacks:
|
|
491
|
+
callback(project_id)
|
|
439
492
|
|
|
440
493
|
self._select_team = Select(
|
|
441
494
|
items=self._get_select_items(),
|
|
@@ -446,6 +499,7 @@ class SelectDatasetTree(Widget):
|
|
|
446
499
|
self._select_team.set_value(self._team_id)
|
|
447
500
|
if not team_is_selectable:
|
|
448
501
|
self._select_team.disable()
|
|
502
|
+
self._select_team_field = Field(self._select_team, title="Team")
|
|
449
503
|
|
|
450
504
|
self._select_workspace = Select(
|
|
451
505
|
items=self._get_select_items(team_id=self._team_id),
|
|
@@ -456,6 +510,7 @@ class SelectDatasetTree(Widget):
|
|
|
456
510
|
self._select_workspace.set_value(self._workspace_id)
|
|
457
511
|
if not workspace_is_selectable:
|
|
458
512
|
self._select_workspace.disable()
|
|
513
|
+
self._select_workspace_field = Field(self._select_workspace, title="Workspace")
|
|
459
514
|
|
|
460
515
|
self._select_project = Select(
|
|
461
516
|
items=self._get_select_items(workspace_id=self._workspace_id),
|
|
@@ -464,14 +519,17 @@ class SelectDatasetTree(Widget):
|
|
|
464
519
|
width_px=self._width,
|
|
465
520
|
)
|
|
466
521
|
self._select_project.set_value(self._project_id)
|
|
522
|
+
self._select_project_field = Field(self._select_project, title="Project")
|
|
467
523
|
|
|
468
|
-
# Register the event handlers.
|
|
524
|
+
# Register the event handlers._select_project
|
|
469
525
|
self._select_team.value_changed(team_selector_handler)
|
|
470
526
|
self._select_workspace.value_changed(workspace_selector_handler)
|
|
471
527
|
self._select_project.value_changed(project_selector_handler)
|
|
472
528
|
|
|
473
529
|
# Adding widgets to the list, so they can be added to the container.
|
|
474
|
-
self._widgets.extend(
|
|
530
|
+
self._widgets.extend(
|
|
531
|
+
[self._select_team_field, self._select_workspace_field, self._select_project_field]
|
|
532
|
+
)
|
|
475
533
|
|
|
476
534
|
def _get_select_items(self, **kwargs) -> List[Select.Item]:
|
|
477
535
|
"""Get the list of items for the team, workspace, and project selectors.
|