Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

.nornir/facett-demo-python-design.md

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, command facett-demo (Streamlit started in-process via streamlit.web.cli, 127.0.0.1:8501), import facett_demo (was facett_demo_python, run from source). It depends on facett[streamlit] — the components with their Streamlit glue, which moved from this app's facet_host.py into facett.streamlit (the app keeps only its dev-mode toggle there). The korp gRPC wire (grpcio + protobuf) is the optional [korp] extra: without it client.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.py by cargo ops publish-pip (Rust, into the staged package — scripts/stamp.py is gone). The container runs the INSTALLED wheel, never the source tree (cargo ops streamlit, cargo ops test-demo: the tests locate the app through facett_demo.__file__). Paths below that say facett_demo_python/ are now facett_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. See facet-wrapped-wheel.md for 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:

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.