patx/wred
Document example and demonstrate SSE and static serving
Commit 5222740 · patx · 2026-10-20T00:54:44-04:00
Comments
No comments yet.
Diff
diff --git a/README.md b/README.md
index 993f982..b4d54c5 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
[](https://harrisonerd.com/wred)
-[Wred](https://harrisonerd.com/wred) A tiny and fast Python/ASGI backend JavaScript web apps. Pronounced "red".
+[Wred](https://harrisonerd.com/wred) A tiny and fast Python/ASGI backend for JavaScript web apps. Pronounced "red".
Your API needs to return some JSON. It probably doesn't need a goddamn cathedral.
@@ -62,6 +62,12 @@ size limits, HTTPS, timeouts, and rate limiting at that layer. Wred buffers requ
bodies in memory and does not impose a body-size limit. Restrict direct access to
the ASGI server so clients cannot bypass the proxy's protections.
+See [`example_app.py`](example_app.py) for validation with msgspec, JWT
+authentication with PyJWT, server-sent events with
+[sse-starlette](https://github.com/sysid/sse-starlette), and frontend serving
+with [ServeStatic](https://archmonger.github.io/ServeStatic/4.4.0/servestatic-asgi/).
+Setup instructions are in its module docstring; run it with `weft example_app:app`.
+
## Routes
Handlers are async and receive a request. Methods default to `('GET',)`;
diff --git a/example_app.py b/example_app.py
index fa1b73e..8c7d9a2 100644
--- a/example_app.py
+++ b/example_app.py
@@ -2,32 +2,50 @@
example_app
~~~~~~~~~~~
- Greeting validation and JWT authentication with Wred and msgspec.
+ Validation, JWT authentication, SSE, and frontend serving with Wred.
Install the example dependencies and run from the repository root::
- python -m pip install -e '.[server]' PyJWT msgspec
+ python -m pip install -e '.[server]' PyJWT msgspec sse-starlette servestatic
export JWT_SECRET="$(python -c 'import secrets; print(secrets.token_hex(32))')"
- weft app:app
+ mkdir -p frontend
+ echo '<h1>Wred example</h1>' > frontend/index.html
+ weft example_app:app
+
+ ServeStatic serves frontend/index.html at http://127.0.0.1:8000/.
+ Replace the placeholder HTML with your frontend build. Existing files take
+ priority over Wred routes; keep greet, me, and events out of that directory.
POST /greet accepts a JSON object with a name of 1–40 characters.
GET /me requires a Bearer token signed with JWT_SECRET using HS256,
with issuer "demo", audience "greetings", a nonempty subject, and an
expiration time.
+ GET /events streams five messages using sse-starlette. Watch them arrive::
+
+ curl -N http://127.0.0.1:8000/events
+
+ EventSourceResponse is returned directly from a Wred handler, while
+ ServeStatic wraps the app. If you use a reverse proxy, configure it to pass
+ SSE events through without buffering.
+
:copyright: (c) 2026 by Harrison Erd.
:license: BSD-3-Clause, see LICENSE for more details.
"""
+import asyncio
import os
+from pathlib import Path
from typing import Annotated
import jwt
import msgspec
+from servestatic import ServeStaticASGI
+from sse_starlette import EventSourceResponse
from wred import App
SECRET = os.environ['JWT_SECRET']
-app = App()
+api = App()
class Greeting(msgspec.Struct, forbid_unknown_fields=True):
@@ -52,7 +70,7 @@ def verified_user(authorization):
return claims['sub']
[email protected]('/greet', methods=['POST'])
[email protected]('/greet', methods=['POST'])
async def greet(request):
try:
data = msgspec.json.decode(await request.body(), type=Greeting)
@@ -63,7 +81,7 @@ async def greet(request):
return {'message': f'Hello, {data.name}!'}
[email protected]('/me')
[email protected]('/me')
async def me(request):
try:
user = verified_user(request.headers.get('authorization'))
@@ -74,3 +92,20 @@ async def me(request):
]
return {'user': user}
+
+
[email protected]('/events')
+async def events(request):
+ async def messages():
+ for number in range(5):
+ yield {'data': f'Event {number}'}
+ await asyncio.sleep(1)
+
+ return EventSourceResponse(messages())
+
+
+app = ServeStaticASGI(
+ api,
+ root=Path(__file__).resolve().parent / 'frontend',
+ index_file=True,
+)