blackbull.server.sender¶
blackbull.server.sender
¶
AbstractWriter
¶
Bases: ABC
Protocol-agnostic async byte-sink.
write() is the single responsibility: deliver bytes and ensure they
are flushed. Backpressure, buffering, and draining are implementation
details of each concrete subclass — callers never call drain() directly.
Implementors wrap a concrete transport (asyncio.StreamWriter, trio
MemorySendStream, curio socket, …). BaseSender only depends on this
interface, so switching the async runtime requires only a new subclass here.
close()
async
¶
Close the underlying transport. Default: no-op.
sendfile(file, offset, count)
async
¶
Send up to count bytes from file starting at offset.
Default implementation raises NotImplementedError so callers
can detect lack of support and fall back to a read+write loop.
Concrete subclasses opt in when the underlying transport
supports a zero-copy path (Linux sendfile(2) /
loop.sendfile).
Used by the static-file middleware via the
http.response.pathsend ASGI extension.
write(data)
abstractmethod
async
¶
Write data to the transport and ensure it is flushed.
writelines(parts)
async
¶
Write multiple byte segments without joining them in user space.
Default joins-and-writes so subclasses can opt out. Override in
transports whose writelines does vectored I/O (writev /
sendmsg) to skip the full-body memcpy on the static-file
cache-hit path.
AsyncioWriter
¶
Bases: AbstractWriter
Adapts an asyncio-compatible stream to AbstractWriter.
The constructor accepts any object that exposes write(bytes) (sync)
and drain() (async) — the asyncio StreamWriter API — so that test
doubles such as MagicMock can be injected without ceremony.
drain() is called inside write() so the asyncio backpressure
mechanism is handled transparently and BaseSender stays runtime-agnostic.
write_timeout (seconds, 0 = disabled) bounds the time spent
in drain() waiting for the kernel send buffer to flush. Defends
against the slow-read shape of slowloris: a client that reads the
response 1 byte/sec fills the send buffer and our drain blocks
indefinitely waiting for the peer's TCP window to reopen. On
timeout we close the transport and raise ConnectionResetError
so the sender treats the failure the same as a peer-side reset.
sendfile(file, offset, count)
async
¶
Zero-copy loop.sendfile against the underlying transport.
Raises NotImplementedError (propagated from the loop) when
the transport is SSL — TLS framing happens in user-space, so
the kernel can't see the plaintext to copy. Callers must catch
that and fall back to a read+write loop.
Drains any pending writes first so headers we already buffered precede the file bytes in wire order.
writelines(parts)
async
¶
Vectored write via the underlying StreamWriter.
asyncio.StreamWriter.writelines hands the iterable to
transport.writelines, which on the selector transport uses
socket.sendmsg(iovec, …) for the immediate-send case and on
uvloop is implemented as a real vectored write. Either way the
body bytes never get copied into a fresh bytes object before
the syscall.
BaseSender
¶
Bases: ABC
Abstract base for ASGI-event → wire-format senders.
__call__ accepts either:
- bytes body + optional status and headers: the sender builds
and sends the full protocol response (start + body) in one call.
- A protocol-specific event dict: dispatched to the appropriate handler.
The actual byte transport is hidden behind AbstractWriter so the sender
logic is decoupled from asyncio internals.
HTTP1Sender
¶
Bases: BaseSender
Translates content or ASGI HTTP send events into HTTP/1.1 wire-format bytes.
__call__ accepts two forms:
High-level (bytes body + status):
await sender(body_bytes, HTTPStatus.OK, headers=[...])
Writes the status line, headers, blank line, and body in one call.
Low-level (ASGI event dict, for internal/error-handler use):
await sender({'type': 'http.response.start', ...})
await sender({'type': 'http.response.body', ...})
http.response.start is buffered until http.response.body arrives so
that Content-Length can be injected when the app omits it.
__call__(body, status=HTTPStatus.OK, headers=[])
async
¶
Dispatch on body and write the resulting HTTP/1.1 bytes.
Accepted forms:
bytes— emit a complete response: status line, headers (withContent-Lengthinjected if absent), blank line, body.{'type': 'http.response.start', ...}— buffer the status, headers, andtrailersflag; nothing is written yet.{'type': 'http.response.body', ...}— on the first call after a buffered start, flush the start (addingContent-Lengthfor single-body responses orTransfer-Encoding: chunkedwhenmore_body=True); subsequent calls write chunk-framed body bytes and the terminal0\r\n\r\nwhen streaming completes.{'type': 'http.response.trailers', ...}— write the terminal0\r\nfollowed by the trailer headers (chunked encoding).
Unknown event types are logged and dropped; non-dict / non-bytes
bodies raise TypeError.
HTTP2Sender
¶
Bases: BaseSender
Translates content or ASGI HTTP send events into HTTP/2 frames.
__call__ accepts three forms:
High-level (bytes body + status):
await sender(body_bytes, HTTPStatus.OK, headers=[...])
Sends a HEADERS frame followed by a DATA frame.
Low-level (ASGI event dict):
await sender({'type': 'http.response.start', ...})
await sender({'type': 'http.response.body', ...})
Control-plane (raw FrameBase instance):
await sender(settings_frame)
Serialises and writes the frame directly.
adjust_initial_window(delta)
¶
RFC 9113 §6.9.2 — adjust this sender's stream flow-control window by the change in SETTINGS_INITIAL_WINDOW_SIZE since the peer's last announcement. The window may legitimately become negative.
apply_settings(max_frame_size=None)
¶
Apply SETTINGS parameters that do not require delta tracking.
wake_window()
¶
Wake any blocked _write_data() after a window credit change.
SenderFactory
¶
Creates the appropriate BaseSender for the given protocol.
All methods accept a raw asyncio-compatible stream writer and wrap it in
AsyncioWriter internally. To support a different async runtime,
implement a new AbstractWriter subclass and pass it directly to the
sender constructors instead.
WebSocketSender
¶
Bases: BaseSender
Translates ASGI websocket send events or WebSocketResponse dicts into WebSocket wire frames (RFC 6455).
__call__ accepts an ASGI event dict (as returned by WebSocketResponse):
- {'type': 'websocket.send', 'text': ...} → text frame (opcode 0x1)
- {'type': 'websocket.send', 'bytes': ...} → binary frame (opcode 0x2)
- {'type': 'websocket.close'} → close frame (opcode 0x8)
- {'type': 'websocket.accept'} → no-op (handshake already sent)
The status and headers parameters are accepted for interface
consistency but are unused for WebSocket connections.