Coverage for sentimatrix / core / exceptions.py: 98%
242 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-01-28 09:30 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-01-28 09:30 +0000
1"""
2Sentimatrix Exception Hierarchy
4Defines a comprehensive exception hierarchy for error handling throughout
5the application. All exceptions inherit from SentimatrixError.
7Exception Hierarchy:
8 SentimatrixError (base)
9 ├── ConfigurationError
10 ├── ValidationError
11 ├── ProviderError
12 │ ├── LLMProviderError
13 │ │ ├── OpenAIError
14 │ │ ├── AnthropicError
15 │ │ ├── GroqError
16 │ │ └── ...
17 │ ├── ScraperError
18 │ │ ├── PlaywrightError
19 │ │ ├── SeleniumError
20 │ │ └── ...
21 │ └── ModelError
22 ├── RateLimitError
23 ├── TimeoutError
24 ├── CacheError
25 └── PipelineError
26"""
28from __future__ import annotations
30from enum import IntEnum
31from typing import Any, Dict, Optional
34class ErrorCode(IntEnum):
35 """Standardized error codes for Sentimatrix errors."""
37 # General errors (1000-1099)
38 UNKNOWN = 1000
39 INTERNAL = 1001
40 NOT_IMPLEMENTED = 1002
42 # Configuration errors (1100-1199)
43 CONFIG_NOT_FOUND = 1100
44 CONFIG_PARSE_ERROR = 1101
45 CONFIG_VALIDATION_ERROR = 1102
46 CONFIG_MISSING_REQUIRED = 1103
48 # Validation errors (1200-1299)
49 VALIDATION_FAILED = 1200
50 INVALID_INPUT = 1201
51 INVALID_FORMAT = 1202
52 MISSING_REQUIRED_FIELD = 1203
54 # Provider errors (1300-1399)
55 PROVIDER_NOT_FOUND = 1300
56 PROVIDER_NOT_AVAILABLE = 1301
57 PROVIDER_INITIALIZATION_FAILED = 1302
59 # LLM errors (1400-1499)
60 LLM_API_ERROR = 1400
61 LLM_AUTHENTICATION_FAILED = 1401
62 LLM_INVALID_MODEL = 1402
63 LLM_CONTENT_FILTERED = 1403
64 LLM_TOKEN_LIMIT_EXCEEDED = 1404
65 LLM_INVALID_RESPONSE = 1405
67 # Scraper errors (1500-1599)
68 SCRAPER_CONNECTION_ERROR = 1500
69 SCRAPER_TIMEOUT = 1501
70 SCRAPER_BLOCKED = 1502
71 SCRAPER_PARSE_ERROR = 1503
72 SCRAPER_CAPTCHA_DETECTED = 1504
73 SCRAPER_RATE_LIMITED = 1505
75 # Model errors (1600-1699)
76 MODEL_NOT_FOUND = 1600
77 MODEL_LOAD_ERROR = 1601
78 MODEL_INFERENCE_ERROR = 1602
79 MODEL_DEVICE_ERROR = 1603
81 # Rate limit errors (1700-1799)
82 RATE_LIMIT_EXCEEDED = 1700
83 QUOTA_EXCEEDED = 1701
84 CONCURRENT_LIMIT_EXCEEDED = 1702
86 # Timeout errors (1800-1899)
87 REQUEST_TIMEOUT = 1800
88 CONNECTION_TIMEOUT = 1801
89 READ_TIMEOUT = 1802
91 # Cache errors (1900-1999)
92 CACHE_CONNECTION_ERROR = 1900
93 CACHE_READ_ERROR = 1901
94 CACHE_WRITE_ERROR = 1902
95 CACHE_SERIALIZATION_ERROR = 1903
97 # Pipeline errors (2000-2099)
98 PIPELINE_EXECUTION_ERROR = 2000
99 PIPELINE_STEP_FAILED = 2001
100 PIPELINE_INVALID_STATE = 2002
103class SentimatrixError(Exception):
104 """
105 Base exception for all Sentimatrix errors.
107 Attributes:
108 message: Human-readable error message
109 code: Standardized error code
110 details: Additional error details/context
111 original_error: Original exception that caused this error
112 """
114 def __init__(
115 self,
116 message: str,
117 code: ErrorCode = ErrorCode.UNKNOWN,
118 details: Optional[Dict[str, Any]] = None,
119 original_error: Optional[Exception] = None,
120 ) -> None:
121 """
122 Initialize a SentimatrixError.
124 Args:
125 message: Human-readable error message
126 code: Error code from ErrorCode enum
127 details: Additional error context
128 original_error: Original exception if wrapping another error
129 """
130 super().__init__(message)
131 self.message = message
132 self.code = code
133 self.details = details or {}
134 self.original_error = original_error
136 def __str__(self) -> str:
137 """Return string representation of the error."""
138 base = f"[{self.code.name}({self.code.value})] {self.message}"
139 if self.details:
140 base += f" | Details: {self.details}"
141 if self.original_error:
142 base += f" | Caused by: {type(self.original_error).__name__}: {self.original_error}"
143 return base
145 def __repr__(self) -> str:
146 """Return detailed representation of the error."""
147 return (
148 f"{self.__class__.__name__}("
149 f"message={self.message!r}, "
150 f"code={self.code}, "
151 f"details={self.details!r})"
152 )
154 def to_dict(self) -> Dict[str, Any]:
155 """Convert error to dictionary for serialization."""
156 return {
157 "error_type": self.__class__.__name__,
158 "message": self.message,
159 "code": self.code.value,
160 "code_name": self.code.name,
161 "details": self.details,
162 "original_error": str(self.original_error) if self.original_error else None,
163 }
166# Configuration Errors
169class ConfigurationError(SentimatrixError):
170 """Raised when there's an error in configuration."""
172 def __init__(
173 self,
174 message: str,
175 code: ErrorCode = ErrorCode.CONFIG_PARSE_ERROR,
176 details: Optional[Dict[str, Any]] = None,
177 original_error: Optional[Exception] = None,
178 ) -> None:
179 super().__init__(message, code, details, original_error)
182class ConfigNotFoundError(ConfigurationError):
183 """Raised when configuration file is not found."""
185 def __init__(self, path: str) -> None:
186 super().__init__(
187 f"Configuration file not found: {path}",
188 code=ErrorCode.CONFIG_NOT_FOUND,
189 details={"path": path},
190 )
193class ConfigValidationError(ConfigurationError):
194 """Raised when configuration validation fails."""
196 def __init__(self, field: str, message: str, value: Any = None) -> None:
197 super().__init__(
198 f"Configuration validation error for '{field}': {message}",
199 code=ErrorCode.CONFIG_VALIDATION_ERROR,
200 details={"field": field, "value": value, "validation_message": message},
201 )
204# Validation Errors
207class ValidationError(SentimatrixError):
208 """Raised when input validation fails."""
210 def __init__(
211 self,
212 message: str,
213 code: ErrorCode = ErrorCode.VALIDATION_FAILED,
214 details: Optional[Dict[str, Any]] = None,
215 original_error: Optional[Exception] = None,
216 ) -> None:
217 super().__init__(message, code, details, original_error)
220class InvalidInputError(ValidationError):
221 """Raised when input data is invalid."""
223 def __init__(self, field: str, message: str, value: Any = None) -> None:
224 super().__init__(
225 f"Invalid input for '{field}': {message}",
226 code=ErrorCode.INVALID_INPUT,
227 details={"field": field, "value": repr(value)[:100] if value else None},
228 )
231class MissingRequiredFieldError(ValidationError):
232 """Raised when a required field is missing."""
234 def __init__(self, field: str) -> None:
235 super().__init__(
236 f"Missing required field: {field}",
237 code=ErrorCode.MISSING_REQUIRED_FIELD,
238 details={"field": field},
239 )
242# Provider Errors
245class ProviderError(SentimatrixError):
246 """Base class for provider-related errors."""
248 def __init__(
249 self,
250 message: str,
251 provider: str,
252 code: ErrorCode = ErrorCode.PROVIDER_NOT_AVAILABLE,
253 details: Optional[Dict[str, Any]] = None,
254 original_error: Optional[Exception] = None,
255 ) -> None:
256 details = details or {}
257 details["provider"] = provider
258 super().__init__(message, code, details, original_error)
259 self.provider = provider
262class ProviderNotFoundError(ProviderError):
263 """Raised when requested provider is not found."""
265 def __init__(self, provider: str) -> None:
266 super().__init__(
267 f"Provider not found: {provider}",
268 provider=provider,
269 code=ErrorCode.PROVIDER_NOT_FOUND,
270 )
273class ProviderInitializationError(ProviderError):
274 """Raised when provider initialization fails."""
276 def __init__(self, provider: str, reason: str) -> None:
277 super().__init__(
278 f"Failed to initialize provider '{provider}': {reason}",
279 provider=provider,
280 code=ErrorCode.PROVIDER_INITIALIZATION_FAILED,
281 details={"reason": reason},
282 )
285# LLM Provider Errors
288class LLMProviderError(ProviderError):
289 """Base class for LLM provider errors."""
291 def __init__(
292 self,
293 message: str,
294 provider: str,
295 model: Optional[str] = None,
296 code: ErrorCode = ErrorCode.LLM_API_ERROR,
297 details: Optional[Dict[str, Any]] = None,
298 original_error: Optional[Exception] = None,
299 ) -> None:
300 details = details or {}
301 if model:
302 details["model"] = model
303 super().__init__(message, provider, code, details, original_error)
304 self.model = model
307class AuthenticationError(LLMProviderError):
308 """Raised when API authentication fails."""
310 def __init__(self, provider: str, message: str = "Authentication failed") -> None:
311 super().__init__(
312 message,
313 provider=provider,
314 code=ErrorCode.LLM_AUTHENTICATION_FAILED,
315 )
318class InvalidModelError(LLMProviderError):
319 """Raised when specified model is invalid or unavailable."""
321 def __init__(self, provider: str, model: str) -> None:
322 super().__init__(
323 f"Invalid or unavailable model: {model}",
324 provider=provider,
325 model=model,
326 code=ErrorCode.LLM_INVALID_MODEL,
327 )
330class ContentFilteredError(LLMProviderError):
331 """Raised when content is filtered by provider's safety systems."""
333 def __init__(self, provider: str, reason: Optional[str] = None) -> None:
334 message = "Content was filtered by safety systems"
335 if reason: 335 ↛ 337line 335 didn't jump to line 337 because the condition on line 335 was always true
336 message += f": {reason}"
337 super().__init__(
338 message,
339 provider=provider,
340 code=ErrorCode.LLM_CONTENT_FILTERED,
341 details={"reason": reason} if reason else None,
342 )
345class TokenLimitExceededError(LLMProviderError):
346 """Raised when token limit is exceeded."""
348 def __init__(
349 self, provider: str, model: str, requested: int, limit: int
350 ) -> None:
351 super().__init__(
352 f"Token limit exceeded: requested {requested}, limit {limit}",
353 provider=provider,
354 model=model,
355 code=ErrorCode.LLM_TOKEN_LIMIT_EXCEEDED,
356 details={"requested_tokens": requested, "token_limit": limit},
357 )
360class InvalidResponseError(LLMProviderError):
361 """Raised when LLM response is invalid or malformed."""
363 def __init__(self, provider: str, reason: str) -> None:
364 super().__init__(
365 f"Invalid response from LLM: {reason}",
366 provider=provider,
367 code=ErrorCode.LLM_INVALID_RESPONSE,
368 details={"reason": reason},
369 )
372# Provider-specific LLM errors
375class OpenAIError(LLMProviderError):
376 """OpenAI-specific error."""
378 def __init__(
379 self,
380 message: str,
381 model: Optional[str] = None,
382 code: ErrorCode = ErrorCode.LLM_API_ERROR,
383 original_error: Optional[Exception] = None,
384 ) -> None:
385 super().__init__(message, "openai", model, code, original_error=original_error)
388class AnthropicError(LLMProviderError):
389 """Anthropic-specific error."""
391 def __init__(
392 self,
393 message: str,
394 model: Optional[str] = None,
395 code: ErrorCode = ErrorCode.LLM_API_ERROR,
396 original_error: Optional[Exception] = None,
397 ) -> None:
398 super().__init__(message, "anthropic", model, code, original_error=original_error)
401class GroqError(LLMProviderError):
402 """Groq-specific error."""
404 def __init__(
405 self,
406 message: str,
407 model: Optional[str] = None,
408 code: ErrorCode = ErrorCode.LLM_API_ERROR,
409 original_error: Optional[Exception] = None,
410 ) -> None:
411 super().__init__(message, "groq", model, code, original_error=original_error)
414class GeminiError(LLMProviderError):
415 """Google Gemini-specific error."""
417 def __init__(
418 self,
419 message: str,
420 model: Optional[str] = None,
421 code: ErrorCode = ErrorCode.LLM_API_ERROR,
422 original_error: Optional[Exception] = None,
423 ) -> None:
424 super().__init__(message, "gemini", model, code, original_error=original_error)
427class OllamaError(LLMProviderError):
428 """Ollama-specific error."""
430 def __init__(
431 self,
432 message: str,
433 model: Optional[str] = None,
434 code: ErrorCode = ErrorCode.LLM_API_ERROR,
435 original_error: Optional[Exception] = None,
436 ) -> None:
437 super().__init__(message, "ollama", model, code, original_error=original_error)
440# Scraper Errors
443class ScraperError(ProviderError):
444 """Base class for scraper errors."""
446 def __init__(
447 self,
448 message: str,
449 provider: str,
450 url: Optional[str] = None,
451 code: ErrorCode = ErrorCode.SCRAPER_CONNECTION_ERROR,
452 details: Optional[Dict[str, Any]] = None,
453 original_error: Optional[Exception] = None,
454 ) -> None:
455 details = details or {}
456 if url:
457 details["url"] = url
458 super().__init__(message, provider, code, details, original_error)
459 self.url = url
462class ScraperConnectionError(ScraperError):
463 """Raised when scraper fails to connect."""
465 def __init__(self, provider: str, url: str, reason: str) -> None:
466 super().__init__(
467 f"Failed to connect to {url}: {reason}",
468 provider=provider,
469 url=url,
470 code=ErrorCode.SCRAPER_CONNECTION_ERROR,
471 details={"reason": reason},
472 )
475class ScraperTimeoutError(ScraperError):
476 """Raised when scraper operation times out."""
478 def __init__(self, provider: str, url: str, timeout: int) -> None:
479 super().__init__(
480 f"Scraper timeout after {timeout}s for {url}",
481 provider=provider,
482 url=url,
483 code=ErrorCode.SCRAPER_TIMEOUT,
484 details={"timeout_seconds": timeout},
485 )
488class ScraperBlockedError(ScraperError):
489 """Raised when scraper is blocked by the target site."""
491 def __init__(self, provider: str, url: str, reason: Optional[str] = None) -> None:
492 message = f"Scraper blocked at {url}"
493 if reason: 493 ↛ 495line 493 didn't jump to line 495 because the condition on line 493 was always true
494 message += f": {reason}"
495 super().__init__(
496 message,
497 provider=provider,
498 url=url,
499 code=ErrorCode.SCRAPER_BLOCKED,
500 details={"reason": reason} if reason else None,
501 )
504class CaptchaDetectedError(ScraperError):
505 """Raised when CAPTCHA is detected."""
507 def __init__(self, provider: str, url: str) -> None:
508 super().__init__(
509 f"CAPTCHA detected at {url}",
510 provider=provider,
511 url=url,
512 code=ErrorCode.SCRAPER_CAPTCHA_DETECTED,
513 )
516class ScraperParseError(ScraperError):
517 """Raised when content parsing fails."""
519 def __init__(self, provider: str, url: str, reason: str) -> None:
520 super().__init__(
521 f"Failed to parse content from {url}: {reason}",
522 provider=provider,
523 url=url,
524 code=ErrorCode.SCRAPER_PARSE_ERROR,
525 details={"reason": reason},
526 )
529# Provider-specific scraper errors
532class PlaywrightError(ScraperError):
533 """Playwright-specific error."""
535 def __init__(
536 self,
537 message: str,
538 url: Optional[str] = None,
539 code: ErrorCode = ErrorCode.SCRAPER_CONNECTION_ERROR,
540 original_error: Optional[Exception] = None,
541 ) -> None:
542 super().__init__(message, "playwright", url, code, original_error=original_error)
545class SeleniumError(ScraperError):
546 """Selenium-specific error."""
548 def __init__(
549 self,
550 message: str,
551 url: Optional[str] = None,
552 code: ErrorCode = ErrorCode.SCRAPER_CONNECTION_ERROR,
553 original_error: Optional[Exception] = None,
554 ) -> None:
555 super().__init__(message, "selenium", url, code, original_error=original_error)
558# Model Errors
561class ModelError(ProviderError):
562 """Base class for ML model errors."""
564 def __init__(
565 self,
566 message: str,
567 model_name: str,
568 code: ErrorCode = ErrorCode.MODEL_INFERENCE_ERROR,
569 details: Optional[Dict[str, Any]] = None,
570 original_error: Optional[Exception] = None,
571 ) -> None:
572 details = details or {}
573 details["model_name"] = model_name
574 super().__init__(message, "model", code, details, original_error)
575 self.model_name = model_name
578class ModelNotFoundError(ModelError):
579 """Raised when model cannot be found or loaded."""
581 def __init__(self, model_name: str) -> None:
582 super().__init__(
583 f"Model not found: {model_name}",
584 model_name=model_name,
585 code=ErrorCode.MODEL_NOT_FOUND,
586 )
589class ModelLoadError(ModelError):
590 """Raised when model fails to load."""
592 def __init__(self, model_name: str, reason: str) -> None:
593 super().__init__(
594 f"Failed to load model '{model_name}': {reason}",
595 model_name=model_name,
596 code=ErrorCode.MODEL_LOAD_ERROR,
597 details={"reason": reason},
598 )
601class ModelInferenceError(ModelError):
602 """Raised when model inference fails."""
604 def __init__(self, model_name: str, reason: str) -> None:
605 super().__init__(
606 f"Inference error in model '{model_name}': {reason}",
607 model_name=model_name,
608 code=ErrorCode.MODEL_INFERENCE_ERROR,
609 details={"reason": reason},
610 )
613class DeviceError(ModelError):
614 """Raised when there's a device-related error."""
616 def __init__(self, model_name: str, device: str, reason: str) -> None:
617 super().__init__(
618 f"Device error for model '{model_name}' on {device}: {reason}",
619 model_name=model_name,
620 code=ErrorCode.MODEL_DEVICE_ERROR,
621 details={"device": device, "reason": reason},
622 )
625# Rate Limit Errors
628class RateLimitError(SentimatrixError):
629 """Raised when rate limit is exceeded."""
631 def __init__(
632 self,
633 message: str,
634 provider: str,
635 retry_after: Optional[int] = None,
636 code: ErrorCode = ErrorCode.RATE_LIMIT_EXCEEDED,
637 details: Optional[Dict[str, Any]] = None,
638 ) -> None:
639 details = details or {}
640 details["provider"] = provider
641 if retry_after:
642 details["retry_after_seconds"] = retry_after
643 super().__init__(message, code, details)
644 self.provider = provider
645 self.retry_after = retry_after
648class QuotaExceededError(RateLimitError):
649 """Raised when API quota is exceeded."""
651 def __init__(self, provider: str, quota_type: str = "requests") -> None:
652 super().__init__(
653 f"API quota exceeded for {provider}: {quota_type}",
654 provider=provider,
655 code=ErrorCode.QUOTA_EXCEEDED,
656 details={"quota_type": quota_type},
657 )
660# Timeout Errors
663class TimeoutError(SentimatrixError):
664 """Raised when an operation times out."""
666 def __init__(
667 self,
668 message: str,
669 timeout: int,
670 operation: str,
671 code: ErrorCode = ErrorCode.REQUEST_TIMEOUT,
672 details: Optional[Dict[str, Any]] = None,
673 ) -> None:
674 details = details or {}
675 details["timeout_seconds"] = timeout
676 details["operation"] = operation
677 super().__init__(message, code, details)
678 self.timeout = timeout
679 self.operation = operation
682class ConnectionTimeoutError(TimeoutError):
683 """Raised when connection times out."""
685 def __init__(self, host: str, timeout: int) -> None:
686 super().__init__(
687 f"Connection timeout to {host} after {timeout}s",
688 timeout=timeout,
689 operation="connection",
690 code=ErrorCode.CONNECTION_TIMEOUT,
691 details={"host": host},
692 )
695class ReadTimeoutError(TimeoutError):
696 """Raised when read operation times out."""
698 def __init__(self, url: str, timeout: int) -> None:
699 super().__init__(
700 f"Read timeout from {url} after {timeout}s",
701 timeout=timeout,
702 operation="read",
703 code=ErrorCode.READ_TIMEOUT,
704 details={"url": url},
705 )
708# Cache Errors
711class CacheError(SentimatrixError):
712 """Base class for cache errors."""
714 def __init__(
715 self,
716 message: str,
717 backend: str,
718 code: ErrorCode = ErrorCode.CACHE_CONNECTION_ERROR,
719 details: Optional[Dict[str, Any]] = None,
720 original_error: Optional[Exception] = None,
721 ) -> None:
722 details = details or {}
723 details["backend"] = backend
724 super().__init__(message, code, details, original_error)
725 self.backend = backend
728class CacheConnectionError(CacheError):
729 """Raised when cache connection fails."""
731 def __init__(self, backend: str, reason: str) -> None:
732 super().__init__(
733 f"Failed to connect to {backend} cache: {reason}",
734 backend=backend,
735 code=ErrorCode.CACHE_CONNECTION_ERROR,
736 details={"reason": reason},
737 )
740class CacheReadError(CacheError):
741 """Raised when cache read fails."""
743 def __init__(self, backend: str, key: str, reason: str) -> None:
744 super().__init__(
745 f"Failed to read from {backend} cache: {reason}",
746 backend=backend,
747 code=ErrorCode.CACHE_READ_ERROR,
748 details={"key": key, "reason": reason},
749 )
752class CacheWriteError(CacheError):
753 """Raised when cache write fails."""
755 def __init__(self, backend: str, key: str, reason: str) -> None:
756 super().__init__(
757 f"Failed to write to {backend} cache: {reason}",
758 backend=backend,
759 code=ErrorCode.CACHE_WRITE_ERROR,
760 details={"key": key, "reason": reason},
761 )
764class CacheSerializationError(CacheError):
765 """Raised when cache serialization fails."""
767 def __init__(self, backend: str, operation: str, reason: str) -> None:
768 super().__init__(
769 f"Cache serialization error during {operation}: {reason}",
770 backend=backend,
771 code=ErrorCode.CACHE_SERIALIZATION_ERROR,
772 details={"operation": operation, "reason": reason},
773 )
776# Pipeline Errors
779class PipelineError(SentimatrixError):
780 """Base class for pipeline errors."""
782 def __init__(
783 self,
784 message: str,
785 pipeline_name: Optional[str] = None,
786 step_name: Optional[str] = None,
787 code: ErrorCode = ErrorCode.PIPELINE_EXECUTION_ERROR,
788 details: Optional[Dict[str, Any]] = None,
789 original_error: Optional[Exception] = None,
790 ) -> None:
791 details = details or {}
792 if pipeline_name:
793 details["pipeline_name"] = pipeline_name
794 if step_name:
795 details["step_name"] = step_name
796 super().__init__(message, code, details, original_error)
797 self.pipeline_name = pipeline_name
798 self.step_name = step_name
801class PipelineStepError(PipelineError):
802 """Raised when a pipeline step fails."""
804 def __init__(
805 self,
806 step_name: str,
807 reason: str,
808 pipeline_name: Optional[str] = None,
809 original_error: Optional[Exception] = None,
810 ) -> None:
811 super().__init__(
812 f"Pipeline step '{step_name}' failed: {reason}",
813 pipeline_name=pipeline_name,
814 step_name=step_name,
815 code=ErrorCode.PIPELINE_STEP_FAILED,
816 details={"reason": reason},
817 original_error=original_error,
818 )
821class PipelineStateError(PipelineError):
822 """Raised when pipeline is in an invalid state."""
824 def __init__(self, pipeline_name: str, state: str, expected: str) -> None:
825 super().__init__(
826 f"Pipeline '{pipeline_name}' in invalid state: {state} (expected: {expected})",
827 pipeline_name=pipeline_name,
828 code=ErrorCode.PIPELINE_INVALID_STATE,
829 details={"current_state": state, "expected_state": expected},
830 )