solara 1.25.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 (34) hide show
  1. solara/__init__.py +1 -1
  2. solara/__main__.py +1 -1
  3. solara/cache.py +9 -4
  4. solara/checks.py +9 -4
  5. solara/minisettings.py +1 -1
  6. solara/server/assets/style.css +1545 -0
  7. solara/server/flask.py +1 -1
  8. solara/server/kernel.py +3 -3
  9. solara/server/patch.py +2 -0
  10. solara/server/reload.py +1 -1
  11. solara/server/server.py +57 -0
  12. solara/server/starlette.py +8 -9
  13. solara/server/static/solara_bootstrap.py +1 -1
  14. solara/server/telemetry.py +5 -2
  15. solara/server/templates/loader-plain.html +1 -1
  16. solara/server/templates/loader-solara.html +1 -1
  17. solara/server/templates/solara.html.j2 +20 -25
  18. solara/util.py +14 -1
  19. solara/website/pages/apps/jupyter-dashboard-1.py +10 -12
  20. solara/website/pages/apps/scatter.py +4 -4
  21. solara/website/pages/doc_use_download.py +1 -1
  22. solara/website/pages/docs/content/04-tutorial/_jupyter_dashboard_1.ipynb +8 -8
  23. solara/website/pages/docs/content/10-howto/ipywidget_libraries.md +1 -1
  24. solara/website/pages/docs/content/95-changelog.md +8 -0
  25. solara/website/public/success.html +16 -7
  26. solara/website/templates/index.html.j2 +16 -15
  27. {solara-1.25.0.dist-info → solara-1.25.1.dist-info}/METADATA +9 -9
  28. {solara-1.25.0.dist-info → solara-1.25.1.dist-info}/RECORD +33 -34
  29. {solara-1.25.0.dist-info → solara-1.25.1.dist-info}/WHEEL +1 -1
  30. solara/server/assets/index.css +0 -14480
  31. {solara-1.25.0.data → solara-1.25.1.data}/data/prefix/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
  32. {solara-1.25.0.data → solara-1.25.1.data}/data/prefix/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
  33. {solara-1.25.0.dist-info → solara-1.25.1.dist-info}/entry_points.txt +0 -0
  34. {solara-1.25.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
@@ -252,9 +252,65 @@ def read_root(path: str, root_path: str = "", render_kwargs={}, use_nbextensions
252
252
  else:
253
253
  nbextensions = []
254
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
+
255
309
  resources = {
256
310
  "theme": "light",
257
311
  "nbextensions": nbextensions,
312
+ "include_css": include_css,
313
+ "include_js": include_js,
258
314
  }
259
315
  template: jinja2.Template = get_jinja_env(app_name="__default__").get_template(template_name)
260
316
  pre_rendered_html = ""
@@ -290,6 +346,7 @@ def read_root(path: str, root_path: str = "", render_kwargs={}, use_nbextensions
290
346
  "assets": settings.assets.dict(),
291
347
  "cdn": cdn,
292
348
  "ipywidget_major_version": ipywidgets_major,
349
+ "solara_version": solara.__version__,
293
350
  "platform": settings.main.platform,
294
351
  "vue3": vue3,
295
352
  "perform_check": settings.main.mode != "production" and solara.checks.should_perform_solara_check(),
@@ -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
@@ -106,7 +105,7 @@ class WebsocketWrapper(websocket.WebsocketWrapper):
106
105
  async def receive(self):
107
106
  if hasattr(self.portal, "start_task_soon"):
108
107
  # version 3+
109
- fut = self.portal.start_task_soon(self.ws.receive)
108
+ fut = self.portal.start_task_soon(self.ws.receive) # type: ignore
110
109
  else:
111
110
  fut = self.portal.spawn_task(self.ws.receive) # type: ignore
112
111
 
@@ -155,7 +154,7 @@ class ServerStarlette(ServerBase):
155
154
  asyncio.set_event_loop(loop)
156
155
 
157
156
  # uvloop will trigger a: RuntimeError: There is no current event loop in thread 'fastapi-thread'
158
- 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")
159
158
  self.server = Server(config=config)
160
159
  self.started.set()
161
160
  self.server.run()
@@ -169,7 +168,6 @@ async def kernel_connection(ws: starlette.websockets.WebSocket):
169
168
  session_id = ws.cookies.get(server.COOKIE_KEY_SESSION_ID)
170
169
 
171
170
  if settings.oauth.private and not has_auth_support:
172
- breakpoint()
173
171
  raise RuntimeError("SOLARA_OAUTH_PRIVATE requires solara-enterprise")
174
172
  if has_auth_support and "session" in ws.scope:
175
173
  user = get_user(ws)
@@ -211,14 +209,14 @@ async def kernel_connection(ws: starlette.websockets.WebSocket):
211
209
  telemetry.connection_close(session_id)
212
210
 
213
211
  # sometimes throws: RuntimeError: Already running asyncio in this thread
214
- anyio.run(run)
212
+ anyio.run(run) # type: ignore
215
213
 
216
214
  # this portal allows us to sync call the websocket calls from this current event loop we are in
217
215
  # each websocket however, is handled from a separate thread
218
216
  try:
219
217
  async with anyio.from_thread.BlockingPortal() as portal:
220
218
  ws_wrapper = WebsocketWrapper(ws, portal)
221
- thread_return = anyio.to_thread.run_sync(websocket_thread_runner, ws, portal)
219
+ thread_return = anyio.to_thread.run_sync(websocket_thread_runner, ws, portal) # type: ignore
222
220
  await thread_return
223
221
  finally:
224
222
  if settings.main.experimental_performance:
@@ -249,7 +247,8 @@ async def root(request: Request, fullpath: str = ""):
249
247
  if settings.main.root_path is None:
250
248
  # use the default root path from the app, which seems to also include the path
251
249
  # if we are mounted under a path
252
- root_path = request.scope.get("root_path", "")
250
+ scope = request.scope
251
+ root_path = scope.get("route_root_path", scope.get("root_path", ""))
253
252
  logger.debug("root_path: %s", root_path)
254
253
  # or use the script-name header, for instance when running under a reverse proxy
255
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.25.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")
@@ -172,11 +172,14 @@ _thread = threading.Thread(target=_track, daemon=True)
172
172
 
173
173
 
174
174
  def server_start():
175
- if settings.main.mode == "development":
175
+ if _auto_restart_enabled:
176
176
  return
177
177
  if not settings.telemetry.mixpanel_enable:
178
178
  return
179
- _thread.start()
179
+ try:
180
+ _thread.start()
181
+ except RuntimeError:
182
+ pass
180
183
 
181
184
 
182
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
@@ -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()
@@ -82,18 +82,16 @@ def crime_map(df):
82
82
 
83
83
  @solara.component
84
84
  def View():
85
- with solara.Column():
86
- dff = crime_filter(df_crime, districts.value, categories.value)
87
- row_count = len(dff)
88
- if row_count > limit.value:
89
- solara.Warning(f"Only showing the first {limit.value} of {row_count:,} crimes on map")
90
- with solara.Column():
91
- with solara.Column(style={"max-height": "400px"}):
92
- crime_map(dff.iloc[: limit.value])
93
- if row_count > 0:
94
- crime_charts(dff)
95
- else:
96
- solara.Warning("You filtered out all the data, no charts shown")
85
+ dff = crime_filter(df_crime, districts.value, categories.value)
86
+ row_count = len(dff)
87
+ if row_count > limit.value:
88
+ solara.Warning(f"Only showing the first {limit.value} of {row_count:,} crimes on map")
89
+ with solara.Column(style={"max-height": "400px"}):
90
+ crime_map(dff.iloc[: limit.value])
91
+ if row_count > 0:
92
+ crime_charts(dff)
93
+ else:
94
+ solara.Warning("You filtered out all the data, no charts shown")
97
95
 
98
96
 
99
97
  @solara.component
@@ -28,10 +28,10 @@ class State:
28
28
 
29
29
  @staticmethod
30
30
  def load_sample():
31
- State.x.value = str("gdpPercap")
32
- State.y.value = str("lifeExp")
33
- State.size.value = str("pop")
34
- State.color.value = str("continent")
31
+ State.x.value = "gdpPercap"
32
+ State.y.value = "lifeExp"
33
+ State.size.value = "pop"
34
+ State.color.value = "continent"
35
35
  State.logx.value = True
36
36
  State.df.value = df_sample
37
37
 
@@ -20,7 +20,7 @@ def DownloadFile(file_path=file_path, url=url, expected_size=expected_size, on_d
20
20
  status = "Done 🎉"
21
21
  else:
22
22
  MEGABYTES = 2.0**20.0
23
- status = "Downloading %s... (%6.2f/%6.2f MB)" % (file_path, downloaded_size / MEGABYTES, expected_size / MEGABYTES)
23
+ status = "Downloading {}... ({:6.2f}/{:6.2f} MB)".format(file_path, downloaded_size / MEGABYTES, expected_size / MEGABYTES)
24
24
  # status = "hi"
25
25
  # return MarkdownIt(f'{status}')
26
26
  assert download.progress is not None
@@ -7,12 +7,12 @@
7
7
  "source": [
8
8
  "# Build your Jupyter dashboard using Solara\n",
9
9
  "\n",
10
- "Welcome to **the** first part of a series of tutorial**s** that will show you how to create a dashboard in Jupyter and deploy it as a standalone web **app**. **Importantly, there will be** no need to rewrite your app in a different framework, no need to use a non-Python solution, **and** no need to use JavaScript or CSS.\n",
10
+ "Welcome to the first part of a series of tutorials that will show you how to create a dashboard in Jupyter and deploy it as a standalone web app. Importantly, there will be no need to rewrite your app in a different framework, no need to use a non-Python solution, and no need to use JavaScript or CSS.\n",
11
11
  "\n",
12
- "Jupyter notebooks are an incredible tool for data analysis, **since they enable** blending code, visualization and narrative in**to** a single document.\n",
12
+ "Jupyter notebooks are an incredible tool for data analysis, since they enable blending code, visualization and narrative into a single document.\n",
13
13
  "However, if the insights need to be presented to a non-technical audience, we usually do not want to show the code.\n",
14
14
  "\n",
15
- "In this tutorial, we will create a simple dashboard using Solara's UI components. **The final product will** allow an end-user to filter,\n",
15
+ "In this tutorial, we will create a simple dashboard using Solara's UI components. The final product will allow an end-user to filter,\n",
16
16
  "visualize and explore a dataset on a map.\n",
17
17
  "\n",
18
18
  "![image](/static/public/docs/tutorial/jupyter-dashboard1.jpg)\n",
@@ -33,7 +33,7 @@
33
33
  "\n",
34
34
  "## The start\n",
35
35
  "\n",
36
- "We will use a subsample of the [San Fanfrisco crime dataset](https://www.kaggle.com/competitions/sf-crime/data) which **contains information on types of crimes and where they were committed**.\n",
36
+ "We will use a subsample of the [San Fanfrisco crime dataset](https://www.kaggle.com/competitions/sf-crime/data) which contains information on types of crimes and where they were committed.\n",
37
37
  "\n",
38
38
  "[Download the CSV file](https://raw.githubusercontent.com/widgetti/solara/master/solara/website/pages/docs/content/04-tutorial/SF_crime_sample.csv.gz) if you want to run this locally, or let the code below sort it out."
39
39
  ]
@@ -365,7 +365,7 @@
365
365
  "id": "08a9644a",
366
366
  "metadata": {},
367
367
  "source": [
368
- "The data looks clean but since we will work with the `Category` and `PdDistrict` column data, lets **convert those columns to title case**."
368
+ "The data looks clean but since we will work with the `Category` and `PdDistrict` column data, lets convert those columns to title case."
369
369
  ]
370
370
  },
371
371
  {
@@ -676,7 +676,7 @@
676
676
  "id": "62df988f",
677
677
  "metadata": {},
678
678
  "source": [
679
- "Using proper software engineering practices, we write a function that filter**s** a dataframe to contain only the rows that match our chosen districts and categories."
679
+ "Using proper software engineering practices, we write a function that filters a dataframe to contain only the rows that match our chosen districts and categories."
680
680
  ]
681
681
  },
682
682
  {
@@ -940,7 +940,7 @@
940
940
  "source": [
941
941
  "## The final dashboard\n",
942
942
  "\n",
943
- "We now have two parts of our UI in separate cells. This can be an amazing experience when developing **in a** notebook, as it flows **naturally** in the data exploration process while writing your notebook.\n",
943
+ "We now have two parts of our UI in separate cells. This can be an amazing experience when developing in a notebook, as it flows naturally in the data exploration process while writing your notebook.\n",
944
944
  "\n",
945
945
  "However, your end user will probably want something more coherent. The components we created are perfectly re-usable, so we put them together in a single UI."
946
946
  ]
@@ -967,7 +967,7 @@
967
967
  "source": [
968
968
  "## Conclusions\n",
969
969
  "\n",
970
- "Using Solara, you created an interactive dashboard **within a** Jupyter notebook. Your Solara components are declarative, and **when** using reactive variables also reactive. **Whether** you change a reactive variables via code or the UI elements, your visualizations and map update automatically.\n",
970
+ "Using Solara, you created an interactive dashboard within a Jupyter notebook. Your Solara components are declarative, and when using reactive variables also reactive. Whether you change a reactive variables via code or the UI elements, your visualizations and map update automatically.\n",
971
971
  "\n",
972
972
  "Your dashboard prototype now runs in your Jupyter notebook environment, but we still have a few steps to we want to take. In our next tutorial, we will focus on deploying our notebook, without making any code changes. In our third tutorial we will expand our dashboard with a few more components and focus on creating a more advanced layout.\n",
973
973
  "\n",
@@ -47,7 +47,7 @@ def Page():
47
47
  )
48
48
  ```
49
49
 
50
- Note that this is about the worst example of something that looks easy in ipyleaflet using the classical API becomes a but more involved in Solara.
50
+ Note that this is about the worst example of something that looks easy in ipyleaflet using the classical API becoming a bit more involved in Solara.
51
51
  In practice, this does not happen often, and your code in general will be shorter and more readable.
52
52
 
53
53
  See also [the basic ipyleaflet example](/examples/libraries/ipyleaflet) and [the advanced ipyleaflet example](/examples/libraries/ipyleaflet_advanced).
@@ -1,5 +1,13 @@
1
1
  # Solara Changelog
2
2
 
3
+ ## Version 1.25.1
4
+
5
+ ### Details
6
+
7
+ * Performance: Removed unnecessary CSS and JS.
8
+ * Performance: Quality of Life - JS and CSS resources automatically reloaded on version change.
9
+ * Bug fix: overlay disabling navigation for display width < 960px.
10
+
3
11
 
4
12
  ## Version 1.25.0
5
13
 
@@ -1,15 +1,24 @@
1
1
  <html>
2
- <script async src="https://www.googletagmanager.com/gtag/js?id=G-G985FTLWGQ"></script>
2
+ <!-- Google Tag Manager -->
3
3
  <script>
4
- window.dataLayer = window.dataLayer || [];
5
- function gtag() { dataLayer.push(arguments); }
6
- gtag('js', new Date());
7
- gtag('config', 'G-G985FTLWGQ', {
8
- cookie_flags: 'secure;samesite=none'
9
- });
4
+ (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
5
+ new Date().getTime(),event:'gtm.js'});
6
+ var f=d.getElementsByTagName(s)[0], j=d.createElement(s), dl=l!='dataLayer'?'&l='+l:'';
7
+ j.async=true;
8
+ j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;
9
+ f.parentNode.insertBefore(j,f);
10
+ })(window,document,'script','dataLayer','GTM-KC98NKNL');
10
11
  </script>
12
+ <!-- End Google Tag Manager -->
11
13
 
12
14
  <body>
15
+ <!-- Google Tag Manager (noscript) -->
16
+ <noscript>
17
+ <iframe src="https://www.googletagmanager.com/ns.html?id=GTM-KC98NKNL"
18
+ height="0" width="0" style="display:none;visibility:hidden"></iframe>
19
+ </noscript>
20
+ <!-- End Google Tag Manager (noscript) -->
21
+
13
22
  Used to detect failed Jupyter and Solara installations.
14
23
  </body>
15
24
 
@@ -13,27 +13,25 @@
13
13
  <link rel="stylesheet" href="/_solara/cdn/@docsearch/css@3/dist/style.css" />
14
14
  <script src="/_solara/cdn/@docsearch/js@3"></script>
15
15
 
16
- <!-- Google tag (gtag.js) -->
17
- <script async src="https://www.googletagmanager.com/gtag/js?id=G-G985FTLWGQ"></script>
18
- <script>
19
- window.dataLayer = window.dataLayer || [];
20
- function gtag(){dataLayer.push(arguments);}
21
- gtag('js', new Date());
22
-
23
- gtag('config', 'G-G985FTLWGQ', {
24
- cookie_flags: 'secure;samesite=none'
25
- });
26
- </script>
27
- <!-- End Google Analytics -->
16
+ <!-- Google Tag Manager -->
17
+ <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
18
+ new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
19
+ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
20
+ 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
21
+ })(window,document,'script','dataLayer','GTM-KC98NKNL');</script>
22
+ <!-- End Google Tag Manager -->
28
23
 
29
- <!-- MailChimp -->
30
- <link href="//cdn-images.mailchimp.com/embedcode/classic-061523.css" rel="stylesheet" type="text/css">
31
- <script type="text/javascript" src="//s3.amazonaws.com/downloads.mailchimp.com/js/mc-validate.js"></script>
32
24
  {{ super() }}
33
25
  {% endblock %}
34
26
 
35
27
  {% block after_pre_rendered_html %}
36
28
  {{ super() }}
29
+
30
+ <!-- Google Tag Manager (noscript) -->
31
+ <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-KC98NKNL"
32
+ height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
33
+ <!-- End Google Tag Manager (noscript) -->
34
+
37
35
  <style id="algolia">
38
36
  .DocSearch-Button {
39
37
  background-color: rgb(255, 238, 197);
@@ -95,5 +93,8 @@
95
93
  });
96
94
  }
97
95
  </script>
96
+ <!-- MailChimp -->
97
+ <link href="//cdn-images.mailchimp.com/embedcode/classic-061523.css" rel="stylesheet" type="text/css">
98
+ <script type="text/javascript" src="//s3.amazonaws.com/downloads.mailchimp.com/js/mc-validate.js"></script>
98
99
  {% endblock %}
99
100
  ```