Add comprehensive docstrings, expand API reference, upgrade to Starlette 1.0

- Add docstrings to all undocumented public methods across API, Request,
  Response, Router, Route, BackgroundQueue, and related classes
- Expand api.rst with autodoc sections for RouteGroup, BackgroundQueue,
  QueryDict, and RateLimiter
- Update starlette dependency to >=1.0
- Drop Python 3.9 support (required by Starlette 1.0), minimum is now 3.10
- Bump version to 3.4.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 21:37:36 -04:00
parent 3c2b1acc19
commit 4f02016ed6
9 changed files with 238 additions and 16 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ if __name__ == "__main__":
$ pip install responder
That's it. Supports Python 3.9+.
That's it. Supports Python 3.10+.
## The Basics
+39
View File
@@ -43,6 +43,45 @@ status code, headers, and cookies.
:inherited-members:
Route Groups
------------
Group related routes under a shared URL prefix — useful for API versioning
and organizing large applications.
.. autoclass:: responder.api.RouteGroup
:members:
Background Queue
----------------
Run tasks in background threads without blocking the response. Available
as ``api.background``.
.. autoclass:: responder.background.BackgroundQueue
:members:
Query Dict
----------
A dictionary subclass for query string parameters with multi-value support.
.. autoclass:: responder.models.QueryDict
:members:
Rate Limiter
------------
In-memory token bucket rate limiter. Limits requests per client IP address
and returns ``429 Too Many Requests`` when exceeded.
.. autoclass:: responder.ext.ratelimit.RateLimiter
:members:
Status Code Helpers
-------------------
+1 -1
View File
@@ -86,7 +86,7 @@ Installation
$ uv pip install responder
Python 3.9 and above. That's it.
Python 3.10 and above. That's it.
.. toctree::
+2 -3
View File
@@ -12,7 +12,7 @@ license = {text = "Apache 2.0"}
authors = [
{ name = "Kenneth Reitz", email = "me@kennethreitz.org" },
]
requires-python = ">=3.9"
requires-python = ">=3.10"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
@@ -21,7 +21,6 @@ classifiers = [
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
@@ -42,7 +41,7 @@ dependencies = [
"pueblo[sfa-full]>=0.0.11",
"pydantic>=2",
"python-multipart",
"starlette[full]>=0.40",
"starlette[full]>=1.0",
"uvicorn[standard]",
]
+1 -1
View File
@@ -1 +1 @@
__version__ = "3.3.0"
__version__ = "3.4.0"
+62
View File
@@ -61,6 +61,31 @@ class API:
lifespan=None,
request_id=False,
):
"""Create a new Responder API instance.
:param debug: If ``True``, enable debug mode with verbose error pages.
:param title: The title of the API, used in OpenAPI documentation.
:param version: The version string for the API (e.g. ``"1.0"``).
:param description: A longer description of the API for OpenAPI docs.
:param terms_of_service: URL to the API's terms of service.
:param contact: Contact information dict for the API (``name``, ``url``, ``email``).
:param license: License information dict (``name``, ``url``).
:param openapi: The OpenAPI version string (e.g. ``"3.0.2"``). Enables OpenAPI schema generation.
:param openapi_route: The URL path for the OpenAPI schema (default ``"/schema.yml"``).
:param static_dir: Directory for static files. Set to ``None`` to disable. Created automatically if missing.
:param static_route: URL prefix for serving static files (default ``"/static"``).
:param templates_dir: Directory for Jinja2 templates (default ``"templates"``).
:param auto_escape: If ``True``, auto-escape HTML/XML in templates.
:param secret_key: Secret key for signing cookie-based sessions. **Always set this in production.**
:param enable_hsts: If ``True``, redirect all HTTP requests to HTTPS.
:param docs_route: URL path for interactive API docs (e.g. ``"/docs"``). Enables OpenAPI if not already set.
:param cors: If ``True``, enable CORS middleware.
:param cors_params: Dict of CORS configuration (``allow_origins``, ``allow_methods``, etc.).
:param allowed_hosts: List of allowed hostnames (e.g. ``["example.com"]``). Defaults to ``["*"]``.
:param openapi_theme: Documentation UI theme: ``"swagger_ui"``, ``"redoc"``, ``"rapidoc"``, or ``"elements"``.
:param lifespan: An async context manager for startup/shutdown logic.
:param request_id: If ``True``, add ``X-Request-ID`` headers to all responses.
""" # noqa: E501
self.background = BackgroundQueue()
self.secret_key = secret_key
@@ -150,12 +175,30 @@ class API:
@property
def static_app(self):
"""The Starlette ``StaticFiles`` application for serving static assets."""
if not hasattr(self, "_static_app"):
assert self.static_dir is not None
self._static_app = StaticFiles(directory=self.static_dir)
return self._static_app
def before_request(self, websocket=False):
"""Register a function to run before every request.
If the hook sets ``resp.status_code``, the route handler is skipped
and the response is sent immediately (short-circuiting).
:param websocket: If ``True``, register as a WebSocket before-request hook instead of HTTP.
Usage::
@api.before_request()
def check_auth(req, resp):
if "Authorization" not in req.headers:
resp.status_code = 401
resp.media = {"error": "unauthorized"}
""" # noqa: E501
def decorator(f):
self.router.before_request(f, websocket=websocket)
return f
@@ -180,6 +223,21 @@ class API:
return decorator
def add_middleware(self, middleware_cls, **middleware_config):
"""Add ASGI middleware to the application.
Middleware wraps the entire application and can inspect or modify
every request and response. Middleware is applied in reverse order —
the last middleware added runs first.
:param middleware_cls: A Starlette-compatible middleware class.
:param middleware_config: Keyword arguments passed to the middleware constructor.
Usage::
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
api.add_middleware(HTTPSRedirectMiddleware)
"""
self.app = middleware_cls(self.app, **middleware_config)
def exception_handler(self, exception_cls):
@@ -501,6 +559,10 @@ class API:
uvicorn.run(self, host=address, port=port, **options)
def run(self, **kwargs):
"""Run the application. Shorthand for :meth:`serve` that inherits the ``debug`` setting.
:param kwargs: Keyword arguments passed through to :meth:`serve`.
"""
if "debug" not in kwargs:
kwargs.update({"debug": self.debug})
self.serve(**kwargs)
+39
View File
@@ -9,7 +9,33 @@ __all__ = ["BackgroundQueue"]
class BackgroundQueue:
"""A queue for running tasks in background threads.
Uses a ``ThreadPoolExecutor`` sized to the number of CPUs. Access it
via ``api.background``.
Usage::
# As a decorator — fire and forget
@api.background.task
def send_email(to, subject):
...
send_email("user@example.com", "Hello")
# Direct submission
future = api.background.run(send_email, "user@example.com", "Hello")
# As a callable (supports async functions)
await api.background(send_email, "user@example.com", "Hello")
"""
def __init__(self, n=None):
"""Create a new background queue.
:param n: Number of worker threads. Defaults to CPU count.
"""
if n is None:
n = multiprocessing.cpu_count()
@@ -18,11 +44,24 @@ class BackgroundQueue:
self.results = []
def run(self, f, *args, **kwargs):
"""Submit a function to run in a background thread.
:param f: The function to run.
:returns: A ``concurrent.futures.Future`` for the result.
"""
f = self.pool.submit(f, *args, **kwargs)
self.results.append(f)
return f
def task(self, f):
"""Decorator that wraps a function to run in the background thread pool.
The decorated function returns a ``Future`` instead of blocking.
Exceptions are printed to stderr via traceback.
:param f: The function to wrap.
"""
def on_future_done(fs):
try:
fs.result()
+79 -10
View File
@@ -49,6 +49,12 @@ class CaseInsensitiveDict(dict):
class QueryDict(dict):
"""A dictionary for query string parameters that handles multi-value keys.
Single-value access returns the last value for a key. Use :meth:`get_list`
to retrieve all values for a multi-value parameter.
"""
def __init__(self, query_string):
self.update(parse_qs(query_string))
@@ -117,6 +123,13 @@ class QueryDict(dict):
class Request:
"""An HTTP request, passed to each view as the first argument.
Provides access to headers, cookies, query parameters, the request body,
session data, and more. Most properties are synchronous; reading the body
(via :attr:`content`, :attr:`text`, or :meth:`media`) requires ``await``.
"""
__slots__ = [
"_starlette",
"formats",
@@ -153,6 +166,7 @@ class Request:
@property
def mimetype(self):
"""The MIME type of the request body, from the ``Content-Type`` header."""
return self.headers.get("Content-Type", "")
@property
@@ -270,6 +284,7 @@ class Request:
@property
def is_secure(self):
"""``True`` if the request was made over HTTPS."""
return self.url.scheme == "https"
def accepts(self, content_type):
@@ -315,6 +330,22 @@ def content_setter(mimetype):
class Response:
"""An HTTP response, passed to each view as the second argument.
Mutate this object to control what gets sent back to the client. Set
:attr:`text`, :attr:`html`, :attr:`media`, or :attr:`content` to define
the body. Use :attr:`headers` and :meth:`set_cookie` to control metadata.
:var text: Set the response body as plain text (sets ``Content-Type: text/plain``).
:var html: Set the response body as HTML (sets ``Content-Type: text/html``).
:var media: Set a Python object (dict, list) to be serialized as JSON (or negotiated format).
:var content: Set the raw response body as bytes.
:var status_code: The HTTP status code (e.g. ``200``, ``404``). Defaults to ``200`` if not set.
:var headers: A dict of response headers.
:var cookies: A ``SimpleCookie`` holding cookies to set on the response.
:var session: A dict of session data. Changes are persisted in a signed cookie.
""" # noqa: E501
__slots__ = [
"req",
"status_code",
@@ -334,23 +365,34 @@ class Response:
def __init__(self, req, *, formats):
self.req = req
#: The HTTP Status Code to use for the Response.
self.status_code: int | None = None
self.content = None #: A bytes representation of the response body.
self.content = None
self.mimetype = None
self.encoding = DEFAULT_ENCODING
self.media = None #: A Python object that will be content-negotiated and
#: sent back to the client. Typically, in JSON formatting.
self.media = None
self._stream = None
self.headers = {} #: A Python dictionary of ``{key: value}``,
#: representing the headers of the response.
self.headers = {}
self.formats = formats
self.cookies: SimpleCookie = SimpleCookie() #: The cookies set in the Response
self.session = (
req.session
) #: The cookie-based session data, in dict form, to add to the Response.
self.cookies: SimpleCookie = SimpleCookie()
self.session = req.session
def stream(self, func, *args, **kwargs):
"""Set up a streaming response from an async generator function.
The generator yields chunks of bytes that are sent to the client
as they are produced, without buffering the full response in memory.
Usage::
@api.route("/stream")
async def stream_data(req, resp):
@resp.stream
async def body():
for i in range(10):
yield f"chunk {i}\\n".encode()
:param func: An async generator function that yields response chunks.
"""
assert inspect.isasyncgenfunction(func)
self._stream = functools.partial(func, *args, **kwargs)
@@ -451,6 +493,12 @@ class Response:
self.mimetype = guessed or "application/octet-stream"
def redirect(self, location, *, set_text=True, status_code=HTTP_301):
"""Redirect the client to a different URL.
:param location: The URL to redirect to.
:param set_text: If ``True``, set a default redirect message as the body.
:param status_code: The HTTP status code (default ``301``).
"""
self.status_code = status_code
if set_text:
self.text = f"Redirecting to: {location}"
@@ -496,6 +544,25 @@ class Response:
secure=False,
httponly=True,
):
"""Set a cookie on the response with full control over directives.
:param key: The cookie name.
:param value: The cookie value.
:param expires: Expiration date string (e.g. ``"Thu, 01 Jan 2026 00:00:00 GMT"``).
:param path: URL path the cookie applies to (default ``"/"``).
:param domain: Domain the cookie is valid for.
:param max_age: Maximum age in seconds before the cookie expires.
:param secure: If ``True``, cookie is only sent over HTTPS.
:param httponly: If ``True`` (default), cookie is inaccessible to JavaScript.
Usage::
resp.set_cookie(
"token", value="abc123",
max_age=3600, secure=True, httponly=True,
)
"""
self.cookies[key] = value
morsel = self.cookies[key]
if expires is not None:
@@ -534,10 +601,12 @@ class Response:
@property
def ok(self):
"""``True`` if the status code is in the 2xx range (success)."""
return 200 <= self.status_code_safe < 300
@property
def status_code_safe(self) -> int:
"""Return the status code, raising ``RuntimeError`` if it hasn't been set."""
if self.status_code is None:
raise RuntimeError("HTTP status code has not been defined")
return self.status_code
+14
View File
@@ -62,6 +62,12 @@ class BaseRoute:
class Route(BaseRoute):
"""An HTTP route that maps a URL pattern to an endpoint.
Supports path parameters with type convertors (``{id:int}``, ``{slug:str}``,
``{pk:uuid}``, ``{value:float}``, ``{rest:path}``).
"""
def __init__(self, route, endpoint, *, before_request=False, methods=None):
assert route.startswith("/"), "Route path must start with '/'"
self.route = route
@@ -197,6 +203,8 @@ class Route(BaseRoute):
class WebSocketRoute(BaseRoute):
"""A WebSocket route that maps a URL pattern to a WebSocket handler."""
def __init__(self, route, endpoint, *, before_request=False):
assert route.startswith("/"), "Route path must start with '/'"
self.route = route
@@ -253,6 +261,12 @@ class WebSocketRoute(BaseRoute):
class Router:
"""The core router that dispatches incoming requests to matching routes.
Handles route matching, before/after request hooks, lifespan events,
and mounted sub-applications.
"""
def __init__(
self, routes=None, default_response=None, before_requests=None, lifespan=None
):