from __future__ import annotations import requests class ProcessingError(Exception): """Error de procesamiento con un codigo corto y rastreable. El atributo ``code`` es un identificador estable en MAYUSCULAS que se antepone al mensaje del job (p.ej. ``[AUTH_MISTRAL] API Key invalida``) para que el usuario pueda buscar y diagnosticar rapidamente el tipo de fallo desde la UI. """ def __init__(self, code: str, detail: str) -> None: super().__init__(f"[{code}] {detail}") self.code = code self.detail = detail @property def labeled(self) -> str: return f"[{self.code}] {self.detail}" class MistralAuthError(ProcessingError): def __init__(self, detail: str = "API Key de Mistral invalida o no autorizada (401)") -> None: super().__init__("AUTH_MISTRAL", detail) class MistralRateLimitError(ProcessingError): def __init__(self, detail: str = "Limite de uso de Mistral alcanzado (429)") -> None: super().__init__("RATE_LIMIT", detail) class MistralServerError(ProcessingError): def __init__(self, status: int, body: str) -> None: super().__init__("MISTRAL_SERVER", f"Mistral HTTP {status}: {body[:200]}") class MistralBatchError(ProcessingError): def __init__(self, detail: str) -> None: super().__init__("BATCH", detail) class MistralOCREmptyError(ProcessingError): def __init__(self, detail: str = "OCR no devolvio contenido") -> None: super().__init__("OCR_EMPTY", detail) class NetworkError(ProcessingError): def __init__(self, detail: str) -> None: super().__init__("NETWORK", detail) NON_RETRYABLE_CODES = { "AUTH_MISTRAL", "CONFIG", "OCR_EMPTY", "MISTRAL_CLIENT", "BAD_PDF", "FILE_IO", } def classify_requests_error(exc: requests.RequestException) -> ProcessingError: if isinstance(exc, requests.Timeout): return NetworkError(f"timeout de red con Mistral: {exc}") if isinstance(exc, requests.ConnectionError): return NetworkError(f"sin conexion a Mistral: {exc}") return NetworkError(f"error de red con Mistral: {exc}") def classify_unknown_error(exc: Exception) -> str: """Etiqueta breve para excepciones no envueltas en ProcessingError.""" name = type(exc).__name__.lower() message = str(exc).lower() if "fitz" in name or "pdf" in name or "pdf" in message or "muPDF" in message: return "PDF_PARSE" if isinstance(exc, FileNotFoundError): return "FILE_IO" if isinstance(exc, (PermissionError, OSError)): return "FILE_IO" if isinstance(exc, ValueError): return "VALIDATION" return "UNKNOWN"