Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update bokeh-fastapi integration #7262

Merged
merged 2 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading