patx/veela

Rewrite README with sarcastic wred-embedded introduction

Commit 35f2957 · patx · 2026-10-13T20:53:08-04:00

Changeset
35f2957c31d7aba1af17b04341b724232c5a7889
Parents
e9b74599d541ec90850591746371677830ac2e36

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/README.md b/README.md
index 0bc0699..4202f17 100644
--- a/README.md
+++ b/README.md
@@ -1,37 +1,29 @@
-# Wred
+# Wred-embedded
 
-A tiny Python/ASGI backend for HTML and JavaScript apps. One file. Standard-library
-JSON and embedded Uvicorn. Pronounced "red".
+A magical, single-file Python framework for simple JavaScript web apps.
 
-This is the local `experiment/embedded-uvicorn` branch, not the published PyPI
-release. [Build instructions and experiment results](experiments/embedded-uvicorn/README.md).
+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?
 
-Your API needs to return some JSON. It probably doesn't need a goddamn cathedral.
+Introducing **Wred-embedded**. All the convenience of copying a file, with the
+architectural confidence of having copied a file.
 
-## What you get
+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.
 
-- Async handlers, explicit HTTP methods, and fixed or parameterized routes.
-- Headers, query parameters, cached body reads, and JSON parsing.
-- JSON responses, custom response tuples, and a small-file helper.
-- Callable ASGI response support and routing relative to mounted paths.
+## Experience the breakthrough
 
-## What you don't get
+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.
 
