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