solara 1.24.0__py2.py3-none-any.whl → 1.25.1__py2.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.
- solara/__init__.py +1 -1
- solara/__main__.py +4 -1
- solara/cache.py +9 -4
- solara/checks.py +9 -4
- solara/lab/components/__init__.py +1 -0
- solara/lab/components/chat.py +203 -0
- solara/minisettings.py +1 -1
- solara/server/assets/style.css +1545 -0
- solara/server/flask.py +1 -1
- solara/server/kernel.py +3 -3
- solara/server/patch.py +2 -0
- solara/server/reload.py +1 -1
- solara/server/server.py +58 -0
- solara/server/settings.py +1 -0
- solara/server/starlette.py +32 -13
- solara/server/static/solara_bootstrap.py +1 -1
- solara/server/telemetry.py +8 -3
- solara/server/templates/loader-plain.html +1 -1
- solara/server/templates/loader-solara.html +1 -1
- solara/server/templates/solara.html.j2 +20 -25
- solara/util.py +15 -2
- solara/website/components/notebook.py +44 -1
- solara/website/pages/__init__.py +3 -0
- solara/website/pages/api/__init__.py +1 -0
- solara/website/pages/api/chat.py +109 -0
- solara/website/pages/apps/jupyter-dashboard-1.py +116 -0
- solara/website/pages/apps/scatter.py +4 -4
- solara/website/pages/doc_use_download.py +1 -1
- solara/website/pages/docs/content/04-tutorial/00-overview.md +1 -0
- solara/website/pages/docs/content/04-tutorial/60-jupyter-dashboard-part1.py +18 -1
- solara/website/pages/docs/content/04-tutorial/_jupyter_dashboard_1.ipynb +607 -14
- solara/website/pages/docs/content/10-howto/ipywidget_libraries.md +1 -1
- solara/website/pages/docs/content/95-changelog.md +31 -0
- solara/website/pages/examples/ai/chatbot.py +96 -0
- solara/website/public/success.html +16 -7
- solara/website/templates/index.html.j2 +16 -15
- {solara-1.24.0.dist-info → solara-1.25.1.dist-info}/METADATA +9 -8
- {solara-1.24.0.dist-info → solara-1.25.1.dist-info}/RECORD +43 -40
- {solara-1.24.0.dist-info → solara-1.25.1.dist-info}/WHEEL +1 -1
- solara/server/assets/index.css +0 -14480
- {solara-1.24.0.data → solara-1.25.1.data}/data/prefix/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
- {solara-1.24.0.data → solara-1.25.1.data}/data/prefix/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
- {solara-1.24.0.dist-info → solara-1.25.1.dist-info}/entry_points.txt +0 -0
- {solara-1.24.0.dist-info → solara-1.25.1.dist-info}/licenses/LICENSE +0 -0
solara/server/flask.py
CHANGED
|
@@ -74,7 +74,7 @@ class WebsocketWrapper(websocket.WebsocketWrapper):
|
|
|
74
74
|
from anyio import to_thread
|
|
75
75
|
|
|
76
76
|
try:
|
|
77
|
-
return await to_thread.run_sync(lambda: self.ws.receive())
|
|
77
|
+
return await to_thread.run_sync(lambda: self.ws.receive()) # type: ignore
|
|
78
78
|
except simple_websocket.ws.ConnectionClosed:
|
|
79
79
|
raise websocket.WebSocketDisconnect()
|
|
80
80
|
|
solara/server/kernel.py
CHANGED
|
@@ -196,7 +196,7 @@ def deserialize_binary_message(bmsg):
|
|
|
196
196
|
SESSION_KEY = b"solara"
|
|
197
197
|
|
|
198
198
|
|
|
199
|
-
class WebsocketStream
|
|
199
|
+
class WebsocketStream:
|
|
200
200
|
def __init__(self, session, channel: str):
|
|
201
201
|
self.session = session
|
|
202
202
|
self.channel = channel
|
|
@@ -222,7 +222,7 @@ def send_websockets(websockets: Set[websocket.WebsocketWrapper], binary_msg):
|
|
|
222
222
|
|
|
223
223
|
class SessionWebsocket(session.Session):
|
|
224
224
|
def __init__(self, *args, **kwargs):
|
|
225
|
-
super(
|
|
225
|
+
super().__init__(*args, **kwargs)
|
|
226
226
|
self.websockets: Set[websocket.WebsocketWrapper] = set() # map from .. msg id to websocket?
|
|
227
227
|
|
|
228
228
|
def close(self):
|
|
@@ -263,7 +263,7 @@ class Kernel(ipykernel.kernelbase.Kernel):
|
|
|
263
263
|
banner = "solara"
|
|
264
264
|
|
|
265
265
|
def __init__(self):
|
|
266
|
-
super(
|
|
266
|
+
super().__init__()
|
|
267
267
|
self.session = SessionWebsocket(parent=self, key=SESSION_KEY)
|
|
268
268
|
self.msg_queue = queue.Queue() # type: ignore
|
|
269
269
|
self.stream = self.iopub_socket = WebsocketStream(self.session, "iopub")
|
solara/server/patch.py
CHANGED
|
@@ -255,6 +255,8 @@ def WidgetContextAwareThread__init__(self, *args, **kwargs):
|
|
|
255
255
|
|
|
256
256
|
|
|
257
257
|
def Thread_debug_run(self):
|
|
258
|
+
if not hasattr(self, "current_context"):
|
|
259
|
+
return Thread__run(self)
|
|
258
260
|
if self.current_context:
|
|
259
261
|
kernel_context.set_context_for_thread(self.current_context, self)
|
|
260
262
|
shell = self.current_context.kernel.shell
|
solara/server/reload.py
CHANGED
|
@@ -100,7 +100,7 @@ else:
|
|
|
100
100
|
self.directories.add(directory)
|
|
101
101
|
|
|
102
102
|
def on_modified(self, event):
|
|
103
|
-
super(
|
|
103
|
+
super().on_modified(event)
|
|
104
104
|
logger.debug("Watch event: %s", event)
|
|
105
105
|
if not event.is_directory:
|
|
106
106
|
if event.src_path in self.files:
|
solara/server/server.py
CHANGED
|
@@ -35,6 +35,7 @@ vue3 = ipyvue.__version__.startswith("3")
|
|
|
35
35
|
# first look at the project directory, then the builtin solara directory
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
@solara.memoize(storage=cache_memory)
|
|
38
39
|
def get_jinja_env(app_name: str) -> jinja2.Environment:
|
|
39
40
|
jinja_loader = jinja2.FileSystemLoader(
|
|
40
41
|
[
|
|
@@ -251,9 +252,65 @@ def read_root(path: str, root_path: str = "", render_kwargs={}, use_nbextensions
|
|
|
251
252
|
else:
|
|
252
253
|
nbextensions = []
|
|
253
254
|
|
|
255
|
+
from markupsafe import Markup
|
|
256
|
+
|
|
257
|
+
def resolve_static_path(path: str) -> Path:
|
|
258
|
+
# this solve a similar problem as the starlette and flask endpoints
|
|
259
|
+
# maybe this can be common code for all of them.
|
|
260
|
+
if path.startswith("/static/public/"):
|
|
261
|
+
directories = [default_app.directory.parent / "public"]
|
|
262
|
+
filename = path[len("/static/public/") :]
|
|
263
|
+
elif path.startswith("/static/assets/"):
|
|
264
|
+
directories = [default_app.directory.parent / "assets", solara_static.parent / "assets"]
|
|
265
|
+
filename = path[len("/static/assets/") :]
|
|
266
|
+
elif path.startswith("/static/"):
|
|
267
|
+
directories = [solara_static.parent / "static"]
|
|
268
|
+
filename = path[len("/static/") :]
|
|
269
|
+
else:
|
|
270
|
+
raise ValueError(f"invalid static path: {path}")
|
|
271
|
+
for directory in directories:
|
|
272
|
+
full_path = directory / filename
|
|
273
|
+
if full_path.exists():
|
|
274
|
+
return full_path
|
|
275
|
+
raise ValueError(f"static path not found: {filename} (path={path}), looked in {directories}")
|
|
276
|
+
|
|
277
|
+
def include_css(path: str) -> Markup:
|
|
278
|
+
filepath = resolve_static_path(path)
|
|
279
|
+
content, hash = solara.util.get_file_hash(filepath)
|
|
280
|
+
url = f"{root_path}{path}?v={hash}"
|
|
281
|
+
# when < 10k we embed, also when we use a url, it can be relative, which can break the url
|
|
282
|
+
embed = len(content) < 1024 * 10 and b"url" not in content
|
|
283
|
+
if embed:
|
|
284
|
+
content_utf8 = content.decode("utf-8")
|
|
285
|
+
code = f"<style>/*\npath={path}\n*/\n{content_utf8}</style>"
|
|
286
|
+
else:
|
|
287
|
+
code = f'<link rel="stylesheet" type="text/css" href="{url}">'
|
|
288
|
+
return Markup(code)
|
|
289
|
+
|
|
290
|
+
def include_js(path: str, module=False) -> Markup:
|
|
291
|
+
filepath = resolve_static_path(path)
|
|
292
|
+
content, hash = solara.util.get_file_hash(filepath)
|
|
293
|
+
content_utf8 = content.decode("utf-8")
|
|
294
|
+
url = f"{root_path}{path}?v={hash}"
|
|
295
|
+
# when < 10k we embed, but if we use currentScript, it can break things
|
|
296
|
+
embed = len(content) < 1024 * 10 and b"currentScript" not in content
|
|
297
|
+
if embed:
|
|
298
|
+
if module:
|
|
299
|
+
code = f'<script type="module">/*\npath={path}\n*/{content_utf8}</script>'
|
|
300
|
+
else:
|
|
301
|
+
code = f"<script>/*\npath={path}\n*/{content_utf8}</script>"
|
|
302
|
+
else:
|
|
303
|
+
if module:
|
|
304
|
+
code = f'<script type="module" src="{url}"></script>'
|
|
305
|
+
else:
|
|
306
|
+
code = f'<script src="{url}"></script>'
|
|
307
|
+
return Markup(code)
|
|
308
|
+
|
|
254
309
|
resources = {
|
|
255
310
|
"theme": "light",
|
|
256
311
|
"nbextensions": nbextensions,
|
|
312
|
+
"include_css": include_css,
|
|
313
|
+
"include_js": include_js,
|
|
257
314
|
}
|
|
258
315
|
template: jinja2.Template = get_jinja_env(app_name="__default__").get_template(template_name)
|
|
259
316
|
pre_rendered_html = ""
|
|
@@ -289,6 +346,7 @@ def read_root(path: str, root_path: str = "", render_kwargs={}, use_nbextensions
|
|
|
289
346
|
"assets": settings.assets.dict(),
|
|
290
347
|
"cdn": cdn,
|
|
291
348
|
"ipywidget_major_version": ipywidgets_major,
|
|
349
|
+
"solara_version": solara.__version__,
|
|
292
350
|
"platform": settings.main.platform,
|
|
293
351
|
"vue3": vue3,
|
|
294
352
|
"perform_check": settings.main.mode != "production" and solara.checks.should_perform_solara_check(),
|
solara/server/settings.py
CHANGED
solara/server/starlette.py
CHANGED
|
@@ -31,6 +31,8 @@ if has_solara_enterprise and sys.version_info[:2] > (3, 6):
|
|
|
31
31
|
else:
|
|
32
32
|
has_auth_support = False
|
|
33
33
|
|
|
34
|
+
import solara
|
|
35
|
+
from solara.server.threaded import ServerBase
|
|
34
36
|
from starlette.applications import Starlette
|
|
35
37
|
from starlette.exceptions import HTTPException
|
|
36
38
|
from starlette.middleware import Middleware
|
|
@@ -42,9 +44,6 @@ from starlette.routing import Mount, Route, WebSocketRoute
|
|
|
42
44
|
from starlette.staticfiles import StaticFiles
|
|
43
45
|
from starlette.types import Receive, Scope, Send
|
|
44
46
|
|
|
45
|
-
import solara
|
|
46
|
-
from solara.server.threaded import ServerBase
|
|
47
|
-
|
|
48
47
|
from . import app as appmod
|
|
49
48
|
from . import kernel_context, server, settings, telemetry, websocket
|
|
50
49
|
from .cdn_helper import cdn_url_path, get_path
|
|
@@ -74,20 +73,39 @@ class WebsocketWrapper(websocket.WebsocketWrapper):
|
|
|
74
73
|
def __init__(self, ws: starlette.websockets.WebSocket, portal: anyio.from_thread.BlockingPortal) -> None:
|
|
75
74
|
self.ws = ws
|
|
76
75
|
self.portal = portal
|
|
76
|
+
self.to_send: List[Union[str, bytes]] = []
|
|
77
|
+
if settings.main.experimental_performance:
|
|
78
|
+
self.task = asyncio.ensure_future(self.process_messages_task())
|
|
79
|
+
|
|
80
|
+
async def process_messages_task(self):
|
|
81
|
+
while True:
|
|
82
|
+
await asyncio.sleep(0.05)
|
|
83
|
+
while len(self.to_send) > 0:
|
|
84
|
+
first = self.to_send.pop(0)
|
|
85
|
+
if isinstance(first, bytes):
|
|
86
|
+
await self.ws.send_bytes(first)
|
|
87
|
+
else:
|
|
88
|
+
await self.ws.send_text(first)
|
|
77
89
|
|
|
78
90
|
def close(self):
|
|
79
91
|
self.portal.call(self.ws.close)
|
|
80
92
|
|
|
81
93
|
def send_text(self, data: str) -> None:
|
|
82
|
-
|
|
94
|
+
if settings.main.experimental_performance:
|
|
95
|
+
self.to_send.append(data)
|
|
96
|
+
else:
|
|
97
|
+
self.portal.call(self.ws.send_bytes, data)
|
|
83
98
|
|
|
84
99
|
def send_bytes(self, data: bytes) -> None:
|
|
85
|
-
|
|
100
|
+
if settings.main.experimental_performance:
|
|
101
|
+
self.to_send.append(data)
|
|
102
|
+
else:
|
|
103
|
+
self.portal.call(self.ws.send_bytes, data)
|
|
86
104
|
|
|
87
105
|
async def receive(self):
|
|
88
106
|
if hasattr(self.portal, "start_task_soon"):
|
|
89
107
|
# version 3+
|
|
90
|
-
fut = self.portal.start_task_soon(self.ws.receive)
|
|
108
|
+
fut = self.portal.start_task_soon(self.ws.receive) # type: ignore
|
|
91
109
|
else:
|
|
92
110
|
fut = self.portal.spawn_task(self.ws.receive) # type: ignore
|
|
93
111
|
|
|
@@ -136,7 +154,7 @@ class ServerStarlette(ServerBase):
|
|
|
136
154
|
asyncio.set_event_loop(loop)
|
|
137
155
|
|
|
138
156
|
# uvloop will trigger a: RuntimeError: There is no current event loop in thread 'fastapi-thread'
|
|
139
|
-
config = Config(self.app, host=self.host, port=self.port, **self.kwargs, loop="asyncio")
|
|
157
|
+
config = Config(self.app, host=self.host, port=self.port, **self.kwargs, access_log=False, loop="asyncio")
|
|
140
158
|
self.server = Server(config=config)
|
|
141
159
|
self.started.set()
|
|
142
160
|
self.server.run()
|
|
@@ -150,7 +168,6 @@ async def kernel_connection(ws: starlette.websockets.WebSocket):
|
|
|
150
168
|
session_id = ws.cookies.get(server.COOKIE_KEY_SESSION_ID)
|
|
151
169
|
|
|
152
170
|
if settings.oauth.private and not has_auth_support:
|
|
153
|
-
breakpoint()
|
|
154
171
|
raise RuntimeError("SOLARA_OAUTH_PRIVATE requires solara-enterprise")
|
|
155
172
|
if has_auth_support and "session" in ws.scope:
|
|
156
173
|
user = get_user(ws)
|
|
@@ -179,8 +196,6 @@ async def kernel_connection(ws: starlette.websockets.WebSocket):
|
|
|
179
196
|
await ws.accept()
|
|
180
197
|
|
|
181
198
|
def websocket_thread_runner(ws: starlette.websockets.WebSocket, portal: anyio.from_thread.BlockingPortal):
|
|
182
|
-
ws_wrapper = WebsocketWrapper(ws, portal)
|
|
183
|
-
|
|
184
199
|
async def run():
|
|
185
200
|
try:
|
|
186
201
|
assert session_id is not None
|
|
@@ -194,15 +209,18 @@ async def kernel_connection(ws: starlette.websockets.WebSocket):
|
|
|
194
209
|
telemetry.connection_close(session_id)
|
|
195
210
|
|
|
196
211
|
# sometimes throws: RuntimeError: Already running asyncio in this thread
|
|
197
|
-
anyio.run(run)
|
|
212
|
+
anyio.run(run) # type: ignore
|
|
198
213
|
|
|
199
214
|
# this portal allows us to sync call the websocket calls from this current event loop we are in
|
|
200
215
|
# each websocket however, is handled from a separate thread
|
|
201
216
|
try:
|
|
202
217
|
async with anyio.from_thread.BlockingPortal() as portal:
|
|
203
|
-
|
|
218
|
+
ws_wrapper = WebsocketWrapper(ws, portal)
|
|
219
|
+
thread_return = anyio.to_thread.run_sync(websocket_thread_runner, ws, portal) # type: ignore
|
|
204
220
|
await thread_return
|
|
205
221
|
finally:
|
|
222
|
+
if settings.main.experimental_performance:
|
|
223
|
+
ws_wrapper.task.cancel()
|
|
206
224
|
try:
|
|
207
225
|
await ws.close()
|
|
208
226
|
except: # noqa
|
|
@@ -229,7 +247,8 @@ async def root(request: Request, fullpath: str = ""):
|
|
|
229
247
|
if settings.main.root_path is None:
|
|
230
248
|
# use the default root path from the app, which seems to also include the path
|
|
231
249
|
# if we are mounted under a path
|
|
232
|
-
|
|
250
|
+
scope = request.scope
|
|
251
|
+
root_path = scope.get("route_root_path", scope.get("root_path", ""))
|
|
233
252
|
logger.debug("root_path: %s", root_path)
|
|
234
253
|
# or use the script-name header, for instance when running under a reverse proxy
|
|
235
254
|
script_name = request.headers.get("script-name")
|
|
@@ -119,7 +119,7 @@ async def main():
|
|
|
119
119
|
]
|
|
120
120
|
for dep in requirements:
|
|
121
121
|
await micropip.install(dep, keep_going=True)
|
|
122
|
-
await micropip.install("/wheels/solara-1.
|
|
122
|
+
await micropip.install("/wheels/solara-1.25.1-py2.py3-none-any.whl", keep_going=True)
|
|
123
123
|
import solara
|
|
124
124
|
|
|
125
125
|
el = solara.Warning("lala")
|
solara/server/telemetry.py
CHANGED
|
@@ -20,6 +20,7 @@ from . import settings
|
|
|
20
20
|
|
|
21
21
|
logger = logging.getLogger("solara.server.telemetry")
|
|
22
22
|
|
|
23
|
+
_auto_restart_enabled = False
|
|
23
24
|
_server_user_id_override = None
|
|
24
25
|
_server_start_time = time.time()
|
|
25
26
|
# Privacy note: mixpanel does not store the IP, only the region
|
|
@@ -100,7 +101,7 @@ def get_server_user_id():
|
|
|
100
101
|
|
|
101
102
|
|
|
102
103
|
def track(event: str, props: Optional[Dict] = None):
|
|
103
|
-
if
|
|
104
|
+
if _auto_restart_enabled:
|
|
104
105
|
return
|
|
105
106
|
if not settings.telemetry.mixpanel_enable:
|
|
106
107
|
return
|
|
@@ -122,6 +123,7 @@ def track(event: str, props: Optional[Dict] = None):
|
|
|
122
123
|
"docker": _docker,
|
|
123
124
|
"compute_platform": _compute_platform,
|
|
124
125
|
"vscode": _vscode,
|
|
126
|
+
"solara_mode": settings.main.mode,
|
|
125
127
|
**(solara_props or {}),
|
|
126
128
|
**(props or {}),
|
|
127
129
|
},
|
|
@@ -170,11 +172,14 @@ _thread = threading.Thread(target=_track, daemon=True)
|
|
|
170
172
|
|
|
171
173
|
|
|
172
174
|
def server_start():
|
|
173
|
-
if
|
|
175
|
+
if _auto_restart_enabled:
|
|
174
176
|
return
|
|
175
177
|
if not settings.telemetry.mixpanel_enable:
|
|
176
178
|
return
|
|
177
|
-
|
|
179
|
+
try:
|
|
180
|
+
_thread.start()
|
|
181
|
+
except RuntimeError:
|
|
182
|
+
pass
|
|
178
183
|
|
|
179
184
|
|
|
180
185
|
def server_stop():
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
<b>Made with <a href="https://solara.dev">Solara</a></b>
|
|
29
29
|
</div>
|
|
30
30
|
</div>
|
|
31
|
-
<div v-else
|
|
31
|
+
<div v-else>
|
|
32
32
|
<jupyter-widget-mount-point mount-id="solara-main">
|
|
33
33
|
A widget with mount-id="solara-main" should go here
|
|
34
34
|
</jupyter-widget-mount-point>
|
|
@@ -15,31 +15,24 @@
|
|
|
15
15
|
{{ pre_rendered_css | safe }}
|
|
16
16
|
|
|
17
17
|
{% if vue3 == True %}
|
|
18
|
-
<link href="{{cdn}}/@widgetti/solara-vuetify3-app@1.
|
|
18
|
+
<link href="{{cdn}}/@widgetti/solara-vuetify3-app@1.1.0/dist/main.css" rel="stylesheet"></link>
|
|
19
19
|
{% else %}
|
|
20
|
-
<link href="{{cdn}}/@widgetti/solara-vuetify-app@6.
|
|
20
|
+
<link href="{{cdn}}/@widgetti/solara-vuetify-app@6.1.0/dist/main.css" rel="stylesheet"></link>
|
|
21
21
|
{% endif %}
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
{% if assets.fontawesome_enabled == True %}
|
|
25
25
|
<link rel="stylesheet" href="{{cdn}}{{assets.fontawesome_path}}" type="text/css">
|
|
26
26
|
{% endif %}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<link href="{{root_path}}/static/assets/style.css" rel="stylesheet">
|
|
31
|
-
</link>
|
|
32
|
-
<link href="{{root_path}}/static/assets/index.css" rel="stylesheet">
|
|
33
|
-
</link>
|
|
27
|
+
{{ resources.include_css("/static/highlight.css") }}
|
|
28
|
+
{{ resources.include_css("/static/highlight-dark.css") }}
|
|
29
|
+
{{ resources.include_css("/static/assets/style.css") }}
|
|
34
30
|
{% if theme.variant == "light" %}
|
|
35
|
-
|
|
36
|
-
</link>
|
|
31
|
+
{{ resources.include_css("/static/assets/theme-light.css") }}
|
|
37
32
|
{% elif theme.variant == "dark" %}
|
|
38
|
-
|
|
39
|
-
</link>
|
|
33
|
+
{{ resources.include_css("/static/assets/theme-dark.css") }}
|
|
40
34
|
{% endif %}
|
|
41
|
-
|
|
42
|
-
</link>
|
|
35
|
+
{{ resources.include_css("/static/assets/custom.css") }}
|
|
43
36
|
|
|
44
37
|
<script id="jupyter-config-data" type="application/json">
|
|
45
38
|
{
|
|
@@ -64,7 +57,7 @@
|
|
|
64
57
|
{% endraw -%}
|
|
65
58
|
{# {% include "transition-domino.html" %} #}
|
|
66
59
|
{% include "loader-" ~ theme.loader ~ ".html" %}
|
|
67
|
-
<v-overlay :value="(connectionStatus != 'connected') & wasConnected">
|
|
60
|
+
<v-overlay style="z-index: 100002;" :value="(connectionStatus != 'connected') & wasConnected">
|
|
68
61
|
<v-progress-circular indeterminate size="128" style="text-align: center;">Server
|
|
69
62
|
disconnected.</v-progress-circular>
|
|
70
63
|
</v-overlay>
|
|
@@ -201,7 +194,7 @@
|
|
|
201
194
|
|
|
202
195
|
<body data-base-url="{{root_path}}/static/">
|
|
203
196
|
{% if perform_check %}
|
|
204
|
-
<iframe src="https://solara.dev/static/public/success.html?system=solara&check=html" style="display: none"></iframe>
|
|
197
|
+
<iframe src="https://solara.dev/static/public/success.html?system=solara&check=html&version={{solara_version}}" style="display: none"></iframe>
|
|
205
198
|
{% endif %}
|
|
206
199
|
{% if theme.variant == "auto" %}
|
|
207
200
|
{% endif %}
|
|
@@ -216,16 +209,18 @@
|
|
|
216
209
|
</div>
|
|
217
210
|
{% block after_pre_rendered_html %}{% endblock %}
|
|
218
211
|
{% if vue3 == True %}
|
|
212
|
+
<link href="{{cdn}}/@widgetti/solara-vuetify3-app@1.1.0/dist/fonts.css" rel="stylesheet"></link>
|
|
219
213
|
{% if production %}
|
|
220
|
-
<script src="{{cdn}}/@widgetti/solara-vuetify3-app@1.
|
|
214
|
+
<script src="{{cdn}}/@widgetti/solara-vuetify3-app@1.1.0/dist/solara-vuetify-app{{ipywidget_major_version}}.min.js"></script>
|
|
221
215
|
{% else %}
|
|
222
|
-
<script src="{{cdn}}/@widgetti/solara-vuetify3-app@1.
|
|
216
|
+
<script src="{{cdn}}/@widgetti/solara-vuetify3-app@1.1.0/dist/solara-vuetify-app{{ipywidget_major_version}}.js"></script>
|
|
223
217
|
{% endif %}
|
|
224
218
|
{% else %}
|
|
219
|
+
<link href="{{cdn}}/@widgetti/solara-vuetify-app@6.1.0/dist/fonts.css" rel="stylesheet" fetchpriority="low"></link>
|
|
225
220
|
{% if production %}
|
|
226
|
-
<script src="{{cdn}}/@widgetti/solara-vuetify-app@6.
|
|
221
|
+
<script src="{{cdn}}/@widgetti/solara-vuetify-app@6.1.0/dist/solara-vuetify-app{{ipywidget_major_version}}.min.js"></script>
|
|
227
222
|
{% else %}
|
|
228
|
-
<script src="{{cdn}}/@widgetti/solara-vuetify-app@6.
|
|
223
|
+
<script src="{{cdn}}/@widgetti/solara-vuetify-app@6.1.0/dist/solara-vuetify-app{{ipywidget_major_version}}.js"></script>
|
|
229
224
|
{% endif %}
|
|
230
225
|
{% endif %}
|
|
231
226
|
{% if production %}
|
|
@@ -238,13 +233,13 @@
|
|
|
238
233
|
console.log("rootPath", solara.rootPath);
|
|
239
234
|
</script>
|
|
240
235
|
|
|
241
|
-
|
|
242
|
-
|
|
236
|
+
{{ resources.include_js("/static/assets/custom.js") }}
|
|
237
|
+
{{ resources.include_js("/static/assets/theme.js") }}
|
|
243
238
|
|
|
244
239
|
<script src="{{cdn}}/requirejs@2.3.6/require.js" crossorigin="anonymous">
|
|
245
240
|
</script>
|
|
246
|
-
|
|
247
|
-
|
|
241
|
+
{{ resources.include_js("/static/main-vuetify.js") }}
|
|
242
|
+
{{ resources.include_js("/static/ansi.js") }}
|
|
248
243
|
|
|
249
244
|
<script>
|
|
250
245
|
solara.production = {{ production | tojson | safe }};
|
solara/util.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import contextlib
|
|
3
|
+
import hashlib
|
|
3
4
|
import os
|
|
4
5
|
import sys
|
|
5
6
|
import threading
|
|
6
7
|
from collections import abc
|
|
7
8
|
from pathlib import Path
|
|
8
|
-
from typing import TYPE_CHECKING, Dict, List, Union
|
|
9
|
+
from typing import TYPE_CHECKING, Dict, List, Tuple, Union
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
11
12
|
import numpy as np
|
|
@@ -108,7 +109,7 @@ def _flatten_style(style: Union[str, Dict, None] = None) -> str:
|
|
|
108
109
|
elif isinstance(style, str):
|
|
109
110
|
return style
|
|
110
111
|
elif isinstance(style, dict):
|
|
111
|
-
return ";".join(f"{k}:{v}" for k, v in style.items())
|
|
112
|
+
return ";".join(f"{k}:{v}" for k, v in style.items()) + ";"
|
|
112
113
|
else:
|
|
113
114
|
raise ValueError(f"Expected style to be a string or dict, got {type(style)}")
|
|
114
115
|
|
|
@@ -252,3 +253,15 @@ def parse_timedelta(size: str) -> float:
|
|
|
252
253
|
return float(size[:-1])
|
|
253
254
|
else:
|
|
254
255
|
return float(size)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def get_file_hash(path: Path, algorithm="md5") -> Tuple[bytes, str]:
|
|
259
|
+
"""Compute the hash of a file. Note that we also return the file content as bytes."""
|
|
260
|
+
data = path.read_bytes()
|
|
261
|
+
if sys.version_info[:2] < (3, 9):
|
|
262
|
+
# usedforsecurity is only available in Python 3.9+
|
|
263
|
+
h = hashlib.new(algorithm)
|
|
264
|
+
else:
|
|
265
|
+
h = hashlib.new(algorithm, usedforsecurity=False) # type: ignore
|
|
266
|
+
h.update(data)
|
|
267
|
+
return data, h.hexdigest()
|
|
@@ -72,7 +72,7 @@ def execute_notebook(path: Path):
|
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
@solara.component
|
|
75
|
-
def
|
|
75
|
+
def NotebookExecute(notebook_path: Path, show_last_expressions=False, auto_show_page=False):
|
|
76
76
|
import IPython.core.pylabtools as pylabtools
|
|
77
77
|
|
|
78
78
|
shell = IPython.get_ipython().kernel.shell
|
|
@@ -132,3 +132,46 @@ def Notebook(notebook_path: Path, show_last_expressions=False, auto_show_page=Fa
|
|
|
132
132
|
else:
|
|
133
133
|
raise ValueError(f"Unknown cell type: {cell.cell_type}, supported types are: code, markdown")
|
|
134
134
|
return main
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@solara.component
|
|
138
|
+
def Notebook(notebook_path: Path, show_last_expressions=False, auto_show_page=False, execute=True, outputs={}):
|
|
139
|
+
if execute:
|
|
140
|
+
return NotebookExecute(notebook_path, show_last_expressions, auto_show_page)
|
|
141
|
+
else:
|
|
142
|
+
with solara.Column(style={"max-width": "100%"}) as main:
|
|
143
|
+
nb: nbformat.NotebookNode = nbformat.read(notebook_path, 4)
|
|
144
|
+
for cell_index, cell in enumerate(nb.cells):
|
|
145
|
+
cell_index += 1
|
|
146
|
+
if cell.cell_type == "code":
|
|
147
|
+
if cell.source.startswith("## solara: skip"):
|
|
148
|
+
continue
|
|
149
|
+
solara.Markdown(
|
|
150
|
+
f"""
|
|
151
|
+
```python
|
|
152
|
+
{cell.source}
|
|
153
|
+
```"""
|
|
154
|
+
)
|
|
155
|
+
if cell.outputs:
|
|
156
|
+
for output in cell.outputs:
|
|
157
|
+
if output.output_type == "display_data":
|
|
158
|
+
solara.display(output.data, raw=True)
|
|
159
|
+
elif output.output_type == "execute_result":
|
|
160
|
+
if "text/html" in output.data:
|
|
161
|
+
solara.display(output.data, raw=True)
|
|
162
|
+
else:
|
|
163
|
+
cell_id = str(cell.id)
|
|
164
|
+
output = outputs.get(cell_id) or outputs.get(cell_index)
|
|
165
|
+
if output is None:
|
|
166
|
+
if cell_id in outputs or cell_index in outputs:
|
|
167
|
+
pass # explicit None
|
|
168
|
+
else:
|
|
169
|
+
solara.display(f"Output missing for cell: {cell_index} id {cell_id}")
|
|
170
|
+
else:
|
|
171
|
+
solara.display(output)
|
|
172
|
+
|
|
173
|
+
elif cell.cell_type == "markdown":
|
|
174
|
+
solara.Markdown(cell.source)
|
|
175
|
+
else:
|
|
176
|
+
raise ValueError(f"Unknown cell type: {cell.cell_type}, supported types are: code, markdown")
|
|
177
|
+
return main
|
solara/website/pages/__init__.py
CHANGED
|
@@ -284,11 +284,13 @@ def Layout(children=[]):
|
|
|
284
284
|
with rv.Row(children=children, class_="solara-page-content-search"):
|
|
285
285
|
pass
|
|
286
286
|
|
|
287
|
+
# absolute = True prevents the drawer from being below the overlay it generates
|
|
287
288
|
# Drawer navigation for top menu
|
|
288
289
|
with rv.NavigationDrawer(
|
|
289
290
|
v_model=show_right_menu,
|
|
290
291
|
on_v_model=set_show_right_menu,
|
|
291
292
|
fixed=True,
|
|
293
|
+
absolute=True,
|
|
292
294
|
right=True,
|
|
293
295
|
hide_overlay=False,
|
|
294
296
|
overlay_color="#000000",
|
|
@@ -308,6 +310,7 @@ def Layout(children=[]):
|
|
|
308
310
|
v_model=show_left_menu,
|
|
309
311
|
on_v_model=set_show_left_menu,
|
|
310
312
|
fixed=True,
|
|
313
|
+
absolute=True,
|
|
311
314
|
hide_overlay=False,
|
|
312
315
|
overlay_color="#000000",
|
|
313
316
|
overlay_opacity=0.5,
|