Skip to content

blackbull.server.recipient

blackbull.server.recipient

AbstractReader

Bases: ABC

Protocol-agnostic async byte-source.

Mirrors AbstractWriter on the receive side. Implementations wrap a concrete transport so that BaseRecipient subclasses stay runtime-agnostic.

AsyncioReader

Bases: AbstractReader

Adapts an asyncio-compatible stream to AbstractReader.

Accepts any object exposing read(), readuntil(), and readexactly() — the asyncio StreamReader API — so that test doubles such as MagicMock can be injected without ceremony.

BaseRecipient

Bases: ABC

Abstract base for ASGI-event receive callables.

__call__ returns an ASGI event dict appropriate to the protocol: - HTTP: {'type': 'http.request', 'body': ..., 'more_body': False} - WebSocket: {'type': 'websocket.connect'}, {'type': 'websocket.receive', ...}, or {'type': 'websocket.disconnect', ...}

The actual byte transport is hidden behind AbstractReader so the recipient logic is decoupled from asyncio internals.

FragmentAssembler

Accumulates RFC 6455 fragmented frames and signals message completion.

Feed each data/continuation frame via feed(). Returns (message_opcode, full_payload) when the final FIN=1 continuation arrives; returns None while still accumulating.

Raises ProtocolError on violations: - CONTINUATION frame with no fragmentation in progress (§5.4) - New TEXT/BINARY opener while a fragmented message is open (§5.4)

feed(opcode, payload, fin, rsv1=False)

Feed one frame; return (message_opcode, full_payload, compressed) on completion, else None.

HTTP1Recipient

Bases: BaseRecipient

Reads an HTTP/1.1 request body and emits a single http.request event.

Body bytes are read lazily on the first __call__ using the Content-Length or Transfer-Encoding header from scope. Subsequent calls return {'type': 'http.disconnect'}.

HTTP2Recipient

Bases: BaseRecipient

Delivers HTTP/2 DATA frames as ASGI http.request events.

The server loop feeds frames via put_DATAFrame() (non-blocking). The ASGI app calls __call__() which suspends until an event is available, hiding the concurrency from both sides.

For GET-style requests (END_STREAM on HEADERS, no DATA frames), the caller invokes :meth:mark_end_of_stream_on_headers instead of pre-queuing an empty http.request event. The Queue is then never allocated — the empty event is synthesized lazily in :meth:__call__ only if the handler reads it.

mark_end_of_stream_on_headers()

Mark this stream as ended on HEADERS (no body to deliver).

Replaces put_event({type: http.request, body: b'', more_body: False}) with a flag — saves one asyncio.Queue allocation per body-less request.

put_DATAFrame(frame)

Enqueue a DATA frame event. Returns False if the queue is full.

put_disconnect()

Unblock a waiting call() with an http.disconnect event.

Skipped when end-of-stream-on-headers has been delivered and no queue was ever created — no consumer can be waiting.

put_event(event)

Enqueue a pre-built event dict. Returns False if the queue is full.

IncompleteReadError

Bases: EOFError

Raised by AbstractReader when the peer closes the connection mid-read.

Mirrors asyncio.IncompleteReadError but is not tied to asyncio, so handlers that depend on AbstractReader remain runtime-agnostic.

ProtocolError

Bases: Exception

Raised when a WebSocket protocol violation is detected (RFC 6455).

close_code is the RFC 6455 §7.4 status code that should appear in the CLOSE frame sent to the peer. Defaults to 1002 (PROTOCOL_ERROR); UTF-8 violations use 1007.

RecipientFactory

Creates the appropriate BaseRecipient for the given protocol.

All methods that need a reader accept a raw asyncio-compatible stream reader and wrap it in AsyncioReader internally.

WebSocketRecipient

Bases: BaseRecipient

Reads WebSocket frames and emits ASGI websocket.* events.

First call returns {'type': 'websocket.connect'}. Subsequent calls read the next frame from the transport: - Text frame → {'type': 'websocket.receive', 'text': ..., 'bytes': None} - Binary frame → {'type': 'websocket.receive', 'text': None, 'bytes': ...} - Close frame → {'type': 'websocket.disconnect', 'code': 1000} - Ping frame → sends Pong immediately, then reads the next frame - Pong frame → silently dropped, reads the next frame

Ping/pong handling requires write access to the transport, so the raw writer is stored alongside the reader.