thordata-sdk 0.2.4__py3-none-any.whl → 1.2.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.
thordata/exceptions.py ADDED
@@ -0,0 +1,355 @@
1
+ """
2
+ Custom exception types for the Thordata Python SDK.
3
+
4
+ Exception Hierarchy:
5
+ ThordataError (base)
6
+ ├── ThordataConfigError - Configuration/initialization issues
7
+ ├── ThordataNetworkError - Network connectivity issues (retryable)
8
+ │ └── ThordataTimeoutError - Request timeout (retryable)
9
+ └── ThordataAPIError - API returned an error
10
+ ├── ThordataAuthError - 401/403 authentication issues
11
+ ├── ThordataRateLimitError - 429/402 rate limit/quota issues
12
+ ├── ThordataServerError - 5xx server errors (retryable)
13
+ └── ThordataValidationError - 400 bad request / validation errors
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any
19
+
20
+ # =============================================================================
21
+ # Base Exception
22
+ # =============================================================================
23
+
24
+
25
+ class ThordataError(Exception):
26
+ """Base error for all Thordata SDK issues."""
27
+
28
+ def __init__(self, message: str) -> None:
29
+ super().__init__(message)
30
+ self.message = message
31
+
32
+
33
+ # =============================================================================
34
+ # Configuration Errors
35
+ # =============================================================================
36
+
37
+
38
+ class ThordataConfigError(ThordataError):
39
+ """
40
+ Raised when the SDK is misconfigured.
41
+
42
+ Examples:
43
+ - Missing required tokens
44
+ - Invalid parameter combinations
45
+ """
46
+
47
+ pass
48
+
49
+
50
+ # =============================================================================
51
+ # Network Errors (Usually Retryable)
52
+ # =============================================================================
53
+
54
+
55
+ class ThordataNetworkError(ThordataError):
56
+ """
57
+ Raised when a network-level error occurs.
58
+
59
+ This is typically retryable (DNS failures, connection refused, etc.)
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ message: str,
65
+ *,
66
+ original_error: Exception | None = None,
67
+ ) -> None:
68
+ super().__init__(message)
69
+ self.original_error = original_error
70
+
71
+
72
+ class ThordataTimeoutError(ThordataNetworkError):
73
+ """
74
+ Raised when a request times out.
75
+
76
+ This is typically retryable.
77
+ """
78
+
79
+ pass
80
+
81
+
82
+ # =============================================================================
83
+ # API Errors
84
+ # =============================================================================
85
+
86
+
87
+ class ThordataAPIError(ThordataError):
88
+ """
89
+ Generic API error raised when the backend returns a non-success code
90
+ or an unexpected response payload.
91
+
92
+ Attributes:
93
+ message: Human-readable error message.
94
+ status_code: HTTP status code from the response (e.g., 401, 500).
95
+ code: Application-level code from the Thordata API JSON response.
96
+ payload: Raw payload (dict/str) returned by the API.
97
+ request_id: Optional request ID for debugging with support.
98
+ """
99
+
100
+ # HTTP status codes that indicate this error type
101
+ HTTP_STATUS_CODES: set[int] = set()
102
+
103
+ def __init__(
104
+ self,
105
+ message: str,
106
+ *,
107
+ status_code: int | None = None,
108
+ code: int | None = None,
109
+ payload: Any = None,
110
+ request_id: str | None = None,
111
+ ) -> None:
112
+ super().__init__(message)
113
+ self.status_code = status_code
114
+ self.code = code
115
+ self.payload = payload
116
+ self.request_id = request_id
117
+
118
+ def __repr__(self) -> str:
119
+ return (
120
+ f"{self.__class__.__name__}("
121
+ f"message={self.message!r}, "
122
+ f"status_code={self.status_code}, "
123
+ f"code={self.code}, "
124
+ f"request_id={self.request_id!r})"
125
+ )
126
+
127
+ @property
128
+ def is_retryable(self) -> bool:
129
+ """Whether this error is typically safe to retry."""
130
+ return False
131
+
132
+
133
+ class ThordataAuthError(ThordataAPIError):
134
+ """
135
+ Authentication or authorization failures.
136
+
137
+ HTTP Status: 401, 403
138
+ Common causes:
139
+ - Invalid or expired token
140
+ - Insufficient permissions
141
+ - IP not whitelisted
142
+ """
143
+
144
+ HTTP_STATUS_CODES = {401, 403}
145
+
146
+ @property
147
+ def is_retryable(self) -> bool:
148
+ return False # Auth errors shouldn't be retried
149
+
150
+
151
+ class ThordataRateLimitError(ThordataAPIError):
152
+ """
153
+ Rate limiting or quota/balance issues.
154
+
155
+ HTTP Status: 429, 402
156
+ Common causes:
157
+ - Too many requests per second
158
+ - Insufficient account balance
159
+ - Quota exceeded
160
+
161
+ Attributes:
162
+ retry_after: Suggested seconds to wait before retrying (if provided).
163
+ """
164
+
165
+ HTTP_STATUS_CODES = {429, 402}
166
+
167
+ def __init__(
168
+ self,
169
+ message: str,
170
+ *,
171
+ retry_after: int | None = None,
172
+ **kwargs: Any,
173
+ ) -> None:
174
+ super().__init__(message, **kwargs)
175
+ self.retry_after = retry_after
176
+
177
+ @property
178
+ def is_retryable(self) -> bool:
179
+ # Rate limits are retryable after waiting
180
+ return True
181
+
182
+
183
+ class ThordataServerError(ThordataAPIError):
184
+ """
185
+ Server-side errors (5xx).
186
+
187
+ HTTP Status: 500, 502, 503, 504
188
+ These are typically transient and safe to retry.
189
+ """
190
+
191
+ HTTP_STATUS_CODES = {500, 502, 503, 504}
192
+
193
+ @property
194
+ def is_retryable(self) -> bool:
195
+ return True
196
+
197
+
198
+ class ThordataValidationError(ThordataAPIError):
199
+ """
200
+ Request validation errors.
201
+
202
+ HTTP Status: 400, 422
203
+ Common causes:
204
+ - Invalid parameters
205
+ - Missing required fields
206
+ - Malformed request body
207
+ """
208
+
209
+ HTTP_STATUS_CODES = {400, 422}
210
+
211
+ @property
212
+ def is_retryable(self) -> bool:
213
+ return False # Bad requests shouldn't be retried
214
+
215
+
216
+ class ThordataNotCollectedError(ThordataAPIError):
217
+ """
218
+ The request was accepted but no valid data could be collected/parsed.
219
+
220
+ API Code: 300
221
+ Billing: Not billed (per Thordata billing rules).
222
+ This error is often transient and typically safe to retry.
223
+ """
224
+
225
+ API_CODES = {300}
226
+ HTTP_STATUS_CODES: set[int] = set()
227
+
228
+ @property
229
+ def is_retryable(self) -> bool:
230
+ return True
231
+
232
+
233
+ # =============================================================================
234
+ # Exception Factory
235
+ # =============================================================================
236
+
237
+
238
+ def raise_for_code(
239
+ message: str,
240
+ *,
241
+ status_code: int | None = None,
242
+ code: int | None = None,
243
+ payload: Any = None,
244
+ request_id: str | None = None,
245
+ ) -> None:
246
+ """
247
+ Factory function to raise the appropriate exception based on status/code.
248
+
249
+ This centralizes the error-mapping logic that was previously duplicated
250
+ across multiple methods.
251
+
252
+ Args:
253
+ message: Human-readable error message.
254
+ status_code: HTTP status code (if available).
255
+ code: Application-level code from API response.
256
+ payload: Raw API response payload.
257
+ request_id: Optional request ID for debugging.
258
+
259
+ Raises:
260
+ ThordataAuthError: For 401/403 codes.
261
+ ThordataRateLimitError: For 429/402 codes.
262
+ ThordataServerError: For 5xx codes.
263
+ ThordataValidationError: For 400/422 codes.
264
+ ThordataAPIError: For all other error codes.
265
+ """
266
+ # Determine the effective error code.
267
+ # Prefer payload `code` when present and not success (200),
268
+ # otherwise fall back to HTTP status when it indicates an error.
269
+ effective_code: int | None = None
270
+
271
+ if code is not None and code != 200:
272
+ effective_code = code
273
+ elif status_code is not None and status_code != 200:
274
+ effective_code = status_code
275
+ else:
276
+ effective_code = code if code is not None else status_code
277
+
278
+ kwargs = {
279
+ "status_code": status_code,
280
+ "code": code,
281
+ "payload": payload,
282
+ "request_id": request_id,
283
+ }
284
+
285
+ # Not collected (API payload code 300, often retryable, not billed)
286
+ # Check this FIRST since 300 is in API_CODES, not HTTP_STATUS_CODES
287
+ if effective_code in ThordataNotCollectedError.API_CODES:
288
+ raise ThordataNotCollectedError(message, **kwargs)
289
+
290
+ # Auth errors
291
+ if effective_code in ThordataAuthError.HTTP_STATUS_CODES:
292
+ raise ThordataAuthError(message, **kwargs)
293
+
294
+ # Rate limit errors
295
+ if effective_code in ThordataRateLimitError.HTTP_STATUS_CODES:
296
+ # Try to extract retry_after from payload
297
+ retry_after = None
298
+ if isinstance(payload, dict):
299
+ retry_after = payload.get("retry_after")
300
+ raise ThordataRateLimitError(message, retry_after=retry_after, **kwargs)
301
+
302
+ # Server errors
303
+ if effective_code is not None and 500 <= effective_code < 600:
304
+ raise ThordataServerError(message, **kwargs)
305
+
306
+ # Validation errors
307
+ if effective_code in ThordataValidationError.HTTP_STATUS_CODES:
308
+ raise ThordataValidationError(message, **kwargs)
309
+
310
+ # Generic API error
311
+ raise ThordataAPIError(message, **kwargs)
312
+
313
+
314
+ # =============================================================================
315
+ # Retry Helper
316
+ # =============================================================================
317
+
318
+
319
+ def is_retryable_exception(exc: Exception) -> bool:
320
+ """
321
+ Check if an exception is safe to retry.
322
+
323
+ Args:
324
+ exc: The exception to check.
325
+
326
+ Returns:
327
+ True if the exception is typically safe to retry.
328
+ """
329
+ # Network errors are retryable
330
+ if isinstance(exc, ThordataNetworkError):
331
+ return True
332
+
333
+ # Check API errors with is_retryable property
334
+ if isinstance(exc, ThordataAPIError):
335
+ return exc.is_retryable
336
+
337
+ # requests/aiohttp specific exceptions
338
+ # (imported dynamically to avoid hard dependency)
339
+ try:
340
+ import requests
341
+
342
+ if isinstance(exc, (requests.Timeout, requests.ConnectionError)):
343
+ return True
344
+ except ImportError:
345
+ pass
346
+
347
+ try:
348
+ import aiohttp
349
+
350
+ if isinstance(exc, (aiohttp.ClientError, aiohttp.ServerTimeoutError)):
351
+ return True
352
+ except ImportError:
353
+ pass
354
+
355
+ return False