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.
Files changed (44) hide show
  1. solara/__init__.py +1 -1
  2. solara/__main__.py +4 -1
  3. solara/cache.py +9 -4
  4. solara/checks.py +9 -4
  5. solara/lab/components/__init__.py +1 -0
  6. solara/lab/components/chat.py +203 -0
  7. solara/minisettings.py +1 -1
  8. solara/server/assets/style.css +1545 -0
  9. solara/server/flask.py +1 -1
  10. solara/server/kernel.py +3 -3
  11. solara/server/patch.py +2 -0
  12. solara/server/reload.py +1 -1
  13. solara/server/server.py +58 -0
  14. solara/server/settings.py +1 -0
  15. solara/server/starlette.py +32 -13
  16. solara/server/static/solara_bootstrap.py +1 -1
  17. solara/server/telemetry.py +8 -3
  18. solara/server/templates/loader-plain.html +1 -1
  19. solara/server/templates/loader-solara.html +1 -1
  20. solara/server/templates/solara.html.j2 +20 -25
  21. solara/util.py +15 -2
  22. solara/website/components/notebook.py +44 -1
  23. solara/website/pages/__init__.py +3 -0
  24. solara/website/pages/api/__init__.py +1 -0
  25. solara/website/pages/api/chat.py +109 -0
  26. solara/website/pages/apps/jupyter-dashboard-1.py +116 -0
  27. solara/website/pages/apps/scatter.py +4 -4
  28. solara/website/pages/doc_use_download.py +1 -1
  29. solara/website/pages/docs/content/04-tutorial/00-overview.md +1 -0
  30. solara/website/pages/docs/content/04-tutorial/60-jupyter-dashboard-part1.py +18 -1
  31. solara/website/pages/docs/content/04-tutorial/_jupyter_dashboard_1.ipynb +607 -14
  32. solara/website/pages/docs/content/10-howto/ipywidget_libraries.md +1 -1
  33. solara/website/pages/docs/content/95-changelog.md +31 -0
  34. solara/website/pages/examples/ai/chatbot.py +96 -0
  35. solara/website/public/success.html +16 -7
  36. solara/website/templates/index.html.j2 +16 -15
  37. {solara-1.24.0.dist-info → solara-1.25.1.dist-info}/METADATA +9 -8
  38. {solara-1.24.0.dist-info → solara-1.25.1.dist-info}/RECORD +43 -40
  39. {solara-1.24.0.dist-info → solara-1.25.1.dist-info}/WHEEL +1 -1
  40. solara/server/assets/index.css +0 -14480
  41. {solara-1.24.0.data → solara-1.25.1.data}/data/prefix/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
  42. {solara-1.24.0.data → solara-1.25.1.data}/data/prefix/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
  43. {solara-1.24.0.dist-info → solara-1.25.1.dist-info}/entry_points.txt +0 -0
  44. {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(object):
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(SessionWebsocket, self).__init__(*args, **kwargs)
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(Kernel, self).__init__()
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(WatcherWatchdog, self).on_modified(event)
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
@@ -151,6 +151,7 @@ class MainSettings(BaseSettings):
151
151
  base_url: str = "" # e.g. https://myapp.solara.run/myapp/
152
152
  platform: str = sys.platform
153
153
  host: str = HOST_DEFAULT
154
+ experimental_performance: bool = False
154
155
 
155
156
  class Config:
156
157
  env_prefix = "solara_"
@@ -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
- self.portal.call(self.ws.send_text, data)
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
- self.portal.call(self.ws.send_bytes, data)
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
- thread_return = anyio.to_thread.run_sync(websocket_thread_runner, ws, portal)
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
- root_path = request.scope.get("root_path", "")
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.24.0-py2.py3-none-any.whl", keep_going=True)
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")
@@ -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 settings.main.mode == "development":
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 settings.main.mode == "development":
175
+ if _auto_restart_enabled:
174
176
  return
175
177
  if not settings.telemetry.mixpanel_enable:
176
178
  return
177
- _thread.start()
179
+ try:
180
+ _thread.start()
181
+ except RuntimeError:
182
+ pass
178
183
 
179
184
 
180
185
  def server_stop():
@@ -11,7 +11,7 @@
11
11
  </v-card-title>
12
12
  </v-card>
13
13
  </div>
14
- <div v-else style="isolation: isolate">
14
+ <div v-else>
15
15
  <jupyter-widget-mount-point v-if="!loading" mount-id="solara-main">
16
16
  A widget with mount-id="solara-main" should go here
17
17
  </jupyter-widget-mount-point>
@@ -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 style="isolation: isolate">
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.0.0/dist/main.css" rel="stylesheet"></link>
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.0.0/dist/main.css" rel="stylesheet"></link>
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
- <link href="{{root_path}}/static/highlight.css" rel="stylesheet">
28
- <link href="{{root_path}}/static/highlight-dark.css" rel="stylesheet">
29
- </link>
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
- <link href="{{root_path}}/static/assets/theme-light.css" rel="stylesheet">
36
- </link>
31
+ {{ resources.include_css("/static/assets/theme-light.css") }}
37
32
  {% elif theme.variant == "dark" %}
38
- <link href="{{root_path}}/static/assets/theme-dark.css" rel="stylesheet">
39
- </link>
33
+ {{ resources.include_css("/static/assets/theme-dark.css") }}
40
34
  {% endif %}
41
- <link href="{{root_path}}/static/assets/custom.css" rel="stylesheet">
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.0.0/dist/solara-vuetify-app{{ipywidget_major_version}}.min.js"></script>
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.0.0/dist/solara-vuetify-app{{ipywidget_major_version}}.js"></script>
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.0.0/dist/solara-vuetify-app{{ipywidget_major_version}}.min.js"></script>
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.0.0/dist/solara-vuetify-app{{ipywidget_major_version}}.js"></script>
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
- <script src="{{root_path}}/static/assets/custom.js"></script>
242
- <script src="{{root_path}}/static/assets/theme.js"></script>
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
- <script src="{{root_path}}/static/main-vuetify.js"></script>
247
- <script src="{{root_path}}/static/ansi.js"></script>
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 Notebook(notebook_path: Path, show_last_expressions=False, auto_show_page=False):
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
@@ -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,
@@ -118,6 +118,7 @@ items = [
118
118
  "name": "Lab (experimental)",
119
119
  "icon": "mdi-flask-outline",
120
120
  "pages": [
121
+ "chat",
121
122
  "confirmation_dialog",
122
123
  "menu",
123
124
  "input_date",