txapi-sdk 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- txapi_sdk-1.0.0/.gitignore +10 -0
- txapi_sdk-1.0.0/CHANGELOG.md +47 -0
- txapi_sdk-1.0.0/LICENSE +21 -0
- txapi_sdk-1.0.0/PKG-INFO +537 -0
- txapi_sdk-1.0.0/README.md +500 -0
- txapi_sdk-1.0.0/pyproject.toml +78 -0
- txapi_sdk-1.0.0/src/txapi/__init__.py +75 -0
- txapi_sdk-1.0.0/src/txapi/_client.py +364 -0
- txapi_sdk-1.0.0/src/txapi/_core.py +302 -0
- txapi_sdk-1.0.0/src/txapi/_endpoints.py +813 -0
- txapi_sdk-1.0.0/src/txapi/_errors.py +190 -0
- txapi_sdk-1.0.0/src/txapi/_generated.py +10599 -0
- txapi_sdk-1.0.0/src/txapi/_pagination.py +140 -0
- txapi_sdk-1.0.0/src/txapi/_protocols.py +41 -0
- txapi_sdk-1.0.0/src/txapi/_ratelimit.py +81 -0
- txapi_sdk-1.0.0/src/txapi/_version.py +7 -0
- txapi_sdk-1.0.0/src/txapi/live.py +632 -0
- txapi_sdk-1.0.0/src/txapi/py.typed +0 -0
- txapi_sdk-1.0.0/src/txapi/types.py +532 -0
- txapi_sdk-1.0.0/src/txapi/webhooks.py +137 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the txApi Python SDK (distribution `txapi-sdk`, import `txapi`). The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versions follow
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- `ConsoleStream.send_command()` right after `async with client.console.connect()` raised
|
|
12
|
+
`APIConnectionError("live console is not connected")` against the real API — `async with`
|
|
13
|
+
only starts connecting in the background. It now waits for the (re)connect within
|
|
14
|
+
`timeout`, raises `APITimeoutError` if none comes, and `APIConnectionError` with the close
|
|
15
|
+
code and reason if the stream was refused (1008) or has ended. The README example works as
|
|
16
|
+
written.
|
|
17
|
+
|
|
18
|
+
## [1.0.0] — 2026-10-14
|
|
19
|
+
|
|
20
|
+
First release, targeting txApi **1.8.0**. Published on PyPI as **`txapi-sdk`** (the name
|
|
21
|
+
`txapi` there is taken); the import name is `txapi`.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- `TxApi` (sync) and `AsyncTxApi` (async) clients with an identical surface, built on `httpx`;
|
|
26
|
+
configuration from arguments or `TXAPI_KEY` / `TXAPI_BASE_URL`, default `server_id`,
|
|
27
|
+
per-call `server_id=` and `timeout=`, custom `http_client=`, `on_request` / `on_response` hooks.
|
|
28
|
+
- One method for each of the 67 endpoints (`account`, `players`, `history`, `server`,
|
|
29
|
+
`server.resources`, `console`, `serverlog`, `events`, `whitelist`, `whitelist.approvals`,
|
|
30
|
+
`admins`), fully type-hinted with `TypedDict` responses (`txapi.types`), docstrings naming
|
|
31
|
+
the required permission.
|
|
32
|
+
- Discord ban/unban flags: `players.ban(..., discord_ban=True)` and
|
|
33
|
+
`history.revoke(..., discord_unban=True)` with the `discord` result.
|
|
34
|
+
- Client-side validation (required fields, maximum lengths) before any request.
|
|
35
|
+
- Error hierarchy (`TxApiError` and one subclass per status, `RateLimitError.quota_exhausted`,
|
|
36
|
+
`APIConnectionError`, `APITimeoutError`, `ValidationError`, `WebhookVerificationError`,
|
|
37
|
+
`CommandError`); the API key never appears in `repr`/`str`.
|
|
38
|
+
- Retries: 429 with `Retry-After` ≤ 60 s for every method; network errors and 502/503/504 for
|
|
39
|
+
GET only; full-jitter backoff; `max_retries=0` disables retries.
|
|
40
|
+
- Rate-limit information (`client.last_rate_limit`, `error.rate_limit`) and raw responses via
|
|
41
|
+
`client.with_raw_response`.
|
|
42
|
+
- Lazy auto-paging iterators (`iter_*`, sync and async) for players, history and events.
|
|
43
|
+
- `verify_webhook` with constant-time HMAC-SHA256 check, timestamp tolerance and secret
|
|
44
|
+
rotation; `WEBHOOK_EVENTS` and the `WebhookEventName` literal type.
|
|
45
|
+
- Live console and server-log streams (`txapi[live]`, `websockets`): async iteration or
|
|
46
|
+
callbacks, `send_command` with FIFO ack correlation and timeout, automatic reconnect on
|
|
47
|
+
1001/1006/1011/1012 and network loss, no reconnect on 1008/1009 with the close reason surfaced.
|
txapi_sdk-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Josef Peter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
txapi_sdk-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: txapi-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for the txApi REST and WebSocket API (txAdmin / FiveM server management)
|
|
5
|
+
Project-URL: Homepage, https://txapi.cloud
|
|
6
|
+
Project-URL: Documentation, https://txapi.cloud/docs/sdks
|
|
7
|
+
Project-URL: Source, https://github.com/josefpeter25/txapi-sdks/tree/main/python
|
|
8
|
+
Project-URL: Issues, https://github.com/josefpeter25/txapi-sdks/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/josefpeter25/txapi-sdks/blob/main/python/CHANGELOG.md
|
|
10
|
+
Author: Josef Peter
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: api,cfx,fivem,gta,sdk,txadmin,txapi
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Requires-Dist: httpx<1,>=0.25
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
33
|
+
Requires-Dist: websockets>=13; extra == 'dev'
|
|
34
|
+
Provides-Extra: live
|
|
35
|
+
Requires-Dist: websockets>=13; extra == 'live'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# txapi-sdk — official Python SDK for the txApi
|
|
39
|
+
|
|
40
|
+
Typed Python client for the [txApi](https://txapi.cloud) REST and WebSocket API: manage your
|
|
41
|
+
FiveM servers' txAdmin — players, bans, warnings, whitelist, server control, the live console,
|
|
42
|
+
the stored event log and txAdmin admins — from Python.
|
|
43
|
+
|
|
44
|
+
- Sync (`TxApi`) **and** async (`AsyncTxApi`) clients with the identical surface
|
|
45
|
+
- Every one of the 67 endpoints of API **1.8.0**, fully type-hinted (`mypy --strict` clean),
|
|
46
|
+
responses as `TypedDict`s
|
|
47
|
+
- Automatic retries that never repeat a ban or a restart, rate-limit information on every call
|
|
48
|
+
- Auto-paging iterators, webhook signature verification, live console / server log streams
|
|
49
|
+
- Python 3.9+, one runtime dependency (`httpx`); `websockets` only for live streams
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install txapi-sdk # REST client + webhooks
|
|
53
|
+
pip install "txapi-sdk[live]" # + live console / server-log streams (websockets)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The distribution is called **`txapi-sdk`** (the name `txapi` on PyPI belongs to someone else);
|
|
57
|
+
the import stays `import txapi`. Every release is also attached to
|
|
58
|
+
[GitHub Releases](https://github.com/josefpeter25/txapi-sdks/releases) and served from
|
|
59
|
+
`https://txapi.cloud/sdks/` (checksums in `SHA256SUMS`), for setups without PyPI access:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install "txapi-sdk[live] @ https://txapi.cloud/sdks/txapi_sdk-1.0.0-py3-none-any.whl"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Contents
|
|
66
|
+
|
|
67
|
+
- [Quickstart](#quickstart)
|
|
68
|
+
- [Configuration](#configuration)
|
|
69
|
+
- [Calling endpoints](#calling-endpoints)
|
|
70
|
+
- [Errors](#errors)
|
|
71
|
+
- [Retries](#retries)
|
|
72
|
+
- [Rate limits and raw responses](#rate-limits-and-raw-responses)
|
|
73
|
+
- [Pagination](#pagination)
|
|
74
|
+
- [Webhooks](#webhooks)
|
|
75
|
+
- [Live console and server log](#live-console-and-server-log)
|
|
76
|
+
- [Hooks, custom HTTP clients, timeouts](#hooks-custom-http-clients-timeouts)
|
|
77
|
+
- [Endpoint reference](#endpoint-reference)
|
|
78
|
+
- [Development](#development)
|
|
79
|
+
|
|
80
|
+
## Quickstart
|
|
81
|
+
|
|
82
|
+
Create an API key in the dashboard (**API keys**), then:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from txapi import TxApi
|
|
86
|
+
|
|
87
|
+
with TxApi(api_key="txp_...", server_id=1) as client:
|
|
88
|
+
page = client.players.search(query="john")
|
|
89
|
+
player = page["players"][0]
|
|
90
|
+
client.players.ban(
|
|
91
|
+
identifier=f"license:{player['license']}",
|
|
92
|
+
reason="Cheating",
|
|
93
|
+
duration_hours=168, # omit (or 0) for a permanent ban
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The same with `asyncio`:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import asyncio
|
|
101
|
+
from txapi import AsyncTxApi
|
|
102
|
+
|
|
103
|
+
async def main() -> None:
|
|
104
|
+
async with AsyncTxApi(server_id=1) as client: # key from TXAPI_KEY
|
|
105
|
+
status = await client.server.status()
|
|
106
|
+
print(status["serverName"], "online" if status.get("online") else "offline")
|
|
107
|
+
await client.server.announcement(message="Restart in 5 minutes")
|
|
108
|
+
|
|
109
|
+
asyncio.run(main())
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
> **Never log or commit your API key.** It is sent as `Authorization: Bearer <key>`; the
|
|
113
|
+
> clients' and errors' `repr()`/`str()` never contain it. Prefer the `TXAPI_KEY` environment
|
|
114
|
+
> variable over hard-coding it. The request headers passed to `on_request` hooks *do* contain
|
|
115
|
+
> it — don't log them.
|
|
116
|
+
|
|
117
|
+
## Configuration
|
|
118
|
+
|
|
119
|
+
| Argument | Default | |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `api_key` | env `TXAPI_KEY` | required — a missing key raises `ValidationError` at construction |
|
|
122
|
+
| `base_url` | env `TXAPI_BASE_URL`, else `https://txapi.cloud` | trailing slash stripped, `/api/v1` is added by the SDK |
|
|
123
|
+
| `server_id` | — | default server for every server-scoped call; each call can pass `server_id=` |
|
|
124
|
+
| `timeout` | `30` | seconds per attempt; each call can pass `timeout=` |
|
|
125
|
+
| `max_retries` | `2` | retries after the first attempt, `0` disables retries |
|
|
126
|
+
| `user_agent` | — | appended to `txapi-python/1.0.0` |
|
|
127
|
+
| `http_client` | — | your own `httpx.Client` / `httpx.AsyncClient` (not closed by the SDK) |
|
|
128
|
+
| `on_request` / `on_response` | — | hooks for logging/metrics |
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
client = TxApi(server_id=1, timeout=10, max_retries=3, user_agent="my-panel/2.1")
|
|
132
|
+
print(client.account.servers()) # account-level: no serverId is sent
|
|
133
|
+
client.server.status(server_id=2) # override the default server for one call
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`txapi.__version__` is the SDK version (`1.0.0`), `txapi.API_VERSION` the API version it was
|
|
137
|
+
built for (`1.8.0`).
|
|
138
|
+
|
|
139
|
+
## Calling endpoints
|
|
140
|
+
|
|
141
|
+
Endpoints are grouped like the API: `client.account`, `players`, `history`, `server`
|
|
142
|
+
(+ `server.resources`), `console`, `serverlog`, `events`, `whitelist` (+ `whitelist.approvals`)
|
|
143
|
+
and `admins`. Path parameters are positional, everything else is keyword-only and
|
|
144
|
+
snake_case (sent as camelCase). `None` arguments are omitted — never sent as `null`.
|
|
145
|
+
Every method's docstring names the permission it needs.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
me = client.account.me()
|
|
149
|
+
print(me["plan"]["code"], me["quota"].get("remaining"), me["plan"]["features"])
|
|
150
|
+
|
|
151
|
+
# Players
|
|
152
|
+
client.players.info(identifier="discord:272800190639898628")
|
|
153
|
+
client.players.warn(identifier="license:abc…", reason="Mic spam")
|
|
154
|
+
client.players.kick(identifier="license:abc…", reason="AFK")
|
|
155
|
+
client.players.dm(identifier="license:abc…", message="Please read the rules")
|
|
156
|
+
client.players.notes(identifier="license:abc…", note="Watch this one")
|
|
157
|
+
client.players.whitelist(identifier="license:abc…", whitelisted=True)
|
|
158
|
+
|
|
159
|
+
# Bans incl. Discord (API 1.8.0): also ban on every Discord server linked via txBot
|
|
160
|
+
result = client.players.ban(identifier="license:abc…", reason="Cheating", discord_ban=True)
|
|
161
|
+
for guild in result.get("discord", {}).get("guilds", []):
|
|
162
|
+
print(guild["guildName"], guild["code"]) # banned, missing_permission, hierarchy, …
|
|
163
|
+
client.history.revoke("BKL5-3SH4", reason="Appeal accepted", discord_unban=True)
|
|
164
|
+
|
|
165
|
+
# Server control
|
|
166
|
+
client.server.restart()
|
|
167
|
+
client.server.schedule_restart(time="+30") # in 30 minutes; or "04:00" (host clock)
|
|
168
|
+
client.server.skip_restart(skip=True)
|
|
169
|
+
client.server.kick_all(reason="Maintenance")
|
|
170
|
+
client.server.resources.restart(name="es_extended")
|
|
171
|
+
|
|
172
|
+
# Console (REST) — see below for the live stream
|
|
173
|
+
client.console.command(command="say Hello")
|
|
174
|
+
print(client.console.buffer()["lines"][-20:])
|
|
175
|
+
|
|
176
|
+
# Stored event log
|
|
177
|
+
board = client.events.leaderboard(since="7d", sort="kills", limit=10)
|
|
178
|
+
stats = client.events.player(player="license:abc…", since="30d")
|
|
179
|
+
deaths = client.events.deaths(pvp=True, since="24h")
|
|
180
|
+
client.events.list(type=["ban", "warn"], since="2026-10-01T00:00:00Z")
|
|
181
|
+
|
|
182
|
+
# Whitelist
|
|
183
|
+
client.whitelist.approve("R1234")
|
|
184
|
+
client.whitelist.approvals.add(identifier="discord:272800190639898628")
|
|
185
|
+
client.whitelist.approvals.remove("discord:272800190639898628") # percent-encoded for you
|
|
186
|
+
|
|
187
|
+
# txAdmin admins
|
|
188
|
+
client.admins.add(name="Moderator Bob", permissions=["players.ban", "console.write"])
|
|
189
|
+
client.admins.edit("Moderator Bob", discord_id="123") # only the fields you pass change
|
|
190
|
+
client.admins.delete("Moderator Bob")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Responses are parsed JSON typed as `TypedDict`s (`txapi.types`). They are plain dicts at
|
|
194
|
+
runtime, so fields the API adds in later minor versions never break anything. txAdmin
|
|
195
|
+
passthrough endpoints (e.g. `players.info`, `server.diagnostics`) return `JsonValue` (`Any`).
|
|
196
|
+
Write endpoints return `{"success": True, "message": "…", "discord"?: …}`.
|
|
197
|
+
|
|
198
|
+
Required arguments are checked before anything is sent: blank required fields and values
|
|
199
|
+
longer than the API allows (ban reason 512, note 2000, resource name 128, console command 1024
|
|
200
|
+
characters) raise `txapi.ValidationError` without a request.
|
|
201
|
+
|
|
202
|
+
## Errors
|
|
203
|
+
|
|
204
|
+
Every exception derives from `txapi.TxApiError` with `status`, `message` (the API's `error`
|
|
205
|
+
text, else the body, else the reason phrase), parsed `body`, `headers` and `rate_limit`.
|
|
206
|
+
|
|
207
|
+
| Status | Exception | Typical cause |
|
|
208
|
+
|---|---|---|
|
|
209
|
+
| 400, 405, 415 | `BadRequestError` | invalid input |
|
|
210
|
+
| 401 | `AuthenticationError` | key missing, invalid, revoked or expired |
|
|
211
|
+
| 403 | `PermissionDeniedError` | plan feature / key scope missing, IP not allowed, server disabled |
|
|
212
|
+
| 404 | `NotFoundError` | unknown player, action, admin or server |
|
|
213
|
+
| 409 | `ConflictError` | e.g. no txAdmin server configured |
|
|
214
|
+
| 422 | `TxAdminRejectedError` | txAdmin refused the action (e.g. player offline) |
|
|
215
|
+
| 429 | `RateLimitError` | `retry_after` seconds, or `quota_exhausted` (no `Retry-After`) |
|
|
216
|
+
| 502 | `TxAdminUnavailableError` (a `ServerError`) | txAdmin unreachable |
|
|
217
|
+
| other 5xx | `ServerError` | |
|
|
218
|
+
| — | `APIConnectionError`, `APITimeoutError` | network problems, timeouts |
|
|
219
|
+
| — | `ValidationError` (also a `ValueError`) | client-side check, nothing was sent |
|
|
220
|
+
| — | `WebhookVerificationError` | see [Webhooks](#webhooks) |
|
|
221
|
+
| — | `CommandError` | live console answered a command with an error |
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from txapi import NotFoundError, RateLimitError, TxAdminRejectedError, TxApiError
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
client.players.kick(identifier="license:abc…", reason="AFK")
|
|
228
|
+
except TxAdminRejectedError as exc:
|
|
229
|
+
print("txAdmin refused:", exc.message) # e.g. the player is offline
|
|
230
|
+
except NotFoundError:
|
|
231
|
+
print("No such player")
|
|
232
|
+
except RateLimitError as exc:
|
|
233
|
+
if exc.quota_exhausted:
|
|
234
|
+
print("Monthly quota used up — resets on the 1st, 00:00 UTC")
|
|
235
|
+
else:
|
|
236
|
+
print(f"Too fast, retry in {exc.retry_after} s")
|
|
237
|
+
except TxApiError as exc:
|
|
238
|
+
print(exc.status, exc.message, exc.body)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`APIConnectionError` and `APITimeoutError` are named so to not shadow Python's built-in
|
|
242
|
+
`ConnectionError`/`TimeoutError` (the spec's names).
|
|
243
|
+
|
|
244
|
+
## Retries
|
|
245
|
+
|
|
246
|
+
- **429 with `Retry-After` ≤ 60 s** is retried for every method after exactly that delay —
|
|
247
|
+
the request was not executed. 429 *without* `Retry-After` means the monthly quota is used
|
|
248
|
+
up and is never retried.
|
|
249
|
+
- **Connection errors, timeouts, 502, 503, 504** are retried for **GET only**. A retried
|
|
250
|
+
POST could ban twice or restart a server twice.
|
|
251
|
+
- Backoff: full jitter, `random() * min(8 s, 0.5 s · 2^attempt)`, unless `Retry-After` says otherwise.
|
|
252
|
+
- `max_retries=0` disables everything. Cancelling an `AsyncTxApi` call (task cancel) stops
|
|
253
|
+
waiting immediately.
|
|
254
|
+
|
|
255
|
+
## Rate limits and raw responses
|
|
256
|
+
|
|
257
|
+
Every response carries the per-minute limit; the SDK parses it into
|
|
258
|
+
`RateLimit(limit, remaining, reset_seconds)`:
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
client.server.status()
|
|
262
|
+
rl = client.last_rate_limit # None before the first response
|
|
263
|
+
print(rl.remaining, "of", rl.limit, "left, resets in", rl.reset_seconds, "s")
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Errors carry it too (`exc.rate_limit`). For status code and headers together with the
|
|
267
|
+
parsed data, use `with_raw_response` — the same methods, returning `txapi.ApiResponse`:
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
raw = client.with_raw_response.players.ban(identifier="license:abc…", reason="x")
|
|
271
|
+
raw.data # {"success": True, "message": "…"}
|
|
272
|
+
raw.status_code # 200
|
|
273
|
+
raw.headers # httpx.Headers
|
|
274
|
+
raw.rate_limit # RateLimit(...)
|
|
275
|
+
raw.http_response # the httpx.Response
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Pagination
|
|
279
|
+
|
|
280
|
+
List endpoints return one page; the `iter_*` variants page through everything lazily
|
|
281
|
+
(the next page is fetched only when the current one is used up):
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
for player in client.players.iter_search(sort_by="tsLastConnection"):
|
|
285
|
+
print(player["displayName"])
|
|
286
|
+
|
|
287
|
+
for ban in client.history.iter_bans():
|
|
288
|
+
if ban.get("isRevoked"):
|
|
289
|
+
continue
|
|
290
|
+
print(ban["id"], ban["playerName"], ban["reason"])
|
|
291
|
+
|
|
292
|
+
for death in client.events.iter_deaths(since="7d", pvp=True):
|
|
293
|
+
print(death["killer"], "→", death["victim"])
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
async for row in client.events.iter_admin_actions(since="24h"): # AsyncTxApi
|
|
298
|
+
print(row)
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
| Iterators | Cursor |
|
|
302
|
+
|---|---|
|
|
303
|
+
| `players.iter_search` / `iter_online` / `iter_lookup` | `offset` = last row's sort key (`playTime`, `tsJoined`, `tsLastConnection`; aliases `sessions`, `lastConnection`), `cursor` = its `license` |
|
|
304
|
+
| `history.iter_bans` / `iter_warns` / `iter_search` | `offset` = last row's `timestamp`, `cursor` = its `id` |
|
|
305
|
+
| `events.iter_list` / `iter_deaths` / `iter_kills` / `iter_chat` / `iter_commands` / `iter_admin_actions` / `iter_actions` / `iter_sessions` | `cursor` = `nextCursor` |
|
|
306
|
+
|
|
307
|
+
Iteration stops on `hasReachedEnd`, a missing `nextCursor` or an empty page. To page by
|
|
308
|
+
hand, pass `offset=`/`cursor=` to the non-iterating method.
|
|
309
|
+
|
|
310
|
+
## Webhooks
|
|
311
|
+
|
|
312
|
+
Webhooks (dashboard → **Webhooks**) are signed with your endpoint secret (`whsec_…`):
|
|
313
|
+
`X-Txapi-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256>`. `txapi.verify_webhook` checks the
|
|
314
|
+
signature (constant time, several `v1` entries allowed during secret rotation) and the
|
|
315
|
+
timestamp (default tolerance 300 s), and returns the event
|
|
316
|
+
`{id, event, createdAt, serverId, data}`:
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
from txapi import WebhookVerificationError, verify_webhook
|
|
320
|
+
|
|
321
|
+
event = verify_webhook(raw_body, signature_header, secret)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
> **Verify the raw request body — the exact bytes you received.** Parsing the JSON and
|
|
325
|
+
> serialising it again (`json.dumps(request.json())`) changes whitespace and key order and
|
|
326
|
+
> the signature will never match.
|
|
327
|
+
|
|
328
|
+
On failure it raises `WebhookVerificationError` with a `reason`: `missing_timestamp`,
|
|
329
|
+
`missing_signature`, `malformed_header`, `timestamp_out_of_tolerance`, `signature_mismatch`,
|
|
330
|
+
`invalid_body` or `missing_secret`. All event names are in `txapi.WEBHOOK_EVENTS`
|
|
331
|
+
(and the `txapi.WebhookEventName` literal type); header names in `txapi.webhooks`.
|
|
332
|
+
|
|
333
|
+
**Flask**
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
from flask import Flask, abort, request
|
|
337
|
+
from txapi import WebhookVerificationError, verify_webhook
|
|
338
|
+
|
|
339
|
+
app = Flask(__name__)
|
|
340
|
+
|
|
341
|
+
@app.post("/txapi/webhook")
|
|
342
|
+
def txapi_webhook():
|
|
343
|
+
try:
|
|
344
|
+
event = verify_webhook(
|
|
345
|
+
request.get_data(), # raw bytes
|
|
346
|
+
request.headers.get("X-Txapi-Signature"),
|
|
347
|
+
WEBHOOK_SECRET,
|
|
348
|
+
)
|
|
349
|
+
except WebhookVerificationError as exc:
|
|
350
|
+
abort(400, exc.reason)
|
|
351
|
+
if event["event"] == "player.banned":
|
|
352
|
+
notify_staff(event["data"])
|
|
353
|
+
return {"ok": True}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**FastAPI** — see [`examples/fastapi_webhook.py`](examples/fastapi_webhook.py):
|
|
357
|
+
|
|
358
|
+
```python
|
|
359
|
+
@app.post("/txapi/webhook")
|
|
360
|
+
async def txapi_webhook(request: Request):
|
|
361
|
+
raw = await request.body() # raw bytes
|
|
362
|
+
try:
|
|
363
|
+
event = verify_webhook(raw, request.headers.get("X-Txapi-Signature"), WEBHOOK_SECRET)
|
|
364
|
+
except WebhookVerificationError as exc:
|
|
365
|
+
raise HTTPException(400, exc.reason)
|
|
366
|
+
...
|
|
367
|
+
return {"ok": True}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Answer with a 2xx quickly; failed deliveries are retried by txApi.
|
|
371
|
+
|
|
372
|
+
## Live console and server log
|
|
373
|
+
|
|
374
|
+
Needs `pip install "txapi-sdk[live]"`. `client.console.connect()` (permission `console.write`)
|
|
375
|
+
and `client.serverlog.connect()` (`logs.view`) return asyncio streams:
|
|
376
|
+
|
|
377
|
+
```python
|
|
378
|
+
import asyncio
|
|
379
|
+
from txapi import AsyncTxApi
|
|
380
|
+
from txapi.live import CloseEvent, InitEvent, LogEvent
|
|
381
|
+
|
|
382
|
+
async def main() -> None:
|
|
383
|
+
async with AsyncTxApi(server_id=1) as client:
|
|
384
|
+
async with client.console.connect() as console:
|
|
385
|
+
print(await console.send_command("status")) # resolves with txAdmin's ack
|
|
386
|
+
async for event in console:
|
|
387
|
+
if isinstance(event, InitEvent): # buffer after every (re)connect
|
|
388
|
+
print(len(event.items), "buffered lines")
|
|
389
|
+
elif isinstance(event, LogEvent): # one event per console line
|
|
390
|
+
print(event.item)
|
|
391
|
+
elif isinstance(event, CloseEvent) and not event.will_reconnect:
|
|
392
|
+
print("closed:", event.code, event.reason)
|
|
393
|
+
|
|
394
|
+
asyncio.run(main())
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Events (`txapi.live`): `OpenEvent(reconnected)`, `InitEvent(items)`, `LogEvent(item)`,
|
|
398
|
+
`ErrorEvent(message)`, `CloseEvent(code, reason, will_reconnect, by_user)`,
|
|
399
|
+
`ReconnectingEvent(attempt, delay, code, reason)` and, with `raw_frames=True`,
|
|
400
|
+
`FrameEvent(frame)`. Instead of iterating you can pass `on_event=` (sync or async callback).
|
|
401
|
+
|
|
402
|
+
- **`send_command(cmd)`** validates the length (≤ 1024), sends `{"type": "command"}` and
|
|
403
|
+
returns the next `ack` — or raises `CommandError` for the next `error` frame. Commands are
|
|
404
|
+
correlated first-in-first-out; timeout 10 s (`command_timeout=` / `timeout=`) raises
|
|
405
|
+
`APITimeoutError`. The command's *output* arrives later as `LogEvent`s. An `error` frame
|
|
406
|
+
that answers no pending command is emitted as `ErrorEvent`.
|
|
407
|
+
- **Reconnects** automatically after close codes 1001, 1006, 1011, 1012 and network loss
|
|
408
|
+
(backoff 1 s → 30 s with jitter; `reconnect=`, `max_reconnect_attempts=`,
|
|
409
|
+
`initial_reconnect_delay=`, `max_reconnect_delay=`). After a reconnect the server sends the
|
|
410
|
+
full buffer again as `InitEvent` — de-duplicate or ignore it.
|
|
411
|
+
- **No reconnect** after 1008 (authentication, permission, quota, too slow — `event.reason`
|
|
412
|
+
and `stream.close_reason` tell you what to fix), 1009, a refused handshake, or `close()`.
|
|
413
|
+
Iteration then ends.
|
|
414
|
+
- Server-log `LogEvent.item`s are txAdmin's structured entries (`dict`) or plain strings.
|
|
415
|
+
- The key travels in the WebSocket URL's query string (that is how the server authenticates
|
|
416
|
+
streams); it is never included in events or error messages.
|
|
417
|
+
|
|
418
|
+
The synchronous `TxApi` hands out the same asyncio streams — use them inside
|
|
419
|
+
`asyncio.run(...)`; there is no blocking stream API. See
|
|
420
|
+
[`examples/live_console.py`](examples/live_console.py).
|
|
421
|
+
|
|
422
|
+
## Hooks, custom HTTP clients, timeouts
|
|
423
|
+
|
|
424
|
+
```python
|
|
425
|
+
import httpx, logging
|
|
426
|
+
|
|
427
|
+
def log_request(request: httpx.Request) -> None:
|
|
428
|
+
logging.info("→ %s %s", request.method, request.url.path) # never log request.headers
|
|
429
|
+
|
|
430
|
+
def log_response(response: httpx.Response) -> None:
|
|
431
|
+
logging.info("← %s", response.status_code)
|
|
432
|
+
|
|
433
|
+
client = TxApi(
|
|
434
|
+
http_client=httpx.Client(limits=httpx.Limits(max_connections=20)),
|
|
435
|
+
on_request=log_request, # called before every attempt
|
|
436
|
+
on_response=log_response, # called for every response, also errors and retries
|
|
437
|
+
timeout=15,
|
|
438
|
+
)
|
|
439
|
+
client.server.diagnostics(timeout=60) # per call
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Hooks can never break a request — exceptions inside them are logged and ignored. With
|
|
443
|
+
`AsyncTxApi` they may be coroutines. A client you pass in is not closed by `close()`.
|
|
444
|
+
|
|
445
|
+
## Endpoint reference
|
|
446
|
+
|
|
447
|
+
| Method | HTTP | Permission |
|
|
448
|
+
|---|---|---|
|
|
449
|
+
| `client.account.me()` | `GET /me` | any key |
|
|
450
|
+
| `client.account.servers()` | `GET /servers` | any key |
|
|
451
|
+
| `client.players.search()` · `iter_search()` | `GET /players/search` | `players.read` |
|
|
452
|
+
| `client.players.online()` · `iter_online()` | `GET /players/online` | `players.read` |
|
|
453
|
+
| `client.players.lookup()` · `iter_lookup()` | `GET /players/lookup` | `players.read` |
|
|
454
|
+
| `client.players.info()` | `GET /players/info` | `players.read` |
|
|
455
|
+
| `client.players.stats()` | `GET /players/stats` | `players.read` |
|
|
456
|
+
| `client.players.warn()` | `POST /players/warn` | `players.write` |
|
|
457
|
+
| `client.players.ban()` | `POST /players/ban` | `players.write` |
|
|
458
|
+
| `client.players.kick()` | `POST /players/kick` | `players.write` |
|
|
459
|
+
| `client.players.dm()` | `POST /players/dm` | `players.write` |
|
|
460
|
+
| `client.players.notes()` | `POST /players/notes` | `players.write` |
|
|
461
|
+
| `client.players.whitelist()` | `POST /players/whitelist` | `players.write` |
|
|
462
|
+
| `client.history.bans()` · `iter_bans()` | `GET /history/bans` | `players.read` |
|
|
463
|
+
| `client.history.warns()` · `iter_warns()` | `GET /history/warns` | `players.read` |
|
|
464
|
+
| `client.history.search()` · `iter_search()` | `GET /history/search` | `players.read` |
|
|
465
|
+
| `client.history.ban_templates()` | `GET /txadmin/ban-templates` | `players.read` |
|
|
466
|
+
| `client.history.action()` | `GET /history/action/{actionId}` | `players.read` |
|
|
467
|
+
| `client.history.stats()` | `GET /history/stats` | `players.read` |
|
|
468
|
+
| `client.history.revoke()` | `POST /history/revoke/{actionId}` | `players.write` |
|
|
469
|
+
| `client.server.status()` | `GET /server/status` | `server.control` |
|
|
470
|
+
| `client.server.diagnostics()` | `GET /server/diagnostics` | `server.control` |
|
|
471
|
+
| `client.server.drops()` | `GET /server/drops` | `server.control` |
|
|
472
|
+
| `client.server.performance()` | `GET /server/performance` | `server.control` |
|
|
473
|
+
| `client.server.restart()` | `POST /server/restart` | `server.control` |
|
|
474
|
+
| `client.server.start()` | `POST /server/start` | `server.control` |
|
|
475
|
+
| `client.server.stop()` | `POST /server/stop` | `server.control` |
|
|
476
|
+
| `client.server.schedule_restart()` | `POST /server/schedule-restart` | `server.control` |
|
|
477
|
+
| `client.server.skip_restart()` | `POST /server/skip-restart` | `server.control` |
|
|
478
|
+
| `client.server.kick_all()` | `POST /server/kickall` | `server.control` |
|
|
479
|
+
| `client.server.announcement()` | `POST /server/announcement` | `server.control` |
|
|
480
|
+
| `client.server.resources.start()` | `POST /server/resource/start` | `server.control` |
|
|
481
|
+
| `client.server.resources.stop()` | `POST /server/resource/stop` | `server.control` |
|
|
482
|
+
| `client.server.resources.restart()` | `POST /server/resource/restart` | `server.control` |
|
|
483
|
+
| `client.server.resources.ensure()` | `POST /server/resource/ensure` | `server.control` |
|
|
484
|
+
| `client.server.resources.refresh()` | `POST /server/resource/refresh` | `server.control` |
|
|
485
|
+
| `client.console.buffer()` | `GET /console` | `console.write` |
|
|
486
|
+
| `client.console.command()` | `POST /console/command` | `console.write` |
|
|
487
|
+
| `client.serverlog.buffer()` | `GET /server/log` | `logs.view` |
|
|
488
|
+
| `client.serverlog.history()` | `GET /server/log/history` | `logs.view` |
|
|
489
|
+
| `client.events.list()` · `iter_list()` | `GET /events` | `logs.view` |
|
|
490
|
+
| `client.events.deaths()` · `iter_deaths()` | `GET /events/deaths` | `logs.view` |
|
|
491
|
+
| `client.events.kills()` · `iter_kills()` | `GET /events/kills` | `logs.view` |
|
|
492
|
+
| `client.events.leaderboard()` | `GET /events/leaderboard` | `logs.view` |
|
|
493
|
+
| `client.events.player()` | `GET /events/player` | `logs.view` |
|
|
494
|
+
| `client.events.summary()` | `GET /events/summary` | `logs.view` |
|
|
495
|
+
| `client.events.chat()` · `iter_chat()` | `GET /events/chat` | `logs.view` |
|
|
496
|
+
| `client.events.commands()` · `iter_commands()` | `GET /events/commands` | `logs.view` |
|
|
497
|
+
| `client.events.admin_actions()` · `iter_admin_actions()` | `GET /events/admin-actions` | `logs.view` |
|
|
498
|
+
| `client.events.actions()` · `iter_actions()` | `GET /events/actions` | `logs.view` |
|
|
499
|
+
| `client.events.sessions()` · `iter_sessions()` | `GET /events/sessions` | `logs.view` |
|
|
500
|
+
| `client.events.playtime()` | `GET /events/playtime` | `logs.view` |
|
|
501
|
+
| `client.events.activity()` | `GET /events/activity` | `logs.view` |
|
|
502
|
+
| `client.events.action_log()` | `GET /txadmin/action-log` | `logs.view` |
|
|
503
|
+
| `client.whitelist.requests()` | `GET /whitelist/requests` | `players.read` |
|
|
504
|
+
| `client.whitelist.approve()` | `POST /whitelist/requests/{requestId}/approve` | `players.write` |
|
|
505
|
+
| `client.whitelist.deny()` | `POST /whitelist/requests/{requestId}/deny` | `players.write` |
|
|
506
|
+
| `client.whitelist.deny_all()` | `POST /whitelist/requests/deny-all` | `players.write` |
|
|
507
|
+
| `client.whitelist.approvals.list()` | `GET /whitelist/approvals` | `players.read` |
|
|
508
|
+
| `client.whitelist.approvals.add()` | `POST /whitelist/approvals` | `players.write` |
|
|
509
|
+
| `client.whitelist.approvals.remove()` | `DELETE /whitelist/approvals/{identifier}` | `players.write` |
|
|
510
|
+
| `client.admins.list()` | `GET /txadmin/admins` | `admin.manage` |
|
|
511
|
+
| `client.admins.get()` | `GET /txadmin/admins/{name}` | `admin.manage` |
|
|
512
|
+
| `client.admins.permissions()` | `GET /txadmin/admins/available-permissions` | `admin.manage` |
|
|
513
|
+
| `client.admins.add()` | `POST /txadmin/admins` | `admin.manage` |
|
|
514
|
+
| `client.admins.edit()` | `PUT /txadmin/admins/{name}` | `admin.manage` |
|
|
515
|
+
| `client.admins.delete()` | `DELETE /txadmin/admins/{name}` | `admin.manage` |
|
|
516
|
+
|
|
517
|
+
Plus `client.console.connect()` (`WS /ws/console`, `console.write`) and
|
|
518
|
+
`client.serverlog.connect()` (`WS /ws/serverlog`, `logs.view`).
|
|
519
|
+
|
|
520
|
+
## Development
|
|
521
|
+
|
|
522
|
+
```bash
|
|
523
|
+
python -m venv .venv && .venv/bin/pip install -e ".[live,dev]"
|
|
524
|
+
.venv/bin/pytest # offline: mock HTTP transport + local WebSocket server
|
|
525
|
+
.venv/bin/mypy --strict src
|
|
526
|
+
.venv/bin/ruff check src scripts tests examples && .venv/bin/ruff format --check src scripts tests examples
|
|
527
|
+
python scripts/generate.py # regenerate src/txapi/_endpoints.py + _generated.py
|
|
528
|
+
python scripts/generate.py --check # CI: fails if the generated code is stale
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
The endpoint methods are generated from `../spec/endpoints.json` (itself generated from the
|
|
532
|
+
API catalog). When the API changes, regenerate; the coverage test fails until every endpoint
|
|
533
|
+
has a method.
|
|
534
|
+
|
|
535
|
+
## License
|
|
536
|
+
|
|
537
|
+
MIT — see [LICENSE](LICENSE).
|