golit 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- golit-1.0.0/.dockerignore +15 -0
- golit-1.0.0/.github/workflows/ci.yml +58 -0
- golit-1.0.0/.github/workflows/release.yml +82 -0
- golit-1.0.0/.gitignore +23 -0
- golit-1.0.0/.python-version +1 -0
- golit-1.0.0/CHANGELOG.md +75 -0
- golit-1.0.0/Cargo.lock +212 -0
- golit-1.0.0/Cargo.toml +24 -0
- golit-1.0.0/DEPLOYMENT.md +163 -0
- golit-1.0.0/LICENSE +202 -0
- golit-1.0.0/Makefile +89 -0
- golit-1.0.0/PKG-INFO +407 -0
- golit-1.0.0/README.md +349 -0
- golit-1.0.0/bench/README.md +469 -0
- golit-1.0.0/bench/__init__.py +5 -0
- golit-1.0.0/bench/apps/__init__.py +1 -0
- golit-1.0.0/bench/apps/dash_app.py +122 -0
- golit-1.0.0/bench/apps/dash_memo.py +46 -0
- golit-1.0.0/bench/apps/dash_memo_server.py +33 -0
- golit-1.0.0/bench/apps/dash_server.py +45 -0
- golit-1.0.0/bench/apps/marimo_gen.py +108 -0
- golit-1.0.0/bench/apps/streamlit_app.py +63 -0
- golit-1.0.0/bench/gen_app.py +222 -0
- golit-1.0.0/bench/http/__init__.py +7 -0
- golit-1.0.0/bench/http/drive.py +108 -0
- golit-1.0.0/bench/http/load.py +115 -0
- golit-1.0.0/bench/http/run_b1_http.py +105 -0
- golit-1.0.0/bench/http/run_b2.py +168 -0
- golit-1.0.0/bench/http/run_b2_push.py +245 -0
- golit-1.0.0/bench/http/serve.py +31 -0
- golit-1.0.0/bench/http/serve_memo.py +21 -0
- golit-1.0.0/bench/http/serverctl.py +70 -0
- golit-1.0.0/bench/instrument.py +127 -0
- golit-1.0.0/bench/plot.py +424 -0
- golit-1.0.0/bench/results/.gitignore +3 -0
- golit-1.0.0/bench/results/b1.csv +131 -0
- golit-1.0.0/bench/results/b1_compare_hero.svg +513 -0
- golit-1.0.0/bench/results/b1_dash.csv +19 -0
- golit-1.0.0/bench/results/b1_dash_bytes.csv +3 -0
- golit-1.0.0/bench/results/b1_dash_crossover.svg +427 -0
- golit-1.0.0/bench/results/b1_dash_http.csv +5 -0
- golit-1.0.0/bench/results/b1_dash_http.svg +371 -0
- golit-1.0.0/bench/results/b1_dash_render.csv +4 -0
- golit-1.0.0/bench/results/b1_dash_render.svg +363 -0
- golit-1.0.0/bench/results/b1_hero.svg +480 -0
- golit-1.0.0/bench/results/b1_http.csv +13 -0
- golit-1.0.0/bench/results/b1_http_hero.svg +479 -0
- golit-1.0.0/bench/results/b1_marimo.csv +19 -0
- golit-1.0.0/bench/results/b1_streamlit.csv +19 -0
- golit-1.0.0/bench/results/b2.csv +11 -0
- golit-1.0.0/bench/results/b2_push.csv +7 -0
- golit-1.0.0/bench/results/b2_saturation.svg +441 -0
- golit-1.0.0/bench/results/b2_scaling.svg +433 -0
- golit-1.0.0/bench/results/b_memo.csv +13 -0
- golit-1.0.0/bench/results/b_memo_http.csv +9 -0
- golit-1.0.0/bench/run_b1.py +156 -0
- golit-1.0.0/bench/run_b1_dash.py +321 -0
- golit-1.0.0/bench/run_b1_dash_http.py +156 -0
- golit-1.0.0/bench/run_b1_marimo.py +224 -0
- golit-1.0.0/bench/run_b1_streamlit.py +136 -0
- golit-1.0.0/bench/run_memo.py +171 -0
- golit-1.0.0/bench/run_memo_http.py +140 -0
- golit-1.0.0/deploy/Dockerfile +37 -0
- golit-1.0.0/deploy/docker-compose.yml +52 -0
- golit-1.0.0/deploy/nginx.conf +36 -0
- golit-1.0.0/deploy/scaling_demo/app.py +58 -0
- golit-1.0.0/deploy/verify_scaling.py +115 -0
- golit-1.0.0/docs/about/benchmarks.md +89 -0
- golit-1.0.0/docs/about/comparison.md +51 -0
- golit-1.0.0/docs/about/contributing.md +87 -0
- golit-1.0.0/docs/about/faq.md +53 -0
- golit-1.0.0/docs/advanced/audio.md +105 -0
- golit-1.0.0/docs/advanced/custom-rendering.md +72 -0
- golit-1.0.0/docs/advanced/deployment.md +97 -0
- golit-1.0.0/docs/advanced/index.md +34 -0
- golit-1.0.0/docs/advanced/live-sources.md +76 -0
- golit-1.0.0/docs/advanced/security.md +82 -0
- golit-1.0.0/docs/advanced/server-push.md +93 -0
- golit-1.0.0/docs/advanced/sessions.md +46 -0
- golit-1.0.0/docs/advanced/video-streams.md +211 -0
- golit-1.0.0/docs/advanced/websockets.md +148 -0
- golit-1.0.0/docs/concepts/architecture.md +68 -0
- golit-1.0.0/docs/concepts/data-flow.md +78 -0
- golit-1.0.0/docs/concepts/index.md +30 -0
- golit-1.0.0/docs/concepts/reactivity.md +85 -0
- golit-1.0.0/docs/index.md +171 -0
- golit-1.0.0/docs/install.md +76 -0
- golit-1.0.0/docs/reference/app.md +23 -0
- golit-1.0.0/docs/reference/charts.md +27 -0
- golit-1.0.0/docs/reference/data.md +9 -0
- golit-1.0.0/docs/reference/gis.md +82 -0
- golit-1.0.0/docs/reference/index.md +59 -0
- golit-1.0.0/docs/reference/layout.md +12 -0
- golit-1.0.0/docs/reference/server.md +47 -0
- golit-1.0.0/docs/reference/ui.md +9 -0
- golit-1.0.0/docs/reference/widgets.md +11 -0
- golit-1.0.0/docs/stylesheets/extra.css +52 -0
- golit-1.0.0/docs/tutorial/charts.md +135 -0
- golit-1.0.0/docs/tutorial/first-app.md +63 -0
- golit-1.0.0/docs/tutorial/index.md +37 -0
- golit-1.0.0/docs/tutorial/inputs.md +190 -0
- golit-1.0.0/docs/tutorial/layout.md +84 -0
- golit-1.0.0/docs/tutorial/maps.md +336 -0
- golit-1.0.0/docs/tutorial/running.md +148 -0
- golit-1.0.0/docs/tutorial/sql.md +71 -0
- golit-1.0.0/docs/tutorial/the-graph.md +118 -0
- golit-1.0.0/docs/tutorial/ui-components.md +110 -0
- golit-1.0.0/docs/tutorial/views.md +107 -0
- golit-1.0.0/examples/audio_recorder/app.py +100 -0
- golit-1.0.0/examples/browser_camera/app.py +73 -0
- golit-1.0.0/examples/charts_gallery/app.py +110 -0
- golit-1.0.0/examples/chat/app.py +36 -0
- golit-1.0.0/examples/components_gallery/app.py +126 -0
- golit-1.0.0/examples/duckdb_sql/app.py +66 -0
- golit-1.0.0/examples/earth_engine/app.py +54 -0
- golit-1.0.0/examples/face_detect/app.py +59 -0
- golit-1.0.0/examples/geo_explorer/app.py +85 -0
- golit-1.0.0/examples/geo_explorer/districts.geojson +1085 -0
- golit-1.0.0/examples/great_tables/app.py +53 -0
- golit-1.0.0/examples/live_great_table/app.py +103 -0
- golit-1.0.0/examples/live_sheets/app.py +129 -0
- golit-1.0.0/examples/modular/README.md +58 -0
- golit-1.0.0/examples/modular/_app.py +13 -0
- golit-1.0.0/examples/modular/app.py +32 -0
- golit-1.0.0/examples/modular/reactives.py +37 -0
- golit-1.0.0/examples/modular/sources.py +24 -0
- golit-1.0.0/examples/modular/views.py +35 -0
- golit-1.0.0/examples/raster_explorer/app.py +77 -0
- golit-1.0.0/examples/rgb_composite/app.py +89 -0
- golit-1.0.0/examples/sales_explorer/app.py +94 -0
- golit-1.0.0/examples/terrain_analysis/app.py +82 -0
- golit-1.0.0/examples/tiled_raster/app.py +87 -0
- golit-1.0.0/examples/vector_tiles/app.py +78 -0
- golit-1.0.0/examples/webcam_stream/app.py +98 -0
- golit-1.0.0/golit_benchmark.md +92 -0
- golit-1.0.0/golit_pages/golit_api_reference/code.html +397 -0
- golit-1.0.0/golit_pages/golit_api_reference/screen.png +0 -0
- golit-1.0.0/golit_pages/golit_app_dashboard_preview/code.html +343 -0
- golit-1.0.0/golit_pages/golit_app_dashboard_preview/screen.png +0 -0
- golit-1.0.0/golit_pages/golit_architecture_scaling/code.html +355 -0
- golit-1.0.0/golit_pages/golit_architecture_scaling/screen.png +0 -0
- golit-1.0.0/golit_pages/golit_benchmark_methodology_document.md +32 -0
- golit-1.0.0/golit_pages/golit_benchmarks_performance/code.html +455 -0
- golit-1.0.0/golit_pages/golit_benchmarks_performance/screen.png +0 -0
- golit-1.0.0/golit_pages/golit_component_gallery/code.html +369 -0
- golit-1.0.0/golit_pages/golit_component_gallery/screen.png +0 -0
- golit-1.0.0/golit_pages/golit_contributing_community/code.html +351 -0
- golit-1.0.0/golit_pages/golit_contributing_community/screen.png +0 -0
- golit-1.0.0/golit_pages/golit_dag_graph_explorer/code.html +345 -0
- golit-1.0.0/golit_pages/golit_dag_graph_explorer/screen.png +0 -0
- golit-1.0.0/golit_pages/golit_deployment_scaling_manager/code.html +412 -0
- golit-1.0.0/golit_pages/golit_deployment_scaling_manager/screen.png +0 -0
- golit-1.0.0/golit_pages/golit_documentation_hub/code.html +308 -0
- golit-1.0.0/golit_pages/golit_documentation_hub/screen.png +0 -0
- golit-1.0.0/golit_pages/golit_error_boundary_logs/code.html +344 -0
- golit-1.0.0/golit_pages/golit_error_boundary_logs/screen.png +0 -0
- golit-1.0.0/golit_pages/golit_getting_started/code.html +304 -0
- golit-1.0.0/golit_pages/golit_getting_started/screen.png +0 -0
- golit-1.0.0/golit_pages/golit_home/code.html +348 -0
- golit-1.0.0/golit_pages/golit_home/screen.png +0 -0
- golit-1.0.0/golit_pages/golit_logic/DESIGN.md +88 -0
- golit-1.0.0/golit_pages/golit_login_auth_portal/code.html +194 -0
- golit-1.0.0/golit_pages/golit_login_auth_portal/screen.png +0 -0
- golit-1.0.0/golit_pages/golit_project_roadmap/code.html +328 -0
- golit-1.0.0/golit_pages/golit_project_roadmap/screen.png +0 -0
- golit-1.0.0/mkdocs.yml +156 -0
- golit-1.0.0/project_scope.md +213 -0
- golit-1.0.0/pyproject.toml +136 -0
- golit-1.0.0/python/golit/__init__.py +84 -0
- golit-1.0.0/python/golit/__main__.py +8 -0
- golit-1.0.0/python/golit/_golit.pyi +70 -0
- golit-1.0.0/python/golit/_loader.py +21 -0
- golit-1.0.0/python/golit/app.py +316 -0
- golit-1.0.0/python/golit/charts.py +18 -0
- golit-1.0.0/python/golit/cli.py +90 -0
- golit-1.0.0/python/golit/data.py +95 -0
- golit-1.0.0/python/golit/engine.py +123 -0
- golit-1.0.0/python/golit/gis.py +1169 -0
- golit-1.0.0/python/golit/hashing.py +96 -0
- golit-1.0.0/python/golit/layout.py +197 -0
- golit-1.0.0/python/golit/nodes.py +62 -0
- golit-1.0.0/python/golit/py.typed +0 -0
- golit-1.0.0/python/golit/registry.py +43 -0
- golit-1.0.0/python/golit/rendering/__init__.py +20 -0
- golit-1.0.0/python/golit/rendering/charts.py +37 -0
- golit-1.0.0/python/golit/rendering/html.py +716 -0
- golit-1.0.0/python/golit/rendering/interactive.py +126 -0
- golit-1.0.0/python/golit/rendering/protocol.py +173 -0
- golit-1.0.0/python/golit/server/__init__.py +30 -0
- golit-1.0.0/python/golit/server/audio.py +70 -0
- golit-1.0.0/python/golit/server/chat.py +168 -0
- golit-1.0.0/python/golit/server/factory.py +119 -0
- golit-1.0.0/python/golit/server/polling.py +87 -0
- golit-1.0.0/python/golit/server/processing.py +86 -0
- golit-1.0.0/python/golit/server/pubsub.py +53 -0
- golit-1.0.0/python/golit/server/redis_pubsub.py +84 -0
- golit-1.0.0/python/golit/server/routes.py +163 -0
- golit-1.0.0/python/golit/server/session.py +214 -0
- golit-1.0.0/python/golit/server/session_store.py +105 -0
- golit-1.0.0/python/golit/server/sse.py +92 -0
- golit-1.0.0/python/golit/server/streaming.py +216 -0
- golit-1.0.0/python/golit/server/tiles.py +84 -0
- golit-1.0.0/python/golit/server/vector_tiles.py +124 -0
- golit-1.0.0/python/golit/ui.py +794 -0
- golit-1.0.0/python/golit/widgets.py +517 -0
- golit-1.0.0/src/core.rs +804 -0
- golit-1.0.0/src/lib.rs +172 -0
- golit-1.0.0/tests/test_audio.py +132 -0
- golit-1.0.0/tests/test_chat.py +125 -0
- golit-1.0.0/tests/test_duckdb.py +70 -0
- golit-1.0.0/tests/test_engine.py +165 -0
- golit-1.0.0/tests/test_gis.py +481 -0
- golit-1.0.0/tests/test_gis_ee.py +94 -0
- golit-1.0.0/tests/test_gis_terrain.py +86 -0
- golit-1.0.0/tests/test_gis_tiles.py +132 -0
- golit-1.0.0/tests/test_gis_vector_tiles.py +157 -0
- golit-1.0.0/tests/test_great_tables.py +50 -0
- golit-1.0.0/tests/test_interactive.py +118 -0
- golit-1.0.0/tests/test_layout.py +140 -0
- golit-1.0.0/tests/test_polling.py +94 -0
- golit-1.0.0/tests/test_processing.py +156 -0
- golit-1.0.0/tests/test_redis_pubsub.py +64 -0
- golit-1.0.0/tests/test_rendering.py +61 -0
- golit-1.0.0/tests/test_server.py +85 -0
- golit-1.0.0/tests/test_session_store.py +101 -0
- golit-1.0.0/tests/test_sse.py +100 -0
- golit-1.0.0/tests/test_streaming.py +231 -0
- golit-1.0.0/tests/test_ui.py +143 -0
- golit-1.0.0/tests/test_widgets.py +77 -0
- golit-1.0.0/uv.lock +4856 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Keep the build context lean — the Dockerfile only needs pyproject/Cargo,
|
|
2
|
+
# src/, python/, examples/, and README. Everything below is build output,
|
|
3
|
+
# caches, or design assets the image doesn't use.
|
|
4
|
+
.git
|
|
5
|
+
target
|
|
6
|
+
.venv
|
|
7
|
+
golit_pages
|
|
8
|
+
tests
|
|
9
|
+
**/__pycache__
|
|
10
|
+
*.pyc
|
|
11
|
+
.pytest_cache
|
|
12
|
+
.ruff_cache
|
|
13
|
+
.mypy_cache
|
|
14
|
+
*.whl
|
|
15
|
+
.DS_Store
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
# Cancel superseded runs on the same ref.
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ci-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
rust:
|
|
15
|
+
name: rust (fmt · clippy · test)
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.11" # pyo3 needs an interpreter to configure + link
|
|
22
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
23
|
+
with:
|
|
24
|
+
components: rustfmt, clippy
|
|
25
|
+
- uses: Swatinem/rust-cache@v2
|
|
26
|
+
- run: cargo fmt --all --check
|
|
27
|
+
- run: cargo clippy --all-targets -- -D warnings
|
|
28
|
+
- run: cargo test
|
|
29
|
+
|
|
30
|
+
python:
|
|
31
|
+
name: py${{ matrix.python-version }} (lint · type · test)
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
strategy:
|
|
34
|
+
fail-fast: false
|
|
35
|
+
matrix:
|
|
36
|
+
python-version: ["3.11", "3.12"]
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v4
|
|
39
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
40
|
+
- uses: Swatinem/rust-cache@v2
|
|
41
|
+
- uses: astral-sh/setup-uv@v5
|
|
42
|
+
- name: Create the virtualenv
|
|
43
|
+
run: uv venv --python ${{ matrix.python-version }}
|
|
44
|
+
- name: Install tooling + dev deps
|
|
45
|
+
# uv pip install (never `uv sync`, which would prune the optional extras).
|
|
46
|
+
run: uv pip install maturin ruff mypy pytest pytest-asyncio httpx fakeredis
|
|
47
|
+
- name: Build the extension + install the test extras
|
|
48
|
+
# gis-terrain is omitted on purpose: WhiteboxTools downloads a binary at
|
|
49
|
+
# runtime, so its tests importorskip and skip cleanly here.
|
|
50
|
+
run: >
|
|
51
|
+
uv run maturin develop
|
|
52
|
+
-E sql,charts,gis,gis-vector-tiles,gis-raster,gis-tiles,gis-ee,vision,vision-cv,tables,redis
|
|
53
|
+
- name: Ruff
|
|
54
|
+
run: uv run ruff check .
|
|
55
|
+
- name: Mypy
|
|
56
|
+
run: uv run mypy
|
|
57
|
+
- name: Pytest
|
|
58
|
+
run: uv run pytest -q
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Tagging v* builds the full wheel matrix + sdist and publishes to PyPI.
|
|
4
|
+
#
|
|
5
|
+
# One-time setup (no secrets needed — uses PyPI Trusted Publishing / OIDC):
|
|
6
|
+
# 1. Create the project on PyPI (or let the first publish create it).
|
|
7
|
+
# 2. PyPI → project → Publishing → add a GitHub Actions publisher:
|
|
8
|
+
# owner = Boadzie, repo = golit, workflow = release.yml, environment = pypi
|
|
9
|
+
# 3. (Repo) Settings → Environments → create an environment named `pypi`.
|
|
10
|
+
# Then: bump the version (pyproject.toml + Cargo.toml), tag `vX.Y.Z`, and push the tag.
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
push:
|
|
14
|
+
tags: ["v*"]
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
wheels:
|
|
21
|
+
name: wheels (${{ matrix.platform.runner }} · ${{ matrix.platform.target }})
|
|
22
|
+
runs-on: ${{ matrix.platform.runner }}
|
|
23
|
+
strategy:
|
|
24
|
+
fail-fast: false
|
|
25
|
+
matrix:
|
|
26
|
+
platform:
|
|
27
|
+
- { runner: ubuntu-latest, target: x86_64 }
|
|
28
|
+
- { runner: ubuntu-latest, target: aarch64 }
|
|
29
|
+
- { runner: macos-14, target: aarch64 } # Apple Silicon; Intel (macos-13/x86_64) dropped
|
|
30
|
+
- { runner: windows-latest, target: x64 }
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
- uses: actions/setup-python@v5
|
|
34
|
+
with:
|
|
35
|
+
python-version: "3.11"
|
|
36
|
+
- name: Build wheels
|
|
37
|
+
uses: PyO3/maturin-action@v1
|
|
38
|
+
with:
|
|
39
|
+
target: ${{ matrix.platform.target }}
|
|
40
|
+
args: --release --out dist
|
|
41
|
+
manylinux: auto # ignored off Linux; builds manylinux on Linux runners
|
|
42
|
+
sccache: "true"
|
|
43
|
+
- uses: actions/upload-artifact@v4
|
|
44
|
+
with:
|
|
45
|
+
name: wheels-${{ matrix.platform.runner }}-${{ matrix.platform.target }}
|
|
46
|
+
path: dist
|
|
47
|
+
|
|
48
|
+
sdist:
|
|
49
|
+
name: sdist
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/checkout@v4
|
|
53
|
+
- name: Build sdist
|
|
54
|
+
uses: PyO3/maturin-action@v1
|
|
55
|
+
with:
|
|
56
|
+
command: sdist
|
|
57
|
+
args: --out dist
|
|
58
|
+
- uses: actions/upload-artifact@v4
|
|
59
|
+
with:
|
|
60
|
+
name: wheels-sdist
|
|
61
|
+
path: dist
|
|
62
|
+
|
|
63
|
+
publish:
|
|
64
|
+
name: publish to PyPI + attach to the release
|
|
65
|
+
needs: [wheels, sdist]
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
environment: pypi
|
|
68
|
+
permissions:
|
|
69
|
+
id-token: write # PyPI Trusted Publishing (OIDC) — no API token in secrets
|
|
70
|
+
contents: write # attach the built artifacts to the GitHub Release
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/download-artifact@v4
|
|
73
|
+
with:
|
|
74
|
+
pattern: wheels-*
|
|
75
|
+
merge-multiple: true
|
|
76
|
+
path: dist
|
|
77
|
+
- name: Publish to PyPI
|
|
78
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
79
|
+
- name: Attach artifacts to the GitHub Release
|
|
80
|
+
uses: softprops/action-gh-release@v2
|
|
81
|
+
with:
|
|
82
|
+
files: dist/*
|
golit-1.0.0/.gitignore
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Rust
|
|
2
|
+
/target
|
|
3
|
+
Cargo.lock
|
|
4
|
+
|
|
5
|
+
# Python
|
|
6
|
+
.venv/
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[cod]
|
|
9
|
+
*.so
|
|
10
|
+
*.pyd
|
|
11
|
+
dist/
|
|
12
|
+
build/
|
|
13
|
+
*.egg-info/
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
|
|
18
|
+
# Docs
|
|
19
|
+
site/
|
|
20
|
+
|
|
21
|
+
# Tooling
|
|
22
|
+
.DS_Store
|
|
23
|
+
golit_pages/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.11
|
golit-1.0.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Golit are recorded here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/), and the project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [1.0.0] — 2026-06-06
|
|
8
|
+
|
|
9
|
+
First stable release. Golit is a high-performance reactive **DAG** framework for Python —
|
|
10
|
+
*reactive data apps that actually ship* — with a Rust kernel and a server-rendered HTMX
|
|
11
|
+
transport, where update cost is proportional to the change, not the program.
|
|
12
|
+
|
|
13
|
+
### Reactive core
|
|
14
|
+
|
|
15
|
+
- Rust + PyO3 reactive kernel: dirty tracking, topological scheduling, memoized propagation.
|
|
16
|
+
- `@app.source` / `@app.reactive` / `@app.view` — dependencies inferred from parameter names; a
|
|
17
|
+
node re-executes only when an upstream node or input changes, and unchanged outputs cascade
|
|
18
|
+
into memo hits (nothing on the wire).
|
|
19
|
+
- Per-session state (worker-local Polars values) over a shared, immutable topology.
|
|
20
|
+
- A larger app can be split across modules (one shared `App` instance + import-for-side-effects).
|
|
21
|
+
|
|
22
|
+
### Inputs & components
|
|
23
|
+
|
|
24
|
+
- Reactive input widgets: `slider`, `number`, `select`, `text`, `checkbox`, `upload`, `radio`,
|
|
25
|
+
`multiselect`, `switch`, `date`, `textarea`, `button`.
|
|
26
|
+
- `golit.ui` — shadcn-styled, server-rendered builders: `card`, `columns`, `grid`, `tabs`,
|
|
27
|
+
`expander`, `accordion`, `divider`, `metric`, `scorecard`, `alert`, `badge`, `progress`,
|
|
28
|
+
`skeleton`, `spinner`, `table`, `markdown`, `code`, `json_view`, `heading`, `caption`.
|
|
29
|
+
- Page layout (`golit.layout`): a sidebar/rows/tabs scaffold, validated at build time.
|
|
30
|
+
|
|
31
|
+
### Rendering
|
|
32
|
+
|
|
33
|
+
- A view may return a `str` (trusted markup), a Polars `DataFrame`, a DuckDB relation, a chart,
|
|
34
|
+
a map, a Great Tables `GT`, anything with `_repr_html_()`, a Matplotlib figure, or `bytes`.
|
|
35
|
+
- Charts: Lets-Plot static SVG; interactive **Plotly / Altair / Bokeh / AnyChart** that hydrate
|
|
36
|
+
on load and across POST/SSE swaps; `chart_spec(lib, dict)` for the raw wire spec.
|
|
37
|
+
- Tables: return a `great_tables` **`GT`** object and Golit auto-renders its self-contained HTML;
|
|
38
|
+
`ui.gt_theme` restyles it to match golit's surface. (`golit[tables]`)
|
|
39
|
+
|
|
40
|
+
### Maps & GIS (`golit.gis`)
|
|
41
|
+
|
|
42
|
+
- Native **MapLibre GL** maps from a GeoDataFrame; choropleths, tooltips, and DuckDB spatial SQL.
|
|
43
|
+
- MVT **vector tiles** for large vector data; single-band, RGB-composite, and tiled-COG **raster**
|
|
44
|
+
maps; **WhiteboxTools** terrain analysis; **Google Earth Engine** overlays.
|
|
45
|
+
|
|
46
|
+
### Realtime
|
|
47
|
+
|
|
48
|
+
- **SSE** server-push channel with a pluggable pub/sub (in-memory single-node, **Redis** fleet).
|
|
49
|
+
- **Live data sources** — `@app.poll(name, interval)`: external data that changes on its own (a
|
|
50
|
+
Google Sheet, an API) is fetched in the background and pushed on a content-hash change.
|
|
51
|
+
- **WebSocket chat** — `@app.on_message` + `ui.chat`.
|
|
52
|
+
- **Video** — server-side **MJPEG** streams (`@app.stream` + `ui.webcam`, with `shared=True`
|
|
53
|
+
fan-out) and **browser-camera** computer vision (`@app.on_frame` + `ui.camera`).
|
|
54
|
+
- **Audio** — a microphone **recorder** (`@app.on_audio` + `ui.recorder`) with in-browser WAV
|
|
55
|
+
capture, inline playback, and download.
|
|
56
|
+
|
|
57
|
+
### SQL
|
|
58
|
+
|
|
59
|
+
- `golit.sql(query, **frames)` — in-process **DuckDB** SQL over Polars frames as a reactive node.
|
|
60
|
+
(`golit[sql]`)
|
|
61
|
+
|
|
62
|
+
### Server, deployment & tooling
|
|
63
|
+
|
|
64
|
+
- Litestar ASGI app via `create_app`; the `golit run app.py` CLI (uvicorn).
|
|
65
|
+
- Horizontal scale: N single-worker instances behind a sticky load balancer + Redis fan-out, with
|
|
66
|
+
a `deploy/` compose stack and an automated, self-validating **cross-node fan-out verifier**.
|
|
67
|
+
- Optional extras: `charts`, `sql`, `gis` / `gis-raster` / `gis-tiles` / `gis-terrain` /
|
|
68
|
+
`gis-ee` / `gis-vector-tiles`, `vision` / `vision-cv`, `tables`, `redis`.
|
|
69
|
+
|
|
70
|
+
### Quality
|
|
71
|
+
|
|
72
|
+
- 17 Rust + 209 Python tests; ruff + mypy clean; a benchmark harness (`bench/`) with measured
|
|
73
|
+
Golit-vs-Dash results.
|
|
74
|
+
|
|
75
|
+
[1.0.0]: https://github.com/Boadzie/golit/releases/tag/v1.0.0
|
golit-1.0.0/Cargo.lock
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 4
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "autocfg"
|
|
7
|
+
version = "1.5.1"
|
|
8
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
9
|
+
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
|
10
|
+
|
|
11
|
+
[[package]]
|
|
12
|
+
name = "cc"
|
|
13
|
+
version = "1.2.63"
|
|
14
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
15
|
+
checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
|
|
16
|
+
dependencies = [
|
|
17
|
+
"find-msvc-tools",
|
|
18
|
+
"shlex",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[[package]]
|
|
22
|
+
name = "cfg-if"
|
|
23
|
+
version = "1.0.4"
|
|
24
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
25
|
+
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|
26
|
+
|
|
27
|
+
[[package]]
|
|
28
|
+
name = "find-msvc-tools"
|
|
29
|
+
version = "0.1.9"
|
|
30
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
31
|
+
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
|
32
|
+
|
|
33
|
+
[[package]]
|
|
34
|
+
name = "golit"
|
|
35
|
+
version = "1.0.0"
|
|
36
|
+
dependencies = [
|
|
37
|
+
"pyo3",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[[package]]
|
|
41
|
+
name = "heck"
|
|
42
|
+
version = "0.5.0"
|
|
43
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
44
|
+
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
45
|
+
|
|
46
|
+
[[package]]
|
|
47
|
+
name = "indoc"
|
|
48
|
+
version = "2.0.7"
|
|
49
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
50
|
+
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
|
51
|
+
dependencies = [
|
|
52
|
+
"rustversion",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[[package]]
|
|
56
|
+
name = "libc"
|
|
57
|
+
version = "0.2.186"
|
|
58
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
59
|
+
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
|
60
|
+
|
|
61
|
+
[[package]]
|
|
62
|
+
name = "memoffset"
|
|
63
|
+
version = "0.9.1"
|
|
64
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
65
|
+
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
|
66
|
+
dependencies = [
|
|
67
|
+
"autocfg",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
[[package]]
|
|
71
|
+
name = "once_cell"
|
|
72
|
+
version = "1.21.4"
|
|
73
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
74
|
+
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
75
|
+
|
|
76
|
+
[[package]]
|
|
77
|
+
name = "portable-atomic"
|
|
78
|
+
version = "1.13.1"
|
|
79
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
80
|
+
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
|
81
|
+
|
|
82
|
+
[[package]]
|
|
83
|
+
name = "proc-macro2"
|
|
84
|
+
version = "1.0.106"
|
|
85
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
86
|
+
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
|
87
|
+
dependencies = [
|
|
88
|
+
"unicode-ident",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
[[package]]
|
|
92
|
+
name = "pyo3"
|
|
93
|
+
version = "0.23.5"
|
|
94
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
95
|
+
checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872"
|
|
96
|
+
dependencies = [
|
|
97
|
+
"cfg-if",
|
|
98
|
+
"indoc",
|
|
99
|
+
"libc",
|
|
100
|
+
"memoffset",
|
|
101
|
+
"once_cell",
|
|
102
|
+
"portable-atomic",
|
|
103
|
+
"pyo3-build-config",
|
|
104
|
+
"pyo3-ffi",
|
|
105
|
+
"pyo3-macros",
|
|
106
|
+
"unindent",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
[[package]]
|
|
110
|
+
name = "pyo3-build-config"
|
|
111
|
+
version = "0.23.5"
|
|
112
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
113
|
+
checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb"
|
|
114
|
+
dependencies = [
|
|
115
|
+
"once_cell",
|
|
116
|
+
"python3-dll-a",
|
|
117
|
+
"target-lexicon",
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
[[package]]
|
|
121
|
+
name = "pyo3-ffi"
|
|
122
|
+
version = "0.23.5"
|
|
123
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
124
|
+
checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d"
|
|
125
|
+
dependencies = [
|
|
126
|
+
"libc",
|
|
127
|
+
"pyo3-build-config",
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
[[package]]
|
|
131
|
+
name = "pyo3-macros"
|
|
132
|
+
version = "0.23.5"
|
|
133
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
134
|
+
checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da"
|
|
135
|
+
dependencies = [
|
|
136
|
+
"proc-macro2",
|
|
137
|
+
"pyo3-macros-backend",
|
|
138
|
+
"quote",
|
|
139
|
+
"syn",
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
[[package]]
|
|
143
|
+
name = "pyo3-macros-backend"
|
|
144
|
+
version = "0.23.5"
|
|
145
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
146
|
+
checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028"
|
|
147
|
+
dependencies = [
|
|
148
|
+
"heck",
|
|
149
|
+
"proc-macro2",
|
|
150
|
+
"pyo3-build-config",
|
|
151
|
+
"quote",
|
|
152
|
+
"syn",
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
[[package]]
|
|
156
|
+
name = "python3-dll-a"
|
|
157
|
+
version = "0.2.15"
|
|
158
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
159
|
+
checksum = "d80ba7540edb18890d444c5aa8e1f1f99b1bdf26fb26ae383135325f4a36042b"
|
|
160
|
+
dependencies = [
|
|
161
|
+
"cc",
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
[[package]]
|
|
165
|
+
name = "quote"
|
|
166
|
+
version = "1.0.45"
|
|
167
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
168
|
+
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
|
169
|
+
dependencies = [
|
|
170
|
+
"proc-macro2",
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
[[package]]
|
|
174
|
+
name = "rustversion"
|
|
175
|
+
version = "1.0.22"
|
|
176
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
177
|
+
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
|
178
|
+
|
|
179
|
+
[[package]]
|
|
180
|
+
name = "shlex"
|
|
181
|
+
version = "2.0.1"
|
|
182
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
183
|
+
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
|
184
|
+
|
|
185
|
+
[[package]]
|
|
186
|
+
name = "syn"
|
|
187
|
+
version = "2.0.117"
|
|
188
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
189
|
+
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
|
190
|
+
dependencies = [
|
|
191
|
+
"proc-macro2",
|
|
192
|
+
"quote",
|
|
193
|
+
"unicode-ident",
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
[[package]]
|
|
197
|
+
name = "target-lexicon"
|
|
198
|
+
version = "0.12.16"
|
|
199
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
200
|
+
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
|
201
|
+
|
|
202
|
+
[[package]]
|
|
203
|
+
name = "unicode-ident"
|
|
204
|
+
version = "1.0.24"
|
|
205
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
206
|
+
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
207
|
+
|
|
208
|
+
[[package]]
|
|
209
|
+
name = "unindent"
|
|
210
|
+
version = "0.2.4"
|
|
211
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
212
|
+
checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
|
golit-1.0.0/Cargo.toml
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "golit"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
description = "Reactive DAG kernel for Golit (dirty tracking, topological scheduling, propagation)."
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
|
|
9
|
+
[lib]
|
|
10
|
+
name = "_golit"
|
|
11
|
+
crate-type = ["cdylib", "rlib"]
|
|
12
|
+
|
|
13
|
+
# NOTE: `extension-module` is intentionally NOT enabled here. maturin enables it
|
|
14
|
+
# for wheel builds via [tool.maturin] features. Leaving it off lets `cargo test`
|
|
15
|
+
# link libpython and run the kernel's unit tests without a wheel build.
|
|
16
|
+
# `generate-import-lib` lets pyo3 synthesize the Windows python3.lib from the abi3
|
|
17
|
+
# stable ABI, so Windows wheels cross-compile (cargo-xwin) without a Windows Python.
|
|
18
|
+
# It's a no-op on non-Windows targets.
|
|
19
|
+
[dependencies]
|
|
20
|
+
pyo3 = { version = "0.23", features = ["abi3-py311", "generate-import-lib"] }
|
|
21
|
+
|
|
22
|
+
[profile.release]
|
|
23
|
+
lto = "thin"
|
|
24
|
+
codegen-units = 1
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Deploying Golit
|
|
2
|
+
|
|
3
|
+
Golit runs as a single process out of the box. This guide covers going wider:
|
|
4
|
+
multiple workers and hosts, with the SSE push channel intact.
|
|
5
|
+
|
|
6
|
+
## The one thing to understand: session state is worker-local
|
|
7
|
+
|
|
8
|
+
Each client gets a server-side **session** — its own kernel graph and its current
|
|
9
|
+
Polars values — kept in the worker's memory and keyed by the `golit_session`
|
|
10
|
+
cookie. That locality is deliberate: it's what makes recompute cost track the
|
|
11
|
+
*change*, not the program. Serializing DataFrames to a shared store on every
|
|
12
|
+
interaction would throw that away.
|
|
13
|
+
|
|
14
|
+
The consequence: a client's requests are cheapest when they reach the worker that
|
|
15
|
+
already holds its session — the initial `GET /`, each `POST /node/...`, and the
|
|
16
|
+
long-lived `GET /events` SSE stream. That's *session affinity* ("sticky sessions").
|
|
17
|
+
It is the recommended default, but with Redis turned on it is no longer load-bearing
|
|
18
|
+
for correctness: the **session store** persists each session's *input* state, so a
|
|
19
|
+
request that lands on a worker without the live session **reconstructs** it from
|
|
20
|
+
those inputs (replay + local recompute) instead of starting from defaults. Affinity
|
|
21
|
+
then just keeps the in-memory session warm and avoids the same session diverging
|
|
22
|
+
across two workers under round-robin.
|
|
23
|
+
|
|
24
|
+
The second consequence: a server-side invalidation (a streaming source, a
|
|
25
|
+
background job, a shared node) originates on one worker but must reach the worker
|
|
26
|
+
holding each *affected* client's SSE connection. That's what **Redis pub/sub**
|
|
27
|
+
provides — one `publish`, delivered to every worker.
|
|
28
|
+
|
|
29
|
+
So horizontal scale is two pieces:
|
|
30
|
+
|
|
31
|
+
| Piece | Mechanism | Without it |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| Keep a client on one worker (recommended) | Load balancer, hashing the `golit_session` cookie | A re-routed client reconstructs from Redis-stored inputs (a cold recompute) — or, with no session store, re-renders from defaults |
|
|
34
|
+
| Reach every worker on invalidation | `GOLIT_REDIS_URL` → `RedisPubSub` | SSE pushes only reach clients on the publishing worker |
|
|
35
|
+
|
|
36
|
+
## Single node (default)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
golit run examples/sales_explorer/app.py
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
One process, in-memory fan-out (`InMemoryPubSub`). Nothing else to configure.
|
|
43
|
+
|
|
44
|
+
## Turning on Redis
|
|
45
|
+
|
|
46
|
+
Set one environment variable; `create_app` selects **both** Redis backends
|
|
47
|
+
automatically — `RedisPubSub` for invalidation fan-out and `RedisSessionStore` for
|
|
48
|
+
durable input state (Redis is an optional dependency — install with the `redis`
|
|
49
|
+
extra):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install "golit[redis]"
|
|
53
|
+
export GOLIT_REDIS_URL=redis://localhost:6379
|
|
54
|
+
golit run examples/sales_explorer/app.py
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Programmatic override, if you'd rather not use the environment:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from golit import create_app
|
|
61
|
+
from golit.server import RedisPubSub, RedisSessionStore
|
|
62
|
+
|
|
63
|
+
application = create_app(
|
|
64
|
+
app,
|
|
65
|
+
pubsub=RedisPubSub("redis://redis:6379"),
|
|
66
|
+
session_store=RedisSessionStore("redis://redis:6379"),
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Horizontal scale: N instances behind a sticky load balancer
|
|
71
|
+
|
|
72
|
+
The supported HA topology is **N single-worker instances**, each on its own
|
|
73
|
+
port/container, behind a load balancer that pins clients by the session cookie,
|
|
74
|
+
all sharing one Redis.
|
|
75
|
+
|
|
76
|
+
> **Why not `uvicorn --workers N`?** uvicorn's workers share one socket with no
|
|
77
|
+
> affinity — the kernel hands each connection to whichever worker is free, so a
|
|
78
|
+
> client's `GET` and its `/events` stream can land on different workers, and
|
|
79
|
+
> neither holds the other's session. `golit run --workers N` exists for
|
|
80
|
+
> convenience and local testing and prints a warning; it is **not** the
|
|
81
|
+
> production path because it can't provide affinity.
|
|
82
|
+
|
|
83
|
+
### nginx
|
|
84
|
+
|
|
85
|
+
Open-source nginx can hash on a cookie, which gives consistent per-session
|
|
86
|
+
routing:
|
|
87
|
+
|
|
88
|
+
```nginx
|
|
89
|
+
upstream golit {
|
|
90
|
+
hash $cookie_golit_session consistent; # sticky by Golit's session cookie
|
|
91
|
+
server app1:8000;
|
|
92
|
+
server app2:8000;
|
|
93
|
+
server app3:8000;
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The SSE stream also needs buffering off and a long read timeout. The full config
|
|
98
|
+
is in [`deploy/nginx.conf`](deploy/nginx.conf).
|
|
99
|
+
|
|
100
|
+
## Run the whole stack locally (podman or docker)
|
|
101
|
+
|
|
102
|
+
[`deploy/`](deploy/) has a complete example: Redis + three single-worker replicas
|
|
103
|
+
+ the nginx sticky balancer.
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# from the repo root
|
|
107
|
+
podman compose -f deploy/docker-compose.yml up --build
|
|
108
|
+
# open http://localhost:8000
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- [`deploy/Dockerfile`](deploy/Dockerfile) — two-stage build: compile the Rust
|
|
112
|
+
kernel to an abi3 wheel, then install it (with the `redis` extra) into a slim
|
|
113
|
+
runtime.
|
|
114
|
+
- [`deploy/docker-compose.yml`](deploy/docker-compose.yml) — `redis`, `app1/2/3`
|
|
115
|
+
(each `GOLIT_REDIS_URL=redis://redis:6379`), and `nginx` on port 8000.
|
|
116
|
+
- [`deploy/nginx.conf`](deploy/nginx.conf) — the cookie-hash upstream.
|
|
117
|
+
|
|
118
|
+
To prove affinity + fan-out: open the app, move the slider (the chart/KPI/table
|
|
119
|
+
swap — that's the local worker), then have a background source publish an
|
|
120
|
+
invalidation and watch it arrive over `/events` on the *other* replicas' clients.
|
|
121
|
+
|
|
122
|
+
## Verify the fan-out (automated)
|
|
123
|
+
|
|
124
|
+
[`deploy/verify_scaling.py`](deploy/verify_scaling.py) turns that manual check into a
|
|
125
|
+
one-command, self-validating proof. It runs two single-worker nodes that share one Redis (the
|
|
126
|
+
same fan-out path as two hosts: *publish → Redis → every worker's SSE*). Node **A** publishes a
|
|
127
|
+
`clock` invalidation every second; node **B** does not — yet an SSE client on **B** receives
|
|
128
|
+
the `node:clock` events, so they can only have crossed Redis from A. A built-in **control**
|
|
129
|
+
re-runs both nodes with Redis off (isolated in-memory pub/sub) and asserts B sees *nothing*,
|
|
130
|
+
proving Redis is load-bearing rather than some local artifact.
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# a Redis to share (podman or docker)
|
|
134
|
+
podman run -d --rm --name golit-redis -p 6379:6379 docker.io/library/redis:7-alpine
|
|
135
|
+
|
|
136
|
+
GOLIT_REDIS_URL=redis://localhost:6379 python deploy/verify_scaling.py
|
|
137
|
+
# -> with Redis -> node B saw node:clock: True
|
|
138
|
+
# without Redis -> node B saw node:clock: False
|
|
139
|
+
# PASS: cross-node fan-out works, and only because of Redis
|
|
140
|
+
|
|
141
|
+
podman rm -f golit-redis
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Point `GOLIT_REDIS_URL` at a Redis the two processes can both reach and the same script proves
|
|
145
|
+
the path across **separate hosts** unchanged. ([`deploy/scaling_demo/app.py`](deploy/scaling_demo/app.py)
|
|
146
|
+
is the tiny clock app it drives.)
|
|
147
|
+
|
|
148
|
+
## Operational notes
|
|
149
|
+
|
|
150
|
+
- **Worker restart loses that worker's warm caches, not the session.** Without a
|
|
151
|
+
session store, clients re-render from defaults on the next `GET /`. With
|
|
152
|
+
`RedisSessionStore` the input state is durable, so the session reconstructs (from
|
|
153
|
+
inputs + a local recompute) on the next request to *any* worker. Either way, keep
|
|
154
|
+
`@app.source` functions cheap/idempotent — the initial render can run again.
|
|
155
|
+
- **Redis never holds DataFrames.** Pub/sub carries small JSON (`node_id`,
|
|
156
|
+
`session`); the session store holds only each session's *input* map
|
|
157
|
+
(`{input_id: value}`). The derived frames stay worker-local — that's the thesis.
|
|
158
|
+
- **Scaling Redis:** a single instance handles a large fan-out fine. Redis
|
|
159
|
+
pub/sub is at-most-once and not persisted — acceptable here because an
|
|
160
|
+
invalidation just asks a worker to recompute current state, which it can always
|
|
161
|
+
redo on the next interaction.
|
|
162
|
+
- **Sizing:** memory is dominated by live session values (Polars frames × active
|
|
163
|
+
sessions). Scale replicas on memory, and front them with the sticky balancer.
|