solara-enterprise 1.45.0__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.
@@ -0,0 +1,3 @@
1
+ "Enterprise features for Solara"
2
+
3
+ __version__ = "1.45.0"
@@ -0,0 +1,13 @@
1
+ from typing import Dict, Optional, cast
2
+
3
+ from solara.lab import Reactive
4
+
5
+ from .components import Avatar, AvatarMenu
6
+ from .utils import get_login_url, get_logout_url
7
+
8
+ __all__ = ["user", "get_login_url", "get_logout_url", "Avatar", "AvatarMenu"]
9
+
10
+ # the current way of generating a key is based in the default value
11
+ # which may collide after a hot reload, since solara itself is not reloaded
12
+ # if we give a fixed key, we can avoid this
13
+ user = Reactive(cast(Optional[Dict], None), key="solara-enterprise.auth.user")
@@ -0,0 +1,118 @@
1
+ from typing import Optional, Union
2
+
3
+ import reacton.ipyvuetify as v
4
+ import solara
5
+
6
+ from .. import auth, license
7
+
8
+
9
+ @solara.component
10
+ def AvatarMenu(image_url: Optional[str] = None, size: Union[int, str] = 40, color: str = "primary", children=[]):
11
+ """Show a menu with the user's avatar and a list of items.
12
+
13
+ By default the menu shows a logout button.
14
+
15
+ ## Example
16
+
17
+ ```solara
18
+ import solara
19
+ from solara_enterprise import auth
20
+
21
+ @solara.component
22
+ def Page():
23
+ if not auth.user.value:
24
+ solara.Info("Login to see your avatar")
25
+ solara.Button("Login", icon_name="mdi-login", href=auth.get_login_url())
26
+ else:
27
+ auth.AvatarMenu() # this shows the user's picture
28
+ solara.Button("Logout", icon_name="mdi-logout", href=auth.get_logout_url())
29
+ ```
30
+
31
+ Note that a common use case is to put the avatar in the [AppBar](/documentation/components/layout/app_bar).
32
+ ```solara
33
+ import solara
34
+ from solara_enterprise import auth
35
+
36
+ @solara.component
37
+ def Page():
38
+ solara.Title("Avatar in the app bar demo")
39
+ if not auth.user.value:
40
+ solara.Info("Login to see your avatar (see the button in the app bar)")
41
+ with solara.AppBar():
42
+ solara.Button(icon_name="mdi-login", href=auth.get_login_url(), icon=True)
43
+ else:
44
+ with solara.AppBar(): # this shows the user's picture in the app bar
45
+ with auth.AvatarMenu():
46
+ solara.Button("Logout", icon_name="mdi-logout", href=auth.get_logout_url(), text=True)
47
+ solara.Button("Fake user settings", icon_name="mdi-account-cog", text=True)
48
+ solara.Info("Logout via the appbar")
49
+ ```
50
+
51
+ ## Arguments
52
+
53
+ * image_url: if not given, the picture from the user's profile will be used (OAuth only)
54
+ * size: size of the avatar (in pixels)
55
+ * color: color of the avatar (if no picture is available)
56
+ * children: list of elements to show in the menu
57
+
58
+ """
59
+ license.check("auth")
60
+
61
+ with v.Html(tag="div", v_on="x.on") as activator:
62
+ Avatar(image_url=image_url, size=size, color=color)
63
+ v.Icon(children=["mdi-menu-down"])
64
+
65
+ if not children:
66
+ children = [solara.Button("logout", icon_name="mdi-logout", href=auth.get_logout_url(), text=True)]
67
+
68
+ with v.Menu(v_slots=[{"name": "activator", "children": activator, "variable": "x"}], offset_y=True) as menu:
69
+ with v.List():
70
+ for child in children:
71
+ with v.ListItem(children=[child]):
72
+ pass
73
+ return menu
74
+
75
+
76
+ @solara.component
77
+ def Avatar(image_url: Optional[str] = None, size: Union[int, str] = 40, color: str = "primary"):
78
+ """Display an avatar with the user's picture or a default icon.
79
+
80
+ ## Example
81
+
82
+ ```solara
83
+ import solara
84
+ from solara_enterprise import auth
85
+
86
+ @solara.component
87
+ def Page():
88
+ if not auth.user.value:
89
+ solara.Info("Login to see your avatar")
90
+ solara.Button("Login", icon_name="mdi-login", href=auth.get_login_url())
91
+ else:
92
+ auth.Avatar() # this shows the user's picture
93
+ solara.Button("Logout", icon_name="mdi-logout", href=auth.get_logout_url())
94
+ ```
95
+
96
+ ## Arguments
97
+
98
+ * image_url: if not given, the picture from the user's profile will be used (OAuth only)
99
+ * size: size of the avatar (in pixels)
100
+ * color: color of the avatar (if no picture is available)
101
+ """
102
+ license.check("auth")
103
+ user = auth.user.value
104
+ if user:
105
+ user_info = user.get("userinfo", {})
106
+ src = image_url
107
+ if src is None:
108
+ src = user_info.get("picture")
109
+ if src:
110
+ with v.Avatar(size=size, class_="ma-2"):
111
+ v.Img(src=src)
112
+ else:
113
+ with v.Avatar(size=size, color=color):
114
+ v.Icon(children=["mdi-account"])
115
+ else:
116
+ with v.Avatar(size=size, color=color):
117
+ with solara.Tooltip("No user"):
118
+ v.Icon(children=["mdi-error"])
@@ -0,0 +1,119 @@
1
+ import json
2
+ import logging
3
+ from typing import Dict, Optional
4
+
5
+ from authlib.integrations.flask_client import OAuth
6
+ from flask import redirect, request, session
7
+ from solara.server import settings
8
+
9
+ from .. import license
10
+
11
+ logger = logging.getLogger("solara.enterprise.auth.starlette")
12
+
13
+
14
+ oauth: Optional[OAuth] = None
15
+ _app = None
16
+
17
+
18
+ def init_flask(app):
19
+ global _app
20
+ _app = app
21
+ init()
22
+
23
+
24
+ def init():
25
+ global oauth
26
+ assert _app is not None
27
+ _app.secret_key = settings.oauth.client_secret
28
+ if settings.oauth.client_id:
29
+ api_base_url = settings.oauth.api_base_url
30
+ if not api_base_url.startswith("https://") and not api_base_url.startswith("http://"):
31
+ api_base_url = f"https://{api_base_url}"
32
+
33
+ oauth = OAuth(_app)
34
+ oauth.register(
35
+ name="oauth1",
36
+ client_id=settings.oauth.client_id,
37
+ client_secret=settings.oauth.client_secret,
38
+ api_base_url=api_base_url,
39
+ server_metadata_url=f"{api_base_url}/.well-known/openid-configuration",
40
+ client_kwargs={"scope": settings.oauth.scope},
41
+ )
42
+
43
+
44
+ def check_oauth():
45
+ assert oauth is not None
46
+ assert oauth.oauth1 is not None
47
+ if oauth.oauth1.client_id != settings.oauth.client_id:
48
+ init()
49
+
50
+
51
+ def authorize():
52
+ check_oauth()
53
+ assert oauth is not None
54
+ assert oauth.oauth1 is not None
55
+
56
+ org_url = session.pop("redirect_uri", settings.main.base_url + "/")
57
+
58
+ token = oauth.oauth1.authorize_access_token()
59
+ # workaround: if token is set in the session in one piece, it is not saved, so we
60
+ # split it up
61
+ token.pop("id_token", None)
62
+ user = token.pop("userinfo", None)
63
+ session["token"] = json.dumps(token)
64
+ session["user"] = json.dumps(user)
65
+
66
+ return redirect(org_url)
67
+
68
+
69
+ def logout():
70
+ redirect_uri = request.args.get("redirect_uri", "/")
71
+ # ideally, we only remove these:
72
+ session.pop("token", None)
73
+ session.pop("user", None)
74
+ session.pop("client_id", None)
75
+ # but authlib sometimes leaves some stuff in the session on failed logins
76
+ # so we clear it all
77
+ return redirect(redirect_uri)
78
+
79
+
80
+ def login(redirect_uri: Optional[str] = None):
81
+ license.check("auth")
82
+ check_oauth()
83
+ assert oauth is not None
84
+ assert oauth.oauth1 is not None
85
+ if "redirect_uri" in request.args:
86
+ # we arrived here via the auth.get_login_url() call, which means the
87
+ # redirect_uri is in the query params
88
+ session["redirect_uri"] = request.args["redirect_uri"]
89
+ else:
90
+ # otherwise we assume we got here via the solara.server.starlette method
91
+ # where it detect we the OAuth.private=True setting, leading to a redirect
92
+ session["redirect_uri"] = str(request.url)
93
+ session["client_id"] = settings.oauth.client_id
94
+ callback_url = str(settings.main.base_url) + "_solara/auth/authorize"
95
+ result = oauth.oauth1.authorize_redirect(callback_url)
96
+ return result
97
+
98
+
99
+ def allowed():
100
+ if settings.oauth.private:
101
+ user = session.get("user")
102
+ if not user:
103
+ return False
104
+ else:
105
+ client_id = session.get("client_id")
106
+ if client_id != settings.oauth.client_id:
107
+ return False
108
+ return True
109
+
110
+
111
+ def get_user() -> Optional[Dict]:
112
+ user = session.get("user")
113
+ if user:
114
+ user = json.loads(session["token"])
115
+ user["userinfo"] = json.loads(session["user"])
116
+ client_id = session.get("client_id")
117
+ if client_id != settings.oauth.client_id:
118
+ user = None
119
+ return user
@@ -0,0 +1,127 @@
1
+ import json
2
+ import sys
3
+ import typing
4
+ from base64 import b64decode, b64encode
5
+
6
+ import itsdangerous
7
+ from itsdangerous.exc import BadSignature
8
+ from starlette.datastructures import MutableHeaders, Secret
9
+ from starlette.middleware.sessions import SessionMiddleware
10
+ from starlette.requests import HTTPConnection
11
+ from starlette.types import ASGIApp, Message, Receive, Scope, Send
12
+
13
+ if sys.version_info >= (3, 8): # pragma: no cover
14
+ from typing import Literal
15
+ else: # pragma: no cover
16
+ from typing_extensions import Literal
17
+
18
+
19
+ # mutable mapping that keeps track of whether it has been modified
20
+ class ModifiedDict(dict):
21
+ def __init__(self, *args, **kwargs):
22
+ super().__init__(*args, **kwargs)
23
+ self.modified = False
24
+
25
+ def __setitem__(self, key, value):
26
+ super().__setitem__(key, value)
27
+ self.modified = True
28
+
29
+ def __delitem__(self, key):
30
+ super().__delitem__(key)
31
+ self.modified = True
32
+
33
+ def clear(self):
34
+ super().clear()
35
+ self.modified = True
36
+
37
+ def pop(self, key, default=None):
38
+ value = super().pop(key, default)
39
+ self.modified = True
40
+ return value
41
+
42
+ def popitem(self):
43
+ value = super().popitem()
44
+ self.modified = True
45
+ return value
46
+
47
+ def setdefault(self, key, default=None):
48
+ value = super().setdefault(key, default)
49
+ self.modified = True
50
+ return value
51
+
52
+ def update(self, *args, **kwargs):
53
+ super().update(*args, **kwargs)
54
+ self.modified = True
55
+
56
+
57
+ class MutateDetectSessionMiddleware(SessionMiddleware):
58
+ def __init__(
59
+ self,
60
+ app: ASGIApp,
61
+ secret_key: typing.Union[str, Secret],
62
+ session_cookie: str = "session",
63
+ max_age: typing.Optional[int] = 14 * 24 * 60 * 60, # 14 days, in seconds
64
+ path: str = "/",
65
+ same_site: Literal["lax", "strict", "none"] = "lax",
66
+ https_only: bool = False,
67
+ ) -> None:
68
+ self.app = app
69
+ self.signer = itsdangerous.TimestampSigner(str(secret_key))
70
+ self.session_cookie = session_cookie
71
+ self.max_age = max_age
72
+ self.path = path
73
+ self.security_flags = "httponly; samesite=" + same_site
74
+ if https_only: # Secure flag can be used with HTTPS only
75
+ self.security_flags += "; secure"
76
+
77
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
78
+ if scope["type"] not in ("http", "websocket"): # pragma: no cover
79
+ await self.app(scope, receive, send)
80
+ return
81
+
82
+ connection = HTTPConnection(scope)
83
+ initial_session_was_empty = True
84
+
85
+ if self.session_cookie in connection.cookies:
86
+ data = connection.cookies[self.session_cookie].encode("utf-8")
87
+ try:
88
+ data = self.signer.unsign(data, max_age=self.max_age)
89
+ scope["session"] = ModifiedDict(json.loads(b64decode(data)))
90
+ initial_session_was_empty = False
91
+ except BadSignature:
92
+ scope["session"] = ModifiedDict()
93
+ else:
94
+ scope["session"] = ModifiedDict()
95
+
96
+ async def send_wrapper(message: Message) -> None:
97
+ if message["type"] == "http.response.start":
98
+ if scope["session"]:
99
+ # this deviates for starlette, we only update the cookie if the session has been modified
100
+ # this avoids race conditions where the session is modified by another request
101
+ if scope["session"].modified:
102
+ # We have session data to persist.
103
+ data = b64encode(json.dumps(scope["session"]).encode("utf-8"))
104
+ data = self.signer.sign(data)
105
+ headers = MutableHeaders(scope=message)
106
+ header_value = "{session_cookie}={data}; path={path}; {max_age}{security_flags}".format( # noqa E501
107
+ session_cookie=self.session_cookie,
108
+ data=data.decode("utf-8"),
109
+ path=self.path,
110
+ max_age=f"Max-Age={self.max_age}; " if self.max_age else "",
111
+ security_flags=self.security_flags,
112
+ )
113
+ headers.append("Set-Cookie", header_value)
114
+ elif not initial_session_was_empty:
115
+ # The session has been cleared.
116
+ headers = MutableHeaders(scope=message)
117
+ header_value = "{session_cookie}={data}; path={path}; {expires}{security_flags}".format( # noqa E501
118
+ session_cookie=self.session_cookie,
119
+ data="null",
120
+ path=self.path,
121
+ expires="expires=Thu, 01 Jan 1970 00:00:00 GMT; ",
122
+ security_flags=self.security_flags,
123
+ )
124
+ headers.append("Set-Cookie", header_value)
125
+ await send(message)
126
+
127
+ await self.app(scope, receive, send_wrapper)
@@ -0,0 +1,118 @@
1
+ import json
2
+ import logging
3
+ from typing import Dict, Optional
4
+
5
+ from authlib.integrations.starlette_client import OAuth
6
+ from solara.server import settings
7
+ from starlette.authentication import (
8
+ AuthCredentials,
9
+ AuthenticationBackend,
10
+ SimpleUser,
11
+ UnauthenticatedUser,
12
+ )
13
+ from starlette.requests import HTTPConnection, Request
14
+ from starlette.responses import RedirectResponse
15
+
16
+ from .. import license
17
+
18
+ logger = logging.getLogger("solara.enterprise.auth.starlette")
19
+
20
+
21
+ oauth: Optional[OAuth] = None
22
+
23
+
24
+ def init():
25
+ global oauth
26
+ if settings.oauth.client_id:
27
+ api_base_url = settings.oauth.api_base_url
28
+ if not api_base_url.startswith("https://") and not api_base_url.startswith("http://"):
29
+ api_base_url = f"https://{api_base_url}"
30
+ oauth = OAuth()
31
+ oauth.register(
32
+ name="oauth1",
33
+ client_id=settings.oauth.client_id,
34
+ client_secret=settings.oauth.client_secret,
35
+ api_base_url=api_base_url,
36
+ server_metadata_url=f"{api_base_url}/.well-known/openid-configuration",
37
+ client_kwargs={"scope": settings.oauth.scope},
38
+ )
39
+
40
+
41
+ def check_oauth():
42
+ assert oauth is not None
43
+ assert oauth.oauth1 is not None
44
+ if oauth.oauth1.client_id != settings.oauth.client_id:
45
+ init()
46
+
47
+
48
+ init()
49
+
50
+
51
+ async def authorize(request: Request):
52
+ check_oauth()
53
+ assert oauth is not None
54
+ assert oauth.oauth1 is not None
55
+
56
+ org_url = request.session.pop("redirect_uri", settings.main.base_url + "/")
57
+
58
+ token = await oauth.oauth1.authorize_access_token(request)
59
+ # workaround: if token is set in the session in one piece, it is not saved, so we
60
+ # split it up
61
+ token.pop("id_token", None)
62
+ user = token.pop("userinfo", None)
63
+ request.session["token"] = json.dumps(token)
64
+ request.session["user"] = json.dumps(user)
65
+
66
+ return RedirectResponse(org_url)
67
+
68
+
69
+ async def logout(request: Request):
70
+ redirect_uri = request.query_params.get("redirect_uri", "/")
71
+ # ideally, we only remove these:
72
+ request.session.pop("token", None)
73
+ request.session.pop("user", None)
74
+ request.session.pop("client_id", None)
75
+ # but authlib sometimes leaves some stuff in the session on failed logins
76
+ # so we clear it all
77
+ request.session.clear()
78
+ return RedirectResponse(redirect_uri)
79
+
80
+
81
+ async def login(request: Request, redirect_uri: Optional[str] = None):
82
+ license.check("auth")
83
+ check_oauth()
84
+ assert oauth is not None
85
+ assert oauth.oauth1 is not None
86
+ if "redirect_uri" in request.query_params:
87
+ # we arrived here via the auth.get_login_url() call, which means the
88
+ # redirect_uri is in the query params
89
+ request.session["redirect_uri"] = request.query_params["redirect_uri"]
90
+ else:
91
+ # otherwise we assume we got here via the solara.server.starlette method
92
+ # where it detect we the OAuth.required=True setting, leading to a redirect
93
+ request.session["redirect_uri"] = str(request.url.path)
94
+ request.session["client_id"] = settings.oauth.client_id
95
+ result = await oauth.oauth1.authorize_redirect(request, str(settings.main.base_url) + "_solara/auth/authorize")
96
+ return result
97
+
98
+
99
+ def get_user(request: HTTPConnection) -> Optional[Dict]:
100
+ user = request.session.get("user")
101
+ if user:
102
+ user = json.loads(request.session["token"])
103
+ user["userinfo"] = json.loads(request.session["user"])
104
+ client_id = request.session.get("client_id")
105
+ if client_id != settings.oauth.client_id:
106
+ user = None
107
+ return user
108
+
109
+
110
+ # only provides request.user.is_authenticated
111
+ class AuthBackend(AuthenticationBackend):
112
+ async def authenticate(self, conn: HTTPConnection):
113
+ user = get_user(conn)
114
+ if user is None:
115
+ return AuthCredentials(), UnauthenticatedUser()
116
+ else:
117
+ username = "noname"
118
+ return AuthCredentials(["authenticated"]), SimpleUser(username)
@@ -0,0 +1,36 @@
1
+ import logging
2
+ import urllib.parse
3
+ from typing import Optional
4
+
5
+ from solara.routing import router_context
6
+ from solara.server import settings
7
+
8
+ logger = logging.getLogger("solara-enterprise.auth")
9
+
10
+
11
+ def get_logout_url(return_to_path: Optional[str] = None):
12
+ if return_to_path is None:
13
+ router = router_context.get()
14
+ return_to_path = router.path
15
+ if return_to_path.startswith("/"):
16
+ return_to_path = return_to_path[1:]
17
+ assert settings.main.base_url is not None
18
+ return_to_app = urllib.parse.quote(settings.main.base_url + return_to_path)
19
+ return_to = urllib.parse.quote(settings.main.base_url + f"_solara/auth/logout?redirect_uri={return_to_app}")
20
+ client_id = settings.oauth.client_id
21
+ url = f"https://{settings.oauth.api_base_url}/{settings.oauth.logout_path}"
22
+ if settings.oauth.logout_path.startswith("http"):
23
+ url = settings.oauth.logout_path
24
+ return f"{url}?returnTo={return_to}&redirect_uri={return_to}&post_logout_redirect_uri={return_to}&client_id={client_id}"
25
+
26
+
27
+ def get_login_url(return_to_path: Optional[str] = None):
28
+ if return_to_path is None:
29
+ router = router_context.get()
30
+ return_to_path = router.path
31
+ if return_to_path.startswith("/"):
32
+ return_to_path = return_to_path[1:]
33
+ assert settings.main.base_url is not None
34
+ redirect_uri = urllib.parse.quote(settings.main.base_url + return_to_path)
35
+ root = settings.main.root_path or ""
36
+ return f"{root}/_solara/auth/login?redirect_uri={redirect_uri}"
@@ -0,0 +1,3 @@
1
+ from .. import license
2
+
3
+ license.check("cache")
@@ -0,0 +1,64 @@
1
+ import hashlib
2
+ import pickle
3
+ import sys
4
+ from typing import Any, Callable, MutableMapping
5
+
6
+
7
+ def make_key(object):
8
+ """Generates a key by pickling the object, and generating an md5 hash of the pickled object.
9
+
10
+ Primitive objects such as short (<100 length) strings and ints are not pickled, and are used as is.
11
+ """
12
+ if isinstance(object, str) and len(object) < 100:
13
+ return object.encode("utf-8")
14
+ elif isinstance(object, int):
15
+ return str(object).encode("utf-8")
16
+ else:
17
+ bytes = pickle.dumps(object)
18
+ if sys.version_info[:2] < (3, 9):
19
+ return hashlib.md5(bytes).digest()
20
+ else:
21
+ return hashlib.md5(bytes, usedforsecurity=False).digest() # type: ignore
22
+
23
+
24
+ class Base(MutableMapping):
25
+ def __init__(self, wrapper_dict, clear=False, prefix=b"", make_key: Callable[[Any], bytes] = make_key):
26
+ assert wrapper_dict is not None
27
+ self._wrapper_dict = wrapper_dict
28
+ self._make_key = make_key
29
+ self.prefix = prefix
30
+ if clear:
31
+ self.clear()
32
+
33
+ def generate_key(self, key) -> bytes:
34
+ return self.prefix + bytes(self._make_key(key))
35
+
36
+ def __getitem__(self, key):
37
+ if isinstance(key, bytes) and key.startswith(self.prefix):
38
+ wrapper_key = key
39
+ else:
40
+ wrapper_key = self.generate_key(key)
41
+ return pickle.loads(self._wrapper_dict[wrapper_key]) # type: ignore
42
+
43
+ def __setitem__(self, key, value):
44
+ if isinstance(key, bytes) and key.startswith(self.prefix):
45
+ wrapper_key = key
46
+ else:
47
+ wrapper_key = self.generate_key(key)
48
+ self._wrapper_dict[wrapper_key] = pickle.dumps(value)
49
+
50
+ def __delitem__(self, key):
51
+ if isinstance(key, bytes) and key.startswith(self.prefix):
52
+ wrapper_key = key
53
+ else:
54
+ wrapper_key = self.generate_key(key)
55
+ del self._wrapper_dict[wrapper_key]
56
+
57
+ def __iter__(self):
58
+ return iter(self.keys())
59
+
60
+ def __len__(self):
61
+ return len(self.keys())
62
+
63
+ def keys(self):
64
+ return self._wrapper_dict.keys()
@@ -0,0 +1,44 @@
1
+ import logging
2
+ import shutil
3
+ from typing import MutableMapping
4
+
5
+ import diskcache
6
+ import solara.settings
7
+ import solara.util
8
+
9
+ logger = logging.getLogger("solara-enterprise.cache.disk")
10
+
11
+
12
+ class Disk(MutableMapping):
13
+ def __init__(
14
+ self,
15
+ clear=False,
16
+ max_size=solara.settings.cache.disk_max_size,
17
+ path=solara.settings.cache.path,
18
+ ):
19
+ # same as solara.cache.Memory
20
+ eviction_policy = "least-recently-used"
21
+ max_size = solara.util.parse_size(max_size)
22
+ if clear:
23
+ try:
24
+ logger.debug("Clearing disk cache: %s", path)
25
+ shutil.rmtree(path)
26
+ except OSError: # Windows wonkiness
27
+ logger.exception(f"Error clearing disk cache: {path}")
28
+
29
+ self.diskcache = diskcache.Cache(path, size_limit=max_size, eviction_policy=eviction_policy)
30
+
31
+ def __getitem__(self, key):
32
+ return self.diskcache[key]
33
+
34
+ def __setitem__(self, key, value):
35
+ self.diskcache[key] = value
36
+
37
+ def __delitem__(self, key):
38
+ del self.diskcache[key]
39
+
40
+ def __iter__(self):
41
+ return iter(self.diskcache)
42
+
43
+ def __len__(self):
44
+ return len(self.diskcache)
@@ -0,0 +1,29 @@
1
+ import logging
2
+ import pickle
3
+ from typing import Any, Callable, MutableMapping
4
+
5
+ import solara.settings
6
+ import solara.util
7
+ from cachetools import LRUCache
8
+
9
+ from solara_enterprise.cache.base import Base, make_key
10
+
11
+ logger = logging.getLogger("solara-enterprise.cache.memory")
12
+
13
+
14
+ def sizeof(obj):
15
+ size = len(pickle.dumps(obj))
16
+ logger.debug("size of %s: %s", obj, size)
17
+ return size
18
+
19
+
20
+ class MemorySize(Base):
21
+ def __init__(
22
+ self,
23
+ max_size=solara.settings.cache.memory_max_size,
24
+ make_key: Callable[[Any], bytes] = make_key,
25
+ sizeof: Callable[[Any], int] = sizeof,
26
+ ):
27
+ maxsize = solara.util.parse_size(max_size)
28
+ _wrapper_dict: MutableMapping[bytes, bytes] = LRUCache(maxsize=maxsize, getsizeof=sizeof)
29
+ super().__init__(_wrapper_dict, make_key=make_key)