o2o-castad-backend/poc/instagram1-difi/models.py

498 lines
12 KiB
Python

"""
Instagram Graph API Pydantic 모델 모듈
API 요청/응답에 사용되는 데이터 모델을 정의합니다.
"""
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Optional
from pydantic import BaseModel, Field
# ==========================================================================
# 공통 모델
# ==========================================================================
class Paging(BaseModel):
"""
페이징 정보
Instagram API의 커서 기반 페이지네이션 정보입니다.
"""
cursors: Optional[dict[str, str]] = Field(
default=None,
description="페이징 커서 (before, after)",
)
next: Optional[str] = Field(
default=None,
description="다음 페이지 URL",
)
previous: Optional[str] = Field(
default=None,
description="이전 페이지 URL",
)
# ==========================================================================
# 인증 모델
# ==========================================================================
class TokenInfo(BaseModel):
"""
토큰 정보
액세스 토큰 교환/갱신 응답에 사용됩니다.
"""
access_token: str = Field(
...,
description="액세스 토큰",
)
token_type: str = Field(
default="bearer",
description="토큰 타입",
)
expires_in: int = Field(
...,
description="토큰 만료 시간 (초)",
)
class TokenDebugData(BaseModel):
"""
토큰 디버그 정보
토큰의 상세 정보를 담고 있습니다.
"""
app_id: str = Field(
...,
description="앱 ID",
)
type: str = Field(
...,
description="토큰 타입 (USER 등)",
)
application: str = Field(
...,
description="앱 이름",
)
expires_at: int = Field(
...,
description="토큰 만료 시각 (Unix timestamp)",
)
is_valid: bool = Field(
...,
description="토큰 유효 여부",
)
scopes: list[str] = Field(
default_factory=list,
description="토큰에 부여된 권한 목록",
)
user_id: str = Field(
...,
description="사용자 ID",
)
data_access_expires_at: Optional[int] = Field(
default=None,
description="데이터 접근 만료 시각 (Unix timestamp)",
)
@property
def expires_at_datetime(self) -> datetime:
"""만료 시각을 UTC datetime으로 변환"""
return datetime.fromtimestamp(self.expires_at, tz=timezone.utc)
@property
def is_expired(self) -> bool:
"""토큰 만료 여부 확인 (UTC 기준)"""
return datetime.now(timezone.utc).timestamp() > self.expires_at
class TokenDebugResponse(BaseModel):
"""토큰 디버그 응답"""
data: TokenDebugData
# ==========================================================================
# 계정 모델
# ==========================================================================
class AccountType(str, Enum):
"""계정 타입"""
BUSINESS = "BUSINESS"
CREATOR = "CREATOR"
PERSONAL = "PERSONAL"
class Account(BaseModel):
"""
Instagram 비즈니스/크리에이터 계정 정보
계정의 기본 정보와 통계를 포함합니다.
"""
id: str = Field(
...,
description="계정 고유 ID",
)
username: str = Field(
...,
description="사용자명 (@username)",
)
name: Optional[str] = Field(
default=None,
description="계정 표시 이름",
)
account_type: Optional[str] = Field(
default=None,
description="계정 타입 (BUSINESS, CREATOR)",
)
profile_picture_url: Optional[str] = Field(
default=None,
description="프로필 사진 URL",
)
followers_count: int = Field(
default=0,
description="팔로워 수",
)
follows_count: int = Field(
default=0,
description="팔로잉 수",
)
media_count: int = Field(
default=0,
description="게시물 수",
)
biography: Optional[str] = Field(
default=None,
description="자기소개",
)
website: Optional[str] = Field(
default=None,
description="웹사이트 URL",
)
# ==========================================================================
# 미디어 모델
# ==========================================================================
class MediaType(str, Enum):
"""미디어 타입"""
IMAGE = "IMAGE"
VIDEO = "VIDEO"
CAROUSEL_ALBUM = "CAROUSEL_ALBUM"
REELS = "REELS"
class ContainerStatus(str, Enum):
"""미디어 컨테이너 상태"""
IN_PROGRESS = "IN_PROGRESS"
FINISHED = "FINISHED"
ERROR = "ERROR"
EXPIRED = "EXPIRED"
class Media(BaseModel):
"""
미디어 정보
이미지, 비디오, 캐러셀, 릴스 등의 미디어 정보를 담습니다.
"""
id: str = Field(
...,
description="미디어 고유 ID",
)
media_type: Optional[MediaType] = Field(
default=None,
description="미디어 타입",
)
media_url: Optional[str] = Field(
default=None,
description="미디어 URL",
)
thumbnail_url: Optional[str] = Field(
default=None,
description="썸네일 URL (비디오용)",
)
caption: Optional[str] = Field(
default=None,
description="캡션 텍스트",
)
timestamp: Optional[datetime] = Field(
default=None,
description="게시 시각",
)
permalink: Optional[str] = Field(
default=None,
description="게시물 고유 링크",
)
like_count: int = Field(
default=0,
description="좋아요 수",
)
comments_count: int = Field(
default=0,
description="댓글 수",
)
children: Optional[list["Media"]] = Field(
default=None,
description="캐러셀 하위 미디어 목록",
)
model_config = {
"json_schema_extra": {
"example": {
"id": "17880000000000000",
"media_type": "IMAGE",
"media_url": "https://example.com/image.jpg",
"caption": "My awesome photo",
"timestamp": "2024-01-01T00:00:00+00:00",
"permalink": "https://www.instagram.com/p/ABC123/",
"like_count": 100,
"comments_count": 10,
}
}
}
class MediaContainer(BaseModel):
"""
미디어 컨테이너 (게시 전 상태)
이미지/비디오 게시 시 생성되는 컨테이너의 상태 정보입니다.
"""
id: str = Field(
...,
description="컨테이너 ID",
)
status_code: Optional[str] = Field(
default=None,
description="상태 코드 (IN_PROGRESS, FINISHED, ERROR)",
)
status: Optional[str] = Field(
default=None,
description="상태 상세 메시지",
)
@property
def is_finished(self) -> bool:
"""컨테이너가 완료 상태인지 확인"""
return self.status_code == ContainerStatus.FINISHED.value
@property
def is_error(self) -> bool:
"""컨테이너가 에러 상태인지 확인"""
return self.status_code == ContainerStatus.ERROR.value
@property
def is_in_progress(self) -> bool:
"""컨테이너가 처리 중인지 확인"""
return self.status_code == ContainerStatus.IN_PROGRESS.value
class MediaList(BaseModel):
"""미디어 목록 응답"""
data: list[Media] = Field(
default_factory=list,
description="미디어 목록",
)
paging: Optional[Paging] = Field(
default=None,
description="페이징 정보",
)
# ==========================================================================
# 인사이트 모델
# ==========================================================================
class InsightValue(BaseModel):
"""
인사이트 값
개별 메트릭의 값을 담습니다.
"""
value: Any = Field(
...,
description="메트릭 값 (숫자 또는 딕셔너리)",
)
end_time: Optional[datetime] = Field(
default=None,
description="측정 종료 시각",
)
class Insight(BaseModel):
"""
인사이트 정보
계정 또는 미디어의 성과 메트릭 정보입니다.
"""
name: str = Field(
...,
description="메트릭 이름",
)
period: str = Field(
...,
description="기간 (day, week, days_28, lifetime)",
)
values: list[InsightValue] = Field(
default_factory=list,
description="메트릭 값 목록",
)
title: str = Field(
...,
description="메트릭 제목",
)
description: Optional[str] = Field(
default=None,
description="메트릭 설명",
)
id: str = Field(
...,
description="인사이트 ID",
)
@property
def latest_value(self) -> Any:
"""최신 값 반환"""
if self.values:
return self.values[-1].value
return None
class InsightResponse(BaseModel):
"""인사이트 응답"""
data: list[Insight] = Field(
default_factory=list,
description="인사이트 목록",
)
def get_metric(self, name: str) -> Optional[Insight]:
"""메트릭 이름으로 인사이트 조회"""
for insight in self.data:
if insight.name == name:
return insight
return None
# ==========================================================================
# 댓글 모델
# ==========================================================================
class Comment(BaseModel):
"""
댓글 정보
미디어에 달린 댓글 또는 답글 정보입니다.
"""
id: str = Field(
...,
description="댓글 고유 ID",
)
text: str = Field(
...,
description="댓글 내용",
)
username: Optional[str] = Field(
default=None,
description="작성자 사용자명",
)
timestamp: Optional[datetime] = Field(
default=None,
description="작성 시각",
)
like_count: int = Field(
default=0,
description="좋아요 수",
)
replies: Optional["CommentList"] = Field(
default=None,
description="답글 목록",
)
class CommentList(BaseModel):
"""댓글 목록 응답"""
data: list[Comment] = Field(
default_factory=list,
description="댓글 목록",
)
paging: Optional[Paging] = Field(
default=None,
description="페이징 정보",
)
# ==========================================================================
# 에러 응답 모델
# ==========================================================================
class APIError(BaseModel):
"""
Instagram API 에러 응답
API에서 반환하는 에러 정보입니다.
"""
message: str = Field(
...,
description="에러 메시지",
)
type: str = Field(
...,
description="에러 타입",
)
code: int = Field(
...,
description="에러 코드",
)
error_subcode: Optional[int] = Field(
default=None,
description="에러 서브코드",
)
fbtrace_id: Optional[str] = Field(
default=None,
description="Facebook 트레이스 ID",
)
class ErrorResponse(BaseModel):
"""에러 응답 래퍼"""
error: APIError
# ==========================================================================
# 모델 업데이트 (순환 참조 해결)
# ==========================================================================
# Pydantic v2에서 순환 참조를 위한 모델 재빌드
Media.model_rebuild()
Comment.model_rebuild()
CommentList.model_rebuild()