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.

6 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

@nghenzi 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

@nghenzi 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.

4 Likes

@philippjfr I created a step by step guide on integrating panel and fastApi similar to the one on django apps. What would be the process to get this add to the main docs? Anything I should revise or add?

Here’s the guide: https://hackmd.io/ileoi_9YT6eEm27hbxTzmA?view

4 Likes

Wow. That is really great.

I have three questions:

  1. does the panel/ Bokeh server have to be started in a thread in the app? Or could it just be run in a separate process via panel serve?

  2. what is your use case for integrating it in a fastapi app?

  3. should you not use the panel Server class instead of the Bokeh server? My understanding is that it is needed for example when you start using some specific non-Bokeh based widgets, layouts or panes.

The first step to get it added to the Panel docs would be to open a feature request on GitHub.

I think it would be helpful if you describe one or more ways it could be added in the request.

My main question is if the Panel docs mainly should refer to your doc and example or whether the example should be included in the gallery of Panel. Personally I think it makes sense to add it similarly to adding flask and django examples.

In the latter case it would be added via a PR.

Really great writeup @t-houssian! Agree with Marc. I’d probably like to have some form of nesting of the TOC tree in the user guide and then put Flask, Django and Fast API guides under a header about “Embedding Panel Apps”. Let’s start with a PR and then we can iterate.

2 Likes

Thanks for the response and feedback! As for your questions:

  1. For this example I used a thread. I tried using panel serve at first but couldn’t quite get that working, any pointers on what I’d need to do?

  2. Our case for using fastApi with panel is to embed the panel app in our internal automation app that we’ve been building out with fastApi. We wanted to keep using fastApi to take advantage of it’s speed and extensive framework, but also wanted to use the data visualization tools in panel.

  3. I didn’t realize their was a panel server class in panel. I will have to check that out.

Maybe could you help me re-write this part to use panel instead of bokeh:

def bk_worker():
    server = Server({'/app': createApp},
        port=5000, io_loop=IOLoop(), 
        allow_websocket_origin=["*"])

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

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

Here is what I already tried:

pn.serve({'/app': createApp},
        port=5000, address="127.0.0.1",
         websocket_origin=["*"]
1 Like

Thanks, sounds good! I will work on getting a PR up.

Hi @t-houssian

Could you try if replacing from bokeh.server.server import Server with from panel.io.server import Server works?

Ah, yes, that did work!

1 Like