wred 0.1__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.
- wred-0.1/.coveragerc +8 -0
- wred-0.1/.gitignore +25 -0
- wred-0.1/LICENSE +29 -0
- wred-0.1/PKG-INFO +233 -0
- wred-0.1/README.md +220 -0
- wred-0.1/docs/index.html +203 -0
- wred-0.1/docs/wred-logo.png +0 -0
- wred-0.1/example_app/app.py +57 -0
- wred-0.1/example_app/auth.py +283 -0
- wred-0.1/example_app/notes.py +182 -0
- wred-0.1/example_app/pages.py +50 -0
- wred-0.1/example_app/rate_limit.py +47 -0
- wred-0.1/example_app/static/app.css +126 -0
- wred-0.1/example_app/static/app.js +330 -0
- wred-0.1/example_app/static/index.html +106 -0
- wred-0.1/example_app/storage.py +7 -0
- wred-0.1/pyproject.toml +15 -0
- wred-0.1/tests/__init__.py +0 -0
- wred-0.1/tests/test_notebook.py +314 -0
- wred-0.1/tests/test_wred.py +285 -0
- wred-0.1/wred.py +166 -0
wred-0.1/.coveragerc
ADDED
wred-0.1/.gitignore
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Python caches
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
|
|
5
|
+
# Virtual environments
|
|
6
|
+
.venv/
|
|
7
|
+
venv/
|
|
8
|
+
|
|
9
|
+
# Build artifacts
|
|
10
|
+
build/
|
|
11
|
+
dist/
|
|
12
|
+
*.egg-info/
|
|
13
|
+
|
|
14
|
+
# Test and coverage output
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.coverage
|
|
17
|
+
.coverage.*
|
|
18
|
+
htmlcov/
|
|
19
|
+
coverage.xml
|
|
20
|
+
|
|
21
|
+
# Local secrets
|
|
22
|
+
.env
|
|
23
|
+
.env.*
|
|
24
|
+
!.env.example
|
|
25
|
+
.aws/
|
wred-0.1/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Harrison Erd
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
17
|
+
may be used to endorse or promote products derived from this software
|
|
18
|
+
without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
wred-0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wred
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: An ultra-minimal ASGI framework for JSON APIs.
|
|
5
|
+
Author-email: Harrison Erd <harrisonerd@gmail.com>
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-Expression: BSD-3-Clause
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: orjson
|
|
11
|
+
Requires-Dist: granian
|
|
12
|
+
|
|
13
|
+
# Wred
|
|
14
|
+
|
|
15
|
+
Wred handles HTTP routing, requests, and JSON responses. Need CORS, sessions,
|
|
16
|
+
authentication, or compression? Wrap it with existing ASGI middleware.
|
|
17
|
+
No need to reinvent what ASGI already makes possible.
|
|
18
|
+
|
|
19
|
+
Your API needs to return some JSON. It probably doesn't need a goddamn cathedral.
|
|
20
|
+
|
|
21
|
+
Oh, and it's pronounced "red", like the color.
|
|
22
|
+
|
|
23
|
+
## What you get
|
|
24
|
+
|
|
25
|
+
- Async handlers and a route decorator.
|
|
26
|
+
- Request headers, cached body reads, and JSON parsing.
|
|
27
|
+
- JSON responses (dicts and lists) & static file serving.
|
|
28
|
+
- Response tuples: `(body, content_type, status[, headers])`.
|
|
29
|
+
|
|
30
|
+
## What you don't get
|
|
31
|
+
|
|
32
|
+
Auth, sessions, databases, validation schemas, templates, CORS, CSRF protection,
|
|
33
|
+
rate limiting, dependency injection, generated docs, background jobs, WebSockets,
|
|
34
|
+
streaming, or a reason to argue about folder structure.
|
|
35
|
+
|
|
36
|
+
Your application chooses those pieces. Use existing libraries without making
|
|
37
|
+
them everyone else's dependency. Wred doesn't need to know they exist.
|
|
38
|
+
|
|
39
|
+
## Use it
|
|
40
|
+
|
|
41
|
+
Python 3.11+. Clone the repo, then install it:
|
|
42
|
+
|
|
43
|
+
Procure your goods, yeah, use `uv` not `pip`, its 2026:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
$ git clone https://gitman.io/git/patx/wred
|
|
47
|
+
$ cd wred
|
|
48
|
+
$ uv sync
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Put this in your pipe (`app.py`):
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from wred import App
|
|
55
|
+
|
|
56
|
+
app = App()
|
|
57
|
+
|
|
58
|
+
@app.route('/')
|
|
59
|
+
async def say_hello(request):
|
|
60
|
+
return {'hello': 'world'}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
And smoke it:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv run granian --interface asginl app:app
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`asginl` disables lifespan handling. Wred doesn't implement it.
|
|
70
|
+
|
|
71
|
+
The server can run a middleware wrapper around Wred as its `app` too.
|
|
72
|
+
[Starlette's middleware](https://starlette.dev/middleware/#using-middleware-in-other-frameworks)
|
|
73
|
+
works with other ASGI applications: CORS, GZip, TrustedHost, HTTPS redirects,
|
|
74
|
+
and cookie sessions are available without changing Wred. Install only what
|
|
75
|
+
your app needs; Starlette isn't a Wred dependency. Choose standard ASGI wrappers
|
|
76
|
+
rather than integrations that require another framework's application internals.
|
|
77
|
+
|
|
78
|
+
## Routes
|
|
79
|
+
|
|
80
|
+
Handlers are async. The first argument is the request. `methods` defaults to
|
|
81
|
+
`['GET']`; path parameters arrive as strings, passed by keyword.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
@app.route('/users/{user_id}')
|
|
85
|
+
async def get_user(request, user_id):
|
|
86
|
+
return {'id': user_id}
|
|
87
|
+
|
|
88
|
+
@app.route('/users', methods=['POST'])
|
|
89
|
+
async def create_user(request):
|
|
90
|
+
return await request.json(), 'application/json', 201
|
|
91
|
+
|
|
92
|
+
@app.route('/users/{user_id}', methods=['PUT', 'PATCH'])
|
|
93
|
+
async def update_user(request, user_id):
|
|
94
|
+
return {'id': user_id, 'method': request.method}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Different handlers can share a path with different methods. Fixed routes win
|
|
98
|
+
over dynamic routes for the requested method; dynamic routes are checked in
|
|
99
|
+
registration order. Registering the same path and method again replaces its handler.
|
|
100
|
+
Unmatched paths or methods return JSON 404. No automatic HEAD, OPTIONS, or 405.
|
|
101
|
+
|
|
102
|
+
## Requests
|
|
103
|
+
|
|
104
|
+
| API | What you get |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| `request.path` | URL path, without the query string. |
|
|
107
|
+
| `request.method` | HTTP method, such as `GET` or `POST`. |
|
|
108
|
+
| `request.headers` | Dictionary of decoded headers; use lowercase names. |
|
|
109
|
+
| `request.scope` | Raw ASGI scope: query string, client address, scheme, etc. |
|
|
110
|
+
| `await request.body()` | Raw bytes, cached after the first read. |
|
|
111
|
+
| `await request.json()` | Parsed JSON; an empty body becomes `{}`. |
|
|
112
|
+
|
|
113
|
+
JSON parsing isn't validation. Arrays and scalars parse too. Check your own inputs.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from urllib.parse import parse_qs
|
|
117
|
+
from wred import WredRequestError
|
|
118
|
+
|
|
119
|
+
@app.route('/echo', methods=['POST'])
|
|
120
|
+
async def echo(request):
|
|
121
|
+
data = await request.json()
|
|
122
|
+
if not isinstance(data, dict):
|
|
123
|
+
raise WredRequestError('Send a JSON object')
|
|
124
|
+
return {'received': data}
|
|
125
|
+
|
|
126
|
+
@app.route('/search')
|
|
127
|
+
async def search(request):
|
|
128
|
+
query = parse_qs(request.scope.get('query_string', b'').decode('utf-8'))
|
|
129
|
+
return {'q': query.get('q', [''])[0]}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Read a bearer credential with `request.headers.get('authorization', '')`.
|
|
133
|
+
Check it in your application or let authentication middleware populate the ASGI
|
|
134
|
+
scope, then enforce access in your handlers. Middleware can also add session
|
|
135
|
+
data: Starlette's SessionMiddleware exposes it through `request.scope['session']`,
|
|
136
|
+
not a Wred-specific session property. Its cookies are signed, not encrypted.
|
|
137
|
+
|
|
138
|
+
## Responses
|
|
139
|
+
|
|
140
|
+
Return a dict or list for JSON with HTTP 200. Return bytes for
|
|
141
|
+
`application/octet-stream`. No `json()` helper. No ceremony.
|
|
142
|
+
|
|
143
|
+
For a custom response, return `(body, content_type, status)` or
|
|
144
|
+
`(body, content_type, status, headers)`. Headers are a list of string pairs,
|
|
145
|
+
so repeated headers such as `Set-Cookie` work too.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
@app.route('/items')
|
|
149
|
+
async def items(request):
|
|
150
|
+
return [{'id': 1}, {'id': 2}]
|
|
151
|
+
|
|
152
|
+
@app.route('/items', methods=['POST'])
|
|
153
|
+
async def create_item(request):
|
|
154
|
+
return {'id': 3}, 'application/json', 201, [('Location', '/items/3')]
|
|
155
|
+
|
|
156
|
+
@app.route('/old')
|
|
157
|
+
async def redirect(request):
|
|
158
|
+
return b'', 'text/plain', 303, [('Location', '/')]
|
|
159
|
+
|
|
160
|
+
@app.route('/items/{item_id}', methods=['DELETE'])
|
|
161
|
+
async def delete_item(request, item_id):
|
|
162
|
+
return b'', 'application/json', 204
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Dicts and lists always get `application/json`, even inside a tuple.
|
|
166
|
+
Other bodies must be bytes: encode text yourself, for example
|
|
167
|
+
`return 'hello'.encode(), 'text/plain; charset=utf-8', 200`.
|
|
168
|
+
Custom headers can carry cookies, cache policies, CORS rules, or `Retry-After`;
|
|
169
|
+
Wred sends them, you decide what they mean.
|
|
170
|
+
For behavior across every route, an ASGI wrapper can intercept requests and
|
|
171
|
+
responses to apply CORS or compression. Keep that plumbing out of your handlers.
|
|
172
|
+
|
|
173
|
+
## Static files
|
|
174
|
+
|
|
175
|
+
`await app.static(filename, root=directory)` returns `(bytes, content_type, status)`.
|
|
176
|
+
The root is required and accepts a string or `Path`.
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from pathlib import Path
|
|
180
|
+
|
|
181
|
+
static_root = Path(__file__).parent / 'static'
|
|
182
|
+
|
|
183
|
+
@app.route('/')
|
|
184
|
+
async def index(request):
|
|
185
|
+
return await app.static('index.html', root=static_root)
|
|
186
|
+
|
|
187
|
+
@app.route('/static/{filename}')
|
|
188
|
+
async def asset(request, filename):
|
|
189
|
+
return await app.static(filename, root=static_root)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The helper guesses the MIME type and rejects files outside the root, including
|
|
193
|
+
symlink escapes. Missing or unreadable files return a plain-text 404. Files are
|
|
194
|
+
read in a worker thread, entirely into memory. Small files. Don't feed it a movie.
|
|
195
|
+
|
|
196
|
+
## Errors
|
|
197
|
+
|
|
198
|
+
Malformed JSON and `WredRequestError` become HTTP 400 with `{'error': message}`.
|
|
199
|
+
Unsupported handler response types become JSON 500 errors. Other exceptions
|
|
200
|
+
aren't caught by Wred; fix your shit or handle them in your application.
|
|
201
|
+
|
|
202
|
+
## Example app
|
|
203
|
+
|
|
204
|
+
The [`example_app`](https://gitman.io/patx/wred/src/example_app) notebook demonstrates signup, login,
|
|
205
|
+
refresh cookies, logout, protected endpoints, note CRUD, and rate limiting.
|
|
206
|
+
Those features belong to the example, not the framework.
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
uv run granian --interface asginl --workers 1 example_app.app:app
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Open <http://127.0.0.1:8000>. Demo data lives in memory and disappears on restart.
|
|
213
|
+
|
|
214
|
+
## Benchmarks
|
|
215
|
+
|
|
216
|
+
`GET /` returning `{"hello": "world"}`, measured on October 13, 2026.
|
|
217
|
+
Three runs per framework, with the run order rotated each round.
|
|
218
|
+
|
|
219
|
+
| Framework | Median requests/sec | Min | Max |
|
|
220
|
+
| --- | ---: | ---: | ---: |
|
|
221
|
+
| Wred 0.01 | **272,088** | 262,187 | 277,656 |
|
|
222
|
+
| BlackSheep 2.6.4 | 211,619 | 205,719 | 220,455 |
|
|
223
|
+
| MicroPie 0.32 | 207,720 | 197,520 | 216,010 |
|
|
224
|
+
|
|
225
|
+
Python 3.14.4, Granian 2.8.4, eight workers, HTTP/1 keep-alive, asyncio,
|
|
226
|
+
and access logging disabled. `wrk -t4 -c256 -d20s --latency`, after a
|
|
227
|
+
five-second warmup for each run. Client and server shared the same machine
|
|
228
|
+
over loopback. All nine measured runs had zero socket errors or non-success responses.
|
|
229
|
+
|
|
230
|
+
Wred delivered about 31% more requests/sec than MicroPie in this workload.
|
|
231
|
+
It's a tiny JSON response, not your database, auth, or entire application.
|
|
232
|
+
Benchmark your own shit.
|
|
233
|
+
|
wred-0.1/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Wred
|
|
2
|
+
|
|
3
|
+
Wred handles HTTP routing, requests, and JSON responses. Need CORS, sessions,
|
|
4
|
+
authentication, or compression? Wrap it with existing ASGI middleware.
|
|
5
|
+
No need to reinvent what ASGI already makes possible.
|
|
6
|
+
|
|
7
|
+
Your API needs to return some JSON. It probably doesn't need a goddamn cathedral.
|
|
8
|
+
|
|
9
|
+
Oh, and it's pronounced "red", like the color.
|
|
10
|
+
|
|
11
|
+
## What you get
|
|
12
|
+
|
|
13
|
+
- Async handlers and a route decorator.
|
|
14
|
+
- Request headers, cached body reads, and JSON parsing.
|
|
15
|
+
- JSON responses (dicts and lists) & static file serving.
|
|
16
|
+
- Response tuples: `(body, content_type, status[, headers])`.
|
|
17
|
+
|
|
18
|
+
## What you don't get
|
|
19
|
+
|
|
20
|
+
Auth, sessions, databases, validation schemas, templates, CORS, CSRF protection,
|
|
21
|
+
rate limiting, dependency injection, generated docs, background jobs, WebSockets,
|
|
22
|
+
streaming, or a reason to argue about folder structure.
|
|
23
|
+
|
|
24
|
+
Your application chooses those pieces. Use existing libraries without making
|
|
25
|
+
them everyone else's dependency. Wred doesn't need to know they exist.
|
|
26
|
+
|
|
27
|
+
## Use it
|
|
28
|
+
|
|
29
|
+
Python 3.11+. Clone the repo, then install it:
|
|
30
|
+
|
|
31
|
+
Procure your goods, yeah, use `uv` not `pip`, its 2026:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
$ git clone https://gitman.io/git/patx/wred
|
|
35
|
+
$ cd wred
|
|
36
|
+
$ uv sync
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Put this in your pipe (`app.py`):
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from wred import App
|
|
43
|
+
|
|
44
|
+
app = App()
|
|
45
|
+
|
|
46
|
+
@app.route('/')
|
|
47
|
+
async def say_hello(request):
|
|
48
|
+
return {'hello': 'world'}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
And smoke it:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv run granian --interface asginl app:app
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`asginl` disables lifespan handling. Wred doesn't implement it.
|
|
58
|
+
|
|
59
|
+
The server can run a middleware wrapper around Wred as its `app` too.
|
|
60
|
+
[Starlette's middleware](https://starlette.dev/middleware/#using-middleware-in-other-frameworks)
|
|
61
|
+
works with other ASGI applications: CORS, GZip, TrustedHost, HTTPS redirects,
|
|
62
|
+
and cookie sessions are available without changing Wred. Install only what
|
|
63
|
+
your app needs; Starlette isn't a Wred dependency. Choose standard ASGI wrappers
|
|
64
|
+
rather than integrations that require another framework's application internals.
|
|
65
|
+
|
|
66
|
+
## Routes
|
|
67
|
+
|
|
68
|
+
Handlers are async. The first argument is the request. `methods` defaults to
|
|
69
|
+
`['GET']`; path parameters arrive as strings, passed by keyword.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
@app.route('/users/{user_id}')
|
|
73
|
+
async def get_user(request, user_id):
|
|
74
|
+
return {'id': user_id}
|
|
75
|
+
|
|
76
|
+
@app.route('/users', methods=['POST'])
|
|
77
|
+
async def create_user(request):
|
|
78
|
+
return await request.json(), 'application/json', 201
|
|
79
|
+
|
|
80
|
+
@app.route('/users/{user_id}', methods=['PUT', 'PATCH'])
|
|
81
|
+
async def update_user(request, user_id):
|
|
82
|
+
return {'id': user_id, 'method': request.method}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Different handlers can share a path with different methods. Fixed routes win
|
|
86
|
+
over dynamic routes for the requested method; dynamic routes are checked in
|
|
87
|
+
registration order. Registering the same path and method again replaces its handler.
|
|
88
|
+
Unmatched paths or methods return JSON 404. No automatic HEAD, OPTIONS, or 405.
|
|
89
|
+
|
|
90
|
+
## Requests
|
|
91
|
+
|
|
92
|
+
| API | What you get |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| `request.path` | URL path, without the query string. |
|
|
95
|
+
| `request.method` | HTTP method, such as `GET` or `POST`. |
|
|
96
|
+
| `request.headers` | Dictionary of decoded headers; use lowercase names. |
|
|
97
|
+
| `request.scope` | Raw ASGI scope: query string, client address, scheme, etc. |
|
|
98
|
+
| `await request.body()` | Raw bytes, cached after the first read. |
|
|
99
|
+
| `await request.json()` | Parsed JSON; an empty body becomes `{}`. |
|
|
100
|
+
|
|
101
|
+
JSON parsing isn't validation. Arrays and scalars parse too. Check your own inputs.
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from urllib.parse import parse_qs
|
|
105
|
+
from wred import WredRequestError
|
|
106
|
+
|
|
107
|
+
@app.route('/echo', methods=['POST'])
|
|
108
|
+
async def echo(request):
|
|
109
|
+
data = await request.json()
|
|
110
|
+
if not isinstance(data, dict):
|
|
111
|
+
raise WredRequestError('Send a JSON object')
|
|
112
|
+
return {'received': data}
|
|
113
|
+
|
|
114
|
+
@app.route('/search')
|
|
115
|
+
async def search(request):
|
|
116
|
+
query = parse_qs(request.scope.get('query_string', b'').decode('utf-8'))
|
|
117
|
+
return {'q': query.get('q', [''])[0]}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Read a bearer credential with `request.headers.get('authorization', '')`.
|
|
121
|
+
Check it in your application or let authentication middleware populate the ASGI
|
|
122
|
+
scope, then enforce access in your handlers. Middleware can also add session
|
|
123
|
+
data: Starlette's SessionMiddleware exposes it through `request.scope['session']`,
|
|
124
|
+
not a Wred-specific session property. Its cookies are signed, not encrypted.
|
|
125
|
+
|
|
126
|
+
## Responses
|
|
127
|
+
|
|
128
|
+
Return a dict or list for JSON with HTTP 200. Return bytes for
|
|
129
|
+
`application/octet-stream`. No `json()` helper. No ceremony.
|
|
130
|
+
|
|
131
|
+
For a custom response, return `(body, content_type, status)` or
|
|
132
|
+
`(body, content_type, status, headers)`. Headers are a list of string pairs,
|
|
133
|
+
so repeated headers such as `Set-Cookie` work too.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
@app.route('/items')
|
|
137
|
+
async def items(request):
|
|
138
|
+
return [{'id': 1}, {'id': 2}]
|
|
139
|
+
|
|
140
|
+
@app.route('/items', methods=['POST'])
|
|
141
|
+
async def create_item(request):
|
|
142
|
+
return {'id': 3}, 'application/json', 201, [('Location', '/items/3')]
|
|
143
|
+
|
|
144
|
+
@app.route('/old')
|
|
145
|
+
async def redirect(request):
|
|
146
|
+
return b'', 'text/plain', 303, [('Location', '/')]
|
|
147
|
+
|
|
148
|
+
@app.route('/items/{item_id}', methods=['DELETE'])
|
|
149
|
+
async def delete_item(request, item_id):
|
|
150
|
+
return b'', 'application/json', 204
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Dicts and lists always get `application/json`, even inside a tuple.
|
|
154
|
+
Other bodies must be bytes: encode text yourself, for example
|
|
155
|
+
`return 'hello'.encode(), 'text/plain; charset=utf-8', 200`.
|
|
156
|
+
Custom headers can carry cookies, cache policies, CORS rules, or `Retry-After`;
|
|
157
|
+
Wred sends them, you decide what they mean.
|
|
158
|
+
For behavior across every route, an ASGI wrapper can intercept requests and
|
|
159
|
+
responses to apply CORS or compression. Keep that plumbing out of your handlers.
|
|
160
|
+
|
|
161
|
+
## Static files
|
|
162
|
+
|
|
163
|
+
`await app.static(filename, root=directory)` returns `(bytes, content_type, status)`.
|
|
164
|
+
The root is required and accepts a string or `Path`.
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from pathlib import Path
|
|
168
|
+
|
|
169
|
+
static_root = Path(__file__).parent / 'static'
|
|
170
|
+
|
|
171
|
+
@app.route('/')
|
|
172
|
+
async def index(request):
|
|
173
|
+
return await app.static('index.html', root=static_root)
|
|
174
|
+
|
|
175
|
+
@app.route('/static/{filename}')
|
|
176
|
+
async def asset(request, filename):
|
|
177
|
+
return await app.static(filename, root=static_root)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The helper guesses the MIME type and rejects files outside the root, including
|
|
181
|
+
symlink escapes. Missing or unreadable files return a plain-text 404. Files are
|
|
182
|
+
read in a worker thread, entirely into memory. Small files. Don't feed it a movie.
|
|
183
|
+
|
|
184
|
+
## Errors
|
|
185
|
+
|
|
186
|
+
Malformed JSON and `WredRequestError` become HTTP 400 with `{'error': message}`.
|
|
187
|
+
Unsupported handler response types become JSON 500 errors. Other exceptions
|
|
188
|
+
aren't caught by Wred; fix your shit or handle them in your application.
|
|
189
|
+
|
|
190
|
+
## Example app
|
|
191
|
+
|
|
192
|
+
The [`example_app`](https://gitman.io/patx/wred/src/example_app) notebook demonstrates signup, login,
|
|
193
|
+
refresh cookies, logout, protected endpoints, note CRUD, and rate limiting.
|
|
194
|
+
Those features belong to the example, not the framework.
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
uv run granian --interface asginl --workers 1 example_app.app:app
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Open <http://127.0.0.1:8000>. Demo data lives in memory and disappears on restart.
|
|
201
|
+
|
|
202
|
+
## Benchmarks
|
|
203
|
+
|
|
204
|
+
`GET /` returning `{"hello": "world"}`, measured on October 13, 2026.
|
|
205
|
+
Three runs per framework, with the run order rotated each round.
|
|
206
|
+
|
|
207
|
+
| Framework | Median requests/sec | Min | Max |
|
|
208
|
+
| --- | ---: | ---: | ---: |
|
|
209
|
+
| Wred 0.01 | **272,088** | 262,187 | 277,656 |
|
|
210
|
+
| BlackSheep 2.6.4 | 211,619 | 205,719 | 220,455 |
|
|
211
|
+
| MicroPie 0.32 | 207,720 | 197,520 | 216,010 |
|
|
212
|
+
|
|
213
|
+
Python 3.14.4, Granian 2.8.4, eight workers, HTTP/1 keep-alive, asyncio,
|
|
214
|
+
and access logging disabled. `wrk -t4 -c256 -d20s --latency`, after a
|
|
215
|
+
five-second warmup for each run. Client and server shared the same machine
|
|
216
|
+
over loopback. All nine measured runs had zero socket errors or non-success responses.
|
|
217
|
+
|
|
218
|
+
Wred delivered about 31% more requests/sec than MicroPie in this workload.
|
|
219
|
+
It's a tiny JSON response, not your database, auth, or entire application.
|
|
220
|
+
Benchmark your own shit.
|