Skip to main content

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.

version python license tests built with changelog

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 GT object 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.ui library, server-rendered with Tailwind.
  • RealtimeSSE 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.poll streams 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

  1. 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.
  2. SSE out — nodes dirtied server-side (streaming sources, background jobs, shared nodes) are pushed over /events as named node:<id> events.
  3. 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_spec skips the figure object. Returning a raw spec dict instead of a go.Figure cuts 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

golit-1.1.0.tar.gz (4.2 MB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

golit-1.1.0-cp311-abi3-win_amd64.whl (274.3 kB view details)

Uploaded CPython 3.11+Windows x86-64

golit-1.1.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (399.4 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.17+ x86-64

golit-1.1.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (399.9 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.17+ ARM64

golit-1.1.0-cp311-abi3-macosx_11_0_arm64.whl (370.0 kB view details)

Uploaded CPython 3.11+macOS 11.0+ ARM64

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

Hashes for golit-1.1.0.tar.gz
Algorithm Hash digest
SHA256 712596f6bad26a79248fdb2c895028ab7d2866603f09428c54b2b8a0de65f68e
MD5 88682ec66e376fe667fb8261cbba01d3
BLAKE2b-256 c3fe99301b7fa5dd33de61266b4e39d4553a4890b9c2b9b971a0423e29dbe872

See more details on using hashes here.

Provenance

The following attestation bundles were made for golit-1.1.0.tar.gz:

Publisher: release.yml on Boadzie/golit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for golit-1.1.0-cp311-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 9eab4ffe5c659380b3aa9f8e4999d8a0fdcd2655adf8bfdf0e9158705be4e158
MD5 1dddc480a0188bbc3c4e6a3afc5fc5c4
BLAKE2b-256 3d48dc31cb9d788712adcd06dec10da6eb62c5796b309cdad10f84012c1c608d

See more details on using hashes here.

Provenance

The following attestation bundles were made for golit-1.1.0-cp311-abi3-win_amd64.whl:

Publisher: release.yml on Boadzie/golit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file golit-1.1.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for golit-1.1.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 bb97800753ce9d832d613741c24ea25ba402c81e70707f70f554a3f0a6238d43
MD5 a2d125f858c594f58cacd29507b3918d
BLAKE2b-256 92d7ddbc5d8c4d0a565ceb92f30a914f83299497a165e6bf4d44fd35630ff09c

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file golit-1.1.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for golit-1.1.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 b3ba786e9554bdbb935f7510731c429314b082a29cd9a868bff5b5e116b87dda
MD5 4c8f85628209b3c895b901ecc721603e
BLAKE2b-256 465e1d8e0f4b5dfe884041bb0308d120d8523127e07ef2310f9b4db383320b5f

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for golit-1.1.0-cp311-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 f79768be815e943a81ca1cfd58904725d94184e3affc6857c4098d9092d78e4a
MD5 aaa0806750c7681b61b3bfcb41762def
BLAKE2b-256 c2486e032e12e4db1f74273362d94710419dcb3bc090e4ebb21fd8606c2ab88e

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page