Embed Panel in fastApi

Hi All! Does anyone know how to embed a panel app in fastApi? I tried something similar to embedding in Django but can’t seem to get it to work. Here is the code I am trying so far.

In main.py:

@app.get("/panel", response_class=HTMLResponse)
async def read_root(request: Request):
    script = server_document(createApp(HTMLResponse))
    return templates.TemplateResponse("panel.html", {"request": request, "script": script})

in pn_app.py

import panel as pn
from .app1 import DataSetSelect

def createApp(doc):
    
    apex = DataSetSelect()

    col = pn.Column(apex.add_title, apex.param.input_name, apex.param.age,
        apex.param.add_entry, apex.add_graph)

    return col.server_doc(doc)

in panel.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>App 1</title>
    </head>
    <body>
        {{ script|safe }}
    </body>
</html>

My current error is

AttributeError: type object 'HTMLResponse' has no attribute 'session_context'

Not sure exactly what to do. Any help is appreciated! This is related to this https://github.com/holoviz/panel/issues/537 which I commented on.

Just to say that I would love to work on this but can’t say when I’ll have the time to dig into it yet. I think eventually I’d like to replace the Tornado based server with a more performant FastAPI one.

4 Likes

Hi @t-houssian

One way to get help from users interested in helping out but not experienced in Panel + fastAPI would be creating a minimum reproducible repository with very specific instructions on how to install+run it and the specific issue(s).

Then it would be simple to at least get started helping out and using the knowledge we have from other use cases.

1 Like

I am not sure if this can help you, but you can check it the following page. He embedded an panel app in starlette (fast api is based on that).

3 Likes

… and integrate with FastAPIs rest api @philippjfr ???

You would really have to explain to me what you mean by that.

1 Like

I could make it to work with bokeh only (you need a templates folder with the embed.html)

# import panel as pn

from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from starlette.middleware.cors import CORSMiddleware
from starlette.middleware import Middleware

from threading import Thread

from tornado.ioloop import IOLoop

from bokeh.embed import server_document
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, Slider
from bokeh.plotting import figure
from bokeh.sampledata.sea_surface_temperature import sea_surface_temperature
from bokeh.server.server import Server
from bokeh.themes import Theme

app = FastAPI(title="NC Plot",
              description="Prototype API for plotting",
              version="0.0.1",
              )

