A high-performance Reactive DAG framework for Python — reactive data apps that actually ship.
Project description
Golit
Reactive data apps that actually ship.
A high-performance reactive Directed Acyclic Graph (DAG) framework for Python. Golit maps your data dependencies once, then on every interaction recomputes only the nodes that changed — not your whole script.
Documentation · Changelog · Examples · Benchmarks · Architecture · Deployment
Golit pairs the authoring speed of a notebook-style data app with the runtime discipline of a compiled reactive graph. Your dashboard is a set of plain Python functions; Golit infers the dependency graph between them, and a Rust kernel ensures a user interaction re-runs the minimal subgraph it touched and ships only the affected HTML fragments over the wire. The guiding thesis: update cost is proportional to the change, not the size of the program.
Features
- Rust reactive kernel (PyO3) — dirty tracking, topological scheduling, memoized propagation.
- Polars data held Python-side; only node ids/hashes cross the FFI boundary.
- SQL nodes — reactive nodes written as in-process DuckDB SQL over Polars frames (
golit.sql). - Litestar orchestration with HTMX server-rendered fragment transport — no client framework.
- Charts — Lets-Plot static SVG, plus interactive Plotly / Altair / Bokeh / AnyChart.
- Tables — styled tables from Polars, or return a Great Tables
GTobject for a polished, auto-rendered display table. - Maps — native MapLibre GL: GeoDataFrame vector, rioxarray/xarray raster, and DuckDB spatial SQL (
golit.gis). - Components — reactive input widgets plus a shadcn-styled
golit.uilibrary, server-rendered with Tailwind. - Realtime — SSE push with pluggable pub/sub (in-memory or Redis), WebSocket chat, video (server-side MJPEG + browser-camera CV), and audio (mic recorder).
- Live sources —
@app.pollstreams external data that changes on its own (a Google Sheet, an API); only real changes re-render and hit the wire. - Scales horizontally — N single-worker instances behind a sticky load balancer with Redis fan-out.
Installation
Golit ships prebuilt wheels for Python 3.11+ (the abi3 stable ABI):
pip install golit # core
pip install "golit[charts]" # interactive Plotly / Altair / Bokeh
pip install "golit[sql]" # DuckDB SQL nodes over Polars frames
pip install "golit[gis]" # native MapLibre maps from GeoDataFrames
pip install "golit[gis-raster]" # raster maps from rioxarray/xarray arrays
pip install "golit[tables]" # Great Tables display tables (auto-rendered from a view)
pip install "golit[vision]" # webcam / MJPEG video streams (Pillow)
pip install "golit[vision-cv]" # + OpenCV for real CV models (face detection)
pip install "golit[redis]" # Redis fan-out for multi-worker
Quickstart
make dev # uv venv (3.11) + deps + build the Rust kernel (maturin)
make test # cargo test + pytest
make run # golit run examples/sales_explorer/app.py
Then open http://127.0.0.1:8000.
Programming model
Nodes are plain Python functions. Dependencies are inferred from parameters: a parameter named after another node is an edge; a parameter defaulting to a widget is an input.
import polars as pl
from golit import App, create_app, slider, upload
from golit.charts import aes, geom_bar, ggplot, ggsize
app = App(title="Sales Explorer")
SAMPLE = pl.DataFrame({"region": ["North", "South", "East", "West"], "revenue": [120, 90, 70, 40]})
@app.source
def data(file=upload("Upload CSV")) -> pl.DataFrame:
return SAMPLE if file is None else pl.read_csv(file) # renders before any upload
@app.reactive
def filtered(data: pl.DataFrame, threshold: int = slider(0, 100, default=20)) -> pl.DataFrame:
return data.filter(pl.col("revenue") > threshold) # re-runs only when data/threshold change
@app.view
def chart(filtered: pl.DataFrame):
return ggplot(filtered, aes("region", "revenue")) + geom_bar(stat="identity") + ggsize(640, 360)
application = create_app(app) # an ASGI app; `golit run app.py` serves it
Moving the slider dirties threshold → filtered → chart. The data node is never touched; only
the chart fragment is re-rendered and swapped. A view that depends only on data (e.g. a dataset
overview) is not re-rendered on a slider move — that selective recompute is the whole point. See
examples/sales_explorer/app.py.
How a change flows
- POST in — a committed input (
hx-post) runs the dirty subgraph; the response carries only the affected view fragments as out-of-band HTMX swaps. - SSE out — nodes dirtied server-side (streaming sources, background jobs, shared nodes) are
pushed over
/eventsas namednode:<id>events. - Memoization — a node re-executes only when its inputs hash differently; an unchanged output cascades into memo hits downstream (nothing on the wire).
Performance
The thesis is update cost is proportional to the change, not the program — and the benchmark harness measures it (dev laptop, loopback; reproducible, not yet the published cloud figure). The honest summary:
- A single filter → chart updates in ~2 ms over HTTP — the same as Dash. Idiomatic Dash is a hand-wired reactive DAG, so on one chain both do identical work and tie. Saying otherwise would be a strawman.
- The gap opens on the shape real dashboards have: shared upstream work. When one expensive step (a load, a join, a sort) feeds several views, moving a control that touches only one view makes Golit re-run only that view — the shared upstream is memoized and executes zero times — while Dash recomputes it every callback. Over real HTTP that's ~1.6× faster at 100K rows, ~5.5× at 1M, ~8.3× at 2M, and the lead widens with the app.
chart_specskips the figure object. Returning a raw spec dict instead of ago.Figurecuts the per-update round-trip to ~1.5 ms and ~635 B (vs ~6.9 KB) — ~1.4× faster than figure-returning Dash with a ~10× smaller payload, same chart.
See bench/README.md for the methodology and one-command repros.
SQL nodes
A reactive node can be written as SQL instead of Polars. golit.sql(query, **frames) runs DuckDB
in-process over the named upstream frames and returns Polars, so the node memoizes and renders like
any other — inputs feed straight into the query.
from golit import sql
@app.reactive
def by_region(data: pl.DataFrame, threshold: int = slider(0, 200, default=40)):
return sql(
"SELECT region, sum(revenue)::BIGINT AS revenue "
f"FROM d WHERE revenue > {int(threshold)} GROUP BY region ORDER BY region",
d=data,
)
DuckDB exchanges data with Polars zero-copy. A raw duckdb.sql(...) relation returned from a node is
auto-detected and materialized too. Optional dependency: pip install "golit[sql]"; it is imported
only inside sql(), never at framework import time. See
examples/duckdb_sql/app.py.
Charts
Lets-Plot renders to static SVG (no client runtime). For interactivity, return a Plotly, Altair,
or Bokeh figure — Golit auto-detects it and renders a client-side chart that hydrates on the
initial load and across POST/SSE swaps. AnyChart (no Python package) is available via
anychart().
@app.view
def chart(by_region):
import plotly.express as px
return px.bar(by_region, x="region", y="revenue") # Polars frame in directly
For a view that rebuilds its chart on every interaction, chart_spec(lib, dict) hands Golit the raw
wire-format spec directly, skipping the figure-object build and to_json (see
Performance). See examples/charts_gallery/app.py.
Maps
A map is a reactive view like any other — a control rebuilds it. Return a GeoPandas
GeoDataFrame and Golit renders a native MapLibre GL map; golit.gis.geo_map adds
choropleths, tooltips, and basemaps, and golit.gis.spatial_sql runs DuckDB ST_* queries that feed
it. No client map framework — the server ships the GeoJSON and the style rules; the GPU draws. For
large vector data, golit.gis.vector_tiles keeps the GeoDataFrame server-side and streams MVT
vector tiles (pip install "golit[gis-vector-tiles]") so 100k+ features render without inlining
the whole GeoJSON.
import golit.gis as gis
@app.view
def map(regions): # regions is a filtered GeoDataFrame
return gis.geo_map(regions, color="revenue", tooltip=["name", "revenue"])
Raster works too: gis.raster(dataarray) colormaps a georeferenced rioxarray/xarray array (or
GeoTIFF) to a MapLibre image layer, gis.rgb(stack, bands=…) renders a multiband raster as a
true/false-color satellite composite (pip install "golit[gis-raster]"), and
gis.tiles("scene.tif") streams a very large COG as on-demand z/x/y tiles via rio-tiler
(pip install "golit[gis-tiles]") — only the visible window crosses the wire. And
gis.terrain(dem, "hillshade") runs WhiteboxTools terrain analysis (slope, flow accumulation, …)
into a renderable raster (pip install "golit[gis-terrain]"), and gis.ee_layer(image, vis=…)
overlays Google Earth Engine imagery as live tiles (pip install "golit[gis-ee]"). Install
vector with pip install "golit[gis]" (DuckDB spatial rides on the sql extra). Moving a control
re-runs only the filter + map node — the fragment swaps in place on the initial load and after a
POST/SSE. See
examples/geo_explorer/app.py (vector),
examples/vector_tiles/app.py (60k-feature vector tiles),
examples/raster_explorer/app.py (raster),
examples/rgb_composite/app.py (RGB composite),
examples/tiled_raster/app.py (tiled COG),
examples/terrain_analysis/app.py (terrain), and
examples/earth_engine/app.py (Earth Engine).
Realtime: video and audio
Some views don't re-render on a change — they hold a live connection. golit.ui.chat(channel)
opens a WebSocket-backed chat panel (@app.on_message adds bot/moderation logic). For computer
vision, @app.stream(name) + ui.webcam(name) push a server-side MJPEG feed the browser plays
in a plain <img> — a host camera, a detector drawing boxes, a synthetic animation — and
shared=True fans one producer out to many viewers. The mirror, @app.on_frame(name) +
ui.camera(name), streams the visitor's own webcam up over a WebSocket, runs your handler on each
frame server-side, and paints the annotated result back.
import numpy as np
import golit.ui as ui
@app.on_frame("faces")
def detect(frame: np.ndarray) -> np.ndarray: # (H, W, 3) uint8 RGB in and out
... # run your model, draw boxes
return frame
@app.view
def live() -> str:
return ui.camera("faces", title="Your camera")
Frames are JPEG bytes or (H, W, 3) RGB arrays (encoded with Pillow). Sync handlers run in a
worker thread; one frame is in flight at a time, so a slow model lowers the rate instead of backing
up, and a producer/handler that errors is logged without dropping the stream. pip install "golit[vision]" (or [vision-cv] for OpenCV). See
examples/webcam_stream (server feed),
examples/browser_camera (browser camera), and
examples/face_detect (real OpenCV face detection).
For audio, ui.recorder(name) captures the visitor's mic and uploads each clip as 16-bit WAV
(with inline playback + a download link for the clip); the @app.on_audio(name) handler decodes it
(Python's stdlib wave — no ffmpeg) and returns a result to show, or audio to play back. See
examples/audio_recorder and
Audio recording.
Components
Inputs (reactive) — slider, number, select, text, checkbox, upload, radio,
multiselect, switch, date, textarea, button.
Display (golit.ui) — card, columns, grid, tabs, expander, accordion, divider,
metric, scorecard, alert, badge, progress, skeleton, spinner, table, markdown,
code, json_view, heading, caption.
Realtime (golit.ui) — chat, webcam, camera, recorder.
import golit.ui as ui
@app.view
def panel(by_region, total):
return ui.card(
ui.columns([ui.metric("Revenue", f"${total:,}", delta="+8%"), chart_fig]),
title="Overview",
)
Components compose through the renderer, so any argument can be a DataFrame, a chart figure, another
component, or trusted HTML. See
examples/components_gallery/app.py.
Page layout
By default views stack under one controls panel. golit.layout arranges the reactive view fragments
into a sidebar, rows, tabs, etc. — the layout is static scaffold, so each view keeps its id and
still swaps in place on POST/SSE.
from golit import layout as L
app.layout = L.Sidebar(
L.Controls(), # all inputs, in the sidebar
L.Stack(
L.Row(L.View("kpi"), L.View("status")),
L.Tabs({"Chart": L.View("chart"), "Data": L.View("table")}),
),
)
References are validated at build time: every View/Control must resolve to a real view/input and
be placed at most once.
Deploying and scaling
A single process needs nothing extra. To scale horizontally, set GOLIT_REDIS_URL and run N
single-worker instances behind a sticky (session-cookie) load balancer — session state is
worker-local by design, and Redis fans server-side invalidations across the fleet. A runnable
podman/nginx stack lives in deploy/.
pip install "golit[redis]"
export GOLIT_REDIS_URL=redis://localhost:6379
golit run examples/sales_explorer/app.py
See DEPLOYMENT.md for the full topology and why uvicorn --workers can't provide
session affinity.
Architecture
| Tier | Role | Technology |
|---|---|---|
| 0 | Reactive kernel | Rust + PyO3 (src/, golit._golit) |
| 1 | Orchestrator | Litestar + SSE/Redis fan-out (golit.server) |
| 2 | Transport | HTMX fragments; static SVG, interactive charts, native maps (golit.rendering) |
| 3 | Local shield | Alpine.js (widget immediacy, tab state) |
See project_scope.md for the full architecture and
golit_benchmark.md for the benchmark methodology.
Status
v1.0.0 — first stable release (see the CHANGELOG).
Built end-to-end and green (17 cargo + 209 pytest, ruff + mypy clean): Rust kernel, reactive
engine, rendering (static and interactive charts, native MapLibre maps, auto-rendered Great
Tables), the golit.ui component library, page layout, DuckDB SQL nodes, GIS (vector maps + MVT
vector tiles for large data; single-band, RGB-composite, tiled-COG raster maps; WhiteboxTools
terrain; Earth Engine overlays; spatial SQL — golit.gis), Litestar server (POST + SSE), Redis
pub/sub fan-out, multi-worker deployment, live polled sources (@app.poll), realtime WebSocket chat,
video (server-side MJPEG streams + browser-camera CV), and audio (mic recorder), the benchmark
harness (bench/, with measured Golit-vs-Dash results), and the examples.
Deferred: a standard-cloud-instance benchmark publication and the wider design suite in
golit_pages/.
Development
make dev # set up venv + build extension
make test # cargo test + pytest
make lint # ruff + mypy
make build # release wheel
License
Golit is released under the Apache License 2.0. © 2026 Daniel Boadzie.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file golit-1.1.0.tar.gz.
File metadata
- Download URL: golit-1.1.0.tar.gz
- Upload date:
- Size: 4.2 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
712596f6bad26a79248fdb2c895028ab7d2866603f09428c54b2b8a0de65f68e
|
|
| MD5 |
88682ec66e376fe667fb8261cbba01d3
|
|
| BLAKE2b-256 |
c3fe99301b7fa5dd33de61266b4e39d4553a4890b9c2b9b971a0423e29dbe872
|
Provenance
The following attestation bundles were made for golit-1.1.0.tar.gz:
Publisher:
release.yml on Boadzie/golit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
golit-1.1.0.tar.gz -
Subject digest:
712596f6bad26a79248fdb2c895028ab7d2866603f09428c54b2b8a0de65f68e - Sigstore transparency entry: 1819643342
- Sigstore integration time:
-
Permalink:
Boadzie/golit@00182d9d3b2da148f26aec604afbd129bd916b0b -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/Boadzie
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@00182d9d3b2da148f26aec604afbd129bd916b0b -
Trigger Event:
push
-
Statement type:
File details
Details for the file golit-1.1.0-cp311-abi3-win_amd64.whl.
File metadata
- Download URL: golit-1.1.0-cp311-abi3-win_amd64.whl
- Upload date:
- Size: 274.3 kB
- Tags: CPython 3.11+, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9eab4ffe5c659380b3aa9f8e4999d8a0fdcd2655adf8bfdf0e9158705be4e158
|
|
| MD5 |
1dddc480a0188bbc3c4e6a3afc5fc5c4
|
|
| BLAKE2b-256 |
3d48dc31cb9d788712adcd06dec10da6eb62c5796b309cdad10f84012c1c608d
|
Provenance
The following attestation bundles were made for golit-1.1.0-cp311-abi3-win_amd64.whl:
Publisher:
release.yml on Boadzie/golit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
golit-1.1.0-cp311-abi3-win_amd64.whl -
Subject digest:
9eab4ffe5c659380b3aa9f8e4999d8a0fdcd2655adf8bfdf0e9158705be4e158 - Sigstore transparency entry: 1819643362
- Sigstore integration time:
-
Permalink:
Boadzie/golit@00182d9d3b2da148f26aec604afbd129bd916b0b -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/Boadzie
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@00182d9d3b2da148f26aec604afbd129bd916b0b -
Trigger Event:
push
-
Statement type:
File details
Details for the file golit-1.1.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: golit-1.1.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 399.4 kB
- Tags: CPython 3.11+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bb97800753ce9d832d613741c24ea25ba402c81e70707f70f554a3f0a6238d43
|
|
| MD5 |
a2d125f858c594f58cacd29507b3918d
|
|
| BLAKE2b-256 |
92d7ddbc5d8c4d0a565ceb92f30a914f83299497a165e6bf4d44fd35630ff09c
|
Provenance
The following attestation bundles were made for golit-1.1.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
release.yml on Boadzie/golit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
golit-1.1.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
bb97800753ce9d832d613741c24ea25ba402c81e70707f70f554a3f0a6238d43 - Sigstore transparency entry: 1819643355
- Sigstore integration time:
-
Permalink:
Boadzie/golit@00182d9d3b2da148f26aec604afbd129bd916b0b -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/Boadzie
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@00182d9d3b2da148f26aec604afbd129bd916b0b -
Trigger Event:
push
-
Statement type:
File details
Details for the file golit-1.1.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: golit-1.1.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 399.9 kB
- Tags: CPython 3.11+, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b3ba786e9554bdbb935f7510731c429314b082a29cd9a868bff5b5e116b87dda
|
|
| MD5 |
4c8f85628209b3c895b901ecc721603e
|
|
| BLAKE2b-256 |
465e1d8e0f4b5dfe884041bb0308d120d8523127e07ef2310f9b4db383320b5f
|
Provenance
The following attestation bundles were made for golit-1.1.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:
Publisher:
release.yml on Boadzie/golit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
golit-1.1.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl -
Subject digest:
b3ba786e9554bdbb935f7510731c429314b082a29cd9a868bff5b5e116b87dda - Sigstore transparency entry: 1819643347
- Sigstore integration time:
-
Permalink:
Boadzie/golit@00182d9d3b2da148f26aec604afbd129bd916b0b -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/Boadzie
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@00182d9d3b2da148f26aec604afbd129bd916b0b -
Trigger Event:
push
-
Statement type:
File details
Details for the file golit-1.1.0-cp311-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: golit-1.1.0-cp311-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 370.0 kB
- Tags: CPython 3.11+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f79768be815e943a81ca1cfd58904725d94184e3affc6857c4098d9092d78e4a
|
|
| MD5 |
aaa0806750c7681b61b3bfcb41762def
|
|
| BLAKE2b-256 |
c2486e032e12e4db1f74273362d94710419dcb3bc090e4ebb21fd8606c2ab88e
|
Provenance
The following attestation bundles were made for golit-1.1.0-cp311-abi3-macosx_11_0_arm64.whl:
Publisher:
release.yml on Boadzie/golit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
golit-1.1.0-cp311-abi3-macosx_11_0_arm64.whl -
Subject digest:
f79768be815e943a81ca1cfd58904725d94184e3affc6857c4098d9092d78e4a - Sigstore transparency entry: 1819643366
- Sigstore integration time:
-
Permalink:
Boadzie/golit@00182d9d3b2da148f26aec604afbd129bd916b0b -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/Boadzie
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@00182d9d3b2da148f26aec604afbd129bd916b0b -
Trigger Event:
push
-
Statement type: