Document example and demonstrate SSE and static serving

Commit 5222740 · patx · 2026-10-20T00:54:44-04:00

Changeset
5222740f0054e45c59a2f518dcfc5353fdbdc368
Parents
74bfac1205d665ba133f851c363eb114ed45db24

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/README.md b/README.md
index 993f982..b4d54c5 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 [![Logo](https://patx.gitman.io/wred/wred-logo.png)](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,
+)