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

1""" 

2Sentimatrix Exception Hierarchy 

3 

4Defines a comprehensive exception hierarchy for error handling throughout 

5the application. All exceptions inherit from SentimatrixError. 

6 

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""" 

27 

28from __future__ import annotations 

29 

30from enum import IntEnum 

31from typing import Any, Dict, Optional 

32 

33 

34class ErrorCode(IntEnum): 

35 """Standardized error codes for Sentimatrix errors.""" 

36 

37 # General errors (1000-1099) 

38 UNKNOWN = 1000 

39 INTERNAL = 1001 

40 NOT_IMPLEMENTED = 1002 

41 

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 

47 

48 # Validation errors (1200-1299) 

49 VALIDATION_FAILED = 1200 

50 INVALID_INPUT = 1201 

51 INVALID_FORMAT = 1202 

52 MISSING_REQUIRED_FIELD = 1203 

53 

54 # Provider errors (1300-1399) 

55 PROVIDER_NOT_FOUND = 1300 

56 PROVIDER_NOT_AVAILABLE = 1301 

57 PROVIDER_INITIALIZATION_FAILED = 1302 

58 

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 

66 

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 

74 

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 

80 

81 # Rate limit errors (1700-1799) 

82 RATE_LIMIT_EXCEEDED = 1700 

83 QUOTA_EXCEEDED = 1701 

84 CONCURRENT_LIMIT_EXCEEDED = 1702 

85 

86 # Timeout errors (1800-1899) 

87 REQUEST_TIMEOUT = 1800 

88 CONNECTION_TIMEOUT = 1801 

89 READ_TIMEOUT = 1802 

90 

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 

96 

97 # Pipeline errors (2000-2099) 

98 PIPELINE_EXECUTION_ERROR = 2000 

99 PIPELINE_STEP_FAILED = 2001 

100 PIPELINE_INVALID_STATE = 2002 

101 

102 

103class SentimatrixError(Exception): 

104 """ 

105 Base exception for all Sentimatrix errors. 

106 

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 """ 

113 

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. 

123 

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 

135 

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 

144 

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 ) 

153 

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 } 

164 

165 

166# Configuration Errors 

167 

168 

169class ConfigurationError(SentimatrixError): 

170 """Raised when there's an error in configuration.""" 

171 

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) 

180 

181 

182class ConfigNotFoundError(ConfigurationError): 

183 """Raised when configuration file is not found.""" 

184 

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 ) 

191 

192 

193class ConfigValidationError(ConfigurationError): 

194 """Raised when configuration validation fails.""" 

195 

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 ) 

202 

203 

204# Validation Errors 

205 

206 

207class ValidationError(SentimatrixError): 

208 """Raised when input validation fails.""" 

209 

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) 

218 

219 

220class InvalidInputError(ValidationError): 

221 """Raised when input data is invalid.""" 

222 

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 ) 

229 

230 

231class MissingRequiredFieldError(ValidationError): 

232 """Raised when a required field is missing.""" 

233 

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 ) 

240 

241 

242# Provider Errors 

243 

244 

245class ProviderError(SentimatrixError): 

246 """Base class for provider-related errors.""" 

247 

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 

260 

261 

262class ProviderNotFoundError(ProviderError): 

263 """Raised when requested provider is not found.""" 

264 

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 ) 

271 

272 

273class ProviderInitializationError(ProviderError): 

274 """Raised when provider initialization fails.""" 

275 

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 ) 

283 

284 

285# LLM Provider Errors 

286 

287 

288class LLMProviderError(ProviderError): 

289 """Base class for LLM provider errors.""" 

290 

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 

305 

306 

307class AuthenticationError(LLMProviderError): 

308 """Raised when API authentication fails.""" 

309 

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 ) 

316 

317 

318class InvalidModelError(LLMProviderError): 

319 """Raised when specified model is invalid or unavailable.""" 

320 

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 ) 

328 

329 

330class ContentFilteredError(LLMProviderError): 

331 """Raised when content is filtered by provider's safety systems.""" 

332 

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 ) 

343 

344 

345class TokenLimitExceededError(LLMProviderError): 

346 """Raised when token limit is exceeded.""" 

347 

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 ) 

358 

359 

360class InvalidResponseError(LLMProviderError): 

361 """Raised when LLM response is invalid or malformed.""" 

