86 lines
2.6 KiB
Python
86 lines
2.6 KiB
Python
|
|
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
|