Named events
Register coroutine handlers by function name, then receive decoded WSMessage objects for matching events.
Python · WebSocket client
aiohttp_ws_clientConnect to a Salus WebSocket endpoint, route named events, exchange request/response messages, and reconstruct chunked payloads.
Register coroutine handlers by function name, then receive decoded WSMessage objects for matching events.
Attach a generated response_id, wait for a matching RESPONSE, and return the response message.
Send strings, numbers, bytes, dictionaries, or lists. Bytes are base64 encoded; JSON values remain structured.
After a connection ends, retry a configurable number of times with a fixed delay between attempts.
on_connect are awaited or scheduled as coroutines. Define them with async def.
Install aiohttp and make toolbox importable; the current file imports both. Create your own client with a ws:// or wss:// URL and register handlers before connecting. Importing also constructs a module-level demo client without starting it; its demo callback reads a local PDF if that demo client is later connected. Use a new Client instance, as below.
from aiohttp_ws_client import Client
client = Client("wss://example.com/ws")
@client.event()
async def notification(message):
print("notification:", message.data)
@client.r_event()
async def add(message):
left = message.data["left"]
right = message.data["right"]
return {"total": left + right}
async def connected(active_client):
response = await active_client.send_req(
{"left": 2, "right": 3},
"add",
timeout=2_000,
)
if response is None:
print("request timed out")
else:
print(response.data)
client.on_connect = connected
client.run()
__name__ as the event key. A function named notification handles the wire event "notification"; matching is case-sensitive.
In an async application, await websocket() from your own loop instead of calling the blocking run() helper.
import asyncio
from aiohttp_ws_client import Client
async def main():
client = Client("wss://example.com/ws")
await client.websocket(headers={"Authorization": "Bearer …"})
asyncio.run(main())
The dispatcher selects one of three paths after decoding and, when needed, reconstructing a message:
RESPONSE with a known response_id completes a pending send_req().response_id is sent to a matching r_event handler. Its return value is sent back as RESPONSE.response_id is sent to a matching event handler.@client.event()
async def account_updated(message):
account = message.data
print(account["id"])
@client.r_event()
async def lookup_account(message):
account_id = message.data["account_id"]
return {"id": account_id, "status": "active"}
The returned value must be accepted by send_msg(): str, int, float, bytes, dict, or list.
reply = await client.send_req(
{"account_id": 42},
"lookup_account",
check_interval=10, # milliseconds
timeout=2_000, # milliseconds
)
if reply is None:
print("No reply arrived before the deadline")
else:
print(reply.event, reply.data)
None
send_req() does not raise a timeout exception. It polls the pending response table and returns None if no response arrives in time.
Every logical message is sent as a JSON text frame with three required keys.
{
"event": "account_updated",
"type": "json",
"data": {"id": 42, "status": "active"}
}
| Field | Required | Meaning |
|---|---|---|
event | Yes | Case-sensitive handler name. RESPONSE is reserved for replies. |
type | Yes | string, bytes, or json. |
data | Yes | The encoded payload. Bytes use base64; JSON data is embedded directly. |
response_id | No | Correlates a request with its response. |
construct_id | No | Groups frames belonging to one chunked message. |
construct_index | With construct | Zero-based chunk order. |
construct_final | With construct | Marks the final chunk and triggers reconstruction. |
| Python input | Wire type | Wire data | Decoded message.data |
|---|---|---|---|
str | string | String | str |
int, float | string | String form | str |
bytes | bytes | Base64 string | bytes |
dict, list | json | JSON object or array | dict or list |
Other input types raise TypeError. Booleans are not accepted because the implementation checks exact class names rather than using normal numeric subtype behavior.
_stream_msg() divides a large logical payload into messages with a shared construct ID. The receiver buffers chunks, sorts them by index, and dispatches only after the final chunk arrives.
await client._stream_msg(
large_payload,
"archive_uploaded",
chunk_size=100 * 2**10,
)
_stream_msg() as internal. The current implementation has inconsistent byte/string chunk handling; test it against the exact server version before depending on it.
websocket() creates an aiohttp.ClientSession and connects with max_msg_size.client.ws, marks the client connected, and schedules on_connect(client).check(). Handler exceptions are printed and the receive loop continues.re_dur seconds and retries up to re_ret times.re_failed becomes true and the connection ends.A reconnected socket that lasts fewer than 20 seconds is treated as an unstable reconnect and raises TimeoutError into the retry loop.
on_connect runs in its own task
The receive loop does not wait for it to finish. Handle exceptions inside the callback if you need deterministic reporting.
ping() uses the application-level PING request handler installed by the constructor. It is not an aiohttp WebSocket control-frame ping.
latency_ms = await client.ping(
msg_size=128,
trial_count=5,
send_as_bytes=False,
timeout=1_000,
)
print(f"Average: {latency_ms} ms")
| Attribute | Default | Purpose |
|---|---|---|
url | Constructor value | WebSocket URL passed to ws_connect(). |
max_msg_size | 4_194_304_000 | Maximum aiohttp message size in bytes. |
re_ret | 2 | Reconnect attempts after a disconnect. |
re_dur | 5 | Seconds to wait before each reconnect attempt. |
re_log | True | Print connection and reconnect status. |
connected | False | True while the socket receive loop is active. |
reconnecting | False | True after a disconnect while retrying. |
reconnected | False | Records that at least one reconnection succeeded. |
re_failed | False | Stops further retries after failure or shutdown logic sets it. |
ws | None | Current aiohttp WebSocket connection. |
events, r_events | Mappings | Registered one-way and request handler functions. |
reqs, constructs | Mappings | Internal pending-response and chunk-reassembly state. |
client = Client("wss://example.com/ws")
client.max_msg_size = 16 * 2**20
client.re_ret = 5
client.re_dur = 2
client.re_log = False
Client API referenceThe methods below reflect the historical client snapshot. Methods without a leading underscore are presented as public, except where a caveat is explicitly noted.
Client(url)
ws:// or wss://.event()client.event()
Returns a decorator that registers a one-way event handler under the decorated function’s name.
r_event()client.r_event()
Returns a decorator that registers a request handler. The handler’s return value is automatically sent as a RESPONSE.
on_connectclient.on_connect = async_callable
A property for the callback scheduled after each successful connection. Assigning a non-callable raises TypeError.
run()client.run()
Schedules websocket() on the current event loop and calls run_forever(). This blocks the calling thread and is intended for a standalone process.
websocket()await client.websocket(headers={})
aiohttp.ClientSession.None after the connection and reconnect lifecycle finishes.send_msg()await client.send_msg(data, event, msg_dict=None)
event, type, and data.None. If no socket exists, it silently sends nothing.send_req()await client.send_req(data, event, check_interval=10, timeout=2000)
WSMessage, or None when no socket is assigned or the wait times out. A reset invokes the broken close() path described below and can raise instead.check()await client.check(raw_msg)
Internal dispatch entry point for an aiohttp message. Returns True if it buffered or handled the message and False if parsing failed or no handler matched.
ping()await client.ping(msg_size=0, trial_count=1, send_as_bytes=False, timeout=1000, interval=.5)
Sends repeated application-level PING requests and returns the average elapsed milliseconds. It does not check whether a reply arrived, so a measured duration can represent timeouts. Despite its name, interval is passed as the response polling interval.
close()await client.close()
Intended to close the active socket and clear client state. The current implementation references undefined names; see Known caveats before calling it.
_stream_msg()await client._stream_msg(data, event, chunk_size=102400)
Private helper that splits a payload into construct frames. Treat it as experimental and test interoperability for the payload type you use.
WSMessage API referenceWSMessage is the decoded object supplied to handlers and returned by successful requests.
| Attribute | Meaning |
|---|---|
msg_type | Original aiohttp.WSMsgType. |
msg | Original aiohttp message, when created with from_msg(). |
event | Decoded event name. |
type | Protocol payload type: string, bytes, or json. |
data | Decoded application payload. |
response_id | Request correlation ID, if present. |
construct_id | Chunk group ID, if present. |
construct_index | Chunk position. |
construct_final | Whether this is the final chunk. |
WSMessage.from_msg(msg)Builds a message from an aiohttp WebSocket text message. Non-text frames produce a mostly empty WSMessage.
WSMessage.from_msg_dict(msg_dict)Builds a message from an already decoded protocol dictionary. The current construct-ID assignment contains a key mix-up; see the caveats below.
WSMessage.construct_msg(msg_list)Sorts chunks by construct_index, joins their data, and decodes the reconstructed value according to the first chunk’s type.
WSMessage.json_load(s)Thin wrapper around json.loads(s).
ts(dt=None) returns dt.timestamp(), using a naive datetime.utcnow() when omitted. Since timestamp() interprets a naive datetime as local time, the default can be offset on non-UTC hosts; pass an aware UTC datetime for accurate timestamps. from_ts(t) calls datetime.fromtimestamp(t), which returns local time for the host environment.
A Client owns connection state and callback dictionaries. It creates a native aiohttp.ClientWebSocketResponse in client.ws when websocket() connects. Each received text envelope becomes a separate WSMessage. User handlers receive that message only, so capture the owning client in a closure when sending a reply or another request.
| Object/member | Arguments and result | State and usage |
|---|---|---|
Client(url) | Synchronous constructor; returns a disconnected client. | Does not connect. Set max_msg_size, re_ret, re_dur, callbacks, and event handlers before starting. |
client.event() / client.r_event() | Return decorators; each decorator accepts an async function and returns it. | Keyed by the function's case-sensitive name. Re-registering a name replaces the stored callback. Request handlers must return a supported payload; ordinary event return values are ignored. |
client.on_connect | Assign an async callable accepting this Client. | Scheduled after each successful connection, rather than awaited by the receive loop. The property rejects non-callables but does not itself check coroutine status. |
client.websocket(headers={}) | Async, long-lived operation returning None after its receive/retry lifecycle. | Creates a session and sets ws/connected. Use an application task in an existing loop. The current reconnect and session-cleanup limitations still apply. |
client.send_msg(data, event, msg_dict=None) | Async; normally returns None. | Encodes and sends immediately. If ws is None, returns without sending. If provided, msg_dict is mutated with event, type, and data; do not reuse it across concurrent sends. |
client.send_req(data, event, check_interval=10, timeout=2000) | Async; returns a decoded WSMessage or None on timeout/no assigned socket. | Creates a correlation ID in reqs, sends, then polls until a reply arrives or the timeout expires. Both timing arguments are milliseconds. A send failure can raise; it is not a guaranteed-None operation. |
client.check(raw_msg) | Async; returns True when buffered/dispatched, otherwise False. | Normally owned by the receive loop. Populates reqs for replies and constructs for partial messages. It awaits application handlers directly. |
client.ping(msg_size=0, trial_count=1, send_as_bytes=False, timeout=1000, interval=.5) | Async; returns average elapsed milliseconds as a number. | Uses the built-in PING event and does not establish that every trial received a reply. Use send_req(..., "PING") directly when success/failure matters. |
WSMessage() | Synchronous construction of an empty message. | Most applications use messages supplied by callbacks. Relevant fields are data, event, type, response_id, and the original msg/msg_type where available. |
WSMessage.from_msg_dict(envelope) / WSMessage.from_msg(raw) | Call on the class; return a new decoded object synchronously. | The former accepts the protocol dictionary; the latter accepts an aiohttp message. Required dictionary fields are event, type, and data. Invalid input may raise; Client.check() catches initial decode errors. |
WSMessage.construct_msg(parts) | Call on the class; returns the combined message. | Sorts the supplied list in place by construct_index. Existing chunk-format limitations apply; ordinary send_msg() avoids this experimental path. |
connected describes the receive-loop state; ws can still refer to a closed socket after a disconnect. Check client.ws is not None and not client.ws.closed before low-level operations. reqs and constructs are implementation state, not application storage.
Use the class factory to inspect the same object shape an event handler receives. Ordinary messages do not require construct fields.
from aiohttp_ws_client import WSMessage
message = WSMessage.from_msg_dict({
"event": "RESULT", "type": "json",
"data": {"count": 3, "ready": True},
"response_id": "request-1",
})
print(message.event) # RESULT
print(message.data["count"]) # 3
print(message.response_id) # request-1The server for this example must implement a LOOKUP request event. The event callback schedules work and returns so the receive loop can process the reply. Retain task references until completion and handle request errors inside the task.
import asyncio
from aiohttp_ws_client import Client
client = Client("wss://example.com/ws")
jobs = set()
async def fetch_details(item_id):
try:
reply = await client.send_req({"id": item_id}, "LOOKUP", timeout=3000)
if reply is None:
print("No details received")
else:
print(reply.data)
except Exception as error:
print("Lookup failed:", error)
@client.event()
async def ITEM_CHANGED(message):
task = asyncio.create_task(fetch_details(message.data["id"]))
jobs.add(task)
task.add_done_callback(jobs.discard)
client.run()Bytes passed to send_msg() use base64 in the JSON envelope and arrive decoded. This callback accepts a file fragment, acknowledges its byte count, then closes the current socket while disabling reconnect. It works around the module's broken Client.close(); the separate unclosed-session limitation remains.
from aiohttp_ws_client import Client
client = Client("wss://example.com/ws")
client.max_msg_size = 1024 * 1024
@client.event()
async def FILE_FRAGMENT(message):
if message.type != "bytes":
return
fragment = message.data
await client.send_msg({"received_bytes": len(fragment)}, "ACK")
client.re_failed = True
if client.ws is not None and not client.ws.closed:
await client.ws.close()
client.run()Confirm that the wire event exactly matches the decorated function name, including case. Use @client.event() for messages without response_id and @client.r_event() for messages that contain one.
send_req() always returns NoneVerify the client is connected, the remote handler sends an event named RESPONSE, and the response preserves the original response_id. Increase timeout if the handler takes longer than two seconds.
This is expected: int and float inputs use the protocol type string. Wrap numeric data in a dictionary or list if you need to preserve JSON number types.
A successful reconnect lasting less than 20 seconds is treated as unstable. Check server logs, authentication expiry, proxy idle limits, and whether the endpoint accepts the same headers on reconnect.
check() catches decoding failures and returns False. The receive loop also catches handler exceptions, prints a traceback, and continues. Capture standard error in production so these failures remain visible.
close() checks self.closed even though the constructor does not initialize it, and references ws instead of self.ws. It can raise AttributeError or NameError.websocket() without the original headers, so authentication headers from the initial call are not preserved. The initial connection attempt occurs outside the retry loop and may raise directly.websocket(headers={}) uses a mutable default argument. The function does not currently mutate it, but callers should still pass an explicit dictionary.ClientSession is not wrapped in async with or explicitly closed, which can produce unclosed-session warnings.send_req() on the same client, the loop cannot read the reply until that handler returns. Schedule outbound request work in a separate task, as the built-in on_connect path does.send_req() polls shared state instead of awaiting a future; very small or invalid check_interval values can create division errors or ineffective timeouts.ping(interval=.5) passes .5 as milliseconds, which truncates timeout / check_interval and sleeps for 0.0005 seconds per poll.send_msg() prints every serialized outgoing message, which may expose payloads in logs._stream_msg() and construct_msg() disagree on some text/byte joins in the current module. Validate chunked messages end to end.WSMessage.from_msg_dict() assigns construct_id from response_id instead of construct_id.on_connect(client) function. That function is demo code that reads a local PDF and is not part of the reusable client API.Generated from modules/aiohttp_ws_client.py; no module version is declared. Includes public functions, classes, directly declared methods, properties, and Python protocol methods. Conditional APIs may require optional dependencies. Inherited members and dynamically assigned attributes are explained in the guide where relevant.
Signatures and docstrings are extracted without importing or running the module. A property or AsyncProperty decorator changes how a member is accessed; see its decorators and the guide.
ts(dt=None)Source line 47
| Parameter | Passing convention | Default / required |
|---|---|---|
dt | positional or keyword | None |
from_ts(t)Source line 51
| Parameter | Passing convention | Default / required |
|---|---|---|
t | positional or keyword | required |
class ClientSource line 57
Construct: Client(url)
Fields assigned by the constructor: connected, constructs, default_status, error_handlers, events, max_msg_size, r_events, re_dur, re_failed, re_log, re_ret, reconnected, reconnecting, reqs, routes, url, ws, ws_endpoints. Some assignments may be conditional; see the object guide for meaning and lifecycle.
Declared functions, properties, and nested objects:
Client.__init__ — methodClient.r_event — methodClient.event — methodClient.on_connect — propertyClient.on_connect — property setterClient.run — methodClient.websocket — async methodClient.close — async methodClient.send_req — async methodClient.send_msg — async methodClient.check — async methodClient.ping — async methodClient.__init__(self, url)Source line 58
| Parameter | Passing convention | Default / required |
|---|---|---|
url | positional or keyword | required |
Client.r_event(self)Source line 88
No caller-supplied parameters are declared.
Client.event(self)Source line 93
No caller-supplied parameters are declared.
Client.on_connect(self)Source line 100
Decorators: @property
No caller-supplied parameters are declared.
Client.on_connect(self, value)Source line 103
Decorators: @on_connect.setter
| Parameter | Passing convention | Default / required |
|---|---|---|
value | positional or keyword | required |
Client.run(self)Source line 108
No caller-supplied parameters are declared.
async Client.websocket(self, headers={})Source line 113
| Parameter | Passing convention | Default / required |
|---|---|---|
headers | positional or keyword | {} |
Declared with async def. Normal calls return an awaitable, or an async generator if the body yields. Decorated properties may be accessed differently; follow the object guide.
async Client.close(self)Source line 168
No caller-supplied parameters are declared.
Declared with async def. Normal calls return an awaitable, or an async generator if the body yields. Decorated properties may be accessed differently; follow the object guide.
async Client.send_req(self, data, event, check_interval=10, timeout=2000)Source line 174
| Parameter | Passing convention | Default / required |
|---|---|---|
data | positional or keyword | required |
event | positional or keyword | required |
check_interval | positional or keyword | 10 |
timeout | positional or keyword | 2000 |
Declared with async def. Normal calls return an awaitable, or an async generator if the body yields. Decorated properties may be accessed differently; follow the object guide.
async Client.send_msg(self, data, event, msg_dict=None)Source line 201
| Parameter | Passing convention | Default / required |
|---|---|---|
data | positional or keyword | required |
event | positional or keyword | required |
msg_dict | positional or keyword | None |
Declared with async def. Normal calls return an awaitable, or an async generator if the body yields. Decorated properties may be accessed differently; follow the object guide.
async Client.check(self, raw_msg)Source line 234
| Parameter | Passing convention | Default / required |
|---|---|---|
raw_msg | positional or keyword | required |
Declared with async def. Normal calls return an awaitable, or an async generator if the body yields. Decorated properties may be accessed differently; follow the object guide.
async Client.ping(self, msg_size=0, trial_count=1, send_as_bytes=False, timeout=1000, interval=0.5)Source line 300
| Parameter | Passing convention | Default / required |
|---|---|---|
msg_size | positional or keyword | 0 |
trial_count | positional or keyword | 1 |
send_as_bytes | positional or keyword | False |
timeout | positional or keyword | 1000 |
interval | positional or keyword | 0.5 |
Declared with async def. Normal calls return an awaitable, or an async generator if the body yields. Decorated properties may be accessed differently; follow the object guide.
class WSMessageSource line 312
Construct: WSMessage()
Fields assigned by the constructor: construct_final, construct_id, construct_index, data, event, msg, msg_type, response_id, type. Some assignments may be conditional; see the object guide for meaning and lifecycle.
Declared functions, properties, and nested objects:
WSMessage.json_load — methodWSMessage.__init__ — methodWSMessage.__repr__ — methodWSMessage.construct_msg — methodWSMessage.from_msg — methodWSMessage.from_msg_dict — methodWSMessage.json_load(s)Source line 313
| Parameter | Passing convention | Default / required |
|---|---|---|
s | positional or keyword | required |
WSMessage.__init__(self)Source line 315
No caller-supplied parameters are declared.
WSMessage.__repr__(self)Source line 325
No caller-supplied parameters are declared.
WSMessage.construct_msg(msg_list)Source line 327
| Parameter | Passing convention | Default / required |
|---|---|---|
msg_list | positional or keyword | required |
WSMessage.from_msg(msg)Source line 346
| Parameter | Passing convention | Default / required |
|---|---|---|
msg | positional or keyword | required |
WSMessage.from_msg_dict(msg_dict)Source line 374
| Parameter | Passing convention | Default / required |
|---|---|---|
msg_dict | positional or keyword | required |
async on_connect(client)Source line 406
| Parameter | Passing convention | Default / required |
|---|---|---|
client | positional or keyword | required |
Declared with async def. Normal calls return an awaitable, or an async generator if the body yields. Decorated properties may be accessed differently; follow the object guide.