mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
0cbcaf9c4f
## Summary
Comprehensive post-v3.0.0 modernization: new features, bug fixes,
dependency cleanup, docs, and test coverage.
**New features:**
- **HTTP method filtering** — `@api.route("/data", methods=["GET"])`
- **Lifespan context manager** — modern async startup/shutdown
- **`api.exception_handler()`** — custom error handling per exception
type
- **`api.graphql()`** — one-liner GraphQL setup
- **`resp.file()`** — serve files from disk with auto content-type
- **before_request short-circuit** — set status code to skip route
handler
- **`req.path_params`** / **`req.client`** / **`req.is_json`** — new
request properties
- **`uuid`** and **`path`** route convertors
- **PEP 561 `py.typed`** marker
**Bug fixes:**
- Fix multipart parser losing headers
- Fix `url_for()` with typed params (`{id:int}`)
- Fix `resp.body` encoding crash on bytes content
- Fix Python 3.9 type syntax (`from __future__ import annotations`)
- Fix broken session test and no-op file upload test
- Fix helloworld example 404 on root path
**Dependencies:**
- Flattened — `pip install responder` gets everything
- Core: just starlette + uvicorn (down from 10 deps)
**Docs & README:**
- All new features documented in tour
- Modernized README features list
- Deployment guide: Docker, cloud, uvicorn
- Removed Pipenv, extras, stale references throughout
**Tests & quality:**
- 117 tests (up from 92), 91% coverage, 0 warnings
- CaseInsensitiveDict, GraphQL edge cases, staticfiles tests
- Ruff clean, all `tmpdir` → `tmp_path`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
67 lines
2.1 KiB
Python
67 lines
2.1 KiB
Python
import json
|
|
|
|
from .templates import GRAPHIQL
|
|
|
|
|
|
class GraphQLView:
|
|
def __init__(self, *, api, schema):
|
|
self.api = api
|
|
self.schema = schema
|
|
|
|
@staticmethod
|
|
async def _resolve_graphql_query(req, resp):
|
|
if "json" in req.mimetype:
|
|
json_media = await req.media("json")
|
|
if "query" not in json_media:
|
|
resp.status_code = 400
|
|
resp.media = {"errors": ["'query' key is required in the JSON payload"]}
|
|
return None, None, None
|
|
return (
|
|
json_media["query"],
|
|
json_media.get("variables"),
|
|
json_media.get("operationName"),
|
|
)
|
|
|
|
# Support query/q in params.
|
|
if "query" in req.params:
|
|
return req.params["query"], None, None
|
|
if "q" in req.params:
|
|
return req.params["q"], None, None
|
|
|
|
# Otherwise, the request text is used (typical).
|
|
return await req.text, None, None
|
|
|
|
async def graphql_response(self, req, resp):
|
|
show_graphiql = req.method == "get" and req.accepts("text/html")
|
|
|
|
if show_graphiql:
|
|
resp.content = self.api.templates.render_string(
|
|
GRAPHIQL, endpoint=req.url.path
|
|
)
|
|
return None
|
|
|
|
query, variables, operation_name = await self._resolve_graphql_query(req, resp)
|
|
if query is None:
|
|
return None
|
|
|
|
context = {"request": req, "response": resp}
|
|
result = self.schema.execute(
|
|
query, variables=variables, operation_name=operation_name, context=context
|
|
)
|
|
|
|
response_data = {}
|
|
if result.errors:
|
|
response_data["errors"] = [{"message": str(e)} for e in result.errors]
|
|
if result.data is not None:
|
|
response_data["data"] = result.data
|
|
|
|
resp.media = response_data
|
|
status_code = 200 if not result.errors else 400
|
|
return (query, json.dumps(response_data), status_code)
|
|
|
|
async def on_request(self, req, resp):
|
|
await self.graphql_response(req, resp)
|
|
|
|
async def __call__(self, req, resp):
|
|
await self.on_request(req, resp)
|