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")

@app.source
def data(file=upload("Upload CSV")) -> pl.DataFrame:
    return pl.read_csv(file)

@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.0.2.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.0.2-cp311-abi3-win_amd64.whl (260.1 kB view details)

Uploaded CPython 3.11+Windows x86-64

golit-1.0.2-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (385.2 kB view details)

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

golit-1.0.2-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (385.7 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.17+ ARM64

golit-1.0.2-cp311-abi3-macosx_11_0_arm64.whl (355.9 kB view details)

Uploaded CPython 3.11+macOS 11.0+ ARM64

File details

Details for the file golit-1.0.2.tar.gz.

File metadata

  • Download URL: golit-1.0.2.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.0.2.tar.gz
Algorithm Hash digest
SHA256 cf93fc98fb36b3f52b128393bb86dcbde21cc4df18297b0806528f30573e5714
MD5 3277a6e4e2ebec7f132176852a7af927
BLAKE2b-256 88d740f437518582f24393d28bde7b6c4031414b74bd5b6a24c8429fa2f0e126

See more details on using hashes here.

Provenance

The following attestation bundles were made for golit-1.0.2.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.0.2-cp311-abi3-win_amd64.whl.

File metadata

  • Download URL: golit-1.0.2-cp311-abi3-win_amd64.whl
  • Upload date:
  • Size: 260.1 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.0.2-cp311-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 adb32125b3c90b4be335decbedf3e6ebb303297dad27ff7326d157ab798fbae1
MD5 33ec898a0ca67d10aec7b1343bcfb88d
BLAKE2b-256 57314c06074a641b1ea83c7065f18020dba1152cc1955ac2fd648887b6a1d49e

See more details on using hashes here.

Provenance

The following attestation bundles were made for golit-1.0.2-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.0.2-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for golit-1.0.2-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 73ca057db867126ff65ff691b7957a6267444bd16b2a4beda249a715290068fc
MD5 26c2d6aafb70d480935aeff987230d12
BLAKE2b-256 fdad64cbc3415a0d13656db42e0756397ab860e661b134829c217fd312d7111a

See more details on using hashes here.

Provenance

The following attestation bundles were made for golit-1.0.2-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.0.2-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for golit-1.0.2-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 ce72f273ffdbc0149dd7d259ffd52be38669a6de88c3a92cde5ee5c4bf193a89
MD5 fcc4b6cdccebc78b9ff19f15568b60d9
BLAKE2b-256 555f05dd414f58ed72425de027f291b933bd275d6a1aa5f6190c59317c5a481e

See more details on using hashes here.

Provenance

The following attestation bundles were made for golit-1.0.2-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.0.2-cp311-abi3-macosx_11_0_arm64.whl.

File metadata

  • Download URL: golit-1.0.2-cp311-abi3-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 355.9 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.0.2-cp311-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 59badf5d759d865886ef86555aa8f8a01dfcb5c436a047ebda38ce2ea326af0b
MD5 a93ec488f93078e167f76e79562a2cef
BLAKE2b-256 8a775e3040299bbd96685898b173bbfbc2e95689b8dcce96ed1bf0b761218779

See more details on using hashes here.

Provenance

The following attestation bundles were made for golit-1.0.2-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