Skip to main content

avalon

Real-time web framework. HTTP routes, WebSockets and broadcast channels in one small, dependency-free package built on asyncio.

Avalon is for apps where the server pushes data to clients as it happens, such as chat, live dashboards, multiplayer cursors and notifications. You don't have to wire an HTTP framework, a WebSocket library and a pub/sub layer together yourself. Everything is plain standard-library Python you can read in one sitting.

Features

  • Zero dependencies: pure standard library (asyncio, hashlib, json, ...), Python 3.10+.
  • Decorator routing with typed path parameters: {id:int}, {x:float}, {rest:path}.
  • Sync or async handlers: return a Response, a dict/list (JSON), str, bytes or None.
  • WebSockets built in (RFC 6455): text/binary messages, fragmentation, automatic ping/pong, close handshake, size limits and async for msg in ws iteration.
  • Broadcast channels: app.hub.join(room, ws) and await app.hub.broadcast(room, msg). Disconnected sockets are cleaned up automatically.
  • HTTP/1.1 keep-alive, HEAD support and JSON error responses (HTTPError).
  • WebSocket client (avalon.connect) for tests, scripts and service-to-service links.
  • Fully type-hinted (py.typed).

Install

pip install avalon
# or, from a checkout:
pip install .

Quickstart

from avalon import Avalon

app = Avalon()

@app.get("/hello/{name}")
def hello(request):
    return {"hello": request.params["name"]}

@app.websocket("/ws/{room}")
async def room(ws):
    name = ws.params["room"]
    app.hub.join(name, ws)
    async for message in ws:
        await app.hub.broadcast(name, {"room": name, "text": message})

if __name__ == "__main__":
    app.run("127.0.0.1", 8000)

Talk to it from Python:

import asyncio
from avalon import connect

async def main():
    ws = await connect("ws://127.0.0.1:8000/ws/lobby")
    await ws.send("hi!")
    print(await ws.receive_json())   # {'room': 'lobby', 'text': 'hi!'}
    await ws.close()

asyncio.run(main())

A complete multi-room chat with a browser UI lives in examples/chat.py:

PYTHONPATH=src python3 examples/chat.py          # http://127.0.0.1:8000
PYTHONPATH=src python3 examples/chat.py --demo   # scripted two-client demo, then exit

API overview

Everything below is importable from the top-level avalon package.

Avalon(*, keep_alive_timeout=15.0)

Member Description
@app.route(path, methods=("GET",)) Register an HTTP handler handler(request); may be sync or async.
@app.get(path) / @app.post(path) Shorthands for route with one method.
@app.websocket(path) Register an async def handler(ws). Non-async functions raise TypeError.
app.hub The app's Hub (broadcast channels).
app.sockets set[WebSocket] of connections currently being served.
app.router The underlying Router.
await app.dispatch(request) Route a Request and return a Response (never raises).
await app.handle_connection(reader, writer) Serve one TCP connection (for custom servers).
await app.serve(host="127.0.0.1", port=8000) Start listening and return an asyncio.Server (port=0 picks a free port).
app.run(host="127.0.0.1", port=8000) Blocking: serve forever until Ctrl+C.

GET routes also answer HEAD. Unknown paths return 404, a known path with the wrong method returns 405, and an unhandled exception returns 500 (logged on the avalon logger). All error bodies are JSON: {"error": "..."}.

Handler return values: to_response(value)

Return Response
Response (or subclass) sent as-is
dict / list JSONResponse (application/json)
str text/plain; charset=utf-8
bytes application/octet-stream
None 204 No Content

Anything else raises TypeError, which the app turns into a 500.

HTTP

  • Request: method, path (URL-decoded), query: dict[str, str], headers: dict[str, str] (lower-case names), body: bytes, params: dict (converted path params), version; methods text() and json() (raises HTTPError(400) on invalid JSON); properties keep_alive and is_websocket.
  • Response(body=b"", status=200, headers=None, media_type=None): the default media type is text/plain; charset=utf-8. encode(keep_alive=False) returns the wire bytes.
  • JSONResponse(body, status=200, headers=None): serializes any JSON-compatible body with json.dumps.
  • HTMLResponse(body, status=200, headers=None): text/html; charset=utf-8.
  • HTTPError(status, detail=None): raise it in a handler to return {"error": detail} with that status. detail defaults to the standard reason phrase.

WebSockets

  • WebSocket: path, params, headers, state (a free-form dict), closed, close_code, max_size
    • await ws.send(str | bytes) sends a text or binary message. await ws.send_json(obj) sends JSON as text.
    • await ws.receive() -> str | bytes and await ws.receive_json().
    • async for msg in ws: yields messages until the peer closes.
    • await ws.ping(data=b"") and await ws.close(code=1000, reason="").
  • WebSocketClosed(code, reason): raised by receive/send once the connection is closed. Inside a handler you can let it propagate, because the app treats it as a normal disconnect. An unhandled exception in a handler closes the socket with code 1011.
  • Opcode: frame opcode enum (TEXT, BINARY, CLOSE, PING, PONG, CONTINUATION).
  • await connect(url, *, headers=None) -> WebSocket: client for ws:// URLs. It raises HandshakeError (with .status) if the server rejects the upgrade.

Channels: Hub

Method Description
join(channel, ws) / leave(channel, ws) Subscribe or unsubscribe a socket.
leave_all(ws) Remove a socket from every channel (done automatically on disconnect).
members(channel) -> set[WebSocket] Snapshot of a channel's sockets.
channels() -> list[str] Sorted names of non-empty channels.
await broadcast(channel, message, *, exclude=None) -> int Send to all members concurrently. Non-str/bytes messages are JSON-encoded. Dead sockets are dropped. Returns the delivery count.

Routing

  • Router(): add(path, handler, methods=("GET",), kind="http" | "websocket") -> Route and resolve(path, method="GET", kind="http") -> (Route, params), which raises HTTPError(404/405).
  • Route: path, handler, methods, kind, match(path) -> dict | None.
  • Path converters: {name} / {name:str} (one segment), {name:int}, {name:float}, {name:path} (may contain /). The first registered match wins.

Limitations

Avalon is deliberately small. It has no TLS (put it behind a reverse proxy), no WebSocket extensions or subprotocol negotiation, no chunked request bodies (they are rejected with 501), and hubs are in-process only, so they don't fan out across multiple workers.

Development

python3 -m unittest discover -s tests -v
# or, with pytest installed:
python3 -m pytest

License

MIT

Metadata

Release files for avalon 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for avalon 0.2.0
File Size Uploaded Provenance
avalon-0.2.0.tar.gz 21.8 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for avalon 0.2.0
File Interpreter ABI Platform
avalon-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 39.8 kB

Release files / avalon-0.2.0.tar.gz

Download URL avalon-0.2.0.tar.gz
Size 21.8 kB
Tags Source
SHA-256 checksum
How to use checksums
31d3986169b2b40bcaf3457fafd76f603874ab4afbee83fbb51b5899d78a0a83
BLAKE2b-256 checksum
How to use checksums
af2d5171134aa08e05747188498d4a565160f8d27683fb4a836d495510ad8a24
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release files / avalon-0.2.0-py3-none-any.whl

Download URL avalon-0.2.0-py3-none-any.whl
Size 18.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6496c0491492047ac8abb05c84d02f041aaf99358930f8590add2fe35134385e
BLAKE2b-256 checksum
How to use checksums
02e3edf5257942cbeaaec21522291aa3bbbb2b445f1896fe802ac3ff209847f4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Security for avalon 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files No provenance

0.1.0

0.1

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page