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 1, 2024
1 parent 0a50779 commit fa16d9c
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 8 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)
2 changes: 2 additions & 0 deletions solara/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ def asset_directories():
def read_root(
path: str,
root_path: str = "",
jupyter_root_path: str = "",
render_kwargs={},
use_nbextensions=True,
ssg_data=None,
Expand Down Expand Up @@ -377,6 +378,7 @@ def include_js(path: str, module=False) -> Markup:
"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 fa16d9c

Please sign in to comment.