This release is a pre-release and may not be stable for production use.
atsq — Asynchronous TeamSpeak Query
The successor to py-ts3: an asyncio-native ServerQuery client for TeamSpeak 3 and TeamSpeak 6 over SSH — automatic keepalive, reconnect and event dispatch, on modern Python (3.12–3.14+). Every release is tested live against real TS3 and TS6 servers.
Install
uv add atsq # or: pip install atsq
Usage
import atsq
client = atsq.Client("ts.example.com", 10022, password="...",
server_id=1, # or server_port=9987
nickname="My Bot", # re-applied on reconnect
register_events=atsq.ALL_EVENTS)
@client.on("cliententerview")
async def on_join(event: atsq.Event) -> None:
if event["client_type"] == atsq.ClientType.VOICE:
print("joined:", event["client_unique_identifier"])
@client.on("clientleftview")
async def on_leave(event: atsq.Event) -> None:
print("left:", event.get("clid"))
await client.run_forever() # reconnects with backoff; keepalive automatic
One-shot administrative session
async with await atsq.connect("ts.example.com", 10022,
password="...", server_id=1) as ts:
for row in await ts.client_list("uid", "mytsid"): # -mytsid: TS6 only
print(row["clid"], row["client_nickname"])
cid = await ts.channel_create("Lounge", channel_flag_permanent=1)
bans = await ts.ban_find(ip="203.0.113.7") # TS6 only, [] if none
Pull-style event consumption instead of handlers:
async for event in client.events():
handle(event)
# or: event = await client.wait_for_event(timeout=240)
Generic commands, pipelining and wire constants
Anything without a typed wrapper goes through the escape-safe exec(),
including pipelined bulk commands (many parameter blocks, one round trip):
rows = await ts.exec("servergrouplist")
await ts.exec("clientmove", clid=5, cid=42)
await ts.exec("channeladdperm", cid=60, blocks=[
{"permsid": "i_channel_needed_join_power", "permvalue": 75},
{"permsid": "i_channel_needed_subscribe_power", "permvalue": 60},
])
Wire constants are StrEnums that compare directly against event/row values:
from atsq import ReasonId, TargetMode, ClientType, LEAVE_REASONS
if event["reasonid"] == ReasonId.CONNECT and event["client_type"] == ClientType.VOICE:
...
if event.get("reasonid") in LEAVE_REASONS:
...
File transfer (icons, avatars, channel files)
Same API against TS3 and TS6:
ft = atsq.FileTransfer(client)
icon_id = await ft.upload_icon(png_bytes) # crc32-named, returns the id
data = await ft.download("/file.bin", cid=42)
rows = await ft.file_list(cid=42, path="/") # [] for empty dirs
await ft.delete_file("/file.bin", cid=42)
Error handling
try:
await ts.use(99)
except atsq.QueryError as e: # error id != 0; str(e) carries the server msg
print(e.error_id, e.msg)
except atsq.QueryTimeoutError: # no response in time (connection is closed)
...
except atsq.ConnectionClosedError: # connection gone
...
atsq.FloodError (id 524) is retried automatically (flood_retries, default 2).
Defaults worth knowing
- Keepalive: automatic
whoamiafter 240 s idle (servers kick at ~300 s). Configure viakeepalive_interval;0disables. - Flood protection: an
error 524is retried automatically after the wait the server asks for (flood_retries, default 2;0disables). Allowlisted IPs (query_ip_allowlist.txt) never hit it in the first place. - Reconnect (
run_forever): exponential backoff 5 s → 300 s; a server message containing "banned" waits 300 s.use/servernotifyregisterandon_readyre-run after every reconnect. - Host keys: verification is off by default (TeamSpeak servers generate
ephemeral query host keys). Pin one in production:
atsq.connect(..., known_hosts=...)(forwarded to asyncssh). - close() sends
quit: on TS6 a query client that silently drops the SSH connection never produces anotifyclientleftview; a cleanquitdoes (on both generations). - Guest sessions (TS6 beta13+):
atsq.connect(host, username="guest")needs no password;await ts.login("serveradmin", pw)elevates in-band (and re-selects the virtual server, whichlogin/logoutreset). - Event sources:
ALL_EVENTSincludes the TS6-onlybanssource (banupdateevents withop=add|del); it is skipped on TS3.server_notify_unregister("textserver")is selective on both generations (TS3 drops everything, so atsq re-registers the rest). - Snapshots work via plain
exec("serversnapshotcreate")/exec("serversnapshotdeploy", version=..., data=...)— deploy deselects the session; calluseagain afterwards.
Migrating from py-ts3
| py-ts3 | atsq |
|---|---|
TS3ServerConnection("telnet://user:pass@host:10011") |
await atsq.connect(host, 10022, username=..., password=...) (SSH) |
conn.exec_("clientlist", "uid") |
await ts.exec("clientlist", "uid") or await ts.client_list("uid") |
response resp[0]["cldbid"] |
same shape: rows[0]["cldbid"] (list[dict[str, str]]) |
conn.wait_for_event(timeout=240) |
await client.wait_for_event(timeout=240) or @client.on(...) |
event[0]["reasonid"] |
event["reasonid"] (Event is a Mapping[str, str]) |
conn.send_keepalive() |
automatic (or await ts.send_keepalive()) |
ts3.query.TS3QueryError |
atsq.QueryError (str() still contains the server msg) |
ts3.query.TS3TimeoutError |
atsq.QueryTimeoutError |
query builder .pipe() |
exec(cmd, blocks=[{...}, {...}], **shared) |
ts3.definitions constants |
atsq.ReasonId / TargetMode / ClientType / LEAVE_REASONS |
ts3.filetransfer.TS3FileTransfer |
atsq.FileTransfer (asyncio, TS3+TS6) |
| manual reconnect loop | await client.run_forever() |
Enable SSH query on a TS3 server with TS3SERVER_QUERY_PROTOCOLS=raw,ssh; on
TS6 it is the only line protocol (TSSERVER_QUERY_SSH_ENABLED=1, password via
TSSERVER_QUERY_ADMIN_PASSWORD).
Docs
- TS3 vs TS6 dialect findings — probe-verified differences between the generations and how atsq handles them.
- Testing policy — every function is covered by a unit test and a live test against real servers; CI enforces it with a coverage gate.
Development
uv sync
uv run pytest # unit + fake-transport tests (no docker)
./scripts/run-integration-tests.sh # full suite vs real TS3 + TS6 in docker
License
Metadata
Release files for atsq 1.0.0a5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| atsq-1.0.0a5.tar.gz | 93.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| atsq-1.0.0a5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 120.9 kB
Release files / atsq-1.0.0a5.tar.gz
| Download URL | atsq-1.0.0a5.tar.gz |
|---|---|
| Size | 93.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4de87e6cafa9c86bd2a7a4db9be6e0983f2d449f4dad4ac21aa12e11a636321e
|
|
BLAKE2b-256 checksum How to use checksums |
b1bf6401eba9ec774179234037b263c05bff7396cf88d128a757757c2bf4126d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency logRelease files / atsq-1.0.0a5-py3-none-any.whl
| Download URL | atsq-1.0.0a5-py3-none-any.whl |
|---|---|
| Size | 27.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f9f45147639ca5d34ea3dfb0994a5663a7b1e94bc1cf63eeb6023dbaf234f838
|
|
BLAKE2b-256 checksum How to use checksums |
694260378fad92a9f64aa6ff4ccfcf10384fb44023a10c49eaec39dd2bc04961
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency log