Skip to content

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 Connection, or an ASGI scope dict from a host.

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, loop picks up the running loop once there is one.

None
observer_shutdown_timeout float

Seconds to wait at shutdown for detached @app.on observers to finish.

5.0
trusted_proxies list[str] | str | None

Addresses or CIDRs whose forwarded headers are honoured; installs TrustedProxy.

None
config AppConfig | None

Deploy settings run falls back to. The Configuration guide gives the resolution order.

None
cache_max int | None

Route-resolution cache size. None keeps the router's own default.

None
asgi bool

Mark the app as driven by an external ASGI host, so __call__ converts at the boundary in both directions.

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. 'echo', 'mqtt'); must be unique.

required
handler Callable[..., Awaitable[None]]

Async (reader, writer, ctx) coroutine.

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. mqtts://). Requires the server to be configured with a certificate; startup fails fast otherwise.

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 workers > 1 it is reached on its own port only; the shared port would answer from whichever worker accepted. A stateful protocol with no dedicated port cannot be given one owner at all and is refused before the workers fork. Pass False for a protocol that keeps nothing between exchanges.

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 ['*'] for wildcard.

'*'
allow_methods list[str] | None

HTTP methods permitted in cross-origin requests. Defaults to ['GET', 'POST', 'HEAD', 'OPTIONS'].

None
allow_headers list[str] | str

Request headers permitted; ['*'] allows all.

'*'
allow_credentials bool

Emit Access-Control-Allow-Credentials: true. Cannot be combined with allow_origins=['*'].

False
expose_headers list[str] | None

Response headers the browser JS may read.

None
max_age int | None

Preflight cache lifetime in seconds. None omits the header.

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. "app_startup").

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.

JSONResponse

Bases: Response

JSON response; return from a handler or pass to BlackBull send.

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

path str

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

name str

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

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.

accepted property

True once accept has completed the handshake.

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 text field

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.Headers instance — 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.