supervisely 6.73.452__py3-none-any.whl → 6.73.453__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/content.py +14 -6
- supervisely/app/fastapi/multi_user.py +88 -0
- supervisely/app/fastapi/subapp.py +71 -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.453.dist-info}/METADATA +1 -1
- {supervisely-6.73.452.dist-info → supervisely-6.73.453.dist-info}/RECORD +12 -11
- {supervisely-6.73.452.dist-info → supervisely-6.73.453.dist-info}/LICENSE +0 -0
- {supervisely-6.73.452.dist-info → supervisely-6.73.453.dist-info}/WHEEL +0 -0
- {supervisely-6.73.452.dist-info → supervisely-6.73.453.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.452.dist-info → supervisely-6.73.453.dist-info}/top_level.txt +0 -0
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,17 +1,18 @@
|
|
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
|
@@ -32,6 +33,7 @@ from fastapi.responses import JSONResponse
|
|
32
33
|
from fastapi.routing import APIRouter
|
33
34
|
from fastapi.staticfiles import StaticFiles
|
34
35
|
|
36
|
+
import supervisely.app.fastapi.multi_user as multi_user
|
35
37
|
import supervisely.io.env as sly_env
|
36
38
|
from supervisely._utils import (
|
37
39
|
is_debug_with_sly_net,
|
@@ -68,6 +70,7 @@ HEALTH_ENDPOINTS = ["/health", "/is_ready"]
|
|
68
70
|
# Context variable for response time
|
69
71
|
response_time_ctx: ContextVar[float] = ContextVar("response_time", default=None)
|
70
72
|
|
73
|
+
|
71
74
|
class ReadyzFilter(logging.Filter):
|
72
75
|
def filter(self, record):
|
73
76
|
if "/readyz" in record.getMessage() or "/livez" in record.getMessage():
|
@@ -623,18 +626,30 @@ def create(
|
|
623
626
|
shutdown(process_id, before_shutdown_callbacks)
|
624
627
|
|
625
628
|
if headless is False:
|
626
|
-
|
627
629
|
@app.post("/data")
|
628
630
|
async def send_data(request: Request):
|
629
|
-
|
630
|
-
|
631
|
+
if not sly_env.is_multiuser_mode_enabled():
|
632
|
+
data = DataJson()
|
633
|
+
response = JSONResponse(content=dict(data))
|
634
|
+
return response
|
635
|
+
user_id = await multi_user.extract_user_id_from_request(request)
|
636
|
+
multi_user.remember_cookie(request, user_id)
|
637
|
+
with multi_user.session_context(user_id):
|
638
|
+
data = DataJson()
|
639
|
+
response = JSONResponse(content=dict(data))
|
631
640
|
return response
|
632
641
|
|
633
642
|
@app.post("/state")
|
634
643
|
async def send_state(request: Request):
|
635
|
-
|
636
|
-
|
637
|
-
|
644
|
+
if not sly_env.is_multiuser_mode_enabled():
|
645
|
+
state = StateJson()
|
646
|
+
response = JSONResponse(content=dict(state))
|
647
|
+
else:
|
648
|
+
user_id = await multi_user.extract_user_id_from_request(request)
|
649
|
+
multi_user.remember_cookie(request, user_id)
|
650
|
+
with multi_user.session_context(user_id):
|
651
|
+
state = StateJson()
|
652
|
+
response = JSONResponse(content=dict(state))
|
638
653
|
gettrace = getattr(sys, "gettrace", None)
|
639
654
|
if (gettrace is not None and gettrace()) or is_development():
|
640
655
|
response.headers["x-debug-mode"] = "1"
|
@@ -813,41 +828,55 @@ def _init(
|
|
813
828
|
async def get_state_from_request(request: Request, call_next):
|
814
829
|
# Start timer for response time measurement
|
815
830
|
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
831
|
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
832
|
+
async def _process_request(request: Request, call_next):
|
833
|
+
if "application/json" in request.headers.get("Content-Type", ""):
|
834
|
+
content = await request.json()
|
835
|
+
request.state.context = content.get("context")
|
836
|
+
request.state.state = content.get("state")
|
837
|
+
request.state.api_token = content.get(
|
838
|
+
"api_token",
|
839
|
+
(
|
840
|
+
request.state.context.get("apiToken")
|
841
|
+
if request.state.context is not None
|
842
|
+
else None
|
843
|
+
),
|
844
|
+
)
|
845
|
+
request.state.server_address = content.get(
|
846
|
+
"server_address", sly_env.server_address(raise_not_found=False)
|
847
|
+
)
|
848
|
+
if (
|
849
|
+
request.state.server_address is not None
|
850
|
+
and request.state.api_token is not None
|
851
|
+
):
|
852
|
+
request.state.api = Api(
|
853
|
+
request.state.server_address, request.state.api_token
|
854
|
+
)
|
855
|
+
else:
|
856
|
+
request.state.api = None
|
857
|
+
|
858
|
+
try:
|
859
|
+
response = await call_next(request)
|
860
|
+
except Exception as exc:
|
861
|
+
need_to_handle_error = is_production()
|
862
|
+
response = await process_server_error(
|
863
|
+
request, exc, need_to_handle_error
|
864
|
+
)
|
865
|
+
|
866
|
+
return response
|
867
|
+
|
868
|
+
if not sly_env.is_multiuser_mode_enabled():
|
869
|
+
if headless is False:
|
870
|
+
await StateJson.from_request(request)
|
871
|
+
response = await _process_request(request, call_next)
|
872
|
+
else:
|
873
|
+
user_id = await multi_user.extract_user_id_from_request(request)
|
874
|
+
multi_user.remember_cookie(request, user_id)
|
875
|
+
|
876
|
+
with multi_user.session_context(user_id):
|
877
|
+
if headless is False:
|
878
|
+
await StateJson.from_request(request, local=False)
|
879
|
+
response = await _process_request(request, call_next)
|
851
880
|
# Calculate response time and set it for uvicorn logger in ms
|
852
881
|
elapsed_ms = round((time.perf_counter() - start_time) * 1000)
|
853
882
|
response_time_ctx.set(elapsed_ms)
|
@@ -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
|
@@ -83,12 +83,12 @@ supervisely/api/volume/volume_figure_api.py,sha256=Fs7j3h76kw7EI-o3vJHjpvL4Vxn3F
|
|
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
85
|
supervisely/app/__init__.py,sha256=4yW79U_xvo7vjg6-vRhjtt0bO8MxMSx2PD8dMamS9Q8,633
|
86
|
-
supervisely/app/content.py,sha256=
|
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
|
@@ -97,13 +97,14 @@ supervisely/app/fastapi/__init__.py,sha256=kNhkaGuBKn9-GNnPOmikIHqhjL-j66xmZaBbj
|
|
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=drAFAlFUOTehDW3Qw1ubRLsLXY1azXBwc9afu3V6ado,51131
|
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.453.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
1133
|
+
supervisely-6.73.453.dist-info/METADATA,sha256=Uh7wicYjtwccj5vCA90sExPqWVYzTdf61S2flSm5M9g,35480
|
1134
|
+
supervisely-6.73.453.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
1135
|
+
supervisely-6.73.453.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
|
1136
|
+
supervisely-6.73.453.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
|
1137
|
+
supervisely-6.73.453.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|