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.