from urllib.parse import urljoin import tornado import os from bokeh.command.util import build_single_handler_application from bokeh.embed.bundle import extension_dirs from bokeh.embed.server import server_html_page_for_session from bokeh.protocol import Protocol from bokeh.protocol.exceptions import ProtocolError from bokeh.protocol.receiver import Receiver from bokeh.server.connection import ServerConnection from bokeh.server.contexts import ApplicationContext from bokeh.server.protocol_handler import ProtocolHandler from bokeh.server.views.doc_handler import DocHandler from bokeh.server.views.multi_root_static_handler import MultiRootStaticHandler from bokeh.server.views.static_handler import StaticHandler from bokeh.server.views.ws import WSHandler from bokeh.server.auth_provider import NullAuth from bokeh.util.token import get_session_id from tornado.web import StaticFileHandler from ..config import config from ..util import edit_readonly, fullpath from .state import state from .resources import DIST_DIR, Resources _RESOURCES = None _APPS = {} def url_path_join(*pieces): """Join components of url into a relative url Use to prevent double slash when joining subpath. This will leave the initial and final / in place """ initial = pieces[0].startswith('/') final = pieces[-1].endswith('/') stripped = [s.strip('/') for s in pieces] result = '/'.join(s for s in stripped if s) if initial: result = '/' + result if final: result = result + '/' if result == '//': result = '/' return result class ServerApplicationProxy: """ A wrapper around the jupyter_server.serverapp.ServerWebApplication to make it compatible with the expected BokehTornado application API. """ auth_provider = NullAuth() generate_session_ids = True sign_sessions = False include_headers = None include_cookies = None exclude_headers = None exclude_cookies = None session_token_expiration = 300 secret_key = None websocket_origins = '*' def __init__(self, app, **kw): self._app = app @property def root_dir(self): """ Gets the root directory of the jupyter server app This is useful as the path sent received by the handler may be different from the root dir. Reference: https://github.com/holoviz/panel/issues/3170 """ return fullpath(self._app.settings['server_root_dir']) def __getattr__(self, key): return getattr(self._app, key) class PanelHandler(DocHandler): def __init__(self, app, request, *args, **kw): kw['application_context'] = None kw['bokeh_websocket_path'] = None proxy = ServerApplicationProxy(app) super().__init__(proxy, request, *args, **kw) def initialize(self, *args, **kws): pass async def get(self, path, *args, **kwargs): path = os.path.join(self.application.root_dir, fullpath(path)) if path in _APPS: app, context = _APPS[path] else: if path.endswith('yml') or path.endswith('.yaml'): from lumen.config import config from lumen.command import build_single_handler_application as build_lumen config.dev = True app = build_lumen(path, argv=None) else: app = build_single_handler_application(path) context = ApplicationContext(app, url=path) context._loop = tornado.ioloop.IOLoop.current() _APPS[path] = (app, context) self.application_context = context session = await self.get_session() page = server_html_page_for_session( session, resources=RESOURCES, title=session.document.title, template=session.document.template, template_variables=session.document.template_variables ) self.set_header("Content-Type", 'text/html') self.write(page) class PanelWSHandler(WSHandler): def __init__(self, app, request, *args, **kw): kw['application_context'] = None proxy = ServerApplicationProxy(app) super().__init__(proxy, request, *args, **kw) def initialize(self, *args, **kwargs): pass async def open(self, path, *args, **kwargs): path = os.path.join(self.application.root_dir, fullpath(path)) _, context = _APPS[path] token = self._token if self.selected_subprotocol != 'bokeh': self.close() raise ProtocolError("Subprotocol header is not 'bokeh'") elif token is None: self.close() raise ProtocolError("No token received in subprotocol header") session_id = get_session_id(token) await context.create_session_if_needed(session_id, self.request, token) session = context.get_session(session_id) try: protocol = Protocol() self.receiver = Receiver(protocol) self.handler = ProtocolHandler() self.connection = self.new_connection(protocol, context, session) except ProtocolError as e: self.close() raise e msg = self.connection.protocol.create('ACK') await self.send_message(msg) def new_connection(self, protocol, application_context, session): connection = ServerConnection(protocol, self, application_context, session) return connection def on_close(self): if self.connection is not None: self.connection.detach_session() def _load_jupyter_server_extension(notebook_app): global RESOURCES base_url = notebook_app.web_app.settings["base_url"] # Configure Panel RESOURCES = Resources( mode="server", root_url=urljoin(base_url, 'panel-preview'), path_versioner=StaticHandler.append_version ) config.autoreload = True with edit_readonly(state): state.base_url = url_path_join(base_url, '/panel-preview/') state.rel_path = url_path_join(base_url, '/panel-preview') print(state.base_url, urljoin(base_url, 'panel-preview')) # Set up handlers notebook_app.web_app.add_handlers( host_pattern=r".*$", host_handlers=[ (urljoin(base_url, r"panel-preview/static/extensions/(.*)"), MultiRootStaticHandler, dict(root=extension_dirs)), (urljoin(base_url, r"panel-preview/static/(.*)"), StaticHandler), (urljoin(base_url, r"panel-preview/render/(.*)/ws"), PanelWSHandler), (urljoin(base_url, r"panel-preview/render/(.*)"), PanelHandler, {}), (urljoin(base_url, r"panel_dist/(.*)"), StaticFileHandler, dict(path=DIST_DIR)) ] ) # compat for older versions of Jupyter load_jupyter_server_extension = _load_jupyter_server_extension