app.add_middleware(
    CORSMiddleware,
    allow_origins=['*'],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

templates = Jinja2Templates(directory="templates")


def pnapp(curdoc):
    df = sea_surface_temperature.copy()
    source = ColumnDataSource(data=df)

    plot = figure(x_axis_type='datetime', y_range=(0, 25), y_axis_label='Temperature (Celsius)',
                  title="Sea Surface Temperature at 43.18, -70.43")
    plot.line('time', 'temperature', source=source)

    def callback(attr, old, new):
        if new == 0:
            data = df
        else:
            data = df.rolling('{0}D'.format(new)).mean()
        source.data = ColumnDataSource.from_df(data)

    slider = Slider(start=0, end=30, value=0, step=1, title="Smoothing by N Days")
    slider.on_change('value', callback)

    cond = True

    if cond:
        curdoc.add_root(column(slider, plot))
    else:
        # col = pn.Column(slider, plot)
        col.server_doc(curdoc)


@app.get("/")
async def bkapp_page(request: Request):
    print ('reeee')
    script = server_document('http://127.0.0.1:5006/bkapp')
    # return {"item_id": "Foo"}
    # print ('reeee',request)
    
    return templates.TemplateResponse("embed.html", {"request": request, "script": script})

def bk_worker():
    # Can't pass num_procs > 1 in this configuration. If you need to run multiple
    # processes, see e.g. flask_gunicorn_embed.py
    server = Server({'/bkapp': pnapp}, port=5006, io_loop=IOLoop(), 
            allow_websocket_origin=["*"])
    server.start()
    server.io_loop.start()

th = Thread(target=bk_worker)
th.daemon = True
th.start()

with panel it does not found the static folder

I guess it should be easy to fix.

2 Likes

I could not find a nice solution, but copying the css files of panel to an static folder served by fast api, I could embed the panel app in fast api. For sure someone could give some hint how to do it correctly

and the code

import panel as pn, os

from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles

from starlette.middleware.cors import CORSMiddleware
from starlette.middleware import Middleware

from threading import Thread

from tornado.ioloop import IOLoop
from tornado import web

from bokeh.embed import server_document
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, Slider
from bokeh.plotting import figure
from bokeh.sampledata.sea_surface_temperature import sea_surface_temperature
from bokeh.server.server import Server
from bokeh.themes import Theme


# with pn.util.edit_readonly(pn.state):
#         pn.state.base_url = "http://127.0.0.1:5006"
#         pn.state.rel_path = "http://127.0.0.1:8000"

app = FastAPI(title="NC Plot",
              description="Prototype API for plotting",
              version="0.0.1",
              )

app.add_middleware(
    CORSMiddleware,
    allow_origins=['*'],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.mount("/static", StaticFiles(directory="static"), name="static")

templates = Jinja2Templates(directory="templates")


def pnapp(curdoc):
    df = sea_surface_temperature.copy()
    source = ColumnDataSource(data=df)

    plot = figure(x_axis_type='datetime', y_range=(0, 25), y_axis_label='Temperature (Celsius)',
                  title="Sea Surface Temperature at 43.18, -70.43")
    plot.line('time', 'temperature', source=source)

    def callback(attr, old, new):
        if new == 0:
            data = df
        else:
            data = df.rolling('{0}D'.format(new)).mean()
        source.data = ColumnDataSource.from_df(data)

    slider = Slider(start=0, end=30, value=0, step=1, title="Smoothing by N Days")
    slider.on_change('value', callback)

    cond = False
    print (pn.state.rel_path)

    # with pn.util.edit_readonly:
    #     pn.state.base_url = "http://127.0.0.1:5006"

    print ('urls', pn.state.base_url, 'efff', pn.state.rel_path)

    if cond:
        curdoc.add_root(column(slider, plot))
    else:
        col = pn.Column(slider, plot)
        col.server_doc(curdoc)


to

@app.get("/")
async def bkapp_page(request: Request):
    print ('reeee')
    script = server_document('http://127.0.0.1:5006/bkapp')
    # return {"item_id": "Foo"}
    # print ('reeee',request)
    
    return templates.TemplateResponse("embed.html", {"request": request, "script": script})

def bk_worker():
    # Can't pass num_procs > 1 in this configuration. If you need to run multiple
    # processes, see e.g. flask_gunicorn_embed.py
    # with pn.util.edit_readonly(pn.state):
    #     pn.state.base_url = "http://127.0.0.1:5006"
    #     pn.state.rel_path = "http://127.0.0.1:5006"

    server = Server({'/bkapp': pnapp}, port=5006, io_loop=IOLoop(), 
            allow_websocket_origin=["*"])

    # handlers = [ (
    #                     "http://127.0.0.1:5006/extensions/panel/css/(.*)",
    #                     web.StaticFileHandler,
    #                     {"path": os.path.join(os.path.dirname(__file__), "static")},
    #              ) ]

    # server._tornado.add_handlers(r".*", handlers)

    server.start()
    server.io_loop.start()

th = Thread(target=bk_worker)
th.daemon = True
th.start()

to run the example ‘uvicorn filename:app --reload’. It is needed a templates folder with the html files and an static folder with the css files of panel.

2 Likes

@nghenzi2019 Thanks for the example! I’ve been trying to get it working but I am running into this error, do you by chance know what might be the cause?

GET http://127.0.0.1:5006/bkapp/autoload.js?bokeh-autoload-element=1002&bokeh-app-path=/bkapp&bokeh-absolute-url=http://127.0.0.1:5006/bkapp net::ERR_CONNECTION_REFUSED
1 Like

I think you need to change 127.0.01 by 206.189.202.159.i am not an expert, but 127.0.0.1 indicates local host when you use your own pc. When you use other pc in the network you need to indicate the ip where the server is located. Maybe it is need the address parameter when the bokeh server is created. Now i am in the cellphone, but i will check it later

1 Like

I tried it in my local network and you need to change 3 things

1- The address where fastapi goes to look the bokeh script for executing itself in client

@app.get("/")
async def bkapp_page(request: Request):
    print ('reeee')
    script = server_document('http://206.189.202.159:5006/bkapp')
  1. add adress = ‘0.0.0.0’, in order for bokeh server listens all the addresses from where a request can come
    server = Server({'/bkapp': pnapp}, port=5006, address = '0.0.0.0', io_loop=IOLoop(), 
            allow_websocket_origin=["*"])
  1. in fast api you need to specify the host flag, which is similar to the address parameter in bokeh server
uvicorn filename:app --reload --host 0.0.0.0

One final note, if you use the templates of panel, to the static folder you are going to need put the css of the templates there too. It should be a way in starlette to indicate to look for it in the panel static folder, but i can not find it yet.

1 Like

@nghenzi2019 It worked! Thanks so much man! You saved me on this one!

Here is the end code I ended up using:

In main.py

from bokeh.embed import server_document
from threading import Thread
from bokeh.server.server import Server
from tornado.ioloop import IOLoop
from starlette.middleware.cors import CORSMiddleware
from starlette.middleware import Middleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=['*'],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/panel")
async def bkapp_page(request: Request):
    print ('reeee', flush=True)
    script = server_document('http://206.189.202.59:5006/bkapp')
    return templates.TemplateResponse("panel.html", {"request": request, "script": script})

def bk_worker():
    server = Server({'/bkapp': createApp}, port=5006, address='0.0.0.0', io_loop=IOLoop(), 
            allow_websocket_origin=["*"]) 

    server.start()
    server.io_loop.start()

th = Thread(target=bk_worker)
th.daemon = True
th.start()

in app1.py:

import param
import panel as pn
import pandas as pd
import holoviews as hv

class DataSetSelect(param.Parameterized):
    """
    This is where all the action happens. This is a self
    contained class where all the parameters are dynamic
    and watch each other for changes. 
    See https://panel.holoviz.org/ for details
    and https://discourse.holoviz.org/c/panel/5 for help
    Widgets and Parameters in this class need to be added
    to pn_app.py to be able to display.
    """
    hv.extension('bokeh')
    pn.config.sizing_mode="stretch_width"

    css = """
    """

    pn.extension(raw_css=[css])

    title = '##' + 'Test App'
    message = ''

    df = param.DataFrame(pd.DataFrame())

    input_name = param.String("")

    age = param.Integer(50000, bounds=(-200, 100000))

    add_entry = param.Action(lambda self: self.param.trigger('add_entry'), label='Add Entry')

    def add_title(self):
        return self.title

    @param.depends('add_entry', watch=True)
    def update_graph(self):
        data = {'Name':[self.input_name],
        'Age':[self.age]}
        df_temp = pd.DataFrame(data)
        self.df = self.df.append(df_temp)

    @param.depends('df')
    def add_graph(self):
        return self.df

in pn_app.py:

import panel as pn
from .app1 import DataSetSelect

def createApp(curdoc):
    
    apex = DataSetSelect()

    col = pn.Column(apex.add_title, apex.param.input_name, apex.param.age,
        apex.param.add_entry, apex.add_graph)

    return col.server_doc(curdoc)

in panel.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>App 1</title>
    </head>
    <body>
        {{ script|safe }}
    </body>
</html>

I didn’t even need to have a static folder or manually add in the css!
Here is what my directory structure looks like:
Screenshot from 2021-10-08 13-32-02

And here is what the output shows (Fully working with callbacks):

I also had to set an environment variable: BOKEH_ALLOW_WS_ORIGIN=206.189.202.59:8090

And use uvicorn main:app --host 0.0.0.0 --port 8090 --reload
To run it.

2 Likes