Wred is a Python ASGI router for JSON APIs.
It uses orjson to read and write JSON. Pronounced "red".
Handlers are async functions. They receive a request and return a dict, list, bytes, response tuple, or callable ASGI response. Routes support path parameters and explicit HTTP methods. The request object provides headers, query parameters, cached body reads, and JSON parsing. Response tuples let you set the status, content type, and headers; callable ASGI responses can handle streaming.
Bring your own validation, authentication, and database libraries. Call them
directly from your handlers and handle their errors there, or in middleware.
Wred does not configure them for you or require particular libraries. orjson
is its only required dependency; you also need an ASGI server to run the app.
Sessions, CORS, CSRF protection, and rate limiting need middleware or other application setup. Wred has no built-in form or multipart parsing, templates, dependency injection, generated API docs, background jobs, static file serving, or streaming helpers. It handles HTTP only, with no WebSocket support.
Install and run
Requires Python 3.11 or later.
pip install wred
Save the following in app.py:
from wred import App
app = App()
@app.route('/')
async def hello(request):
return {'hello': 'world'}
To run it with Weft, install the optional server extra:
pip install 'wred[server]'
weft app:app
You can use another ASGI server instead of Weft.
Open http://127.0.0.1:8000. Wred is HTTP-only.
Unsupported protocols raise RuntimeError without sending HTTP messages,
following ASGI guidance.
You can also export a middleware wrapper as app.
For public deployments, serve Wred behind a reverse proxy such as Nginx or Cloudflare. Configure request-body size limits, HTTPS, timeouts, and rate limiting at that layer. Wred buffers request 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 for validation with msgspec, JWT
authentication with PyJWT, server-sent events with sse-starlette, and frontend serving with ServeStatic.
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',);
path parameters are strings passed by keyword.
@app.route('/users/{user_id}')
async def get_user(request, user_id):
return {'id': user_id}
@app.route('/users', methods=['POST'])
async def create_user(request):
return await request.json(), [('Content-Type', 'application/json')], 201
@app.route('/users/{user_id}', methods=['PUT', 'PATCH'])
async def update_user(request, user_id):
return {'id': user_id, 'method': request.method}
Different methods can have separate handlers on the same path. Fixed routes
win over dynamic routes for that method; dynamic routes follow registration
order. Registering the same path and method again replaces its handler.
Trailing slashes are ignored when registering and matching routes: /users
and /users/ use the same handler, without a redirect.
Unmatched paths or methods return JSON 404. No automatic HEAD, OPTIONS, or 405.
If you mount your Wred application at /api, a /users route in that app
would become /api/users. Routing respects root_path; request.path and
the original scope keep the full incoming path.
Wred does not provide a mount method. To serve another ASGI app alongside Wred,
use an external router and mount the other app at /example and Wred at /,
with the catch-all last. Handle ASGI lifespan events in the outer app;
Wred only accepts HTTP scopes.
Requests
Since Wred 0.10, request.query is a cached property, matching request.headers.
Wred 0.9 uses request.query(); remove the parentheses when upgrading.
request.path-> Path without the query stringrequest.method-> HTTP methodrequest.headers-> Header dict decoded and cached on first access, with lowercase names and Latin-1 values; repeated Cookie fields join with;, other repeated names keep their last valuerequest.scope-> Original ASGI scope, including client, scheme, and middleware staterequest.query-> Dict of string keys and values, parsed and cached on first access.await request.body()-> Cached body raw bytesawait request.json()-> Parsed JSON body; an empty body becomes{}
@app.route('/search')
async def search(request):
return {'q': request.query.get('q', '')}
Query parameters preserve blank values; repeated keys keep the last value.
Invalid UTF-8 in raw or percent-encoded query bytes raises UnicodeDecodeError.
JSON can be an object, array, or scalar. Wred does not validate it or convert
field types.
Authentication middleware can populate the scope for handlers to check.
Responses
Dicts and lists become application/json with HTTP 200. Bytes default to
application/octet-stream. Custom responses use exactly (body, headers, status).
Tuple bodies must be bytes, dicts, or lists. Dicts and lists are JSON-encoded.
Explicit tuples send only the supplied headers; no content type is added, even
for JSON bodies. Include Content-Type when wanted, or use an empty list to send
no headers. Headers are string pairs, with names lowercased and encoded for ASGI;
order and repeated headers such as Set-Cookie are preserved.
Wred 0.12 replaces the old (body, content_type, status[, headers]) format with
(body, headers, status). When
upgrading, move explicit content types into the headers list; four-item tuples
are no longer accepted.
{'hello': 'world'}-> JSON object, 200[{'id': 1}, {'id': 2}]-> JSON array, 200b'hello', [('Content-Type', 'text/plain')], 200-> Text response{'id': 3}, [('Content-Type', 'application/json'), ('Location', '/items/3')], 201-> Created resource with a custom header{'error': 'Invalid'}, [('Content-Type', 'application/problem+json')], 400-> JSON with a custom media typeb'', [('Location', '/')], 303-> Redirectb'', [], 204-> No content
If a handler returns a callable, Wred calls it with (scope, receive, send).
This lets you use ASGI response classes from other libraries for streaming,
downloads, or SSE.
Serving the frontend
Wred does not serve static files. Use a web server such as Caddy or Nginx, a static host, or an ASGI app such as ServeStatic. A frontend development server such as Vite can proxy API requests to Wred during development.
Serving the frontend and API from one origin lets browser code use relative
URLs such as fetch('/api/hello') without CORS configuration. If you use
separate origins, configure CORS in middleware and account for cookie and
credential rules. Opening HTML directly with file:// does not avoid those
browser restrictions.
Errors
Exceptions propagate to your ASGI server or middleware. Malformed JSON raises
orjson.JSONDecodeError, invalid UTF-8 query bytes (raw or percent-encoded) raise UnicodeDecodeError,
client disconnects during body reads raise ConnectionError, and unsupported
response types raise TypeError. Wred does not turn exceptions into responses.
Malformed response tuples can raise ValueError during unpacking.
Return response tuples for expected application errors. Catch parsing errors in your handler when you want a JSON 400 response:
import orjson
@app.route('/items', methods=['POST'])
async def create_item(request):
try:
data = await request.json()
except orjson.JSONDecodeError:
return {'error': 'Invalid JSON body'}, [('Content-Type', 'application/json')], 400
if not isinstance(data, dict) or not data.get('title'):
return {'error': 'A title is required'}, [('Content-Type', 'application/json')], 400
return data, [('Content-Type', 'application/json')], 201
Validate inputs before returning a streaming response. Once streaming starts, you cannot change the status or replace the body with an error response. Unhandled exceptions reach the server; Wred does not send a second response.
Performance
Fixed routes use dictionary lookups. Parameterized routes are matched in registration order. Headers and query parameters are parsed on first access and cached. Reading the body buffers it in memory; later reads reuse those bytes.
See the Web Frameworks Benchmark for benchmark results.
Metadata
Release files for wred 0.12
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| wred-0.12.tar.gz | 306.1 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| wred-0.12-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 313.9 kB
Release files / wred-0.12.tar.gz
| Download URL | wred-0.12.tar.gz |
|---|---|
| Size | 306.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
438c7abfd6fd85d339791259b6dfd29338f0127ab940d1a36aa968bf338d38c7
|
|
BLAKE2b-256 checksum How to use checksums |
ff563ed5a877c9fe5b4d28c4e8aec4e0e6658aa03a57d984c8f83a674449cde3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
python-requests/2.32.5
|
Release files / wred-0.12-py3-none-any.whl
| Download URL | wred-0.12-py3-none-any.whl |
|---|---|
| Size | 7.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fefb550485fe263113f5e64d40858dbfd78774fba404296c7891d917b98d765f
|
|
BLAKE2b-256 checksum How to use checksums |
89fc744efa47fa09ae1e0175e26db1a801e63b048a5be9837da49fde5311a5fb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
python-requests/2.32.5
|
Security for wred 0.12
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.
To learn more about protecting yourself from malicious actors, visit our package security documentation.