Hi all,
Was trying Running Panel apps in FASTAPI and have the example up and running.
But in a more real world use case, I would need to run it within routers - even if just for source code structuring.
Taking Marc’s example and extending it as below:
import panel as pn
from fastapi import APIRouter, FastAPI
from panel.io.fastapi import add_application
app = FastAPI()
router = APIRouter()
@app.get("/")
async def read_root():
return {"Hello": "World"}
@add_application('/panel_1', app=app, title='My Panel App')
def create_panel_app():
"""!
Create a dummy panel application over a FastAPI interface
See here: https://github.com/holoviz/panel/issues/7338
For making API args accessible within the method
"""
slider_1 = pn.widgets.IntSlider(name='Slider', start=0, end=10, value=3)
if pn.state.location:
# To populate slider_value in the API call, use a string like
# /panel?slider_1_value=6&slider_A_value=3
# If the GET params are not entered in the call, then the previous
# entry is used
pn.state.location.sync(slider_1, {"value": "slider_1_value"})
return pn.Row(slider_1.rx() * '⭐')
@add_application('/panel_2', app=router, title='My Panel App')
def create_panel_app_router():
"""!
Dummy panel within a router
"""
slider_1 = pn.widgets.IntSlider(name='Slider', start=0, end=10, value=3)
if pn.state.location:
print(pn.state.location.sync(slider_1, {"value": "slider_1_value"}))
return pn.Row(slider_1.rx() * '⭐')
app.include_router(router,
prefix="/test_router",
)
But on initial running, there were complaints within the bokeh_fastapi.application.BokehFastAPI class because a Router instance has been supplied rather than a FastAPI app instance.
I then tried to dummy this module by modifying as code snip below (only top to tail of section that changed shown). Basically I mapped the Router onto a dummy representation of a FastAPI app.
This worked… but only if prefix=“/test_router” does not exist when specifying the app.include_router() line in main.py
While this in itself is useful - it does allow source code structuring - it would be neater if it did align with the router paths, so onward the investigation went.
When specifying the prefix, while the endpoint is visible on the openAPI /docs page, attempting to access the endpoint will return multiple failures to access .js files in paths:
/test_router/static/extensions/panel/… …/es-module-shims.min.js
/test_router/static/js/bokeh.min.js
/test_router/static/js/bokeh-gl.min.js
/test_router/static/extensions/panel/panel.min.js
/test_router/static/js/bokeh-widgets.min.js
/test_router/static/js/bokeh-tables.min.js
Circled around it a few times now from different directions trying to specify StaticFiles and mount the app - but to be honest not really knowing what I’m doing its akin to pinning the tail on a donkey!
So putting this infront of the experts who do actually understand the library and see what you all think.
bokeh_fastapi\application.py
from fastapi import (
applications as fastapi_applications,
FastAPI,
routing as fastapi_routing,
)
# -----------------------------------------------------------------------------
class RouterApp:
"""
A dummy representation of a Router instance in the same structure as a
FastAPI instance that enables use of BokehFastAPI without cascading changes
router (FastAPI Router) :
FastAPI router that we will serve the application through
"""
# -------------------------------------------------------------------------
def __init__(self, *,
router: fastapi_routing.APIRouter,
prefix: str,
):
"""
Create this dummy FastAPI instance
"""
self.router = router
self.add_api_route = router.add_api_route
self.add_websocket_route = router.add_websocket_route
self.root_path = prefix
self.get = router.get
# -----------------------------------------------------------------------------
class BokehFastAPI:
"""
applications (dict[str,Application] or Application) :
A map from paths to ``Application`` instances.
If the value is a single Application, then the following mapping
is generated:
.. code-block:: python
applications = {{ '/' : applications }}
When a connection comes in to a given path, the associate
Application is used to generate a new document for the session.
app (FastAPI, optional) :
FastAPI app to serve the ``applications`` from.
prefix (str, optional) :
A URL prefix to use for all Bokeh server paths. (default: None)
websocket_origins (Sequence[str], optional) :
A set of websocket origins permitted to connect to this server.
secret_key (str, optional) :
A secret key for signing session IDs.
Defaults to the current value of the environment variable
``BOKEH_SECRET_KEY``
sign_sessions (bool, optional) :
Whether to cryptographically sign session IDs
Defaults to the current value of the environment variable
``BOKEH_SIGN_SESSIONS``. If ``True``, then ``secret_key`` must
also be provided (either via environment setting or passed as
a parameter value)
keep_alive_milliseconds (int, optional) :
Number of milliseconds between keep-alive pings
(default: {DEFAULT_KEEP_ALIVE_MS})
Pings normally required to keep the websocket open. Set to 0 to
disable pings.
check_unused_sessions_milliseconds (int, optional) :
Number of milliseconds between checking for unused sessions
(default: {DEFAULT_CHECK_UNUSED_MS})
unused_session_lifetime_milliseconds (int, optional) :
Number of milliseconds for unused session lifetime
(default: {DEFAULT_UNUSED_LIFETIME_MS})
include_headers (list, optional) :
List of request headers to include in session context
(by default all headers are included)
exclude_headers (list, optional) :
List of request headers to exclude in session context
(by default all headers are included)
include_cookies (list, optional) :
List of cookies to include in session context
(by default all cookies are included)
exclude_cookies (list, optional) :
List of cookies to exclude in session context
(by default all cookies are included)
"""
def __init__(
self,
applications: Mapping[str, Application | ModifyDoc] | Application | ModifyDoc,
app: FastAPI | None = None,
prefix: str | None = None,
websocket_origins: Sequence[str] | None = None,
secret_key: bytes | None = settings.secret_key_bytes(),
sign_sessions: bool = settings.sign_sessions(),
keep_alive_milliseconds: int = DEFAULT_KEEP_ALIVE_MS,
check_unused_sessions_milliseconds: int = DEFAULT_CHECK_UNUSED_MS,
unused_session_lifetime_milliseconds: int = DEFAULT_UNUSED_LIFETIME_MS,
include_headers: list[str] | None = None,
include_cookies: list[str] | None = None,
exclude_headers: list[str] | None = None,
exclude_cookies: list[str] | None = None,
):
if callable(applications):
applications = Application(FunctionHandler(applications))
if isinstance(applications, Application):
applications = {"/": applications}
else:
applications = dict(applications)
for url, application in applications.items():
if callable(application):
applications[url] = application = Application(
FunctionHandler(application)
)
if all(
not isinstance(handler, DocumentLifecycleHandler)
for handler in application._handlers
):
application.add(DocumentLifecycleHandler())
applications = cast(dict[str, Application], applications)
# Wrap applications in ApplicationContext
self._applications = {}
for url, application in applications.items():
self._applications[url] = ApplicationContext(application, url=url)
if app is None:
app = FastAPI()
if isinstance(app, fastapi_applications.FastAPI):
self.app = app
elif isinstance(app, fastapi_routing.APIRouter):
self.app = RouterApp(
router=app,
prefix=prefix,
)