VarnaPy — reactive Python web UI framework, build beautiful apps without JavaScript
Project description
VarnaPy
Reactive Python web UI framework — build beautiful apps without JavaScript.
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e7d2a9d5c67dae421e8ed3401988469df71486ea24c8e40a7cb3430756820a83
|
|
| MD5 |
17d95e6026b95a573e0724cedf0dff63
|
|
| BLAKE2b-256 |
c3ab23461cc257baa9a2535618481d3c6bc1baa8a7bfcaf8967a055db431c654
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8d11d5b53fc845ec8024daaeb68f5c9f57c38a0088df6cbd993de440fa32c5bb
|
|
| MD5 |
09dde75d6ed3989a6061b942a5ae7d5a
|
|
| BLAKE2b-256 |
91eaa0b6cc50a855acb639d129b85851823a8d9f4adc5d67e784a57414565af2
|