258 lines
7.2 KiB
Python
258 lines
7.2 KiB
Python
"""
|
|
Instagram Graph API 커스텀 예외 모듈
|
|
|
|
Instagram API 에러 코드에 맞는 계층화된 예외 클래스를 정의합니다.
|
|
"""
|
|
|
|
from typing import Optional
|
|
|
|
|
|
class InstagramAPIError(Exception):
|
|
"""
|
|
Instagram API 기본 예외
|
|
|
|
모든 Instagram API 관련 예외의 기본 클래스입니다.
|
|
|
|
Attributes:
|
|
message: 에러 메시지
|
|
code: Instagram API 에러 코드
|
|
subcode: Instagram API 에러 서브코드
|
|
fbtrace_id: Facebook 트레이스 ID (디버깅용)
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
code: Optional[int] = None,
|
|
subcode: Optional[int] = None,
|
|
fbtrace_id: Optional[str] = None,
|
|
):
|
|
self.message = message
|
|
self.code = code
|
|
self.subcode = subcode
|
|
self.fbtrace_id = fbtrace_id
|
|
super().__init__(self.message)
|
|
|
|
def __str__(self) -> str:
|
|
parts = [self.message]
|
|
if self.code is not None:
|
|
parts.append(f"code={self.code}")
|
|
if self.subcode is not None:
|
|
parts.append(f"subcode={self.subcode}")
|
|
if self.fbtrace_id:
|
|
parts.append(f"fbtrace_id={self.fbtrace_id}")
|
|
return " | ".join(parts)
|
|
|
|
def __repr__(self) -> str:
|
|
return (
|
|
f"{self.__class__.__name__}("
|
|
f"message={self.message!r}, "
|
|
f"code={self.code}, "
|
|
f"subcode={self.subcode}, "
|
|
f"fbtrace_id={self.fbtrace_id!r})"
|
|
)
|
|
|
|
|
|
class AuthenticationError(InstagramAPIError):
|
|
"""
|
|
인증 관련 에러
|
|
|
|
토큰이 만료되었거나, 유효하지 않거나, 앱 권한이 없는 경우 발생합니다.
|
|
|
|
관련 에러 코드:
|
|
- code=190, subcode=458: 앱에 권한 없음
|
|
- code=190, subcode=463: 토큰 만료
|
|
- code=190, subcode=467: 유효하지 않은 토큰
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class RateLimitError(InstagramAPIError):
|
|
"""
|
|
Rate Limit 초과 에러
|
|
|
|
시간당 API 호출 제한(200회/시간/사용자)을 초과한 경우 발생합니다.
|
|
HTTP 429 응답 또는 API 에러 코드 4와 함께 발생합니다.
|
|
|
|
Attributes:
|
|
retry_after: 재시도까지 대기해야 하는 시간 (초)
|
|
|
|
관련 에러 코드:
|
|
- code=4: Rate limit 초과
|
|
- code=17: User request limit reached
|
|
- code=341: Application request limit reached
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
retry_after: Optional[int] = None,
|
|
code: Optional[int] = 4,
|
|
subcode: Optional[int] = None,
|
|
fbtrace_id: Optional[str] = None,
|
|
):
|
|
super().__init__(message, code, subcode, fbtrace_id)
|
|
self.retry_after = retry_after
|
|
|
|
def __str__(self) -> str:
|
|
base = super().__str__()
|
|
if self.retry_after is not None:
|
|
return f"{base} | retry_after={self.retry_after}s"
|
|
return base
|
|
|
|
|
|
class InstagramPermissionError(InstagramAPIError):
|
|
"""
|
|
권한 부족 에러
|
|
|
|
요청한 작업을 수행할 권한이 없는 경우 발생합니다.
|
|
Python 내장 PermissionError와 구분하기 위해 접두사를 사용합니다.
|
|
|
|
관련 에러 코드:
|
|
- code=10: 권한 거부됨
|
|
- code=200: 비즈니스 계정 필요
|
|
- code=230: 이 권한에 대한 앱 검토 필요
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
# 하위 호환성을 위한 alias (deprecated - InstagramPermissionError 사용 권장)
|
|
PermissionError = InstagramPermissionError
|
|
|
|
|
|
class MediaPublishError(InstagramAPIError):
|
|
"""
|
|
미디어 게시 실패 에러
|
|
|
|
이미지/비디오 게시 과정에서 발생하는 에러입니다.
|
|
|
|
발생 원인:
|
|
- 이미지/비디오 URL 접근 불가
|
|
- 지원하지 않는 미디어 포맷
|
|
- 컨테이너 생성 실패
|
|
- 게시 타임아웃
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class InvalidRequestError(InstagramAPIError):
|
|
"""
|
|
잘못된 요청 에러
|
|
|
|
요청 파라미터가 잘못되었거나 필수 값이 누락된 경우 발생합니다.
|
|
|
|
관련 에러 코드:
|
|
- code=100: 유효하지 않은 파라미터
|
|
- code=21009: 지원하지 않는 POST 요청
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class ResourceNotFoundError(InstagramAPIError):
|
|
"""
|
|
리소스를 찾을 수 없음 에러
|
|
|
|
요청한 미디어, 댓글, 계정 등이 존재하지 않는 경우 발생합니다.
|
|
|
|
관련 에러 코드:
|
|
- code=803: 객체가 존재하지 않음
|
|
- code=100 + subcode=33: 객체가 존재하지 않음 (다른 형태)
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class ContainerStatusError(InstagramAPIError):
|
|
"""
|
|
컨테이너 상태 에러
|
|
|
|
미디어 컨테이너가 ERROR 상태가 되었을 때 발생합니다.
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class ContainerTimeoutError(InstagramAPIError):
|
|
"""
|
|
컨테이너 타임아웃 에러
|
|
|
|
미디어 컨테이너가 지정된 시간 내에 FINISHED 상태가 되지 않은 경우 발생합니다.
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
# ==========================================================================
|
|
# 에러 코드 → 예외 클래스 매핑
|
|
# ==========================================================================
|
|
|
|
ERROR_CODE_MAPPING: dict[int, type[InstagramAPIError]] = {
|
|
4: RateLimitError, # Rate limit
|
|
10: InstagramPermissionError, # Permission denied
|
|
17: RateLimitError, # User request limit
|
|
100: InvalidRequestError, # Invalid parameter
|
|
190: AuthenticationError, # Invalid OAuth access token
|
|
200: InstagramPermissionError, # Requires business account
|
|
230: InstagramPermissionError, # App review required
|
|
341: RateLimitError, # Application request limit
|
|
803: ResourceNotFoundError, # Object does not exist
|
|
}
|
|
|
|
# (code, subcode) 세부 매핑 - 더 정확한 예외 분류
|
|
ERROR_CODE_SUBCODE_MAPPING: dict[tuple[int, int], type[InstagramAPIError]] = {
|
|
(100, 33): ResourceNotFoundError, # Object does not exist
|
|
(190, 458): AuthenticationError, # App not authorized
|
|
(190, 463): AuthenticationError, # Token expired
|
|
(190, 467): AuthenticationError, # Invalid token
|
|
}
|
|
|
|
|
|
def create_exception_from_error(
|
|
message: str,
|
|
code: Optional[int] = None,
|
|
subcode: Optional[int] = None,
|
|
fbtrace_id: Optional[str] = None,
|
|
) -> InstagramAPIError:
|
|
"""
|
|
API 에러 응답에서 적절한 예외 객체 생성
|
|
|
|
(code, subcode) 조합을 먼저 확인하고, 없으면 code만으로 매핑합니다.
|
|
|
|
Args:
|
|
message: 에러 메시지
|
|
code: API 에러 코드
|
|
subcode: API 에러 서브코드
|
|
fbtrace_id: Facebook 트레이스 ID
|
|
|
|
Returns:
|
|
적절한 예외 클래스의 인스턴스
|
|
"""
|
|
exception_class = InstagramAPIError
|
|
|
|
# 먼저 (code, subcode) 조합으로 정확한 매핑 시도
|
|
if code is not None and subcode is not None:
|
|
key = (code, subcode)
|
|
if key in ERROR_CODE_SUBCODE_MAPPING:
|
|
exception_class = ERROR_CODE_SUBCODE_MAPPING[key]
|
|
return exception_class(
|
|
message=message,
|
|
code=code,
|
|
subcode=subcode,
|
|
fbtrace_id=fbtrace_id,
|
|
)
|
|
|
|
# 기본 코드 매핑
|
|
if code is not None:
|
|
exception_class = ERROR_CODE_MAPPING.get(code, InstagramAPIError)
|
|
|
|
return exception_class(
|
|
message=message,
|
|
code=code,
|
|
subcode=subcode,
|
|
fbtrace_id=fbtrace_id,
|
|
)
|