amz_ad_client.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. import requests
  2. import json
  3. import time
  4. from cachetools import TTLCache
  5. from urllib.parse import urljoin
  6. from typing import List, Literal, Iterable, Iterator
  7. import gzip
  8. from pathlib import Path
  9. from logging import getLogger
  10. URL_AUTH = "https://api.amazon.com/auth/o2/token"
  11. URL_AD_API = "https://advertising-api.amazon.com"
  12. cache = TTLCache(maxsize=10, ttl=3200)
  13. logger = getLogger(__name__)
  14. def gz_decompress(file_path: str, chunk_size: int = 1024 * 1024):
  15. decompressed_file = file_path.rstrip(".gz")
  16. with open(decompressed_file, "wb") as pw:
  17. zf = gzip.open(file_path, mode='rb')
  18. while True:
  19. chunk = zf.read(size=chunk_size)
  20. if not chunk:
  21. break
  22. pw.write(chunk)
  23. return decompressed_file
  24. class BaseClient:
  25. def __init__(
  26. self, lwa_client_id: str, lwa_client_secret: str, refresh_token: str = None, profile_id: str = None,
  27. data_path: str = "./"
  28. ):
  29. self.lwa_client_id = lwa_client_id
  30. self.lwa_client_secret = lwa_client_secret
  31. self.refresh_token = refresh_token
  32. self.profile_id = profile_id
  33. self.data_path = Path(data_path)
  34. if not self.data_path.exists():
  35. self.data_path.mkdir(parents=True)
  36. @property
  37. def access_token(self) -> str:
  38. try:
  39. return cache[self.refresh_token]
  40. except KeyError:
  41. resp = requests.post(URL_AUTH, data={
  42. "grant_type": "refresh_token",
  43. "client_id": self.lwa_client_id,
  44. "refresh_token": self.refresh_token,
  45. "client_secret": self.lwa_client_secret,
  46. })
  47. if resp.status_code != 200:
  48. raise Exception(resp.text)
  49. js = resp.json()
  50. cache[self.refresh_token] = js["access_token"]
  51. self.refresh_token = js["refresh_token"]
  52. return js["access_token"]
  53. @property
  54. def auth_headers(self):
  55. return {
  56. "Amazon-Advertising-API-ClientId": self.lwa_client_id,
  57. "Amazon-Advertising-API-Scope": self.profile_id,
  58. "Authorization": f"Bearer {self.access_token}",
  59. }
  60. def _request(self, url_path: str, method: str = "GET", headers: dict = None, params: dict = None,
  61. body: dict = None):
  62. head = self.auth_headers
  63. if headers:
  64. head.update(headers)
  65. resp = requests.request(
  66. method=method,
  67. url=urljoin(URL_AD_API, url_path),
  68. headers=head,
  69. params=params,
  70. json=body,
  71. )
  72. js = resp.json()
  73. return js
  74. class SPClient(BaseClient):
  75. def get_campaigns(self, **body):
  76. url_path = "/sp/campaigns/list"
  77. headers = {
  78. "Accept": "application/vnd.spcampaign.v3+json",
  79. "Content-Type": "application/vnd.spcampaign.v3+json"
  80. }
  81. return self._request(url_path, method="POST", headers=headers, body=body)
  82. def iter_campaigns(self, **body) -> Iterator[dict]:
  83. if "maxResults" not in body:
  84. body["maxResults"] = 100
  85. while True:
  86. info: dict = self.get_campaigns(**body)
  87. yield from info["campaigns"]
  88. if not info.get("nextToken"):
  89. break
  90. body["nextToken"] = info["nextToken"]
  91. logger.info(f"总共数量:{info['totalResults']}")
  92. def get_ad_groups(self, **body):
  93. url_path = "/sp/adGroups/list"
  94. headers = {
  95. "Accept": "application/vnd.spadGroup.v3+json",
  96. "Content-Type": "application/vnd.spadGroup.v3+json"
  97. }
  98. return self._request(url_path, method="POST", body=body, headers=headers)
  99. def iter_adGroups(self, **body) -> Iterator[dict]:
  100. if "maxResults" not in body:
  101. body["maxResults"] = 100
  102. while True:
  103. info: dict = self.get_ad_groups(**body)
  104. yield from info["adGroups"]
  105. if not info.get("nextToken"):
  106. break
  107. body["nextToken"] = info["nextToken"]
  108. logger.info(f"总共数量:{info['totalResults']}")
  109. def get_ads(self, **body):
  110. url_path = "/sp/productAds/list"
  111. headers = {
  112. "Accept": "application/vnd.spproductAd.v3+json",
  113. "Content-Type": "application/vnd.spproductAd.v3+json"
  114. }
  115. return self._request(url_path, method="POST", body=body, headers=headers)
  116. def iter_ads(self, **body) -> Iterator[dict]:
  117. if "maxResults" not in body:
  118. body["maxResults"] = 100
  119. while True:
  120. info: dict = self.get_ads(**body)
  121. yield from info["productAds"]
  122. if not info.get("nextToken"):
  123. break
  124. body["nextToken"] = info["nextToken"]
  125. logger.info(f"总共数量:{info['totalResults']}")
  126. def get_keywords(self, **body):
  127. url_path = "/sp/keywords/list"
  128. headers = {
  129. "Accept": "application/vnd.spKeyword.v3+json",
  130. "Content-Type": "application/vnd.spKeyword.v3+json"
  131. }
  132. return self._request(url_path, method="POST", body=body, headers=headers)
  133. def iter_keywords(self, **body) -> Iterator[dict]:
  134. if "maxResults" not in body:
  135. body["maxResults"] = 100
  136. while True:
  137. info: dict = self.get_keywords(**body)
  138. yield from info["keywords"]
  139. if not info.get("nextToken"):
  140. break
  141. body["nextToken"] = info["nextToken"]
  142. logger.info(f"总共数量:{info['totalResults']}")
  143. def get_targets(self, **body):
  144. url_path = "/sp/targets/list"
  145. headers = {
  146. "Accept": "application/vnd.sptargetingClause.v3+json",
  147. "Content-Type": "application/vnd.sptargetingClause.v3+json"
  148. }
  149. return self._request(url_path, method="POST", body=body, headers=headers)
  150. def iter_targets(self, **body) -> Iterator[dict]:
  151. if "maxResults" not in body:
  152. body["maxResults"] = 100
  153. while True:
  154. info: dict = self.get_targets(**body)
  155. yield from info["targetingClauses"]
  156. if not info.get("nextToken"):
  157. break
  158. body["nextToken"] = info["nextToken"]
  159. logger.info(f"总共数量:{info['totalResults']}")
  160. def get_budget(self, campaign_ids: list):
  161. url_path = "/sp/campaigns/budget/usage"
  162. body = {
  163. "campaignIds": campaign_ids
  164. }
  165. return self._request(url_path, method="POST", body=body)
  166. def get_adgroup_bidrecommendation(self,campaignId:str,adGroupId:str,targetingExpressions:list,recommendationType:str="BIDS_FOR_EXISTING_AD_GROUP"):
  167. url_path = "/sp/targets/bid/recommendations"
  168. headers = {
  169. "Accept": "application/vnd.spthemebasedbidrecommendation.v3+json",
  170. "Content-Type": "application/vnd.spthemebasedbidrecommendation.v3+json"
  171. }
  172. body = {"campaignId":campaignId,
  173. "adGroupId":adGroupId,
  174. "recommendationType":recommendationType,
  175. "targetingExpressions":targetingExpressions}
  176. return self._request(url_path, method="POST", body=body, headers=headers)
  177. def get_keyword_bidrecommendation(self,adGroupId:str,keyword:list,matchType:list):
  178. keywords = list(map(lambda x:{"keyword":x[0],"matchType":x[1]},list(zip(keyword,matchType))))
  179. print(keywords)
  180. url_path = "/v2/sp/keywords/bidRecommendations"
  181. body = {"adGroupId":adGroupId,
  182. "keywords":keywords}
  183. return self._request(url_path, method="POST", body=body)
  184. class SBClient(BaseClient):
  185. def get_campaigns(self, **body):
  186. url_path = "/sb/v4/campaigns/list"
  187. headers = {
  188. "Accept": "application/vnd.sbcampaignresouce.v4+json",
  189. "Content-Type": "application/vnd.sbcampaignresouce.v4+json"
  190. }
  191. return self._request(url_path, method="POST", body=body, headers=headers)
  192. def iter_campaigns(self, **body) -> Iterator[dict]:
  193. if "maxResults" not in body:
  194. body["maxResults"] = 100
  195. while True:
  196. info: dict = self.get_campaigns(**body)
  197. yield from info["campaigns"]
  198. if not info.get("nextToken"):
  199. break
  200. body["nextToken"] = info["nextToken"]
  201. # logger.info(f"总共数量:{info['totalResults']}")
  202. def get_ad_groups(self,**body):
  203. url_path = "/sb/v4/adGroups/list"
  204. headers = {
  205. 'Content-Type': "application/vnd.sbadgroupresource.v4+json",
  206. 'Accept': "application/vnd.sbadgroupresource.v4+json"
  207. }
  208. return self._request(url_path, method="POST", headers=headers,body=body)
  209. def iter_adGroups(self, **body) -> Iterator[dict]:
  210. if "maxResults" not in body:
  211. body["maxResults"] = 100
  212. while True:
  213. info: dict = self.get_ad_groups(**body)
  214. print(info)
  215. yield from info["adGroups"]
  216. if not info.get("nextToken"):
  217. break
  218. body["nextToken"] = info["nextToken"]
  219. def get_ads(self,**body):
  220. url_path = "/sb/v4/ads/list"
  221. headers = {'Content-Type': "application/vnd.sbadresource.v4+json",
  222. 'Accept': "application/vnd.sbadresource.v4+json"
  223. }
  224. return self._request(url_path, method="POST", headers=headers,body=body)
  225. def iter_ads(self,**body):
  226. if "maxResults" not in body:
  227. body["maxResults"] = 100
  228. while True:
  229. info: dict = self.get_ads(**body)
  230. print(info)
  231. yield from info["ads"]
  232. if not info.get("nextToken"):
  233. break
  234. body["nextToken"] = info["nextToken"]
  235. def get_keywords(self):
  236. url_path = "/sb/keywords"
  237. return self._request(url_path, method="GET")
  238. def get_targets(self,**body):
  239. url_path = "/sb/targets/list"
  240. return self._request(url_path, method="POST", body=body)
  241. def iter_targets(self,**body):
  242. if "maxResults" not in body:
  243. body["maxResults"] = 100
  244. while True:
  245. info: dict = self.get_targets(**body)
  246. # print(info)
  247. yield from info["targets"]
  248. if not info.get("nextToken"):
  249. break
  250. body["nextToken"] = info["nextToken"]
  251. def get_budget(self,campaignIds:list):
  252. url_path = "/sb/campaigns/budget/usage"
  253. body = {"campaignIds":campaignIds}
  254. return self._request(url_path, method="POST",body=body)
  255. def get_keyword_bidrecommendation(self,**body):
  256. url_path = "/sb/recommendations/bids"
  257. return self._request(url_path, method="POST", body=body)
  258. def get_report(
  259. self,
  260. record_type: Literal['campaigns', 'adGroups', 'ads', 'targets', 'keywords'],
  261. report_date: str,
  262. metrics: List[str],
  263. segment: Literal['placement', 'query'] = None,
  264. creative_type: Literal['video', 'all'] = "all",
  265. download: bool = True
  266. ):
  267. """
  268. @param download: 是否下载文件
  269. @param record_type:
  270. @param report_date: 格式为YYYYMMDD,以请求的卖家市场所对应的时区为准,超过60天的报告不可用
  271. @param metrics:
  272. @param segment:
  273. @param creative_type:
  274. None:仅包含非视频广告
  275. 'video':仅包含视频广告
  276. 'all':包含视频和非视频广告
  277. @return:
  278. """
  279. url = f"/v2/hsa/{record_type}/report"
  280. body = {
  281. "reportDate": report_date,
  282. "metrics": ",".join(metrics),
  283. "creativeType": creative_type,
  284. "segment": segment
  285. }
  286. if record_type == "ads":
  287. body["creativeType"] = "all"
  288. ret = self._request(url, method="POST", body=body)
  289. report_id = ret["reportId"]
  290. status = ret["status"]
  291. if status == "FAILURE":
  292. raise Exception(ret)
  293. logger.info(f"创建报告成功:{ret}")
  294. while status == "IN_PROGRESS":
  295. logger.debug(f"报告{report_id}正在处理中...")
  296. time.sleep(3)
  297. ret = self._request(f"/v2/reports/{report_id}")
  298. status = ret["status"]
  299. if status == "FAILURE":
  300. raise Exception(ret)
  301. logger.info(f"报告处理完成:{ret}")
  302. if download:
  303. self.download_report(report_id, str(self.data_path / f"sb_{record_type}.json.gz"))
  304. else:
  305. return ret
  306. def download_report(self, report_id: str, file_path: str, decompress: bool = True) -> str:
  307. url = urljoin(URL_AD_API, f"/v2/reports/{report_id}/download")
  308. resp = requests.get(url, headers=self.auth_headers, stream=True)
  309. logger.info(f"开始下载报告:{report_id}")
  310. with open(file_path, "wb") as file:
  311. for data in resp.iter_content(chunk_size=10 * 1024):
  312. file.write(data)
  313. logger.info(f"报告{report_id}下载完成:{file_path}")
  314. if not decompress:
  315. return file_path
  316. de_file = gz_decompress(file_path)
  317. logger.info(f"解压完成:{de_file}")
  318. return de_file
  319. class SDClient(BaseClient):
  320. def get_campaigns(self, **params) -> List[dict]:
  321. url_path = "/sd/campaigns"
  322. return self._request(url_path, params=params)
  323. class Account(BaseClient):
  324. def get_portfolio(self):
  325. url_path = "/v2/portfolios/extended"
  326. return self._request(url_path)
  327. if __name__ == '__main__':
  328. AWS_CREDENTIALS = {
  329. 'lwa_client_id': 'amzn1.application-oa2-client.ebd701cd07854fb38c37ee49ec4ba109',
  330. 'refresh_token': "Atzr|IwEBIL4ur8kbcwRyxVu_srprAAoTYzujnBvA6jU-0SMxkRgOhGjYJSUNGKvw24EQwJa1jG5RM76mQD2P22AKSq8qSD94LddoXGdKDO74eQVYl0RhuqOMFqdrEZpp1p4bIR6_N8VeSJDHr7UCuo8FiabkSHrkq7tsNvRP-yI-bnpQv4EayPBh7YwHVX3hYdRbhxaBvgJENgCuiEPb35Q2-Z6w6ujjiKUAK2VSbCFpENlEfcHNsjDeY7RCvFlwlCoHj1IeiNIaFTE9yXFu3aEWlExe3LzHv6PZyunEi88QJSXKSh56Um0e0eEg05rMv-VBM83cAqc5POmZnTP1vUdZO8fQv3NFLZ-xU6e1WQVxVPi5Cyqk4jYhGf1Y9t98N654y0tVvw74qNIsTrB-8bGS0Uhfe24oBEWmzObvBY3zhtT1d42myGUJv4pMTU6yPoS83zhPKm3LbUDEpBA1hvvc_09jHk7vUEAuFB-UAZzlht2C1yklzQ",
  331. 'lwa_client_secret': 'cbf0514186db4df91e04a8905f0a91b605eae4201254ced879d8bb90df4b474d',
  332. 'profile_id': "3006125408623189"
  333. }
  334. # sp = SPClient(**AWS_CREDENTIALS)
  335. # print(sp.get_keyword_bidrecommendation(adGroupId="119753215871672",keyword=["8mp security camera system","8mp security camera system"],matchType=["broad","exact"]))
  336. sb = SBClient(**AWS_CREDENTIALS)
  337. # print(list(sb.iter_targets()))
  338. print(sb.get_keyword_bidrecommendation(**{'campaignId': 27333596383941,'keywords':[{"matchType":'broad',"keywordText":"4k security camera system"}]}))
  339. print(sb.get_budget([27333596383941]))
  340. # sd = SDClient(**AWS_CREDENTIALS)
  341. # print(sd.get_campaigns(startIndex=10, count=10))
  342. # sb = SBClient(**AWS_CREDENTIALS)
  343. # metrics = [
  344. # 'applicableBudgetRuleId',
  345. # 'applicableBudgetRuleName',
  346. # 'attributedConversions14d',
  347. # 'attributedConversions14dSameSKU',
  348. # 'attributedDetailPageViewsClicks14d',
  349. # 'attributedOrderRateNewToBrand14d',
  350. # 'attributedOrdersNewToBrand14d',
  351. # 'attributedOrdersNewToBrandPercentage14d',
  352. # 'attributedSales14d',
  353. # 'attributedSales14dSameSKU',
  354. # 'attributedSalesNewToBrand14d',
  355. # 'attributedSalesNewToBrandPercentage14d',
  356. # 'attributedUnitsOrderedNewToBrand14d',
  357. # 'attributedUnitsOrderedNewToBrandPercentage14d',
  358. # 'campaignBudget',
  359. # 'campaignBudgetType',
  360. # 'campaignId',
  361. # 'campaignName',
  362. # 'campaignRuleBasedBudget',
  363. # 'campaignStatus',
  364. # 'clicks',
  365. # 'cost',
  366. # 'dpv14d',
  367. # 'impressions',
  368. # 'unitsSold14d',
  369. # 'attributedBrandedSearches14d',
  370. # 'topOfSearchImpressionShare']
  371. # sb.get_report(
  372. # record_type="campaigns",
  373. # report_date="20231008",
  374. # metrics=metrics
  375. # )