Skip to main content

BlackHole Messaging — Python client

Gravicode Studios, led by Kang Fadhil.

Asyncio client for the BlackHole binary protocol: RPC, Pub/Sub, Streaming and Batching over TCP. Speaks the same wire format as the .NET library, verified against it by the interop suite.

Requires Python 3.10+. No dependencies.

Install

pip install blackhole-messaging

Or from this repository:

cd BlackHole/clients/python && pip install -e .

Transports

TCP everywhere, plus Unix domain sockets on Linux and macOS:

client = await connect("127.0.0.1", 5000)                  # TCP
client = await connect_unix("/tmp/blackhole.sock")         # Unix domain socket

Both carry the same wire format; only the connection setup differs. CPython exposes no AF_UNIX to asyncio on Windows, so connect_unix raises there — check BlackHoleClient.unix_supported(). Named pipes and shared memory are .NET-only; see docs/transports.md.

Compare them yourself:

PYTHONPATH=. python example/benchmark.py

Thirty seconds

import asyncio
from blackhole import connect

async def main():
    async with await connect("127.0.0.1", 5000) as client:
        # RPC
        print(await client.call_text("upper", "halo blackhole"))   # HALO BLACKHOLE

        # Pub/Sub, with MQTT-style wildcards
        await client.subscribe(
            "sensor/+/temperature",
            lambda topic, payload: print(topic, payload.decode()),
        )
        await client.publish("sensor/tank-3/temperature", "28.4")
        await asyncio.sleep(1)

asyncio.run(main())

RPC

result = await client.call("echo", b"bytes")
text   = await client.call_text("upper", "halo", timeout=5.0)
await client.notify("log", b"fire and forget")

Every call has a deadline — default_timeout is 30 seconds. Failures raise RpcError rather than hanging:

from blackhole import RpcError

try:
    await client.call("risky", payload, timeout=5.0)
except RpcError as error:
    # Raised when the handler failed, the method is unknown, the deadline passed,
    # or the connection dropped mid-call.
    print(error.method, error)

Serve methods the peer may call on you — handlers may be sync or async, and return bytes or str:

client.register("device/status", lambda request: "ok: 4 sensors online")
client.register("device/read", async_handler)

Pub/Sub

+ matches one segment, # matches the remainder.

await client.subscribe("sensor/+/temperature", on_reading)   # per-filter handler
await client.subscribe("alarm/#", on_alarm)
client.on_publish(lambda topic, payload: ...)                # everything

await client.publish("sensor/tank-3/temperature", "28.4")
await client.unsubscribe("alarm/#")

Streaming

sent = await client.send_stream(
    "firmware-2026",
    open("firmware.bin", "rb"),          # bytes or any binary file object
    descriptor=StreamDescriptor("firmware.bin", size, "application/octet-stream"),
    chunk_size=16 * 1024,
    progress=lambda sent: print(f"{sent / 1024:,.0f} KiB"),
)

client.on_stream(lambda stream_id, descriptor, data: save(stream_id, data))

Chunks are written into the socket buffer and drained once per 64 KiB rather than per chunk, so a small chunk size does not mean a small write.

Batching

from blackhole import Message, MessageType

await client.send_batch([
    Message(MessageType.PUBLISH, f"log/entry/{i}", f"line {i}".encode())
    for i in range(1000)
])

One frame, one socket write. The envelope holds complete BlackHole frames, so the peer unpacks it with the same decoder and each message routes individually.

Wire your handlers before the read loop starts

configure runs after the client is built but before anything is delivered. A server that pushes the instant it accepts would otherwise beat a handler registered after connect returns:

client = await connect(
    "127.0.0.1", 5000,
    configure=lambda c: c.on_publish(handler),
)

The one rule

A received payload is only guaranteed for the duration of your handler. Copy it if you keep it:

client.on_publish(lambda topic, payload: queue.append((topic, bytes(payload))))

Connection

await client.ping()                  # round trip in seconds
client.statistics                    # messages and bytes, both directions
client.is_closed
await client.wait_closed()

ping is timed with perf_counter, not monotonic: on Windows the latter has roughly 15 ms resolution, coarser than a loopback round trip, and would report zero.

Testing

python -m pytest tests/                     # 49 tests
python -m pytest tests/test_protocol.py     # codec only, no .NET needed

The interop suite starts the real .NET server and asserts against it. See ../README.md.

Example

dotnet run --project ../../tests/BlackHole.InteropServer -- --port 5000
PYTHONPATH=. python example/demo.py --port 5000

Built by Gravicode Studios, led by Kang Fadhil.

Metadata

Release files for blackhole-messaging 3.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for blackhole-messaging 3.1.0
File Size Uploaded
blackhole_messaging-3.1.0.tar.gz 20.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for blackhole-messaging 3.1.0
File Interpreter ABI Platform
blackhole_messaging-3.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 34.6 kB

Release files / blackhole_messaging-3.1.0.tar.gz

Download URL blackhole_messaging-3.1.0.tar.gz
Size 20.0 kB
Tags Source
SHA-256 checksum
How to use checksums
30d93256bfb84b3e9bf81fcbed097ee664594fc15a7abe9592054932e01489cf
BLAKE2b-256 checksum
How to use checksums
6d9de25cc109297c07fba55f5080ef2dad907aab641a44322e76c5bb5f961d1e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.10

Release files / blackhole_messaging-3.1.0-py3-none-any.whl

Download URL blackhole_messaging-3.1.0-py3-none-any.whl
Size 14.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2ffef9602cd6ab5203422e47457cd7e95cb40f9e653b404672371f682a2fa78d
BLAKE2b-256 checksum
How to use checksums
2020559dc8dd1feb9efba2da9bc228204f48883ebcbbcc14e54ddae955fc3d4e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.10

Release history Release notifications | RSS feed

This release

3.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page