from __future__ import annotations from yuxi.channel.errors import ErrorSeverity from yuxi.channel.protocols import ClassifiedError, ErrorHandlingProtocol MAX_RETRIES = 3 def classify_error( status_code: int | None, response_body: dict | str | None ) -> tuple[ErrorSeverity, str, float | None]: description = "" if isinstance(response_body, dict): errors = response_body.get("errorMessages", response_body.get("errors", {})) if isinstance(errors, list) and errors: description = errors[0] elif isinstance(errors, dict) and errors: description = list(errors.values())[0] elif isinstance(response_body, str): description = response_body if status_code == 429: return ErrorSeverity.RATE_LIMITED, description, 30.0 if status_code == 401: return ErrorSeverity.FORBIDDEN, description, None if status_code == 403: return ErrorSeverity.FORBIDDEN, description, None if status_code == 404: return ErrorSeverity.FATAL, description, None if status_code == 400: return ErrorSeverity.FATAL, description, None if status_code and status_code >= 500: return ErrorSeverity.RETRYABLE, description, None if status_code is None: return ErrorSeverity.NETWORK, description, None return ErrorSeverity.FATAL, description, None class JiraErrorHandler(ErrorHandlingProtocol): def classify_error(self, error: BaseException) -> ClassifiedError: import httpx if isinstance(error, httpx.HTTPStatusError): status_code = error.response.status_code body = error.response.json() if error.response.content else {} severity, desc, retry_after = classify_error(status_code, body) return ClassifiedError( severity=severity, retry_after_ms=int(retry_after * 1000) if retry_after else 0, original_error=error, error_message=desc, ) return ClassifiedError( severity=ErrorSeverity.NETWORK, retry_after_ms=0, original_error=error, error_message=str(error), ) def is_retryable(self, error: BaseException) -> bool: classified = self.classify_error(error) return classified.severity in (ErrorSeverity.RETRYABLE, ErrorSeverity.RATE_LIMITED, ErrorSeverity.NETWORK) def should_backoff(self, error: BaseException, attempt: int) -> int: if not self.is_retryable(error): return 0 if attempt >= MAX_RETRIES: return 0 return min(2**attempt * 1000, 10000)