blackbull.router¶
blackbull.router
¶
URL routing for BlackBull.
Router maps (path, method, scheme) triples to handler chains. Paths
support exact strings, regex patterns, and {name} / {name:converter}
parameter syntax; ErrorRouter does the same for HTTPStatus codes and
exception classes. _register_chain composes per-route middlewares with
functools.partial so each middleware receives call_next bound to the
next link.
RouteGroup is defined in :mod:blackbull.app to avoid a circular
import; this module re-exports it lazily through __getattr__.
QUERY = 'QUERY'
module-attribute
¶
The HTTP QUERY method (RFC 10008) — safe, idempotent, cacheable, with a request body.
http.HTTPMethod has no QUERY member (RFC 10008 postdates the 3.15
feature freeze; earliest stdlib arrival is 3.16), so BlackBull exports the
method as a plain string usable anywhere a method is accepted::
from blackbull import QUERY
@app.route(path='/search', methods=[QUERY])
async def search(body: bytes): ...
Because http.HTTPMethod is a StrEnum, this string stays equal- and
hash-compatible with a future HTTPMethod.QUERY member — routes
registered with it need no migration.
ConfigurationError
¶
Bases: Exception
Raised by Router.validate() when route definitions are inconsistent.
ErrorRouter
¶
Maps HTTP error statuses and exception classes to ASGI error-handler functions.
Keys accepted by setitem / getitem: - HTTPStatus value (e.g. HTTPStatus.NOT_FOUND) - Exception class (e.g. ValueError)
Lookup rules
- HTTPStatus key: exact match only.
- Exception class: walks the MRO so a handler registered for a base class (e.g. Exception) catches all unhandled subclasses.
- On a miss, returns the default handler passed at construction
(
Nonewhen no default was given — caller decides the fallback).
Usage::
errors = ErrorRouter()
@errors[HTTPStatus.NOT_FOUND]
async def handle_404(conn, receive, send):
...
@errors[ValueError]
async def handle_value_error(conn, receive, send):
...
handler = errors[HTTPStatus.NOT_FOUND] # → handle_404
handler = errors[KeyError()] # → handle_value_error via MRO (if registered)
handler = errors[KeyError] # same, accepting the class directly
__call__(key)
¶
Decorator form: @errors[HTTPStatus.NOT_FOUND]
__getitem__(key)
¶
Return the registered handler for key, or None if not found.
Accepts
- HTTPStatus → exact match
- exception class → MRO walk
- exception instance → MRO walk on type(key)
__init__(default=None)
¶
default is returned on any lookup miss (error statuses and
unmatched exceptions) instead of None. Only explicitly
registered handlers appear in the two registries, so "which statuses
have custom handlers" stays inspectable.
HTTPException
¶
Bases: Exception
An exception carrying the HTTP status the dispatcher should report.
Raising this from a handler — or from the framework's own request-body
adapter — makes BlackBull._dispatch answer with status instead of
the generic 500, and (for 4xx) log it quietly as a client error rather
than dumping a server traceback. detail is an optional
human-readable message surfaced in development-mode error pages.
MethodNotApplicable
¶
Bases: Exception
Raised when the path exists but the HTTP method is not allowed.
PathNotRegistered
¶
Bases: KeyError
Raised when no registered path matches the requested path.
RouteInfo
¶
Bases: NamedTuple
Immutable snapshot of a single registered route entry.
Returned by :meth:BlackBull.get_routes. One entry is produced per
(route, method) pair — a route registered with
methods=[GET, POST] yields two RouteInfo records.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
str
|
HTTP method string (e.g. |
path |
str
|
URL template (e.g. |
name |
str
|
Endpoint name, or |
Router
¶
String paths live in the routing trie (sole store, including
{param} segments); raw re.Pattern routes live in
self._raw_regex and are scanned only on a trie miss.
cache_max
property
writable
¶
The lookup cache's entry bound (0 disables caching). Kept as a
property delegating to :class:_LookupCache so the constructor knob
and router.cache_max = N retuning both flow to the one store.
__contains__(item)
¶
Accept either a plain str (path only) or a (path, method, scheme) tuple. True when the path was registered verbatim as a string route, or when any raw re.Pattern route matches it.
__getitem__(key)
¶
key: (path: str, method: str | HTTPMethod, scheme: Scheme)
Uses the routing trie for O(path-depth) lookup of string-path routes, then falls back to a linear scan of raw re.Pattern routes.
Results are cached (up to cache_max entries) so repeated requests
to the same (path, method, scheme) skip the trie traversal entirely
after the first hit. The query→miss→resolve→store flow lives here;
the cache mechanics are delegated to _cache_get / _cache_set
so the cache strategy can be swapped without touching this method.
__setitem__(key, value)
¶
If key[0] is a str:
- Insert it into the routing trie under the normalised
(path, methods, scheme) key. {param} / {param:converter}
placeholders become parameter segments with converter functions.
If key[0] is a re.Pattern: - Store it in self._raw_regex (scanned on trie miss).
When scheme is omitted it is stored as _ANY_SCHEME, which matches any scheme at lookup time.
get_routes()
¶
Return a snapshot of all registered routes as :class:RouteInfo.
Routes are returned in registration order. A route registered with
multiple methods (e.g. methods=[GET, POST]) produces one entry
per method, in the order the methods were declared. The returned
list is a fresh shallow copy — callers may sort, filter, or mutate
it without affecting the live router.
register_converter(type_, converter)
¶
Register converter to turn a handler that returns type_ into a sendable (a Response, str/bytes, or JSON-able).
route(methods=[HTTPMethod.GET], path='/', scheme=Scheme.http, functions=[], middlewares=[], name=None, accept_query=None)
¶
Register a function or middleware chain in the routing table.
Three calling conventions:
1. Decorator with no extra middlewares (functions and middlewares
both empty) — returns a decorator via route_fn.
2. functions=[...] — registers a pre-built chain immediately;
returns None (same as before).
3. middlewares=[...] — returns a decorator; the decorated handler is
appended to the middleware list before the chain is registered.
name registers the route for use with url_path_for().
accept_query (RFC 10008) declares the request media types the
route accepts — the value of the Accept-Query response header, and
not a switch for the QUERY method (methods are accepted by being
listed in methods). It drives that header plus Content-Type
enforcement (400 missing / 415 unsupported) on QUERY requests — see
BlackBull.route.
route_fn(methods=[HTTPMethod.GET], path='/', scheme=Scheme.http, name=None, accept_query=None)
¶
Return a decorator that registers the decorated handler.
accept_query (RFC 10008) is the list of request media types the
route accepts — the Accept-Query header value driving Content-Type
enforcement on QUERY requests. It is not a switch for the QUERY
method (methods are accepted by methods); it installs the route
hooks in :func:_accept_query_hooks. See BlackBull.route.
url_path_for(name, /, **params)
¶
Return the path for the named route with params substituted.
Raises KeyError if the name is unknown, ValueError if required params are missing.
validate()
¶
Check all route definitions for consistency, then freeze the router.
Checks performed:
- Every converter spec names a known converter.
- Every path param appears in the handler signature (simplified handlers).
- Converter output type matches the handler's annotation.
Raises :class:ConfigurationError listing all violations found.
Sets self._frozen = True on success so no further routes can
be added. Called once at app boot from :meth:BlackBull.run /
:meth:BlackBull.serve — handler bugs that violate the contract
surface before the first request is served, not after.
UnprocessableQuery
¶
Bases: HTTPException
RFC 10008 §2.2 — a well-formed QUERY whose contents cannot be processed.
Raise this from a QUERY handler when the request media type was accepted
(so 400/415 do not apply) but the query itself is semantically invalid —
references an unknown field, violates a constraint, etc. The dispatcher
answers 422 Unprocessable Content and, being an :class:HTTPException
subclass, it flows through the normal error-router path and quiet 4xx
logging.
request_media_type(conn)
¶
Return the request's media type (Content-Type sans parameters),
lowercased; '' when no Content-Type is present.
conn.headers is always a :class:~blackbull.headers.Headers, so a plain
.get suffices.