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/_errors.py ADDED
@@ -0,0 +1,190 @@
1
+ """Exception hierarchy of the txApi SDK (SPEC §4).
2
+
3
+ Every exception raised by the SDK derives from :class:`TxApiError`.
4
+ None of them ever contains the API key: messages come from the API's
5
+ ``error`` text, the response body or the HTTP reason phrase.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ import httpx
13
+
14
+ from ._ratelimit import RateLimit
15
+
16
+ __all__ = [
17
+ "APIConnectionError",
18
+ "APITimeoutError",
19
+ "AuthenticationError",
20
+ "BadRequestError",
21
+ "CommandError",
22
+ "ConflictError",
23
+ "NotFoundError",
24
+ "PermissionDeniedError",
25
+ "RateLimitError",
26
+ "ServerError",
27
+ "TxAdminRejectedError",
28
+ "TxAdminUnavailableError",
29
+ "TxApiError",
30
+ "ValidationError",
31
+ "WebhookVerificationError",
32
+ ]
33
+
34
+
35
+ class TxApiError(Exception):
36
+ """Base class of every error raised by the SDK.
37
+
38
+ Attributes:
39
+ message: Human-readable message (the API's ``error`` text when available).
40
+ status: HTTP status code, ``None`` for errors without a response.
41
+ body: Parsed response body (``dict``/``list``/``str``), ``None`` without a response.
42
+ headers: Response headers, ``None`` without a response.
43
+ rate_limit: Rate-limit state of the failed response (SPEC §6), if sent.
44
+ """
45
+
46
+ message: str
47
+ status: int | None
48
+ body: Any
49
+ headers: httpx.Headers | None
50
+ rate_limit: RateLimit | None
51
+
52
+ def __init__(
53
+ self,
54
+ message: str,
55
+ *,
56
+ status: int | None = None,
57
+ body: Any = None,
58
+ headers: httpx.Headers | None = None,
59
+ rate_limit: RateLimit | None = None,
60
+ ) -> None:
61
+ super().__init__(message)
62
+ self.message = message
63
+ self.status = status
64
+ self.body = body
65
+ self.headers = headers
66
+ self.rate_limit = rate_limit
67
+
68
+ def __str__(self) -> str:
69
+ if self.status is not None:
70
+ return f"{self.status}: {self.message}"
71
+ return self.message
72
+
73
+ def __repr__(self) -> str:
74
+ return f"{type(self).__name__}(status={self.status!r}, message={self.message!r})"
75
+
76
+
77
+ class BadRequestError(TxApiError):
78
+ """HTTP 400, 405 or 415 — the request was invalid."""
79
+
80
+
81
+ class AuthenticationError(TxApiError):
82
+ """HTTP 401 — key missing, invalid, revoked or expired."""
83
+
84
+
85
+ class PermissionDeniedError(TxApiError):
86
+ """HTTP 403 — plan feature or key scope missing, IP not allowed, server disabled,
87
+ or the txAdmin account lacks a permission."""
88
+
89
+
90
+ class NotFoundError(TxApiError):
91
+ """HTTP 404 — unknown player, action, admin or server."""
92
+
93
+
94
+ class ConflictError(TxApiError):
95
+ """HTTP 409 — e.g. no txAdmin server configured."""
96
+
97
+
98
+ class TxAdminRejectedError(TxApiError):
99
+ """HTTP 422 — txAdmin refused the action (e.g. the player is offline)."""
100
+
101
+
102
+ class RateLimitError(TxApiError):
103
+ """HTTP 429.
104
+
105
+ Attributes:
106
+ retry_after: Seconds from ``Retry-After``; ``None`` when the header is missing.
107
+ quota_exhausted: ``True`` when ``Retry-After`` is missing — the monthly quota
108
+ is used up (it resets on the 1st of the month, 00:00 UTC). Retrying will not help.
109
+ """
110
+
111
+ retry_after: float | None
112
+ quota_exhausted: bool
113
+
114
+ def __init__(
115
+ self,
116
+ message: str,
117
+ *,
118
+ retry_after: float | None = None,
119
+ status: int | None = 429,
120
+ body: Any = None,
121
+ headers: httpx.Headers | None = None,
122
+ rate_limit: RateLimit | None = None,
123
+ ) -> None:
124
+ super().__init__(message, status=status, body=body, headers=headers, rate_limit=rate_limit)
125
+ self.retry_after = retry_after
126
+ self.quota_exhausted = retry_after is None
127
+
128
+
129
+ class ServerError(TxApiError):
130
+ """HTTP 5xx (other than 502)."""
131
+
132
+
133
+ class TxAdminUnavailableError(ServerError):
134
+ """HTTP 502 — txAdmin is unreachable or answered garbage."""
135
+
136
+
137
+ class APIConnectionError(TxApiError):
138
+ """Transport problem: DNS, refused connection, reset, TLS … (spec name: ``ConnectionError``)."""
139
+
140
+
141
+ class APITimeoutError(APIConnectionError):
142
+ """The request (or a live command) timed out (spec name: ``TimeoutError``)."""
143
+
144
+
145
+ class ValidationError(TxApiError, ValueError):
146
+ """Client-side check failed; no request was sent (SPEC §2)."""
147
+
148
+
149
+ class WebhookVerificationError(TxApiError):
150
+ """A webhook delivery failed signature verification (SPEC §8).
151
+
152
+ Attributes:
153
+ reason: Machine-friendly reason, e.g. ``"signature_mismatch"``.
154
+ """
155
+
156
+ reason: str
157
+
158
+ def __init__(self, message: str, *, reason: str) -> None:
159
+ super().__init__(message)
160
+ self.reason = reason
161
+
162
+
163
+ class CommandError(TxApiError):
164
+ """The live console answered a :meth:`~txapi.live.ConsoleStream.send_command`
165
+ with an ``error`` frame (empty/too long command, rate limit, quota, txAdmin down)."""
166
+
167
+
168
+ _STATUS_ERRORS: dict[int, type[TxApiError]] = {
169
+ 400: BadRequestError,
170
+ 405: BadRequestError,
171
+ 415: BadRequestError,
172
+ 401: AuthenticationError,
173
+ 403: PermissionDeniedError,
174
+ 404: NotFoundError,
175
+ 409: ConflictError,
176
+ 422: TxAdminRejectedError,
177
+ 502: TxAdminUnavailableError,
178
+ }
179
+
180
+
181
+ def error_class_for_status(status: int) -> type[TxApiError]:
182
+ """Exception class for an HTTP error status (SPEC §4)."""
183
+ if status == 429:
184
+ return RateLimitError
185
+ known = _STATUS_ERRORS.get(status)
186
+ if known is not None:
187
+ return known
188
+ if status >= 500:
189
+ return ServerError
190
+ return TxApiError