-Auth, sessions, databases, validation schemas, templates, form or multipart
-parsing, CORS, CSRF protection, rate limiting, dependency injection, generated
-docs, background jobs, WebSockets, or built-in streaming.
-
-Use existing ASGI middleware for CORS, authentication, sessions, or compression.
-[Starlette's middleware](https://starlette.dev/middleware/#using-middleware-in-other-frameworks)
-can wrap Wred directly. Your app chooses its dependencies; Wred doesn't need to
-know they exist.
-
-## Install and run
+```bash
+wget https://gitman.io/patx/wred-embedded/src/wred.py
+```
 
-Python 3.11+. Copy the generated `wred.py` beside your application. No pip install
-and no neighboring files are required at runtime.
+Put it next to your application. That's installation now. You're welcome.
 
-Save as `app.py`:
+Save this as `app.py`:
 
 ```python
 from wred import App
@@ -39,33 +31,49 @@ from wred import App
 app = App()
 
 @app.route('/')
-async def hello(request):
-    return {'hello': 'world'}
+async def index(request):
+    return {"hello": "world"}
 
 if __name__ == '__main__':
     app.run()
 ```
 
-And smoke it:
-
 ```bash
 python app.py
 ```
 
-Open <http://127.0.0.1:8000>. Use `app.run(host="0.0.0.0", port=8080, workers=4)` to change
-the binding and worker count. Multiple workers require a module-level app and
-the `if __name__ == '__main__':` guard. The default is one worker, with h11,
-asyncio, and no WebSockets
-or lifespan. Wred can still run under independently installed ASGI servers,
-including Granian with `--interface asginl`.
-Unsupported protocols raise `RuntimeError` without sending HTTP messages,
-following [ASGI guidance](https://asgi.readthedocs.io/en/latest/specs/main.html#protocol-specifications).
-You can also export a middleware wrapper as `app`.
+Open <http://127.0.0.1:8000>. JSON comes out. From one file. Well, two files,
+but one of them is yours, so that doesn't count.
+
+## Where did the dependencies go?
+
+What a lovely question. Look how tidy your project directory is.
+
+Wred-embedded 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.
+
+Other frameworks make you manage dependencies. We've elevated that relationship
+to a matter of trust.
 
-## Routes
+## Revolutionary features
 
-Handlers are async and receive a request. Methods default to `['GET']`;
-path parameters are strings passed by keyword.
+- **Single-file distribution.** Count the files. Please use this metric exclusively.
+- **Async routes.** Your function can wait for things without making it everyone's problem.
+- **JSON responses.** Return a dict. Receive the full enterprise experience of a dict.
+- **Path parameters.** Put a variable in a URL. The future has arrived at `/users/3`.
+- **A built-in server.** Call `app.run()`. Accept the miracle. Move on.
+- **No runtime downloads.** Everything required has already been thoughtfully accounted for. Somewhere.
+
+Authentication, databases, sessions, validation, CORS, rate limiting, and templates
+remain exciting opportunities for you to demonstrate initiative. Bring your own
+libraries or ASGI middleware. This is a framework, not a group project where we
+do all the work.
+
+## Put something on the internet, locally
+
+Routes default to GET. Handlers are async, receive a request, and get path
+parameters as strings passed by keyword.
 
 ```python
 @app.route('/users/{user_id}')
@@ -74,70 +82,52 @@ async def get_user(request, user_id):
 
 @app.route('/users', methods=['POST'])
 async def create_user(request):
-    return await request.json(), 'application/json', 201
+    data = await request.json()
+    return data, 'application/json', 201
 
[email protected]('/users/{user_id}', methods=['PUT', 'PATCH'])
-async def update_user(request, user_id):
-    return {'id': user_id, 'method': request.method}
[email protected]('/search')
+async def search(request):
+    return {'q': request.query().get('q', '')}
 ```
 
-Different methods can have separate handlers on the same path. Fixed routes
-win over dynamic routes for that method; dynamic routes follow registration
-order. Registering the same path and method again replaces its handler.
-Unmatched paths or methods return JSON 404. No automatic HEAD, OPTIONS, or 405.
+Add these above `app.run()`. Even revolutionary technology reads from top to bottom.
 
-Mounting at `/api` makes a `/users` route handle `/api/users`. Routing respects
-`root_path`; `request.path` and the original scope keep the full incoming path.
+Fixed routes take priority over parameterized routes. Unmatched paths or methods
+return JSON 404. Automatic HEAD, OPTIONS, and 405 handling have been left out to
+preserve the thrill of discovery.
 
-## Requests
+### Things you can ask a request
 
-| API | Returns |
+| API | What you get |
 | --- | --- |
-| `request.path` | Path without the query string. |
-| `request.method` | HTTP method. |
-| `request.headers` | Decoded header dict; use lowercase keys. |
-| `request.scope` | Original ASGI scope, including client, scheme, and middleware state. |
-| `request.query()` | Cached dict of string keys and values. No `await`. |
+| `request.path` | The path, without the query string. |
+| `request.method` | The HTTP method. |
+| `request.headers` | A decoded header dict. Use lowercase keys. |
+| `request.query()` | A cached dict of strings. No `await` required. A rare efficiency. |
 | `await request.body()` | Cached raw bytes. |
-| `await request.json()` | Parsed JSON; an empty body becomes `{}`. |
+| `await request.json()` | Parsed JSON. An empty body becomes `{}`. |
+| `request.scope` | The original ASGI scope, for when the abstraction gets too relaxing. |
 
-```python
[email protected]('/search')
-async def search(request):
-    return {'q': request.query().get('q', '')}
-```
-
-Query parameters preserve blank values; repeated keys keep the last value.
-JSON can be an object, array, or scalar. Validate inputs and convert types yourself.
+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.
 
-Authentication middleware can populate the scope for handlers to check.
-Starlette's SessionMiddleware uses `request.scope['session']`; its cookies are
-signed, not encrypted. Wred doesn't supply a session property.
+### Things you can give back
 
-## Responses
-
-Dicts and lists become JSON with HTTP 200. Bytes default to
-`application/octet-stream`. Custom responses use
+Dicts and lists become JSON with status 200. Bytes default to
+`application/octet-stream`. For special occasions, return
 `(body, content_type, status[, headers])`:
 
-| Handler return | Response |
-| --- | --- |
-| `{'hello': 'world'}` | JSON object, 200. |
-| `[{'id': 1}, {'id': 2}]` | JSON array, 200. |
-| `b'hello', 'text/plain', 200` | Text response. |
-| `{'id': 3}, 'application/json', 201, [('Location', '/items/3')]` | Created resource with a custom header. |
-| `b'', 'text/plain', 303, [('Location', '/')]` | Redirect. |
-| `b'', 'application/json', 204` | No content. |
-
-Dicts and lists always use `application/json`, including inside tuples.
-Other tuple bodies must be bytes. Headers are string pairs; repeated
-`Set-Cookie` headers are supported.
+```python
+return {'id': 3}, 'application/json', 201, [('Location', '/users/3')]
+```
 
-Return a callable ASGI response object to let it manage status, headers, and
-body through `(scope, receive, send)`. Reuse external response classes for
-streaming, downloads, or SSE. Use ASGI middleware for behavior across routes.
+Dicts and lists stay JSON inside tuples. Other tuple bodies must be bytes.
+Headers are string pairs; repeated `Set-Cookie` headers work too. You can also
+return a callable ASGI response object if you've brought something fancier.
+We won't be offended. We contain multitudes.
 
-## Static files
+### The static file experience
 
 ```python
 from pathlib import Path
@@ -147,33 +137,57 @@ static_root = Path(__file__).parent / 'static'
 @app.route('/')
 async def index(request):
     return await app.static('index.html', root=static_root)
+```
 
[email protected]('/static/{filename}')
-async def asset(request, filename):
-    return await app.static(filename, root=static_root)
+Create `static/index.html` and use this instead of the hello-world route.
+`root` is required. Files are read entirely into memory; missing, unreadable,
+and outside-root files return 404. Ideal for a little HTML. Less ideal for
+your director's cut.
+
+## Advanced enterprise configuration
+
+```python
+if __name__ == '__main__':
+    app.run(host='0.0.0.0', port=8080, workers=4)
 ```
 
-`static(filename, root=...)` returns `(bytes, content_type, status)`.
-The root is required and accepts a string or `Path`. MIME types are guessed;
-missing, unreadable, or outside-root files return plain-text 404, including
-symlink escapes. Files are read in a worker thread, entirely into memory.
-Small files. Don't feed it a movie.
+Four workers. One file. Please allow the procurement department a moment.
+
+The default is `127.0.0.1:8000` with one worker. Multiple workers need a
+module-level app and the `if __name__ == '__main__':` guard shown above.
+The bundled server uses asyncio and h11, with WebSockets and lifespan disabled.
+`app.run()` also needs `uvicorn`, `h11`, and `click` not to be imported already.
+The magic prefers to make its own introductions.
 
-## Errors
+The app is still an ASGI callable, so you can run it with a separately installed
+ASGI server. That does involve installing something. We'll try to be brave.
 
-Malformed JSON, invalid UTF-8 query strings, and `WredRequestError` produce
-JSON 400 responses: `{'error': message}`. Raise `WredRequestError` from a handler
-for application validation failures. Unsupported response types produce JSON
-500 errors; other exceptions propagate.
+## For those who insist on looking inside
 
-## Example app
+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 latter is enjoying a carefully curated lifestyle.
 
-The [notebook demo](https://gitman.io/patx/wred/src/example_app) includes signup,
-login, refresh cookies, logout, protected note CRUD, and rate limiting.
-Those features belong to the app, not Wred.
+To regenerate it:
 
 ```bash
-python -m example_app.app
+python scripts/build_embedded.py
 ```
 
-Open <http://127.0.0.1:8000>. Data lives in memory and disappears on restart.
+The builder fetches hash-pinned wheels on the first run and reuses its cache
+afterward. Pins live in [scripts/embedded-lock.json](scripts/embedded-lock.json).
+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.
+
+## 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
+absolutely nobody's requirements document.
+
+See [LICENSE](LICENSE) and [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt)
+for the framework and bundled component licenses. The dependencies may have
+achieved a higher plane of existence, but their license notices remain here
+with the rest of us.