facett-demo (was facett-demo-python) — the Streamlit companion to facett-demo
0.2.0 (2026-10-02): a PyPI package.
pip install facett-demo, commandfacett-demo(Streamlit started in-process viastreamlit.web.cli, 127.0.0.1:8501), importfacett_demo(wasfacett_demo_python, run from source). It depends onfacett[streamlit]— the components with their Streamlit glue, which moved from this app'sfacet_host.pyintofacett.streamlit(the app keeps only its dev-mode toggle there). The korp gRPC wire (grpcio+protobuf) is the optional[korp]extra: without itclient.korp_available()is false, the sidebar says "offline mode", and each korp tab mounts the same components on the gallery's payloads under a "NOT live data" banner (panes/offline_pane.py). The version is read from the installed metadata; the git hash is baked into_buildinfo.pybycargo ops publish-pip(Rust, into the staged package —scripts/stamp.pyis gone). The container runs the INSTALLED wheel, never the source tree (cargo ops streamlit,cargo ops test-demo: the tests locate the app throughfacett_demo.__file__). Paths below that sayfacett_demo_python/are nowfacett_demo/; publishing:.nornir/pypi-publish.md.
facett-demo-python is the Python track of facett-demo: a Streamlit app that mirrors facett-demo's three core surfaces using the Python data-science stack, driven by LIVE data from the korp-server gRPC control plane. It is a documented exception to the suite's Rust-100% law — demo/companion tooling for the Python/data-science ecosystem, NOT a replacement for the Rust facett framework.
Superseded in part. The map and graph panes originally re-implemented facett surfaces in pydeck / networkx+plotly. They now embed the REAL components as wasm through
facet-wrapped-web. Seefacet-wrapped-wheel.mdfor the analysis and the data seam; this file documents the demo's placement and its korp data plumbing.
Placement (why facett/py/)
It lives at facett/py/ — a pip-installable package (facett-demo-python, module facett_demo_python) beside the Rust crates, so the Python demo sits next to the framework it mirrors without polluting the Cargo workspace. It is NOT a workspace crate and is not built by cargo.
facett/py/
pyproject.toml # pip-installable; console script `facett-demo-python`
requirements.txt # pinned, verified on Python 3.14 (oden)
README.md
scripts/stamp.py # bakes git-hash+build-time -> _buildinfo.py at build
facett_demo_python/
__init__.py # exports version/build_info
buildinfo.py # version+git-hash (baked -> live-git -> "unknown")
client.py # ONE gRPC seam over korp-server; plain-dataclass returns
theme.py # palette + OSM feature-class classification
app.py # Streamlit entrypoint (sidebar + 3 tabs); `streamlit run`
cli.py # `--version` / `about` / `run`
proto/ # generated korp_pb2 / korp_pb2_grpc (+ korp.proto copy)
panes/
map_pane.py # 🗺 Sweden map (pydeck / deck.gl)
graph_pane.py # 🕸 Graph (networkx + plotly)
population_pane.py # 📊 Iceberg browser + Population diagrams
tests/test_app.py # headless robot-UI (Streamlit AppTest)
The existing sibling crates facett-py (PyO3 native bindings) and facet-wrapped (the wasm bundle) / facet-wrapped-web (the wheel) (a WASM Streamlit component) are unrelated: this is a standalone Streamlit application, not a component or a native binding.
Data source — LIVE korp-server, one RPC per pane
The control plane is korp-server gRPC at :50051 (override with $KORP_SERVER). Stubs are generated from korp/proto/korp.proto with grpc_tools.protoc (the grpc stub's top-level import korp_pb2 is rewritten to from . import korp_pb2 so it imports as a package submodule). Every pane reads through client.KorpClient, which returns plain dataclasses (no protobuf leaks into the UI) and raises KorpUnavailable carrying the server's own error string.
| Pane | Viz library | korp-server RPC(s) | Notes |
| 🗺 Map | pydeck PathLayer + Scatterplot + Text |
Assets → region roster; MapQuery → viewport geometry |
Ways coloured by OSM feature class (theme.classify_way); place-name nodes labelled. Server prunes GeoParquet row-groups — ZERO local files, ZERO S3 on the client. |
| 🕸 Graph | networkx spring layout + plotly |
Graph (Cypher; blank graph ⇒ server default) |
Node colour encodes node label (fixed categorical order). Fat node returns a seed scene when no live FalkorDB is attached. |
| 📊 Population | pandas + plotly bars (the ONLY pane allowed a Python renderer — it duplicates no facett component) |
IcebergTables (catalog+schema+stats); TablePreview (schema + capped, server-formatted rows); Population (aggregate tables + chart series) |
Bars encode magnitude → single sequential hue. All 3 RPCs over the bound skade/demo warehouse. |
| sidebar | — | Health |
Shows the connected server's version (hash) + uptime, so it is always obvious WHICH build backs the demo. |
Honest states (LAW #6 — no blank false-green)
Each pane either shows live data or a loud panel: st.error for an unreachable server / failed render (carrying the server's error string), st.warning for an honest empty result, st.info for empty sub-sections. The sidebar shows connected (green) or server unreachable (red) from the Health probe. There is no code path that renders an empty-but-green pane.
Known live caveat on oden: MinIO/S3 (:9000) may be offline, so Assets returns its listing error — but MapQuery still renders from the local sweden GeoParquet (~/.korp/work/sweden), so the map is populated regardless. Both facts are surfaced (the S3 warning + the row-group provenance in the status line).
Version + git-hash (build-version-githash LAW)
buildinfo.build_info() resolves the hash three tiers deep: a baked _buildinfo.py (written by scripts/stamp.py at build/wheel time) → a live git rev-parse --short HEAD of the checkout → the honest literal "unknown". Shown in the sidebar ("App build") AND answered by facett-demo-python --version / about. _buildinfo.py is git-ignored so dev checkouts always show the live hash; wheels carry the baked stamp.
Headless robot-UI test (LAW)
tests/test_app.py drives the app with streamlit.testing.v1.AppTest (no browser). It auto-detects whether korp-server is reachable and asserts the matching contract, so it is green both on oden (server up) and in a bare CI:
- app runs without exception; title + sidebar build stamp present;
- all three pane headers render (tabs execute all children in one pass);
- server up: sidebar shows
connected+ the serverversion; Map/Graph/ Population metrics are present (they only appear when live data arrived), and at least one dataframe (the Iceberg catalog) is non-empty — never a blank green; - bad target: pointing the sidebar server box at an unreachable target reruns to an HONEST error panel, not a crash and not a silent blank.
AppTest result on oden (server up, korp 0.1.0): 6 passed.
Run it
cd facett/py && python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt && pip install -e .
streamlit run facett_demo_python/app.py --server.port 8501 --server.address 127.0.0.1
On oden it is deployed as a systemd --user transient unit (facett-demo-python.service, port 8501, linger on) so it survives the shell that launched it — mirroring the oden rootless-podman/linger infra pattern.
Grouped like facett-demo, with the demo's size + generator (lane F, 2026-10-02)
Owner order 2026-10-02: what facett-demo got on 2026-10-01 (grouped tabs, the size + generator control, the Universe tab, decograph/graphpan on the resident GPU lane) must also be in this Streamlit demo.
- Top tabs = facett-demo's tab groups.
gallery_pane.GROUPSmaps each registry namespace to the demo's group (facett-demo/src/lib.rsTAB_GROUPS): graph → 🕸 Grafer, map → 🗺 Kartor, data → 📊 Tabeller & diagram (the registry'sdatanamespace holds both the demo's Diagram and Tabeller members), ops → ⚙ Bygg & jobb, ui → 🎛 App & UI, anything else → Övrigt. Each group opens on 🎨 Komponenter (the gallery's wasm components of that group) and carries its live korp pane beside it: Graph under Grafer, Map under Kartor, Population under Tabeller & diagram, Errors under App & UI (where the demo keepsErrors). The gallery heading and its mode switch sit above the tabs, once. - Size + generator next to every generated component. A manifest entry with
generate(graphnav,graph2d,graph3d,graphpan,graphview,lineage, and the newuniverseanddecograph) gets a picker column: data source, size, seed — the sources and sizes read off the SHIPPED manifest (generate.sources,generate.sizes,generate.max), so the picker cannot offer what the component will not build. The pick goes to the wasm as a generator REQUEST,{"generate": {"source": "sbm", "size": 100000, "seed": 1}}; the graph is generated inside the wasm byfacett-graphgen(the demo's own generators and lowerings, moved to a library for this), so 100 000 nodes never cross from Python.?gen=sbm:100000:1preselects it (the demo's deep-link shape),?pick=still picks the component. - Above 5 000 nodes the component's
facet_stateis its summary (counts + lane readouts), exactly as on the demo's pages bundle. - Tests:
py/tests/test_gallery_pane.py(the wheel ships the seam with the right ceilings;?gen=mounts the request; the picker drives it; the tabs are the demo's groups; the link parser);facet-wrapped/src/generated.rsandfacett-graphgen/src/lib.rs(Rust: every generated entry builds from a request, bad requests are refused by name, sizes clamp, the summary above 5 000, and the seam builds the same model as the demo tab for the same request).