dexscreen 0.0.1__py3-none-any.whl → 0.0.4__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.
- dexscreen/__init__.py +87 -0
- dexscreen/api/client.py +275 -42
- dexscreen/core/exceptions.py +1067 -0
- dexscreen/core/http.py +861 -117
- dexscreen/core/validators.py +542 -0
- dexscreen/stream/polling.py +288 -78
- dexscreen/utils/__init__.py +54 -1
- dexscreen/utils/filters.py +182 -12
- dexscreen/utils/logging_config.py +421 -0
- dexscreen/utils/middleware.py +363 -0
- dexscreen/utils/ratelimit.py +212 -8
- dexscreen/utils/retry.py +357 -0
- {dexscreen-0.0.1.dist-info → dexscreen-0.0.4.dist-info}/METADATA +52 -1
- dexscreen-0.0.4.dist-info/RECORD +22 -0
- dexscreen-0.0.1.dist-info/RECORD +0 -17
- {dexscreen-0.0.1.dist-info → dexscreen-0.0.4.dist-info}/WHEEL +0 -0
- {dexscreen-0.0.1.dist-info → dexscreen-0.0.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1067 @@
|
|
1
|
+
"""
|
2
|
+
Comprehensive exception hierarchy for the dexscreen project.
|
3
|
+
|
4
|
+
This module provides structured exceptions that allow for granular error handling
|
5
|
+
at different levels of the application. All exceptions preserve context and provide
|
6
|
+
meaningful error messages for debugging and monitoring.
|
7
|
+
|
8
|
+
Note: This module maintains backward compatibility with the original exception structure
|
9
|
+
while providing enhanced functionality for better error handling.
|
10
|
+
"""
|
11
|
+
|
12
|
+
import datetime as dt
|
13
|
+
from typing import Any, Optional, Union
|
14
|
+
|
15
|
+
|
16
|
+
class DexscreenError(Exception):
|
17
|
+
"""
|
18
|
+
Base exception for all dexscreen-related errors.
|
19
|
+
|
20
|
+
This is the root exception that all other dexscreen exceptions inherit from.
|
21
|
+
Use this for broad exception handling when you want to catch any dexscreen error.
|
22
|
+
|
23
|
+
Attributes:
|
24
|
+
message: Human-readable error message
|
25
|
+
context: Additional context information about the error
|
26
|
+
timestamp: When the error occurred
|
27
|
+
original_error: The original exception that caused this error (if any)
|
28
|
+
|
29
|
+
Example:
|
30
|
+
try:
|
31
|
+
client.get_pair("invalid_address")
|
32
|
+
except DexscreenError as e:
|
33
|
+
logger.error(f"Dexscreen error: {e}")
|
34
|
+
# This will catch any dexscreen-specific error
|
35
|
+
"""
|
36
|
+
|
37
|
+
def __init__(
|
38
|
+
self, message: str, context: Optional[dict[str, Any]] = None, original_error: Optional[Exception] = None
|
39
|
+
):
|
40
|
+
super().__init__(message)
|
41
|
+
self.message = message
|
42
|
+
self.context = context or {}
|
43
|
+
self.timestamp = dt.datetime.now(dt.timezone.utc)
|
44
|
+
self.original_error = original_error
|
45
|
+
|
46
|
+
def __str__(self) -> str:
|
47
|
+
base_message = self.message
|
48
|
+
if self.context:
|
49
|
+
context_str = ", ".join(f"{k}={v}" for k, v in self.context.items())
|
50
|
+
base_message += f" (context: {context_str})"
|
51
|
+
return base_message
|
52
|
+
|
53
|
+
def __repr__(self) -> str:
|
54
|
+
return f"{self.__class__.__name__}('{self.message}', context={self.context})"
|
55
|
+
|
56
|
+
|
57
|
+
# =============================================================================
|
58
|
+
# VALIDATION ERRORS (Backward Compatible)
|
59
|
+
# =============================================================================
|
60
|
+
|
61
|
+
|
62
|
+
class ValidationError(DexscreenError):
|
63
|
+
"""Base exception for input validation errors"""
|
64
|
+
|
65
|
+
pass
|
66
|
+
|
67
|
+
|
68
|
+
class InvalidAddressError(ValidationError):
|
69
|
+
"""
|
70
|
+
Raised when an invalid address is provided.
|
71
|
+
|
72
|
+
Enhanced version that maintains backward compatibility while adding new features.
|
73
|
+
|
74
|
+
Attributes:
|
75
|
+
address: The invalid address
|
76
|
+
reason: Reason why the address is invalid
|
77
|
+
address_type: Type of address ("token", "pair", "contract", etc.)
|
78
|
+
expected_format: Description of expected format
|
79
|
+
"""
|
80
|
+
|
81
|
+
def __init__(
|
82
|
+
self,
|
83
|
+
address: str,
|
84
|
+
reason: str = "Invalid address format",
|
85
|
+
address_type: Optional[str] = None,
|
86
|
+
expected_format: Optional[str] = None,
|
87
|
+
context: Optional[dict[str, Any]] = None,
|
88
|
+
original_error: Optional[Exception] = None,
|
89
|
+
):
|
90
|
+
# Build context
|
91
|
+
full_context = context or {}
|
92
|
+
full_context.update({"address": address, "address_type": address_type, "expected_format": expected_format})
|
93
|
+
|
94
|
+
super().__init__(f"{reason}: '{address}'", full_context, original_error)
|
95
|
+
self.address = address
|
96
|
+
self.reason = reason
|
97
|
+
self.address_type = address_type
|
98
|
+
self.expected_format = expected_format
|
99
|
+
|
100
|
+
|
101
|
+
class InvalidChainIdError(ValidationError):
|
102
|
+
"""
|
103
|
+
Raised when an invalid chain ID is provided.
|
104
|
+
|
105
|
+
Enhanced version with improved functionality.
|
106
|
+
|
107
|
+
Attributes:
|
108
|
+
chain_id: The invalid chain ID
|
109
|
+
valid_chains: List of valid chain IDs
|
110
|
+
supported_chains: Alias for valid_chains (for compatibility)
|
111
|
+
"""
|
112
|
+
|
113
|
+
def __init__(
|
114
|
+
self,
|
115
|
+
chain_id: str,
|
116
|
+
valid_chains: Optional[list[str]] = None,
|
117
|
+
context: Optional[dict[str, Any]] = None,
|
118
|
+
original_error: Optional[Exception] = None,
|
119
|
+
):
|
120
|
+
self.chain_id = chain_id
|
121
|
+
self.valid_chains = valid_chains or []
|
122
|
+
self.supported_chains = self.valid_chains # Alias for compatibility
|
123
|
+
|
124
|
+
# Build context
|
125
|
+
full_context = context or {}
|
126
|
+
full_context.update({"chain_id": chain_id, "valid_chains": self.valid_chains})
|
127
|
+
|
128
|
+
if self.valid_chains:
|
129
|
+
message = f"Invalid chain ID '{chain_id}'. Valid chains: {', '.join(self.valid_chains)}"
|
130
|
+
else:
|
131
|
+
message = f"Invalid chain ID '{chain_id}'"
|
132
|
+
|
133
|
+
super().__init__(message, full_context, original_error)
|
134
|
+
|
135
|
+
|
136
|
+
# Alias for backward compatibility
|
137
|
+
InvalidChainError = InvalidChainIdError
|
138
|
+
|
139
|
+
|
140
|
+
class InvalidParameterError(ValidationError):
|
141
|
+
"""Raised when a parameter has an invalid value"""
|
142
|
+
|
143
|
+
def __init__(
|
144
|
+
self,
|
145
|
+
parameter: str,
|
146
|
+
value: Any,
|
147
|
+
expected: str,
|
148
|
+
context: Optional[dict[str, Any]] = None,
|
149
|
+
original_error: Optional[Exception] = None,
|
150
|
+
):
|
151
|
+
full_context = context or {}
|
152
|
+
full_context.update({"parameter": parameter, "value": value, "expected": expected})
|
153
|
+
|
154
|
+
super().__init__(f"Invalid {parameter}: {value}. Expected: {expected}", full_context, original_error)
|
155
|
+
self.parameter = parameter
|
156
|
+
self.value = value
|
157
|
+
self.expected = expected
|
158
|
+
|
159
|
+
|
160
|
+
class InvalidRangeError(ValidationError):
|
161
|
+
"""Raised when a numeric parameter is outside valid range"""
|
162
|
+
|
163
|
+
def __init__(
|
164
|
+
self,
|
165
|
+
parameter: str,
|
166
|
+
value: Union[int, float],
|
167
|
+
min_value: Optional[Union[int, float]] = None,
|
168
|
+
max_value: Optional[Union[int, float]] = None,
|
169
|
+
context: Optional[dict[str, Any]] = None,
|
170
|
+
original_error: Optional[Exception] = None,
|
171
|
+
):
|
172
|
+
self.parameter = parameter
|
173
|
+
self.value = value
|
174
|
+
self.min_value = min_value
|
175
|
+
self.max_value = max_value
|
176
|
+
|
177
|
+
full_context = context or {}
|
178
|
+
full_context.update({"parameter": parameter, "value": value, "min_value": min_value, "max_value": max_value})
|
179
|
+
|
180
|
+
if min_value is not None and max_value is not None:
|
181
|
+
message = f"Invalid {parameter}: {value}. Must be between {min_value} and {max_value}"
|
182
|
+
elif min_value is not None:
|
183
|
+
message = f"Invalid {parameter}: {value}. Must be >= {min_value}"
|
184
|
+
elif max_value is not None:
|
185
|
+
message = f"Invalid {parameter}: {value}. Must be <= {max_value}"
|
186
|
+
else:
|
187
|
+
message = f"Invalid {parameter}: {value}"
|
188
|
+
|
189
|
+
super().__init__(message, full_context, original_error)
|
190
|
+
|
191
|
+
|
192
|
+
class InvalidTypeError(ValidationError):
|
193
|
+
"""Raised when a parameter has an incorrect type"""
|
194
|
+
|
195
|
+
def __init__(
|
196
|
+
self,
|
197
|
+
parameter: str,
|
198
|
+
value: Any,
|
199
|
+
expected_type: str,
|
200
|
+
context: Optional[dict[str, Any]] = None,
|
201
|
+
original_error: Optional[Exception] = None,
|
202
|
+
):
|
203
|
+
full_context = context or {}
|
204
|
+
full_context.update(
|
205
|
+
{
|
206
|
+
"parameter": parameter,
|
207
|
+
"value": value,
|
208
|
+
"received_type": type(value).__name__,
|
209
|
+
"expected_type": expected_type,
|
210
|
+
}
|
211
|
+
)
|
212
|
+
|
213
|
+
super().__init__(
|
214
|
+
f"Invalid type for {parameter}: {type(value).__name__}. Expected: {expected_type}",
|
215
|
+
full_context,
|
216
|
+
original_error,
|
217
|
+
)
|
218
|
+
self.parameter = parameter
|
219
|
+
self.value = value
|
220
|
+
self.expected_type = expected_type
|
221
|
+
|
222
|
+
|
223
|
+
class TooManyItemsError(ValidationError):
|
224
|
+
"""Raised when too many items are provided for a list parameter"""
|
225
|
+
|
226
|
+
def __init__(
|
227
|
+
self,
|
228
|
+
parameter: str,
|
229
|
+
count: int,
|
230
|
+
max_allowed: int,
|
231
|
+
context: Optional[dict[str, Any]] = None,
|
232
|
+
original_error: Optional[Exception] = None,
|
233
|
+
):
|
234
|
+
full_context = context or {}
|
235
|
+
full_context.update({"parameter": parameter, "count": count, "max_allowed": max_allowed})
|
236
|
+
|
237
|
+
super().__init__(f"Too many {parameter}: {count}. Maximum allowed: {max_allowed}", full_context, original_error)
|
238
|
+
self.parameter = parameter
|
239
|
+
self.count = count
|
240
|
+
self.max_allowed = max_allowed
|
241
|
+
|
242
|
+
|
243
|
+
class EmptyListError(ValidationError):
|
244
|
+
"""Raised when an empty list is provided where items are required"""
|
245
|
+
|
246
|
+
def __init__(
|
247
|
+
self, parameter: str, context: Optional[dict[str, Any]] = None, original_error: Optional[Exception] = None
|
248
|
+
):
|
249
|
+
full_context = context or {}
|
250
|
+
full_context.update({"parameter": parameter})
|
251
|
+
|
252
|
+
super().__init__(f"Empty {parameter} list. At least one item is required", full_context, original_error)
|
253
|
+
self.parameter = parameter
|
254
|
+
|
255
|
+
|
256
|
+
class InvalidFilterError(ValidationError):
|
257
|
+
"""Raised when filter configuration is invalid"""
|
258
|
+
|
259
|
+
def __init__(
|
260
|
+
self,
|
261
|
+
message: str,
|
262
|
+
filter_type: Optional[str] = None,
|
263
|
+
invalid_parameters: Optional[list[str]] = None,
|
264
|
+
context: Optional[dict[str, Any]] = None,
|
265
|
+
original_error: Optional[Exception] = None,
|
266
|
+
):
|
267
|
+
full_context = context or {}
|
268
|
+
full_context.update({"filter_type": filter_type, "invalid_parameters": invalid_parameters or []})
|
269
|
+
|
270
|
+
super().__init__(f"Invalid filter configuration: {message}", full_context, original_error)
|
271
|
+
self.filter_type = filter_type
|
272
|
+
self.invalid_parameters = invalid_parameters or []
|
273
|
+
|
274
|
+
|
275
|
+
class InvalidIntervalError(ValidationError):
|
276
|
+
"""Raised when polling interval is invalid"""
|
277
|
+
|
278
|
+
def __init__(
|
279
|
+
self,
|
280
|
+
interval: float,
|
281
|
+
min_interval: float = 0.1,
|
282
|
+
max_interval: float = 3600.0,
|
283
|
+
context: Optional[dict[str, Any]] = None,
|
284
|
+
original_error: Optional[Exception] = None,
|
285
|
+
):
|
286
|
+
full_context = context or {}
|
287
|
+
full_context.update({"interval": interval, "min_interval": min_interval, "max_interval": max_interval})
|
288
|
+
|
289
|
+
super().__init__(
|
290
|
+
f"Invalid interval: {interval}s. Must be between {min_interval}s and {max_interval}s",
|
291
|
+
full_context,
|
292
|
+
original_error,
|
293
|
+
)
|
294
|
+
self.interval = interval
|
295
|
+
self.min_interval = min_interval
|
296
|
+
self.max_interval = max_interval
|
297
|
+
|
298
|
+
|
299
|
+
class InvalidCallbackError(ValidationError):
|
300
|
+
"""Raised when callback function is invalid"""
|
301
|
+
|
302
|
+
def __init__(
|
303
|
+
self,
|
304
|
+
callback: Any,
|
305
|
+
reason: str,
|
306
|
+
context: Optional[dict[str, Any]] = None,
|
307
|
+
original_error: Optional[Exception] = None,
|
308
|
+
):
|
309
|
+
full_context = context or {}
|
310
|
+
full_context.update({"callback_type": type(callback).__name__, "reason": reason})
|
311
|
+
|
312
|
+
super().__init__(f"Invalid callback: {reason}", full_context, original_error)
|
313
|
+
self.callback = callback
|
314
|
+
self.reason = reason
|
315
|
+
|
316
|
+
|
317
|
+
class InvalidUrlError(ValidationError):
|
318
|
+
"""Raised when URL format is invalid"""
|
319
|
+
|
320
|
+
def __init__(
|
321
|
+
self,
|
322
|
+
url: str,
|
323
|
+
reason: str = "Invalid URL format",
|
324
|
+
context: Optional[dict[str, Any]] = None,
|
325
|
+
original_error: Optional[Exception] = None,
|
326
|
+
):
|
327
|
+
full_context = context or {}
|
328
|
+
full_context.update({"url": url, "reason": reason})
|
329
|
+
|
330
|
+
super().__init__(f"{reason}: '{url}'", full_context, original_error)
|
331
|
+
self.url = url
|
332
|
+
self.reason = reason
|
333
|
+
|
334
|
+
|
335
|
+
class RateLimitConfigError(ValidationError):
|
336
|
+
"""Raised when rate limit configuration is invalid"""
|
337
|
+
|
338
|
+
def __init__(
|
339
|
+
self, message: str, context: Optional[dict[str, Any]] = None, original_error: Optional[Exception] = None
|
340
|
+
):
|
341
|
+
super().__init__(f"Invalid rate limit configuration: {message}", context, original_error)
|
342
|
+
|
343
|
+
|
344
|
+
class HttpClientConfigError(ValidationError):
|
345
|
+
"""Raised when HTTP client configuration is invalid"""
|
346
|
+
|
347
|
+
def __init__(
|
348
|
+
self, message: str, context: Optional[dict[str, Any]] = None, original_error: Optional[Exception] = None
|
349
|
+
):
|
350
|
+
super().__init__(f"Invalid HTTP client configuration: {message}", context, original_error)
|
351
|
+
|
352
|
+
|
353
|
+
# =============================================================================
|
354
|
+
# API ERRORS
|
355
|
+
# =============================================================================
|
356
|
+
|
357
|
+
|
358
|
+
class APIError(DexscreenError):
|
359
|
+
"""
|
360
|
+
Base class for all API-related errors.
|
361
|
+
|
362
|
+
Use this to catch any API-related issue, including rate limits,
|
363
|
+
authentication, and invalid responses.
|
364
|
+
"""
|
365
|
+
|
366
|
+
pass
|
367
|
+
|
368
|
+
|
369
|
+
class RateLimitError(APIError):
|
370
|
+
"""
|
371
|
+
Raised when API rate limits are exceeded.
|
372
|
+
|
373
|
+
Attributes:
|
374
|
+
retry_after: Number of seconds to wait before retrying (if known)
|
375
|
+
limit_type: Type of rate limit ("requests_per_minute", "requests_per_second", etc.)
|
376
|
+
current_count: Current number of requests made (if available)
|
377
|
+
limit: Maximum allowed requests (if available)
|
378
|
+
"""
|
379
|
+
|
380
|
+
def __init__(
|
381
|
+
self,
|
382
|
+
message: str = "Rate limit exceeded",
|
383
|
+
retry_after: Optional[float] = None,
|
384
|
+
limit_type: Optional[str] = None,
|
385
|
+
current_count: Optional[int] = None,
|
386
|
+
limit: Optional[int] = None,
|
387
|
+
context: Optional[dict[str, Any]] = None,
|
388
|
+
original_error: Optional[Exception] = None,
|
389
|
+
):
|
390
|
+
full_context = context or {}
|
391
|
+
full_context.update(
|
392
|
+
{"retry_after": retry_after, "limit_type": limit_type, "current_count": current_count, "limit": limit}
|
393
|
+
)
|
394
|
+
|
395
|
+
super().__init__(message, full_context, original_error)
|
396
|
+
self.retry_after = retry_after
|
397
|
+
self.limit_type = limit_type
|
398
|
+
self.current_count = current_count
|
399
|
+
self.limit = limit
|
400
|
+
|
401
|
+
|
402
|
+
class AuthenticationError(APIError):
|
403
|
+
"""Raised when API authentication fails"""
|
404
|
+
|
405
|
+
pass
|
406
|
+
|
407
|
+
|
408
|
+
class InvalidResponseError(APIError):
|
409
|
+
"""
|
410
|
+
Raised when the API returns an invalid or unexpected response.
|
411
|
+
|
412
|
+
Attributes:
|
413
|
+
response_data: The actual response data received
|
414
|
+
expected_format: Description of what was expected
|
415
|
+
status_code: HTTP status code (if available)
|
416
|
+
"""
|
417
|
+
|
418
|
+
def __init__(
|
419
|
+
self,
|
420
|
+
message: str,
|
421
|
+
response_data: Any = None,
|
422
|
+
expected_format: Optional[str] = None,
|
423
|
+
status_code: Optional[int] = None,
|
424
|
+
context: Optional[dict[str, Any]] = None,
|
425
|
+
original_error: Optional[Exception] = None,
|
426
|
+
):
|
427
|
+
full_context = context or {}
|
428
|
+
full_context.update(
|
429
|
+
{"response_data": response_data, "expected_format": expected_format, "status_code": status_code}
|
430
|
+
)
|
431
|
+
|
432
|
+
super().__init__(message, full_context, original_error)
|
433
|
+
self.response_data = response_data
|
434
|
+
self.expected_format = expected_format
|
435
|
+
self.status_code = status_code
|
436
|
+
|
437
|
+
|
438
|
+
class APILimitError(APIError):
|
439
|
+
"""
|
440
|
+
Raised when API limits are exceeded (different from rate limiting).
|
441
|
+
|
442
|
+
Attributes:
|
443
|
+
limit_type: Type of limit exceeded ("max_addresses", "payload_size", etc.)
|
444
|
+
current_value: Current value that exceeded the limit
|
445
|
+
max_allowed: Maximum allowed value
|
446
|
+
"""
|
447
|
+
|
448
|
+
def __init__(
|
449
|
+
self,
|
450
|
+
message: str,
|
451
|
+
limit_type: Optional[str] = None,
|
452
|
+
current_value: Optional[Union[int, float]] = None,
|
453
|
+
max_allowed: Optional[Union[int, float]] = None,
|
454
|
+
context: Optional[dict[str, Any]] = None,
|
455
|
+
original_error: Optional[Exception] = None,
|
456
|
+
):
|
457
|
+
full_context = context or {}
|
458
|
+
full_context.update({"limit_type": limit_type, "current_value": current_value, "max_allowed": max_allowed})
|
459
|
+
|
460
|
+
super().__init__(message, full_context, original_error)
|
461
|
+
self.limit_type = limit_type
|
462
|
+
self.current_value = current_value
|
463
|
+
self.max_allowed = max_allowed
|
464
|
+
|
465
|
+
|
466
|
+
class ServerError(APIError):
|
467
|
+
"""
|
468
|
+
Raised when the API server encounters an internal error.
|
469
|
+
|
470
|
+
Attributes:
|
471
|
+
status_code: HTTP status code
|
472
|
+
retry_recommended: Whether retrying the request is recommended
|
473
|
+
"""
|
474
|
+
|
475
|
+
def __init__(
|
476
|
+
self,
|
477
|
+
message: str,
|
478
|
+
status_code: Optional[int] = None,
|
479
|
+
retry_recommended: bool = True,
|
480
|
+
context: Optional[dict[str, Any]] = None,
|
481
|
+
original_error: Optional[Exception] = None,
|
482
|
+
):
|
483
|
+
full_context = context or {}
|
484
|
+
full_context.update({"status_code": status_code, "retry_recommended": retry_recommended})
|
485
|
+
|
486
|
+
super().__init__(message, full_context, original_error)
|
487
|
+
self.status_code = status_code
|
488
|
+
self.retry_recommended = retry_recommended
|
489
|
+
|
490
|
+
|
491
|
+
# =============================================================================
|
492
|
+
# HTTP CLIENT EXCEPTIONS (Enhanced Backward Compatible)
|
493
|
+
# =============================================================================
|
494
|
+
|
495
|
+
|
496
|
+
class HttpError(DexscreenError):
|
497
|
+
"""Base exception for HTTP-related errors"""
|
498
|
+
|
499
|
+
pass
|
500
|
+
|
501
|
+
|
502
|
+
class HttpRequestError(HttpError):
|
503
|
+
"""Raised when an HTTP request fails"""
|
504
|
+
|
505
|
+
def __init__(
|
506
|
+
self,
|
507
|
+
method: str,
|
508
|
+
url: str,
|
509
|
+
status_code: Optional[int] = None,
|
510
|
+
response_text: Optional[str] = None,
|
511
|
+
original_error: Optional[Exception] = None,
|
512
|
+
context: Optional[dict[str, Any]] = None,
|
513
|
+
):
|
514
|
+
full_context = context or {}
|
515
|
+
full_context.update({"method": method, "url": url, "status_code": status_code, "response_text": response_text})
|
516
|
+
|
517
|
+
# Build error message
|
518
|
+
message = f"HTTP {method} request to '{url}' failed"
|
519
|
+
if status_code:
|
520
|
+
message += f" with status {status_code}"
|
521
|
+
if response_text and len(response_text) < 200:
|
522
|
+
message += f": {response_text}"
|
523
|
+
if original_error:
|
524
|
+
message += f" (original error: {type(original_error).__name__}: {original_error})"
|
525
|
+
|
526
|
+
super().__init__(message, full_context, original_error)
|
527
|
+
self.method = method
|
528
|
+
self.url = url
|
529
|
+
self.status_code = status_code
|
530
|
+
self.response_text = response_text
|
531
|
+
|
532
|
+
|
533
|
+
class HttpTimeoutError(HttpError):
|
534
|
+
"""Raised when an HTTP request times out"""
|
535
|
+
|
536
|
+
def __init__(
|
537
|
+
self,
|
538
|
+
method: str,
|
539
|
+
url: str,
|
540
|
+
timeout: float,
|
541
|
+
original_error: Optional[Exception] = None,
|
542
|
+
context: Optional[dict[str, Any]] = None,
|
543
|
+
):
|
544
|
+
full_context = context or {}
|
545
|
+
full_context.update({"method": method, "url": url, "timeout": timeout})
|
546
|
+
|
547
|
+
message = f"HTTP {method} request to '{url}' timed out after {timeout}s"
|
548
|
+
if original_error:
|
549
|
+
message += f" (original error: {type(original_error).__name__}: {original_error})"
|
550
|
+
|
551
|
+
super().__init__(message, full_context, original_error)
|
552
|
+
self.method = method
|
553
|
+
self.url = url
|
554
|
+
self.timeout = timeout
|
555
|
+
|
556
|
+
|
557
|
+
class HttpConnectionError(HttpError):
|
558
|
+
"""Raised when unable to establish HTTP connection"""
|
559
|
+
|
560
|
+
def __init__(
|
561
|
+
self,
|
562
|
+
method: str,
|
563
|
+
url: str,
|
564
|
+
original_error: Optional[Exception] = None,
|
565
|
+
context: Optional[dict[str, Any]] = None,
|
566
|
+
):
|
567
|
+
full_context = context or {}
|
568
|
+
full_context.update({"method": method, "url": url})
|
569
|
+
|
570
|
+
message = f"Failed to connect for HTTP {method} request to '{url}'"
|
571
|
+
if original_error:
|
572
|
+
message += f" (original error: {type(original_error).__name__}: {original_error})"
|
573
|
+
|
574
|
+
super().__init__(message, full_context, original_error)
|
575
|
+
self.method = method
|
576
|
+
self.url = url
|
577
|
+
|
578
|
+
|
579
|
+
class HttpResponseParsingError(HttpError):
|
580
|
+
"""Raised when unable to parse HTTP response (e.g., invalid JSON)"""
|
581
|
+
|
582
|
+
def __init__(
|
583
|
+
self,
|
584
|
+
method: str,
|
585
|
+
url: str,
|
586
|
+
content_type: Optional[str] = None,
|
587
|
+
response_content: Optional[str] = None,
|
588
|
+
original_error: Optional[Exception] = None,
|
589
|
+
context: Optional[dict[str, Any]] = None,
|
590
|
+
):
|
591
|
+
full_context = context or {}
|
592
|
+
full_context.update(
|
593
|
+
{"method": method, "url": url, "content_type": content_type, "response_content": response_content}
|
594
|
+
)
|
595
|
+
|
596
|
+
message = f"Failed to parse response from HTTP {method} request to '{url}'"
|
597
|
+
if content_type:
|
598
|
+
message += f" (content-type: {content_type})"
|
599
|
+
if original_error:
|
600
|
+
message += f" (original error: {type(original_error).__name__}: {original_error})"
|
601
|
+
|
602
|
+
super().__init__(message, full_context, original_error)
|
603
|
+
self.method = method
|
604
|
+
self.url = url
|
605
|
+
self.content_type = content_type
|
606
|
+
self.response_content = response_content
|
607
|
+
|
608
|
+
|
609
|
+
class HttpSessionError(HttpError):
|
610
|
+
"""Raised when HTTP session creation or management fails"""
|
611
|
+
|
612
|
+
def __init__(
|
613
|
+
self, message: str, original_error: Optional[Exception] = None, context: Optional[dict[str, Any]] = None
|
614
|
+
):
|
615
|
+
full_context = context or {}
|
616
|
+
|
617
|
+
if original_error:
|
618
|
+
message += f" (original error: {type(original_error).__name__}: {original_error})"
|
619
|
+
|
620
|
+
super().__init__(message, full_context, original_error)
|
621
|
+
|
622
|
+
|
623
|
+
# =============================================================================
|
624
|
+
# NETWORK ERRORS
|
625
|
+
# =============================================================================
|
626
|
+
|
627
|
+
|
628
|
+
class NetworkError(DexscreenError):
|
629
|
+
"""Base class for all network-related errors"""
|
630
|
+
|
631
|
+
pass
|
632
|
+
|
633
|
+
|
634
|
+
class ConnectionError(NetworkError):
|
635
|
+
"""
|
636
|
+
Raised when connection to the API server fails.
|
637
|
+
|
638
|
+
Attributes:
|
639
|
+
endpoint: The endpoint that failed to connect
|
640
|
+
timeout: Connection timeout value (if applicable)
|
641
|
+
"""
|
642
|
+
|
643
|
+
def __init__(
|
644
|
+
self,
|
645
|
+
message: str,
|
646
|
+
endpoint: Optional[str] = None,
|
647
|
+
timeout: Optional[float] = None,
|
648
|
+
context: Optional[dict[str, Any]] = None,
|
649
|
+
original_error: Optional[Exception] = None,
|
650
|
+
):
|
651
|
+
full_context = context or {}
|
652
|
+
full_context.update({"endpoint": endpoint, "timeout": timeout})
|
653
|
+
|
654
|
+
super().__init__(message, full_context, original_error)
|
655
|
+
self.endpoint = endpoint
|
656
|
+
self.timeout = timeout
|
657
|
+
|
658
|
+
|
659
|
+
class TimeoutError(NetworkError):
|
660
|
+
"""
|
661
|
+
Raised when a request times out.
|
662
|
+
|
663
|
+
Attributes:
|
664
|
+
timeout_duration: How long the request waited before timing out
|
665
|
+
operation_type: Type of operation that timed out ("request", "connection", etc.)
|
666
|
+
"""
|
667
|
+
|
668
|
+
def __init__(
|
669
|
+
self,
|
670
|
+
message: str,
|
671
|
+
timeout_duration: Optional[float] = None,
|
672
|
+
operation_type: Optional[str] = None,
|
673
|
+
context: Optional[dict[str, Any]] = None,
|
674
|
+
original_error: Optional[Exception] = None,
|
675
|
+
):
|
676
|
+
full_context = context or {}
|
677
|
+
full_context.update({"timeout_duration": timeout_duration, "operation_type": operation_type})
|
678
|
+
|
679
|
+
super().__init__(message, full_context, original_error)
|
680
|
+
self.timeout_duration = timeout_duration
|
681
|
+
self.operation_type = operation_type
|
682
|
+
|
683
|
+
|
684
|
+
class ProxyError(NetworkError):
|
685
|
+
"""
|
686
|
+
Raised when proxy-related errors occur.
|
687
|
+
|
688
|
+
Attributes:
|
689
|
+
proxy_url: The proxy URL that caused the error
|
690
|
+
proxy_type: Type of proxy ("http", "socks5", etc.)
|
691
|
+
"""
|
692
|
+
|
693
|
+
def __init__(
|
694
|
+
self,
|
695
|
+
message: str,
|
696
|
+
proxy_url: Optional[str] = None,
|
697
|
+
proxy_type: Optional[str] = None,
|
698
|
+
context: Optional[dict[str, Any]] = None,
|
699
|
+
original_error: Optional[Exception] = None,
|
700
|
+
):
|
701
|
+
full_context = context or {}
|
702
|
+
full_context.update({"proxy_url": proxy_url, "proxy_type": proxy_type})
|
703
|
+
|
704
|
+
super().__init__(message, full_context, original_error)
|
705
|
+
self.proxy_url = proxy_url
|
706
|
+
self.proxy_type = proxy_type
|
707
|
+
|
708
|
+
|
709
|
+
# =============================================================================
|
710
|
+
# DATA VALIDATION ERRORS (Additional)
|
711
|
+
# =============================================================================
|
712
|
+
|
713
|
+
|
714
|
+
class DataFormatError(ValidationError):
|
715
|
+
"""
|
716
|
+
Raised when data doesn't match expected format.
|
717
|
+
|
718
|
+
Attributes:
|
719
|
+
field_name: Name of the field with invalid format
|
720
|
+
received_value: The actual value received
|
721
|
+
expected_type: Expected data type or format
|
722
|
+
"""
|
723
|
+
|
724
|
+
def __init__(
|
725
|
+
self,
|
726
|
+
message: str,
|
727
|
+
field_name: Optional[str] = None,
|
728
|
+
received_value: Any = None,
|
729
|
+
expected_type: Optional[str] = None,
|
730
|
+
context: Optional[dict[str, Any]] = None,
|
731
|
+
original_error: Optional[Exception] = None,
|
732
|
+
):
|
733
|
+
full_context = context or {}
|
734
|
+
full_context.update(
|
735
|
+
{"field_name": field_name, "received_value": received_value, "expected_type": expected_type}
|
736
|
+
)
|
737
|
+
|
738
|
+
super().__init__(message, full_context, original_error)
|
739
|
+
self.field_name = field_name
|
740
|
+
self.received_value = received_value
|
741
|
+
self.expected_type = expected_type
|
742
|
+
|
743
|
+
|
744
|
+
class MissingDataError(ValidationError):
|
745
|
+
"""
|
746
|
+
Raised when required data is missing.
|
747
|
+
|
748
|
+
Attributes:
|
749
|
+
missing_fields: List of missing required fields
|
750
|
+
data_source: Source of the data ("api_response", "user_input", etc.)
|
751
|
+
"""
|
752
|
+
|
753
|
+
def __init__(
|
754
|
+
self,
|
755
|
+
message: str,
|
756
|
+
missing_fields: Optional[list[str]] = None,
|
757
|
+
data_source: Optional[str] = None,
|
758
|
+
context: Optional[dict[str, Any]] = None,
|
759
|
+
original_error: Optional[Exception] = None,
|
760
|
+
):
|
761
|
+
full_context = context or {}
|
762
|
+
full_context.update({"missing_fields": missing_fields or [], "data_source": data_source})
|
763
|
+
|
764
|
+
super().__init__(message, full_context, original_error)
|
765
|
+
self.missing_fields = missing_fields or []
|
766
|
+
self.data_source = data_source
|
767
|
+
|
768
|
+
|
769
|
+
# =============================================================================
|
770
|
+
# STREAMING ERRORS
|
771
|
+
# =============================================================================
|
772
|
+
|
773
|
+
|
774
|
+
class StreamError(DexscreenError):
|
775
|
+
"""Base class for all streaming/WebSocket-related errors"""
|
776
|
+
|
777
|
+
pass
|
778
|
+
|
779
|
+
|
780
|
+
class StreamConnectionError(StreamError):
|
781
|
+
"""
|
782
|
+
Raised when WebSocket/streaming connection fails.
|
783
|
+
|
784
|
+
Attributes:
|
785
|
+
stream_url: The streaming endpoint URL
|
786
|
+
reconnect_attempts: Number of reconnection attempts made
|
787
|
+
max_reconnect_attempts: Maximum allowed reconnection attempts
|
788
|
+
"""
|
789
|
+
|
790
|
+
def __init__(
|
791
|
+
self,
|
792
|
+
message: str,
|
793
|
+
stream_url: Optional[str] = None,
|
794
|
+
reconnect_attempts: Optional[int] = None,
|
795
|
+
max_reconnect_attempts: Optional[int] = None,
|
796
|
+
context: Optional[dict[str, Any]] = None,
|
797
|
+
original_error: Optional[Exception] = None,
|
798
|
+
):
|
799
|
+
full_context = context or {}
|
800
|
+
full_context.update(
|
801
|
+
{
|
802
|
+
"stream_url": stream_url,
|
803
|
+
"reconnect_attempts": reconnect_attempts,
|
804
|
+
"max_reconnect_attempts": max_reconnect_attempts,
|
805
|
+
}
|
806
|
+
)
|
807
|
+
|
808
|
+
super().__init__(message, full_context, original_error)
|
809
|
+
self.stream_url = stream_url
|
810
|
+
self.reconnect_attempts = reconnect_attempts
|
811
|
+
self.max_reconnect_attempts = max_reconnect_attempts
|
812
|
+
|
813
|
+
|
814
|
+
class StreamTimeoutError(StreamError):
|
815
|
+
"""
|
816
|
+
Raised when streaming operations timeout.
|
817
|
+
|
818
|
+
Attributes:
|
819
|
+
timeout_duration: How long the operation waited before timing out
|
820
|
+
operation: The operation that timed out ("subscribe", "unsubscribe", "message", etc.)
|
821
|
+
"""
|
822
|
+
|
823
|
+
def __init__(
|
824
|
+
self,
|
825
|
+
message: str,
|
826
|
+
timeout_duration: Optional[float] = None,
|
827
|
+
operation: Optional[str] = None,
|
828
|
+
context: Optional[dict[str, Any]] = None,
|
829
|
+
original_error: Optional[Exception] = None,
|
830
|
+
):
|
831
|
+
full_context = context or {}
|
832
|
+
full_context.update({"timeout_duration": timeout_duration, "operation": operation})
|
833
|
+
|
834
|
+
super().__init__(message, full_context, original_error)
|
835
|
+
self.timeout_duration = timeout_duration
|
836
|
+
self.operation = operation
|
837
|
+
|
838
|
+
|
839
|
+
class SubscriptionError(StreamError):
|
840
|
+
"""
|
841
|
+
Raised when subscription operations fail.
|
842
|
+
|
843
|
+
Attributes:
|
844
|
+
subscription_id: ID of the failed subscription
|
845
|
+
subscription_type: Type of subscription ("pair", "token", etc.)
|
846
|
+
operation: The operation that failed ("subscribe", "unsubscribe", "update", etc.)
|
847
|
+
"""
|
848
|
+
|
849
|
+
def __init__(
|
850
|
+
self,
|
851
|
+
message: str,
|
852
|
+
subscription_id: Optional[str] = None,
|
853
|
+
subscription_type: Optional[str] = None,
|
854
|
+
operation: Optional[str] = None,
|
855
|
+
context: Optional[dict[str, Any]] = None,
|
856
|
+
original_error: Optional[Exception] = None,
|
857
|
+
):
|
858
|
+
full_context = context or {}
|
859
|
+
full_context.update(
|
860
|
+
{"subscription_id": subscription_id, "subscription_type": subscription_type, "operation": operation}
|
861
|
+
)
|
862
|
+
|
863
|
+
super().__init__(message, full_context, original_error)
|
864
|
+
self.subscription_id = subscription_id
|
865
|
+
self.subscription_type = subscription_type
|
866
|
+
self.operation = operation
|
867
|
+
|
868
|
+
|
869
|
+
class StreamDataError(StreamError):
|
870
|
+
"""
|
871
|
+
Raised when streaming data is invalid or corrupted.
|
872
|
+
|
873
|
+
Attributes:
|
874
|
+
data_type: Type of data that was invalid ("pair_update", "token_data", etc.)
|
875
|
+
raw_data: The raw data that caused the error
|
876
|
+
"""
|
877
|
+
|
878
|
+
def __init__(
|
879
|
+
self,
|
880
|
+
message: str,
|
881
|
+
data_type: Optional[str] = None,
|
882
|
+
raw_data: Any = None,
|
883
|
+
context: Optional[dict[str, Any]] = None,
|
884
|
+
original_error: Optional[Exception] = None,
|
885
|
+
):
|
886
|
+
full_context = context or {}
|
887
|
+
full_context.update({"data_type": data_type, "raw_data": raw_data})
|
888
|
+
|
889
|
+
super().__init__(message, full_context, original_error)
|
890
|
+
self.data_type = data_type
|
891
|
+
self.raw_data = raw_data
|
892
|
+
|
893
|
+
|
894
|
+
# =============================================================================
|
895
|
+
# CONFIGURATION ERRORS
|
896
|
+
# =============================================================================
|
897
|
+
|
898
|
+
|
899
|
+
class ConfigurationError(DexscreenError):
|
900
|
+
"""Base class for all configuration-related errors"""
|
901
|
+
|
902
|
+
pass
|
903
|
+
|
904
|
+
|
905
|
+
class InvalidConfigError(ConfigurationError):
|
906
|
+
"""
|
907
|
+
Raised when configuration parameters are invalid.
|
908
|
+
|
909
|
+
Attributes:
|
910
|
+
config_key: The configuration key that's invalid
|
911
|
+
config_value: The invalid configuration value
|
912
|
+
expected_values: List of expected/valid values (if applicable)
|
913
|
+
"""
|
914
|
+
|
915
|
+
def __init__(
|
916
|
+
self,
|
917
|
+
message: str,
|
918
|
+
config_key: Optional[str] = None,
|
919
|
+
config_value: Any = None,
|
920
|
+
expected_values: Optional[list] = None,
|
921
|
+
context: Optional[dict[str, Any]] = None,
|
922
|
+
original_error: Optional[Exception] = None,
|
923
|
+
):
|
924
|
+
full_context = context or {}
|
925
|
+
full_context.update(
|
926
|
+
{"config_key": config_key, "config_value": config_value, "expected_values": expected_values}
|
927
|
+
)
|
928
|
+
|
929
|
+
super().__init__(message, full_context, original_error)
|
930
|
+
self.config_key = config_key
|
931
|
+
self.config_value = config_value
|
932
|
+
self.expected_values = expected_values
|
933
|
+
|
934
|
+
|
935
|
+
class MissingConfigError(ConfigurationError):
|
936
|
+
"""
|
937
|
+
Raised when required configuration is missing.
|
938
|
+
|
939
|
+
Attributes:
|
940
|
+
required_configs: List of missing required configuration keys
|
941
|
+
config_source: Source where config should be provided ("env", "kwargs", "file", etc.)
|
942
|
+
"""
|
943
|
+
|
944
|
+
def __init__(
|
945
|
+
self,
|
946
|
+
message: str,
|
947
|
+
required_configs: Optional[list[str]] = None,
|
948
|
+
config_source: Optional[str] = None,
|
949
|
+
context: Optional[dict[str, Any]] = None,
|
950
|
+
original_error: Optional[Exception] = None,
|
951
|
+
):
|
952
|
+
full_context = context or {}
|
953
|
+
full_context.update({"required_configs": required_configs or [], "config_source": config_source})
|
954
|
+
|
955
|
+
super().__init__(message, full_context, original_error)
|
956
|
+
self.required_configs = required_configs or []
|
957
|
+
self.config_source = config_source
|
958
|
+
|
959
|
+
|
960
|
+
class FilterConfigError(ConfigurationError):
|
961
|
+
"""
|
962
|
+
Raised when filter configuration is invalid.
|
963
|
+
|
964
|
+
This is an alias for InvalidFilterError for better categorization.
|
965
|
+
"""
|
966
|
+
|
967
|
+
def __init__(
|
968
|
+
self,
|
969
|
+
message: str,
|
970
|
+
filter_type: Optional[str] = None,
|
971
|
+
invalid_parameters: Optional[list[str]] = None,
|
972
|
+
context: Optional[dict[str, Any]] = None,
|
973
|
+
original_error: Optional[Exception] = None,
|
974
|
+
):
|
975
|
+
full_context = context or {}
|
976
|
+
full_context.update({"filter_type": filter_type, "invalid_parameters": invalid_parameters or []})
|
977
|
+
|
978
|
+
super().__init__(f"Invalid filter configuration: {message}", full_context, original_error)
|
979
|
+
self.filter_type = filter_type
|
980
|
+
self.invalid_parameters = invalid_parameters or []
|
981
|
+
|
982
|
+
|
983
|
+
# =============================================================================
|
984
|
+
# CONVENIENCE FUNCTIONS
|
985
|
+
# =============================================================================
|
986
|
+
|
987
|
+
|
988
|
+
def is_retryable_error(error: Exception) -> bool:
|
989
|
+
"""
|
990
|
+
Determine if an error is retryable.
|
991
|
+
|
992
|
+
Args:
|
993
|
+
error: The exception to check
|
994
|
+
|
995
|
+
Returns:
|
996
|
+
True if the error suggests that retrying might succeed
|
997
|
+
"""
|
998
|
+
retryable_types = (
|
999
|
+
TimeoutError,
|
1000
|
+
ConnectionError,
|
1001
|
+
ServerError,
|
1002
|
+
StreamConnectionError,
|
1003
|
+
StreamTimeoutError,
|
1004
|
+
HttpTimeoutError,
|
1005
|
+
HttpConnectionError,
|
1006
|
+
)
|
1007
|
+
|
1008
|
+
if isinstance(error, retryable_types):
|
1009
|
+
return True
|
1010
|
+
|
1011
|
+
if isinstance(error, ServerError) and error.retry_recommended:
|
1012
|
+
return True
|
1013
|
+
|
1014
|
+
return isinstance(error, RateLimitError) # Can retry after waiting
|
1015
|
+
|
1016
|
+
|
1017
|
+
def should_wait_before_retry(error: Exception) -> Optional[float]:
|
1018
|
+
"""
|
1019
|
+
Get recommended wait time before retrying an error.
|
1020
|
+
|
1021
|
+
Args:
|
1022
|
+
error: The exception to check
|
1023
|
+
|
1024
|
+
Returns:
|
1025
|
+
Number of seconds to wait, or None if no specific wait time is recommended
|
1026
|
+
"""
|
1027
|
+
if isinstance(error, RateLimitError) and error.retry_after:
|
1028
|
+
return error.retry_after
|
1029
|
+
|
1030
|
+
if isinstance(error, (TimeoutError, HttpTimeoutError)):
|
1031
|
+
return 1.0 # Short wait for timeouts
|
1032
|
+
|
1033
|
+
if isinstance(error, (ConnectionError, HttpConnectionError)):
|
1034
|
+
return 2.0 # Longer wait for connection issues
|
1035
|
+
|
1036
|
+
if isinstance(error, ServerError):
|
1037
|
+
return 5.0 # Even longer wait for server errors
|
1038
|
+
|
1039
|
+
return None
|
1040
|
+
|
1041
|
+
|
1042
|
+
def get_error_category(error: Exception) -> str:
|
1043
|
+
"""
|
1044
|
+
Get the category of an error for monitoring/logging purposes.
|
1045
|
+
|
1046
|
+
Args:
|
1047
|
+
error: The exception to categorize
|
1048
|
+
|
1049
|
+
Returns:
|
1050
|
+
String category of the error
|
1051
|
+
"""
|
1052
|
+
if isinstance(error, APIError):
|
1053
|
+
return "api"
|
1054
|
+
elif isinstance(error, NetworkError):
|
1055
|
+
return "network"
|
1056
|
+
elif isinstance(error, ValidationError):
|
1057
|
+
return "validation"
|
1058
|
+
elif isinstance(error, StreamError):
|
1059
|
+
return "streaming"
|
1060
|
+
elif isinstance(error, ConfigurationError):
|
1061
|
+
return "configuration"
|
1062
|
+
elif isinstance(error, HttpError):
|
1063
|
+
return "http"
|
1064
|
+
elif isinstance(error, DexscreenError):
|
1065
|
+
return "dexscreen"
|
1066
|
+
else:
|
1067
|
+
return "unknown"
|