498 lines
12 KiB
Python
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()
|