From 93f418092be737a2c65342b88f3266c4a8a3a95e Mon Sep 17 00:00:00 2001 From: David Brochart Date: Tue, 5 Sep 2023 16:50:22 +0200 Subject: [PATCH] Add Jupyverse contents --- voila/app.py | 36 ++++++++-- voila/jupyverse/LICENSE | 32 +++++++++ voila/jupyverse/README.md | 3 + voila/jupyverse/fps_voila/__init__.py | 19 ++++++ voila/jupyverse/fps_voila/main.py | 94 +++++++++++++++++++++++++++ voila/jupyverse/fps_voila/routes.py | 54 +++++++++++++++ voila/jupyverse/pyproject.toml | 48 ++++++++++++++ 7 files changed, 281 insertions(+), 5 deletions(-) create mode 100644 voila/jupyverse/LICENSE create mode 100644 voila/jupyverse/README.md create mode 100644 voila/jupyverse/fps_voila/__init__.py create mode 100644 voila/jupyverse/fps_voila/main.py create mode 100644 voila/jupyverse/fps_voila/routes.py create mode 100644 voila/jupyverse/pyproject.toml diff --git a/voila/app.py b/voila/app.py index eb60b3059..28b844d9c 100644 --- a/voila/app.py +++ b/voila/app.py @@ -95,6 +95,10 @@ class Voila(Application): examples = "voila example.ipynb --port 8888" flags = { + "jupyverse": ( + {"Voila": {"jupyverse": True}}, + _("Use Jupyverse backend."), + ), "debug": ( { "Voila": {"log_level": logging.DEBUG}, @@ -128,6 +132,11 @@ class Voila(Application): ), } + jupyverse = Bool( + False, + config=True, + help=_("Use Jupyverse backend"), + ) description = Unicode( """voila [OPTIONS] NOTEBOOK_FILENAME @@ -776,11 +785,28 @@ def start(self): settings = self.init_settings() - self.app = tornado.web.Application(**settings) - self.app.settings.update(self.tornado_settings) - handlers = self.init_handlers() - self.app.add_handlers(".*$", handlers) - self.listen() + if self.jupyverse: + try: + from fps_voila import run_application + except ImportError: + raise RuntimeError( + "Please install fps-voila in order to use the Jupyverse backend" + ) + + run_application( + settings, + self.voila_configuration, + self.static_paths, + self.base_url, + self.ip, + self.port, + ) + else: + self.app = tornado.web.Application(**settings) + self.app.settings.update(self.tornado_settings) + handlers = self.init_handlers() + self.app.add_handlers(".*$", handlers) + self.listen() def _handle_signal_stop(self, sig, frame): self.log.info("Handle signal %s." % sig) diff --git a/voila/jupyverse/LICENSE b/voila/jupyverse/LICENSE new file mode 100644 index 000000000..3fc5b3f23 --- /dev/null +++ b/voila/jupyverse/LICENSE @@ -0,0 +1,32 @@ +BSD License + +Copyright (c) 2018 Voilà contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + a. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + b. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + c. Neither the name of the authors nor the names of the contributors to + this package may be used to endorse or promote products + derived from this software without specific prior written + permission. + + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/voila/jupyverse/README.md b/voila/jupyverse/README.md new file mode 100644 index 000000000..cc6cdb3eb --- /dev/null +++ b/voila/jupyverse/README.md @@ -0,0 +1,3 @@ +# fps-voila + +An FPS plugin for the Voilà API. diff --git a/voila/jupyverse/fps_voila/__init__.py b/voila/jupyverse/fps_voila/__init__.py new file mode 100644 index 000000000..8437bc275 --- /dev/null +++ b/voila/jupyverse/fps_voila/__init__.py @@ -0,0 +1,19 @@ +from asphalt.core import run_application as _run_application + +from .main import RootComponent + + +__version__ = "0.1.0" + + +def run_application(settings, voila_configuration, static_paths, base_url, ip, port): + _run_application( + RootComponent( + settings, + voila_configuration, + static_paths, + base_url, + ip, + port, + ) + ) diff --git a/voila/jupyverse/fps_voila/main.py b/voila/jupyverse/fps_voila/main.py new file mode 100644 index 000000000..8c8355f00 --- /dev/null +++ b/voila/jupyverse/fps_voila/main.py @@ -0,0 +1,94 @@ +import pkg_resources + +from asphalt.core import Component, ContainerComponent, Context +from jupyverse_api.app import App +from jupyverse_api.auth import Auth + +from .routes import Voila + + +class VoilaComponent(Component): + def __init__( + self, + *, + settings, + voila_configuration, + static_paths, + base_url, + ): + super().__init__() + self.settings = settings + self.voila_configuration = voila_configuration + self.static_paths = static_paths + self.base_url = base_url + + async def start( + self, + ctx: Context, + ) -> None: + app = await ctx.request_resource(App) + auth = await ctx.request_resource(Auth) + + voila = Voila( + app, + auth, + self.settings, + self.voila_configuration, + self.static_paths, + self.base_url, + ) + ctx.add_resource(voila) + + +class RootComponent(ContainerComponent): + def __init__( + self, + settings, + voila_configuration, + static_paths, + base_url, + ip, + port, + ): + super().__init__() + self.settings = settings + self.voila_configuration = voila_configuration + self.static_paths = static_paths + self.base_url = base_url + self.ip = ip + self.port = port + + async def start(self, ctx: Context) -> None: + asphalt_components = { + ep.name: ep + for ep in pkg_resources.iter_entry_points(group="asphalt.components") + } + + self.add_component( + "fastapi", + asphalt_components["fastapi"].load(), + host=self.ip, + port=self.port, + ) + self.add_component( + "voila", + asphalt_components["voila"].load(), + settings=self.settings, + voila_configuration=self.voila_configuration, + static_paths=self.static_paths, + base_url=self.base_url, + ) + self.add_component( + "contents", + asphalt_components["contents"].load(), + prefix="/voila", + ) + self.add_component( + "auth", + asphalt_components["noauth"].load(), + ) + self.add_component( + "app", + asphalt_components["app"].load(), + ) + await super().start(ctx) diff --git a/voila/jupyverse/fps_voila/routes.py b/voila/jupyverse/fps_voila/routes.py new file mode 100644 index 000000000..004328ac1 --- /dev/null +++ b/voila/jupyverse/fps_voila/routes.py @@ -0,0 +1,54 @@ +import logging +from typing import List + +from fastapi import APIRouter, Request +from fastapi.responses import HTMLResponse +from starlette.staticfiles import PathLike, StaticFiles +from jupyverse_api import Router +from jupyverse_api.app import App +from jupyverse_api.auth import Auth +from voila.utils import get_page_config + + +logger = logging.getLogger(__name__) + + +class Voila(Router): + def __init__( + self, + app: App, + auth: Auth, + settings, + voila_configuration, + static_paths, + base_url, + ): + super().__init__(app=app) + + router = APIRouter() + + @router.get("/", response_class=HTMLResponse) + async def get_root(request: Request): + page_config = get_page_config( + base_url=base_url, + settings=settings, + log=logger, + voila_configuration=voila_configuration, + ) + template = settings["voila_jinja2_env"].get_template("tree-lab.html") + return template.render( + page_config=page_config, + ) + + self.include_router(router) + + self.mount( + "/voila/static", + MultiStaticFiles(directories=static_paths, check_dir=False), + ) + + +class MultiStaticFiles(StaticFiles): + def __init__(self, directories: List[PathLike] = [], **kwargs) -> None: + super().__init__(**kwargs) + self.all_directories = self.all_directories + directories diff --git a/voila/jupyverse/pyproject.toml b/voila/jupyverse/pyproject.toml new file mode 100644 index 000000000..27a343ae2 --- /dev/null +++ b/voila/jupyverse/pyproject.toml @@ -0,0 +1,48 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "fps_voila" +description = "An FPS plugin for the Voilà API" +readme = "README.md" +requires-python = ">=3.8" +authors = [ + { name = "Voila Development Team" }, +] +keywords = [ + "Jupyter", + "JupyterLab", + "Voila", +] +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 3", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ + "fps-contents >0.2.4,<1" +] +dynamic = [ "version",] + +[project.license] +file = "LICENSE" + +[project.entry-points] +"asphalt.components" = {voila = "fps_voila.main:VoilaComponent"} +"jupyverse.components" = {voila = "fps_voila.main:VoilaComponent"} + +[project.urls] +Homepage = "https://github.com/voila-dashboards/voila" + +[tool.hatch.version] +path = "fps_voila/__init__.py"