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.1.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.1-cp311-abi3-win_amd64.whl (260.2 kB view details)

Uploaded CPython 3.11+Windows x86-64

golit-1.0.1-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (385.0 kB view details)

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

golit-1.0.1-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (385.4 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.17+ ARM64

golit-1.0.1-cp311-abi3-macosx_11_0_arm64.whl (355.6 kB view details)

Uploaded CPython 3.11+macOS 11.0+ ARM64

File details

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

File metadata

  • Download URL: golit-1.0.1.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.1.tar.gz
Algorithm Hash digest
SHA256 34b696d26627f7ee171e2d26caee132f45dfeae13769f70fae713a2413c033d7
MD5 6a8b8e76a75135486674dfe282210454
BLAKE2b-256 9f389dada581f3080abaf10280f207ba021de691fcf88cd5098079c2c8589bc5

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: golit-1.0.1-cp311-abi3-win_amd64.whl
  • Upload date:
  • Size: 260.2 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.1-cp311-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 fa04f13a889b6b07b418d85464e2625fed21dc483b06b4898adc98b2931cfed7
MD5 e06f5c973cf3c5732e3196f9182500b0
BLAKE2b-256 ebde9cfac1152be079acde61a9a5fb6623b2108c3a84e107e5b98232e1960a91

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for golit-1.0.1-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 f032ac957655a6743377c523c0e293d38f2ce6fefb586278398e1c8848942187
MD5 643227749bf96942ff7814a97e0c6482
BLAKE2b-256 5a47fe5df442a89bba0b06af905bc40c762fa7b4661eeb9229cc9ee82533297e

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for golit-1.0.1-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 e773ef1a12af5a2f7ab8756e8513932946994338b244defca8b0b3e118cb7bb5
MD5 603916044772dead8d3fbf88fcab13ef
BLAKE2b-256 218ff47f90fb0b382da1e4fd6d44aa3179d5f9daa31923ae3dd1660319859c29

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: golit-1.0.1-cp311-abi3-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 355.6 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.1-cp311-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 1fd91bd0daaa6c378c25ae645940653e980ed1b51b2f9754bf11a1aceee09759
MD5 cc8c02910c4d6830b19a0008e08885be
BLAKE2b-256 bd3426f8e509767e6e813535fdd8cfaf86d9cd4999650a28c01d6067612187b6

See more details on using hashes here.

Provenance

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