Skip to content

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 (None when 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. "GET", "BREW").

path str

URL template (e.g. "/api/echo/{name}").

name str

Endpoint name, or "" if the route was registered unnamed.

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.