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 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
+ [![tests](https://github.com/rafay183/velo/actions/workflows/tests.yml/badge.svg)](https://github.com/rafay183/velo/actions/workflows/tests.yml)
25
+ [![PyPI](https://img.shields.io/pypi/v/velo-ui.svg)](https://pypi.org/project/velo-ui/)
26
+ [![Python](https://img.shields.io/badge/python-3.9%2B-blue)](#)
27
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
28
+ [![Zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](#)
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
+ ![Dashboard](docs/dashboard.png)
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
@@ -0,0 +1,204 @@
1
+ # ⚡ Velo
2
+
3
+ [![tests](https://github.com/rafay183/velo/actions/workflows/tests.yml/badge.svg)](https://github.com/rafay183/velo/actions/workflows/tests.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/velo-ui.svg)](https://pypi.org/project/velo-ui/)
5
+ [![Python](https://img.shields.io/badge/python-3.9%2B-blue)](#)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
7
+ [![Zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](#)
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
+ ![Dashboard](docs/dashboard.png)
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/*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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__"]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()