supervisely 6.73.410__py3-none-any.whl → 6.73.470__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 +136 -1
- supervisely/_utils.py +81 -0
- supervisely/annotation/json_geometries_map.py +2 -0
- supervisely/annotation/label.py +80 -3
- supervisely/api/annotation_api.py +9 -9
- supervisely/api/api.py +67 -43
- supervisely/api/app_api.py +72 -5
- supervisely/api/dataset_api.py +108 -33
- supervisely/api/entity_annotation/figure_api.py +113 -49
- supervisely/api/image_api.py +82 -0
- supervisely/api/module_api.py +10 -0
- supervisely/api/nn/deploy_api.py +15 -9
- supervisely/api/nn/ecosystem_models_api.py +201 -0
- supervisely/api/nn/neural_network_api.py +12 -3
- supervisely/api/pointcloud/pointcloud_api.py +38 -0
- supervisely/api/pointcloud/pointcloud_episode_annotation_api.py +3 -0
- supervisely/api/project_api.py +213 -6
- supervisely/api/task_api.py +11 -1
- supervisely/api/video/video_annotation_api.py +4 -2
- supervisely/api/video/video_api.py +79 -1
- supervisely/api/video/video_figure_api.py +24 -11
- supervisely/api/volume/volume_api.py +38 -0
- 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 +175 -42
- supervisely/app/fastapi/templating.py +1 -1
- 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/__init__.py +11 -1
- supervisely/app/widgets/agent_selector/template.html +1 -0
- supervisely/app/widgets/card/card.py +20 -0
- supervisely/app/widgets/dataset_thumbnail/dataset_thumbnail.py +11 -2
- supervisely/app/widgets/dataset_thumbnail/template.html +3 -1
- supervisely/app/widgets/deploy_model/deploy_model.py +750 -0
- supervisely/app/widgets/dialog/dialog.py +12 -0
- supervisely/app/widgets/dialog/template.html +2 -1
- supervisely/app/widgets/dropdown_checkbox_selector/__init__.py +0 -0
- supervisely/app/widgets/dropdown_checkbox_selector/dropdown_checkbox_selector.py +87 -0
- supervisely/app/widgets/dropdown_checkbox_selector/template.html +12 -0
- supervisely/app/widgets/ecosystem_model_selector/__init__.py +0 -0
- supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +195 -0
- supervisely/app/widgets/experiment_selector/experiment_selector.py +454 -263
- supervisely/app/widgets/fast_table/fast_table.py +713 -126
- supervisely/app/widgets/fast_table/script.js +492 -95
- supervisely/app/widgets/fast_table/style.css +54 -0
- supervisely/app/widgets/fast_table/template.html +45 -5
- supervisely/app/widgets/heatmap/__init__.py +0 -0
- supervisely/app/widgets/heatmap/heatmap.py +523 -0
- supervisely/app/widgets/heatmap/script.js +378 -0
- supervisely/app/widgets/heatmap/style.css +227 -0
- supervisely/app/widgets/heatmap/template.html +21 -0
- supervisely/app/widgets/input_tag/input_tag.py +102 -15
- supervisely/app/widgets/input_tag_list/__init__.py +0 -0
- supervisely/app/widgets/input_tag_list/input_tag_list.py +274 -0
- supervisely/app/widgets/input_tag_list/template.html +70 -0
- supervisely/app/widgets/radio_table/radio_table.py +10 -2
- supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
- supervisely/app/widgets/radio_tabs/template.html +1 -0
- supervisely/app/widgets/select/select.py +6 -4
- supervisely/app/widgets/select_dataset/select_dataset.py +6 -0
- supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +83 -7
- supervisely/app/widgets/table/table.py +68 -13
- supervisely/app/widgets/tabs/tabs.py +22 -6
- supervisely/app/widgets/tabs/template.html +5 -1
- supervisely/app/widgets/transfer/style.css +3 -0
- supervisely/app/widgets/transfer/template.html +3 -1
- supervisely/app/widgets/transfer/transfer.py +48 -45
- supervisely/app/widgets/tree_select/tree_select.py +2 -0
- supervisely/convert/image/csv/csv_converter.py +24 -15
- supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +43 -41
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +75 -51
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +137 -124
- supervisely/convert/video/video_converter.py +2 -2
- supervisely/geometry/polyline_3d.py +110 -0
- supervisely/io/env.py +161 -1
- supervisely/nn/artifacts/__init__.py +1 -1
- supervisely/nn/artifacts/artifacts.py +10 -2
- supervisely/nn/artifacts/detectron2.py +1 -0
- supervisely/nn/artifacts/hrda.py +1 -0
- supervisely/nn/artifacts/mmclassification.py +20 -0
- supervisely/nn/artifacts/mmdetection.py +5 -3
- supervisely/nn/artifacts/mmsegmentation.py +1 -0
- supervisely/nn/artifacts/ritm.py +1 -0
- supervisely/nn/artifacts/rtdetr.py +1 -0
- supervisely/nn/artifacts/unet.py +1 -0
- supervisely/nn/artifacts/utils.py +3 -0
- supervisely/nn/artifacts/yolov5.py +2 -0
- supervisely/nn/artifacts/yolov8.py +1 -0
- supervisely/nn/benchmark/semantic_segmentation/metric_provider.py +18 -18
- supervisely/nn/experiments.py +9 -0
- supervisely/nn/inference/cache.py +37 -17
- supervisely/nn/inference/gui/serving_gui_template.py +39 -13
- supervisely/nn/inference/inference.py +953 -211
- supervisely/nn/inference/inference_request.py +15 -8
- supervisely/nn/inference/instance_segmentation/instance_segmentation.py +1 -0
- supervisely/nn/inference/object_detection/object_detection.py +1 -0
- supervisely/nn/inference/predict_app/__init__.py +0 -0
- supervisely/nn/inference/predict_app/gui/__init__.py +0 -0
- supervisely/nn/inference/predict_app/gui/classes_selector.py +160 -0
- supervisely/nn/inference/predict_app/gui/gui.py +915 -0
- supervisely/nn/inference/predict_app/gui/input_selector.py +344 -0
- supervisely/nn/inference/predict_app/gui/model_selector.py +77 -0
- supervisely/nn/inference/predict_app/gui/output_selector.py +179 -0
- supervisely/nn/inference/predict_app/gui/preview.py +93 -0
- supervisely/nn/inference/predict_app/gui/settings_selector.py +881 -0
- supervisely/nn/inference/predict_app/gui/tags_selector.py +110 -0
- supervisely/nn/inference/predict_app/gui/utils.py +399 -0
- supervisely/nn/inference/predict_app/predict_app.py +176 -0
- supervisely/nn/inference/session.py +47 -39
- supervisely/nn/inference/tracking/bbox_tracking.py +5 -1
- supervisely/nn/inference/tracking/point_tracking.py +5 -1
- supervisely/nn/inference/tracking/tracker_interface.py +4 -0
- supervisely/nn/inference/uploader.py +9 -5
- supervisely/nn/model/model_api.py +44 -22
- supervisely/nn/model/prediction.py +15 -1
- supervisely/nn/model/prediction_session.py +70 -14
- supervisely/nn/prediction_dto.py +7 -0
- supervisely/nn/tracker/__init__.py +6 -8
- supervisely/nn/tracker/base_tracker.py +54 -0
- supervisely/nn/tracker/botsort/__init__.py +1 -0
- supervisely/nn/tracker/botsort/botsort_config.yaml +30 -0
- supervisely/nn/tracker/botsort/osnet_reid/__init__.py +0 -0
- supervisely/nn/tracker/botsort/osnet_reid/osnet.py +566 -0
- supervisely/nn/tracker/botsort/osnet_reid/osnet_reid_interface.py +88 -0
- supervisely/nn/tracker/botsort/tracker/__init__.py +0 -0
- supervisely/nn/tracker/{bot_sort → botsort/tracker}/basetrack.py +1 -2
- supervisely/nn/tracker/{utils → botsort/tracker}/gmc.py +51 -59
- supervisely/nn/tracker/{deep_sort/deep_sort → botsort/tracker}/kalman_filter.py +71 -33
- supervisely/nn/tracker/botsort/tracker/matching.py +202 -0
- supervisely/nn/tracker/{bot_sort/bot_sort.py → botsort/tracker/mc_bot_sort.py} +68 -81
- supervisely/nn/tracker/botsort_tracker.py +273 -0
- supervisely/nn/tracker/calculate_metrics.py +264 -0
- supervisely/nn/tracker/utils.py +273 -0
- supervisely/nn/tracker/visualize.py +520 -0
- supervisely/nn/training/gui/gui.py +152 -49
- supervisely/nn/training/gui/hyperparameters_selector.py +1 -1
- supervisely/nn/training/gui/model_selector.py +8 -6
- supervisely/nn/training/gui/train_val_splits_selector.py +144 -71
- supervisely/nn/training/gui/training_artifacts.py +3 -1
- supervisely/nn/training/train_app.py +225 -46
- supervisely/project/pointcloud_episode_project.py +12 -8
- supervisely/project/pointcloud_project.py +12 -8
- supervisely/project/project.py +221 -75
- supervisely/template/experiment/experiment.html.jinja +105 -55
- supervisely/template/experiment/experiment_generator.py +258 -112
- supervisely/template/experiment/header.html.jinja +31 -13
- supervisely/template/experiment/sly-style.css +7 -2
- supervisely/versions.json +3 -1
- supervisely/video/sampling.py +42 -20
- supervisely/video/video.py +41 -12
- supervisely/video_annotation/video_figure.py +38 -4
- 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.410.dist-info → supervisely-6.73.470.dist-info}/METADATA +22 -14
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/RECORD +167 -148
- supervisely_lib/__init__.py +6 -1
- supervisely/app/widgets/experiment_selector/style.css +0 -27
- supervisely/app/widgets/experiment_selector/template.html +0 -61
- supervisely/nn/tracker/bot_sort/__init__.py +0 -21
- supervisely/nn/tracker/bot_sort/fast_reid_interface.py +0 -152
- supervisely/nn/tracker/bot_sort/matching.py +0 -127
- supervisely/nn/tracker/bot_sort/sly_tracker.py +0 -401
- supervisely/nn/tracker/deep_sort/__init__.py +0 -6
- supervisely/nn/tracker/deep_sort/deep_sort/__init__.py +0 -1
- supervisely/nn/tracker/deep_sort/deep_sort/detection.py +0 -49
- supervisely/nn/tracker/deep_sort/deep_sort/iou_matching.py +0 -81
- supervisely/nn/tracker/deep_sort/deep_sort/linear_assignment.py +0 -202
- supervisely/nn/tracker/deep_sort/deep_sort/nn_matching.py +0 -176
- supervisely/nn/tracker/deep_sort/deep_sort/track.py +0 -166
- supervisely/nn/tracker/deep_sort/deep_sort/tracker.py +0 -145
- supervisely/nn/tracker/deep_sort/deep_sort.py +0 -301
- supervisely/nn/tracker/deep_sort/generate_clip_detections.py +0 -90
- supervisely/nn/tracker/deep_sort/preprocessing.py +0 -70
- supervisely/nn/tracker/deep_sort/sly_tracker.py +0 -273
- supervisely/nn/tracker/tracker.py +0 -285
- supervisely/nn/tracker/utils/kalman_filter.py +0 -492
- supervisely/nn/tracking/__init__.py +0 -1
- supervisely/nn/tracking/boxmot.py +0 -114
- supervisely/nn/tracking/tracking.py +0 -24
- /supervisely/{nn/tracker/utils → app/widgets/deploy_model}/__init__.py +0 -0
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/LICENSE +0 -0
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/WHEEL +0 -0
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/top_level.txt +0 -0
|
@@ -42,7 +42,7 @@ class CustomStaticFiles(StaticFiles):
|
|
|
42
42
|
def _get_range_header(range_header: str, file_size: int) -> typing.Tuple[int, int]:
|
|
43
43
|
def _invalid_range():
|
|
44
44
|
return HTTPException(
|
|
45
|
-
status.HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE,
|
|
45
|
+
status.HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE, #TODO: change to status.HTTP_416_RANGE_NOT_SATISFIABLE if update starlette to 0.48.0+
|
|
46
46
|
detail=f"Invalid request range (Range:{range_header!r})",
|
|
47
47
|
)
|
|
48
48
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from typing import Optional, Union
|
|
4
|
+
|
|
5
|
+
from fastapi import Request
|
|
6
|
+
|
|
7
|
+
import supervisely.io.env as sly_env
|
|
8
|
+
from supervisely.api.module_api import ApiField
|
|
9
|
+
from supervisely.app.fastapi.websocket import WebsocketManager
|
|
10
|
+
from supervisely.sly_logger import logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _parse_int(value):
|
|
14
|
+
try:
|
|
15
|
+
return int(value)
|
|
16
|
+
except (TypeError, ValueError):
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _user_identity_from_cookie(request: Request) -> Optional[str]:
|
|
21
|
+
cookie_header = request.headers.get("cookie")
|
|
22
|
+
if not cookie_header:
|
|
23
|
+
return None
|
|
24
|
+
return hashlib.sha256(cookie_header.encode("utf-8")).hexdigest()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def extract_user_id_from_request(request: Request) -> Optional[Union[int, str]]:
|
|
28
|
+
"""Extract user ID from various parts of the request."""
|
|
29
|
+
if not sly_env.is_multiuser_mode_enabled():
|
|
30
|
+
return None
|
|
31
|
+
user_id = _parse_int(request.query_params.get("userId"))
|
|
32
|
+
if user_id is None:
|
|
33
|
+
header_user = _parse_int(request.headers.get("x-user-id"))
|
|
34
|
+
if header_user is not None:
|
|
35
|
+
user_id = header_user
|
|
36
|
+
if user_id is None:
|
|
37
|
+
referer = request.headers.get("referer", "")
|
|
38
|
+
if referer:
|
|
39
|
+
from urllib.parse import parse_qs, urlparse
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
parsed_url = urlparse(referer)
|
|
43
|
+
query_params = parse_qs(parsed_url.query)
|
|
44
|
+
referer_user = query_params.get("userId", [None])[0]
|
|
45
|
+
user_id = _parse_int(referer_user)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.error(f"Error parsing userId from referer: {e}")
|
|
48
|
+
if user_id is None and "application/json" in request.headers.get("Content-Type", ""):
|
|
49
|
+
try:
|
|
50
|
+
payload = await request.json()
|
|
51
|
+
except Exception:
|
|
52
|
+
payload = {}
|
|
53
|
+
context = payload.get("context") or {}
|
|
54
|
+
user_id = _parse_int(context.get("userId") or context.get(ApiField.USER_ID))
|
|
55
|
+
if user_id is None:
|
|
56
|
+
state_payload = payload.get("state") or {}
|
|
57
|
+
user_id = _parse_int(state_payload.get("userId") or state_payload.get(ApiField.USER_ID))
|
|
58
|
+
if user_id is None:
|
|
59
|
+
user_id = _user_identity_from_cookie(request)
|
|
60
|
+
return user_id
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@contextmanager
|
|
64
|
+
def session_context(user_id: Optional[Union[int, str]]):
|
|
65
|
+
"""
|
|
66
|
+
Context manager to set and reset user context for multiuser applications.
|
|
67
|
+
Call this at the beginning of a request handling to ensure the correct user context is set in environment variables (`supervisely_multiuser_app_user_id` ContextVar).
|
|
68
|
+
"""
|
|
69
|
+
if not sly_env.is_multiuser_mode_enabled() or user_id is None:
|
|
70
|
+
yield
|
|
71
|
+
return
|
|
72
|
+
token = sly_env.set_user_for_multiuser_app(user_id)
|
|
73
|
+
try:
|
|
74
|
+
yield
|
|
75
|
+
finally:
|
|
76
|
+
sly_env.reset_user_for_multiuser_app(token)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def remember_cookie(request: Request, user_id: Optional[Union[int, str]]):
|
|
80
|
+
"""
|
|
81
|
+
Remember user cookie for the given user ID. This is used to associate WebSocket connections with users in multiuser applications based on cookies.
|
|
82
|
+
Allows WebSocket connections to be correctly routed to the appropriate user.
|
|
83
|
+
"""
|
|
84
|
+
if not sly_env.is_multiuser_mode_enabled() or user_id is None:
|
|
85
|
+
return
|
|
86
|
+
cookie_header = request.headers.get("cookie")
|
|
87
|
+
if cookie_header:
|
|
88
|
+
WebsocketManager().remember_user_cookie(cookie_header, user_id)
|
|
@@ -1,21 +1,25 @@
|
|
|
1
|
+
import hashlib
|
|
1
2
|
import inspect
|
|
2
3
|
import json
|
|
3
4
|
import os
|
|
4
5
|
import signal
|
|
5
6
|
import sys
|
|
6
|
-
|
|
7
|
+
import time
|
|
8
|
+
from contextlib import contextmanager, suppress
|
|
9
|
+
from contextvars import ContextVar
|
|
7
10
|
from functools import wraps
|
|
8
11
|
from pathlib import Path
|
|
9
12
|
from threading import Event as ThreadingEvent
|
|
10
13
|
from threading import Thread
|
|
11
14
|
from time import sleep
|
|
12
|
-
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
|
|
15
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
|
|
13
16
|
|
|
14
17
|
import arel
|
|
15
18
|
import jinja2
|
|
16
19
|
import numpy as np
|
|
17
20
|
import psutil
|
|
18
21
|
from async_asgi_testclient import TestClient
|
|
22
|
+
from cachetools import TTLCache
|
|
19
23
|
from fastapi import (
|
|
20
24
|
Depends,
|
|
21
25
|
FastAPI,
|
|
@@ -30,6 +34,7 @@ from fastapi.responses import JSONResponse
|
|
|
30
34
|
from fastapi.routing import APIRouter
|
|
31
35
|
from fastapi.staticfiles import StaticFiles
|
|
32
36
|
|
|
37
|
+
import supervisely.app.fastapi.multi_user as multi_user
|
|
33
38
|
import supervisely.io.env as sly_env
|
|
34
39
|
from supervisely._utils import (
|
|
35
40
|
is_debug_with_sly_net,
|
|
@@ -61,6 +66,14 @@ SUPERVISELY_SERVER_PATH_PREFIX = sly_env.supervisely_server_path_prefix()
|
|
|
61
66
|
if SUPERVISELY_SERVER_PATH_PREFIX and not SUPERVISELY_SERVER_PATH_PREFIX.startswith("/"):
|
|
62
67
|
SUPERVISELY_SERVER_PATH_PREFIX = f"/{SUPERVISELY_SERVER_PATH_PREFIX}"
|
|
63
68
|
|
|
69
|
+
HEALTH_ENDPOINTS = ["/health", "/is_ready"]
|
|
70
|
+
|
|
71
|
+
# Context variable for response time
|
|
72
|
+
response_time_ctx: ContextVar[float] = ContextVar("response_time", default=None)
|
|
73
|
+
|
|
74
|
+
# Mapping from user_id to Api instance
|
|
75
|
+
_USER_API_CACHE = TTLCache(maxsize=500, ttl=60 * 15) # Cache up to 15 minutes
|
|
76
|
+
|
|
64
77
|
|
|
65
78
|
class ReadyzFilter(logging.Filter):
|
|
66
79
|
def filter(self, record):
|
|
@@ -70,11 +83,22 @@ class ReadyzFilter(logging.Filter):
|
|
|
70
83
|
return True
|
|
71
84
|
|
|
72
85
|
|
|
86
|
+
class ResponseTimeFilter(logging.Filter):
|
|
87
|
+
def filter(self, record):
|
|
88
|
+
# Check if this is an HTTP access log line by logger name
|
|
89
|
+
if getattr(record, "name", "") == "uvicorn.access":
|
|
90
|
+
response_time = response_time_ctx.get(None)
|
|
91
|
+
if response_time is not None:
|
|
92
|
+
record.responseTime = int(response_time)
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
|
|
73
96
|
def _init_uvicorn_logger():
|
|
74
97
|
uvicorn_logger = logging.getLogger("uvicorn.access")
|
|
75
98
|
for handler in uvicorn_logger.handlers:
|
|
76
99
|
handler.setFormatter(create_formatter())
|
|
77
100
|
uvicorn_logger.addFilter(ReadyzFilter())
|
|
101
|
+
uvicorn_logger.addFilter(ResponseTimeFilter())
|
|
78
102
|
|
|
79
103
|
|
|
80
104
|
_init_uvicorn_logger()
|
|
@@ -606,18 +630,30 @@ def create(
|
|
|
606
630
|
shutdown(process_id, before_shutdown_callbacks)
|
|
607
631
|
|
|
608
632
|
if headless is False:
|
|
609
|
-
|
|
610
633
|
@app.post("/data")
|
|
611
634
|
async def send_data(request: Request):
|
|
612
|
-
|
|
613
|
-
|
|
635
|
+
if not sly_env.is_multiuser_mode_enabled():
|
|
636
|
+
data = DataJson()
|
|
637
|
+
response = JSONResponse(content=dict(data))
|
|
638
|
+
return response
|
|
639
|
+
user_id = await multi_user.extract_user_id_from_request(request)
|
|
640
|
+
multi_user.remember_cookie(request, user_id)
|
|
641
|
+
with multi_user.session_context(user_id):
|
|
642
|
+
data = DataJson()
|
|
643
|
+
response = JSONResponse(content=dict(data))
|
|
614
644
|
return response
|
|
615
645
|
|
|
616
646
|
@app.post("/state")
|
|
617
647
|
async def send_state(request: Request):
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
648
|
+
if not sly_env.is_multiuser_mode_enabled():
|
|
649
|
+
state = StateJson()
|
|
650
|
+
response = JSONResponse(content=dict(state))
|
|
651
|
+
else:
|
|
652
|
+
user_id = await multi_user.extract_user_id_from_request(request)
|
|
653
|
+
multi_user.remember_cookie(request, user_id)
|
|
654
|
+
with multi_user.session_context(user_id):
|
|
655
|
+
state = StateJson()
|
|
656
|
+
response = JSONResponse(content=dict(state))
|
|
621
657
|
gettrace = getattr(sys, "gettrace", None)
|
|
622
658
|
if (gettrace is not None and gettrace()) or is_development():
|
|
623
659
|
response.headers["x-debug-mode"] = "1"
|
|
@@ -794,41 +830,64 @@ def _init(
|
|
|
794
830
|
|
|
795
831
|
@app.middleware("http")
|
|
796
832
|
async def get_state_from_request(request: Request, call_next):
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
)
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
833
|
+
# Start timer for response time measurement
|
|
834
|
+
start_time = time.perf_counter()
|
|
835
|
+
|
|
836
|
+
async def _process_request(request: Request, call_next):
|
|
837
|
+
if "application/json" in request.headers.get("Content-Type", ""):
|
|
838
|
+
content = await request.json()
|
|
839
|
+
request.state.context = content.get("context")
|
|
840
|
+
request.state.state = content.get("state")
|
|
841
|
+
request.state.api_token = content.get(
|
|
842
|
+
"api_token",
|
|
843
|
+
(
|
|
844
|
+
request.state.context.get("apiToken")
|
|
845
|
+
if request.state.context is not None
|
|
846
|
+
else None
|
|
847
|
+
),
|
|
848
|
+
)
|
|
849
|
+
request.state.server_address = content.get(
|
|
850
|
+
"server_address", sly_env.server_address(raise_not_found=False)
|
|
851
|
+
)
|
|
852
|
+
if (
|
|
853
|
+
request.state.server_address is not None
|
|
854
|
+
and request.state.api_token is not None
|
|
855
|
+
):
|
|
856
|
+
request.state.api = Api(
|
|
857
|
+
request.state.server_address, request.state.api_token
|
|
858
|
+
)
|
|
859
|
+
if sly_env.is_multiuser_mode_enabled():
|
|
860
|
+
user_id = sly_env.user_from_multiuser_app()
|
|
861
|
+
if user_id is not None:
|
|
862
|
+
_USER_API_CACHE[user_id] = request.state.api
|
|
863
|
+
else:
|
|
864
|
+
request.state.api = None
|
|
826
865
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
866
|
+
try:
|
|
867
|
+
response = await call_next(request)
|
|
868
|
+
except Exception as exc:
|
|
869
|
+
need_to_handle_error = is_production()
|
|
870
|
+
response = await process_server_error(
|
|
871
|
+
request, exc, need_to_handle_error
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
return response
|
|
875
|
+
|
|
876
|
+
if not sly_env.is_multiuser_mode_enabled():
|
|
877
|
+
if headless is False:
|
|
878
|
+
await StateJson.from_request(request)
|
|
879
|
+
response = await _process_request(request, call_next)
|
|
880
|
+
else:
|
|
881
|
+
user_id = await multi_user.extract_user_id_from_request(request)
|
|
882
|
+
multi_user.remember_cookie(request, user_id)
|
|
883
|
+
|
|
884
|
+
with multi_user.session_context(user_id):
|
|
885
|
+
if headless is False:
|
|
886
|
+
await StateJson.from_request(request, local=False)
|
|
887
|
+
response = await _process_request(request, call_next)
|
|
888
|
+
# Calculate response time and set it for uvicorn logger in ms
|
|
889
|
+
elapsed_ms = round((time.perf_counter() - start_time) * 1000)
|
|
890
|
+
response_time_ctx.set(elapsed_ms)
|
|
832
891
|
return response
|
|
833
892
|
|
|
834
893
|
def verify_localhost(request: Request):
|
|
@@ -905,7 +964,33 @@ class Application(metaclass=Singleton):
|
|
|
905
964
|
Callable
|
|
906
965
|
] = None, # function to check if the app is ready for requests (e.g serving app: model is served and ready)
|
|
907
966
|
show_header: bool = True,
|
|
967
|
+
hide_health_check_logs: bool = True, # whether to hide health check logs in info level
|
|
968
|
+
health_check_endpoints: Optional[List[str]] = None, # endpoints to check health of the app
|
|
908
969
|
):
|
|
970
|
+
"""Initialize the Supervisely Application.
|
|
971
|
+
|
|
972
|
+
:param layout: Main layout of the application.
|
|
973
|
+
:type layout: Widget
|
|
974
|
+
:param templates_dir: Directory with Jinja2 templates. It is preferred to use `layout` instead of `templates_dir`.
|
|
975
|
+
:type templates_dir: str, optional
|
|
976
|
+
:param static_dir: Directory with static files (e.g. CSS, JS), used for serving static content.
|
|
977
|
+
:type static_dir: str, optional
|
|
978
|
+
:param hot_reload: Whether to enable hot reload during development (default is False).
|
|
979
|
+
:type hot_reload: bool, optional
|
|
980
|
+
:param session_info_extra_content: Additional content to be displayed in the session info area.
|
|
981
|
+
:type session_info_extra_content: Widget, optional
|
|
982
|
+
:param session_info_solid: Whether to use solid background for the session info area.
|
|
983
|
+
:type session_info_solid: bool, optional
|
|
984
|
+
:param ready_check_function: Function to check if the app is ready for requests.
|
|
985
|
+
:type ready_check_function: Callable, optional
|
|
986
|
+
:param show_header: Whether to show the header in the application.
|
|
987
|
+
:type show_header: bool, optional
|
|
988
|
+
:param hide_health_check_logs: Whether to hide health check logs in info level.
|
|
989
|
+
:type hide_health_check_logs: bool, optional
|
|
990
|
+
:param health_check_endpoints: List of additional endpoints to check health of the app.
|
|
991
|
+
Add your custom endpoints here to be able to manage logging of health check requests on info level with `hide_health_check_logs`.
|
|
992
|
+
:type health_check_endpoints: List[str], optional
|
|
993
|
+
"""
|
|
909
994
|
self._favicon = os.environ.get("icon", "https://cdn.supervisely.com/favicon.ico")
|
|
910
995
|
JinjaWidgets().context["__favicon__"] = self._favicon
|
|
911
996
|
JinjaWidgets().context["__no_html_mode__"] = True
|
|
@@ -980,6 +1065,17 @@ class Application(metaclass=Singleton):
|
|
|
980
1065
|
hot_reload=hot_reload,
|
|
981
1066
|
before_shutdown_callbacks=self._before_shutdown_callbacks,
|
|
982
1067
|
)
|
|
1068
|
+
|
|
1069
|
+
# add filter to hide health check logs for info level
|
|
1070
|
+
if health_check_endpoints is None or len(health_check_endpoints) == 0:
|
|
1071
|
+
self._health_check_endpoints = HEALTH_ENDPOINTS
|
|
1072
|
+
else:
|
|
1073
|
+
health_check_endpoints = [endpoint.strip() for endpoint in health_check_endpoints]
|
|
1074
|
+
self._health_check_endpoints = HEALTH_ENDPOINTS + health_check_endpoints
|
|
1075
|
+
|
|
1076
|
+
if hide_health_check_logs:
|
|
1077
|
+
self._setup_health_check_filter()
|
|
1078
|
+
|
|
983
1079
|
self.test_client = TestClient(self._fastapi)
|
|
984
1080
|
|
|
985
1081
|
if not headless:
|
|
@@ -1126,6 +1222,34 @@ class Application(metaclass=Singleton):
|
|
|
1126
1222
|
def set_ready_check_function(self, func: Callable):
|
|
1127
1223
|
self._ready_check_function = func
|
|
1128
1224
|
|
|
1225
|
+
def _setup_health_check_filter(self):
|
|
1226
|
+
"""Setup filter to hide health check logs for info level."""
|
|
1227
|
+
|
|
1228
|
+
class HealthCheckFilter(logging.Filter):
|
|
1229
|
+
def __init__(self, app_instance):
|
|
1230
|
+
super().__init__()
|
|
1231
|
+
self.app: Application = app_instance
|
|
1232
|
+
|
|
1233
|
+
def filter(self, record):
|
|
1234
|
+
# Hide health check requests if NOT in debug mode
|
|
1235
|
+
if not self.app._fastapi.debug and hasattr(record, "getMessage"):
|
|
1236
|
+
message = record.getMessage()
|
|
1237
|
+
# Check if the message contains health check paths
|
|
1238
|
+
if any(path in message for path in self.app._health_check_endpoints):
|
|
1239
|
+
return False
|
|
1240
|
+
return True
|
|
1241
|
+
|
|
1242
|
+
# Apply filter to uvicorn access logger
|
|
1243
|
+
health_filter = HealthCheckFilter(self)
|
|
1244
|
+
uvicorn_logger = logging.getLogger("uvicorn.access")
|
|
1245
|
+
|
|
1246
|
+
# Remove old filters of this type, if any (for safety)
|
|
1247
|
+
uvicorn_logger.filters = [
|
|
1248
|
+
f for f in uvicorn_logger.filters if not isinstance(f, HealthCheckFilter)
|
|
1249
|
+
]
|
|
1250
|
+
|
|
1251
|
+
uvicorn_logger.addFilter(health_filter)
|
|
1252
|
+
|
|
1129
1253
|
|
|
1130
1254
|
def set_autostart_flag_from_state(default: Optional[str] = None):
|
|
1131
1255
|
"""Set `autostart` flag recieved from task state. Env name: `modal.state.autostart`.
|
|
@@ -1190,3 +1314,12 @@ def call_on_autostart(
|
|
|
1190
1314
|
|
|
1191
1315
|
def get_name_from_env(default="Supervisely App"):
|
|
1192
1316
|
return os.environ.get("APP_NAME", default)
|
|
1317
|
+
|
|
1318
|
+
def session_user_api() -> Optional[Api]:
|
|
1319
|
+
"""Returns the API instance for the current session user."""
|
|
1320
|
+
if not sly_env.is_multiuser_mode_enabled():
|
|
1321
|
+
return Api.from_env()
|
|
1322
|
+
user_id = sly_env.user_from_multiuser_app()
|
|
1323
|
+
if user_id is None:
|
|
1324
|
+
return None
|
|
1325
|
+
return _USER_API_CACHE.get(user_id, None)
|
|
@@ -11,7 +11,7 @@ from supervisely.app.singleton import Singleton
|
|
|
11
11
|
from supervisely.app.widgets_context import JinjaWidgets
|
|
12
12
|
|
|
13
13
|
# https://github.com/supervisely/js-bundle
|
|
14
|
-
js_bundle_version = "2.
|
|
14
|
+
js_bundle_version = "2.2.2"
|
|
15
15
|
|
|
16
16
|
# https://github.com/supervisely-ecosystem/supervisely-app-frontend-js
|
|
17
17
|
js_frontend_version = "v0.0.56"
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import hashlib
|
|
2
|
+
import time
|
|
3
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
4
|
+
|
|
2
5
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
6
|
+
|
|
7
|
+
import supervisely.io.env as sly_env
|
|
3
8
|
from supervisely.app.singleton import Singleton
|
|
4
9
|
|
|
5
10
|
|
|
@@ -8,6 +13,9 @@ class WebsocketManager(metaclass=Singleton):
|
|
|
8
13
|
self.app = None
|
|
9
14
|
self.path = path
|
|
10
15
|
self.active_connections: List[WebSocket] = []
|
|
16
|
+
self._connection_users: Dict[WebSocket, Optional[Union[int, str]]] = {}
|
|
17
|
+
self._cookie_user_map: Dict[str, Tuple[Union[int, str], float]] = {}
|
|
18
|
+
self._cookie_ttl_seconds = 60 * 60
|
|
11
19
|
|
|
12
20
|
def set_app(self, app: FastAPI):
|
|
13
21
|
if self.app is not None:
|
|
@@ -17,17 +25,42 @@ class WebsocketManager(metaclass=Singleton):
|
|
|
17
25
|
|
|
18
26
|
async def connect(self, websocket: WebSocket):
|
|
19
27
|
await websocket.accept()
|
|
28
|
+
user_id = self._resolve_user_id(websocket)
|
|
20
29
|
self.active_connections.append(websocket)
|
|
30
|
+
self._connection_users[websocket] = user_id
|
|
21
31
|
|
|
22
32
|
def disconnect(self, websocket: WebSocket):
|
|
23
|
-
self.active_connections
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
if websocket in self.active_connections:
|
|
34
|
+
self.active_connections.remove(websocket)
|
|
35
|
+
self._connection_users.pop(websocket, None)
|
|
36
|
+
|
|
37
|
+
def remember_user_cookie(
|
|
38
|
+
self, cookie_header: Optional[str], user_id: Optional[Union[int, str]]
|
|
39
|
+
):
|
|
40
|
+
if cookie_header is None or user_id is None:
|
|
41
|
+
return
|
|
42
|
+
fingerprint = self._cookie_fingerprint(cookie_header)
|
|
43
|
+
if fingerprint is None:
|
|
44
|
+
return
|
|
45
|
+
self._purge_cookie_cache()
|
|
46
|
+
self._cookie_user_map[fingerprint] = (user_id, time.monotonic())
|
|
47
|
+
|
|
48
|
+
async def broadcast(self, d: dict, user_id: Optional[Union[int, str]] = None):
|
|
49
|
+
if sly_env.is_multiuser_mode_enabled():
|
|
50
|
+
if user_id is None:
|
|
51
|
+
user_id = sly_env.user_from_multiuser_app()
|
|
52
|
+
if user_id is None:
|
|
53
|
+
targets = list(self.active_connections)
|
|
54
|
+
else:
|
|
55
|
+
targets = [
|
|
56
|
+
connection
|
|
57
|
+
for connection in self.active_connections
|
|
58
|
+
if self._connection_users.get(connection) == user_id
|
|
59
|
+
]
|
|
60
|
+
else:
|
|
61
|
+
targets = list(self.active_connections)
|
|
62
|
+
|
|
63
|
+
for connection in list(targets):
|
|
31
64
|
await connection.send_json(d)
|
|
32
65
|
|
|
33
66
|
async def endpoint(self, websocket: WebSocket):
|
|
@@ -37,3 +70,38 @@ class WebsocketManager(metaclass=Singleton):
|
|
|
37
70
|
data = await websocket.receive_text()
|
|
38
71
|
except WebSocketDisconnect:
|
|
39
72
|
self.disconnect(websocket)
|
|
73
|
+
|
|
74
|
+
def _resolve_user_id(self, websocket: WebSocket) -> Optional[int]:
|
|
75
|
+
if not sly_env.is_multiuser_mode_enabled():
|
|
76
|
+
return None
|
|
77
|
+
query_user = websocket.query_params.get("userId")
|
|
78
|
+
if query_user is not None:
|
|
79
|
+
try:
|
|
80
|
+
return int(query_user)
|
|
81
|
+
except ValueError:
|
|
82
|
+
pass
|
|
83
|
+
fingerprint = self._cookie_fingerprint(websocket.headers.get("cookie"))
|
|
84
|
+
if fingerprint is None:
|
|
85
|
+
return None
|
|
86
|
+
cached = self._cookie_user_map.get(fingerprint)
|
|
87
|
+
if cached is None:
|
|
88
|
+
return None
|
|
89
|
+
user_id, ts = cached
|
|
90
|
+
if time.monotonic() - ts > self._cookie_ttl_seconds:
|
|
91
|
+
self._cookie_user_map.pop(fingerprint, None)
|
|
92
|
+
return None
|
|
93
|
+
return user_id
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def _cookie_fingerprint(cookie_header: Optional[str]) -> Optional[str]:
|
|
97
|
+
if not cookie_header:
|
|
98
|
+
return None
|
|
99
|
+
return hashlib.sha256(cookie_header.encode("utf-8")).hexdigest()
|
|
100
|
+
|
|
101
|
+
def _purge_cookie_cache(self) -> None:
|
|
102
|
+
if not self._cookie_user_map:
|
|
103
|
+
return
|
|
104
|
+
cutoff = time.monotonic() - self._cookie_ttl_seconds
|
|
105
|
+
expired = [key for key, (_, ts) in self._cookie_user_map.items() if ts < cutoff]
|
|
106
|
+
for key in expired:
|
|
107
|
+
self._cookie_user_map.pop(key, None)
|
supervisely/app/singleton.py
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
1
|
+
import supervisely.io.env as sly_env
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
class Singleton(type):
|
|
2
5
|
_instances = {}
|
|
6
|
+
_nested_instances = {}
|
|
3
7
|
|
|
4
8
|
def __call__(cls, *args, **kwargs):
|
|
5
9
|
local = kwargs.pop("__local__", False)
|
|
6
10
|
if local is False:
|
|
7
11
|
if cls not in cls._instances:
|
|
8
12
|
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
|
13
|
+
|
|
14
|
+
if sly_env.is_multiuser_mode_enabled():
|
|
15
|
+
from supervisely.app.content import DataJson, StateJson
|
|
16
|
+
from copy import deepcopy
|
|
17
|
+
|
|
18
|
+
# Initialize nested instances dict once
|
|
19
|
+
nested_instances = cls._nested_instances.setdefault(cls, {})
|
|
20
|
+
|
|
21
|
+
user_id = sly_env.user_from_multiuser_app()
|
|
22
|
+
if user_id is not None and cls in (StateJson, DataJson):
|
|
23
|
+
if user_id not in nested_instances:
|
|
24
|
+
# Create new instance and copy data
|
|
25
|
+
instance = super(Singleton, cls).__call__(*args, **kwargs)
|
|
26
|
+
instance.update(deepcopy(dict(cls._instances[cls])))
|
|
27
|
+
nested_instances[user_id] = instance
|
|
28
|
+
|
|
29
|
+
return nested_instances[user_id]
|
|
9
30
|
return cls._instances[cls]
|
|
10
31
|
else:
|
|
11
32
|
return super(Singleton, cls).__call__(*args, **kwargs)
|
|
@@ -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
|
+
)
|
|
@@ -62,6 +62,7 @@ from supervisely.app.widgets.video_player.video_player import VideoPlayer
|
|
|
62
62
|
from supervisely.app.widgets.radio_group.radio_group import RadioGroup
|
|
63
63
|
from supervisely.app.widgets.switch.switch import Switch
|
|
64
64
|
from supervisely.app.widgets.input_tag.input_tag import InputTag
|
|
65
|
+
|
|
65
66
|
from supervisely.app.widgets.file_viewer.file_viewer import FileViewer
|
|
66
67
|
from supervisely.app.widgets.switch.switch import Switch
|
|
67
68
|
from supervisely.app.widgets.folder_thumbnail.folder_thumbnail import FolderThumbnail
|
|
@@ -151,4 +152,13 @@ from supervisely.app.widgets.experiment_selector.experiment_selector import Expe
|
|
|
151
152
|
from supervisely.app.widgets.bokeh.bokeh import Bokeh
|
|
152
153
|
from supervisely.app.widgets.run_app_button.run_app_button import RunAppButton
|
|
153
154
|
from supervisely.app.widgets.select_collection.select_collection import SelectCollection
|
|
154
|
-
from supervisely.app.widgets.sampling.sampling import Sampling
|
|
155
|
+
from supervisely.app.widgets.sampling.sampling import Sampling
|
|
156
|
+
from supervisely.app.widgets.input_tag_list.input_tag_list import InputTagList
|
|
157
|
+
from supervisely.app.widgets.deploy_model.deploy_model import DeployModel
|
|
158
|
+
from supervisely.app.widgets.dropdown_checkbox_selector.dropdown_checkbox_selector import (
|
|
159
|
+
DropdownCheckboxSelector,
|
|
160
|
+
)
|
|
161
|
+
from supervisely.app.widgets.ecosystem_model_selector.ecosystem_model_selector import (
|
|
162
|
+
EcosystemModelSelector,
|
|
163
|
+
)
|
|
164
|
+
from supervisely.app.widgets.heatmap.heatmap import Heatmap
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
:group-id="data.{{{ widget.widget_id }}}.teamId"
|
|
6
6
|
:options="state.{{{ widget.widget_id }}}.options"
|
|
7
7
|
:is-community="data.{{{ widget.widget_id }}}.isCommunity"
|
|
8
|
+
:disabled="data.{{{ widget.widget_id }}}.disabled"
|
|
8
9
|
{% if widget._changes_handled == true %}
|
|
9
10
|
@input="state.{{{ widget.widget_id }}}.agentId = $event; post('/{{{ widget.widget_id }}}/value_changed')"
|
|
10
11
|
{% else %}
|