amz_ad_client.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751
  1. import requests
  2. from urllib3.util.retry import Retry
  3. from requests.adapters import HTTPAdapter
  4. import json
  5. import time
  6. from cachetools import TTLCache
  7. from urllib.parse import urljoin
  8. from typing import List, Literal, Iterable, Iterator
  9. import gzip
  10. from pathlib import Path
  11. import s3fs
  12. from s3fs import S3FileSystem
  13. import logging
  14. URL_AUTH = "https://api.amazon.com/auth/o2/token"
  15. URL_AD_API = "https://advertising-api.amazon.com"
  16. cache = TTLCache(maxsize=10, ttl=3200)
  17. logger = logging.getLogger(__name__)
  18. class RateLimitError(Exception):
  19. def __init__(self, retry_after: str = None):
  20. self.retry_after = retry_after
  21. def gz_decompress(file_path: str, chunk_size: int = 1024 * 1024):
  22. decompressed_file = file_path.rstrip(".gz")
  23. with open(decompressed_file, "wb") as pw:
  24. zf = gzip.open(file_path, mode='rb')
  25. while True:
  26. chunk = zf.read(size=chunk_size)
  27. if not chunk:
  28. break
  29. pw.write(chunk)
  30. return decompressed_file
  31. class BaseClient:
  32. def __init__(
  33. self, lwa_client_id: str, lwa_client_secret: str, refresh_token: str = None, profile_id: str = None,
  34. data_path: str = "./"
  35. ):
  36. self.lwa_client_id = lwa_client_id
  37. self.lwa_client_secret = lwa_client_secret
  38. self.refresh_token = refresh_token
  39. self.profile_id = profile_id
  40. self.data_path = Path(data_path)
  41. if not self.data_path.exists():
  42. self.data_path.mkdir(parents=True)
  43. retry_strategy = Retry(
  44. total=5, # 重试次数
  45. allowed_methods=["GET", "POST"],
  46. # 强制重试的状态码,在method_whitelist中的请求方法才会重试
  47. status_forcelist=[429, 500, 502, 503, 504],
  48. raise_on_status=False, # 在status_forcelist中的状态码达到重试次数后是否抛出异常
  49. # backoff_factor * (2 ** (retry_time-1)), 即间隔1s, 2s, 4s, 8s, ...
  50. backoff_factor=2,
  51. )
  52. adapter = HTTPAdapter(max_retries=retry_strategy)
  53. self.session = requests.session()
  54. self.session.mount("https://", adapter)
  55. @property
  56. def access_token(self) -> str:
  57. try:
  58. return cache[self.refresh_token]
  59. except KeyError:
  60. resp = requests.post(URL_AUTH, data={
  61. "grant_type": "refresh_token",
  62. "client_id": self.lwa_client_id,
  63. "refresh_token": self.refresh_token,
  64. "client_secret": self.lwa_client_secret,
  65. })
  66. if resp.status_code != 200:
  67. raise Exception(resp.text)
  68. js = resp.json()
  69. cache[self.refresh_token] = js["access_token"]
  70. self.refresh_token = js["refresh_token"]
  71. return js["access_token"]
  72. @property
  73. def auth_headers(self):
  74. return {
  75. "Amazon-Advertising-API-ClientId": self.lwa_client_id,
  76. "Amazon-Advertising-API-Scope": self.profile_id,
  77. "Authorization": f"Bearer {self.access_token}",
  78. }
  79. def _request(self, url_path: str, method: str = "GET", headers: dict = None, params: dict = None,
  80. body: dict = None):
  81. head = self.auth_headers
  82. if headers:
  83. head.update(headers)
  84. resp = self.session.request(
  85. method=method,
  86. url=urljoin(URL_AD_API, url_path),
  87. headers=head,
  88. params=params,
  89. json=body,
  90. )
  91. if resp.status_code == 429:
  92. raise RateLimitError(resp.headers.get("Retry-After"))
  93. if resp.status_code >= 400:
  94. raise Exception(resp.text)
  95. return resp.json()
  96. def get_profilesInfo(self):
  97. url_path = "/v2/profiles"
  98. return self._request(url_path)
  99. class SPClient(BaseClient):
  100. def get_campaigns(self, **body):
  101. url_path = "/sp/campaigns/list"
  102. headers = {
  103. "Accept": "application/vnd.spcampaign.v3+json",
  104. "Content-Type": "application/vnd.spcampaign.v3+json"
  105. }
  106. return self._request(url_path, method="POST", headers=headers, body=body)
  107. def iter_campaigns(self, **body) -> Iterator[dict]:
  108. if "maxResults" not in body:
  109. body["maxResults"] = 100
  110. while True:
  111. info: dict = self.get_campaigns(**body)
  112. yield from info["campaigns"]
  113. if not info.get("nextToken"):
  114. break
  115. body["nextToken"] = info["nextToken"]
  116. logger.info(f"总共数量:{info['totalResults']}")
  117. def get_ad_groups(self, **body):
  118. url_path = "/sp/adGroups/list"
  119. headers = {
  120. "Accept": "application/vnd.spadGroup.v3+json",
  121. "Content-Type": "application/vnd.spadGroup.v3+json"
  122. }
  123. return self._request(url_path, method="POST", body=body, headers=headers)
  124. def iter_adGroups(self, **body) -> Iterator[dict]:
  125. if "maxResults" not in body:
  126. body["maxResults"] = 100
  127. while True:
  128. info: dict = self.get_ad_groups(**body)
  129. yield from info["adGroups"]
  130. if not info.get("nextToken"):
  131. break
  132. body["nextToken"] = info["nextToken"]
  133. logger.info(f"总共数量:{info['totalResults']}")
  134. def get_ads(self, **body):
  135. url_path = "/sp/productAds/list"
  136. headers = {
  137. "Accept": "application/vnd.spproductAd.v3+json",
  138. "Content-Type": "application/vnd.spproductAd.v3+json"
  139. }
  140. return self._request(url_path, method="POST", body=body, headers=headers)
  141. def iter_ads(self, **body) -> Iterator[dict]:
  142. if "maxResults" not in body:
  143. body["maxResults"] = 100
  144. while True:
  145. info: dict = self.get_ads(**body)
  146. yield from info["productAds"]
  147. if not info.get("nextToken"):
  148. break
  149. body["nextToken"] = info["nextToken"]
  150. logger.info(f"总共数量:{info['totalResults']}")
  151. def get_keywords(self, **body):
  152. url_path = "/sp/keywords/list"
  153. headers = {
  154. "Accept": "application/vnd.spKeyword.v3+json",
  155. "Content-Type": "application/vnd.spKeyword.v3+json"
  156. }
  157. return self._request(url_path, method="POST", body=body, headers=headers)
  158. def iter_keywords(self, **body) -> Iterator[dict]:
  159. if "maxResults" not in body:
  160. body["maxResults"] = 100
  161. while True:
  162. info: dict = self.get_keywords(**body)
  163. yield from info["keywords"]
  164. if not info.get("nextToken"):
  165. break
  166. body["nextToken"] = info["nextToken"]
  167. logger.info(f"总共数量:{info['totalResults']}")
  168. def get_targets(self, **body):
  169. url_path = "/sp/targets/list"
  170. headers = {
  171. "Accept": "application/vnd.sptargetingClause.v3+json",
  172. "Content-Type": "application/vnd.sptargetingClause.v3+json"
  173. }
  174. return self._request(url_path, method="POST", body=body, headers=headers)
  175. def iter_targets(self, **body) -> Iterator[dict]:
  176. if "maxResults" not in body:
  177. body["maxResults"] = 100
  178. while True:
  179. info: dict = self.get_targets(**body)
  180. yield from info["targetingClauses"]
  181. if not info.get("nextToken"):
  182. break
  183. body["nextToken"] = info["nextToken"]
  184. logger.info(f"总共数量:{info['totalResults']}")
  185. def get_budget(self, campaign_ids: list):
  186. url_path = "/sp/campaigns/budget/usage"
  187. body = {
  188. "campaignIds": campaign_ids
  189. }
  190. return self._request(url_path, method="POST", body=body)
  191. def get_adgroup_bidrecommendation(
  192. self, campaignId: str, adGroupId: str, targetingExpressions: list,
  193. recommendationType: str = "BIDS_FOR_EXISTING_AD_GROUP"):
  194. url_path = "/sp/targets/bid/recommendations"
  195. headers = {
  196. "Accept": "application/vnd.spthemebasedbidrecommendation.v3+json",
  197. "Content-Type": "application/vnd.spthemebasedbidrecommendation.v3+json"
  198. }
  199. body = {
  200. "campaignId": campaignId,
  201. "adGroupId": adGroupId,
  202. "recommendationType": recommendationType,
  203. "targetingExpressions": targetingExpressions
  204. }
  205. return self._request(url_path, method="POST", body=body, headers=headers)
  206. def get_keyword_bidrecommendation(self, adGroupId: str, keyword: list, matchType: list):
  207. keywords = list(map(lambda x: {"keyword": x[0], "matchType": x[1]}, list(zip(keyword, matchType))))
  208. url_path = "/v2/sp/keywords/bidRecommendations"
  209. body = {"adGroupId": adGroupId,
  210. "keywords": keywords}
  211. return self._request(url_path, method="POST", body=body)
  212. def get_v3_report(self,
  213. groupby:list,
  214. columns:list,
  215. startDate:str,
  216. endDate:str,
  217. reportType: Literal['spCampaigns','spAdvertisedProduct' ,'spPurchasedProduct', 'spTargeting', 'spSearchTerm'],
  218. timeUnit="DAILY",
  219. download=True):
  220. """
  221. @param groupby: 聚合条件,[campaign,adGroup, searchTerm,purchasedAsin,campaignPlacement,targeting,searchTerm,advertiser,asin]
  222. columns: 需要获取的字段
  223. """
  224. url_path = "/reporting/reports"
  225. headers = {
  226. "Content-Type":"application/vnd.createasyncreportrequest.v3+json"
  227. }
  228. body = {
  229. "name":"SP campaigns report",
  230. "startDate":startDate,
  231. "endDate":endDate,
  232. "configuration":{
  233. "adProduct":"SPONSORED_PRODUCTS",
  234. "groupBy":groupby,
  235. "columns":columns,
  236. "reportTypeId":reportType,
  237. "timeUnit":timeUnit,
  238. "format":"GZIP_JSON"
  239. }
  240. }
  241. ret = self._request(url_path,method="POST",headers=headers,body=body)
  242. # print(ret)
  243. report_id = ret["reportId"]
  244. status = ret["status"]
  245. if status == "FAILURE":
  246. raise Exception(ret)
  247. logger.info(f"创建报告成功:{ret}")
  248. while status in ["PROCESSING","PENDING"]:
  249. logger.debug(f"报告{report_id}正在处理中...")
  250. time.sleep(3)
  251. ret = self._request(f"/reporting/reports/{report_id}")
  252. print(ret)
  253. status = ret["status"]
  254. if status == "FAILURE":
  255. raise Exception(ret)
  256. logger.info(f"报告处理完成:{ret}")
  257. if download:
  258. pid = self.profile_id
  259. reportrel= self.download_v3_report(ret['url'],f"s3://reportforspsbsd/zosi/us/sp/{str(groupby)}_{startDate}_{endDate}_{reportType}_{str(pid)}.json.gz")
  260. return reportrel
  261. else:
  262. return ret
  263. def download_v3_report(self, url, file_path: str, decompress: bool = True) -> str:
  264. resp = requests.get(url, stream=True, allow_redirects=True)
  265. print(resp)
  266. kwargs = {'region_name': 'us-east-1', 'endpoint_url': "https://s3.amazonaws.com",
  267. 'aws_access_key_id': 'AKIARBAGHTGORIFN44VQ',
  268. 'aws_secret_access_key': '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'}
  269. s3_ = S3FileSystem(client_kwargs=kwargs)
  270. # print()
  271. with s3_.open(file_path, 'wb') as f:
  272. for data in resp.iter_content(chunk_size=10 * 1024):
  273. f.write(data)
  274. if not decompress:
  275. return file_path
  276. with s3_.open(file_path, 'rb') as f: # 读取s3数据
  277. data = gzip.GzipFile(fileobj=f, mode='rb')
  278. de_file = json.load(data)
  279. logger.info(f"解压完成:{de_file}")
  280. # print(de_file)
  281. return de_file
  282. class SBClient(BaseClient):
  283. def get_campaigns(self, **body):
  284. url_path = "/sb/v4/campaigns/list"
  285. headers = {
  286. "Accept": "application/vnd.sbcampaignresouce.v4+json",
  287. "Content-Type": "application/vnd.sbcampaignresouce.v4+json"
  288. }
  289. return self._request(url_path, method="POST", body=body, headers=headers)
  290. def iter_campaigns(self, **body) -> Iterator[dict]:
  291. if "maxResults" not in body:
  292. body["maxResults"] = 100
  293. while True:
  294. info: dict = self.get_campaigns(**body)
  295. yield from info["campaigns"]
  296. if not info.get("nextToken"):
  297. break
  298. body["nextToken"] = info["nextToken"]
  299. # logger.info(f"总共数量:{info['totalResults']}")
  300. def get_ad_groups(self, **body):
  301. url_path = "/sb/v4/adGroups/list"
  302. headers = {
  303. 'Content-Type': "application/vnd.sbadgroupresource.v4+json",
  304. 'Accept': "application/vnd.sbadgroupresource.v4+json"
  305. }
  306. return self._request(url_path, method="POST", headers=headers, body=body)
  307. def iter_adGroups(self, **body) -> Iterator[dict]:
  308. if "maxResults" not in body:
  309. body["maxResults"] = 100
  310. while True:
  311. info: dict = self.get_ad_groups(**body)
  312. # print(info)
  313. yield from info["adGroups"]
  314. if not info.get("nextToken"):
  315. break
  316. body["nextToken"] = info["nextToken"]
  317. def get_ads(self, **body):
  318. url_path = "/sb/v4/ads/list"
  319. headers = {
  320. 'Content-Type': "application/vnd.sbadresource.v4+json",
  321. 'Accept': "application/vnd.sbadresource.v4+json"
  322. }
  323. return self._request(url_path, method="POST", headers=headers, body=body)
  324. def iter_ads(self, **body):
  325. if "maxResults" not in body:
  326. body["maxResults"] = 100
  327. while True:
  328. info: dict = self.get_ads(**body)
  329. print(info)
  330. yield from info["ads"]
  331. if not info.get("nextToken"):
  332. break
  333. body["nextToken"] = info["nextToken"]
  334. def get_keywords(self,**param):
  335. url_path = "/sb/keywords"
  336. return self._request(url_path, method="GET",params=param)
  337. def iter_keywords(self,**param):
  338. if "startIndex" not in param:
  339. param["startIndex"] = 0
  340. param["count"] = 5000
  341. while True:
  342. info:list = self.get_keywords(**param)
  343. # print(info)
  344. if len(info) == 0:
  345. break
  346. param["startIndex"] += 5000
  347. yield info
  348. def get_targets(self, **body):
  349. url_path = "/sb/targets/list"
  350. return self._request(url_path, method="POST", body=body)
  351. def iter_targets(self, **body):
  352. if "maxResults" not in body:
  353. body["maxResults"] = 100
  354. while True:
  355. info: dict = self.get_targets(**body)
  356. # print(info)
  357. yield from info["targets"]
  358. if not info.get("nextToken"):
  359. break
  360. body["nextToken"] = info["nextToken"]
  361. def get_budget(self, campaignIds: list):
  362. url_path = "/sb/campaigns/budget/usage"
  363. body = {"campaignIds": campaignIds}
  364. return self._request(url_path, method="POST", body=body)
  365. def get_keyword_bidrecommendation(self, **body):
  366. url_path = "/sb/recommendations/bids"
  367. return self._request(url_path, method="POST", body=body)
  368. def get_v3_report(self,
  369. groupby:list,
  370. columns:list,
  371. startDate:str,
  372. endDate:str,
  373. reportType: Literal['sbCampaigns', 'sbPurchasedProduct', 'sbTargeting', 'sbSearchTerm'],
  374. timeUnit="DAILY",
  375. download=True):
  376. """
  377. Now about reportType is only sbPurchasedProduct available.
  378. @param groupby: 聚合条件
  379. @param columns: 需要获取的字段[campaign,purchasedAsin,targeting,searchTerm]
  380. @param startDate: 请求开始的日期
  381. @param endDate: 请求结束的日期
  382. @param reportType: 广告类型
  383. @param timeUnit: 时间指标-[DAILY, SUMMARY]
  384. @param download: 下载报告
  385. """
  386. url_path = "/reporting/reports"
  387. headers = {
  388. "Content-Type":"application/vnd.createasyncreportrequest.v3+json"
  389. }
  390. body = {
  391. "name":"SB campaigns report",
  392. "startDate":startDate,
  393. "endDate":endDate,
  394. "configuration":{
  395. "adProduct":"SPONSORED_BRANDS",
  396. "groupBy":groupby,
  397. "columns":columns,
  398. "reportTypeId":reportType,
  399. "timeUnit":timeUnit,
  400. "format":"GZIP_JSON"
  401. }
  402. }
  403. ret = self._request(url_path,method="POST",headers=headers,body=body)
  404. # print(ret)
  405. report_id = ret["reportId"]
  406. status = ret["status"]
  407. if status == "FAILURE":
  408. raise Exception(ret)
  409. logger.info(f"创建报告成功:{ret}")
  410. while status in ["PROCESSING","PENDING"]:
  411. logger.debug(f"报告{report_id}正在处理中...")
  412. time.sleep(3)
  413. ret = self._request(f"/reporting/reports/{report_id}")
  414. print(ret)
  415. status = ret["status"]
  416. if status == "FAILURE":
  417. raise Exception(ret)
  418. logger.info(f"报告处理完成:{ret}")
  419. if download:
  420. pid = self.profile_id
  421. reportrel= self.download_v3_report(ret['url'],f"s3://reportforspsbsd/zosi/us/sb/{startDate}_{endDate}_{reportType}_{str(groupby)}_{str(pid)}.json.gz")
  422. return reportrel
  423. else:
  424. return ret
  425. def download_v3_report(self, url, file_path: str, decompress: bool = True) -> str:
  426. resp = requests.get(url, stream=True, allow_redirects=True)
  427. print(resp)
  428. kwargs = {'region_name': 'us-east-1', 'endpoint_url': "https://s3.amazonaws.com",
  429. 'aws_access_key_id': 'AKIARBAGHTGORIFN44VQ',
  430. 'aws_secret_access_key': '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'}
  431. s3_ = S3FileSystem(client_kwargs=kwargs)
  432. # print()
  433. with s3_.open(file_path, 'wb') as f:
  434. for data in resp.iter_content(chunk_size=10 * 1024):
  435. f.write(data)
  436. if not decompress:
  437. return file_path
  438. with s3_.open(file_path, 'rb') as f: # 读取s3数据
  439. data = gzip.GzipFile(fileobj=f, mode='rb')
  440. de_file = json.load(data)
  441. logger.info(f"解压完成:{de_file}")
  442. # print(de_file)
  443. return de_file
  444. def get_v2_report(
  445. self,
  446. record_type: Literal['campaigns', 'adGroups', 'ads', 'targets', 'keywords'],
  447. report_date: str,
  448. metrics: List[str],
  449. segment: Literal['placement', 'query'] = None,
  450. creative_type: Literal['video', 'all'] = "all",
  451. download: bool = True
  452. ):
  453. """
  454. @param download: 是否下载文件
  455. @param record_type:
  456. @param report_date: 格式为YYYYMMDD,以请求的卖家市场所对应的时区为准,超过60天的报告不可用
  457. @param metrics:
  458. @param segment:
  459. @param creative_type:
  460. None:仅包含非视频广告
  461. 'video':仅包含视频广告
  462. 'all':包含视频和非视频广告
  463. @return:
  464. """
  465. url = f"/v2/hsa/{record_type}/report"
  466. body = {
  467. "reportDate": report_date,
  468. "metrics": ",".join(metrics),
  469. "creativeType": creative_type,
  470. "segment": segment
  471. }
  472. if record_type == "ads":
  473. body["creativeType"] = "all"
  474. ret = self._request(url, method="POST", body=body)
  475. report_id = ret["reportId"]
  476. status = ret["status"]
  477. if status == "FAILURE":
  478. raise Exception(ret)
  479. logger.info(f"创建报告成功:{ret}")
  480. while status == "IN_PROGRESS":
  481. logger.debug(f"报告{report_id}正在处理中...")
  482. time.sleep(3)
  483. ret = self._request(f"/v2/reports/{report_id}")
  484. print(ret)
  485. status = ret["status"]
  486. if status == "FAILURE":
  487. raise Exception(ret)
  488. logger.info(f"报告处理完成:{ret}")
  489. if download:
  490. pid = self.profile_id
  491. reportrel= self.download_v2_report(report_id, f"s3://reportforspsbsd/zosi/us/sb/{str(report_date)}_{record_type}_{creative_type}_{segment}_{str(pid)}.gz")
  492. return reportrel
  493. else:
  494. return ret
  495. def download_v2_report(self, report_id: str, file_path: str, decompress: bool = True) -> str:
  496. url = urljoin(URL_AD_API, f"/v2/reports/{report_id}/download")
  497. resp = requests.get(url, headers=self.auth_headers, stream=True, allow_redirects=True)
  498. logger.info(f"开始下载报告:{report_id}")
  499. kwargs = {'region_name': 'us-east-1', 'endpoint_url': "https://s3.amazonaws.com",
  500. 'aws_access_key_id': 'AKIARBAGHTGORIFN44VQ',
  501. 'aws_secret_access_key': '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'}
  502. s3_ = S3FileSystem(client_kwargs=kwargs)
  503. # print()
  504. with s3_.open(file_path, 'wb') as f:
  505. for data in resp.iter_content(chunk_size=10 * 1024):
  506. f.write(data)
  507. logger.info(f"报告{report_id}下载完成:{file_path}")
  508. if not decompress:
  509. return file_path
  510. with s3_.open(file_path, 'rb') as f: # 读取s3数据
  511. data = gzip.GzipFile(fileobj=f, mode='rb')
  512. de_file = json.load(data)
  513. logger.info(f"解压完成:{de_file}")
  514. # print(de_file)
  515. return de_file
  516. class SDClient(BaseClient):
  517. def get_campaigns(self, **params) -> List[dict]:
  518. url_path = "/sd/campaigns"
  519. return self._request(url_path, params=params)
  520. def get_adGroups(self,**params):
  521. url_path = '/sd/adGroups'
  522. return self._request(url_path, params=params)
  523. def iter_adGroups(self,**param):
  524. if "startIndex" not in param:
  525. param["startIndex"] = 0
  526. param["count"] = 5000
  527. while True:
  528. info:list = self.get_adGroups(**param)
  529. # print(info)
  530. if len(info) == 0:
  531. break
  532. param["startIndex"] += 5000
  533. yield info
  534. def get_ads(self,**params):
  535. url_path = '/sd/productAds'
  536. return self._request(url_path, params=params)
  537. def iter_ads(self,**param):
  538. if "startIndex" not in param:
  539. param["startIndex"] = 0
  540. param["count"] = 5000
  541. while True:
  542. info:list = self.get_ads(**param)
  543. # print(info)
  544. if len(info) == 0:
  545. break
  546. param["startIndex"] += 5000
  547. yield info
  548. def get_targets(self,**params):
  549. url_path = '/sd/targets'
  550. return self._request(url_path, params=params)
  551. def iter_targets(self,**param):
  552. if "startIndex" not in param:
  553. param["startIndex"] = 0
  554. param["count"] = 5000
  555. while True:
  556. info:list = self.get_targets(**param)
  557. # print(info)
  558. if len(info) == 0:
  559. break
  560. param["startIndex"] += 5000
  561. yield info
  562. def get_budget(self, campaignIds: list):
  563. url_path = "/sd/campaigns/budget/usage"
  564. body = {"campaignIds": campaignIds}
  565. return self._request(url_path, method="POST", body=body)
  566. def get_target_bidrecommendation(self,tactic:str,products:list,typeFilter:list,themes:dict,locale:str='en_US'):#
  567. url_path = '/sd/targets/recommendations'
  568. headers ={
  569. 'Content-Type':"application/vnd.sdtargetingrecommendations.v3.3+json",
  570. 'Accept':"application/vnd.sdtargetingrecommendations.v3.3+json"
  571. }
  572. # "tactic":"T00020",
  573. # "products":[{"asin":"B00MP57IOY"}],
  574. # "typeFilter":["PRODUCT"],
  575. # "themes":{"product":[{"name":"TEST","expression":[{"type":"asinBrandSameAs"}]}]}
  576. body = {
  577. "tactic":tactic,
  578. "products":products,
  579. "typeFilter":typeFilter,
  580. "themes":themes
  581. }
  582. return self._request(url_path, method="POST", headers=headers,body=body,params={"locale":locale})
  583. def get_v2_report(
  584. self,
  585. record_type: Literal['campaigns', 'adGroups', 'productAds', 'targets', 'asins'],
  586. report_date: str,
  587. metrics: List[str],
  588. segment: Literal['matchedTarget'] = None,
  589. tactic: Literal['T00020', 'T00030'] = None,
  590. download: bool = True
  591. ):
  592. """
  593. @param download: 是否下载文件
  594. @param record_type:
  595. @param report_date: 格式为YYYYMMDD,以请求的卖家市场所对应的时区为准,超过60天的报告不可用
  596. @param metrics:
  597. @param segment:
  598. @param tactic:
  599. T00020: contextual targeting
  600. T00030: audience targeting
  601. @return:
  602. """
  603. url = f"/sd/{record_type}/report"
  604. body = {
  605. "reportDate": report_date,
  606. "metrics": ",".join(metrics),
  607. "tactic": tactic,
  608. "segment": segment
  609. }
  610. ret = self._request(url, method="POST", body=body)
  611. report_id = ret["reportId"]
  612. status = ret["status"]
  613. print(ret)
  614. if status == "FAILURE":
  615. raise Exception(ret)
  616. logger.info(f"创建报告成功:{ret}")
  617. while status == "IN_PROGRESS":
  618. logger.debug(f"报告{report_id}正在处理中...")
  619. time.sleep(3)
  620. ret = self._request(f"/v2/reports/{report_id}")
  621. print(ret)
  622. status = ret["status"]
  623. if status == "FAILURE":
  624. raise Exception(ret)
  625. logger.info(f"报告处理完成:{ret}")
  626. if download:
  627. pid = self.profile_id
  628. reportrel= self.download_v2_report(report_id, f"s3://reportforspsbsd/zosi/us/sd/{str(report_date)}_{record_type}_{tactic}_{segment}_{str(pid)}.gz")
  629. return reportrel
  630. else:
  631. return ret
  632. def download_v2_report(self, report_id: str, file_path: str, decompress: bool = True) -> str:
  633. url = urljoin(URL_AD_API, f"/v2/reports/{report_id}/download")
  634. resp = requests.get(url, headers=self.auth_headers, stream=True, allow_redirects=True)
  635. logger.info(f"开始下载报告:{report_id}")
  636. kwargs = {'region_name': 'us-east-1', 'endpoint_url': "https://s3.amazonaws.com",
  637. 'aws_access_key_id': 'AKIARBAGHTGORIFN44VQ',
  638. 'aws_secret_access_key': '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'}
  639. s3_ = S3FileSystem(client_kwargs=kwargs)
  640. # print()
  641. with s3_.open(file_path, 'wb') as f:
  642. for data in resp.iter_content(chunk_size=10 * 1024):
  643. f.write(data)
  644. logger.info(f"报告{report_id}下载完成:{file_path}")
  645. if not decompress:
  646. return file_path
  647. with s3_.open(file_path, 'rb') as f: # 读取s3数据
  648. data = gzip.GzipFile(fileobj=f, mode='rb')
  649. de_file = json.load(data)
  650. logger.info(f"解压完成:{de_file}")
  651. # print(de_file)
  652. return de_file
  653. class Account(BaseClient):
  654. def get_portfolios(self):
  655. url_path = "/v2/portfolios/extended"
  656. return self._request(url_path)
  657. def iter_portfolios(self):
  658. yield from self.get_portfolios()
  659. AccountClient = Account
  660. if __name__ == '__main__':
  661. AWS_CREDENTIALS = {
  662. 'lwa_client_id': 'amzn1.application-oa2-client.ebd701cd07854fb38c37ee49ec4ba109',
  663. 'refresh_token': "Atzr|IwEBIL4ur8kbcwRyxVu_srprAAoTYzujnBvA6jU-0SMxkRgOhGjYJSUNGKvw24EQwJa1jG5RM76mQD2P22AKSq8qSD94LddoXGdKDO74eQVYl0RhuqOMFqdrEZpp1p4bIR6_N8VeSJDHr7UCuo8FiabkSHrkq7tsNvRP-yI-bnpQv4EayPBh7YwHVX3hYdRbhxaBvgJENgCuiEPb35Q2-Z6w6ujjiKUAK2VSbCFpENlEfcHNsjDeY7RCvFlwlCoHj1IeiNIaFTE9yXFu3aEWlExe3LzHv6PZyunEi88QJSXKSh56Um0e0eEg05rMv-VBM83cAqc5POmZnTP1vUdZO8fQv3NFLZ-xU6e1WQVxVPi5Cyqk4jYhGf1Y9t98N654y0tVvw74qNIsTrB-8bGS0Uhfe24oBEWmzObvBY3zhtT1d42myGUJv4pMTU6yPoS83zhPKm3LbUDEpBA1hvvc_09jHk7vUEAuFB-UAZzlht2C1yklzQ",
  664. 'lwa_client_secret': 'cbf0514186db4df91e04a8905f0a91b605eae4201254ced879d8bb90df4b474d',
  665. 'profile_id': "3006125408623189"
  666. }
  667. # sp = SPClient(**AWS_CREDENTIALS)
  668. # print(sp.get_keyword_bidrecommendation(
  669. # adGroupId="119753215871672",
  670. # keyword=["8mp security camera system","8mp security camera system"],
  671. # matchType=["broad","exact"]))
  672. sd = Account(**AWS_CREDENTIALS)
  673. print(sd.get_profilesInfo())
  674. # print(sb.get_keyword_bidrecommendation(**{'campaignId': 27333596383941, 'keywords': [
  675. # {"matchType": 'broad', "keywordText": "4k security camera system"}]}))
  676. # a = list(sd.iter_targets(**{"campaignIdFilter":"257424912382921"})) #list(sd.iter_targets())#
  677. # print(a,len(a))
  678. # sb = SBClient(**AWS_CREDENTIALS)
  679. # print(sd.get_v2_report(record_type="campaigns",report_date="20231020",tactic="T00030",metrics=['impressions']))