Skip to content

Commit

Permalink
Update bokeh-fastapi integration (#7262)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored Sep 12, 2024
1 parent b69b78b commit 30e11a1
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 32 deletions.
11 changes: 11 additions & 0 deletions panel/io/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,15 @@ def build_applications(
else:
handler = FunctionHandler(partial(_eval_panel, app, server_id, title_, location, admin))
apps[slug] = Application(handler, admin=admin)

if admin:
if '/admin' in apps:
raise ValueError(
'Cannot enable admin panel because another app is being served '
'on the /admin endpoint'
)
from .admin import admin_panel
admin_handler = FunctionHandler(admin_panel)
apps['/admin'] = Application(admin_handler)

return apps
97 changes: 78 additions & 19 deletions panel/io/fastapi.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
from __future__ import annotations

import asyncio
import socket
import uuid

from functools import wraps
from typing import TYPE_CHECKING, Mapping, cast
from typing import (
TYPE_CHECKING, Any, Mapping, cast,
)

from ..config import config
from .application import build_applications
from .document import _cleanup_doc, extra_socket_handlers
from .resources import COMPONENT_PATH
from .server import ComponentResourceHandler
from .server import ComponentResourceHandler, server_html_page_for_session
from .state import state
from .threads import StoppableThread

try:
from bokeh_fastapi import BokehFastAPI
from bokeh_fastapi.handler import WSHandler
from bokeh_fastapi.handler import DocHandler, WSHandler
from fastapi import (
FastAPI, HTTPException, Query, Request,
)
Expand All @@ -34,6 +38,9 @@
# Private API
#---------------------------------------------------------------------

DocHandler.render_session = server_html_page_for_session


def dispatch_fastapi(conn, events=None, msg=None):
if msg is None:
msg = conn.protocol.create("PATCH-DOC", events)
Expand Down Expand Up @@ -61,6 +68,11 @@ async def liveness_handler(request: Request, endpoint: str | None = Query(None))
else:
return {str(request.url.path): True}

def add_history_handler(app, endpoint):
@app.get(endpoint, response_model=dict[str, int | dict[str, Any]])
async def history_handler(request: Request):
return state.session_info

#---------------------------------------------------------------------
# Public API
#---------------------------------------------------------------------
Expand All @@ -71,6 +83,7 @@ def add_applications(
title: str | dict[str, str] | None = None,
location: bool | Location = True,
admin: bool = False,
session_history: int | None = None,
liveness: bool | str = False,
**kwargs
):
Expand All @@ -92,6 +105,11 @@ def add_applications(
set the URL location.
admin: boolean (default=False)
Whether to enable the admin panel
session_history: int (optional, default=None)
The amount of session history to accumulate. If set to non-zero
and non-None value will launch a REST endpoint at
/rest/session_info, which returns information about the session
history.
liveness: bool | str (optional, default=False)
Whether to add a liveness endpoint. If a string is provided
then this will be used as the endpoint, otherwise the endpoint
Expand All @@ -100,7 +118,15 @@ def add_applications(
Additional keyword arguments to pass to the BokehFastAPI application
"""
apps = build_applications(panel, title=title, location=location, admin=admin)
ws_origins = kwargs.pop('websocket_origin', [])
if ws_origins and not isinstance(ws_origins, list):
ws_origins = [ws_origins]
kwargs['websocket_origins'] = ws_origins

application = BokehFastAPI(apps, app=app, **kwargs)
if session_history is not None:
config.session_history = session_history
add_history_handler(application.app, endpoint='/session_info')
if liveness:
liveness_endpoint = liveness if isinstance(liveness, str) else '/liveness'
add_liveness_handler(application.app, endpoint=liveness_endpoint, applications=apps)
Expand Down Expand Up @@ -165,6 +191,7 @@ def wrapper(*args, **kwargs):
def get_server(
panel: TViewableFuncOrPath | Mapping[str, TViewableFuncOrPath],
port: int | None = 0,
show: bool = True,
start: bool = False,
title: str | dict[str, str] | None = None,
location: bool | Location = True,
Expand All @@ -181,6 +208,8 @@ def get_server(
dictionary mapping from the URL slug to either.
port: int (optional, default=0)
Allows specifying a specific port.
show : boolean (optional, default=True)
Whether to open the server in a new browser tab on start
start : boolean(optional, default=False)
Whether to start the Server.
title : str or {str: str} (optional, default=None)
Expand All @@ -195,8 +224,11 @@ def get_server(
Whether to add a liveness endpoint. If a string is provided
then this will be used as the endpoint, otherwise the endpoint
will be hosted at /liveness.
start : boolean(optional, default=False)
Whether to start the Server.
session_history: int (optional, default=None)
The amount of session history to accumulate. If set to non-zero
and non-None value will launch a REST endpoint at
/rest/session_info, which returns information about the session
history.
**kwargs:
Additional keyword arguments to pass to the BokehFastAPI application
"""
Expand All @@ -209,26 +241,49 @@ def get_server(
"panel.io.fastapi.add_applications API."
) from e

address = kwargs.pop('address', None)
if not port:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 0)) # Bind to any available port
port = sock.getsockname()[1] # Get the dynamically assigned port
sock.close()

loop = kwargs.pop('loop')
config_kwargs = {}
if loop:
config_kwargs['loop'] = loop
asyncio.set_event_loop(loop)
server_id = kwargs.pop('server_id', uuid.uuid4().hex)
application = add_applications(
panel, title=title, location=location, admin=admin, **kwargs
)

config = uvicorn.Config(application.app, port=port, loop=loop)
if show:
@application.app.on_event('startup')
def show_callback():
prefix = kwargs.get('prefix', '')
address_string = 'localhost'
if address is not None and address != '':
address_string = address
url = f"http://{address_string}:{config.port}{prefix}"
from bokeh.util.browser import view
view(url, new='tab')

config = uvicorn.Config(application.app, port=port, **config_kwargs)
server = uvicorn.Server(config)

state._servers[server_id] = (server, panel, [])
if start:
if loop:
try:
loop.run_until_complete(server.serve())
except asyncio.CancelledError:
pass
else:
server.run()
if not start:
return server

if loop:
try:
loop.run_until_complete(server.serve())
except asyncio.CancelledError:
pass
else:
server.run()

return server


Expand All @@ -241,10 +296,10 @@ def serve(
show: bool = True,
start: bool = True,
title: str | None = None,
verbose: bool = True,
location: bool = True,
threaded: bool = False,
admin: bool = False,
session_history: int | None = None,
liveness: bool | str = False,
**kwargs
) -> StoppableThread | Server:
Expand Down Expand Up @@ -283,8 +338,6 @@ def serve(
title: str or {str: str} (optional, default=None)
An HTML title for the application or a dictionary mapping
from the URL slug to a customized title
verbose: boolean (optional, default=True)
Whether to print the address and port
location : boolean or panel.io.location.Location
Whether to create a Location component to observe and
set the URL location.
Expand All @@ -296,15 +349,21 @@ def serve(
Whether to add a liveness endpoint. If a string is provided
then this will be used as the endpoint, otherwise the endpoint
will be hosted at /liveness.
session_history: int (optional, default=None)
The amount of session history to accumulate. If set to non-zero
and non-None value will launch a REST endpoint at
/rest/session_info, which returns information about the session
history.
kwargs: dict
Additional keyword arguments to pass to Server instance
"""
# Empty layout are valid and the Bokeh warning is silenced as usually
# not relevant to Panel users.
kwargs = dict(kwargs, **dict(
port=port, address=address, websocket_origin=websocket_origin,
loop=loop, show=show, start=start, title=title, verbose=verbose,
location=location, admin=admin, liveness=liveness
loop=loop, show=show, start=start, title=title,
location=location, admin=admin, liveness=liveness,
session_history=session_history
))
if threaded:
# To ensure that we have correspondence between state._threads and state._servers
Expand Down
17 changes: 4 additions & 13 deletions panel/io/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import tornado

# Bokeh imports
from bokeh.application.handlers.function import FunctionHandler
from bokeh.core.json_encoder import serialize_json
from bokeh.core.templates import AUTOLOAD_JS, FILE, MACROS
from bokeh.core.validation import silence
Expand Down Expand Up @@ -60,7 +59,7 @@
from ..config import config
from ..util import edit_readonly, fullpath
from ..util.warnings import warn
from .application import Application, build_applications
from .application import build_applications
from .document import ( # noqa
_cleanup_doc, init_doc, unlocked, with_lock,
)
Expand Down Expand Up @@ -1000,7 +999,9 @@ def flask_handler(slug, app):
)

if warm or config.autoreload:
for app in apps.values():
for endpoint, app in apps.items():
if endpoint == '/admin':
continue
if config.autoreload:
with record_modules(list(apps.values())):
session = generate_session(app)
Expand All @@ -1010,16 +1011,6 @@ def flask_handler(slug, app):
state._on_load(None)
_cleanup_doc(session.document, destroy=True)

if admin:
if '/admin' in apps:
raise ValueError(
'Cannot enable admin panel because another app is being served '
'on the /admin endpoint'
)
from .admin import admin_panel
admin_handler = FunctionHandler(admin_panel)
apps['/admin'] = Application(admin_handler)

extra_patterns += get_static_routes(static_dirs)

if session_history is not None:
Expand Down

0 comments on commit 30e11a1

Please sign in to comment.