dexscreen 0.0.2__py3-none-any.whl → 0.0.5__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.
@@ -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"