362 

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 ) 

370 

371 

372# Provider-specific LLM errors 

373 

374 

375class OpenAIError(LLMProviderError): 

376 """OpenAI-specific error.""" 

377 

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) 

386 

387 

388class AnthropicError(LLMProviderError): 

389 """Anthropic-specific error.""" 

390 

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) 

399 

400 

401class GroqError(LLMProviderError): 

402 """Groq-specific error.""" 

403 

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) 

412 

413 

414class GeminiError(LLMProviderError): 

415 """Google Gemini-specific error.""" 

416 

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) 

425 

426 

427class OllamaError(LLMProviderError): 

428 """Ollama-specific error.""" 

429 

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) 

438 

439 

440# Scraper Errors 

441 

442 

443class ScraperError(ProviderError): 

444 """Base class for scraper errors.""" 

445 

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 

460 

461 

462class ScraperConnectionError(ScraperError): 

463 """Raised when scraper fails to connect.""" 

464 

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 ) 

473 

474 

475class ScraperTimeoutError(ScraperError): 

476 """Raised when scraper operation times out.""" 

477 

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 ) 

486 

487 

488class ScraperBlockedError(ScraperError): 

489 """Raised when scraper is blocked by the target site.""" 

490 

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 ) 

502 

503 

504class CaptchaDetectedError(ScraperError): 

505 """Raised when CAPTCHA is detected.""" 

506 

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 ) 

514 

515 

516class ScraperParseError(ScraperError): 

517 """Raised when content parsing fails.""" 

518 

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 ) 

527 

528 

529# Provider-specific scraper errors 

530 

531 

532class PlaywrightError(ScraperError): 

533 """Playwright-specific error.""" 

534 

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) 

543 

544 

545class SeleniumError(ScraperError): 

546 """Selenium-specific error.""" 

547 

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) 

556 

557 

558# Model Errors 

559 

560 

561class ModelError(ProviderError): 

562 """Base class for ML model errors.""" 

563 

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 

576 

577 

578class ModelNotFoundError(ModelError): 

579 """Raised when model cannot be found or loaded.""" 

580 

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 ) 

587 

588 

589class ModelLoadError(ModelError): 

590 """Raised when model fails to load.""" 

591 

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 ) 

599 

600 

601class ModelInferenceError(ModelError): 

602 """Raised when model inference fails.""" 

603 

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 ) 

611 

612 

613class DeviceError(ModelError): 

614 """Raised when there's a device-related error.""" 

615 

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 ) 

623 

624 

625# Rate Limit Errors 

626 

627 

628class RateLimitError(SentimatrixError): 

629 """Raised when rate limit is exceeded.""" 

630 

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 

646 

647 

648class QuotaExceededError(RateLimitError): 

649 """Raised when API quota is exceeded.""" 

650 

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 ) 

658 

659 

660# Timeout Errors 

661 

662 

663class TimeoutError(SentimatrixError): 

664 """Raised when an operation times out.""" 

665 

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 

680 

681 

682class ConnectionTimeoutError(TimeoutError): 

683 """Raised when connection times out.""" 

684 

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 ) 

693 

694 

695class ReadTimeoutError(TimeoutError): 

696 """Raised when read operation times out.""" 

697 

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 ) 

706 

707 

708# Cache Errors 

709 

710 

711class CacheError(SentimatrixError): 

712 """Base class for cache errors.""" 

713 

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 

726 

727 

728class CacheConnectionError(CacheError): 

729 """Raised when cache connection fails.""" 

730 

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 ) 

738 

739 

740class CacheReadError(CacheError): 

741 """Raised when cache read fails.""" 

742 

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 ) 

750 

751 

752class CacheWriteError(CacheError): 

753 """Raised when cache write fails.""" 

754 

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 ) 

762 

763 

764class CacheSerializationError(CacheError): 

765 """Raised when cache serialization fails.""" 

766 

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 ) 

774 

775 

776# Pipeline Errors 

777 

778 

779class PipelineError(SentimatrixError): 

780 """Base class for pipeline errors.""" 

781 

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 

799 

800 

801class PipelineStepError(PipelineError): 

802 """Raised when a pipeline step fails.""" 

803 

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 ) 

819 

820 

821class PipelineStateError(PipelineError): 

822 """Raised when pipeline is in an invalid state.""" 

823 

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 )