supervisely 6.73.431__py3-none-any.whl → 6.73.433__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.
@@ -185,6 +185,7 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
185
185
  filters: Optional[List[Dict[str, str]]] = None,
186
186
  recursive: Optional[bool] = False,
187
187
  parent_id: Optional[int] = None,
188
+ include_custom_data: Optional[bool] = False,
188
189
  ) -> List[DatasetInfo]:
189
190
  """
190
191
  Returns list of dataset in the given project, or list of nested datasets
@@ -200,6 +201,9 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
200
201
  :type recursive: bool, optional
201
202
  :param parent_id: Parent Dataset ID. If set to None, the search will be performed at the top level of the Project,
202
203
  otherwise the search will be performed in the specified Dataset.
204
+ :type parent_id: Union[int, None], optional
205
+ :param include_custom_data: If True, the response will include the `custom_data` field for each Dataset.
206
+ :type include_custom_data: bool, optional
203
207
  :return: List of all Datasets with information for the given Project. See :class:`info_sequence<info_sequence>`
204
208
  :rtype: :class:`List[DatasetInfo]`
205
209
  :Usage example:
@@ -246,14 +250,16 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
246
250
  filters.append({"field": ApiField.PARENT_ID, "operator": "=", "value": parent_id})
247
251
  recursive = True
248
252
 
249
- return self.get_list_all_pages(
250
- "datasets.list",
251
- {
252
- ApiField.PROJECT_ID: project_id,
253
- ApiField.FILTER: filters,
254
- ApiField.RECURSIVE: recursive,
255
- },
256
- )
253
+ method = "datasets.list"
254
+ data = {
255
+ ApiField.PROJECT_ID: project_id,
256
+ ApiField.FILTER: filters,
257
+ ApiField.RECURSIVE: recursive,
258
+ }
259
+ if include_custom_data:
260
+ data[ApiField.EXTRA_FIELDS] = [ApiField.CUSTOM_DATA]
261
+
262
+ return self.get_list_all_pages(method, data)
257
263
 
258
264
  def get_info_by_id(self, id: int, raise_error: Optional[bool] = False) -> DatasetInfo:
259
265
  """
@@ -304,6 +310,7 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
304
310
  description: Optional[str] = "",
305
311
  change_name_if_conflict: Optional[bool] = False,
306
312
  parent_id: Optional[int] = None,
313
+ custom_data: Optional[Dict[Any, Any]] = None,
307
314
  ) -> DatasetInfo:
308
315
  """
309
316
  Create Dataset with given name in the given Project.
@@ -318,6 +325,9 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
318
325
  :type change_name_if_conflict: bool, optional
319
326
  :param parent_id: Parent Dataset ID. If set to None, then the Dataset will be created at
320
327
  the top level of the Project, otherwise the Dataset will be created in a specified Dataset.
328
+ :type parent_id: Union[int, None]
329
+ :param custom_data: Custom data to store in the Dataset.
330
+ :type custom_data: Dict[Any, Any], optional
321
331
  :return: Information about Dataset. See :class:`info_sequence<info_sequence>`
322
332
  :rtype: :class:`DatasetInfo`
323
333
  :Usage example:
@@ -345,15 +355,16 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
345
355
  change_name_if_conflict=change_name_if_conflict,
346
356
  parent_id=parent_id,
347
357
  )
348
- response = self._api.post(
349
- "datasets.add",
350
- {
351
- ApiField.PROJECT_ID: project_id,
352
- ApiField.NAME: effective_name,
353
- ApiField.DESCRIPTION: description,
354
- ApiField.PARENT_ID: parent_id,
355
- },
356
- )
358
+ method = "datasets.add"
359
+ payload = {
360
+ ApiField.PROJECT_ID: project_id,
361
+ ApiField.NAME: effective_name,
362
+ ApiField.DESCRIPTION: description,
363
+ ApiField.PARENT_ID: parent_id,
364
+ }
365
+ if custom_data is not None:
366
+ payload[ApiField.CUSTOM_DATA] = custom_data
367
+ response = self._api.post(method, payload)
357
368
  return self._convert_json_info(response.json())
358
369
 
359
370
  def get_or_create(
@@ -564,6 +575,7 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
564
575
  new_dataset_name,
565
576
  dataset.description,
566
577
  change_name_if_conflict=change_name_if_conflict,
578
+ custom_data=dataset.custom_data,
567
579
  )
568
580
  items_api.copy_batch(
569
581
  new_dataset.id, src_item_ids, change_name_if_conflict, with_annotations
@@ -797,6 +809,7 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
797
809
  sort_order: Optional[str] = None,
798
810
  per_page: Optional[int] = None,
799
811
  page: Union[int, Literal["all"]] = "all",
812
+ include_custom_data: Optional[bool] = False,
800
813
  ) -> dict:
801
814
  """
802
815
  List all available datasets from all available teams for the user that match the specified filtering criteria.
@@ -807,22 +820,20 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
807
820
  - 'operator': Takes values '=', 'eq', '!=', 'not', 'in', '!in', '>', 'gt', '>=', 'gte', '<', 'lt', '<=', 'lte'
808
821
  - 'value': Takes on values according to the meaning of 'field' or null
809
822
  :type filters: List[Dict[str, str]], optional
810
-
811
823
  :param sort: Specifies by which parameter to sort the project list.
812
824
  Takes values 'id', 'name', 'size', 'createdAt', 'updatedAt'
813
825
  :type sort: str, optional
814
-
815
826
  :param sort_order: Determines which value to list from.
816
827
  :type sort_order: str, optional
817
-
818
828
  :param per_page: Number of first items found to be returned.
819
829
  'None' will return the first page with a default size of 20000 datasets.
820
830
  :type per_page: int, optional
821
-
822
831
  :param page: Page number, used to retrieve the following items if the number of them found is more than per_page.
823
832
  The default value is 'all', which retrieves all available datasets.
824
833
  'None' will return the first page with datasets, the amount of which is set in param 'per_page'.
825
834
  :type page: Union[int, Literal["all"]], optional
835
+ :param include_custom_data: If True, the response will include the `custom_data` field for each Dataset.
836
+ :type include_custom_data: bool, optional
826
837
 
827
838
  :return: Search response information and 'DatasetInfo' of all datasets that are searched by a given criterion.
828
839
  :rtype: dict
@@ -899,6 +910,8 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
899
910
  request_body[ApiField.PER_PAGE] = per_page
900
911
  if page is not None and page != "all":
901
912
  request_body[ApiField.PAGE] = page
913
+ if include_custom_data:
914
+ request_body[ApiField.EXTRA_FIELDS] = [ApiField.CUSTOM_DATA]
902
915
 
903
916
  first_response = self._api.post(method, request_body).json()
904
917
 
@@ -12,10 +12,12 @@ from typing import (
12
12
  Any,
13
13
  Callable,
14
14
  Dict,
15
+ Generator,
15
16
  List,
16
17
  Literal,
17
18
  NamedTuple,
18
19
  Optional,
20
+ Tuple,
19
21
  Union,
20
22
  )
21
23
 
@@ -38,6 +40,7 @@ from supervisely.annotation.annotation import TagCollection
38
40
  from supervisely.annotation.obj_class import ObjClass
