From df71a00d24d56d720a20c51ea16a097791836a0e Mon Sep 17 00:00:00 2001 From: Jeroen Van Der Donckt Date: Thu, 1 Oct 2026 09:16:39 +0200 Subject: [PATCH 1/4] docs: add a web apps guide for Streamlit, Dash and Gradio --- CHANGELOG.md | 5 + docs/guides/embedding.md | 6 +- docs/guides/web-apps.md | 249 +++++++++++++++++++++++++++++++++++++++ docs/index.md | 2 + mkdocs.yml | 1 + 5 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 docs/guides/web-apps.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 9657afd7..c1279290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ independently. `flexviz` pins a compatible `flexviz-polars` range. ## [Unreleased] +### Added + +- The Web apps guide shows a dashboard in a Streamlit, Dash, Gradio, or Flask + app, on the same port as the app. + ### Security - The server sends no CORS headers any more, so a page of another site cannot diff --git a/docs/guides/embedding.md b/docs/guides/embedding.md index 4470eecc..1b425fe7 100644 --- a/docs/guides/embedding.md +++ b/docs/guides/embedding.md @@ -43,8 +43,10 @@ mount_into(app, prefix="/flexviz") The FlexViz endpoints (`/dashboard/update`, `/share`, `/view`, `/sources`) then live under the prefix. The mounted app brings its own gzip middleware, so responses are compressed regardless of the host app's setup. -For Flask or other WSGI hosts, use -`werkzeug.middleware.dispatcher.DispatcherMiddleware` instead. +A WSGI app, such as Flask, cannot mount FlexViz directly, because FlexViz is an +ASGI app. Wrap FlexViz with `a2wsgi`, as +[Web apps](web-apps.md#flask-and-other-wsgi-apps) shows. The same guide mounts +FlexViz into Streamlit, Dash, and Gradio. ## Serving a dashboard from a URL diff --git a/docs/guides/web-apps.md b/docs/guides/web-apps.md new file mode 100644 index 00000000..c30de43a --- /dev/null +++ b/docs/guides/web-apps.md @@ -0,0 +1,249 @@ +# Web apps + +You can show a FlexViz dashboard in a Streamlit, Dash, Gradio, or other Python +web app. The web app owns the page, the widgets, and the layout. FlexViz owns +the figures and runs their queries on your data. To run FlexViz as a separate +server instead, see [Embedding](embedding.md). + +The procedure is the same for each web framework: + +1. Register the data source with `register_source()`. +2. Mount the FlexViz app on the server of the web app, under `/flexviz`. +3. Build a `/view` URL with `share_url(server_url="/flexviz", source_name=...)`. +4. Show the URL in an iframe. + +The web app and FlexViz then use one server and one port, so you deploy one +process. The URL has no host name, so it works on each host that serves the web +app. + +These rules apply to every web framework: + +- Give the `Dashboard` the same data as the source: the registered frame, or a + lazy scan of the same file. A scan reads no rows until a query runs. A + `Dashboard()` without data gives figures without a source name, so each + update fails and the panels stay empty. +- Give `share_url()` the name of the registered source in `source_name`. +- Use the same prefix in the mount and in `server_url`. If a proxy serves the + web app under a path, such as `/app`, put that path in front: + `server_url="/app/flexviz"`. +- Register all sources before the server starts. + +## Streamlit + +Streamlit 1.57 or newer can add routes to its own server with `st.App`. Use +two files. `app.py` runs one time, when the server starts. `page.py` is the +Streamlit script, which runs again after each widget change. + +```python +# app.py +import flexviz +import polars as pl +import streamlit as st +from starlette.routing import Mount + +flexviz.register_source("readings", pl.scan_parquet("readings.parquet"), cache=True) +app = st.App("page.py", routes=[Mount("/flexviz", app=flexviz.app)]) +``` + +```python +# page.py +import polars as pl +import streamlit as st +from flexviz import Dashboard + +st.title("Sensor readings") +bins = st.slider("Histogram bins", 10, 200, 60) + + +@st.cache_data +def view_url(bins: int) -> str: + dashboard = Dashboard(pl.scan_parquet("readings.parquet"), cache=True) + dashboard.add_figure(title="Power").add_line(x="timestamp", y="power") + dashboard.add_figure(title="Distribution").add_histogram(x="power", bins=bins) + return dashboard.share_url(server_url="/flexviz", source_name="readings", cols=1) + + +st.iframe(view_url(bins), height=900) +``` + +Start the app: + +```bash +streamlit run app.py +``` + +Streamlit finds the `app` object in `app.py`. It then serves the page and +FlexViz on one port, 8501 by default. + +### Keep the view across reruns + +A new `Dashboard` gets new uids, so each run of `page.py` gives a new URL. The +iframe then loads the dashboard again, and the user loses the zoom and the +selections. + +`st.cache_data` prevents this reset. It returns the same URL for the same +arguments, and Streamlit keeps an iframe whose URL does not change. In the +example, only a change of `bins` loads the dashboard again. + +### Set the height + +`st.iframe` cannot measure the height of the dashboard, so it uses 400 px. Set +the height yourself. A panel is 400 px by default, and the toolbar is about +45 px. While a selection is active, a 45 px bar with the active filters shows +at the bottom. The two stacked panels in the example thus need 890 px. See +[Who owns width and height](embedding.md#who-owns-width-and-height). + +## Dash + +Dash 4.2 or newer can run on FastAPI. Install it with the FastAPI extra: + +```bash +pip install "dash[fastapi]" +``` + +Then mount FlexViz on `app.server`: + +```python +# app.py +import polars as pl +from dash import Dash, html +from flexviz import Dashboard, mount_into, register_source + +lf = pl.scan_parquet("readings.parquet") +register_source("readings", lf, cache=True) + +dashboard = Dashboard(lf, cache=True) +dashboard.add_figure(title="Power").add_line(x="timestamp", y="power") +url = dashboard.share_url(server_url="/flexviz", source_name="readings", cols=1) + +app = Dash(__name__, backend="fastapi") +mount_into(app.server, prefix="/flexviz") +app.layout = html.Iframe( + src=url, style={"width": "100%", "height": "460px", "border": 0} +) + +if __name__ == "__main__": + app.run() +``` + +Start it with `python app.py`. The URL is a plain string, so a Dash callback +can return a new one to `html.Iframe.src`. + +On the default Flask backend, or before Dash 4.2, `app.server` is a Flask app. +Apply the [Flask recipe](#flask-and-other-wsgi-apps) to it. + +## Gradio + +Gradio mounts into a FastAPI app with `gr.mount_gradio_app`. Mount FlexViz on +the same FastAPI app first: + +```python +# app.py +import gradio as gr +import polars as pl +import uvicorn +from fastapi import FastAPI +from flexviz import Dashboard, mount_into, register_source + +lf = pl.scan_parquet("readings.parquet") +register_source("readings", lf, cache=True) + +dashboard = Dashboard(lf, cache=True) +dashboard.add_figure(title="Power").add_line(x="timestamp", y="power") +url = dashboard.share_url(server_url="/flexviz", source_name="readings", cols=1) + +with gr.Blocks() as demo: + gr.HTML( + f'' + ) + +app = FastAPI() +mount_into(app, prefix="/flexviz") +app = gr.mount_gradio_app(app, demo, path="/") + +if __name__ == "__main__": + uvicorn.run(app, host="127.0.0.1", port=7860) +``` + +!!! warning "Mount FlexViz before Gradio" + Gradio at `path="/"` answers every path that no earlier route answers. If + you mount Gradio first, `/flexviz/view` returns 404. + +## Other web frameworks + +### FastAPI and Starlette apps + +A web framework that runs on FastAPI or Starlette takes the same mount. For +example, the `app` of NiceGUI is a FastAPI app, so +`mount_into(app, prefix="/flexviz")` adds FlexViz to it. Show the URL in the +iframe element of the framework. + +### Flask and other WSGI apps + +A WSGI app, such as Flask, cannot mount FlexViz directly, because FlexViz is an +ASGI app. Install [a2wsgi](https://pypi.org/project/a2wsgi/) to wrap FlexViz: + +```bash +pip install a2wsgi +``` + +Then mount the wrapped app with the dispatcher of Werkzeug. The example leaves +out the source and the `url`. Build them as in the Dash example. + +```python +import flexviz +from a2wsgi import ASGIMiddleware +from flask import Flask +from werkzeug.middleware.dispatcher import DispatcherMiddleware + +app = Flask(__name__) +app.wsgi_app = DispatcherMiddleware( + app.wsgi_app, {"/flexviz": ASGIMiddleware(flexviz.app)} +) + + +@app.get("/") +def index(): + return ( + f'' + ) +``` + +Start the app with `flask --app app run`. + +## Limits + +- The Python code of the web app cannot read the zoom or the selections in the + dashboard. The **Share** button gives a URL with the full view. +- The server always queries the registered source. A filter on the frame of + the `Dashboard`, such as `lf.filter(...)`, thus has no effect. To show a + subset, register it as a separate source, and let a widget select the + `source_name`. +- A widget can change the spec: data columns, traces, bins, or layout. To + filter rows while you explore, select in a FlexViz figure. +- A new URL loads the dashboard again, and the view goes back to its start. + +## Security + +The FlexViz routes have no authentication. Each user who can open the web app +can query the registered sources through `/flexviz`. Before you deploy the web +app, put authentication in front of it. Make sure that the authentication also +covers the `/flexviz` routes. + +Streamlit listens on all network interfaces by default. For local use, start +it with `streamlit run app.py --server.address 127.0.0.1`. + +The `Host` check of `show()` and `flexviz serve` does not apply to a mounted +app. On a loopback address, wrap FlexViz in Starlette's `TrustedHostMiddleware` +before you mount it: + +```python +from starlette.middleware.trustedhost import TrustedHostMiddleware + +guarded = TrustedHostMiddleware(flexviz.app, allowed_hosts=["localhost", "127.0.0.1"]) +``` + +Mount `guarded` in place of `flexviz.app`, for example with +`app.mount("/flexviz", guarded)`. For Flask, wrap `guarded` in +`ASGIMiddleware`. FlexViz then refuses a request for another host name, which +blocks DNS rebinding. diff --git a/docs/index.md b/docs/index.md index c14a536b..abf9b0fd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -192,5 +192,7 @@ and `add_geo_line`. See the [Figure API](api/figure.md) for every parameter. caching for static data and zero-latency brushing. - [Embedding](guides/embedding.md): mount FlexViz into an existing FastAPI app. +- [Web apps](guides/web-apps.md): show a dashboard in a Streamlit, Dash, or + Gradio app. - [Agents](guides/ai-agents.md): drive FlexViz from a coding agent and read back what the human explored. diff --git a/mkdocs.yml b/mkdocs.yml index ae5a2116..e054df03 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -79,6 +79,7 @@ nav: - Data sources: guides/data-sources.md - Sharing views: guides/sharing.md - Embedding: guides/embedding.md + - Web apps: guides/web-apps.md - Agents: guides/ai-agents.md - API reference: - Figure: api/figure.md From fc4f5b97c006b985ee0f1b531b570522d0b10eb0 Mon Sep 17 00:00:00 2001 From: Jeroen Van Der Donckt Date: Thu, 1 Oct 2026 09:16:39 +0200 Subject: [PATCH 2/4] fix(server): point WSGI hosts to a2wsgi in the mount_into hint --- CHANGELOG.md | 6 ++++++ flexviz/server.py | 11 ++++++----- tests/test_integration.py | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1279290..b3713f2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,12 @@ independently. `flexviz` pins a compatible `flexviz-polars` range. - `flexviz.server.show_server`. Use `Figure.show()`, `Dashboard.show()` or `flexviz serve`. +### Fixed + +- The `mount_into()` error now tells users of Flask and other WSGI apps to + wrap FlexViz with `a2wsgi`. The old hint, a bare `DispatcherMiddleware`, + failed on each request with a `TypeError`. + ## [0.1.0b5] - 2026-09-27 ### Added diff --git a/flexviz/server.py b/flexviz/server.py index 0415d5ca..10e84632 100644 --- a/flexviz/server.py +++ b/flexviz/server.py @@ -736,9 +736,9 @@ async def dashboard_update( def mount_into(host_app: Any, prefix: str = "/flexviz") -> None: """Mount the flexviz FastAPI app on *host_app* under *prefix*. - Works with FastAPI / Starlette (``host_app.mount``). For Flask/WSGI - hosts, use ``werkzeug.middleware.dispatcher.DispatcherMiddleware`` - instead. + Works with FastAPI / Starlette (``host_app.mount``). A Flask/WSGI host + cannot call an ASGI app: wrap ``app`` in ``a2wsgi.ASGIMiddleware`` and + mount that with ``werkzeug.middleware.dispatcher.DispatcherMiddleware``. The mounted app carries its own ``GZipMiddleware(minimum_size=1024)`` (cube cross-filter design §8.1 — the wire-size mitigation), so all @@ -758,8 +758,9 @@ def mount_into(host_app: Any, prefix: str = "/flexviz") -> None: else: raise TypeError( f"host_app of type {type(host_app).__name__!r} does not support " - ".mount(). Use werkzeug.middleware.dispatcher.DispatcherMiddleware " - "for Flask/WSGI hosts." + ".mount(). For Flask/WSGI hosts, wrap flexviz.app in " + "a2wsgi.ASGIMiddleware and mount that with " + "werkzeug.middleware.dispatcher.DispatcherMiddleware." ) diff --git a/tests/test_integration.py b/tests/test_integration.py index 66c20d31..91190b75 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1714,7 +1714,7 @@ def test_mount_routes_accessible(self): def test_mount_raises_for_non_asgi(self): from flexviz.server import mount_into - with pytest.raises(TypeError, match="does not support .mount"): + with pytest.raises(TypeError, match=r"does not support \.mount.*a2wsgi"): mount_into(object(), "/fv") From 6fe06a4170d289cdf2847b04286302579d206a61 Mon Sep 17 00:00:00 2001 From: Jeroen Van Der Donckt Date: Thu, 1 Oct 2026 14:03:25 +0200 Subject: [PATCH 3/4] docs: add example data, cache and history notes to the web apps guide --- docs/guides/web-apps.md | 144 +++++++++++++++++++++++++++------------- 1 file changed, 98 insertions(+), 46 deletions(-) diff --git a/docs/guides/web-apps.md b/docs/guides/web-apps.md index c30de43a..a3334cfe 100644 --- a/docs/guides/web-apps.md +++ b/docs/guides/web-apps.md @@ -19,20 +19,48 @@ app. These rules apply to every web framework: - Give the `Dashboard` the same data as the source: the registered frame, or a - lazy scan of the same file. A scan reads no rows until a query runs. A - `Dashboard()` without data gives figures without a source name, so each - update fails and the panels stay empty. + lazy scan of the same file. - Give `share_url()` the name of the registered source in `source_name`. -- Use the same prefix in the mount and in `server_url`. If a proxy serves the - web app under a path, such as `/app`, put that path in front: - `server_url="/app/flexviz"`. +- Use the same prefix in the mount and in `server_url`. +- If a proxy serves the web app under a path, such as `/app`, put that path in + front: `server_url="/app/flexviz"`. - Register all sources before the server starts. +- Do not change the data of a registered source while the server runs. To show + new data, restart the server. + +A broken rule shows only in the browser. A lazy scan reads no rows until a +query runs, so the web app can build its `Dashboard` on a scan at no cost. A +`Dashboard()` without data gives figures without a source name, and each update +of these figures fails. After a change of a scanned file, the updates also +fail. With `cache=True`, the first view of each figure can show the old data. +See [Caching and live brushing](caching-and-live-brushing.md). + +## Example data + +The examples read `readings.parquet`, a file with a `timestamp` and a `power` +column. + +To make a test file with 2 million rows, run this script one time: + +```python +# make_data.py +import numpy as np +import polars as pl + +n = 2_000_000 +pl.select( + timestamp=pl.datetime(2026, 1, 1) + pl.duration(seconds=pl.int_range(n)), + power=pl.Series(np.random.default_rng(0).standard_normal(n).cumsum()), +).write_parquet("readings.parquet") +``` ## Streamlit -Streamlit 1.57 or newer can add routes to its own server with `st.App`. Use -two files. `app.py` runs one time, when the server starts. `page.py` is the -Streamlit script, which runs again after each widget change. +Streamlit 1.57 or newer can add routes to its own server with `st.App`. The +app then has two files. `app.py` runs one time, when the server starts. +`page.py` is the Streamlit script, which runs again after each widget change. + +Put the source and the mount in `app.py`: ```python # app.py @@ -45,6 +73,8 @@ flexviz.register_source("readings", pl.scan_parquet("readings.parquet"), cache=T app = st.App("page.py", routes=[Mount("/flexviz", app=flexviz.app)]) ``` +Put the page in `page.py`: + ```python # page.py import polars as pl @@ -87,21 +117,25 @@ example, only a change of `bins` loads the dashboard again. ### Set the height -`st.iframe` cannot measure the height of the dashboard, so it uses 400 px. Set -the height yourself. A panel is 400 px by default, and the toolbar is about -45 px. While a selection is active, a 45 px bar with the active filters shows -at the bottom. The two stacked panels in the example thus need 890 px. See +`st.iframe` cannot measure the height of the dashboard, so it uses 400 px. A +panel is 400 px by default, and the toolbar is about 45 px. While a selection +is active, a 45 px bar with the active filters shows at the bottom. The two +stacked panels in the example thus need 890 px. See [Who owns width and height](embedding.md#who-owns-width-and-height). +Set the height of the iframe yourself, as `height=900` does in `page.py`. + ## Dash -Dash 4.2 or newer can run on FastAPI. Install it with the FastAPI extra: +Dash 4.2 or newer can run on FastAPI. + +Install Dash with the FastAPI extra: ```bash pip install "dash[fastapi]" ``` -Then mount FlexViz on `app.server`: +Mount FlexViz on `app.server`: ```python # app.py @@ -119,23 +153,28 @@ url = dashboard.share_url(server_url="/flexviz", source_name="readings", cols=1) app = Dash(__name__, backend="fastapi") mount_into(app.server, prefix="/flexviz") app.layout = html.Iframe( - src=url, style={"width": "100%", "height": "460px", "border": 0} + src=url, + title="Sensor readings dashboard", + style={"width": "100%", "height": "460px", "border": 0}, ) if __name__ == "__main__": app.run() ``` -Start it with `python app.py`. The URL is a plain string, so a Dash callback -can return a new one to `html.Iframe.src`. +Start the app with `python app.py`. -On the default Flask backend, or before Dash 4.2, `app.server` is a Flask app. -Apply the [Flask recipe](#flask-and-other-wsgi-apps) to it. +The URL is a plain string, so a Dash callback can return a new one to +`html.Iframe.src`. + +On the default Flask backend, or before Dash 4.2, apply the +[Flask recipe](#flask-and-other-wsgi-apps) to `app.server`. ## Gradio -Gradio mounts into a FastAPI app with `gr.mount_gradio_app`. Mount FlexViz on -the same FastAPI app first: +Gradio mounts into a FastAPI app with `gr.mount_gradio_app`. + +Mount FlexViz on the same FastAPI app, before Gradio: ```python # app.py @@ -154,7 +193,8 @@ url = dashboard.share_url(server_url="/flexviz", source_name="readings", cols=1) with gr.Blocks() as demo: gr.HTML( - f'' + f'' ) app = FastAPI() @@ -175,20 +215,22 @@ if __name__ == "__main__": A web framework that runs on FastAPI or Starlette takes the same mount. For example, the `app` of NiceGUI is a FastAPI app, so -`mount_into(app, prefix="/flexviz")` adds FlexViz to it. Show the URL in the -iframe element of the framework. +`mount_into(app, prefix="/flexviz")` adds FlexViz to it. ### Flask and other WSGI apps A WSGI app, such as Flask, cannot mount FlexViz directly, because FlexViz is an -ASGI app. Install [a2wsgi](https://pypi.org/project/a2wsgi/) to wrap FlexViz: +ASGI app. The [a2wsgi](https://pypi.org/project/a2wsgi/) package wraps FlexViz +as a WSGI app. The example leaves out the source and the `url`, which are the +same as in the Dash example. + +Install a2wsgi: ```bash pip install a2wsgi ``` -Then mount the wrapped app with the dispatcher of Werkzeug. The example leaves -out the source and the `url`. Build them as in the Dash example. +Mount the wrapped app with the dispatcher of Werkzeug: ```python import flexviz @@ -205,7 +247,8 @@ app.wsgi_app = DispatcherMiddleware( @app.get("/") def index(): return ( - f'' + f'' ) ``` @@ -216,26 +259,37 @@ Start the app with `flask --app app run`. - The Python code of the web app cannot read the zoom or the selections in the dashboard. The **Share** button gives a URL with the full view. - The server always queries the registered source. A filter on the frame of - the `Dashboard`, such as `lf.filter(...)`, thus has no effect. To show a - subset, register it as a separate source, and let a widget select the - `source_name`. -- A widget can change the spec: data columns, traces, bins, or layout. To - filter rows while you explore, select in a FlexViz figure. + the `Dashboard`, such as `lf.filter(...)`, thus has no effect. +- A subset of the data needs its own registered source. A widget can then + select it through `source_name`. +- A widget can change the spec: data columns, traces, bins, or layout. A + selection in a FlexViz figure filters the rows of the other figures. - A new URL loads the dashboard again, and the view goes back to its start. ## Security The FlexViz routes have no authentication. Each user who can open the web app -can query the registered sources through `/flexviz`. Before you deploy the web -app, put authentication in front of it. Make sure that the authentication also -covers the `/flexviz` routes. +can query the registered sources through `/flexviz`. The mount also serves +`/flexviz/h/N`, which reads the agent history file `.flexviz/history.jsonl` in +the working folder of the server. See the +[safety notes for agents](ai-agents.md#safety-notes). + +Streamlit listens on all network interfaces by default. The `Host` check of +`show()` and `flexviz serve` does not apply to a mounted app. + +Before you deploy the web app: + +- Put authentication in front of it. Make sure that the authentication also + covers the `/flexviz` routes. +- Do not run it from a folder that holds `.flexviz/history.jsonl`. -Streamlit listens on all network interfaces by default. For local use, start -it with `streamlit run app.py --server.address 127.0.0.1`. +For local use: -The `Host` check of `show()` and `flexviz serve` does not apply to a mounted -app. On a loopback address, wrap FlexViz in Starlette's `TrustedHostMiddleware` -before you mount it: +- Start Streamlit with `streamlit run app.py --server.address 127.0.0.1`. +- Wrap FlexViz in Starlette's `TrustedHostMiddleware`, as the example that + follows shows. Then mount `guarded` in place of `flexviz.app`, for example + with `app.mount("/flexviz", guarded)`. For Flask, wrap `guarded` in + `ASGIMiddleware`. ```python from starlette.middleware.trustedhost import TrustedHostMiddleware @@ -243,7 +297,5 @@ from starlette.middleware.trustedhost import TrustedHostMiddleware guarded = TrustedHostMiddleware(flexviz.app, allowed_hosts=["localhost", "127.0.0.1"]) ``` -Mount `guarded` in place of `flexviz.app`, for example with -`app.mount("/flexviz", guarded)`. For Flask, wrap `guarded` in -`ASGIMiddleware`. FlexViz then refuses a request for another host name, which -blocks DNS rebinding. +The middleware refuses a request for another host name, which blocks DNS +rebinding. From 0f31aacf1767acf0dbae57bca3478e8f349b5593 Mon Sep 17 00:00:00 2001 From: Jeroen Van Der Donckt Date: Sat, 3 Oct 2026 17:03:21 +0200 Subject: [PATCH 4/4] docs: fix the host check example in the web apps guide --- docs/guides/web-apps.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/guides/web-apps.md b/docs/guides/web-apps.md index a3334cfe..a9c798b0 100644 --- a/docs/guides/web-apps.md +++ b/docs/guides/web-apps.md @@ -287,15 +287,23 @@ For local use: - Start Streamlit with `streamlit run app.py --server.address 127.0.0.1`. - Wrap FlexViz in Starlette's `TrustedHostMiddleware`, as the example that - follows shows. Then mount `guarded` in place of `flexviz.app`, for example - with `app.mount("/flexviz", guarded)`. For Flask, wrap `guarded` in - `ASGIMiddleware`. + follows shows. ```python +import flexviz from starlette.middleware.trustedhost import TrustedHostMiddleware guarded = TrustedHostMiddleware(flexviz.app, allowed_hosts=["localhost", "127.0.0.1"]) ``` +Mount `guarded` in place of `flexviz.app`: + +| Web framework | Mount | +| ------------- | ---------------------------------------------------------- | +| Streamlit | `Mount("/flexviz", app=guarded)` in the routes of `st.App` | +| Dash | `app.server.mount("/flexviz", guarded)` | +| Gradio | `app.mount("/flexviz", guarded)`, before Gradio | +| Flask | `ASGIMiddleware(guarded)` in the `DispatcherMiddleware` | + The middleware refuses a request for another host name, which blocks DNS rebinding.