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/__init__.py +151 -0
- thordata/_example_utils.py +77 -0
- thordata/_utils.py +190 -0
- thordata/async_client.py +1675 -0
- thordata/client.py +1644 -0
- thordata/demo.py +138 -0
- thordata/enums.py +384 -0
- thordata/exceptions.py +355 -0
- thordata/models.py +1197 -0
- thordata/retry.py +382 -0
- thordata/serp_engines.py +166 -0
- thordata_sdk-1.2.0.dist-info/METADATA +208 -0
- thordata_sdk-1.2.0.dist-info/RECORD +16 -0
- {thordata_sdk-0.2.4.dist-info → thordata_sdk-1.2.0.dist-info}/WHEEL +1 -1
- thordata_sdk-1.2.0.dist-info/licenses/LICENSE +21 -0
- thordata_sdk-1.2.0.dist-info/top_level.txt +1 -0
- thordata_sdk/__init__.py +0 -9
- thordata_sdk/async_client.py +0 -247
- thordata_sdk/client.py +0 -303
- thordata_sdk/enums.py +0 -20
- thordata_sdk/parameters.py +0 -41
- thordata_sdk-0.2.4.dist-info/LICENSE +0 -201
- thordata_sdk-0.2.4.dist-info/METADATA +0 -113
- thordata_sdk-0.2.4.dist-info/RECORD +0 -10
- thordata_sdk-0.2.4.dist-info/top_level.txt +0 -1
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
|