Run the notebook
A real little app: accounts, a notes editor, MongoDB, and just enough moving parts to explain why people invent architecture diagrams.
Run from the repository root
The notebook source lives in example_app/; it is not installed by pip install wred. Clone the repository, or use your existing checkout.
git clone https://gitman.io/git/patx/wred
cd wred
python -m venv .venv
. .venv/bin/activate
python -m pip install -e '.[server]' pymongo servestaticUse a current PyMongo release that provides AsyncMongoClient. The app imports the native async client, not Motor and not a synchronous client wearing an async function as a disguise.
Give it a database
Have MongoDB listening on 127.0.0.1:27017. An existing local instance is fine. If you already use Docker, this starts a local development database with a persistent volume. The published port binds to loopback:
docker run -d --name wred-mongo \
-p 127.0.0.1:27017:27017 \
-v wred-mongo-data:/data/db mongo:8This local example has no database credentials and binds the published port to loopback. Use your own authenticated URI for a remote database. Don’t promote the development database to the internet because the command was conveniently short.
export MONGODB_URI='mongodb://127.0.0.1:27017'
export MONGODB_DATABASE='wred_notebook'
weft example_app.asgi:app --lifespan onOpen http://127.0.0.1:8000/. These environment values are the defaults; setting them explicitly makes the configuration visible. --lifespan on asks the server to require the example’s startup and shutdown protocol.
Take it for a spin
- Create an account. Usernames accept 1–64 characters; passwords accept 15–128 characters.
- Write two notes, pin one, search for a word, and try the pagination controls.
- Export your notes. Log out and log back in to see the stored data.
- Click the rate-limit demonstration. A fresh client gets three accepted requests in ten seconds; the extra requests return 429.
For a quick HTTP check, use a second terminal:
curl -i http://127.0.0.1:8000/api/health
# HTTP 200, {"status":"ok"}
curl -i http://127.0.0.1:8000/api/notes
# HTTP 401: notes require a bearer tokenFollow the files
| File | What to read for |
|---|---|
example_app/asgi.py | Exports the server entry point: app = create_app(). |
example_app/app.py | Builds Wred, registers routes, wraps static files, and owns lifespan. |
example_app/storage.py | Async MongoDB operations, indexes, note ownership filters, and session persistence. |
example_app/auth.py | Password checks, bearer tokens, refresh cookies, and authentication decorators. |
example_app/notes.py | Validation, CRUD, filtering, pagination, and export. |
example_app/pages.py | Health, redirect, and rate-limit demonstration routes. |
example_app/rate_limit.py | The application-owned sliding-window decorator. |
example_app/static/app.js | Fetch calls, refresh coordination, editor state, and rendering. |
Open these files in your checkout alongside the tutorial. The source repository has the same layout.
The constructor in example_app/app.py assembles the HTTP app:
self.http = App()
rate_limit = RateLimiter()
pages.register_routes(self.http, rate_limit)
auth.register_routes(self.http, self.database, rate_limit)
notes.register_routes(self.http, self.database)
self.http = ServeStaticASGI(
self.http,
root=str(STATIC_ROOT),
index_file=True,
autorefresh=True,
max_age=0,
)
self.http.add_files(str(STATIC_ROOT), prefix='static/')The wrapper does the housekeeping
NotebookApp handles lifespan.startup by initializing the database and creating indexes. It reports startup failure if that fails. Shutdown closes the client. Ordinary requests go to its self.http application.
Wred itself handles HTTP only. Sending lifespan directly to a bare Wred app raises RuntimeError; the notebook wrapper is the part that implements it. This is deliberate composition, which is a fancy way of saying the code that opens the database also closes it.
create_app(database=...) also accepts an application-owned database object with the methods used by the routes, including async initialize() and close(). Each app has independent rate-limit state.
async def __call__(self, scope, receive, send):
if scope['type'] != 'lifespan':
return await self.http(scope, receive, send)
while True:
message = await receive()
if message['type'] == 'lifespan.startup':
try:
await self.database.initialize()
except Exception as error:
await self.database.close()
await send({
'type': 'lifespan.startup.failed',
'message': str(error),
})
return
await send({'type': 'lifespan.startup.complete'})
elif message['type'] == 'lifespan.shutdown':
await self.database.close()
await send({'type': 'lifespan.shutdown.complete'})
returnHow the browser gets its files
The wrapper places ServeStaticASGI around Wred. It serves the static directory with an index file and also adds the /static/ asset prefix. Unmatched requests fall through to the API.
The example enables autorefresh=True and max_age=0 for editing. For deployment, choose intentional cache settings or move built assets to your reverse proxy. The frontend chapter covers alternatives.
