FastAPI-Integration#

Panel läuft üblicherweise auf einem Bokeh-Server, der wiederum auf Tornado läuft. Es ist jedoch auch oft nützlich, eine Panel-App in eine große Webanwendung einzubetten, wie z.B. einen FastAPI-Webserver. Die Integration in FastAPI ist einfacher im Vergleich zu anderen wie z.B. Flask, da es ein leichtgewichtigeres Framework ist. Die Verwendung von Panel mit FastAPI erfordert nur ein wenig mehr Aufwandals bei Notebooks und Bokeh-Servern.

Konfiguration#

Bevor wir mit dem Hinzufügen einer Bokeh-Anwendung zu unserem FastApi-Server beginnen, müssen wir einige der grundlegenden Konfigurationen in fastAPI/main.py einrichten:

  1. Zunächst importieren wir alle erforderlichen Elemente:

    1from bokeh.embed import server_document
    2from fastapi import FastAPI, Request
    3from fastapi.templating import Jinja2Templates
    4import panel as pn
    
  2. Als nächstes definieren wir app als Instanz von FastAPI und definieren den Pfad zum Vorlagenverzeichnis:

    8app = FastAPI()
    9templates = Jinja2Templates(directory = "templates")
    
  3. Nun erstellen wir unsere erste Routine über eine asynchrone Funktion und verweisen sie an unseren BokehServer:

    11@app.get("/")
    12async def bkapp_page(request: Request):
    13    script = server_document("http://127.0.0.1:5000/app")
    14    return templates.TemplateResponse("base.html",
    15            {"request": request, "script": script})
    
  4. Wie ihr dem Code ansehen könnt, wird ein Jinja2-Template fastAPI/templates/base.html erwartet. Dieses kann z.B. folgenden Inhalt haben:

    1<!DOCTYPE html>
    2<html>
    3    <head>
    4        <title>Panel in FastAPI: sliders</title>
    5    </head>
    6    <body>
    7        {{ script|safe }}
    8    </body>
    9</html>
    
  5. Kehren wir nun zurück zu unserer fastAPI/main.py-Datei um mit pn.serve() unseren Bokeh-Server zu starten:

    18pn.serve({"/app": createApp},
    19        address = "127.0.0.1",
    20        port = 5000,
    21        show = False,
    22        allow_websocket_origin = ["127.0.0.1:8000"])
    
    createApp

    ruft in diesem Beispiel unsere Panel-App auf, die jedoch erst im nächsten Abschnitt behandelt wird.

    address, port

    Adresse und Port, an dem der Server auf Anfragen lauscht; in unserem Fall also http://127.0.0.1:5000.

    show=False

    sorgt dafür, dass der Bokeh-Server zwar gestartet wird, jedoch nicht unmittelbar im Browser angezeigt wird.

    allow_websocket_origin

    listet die Hosts auf, die sich mit dem Websocket verbinden können. In unserem Beispiel soll das fastApi sein, also verwenden wir 127.0.0.1:8000.

  6. Nun definieren wir die sliders-App auf Basis einer Standardvorlage für FastAPI-Apps, die zeigt, wie Panel und FastAPI integriert werden können:

    fastAPI/sliders/sinewave.py

    ein parametrisiertes Objekt, das euren bereits vorhandenen Code darstellt:

     1import numpy as np
     2import param
     3
     4from bokeh.models import ColumnDataSource
     5from bokeh.plotting import figure
     6
     7
     8class SineWave(param.Parameterized):
     9    offset = param.Number(default=0.0, bounds=(-5.0, 5.0))
    10    amplitude = param.Number(default=1.0, bounds=(-5.0, 5.0))
    11    phase = param.Number(default=0.0, bounds=(0.0, 2 * np.pi))
    12    frequency = param.Number(default=1.0, bounds=(0.1, 5.1))
    13    N = param.Integer(default=200, bounds=(0, None))
    14    x_range = param.Range(default=(0, 4 * np.pi), bounds=(0, 4 * np.pi))
    15    y_range = param.Range(default=(-2.5, 2.5), bounds=(-10, 10))
    16
    17    def __init__(self, **params):
    18        super(SineWave, self).__init__(**params)
    19        x, y = self.sine()
    20        self.cds = ColumnDataSource(data=dict(x=x, y=y))
    21        self.plot = figure(plot_height=400, plot_width=400,
    22                           tools="crosshair, pan, reset, save, wheel_zoom",
    23                           x_range=self.x_range, y_range=self.y_range)
    24        self.plot.line('x', 'y', source=self.cds, line_width=3, line_alpha=0.6)
    25
    26    @param.depends('N', 'frequency', 'amplitude', 'offset', 'phase', 'x_range', 'y_range', watch=True)
    27    def update_plot(self):
    28        x, y = self.sine()
    29        self.cds.data = dict(x=x, y=y)
    30        self.plot.x_range.start, self.plot.x_range.end = self.x_range
    31        self.plot.y_range.start, self.plot.y_range.end = self.y_range
    32
    33    def sine(self):
    34        x = np.linspace(0, 4 * np.pi, self.N)
    35        y = self.amplitude * np.sin(self.frequency * x + self.phase) + self.offset
    36        return x, y
    
    fastAPI/sliders/pn_app.py

    erstellt eine App-Funktion aus der SineWave-Klasse:

    1import panel as pn
    2
    3from .sinewave import SineWave
    4
    5
    6def createApp():
    7    sw = SineWave()
    8    return pn.Row(sw.param, sw.plot).servable()
    
  7. Schließlich kehren wir zu unserer fastAPI/main.py zurück und importieren die createApp-Funktion:

    4from sliders.pn_app import createApp
    

Die Dateistruktur sollte nun folgendermaßen aussehen:

fastAPI
├── main.py
├── sliders
│   ├── pn_app.py
│   └── sinewave.py
└── templates
    └── base.html

Ihr könnt den Server nun starten mit:

$ bin/uvicorn main:app --reload
INFO:     Will watch for changes in these directories: ['/srv/jupyter/jupyter-tutorial/docs/web/dashboards/panel/fastAPI']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [218214] using StatReload
Launching server at http://127.0.0.1:5000
INFO:     Started server process [218216]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

Anschließend solltet ihr im Web-Browser unter der URL http://127.0.0.1:8000 folgendes sehen:

Widgets und Sinuskurve in Bokeh-Plot