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.
@@ -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
- await self.synchronize_changes()
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
- run_sync(self.synchronize_changes())
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
- return cls(d, __local__=True)
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
- data = DataJson()
630
- response = JSONResponse(content=dict(data))
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
- state = StateJson()
636
-
637
- response = JSONResponse(content=dict(state))
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
- try:
847
- response = await call_next(request)
848
- except Exception as exc:
849
- need_to_handle_error = is_production()
850
- response = await process_server_error(request, exc, need_to_handle_error)
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
- from typing import List
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.remove(websocket)
24
-
25
- async def broadcast(self, d: dict):
26
- # if self.app is None:
27
- # raise ValueError(
28
- # "WebSocket is not initialized, use Websocket middleware for that"
29
- # )
30
- for connection in self.active_connections:
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)
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: supervisely
3
- Version: 6.73.452
3
+ Version: 6.73.453
4
4
  Summary: Supervisely Python SDK.
5
5
  Home-page: https://github.com/supervisely/supervisely
6
6
  Author: Supervisely
@@ -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=MwUr2zlCMKU7YD_DIubq0WA2LuQ5dvlv1rsMp76paEg,8083
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=g8FI4vqt76tXF09tsb05N_N4ouTI0LJGZekrgVkulg0,406
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=GxMeYMy7lCkef8R_61F2XqxKjJjtyAk7CNDSCIc07ZY,50127
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=TlRSPOAhRItTv1HGvdukK1ZvhRjMUxRa-lJlsRR9rJw,1308
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=wf15PXGd_xzY6c4g8ywPGcZ25A8a9Jv_BXg9-x61cgg,24884
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.452.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1132
- supervisely-6.73.452.dist-info/METADATA,sha256=o1IIrOeH7ZDHD8bH7l_R6v7BpmNSoI1i5P0O0c-eNFg,35480
1133
- supervisely-6.73.452.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
1134
- supervisely-6.73.452.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1135
- supervisely-6.73.452.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1136
- supervisely-6.73.452.dist-info/RECORD,,
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,,