supervisely 6.73.452__py3-none-any.whl → 6.73.454__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/app/__init__.py +1 -1
- supervisely/app/content.py +14 -6
- supervisely/app/fastapi/__init__.py +1 -0
- supervisely/app/fastapi/multi_user.py +88 -0
- supervisely/app/fastapi/subapp.py +88 -42
- supervisely/app/fastapi/websocket.py +77 -9
- supervisely/app/singleton.py +21 -0
- supervisely/io/env.py +76 -1
- {supervisely-6.73.452.dist-info → supervisely-6.73.454.dist-info}/METADATA +1 -1
- {supervisely-6.73.452.dist-info → supervisely-6.73.454.dist-info}/RECORD +14 -13
- {supervisely-6.73.452.dist-info → supervisely-6.73.454.dist-info}/LICENSE +0 -0
- {supervisely-6.73.452.dist-info → supervisely-6.73.454.dist-info}/WHEEL +0 -0
- {supervisely-6.73.452.dist-info → supervisely-6.73.454.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.452.dist-info → supervisely-6.73.454.dist-info}/top_level.txt +0 -0
supervisely/app/__init__.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
from fastapi import FastAPI
|
2
2
|
from supervisely.app.content import StateJson, DataJson
|
3
3
|
from supervisely.app.content import get_data_dir, get_synced_data_dir
|
4
|
-
from supervisely.app.fastapi.subapp import call_on_autostart
|
4
|
+
from supervisely.app.fastapi.subapp import call_on_autostart, session_user_api
|
5
5
|
import supervisely.app.fastapi as fastapi
|
6
6
|
import supervisely.app.widgets as widgets
|
7
7
|
import supervisely.app.development as development
|
supervisely/app/content.py
CHANGED
@@ -11,6 +11,7 @@ import threading
|
|
11
11
|
import time
|
12
12
|
import traceback
|
13
13
|
from concurrent.futures import ThreadPoolExecutor
|
14
|
+
from typing import Optional, Union
|
14
15
|
|
15
16
|
import jsonpatch
|
16
17
|
from fastapi import Request
|
@@ -109,16 +110,22 @@ class _PatchableJson(dict):
|
|
109
110
|
patch.apply(self._last, in_place=True)
|
110
111
|
self._last = copy.deepcopy(self._last)
|
111
112
|
|
112
|
-
async def synchronize_changes(self):
|
113
|
+
async def synchronize_changes(self, user_id: Optional[Union[int, str]] = None):
|
113
114
|
patch = self._get_patch()
|
114
115
|
await self._apply_patch(patch)
|
115
|
-
await self._ws.broadcast(self.get_changes(patch))
|
116
|
+
await self._ws.broadcast(self.get_changes(patch), user_id=user_id)
|
116
117
|
|
117
118
|
async def send_changes_async(self):
|
118
|
-
|
119
|
+
user_id = None
|
120
|
+
if sly_env.is_multiuser_mode_enabled():
|
121
|
+
user_id = sly_env.user_from_multiuser_app()
|
122
|
+
await self.synchronize_changes(user_id=user_id)
|
119
123
|
|
120
124
|
def send_changes(self):
|
121
|
-
|
125
|
+
user_id = None
|
126
|
+
if sly_env.is_multiuser_mode_enabled():
|
127
|
+
user_id = sly_env.user_from_multiuser_app()
|
128
|
+
run_sync(self.synchronize_changes(user_id=user_id))
|
122
129
|
|
123
130
|
def raise_for_key(self, key: str):
|
124
131
|
if key in self:
|
@@ -139,7 +146,7 @@ class StateJson(_PatchableJson, metaclass=Singleton):
|
|
139
146
|
await StateJson._replace_global(dict(self))
|
140
147
|
|
141
148
|
@classmethod
|
142
|
-
async def from_request(cls, request: Request) -> StateJson:
|
149
|
+
async def from_request(cls, request: Request, local: bool = True) -> StateJson:
|
143
150
|
if "application/json" not in request.headers.get("Content-Type", ""):
|
144
151
|
return None
|
145
152
|
content = await request.json()
|
@@ -149,7 +156,8 @@ class StateJson(_PatchableJson, metaclass=Singleton):
|
|
149
156
|
# TODO: should we always replace STATE with {}?
|
150
157
|
d = content.get(Field.STATE, {})
|
151
158
|
await cls._replace_global(d)
|
152
|
-
|
159
|
+
|
160
|
+
return cls(d, __local__=local)
|
153
161
|
|
154
162
|
@classmethod
|
155
163
|
async def _replace_global(cls, d: dict):
|
@@ -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,23 +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
|
7
|
-
from contextlib import suppress
|
8
|
+
from contextlib import contextmanager, suppress
|
8
9
|
from contextvars import ContextVar
|
9
10
|
from functools import wraps
|
10
11
|
from pathlib import Path
|
11
12
|
from threading import Event as ThreadingEvent
|
12
13
|
from threading import Thread
|
13
14
|
from time import sleep
|
14
|
-
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
|
15
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
|
15
16
|
|
16
17
|
import arel
|
17
18
|
import jinja2
|
18
19
|
import numpy as np
|
19
20
|
import psutil
|
20
21
|
from async_asgi_testclient import TestClient
|
22
|
+
from cachetools import TTLCache
|
21
23
|
from fastapi import (
|
22
24
|
Depends,
|
23
25
|
FastAPI,
|
@@ -32,6 +34,7 @@ from fastapi.responses import JSONResponse
|
|
32
34
|
from fastapi.routing import APIRouter
|
33
35
|
from fastapi.staticfiles import StaticFiles
|
34
36
|
|
37
|
+
import supervisely.app.fastapi.multi_user as multi_user
|
35
38
|
import supervisely.io.env as sly_env
|
36
39
|
from supervisely._utils import (
|
37
40
|
is_debug_with_sly_net,
|
@@ -68,6 +71,10 @@ HEALTH_ENDPOINTS = ["/health", "/is_ready"]
|
|
68
71
|
# Context variable for response time
|
69
72
|
response_time_ctx: ContextVar[float] = ContextVar("response_time", default=None)
|
70
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
|
+
|
77
|
+
|
71
78
|
class ReadyzFilter(logging.Filter):
|
72
79
|
def filter(self, record):
|
73
80
|
if "/readyz" in record.getMessage() or "/livez" in record.getMessage():
|
@@ -623,18 +630,30 @@ def create(
|
|
623
630
|
shutdown(process_id, before_shutdown_callbacks)
|
624
631
|
|
625
632
|
if headless is False:
|
626
|
-
|
627
633
|
@app.post("/data")
|
628
634
|
async def send_data(request: Request):
|
629
|
-
|
630
|
-
|
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))
|
631
644
|
return response
|
632
645
|
|
633
646
|
@app.post("/state")
|
634
647
|
async def send_state(request: Request):
|
635
|
-
|
636
|
-
|
637
|
-
|
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))
|
638
657
|
gettrace = getattr(sys, "gettrace", None)
|
639
658
|
if (gettrace is not None and gettrace()) or is_development():
|
640
659
|
response.headers["x-debug-mode"] = "1"
|
@@ -813,41 +832,59 @@ def _init(
|
|
813
832
|
async def get_state_from_request(request: Request, call_next):
|
814
833
|
# Start timer for response time measurement
|
815
834
|
start_time = time.perf_counter()
|
816
|
-
if headless is False:
|
817
|
-
await StateJson.from_request(request)
|
818
|
-
|
819
|
-
if not ("application/json" not in request.headers.get("Content-Type", "")):
|
820
|
-
# {'command': 'inference_batch_ids', 'context': {}, 'state': {'dataset_id': 49711, 'batch_ids': [3120204], 'settings': None}, 'user_api_key': 'XXX', 'api_token': 'XXX', 'instance_type': None, 'server_address': 'https://app.supervisely.com'}
|
821
|
-
content = await request.json()
|
822
|
-
|
823
|
-
request.state.context = content.get("context")
|
824
|
-
request.state.state = content.get("state")
|
825
|
-
request.state.api_token = content.get(
|
826
|
-
"api_token",
|
827
|
-
(
|
828
|
-
request.state.context.get("apiToken")
|
829
|
-
if request.state.context is not None
|
830
|
-
else None
|
831
|
-
),
|
832
|
-
)
|
833
|
-
# logger.debug(f"middleware request api_token {request.state.api_token}")
|
834
|
-
request.state.server_address = content.get(
|
835
|
-
"server_address", sly_env.server_address(raise_not_found=False)
|
836
|
-
)
|
837
|
-
# request.state.server_address = sly_env.server_address(raise_not_found=False)
|
838
|
-
# logger.debug(f"middleware request server_address {request.state.server_address}")
|
839
|
-
# logger.debug(f"middleware request context {request.state.context}")
|
840
|
-
# logger.debug(f"middleware request state {request.state.state}")
|
841
|
-
if request.state.server_address is not None and request.state.api_token is not None:
|
842
|
-
request.state.api = Api(request.state.server_address, request.state.api_token)
|
843
|
-
else:
|
844
|
-
request.state.api = None
|
845
835
|
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
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
|
865
|
+
|
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)
|
851
888
|
# Calculate response time and set it for uvicorn logger in ms
|
852
889
|
elapsed_ms = round((time.perf_counter() - start_time) * 1000)
|
853
890
|
response_time_ctx.set(elapsed_ms)
|
@@ -1277,3 +1314,12 @@ def call_on_autostart(
|
|
1277
1314
|
|
1278
1315
|
def get_name_from_env(default="Supervisely App"):
|
1279
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)
|
@@ -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)
|
supervisely/io/env.py
CHANGED
@@ -1,10 +1,14 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
import json
|
3
3
|
import os
|
4
|
+
from contextvars import ContextVar, Token
|
4
5
|
from typing import Callable, List, Literal, Optional, Union
|
5
6
|
|
6
7
|
RAISE_IF_NOT_FOUND = True
|
7
|
-
|
8
|
+
_MULTIUSER_USER_CTX: ContextVar[Optional[Union[int, str]]] = ContextVar(
|
9
|
+
"supervisely_multiuser_app_user_id",
|
10
|
+
default=None,
|
11
|
+
)
|
8
12
|
|
9
13
|
def flag_from_env(s: str) -> bool:
|
10
14
|
"""Returns True if passed string is a flag, False otherwise.
|
@@ -771,3 +775,74 @@ def add_uploaded_ids_to_env(dataset_id: int, ids: List[int]) -> None:
|
|
771
775
|
else:
|
772
776
|
uploaded[str(dataset_id)].extend(ids)
|
773
777
|
os.environ["UPLOADED_IDS"] = json.dumps(uploaded)
|
778
|
+
|
779
|
+
|
780
|
+
def is_multiuser_mode_enabled() -> bool:
|
781
|
+
"""Returns multiuser app mode flag from environment variable using following keys:
|
782
|
+
- SUPERVISELY_MULTIUSER_APP_MODE
|
783
|
+
:return: multiuser app mode flag
|
784
|
+
:rtype: bool
|
785
|
+
"""
|
786
|
+
return _parse_from_env(
|
787
|
+
name="is_multiuser_mode_enabled",
|
788
|
+
keys=["SUPERVISELY_MULTIUSER_APP_MODE"],
|
789
|
+
default=False,
|
790
|
+
raise_not_found=False,
|
791
|
+
postprocess_fn=flag_from_env,
|
792
|
+
)
|
793
|
+
|
794
|
+
|
795
|
+
def enable_multiuser_app_mode() -> None:
|
796
|
+
"""
|
797
|
+
Enables multiuser app mode by setting the environment variable.
|
798
|
+
This function can be used to activate multiuser mode in the application allowing
|
799
|
+
separation of user DataJson/StateJson.
|
800
|
+
"""
|
801
|
+
os.environ["SUPERVISELY_MULTIUSER_APP_MODE"] = "true"
|
802
|
+
|
803
|
+
|
804
|
+
def disable_multiuser_app_mode() -> None:
|
805
|
+
"""Disables multiuser app mode by removing the environment variable."""
|
806
|
+
os.environ.pop("SUPERVISELY_MULTIUSER_APP_MODE", None)
|
807
|
+
|
808
|
+
|
809
|
+
def set_user_for_multiuser_app(user_id: Optional[Union[int, str]]) -> Token:
|
810
|
+
"""
|
811
|
+
Sets the user ID for multiuser app mode by setting the environment variable.
|
812
|
+
This function should be used in multiuser mode to separate user DataJson/StateJson.
|
813
|
+
|
814
|
+
:param user_id: The user ID (or session key) to set for the current request.
|
815
|
+
:type user_id: int | str
|
816
|
+
:return: A context token that can be used to reset the user ID later.
|
817
|
+
:rtype: Token
|
818
|
+
:raises RuntimeError: If multiuser app mode is not enabled.
|
819
|
+
"""
|
820
|
+
if not is_multiuser_mode_enabled():
|
821
|
+
raise RuntimeError("Multiuser app mode is not enabled. Cannot set user ID.")
|
822
|
+
return _MULTIUSER_USER_CTX.set(user_id)
|
823
|
+
|
824
|
+
|
825
|
+
def reset_user_for_multiuser_app(token: Token) -> None:
|
826
|
+
"""
|
827
|
+
Resets the user ID for multiuser app mode using the provided context token.
|
828
|
+
|
829
|
+
:param token: Context token obtained from `set_user_for_multiuser_app`.
|
830
|
+
:type token: Token
|
831
|
+
"""
|
832
|
+
if not is_multiuser_mode_enabled():
|
833
|
+
return
|
834
|
+
_MULTIUSER_USER_CTX.reset(token)
|
835
|
+
|
836
|
+
|
837
|
+
def user_from_multiuser_app() -> Optional[Union[int, str]]:
|
838
|
+
"""
|
839
|
+
Retrieves the user ID for multiuser app mode from the environment variable.
|
840
|
+
|
841
|
+
:return: The user ID if set, otherwise None.
|
842
|
+
:rtype: Optional[Union[int, str]]
|
843
|
+
"""
|
844
|
+
if not is_multiuser_mode_enabled():
|
845
|
+
return None
|
846
|
+
user_id = _MULTIUSER_USER_CTX.get(None)
|
847
|
+
if user_id is not None:
|
848
|
+
return user_id
|
@@ -82,28 +82,29 @@ supervisely/api/volume/volume_api.py,sha256=7mMfKY4HdzrbBvGZFE1SJiRi4OsgDyk3azz4
|
|
82
82
|
supervisely/api/volume/volume_figure_api.py,sha256=Fs7j3h76kw7EI-o3vJHjpvL4Vxn3Fu-DzhArgK_qrPk,26523
|
83
83
|
supervisely/api/volume/volume_object_api.py,sha256=F7pLV2MTlBlyN6fEKdxBSUatIMGWSuu8bWj3Hvcageo,2139
|
84
84
|
supervisely/api/volume/volume_tag_api.py,sha256=yNGgXz44QBSW2VGlNDOVLqLXnH8Q2fFrxDFb_girYXA,3639
|
85
|
-
supervisely/app/__init__.py,sha256=
|
86
|
-
supervisely/app/content.py,sha256=
|
85
|
+
supervisely/app/__init__.py,sha256=Z3RrMZBsaDuvSOqinD5K2rgQsQ-PuydQ1LaXbB4bXc8,651
|
86
|
+
supervisely/app/content.py,sha256=x4EZ3BbqU_nAaoXqbRspebOXDv_b-Smt0TLC6Tk0Yq0,8484
|
87
87
|
supervisely/app/exceptions.py,sha256=w01IUAq7SUu4FJDahTdR9tCoDnbobFQ6huL0uHCWxZs,1370
|
88
88
|
supervisely/app/export_template.py,sha256=_fI8B2Up0luRbRC6NxaJt-c5Z9MeJYh-RzFv0GARKSw,2801
|
89
89
|
supervisely/app/import_template.py,sha256=H2jaVekt9GZvz6FdzfrT8sJTS3VJY8JeXTCJiRv8jWI,50040
|
90
90
|
supervisely/app/jinja2.py,sha256=H3TCoLDCdTfO7Fq3ZDtcBNqfyZBlNr7p8qcKrE88HE8,341
|
91
|
-
supervisely/app/singleton.py,sha256
|
91
|
+
supervisely/app/singleton.py,sha256=-ylcXdKL-qsqgnuPl4vjr62vqxKrjLq_xeFHMqWYI64,1334
|
92
92
|
supervisely/app/widgets_context.py,sha256=JkAd4o87eqhkGJbai-Yq6YHULiPsbp1VwGOSBRxTOgY,313
|
93
93
|
supervisely/app/development/__init__.py,sha256=f2SpWBcCFPbSEBsJijH25eCAK8NcY2An5NU9vPgCBVo,135
|
94
94
|
supervisely/app/development/development.py,sha256=Ij3tn7HmkHr2RRoYxlpK9xkTGnpFjZwy2kArn6ZxVnA,12505
|
95
95
|
supervisely/app/development/sly-net.sh,sha256=M-RWQ7kygrJm88WGsOW0tEccO0fDgj_xGLI8Ifm-Ie4,1111
|
96
|
-
supervisely/app/fastapi/__init__.py,sha256=
|
96
|
+
supervisely/app/fastapi/__init__.py,sha256=ZT7hgv34YNg0CBWHcdvksqXHCcN23f1YdXp-JZd87Ek,482
|
97
97
|
supervisely/app/fastapi/custom_static_files.py,sha256=5todaVIvUG9sAt6vu1IujJn8N7zTmFhVUfeCVbuXbvc,3391
|
98
98
|
supervisely/app/fastapi/dialog_window.html,sha256=ffaAxjK0TQRa7RrY5oA4uE6RzFuS0VnRG1pfoIzTqVM,1183
|
99
99
|
supervisely/app/fastapi/index.html,sha256=dz_e-0RE5ZbOU0ToUaEHe1ROI6Tc3SPL-mHt1CpZTxQ,8793
|
100
|
+
supervisely/app/fastapi/multi_user.py,sha256=m8Iy0ibTy85C7JkkovcRbDOirmIaz8BOOmAckDLGItk,3341
|
100
101
|
supervisely/app/fastapi/no_html_main.html,sha256=NhQP7noyORBx72lFh1CQKgBRupkWjiq6Gaw-9Hkvg7c,37
|
101
102
|
supervisely/app/fastapi/offline.py,sha256=CwMMkJ1frD6wiZS-SEoNDtQ1UJcJe1Ob6ohE3r4CQL8,7414
|
102
103
|
supervisely/app/fastapi/request.py,sha256=NU7rKmxJ1pfkDZ7_yHckRcRAueJRQIqCor11UO2OHr8,766
|
103
|
-
supervisely/app/fastapi/subapp.py,sha256=
|
104
|
+
supervisely/app/fastapi/subapp.py,sha256=N5ly1SmAslUlzA2b8NRscaTTYkoubak3xmYkp5er8t4,51854
|
104
105
|
supervisely/app/fastapi/templating.py,sha256=etFkNJKvoWsHrHpmgDUuybZXcLuzS1IBRxvPnZlT9K8,2928
|
105
106
|
supervisely/app/fastapi/utils.py,sha256=t_UquzlFrdkKtAJmH6eJ279pE8Aa3BaIu4XjX-SEaIE,946
|
106
|
-
supervisely/app/fastapi/websocket.py,sha256=
|
107
|
+
supervisely/app/fastapi/websocket.py,sha256=2RqHoN7a_qPz8WMDSxCih69yyi11AaMiVtBhsyg4gxg,3953
|
107
108
|
supervisely/app/v1/__init__.py,sha256=OdU0PYv6hLwahYoyaLFO8m3cbJSchvPbqxuG1N3T734,848
|
108
109
|
supervisely/app/v1/app_config.md,sha256=-8GKbiQoX25RhEj3EDJ7TxiYuFw5wL2TO3qV5AJLZTs,2536
|
109
110
|
supervisely/app/v1/app_service.py,sha256=KGWns_M3OkQLdtsGpH3rBlA2hmSeqGkA-M5q5sTqxe4,23090
|
@@ -735,7 +736,7 @@ supervisely/imaging/font.py,sha256=0XcmWhlw7y2PAhrWgcsfInyRWj0WnlFpMSEXXilR8UA,2
|
|
735
736
|
supervisely/imaging/image.py,sha256=1KNc4qRbP9OlI4Yta07Kc2ohAgSBJ_9alF9Jag74w30,41873
|
736
737
|
supervisely/io/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
737
738
|
supervisely/io/docker_utils.py,sha256=hb_HXGM8IYB0PF-nD7NxMwaHgzaxIFxofsUzQ_RCUZI,7935
|
738
|
-
supervisely/io/env.py,sha256=
|
739
|
+
supervisely/io/env.py,sha256=9H9qXeqxw59zMAhSqlxucSc0MRfK6XI50SFcKhGMq4I,27448
|
739
740
|
supervisely/io/exception_handlers.py,sha256=22LPlLgyq59DnrhpaFrbGBYJE7uxO64VTZjsPJC0PLE,36757
|
740
741
|
supervisely/io/fs.py,sha256=pzNAK5fbT3G-7PKRY5oYcOPjgXeZ9x5Dyry7fzzZsr8,63604
|
741
742
|
supervisely/io/fs_cache.py,sha256=985gvBGzveLcDudgz10E4EWVjP9jxdU1Pa0GFfCBoCA,6520
|
@@ -1128,9 +1129,9 @@ supervisely/worker_proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
|
|
1128
1129
|
supervisely/worker_proto/worker_api_pb2.py,sha256=VQfi5JRBHs2pFCK1snec3JECgGnua3Xjqw_-b3aFxuM,59142
|
1129
1130
|
supervisely/worker_proto/worker_api_pb2_grpc.py,sha256=3BwQXOaP9qpdi0Dt9EKG--Lm8KGN0C5AgmUfRv77_Jk,28940
|
1130
1131
|
supervisely_lib/__init__.py,sha256=yRwzEQmVwSd6lUQoAUdBngKEOlnoQ6hA9ZcoZGJRNC4,331
|
1131
|
-
supervisely-6.73.
|
1132
|
-
supervisely-6.73.
|
1133
|
-
supervisely-6.73.
|
1134
|
-
supervisely-6.73.
|
1135
|
-
supervisely-6.73.
|
1136
|
-
supervisely-6.73.
|
1132
|
+
supervisely-6.73.454.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
1133
|
+
supervisely-6.73.454.dist-info/METADATA,sha256=7C7wpc584Nvw35hqqz7ByMADZ42r06AvxDFgg9yO4Hk,35480
|
1134
|
+
supervisely-6.73.454.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
1135
|
+
supervisely-6.73.454.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
|
1136
|
+
supervisely-6.73.454.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
|
1137
|
+
supervisely-6.73.454.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|