Files
responder/README.md
T

336 lines
10 KiB
Markdown

<h1 align="center">Responder</h1>
<p align="center">
<strong>Web services for humans.</strong><br>
A familiar HTTP Service Framework for Python, powered by Starlette.
</p>
<p align="center">
<a href="https://pypi.org/project/responder/"><img alt="PyPI" src="https://img.shields.io/pypi/v/responder.svg"></a>
<a href="https://pypi.org/project/responder/"><img alt="Python versions" src="https://img.shields.io/pypi/pyversions/responder.svg"></a>
<a href="https://pypi.org/project/responder/"><img alt="License" src="https://img.shields.io/pypi/l/responder.svg"></a>
</p>
<p align="center">
<a href="https://responder.kennethreitz.org">Documentation</a>
· <a href="https://responder.kennethreitz.org/quickstart.html">Quickstart</a>
· <a href="https://responder.kennethreitz.org/tour.html">Tour</a>
· <a href="https://responder.kennethreitz.org/examples.html">Examples</a>
· <a href="https://github.com/kennethreitz/responder/blob/main/CHANGELOG.md">Changelog</a>
</p>
```python
import responder
api = responder.API()
@api.get("/hello/{name}")
def hello(req, resp, *, name):
resp.media = {"hello": name}
if __name__ == "__main__":
api.run()
```
```console
$ pip install responder
$ python app.py
```
Open `http://127.0.0.1:5042/hello/world`. That's it.
Responder is the friendly request/response shape of Flask and Falcon, brought
to ASGI with Starlette underneath. Every view receives a `req` and a `resp`.
Read from one, write to the other. Sync and async views both work.
## Why Responder?
Responder is for people who like small, expressive web frameworks with real
batteries included.
| You want | Responder gives you |
| --- | --- |
| A simple mental model | `def view(req, resp): ...` with mutable request and response objects |
| Modern Python I/O | ASGI, Starlette routing, uvicorn by default, optional Granian |
| Real API contracts | Typed request/response validation and generated OpenAPI 3.0/3.1 |
| Live typed data | Validated SSE and NDJSON with streaming generated clients |
| Pleasant responses | `resp.text`, `resp.html`, `resp.media`, `resp.file()`, `resp.problem()` |
| Production ergonomics | request IDs, structured logging, rate limiting, health checks, metrics |
| Safer defaults | Problem Details errors, capped request bodies, secure session guidance |
| Escape hatches | mount Flask, Django, WSGI, ASGI apps, or plain routers under one API |
## The Shape
Routes look like Python strings, because they are meant to be read.
```python
@api.get("/users/{user_id:int}")
def get_user(req, resp, *, user_id):
resp.media = {"id": user_id, "name": "Ada"}
```
Write the response you mean:
```python
resp.text = "hello"
resp.html = "<h1>Hello</h1>"
resp.media = {"ok": True} # JSON by default, YAML/msgpack by negotiation
resp.file("report.pdf") # content type detected for you
resp.status_code = 201
resp.headers["Location"] = "/items/1"
```
Read requests without ceremony:
```python
@api.post("/echo")
async def echo(req, resp):
payload = await req.media()
resp.media = {"you_sent": payload}
```
Return values work too, when that style feels right:
```python
@api.get("/ping")
def ping(req, resp):
return {"pong": True}
```
## A Real Endpoint
Responder can stay tiny, but it does not stop at toy apps.
```python
from pydantic import BaseModel, Field
import responder
from responder.ext.auth import BearerAuth
class ItemIn(BaseModel):
name: str = Field(min_length=1)
price: float = Field(gt=0)
class ItemOut(ItemIn):
id: int
class User(BaseModel):
name: str
scopes: list[str]
api = responder.API(
title="Store API",
version="1.0",
openapi="3.1.0",
docs_route="/docs",
request_id=True,
)
users = {"secret-token": User(name="Ada", scopes=["items:write"])}
auth = BearerAuth(verify=lambda token: users.get(token), bearer_format="opaque")
writer = api.policy("writer", auth.requires("items:write"))
@api.post(
"/items",
auth=writer,
status_code=201,
summary="Create an item",
)
def create_item(req, resp, *, item: ItemIn, user) -> ItemOut:
return ItemOut(id=1, **item.model_dump())
```
You get validation, auth enforcement, a documented request body, a documented
response body, `401`/`403`/`422` Problem Details responses, request IDs, and
Swagger UI at `/docs`. The same inference works for collections:
```python
@api.get("/items")
def list_items(req, resp) -> list[ItemOut]:
return items
```
Contracts can stream too. Each event is validated and serialized as it is
yielded, OpenAPI carries the item schema, and generated clients expose a lazy
iterator instead of buffering the response:
```python
from collections.abc import AsyncIterator
@api.sse("/inventory/events", heartbeat=15)
async def inventory_events(
req, resp
) -> AsyncIterator[responder.SSE[ItemOut]]:
async for item in inventory.watch():
yield responder.SSE(item, event="item", id=item.id)
@api.ndjson("/inventory/export")
async def inventory_export(req, resp) -> AsyncIterator[ItemOut]:
async for item in inventory.all():
yield item
```
## What's Included
| Area | Highlights |
| --- | --- |
| Routing | `@api.get`, `@api.post`, route groups, class-based views, typed path convertors |
| Validation | Pydantic body models, query/header/cookie markers, typed response models |
| OpenAPI | OpenAPI 3.0/3.1, Swagger UI, examples, security schemes, generated clients |
| Responses | JSON/YAML/msgpack, files, typed SSE/NDJSON, byte ranges, ETags |
| Security | signed sessions, server-side sessions, CSRF protection, auth helpers, JWT/OAuth2 |
| Operations | request IDs, structured access logs, health checks, Prometheus metrics |
| Limits | request body caps, streaming multipart uploads, in-memory/Redis rate limiting |
| Composition | dependencies with teardown, background tasks, WSGI/ASGI mounting, WebSockets |
| Deployment | built-in uvicorn runner, optional Granian, proxy-header support |
| Testing | in-process `api.requests` and configurable `api.test_client(...)` |
## v9 Highlights
Responder 9 tightened the production story while keeping the familiar API:
- Multipart uploads stream from the wire and spool to disk instead of buffering
entire files in memory.
- Request bodies are capped at 100 MiB by default; pass
`API(max_request_size=None)` for the legacy unlimited behavior.
- `API(csrf=True)` adds session-bound CSRF protection for unsafe requests, with
per-route opt-outs for webhooks.
- `API(trust_proxy_headers=True)` rewrites scheme, host, and client IP from
trusted reverse-proxy headers.
- Framework-generated errors use RFC 9457-style `application/problem+json`
responses by default.
- OpenAPI documents operational responses such as CSRF `403`, body-cap `413`,
rate-limit `429`, fail-closed limiter `503`, validation `422`, and timeout
`504` where they can actually happen.
Upgrading from an earlier major version? Start with the
[v9 migration guide](https://responder.kennethreitz.org/migration-v9.html).
## Installation
```console
$ pip install responder
```
Python 3.11 and newer are supported.
Optional extras:
```console
$ pip install "responder[server]" # Granian production server
$ pip install "responder[graphql]" # GraphQL with Graphene
$ pip install "responder[jwt]" # JWT auth helpers
$ pip install "responder[orjson]" # orjson JSON backend
```
With `uv`:
```console
$ uv add responder
```
## Run It
```python
# app.py
import responder
api = responder.API()
@api.get("/")
def index(req, resp):
resp.text = "hello, world!"
if __name__ == "__main__":
api.run(port=8000)
```
```console
$ python app.py
```
Or through the CLI:
```console
$ responder run app.py
```
## OpenAPI and Clients
Turn on OpenAPI with two arguments:
```python
api = responder.API(
title="Acme API",
version="1.0",
openapi="3.1.0",
docs_route="/docs",
)
```
Responder builds the schema from routes, type hints, Pydantic models, auth
helpers, and framework behavior. The docs UI appears at `/docs`, the schema at
`/schema.yml`, and client code can be generated for Python, JavaScript,
TypeScript, Ruby, and PHP.
```console
$ responder client --class-name StoreClient --output store_client.py app:api
```
## Examples Worth Reading
| Example | Why it is useful |
| --- | --- |
| [`examples/atelier.py`](examples/atelier.py) | The golden contract app: auth, policies, examples, OpenAPI, generated-client coverage |
| [`examples/todo.py`](examples/todo.py) | A practical typed Todo API with protected writes and polished schema metadata |
| [`examples/fortunes.py`](examples/fortunes.py) | Tiny app wrapping the local `fortune` CLI tool |
| [`examples/tarot.py`](examples/tarot.py) | A playful API that shuffles, lists, and deals tarot cards |
| [`examples/sse_stream.py`](examples/sse_stream.py) | Typed Server-Sent Events with metadata, heartbeats, and OpenAPI |
| [`examples/websocket_chat.py`](examples/websocket_chat.py) | WebSocket chat with Responder's route style |
| [`examples/marimo_mount.py`](examples/marimo_mount.py) | Mounting a marimo notebook app under Responder |
Run most examples with:
```console
$ responder run examples/todo.py
```
## Philosophy
Responder is intentionally familiar. If you know Flask, Falcon, Requests, or
Starlette, you already know most of the ideas. The framework tries to make the
simple thing feel natural, then keeps enough power nearby for real services:
typed contracts, OpenAPI, auth, rate limiting, streaming uploads, websockets,
and production middleware.
It is a passion project and a practical toolkit. It is especially good for
personal services, internal tools, prototypes, teaching, research apps, and
small APIs where clarity matters more than ceremony.
## Documentation
The full guide lives at [responder.kennethreitz.org](https://responder.kennethreitz.org).
- [Quickstart](https://responder.kennethreitz.org/quickstart.html)
- [Tour](https://responder.kennethreitz.org/tour.html)
- [Examples](https://responder.kennethreitz.org/examples.html)
- [Deployment](https://responder.kennethreitz.org/deployment.html)
- [Testing](https://responder.kennethreitz.org/testing.html)
- [API reference](https://responder.kennethreitz.org/api.html)
- [Changelog](CHANGELOG.md)
## License
Apache-2.0.