Skip to content

Commit

Permalink
feat: render ipypopout content in jupyter notebook and lab
Browse files Browse the repository at this point in the history
Instead of having to rely on voila to display widgets in a popout
window, solara itself now can render it. This allows project to
drop the dependency on voila.
  • Loading branch information
maartenbreddels committed Oct 2, 2024
1 parent 0a50779 commit a4fae04
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 9 deletions.
2 changes: 1 addition & 1 deletion solara/server/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ def assets(path):
return flask.Response("not found", status=404)


@blueprint.route("/static/nbextensions/<dir>/<filename>")
@blueprint.route("/jupyter/nbextensions/<dir>/<filename>")
def nbext(dir, filename):
if not allowed():
abort(401)
Expand Down
13 changes: 12 additions & 1 deletion solara/server/jupyter/server_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,33 @@

from solara.server.cdn_helper import cdn_url_path
from solara.server.jupyter.cdn_handler import CdnHandler
from .solara import SolaraHandler, Assets, ReadyZ


def _jupyter_server_extension_paths():
return [{"module": "solara.server.jupyter.server_extension"}]


def _load_jupyter_server_extension(server_app):
# a dummy app, so that server.read_root can be used
import solara.server.app

solara.server.app.apps["__default__"] = solara.server.app.AppScript("solara.server.jupyter.solara:Page")

web_app = server_app.web_app

host_pattern = ".*$"
base_url = url_path_join(web_app.settings["base_url"])
print("base_url", base_url)

web_app.add_handlers(
host_pattern,
[
(url_path_join(base_url, f"/{cdn_url_path}/(.*)"), CdnHandler, {}),
(url_path_join(base_url, f"/{cdn_url_path}/(.*)"), CdnHandler, {}), # kept for backward compatibility
(url_path_join(base_url, f"/solara/{cdn_url_path}/(.*)"), CdnHandler, {}),
(url_path_join(base_url, "/solara/static/assets/(.*)"), Assets, {}),
(url_path_join(base_url, "/solara/readyz"), ReadyZ, {}),
(url_path_join(base_url, "/solara(.*)"), SolaraHandler, {}),
],
)

Expand Down
94 changes: 94 additions & 0 deletions solara/server/jupyter/solara.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import json
import logging
import os
from pathlib import Path

import tornado.web
from jupyter_server.base.handlers import JupyterHandler
import solara.server.server as server

from solara.server.utils import path_is_child_of

logger = logging.getLogger("solara.server.jupyter.solara")


import solara


@solara.component
def Page():
solara.Error("Hi, you should not see this, we only support ipypopout for now")


class SolaraHandler(JupyterHandler):
async def get(self, path=None):
try:
# base url ends with /
base_url = self.settings["base_url"]
# root_path's do not end with /
jupyter_root_path = ""
if base_url and base_url.endswith("/"):
jupyter_root_path = base_url[:-1]
root_path = f"{jupyter_root_path}/solara"
content = server.read_root(path="", root_path=root_path, jupyter_root_path=jupyter_root_path)
except Exception as e:
logger.exception(e)
raise tornado.web.HTTPError(500)

if content is None:
raise tornado.web.HTTPError(404)
else:
self.set_header("Content-Type", "text/html")
self.write(content)


# similar to voila
class MultiStaticFileHandler(tornado.web.StaticFileHandler):
"""A static file handler that 'merges' a list of directories
If initialized like this::
application = web.Application([
(r"/content/(.*)", web.MultiStaticFileHandler, {"paths": ["/var/1", "/var/2"]}),
])
A file will be looked up in /var/1 first, then in /var/2.
"""

def initialize(self, paths, default_filename=None): # type: ignore
self.roots = paths
super().initialize(path=paths[0], default_filename=default_filename)

def get_absolute_path(self, root: str, path: str) -> str: # type: ignore
# find the first absolute path that exists
self.root = self.roots[0]
abspath = os.path.abspath(os.path.join(root, path))
for root in self.roots[1:]:
abspath = os.path.abspath(os.path.join(root, path))
# return early if someone tries to access a file outside of the directory
if os.path.exists(abspath):
self.root = root # make sure all the other methods in the base class know how to find the file
break

# tornado probably already does this, but just to be sure we don't serve files outside of the directory
# and that we are consistent with starlette and flask
if not path_is_child_of(Path(abspath), Path(self.root)):
raise PermissionError(f"Trying to read from outside of cache directory: {abspath} is not a subdir of {self.root}")