39
41
  from supervisely.annotation.obj_class_collection import ObjClassCollection
40
42
  from supervisely.annotation.tag_meta import TagMeta, TagValueType
43
+ from supervisely.api.dataset_api import DatasetInfo
41
44
  from supervisely.api.module_api import (
42
45
  ApiField,
43
46
  CloneableModuleApi,
@@ -2556,3 +2559,104 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
2556
2559
  api.project.calculate_embeddings(project_id)
2557
2560
  """
2558
2561
  self._api.post("embeddings.calculate-project-embeddings", {ApiField.PROJECT_ID: id})
2562
+
2563
+ def recreate_structure_generator(
2564
+ self,
2565
+ src_project_id: int,
2566
+ dst_project_id: Optional[int] = None,
2567
+ dst_project_name: Optional[str] = None,
2568
+ ) -> Generator[Tuple[DatasetInfo, DatasetInfo], None, None]:
2569
+ """This method can be used to recreate a project with hierarchial datasets (without the data itself) and
2570
+ yields the tuple of source and destination DatasetInfo objects.
2571
+
2572
+ :param src_project_id: Source project ID
2573
+ :type src_project_id: int
2574
+ :param dst_project_id: Destination project ID
2575
+ :type dst_project_id: int, optional
2576
+ :param dst_project_name: Name of the destination project. If `dst_project_id` is None, a new project will be created with this name. If `dst_project_id` is provided, this parameter will be ignored.
2577
+ :type dst_project_name: str, optional
2578
+
2579
+ :return: Generator of tuples of source and destination DatasetInfo objects
2580
+ :rtype: Generator[Tuple[DatasetInfo, DatasetInfo], None, None]
2581
+
2582
+ :Usage example:
2583
+
2584
+ .. code-block:: python
2585
+
2586
+ import supervisely as sly
2587
+
2588
+ api = sly.Api.from_env()
2589
+
2590
+ src_project_id = 123
2591
+ dst_project_id = api.project.create("new_project", "images").id
2592
+
2593
+ for src_ds, dst_ds in api.project.recreate_structure_generator(src_project_id, dst_project_id):
2594
+ print(f"Recreated dataset {src_ds.id} -> {dst_ds.id}")
2595
+ # Implement your logic here to process the datasets.
2596
+ """
2597
+ if dst_project_id is None:
2598
+ src_project_info = self._api.project.get_info_by_id(src_project_id)
2599
+ dst_project_info = self._api.project.create(
2600
+ src_project_info.workspace_id,
2601
+ dst_project_name or f"Recreation of {src_project_info.name}",
2602
+ src_project_info.type,
2603
+ src_project_info.description,
2604
+ change_name_if_conflict=True,
2605
+ )
2606
+ dst_project_id = dst_project_info.id
2607
+
2608
+ datasets = self._api.dataset.get_list(src_project_id, recursive=True, include_custom_data=True)
2609
+ src_to_dst_ids = {}
2610
+
2611
+ for src_dataset_info in datasets:
2612
+ dst_dataset_info = self._api.dataset.create(
2613
+ dst_project_id,
2614
+ src_dataset_info.name,
2615
+ description=src_dataset_info.description,
2616
+ parent_id=src_to_dst_ids.get(src_dataset_info.parent_id),
2617
+ custom_data=src_dataset_info.custom_data,
2618
+ )
2619
+ src_to_dst_ids[src_dataset_info.id] = dst_dataset_info.id
2620
+
2621
+ yield src_dataset_info, dst_dataset_info
2622
+
2623
+ def recreate_structure(
2624
+ self,
2625
+ src_project_id: int,
2626
+ dst_project_id: Optional[int] = None,
2627
+ dst_project_name: Optional[str] = None,
2628
+ ) -> Tuple[List[DatasetInfo], List[DatasetInfo]]:
2629
+ """This method can be used to recreate a project with hierarchial datasets (without the data itself).
2630
+
2631
+ :param src_project_id: Source project ID
2632
+ :type src_project_id: int
2633
+ :param dst_project_id: Destination project ID
2634
+ :type dst_project_id: int, optional
2635
+ :param dst_project_name: Name of the destination project. If `dst_project_id` is None, a new project will be created with this name. If `dst_project_id` is provided, this parameter will be ignored.
2636
+ :type dst_project_name: str, optional
2637
+
2638
+ :return: Destination project ID
2639
+ :rtype: int
2640
+
2641
+ :Usage example:
2642
+
2643
+ .. code-block:: python
2644
+
2645
+ import supervisely as sly
2646
+
2647
+ api = sly.Api.from_env()
2648
+
2649
+ src_project_id = 123
2650
+ dst_project_name = "New Project"
2651
+
2652
+ dst_project_id = api.project.recreate_structure(src_project_id, dst_project_name=dst_project_name)
2653
+ print(f"Recreated project {src_project_id} -> {dst_project_id}")
2654
+ """
2655
+ infos = []
2656
+ for src_info, dst_info in self.recreate_structure_generator(
2657
+ src_project_id, dst_project_id, dst_project_name
2658
+ ):
2659
+ infos.append((src_info, dst_info))
2660
+
2661
+
2662
+ return infos
@@ -19,6 +19,10 @@ class DatasetThumbnail(Widget):
19
19
  :type dataset_info: Optional[DatasetInfo]
20
20
  :param show_project_name: if True, project name will be shown
21
21
  :type show_project_name: Optional[bool]
22
+ :param remove_margins: if True, removes margins around the widget
23
+ :type remove_margins: bool, optional
24
+ :param custom_name: custom name for the dataset (if None, actual dataset name will be used)
25
+ :type custom_name: str, optional
22
26
  :param widget_id: An identifier of the widget.
23
27
  :type widget_id: str, optional
24
28
 
@@ -38,18 +42,21 @@ class DatasetThumbnail(Widget):
38
42
  project_info: Optional[ProjectInfo] = None,
39
43
  dataset_info: Optional[DatasetInfo] = None,
40
44
  show_project_name: Optional[bool] = True,
45
+ remove_margins: bool = False,
46
+ custom_name: str = None,
41
47
  widget_id: Optional[str] = None,
42
48
  ):
43
49
  self._project_info: ProjectInfo = None
44
50
  self._dataset_info: DatasetInfo = None
45
51
  self._id: int = None
46
- self._name: str = None
52
+ self._name: str = custom_name
47
53
  self._description: str = None
48
54
  self._url: str = None
49
55
  self._image_preview_url: str = None
50
56
  self._show_project_name: bool = show_project_name
51
57
  self._project_name: str = None
52
58
  self._project_url: str = None
59
+ self._remove_margins: bool = remove_margins
53
60
  self._set_info(project_info, dataset_info, show_project_name)
54
61
 
55
62
  super().__init__(widget_id=widget_id, file_path=__file__)
@@ -79,6 +86,7 @@ class DatasetThumbnail(Widget):
79
86
  "show_project_name": self._show_project_name,
80
87
  "project_name": self._project_name,
81
88
  "project_url": self._project_url,
89
+ "removeMargins": self._remove_margins,
82
90
  }
83
91
 
84
92
  def get_json_state(self) -> None:
@@ -96,7 +104,8 @@ class DatasetThumbnail(Widget):
96
104
  self._project_info = project_info
97
105
  self._dataset_info = dataset_info
98
106
  self._id = dataset_info.id
99
- self._name = dataset_info.name
107
+ if self._name is None:
108
+ self._name = dataset_info.name
100
109
  self._description = f"{self._dataset_info.items_count} {self._project_info.type} in dataset"
101
110
  self._url = Dataset.get_url(project_id=project_info.id, dataset_id=dataset_info.id)
102
111
  self._image_preview_url = dataset_info.image_preview_url
@@ -1,4 +1,6 @@
1
- <sly-field title="" :description="data.{{{widget.widget_id}}}.description">
1
+ <link rel="stylesheet" href="./sly/css/app/widgets/project_thumbnail/style.css" />
2
+
3
+ <sly-field {% if widget._remove_margins %} class="remove-margins" {% endif %} title="" :description="data.{{{widget.widget_id}}}.description">
2
4
  <span slot="title">
3
5
  <span v-if="data.{{{widget.widget_id}}}.show_project_name">
4
6
  <a target="_blank" :href="data.{{{widget.widget_id}}}.project_url">
@@ -46,6 +46,21 @@ class FastTable(Widget):
46
46
  :type width: str, optional
47
47
  :param widget_id: Unique widget identifier.
48
48
  :type widget_id: str, optional
49
+ :param show_header: Whether to show table header
50
+ :type show_header: bool, optional
51
+ :param is_radio: Enable radio button selection mode (single row selection)
52
+ :type is_radio: bool, optional
53
+ :param is_selectable: Enable multiple row selection
54
+ :type is_selectable: bool, optional
55
+ :param header_left_content: Widget to display in the left side of the header
56
+ :type header_left_content: Widget, optional
57
+ :param header_right_content: Widget to display in the right side of the header
58
+ :type header_right_content: Widget, optional
59
+ :param max_selected_rows: Maximum number of rows that can be selected
60
+ :type max_selected_rows: int, optional
61
+ :param search_position: Position of the search input ("left" or "right")
62
+ :type search_position: Literal["left", "right"], optional
63
+
49
64
 
50
65
  :Usage example:
51
66
  .. code-block:: python
@@ -141,6 +156,8 @@ class FastTable(Widget):
141
156
  is_selectable: bool = False,
142
157
  header_left_content: Optional[Widget] = None,
143
158
  header_right_content: Optional[Widget] = None,
159
+ max_selected_rows: Optional[int] = None,
160
+ search_position: Optional[Literal["left", "right"]] = None,
144
161
  ):
145
162
  self._supported_types = tuple([pd.DataFrame, list, type(None)])
146
163
  self._row_click_handled = False
@@ -170,8 +187,9 @@ class FastTable(Widget):
170
187
  self._searched_data = None
171
188
  self._active_page = 1
172
189
  self._width = width
173
- self._selected_rows = None
190
+ self._selected_rows = []
174
191
  self._selected_cell = None
192
+ self._clicked_row = None
175
193
  self._is_row_clickable = False
176
194
  self._is_cell_clickable = False
177
195
  self._search_str = ""
@@ -179,6 +197,9 @@ class FastTable(Widget):
179
197
  self._project_meta = self._unpack_project_meta(project_meta)
180
198
  self._header_left_content = header_left_content
181
199
  self._header_right_content = header_right_content
200
+ self._max_selected_rows = max_selected_rows
201
+ acceptable_search_positions = ["left", "right"]
202
+ self._search_position = search_position if search_position in acceptable_search_positions else "left"
182
203
 
183
204
  # table_options
184
205
  self._page_size = page_size
@@ -234,7 +255,9 @@ class FastTable(Widget):
234
255
  self._searched_data = self._search(search_value)
235
256
  self._rows_total = len(self._searched_data)
236
257
 
237
- if self._rows_total > 0 and self._active_page == 0: # if previous filtered data was empty
258
+ # if active page is greater than the number of pages (e.g. after filtering)
259
+ max_page = (self._rows_total - 1) // self._page_size + 1
260
+ if (self._rows_total > 0 and self._active_page == 0) or self._active_page > max_page:
238
261
  self._active_page = 1
239
262
  StateJson()[self.widget_id]["page"] = self._active_page
240
263
 
@@ -260,7 +283,13 @@ class FastTable(Widget):
260
283
  - isRowClickable: whether rows are clickable
261
284
  - isCellClickable: whether cells are clickable
262
285
  - fixColumns: number of fixed columns
286
+ - isRadio: whether radio button selection mode is enabled
287
+ - isRowSelectable: whether multiple row selection is enabled
288
+ - maxSelectedRows: maximum number of rows that can be selected
289
+ - searchPosition: position of the search input ("left" or "right")
263
290
  - pageSize: number of rows per page
291
+ - showHeader: whether to show table header
292
+ - selectionChangedHandled: whether selection changed event listener is set
264
293
 
265
294
  :return: Dictionary with widget data
266
295
  :rtype: Dict[str, Any]
@@ -276,6 +305,9 @@ class FastTable(Widget):
276
305
  "isCellClickable": self._is_cell_clickable,
277
306
  "fixColumns": self._fix_columns,
278
307
  "isRadio": self._is_radio,
308
+ "isRowSelectable": self._is_selectable,
309
+ "maxSelectedRows": self._max_selected_rows,
310
+ "searchPosition": self._search_position
279
311
  },
280
312
  "pageSize": self._page_size,
281
313
  "showHeader": self._show_header,
@@ -286,8 +318,9 @@ class FastTable(Widget):
286
318
  """Returns dictionary with widget state.
287
319
  Dictionary contains the following fields:
288
320
  - search: search string
289
- - selectedRow: selected row
321
+ - selectedRows: selected rows
290
322
  - selectedCell: selected cell
323
+ - clickedRow: clicked row
291
324
  - page: active page
292
325
  - sort: sorting options with the following fields:
293
326
  - column: index of the column to sort by
@@ -300,6 +333,7 @@ class FastTable(Widget):
300
333
  "search": self._search_str,
301
334
  "selectedRows": self._selected_rows,
302
335
  "selectedCell": self._selected_cell,
336
+ "clickedRow": self._clicked_row,
303
337
  "page": self._active_page,
304
338
  "sort": {
305
339
  "column": self._sort_column_idx,
@@ -383,38 +417,95 @@ class FastTable(Widget):
383
417
  filter_function = self._default_filter_function
384
418
  self._filter_function = filter_function
385
419
 
386
- def read_json(self, data: Dict, meta: Dict = None) -> None:
420
+ def read_json(self, data: Dict, meta: Dict = None, custom_columns: Optional[List[Union[str, tuple]]] = None) -> None:
387
421
  """Replace table data with options and project meta in the widget
388
422
 
389
- :param data: Table data with options
423
+ :param data: Table data with options:
424
+ - data: table data
425
+ - columns: list of column names
426
+ - projectMeta: project meta information - if provided
427
+ - columnsOptions: list of dicts with options for each column
428
+ - total: total number of rows
429
+ - options: table options with the following fields:
430
+ - isRowClickable: whether rows are clickable
431
+ - isCellClickable: whether cells are clickable
432
+ - fixColumns: number of fixed columns
433
+ - isRadio: whether radio button selection mode is enabled
434
+ - isRowSelectable: whether multiple row selection is enabled
435
+ - maxSelectedRows: maximum number of rows that can be selected
436
+ - searchPosition: position of the search input ("left" or "right")
437
+ - pageSize: number of rows per page
438
+ - showHeader: whether to show table header
439
+ - selectionChangedHandled: whether selection changed event listener is set
440
+
390
441
  :type data: dict
391
442
  :param meta: Project meta information
392
443
  :type meta: dict
444
+ :param custom_columns: List of column names. Can include widgets as tuples (column_name, widget)
445
+ :type custom_columns: List[Union[str, tuple]], optional
446
+
447
+ Example of data dict:
448
+ .. code-block:: python
449
+
450
+ data = {
451
+ "data": [["apple", "21"], ["banana", "15"]],
452
+ "columns": ["Class", "Items"],
453
+ "columnsOptions": [
454
+ { "type": "class"},
455
+ { "maxValue": 21, "postfix": "pcs", "tooltip": "description text", "subtitle": "boxes" }
456
+ ],
457
+ "options": {
458
+ "isRowClickable": True,
459
+ "isCellClickable": True,
460
+ "fixColumns": 1,
461
+ "isRadio": False,
462
+ "isRowSelectable": True,
463
+ "maxSelectedRows": 5,
464
+ "searchPosition": "right",
465
+ "sort": {"column": 0, "order": "asc"},
466
+ },
467
+ }
393
468
  """
394
- self._columns_first_idx = self._prepare_json_data(data, "columns")
395
469
  self._columns_options = self._prepare_json_data(data, "columnsOptions")
470
+ self._read_custom_columns(custom_columns)
471
+ if not self._columns_first_idx:
472
+ self._columns_first_idx = self._prepare_json_data(data, "columns")
396
473
  self._table_options = self._prepare_json_data(data, "options")
397
474
  self._project_meta = self._unpack_project_meta(meta)
398
- self._parsed_source_data = data.get("data", None)
399
- self._source_data = self._prepare_input_data(self._parsed_source_data)
400
- self._sliced_data = self._slice_table_data(self._source_data)
401
- self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
475
+ table_data = data.get("data", None)
476
+ self._validate_input_data(table_data)
477
+ self._source_data = self._prepare_input_data(table_data)
478
+ (
479
+ self._parsed_source_data,
480
+ self._sliced_data,
481
+ self._parsed_active_data,
482
+ ) = self._prepare_working_data()
402
483
  self._rows_total = len(self._parsed_source_data["data"])
403
484
  init_options = DataJson()[self.widget_id]["options"]
404
485
  init_options.update(self._table_options)
405
486
  sort = init_options.pop("sort", {"column": None, "order": None})
406
- page_size = init_options.pop("pageSize", 10)
487
+ self._active_page = 1
488
+ self._sort_column_idx = sort.get("column", None)
489
+ if self._sort_column_idx is not None and self._sort_column_idx > len(self._columns_first_idx) - 1:
490
+ self._sort_column_idx = None
491
+ self._sort_order = sort.get("order", None)
492
+ self._page_size = init_options.pop("pageSize", 10)
407
493
  DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
408
494
  DataJson()[self.widget_id]["columns"] = self._parsed_active_data["columns"]
409
495
  DataJson()[self.widget_id]["columnsOptions"] = self._columns_options
410
496
  DataJson()[self.widget_id]["options"] = init_options
411
497
  DataJson()[self.widget_id]["total"] = len(self._source_data)
412
- DataJson()[self.widget_id]["pageSize"] = page_size
498
+ DataJson()[self.widget_id]["pageSize"] = self._page_size
413
499
  DataJson()[self.widget_id]["projectMeta"] = self._project_meta
414
- StateJson()[self.widget_id]["sort"] = sort
500
+ StateJson()[self.widget_id]["sort"]["column"] = self._sort_column_idx
501
+ StateJson()[self.widget_id]["sort"]["order"] = self._sort_order
502
+ StateJson()[self.widget_id]["page"] = self._active_page
503
+ StateJson()[self.widget_id]["selectedRows"] = []
504
+ StateJson()[self.widget_id]["selectedCell"] = None
505
+ self._maybe_update_selected_row()
506
+ self._validate_sort_attrs()
415
507
  DataJson().send_changes()
416
508
  StateJson().send_changes()
417
- self.clear_selection()
418
509
 
419
510
  def read_pandas(self, data: pd.DataFrame) -> None:
420
511
  """Replace table data (rows and columns) in the widget.
@@ -440,6 +531,24 @@ class FastTable(Widget):
440
531
  def to_json(self, active_page: Optional[bool] = False) -> Dict[str, Any]:
441
532
  """Export table data with current options as dict.
442
533
 
534
+ Dictionary contains the following fields:
535
+ - data: table data
536
+ - columns: list of column names
537
+ - projectMeta: project meta information - if provided
538
+ - columnsOptions: list of dicts with options for each column
539
+ - total: total number of rows
540
+ - options: table options with the following fields:
541
+ - isRowClickable: whether rows are clickable
542
+ - isCellClickable: whether cells are clickable
543
+ - fixColumns: number of fixed columns
544
+ - isRadio: whether radio button selection mode is enabled
545
+ - isRowSelectable: whether multiple row selection is enabled
546
+ - maxSelectedRows: maximum number of rows that can be selected
547
+ - searchPosition: position of the search input ("left" or "right")
548
+ - pageSize: number of rows per page
549
+ - showHeader: whether to show table header
550
+ - selectionChangedHandled: whether selection changed event listener is set
551
+
443
552
  :param active_page: Specifies the size of the data to be exported. If True - returns only the active page of the table
444
553
  :type active_page: Optional[bool]
445
554
  :return: Table data with current options
@@ -477,7 +586,7 @@ class FastTable(Widget):
477
586
 
478
587
  def clear_selection(self) -> None:
479
588
  """Clears the selection of the table."""
480
- StateJson()[self.widget_id]["selectedRows"] = None
589
+ StateJson()[self.widget_id]["selectedRows"] = []
481
590
  StateJson()[self.widget_id]["selectedCell"] = None
482
591
  StateJson().send_changes()
483
592
  self._maybe_update_selected_row()
@@ -558,15 +667,15 @@ class FastTable(Widget):
558
667
  if self._rows_total != 0:
559
668
  self.select_row(0)
560
669
  else:
561
- self._selected_rows = None
562
- StateJson()[self.widget_id]["selectedRows"] = None
670
+ self._selected_rows = []
671
+ StateJson()[self.widget_id]["selectedRows"] = self._selected_rows
563
672
  StateJson().send_changes()
564
673
  return
565
674
  if not self._selected_rows:
566
675
  return
567
676
  if self._rows_total == 0:
568
- self._selected_rows = None
569
- StateJson()[self.widget_id]["selectedRows"] = None
677
+ self._selected_rows = []
678
+ StateJson()[self.widget_id]["selectedRows"] = self._selected_rows
570
679
  StateJson().send_changes()
571
680
  return
572
681
  if self._is_selectable:
@@ -1115,8 +1224,12 @@ class FastTable(Widget):
1115
1224
 
1116
1225
  @server.post(selection_changed_route_path)
1117
1226
  def _selection_changed():
1118
- selected_row = self.get_selected_row()
1119
- func(selected_row)
1227
+ if self._is_radio:
1228
+ selected_row = self.get_selected_row()
1229
+ func(selected_row)
1230
+ elif self._is_selectable:
1231
+ selected_rows = self.get_selected_rows()
1232
+ func(selected_rows)
1120
1233
 
1121
1234
  self._selection_changed_handled = True
1122
1235
  DataJson()[self.widget_id]["selectionChangedHandled"] = True
@@ -1201,3 +1314,25 @@ class FastTable(Widget):
1201
1314
 
1202
1315
  idxs = self._source_data[self._source_data[column].isin(values)].index.tolist()
1203
1316
  self.select_rows(idxs)
1317
+
1318
+ def _read_custom_columns(self, columns: List[Union[str, tuple]]) -> None:
1319
+ if not columns:
1320
+ return
1321
+ self._columns = columns
1322
+ self._columns_options = self._columns_options or [{} for _ in columns]
1323
+ self._columns_data = []
1324
+ self._columns_first_idx = []
1325
+ for i, col in enumerate(columns):
1326
+ if isinstance(col, str):
1327
+ self._columns_first_idx.append(col)
1328
+ self._columns_data.append(self.ColumnData(name=col))
1329
+ elif isinstance(col, tuple):
1330
+ self._columns_first_idx.append(col[0])
1331
+ self._columns_data.append(
1332
+ self.ColumnData(name=col[0], is_widget=True, widget=col[1])
1333
+ )
1334
+ self._columns_options[i]["customCell"] = True
1335
+ else:
1336
+ raise TypeError(f"Column name must be a string or a tuple, got {type(col)}")
1337
+
1338
+ self._validate_sort_attrs()
@@ -17,7 +17,7 @@ Vue.component('fast-table', {
17
17
  style="flex-grow: 1"
18
18
  >
19
19
  <slot name="header-left-side-start" />
20
- <div class="relative w-full md:max-w-[14rem]">
20
+ <div v-if="settings.searchPosition !== 'right'" class="relative w-full md:max-w-[14rem]">
21
21
  <i class="zmdi zmdi-search h-4 absolute top-2 left-2.5 opacity-50" />
22
22
  <i
23
23
  v-if="search"
@@ -35,6 +35,22 @@ Vue.component('fast-table', {
35
35
  </div>
36
36
  <slot name="header-left-side-end" />
37
37
  </div>
38
+ <div v-if="settings.searchPosition === 'right'" class="relative w-full md:max-w-[14rem]">
39
+ <i class="zmdi zmdi-search h-4 absolute top-2 left-2.5 opacity-50" />
40
+ <i
41
+ v-if="search"
42
+ class="zmdi zmdi-close h-4 absolute top-2.5 right-3 opacity-50 cursor-pointer"
43
+ @click="searchChanged('')"
44
+ />
45
+ <input
46
+ :value="search"
47
+ type="text"
48
+ :placeholder="\`\${name ? \`Search for \${name}...\` : 'Search'}\`"
49
+ class="text-sm rounded-md px-3 py-1.5 text-gray-900 shadow-sm placeholder:text-gray-400 border border-slate-200 w-full pl-8 bg-slate-50"
50
+ @input="searchChanged($event.target.value)"
51
+ @keydown.esc="searchChanged('')"
52
+ >
53
+ </div>
38
54
  <div
39
55
  v-if="data && data.length"
40
56
  class="text-[.9rem] text-slate-500 flex items-center gap-0 w-full md:w-auto whitespace-nowrap"
@@ -73,6 +89,7 @@ Vue.component('fast-table', {
73
89
  <table
74
90
  ref="tableBox"
75
91
  class="w-full text-[.8rem] md:text-[.9rem] mb-1"
92
+ :key="'table-' + page"
76
93
  >
77
94
  <thead>
78
95
  <tr>
@@ -84,17 +101,25 @@ Vue.component('fast-table', {
84
101
  >
85
102
  </th>
86
103
  <th
87
- v-else-if="settings.isRowSelectable"
104
+ v-if="settings.isRowSelectable && (!settings.maxSelectedRows || settings.maxSelectedRows > 1)"
88
105
  class="px-2 md:px-3 py-2.5 whitespace-nowrap first:pl-3 last:pr-3 md:last:pr-6 first:text-left cursor-pointer sticky top-0 bg-slate-50 box-content shadow-[inset_0_-2px_0_#dfe6ec] group"
89
106
  :class="{ 'first:sticky first:left-0 first:z-20 first:shadow-[inset_-2px_-2px_0_#dfe6ec]': fixColumns }"
90
107
  >
91
108
  <el-checkbox
92
- v-if="settings.isRowSelectable"
109
+ v-if="settings.isRowSelectable && (!settings.maxSelectedRows || settings.maxSelectedRows > 1)"
93
110
  size="small"
94
- :value="isAllRowsSelected"
95
- @input="selectAllRows"
111
+ v-model="headerCheckboxModel"
112
+ :indeterminate="isSomeOnPageSelected && !isAllOnPageSelected"
113
+ :key="'headercb-' + page + '-' + (data && data.map(r => r.idx).join(',')) + '-' + (selectedRows && selectedRows.map(r => r.idx).join(',')) + '-' + (isAllOnPageSelected?1:0) + '-' + (isSomeOnPageSelected?1:0)"
114
+ style="transform: scale(1.2);"
96
115
  />
97
116
  </th>
117
+ <th
118
+ v-if="settings.isRowSelectable && settings.maxSelectedRows === 1"
119
+ class="px-2 md:px-3 py-2.5 whitespace-nowrap first:pl-3 last:pr-3 md:last:pr-6 first:text-left cursor-pointer sticky top-0 bg-slate-50 box-content shadow-[inset_0_-2px_0_#dfe6ec] group"
120
+ :class="{ 'first:sticky first:left-0 first:z-20 first:shadow-[inset_-2px_-2px_0_#dfe6ec]': fixColumns }"
121
+ >
122
+ </th>
98
123
  <th
99
124
  v-for="(c,idx) in columns.slice(0, columnNumberLimit)"
100
125
  class="px-2 md:px-3 py-2.5 whitespace-nowrap first:pl-3 last:pr-3 md:first:pl-6 md:last:pr-6 first:text-left cursor-pointer sticky top-0 bg-slate-50 box-content shadow-[inset_0_-2px_0_#dfe6ec] group"
@@ -140,12 +165,13 @@ Vue.component('fast-table', {
140
165
  </th>
141
166
  </tr>
142
167
  </thead>
143
- <tbody>
168
+ <tbody :key="'page-' + page">
144
169
  <tr
145
170
  v-for="row in data"
171
+ :key="'row-' + rowKeyValue(row)"
146
172
  class="border-b border-gray-200 last:border-0 group"
147
173
  :class="{ 'cursor-pointer': settings.isRowClickable }"
148
- @click="settings.isRowClickable && $emit('row-click', { idx: row.idx, row: row.items, columnsSettings })"
174
+ @click="settings.isRowClickable && $emit('row-click', { idx: rowKeyValue(row), row: row.items, columnsSettings })"
149
175
  >
150
176
  <td
151
177
  v-if="settings.isRadio"
@@ -169,8 +195,11 @@ Vue.component('fast-table', {
169
195
  >
170
196
  <el-checkbox
171
197
  size="small"
172
- :value="!!selectedRowsById[row.id]"
173
- @input="updateSelectedRows(row)"
198
+ v-model="rowCheckboxModel[rowKeyValue(row)]"
199
+ @change="onRowCheckboxChange(row, rowCheckboxModel[rowKeyValue(row)])"
200
+ @click.stop
201
+ :key="'rowcb-' + page + '-' + rowKeyValue(row)"
202
+ style="transform: scale(1.2);"
174
203
  />
175
204
  </td>
176
205
 
@@ -314,11 +343,16 @@ Vue.component('fast-table', {
314
343
  type: Boolean,
315
344
  default: false,
316
345
  },
346
+ rowKey: {
347
+ type: String,
348
+ default: 'idx',
349
+ },
317
350
  },
318
351
 
319
352
  data() {
320
353
  return {
321
354
  columnNumberLimit: 50,
355
+ rowCheckboxModel: {},
322
356
  };
323
357
  },
324
358
 
@@ -342,6 +376,7 @@ Vue.component('fast-table', {
342
376
  settings() {
343
377
  const defaultSettings = {
344
378
  showHeaderControls: true,
379
+ searchPosition: 'left',
345
380
  };
346
381
  return {
347
382
  ...defaultSettings,
@@ -353,10 +388,6 @@ Vue.component('fast-table', {
353
388
  return this.pageSize || 50;
354
389
  },
355
390
 
356
- selectedRowsById() {
357
- return _.keyBy(this.selectedRows, 'id');
358
- },
359
-
360
391
  classesMap() {
361
392
  return this.projectMeta ? _.keyBy(this.projectMeta.classes, 'title') : {};
362
393
  },
@@ -373,15 +404,53 @@ Vue.component('fast-table', {
373
404
  return this.columnsSettings.find(i => i?.subtitle);
374
405
  },
375
406
 
376
- isAllRowsSelected() {
377
- if (!this.selectedRows?.length) return false;
378
- return this.data.every(r => this.selectedRowsById[r.id]);
407
+ isAllOnPageSelected() {
408
+ const rows = this.data || [];
409
+ if (rows.length === 0) return false;
410
+ const selectedIdx = new Set((this.selectedRows || []).map(r => this.rowKeyValue(r)));
411
+ return rows.every(r => selectedIdx.has(this.rowKeyValue(r)));
412
+ },
413
+
414
+ isSomeOnPageSelected() {
415
+ const rows = this.data || [];
416
+ if (rows.length === 0) return false;
417
+ const selectedIdx = new Set((this.selectedRows || []).map(r => this.rowKeyValue(r)));
418
+ const count = rows.filter(r => selectedIdx.has(this.rowKeyValue(r))).length;
419
+ return count > 0 && count < rows.length;
420
+ },
421
+
422
+ selectedIdxSet() {
423
+ return new Set((this.selectedRows || []).map(r => this.rowKeyValue(r)));
379
424
  },
425
+
426
+ headerCheckboxModel: {
427
+ get() {
428
+ return this.isAllOnPageSelected;
429
+ },
430
+ set(val) {
431
+ this.selectAllRows(val);
432
+ }
433
+ },
434
+ },
435
+
436
+ watch: {
437
+ data: {
438
+ immediate: true,
439
+ handler() {
440
+ this.syncRowCheckboxModel();
441
+ }
442
+ },
443
+ selectedRows: {
444
+ immediate: true,
445
+ handler() {
446
+ this.syncRowCheckboxModel();
447
+ }
448
+ }
380
449
  },
381
450
 
382
451
  methods: {
383
452
  _updateData() {
384
- this.$emit('filters-changed');
453
+ this.$emit('filters-changed')
385
454
  },
386
455
 
387
456
  searchChanged(val) {
@@ -409,31 +478,85 @@ Vue.component('fast-table', {
409
478
  this.updateData();
410
479
  },
411
480
 
412
- selectAllRows() {
413
- if (this.isAllRowsSelected) {
414
- const curRowsSet = new Set(this.data.map(r => r.id));
415
- this.$emit('update:selected-rows', [...this.selectedRows.filter(r => !curRowsSet.has(r.id))]);
481
+ selectAllRows(checked) {
482
+ if (!this.data || this.data.length === 0) return;
483
+
484
+ const current = Array.isArray(this.selectedRows) ? [...this.selectedRows] : [];
485
+ const max = this.settings && this.settings.maxSelectedRows ? this.settings.maxSelectedRows : 0;
486
+
487
+ if (checked) {
488
+ const selectedIdx = new Set(current.map(r => this.rowKeyValue(r)));
489
+ const result = [...current];
490
+
491
+ if (max && max > 0) {
492
+ let quota = Math.max(0, max - result.length);
493
+ for (const r of this.data) {
494
+ const key = this.rowKeyValue(r);
495
+ if (!selectedIdx.has(key)) {
496
+ if (quota <= 0) break;
497
+ result.push(_.cloneDeep(r));
498
+ selectedIdx.add(key);
499
+ quota -= 1;
500
+ }
501
+ }
502
+ this.$emit('update:selected-rows', result);
503
+ } else {
504
+ for (const r of this.data) {
505
+ const key = this.rowKeyValue(r);
506
+ if (!selectedIdx.has(key)) {
507
+ result.push(_.cloneDeep(r));
508
+ selectedIdx.add(key);
509
+ }
510
+ }
511
+ this.$emit('update:selected-rows', result);
512
+ }
416
513
  } else {
417
- const selected = [...this.selectedRows];
418
-
419
- this.data.forEach((row) => {
420
- if (this.selectedRowsById[row.id]) return;
421
-
422
- selected.push(row);
423
- });
424
-
425
- this.$emit('update:selected-rows', selected);
514
+ const pageIdx = new Set(this.data.map(r => this.rowKeyValue(r)));
515
+ const result = current.filter(r => !pageIdx.has(this.rowKeyValue(r)));
516
+ this.$emit('update:selected-rows', result);
426
517
  }
427
518
  },
428
519
 
429
- updateSelectedRows(row) {
430
- const isRowSelected = this.selectedRowsById[row.id];
431
-
432
- if (!isRowSelected) {
433
- this.$emit('update:selected-rows', [...this.selectedRows, row]);
520
+ syncRowCheckboxModel() {
521
+ const map = {};
522
+ const selected = new Set((this.selectedRows || []).map(r => this.rowKeyValue(r)));
523
+ for (const r of (this.data || [])) {
524
+ const key = this.rowKeyValue(r);
525
+ map[key] = selected.has(key);
526
+ }
527
+ this.rowCheckboxModel = map;
528
+ },
529
+
530
+ onRowCheckboxChange(row, checked) {
531
+ this.updateSelectedRows(row, checked);
532
+ },
533
+
534
+ updateSelectedRows(row, checked) {
535
+ checked = typeof checked === 'boolean' ? checked : !!checked;
536
+ const current = Array.isArray(this.selectedRows) ? [...this.selectedRows] : [];
537
+ const key = this.rowKeyValue(row);
538
+ const exists = current.some(r => this.rowKeyValue(r) === key);
539
+ const max = this.settings && this.settings.maxSelectedRows ? this.settings.maxSelectedRows : 0;
540
+
541
+ let result = current;
542
+ if (checked) {
543
+ if (max && max > 0) {
544
+ if (max === 1) {
545
+ result = [_.cloneDeep(row)];
546
+ this.$emit('update:selected-rows', result);
547
+ return;
548
+ }
549
+ if (!exists && current.length >= max) {
550
+ // revert checkbox state if over the limit
551
+ this.$nextTick(() => { this.$set(this.rowCheckboxModel, key, false); });
552
+ return;
553
+ }
554
+ }
555
+ if (!exists) result = [...current, _.cloneDeep(row)];
434
556
  } else {
435
- this.$emit('update:selected-rows', [...this.selectedRows.filter(p => p.id !== row.id)]);
557
+ if (exists) result = current.filter(r => this.rowKeyValue(r) !== key);
436
558
  }
559
+ this.$emit('update:selected-rows', result);
437
560
  },
438
561
 
439
562
  updateSelectedRadio(row) {
@@ -453,6 +576,12 @@ Vue.component('fast-table', {
453
576
  if (!this.search) return text;
454
577
  return text.toString().replace(new RegExp(this.search, 'gi'), match => '<span class="bg-yellow-400">'+match+'</span>');
455
578
  },
579
+
580
+ rowKeyValue(row) {
581
+ if (!row) return undefined;
582
+ const keyName = this.rowKey || 'idx';
583
+ return row[keyName] != null ? row[keyName] : row.idx;
584
+ },
456
585
  },
457
586
 
458
587
  mounted() {
@@ -704,8 +704,38 @@
704
704
  border-width: 1px;
705
705
  }
706
706
 
707
- .header-left-side-start-cls {
708
- color: black;
707
+ .tailwind .el-checkbox__inner {
708
+ --tw-border-opacity: 1;
709
+ border-width: 1px;
710
+ border-color: rgb(226 232 240 / var(--tw-border-opacity));
711
+ }
712
+ .tailwind .el-checkbox__input.is-checked .el-checkbox__inner,
713
+ .tailwind .el-checkbox__input.is-indeterminate .el-checkbox__inner {
714
+ --tw-bg-opacity: 1;
715
+ background-color: rgb(14 165 233 / var(--tw-bg-opacity));
716
+ border-color: rgb(14 165 233 / var(--tw-bg-opacity));
717
+ }
718
+ /* refine the checkmark so it's thinner and not a full box */
719
+ .tailwind .el-checkbox__inner::after {
720
+ border-left: 0 !important;
721
+ border-top: 0 !important;
722
+ }
723
+ .tailwind .el-checkbox__input.is-checked .el-checkbox__inner::after {
724
+ border-right: 1.5px solid #fff;
725
+ border-bottom: 1.5px solid #fff;
726
+ border-left: 0 !important;
727
+ border-top: 0 !important;
728
+ }
729
+ .tailwind .el-checkbox__input.is-indeterminate .el-checkbox__inner::before {
730
+ background-color: #fff;
731
+ height: 2px;
732
+ }
733
+
734
+ .tailwind .el-checkbox__inner::after {
735
+ left: 4px;
736
+ top: 1px;
737
+ width: 4px;
738
+ height: 8px;
709
739
  }
710
740
 
711
741
  .sly-fast-table-disable-overlay {
@@ -24,7 +24,7 @@
24
24
  if
25
25
  widget._row_click_handled
26
26
  %}
27
- @row-click="state.{{{widget.widget_id}}}.selectedRow = $event; post('/{{{widget.widget_id}}}/row_clicked_cb')"
27
+ @row-click="state.{{{widget.widget_id}}}.clickedRow = $event; post('/{{{widget.widget_id}}}/row_clicked_cb')"
28
28
  {%
29
29
  endif
30
30
  %}
supervisely/versions.json CHANGED
@@ -23,5 +23,6 @@
23
23
  "6.13.8": "6.73.394",
24
24
  "6.14.0": "6.73.400",
25
25
  "6.14.4": "6.73.410",
26
- "6.15.0": "6.73.431"
26
+ "6.15.0": "6.73.431",
27
+ "6.15.2": "6.73.433"
27
28
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: supervisely
3
- Version: 6.73.431
3
+ Version: 6.73.433
4
4
  Summary: Supervisely Python SDK.
5
5
  Home-page: https://github.com/supervisely/supervisely
6
6
  Author: Supervisely
@@ -4,7 +4,7 @@ supervisely/_utils.py,sha256=59vOeNOnmVHODnlAvULT8jdwvHHVTs2FOtAFw8mvaqE,20643
4
4
  supervisely/function_wrapper.py,sha256=R5YajTQ0GnRp2vtjwfC9hINkzQc0JiyGsu8TER373xY,1912
5
5
  supervisely/sly_logger.py,sha256=z92Vu5hmC0GgTIJO1n6kPDayRW9__8ix8hL6poDZj-Y,6274
6
6
  supervisely/tiny_timer.py,sha256=hkpe_7FE6bsKL79blSs7WBaktuPavEVu67IpEPrfmjE,183
7
- supervisely/versions.json,sha256=Cppk4ndoCkIEclgQwVKzJjFQjahjOPpVuoo4LrM9eTE,585
7
+ supervisely/versions.json,sha256=J7V7XPpDno0fvmNaAXXcxNksXqpRFaYRrG4POC0UDKQ,608
8
8
  supervisely/annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  supervisely/annotation/annotation.py,sha256=th4gsIU-LNGcMRohHrtupmjxDwdzx1g4_0xIAa6NyJU,114717
10
10
  supervisely/annotation/annotation_transforms.py,sha256=TlVy_gUbM-XH6GbLpZPrAi6pMIGTr7Ow02iSKOSTa-I,9582
@@ -26,7 +26,7 @@ supervisely/api/annotation_api.py,sha256=JdcCKuy_7PvtNcHsRhi4ANhn3aynLY44rtbqau2
26
26
  supervisely/api/api.py,sha256=gRItQzO6Xj7k_pJIpVUS2dV1vkTTHV25_1Uia6xxZSc,67930
27
27
  supervisely/api/app_api.py,sha256=Q6XxLxp3D_Vc3PIVyBmP7wJtTLbgYCPNOLND5UvJhMw,79010
28
28
  supervisely/api/constants.py,sha256=WfqIcEpRnU4Mcfb6q0njeRs2VVSoTAJaIyrqBkBjP8I,253
29
- supervisely/api/dataset_api.py,sha256=7idBMFL8jumWNw-wlBAbQWC09RskG-3GlidfPDukq3Q,47930
29
+ supervisely/api/dataset_api.py,sha256=BD6kG2lj826ajWjHxmiKEsyWb2Ov6CyTQlItzAxADbo,48955
30
30
  supervisely/api/entities_collection_api.py,sha256=Be13HsfMFLmq9XpiOfQog0Y569kbUn52hXv6x5vX3Vg,22624
31
31
  supervisely/api/file_api.py,sha256=gNXNsikocSYRojoZrVmXIqXycqXm0e320piAwaLN6JI,92978
32
32
  supervisely/api/github_api.py,sha256=NIexNjEer9H5rf5sw2LEZd7C1WR-tK4t6IZzsgeAAwQ,623
@@ -39,7 +39,7 @@ supervisely/api/labeling_queue_api.py,sha256=ilNjAL1d9NSa9yabQn6E-W26YdtooT3ZGXI
39
39
  supervisely/api/module_api.py,sha256=8Asdr3llhC8XQ98xGdR03hpe2GYznJONzNfgN-mQYv8,46458
40
40
  supervisely/api/object_class_api.py,sha256=7-npNFMYjWNtSXYZg6syc6bX56_oCzDU2kFRPGQWCwA,10399
41
41
  supervisely/api/plugin_api.py,sha256=SFm0IlTTOjuHBLUMgG4d4k6U3cWJocE-SVb-f08fwMQ,5286
42
- supervisely/api/project_api.py,sha256=2Y8h1R--aIuvZlMSNhRoBMmSqL2U0Zz19sLzePbgiLU,97499
42
+ supervisely/api/project_api.py,sha256=R4KjWUiO1pLQhsd6hDTgRWjaSdEiSE_bFI8UZ9Lo3pQ,101688
43
43
  supervisely/api/project_class_api.py,sha256=5cyjdGPPb2tpttu5WmYoOxUNiDxqiojschkhZumF0KM,1426
44
44
  supervisely/api/remote_storage_api.py,sha256=1O4rTIwW8s9gxC00yvFuKbEMGNsa7YSRlZ8j494ARwY,17793
45
45
  supervisely/api/report_api.py,sha256=Om7CGulUbQ4BuJ16eDtz7luLe0JQNqab-LoLpUXu7YE,7123
@@ -216,8 +216,8 @@ supervisely/app/widgets/custom_models_selector/custom_models_selector.py,sha256=
216
216
  supervisely/app/widgets/custom_models_selector/style.css,sha256=-zPPXHnJvatYj_xVVAb7T8uoSsUTyhm5xCKWkkFQ78E,548
217
217
  supervisely/app/widgets/custom_models_selector/template.html,sha256=Ot7dluekJEUtBWYezAVmcg6PbcEtb2_MRaTBhEnvRjk,2888
218
218
  supervisely/app/widgets/dataset_thumbnail/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
219
- supervisely/app/widgets/dataset_thumbnail/dataset_thumbnail.py,sha256=f7l7Te4jPRtvBcjTuUoo_AFRjpTujJHKHNYyt5EIx3Q,4646
220
- supervisely/app/widgets/dataset_thumbnail/template.html,sha256=yXlpgxkuQEklVY8iXLydKZ07vpG5RLHAmLCKb58l-ac,677
219
+ supervisely/app/widgets/dataset_thumbnail/dataset_thumbnail.py,sha256=C-PaA9TYTs_AEC-rZr789ww6NMNkBOgZOSKkXHqgs1k,5106
220
+ supervisely/app/widgets/dataset_thumbnail/template.html,sha256=zQZrSLeqbUGR3W0XxuztvEgS1ndnzxvhAhC1JbgmVtk,828
221
221
  supervisely/app/widgets/date_picker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
222
222
  supervisely/app/widgets/date_picker/date_picker.py,sha256=76tXh9OamTk7lz7aQJvKli7T-vbadULs8DpkndlvbeM,8213
223
223
  supervisely/app/widgets/date_picker/template.html,sha256=bQ8y6W7ZIXxQ3xRH6SlARBSAhHMOjDZqMddbdOZ69YM,699
@@ -266,10 +266,10 @@ supervisely/app/widgets/empty/template.html,sha256=aDBKkin5aLuqByzNN517-rTYCGIg5
266
266
  supervisely/app/widgets/experiment_selector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
267
267
  supervisely/app/widgets/experiment_selector/experiment_selector.py,sha256=9UseS99hb0ln4QLSeSIfXM7-pIpqLyWsyWoYQsELlnM,28402
268
268
  supervisely/app/widgets/fast_table/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
269
- supervisely/app/widgets/fast_table/fast_table.py,sha256=WEiQrWLBXx-USGhj5REn7AhlrLjA-5zQhQpedRfJF8E,49273
270
- supervisely/app/widgets/fast_table/script.js,sha256=cHjXm_tq5dFTgBxBKaS6lvT67wiaTjxnU2uWHF7hNoY,17327
271
- supervisely/app/widgets/fast_table/style.css,sha256=VwE8LHybH3vvZboaVFyfN7pE6MoIF_7fEdwNdjQMFUM,15763
272
- supervisely/app/widgets/fast_table/template.html,sha256=P0JVtohKUZcCI4aKDtnr6SxudHyYmWeGvjc8wm1VRs0,2788
269
+ supervisely/app/widgets/fast_table/fast_table.py,sha256=OlClGdTcERueYBNR8uITYQa-8mwNT53NeIlJokebRN0,56293
270
+ supervisely/app/widgets/fast_table/script.js,sha256=V0l3hlRjejsVM6lSYDF6b79NZgegSdLKH0RBrhumnPU,22728
271
+ supervisely/app/widgets/fast_table/style.css,sha256=DwVGIX4Hap_rG7D570pfyFRAUSMtfHUZEQYs9C6JrAY,16685
272
+ supervisely/app/widgets/fast_table/template.html,sha256=KTeZs6F8bUYOVZ58D7C4yrDTXKB9wvqEVT4RgRZr4sk,2787
273
273
  supervisely/app/widgets/field/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
274
274
  supervisely/app/widgets/field/field.py,sha256=FqBqfIEcUK7R3va30YS_zijWTUSD5QT3-kpLKJNPq7o,6258
275
275
  supervisely/app/widgets/field/style.css,sha256=sLMbqxS4EDPykcQZNMXeYdR4v1y1OP6iqcX0tRz7C68,56
@@ -1127,9 +1127,9 @@ supervisely/worker_proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
1127
1127
  supervisely/worker_proto/worker_api_pb2.py,sha256=VQfi5JRBHs2pFCK1snec3JECgGnua3Xjqw_-b3aFxuM,59142
1128
1128
  supervisely/worker_proto/worker_api_pb2_grpc.py,sha256=3BwQXOaP9qpdi0Dt9EKG--Lm8KGN0C5AgmUfRv77_Jk,28940
1129
1129
  supervisely_lib/__init__.py,sha256=7-3QnN8Zf0wj8NCr2oJmqoQWMKKPKTECvjH9pd2S5vY,159
1130
- supervisely-6.73.431.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1131
- supervisely-6.73.431.dist-info/METADATA,sha256=Tdduz5-kfqxj39qOe4tcX_-58Hp904M_Ja2X3M6G_LA,35433
1132
- supervisely-6.73.431.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
1133
- supervisely-6.73.431.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1134
- supervisely-6.73.431.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1135
- supervisely-6.73.431.dist-info/RECORD,,
1130
+ supervisely-6.73.433.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1131
+ supervisely-6.73.433.dist-info/METADATA,sha256=eJ_LTlSVbr_NSUaG-xiuO2hpxMwnqKAtuuQbyu4sg64,35433
1132
+ supervisely-6.73.433.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
1133
+ supervisely-6.73.433.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1134
+ supervisely-6.73.433.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1135
+ supervisely-6.73.433.dist-info/RECORD,,