velo-ui 0.1.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.
- velo_ui-0.1.0/LICENSE +9 -0
- velo_ui-0.1.0/PKG-INFO +225 -0
- velo_ui-0.1.0/README.md +204 -0
- velo_ui-0.1.0/pyproject.toml +37 -0
- velo_ui-0.1.0/setup.cfg +4 -0
- velo_ui-0.1.0/tests/test_velo.py +157 -0
- velo_ui-0.1.0/velo/__init__.py +18 -0
- velo_ui-0.1.0/velo/__main__.py +4 -0
- velo_ui-0.1.0/velo/cli.py +135 -0
- velo_ui-0.1.0/velo/core.py +898 -0
- velo_ui-0.1.0/velo/markdown.py +112 -0
- velo_ui-0.1.0/velo/server.py +303 -0
- velo_ui-0.1.0/velo/static/app.css +293 -0
- velo_ui-0.1.0/velo/static/app.js +484 -0
- velo_ui-0.1.0/velo_ui.egg-info/PKG-INFO +225 -0
- velo_ui-0.1.0/velo_ui.egg-info/SOURCES.txt +17 -0
- velo_ui-0.1.0/velo_ui.egg-info/dependency_links.txt +1 -0
- velo_ui-0.1.0/velo_ui.egg-info/entry_points.txt +2 -0
- velo_ui-0.1.0/velo_ui.egg-info/top_level.txt +1 -0
velo_ui-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Muhammad Rafay Shaikh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
velo_ui-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: velo-ui
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Beautiful, reactive Python web apps in minutes. Zero dependencies.
|
|
5
|
+
Author: Muhammad Rafay Shaikh
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/rafay183/velo
|
|
8
|
+
Project-URL: Issues, https://github.com/rafay183/velo/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/rafay183/velo/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: web,ui,dashboard,streamlit alternative,gradio alternative,data apps
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Education
|
|
16
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# ⚡ Velo
|
|
23
|
+
|
|
24
|
+
[](https://github.com/rafay183/velo/actions/workflows/tests.yml)
|
|
25
|
+
[](https://pypi.org/project/velo-ui/)
|
|
26
|
+
[](#)
|
|
27
|
+
[](LICENSE)
|
|
28
|
+
[](#)
|
|
29
|
+
|
|
30
|
+
**Beautiful, reactive Python web apps in minutes. Zero dependencies.**
|
|
31
|
+
|
|
32
|
+
Velo lets anyone turn a Python script into a polished web app, with dashboards, forms, data tools and chat UIs. It runs on your laptop and deploys anywhere with one command. The whole framework is about 140 KB of pure-stdlib Python plus a 16 KB (gzipped) client.
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import velo
|
|
36
|
+
|
|
37
|
+
app = velo.App("Hello", accent="#6366f1")
|
|
38
|
+
|
|
39
|
+
@app.page("/")
|
|
40
|
+
def home(ui):
|
|
41
|
+
name = ui.input("Your name", placeholder="Ada")
|
|
42
|
+
n = ui.slider("How excited?", 1, 10, 7)
|
|
43
|
+
ui.title(f"Hello {name or 'world'}" + " 🎉" * n)
|
|
44
|
+
|
|
45
|
+
app.run()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
python app.py # or: velo run app.py
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
<p><img src="docs/dark.png" width="49%"> <img src="docs/data.png" width="49%"></p>
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Why Velo?
|
|
59
|
+
|
|
60
|
+
| | **Velo** | Typical script-rerun frameworks |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| Install size | **0 dependencies**, ~140 KB | Dozens of transitive packages |
|
|
63
|
+
| What re-runs on a click | **Only the page function** | Often the whole script |
|
|
64
|
+
| UI updates | **DOM morphing**: no flicker, focus and caret kept | Partial or full re-paint |
|
|
65
|
+
| Hot reload | **Keeps your state** (widgets and `ui.state` survive a save) | Session usually resets |
|
|
66
|
+
| Syntax error while editing | Overlay in the browser; app keeps running | Varies |
|
|
67
|
+
| Tables | Built-in search, sort and paging on the client | Varies |
|
|
68
|
+
| Charts | Built-in SVG line, area, bar and donut with tooltips, no deps | Usually needs a plotting library |
|
|
69
|
+
| Typing in inputs | Debounced; sliders throttled | Often one round trip per change |
|
|
70
|
+
| Live push from Python | `app.push()` from any thread (SSE) | Limited |
|
|
71
|
+
| Deploy | `velo docker` (vendors itself), honours `$PORT` | Extra setup |
|
|
72
|
+
| Theme | Light and dark with a toggle; one accent colour themes everything | Config files |
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Install
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install velo-ui
|
|
80
|
+
velo new myapp && cd myapp && velo run app.py
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Needs Python 3.9+. Nothing else.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## The 60-second tour
|
|
88
|
+
|
|
89
|
+
### Pages and navigation
|
|
90
|
+
Two or more pages give you an automatic sidebar nav with client-side routing.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
@app.page("/", title="Dashboard", icon="📊")
|
|
94
|
+
def dashboard(ui): ...
|
|
95
|
+
|
|
96
|
+
@app.page("/settings", title="Settings", icon="⚙️")
|
|
97
|
+
def settings(ui): ...
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Widgets return values
|
|
101
|
+
```python
|
|
102
|
+
name = ui.input("Name", placeholder="…") # also: type="password" | "email"
|
|
103
|
+
bio = ui.textarea("Bio", rows=4)
|
|
104
|
+
age = ui.number("Age", 30, min=0, max=120)
|
|
105
|
+
level = ui.slider("Level", 0, 100, 50, suffix="%")
|
|
106
|
+
city = ui.select("City", ["Karachi", "Lahore"]) # returns the object, any type
|
|
107
|
+
plan = ui.radio("Plan", ["Free", "Pro", "Team"]) # segmented control
|
|
108
|
+
tags = ui.multiselect("Tags", ["a", "b", "c"]) # chips
|
|
109
|
+
ok = ui.checkbox("I agree"); dark = ui.toggle("Dark mode")
|
|
110
|
+
day = ui.date("Start"); col = ui.color("Brand", "#6366f1")
|
|
111
|
+
f = ui.upload("CSV file", accept=".csv") # UploadedFile(.name .data .text())
|
|
112
|
+
if ui.button("Save", icon="💾"): ... # True on the click run
|
|
113
|
+
ui.download("Export", data, "report.csv")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### State and callbacks
|
|
117
|
+
```python
|
|
118
|
+
@app.page("/")
|
|
119
|
+
def counter(ui):
|
|
120
|
+
ui.init(count=0) # defaults, set once per browser session
|
|
121
|
+
|
|
122
|
+
def add(step):
|
|
123
|
+
ui.state.count += step
|
|
124
|
+
|
|
125
|
+
with ui.row():
|
|
126
|
+
ui.button("−", on_click=add, args=(-1,), kind="secondary")
|
|
127
|
+
ui.button("+", on_click=add, args=(1,))
|
|
128
|
+
ui.metric("Count", ui.state.count)
|
|
129
|
+
```
|
|
130
|
+
Callbacks run **before** the re-render, so the page always shows consistent state. Use `ui.get("key")` and `ui.set("key", v)` to read or write any widget that has a `key=`.
|
|
131
|
+
|
|
132
|
+
### Layout
|
|
133
|
+
```python
|
|
134
|
+
a, b, c = ui.columns(3) # or ui.columns([2, 1])
|
|
135
|
+
a.metric("Revenue", "$12.4k", "+8%") # containers proxy every component…
|
|
136
|
+
with b.card("Settings", "Optional subtitle"): # …or use `with`
|
|
137
|
+
ui.toggle("Enabled")
|
|
138
|
+
t1, t2 = ui.tabs(["Chart", "Data"]) # tabs switch instantly on the client
|
|
139
|
+
with ui.expander("Advanced"): ...
|
|
140
|
+
with ui.sidebar: ui.select("Region", [...])
|
|
141
|
+
with ui.row(): ... # horizontal button group
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Data and charts (pandas optional)
|
|
145
|
+
```python
|
|
146
|
+
ui.table(df) # DataFrame, list of dicts, dict of lists…
|
|
147
|
+
ui.line_chart(df, x="date", y=["sales", "cost"])
|
|
148
|
+
ui.area_chart(rows, x="day", y="revenue", title="Revenue")
|
|
149
|
+
ui.bar_chart(rows, x="day", y=["a", "b"], stacked=True)
|
|
150
|
+
ui.pie_chart({"k": ["A", "B"], "v": [60, 40]})
|
|
151
|
+
ui.metric("Users", 1520, delta=-12, inverse=True) # inverse: down is good
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Text and feedback
|
|
155
|
+
`ui.title`, `ui.header`, `ui.subheader`, `ui.text`, `ui.caption`, `ui.markdown` (safe), `ui.code`, `ui.json`, `ui.badge`, `ui.link`, `ui.image` (URL, path, bytes or PIL), `ui.progress`, `ui.info`, `ui.success`, `ui.warning`, `ui.error`, `ui.toast`, `ui.divider`, `ui.html` (trusted).
|
|
156
|
+
|
|
157
|
+
### Chat apps
|
|
158
|
+
```python
|
|
159
|
+
ui.init(msgs=[])
|
|
160
|
+
if q := ui.chat_input("Ask me…"):
|
|
161
|
+
ui.state.msgs += [("user", q), ("assistant", my_llm(q))]
|
|
162
|
+
for role, text in ui.state.msgs:
|
|
163
|
+
ui.chat_message(role, text)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Speed tools
|
|
167
|
+
```python
|
|
168
|
+
@velo.cache(ttl=300) # shared across sessions, TTL + LRU
|
|
169
|
+
def load_data(path): ...
|
|
170
|
+
|
|
171
|
+
ui.autorefresh(5) # live dashboards: re-run every 5 s while visible
|
|
172
|
+
app.push() # from a background thread: refresh all open browsers
|
|
173
|
+
app.push(ui.session_id) # …or just one
|
|
174
|
+
ui.rerun(); ui.navigate("/other")
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Deploy
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
velo run app.py --prod # 0.0.0.0, no reload, no tracebacks, gzip on
|
|
183
|
+
velo docker app.py # writes Dockerfile and vendors velo
|
|
184
|
+
docker build -t myapp . && docker run -p 8000:8000 myapp
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Velo reads `$PORT` and `$VELO_HOST`, so the same app runs unchanged on Render, Railway, Fly.io, Cloud Run, Koyeb, Heroku or a VPS. `/health` is there for load balancers. Behind nginx, turn off buffering for `/api/stream` (Velo already sends `X-Accel-Buffering: no`).
|
|
188
|
+
|
|
189
|
+
## CLI
|
|
190
|
+
|
|
191
|
+
| Command | What it does |
|
|
192
|
+
|---|---|
|
|
193
|
+
| `velo run app.py [--port N] [--host H] [--open] [--prod] [--no-reload]` | Run an app |
|
|
194
|
+
| `velo new NAME` | Scaffold a starter app |
|
|
195
|
+
| `velo docker [app.py] [--port N]` | Generate a Dockerfile and vendor Velo |
|
|
196
|
+
| `velo version` | Print the version |
|
|
197
|
+
|
|
198
|
+
## How it works
|
|
199
|
+
|
|
200
|
+
1. The browser opens one SSE stream (used for hot reload and server push) and sends small JSON events (`set`, `click`, `submit`) to `/api/render`.
|
|
201
|
+
2. The server applies the event to that browser's session, runs callbacks, then re-runs **only the page function**. That builds a compact component tree.
|
|
202
|
+
3. The client morphs the live DOM to match the tree. Unchanged nodes aren't touched, so focus, caret, scroll position, open tabs and table sort all survive.
|
|
203
|
+
4. When you save a file, the reloader re-executes your module, swaps the page functions into the running app and tells browsers to re-render. Sessions stay where they were.
|
|
204
|
+
|
|
205
|
+
## Customising the look
|
|
206
|
+
|
|
207
|
+
Pass `App(accent="#10b981", theme="dark", wide=True, icon="🚀")`, or override any CSS variable (`--accent`, `--radius`, `--font`…) with `ui.html("<style>:root{--r:6px}</style>")`.
|
|
208
|
+
|
|
209
|
+
## Current limits
|
|
210
|
+
|
|
211
|
+
- Sessions live in memory: use one process per app, or sticky sessions if you scale out.
|
|
212
|
+
- Uploads travel as base64 JSON (practical up to roughly 25 MB).
|
|
213
|
+
- No streaming of partial output inside a single run yet. For long jobs, use a background thread with `app.push()`.
|
|
214
|
+
|
|
215
|
+
## Testing
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
python -m pytest -q # or: python tests/test_velo.py
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Contributing
|
|
222
|
+
|
|
223
|
+
Contributions are very welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md), pick a **good first issue**, and say hi in Discussions. Please follow the [Code of Conduct](CODE_OF_CONDUCT.md).
|
|
224
|
+
|
|
225
|
+
MIT licensed. © 2026 Muhammad Rafay Shaikh
|
velo_ui-0.1.0/README.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# ⚡ Velo
|
|
2
|
+
|
|
3
|
+
[](https://github.com/rafay183/velo/actions/workflows/tests.yml)
|
|
4
|
+
[](https://pypi.org/project/velo-ui/)
|
|
5
|
+
[](#)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](#)
|
|
8
|
+
|
|
9
|
+
**Beautiful, reactive Python web apps in minutes. Zero dependencies.**
|
|
10
|
+
|
|
11
|
+
Velo lets anyone turn a Python script into a polished web app, with dashboards, forms, data tools and chat UIs. It runs on your laptop and deploys anywhere with one command. The whole framework is about 140 KB of pure-stdlib Python plus a 16 KB (gzipped) client.
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import velo
|
|
15
|
+
|
|
16
|
+
app = velo.App("Hello", accent="#6366f1")
|
|
17
|
+
|
|
18
|
+
@app.page("/")
|
|
19
|
+
def home(ui):
|
|
20
|
+
name = ui.input("Your name", placeholder="Ada")
|
|
21
|
+
n = ui.slider("How excited?", 1, 10, 7)
|
|
22
|
+
ui.title(f"Hello {name or 'world'}" + " 🎉" * n)
|
|
23
|
+
|
|
24
|
+
app.run()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
python app.py # or: velo run app.py
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
<p><img src="docs/dark.png" width="49%"> <img src="docs/data.png" width="49%"></p>
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Why Velo?
|
|
38
|
+
|
|
39
|
+
| | **Velo** | Typical script-rerun frameworks |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| Install size | **0 dependencies**, ~140 KB | Dozens of transitive packages |
|
|
42
|
+
| What re-runs on a click | **Only the page function** | Often the whole script |
|
|
43
|
+
| UI updates | **DOM morphing**: no flicker, focus and caret kept | Partial or full re-paint |
|
|
44
|
+
| Hot reload | **Keeps your state** (widgets and `ui.state` survive a save) | Session usually resets |
|
|
45
|
+
| Syntax error while editing | Overlay in the browser; app keeps running | Varies |
|
|
46
|
+
| Tables | Built-in search, sort and paging on the client | Varies |
|
|
47
|
+
| Charts | Built-in SVG line, area, bar and donut with tooltips, no deps | Usually needs a plotting library |
|
|
48
|
+
| Typing in inputs | Debounced; sliders throttled | Often one round trip per change |
|
|
49
|
+
| Live push from Python | `app.push()` from any thread (SSE) | Limited |
|
|
50
|
+
| Deploy | `velo docker` (vendors itself), honours `$PORT` | Extra setup |
|
|
51
|
+
| Theme | Light and dark with a toggle; one accent colour themes everything | Config files |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install velo-ui
|
|
59
|
+
velo new myapp && cd myapp && velo run app.py
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Needs Python 3.9+. Nothing else.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## The 60-second tour
|
|
67
|
+
|
|
68
|
+
### Pages and navigation
|
|
69
|
+
Two or more pages give you an automatic sidebar nav with client-side routing.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
@app.page("/", title="Dashboard", icon="📊")
|
|
73
|
+
def dashboard(ui): ...
|
|
74
|
+
|
|
75
|
+
@app.page("/settings", title="Settings", icon="⚙️")
|
|
76
|
+
def settings(ui): ...
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Widgets return values
|
|
80
|
+
```python
|
|
81
|
+
name = ui.input("Name", placeholder="…") # also: type="password" | "email"
|
|
82
|
+
bio = ui.textarea("Bio", rows=4)
|
|
83
|
+
age = ui.number("Age", 30, min=0, max=120)
|
|
84
|
+
level = ui.slider("Level", 0, 100, 50, suffix="%")
|
|
85
|
+
city = ui.select("City", ["Karachi", "Lahore"]) # returns the object, any type
|
|
86
|
+
plan = ui.radio("Plan", ["Free", "Pro", "Team"]) # segmented control
|
|
87
|
+
tags = ui.multiselect("Tags", ["a", "b", "c"]) # chips
|
|
88
|
+
ok = ui.checkbox("I agree"); dark = ui.toggle("Dark mode")
|
|
89
|
+
day = ui.date("Start"); col = ui.color("Brand", "#6366f1")
|
|
90
|
+
f = ui.upload("CSV file", accept=".csv") # UploadedFile(.name .data .text())
|
|
91
|
+
if ui.button("Save", icon="💾"): ... # True on the click run
|
|
92
|
+
ui.download("Export", data, "report.csv")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### State and callbacks
|
|
96
|
+
```python
|
|
97
|
+
@app.page("/")
|
|
98
|
+
def counter(ui):
|
|
99
|
+
ui.init(count=0) # defaults, set once per browser session
|
|
100
|
+
|
|
101
|
+
def add(step):
|
|
102
|
+
ui.state.count += step
|
|
103
|
+
|
|
104
|
+
with ui.row():
|
|
105
|
+
ui.button("−", on_click=add, args=(-1,), kind="secondary")
|
|
106
|
+
ui.button("+", on_click=add, args=(1,))
|
|
107
|
+
ui.metric("Count", ui.state.count)
|
|
108
|
+
```
|
|
109
|
+
Callbacks run **before** the re-render, so the page always shows consistent state. Use `ui.get("key")` and `ui.set("key", v)` to read or write any widget that has a `key=`.
|
|
110
|
+
|
|
111
|
+
### Layout
|
|
112
|
+
```python
|
|
113
|
+
a, b, c = ui.columns(3) # or ui.columns([2, 1])
|
|
114
|
+
a.metric("Revenue", "$12.4k", "+8%") # containers proxy every component…
|
|
115
|
+
with b.card("Settings", "Optional subtitle"): # …or use `with`
|
|
116
|
+
ui.toggle("Enabled")
|
|
117
|
+
t1, t2 = ui.tabs(["Chart", "Data"]) # tabs switch instantly on the client
|
|
118
|
+
with ui.expander("Advanced"): ...
|
|
119
|
+
with ui.sidebar: ui.select("Region", [...])
|
|
120
|
+
with ui.row(): ... # horizontal button group
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Data and charts (pandas optional)
|
|
124
|
+
```python
|
|
125
|
+
ui.table(df) # DataFrame, list of dicts, dict of lists…
|
|
126
|
+
ui.line_chart(df, x="date", y=["sales", "cost"])
|
|
127
|
+
ui.area_chart(rows, x="day", y="revenue", title="Revenue")
|
|
128
|
+
ui.bar_chart(rows, x="day", y=["a", "b"], stacked=True)
|
|
129
|
+
ui.pie_chart({"k": ["A", "B"], "v": [60, 40]})
|
|
130
|
+
ui.metric("Users", 1520, delta=-12, inverse=True) # inverse: down is good
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Text and feedback
|
|
134
|
+
`ui.title`, `ui.header`, `ui.subheader`, `ui.text`, `ui.caption`, `ui.markdown` (safe), `ui.code`, `ui.json`, `ui.badge`, `ui.link`, `ui.image` (URL, path, bytes or PIL), `ui.progress`, `ui.info`, `ui.success`, `ui.warning`, `ui.error`, `ui.toast`, `ui.divider`, `ui.html` (trusted).
|
|
135
|
+
|
|
136
|
+
### Chat apps
|
|
137
|
+
```python
|
|
138
|
+
ui.init(msgs=[])
|
|
139
|
+
if q := ui.chat_input("Ask me…"):
|
|
140
|
+
ui.state.msgs += [("user", q), ("assistant", my_llm(q))]
|
|
141
|
+
for role, text in ui.state.msgs:
|
|
142
|
+
ui.chat_message(role, text)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Speed tools
|
|
146
|
+
```python
|
|
147
|
+
@velo.cache(ttl=300) # shared across sessions, TTL + LRU
|
|
148
|
+
def load_data(path): ...
|
|
149
|
+
|
|
150
|
+
ui.autorefresh(5) # live dashboards: re-run every 5 s while visible
|
|
151
|
+
app.push() # from a background thread: refresh all open browsers
|
|
152
|
+
app.push(ui.session_id) # …or just one
|
|
153
|
+
ui.rerun(); ui.navigate("/other")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Deploy
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
velo run app.py --prod # 0.0.0.0, no reload, no tracebacks, gzip on
|
|
162
|
+
velo docker app.py # writes Dockerfile and vendors velo
|
|
163
|
+
docker build -t myapp . && docker run -p 8000:8000 myapp
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Velo reads `$PORT` and `$VELO_HOST`, so the same app runs unchanged on Render, Railway, Fly.io, Cloud Run, Koyeb, Heroku or a VPS. `/health` is there for load balancers. Behind nginx, turn off buffering for `/api/stream` (Velo already sends `X-Accel-Buffering: no`).
|
|
167
|
+
|
|
168
|
+
## CLI
|
|
169
|
+
|
|
170
|
+
| Command | What it does |
|
|
171
|
+
|---|---|
|
|
172
|
+
| `velo run app.py [--port N] [--host H] [--open] [--prod] [--no-reload]` | Run an app |
|
|
173
|
+
| `velo new NAME` | Scaffold a starter app |
|
|
174
|
+
| `velo docker [app.py] [--port N]` | Generate a Dockerfile and vendor Velo |
|
|
175
|
+
| `velo version` | Print the version |
|
|
176
|
+
|
|
177
|
+
## How it works
|
|
178
|
+
|
|
179
|
+
1. The browser opens one SSE stream (used for hot reload and server push) and sends small JSON events (`set`, `click`, `submit`) to `/api/render`.
|
|
180
|
+
2. The server applies the event to that browser's session, runs callbacks, then re-runs **only the page function**. That builds a compact component tree.
|
|
181
|
+
3. The client morphs the live DOM to match the tree. Unchanged nodes aren't touched, so focus, caret, scroll position, open tabs and table sort all survive.
|
|
182
|
+
4. When you save a file, the reloader re-executes your module, swaps the page functions into the running app and tells browsers to re-render. Sessions stay where they were.
|
|
183
|
+
|
|
184
|
+
## Customising the look
|
|
185
|
+
|
|
186
|
+
Pass `App(accent="#10b981", theme="dark", wide=True, icon="🚀")`, or override any CSS variable (`--accent`, `--radius`, `--font`…) with `ui.html("<style>:root{--r:6px}</style>")`.
|
|
187
|
+
|
|
188
|
+
## Current limits
|
|
189
|
+
|
|
190
|
+
- Sessions live in memory: use one process per app, or sticky sessions if you scale out.
|
|
191
|
+
- Uploads travel as base64 JSON (practical up to roughly 25 MB).
|
|
192
|
+
- No streaming of partial output inside a single run yet. For long jobs, use a background thread with `app.push()`.
|
|
193
|
+
|
|
194
|
+
## Testing
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
python -m pytest -q # or: python tests/test_velo.py
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Contributing
|
|
201
|
+
|
|
202
|
+
Contributions are very welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md), pick a **good first issue**, and say hi in Discussions. Please follow the [Code of Conduct](CODE_OF_CONDUCT.md).
|
|
203
|
+
|
|
204
|
+
MIT licensed. © 2026 Muhammad Rafay Shaikh
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "velo-ui"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Beautiful, reactive Python web apps in minutes. Zero dependencies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Muhammad Rafay Shaikh" }]
|
|
14
|
+
keywords = ["web", "ui", "dashboard", "streamlit alternative", "gradio alternative", "data apps"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Intended Audience :: Education",
|
|
21
|
+
"Topic :: Software Development :: User Interfaces",
|
|
22
|
+
]
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/rafay183/velo"
|
|
27
|
+
Issues = "https://github.com/rafay183/velo/issues"
|
|
28
|
+
Changelog = "https://github.com/rafay183/velo/blob/main/CHANGELOG.md"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
velo = "velo.cli:main"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools]
|
|
34
|
+
packages = ["velo"]
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.package-data]
|
|
37
|
+
velo = ["static/*"]
|
velo_ui-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Run: python -m pytest -q (or: python tests/test_velo.py)"""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
import urllib.request
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
10
|
+
import velo # noqa: E402
|
|
11
|
+
from velo import server # noqa: E402
|
|
12
|
+
from velo.markdown import render as md # noqa: E402
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def make_app():
|
|
16
|
+
app = velo.App("Test")
|
|
17
|
+
|
|
18
|
+
@app.page("/")
|
|
19
|
+
def home(ui):
|
|
20
|
+
ui.init(n=0)
|
|
21
|
+
|
|
22
|
+
def inc(step=1):
|
|
23
|
+
ui.state.n += step
|
|
24
|
+
|
|
25
|
+
ui.button("Add", on_click=inc, args=(2,))
|
|
26
|
+
ui.text(f"n={ui.state.n}")
|
|
27
|
+
v = ui.slider("S", 0, 10, 5)
|
|
28
|
+
c = ui.select("C", [1, 2, 3], value=2)
|
|
29
|
+
m = ui.multiselect("M", ["a", "b", "c"], value=["a"])
|
|
30
|
+
t = ui.toggle("T")
|
|
31
|
+
ui.text(f"{v}|{c!r}|{m}|{t}")
|
|
32
|
+
if ui.button("Boom"):
|
|
33
|
+
raise RuntimeError("kaboom")
|
|
34
|
+
msg = ui.chat_input("say")
|
|
35
|
+
if msg:
|
|
36
|
+
ui.text(f"got:{msg}")
|
|
37
|
+
|
|
38
|
+
@app.page("/two", title="Two")
|
|
39
|
+
def two(ui):
|
|
40
|
+
ui.title("Page two")
|
|
41
|
+
return app
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def texts(out):
|
|
45
|
+
return [n["p"]["text"] for n in out["tree"] if n["t"] == "text"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_initial_render_and_nav():
|
|
49
|
+
out = make_app().dispatch("s", "/", None)
|
|
50
|
+
assert texts(out)[:2] == ["n=0", "5|2|['a']|False"]
|
|
51
|
+
assert [p["path"] for p in out["nav"]] == ["/", "/two"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_callbacks_and_values():
|
|
55
|
+
app = make_app()
|
|
56
|
+
app.dispatch("s", "/", None)
|
|
57
|
+
app.dispatch("s", "/", {"type": "click", "k": "button:Add"})
|
|
58
|
+
app.dispatch("s", "/", {"type": "set", "k": "slider:S", "v": 99}) # clamped to max
|
|
59
|
+
app.dispatch("s", "/", {"type": "set", "k": "select:C", "v": 2}) # index -> option
|
|
60
|
+
app.dispatch("s", "/", {"type": "set", "k": "multiselect:M", "v": [1, 2]})
|
|
61
|
+
out = app.dispatch("s", "/", {"type": "set", "k": "toggle:T", "v": True})
|
|
62
|
+
assert texts(out)[:2] == ["n=2", "10|3|['b', 'c']|True"]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_sessions_are_isolated():
|
|
66
|
+
app = make_app()
|
|
67
|
+
app.dispatch("a", "/", None)
|
|
68
|
+
app.dispatch("a", "/", {"type": "click", "k": "button:Add"})
|
|
69
|
+
assert texts(app.dispatch("b", "/", None))[0] == "n=0"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_invalid_values_ignored():
|
|
73
|
+
app = make_app()
|
|
74
|
+
app.dispatch("s", "/", None)
|
|
75
|
+
out = app.dispatch("s", "/", {"type": "set", "k": "select:C", "v": 99})
|
|
76
|
+
assert "5|2|" in texts(out)[1]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_errors_are_contained():
|
|
80
|
+
app = make_app()
|
|
81
|
+
app.dispatch("s", "/", None)
|
|
82
|
+
out = app.dispatch("s", "/", {"type": "click", "k": "button:Boom"})
|
|
83
|
+
err = [n for n in out["tree"] if n["t"] == "error"]
|
|
84
|
+
assert err and "kaboom" in err[0]["p"]["text"]
|
|
85
|
+
assert "velo/core.py" not in err[0]["p"]["tb"] # framework frames hidden
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_chat_input_returns_once():
|
|
89
|
+
app = make_app()
|
|
90
|
+
app.dispatch("s", "/", None)
|
|
91
|
+
out = app.dispatch("s", "/", {"type": "submit", "k": "chat_input:say", "v": "hi"})
|
|
92
|
+
assert "got:hi" in texts(out)
|
|
93
|
+
assert "got:hi" not in texts(app.dispatch("s", "/", None))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_404():
|
|
97
|
+
out = make_app().dispatch("s", "/nope", None)
|
|
98
|
+
assert out["tree"][0]["t"] == "notfound"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_cache():
|
|
102
|
+
calls = []
|
|
103
|
+
|
|
104
|
+
@velo.cache(ttl=60)
|
|
105
|
+
def f(x):
|
|
106
|
+
calls.append(x)
|
|
107
|
+
return x * 2
|
|
108
|
+
|
|
109
|
+
assert f(2) == 4 and f(2) == 4 and f(3) == 6
|
|
110
|
+
assert calls == [2, 3]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_tabular_shapes():
|
|
114
|
+
from velo.core import _tabular, _chart_data
|
|
115
|
+
assert _tabular([{"a": 1}, {"a": 2, "b": 3}]) == (["a", "b"], [[1, None], [2, 3]])
|
|
116
|
+
assert _tabular({"x": [1, 2], "y": [3, 4]}) == (["x", "y"], [[1, 3], [2, 4]])
|
|
117
|
+
xs, s = _chart_data({"day": ["a", "b"], "v": [1, 2]})
|
|
118
|
+
assert xs == ["a", "b"] and s[0]["name"] == "v"
|
|
119
|
+
try:
|
|
120
|
+
import pandas as pd
|
|
121
|
+
cols, rows = _tabular(pd.DataFrame({"a": [1, 2], "b": [0.5, float("nan")]}))
|
|
122
|
+
assert cols == ["a", "b"] and rows[1] == [2, None]
|
|
123
|
+
except ImportError:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_markdown_escapes_html():
|
|
128
|
+
out = md("**hi** <script>x</script> [l](javascript:alert(1))")
|
|
129
|
+
assert "<strong>hi</strong>" in out and "<script>" not in out and "javascript:" not in out
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_http_roundtrip():
|
|
133
|
+
app = make_app()
|
|
134
|
+
httpd = server._Server(("127.0.0.1", 0), server._Handler)
|
|
135
|
+
httpd.app = app
|
|
136
|
+
threading.Thread(target=httpd.serve_forever, daemon=True).start()
|
|
137
|
+
base = f"http://127.0.0.1:{httpd.server_address[1]}"
|
|
138
|
+
try:
|
|
139
|
+
assert b"window.VELO" in urllib.request.urlopen(base + "/").read()
|
|
140
|
+
assert urllib.request.urlopen(base + "/_velo/app.js").status == 200
|
|
141
|
+
assert json.loads(urllib.request.urlopen(base + "/health").read())["ok"]
|
|
142
|
+
req = urllib.request.Request(base + "/api/render", data=json.dumps(
|
|
143
|
+
{"sid": "h", "path": "/", "event": {"type": "click", "k": "button:Add"}}).encode())
|
|
144
|
+
app.dispatch("h", "/", None)
|
|
145
|
+
out = json.loads(urllib.request.urlopen(req).read())
|
|
146
|
+
assert texts(out)[0] == "n=2"
|
|
147
|
+
finally:
|
|
148
|
+
httpd.shutdown()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
t0 = time.time()
|
|
153
|
+
fns = [v for k, v in dict(globals()).items() if k.startswith("test_")]
|
|
154
|
+
for fn in fns:
|
|
155
|
+
fn()
|
|
156
|
+
print("✓", fn.__name__)
|
|
157
|
+
print(f"\n{len(fns)} passed in {time.time() - t0:.2f}s")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Velo — beautiful, reactive Python web apps. Zero dependencies.
|
|
2
|
+
|
|
3
|
+
import velo
|
|
4
|
+
|
|
5
|
+
app = velo.App("Hello")
|
|
6
|
+
|
|
7
|
+
@app.page("/")
|
|
8
|
+
def home(ui):
|
|
9
|
+
name = ui.input("Your name")
|
|
10
|
+
ui.title(f"Hello, {name or 'world'} 👋")
|
|
11
|
+
|
|
12
|
+
app.run()
|
|
13
|
+
"""
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
|
|
16
|
+
from .core import App, UI, State, UploadedFile, Rerun, cache # noqa: E402
|
|
17
|
+
|
|
18
|
+
__all__ = ["App", "UI", "State", "UploadedFile", "Rerun", "cache", "__version__"]
|