Skip to main content

Logo

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 string
  • request.method -> HTTP method
  • request.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 value
  • request.scope -> Original ASGI scope, including client, scheme, and middleware state
  • request.query -> Dict of string keys and values, parsed and cached on first access.
  • await request.body() -> Cached body raw bytes
  • await 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, 200
  • b'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 type
  • b'', [('Location', '/')], 303 -> Redirect
  • b'', [], 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)

Source distribution for wred 0.12
File Size Uploaded Provenance
wred-0.12.tar.gz 306.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for wred 0.12
File Interpreter ABI Platform
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.

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