Skip to main content

VarnaPy — reactive Python web UI framework, build beautiful apps without JavaScript

Project description

VarnaPy

Reactive Python web UI framework — build beautiful apps without JavaScript.

PyPI Python CI License: MIT


Why VarnaPy?

Feature VarnaPy Streamlit Gradio
Targeted re-renders (no full page rerun)
Multi-page SPA routing partial
Component override system
Third-party plugin ecosystem partial
Dark mode partial
File upload
Charts (Plotly) partial
DataGrid with sort + pagination
WebSocket protocol (sub-6KB JS)

Installation

pip install varnapy

Optional extras:

pip install "varnapy[charts]"   # Plotly Chart component
pip install "varnapy[share]"    # Public tunnel (share=True)
pip install "varnapy[all]"      # Everything

Quickstart

import varnapy as ui

app = ui.App("My App")

@app.page("/")
def home(session):
    count = ui.state(0)

    with ui.Column(gap="md"):
        ui.Heading("Hello from VarnaPy", level=1)
        ui.Button("Click me", on_click=lambda: count.set(count.value + 1))
        ui.Text(lambda: f"Clicked {count.value} times")

app.run()
varnapy run app.py
# → http://127.0.0.1:8000

Core Concepts

Reactive State

# Simple value
count = ui.state(0)
count.set(10)
count.update(lambda v: v + 1)
count.value  # → 11

# Derived / computed
full_name = ui.computed(lambda: f"{first.value} {last.value}")

# Side effects
ui.effect(lambda: save_to_db(query.value))

# Mutable containers
items = ui.list_state(["a", "b"])
items.append("c")

config = ui.dict_state({"debug": False})
config["debug"] = True

Any ReactiveVar read inside a component's render function automatically subscribes that component. When the value changes, only that component re-renders — never the full page.

Component Library

Layout

ui.Row(gap="md")
ui.Column(gap="lg")
ui.Grid(cols=3)
ui.Card(title="Stats", description="Overview")
ui.Tabs(["Overview", "Details", "Settings"])
ui.Modal(open=show_modal, on_close=lambda: show_modal.set(False))
ui.Sidebar(open=sidebar_open)

Input

ui.Button("Save", on_click=save, variant="primary")
ui.TextInput(label="Name", value=name, on_change=lambda v: name.set(v))
ui.NumberInput(label="Amount", value=amt, min=0, max=1000)
ui.Select(label="Status", options=["Draft", "Published"], value=status)
ui.Checkbox(label="Agree", checked=agreed)
ui.Switch(label="Dark mode", checked=dark_mode, on_change=toggle_theme)
ui.Slider(label="Volume", value=vol, min=0, max=100)
ui.FileUpload("Drop files here", multiple=True, on_change=handle_files)

Display

ui.Text("Body text")
ui.Heading("Page Title", level=1)
ui.Metric(label="Revenue", value="$12,400", delta="+8.2%")
ui.Badge("Active", variant="success")
ui.Progress(75.0, label="Upload progress")
ui.Table(data, columns=["name", "email", "status"])
ui.Chart(plotly_figure, height="400px")
ui.Code("print('hello')", language="python")

Feedback & Navigation

ui.Alert("Saved!", variant="success")
ui.Spinner(size="md")
ui.NavMenu(brand="My App")
ui.NavItem("Home", href="/", active=True)
ui.Pagination(page=current_page, total_pages=10)

Advanced

ui.DataGrid(data, columns=[{"key": "name", "label": "Name", "sortable": True}], page_size=20)
ui.CustomComponent()  # base class for custom components with client-side JS

Multi-Page Routing

@app.page("/")
def home(session):
    ui.Heading("Home")
    ui.Button("Go to profile", on_click=lambda: session.navigate("/profile/42"))

@app.page("/profile/{user_id}")
def profile(session, user_id: str):
    ui.Heading(f"Profile: {user_id}")

Navigation is SPA-style — no full page reload.

Dark Mode

dark = ui.state(False)

async def toggle(v: bool) -> None:
    dark.set(v)
    await session.set_theme("dark" if v else "light")

ui.Switch(label="Dark mode", checked=dark, on_change=toggle)

Component Overrides

Replace any built-in component app-wide or per-page:

from varnapy.components._base import Component

class MyButton(Component):
    def render(self) -> str:
        return f'<button {self._attrs("my-btn")} data-flux-event="click">{self._label}</button>'

# App-wide
ui.override("Button", MyButton)

# Single page only
@app.page("/special", overrides={"Button": MyButton})
def special(session): ...

Third-Party Plugins

Publish component libraries to PyPI with a single entry-point:

# your plugin's pyproject.toml
[project.entry-points."varnapy.components"]
my_plugin = "my_package:register"
# my_package/__init__.py
def register(registry):
    registry.add("MyWidget", MyWidget)

VarnaPy discovers installed plugins automatically on startup.


CLI

varnapy run app.py                   # dev server, hot-reload, opens browser
varnapy run app.py --port 3000       # custom port
varnapy run app.py --no-open         # don't open browser
varnapy run app.py --no-reload       # disable hot-reload
varnapy run app.py --prod            # production mode (no dev overlay)
varnapy run app.py --share           # public URL via localtunnel

varnapy new my_app                   # scaffold new project
varnapy export-docker app.py         # generate Dockerfile + docker-compose.yml

Testing

from varnapy.testing import FluxTestClient

def test_counter():
    app = ui.App("test")

    @app.page("/")
    def home(session):
        count = ui.state(0)
        ui.Button("Inc", on_click=lambda: count.set(count.value + 1))
        ui.Text(lambda: str(count.value))

    with FluxTestClient(app) as client:
        client.connect("/")
        assert "0" in client.html()
        client.click("Inc")
        assert "1" in client.html()

Production Deployment

Docker

varnapy export-docker app.py --output ./deploy
docker compose -f deploy/docker-compose.yml up

Manual

pip install "varnapy[all]" gunicorn
gunicorn app:app._fastapi -w 1 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000

Note: Use 1 worker per process (sessions are in-process). For horizontal scaling, add Redis-backed session sharing (roadmap).


Examples

Example Description
hello_world.py Counter in 15 lines
multi_page.py SPA routing, DataGrid, Chart, FileUpload

License

MIT — see LICENSE.

Built by Minato3000.

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

varnapy-0.1.0.tar.gz (928.5 kB view details)

Uploaded Source

Built Distribution

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

varnapy-0.1.0-py3-none-any.whl (63.4 kB view details)

Uploaded Python 3

File details

Details for the file varnapy-0.1.0.tar.gz.

File metadata

  • Download URL: varnapy-0.1.0.tar.gz
  • Upload date:
  • Size: 928.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for varnapy-0.1.0.tar.gz
Algorithm Hash digest
SHA256 e7d2a9d5c67dae421e8ed3401988469df71486ea24c8e40a7cb3430756820a83
MD5 17d95e6026b95a573e0724cedf0dff63
BLAKE2b-256 c3ab23461cc257baa9a2535618481d3c6bc1baa8a7bfcaf8967a055db431c654

See more details on using hashes here.

File details

Details for the file varnapy-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: varnapy-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 63.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for varnapy-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8d11d5b53fc845ec8024daaeb68f5c9f57c38a0088df6cbd993de440fa32c5bb
MD5 09dde75d6ed3989a6061b942a5ae7d5a
BLAKE2b-256 91eaa0b6cc50a855acb639d129b85851823a8d9f4adc5d67e784a57414565af2

See more details on using hashes here.

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