""" 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()