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 Aufwand als 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:

    fastAPI/main.py
    1import panel as pn
    2from bokeh.embed import server_document
    3
    4from fastapi import FastAPI, Request
    5from fastapi.templating import Jinja2Templates
    
  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 Bokeh-Server:

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

    fastAPI/templates/base.html
    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:

    21    {"/app": createApp},
    22    port=5000,
    23    allow_websocket_origin=["127.0.0.1:8000"],
    24    address="127.0.0.1",
    25    show=False,
    26)
    
    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.

    Ggf. muss noch die Umgebungsvariable BOKEH_ALLOW_WS_ORIGIN gesetzt werden mit:

    $ export BOKEH_ALLOW_WS_ORIGIN=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:

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

    erstellt eine App-Funktion aus der SineWave-Klasse:

    fastAPI/sliders/pn_app.py
    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:

    fastAPI/main.py
    3from 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:

$ uv run 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