from __future__ import annotations from enum import StrEnum class TwitterErrorKind(StrEnum): RETRYABLE = "retryable" RATE_LIMITED = "rate_limited" AUTH = "auth" NOT_AUTHORIZED_DM = "not_authorized_dm" DUPLICATE = "duplicate" MESSAGE_TOO_LONG = "message_too_long" MEDIA_TOO_LARGE = "media_too_large" MEDIA_INVALID = "media_invalid" ACCOUNT_LOCKED = "account_locked" FORBIDDEN = "forbidden" NOT_FOUND = "not_found" FATAL = "fatal" NETWORK = "network" def classify_error( status_code: int | None, error_body: dict | str | None, ) -> tuple[TwitterErrorKind, str, float | None]: description = "" if isinstance(error_body, dict): description = error_body.get("detail", error_body.get("title", "")) errors_list = error_body.get("errors", []) if errors_list: description = errors_list[0].get("message", description) elif isinstance(error_body, str): description = error_body retry_after = None if isinstance(error_body, dict): retry_after = error_body.get("retry_after") if status_code == 429: return ( TwitterErrorKind.RATE_LIMITED, description, float(retry_after) if retry_after else 180.0, ) if status_code == 401: return TwitterErrorKind.AUTH, description, None if status_code == 403: if "not authorized to send" in description.lower(): return TwitterErrorKind.NOT_AUTHORIZED_DM, description, None if "duplicate" in description.lower(): return TwitterErrorKind.DUPLICATE, description, None return TwitterErrorKind.FORBIDDEN, description, None if status_code == 400: if "too long" in description.lower() or "length" in description.lower(): return TwitterErrorKind.MESSAGE_TOO_LONG, description, None if "media" in description.lower(): return TwitterErrorKind.MEDIA_INVALID, description, None if "duplicate" in description.lower(): return TwitterErrorKind.DUPLICATE, description, None return TwitterErrorKind.FATAL, description, None if status_code == 404: return TwitterErrorKind.NOT_FOUND, description, None if status_code and status_code >= 500: return TwitterErrorKind.RETRYABLE, description, None if status_code is None: return TwitterErrorKind.NETWORK, description, None return TwitterErrorKind.FATAL, description, None def is_retryable(kind: TwitterErrorKind) -> bool: return kind in ( TwitterErrorKind.RETRYABLE, TwitterErrorKind.RATE_LIMITED, TwitterErrorKind.NETWORK, ) MAX_RETRIES = 3