mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 06:46:14 +00:00
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:
@@ -17,7 +17,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
$ pip install responder
|
$ pip install responder
|
||||||
|
|
||||||
That's it. Supports Python 3.9+.
|
That's it. Supports Python 3.10+.
|
||||||
|
|
||||||
## The Basics
|
## The Basics
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,45 @@ status code, headers, and cookies.
|
|||||||
:inherited-members:
|
: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
|
Status Code Helpers
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ Installation
|
|||||||
|
|
||||||
$ uv pip install responder
|
$ uv pip install responder
|
||||||
|
|
||||||
Python 3.9 and above. That's it.
|
Python 3.10 and above. That's it.
|
||||||
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
|
|||||||
+2
-3
@@ -12,7 +12,7 @@ license = {text = "Apache 2.0"}
|
|||||||
authors = [
|
authors = [
|
||||||
{ name = "Kenneth Reitz", email = "me@kennethreitz.org" },
|
{ name = "Kenneth Reitz", email = "me@kennethreitz.org" },
|
||||||
]
|
]
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.10"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Environment :: Web Environment",
|
"Environment :: Web Environment",
|
||||||
@@ -21,7 +21,6 @@ classifiers = [
|
|||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
@@ -42,7 +41,7 @@ dependencies = [
|
|||||||
"pueblo[sfa-full]>=0.0.11",
|
"pueblo[sfa-full]>=0.0.11",
|
||||||
"pydantic>=2",
|
"pydantic>=2",
|
||||||
"python-multipart",
|
"python-multipart",
|
||||||
"starlette[full]>=0.40",
|
"starlette[full]>=1.0",
|
||||||
"uvicorn[standard]",
|
"uvicorn[standard]",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "3.3.0"
|
__version__ = "3.4.0"
|
||||||
|
|||||||
@@ -61,6 +61,31 @@ class API:
|
|||||||
lifespan=None,
|
lifespan=None,
|
||||||
request_id=False,
|
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.background = BackgroundQueue()
|
||||||
|
|
||||||
self.secret_key = secret_key
|
self.secret_key = secret_key
|
||||||
@@ -150,12 +175,30 @@ class API:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def static_app(self):
|
def static_app(self):
|
||||||
|
"""The Starlette ``StaticFiles`` application for serving static assets."""
|
||||||
if not hasattr(self, "_static_app"):
|
if not hasattr(self, "_static_app"):
|
||||||
assert self.static_dir is not None
|
assert self.static_dir is not None
|
||||||
self._static_app = StaticFiles(directory=self.static_dir)
|
self._static_app = StaticFiles(directory=self.static_dir)
|
||||||
return self._static_app
|
return self._static_app
|
||||||
|
|
||||||
def before_request(self, websocket=False):
|
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):
|
def decorator(f):
|
||||||
self.router.before_request(f, websocket=websocket)
|
self.router.before_request(f, websocket=websocket)
|
||||||
return f
|
return f
|
||||||
@@ -180,6 +223,21 @@ class API:
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def add_middleware(self, middleware_cls, **middleware_config):
|
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)
|
self.app = middleware_cls(self.app, **middleware_config)
|
||||||
|
|
||||||
def exception_handler(self, exception_cls):
|
def exception_handler(self, exception_cls):
|
||||||
@@ -501,6 +559,10 @@ class API:
|
|||||||
uvicorn.run(self, host=address, port=port, **options)
|
uvicorn.run(self, host=address, port=port, **options)
|
||||||
|
|
||||||
def run(self, **kwargs):
|
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:
|
if "debug" not in kwargs:
|
||||||
kwargs.update({"debug": self.debug})
|
kwargs.update({"debug": self.debug})
|
||||||
self.serve(**kwargs)
|
self.serve(**kwargs)
|
||||||
|
|||||||
@@ -9,7 +9,33 @@ __all__ = ["BackgroundQueue"]
|
|||||||
|
|
||||||
|
|
||||||
class 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):
|
def __init__(self, n=None):
|
||||||
|
"""Create a new background queue.
|
||||||
|
|
||||||
|
:param n: Number of worker threads. Defaults to CPU count.
|
||||||
|
"""
|
||||||
if n is None:
|
if n is None:
|
||||||
n = multiprocessing.cpu_count()
|
n = multiprocessing.cpu_count()
|
||||||
|
|
||||||
@@ -18,11 +44,24 @@ class BackgroundQueue:
|
|||||||
self.results = []
|
self.results = []
|
||||||
|
|
||||||
def run(self, f, *args, **kwargs):
|
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)
|
f = self.pool.submit(f, *args, **kwargs)
|
||||||
self.results.append(f)
|
self.results.append(f)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
def task(self, 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):
|
def on_future_done(fs):
|
||||||
try:
|
try:
|
||||||
fs.result()
|
fs.result()
|
||||||
|
|||||||
+79
-10
@@ -49,6 +49,12 @@ class CaseInsensitiveDict(dict):
|
|||||||
|
|
||||||
|
|
||||||
class QueryDict(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):
|
def __init__(self, query_string):
|
||||||
self.update(parse_qs(query_string))
|
self.update(parse_qs(query_string))
|
||||||
|
|
||||||
@@ -117,6 +123,13 @@ class QueryDict(dict):
|
|||||||
|
|
||||||
|
|
||||||
class Request:
|
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__ = [
|
__slots__ = [
|
||||||
"_starlette",
|
"_starlette",
|
||||||
"formats",
|
"formats",
|
||||||
@@ -153,6 +166,7 @@ class Request:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def mimetype(self):
|
def mimetype(self):
|
||||||
|
"""The MIME type of the request body, from the ``Content-Type`` header."""
|
||||||
return self.headers.get("Content-Type", "")
|
return self.headers.get("Content-Type", "")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -270,6 +284,7 @@ class Request:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_secure(self):
|
def is_secure(self):
|
||||||
|
"""``True`` if the request was made over HTTPS."""
|
||||||
return self.url.scheme == "https"
|
return self.url.scheme == "https"
|
||||||
|
|
||||||
def accepts(self, content_type):
|
def accepts(self, content_type):
|
||||||
@@ -315,6 +330,22 @@ def content_setter(mimetype):
|
|||||||
|
|
||||||
|
|
||||||
class Response:
|
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__ = [
|
__slots__ = [
|
||||||
"req",
|
"req",
|
||||||
"status_code",
|
"status_code",
|
||||||
@@ -334,23 +365,34 @@ class Response:
|
|||||||
|
|
||||||
def __init__(self, req, *, formats):
|
def __init__(self, req, *, formats):
|
||||||
self.req = req
|
self.req = req
|
||||||
#: The HTTP Status Code to use for the Response.
|
|
||||||
self.status_code: int | None = None
|
self.status_code: int | None = None
|
||||||
self.content = None #: A bytes representation of the response body.
|
self.content = None
|
||||||
self.mimetype = None
|
self.mimetype = None
|
||||||
self.encoding = DEFAULT_ENCODING
|
self.encoding = DEFAULT_ENCODING
|
||||||
self.media = None #: A Python object that will be content-negotiated and
|
self.media = None
|
||||||
#: sent back to the client. Typically, in JSON formatting.
|
|
||||||
self._stream = None
|
self._stream = None
|
||||||
self.headers = {} #: A Python dictionary of ``{key: value}``,
|
self.headers = {}
|
||||||
#: representing the headers of the response.
|
|
||||||
self.formats = formats
|
self.formats = formats
|
||||||
self.cookies: SimpleCookie = SimpleCookie() #: The cookies set in the Response
|
self.cookies: SimpleCookie = SimpleCookie()
|
||||||
self.session = (
|
self.session = req.session
|
||||||
req.session
|
|
||||||
) #: The cookie-based session data, in dict form, to add to the Response.
|
|
||||||
|
|
||||||
def stream(self, func, *args, **kwargs):
|
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)
|
assert inspect.isasyncgenfunction(func)
|
||||||
|
|
||||||
self._stream = functools.partial(func, *args, **kwargs)
|
self._stream = functools.partial(func, *args, **kwargs)
|
||||||
@@ -451,6 +493,12 @@ class Response:
|
|||||||
self.mimetype = guessed or "application/octet-stream"
|
self.mimetype = guessed or "application/octet-stream"
|
||||||
|
|
||||||
def redirect(self, location, *, set_text=True, status_code=HTTP_301):
|
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
|
self.status_code = status_code
|
||||||
if set_text:
|
if set_text:
|
||||||
self.text = f"Redirecting to: {location}"
|
self.text = f"Redirecting to: {location}"
|
||||||
@@ -496,6 +544,25 @@ class Response:
|
|||||||
secure=False,
|
secure=False,
|
||||||
httponly=True,
|
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
|
self.cookies[key] = value
|
||||||
morsel = self.cookies[key]
|
morsel = self.cookies[key]
|
||||||
if expires is not None:
|
if expires is not None:
|
||||||
@@ -534,10 +601,12 @@ class Response:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def ok(self):
|
def ok(self):
|
||||||
|
"""``True`` if the status code is in the 2xx range (success)."""
|
||||||
return 200 <= self.status_code_safe < 300
|
return 200 <= self.status_code_safe < 300
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status_code_safe(self) -> int:
|
def status_code_safe(self) -> int:
|
||||||
|
"""Return the status code, raising ``RuntimeError`` if it hasn't been set."""
|
||||||
if self.status_code is None:
|
if self.status_code is None:
|
||||||
raise RuntimeError("HTTP status code has not been defined")
|
raise RuntimeError("HTTP status code has not been defined")
|
||||||
return self.status_code
|
return self.status_code
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ class BaseRoute:
|
|||||||
|
|
||||||
|
|
||||||
class Route(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):
|
def __init__(self, route, endpoint, *, before_request=False, methods=None):
|
||||||
assert route.startswith("/"), "Route path must start with '/'"
|
assert route.startswith("/"), "Route path must start with '/'"
|
||||||
self.route = route
|
self.route = route
|
||||||
@@ -197,6 +203,8 @@ class Route(BaseRoute):
|
|||||||
|
|
||||||
|
|
||||||
class WebSocketRoute(BaseRoute):
|
class WebSocketRoute(BaseRoute):
|
||||||
|
"""A WebSocket route that maps a URL pattern to a WebSocket handler."""
|
||||||
|
|
||||||
def __init__(self, route, endpoint, *, before_request=False):
|
def __init__(self, route, endpoint, *, before_request=False):
|
||||||
assert route.startswith("/"), "Route path must start with '/'"
|
assert route.startswith("/"), "Route path must start with '/'"
|
||||||
self.route = route
|
self.route = route
|
||||||
@@ -253,6 +261,12 @@ class WebSocketRoute(BaseRoute):
|
|||||||
|
|
||||||
|
|
||||||
class Router:
|
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__(
|
def __init__(
|
||||||
self, routes=None, default_response=None, before_requests=None, lifespan=None
|
self, routes=None, default_response=None, before_requests=None, lifespan=None
|
||||||
):
|
):
|
||||||
|
|||||||
Reference in New Issue
Block a user