blackbull¶
blackbull
¶
BlackBull public API. MINOR releases may change contracts.
Importing blackbull loads the server stack. Use blackbull.server.ASGIServer to embed it, or pass a BlackBull instance to an external ASGI host. Request is a deprecated Connection alias; new code should use Connection.
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.
AppConfig
dataclass
¶
Immutable, declarative startup configuration for a BlackBull app.
Every field corresponds one-to-one with a keyword argument of
blackbull.serve / blackbull.BlackBull.run. Fields left
at their sentinel default (None, or 0 for port, or False
for reload) defer to serve's own built-in default unless an
explicit run(...) argument overrides them.
BlackBull
¶
The application: a router, its error handlers, and the ASGI callable.
Register routes with route, or with group when several
should share a middleware prefix; global middleware with use;
lifecycle hooks with on_startup, on_shutdown and
on_warmup. run serves the app on BlackBull's own server,
and the instance is itself a plain ASGI 3.0 callable, so an external host
can drive it instead.
Protocols other than HTTP and WebSocket attach through
add_extension or register_protocol_handler; nothing here
is added by subclassing.
Attributes:
| Name | Type | Description |
|---|---|---|
extensions |
dict[str, object]
|
Installed extensions by name. The Extensions guide says what an extension puts there and how to reach it. |
available_ws_protocols
property
writable
¶
WebSocket subprotocols this app offers, as bytes.
Assigning a list of str encodes each entry.
loop
property
¶
The bound event loop, or None when none is running yet.
Read from synchronous setup code this answers None rather than
raising, so a caller that does not need a loop still works; asyncio
supplies one once the coroutines run.
__call__(conn, receive, send)
async
¶
The ASGI 3.0 callable, and the app's only native/ASGI boundary.
Everything below this method threads a
Connection, in both modes. An ASGI
scope dict arrives only from an external host (uvicorn,
httpx.ASGITransport) or under BB_FORCE_ASGI_SCOPE=1, and is
converted here, once; the same edge wraps the host's send with the
native→ASGI conversion, while BlackBull's own server passes a
native-capable send through untouched. Lifespan is the one scope that
is never a request and never becomes a Connection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conn
|
Connection | dict
|
A |
required |
receive
|
ASGIReceiveCallable | None
|
Optional because a handler that never reads a body never touches it — the router guards on a missing receive and dispatch completes normally. A conforming ASGI host always passes a callable; the tolerance is for direct drives. |
required |
send
|
ASGISendCallable
|
The host's send callable. |
required |
__init__(loop=None, observer_shutdown_timeout=5.0, trusted_proxies=None, config=None, cache_max=None, asgi=False)
¶
Build an application.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
loop
|
AbstractEventLoop | None
|
Event loop to bind to. Left unset,
|
None
|
observer_shutdown_timeout
|
float
|
Seconds to wait at shutdown for
detached |
5.0
|
trusted_proxies
|
list[str] | str | None
|
Addresses or CIDRs whose forwarded headers are
honoured; installs
|
None
|
config
|
AppConfig | None
|
Deploy settings |
None
|
cache_max
|
int | None
|
Route-resolution cache size. |
None
|
asgi
|
bool
|
Mark the app as driven by an external ASGI host, so
|
False
|
add_extension(ext)
¶
Call ext.init_app(app) immediately and return ext.
Subclassing Extension is optional. Optional async startup(app)/shutdown(app) methods are registered as lifespan hooks.
drain_events(timeout=5.0)
async
¶
Wait for detached (@app.on) observers to finish. Returns success.
Only detached observers need waiting for; the events guide says which
do not. False means timeout expired with work still outstanding.
Nothing is cancelled; call again with a longer budget.
enable_grpc(registry)
¶
Serve all four gRPC call shapes from registry over HTTP/2.
Handlers exchange raw message bytes without a protobuf dependency. Use TLS+ALPN or h2c; status is sent in trailers. HTTP routes remain available.
enable_openapi(*, title='BlackBull API', version='0.1.0', description=None, spec_path='/openapi.json', docs_path='/docs')
¶
Publish OpenAPI JSON at spec_path and Swagger UI at docs_path.
Call once. The spec is regenerated per request to include new routes. docs_path=None disables the UI; see docs/guide/openapi.md.
get_routes()
¶
Return a shallow snapshot in registration order, one RouteInfo per HTTP method.
Changing the returned list cannot mutate the live router.
group(middlewares=[])
¶
Return a RouteGroup that prepends middlewares to every route.
intercept(event_name)
¶
Await interceptors in registration order; return the handler unchanged.
Exceptions propagate and abort subsequent interceptors for that event.
on(event_name, *, blocking=False)
¶
Observe an event; the decorated coroutine is returned unchanged.
The default schedules a detached task. blocking=True awaits observers in registration order; cancellation can still interrupt them. Ordinary observer exceptions are logged and isolated. Use intercept for hooks that may change the request, and see docs/guide/events.md for event lifetimes.
on_error(key)
¶
Register a (conn, receive, send) handler for a status or exception class.
An int key is converted to HTTPStatus. conn.state supplies error_status, error_exception when an exception triggered it, and allowed_methods for 405. See docs/guide/error-handling.md for exception-class precedence.
on_shutdown(fn)
¶
Register a zero-argument coroutine; return it unchanged.
Run in registration order before shutdown.complete. An exception aborts remaining hooks.
on_startup(fn)
¶
Register a zero-argument coroutine; return it unchanged.
Run in registration order before startup.complete. An exception aborts remaining hooks and withholds the acknowledgement.
on_warmup(fn)
¶
Register a coroutine to warm the app before it binds or forks.
The hook runs once in the master, before the listening socket is
created and before workers fork; on_startup runs per worker,
after both. In single-worker mode the one process is warmed before it
binds.
Hooks receive the app and must do pure warming only — drive hot
code paths, prime codecs/TLS — and acquire no per-worker resources
(DB pools, sockets, live connections); those belong in
on_startup. Use warm_request to exercise the ASGI
dispatch/handler path in-process, and
blackbull.server.warmup.warm_tls to prime the TLS handshake.
Warm-up is best-effort: a hook's exception is logged and swallowed
(the master degrades to a cold start), and total warm-up time is capped
by BB_WARMUP_BUDGET_S (default 60 s). Multiple hooks run in
registration order. The Workers deployment page shows a hook and
explains what makes a warmed master's heap survive the fork.
raw_handler(name, *, port=None, detector=None, tls=False, stateful=True)
¶
Decorator form of register_protocol_handler.
::
@app.raw_handler('echo', port=9000)
async def echo(reader, writer, ctx):
while data := await reader.read(1024):
await writer.write(data)
register_converter(type_, converter=None)
¶
Convert a custom simplified-handler return type to a supported sendable.
The converter must return Response, str, bytes, None, or a JSON-able mapping, list or dataclass. Pass it directly or omit it to get a decorator. Converters registered after a route still apply to that route.
register_protocol_handler(name, handler, *, detector=None, port=None, tls=False, stateful=True)
¶
Register a handler for a non-ASGI (raw) protocol.
The handler is an async callable (reader, writer, ctx) -> None that
owns the connection for its whole lifetime. When port is set, the
server binds an additional listening socket on it; connections there
skip HTTP detection and go straight to handler.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Protocol name (e.g. |
required |
handler
|
Callable[..., Awaitable[None]]
|
Async |
required |
detector
|
object | None
|
First-byte sniffing on the shared HTTP port, so this protocol can be reached there as well as on its own port. |
None
|
port
|
int | None
|
Dedicated listening port for this protocol. |
None
|
tls
|
bool
|
Serve this port through the server's TLS machinery (e.g.
|
False
|
stateful
|
bool
|
Whether an exchange depends on what an earlier one left
behind — true by default. A stateful protocol is served by one
worker, so with |
True
|
Returns:
| Type | Description |
|---|---|
RawBinding
|
The registered binding. |
route(methods=[HTTPMethod.GET], path='/', scheme=Scheme.http, functions=[], middlewares=[], name=None, accept_query=None)
¶
Register a route handler, optionally wrapping it in middlewares.
accept_query (RFC 10008) names the request media types the
route accepts — it is the value of the Accept-Query response
header, not a switch that enables the QUERY method. (A method is
accepted purely by listing it in methods; QUERY is no different
from GET there.) It is meaningful for the QUERY method, which carries
a request body. When set, the route's responses carry an
Accept-Query header (an RFC 9651 Structured Field list of those
media types), and QUERY requests are Content-Type-enforced: a missing
media type is answered 400, an unaccepted one 415 (with the
Accept-Query header so the client can correct). A handler may raise
UnprocessableQuery for 422 on a well-formed but
unprocessable query. Enforcement applies only to QUERY requests; other
methods on the same route still receive the Accept-Query header.
run(certfile=None, keyfile=None, port=None, unix_path=None, inherited_fd=None, listeners=None, workers=None, max_connections=None, stream_queue_depth=None, ws_queue_depth=None, reload=None, reload_paths=None)
¶
Run synchronously; do not wrap in asyncio.run.
Unset options resolve through BLACKBULL_* environment variables, .env, AppConfig and defaults. BB_* controls server tuning. Multiple workers or reload use a blocking supervisor. External ASGI hosts can call the app; embedded event-loop use requires ASGIServer. See the Configuration guide.
static(url_prefix, root_dir, *, cache=False, index=None, conditional=True)
¶
Mount static GET/HEAD routes under url_prefix.
cache=True retains file bodies in memory. index names a directory index; None disables it. conditional=False disables ETag/Last-Modified and 304 responses. See docs/guide/static-files.md for cache and traversal limits.
to_asgi()
¶
Return the ASGI 3.0 callable to hand to an external host (uvicorn …).
The app is native internally in both modes; BlackBull(asgi=True)
applies the native→ASGI boundary conversion in __call__ whenever
the app is driven (scope entry included), so the app instance itself
is the callable — uvicorn.run(app.to_asgi()) or uvicorn.run(app)
are equivalent. Requires the asgi=True constructor flag: without
it the app is wired for BlackBull's own native server.
url_path_for(name, /, **params)
¶
Return the path for the named route with params substituted.
use(mw)
¶
Register a global middleware applied to every non-lifespan request.
warm_request(conn, *, body=b'', n=1)
async
¶
Invoke the native app n times without sockets, discarding output.
Each iteration copies conn with independent body/state/receive bindings. The synthetic receive yields body once. Intended for on_warmup hooks.
BlackBullDeprecationWarning
¶
Bases: DeprecationWarning
A BlackBull feature that is going away; the message names its entry.
CORS
¶
Cross-Origin Resource Sharing (CORS) middleware.
Handles preflight OPTIONS requests and attaches CORS headers to actual
cross-origin responses. Requests without an Origin header pass through
unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
allow_origins
|
list[str] | str
|
Explicit origin strings or |
'*'
|
allow_methods
|
list[str] | None
|
HTTP methods permitted in cross-origin requests.
Defaults to |
None
|
allow_headers
|
list[str] | str
|
Request headers permitted; |
'*'
|
allow_credentials
|
bool
|
Emit |
False
|
expose_headers
|
list[str] | None
|
Response headers the browser JS may read. |
None
|
max_age
|
int | None
|
Preflight cache lifetime in seconds. |
600
|
Usage::
app = BlackBull()
app.use(CORS(
allow_origins=['https://myapp.example.com'],
allow_credentials=True,
max_age=3600,
))
__init__(allow_origins='*', allow_methods=None, allow_headers='*', allow_credentials=False, expose_headers=None, max_age=600)
¶
Build the middleware.
allow_credentials=True cannot be combined with
allow_origins=['*']: the pair raises ValueError. A wildcard
origin with credentials is what the CORS specification forbids, not
something this middleware chooses to refuse.
ClientDisconnected
¶
Bases: Exception
Raised when the client disconnects before the request body is complete,
and by send once a write has found the client gone.
ASGI signals a mid-body disconnect with an http.disconnect event that
carries no body/more_body keys. Treating it as end-of-message
would return a truncated upload as if it were whole, so
read_body raises this instead — the handler must not process a
partial body as complete. The partial attribute holds whatever body
bytes had arrived before the disconnect.
Connection
dataclass
¶
One HTTP (or WebSocket) request — the single internal representation.
Its creator builds it to the request contract: header names are lowercase
tchar, values carry no edge SP/HTAB and no CTL, host is at most one
valid authority, and scheme is lowercase. The HTTP/1.1 and HTTP/2
parsers and from_scope do; code that builds one directly must too.
The router, dispatcher, middleware and handlers rely on it and check
nothing again. The ASGI scope dict is a derived view
(as_scope). Request is a deprecated alias of this class.
cookies
property
¶
Cookies from the Cookie header, parsed once and cached.
disconnected
property
¶
True once the client dropped mid-request.
A long-running handler polls this to abandon work whose answer nobody is waiting for::
for row in rows:
if conn.disconnected:
break
It goes true when the server notices, at the next receive(),
rather than the instant the peer's FIN lands. The module-level
disconnected reads the same state at the two ASGI boundaries,
where it may live on a scope dict instead.
path_params
property
writable
¶
Router-assigned parameters with converter-coerced values.
query
property
¶
Query params as dict[str, str], parsed once and cached.
Last value wins on repeated keys (?tag=a&tag=b → {'tag': 'b'}).
This is the honest exit for what the declared-parameter form cannot
express — keys not known at registration time, and repeated keys; it
does not coerce types (that is the declared form's job, which also
answers a malformed value with 400 rather than a silent fallback).
query_list
property
¶
Return all query values per key, parsed once; query keeps only the last value.
subprotocols
property
¶
Return client-offered WebSocket subprotocols; empty for HTTP or no offer.
as_scope()
¶
Generate a fresh ASGI 3.0 scope dict (the single native→ASGI point).
Mutations of the returned dict do not affect the Connection, except
that state and extensions are shared by reference so a
buffering middleware's writes reach the handler.
body()
async
¶
Return the complete request body, draining receive at most once.
Repeated calls (and json / text) return the cached
bytes. A mid-body disconnect raises ClientDisconnected. Raises
RuntimeError if stream already consumed the body — the
channel is a single drain, so there is nothing left to buffer.
form()
async
¶
application/x-www-form-urlencoded body, parsed once and cached.
Reads the body through body, so a later json /
text call reuses the cached bytes. On a non-form
Content-Type (or no body) it returns {} rather than raising,
and does not touch the body. Multipart uploads are out of scope — use
stream and parse them manually.
from_scope(scope, receive=None)
classmethod
¶
Build a Connection from an external ASGI scope.
The result keeps the contract of a natively parsed Connection: header
names are lowercase tchar, values carry no edge SP/HTAB and no CTL,
host is at most one valid authority, and scheme is lowercase.
Raises FieldError for headers that cannot meet it. Unknown keys
are ignored; missing optional keys take the field defaults.
json()
async
¶
Parse the cached body as JSON (None on empty/invalid input).
stream()
async
¶
Yield body chunks once; mutually exclusive with body/json/text buffering.
Mixing modes raises RuntimeError; mid-body disconnect raises ClientDisconnected.
text(encoding='utf-8')
async
¶
Decode the cached body as text (errors='replace').
to_asgi_scope(*, force_asgi=False)
¶
Build the compatibility scope with its native Connection stashed.
force_asgi exercises both conversions. Preserve Connection-only OPTIONS state across that rebuild. The caller adds protocol-specific fields afterward; state and extensions remain shared for late updates.
Depends
¶
Inject a zero-argument provider with a default such as db=Depends(get_db).
Async generators yield exactly once and finalize after response sending, LIFO across providers; put cleanup in finally so exceptions cannot skip it. Async functions are awaited; sync functions are called without cleanup. use_cache=True shares one value per provider per request. False invokes it per parameter. Sync generators and nested Depends parameters are rejected.
Event
dataclass
¶
An immutable message dispatched through EventDispatcher.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
The event name (e.g. |
detail |
dict
|
Arbitrary per-event data. |
EventSourceResponse
¶
Bases: StreamingResponse
Format str, UTF-8 bytes or data/event/id/retry mappings as SSE.
Data line endings are normalized. Reject CR/LF in event/id and NUL in id. Defaults to text/event-stream and Cache-Control: no-cache; headers may override.
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.
Headers
¶
Ordered multi-valued headers with bytes names and values.
Lookups ignore name casing; iteration preserves input casing and duplicate order. get returns the first value or its default; getlist returns all matching (name, value) pairs, or an empty list.
__add__(other)
¶
Return a new Headers containing all pairs from self then other.
__eq__(other)
¶
Value equality on the ordered (name, value) pair list.
Two Headers are equal when they carry the same fields in the same
order (RFC 7230 §3.2.2 — order is significant for repeated fields).
Enables Connection round-trip equality.
__getitem__(name)
¶
Return all pairs for name. Raises KeyError if absent.
append(name_or_pairs, value=None)
¶
Append header(s) to the end of the list.
Two-argument form: append(name, value) — adds a single pair.
One-argument form: append(pairs) — adds every pair in the iterable.
from_lowered(pairs)
classmethod
¶
Adopt pairs whose names the caller guarantees are lowercase.
Uppercase names would become unreachable through lookup. Do not mutate the list after handing it over; this path takes ownership instead of copying.
get(name, default=b'')
¶
Return the first value for name, or default if absent.
Mirrors dict.get(key, default): single value, optional default.
For headers that may repeat use getlist(name).
get_combined(name)
¶
Return values joined by ,, or None when absent.
Use only for fields allowing comma combination. For fields such as
Set-Cookie, use getlist instead. A single empty value returns b''.
get_sf_dict(name)
¶
Parse name as a Structured Field Dictionary (RFC 9651).
Multiple field lines are combined first. Returns an ordered
dict of member name → Item / Inner List, or None if the
field is absent or fails strict parsing (per RFC 9651 §4.2 the
whole field is then ignored).
Example::
headers.get_sf_dict(b'priority') # {'u': (2, {}), 'i': (True, {})}
get_sf_item(name)
¶
Parse name as a Structured Field Item (RFC 9651).
Returns (bare_item, parameters), or None if the field is
absent or fails strict parsing (per RFC 9651 §4.2 the whole field
is then ignored).
Example::
headers.get_sf_item(b'deprecation') # (Date(1659578233), {})
get_sf_list(name)
¶
Parse name as a Structured Field List (RFC 9651).
Multiple field lines are combined first. Returns a list of Items /
Inner Lists, or None if the field is absent or fails strict
parsing (per RFC 9651 §4.2 the whole field is then ignored).
Example::
headers.get_sf_list(b'accept-query') # [('a', {}), ('b', {})]
getlist(name)
¶
Return all pairs for name, or [] if the header is absent.
InheritedFd
dataclass
¶
A socket already bound and listening, handed over by a supervisor.
Covers systemd-style activation and the re-exec handoff that keeps the listener continuous across an auto-reload.
Listener
dataclass
¶
One listening socket and what happens on it.
speaks is always a positive name: 'http' selects the stack that
detects HTTP/1.1, h2c and WebSocket upgrades, and a raw protocol names
itself.
tls is the listener's own context, so a second certificate — or mTLS on one port and not another — is sayable.
workers is where ownership is decided, and the only place. Left unset it follows speaks: the HTTP stack is stateless and runs on every worker, while a raw protocol holds state that scattering across workers would scatter with it. Pass it explicitly to override.
RedirectResponse
¶
Bases: Response
Redirect with an empty body and Location; default status is 302.
url must be ASCII; percent-encode non-ASCII characters first.
Response
¶
HTTP body, status and headers; return from a handler or pass to BlackBull send.
An arbitrary ASGI host's raw send does not accept Response objects.
__call__(conn, receive, send)
async
¶
Send a complete native response when invoked directly.
Normalized Response sends use to_native and bypass this method.
to_native()
¶
Convert this response to the unified native message (one send).
The native-path serialiser: a complete Response becomes a single
NativeResponse carrying status, headers,
and body — one object, one send. Symmetric with
NativeResponse.to_asgi (the boundary conversion); streaming
response types drive themselves and are not converted here.
ResponseBody
¶
Bases: dict
http.response.body event with typed property access.
ResponseStart
¶
Bases: dict
http.response.start event with typed property access.
RouteInfo
¶
Bases: NamedTuple
Immutable snapshot of a single registered route entry.
Returned by 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 |
StreamingResponse
¶
Stream from an async iterator; finalize the generator on completion, cancellation or a failed send.
Return from a handler, pass to BlackBull send, or invoke as (conn, receive, send).
Tcp
dataclass
¶
A TCP port, on host or on every interface when it is None.
port=0 asks the OS for a free one; the bound port is read back after
binding.
TrustedProxy
¶
Rewrite conn.client and conn.scheme only behind configured trusted peers.
Proxies must append the observed peer or overwrite the chain. Standalone proto/prefix assertions must replace client-supplied values. Forwarded takes precedence over X-Forwarded-For/Proto; walk right to left to the first untrusted IP. Trust defaults to loopback. See the reverse-proxy guide.
Unix
dataclass
¶
An AF_UNIX path.
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 HTTPException
subclass, it flows through the normal error-router path and quiet 4xx
logging.
WebSocket
¶
One WebSocket connection, as an object.
Constructed by the framework and handed to handlers that ask for it by
annotation (ws: WebSocket) or by name (ws / websocket). A
handler that takes the raw (conn, receive, send) triplet keeps
receiving exactly that, and this object is never built for it.
The handshake is explicit: the connection is not live until
accept returns, and sending before that is an error. To reject a
connection, call close instead of accept.
client
property
¶
(host, port) of the peer; the port is None on a UDS.
client_disconnected
property
¶
True once the peer's close has been observed.
Only ever set by reading — the disconnect arrives on the receive channel, so a handler that has stopped receiving will not see this flip. Use it to break out of a send-only loop that also receives.
close_code
property
¶
The close code, once either side has closed; None while open.
close_reason
property
¶
The peer's close reason, or None until a close has been seen.
connection
property
¶
The underlying Connection.
Everything the handshake carried — headers, cookies, TLS state, the client address — is reachable through it. The shortcuts below cover the fields WebSocket handlers actually reach for.
headers
property
¶
The handshake request headers. Keys are lowercase bytes.
path
property
¶
The request path the handshake arrived on.
path_params
property
¶
Path converters captured by the route that matched.
query_string
property
¶
The raw query string, undecoded, without the leading ?.
subprotocols
property
¶
Subprotocols the client offered, in its order of preference.
__aiter__()
¶
Iterate messages until the peer disconnects.
The disconnect ends the loop rather than raising, so the common shape
is just async for message in ws:.
accept(subprotocol=None, *, headers=None)
async
¶
Complete the handshake.
subprotocol names the one being accepted from subprotocols;
leaving it None keeps the server's automatic negotiation, exactly
as sending {'type': 'websocket.accept', 'subprotocol': None} does
on the raw path. headers are extra response headers for the 101.
Raises WebSocketDisconnect if the peer abandoned the
handshake before it could be completed.
When middleware already accepted for you
(blackbull.middleware.websocket), a bare accept() is a no-op
so the same handler body works with or without it. Asking for a
specific subprotocol or extra headers in that situation raises
instead of silently dropping the request — the 101 has already gone
out and cannot carry them.
close(code=_NORMAL_CLOSURE, reason=None)
async
¶
Close the connection, or reject the handshake.
Called before accept, this rejects the connection — the
client's connect() fails rather than opening and immediately
closing. Idempotent, and a no-op once the peer has already gone, so
a finally: await ws.close() is always safe.
receive()
async
¶
Await one complete message — str for text, bytes for binary.
Fragmented messages have already been reassembled, so what comes back is always a whole application message.
Raises WebSocketDisconnect when the peer closes. Prefer
async for unless you need the close code.
receive_bytes()
async
¶
Await one message, requiring it to be binary.
receive_json()
async
¶
Await one message and parse it as JSON (text or binary).
receive_text()
async
¶
Await one message, requiring it to be text.
send(data)
async
¶
Send data as text or binary, chosen by its Python type.
send_bytes(data)
async
¶
Send one complete binary message.
send_json(data, *, binary=False)
async
¶
JSON-serialise data and send it as one message.
Text by default, which is what browsers and most clients expect; pass
binary=True to send the UTF-8 encoding as a binary frame instead.
send_text(data)
async
¶
Send one complete text message.
WebSocketDisconnect
¶
Bases: Exception
The peer closed the connection.
Raised by WebSocket.receive and its typed variants. Iterating
with async for handles this for you — the loop simply ends — so catch
it only when you call receive() directly and need the close code.
code is the RFC 6455 close code the peer sent, or 1005 when it sent
none. reason is its optional UTF-8 explanation.
WebSocketResponse(content)
¶
Build an ASGI websocket.send event dict from content.
str→{'type': 'websocket.send', 'text': content}bytes→{'type': 'websocket.send', 'bytes': content}- anything else → JSON-serialised into the
textfield
Pass the result directly to the ASGI send callable::
await send(WebSocketResponse('hello'))
__getattr__(name)
¶
Deprecated Request alias for Connection (BBD004).
as_middleware(target)
¶
Decorate a function or class with (conn, receive, send, call_next) middleware.
Normalize downstream HTTP sends to NativeResponse before the middleware's send wrapper. A first request parameter named scope opts into ASGI scope and event dictionaries at both edges, including WebSocket events. Omit the decorator to handle raw downstream send arguments.
cookie_header(name, value, path='/', http_only=True)
¶
Build a set-cookie header tuple for response headers; checked, like
any field, when the NativeResponse carrying it is built.
cookies_from_headers(headers)
¶
Parse the Cookie header(s) into a dict, straight from a headers
object/iterable — no ASGI scope dict involved.
This is the native core: Connection.cookies calls it directly on
conn.headers; parse_cookies is the ASGI-scope-shaped wrapper kept
for external callers that hold a scope dict. Accepts a
blackbull.headers.Headers instance (uses getlist) or a plain
iterable of (name, value) bytes tuples (the ASGI 3.0 form).
parse_cookies(source)
¶
Parse the Cookie request header into a dict.
source is a mapping carrying a 'headers' key — an ASGI scope dict, or
the {'headers': conn.headers} wrapper Connection.cookies passes.
Works identically for HTTP/1.1, HTTP/2, and WebSocket. HTTP/1.1 sends a
single combined Cookie header; HTTP/2 may split it into multiple fields
(RFC 7540 §8.1.2.5). All cookie fields are collected and joined before
parsing, so both wire formats produce the same result.
Accepts either of the two header shapes that may appear on
source['headers']:
- A plain list/iterable of
(name, value)bytes tuples — the standard ASGI 3.0 form, used by external servers (uvicorn, hypercorn,httpx.ASGITransport). - A
blackbull.headers.Headersinstance — what BlackBull's own server attaches as a handler ergonomics enhancement.
parse_response_event(event)
¶
Wrap event in the appropriate typed subclass for dispatch.
The returned object IS the event dict (shallow copy) — pass it directly to downstream send callables without re-serialisation. Trailers and unknown event types are returned unchanged.
ResponseStart/ResponseBody are dict subclasses, not statically
members of ASGISendEvent; a caller threading the result back into a
typed send callable casts at that seam (a runtime no-op) rather than
widening the public union.
read_body(receive)
async
¶
Read the complete request body from the ASGI receive channel.
Raises ClientDisconnected if an http.disconnect arrives
before the body is complete, so a truncated upload is never silently
returned as if whole.
read_json(receive)
async
¶
Read the request body and parse it as JSON.
Returns the parsed JSON value (dict, list, str, int,
float, bool), or None when the body is empty, not valid JSON,
or not decodable. Callers should treat None as a client error and
respond 400::
data = await read_json(receive)
if data is None:
await send(JSONResponse({'error': 'invalid JSON'},
status=HTTPStatus.BAD_REQUEST))
return
Note that a literal JSON null body also parses to None; if that
distinction matters, read the body yourself with read_body.
A mid-body client disconnect propagates as ClientDisconnected
rather than being reported as invalid JSON — a truncated body is a
transport failure, not a parse error.
read_text(receive, encoding='utf-8')
async
¶
Read the request body and decode it as text.
Uses errors='replace' so undecodable bytes become U+FFFD rather than
raising — a malformed body never crashes the handler. Override
encoding for non-UTF-8 payloads.
A mid-body client disconnect still propagates as
ClientDisconnected: a truncated upload must not be decoded and
returned as if it were the complete text.
serve(app, *, certfile=None, keyfile=None, port=0, unix_path=None, inherited_fd=None, listeners=None, workers=None, max_connections=None, stream_queue_depth=None, ws_queue_depth=None, reload=False, reload_paths=None)
¶
Run a BlackBull instance or ASGI 3.0 callable synchronously.
One worker without reload runs in this process; multiple workers or reload use a blocking supervisor. listeners replaces port/unix_path/inherited_fd; passing both is refused. Unset workers, max_connections and queue depths use BB_* settings. Deployment options do not resolve BLACKBULL_* here.