patx/veela
Rebrand Wred-embedded as Veela
Commit 81d27e8 · patx · 2026-10-14T00:33:01-04:00
Comments
No comments yet.
Diff
diff --git a/.coveragerc b/.coveragerc
index da34f9d..c91bfe1 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,7 +1,7 @@
[run]
branch = True
source =
- wred
+ veela
[report]
fail_under = 100
diff --git a/README.md b/README.md
index 86410f6..940b548 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,22 @@
-# Wred-embedded
+# Veela
+
+<img src="https://patx.gitman.io/veela/veela-logo.png" alt="Veela, a magical gremlin mascot with long silver hair" width="180">
A magical, single-file Python framework for simple JavaScript web apps.
+Website: <https://harrisonerd.com/veela>
+
+Named after the Veela from Harry Potter. The framework brings its own magic.
+
For years, developers have suffered under the tyranny of installing a web server
before running a web server. Finally, someone had the courage to ask: what if
there were only one file visible in the folder?
-Introducing **Wred-embedded**. All the convenience of copying a file, with the
+Introducing **Veela**. All the convenience of copying a file, with the
architectural confidence of having copied a file.
No pip install. No dependency resolution. No awkward questions about why
-`wred.py` is that big. Just grab it and go. The technology is incredible.
+`veela.py` is that big. Just grab it and go. The technology is incredible.
## Experience the breakthrough
@@ -18,7 +24,7 @@ You'll need Python 3.11+. We haven't worked out how to put Python in the file
yet. Please respect our privacy during this difficult time.
```bash
-wget https://gitman.io/patx/wred-embedded/raw/wred.py
+wget https://gitman.io/patx/veela/raw/veela.py
```
Put it next to your application. That's installation now. You're welcome.
@@ -26,7 +32,7 @@ Put it next to your application. That's installation now. You're welcome.
Save this as `app.py`:
```python
-from wred import App
+from veela import App
app = App()
@@ -48,7 +54,7 @@ but one of them is yours, so that doesn't count.
What a lovely question. Look how tidy your project directory is.
-Wred-embedded brings the server with it. Nothing to install separately, nothing
+Veela brings the server with it. Nothing to install separately, nothing
to download at runtime. Just a completely ordinary Python file with a very
rich inner life.
@@ -109,7 +115,7 @@ preserve the thrill of discovery.
Repeated query keys keep the last value; blank values are preserved. Input
validation is your department. Malformed JSON and invalid UTF-8 query strings
-get JSON 400 responses. Raise `WredRequestError` for your own validation errors.
+get JSON 400 responses. Raise `VeelaRequestError` for your own validation errors.
### Things you can give back
@@ -174,8 +180,8 @@ ASGI server. That does involve installing something. We'll try to be brave.
## For those who insist on looking inside
-The readable framework source lives in [source/wred.py](source/wred.py).
-The distributed [wred.py](wred.py) is generated. Please edit the former;
+The readable framework source lives in [source/veela.py](source/veela.py).
+The distributed [veela.py](veela.py) is generated. Please edit the former;
the latter is enjoying a carefully curated lifestyle.
To regenerate it:
@@ -191,10 +197,17 @@ This is a build step; users running the generated file don't need it.
If you encounter a large encoded string, that's the innovation. Please don't
unfold it during the demo.
+## Migrating from Wred-embedded
+
+Replace `wred.py` with `veela.py` and change `from wred import ...` to
+`from veela import ...`. The public errors are now `VeelaRequestError` and
+`VeelaResponseError` (formerly `WredRequestError` and `WredResponseError`).
+The old module and error names are no longer provided; the API behavior is unchanged.
+
## Acknowledgments and extremely normal paperwork
Based on [Wred](https://harrisonerd.com/wred), which had the good sense to stop
-before this happened. Wred-embedded is the fork that kept going, encouraged by
+before this happened. Veela is the fork that kept going, encouraged by
absolutely nobody's requirements document.
See [LICENSE](LICENSE) and [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt)
diff --git a/docs/branding-prompts.txt b/docs/branding-prompts.txt
new file mode 100644
index 0000000..5f0065a
--- /dev/null
+++ b/docs/branding-prompts.txt
@@ -0,0 +1,12 @@
+Veela branding image prompts
+Generated with the built-in ImageGen tool.
+
+Mascot design (reference: original Wred-embedded gremlin mascot):
+Use case: logo-brand. Reimagine the supplied old gremlin mascot as Veela: a funny charming FEMALE GREMLIN WITH VERY LONG SILVER HAIR, for a magical Python framework. This reference is the style and silhouette guide. Keep the reference mascot’s extremely simple chunky cartoon design, thick dark burgundy outlines, giant friendly eyes, huge pointed gremlin ears, mischievous open grin with a tiny fang, short rounded arms, stubby legs and big cartoon feet, raised mitten hand doing two golden sparkles. Female coding comes from big flowing silver-white hair reaching the feet, simple swept fringe and tiny eyelashes. A compact round red/rose little magical creature, about two heads tall, oversized head blending into simple squat body. Modest simple burgundy tunic if needed. Hair should be three or four big smooth graphic locks, NOT individually rendered strands. Pale cream face and silver hair with warm rose shadows; red/rose body; gold sparkles. Preserve the old logo’s friendly software-mascot simplicity and bold readable silhouette. Remove the old server vents, server light, paper corner and tape. No realistic woman, human anatomy, bust, cleavage, long human legs, elaborate robes, anime princess, jewelry, V symbol, monogram or letters. No text. Transparent background, square canvas, whole body and all hair comfortably inside with padding. The result should look like the old gremlin’s long-haired female cousin, not an elegant human fairy.
+
+Final palette edit (reference: generated long-haired gremlin):
+Use case: precise-object-edit. Recolor this exact long-haired female gremlin mascot into a new deep teal, sea-glass green, silver and warm gold brand palette. Preserve EXACTLY the character design, pose, silhouette, chunky proportions, giant eyes, pointed ears, mischievous fang grin, flowing long hair, tunic, mitten hands, feet, two sparkles and bold cartoon illustration style. Replace all red skin/body areas with sea-glass turquoise #66b8ac with darker teal shading. Replace burgundy outlines and eye color with deep midnight teal #133f48. Tunic deep teal #176b70. Hair stays silver-white, with pale cool mint shadows instead of pink. Face and inner ears pale mint-cream. Gold sparkles stay warm gold. No red, burgundy or pink anywhere. No new shapes, symbols, text, V mark or human anatomy. Keep full character and hair inside the square canvas with padding. Genuinely transparent background.
+
+Social card (reference: docs/veela-logo.png):
+undefined
+
diff --git a/docs/index.html b/docs/index.html
index 2db02aa..f357f97 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -3,10 +3,17 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
- <meta name="description" content="Wred is a tiny Python/ASGI backend for HTML and JavaScript apps.">
- <title>Wred-embedded | Single file Python/ASGI framework and webserver</title>
+ <meta name="description" content="Veela is a tiny Python/ASGI backend for HTML and JavaScript apps.">
+ <title>Veela | Single file Python/ASGI framework and webserver</title>
+ <meta property="og:title" content="Veela">
+ <meta property="og:description" content="A magical, single-file Python framework for simple JavaScript web apps.">
+ <meta property="og:type" content="website">
+ <meta property="og:image" content="https://patx.gitman.io/veela/social-card.png">
+ <meta property="og:image:alt" content="Veela: a magical, single-file Python framework, with its long-haired gremlin mascot.">
+ <meta property="og:url" content="https://harrisonerd.com/veela">
+ <meta name="twitter:card" content="summary_large_image">
<style>
- :root { color-scheme: light; font-family: Arial, Helvetica, sans-serif; color: #702a36; background: #fff8f8; }
+ :root { color-scheme: light; font-family: Arial, Helvetica, sans-serif; color: #133f48; background: #f4fbf9; }
* { box-sizing: border-box; }
body { margin: 0; }
main { width: 100%; }
@@ -15,12 +22,12 @@
h1 img { display: block; width: min(160px, 100%, 21dvh); height: auto; margin: auto; }
.intro p { margin: 8px auto 0; max-width: 760px; font-size: 18px; line-height: 1.4; }
a { color: inherit; text-underline-offset: 4px; }
- a:hover { color: #c92432; }
- a:focus-visible { outline: 2px solid #c92432; outline-offset: 5px; }
+ a:hover { color: #146e73; }
+ a:focus-visible { outline: 2px solid #146e73; outline-offset: 5px; }
footer { display: flex; justify-content: center; flex-wrap: wrap; gap: 16px 32px; padding: 16px 24px; font-size: 15px; }
.examples { max-width: 800px; margin: 0 auto; padding: 0 24px; }
- .examples pre { margin-bottom: 12px; background: #fff0f0; border: 1px solid #dc8994; border-radius: 6px; }
- pre { margin: 0; padding: 12px 16px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; color: #702a36; font: 16px/1.4 ui-monospace, SFMono-Regular, Consolas, monospace; tab-size: 2; }
+ .examples pre { margin-bottom: 12px; background: #e7f4f1; border: 1px solid #85b6b2; border-radius: 6px; }
+ pre { margin: 0; padding: 12px 16px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; color: #133f48; font: 16px/1.4 ui-monospace, SFMono-Regular, Consolas, monospace; tab-size: 2; }
.install-command { white-space: pre; overflow-wrap: normal; overflow-x: auto; }
code { font: inherit; }
@media (max-width: 700px) {
@@ -32,12 +39,12 @@
<body>
<main>
<header class="intro">
- <h1><a href="https://harrisonerd.com/wred-embedded"><img src="https://patx.gitman.io/wred-embedded/wred-embedded-logo.png" alt="Wred-embedded" width="1254" height="1254"></a></h1>
- <p>Wred-embedded is a magical, single-file Python framework for simple JavaScript web apps. Just grab <a target="_blank" href="https://gitman.io/patx/wred-embedded/src/wred.py">wred.py</a> and go! View the documentation and source <a target="_blank" href="https://gitman.io/patx/wred-embedded">here</a>.</p>
+ <h1><a href="https://harrisonerd.com/veela"><img src="https://patx.gitman.io/veela/veela-logo.png" alt="Veela" width="1254" height="1254"></a></h1>
+ <p>Veela is a magical, single-file Python framework for simple JavaScript web apps. Just grab <a target="_blank" href="https://gitman.io/patx/veela/src/veela.py">veela.py</a> and go! View the documentation and source <a target="_blank" href="https://gitman.io/patx/veela">here</a>.</p>
</header>
<section class="examples" aria-label="Installation and example">
- <pre class="install-command" aria-label="Install"><code>wget https://gitman.io/patx/wred-embedded/raw/wred.py</code></pre>
- <pre aria-label="app.py"><code>from wred import App
+ <pre class="install-command" aria-label="Install"><code>wget https://gitman.io/patx/veela/raw/veela.py</code></pre>
+ <pre aria-label="app.py"><code>from veela import App
app = App()
diff --git a/docs/social-card.png b/docs/social-card.png
index 5b9645f..a831169 100644
Binary files a/docs/social-card.png and b/docs/social-card.png differ
diff --git a/docs/veela-logo.png b/docs/veela-logo.png
new file mode 100644
index 0000000..83360e7
Binary files /dev/null and b/docs/veela-logo.png differ
diff --git a/docs/wred-embedded-logo.png b/docs/wred-embedded-logo.png
deleted file mode 100644
index a51780d..0000000
Binary files a/docs/wred-embedded-logo.png and /dev/null differ
diff --git a/docs/wred-logo.png b/docs/wred-logo.png
deleted file mode 100644
index bec2a6f..0000000
Binary files a/docs/wred-logo.png and /dev/null differ
diff --git a/example_app/app.py b/example_app/app.py
index fe00cca..32e7440 100644
--- a/example_app/app.py
+++ b/example_app/app.py
@@ -7,7 +7,7 @@ Login is restored with an HttpOnly refresh cookie; API calls use bearer tokens.
Storage and rate limits are process-local. Production needs shared storage and HTTPS.
"""
-from wred import App
+from veela import App
from . import auth, notes, pages
diff --git a/example_app/auth.py b/example_app/auth.py
index a8df158..1625d40 100644
--- a/example_app/auth.py
+++ b/example_app/auth.py
@@ -7,7 +7,7 @@ from http.cookies import CookieError, SimpleCookie
from secrets import compare_digest, token_bytes, token_urlsafe
from time import monotonic
-from wred import WredRequestError
+from veela import VeelaRequestError
from .rate_limit import rate_limit
from .storage import users, tokens, refresh_tokens
@@ -80,13 +80,13 @@ def revoke_login(login_id):
async def read_credentials(request):
data = await request.json()
if not isinstance(data, dict):
- raise WredRequestError('Send a JSON object with username and password')
+ raise VeelaRequestError('Send a JSON object with username and password')
username, password = data.get('username'), data.get('password')
if not isinstance(username, str) or not isinstance(password, str):
- raise WredRequestError('Username and password must be strings')
+ raise VeelaRequestError('Username and password must be strings')
username = username.strip().casefold()
if not 1 <= len(username) <= 64 or not 15 <= len(password) <= 128:
- raise WredRequestError('Use a username of 1–64 and a password of 15–128 characters')
+ raise VeelaRequestError('Use a username of 1–64 and a password of 15–128 characters')
return username, password
@@ -134,7 +134,7 @@ def protected(handler):
def register_routes(app):
- """Register authentication endpoints on the supplied Wred app."""
+ """Register authentication endpoints on the supplied Veela app."""
@app.route('/api/signup', methods=['POST'])
@rate_limit(requests=10, seconds=60)
diff --git a/example_app/notes.py b/example_app/notes.py
index 1f8efaa..b6b01a7 100644
--- a/example_app/notes.py
+++ b/example_app/notes.py
@@ -3,7 +3,7 @@
from datetime import UTC, datetime
from secrets import token_urlsafe
-from wred import WredRequestError
+from veela import VeelaRequestError
from .auth import protected
from .storage import notes
@@ -11,17 +11,17 @@ from .storage import notes
async def read_note(request, partial=False):
data = await request.json()
if not isinstance(data, dict) or not data or data.keys() - {'title', 'body', 'pinned'}:
- raise WredRequestError('Send an object containing title, body, or pinned')
+ raise VeelaRequestError('Send an object containing title, body, or pinned')
if not partial and 'title' not in data:
- raise WredRequestError('A title is required')
+ raise VeelaRequestError('A title is required')
if 'title' in data and (
not isinstance(data['title'], str) or not 1 <= len(data['title'].strip()) <= 100
):
- raise WredRequestError('Title must contain 1–100 characters')
+ raise VeelaRequestError('Title must contain 1–100 characters')
if 'body' in data and (not isinstance(data['body'], str) or len(data['body']) > 10_000):
- raise WredRequestError('Body must be a string of at most 10,000 characters')
+ raise VeelaRequestError('Body must be a string of at most 10,000 characters')
if 'pinned' in data and not isinstance(data['pinned'], bool):
- raise WredRequestError('Pinned must be a boolean')
+ raise VeelaRequestError('Pinned must be a boolean')
if 'title' in data:
data['title'] = data['title'].strip()
return data if partial else {'body': '', 'pinned': False, **data}
@@ -44,19 +44,19 @@ def register_routes(app):
async def list_notes(request):
# Keep this demo's query budget at 20 fields, including repeated keys.
if request.scope.get('query_string', b'').count(b'&') >= 20:
- raise WredRequestError('Invalid query string')
+ raise VeelaRequestError('Invalid query string')
query = request.query()
try:
limit = int(query.get('limit') or '5')
offset = int(query.get('offset') or '0')
except ValueError:
- raise WredRequestError('Limit and offset must be integers')
+ raise VeelaRequestError('Limit and offset must be integers')
if not 1 <= limit <= 50 or offset < 0:
- raise WredRequestError('Limit must be 1–50; offset must be zero or greater')
+ raise VeelaRequestError('Limit must be 1–50; offset must be zero or greater')
search = query.get('q', '').strip().casefold()
pinned = query.get('pinned', '')
if pinned not in ('', 'true', 'false'):
- raise WredRequestError('Pinned filter must be true or false')
+ raise VeelaRequestError('Pinned filter must be true or false')
# Apply ownership before searching or counting so other users' data never leaks.
items = [note for note in notes.values() if note['owner'] == request.user['id']]
items = [
diff --git a/example_app/rate_limit.py b/example_app/rate_limit.py
index 22ecccb..d71b317 100644
--- a/example_app/rate_limit.py
+++ b/example_app/rate_limit.py
@@ -1,4 +1,4 @@
-"""Application-owned sliding-window rate limiting, independent of Wred."""
+"""Application-owned sliding-window rate limiting, independent of Veela."""
from collections import deque
from functools import wraps
diff --git a/example_app/static/app.css b/example_app/static/app.css
index 787478b..0c59542 100644
--- a/example_app/static/app.css
+++ b/example_app/static/app.css
@@ -1,11 +1,11 @@
:root {
color-scheme: light;
- --ink: #172746;
- --muted: #64748b;
- --blue: #2457d6;
+ --ink: #133f48;
+ --muted: #58736e;
+ --accent: #176b70;
--paper: #fff;
- --canvas: #f5f7fc;
- --line: #d7e0ee;
+ --canvas: #f4fbf9;
+ --line: #c9ded8;
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
color: var(--ink);
background: var(--canvas);
@@ -18,20 +18,20 @@ button, input, textarea { font: inherit; }
button { cursor: pointer; }
button:disabled { cursor: default; opacity: .45; }
a { color: inherit; }
-button:focus-visible, a:focus-visible { outline: 3px solid #9bb9ff; outline-offset: 4px; }
-input:focus, textarea:focus { outline: 2px solid #9bb9ff; outline-offset: 2px; border-color: var(--blue); }
+button:focus-visible, a:focus-visible { outline: 3px solid #69aaa4; outline-offset: 4px; }
+input:focus, textarea:focus { outline: 2px solid #69aaa4; outline-offset: 2px; border-color: var(--accent); }
button { border: 0; border-radius: 6px; padding: 11px 16px; font-size: 14px; font-weight: 600; }
-.primary { color: #fff; background: var(--blue); }
-.primary:hover:not(:disabled) { background: #1944b1; }
+.primary { color: #fff; background: var(--accent); }
+.primary:hover:not(:disabled) { background: #105459; }
.quiet { color: var(--ink); background: transparent; }
-.quiet:hover:not(:disabled) { background: #e9eef9; }
+.quiet:hover:not(:disabled) { background: #e5f2ee; }
.danger { color: #a1353d; background: transparent; }
.danger:hover:not(:disabled) { background: #fcebee; }
.small { padding: 9px 12px; white-space: nowrap; }
.shell { max-width: 1260px; margin: auto; padding: 0 38px; }
.app-header { display: flex; justify-content: space-between; align-items: center; min-height: 100px; gap: 24px; }
.brand { display: inline-flex; gap: 12px; align-items: center; font-size: 19px; font-weight: 700; text-decoration: none; letter-spacing: -.5px; }
-.book-mark { width: 24px; height: 28px; border: 2px solid var(--blue); border-left: 7px solid var(--blue); border-radius: 2px 5px 5px 2px; transform: rotate(-5deg); }
+.book-mark { width: 24px; height: 28px; border: 2px solid var(--accent); border-left: 7px solid var(--accent); border-radius: 2px 5px 5px 2px; transform: rotate(-5deg); }
.account { display: flex; align-items: center; gap: 8px; font-size: 14px; }
#username { margin-right: 12px; color: var(--muted); overflow-wrap: anywhere; }
.notice { margin: 0 0 20px; padding: 14px 18px; color: #244f48; background: #e5f2ee; border-radius: 6px; line-height: 1.5; }
@@ -39,13 +39,13 @@ button { border: 0; border-radius: 6px; padding: 11px 16px; font-size: 14px; fon
.welcome { display: grid; grid-template-columns: 1.15fr 1fr; align-items: center; gap: 90px; min-height: 660px; padding: 40px 35px 65px; }
.introduction h1 { font-size: clamp(44px, 5.1vw, 68px); line-height: 1.06; font-weight: 650; letter-spacing: -3px; margin: 0 0 25px; }
.introduction > p { color: var(--muted); max-width: 360px; font-size: 18px; line-height: 1.65; margin-bottom: 40px; }
-.paper-preview { max-width: 385px; padding: 28px 32px 24px 40px; background: var(--paper); border: 1px solid var(--line); border-left: 4px solid #9bb9ff; transform: rotate(-3deg); box-shadow: 0 12px 30px #17274608; }
+.paper-preview { max-width: 385px; padding: 28px 32px 24px 40px; background: var(--paper); border: 1px solid var(--line); border-left: 4px solid #69aaa4; transform: rotate(-3deg); box-shadow: 0 12px 30px #133f4808; }
.preview-title { display: block; margin-bottom: 17px; font-family: Georgia, serif; font-size: 23px; }
-.preview-line { display: block; min-height: 34px; line-height: 33px; border-bottom: 1px solid #e8edf5; font-family: Georgia, serif; font-size: 14px; color: var(--muted); }
+.preview-line { display: block; min-height: 34px; line-height: 33px; border-bottom: 1px solid #e5efec; font-family: Georgia, serif; font-size: 14px; color: var(--muted); }
.auth-panel { padding: 32px; background: var(--paper); border: 1px solid var(--line); border-radius: 10px; }
-.auth-switch { display: flex; gap: 4px; background: #eef2f9; border-radius: 6px; padding: 4px; margin-bottom: 28px; }
+.auth-switch { display: flex; gap: 4px; background: #eaf4f1; border-radius: 6px; padding: 4px; margin-bottom: 28px; }
.auth-switch button { width: 50%; color: var(--muted); background: transparent; }
-.auth-switch button[aria-pressed="true"] { background: var(--paper); color: var(--ink); box-shadow: 0 1px 3px #17274612; }
+.auth-switch button[aria-pressed="true"] { background: var(--paper); color: var(--ink); box-shadow: 0 1px 3px #133f4812; }
.auth-panel h2 { margin: 0 0 9px; font-size: 25px; letter-spacing: -.7px; }
.auth-panel > p { margin: 0 0 26px; color: var(--muted); line-height: 1.5; }
label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 9px; }
@@ -55,19 +55,19 @@ input:not([type="checkbox"]), textarea { width: 100%; color: var(--ink); backgro
.field-help { font-size: 12px; color: var(--muted); margin: 0 0 25px; line-height: 1.5; }
.auth-panel .primary { width: 100%; }
.workspace { display: grid; grid-template-columns: 350px minmax(0, 1fr); min-height: 680px; background: var(--paper); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
-.notes-pane { border-right: 1px solid var(--line); padding: 26px 23px; background: #fafbfe; }
+.notes-pane { border-right: 1px solid var(--line); padding: 26px 23px; background: #f8fcfa; }
.pane-heading, .editor-heading { display: flex; justify-content: space-between; align-items: center; gap: 14px; }
.pane-heading { margin-bottom: 24px; }
.pane-heading h1 { font-size: 22px; margin: 0; letter-spacing: -.6px; }
.notes-pane input[type="search"] { font-size: 14px; }
.check { display: flex; align-items: center; gap: 9px; font-weight: 400; font-size: 13px; margin: 0; }
-.check input { accent-color: var(--blue); width: 15px; height: 15px; margin: 0; }
+.check input { accent-color: var(--accent); width: 15px; height: 15px; margin: 0; }
.filter { margin: 16px 0 23px; }
.list-summary { font-size: 12px; color: var(--muted); margin: 0 0 12px; }
.note-list { display: grid; gap: 8px; }
.note-item { display: block; width: 100%; padding: 16px; text-align: left; color: var(--ink); border: 1px solid transparent; background: transparent; }
-.note-item:hover { background: #eef2fa; }
-.note-item[aria-current="true"] { border-color: #b1c6f7; background: #eef3ff; }
+.note-item:hover { background: #eaf4f1; }
+.note-item[aria-current="true"] { border-color: #85b6b2; background: #e4f3ee; }
.note-item strong { display: block; font-size: 15px; font-weight: 650; overflow-wrap: anywhere; }
.note-item .excerpt { display: block; font-family: Georgia, serif; color: var(--muted); font-size: 13px; font-weight: 400; line-height: 1.6; margin-top: 7px; overflow-wrap: anywhere; }
.note-item .note-meta { display: block; color: var(--muted); font-size: 11px; font-weight: 400; margin-top: 12px; }
@@ -76,15 +76,15 @@ input:not([type="checkbox"]), textarea { width: 100%; color: var(--ink); backgro
.pagination button { padding: 9px 6px; font-size: 12px; }
.pagination span { color: var(--muted); font-size: 12px; }
.editor { position: relative; padding: 30px 38px 25px 48px; }
-.editor::before { content: ""; position: absolute; top: 104px; bottom: 105px; left: 25px; border-left: 1px solid #c7d7f7; }
+.editor::before { content: ""; position: absolute; top: 104px; bottom: 105px; left: 25px; border-left: 1px solid #b8d8cf; }
.editor-heading { margin-bottom: 34px; }
.editor-heading h2 { font-size: 16px; font-weight: 600; margin: 0; }
.save-state { color: var(--muted); font-size: 12px; }
.editor label { color: var(--muted); }
#note-title { font-family: Georgia, serif; font-size: 28px; padding: 10px 0 14px; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; margin-bottom: 25px; }
#note-body { font-family: Georgia, serif; font-size: 17px; line-height: 1.75; border: 0; padding: 8px 0; resize: vertical; height: 300px; min-height: 295px; }
-#note-title::placeholder { color: #a0aabc; }
-#note-body::placeholder { color: #a0aabc; }
+#note-title::placeholder { color: #7e9992; }
+#note-body::placeholder { color: #7e9992; }
.editor-details { display: flex; justify-content: space-between; align-items: center; color: var(--muted); font-size: 12px; gap: 14px; padding: 20px 0; border-top: 1px solid var(--line); }
.editor-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; }
.editor-actions .danger { margin-left: auto; }
@@ -93,7 +93,7 @@ input:not([type="checkbox"]), textarea { width: 100%; color: var(--ink); backgro
.demo-tools { margin-top: 20px; padding: 16px 20px; color: var(--muted); border: 1px solid var(--line); border-radius: 6px; font-size: 13px; }
.demo-tools summary { cursor: pointer; color: var(--ink); font-weight: 600; }
.demo-tools p { line-height: 1.6; }
-.demo-tools button { background: #e9eef9; }
+.demo-tools button { background: #e5f2ee; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
@media (max-width: 1000px) {
.shell { padding: 0 24px; }
diff --git a/example_app/static/index.html b/example_app/static/index.html
index 63aebcc..4aaa394 100644
--- a/example_app/static/index.html
+++ b/example_app/static/index.html
@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#2457d6">
- <title>Wred notebook</title>
+ <title>Veela notebook</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="/static/app.css">
<script src="/static/app.js" defer></script>
@@ -12,7 +12,7 @@
<body>
<div class="shell">
<header class="app-header">
- <a class="brand" href="/" aria-label="Wred notebook home"><span class="book-mark" aria-hidden="true"></span>Wred notebook</a>
+ <a class="brand" href="/" aria-label="Veela notebook home"><span class="book-mark" aria-hidden="true"></span>Veela notebook</a>
<div id="account" class="account" hidden>
<span id="username"></span>
<button id="export" class="quiet" type="button">Export notes</button>
diff --git a/scripts/benchmark_embedded.py b/scripts/benchmark_embedded.py
index dd72b19..d524cdd 100644
--- a/scripts/benchmark_embedded.py
+++ b/scripts/benchmark_embedded.py
@@ -14,7 +14,7 @@ import time
import venv
ROOT = Path(__file__).resolve().parents[1]
-APPLICATION = '''from wred import App
+APPLICATION = '''from veela import App
import sys
app = App()
@app.route('/')
@@ -50,11 +50,11 @@ def main():
args = parser.parse_args()
args.output.mkdir(parents=True, exist_ok=True)
rows = []
- with tempfile.TemporaryDirectory(prefix='wred-benchmark-') as directory:
+ with tempfile.TemporaryDirectory(prefix='veela-benchmark-') as directory:
directory = Path(directory)
venv.EnvBuilder(with_pip=False).create(directory / 'venv')
embedded_python = directory / 'venv/bin/python'
- shutil.copyfile(ROOT / 'wred.py', directory / 'wred.py')
+ shutil.copyfile(ROOT / 'veela.py', directory / 'veela.py')
(directory / 'app.py').write_text(APPLICATION)
for run in range(1, args.runs + 1):
order = ['embedded', 'standalone'] if run % 2 else ['standalone', 'embedded']
diff --git a/scripts/build_embedded.py b/scripts/build_embedded.py
index 1be5279..6e58eea 100644
--- a/scripts/build_embedded.py
+++ b/scripts/build_embedded.py
@@ -1,4 +1,4 @@
-"""Rebuild wred.py using hash-verified pure-Python wheels. Network is build-time only."""
+"""Rebuild veela.py using hash-verified pure-Python wheels. Network is build-time only."""
import base64
import hashlib
import io
@@ -56,11 +56,11 @@ def build(wheel_directory):
archive.writestr(info, data, compresslevel=9)
payload = base64.b64encode(buffer.getvalue()).decode('ascii')
licenses = '\n\n'.join(notices)
- source = (ROOT / 'source/wred.py').read_text()
+ source = (ROOT / 'source/veela.py').read_text()
output = source.rstrip() + '\n\n\n_EMBEDDED_ZIP = ' + repr(payload) + '\n'
- (ROOT / 'wred.py').write_text(output)
+ (ROOT / 'veela.py').write_text(output)
(ROOT / 'THIRD_PARTY_LICENSES.txt').write_text(licenses.rstrip() + '\n')
- print(f'Generated wred.py: {len(output.encode()):,} bytes; ZIP: {len(buffer.getvalue()):,} bytes; {len(entries)} entries')
+ print(f'Generated veela.py: {len(output.encode()):,} bytes; ZIP: {len(buffer.getvalue()):,} bytes; {len(entries)} entries')
if __name__ == '__main__':
diff --git a/source/wred.py b/source/veela.py
similarity index 93%
rename from source/wred.py
rename to source/veela.py
index e2a1c61..caded94 100644
--- a/source/wred.py
+++ b/source/veela.py
@@ -1,12 +1,11 @@
"""
-Wred: An ultra micro ASGI API framework.
+Veela: A magical, single-file ASGI API framework and web server.
-Wred is designed for lightweight JSON APIs, offering a minimalistic way to build
+Veela is designed for lightweight JSON APIs, offering a minimalistic way to build
ASGI-compatible web services. It prioritizes simplicity and performance over all.
-This is the amazing magic version of Wred, Wred-embedded ;)
-Original Wred website: https://harrisonerd.com/wred
-Wred-embedded website: https://harrisonerd.com/wred-embedded
+Based on Wred: https://harrisonerd.com/wred
+Veela website: https://harrisonerd.com/veela
Copyright (c) 2026, Harrison Erd. For license information see `LICENSE` AND
`THIRD_PARTY_LICENSES.txt`. Bundles `uvicorn`, `click`, and `h11`.
@@ -46,11 +45,11 @@ def _load_worker_app(module, name):
_loading_worker_app = False
-class WredResponseError(Exception):
+class VeelaResponseError(Exception):
"""Raised when a handler returns a non-JSON response."""
pass
-class WredRequestError(Exception):
+class VeelaRequestError(Exception):
"""Raised when a request cannot be processed (e.g., invalid body)."""
pass
@@ -73,7 +72,7 @@ class Request:
try:
self._query = dict(parse_qsl(self.scope.get('query_string', b'').decode('utf-8'), keep_blank_values=True))
except UnicodeDecodeError:
- raise WredRequestError("Invalid query string")
+ raise VeelaRequestError("Invalid query string")
return self._query
async def body(self) -> bytes:
@@ -83,7 +82,7 @@ class Request:
while True:
message = await self.receive()
if message['type'] != 'http.request':
- raise WredRequestError("Client disconnected")
+ raise VeelaRequestError("Client disconnected")
self._body += message.get('body', b'')
if not message.get('more_body', False):
break
@@ -95,7 +94,7 @@ class Request:
body = await self.body()
return json.loads(body) if body else {}
except (json.JSONDecodeError, UnicodeDecodeError):
- raise WredRequestError("Invalid JSON body")
+ raise VeelaRequestError("Invalid JSON body")
class App:
@@ -142,7 +141,7 @@ class App:
for sig in (signal.SIGINT, signal.SIGTERM):
handlers[sig] = signal.signal(sig, lambda sig, frame: pending_signals.append(sig))
try:
- with tempfile.TemporaryDirectory(prefix='wred-') as directory:
+ with tempfile.TemporaryDirectory(prefix='veela-') as directory:
archive = str(Path(directory) / 'server.zip')
Path(archive).write_bytes(base64.b64decode(_EMBEDDED_ZIP))
sys.path.insert(0, archive)
@@ -219,8 +218,8 @@ class App:
# No match found
await self.send_response(request.send, {"error": "Not Found"}, status=404)
- except (WredResponseError, WredRequestError) as e:
- status = 400 if isinstance(e, WredRequestError) else 500
+ except (VeelaResponseError, VeelaRequestError) as e:
+ status = 400 if isinstance(e, VeelaRequestError) else 500
await self.send_response(request.send, {"error": str(e)}, status=status)
async def dispatch_response(self, request: Request, response: Any) -> None:
@@ -241,7 +240,7 @@ class App:
if isinstance(body, (dict, list)):
body, content_type = json.dumps(body, ensure_ascii=False, separators=(',', ':')).encode('utf-8'), 'application/json'
elif not isinstance(body, bytes):
- raise WredResponseError("Response must be a dictionary, list, bytes, or response tuple")
+ raise VeelaResponseError("Response must be a dictionary, list, bytes, or response tuple")
await send({
'type': 'http.response.start',
'status': status,
diff --git a/tests/test_embedded.py b/tests/test_embedded.py
index 572325d..c6480ff 100644
--- a/tests/test_embedded.py
+++ b/tests/test_embedded.py
@@ -14,7 +14,7 @@ import unittest
import venv
ROOT = Path(__file__).resolve().parents[1]
-APPLICATION = '''from wred import App
+APPLICATION = '''from veela import App
import importlib.metadata
import os
import sys
@@ -47,26 +47,26 @@ async def modules(request):
@app.route('/pid')
async def pid(request):
return {'pid': os.getpid()}
-if os.environ.get('WRED_DEFAULTS'):
+if os.environ.get('VEELA_DEFAULTS'):
app.run()
else:
- app.run(host='0.0.0.0', port=int(os.environ['WRED_TEST_PORT']),
- workers=int(os.environ.get('WRED_TEST_WORKERS', '1')))
+ app.run(host='0.0.0.0', port=int(os.environ['VEELA_TEST_PORT']),
+ workers=int(os.environ.get('VEELA_TEST_WORKERS', '1')))
'''
class StandaloneTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
- cls.temporary = tempfile.TemporaryDirectory(prefix='wred-standalone-test-')
+ cls.temporary = tempfile.TemporaryDirectory(prefix='veela-standalone-test-')
cls.directory = Path(cls.temporary.name)
venv.EnvBuilder(with_pip=False).create(cls.directory / 'venv')
cls.python = cls.directory / 'venv' / ('Scripts/python.exe' if __import__('os').name == 'nt' else 'bin/python')
- shutil.copyfile(ROOT / 'wred.py', cls.directory / 'wred.py')
+ shutil.copyfile(ROOT / 'veela.py', cls.directory / 'veela.py')
(cls.directory / 'app.py').write_text(APPLICATION)
- setup, startup = APPLICATION.split("if os.environ.get('WRED_DEFAULTS'):", 1)
+ setup, startup = APPLICATION.split("if os.environ.get('VEELA_DEFAULTS'):", 1)
guarded = setup + "if __name__ == '__main__':\n" + textwrap.indent(
- "if os.environ.get('WRED_DEFAULTS'):" + startup, ' ')
+ "if os.environ.get('VEELA_DEFAULTS'):" + startup, ' ')
(cls.directory / 'guarded.py').write_text(guarded)
package = cls.directory / 'demo'
package.mkdir()
@@ -86,11 +86,11 @@ class StandaloneTests(unittest.TestCase):
port = s.getsockname()[1]
logfile = tempfile.TemporaryFile(mode='w+')
self.addCleanup(logfile.close)
- environment = {**os.environ, 'WRED_TEST_PORT': str(port), 'WRED_TEST_WORKERS': str(workers)}
+ environment = {**os.environ, 'VEELA_TEST_PORT': str(port), 'VEELA_TEST_WORKERS': str(workers)}
environment.pop('PYTHONPATH', None)
- environment.pop('WRED_DEFAULTS', None)
+ environment.pop('VEELA_DEFAULTS', None)
if default:
- environment['WRED_DEFAULTS'] = '1'
+ environment['VEELA_DEFAULTS'] = '1'
process = subprocess.Popen([str(self.python), *command], cwd=self.directory,
env=environment, stdout=logfile, stderr=logfile,
start_new_session=os.name == 'posix')
@@ -128,7 +128,7 @@ def forbidden_decode(*args, **kwargs):
base64.b64decode = forbidden_decode
assert not list(importlib.metadata.distributions())
started = time.perf_counter()
-import wred
+import veela
elapsed = time.perf_counter() - started
assert not any(n in sys.modules for n in ('uvicorn', 'h11', 'click'))
assert not any('server.zip' in p for p in sys.path)
@@ -182,7 +182,7 @@ print(elapsed)
response.read()
def test_startup_failure_restores_imports_signals_and_temporary_files(self):
- program = '''from wred import App
+ program = '''from veela import App
import pathlib, signal, sys, tempfile
tempfile.tempdir = str(pathlib.Path('scratch').resolve())
pathlib.Path(tempfile.tempdir).mkdir()
@@ -206,7 +206,7 @@ for workers in (1, 2, 1, 2):
text=True, capture_output=True, check=True)
def test_invalid_worker_counts_and_unbound_apps_fail_before_server_loading(self):
- program = '''from wred import App
+ program = '''from veela import App
import sys
for workers in (0, -1, 1.5, None, '2', True):
try:
diff --git a/tests/test_notebook.py b/tests/test_notebook.py
index fd53f06..cfc6577 100644
--- a/tests/test_notebook.py
+++ b/tests/test_notebook.py
@@ -1,4 +1,4 @@
-"""Exercise the example's API contracts through the actual Wred ASGI app."""
+"""Exercise the example's API contracts through the actual Veela ASGI app."""
import importlib
import asyncio
from http.cookies import SimpleCookie
@@ -8,7 +8,7 @@ from unittest.mock import patch
import json
-from tests.test_wred import call_app
+from tests.test_veela import call_app
from example_app import auth, storage, rate_limit
notebook = importlib.import_module('example_app.app')
@@ -75,7 +75,7 @@ class NotebookTests(unittest.IsolatedAsyncioTestCase):
async def test_public_page_assets_health_and_redirect(self):
for path, content_type, marker in [
- ('/', b'text/html', b'Wred notebook'),
+ ('/', b'text/html', b'Veela notebook'),
('/static/app.css', b'text/css', b'.workspace'),
('/static/app.js', b'text/javascript', b'/api/notes'),
]:
diff --git a/tests/test_wred.py b/tests/test_veela.py
similarity index 99%
rename from tests/test_wred.py
rename to tests/test_veela.py
index b46537f..14f9aa1 100644
--- a/tests/test_wred.py
+++ b/tests/test_veela.py
@@ -5,7 +5,7 @@ from unittest.mock import patch
import json
-from wred import App
+from veela import App
async def call_app(app, scope, receive):
@@ -19,7 +19,7 @@ async def call_app(app, scope, receive):
return messages
-class WredTests(unittest.IsolatedAsyncioTestCase):
+class VeelaTests(unittest.IsolatedAsyncioTestCase):
async def test_callable_responses_control_asgi_delivery_on_fixed_and_dynamic_routes(self):
class Response:
def __init__(self, name):
diff --git a/wred.py b/veela.py
similarity index 99%
rename from wred.py
rename to veela.py
index 20ebfac..a93ee93 100644
--- a/wred.py
+++ b/veela.py
@@ -1,12 +1,11 @@
"""
-Wred: An ultra micro ASGI API framework.
+Veela: A magical, single-file ASGI API framework and web server.
-Wred is designed for lightweight JSON APIs, offering a minimalistic way to build
+Veela is designed for lightweight JSON APIs, offering a minimalistic way to build
ASGI-compatible web services. It prioritizes simplicity and performance over all.
-This is the amazing magic version of Wred, Wred-embedded ;)
-Original Wred website: https://harrisonerd.com/wred
-Wred-embedded website: https://harrisonerd.com/wred-embedded
+Based on Wred: https://harrisonerd.com/wred
+Veela website: https://harrisonerd.com/veela
Copyright (c) 2026, Harrison Erd. For license information see `LICENSE` AND
`THIRD_PARTY_LICENSES.txt`. Bundles `uvicorn`, `click`, and `h11`.
@@ -46,11 +45,11 @@ def _load_worker_app(module, name):
_loading_worker_app = False
-class WredResponseError(Exception):
+class VeelaResponseError(Exception):
"""Raised when a handler returns a non-JSON response."""
pass
-class WredRequestError(Exception):
+class VeelaRequestError(Exception):
"""Raised when a request cannot be processed (e.g., invalid body)."""
pass
@@ -73,7 +72,7 @@ class Request:
try:
self._query = dict(parse_qsl(self.scope.get('query_string', b'').decode('utf-8'), keep_blank_values=True))
except UnicodeDecodeError:
- raise WredRequestError("Invalid query string")
+ raise VeelaRequestError("Invalid query string")
return self._query
async def body(self) -> bytes:
@@ -83,7 +82,7 @@ class Request:
while True:
message = await self.receive()
if message['type'] != 'http.request':
- raise WredRequestError("Client disconnected")
+ raise VeelaRequestError("Client disconnected")
self._body += message.get('body', b'')
if not message.get('more_body', False):
break
@@ -95,7 +94,7 @@ class Request:
body = await self.body()
return json.loads(body) if body else {}
except (json.JSONDecodeError, UnicodeDecodeError):
- raise WredRequestError("Invalid JSON body")
+ raise VeelaRequestError("Invalid JSON body")
class App:
@@ -142,7 +141,7 @@ class App:
for sig in (signal.SIGINT, signal.SIGTERM):
handlers[sig] = signal.signal(sig, lambda sig, frame: pending_signals.append(sig))
try:
- with tempfile.TemporaryDirectory(prefix='wred-') as directory:
+ with tempfile.TemporaryDirectory(prefix='veela-') as directory:
archive = str(Path(directory) / 'server.zip')
Path(archive).write_bytes(base64.b64decode(_EMBEDDED_ZIP))
sys.path.insert(0, archive)
@@ -219,8 +218,8 @@ class App:
# No match found
await self.send_response(request.send, {"error": "Not Found"}, status=404)
- except (WredResponseError, WredRequestError) as e:
- status = 400 if isinstance(e, WredRequestError) else 500
+ except (VeelaResponseError, VeelaRequestError) as e:
+ status = 400 if isinstance(e, VeelaRequestError) else 500
await self.send_response(request.send, {"error": str(e)}, status=status)
async def dispatch_response(self, request: Request, response: Any) -> None:
@@ -241,7 +240,7 @@ class App:
if isinstance(body, (dict, list)):
body, content_type = json.dumps(body, ensure_ascii=False, separators=(',', ':')).encode('utf-8'), 'application/json'
elif not isinstance(body, bytes):
- raise WredResponseError("Response must be a dictionary, list, bytes, or response tuple")
+ raise VeelaResponseError("Response must be a dictionary, list, bytes, or response tuple")
await send({
'type': 'http.response.start',
'status': status,