avalon 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
avalon-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nehz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
avalon-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,200 @@
1
+ Metadata-Version: 2.4
2
+ Name: avalon
3
+ Version: 0.2.0
4
+ Summary: Real-time web framework: HTTP routes, WebSockets and broadcast channels on pure asyncio
5
+ Author: nehz
6
+ License: MIT
7
+ Keywords: web,framework,websocket,real-time,asyncio,pubsub
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Framework :: AsyncIO
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
20
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest>=7; extra == "test"
27
+ Dynamic: license-file
28
+
29
+ # avalon
30
+
31
+ **Real-time web framework.** HTTP routes, WebSockets and broadcast channels in one
32
+ small, dependency-free package built on `asyncio`.
33
+
34
+ Avalon is for apps where the server pushes data to clients as it happens, such
35
+ as chat, live dashboards, multiplayer cursors and notifications. You don't have to
36
+ wire an HTTP framework, a WebSocket library and a pub/sub layer together
37
+ yourself. Everything is plain standard-library Python you can read in one sitting.
38
+
39
+ ## Features
40
+
41
+ - **Zero dependencies**: pure standard library (`asyncio`, `hashlib`, `json`, ...), Python 3.10+.
42
+ - **Decorator routing** with typed path parameters: `{id:int}`, `{x:float}`, `{rest:path}`.
43
+ - **Sync or async handlers**: return a `Response`, a `dict`/`list` (JSON), `str`, `bytes` or `None`.
44
+ - **WebSockets built in** (RFC 6455): text/binary messages, fragmentation, automatic ping/pong,
45
+ close handshake, size limits and `async for msg in ws` iteration.
46
+ - **Broadcast channels**: `app.hub.join(room, ws)` and `await app.hub.broadcast(room, msg)`.
47
+ Disconnected sockets are cleaned up automatically.
48
+ - **HTTP/1.1 keep-alive**, `HEAD` support and JSON error responses (`HTTPError`).
49
+ - **WebSocket client** (`avalon.connect`) for tests, scripts and service-to-service links.
50
+ - Fully type-hinted (`py.typed`).
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install avalon
56
+ # or, from a checkout:
57
+ pip install .
58
+ ```
59
+
60
+ ## Quickstart
61
+
62
+ ```python
63
+ from avalon import Avalon
64
+
65
+ app = Avalon()
66
+
67
+ @app.get("/hello/{name}")
68
+ def hello(request):
69
+ return {"hello": request.params["name"]}
70
+
71
+ @app.websocket("/ws/{room}")
72
+ async def room(ws):
73
+ name = ws.params["room"]
74
+ app.hub.join(name, ws)
75
+ async for message in ws:
76
+ await app.hub.broadcast(name, {"room": name, "text": message})
77
+
78
+ if __name__ == "__main__":
79
+ app.run("127.0.0.1", 8000)
80
+ ```
81
+
82
+ Talk to it from Python:
83
+
84
+ ```python
85
+ import asyncio
86
+ from avalon import connect
87
+
88
+ async def main():
89
+ ws = await connect("ws://127.0.0.1:8000/ws/lobby")
90
+ await ws.send("hi!")
91
+ print(await ws.receive_json()) # {'room': 'lobby', 'text': 'hi!'}
92
+ await ws.close()
93
+
94
+ asyncio.run(main())
95
+ ```
96
+
97
+ A complete multi-room chat with a browser UI lives in `examples/chat.py`:
98
+
99
+ ```bash
100
+ PYTHONPATH=src python3 examples/chat.py # http://127.0.0.1:8000
101
+ PYTHONPATH=src python3 examples/chat.py --demo # scripted two-client demo, then exit
102
+ ```
103
+
104
+ ## API overview
105
+
106
+ Everything below is importable from the top-level `avalon` package.
107
+
108
+ ### `Avalon(*, keep_alive_timeout=15.0)`
109
+
110
+ | Member | Description |
111
+ | --- | --- |
112
+ | `@app.route(path, methods=("GET",))` | Register an HTTP handler `handler(request)`; may be sync or async. |
113
+ | `@app.get(path)` / `@app.post(path)` | Shorthands for `route` with one method. |
114
+ | `@app.websocket(path)` | Register an `async def handler(ws)`. Non-async functions raise `TypeError`. |
115
+ | `app.hub` | The app's `Hub` (broadcast channels). |
116
+ | `app.sockets` | `set[WebSocket]` of connections currently being served. |
117
+ | `app.router` | The underlying `Router`. |
118
+ | `await app.dispatch(request)` | Route a `Request` and return a `Response` (never raises). |
119
+ | `await app.handle_connection(reader, writer)` | Serve one TCP connection (for custom servers). |
120
+ | `await app.serve(host="127.0.0.1", port=8000)` | Start listening and return an `asyncio.Server` (`port=0` picks a free port). |
121
+ | `app.run(host="127.0.0.1", port=8000)` | Blocking: serve forever until Ctrl+C. |
122
+
123
+ `GET` routes also answer `HEAD`. Unknown paths return `404`, a known path with the wrong
124
+ method returns `405`, and an unhandled exception returns `500` (logged on the `avalon` logger).
125
+ All error bodies are JSON: `{"error": "..."}`.
126
+
127
+ ### Handler return values: `to_response(value)`
128
+
129
+ | Return | Response |
130
+ | --- | --- |
131
+ | `Response` (or subclass) | sent as-is |
132
+ | `dict` / `list` | `JSONResponse` (`application/json`) |
133
+ | `str` | `text/plain; charset=utf-8` |
134
+ | `bytes` | `application/octet-stream` |
135
+ | `None` | `204 No Content` |
136
+
137
+ Anything else raises `TypeError`, which the app turns into a 500.
138
+
139
+ ### HTTP
140
+
141
+ - `Request`: `method`, `path` (URL-decoded), `query: dict[str, str]`, `headers: dict[str, str]`
142
+ (lower-case names), `body: bytes`, `params: dict` (converted path params), `version`;
143
+ methods `text()` and `json()` (raises `HTTPError(400)` on invalid JSON); properties
144
+ `keep_alive` and `is_websocket`.
145
+ - `Response(body=b"", status=200, headers=None, media_type=None)`: the default media type is
146
+ `text/plain; charset=utf-8`. `encode(keep_alive=False)` returns the wire bytes.
147
+ - `JSONResponse(body, status=200, headers=None)`: serializes any JSON-compatible `body` with `json.dumps`.
148
+ - `HTMLResponse(body, status=200, headers=None)`: `text/html; charset=utf-8`.
149
+ - `HTTPError(status, detail=None)`: raise it in a handler to return `{"error": detail}` with
150
+ that status. `detail` defaults to the standard reason phrase.
151
+
152
+ ### WebSockets
153
+
154
+ - `WebSocket`: `path`, `params`, `headers`, `state` (a free-form dict), `closed`, `close_code`, `max_size`
155
+ - `await ws.send(str | bytes)` sends a text or binary message. `await ws.send_json(obj)` sends JSON as text.
156
+ - `await ws.receive() -> str | bytes` and `await ws.receive_json()`.
157
+ - `async for msg in ws:` yields messages until the peer closes.
158
+ - `await ws.ping(data=b"")` and `await ws.close(code=1000, reason="")`.
159
+ - `WebSocketClosed(code, reason)`: raised by `receive`/`send` once the connection is closed.
160
+ Inside a handler you can let it propagate, because the app treats it as a normal disconnect. An
161
+ unhandled exception in a handler closes the socket with code `1011`.
162
+ - `Opcode`: frame opcode enum (`TEXT`, `BINARY`, `CLOSE`, `PING`, `PONG`, `CONTINUATION`).
163
+ - `await connect(url, *, headers=None) -> WebSocket`: client for `ws://` URLs. It raises
164
+ `HandshakeError` (with `.status`) if the server rejects the upgrade.
165
+
166
+ ### Channels: `Hub`
167
+
168
+ | Method | Description |
169
+ | --- | --- |
170
+ | `join(channel, ws)` / `leave(channel, ws)` | Subscribe or unsubscribe a socket. |
171
+ | `leave_all(ws)` | Remove a socket from every channel (done automatically on disconnect). |
172
+ | `members(channel) -> set[WebSocket]` | Snapshot of a channel's sockets. |
173
+ | `channels() -> list[str]` | Sorted names of non-empty channels. |
174
+ | `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. |
175
+
176
+ ### Routing
177
+
178
+ - `Router()`: `add(path, handler, methods=("GET",), kind="http" | "websocket") -> Route` and
179
+ `resolve(path, method="GET", kind="http") -> (Route, params)`, which raises `HTTPError(404/405)`.
180
+ - `Route`: `path`, `handler`, `methods`, `kind`, `match(path) -> dict | None`.
181
+ - Path converters: `{name}` / `{name:str}` (one segment), `{name:int}`, `{name:float}`,
182
+ `{name:path}` (may contain `/`). The first registered match wins.
183
+
184
+ ## Limitations
185
+
186
+ Avalon is deliberately small. It has no TLS (put it behind a reverse proxy), no
187
+ WebSocket extensions or subprotocol negotiation, no chunked request bodies (they are rejected with `501`), and
188
+ hubs are in-process only, so they don't fan out across multiple workers.
189
+
190
+ ## Development
191
+
192
+ ```bash
193
+ python3 -m unittest discover -s tests -v
194
+ # or, with pytest installed:
195
+ python3 -m pytest
196
+ ```
197
+
198
+ ## License
199
+
200
+ MIT
avalon-0.2.0/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # avalon
2
+
3
+ **Real-time web framework.** HTTP routes, WebSockets and broadcast channels in one
4
+ small, dependency-free package built on `asyncio`.
5
+
6
+ Avalon is for apps where the server pushes data to clients as it happens, such
7
+ as chat, live dashboards, multiplayer cursors and notifications. You don't have to
8
+ wire an HTTP framework, a WebSocket library and a pub/sub layer together
9
+ yourself. Everything is plain standard-library Python you can read in one sitting.
10
+
11
+ ## Features
12
+
13
+ - **Zero dependencies**: pure standard library (`asyncio`, `hashlib`, `json`, ...), Python 3.10+.
14
+ - **Decorator routing** with typed path parameters: `{id:int}`, `{x:float}`, `{rest:path}`.
15
+ - **Sync or async handlers**: return a `Response`, a `dict`/`list` (JSON), `str`, `bytes` or `None`.
16
+ - **WebSockets built in** (RFC 6455): text/binary messages, fragmentation, automatic ping/pong,
17
+ close handshake, size limits and `async for msg in ws` iteration.
18
+ - **Broadcast channels**: `app.hub.join(room, ws)` and `await app.hub.broadcast(room, msg)`.
19
+ Disconnected sockets are cleaned up automatically.
20
+ - **HTTP/1.1 keep-alive**, `HEAD` support and JSON error responses (`HTTPError`).
21
+ - **WebSocket client** (`avalon.connect`) for tests, scripts and service-to-service links.
22
+ - Fully type-hinted (`py.typed`).
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install avalon
28
+ # or, from a checkout:
29
+ pip install .
30
+ ```
31
+
32
+ ## Quickstart
33
+
34
+ ```python
35
+ from avalon import Avalon
36
+
37
+ app = Avalon()
38
+
39
+ @app.get("/hello/{name}")
40
+ def hello(request):
41
+ return {"hello": request.params["name"]}
42
+
43
+ @app.websocket("/ws/{room}")
44
+ async def room(ws):
45
+ name = ws.params["room"]
46
+ app.hub.join(name, ws)
47
+ async for message in ws:
48
+ await app.hub.broadcast(name, {"room": name, "text": message})
49
+
50
+ if __name__ == "__main__":
51
+ app.run("127.0.0.1", 8000)
52
+ ```
53
+
54
+ Talk to it from Python:
55
+
56
+ ```python
57
+ import asyncio
58
+ from avalon import connect
59
+
60
+ async def main():
61
+ ws = await connect("ws://127.0.0.1:8000/ws/lobby")
62
+ await ws.send("hi!")
63
+ print(await ws.receive_json()) # {'room': 'lobby', 'text': 'hi!'}
64
+ await ws.close()
65
+
66
+ asyncio.run(main())
67
+ ```
68
+
69
+ A complete multi-room chat with a browser UI lives in `examples/chat.py`:
70
+
71
+ ```bash
72
+ PYTHONPATH=src python3 examples/chat.py # http://127.0.0.1:8000
73
+ PYTHONPATH=src python3 examples/chat.py --demo # scripted two-client demo, then exit
74
+ ```
75
+
76
+ ## API overview
77
+
78
+ Everything below is importable from the top-level `avalon` package.
79
+
80
+ ### `Avalon(*, keep_alive_timeout=15.0)`
81
+
82
+ | Member | Description |
83
+ | --- | --- |
84
+ | `@app.route(path, methods=("GET",))` | Register an HTTP handler `handler(request)`; may be sync or async. |
85
+ | `@app.get(path)` / `@app.post(path)` | Shorthands for `route` with one method. |
86
+ | `@app.websocket(path)` | Register an `async def handler(ws)`. Non-async functions raise `TypeError`. |
87
+ | `app.hub` | The app's `Hub` (broadcast channels). |
88
+ | `app.sockets` | `set[WebSocket]` of connections currently being served. |
89
+ | `app.router` | The underlying `Router`. |
90
+ | `await app.dispatch(request)` | Route a `Request` and return a `Response` (never raises). |
91
+ | `await app.handle_connection(reader, writer)` | Serve one TCP connection (for custom servers). |
92
+ | `await app.serve(host="127.0.0.1", port=8000)` | Start listening and return an `asyncio.Server` (`port=0` picks a free port). |
93
+ | `app.run(host="127.0.0.1", port=8000)` | Blocking: serve forever until Ctrl+C. |
94
+
95
+ `GET` routes also answer `HEAD`. Unknown paths return `404`, a known path with the wrong
96
+ method returns `405`, and an unhandled exception returns `500` (logged on the `avalon` logger).
97
+ All error bodies are JSON: `{"error": "..."}`.
98
+
99
+ ### Handler return values: `to_response(value)`
100
+
101
+ | Return | Response |
102
+ | --- | --- |
103
+ | `Response` (or subclass) | sent as-is |
104
+ | `dict` / `list` | `JSONResponse` (`application/json`) |
105
+ | `str` | `text/plain; charset=utf-8` |
106
+ | `bytes` | `application/octet-stream` |
107
+ | `None` | `204 No Content` |
108
+
109
+ Anything else raises `TypeError`, which the app turns into a 500.
110
+
111
+ ### HTTP
112
+
113
+ - `Request`: `method`, `path` (URL-decoded), `query: dict[str, str]`, `headers: dict[str, str]`
114
+ (lower-case names), `body: bytes`, `params: dict` (converted path params), `version`;
115
+ methods `text()` and `json()` (raises `HTTPError(400)` on invalid JSON); properties
116
+ `keep_alive` and `is_websocket`.
117
+ - `Response(body=b"", status=200, headers=None, media_type=None)`: the default media type is
118
+ `text/plain; charset=utf-8`. `encode(keep_alive=False)` returns the wire bytes.
119
+ - `JSONResponse(body, status=200, headers=None)`: serializes any JSON-compatible `body` with `json.dumps`.
120
+ - `HTMLResponse(body, status=200, headers=None)`: `text/html; charset=utf-8`.
121
+ - `HTTPError(status, detail=None)`: raise it in a handler to return `{"error": detail}` with
122
+ that status. `detail` defaults to the standard reason phrase.
123
+
124
+ ### WebSockets
125
+
126
+ - `WebSocket`: `path`, `params`, `headers`, `state` (a free-form dict), `closed`, `close_code`, `max_size`
127
+ - `await ws.send(str | bytes)` sends a text or binary message. `await ws.send_json(obj)` sends JSON as text.
128
+ - `await ws.receive() -> str | bytes` and `await ws.receive_json()`.
129
+ - `async for msg in ws:` yields messages until the peer closes.
130
+ - `await ws.ping(data=b"")` and `await ws.close(code=1000, reason="")`.
131
+ - `WebSocketClosed(code, reason)`: raised by `receive`/`send` once the connection is closed.
132
+ Inside a handler you can let it propagate, because the app treats it as a normal disconnect. An
133
+ unhandled exception in a handler closes the socket with code `1011`.
134
+ - `Opcode`: frame opcode enum (`TEXT`, `BINARY`, `CLOSE`, `PING`, `PONG`, `CONTINUATION`).
135
+ - `await connect(url, *, headers=None) -> WebSocket`: client for `ws://` URLs. It raises
136
+ `HandshakeError` (with `.status`) if the server rejects the upgrade.
137
+
138
+ ### Channels: `Hub`
139
+
140
+ | Method | Description |
141
+ | --- | --- |
142
+ | `join(channel, ws)` / `leave(channel, ws)` | Subscribe or unsubscribe a socket. |
143
+ | `leave_all(ws)` | Remove a socket from every channel (done automatically on disconnect). |
144
+ | `members(channel) -> set[WebSocket]` | Snapshot of a channel's sockets. |
145
+ | `channels() -> list[str]` | Sorted names of non-empty channels. |
146
+ | `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. |
147
+
148
+ ### Routing
149
+
150
+ - `Router()`: `add(path, handler, methods=("GET",), kind="http" | "websocket") -> Route` and
151
+ `resolve(path, method="GET", kind="http") -> (Route, params)`, which raises `HTTPError(404/405)`.
152
+ - `Route`: `path`, `handler`, `methods`, `kind`, `match(path) -> dict | None`.
153
+ - Path converters: `{name}` / `{name:str}` (one segment), `{name:int}`, `{name:float}`,
154
+ `{name:path}` (may contain `/`). The first registered match wins.
155
+
156
+ ## Limitations
157
+
158
+ Avalon is deliberately small. It has no TLS (put it behind a reverse proxy), no
159
+ WebSocket extensions or subprotocol negotiation, no chunked request bodies (they are rejected with `501`), and
160
+ hubs are in-process only, so they don't fan out across multiple workers.
161
+
162
+ ## Development
163
+
164
+ ```bash
165
+ python3 -m unittest discover -s tests -v
166
+ # or, with pytest installed:
167
+ python3 -m pytest
168
+ ```
169
+
170
+ ## License
171
+
172
+ MIT
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "avalon"
7
+ version = "0.2.0"
8
+ description = "Real-time web framework: HTTP routes, WebSockets and broadcast channels on pure asyncio"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "nehz" }]
13
+ keywords = ["web", "framework", "websocket", "real-time", "asyncio", "pubsub"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Framework :: AsyncIO",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
27
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
28
+ "Typing :: Typed",
29
+ ]
30
+ dependencies = []
31
+
32
+ [project.optional-dependencies]
33
+ test = ["pytest>=7"]
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
37
+
38
+ [tool.setuptools.package-data]
39
+ avalon = ["py.typed"]
40
+
41
+ [tool.pytest.ini_options]
42
+ testpaths = ["tests"]
43
+ pythonpath = ["src"]
avalon-0.2.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,32 @@
1
+ """Avalon: a small, dependency-free real-time web framework for asyncio.
2
+
3
+ HTTP routes, WebSockets and broadcast channels in one tiny package.
4
+ """
5
+
6
+ from .app import Avalon, to_response
7
+ from .channels import Hub
8
+ from .client import HandshakeError, connect
9
+ from .http import HTMLResponse, HTTPError, JSONResponse, Request, Response
10
+ from .routing import Route, Router
11
+ from .websocket import Opcode, WebSocket, WebSocketClosed
12
+
13
+ __version__ = "0.2.0"
14
+
15
+ __all__ = [
16
+ "Avalon",
17
+ "HTMLResponse",
18
+ "HTTPError",
19
+ "HandshakeError",
20
+ "Hub",
21
+ "JSONResponse",
22
+ "Opcode",
23
+ "Request",
24
+ "Response",
25
+ "Route",
26
+ "Router",
27
+ "WebSocket",
28
+ "WebSocketClosed",
29
+ "connect",
30
+ "to_response",
31
+ "__version__",
32
+ ]