Python
sukko is an asyncio-first Python client for the Sukko real-time platform — for backend services, data pipelines, and notebooks. It's built to the same AsyncAPI v1.4.0 + gateway OpenAPI contracts as the TypeScript SDK, with async-iterator delivery, back-pressure, full gap recovery, automatic auth refresh, and a Jupyter-safe sync wrapper. Fully typed (py.typed).
~10 minutes
Install
pip install sukko
Requires Python 3.11+.
Quickstart (async)
The client is an async context manager. subscribe takes a list of channels, and messages — live and recovered — arrive on the messages() async iterator:
import asyncio
from sukko import SukkoClient
async def main():
async with SukkoClient("wss://gateway.example.com/ws", token="<jwt>") as client:
await client.subscribe(["acme.trades"])
async for msg in client.messages():
print(msg.channel, msg.data)
asyncio.run(main())
Sync client (scripts & notebooks)
SyncSukkoClient runs the async client on its own background event loop, so you can use it from plain scripts or a Jupyter cell with no asyncio boilerplate. Iterate delivery with the blocking stream():
from sukko import SyncSukkoClient
with SyncSukkoClient("wss://gateway.example.com/ws", token="<jwt>") as client:
client.subscribe(["acme.trades"])
for msg in client.stream():
print(msg.channel, msg.data)
Publishing
await client.publish("acme.trades", {"price": 100}) # fire-and-forget over the WS connection
await client.rest_publish("acme.trades", {"price": 100}) # awaitable REST — no WS needed
rest_publish resolves on the gateway's ack and works without a live socket; REST publish is available in every edition. The gateway's REST publish response carries the message's stable identity mid — the same mid subscribers receive on the delivered envelope (omitted when a routing rule fans the publish out to multiple topics; see the TypeScript guide for identity-vs-cursor semantics).
Automatic Recovery
On the Kafka backend the SDK recovers missed messages automatically — it reconnects with backoff + jitter and replays from the last position, surfacing recovered records on the same messages() stream (they carry pos). Where recovery can't be confirmed (the Direct backend, or an SSE reconnect) it emits a PossibleGap per channel rather than dropping silently. Match on the delivery item type to handle each:
from sukko import Message, ReplayMessage, Gap, PossibleGap
async for item in client.messages():
if isinstance(item, (Message, ReplayMessage)):
print(item.channel, item.data) # live + recovered messages
elif isinstance(item, Gap):
print("gap on", item.channel, "before", item.last_pos)
elif isinstance(item, PossibleGap):
print("possible gap on", item.channel) # Direct backend / SSE reconnect
The delivery guarantee is at-least-once within the replay window, best-effort beyond — delivered envelopes carry a stable mid identity you can deduplicate on. You can also request history explicitly (any edition, when the server has history enabled) — it arrives as history-flagged messages on the same stream:
await client.history("acme.trades", limit=50)
Auth
Credentials ride request headers by default (Authorization: Bearer / X-API-Key); pass auth_via="query" to use query-param auth instead. Supply an async get_token callback and long-lived tokens are refreshed automatically. An API-key connection can escalate to a JWT mid-session to gain permission-scoped channels:
client = SukkoClient(url, api_key="<key>", get_token=fetch_fresh_jwt)
...
await client.escalate("<jwt>") # gains JWT permissions; re-subscribes newly-permitted channels
Channels
Channels are "{tenant}.{suffix}" — the tenant is the segment before the first dot, the suffix an opaque dotted remainder:
from sukko import build_channel, parse_channel
build_channel("acme", "trades.btc") # -> "acme.trades.btc"
parse_channel("acme.trades.btc") # -> ParsedChannel(tenant="acme", suffix="trades.btc")
Push subscription management PRO
Register a mobile/browser device for push on a user's behalf (this SDK does not receive push). Push requires Pro (Web Push); registering android/ios devices (FCM/APNs) additionally requires Enterprise — below it the gateway rejects those platforms with 403 EDITION_LIMIT:
device_id = await client.push.subscribe(
platform="android", channels=["acme.alerts"], token="<fcm-token>",
)
await client.push.unsubscribe(device_id)
key = await client.push.get_vapid_key()
Errors
Every failure is a typed subclass of sukko.SukkoError — NotConnectedError, EditionRequiredError, RateLimitError, RecoveryInterruptedError, PublishError, and more. Credentials never appear in an error message, repr, or log record.
Next Steps
- Vanilla TypeScript — the TypeScript SDK, same contracts
- Channels — channel types and scoping
- Authentication — JWT and API key setup