txapi-sdk 1.0.0__py3-none-any.whl
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/__init__.py +75 -0
- txapi/_client.py +364 -0
- txapi/_core.py +302 -0
- txapi/_endpoints.py +813 -0
- txapi/_errors.py +190 -0
- txapi/_generated.py +10599 -0
- txapi/_pagination.py +140 -0
- txapi/_protocols.py +41 -0
- txapi/_ratelimit.py +81 -0
- txapi/_version.py +7 -0
- txapi/live.py +632 -0
- txapi/py.typed +0 -0
- txapi/types.py +532 -0
- txapi/webhooks.py +137 -0
- txapi_sdk-1.0.0.dist-info/METADATA +537 -0
- txapi_sdk-1.0.0.dist-info/RECORD +18 -0
- txapi_sdk-1.0.0.dist-info/WHEEL +4 -0
- txapi_sdk-1.0.0.dist-info/licenses/LICENSE +21 -0
txapi/__init__.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Official Python SDK for the txApi REST and WebSocket API (txAdmin / FiveM).
|
|
2
|
+
|
|
3
|
+
Quickstart::
|
|
4
|
+
|
|
5
|
+
from txapi import TxApi
|
|
6
|
+
|
|
7
|
+
client = TxApi(server_id=1) # key from the TXAPI_KEY environment variable
|
|
8
|
+
page = client.players.search(query="john")
|
|
9
|
+
client.players.ban(identifier="license:" + page["players"][0]["license"],
|
|
10
|
+
reason="Cheating", duration_hours=168)
|
|
11
|
+
|
|
12
|
+
See https://txapi.cloud/docs for the API reference.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from . import types
|
|
18
|
+
from ._client import AsyncTxApi, RequestHook, ResponseHook, TxApi
|
|
19
|
+
from ._core import ApiResponse
|
|
20
|
+
from ._errors import (
|
|
21
|
+
APIConnectionError,
|
|
22
|
+
APITimeoutError,
|
|
23
|
+
AuthenticationError,
|
|
24
|
+
BadRequestError,
|
|
25
|
+
CommandError,
|
|
26
|
+
ConflictError,
|
|
27
|
+
NotFoundError,
|
|
28
|
+
PermissionDeniedError,
|
|
29
|
+
RateLimitError,
|
|
30
|
+
ServerError,
|
|
31
|
+
TxAdminRejectedError,
|
|
32
|
+
TxAdminUnavailableError,
|
|
33
|
+
TxApiError,
|
|
34
|
+
ValidationError,
|
|
35
|
+
WebhookVerificationError,
|
|
36
|
+
)
|
|
37
|
+
from ._ratelimit import RateLimit
|
|
38
|
+
from ._version import API_VERSION, __version__
|
|
39
|
+
from .types import WebhookEvent, WebhookEventName
|
|
40
|
+
from .webhooks import WEBHOOK_EVENTS, verify_webhook
|
|
41
|
+
|
|
42
|
+
VERSION = __version__
|
|
43
|
+
"""Alias of ``__version__`` (the SDK version)."""
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"API_VERSION",
|
|
47
|
+
"VERSION",
|
|
48
|
+
"WEBHOOK_EVENTS",
|
|
49
|
+
"APIConnectionError",
|
|
50
|
+
"APITimeoutError",
|
|
51
|
+
"ApiResponse",
|
|
52
|
+
"AsyncTxApi",
|
|
53
|
+
"AuthenticationError",
|
|
54
|
+
"BadRequestError",
|
|
55
|
+
"CommandError",
|
|
56
|
+
"ConflictError",
|
|
57
|
+
"NotFoundError",
|
|
58
|
+
"PermissionDeniedError",
|
|
59
|
+
"RateLimit",
|
|
60
|
+
"RateLimitError",
|
|
61
|
+
"RequestHook",
|
|
62
|
+
"ResponseHook",
|
|
63
|
+
"ServerError",
|
|
64
|
+
"TxAdminRejectedError",
|
|
65
|
+
"TxAdminUnavailableError",
|
|
66
|
+
"TxApi",
|
|
67
|
+
"TxApiError",
|
|
68
|
+
"ValidationError",
|
|
69
|
+
"WebhookEvent",
|
|
70
|
+
"WebhookEventName",
|
|
71
|
+
"WebhookVerificationError",
|
|
72
|
+
"__version__",
|
|
73
|
+
"types",
|
|
74
|
+
"verify_webhook",
|
|
75
|
+
]
|
txapi/_client.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""The synchronous :class:`TxApi` and asynchronous :class:`AsyncTxApi` clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import inspect
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import Callable, Mapping
|
|
10
|
+
from types import TracebackType
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from . import _core
|
|
16
|
+
from ._core import ApiResponse, ClientConfig, EndpointSpec, PreparedRequest
|
|
17
|
+
from ._errors import APIConnectionError, APITimeoutError, TxApiError
|
|
18
|
+
from ._generated import AsyncGroups, AsyncTxApiWithRawResponse, SyncGroups, TxApiWithRawResponse
|
|
19
|
+
from ._ratelimit import RateLimit, parse_rate_limit
|
|
20
|
+
from .live import LiveConfig
|
|
21
|
+
|
|
22
|
+
__all__ = ["AsyncTxApi", "TxApi"]
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger("txapi")
|
|
25
|
+
|
|
26
|
+
RequestHook = Callable[[httpx.Request], Any]
|
|
27
|
+
"""Called before every attempt. Never log ``request.headers`` — they contain the key."""
|
|
28
|
+
ResponseHook = Callable[[httpx.Response], Any]
|
|
29
|
+
"""Called after every attempt that produced a response (also for errors and retries)."""
|
|
30
|
+
|
|
31
|
+
# Module-level indirections so tests can replace the sleeps.
|
|
32
|
+
_sleep: Callable[[float], None] = time.sleep
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def _async_sleep(seconds: float) -> None:
|
|
36
|
+
await asyncio.sleep(seconds)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _ClientBase:
|
|
40
|
+
_config: ClientConfig
|
|
41
|
+
_on_request: RequestHook | None
|
|
42
|
+
_on_response: ResponseHook | None
|
|
43
|
+
last_rate_limit: RateLimit | None
|
|
44
|
+
"""Rate-limit state of the most recent response (SPEC §6), ``None`` before the first."""
|
|
45
|
+
|
|
46
|
+
def _init_base(
|
|
47
|
+
self,
|
|
48
|
+
api_key: str | None,
|
|
49
|
+
base_url: str | None,
|
|
50
|
+
server_id: int | str | None,
|
|
51
|
+
timeout: float | None,
|
|
52
|
+
max_retries: int | None,
|
|
53
|
+
user_agent: str | None,
|
|
54
|
+
on_request: RequestHook | None,
|
|
55
|
+
on_response: ResponseHook | None,
|
|
56
|
+
) -> None:
|
|
57
|
+
self._config = _core.resolve_config(
|
|
58
|
+
api_key, base_url, server_id, timeout, max_retries, user_agent
|
|
59
|
+
)
|
|
60
|
+
self._on_request = on_request
|
|
61
|
+
self._on_response = on_response
|
|
62
|
+
self.last_rate_limit = None
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def base_url(self) -> str:
|
|
66
|
+
"""Base URL without ``/api/v1`` (no trailing slash)."""
|
|
67
|
+
return self._config.base_url
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def server_id(self) -> int | str | None:
|
|
71
|
+
"""Default server for server-scoped calls."""
|
|
72
|
+
return self._config.server_id
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def timeout(self) -> float:
|
|
76
|
+
"""Default per-attempt timeout in seconds."""
|
|
77
|
+
return self._config.timeout
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def max_retries(self) -> int:
|
|
81
|
+
"""Retries after the first attempt (SPEC §5)."""
|
|
82
|
+
return self._config.max_retries
|
|
83
|
+
|
|
84
|
+
def __repr__(self) -> str:
|
|
85
|
+
return (
|
|
86
|
+
f"{type(self).__name__}(base_url={self._config.base_url!r}, "
|
|
87
|
+
f"server_id={self._config.server_id!r}, api_key=<redacted>)"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def _live_config(self, server_id: int | str | None) -> LiveConfig:
|
|
91
|
+
return LiveConfig(
|
|
92
|
+
base_url=self._config.base_url,
|
|
93
|
+
api_key=self._config.api_key,
|
|
94
|
+
server_id=server_id if server_id is not None else self._config.server_id,
|
|
95
|
+
user_agent=self._config.user_agent,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def _prepare(
|
|
99
|
+
self,
|
|
100
|
+
endpoint: EndpointSpec,
|
|
101
|
+
path: Mapping[str, object],
|
|
102
|
+
query: Mapping[str, object],
|
|
103
|
+
body: Mapping[str, object] | None,
|
|
104
|
+
server_id: int | str | None,
|
|
105
|
+
) -> PreparedRequest:
|
|
106
|
+
return _core.build_request(
|
|
107
|
+
endpoint,
|
|
108
|
+
path=path,
|
|
109
|
+
query=query,
|
|
110
|
+
body=body,
|
|
111
|
+
server_id=server_id,
|
|
112
|
+
default_server_id=self._config.server_id,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _http_request(
|
|
116
|
+
self,
|
|
117
|
+
http: httpx.Client | httpx.AsyncClient,
|
|
118
|
+
prepared: PreparedRequest,
|
|
119
|
+
timeout: float | None,
|
|
120
|
+
) -> httpx.Request:
|
|
121
|
+
headers = _core.default_headers(self._config)
|
|
122
|
+
if prepared.body is not None:
|
|
123
|
+
headers["Content-Type"] = "application/json"
|
|
124
|
+
return http.build_request(
|
|
125
|
+
prepared.method,
|
|
126
|
+
self._config.base_url + prepared.path,
|
|
127
|
+
params=tuple(prepared.params),
|
|
128
|
+
content=prepared.body,
|
|
129
|
+
headers=headers,
|
|
130
|
+
timeout=httpx.Timeout(self._config.timeout if timeout is None else timeout),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def _record(self, response: httpx.Response) -> None:
|
|
134
|
+
rate = parse_rate_limit(response.headers)
|
|
135
|
+
if rate is not None:
|
|
136
|
+
self.last_rate_limit = rate
|
|
137
|
+
|
|
138
|
+
def _transport_error(self, exc: httpx.TransportError) -> TxApiError:
|
|
139
|
+
name = type(exc).__name__
|
|
140
|
+
if isinstance(exc, httpx.TimeoutException):
|
|
141
|
+
return APITimeoutError(f"request timed out ({name})")
|
|
142
|
+
return APIConnectionError(f"connection failed ({name}: {exc})")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _call_hook(hook: Callable[[Any], Any] | None, value: Any) -> Any:
|
|
146
|
+
if hook is None:
|
|
147
|
+
return None
|
|
148
|
+
try:
|
|
149
|
+
return hook(value)
|
|
150
|
+
except Exception:
|
|
151
|
+
logger.exception("txapi: hook raised; ignored")
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class TxApi(_ClientBase, SyncGroups):
|
|
156
|
+
"""Synchronous txApi client.
|
|
157
|
+
|
|
158
|
+
Example::
|
|
159
|
+
|
|
160
|
+
from txapi import TxApi
|
|
161
|
+
|
|
162
|
+
with TxApi(server_id=1) as client: # key from TXAPI_KEY
|
|
163
|
+
page = client.players.search(query="john")
|
|
164
|
+
client.players.ban(identifier="license:" + page["players"][0]["license"],
|
|
165
|
+
reason="Cheating", duration_hours=168)
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
api_key: API key; defaults to the ``TXAPI_KEY`` environment variable.
|
|
169
|
+
base_url: Defaults to ``TXAPI_BASE_URL`` or ``https://txapi.cloud`` (``/api/v1`` is added).
|
|
170
|
+
server_id: Default server for every server-scoped call (each call can override it).
|
|
171
|
+
timeout: Per-attempt timeout in seconds (default 30).
|
|
172
|
+
max_retries: Retries after the first attempt (default 2, ``0`` disables retries).
|
|
173
|
+
user_agent: Appended to ``txapi-python/<version>``.
|
|
174
|
+
http_client: Your own ``httpx.Client`` (proxies, transports …). It is not closed
|
|
175
|
+
by :meth:`close`.
|
|
176
|
+
on_request: Hook called before every attempt (logging/metrics). Exceptions are ignored.
|
|
177
|
+
The request headers contain your key — never log them.
|
|
178
|
+
on_response: Hook called with every response. Exceptions are ignored.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
with_raw_response: TxApiWithRawResponse
|
|
182
|
+
"""The same methods, returning :class:`~txapi.ApiResponse` (data + status, headers,
|
|
183
|
+
rate limit)."""
|
|
184
|
+
|
|
185
|
+
def __init__(
|
|
186
|
+
self,
|
|
187
|
+
api_key: str | None = None,
|
|
188
|
+
*,
|
|
189
|
+
base_url: str | None = None,
|
|
190
|
+
server_id: int | str | None = None,
|
|
191
|
+
timeout: float | None = None,
|
|
192
|
+
max_retries: int | None = None,
|
|
193
|
+
user_agent: str | None = None,
|
|
194
|
+
http_client: httpx.Client | None = None,
|
|
195
|
+
on_request: RequestHook | None = None,
|
|
196
|
+
on_response: ResponseHook | None = None,
|
|
197
|
+
) -> None:
|
|
198
|
+
self._init_base(
|
|
199
|
+
api_key, base_url, server_id, timeout, max_retries, user_agent, on_request, on_response
|
|
200
|
+
)
|
|
201
|
+
self._owns_http = http_client is None
|
|
202
|
+
self._http = http_client if http_client is not None else httpx.Client()
|
|
203
|
+
SyncGroups.__init__(self, self)
|
|
204
|
+
self.with_raw_response = TxApiWithRawResponse(self)
|
|
205
|
+
|
|
206
|
+
def close(self) -> None:
|
|
207
|
+
"""Close the underlying HTTP client (unless it was passed in)."""
|
|
208
|
+
if self._owns_http:
|
|
209
|
+
self._http.close()
|
|
210
|
+
|
|
211
|
+
def __enter__(self) -> TxApi:
|
|
212
|
+
return self
|
|
213
|
+
|
|
214
|
+
def __exit__(
|
|
215
|
+
self,
|
|
216
|
+
exc_type: type[BaseException] | None,
|
|
217
|
+
exc: BaseException | None,
|
|
218
|
+
tb: TracebackType | None,
|
|
219
|
+
) -> None:
|
|
220
|
+
self.close()
|
|
221
|
+
|
|
222
|
+
def _request(
|
|
223
|
+
self,
|
|
224
|
+
endpoint: EndpointSpec,
|
|
225
|
+
*,
|
|
226
|
+
path: Mapping[str, object],
|
|
227
|
+
query: Mapping[str, object],
|
|
228
|
+
body: Mapping[str, object] | None,
|
|
229
|
+
server_id: int | str | None,
|
|
230
|
+
timeout: float | None,
|
|
231
|
+
) -> ApiResponse[Any]:
|
|
232
|
+
prepared = self._prepare(endpoint, path, query, body, server_id)
|
|
233
|
+
attempt = 0
|
|
234
|
+
while True:
|
|
235
|
+
request = self._http_request(self._http, prepared, timeout)
|
|
236
|
+
_call_hook(self._on_request, request)
|
|
237
|
+
try:
|
|
238
|
+
response = self._http.send(request)
|
|
239
|
+
except httpx.TransportError as exc:
|
|
240
|
+
error = self._transport_error(exc)
|
|
241
|
+
delay = _core.retry_delay_for_transport_error(prepared.method, attempt)
|
|
242
|
+
if delay is not None and attempt < self._config.max_retries:
|
|
243
|
+
attempt += 1
|
|
244
|
+
_sleep(delay)
|
|
245
|
+
continue
|
|
246
|
+
raise error from exc
|
|
247
|
+
_call_hook(self._on_response, response)
|
|
248
|
+
self._record(response)
|
|
249
|
+
if response.is_success:
|
|
250
|
+
return _core.make_api_response(response, _core.parse_body(response))
|
|
251
|
+
delay = _core.retry_delay_for_response(prepared.method, response, attempt)
|
|
252
|
+
if delay is not None and attempt < self._config.max_retries:
|
|
253
|
+
attempt += 1
|
|
254
|
+
response.close()
|
|
255
|
+
_sleep(delay)
|
|
256
|
+
continue
|
|
257
|
+
raise _core.error_from_response(response, _core.parse_body(response))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class AsyncTxApi(_ClientBase, AsyncGroups):
|
|
261
|
+
"""Asynchronous txApi client — same surface as :class:`TxApi`, every call is awaited.
|
|
262
|
+
|
|
263
|
+
Example::
|
|
264
|
+
|
|
265
|
+
import asyncio
|
|
266
|
+
from txapi import AsyncTxApi
|
|
267
|
+
|
|
268
|
+
async def main() -> None:
|
|
269
|
+
async with AsyncTxApi(server_id=1) as client:
|
|
270
|
+
status = await client.server.status()
|
|
271
|
+
async for row in client.history.iter_bans():
|
|
272
|
+
print(row["id"], row["reason"])
|
|
273
|
+
|
|
274
|
+
asyncio.run(main())
|
|
275
|
+
|
|
276
|
+
Takes the same arguments as :class:`TxApi`; ``http_client`` is an ``httpx.AsyncClient``.
|
|
277
|
+
Hooks may be plain functions or coroutines.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
with_raw_response: AsyncTxApiWithRawResponse
|
|
281
|
+
"""The same methods, returning :class:`~txapi.ApiResponse` (data + status, headers,
|
|
282
|
+
rate limit)."""
|
|
283
|
+
|
|
284
|
+
def __init__(
|
|
285
|
+
self,
|
|
286
|
+
api_key: str | None = None,
|
|
287
|
+
*,
|
|
288
|
+
base_url: str | None = None,
|
|
289
|
+
server_id: int | str | None = None,
|
|
290
|
+
timeout: float | None = None,
|
|
291
|
+
max_retries: int | None = None,
|
|
292
|
+
user_agent: str | None = None,
|
|
293
|
+
http_client: httpx.AsyncClient | None = None,
|
|
294
|
+
on_request: RequestHook | None = None,
|
|
295
|
+
on_response: ResponseHook | None = None,
|
|
296
|
+
) -> None:
|
|
297
|
+
self._init_base(
|
|
298
|
+
api_key, base_url, server_id, timeout, max_retries, user_agent, on_request, on_response
|
|
299
|
+
)
|
|
300
|
+
self._owns_http = http_client is None
|
|
301
|
+
self._http = http_client if http_client is not None else httpx.AsyncClient()
|
|
302
|
+
AsyncGroups.__init__(self, self)
|
|
303
|
+
self.with_raw_response = AsyncTxApiWithRawResponse(self)
|
|
304
|
+
|
|
305
|
+
async def close(self) -> None:
|
|
306
|
+
"""Close the underlying HTTP client (unless it was passed in)."""
|
|
307
|
+
if self._owns_http:
|
|
308
|
+
await self._http.aclose()
|
|
309
|
+
|
|
310
|
+
async def __aenter__(self) -> AsyncTxApi:
|
|
311
|
+
return self
|
|
312
|
+
|
|
313
|
+
async def __aexit__(
|
|
314
|
+
self,
|
|
315
|
+
exc_type: type[BaseException] | None,
|
|
316
|
+
exc: BaseException | None,
|
|
317
|
+
tb: TracebackType | None,
|
|
318
|
+
) -> None:
|
|
319
|
+
await self.close()
|
|
320
|
+
|
|
321
|
+
async def _hook(self, hook: Callable[[Any], Any] | None, value: Any) -> None:
|
|
322
|
+
result = _call_hook(hook, value)
|
|
323
|
+
if inspect.isawaitable(result):
|
|
324
|
+
try:
|
|
325
|
+
await result
|
|
326
|
+
except Exception:
|
|
327
|
+
logger.exception("txapi: hook raised; ignored")
|
|
328
|
+
|
|
329
|
+
async def _request(
|
|
330
|
+
self,
|
|
331
|
+
endpoint: EndpointSpec,
|
|
332
|
+
*,
|
|
333
|
+
path: Mapping[str, object],
|
|
334
|
+
query: Mapping[str, object],
|
|
335
|
+
body: Mapping[str, object] | None,
|
|
336
|
+
server_id: int | str | None,
|
|
337
|
+
timeout: float | None,
|
|
338
|
+
) -> ApiResponse[Any]:
|
|
339
|
+
prepared = self._prepare(endpoint, path, query, body, server_id)
|
|
340
|
+
attempt = 0
|
|
341
|
+
while True:
|
|
342
|
+
request = self._http_request(self._http, prepared, timeout)
|
|
343
|
+
await self._hook(self._on_request, request)
|
|
344
|
+
try:
|
|
345
|
+
response = await self._http.send(request)
|
|
346
|
+
except httpx.TransportError as exc:
|
|
347
|
+
error = self._transport_error(exc)
|
|
348
|
+
delay = _core.retry_delay_for_transport_error(prepared.method, attempt)
|
|
349
|
+
if delay is not None and attempt < self._config.max_retries:
|
|
350
|
+
attempt += 1
|
|
351
|
+
await _async_sleep(delay)
|
|
352
|
+
continue
|
|
353
|
+
raise error from exc
|
|
354
|
+
await self._hook(self._on_response, response)
|
|
355
|
+
self._record(response)
|
|
356
|
+
if response.is_success:
|
|
357
|
+
return _core.make_api_response(response, _core.parse_body(response))
|
|
358
|
+
delay = _core.retry_delay_for_response(prepared.method, response, attempt)
|
|
359
|
+
if delay is not None and attempt < self._config.max_retries:
|
|
360
|
+
attempt += 1
|
|
361
|
+
await response.aclose()
|
|
362
|
+
await _async_sleep(delay)
|
|
363
|
+
continue
|
|
364
|
+
raise _core.error_from_response(response, _core.parse_body(response))
|