return abspath


class Assets(MultiStaticFileHandler):
def initialize(self): # type: ignore
super().initialize(server.asset_directories())
logging.error("Using %r as assets directories", self.roots)


class ReadyZ(JupyterHandler):
def get(self):
json_data, status = server.readyz()
json_response = json.dumps(json_data)
self.set_header("Content-Type", "application/json")
self.set_status(status)
self.write(json_response)
7 changes: 6 additions & 1 deletion solara/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import sys
import time
from pathlib import Path
from typing import Dict, List, Optional, Tuple, TypeVar
from typing import Dict, List, Optional, Tuple, TypeVar, Union

import ipykernel
import ipyvue
Expand Down Expand Up @@ -268,6 +268,7 @@ def asset_directories():
def read_root(
path: str,
root_path: str = "",
jupyter_root_path: Union[str, None] = None,
render_kwargs={},
use_nbextensions=True,
ssg_data=None,
Expand Down Expand Up @@ -373,10 +374,14 @@ def include_js(path: str, module=False) -> Markup:
else:
cdn = solara.settings.assets.cdn

if jupyter_root_path is None:
jupyter_root_path = f"{root_path}/jupyter"

render_settings = {
"title": title,
"path": path,
"root_path": root_path,
"jupyter_root_path": jupyter_root_path,
"resources": resources,
"theme": settings.theme.dict(),
"production": settings.main.mode == "production",
Expand Down
2 changes: 1 addition & 1 deletion solara/server/starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ def expand(named_tuple):
*([Mount(f"/{cdn_url_path}", app=StaticCdn(directory=settings.assets.proxy_cache_dir))] if solara.settings.assets.proxy else []),
Mount(f"{prefix}/static/public", app=StaticPublic()),
Mount(f"{prefix}/static/assets", app=StaticAssets()),
Mount(f"{prefix}/static/nbextensions", app=StaticNbFiles()),
Mount(f"{prefix}/jupyter/nbextensions", app=StaticNbFiles()),
Mount(f"{prefix}/static", app=StaticFilesOptionalAuth(directory=server.solara_static)),
Route("/{fullpath:path}", endpoint=root),
]
Expand Down
2 changes: 1 addition & 1 deletion solara/server/static/main-vuetify.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ async function solaraInit(mountId, appName) {
window.navigator.sendBeacon(close_url);
}
});
let kernel = await solara.connectKernel(solara.rootPath + '/jupyter', kernelId)
let kernel = await solara.connectKernel(solara.jupyterRootPath, kernelId)
if (!kernel) {
return;
}
Expand Down
9 changes: 5 additions & 4 deletions solara/server/templates/solara.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

<script id="jupyter-config-data" type="application/json">
{
"baseUrl": "{{root_path}}/",
"baseUrl": "{{jupyter_root_path}}/",
"kernelId": "1234"
}
</script>
Expand Down Expand Up @@ -243,12 +243,13 @@
{% endif %}
<script>
solara.rootPath = {{ root_path | tojson | safe}};
solara.jupyterRootPath = {{ jupyter_root_path | tojson | safe}};
solara.cdn = {{ cdn | tojson | safe }};
// the vue templates expect it to not have a trailing slash
solara.cdn = solara.cdn.replace(/\/$/, '');
// keep this for backwards compatibility
window.solara_cdn = solara.cdn;
console.log("rootPath", solara.rootPath);
console.log("solara config", {rootPath: solara.rootPath, jupyterRootPath: solara.jupyterRootPath, cdn: solara.cdn});
async function changeThemeCSS(theme) {
let css = await fetch(`${solara.rootPath}/static/assets/theme-${theme}.css`).then(r => r.text());
Expand Down Expand Up @@ -441,7 +442,7 @@
{% endif -%}
nbextensionHashes = {{ resources.nbextensions_hashes | tojson | safe }};
requirejs.config({
baseUrl: '{{root_path}}/static/',
baseUrl: '{{jupyter_root_path}}',
waitSeconds: 3000,
map: {
'*': {
Expand All @@ -466,7 +467,7 @@
});
requirejs([
{% for ext in resources.nbextensions if ext != 'jupyter-vuetify/extension' and ext != 'jupyter-vue/extension' -%}
"{{root_path}}/static/nbextensions/{{ ext }}.js",
"{{jupyter_root_path}}/nbextensions/{{ ext }}.js",
{% endfor %}
]);
(async function () {
Expand Down

0 comments on commit a4fae04

Please sign in to comment.