import logging from collections.abc import Iterator from urllib.parse import urljoin from fansifter_common.httpclient.base import HTTPClient from pydantic import TypeAdapter from app.app_types import PlainToken from app.exceptions import TikTokException from app.models import CustomAudience from app.utils import chunks from .models import AudiencesDeleteResponse, AudiencesListResponse logger = logging.getLogger(__name__) CustomAudienceList = TypeAdapter(list[CustomAudience]) class TiktokClient(HTTPClient): base_url = "https://business-api.tiktok.com" api_version = "open_api/v1.3" DEFAULT_REQUEST_TIMEOUT = 10 DEFAULT_LIMIT = 100 def __init__(self, default_timeout: int | None = None) -> None: super().__init__( client_options={"timeout": default_timeout or self.DEFAULT_REQUEST_TIMEOUT} ) def build_url(self, path: str) -> str: if not path.startswith("/"): path = f"/{path}" return urljoin(self.base_url, f"{self.api_version}{path}") def get_audiences( self, ad_account: str, user_access_token: PlainToken ) -> list[CustomAudience]: return list(self.iter_audiences(ad_account, user_access_token)) def iter_audiences( self, ad_account: str, user_access_token: PlainToken, page_number: int = 1 ) -> Iterator[CustomAudience]: response_obj = self.request( "GET", self.build_url("/dmp/custom_audience/list/"), type=AudiencesListResponse[CustomAudience], headers={"Access-Token": user_access_token}, params={ "advertiser_id": ad_account, "page_size": self.DEFAULT_LIMIT, "page": page_number, }, ) if response_obj.code != 0: raise TikTokException(response_obj) yield from response_obj.data.audiences if response_obj.data.page_info.total_page > page_number: yield from self.iter_audiences( ad_account=ad_account, user_access_token=user_access_token, page_number=page_number + 1, ) def delete_audiences( self, ad_account: str, audiences: list[CustomAudience], user_access_token: PlainToken, ) -> None: headers = {"Access-Token": user_access_token} for audiences_chunk in chunks(audiences, chunk_size=100): params = { "advertiser_id": ad_account, "custom_audience_ids": [audience.id for audience in audiences_chunk], } response_obj = self.request( "POST", self.build_url("/dmp/custom_audience/delete/"), type=AudiencesDeleteResponse, headers=headers, json=params, ) if response_obj.code != 0: raise TikTokException(response_obj)