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.
- solara/__init__.py +1 -1
- solara/__main__.py +1 -1
- solara/cache.py +9 -4
- solara/checks.py +9 -4
- 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 +57 -0
- solara/server/starlette.py +8 -9
- solara/server/static/solara_bootstrap.py +1 -1
- solara/server/telemetry.py +5 -2
- 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 +14 -1
- solara/website/pages/apps/jupyter-dashboard-1.py +10 -12
- solara/website/pages/apps/scatter.py +4 -4
- solara/website/pages/doc_use_download.py +1 -1
- solara/website/pages/docs/content/04-tutorial/_jupyter_dashboard_1.ipynb +8 -8
- solara/website/pages/docs/content/10-howto/ipywidget_libraries.md +1 -1
- solara/website/pages/docs/content/95-changelog.md +8 -0
- solara/website/public/success.html +16 -7
- solara/website/templates/index.html.j2 +16 -15
- {solara-1.25.0.dist-info → solara-1.25.1.dist-info}/METADATA +9 -9
- {solara-1.25.0.dist-info → solara-1.25.1.dist-info}/RECORD +33 -34
- {solara-1.25.0.dist-info → solara-1.25.1.dist-info}/WHEEL +1 -1
- solara/server/assets/index.css +0 -14480
- {solara-1.25.0.data → solara-1.25.1.data}/data/prefix/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
- {solara-1.25.0.data → solara-1.25.1.data}/data/prefix/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
- {solara-1.25.0.dist-info → solara-1.25.1.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
@@ -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(),
|
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
|
|
@@ -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
|
-
|
|
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.
|
|
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
|
@@ -172,11 +172,14 @@ _thread = threading.Thread(target=_track, daemon=True)
|
|
|
172
172
|
|
|
173
173
|
|
|
174
174
|
def server_start():
|
|
175
|
-
if
|
|
175
|
+
if _auto_restart_enabled:
|
|
176
176
|
return
|
|
177
177
|
if not settings.telemetry.mixpanel_enable:
|
|
178
178
|
return
|
|
179
|
-
|
|
179
|
+
try:
|
|
180
|
+
_thread.start()
|
|
181
|
+
except RuntimeError:
|
|
182
|
+
pass
|
|
180
183
|
|
|
181
184
|
|
|
182
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
|
|
@@ -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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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 =
|
|
32
|
-
State.y.value =
|
|
33
|
-
State.size.value =
|
|
34
|
-
State.color.value =
|
|
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
|
|
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
|
|
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,
|
|
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.
|
|
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
|
"\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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
2
|
+
<!-- Google Tag Manager -->
|
|
3
3
|
<script>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
17
|
-
<script
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
```
|