mirror of
https://github.com/kennethreitz/responder.git
synced 2026-07-21 18:39:29 +00:00
v6.0.2
Closes 11 of 12 verified findings (the 12th — form+model on one op — is an invalid HTTP combo, left as a harmless orphan component). - models.py _is_not_modified: normalize naive/aware datetimes -> UTC inside the try (fixes remote 500 on If-Modified-Since: ...-0000 for every file route). - models.py _parsed_form: buffer through size-checked self.content, so max_request_size is enforced on multipart AND the body stays replayable (fixes "Stream consumed" 500 + size-cap bypass). Single root cause. - openapi: register the real $defs body for self-referential models (was an empty self-$ref); log a warning on component __name__ collisions. - auth: APIKeyAuth(location="query") falls back to .query_params (works on WS). - routes _invoke_provider: detect generator-ness on __call__ for callable instances (teardown now runs). - DI overrides reach into the app-scope graph (_RequestResolver resolves an app-dep request-scoped when its subgraph touches an override); per-key restore. - models.py: 304 carries negotiated Vary; conditional wins over Range (304 not 206). Adds a cross-feature interaction regression suite. 547 tests pass; ruff + mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Responder
A familiar HTTP Service Framework for Python, powered by Starlette.
import responder
api = responder.API()
@api.route("/{greeting}")
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
if __name__ == "__main__":
api.run()
$ pip install responder
That's it. Supports Python 3.10+.
The Basics
resp.textsends back text.resp.htmlsends back HTML.resp.contentsends back bytes.resp.mediasends back JSON (or YAML, with content negotiation).resp.file("path.pdf")serves a file with automatic content-type detection.req.headersis case-insensitive.req.paramsgives you query parameters.- Both sync and async views work — the
asyncis optional.
Highlights
# Type-safe route parameters
@api.route("/users/{user_id:int}")
async def get_user(req, resp, *, user_id):
resp.media = {"id": user_id}
# HTTP method filtering
@api.route("/items", methods=["POST"])
async def create_item(req, resp):
data = await req.media()
resp.media = {"created": data}
# Class-based views
@api.route("/things/{id}")
class ThingResource:
def on_get(self, req, resp, *, id):
resp.media = {"id": id}
def on_post(self, req, resp, *, id):
resp.text = "created"
# Before-request hooks (auth, rate limiting, etc.)
@api.route(before_request=True)
def check_auth(req, resp):
if not req.headers.get("Authorization"):
resp.status_code = 401
resp.media = {"error": "unauthorized"}
# Custom error handling
@api.exception_handler(ValueError)
async def handle_error(req, resp, exc):
resp.status_code = 400
resp.media = {"error": str(exc)}
# Lifespan events
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
print("starting up")
yield
print("shutting down")
api = responder.API(lifespan=lifespan)
# GraphQL
import graphene
api.graphql("/graphql", schema=graphene.Schema(query=Query))
# WebSockets
@api.route("/ws", websocket=True)
async def websocket(ws):
await ws.accept()
while True:
name = await ws.receive_text()
await ws.send_text(f"Hello {name}!")
# Mount WSGI/ASGI apps
from flask import Flask
flask_app = Flask(__name__)
api.mount("/flask", flask_app)
# Background tasks
@api.route("/work")
def do_work(req, resp):
@api.background.task
def process():
import time; time.sleep(10)
process()
resp.media = {"status": "processing"}
Built-in OpenAPI docs, cookie-based sessions, gzip compression, static file serving, Jinja2 templates, and a production uvicorn server.
Route convertors: str, int, float, uuid, path.
Documentation
Description
Languages
Python
99.7%
HTML
0.2%