amz_ad_client.py 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976
  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_budgetrecommendation(self, campaign_ids):
  118. url_path = "/sp/campaigns/budgetRecommendations"
  119. body = {
  120. "campaignIds": campaign_ids
  121. }
  122. headers = {
  123. "Accept": "application/vnd.budgetrecommendation.v3+json",
  124. "Content-Type": "application/vnd.budgetrecommendation.v3+json"
  125. }
  126. return self._request(url_path, method="POST", headers=headers, body=body)
  127. def iter_budgetrecommendation(self,campaign_ids):
  128. for i in range(0,len(campaign_ids),100):
  129. campaign_id = campaign_ids[i:i+100]
  130. info: list = self.get_budgetrecommendation(campaign_id)
  131. yield from info["budgetRecommendationsSuccessResults"]
  132. def get_ad_groups(self, **body):
  133. url_path = "/sp/adGroups/list"
  134. headers = {
  135. "Accept": "application/vnd.spadGroup.v3+json",
  136. "Content-Type": "application/vnd.spadGroup.v3+json"
  137. }
  138. return self._request(url_path, method="POST", body=body, headers=headers)
  139. def iter_adGroups(self, **body) -> Iterator[dict]:
  140. if "maxResults" not in body:
  141. body["maxResults"] = 100
  142. while True:
  143. info: dict = self.get_ad_groups(**body)
  144. yield from info["adGroups"]
  145. if not info.get("nextToken"):
  146. break
  147. body["nextToken"] = info["nextToken"]
  148. logger.info(f"总共数量:{info['totalResults']}")
  149. def get_ads(self, **body):
  150. url_path = "/sp/productAds/list"
  151. headers = {
  152. "Accept": "application/vnd.spproductAd.v3+json",
  153. "Content-Type": "application/vnd.spproductAd.v3+json"
  154. }
  155. return self._request(url_path, method="POST", body=body, headers=headers)
  156. def iter_ads(self, **body) -> Iterator[dict]:
  157. if "maxResults" not in body:
  158. body["maxResults"] = 100
  159. while True:
  160. info: dict = self.get_ads(**body)
  161. yield from info["productAds"]
  162. if not info.get("nextToken"):
  163. break
  164. body["nextToken"] = info["nextToken"]
  165. logger.info(f"总共数量:{info['totalResults']}")
  166. def get_keywords(self, **body):
  167. url_path = "/sp/keywords/list"
  168. headers = {
  169. "Accept": "application/vnd.spKeyword.v3+json",
  170. "Content-Type": "application/vnd.spKeyword.v3+json"
  171. }
  172. return self._request(url_path, method="POST", body=body, headers=headers)
  173. def iter_keywords(self, **body) -> Iterator[dict]:
  174. if "maxResults" not in body:
  175. body["maxResults"] = 100
  176. while True:
  177. info: dict = self.get_keywords(**body)
  178. yield from info["keywords"]
  179. if not info.get("nextToken"):
  180. break
  181. body["nextToken"] = info["nextToken"]
  182. logger.info(f"总共数量:{info['totalResults']}")
  183. def get_targets(self, **body):
  184. url_path = "/sp/targets/list"
  185. headers = {
  186. "Accept": "application/vnd.sptargetingClause.v3+json",
  187. "Content-Type": "application/vnd.sptargetingClause.v3+json"
  188. }
  189. return self._request(url_path, method="POST", body=body, headers=headers)
  190. def iter_targets(self, **body) -> Iterator[dict]:
  191. if "maxResults" not in body:
  192. body["maxResults"] = 100
  193. while True:
  194. info: dict = self.get_targets(**body)
  195. yield from info["targetingClauses"]
  196. if not info.get("nextToken"):
  197. break
  198. body["nextToken"] = info["nextToken"]
  199. logger.info(f"总共数量:{info['totalResults']}")
  200. def get_budget(self, campaign_ids: list):
  201. url_path = "/sp/campaigns/budget/usage"
  202. body = {
  203. "campaignIds": campaign_ids
  204. }
  205. return self._request(url_path, method="POST", body=body)
  206. def get_adgroup_bidrecommendation(
  207. self, campaignId: str, adGroupId: str, targetingExpressions: list,
  208. recommendationType: str = "BIDS_FOR_EXISTING_AD_GROUP"):
  209. url_path = "/sp/targets/bid/recommendations"
  210. headers = {
  211. "Accept": "application/vnd.spthemebasedbidrecommendation.v3+json",
  212. "Content-Type": "application/vnd.spthemebasedbidrecommendation.v3+json"
  213. }
  214. body = {
  215. "campaignId": campaignId,
  216. "adGroupId": adGroupId,
  217. "recommendationType": recommendationType,
  218. "targetingExpressions": targetingExpressions
  219. }
  220. return self._request(url_path, method="POST", body=body, headers=headers)
  221. def get_keyword_bidrecommendation(self, adGroupId: str, keyword: list, matchType: list):
  222. keywords = list(map(lambda x: {"keyword": x[0], "matchType": x[1]}, list(zip(keyword, matchType))))
  223. url_path = "/v2/sp/keywords/bidRecommendations"
  224. body = {"adGroupId": adGroupId,
  225. "keywords": keywords}
  226. return self._request(url_path, method="POST", body=body)
  227. def get_targets_bid_recommendations(self,campaignId:str=None,
  228. adGroupId:str=None,
  229. asins:list=None,
  230. bid:float=None,
  231. keyword:str=None,
  232. userSelectedKeyword:bool=False,
  233. matchType:Literal["BROAD","EXACT","PHRASE"]="BROAD",
  234. recommendationType:Literal['KEYWORDS_FOR_ASINS','KEYWORDS_FOR_ADGROUP']="KEYWORDS_FOR_ASINS",
  235. sortDimension:Literal["CLICKS","CONVERSIONS","DEFAULT"]="DEFAULT",
  236. locale:Literal["ar_EG" ,"de_DE", "en_AE", "en_AU", "en_CA", "en_GB", "en_IN", "en_SA", "en_SG", "en_US",
  237. "es_ES", "es_MX", "fr_FR", "it_IT", "ja_JP", "nl_NL", "pl_PL", "pt_BR", "sv_SE", "tr_TR", "zh_CN"]="en_US"):
  238. url_path = "/sp/targets/keywords/recommendations"
  239. body = {
  240. "recommendationType": recommendationType,
  241. "targets": [
  242. {
  243. "matchType": matchType,
  244. "keyword": keyword,
  245. "bid": bid,
  246. "userSelectedKeyword": userSelectedKeyword
  247. }
  248. ],
  249. "maxRecommendations": "200",
  250. "sortDimension": sortDimension,
  251. "locale": locale
  252. }
  253. if adGroupId is not None:
  254. body["campaignId"]=campaignId
  255. body["adGroupId"]= adGroupId
  256. else:
  257. body['asins'] = asins
  258. return self._request(url_path, method="POST", body=body)
  259. def get_v3_report(self,
  260. groupby:list,
  261. columns:list,
  262. startDate:str,
  263. endDate:str,
  264. reportType: Literal['spCampaigns','spAdvertisedProduct' ,'spPurchasedProduct', 'spTargeting', 'spSearchTerm'],
  265. timeUnit="DAILY",
  266. download=True):
  267. """
  268. @param groupby: 聚合条件,[campaign,adGroup, searchTerm,purchasedAsin,campaignPlacement,targeting,searchTerm,advertiser,asin]
  269. columns: 需要获取的字段
  270. """
  271. url_path = "/reporting/reports"
  272. headers = {
  273. "Content-Type":"application/vnd.createasyncreportrequest.v3+json"
  274. }
  275. body = {
  276. "name":"SP campaigns report",
  277. "startDate":startDate,
  278. "endDate":endDate,
  279. "configuration":{
  280. "adProduct":"SPONSORED_PRODUCTS",
  281. "groupBy":groupby,
  282. "columns":columns,
  283. "reportTypeId":reportType,
  284. "timeUnit":timeUnit,
  285. "format":"GZIP_JSON"
  286. }
  287. }
  288. ret = self._request(url_path,method="POST",headers=headers,body=body)
  289. # print(ret)
  290. report_id = ret["reportId"]
  291. status = ret["status"]
  292. if status == "FAILURE":
  293. raise Exception(ret)
  294. logger.info(f"创建报告成功:{ret}")
  295. while status in ["PROCESSING","PENDING"]:
  296. logger.debug(f"报告{report_id}正在处理中...")
  297. time.sleep(4)
  298. try:
  299. ret = self._request(f"/reporting/reports/{report_id}")
  300. except:
  301. time.sleep(15)
  302. ret = self._request(f"/reporting/reports/{report_id}")
  303. print(ret)
  304. status = ret["status"]
  305. if status == "FAILURE":
  306. raise Exception(ret)
  307. logger.info(f"报告处理完成:{ret}")
  308. if download:
  309. pid = self.profile_id
  310. report_info = {'groupby': groupby,
  311. 'columns': columns,
  312. 'startDate': startDate,
  313. 'endDate': endDate,
  314. 'reportType': reportType,
  315. 'timeUnit': timeUnit,
  316. 'download': download}
  317. reportrel= self.download_v3_report(report_info,ret['url'],f"s3://reportforspsbsd/zosi/us/sp/{str(groupby)}_{startDate}_{endDate}_{reportType}_{str(pid)}.json.gz")
  318. return reportrel
  319. else:
  320. return ret
  321. def download_v3_report(self,report_info, url, file_path: str, decompress: bool = True) -> str:
  322. resp = requests.get(url, stream=True, allow_redirects=True)
  323. # print(resp)
  324. if resp.status_code in [200, 207]:
  325. kwargs = {'region_name': 'us-east-1', 'endpoint_url': "https://s3.amazonaws.com",
  326. 'aws_access_key_id': 'AKIARBAGHTGORIFN44VQ',
  327. 'aws_secret_access_key': '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'}
  328. s3_ = S3FileSystem(client_kwargs=kwargs)
  329. # print()
  330. with s3_.open(file_path, 'wb') as f:
  331. for data in resp.iter_content(chunk_size=10 * 1024):
  332. f.write(data)
  333. if not decompress:
  334. return file_path
  335. with s3_.open(file_path, 'rb') as f: # 读取s3数据
  336. data = gzip.GzipFile(fileobj=f, mode='rb')
  337. de_file = json.load(data)
  338. logger.info(f"解压完成:{de_file}")
  339. # print(de_file)
  340. return de_file
  341. else:
  342. logger.info(f"状态码{resp.status_code},开始重试")
  343. self.get_v3_report(report_info['groupby'], report_info['columns'], report_info['startDate'],
  344. report_info['endDate'],
  345. report_info['reportType'], report_info['timeUnit'], report_info['download'])
  346. class SBClient(BaseClient):
  347. def get_campaigns(self, **body):
  348. url_path = "/sb/v4/campaigns/list"
  349. headers = {
  350. "Accept": "application/vnd.sbcampaignresouce.v4+json",
  351. "Content-Type": "application/vnd.sbcampaignresouce.v4+json"
  352. }
  353. return self._request(url_path, method="POST", body=body, headers=headers)
  354. def get_campaign_v3(self, campaignId):
  355. if campaignId is None:
  356. url_path = f'/sb/campaigns'
  357. else:
  358. url_path = f'/sb/campaigns/{campaignId}'
  359. return self._request(url_path, method="GET")
  360. def iter_campaigns(self, **body) -> Iterator[dict]:
  361. if "maxResults" not in body:
  362. body["maxResults"] = 100
  363. while True:
  364. info: dict = self.get_campaigns(**body)
  365. yield from info["campaigns"]
  366. if not info.get("nextToken"):
  367. break
  368. body["nextToken"] = info["nextToken"]
  369. # logger.info(f"总共数量:{info['totalResults']}")
  370. def get_ad_groups(self, **body):
  371. url_path = "/sb/v4/adGroups/list"
  372. headers = {
  373. 'Content-Type': "application/vnd.sbadgroupresource.v4+json",
  374. 'Accept': "application/vnd.sbadgroupresource.v4+json"
  375. }
  376. return self._request(url_path, method="POST", headers=headers, body=body)
  377. def iter_adGroups(self, **body) -> Iterator[dict]:
  378. if "maxResults" not in body:
  379. body["maxResults"] = 100
  380. while True:
  381. info: dict = self.get_ad_groups(**body)
  382. # print(info)
  383. yield from info["adGroups"]
  384. if not info.get("nextToken"):
  385. break
  386. body["nextToken"] = info["nextToken"]
  387. def get_ads(self, **body):
  388. url_path = "/sb/v4/ads/list"
  389. headers = {
  390. 'Content-Type': "application/vnd.sbadresource.v4+json",
  391. 'Accept': "application/vnd.sbadresource.v4+json"
  392. }
  393. return self._request(url_path, method="POST", headers=headers, body=body)
  394. def iter_ads(self, **body):
  395. if "maxResults" not in body:
  396. body["maxResults"] = 100
  397. while True:
  398. info: dict = self.get_ads(**body)
  399. # print(info)
  400. yield from info["ads"]
  401. if not info.get("nextToken"):
  402. break
  403. body["nextToken"] = info["nextToken"]
  404. def get_keywords(self,**param):
  405. url_path = "/sb/keywords"
  406. return self._request(url_path, method="GET",params=param)
  407. def get_keyword(self,keywordid):
  408. url_path = f'/sb/keywords/{keywordid}'
  409. return self._request(url_path,method="GET")
  410. def iter_keywords(self,**param):
  411. if "startIndex" not in param:
  412. param["startIndex"] = 0
  413. param["count"] = 5000
  414. while True:
  415. info:list = self.get_keywords(**param)
  416. # print(info)
  417. if len(info) == 0:
  418. break
  419. param["startIndex"] += 5000
  420. yield info
  421. def get_targets(self, **body):
  422. url_path = "/sb/targets/list"
  423. return self._request(url_path, method="POST", body=body)
  424. def iter_targets(self, **body):
  425. if "maxResults" not in body:
  426. body["maxResults"] = 100
  427. while True:
  428. info: dict = self.get_targets(**body)
  429. # print(info)
  430. yield from info["targets"]
  431. if not info.get("nextToken"):
  432. break
  433. body["nextToken"] = info["nextToken"]
  434. def get_budget(self, campaignIds: list):
  435. url_path = "/sb/campaigns/budget/usage"
  436. body = {"campaignIds": campaignIds}
  437. return self._request(url_path, method="POST", body=body)
  438. def get_keyword_bidrecommendation(self, **body):
  439. url_path = "/sb/recommendations/bids"
  440. return self._request(url_path, method="POST", body=body)
  441. def get_v3_report(self,
  442. groupby:list,
  443. columns:list,
  444. startDate:str,
  445. endDate:str,
  446. reportType: Literal['sbCampaigns', 'sbPurchasedProduct', 'sbTargeting', 'sbSearchTerm'],
  447. timeUnit="DAILY",
  448. download=True):
  449. """
  450. Now about reportType is only sbPurchasedProduct available.
  451. @param groupby: 聚合条件
  452. @param columns: 需要获取的字段[campaign,purchasedAsin,targeting,searchTerm]
  453. @param startDate: 请求开始的日期
  454. @param endDate: 请求结束的日期
  455. @param reportType: 广告类型
  456. @param timeUnit: 时间指标-[DAILY, SUMMARY]
  457. @param download: 下载报告
  458. """
  459. url_path = "/reporting/reports"
  460. headers = {
  461. "Content-Type":"application/vnd.createasyncreportrequest.v3+json"
  462. }
  463. body = {
  464. "name":"SB campaigns report",
  465. "startDate":startDate,
  466. "endDate":endDate,
  467. "configuration":{
  468. "adProduct":"SPONSORED_BRANDS",
  469. "groupBy":groupby,
  470. "columns":columns,
  471. "reportTypeId":reportType,
  472. "timeUnit":timeUnit,
  473. "format":"GZIP_JSON"
  474. }
  475. }
  476. ret = self._request(url_path,method="POST",headers=headers,body=body)
  477. # print(ret)
  478. report_id = ret["reportId"]
  479. status = ret["status"]
  480. if status == "FAILURE":
  481. raise Exception(ret)
  482. logger.info(f"创建报告成功:{ret}")
  483. while status in ["PROCESSING","PENDING"]:
  484. logger.debug(f"报告{report_id}正在处理中...")
  485. time.sleep(4)
  486. try:
  487. ret = self._request(f"/reporting/reports/{report_id}")
  488. except:
  489. time.sleep(15)
  490. ret = self._request(f"/reporting/reports/{report_id}")
  491. print(ret)
  492. status = ret["status"]
  493. if status == "FAILURE":
  494. raise Exception(ret)
  495. logger.info(f"报告处理完成:{ret}")
  496. if download:
  497. pid = self.profile_id
  498. report_info = {'groupby': groupby,
  499. 'columns': columns,
  500. 'startDate': startDate,
  501. 'endDate': endDate,
  502. 'reportType': reportType,
  503. 'timeUnit': timeUnit,
  504. 'download': download}
  505. reportrel= self.download_v3_report(report_info,ret['url'],f"s3://reportforspsbsd/zosi/us/sb/{startDate}_{endDate}_{reportType}_{str(groupby)}_{str(pid)}.json.gz")
  506. return reportrel
  507. else:
  508. return ret
  509. def download_v3_report(self, report_info,url, file_path: str, decompress: bool = True) -> str:
  510. resp = requests.get(url, stream=True, allow_redirects=True)
  511. # print(resp)
  512. if resp.status_code in [200, 207]:
  513. kwargs = {'region_name': 'us-east-1', 'endpoint_url': "https://s3.amazonaws.com",
  514. 'aws_access_key_id': 'AKIARBAGHTGORIFN44VQ',
  515. 'aws_secret_access_key': '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'}
  516. s3_ = S3FileSystem(client_kwargs=kwargs)
  517. # print()
  518. with s3_.open(file_path, 'wb') as f:
  519. for data in resp.iter_content(chunk_size=10 * 1024):
  520. f.write(data)
  521. if not decompress:
  522. return file_path
  523. with s3_.open(file_path, 'rb') as f: # 读取s3数据
  524. data = gzip.GzipFile(fileobj=f, mode='rb')
  525. de_file = json.load(data)
  526. logger.info(f"解压完成:{de_file}")
  527. # print(de_file)
  528. return de_file
  529. else:
  530. logger.info(f"状态码{resp.status_code},开始重试")
  531. self.get_v3_report(report_info['groupby'], report_info['columns'], report_info['startDate'],
  532. report_info['endDate'],
  533. report_info['reportType'], report_info['timeUnit'], report_info['download'])
  534. def get_v2_report(
  535. self,
  536. record_type: Literal['campaigns', 'adGroups', 'ads', 'targets', 'keywords'],
  537. report_date: str,
  538. metrics: List[str],
  539. segment: Literal['placement', 'query'] = None,
  540. creative_type: Literal['video', 'all'] = "all",
  541. download: bool = True
  542. ):
  543. """
  544. @param download: 是否下载文件
  545. @param record_type:
  546. @param report_date: 格式为YYYYMMDD,以请求的卖家市场所对应的时区为准,超过60天的报告不可用
  547. @param metrics:
  548. @param segment:
  549. @param creative_type:
  550. None:仅包含非视频广告
  551. 'video':仅包含视频广告
  552. 'all':包含视频和非视频广告
  553. @return:
  554. """
  555. url = f"/v2/hsa/{record_type}/report"
  556. body = {
  557. "reportDate": report_date,
  558. "metrics": ",".join(metrics),
  559. "creativeType": creative_type,
  560. "segment": segment
  561. }
  562. if record_type == "ads":
  563. body["creativeType"] = "all"
  564. ret = self._request(url, method="POST", body=body)
  565. report_id = ret["reportId"]
  566. status = ret["status"]
  567. if status == "FAILURE":
  568. raise Exception(ret)
  569. logger.info(f"创建报告成功:{ret}")
  570. while status == "IN_PROGRESS":
  571. logger.debug(f"报告{report_id}正在处理中...")
  572. time.sleep(4)
  573. try:
  574. ret = self._request(f"/v2/reports/{report_id}")
  575. except:
  576. time.sleep(15)
  577. ret = self._request(f"/v2/reports/{report_id}")
  578. print(ret)
  579. status = ret["status"]
  580. if status == "FAILURE":
  581. raise Exception(ret)
  582. logger.info(f"报告处理完成:{ret}")
  583. if download:
  584. pid = self.profile_id
  585. report_info = {"record_type": record_type, "report_date": report_date, "metrics": metrics,
  586. "segment": segment, "creative_type": creative_type, "download": download}
  587. reportrel= self.download_v2_report(report_info,report_id, f"s3://reportforspsbsd/zosi/us/sb/{str(report_date)}_{record_type}_{creative_type}_{segment}_{str(pid)}.gz")
  588. return reportrel
  589. else:
  590. return ret
  591. def download_v2_report(self,report_info,report_id: str, file_path: str, decompress: bool = True) -> str:
  592. url = urljoin(URL_AD_API, f"/v2/reports/{report_id}/download")
  593. resp = requests.get(url, headers=self.auth_headers, stream=True, allow_redirects=True)
  594. if resp.status_code in [200, 207]:
  595. logger.info(f"开始下载报告:{report_id}")
  596. kwargs = {'region_name': 'us-east-1', 'endpoint_url': "https://s3.amazonaws.com",
  597. 'aws_access_key_id': 'AKIARBAGHTGORIFN44VQ',
  598. 'aws_secret_access_key': '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'}
  599. s3_ = S3FileSystem(client_kwargs=kwargs)
  600. # print()
  601. with s3_.open(file_path, 'wb') as f:
  602. for data in resp.iter_content(chunk_size=10 * 1024):
  603. f.write(data)
  604. logger.info(f"报告{report_id}下载完成:{file_path}")
  605. if not decompress:
  606. return file_path
  607. with s3_.open(file_path, 'rb') as f: # 读取s3数据
  608. data = gzip.GzipFile(fileobj=f, mode='rb')
  609. de_file = json.load(data)
  610. logger.info(f"解压完成:{de_file}")
  611. # print(de_file)
  612. return de_file
  613. else:
  614. logger.info(f"状态码{resp.status_code},开始重试")
  615. self.get_v2_report(report_info['record_type'], report_info['report_date'], report_info['metrics'],
  616. report_info['segment'], report_info['creative_type'], report_info['download'])
  617. class SDClient(BaseClient):
  618. def get_campaigns(self, **params) -> List[dict]:
  619. url_path = "/sd/campaigns"
  620. return self._request(url_path, params=params)
  621. def get_campaigns_extended(self, **params) -> List[dict]:
  622. url_path = "/sd/campaigns/extended"
  623. return self._request(url_path, params=params)
  624. def get_adGroups(self,**params):
  625. url_path = '/sd/adGroups'
  626. return self._request(url_path, params=params)
  627. def iter_adGroups(self,**param):
  628. if "startIndex" not in param:
  629. param["startIndex"] = 0
  630. param["count"] = 5000
  631. while True:
  632. info:list = self.get_adGroups(**param)
  633. # print(info)
  634. if len(info) == 0:
  635. break
  636. param["startIndex"] += 5000
  637. yield info
  638. def get_ads(self,**params):
  639. url_path = '/sd/productAds'
  640. return self._request(url_path, params=params)
  641. def iter_ads(self,**param):
  642. if "startIndex" not in param:
  643. param["startIndex"] = 0
  644. param["count"] = 5000
  645. while True:
  646. info:list = self.get_ads(**param)
  647. # print(info)
  648. if len(info) == 0:
  649. break
  650. param["startIndex"] += 5000
  651. yield info
  652. def get_targets(self,**params):
  653. url_path = '/sd/targets'
  654. return self._request(url_path, params=params)
  655. def iter_targets(self,**param):
  656. if "startIndex" not in param:
  657. param["startIndex"] = 0
  658. param["count"] = 5000
  659. while True:
  660. info:list = self.get_targets(**param)
  661. # print(info)
  662. if len(info) == 0:
  663. break
  664. param["startIndex"] += 5000
  665. yield info
  666. def get_budget(self, campaignIds: list):
  667. url_path = "/sd/campaigns/budget/usage"
  668. body = {"campaignIds": campaignIds}
  669. return self._request(url_path, method="POST", body=body)
  670. def get_target_bidrecommendation(self,tactic:str,products:list,typeFilter:list,themes:dict,locale:str='en_US'):#
  671. url_path = '/sd/targets/recommendations'
  672. headers ={
  673. 'Content-Type':"application/vnd.sdtargetingrecommendations.v3.3+json",
  674. 'Accept':"application/vnd.sdtargetingrecommendations.v3.3+json"
  675. }
  676. # "tactic":"T00020",
  677. # "products":[{"asin":"B00MP57IOY"}],
  678. # "typeFilter":["PRODUCT"],
  679. # "themes":{"product":[{"name":"TEST","expression":[{"type":"asinBrandSameAs"}]}]}
  680. body = {
  681. "tactic":tactic,
  682. "products":products,
  683. "typeFilter":typeFilter,
  684. "themes":themes
  685. }
  686. return self._request(url_path, method="POST", headers=headers,body=body,params={"locale":locale})
  687. def get_v3_report(self,
  688. groupby:list,
  689. columns:list,
  690. startDate:str,
  691. endDate:str,
  692. reportType: Literal['sdCampaigns', 'sdPurchasedProduct', 'sdTargeting', 'sdSearchTerm'],
  693. timeUnit="DAILY",
  694. download=True):
  695. """
  696. Now about reportType is only sbPurchasedProduct available.
  697. @param groupby: 聚合条件
  698. @param columns: 需要获取的字段[campaign,purchasedAsin,targeting,searchTerm]
  699. @param startDate: 请求开始的日期
  700. @param endDate: 请求结束的日期
  701. @param reportType: 广告类型
  702. @param timeUnit: 时间指标-[DAILY, SUMMARY]
  703. @param download: 下载报告
  704. """
  705. url_path = "/reporting/reports"
  706. headers = {
  707. "Content-Type":"application/vnd.createasyncreportrequest.v3+json"
  708. }
  709. body = {
  710. "name":"SD campaigns report",
  711. "startDate":startDate,
  712. "endDate":endDate,
  713. "configuration":{
  714. "adProduct":"SPONSORED_DISPLAY",
  715. "groupBy":groupby,
  716. "columns":columns,
  717. "reportTypeId":reportType,
  718. "timeUnit":timeUnit,
  719. "format":"GZIP_JSON"
  720. }
  721. }
  722. ret = self._request(url_path,method="POST",headers=headers,body=body)
  723. # print(ret)
  724. report_id = ret["reportId"]
  725. status = ret["status"]
  726. if status == "FAILURE":
  727. raise Exception(ret)
  728. logger.info(f"创建报告成功:{ret}")
  729. while status in ["PROCESSING","PENDING"]:
  730. logger.debug(f"报告{report_id}正在处理中...")
  731. time.sleep(4)
  732. try:
  733. ret = self._request(f"/reporting/reports/{report_id}")
  734. except:
  735. time.sleep(15)
  736. ret = self._request(f"/reporting/reports/{report_id}")
  737. print(ret)
  738. status = ret["status"]
  739. if status == "FAILURE":
  740. raise Exception(ret)
  741. logger.info(f"报告处理完成:{ret}")
  742. if download:
  743. pid = self.profile_id
  744. report_info = {'groupby':groupby,
  745. 'columns':columns,
  746. 'startDate':startDate,
  747. 'endDate':endDate,
  748. 'reportType': reportType,
  749. 'timeUnit':timeUnit,
  750. 'download':download}
  751. reportrel= self.download_v3_report(report_info,ret['url'],f"s3://reportforspsbsd/zosi/us/sd/{startDate}_{endDate}_{reportType}_{str(groupby)}_{str(pid)}.json.gz")
  752. return reportrel
  753. else:
  754. return ret
  755. def download_v3_report(self,report_info, url, file_path: str, decompress: bool = True) -> str:
  756. resp = requests.get(url, stream=True, allow_redirects=True)
  757. # print(resp)
  758. if resp.status_code in [200,207]:
  759. kwargs = {'region_name': 'us-east-1', 'endpoint_url': "https://s3.amazonaws.com",
  760. 'aws_access_key_id': 'AKIARBAGHTGORIFN44VQ',
  761. 'aws_secret_access_key': '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'}
  762. s3_ = S3FileSystem(client_kwargs=kwargs)
  763. # print()
  764. with s3_.open(file_path, 'wb') as f:
  765. for data in resp.iter_content(chunk_size=10 * 1024):
  766. f.write(data)
  767. if not decompress:
  768. return file_path
  769. with s3_.open(file_path, 'rb') as f: # 读取s3数据
  770. data = gzip.GzipFile(fileobj=f, mode='rb')
  771. de_file = json.load(data)
  772. logger.info(f"解压完成:{de_file}")
  773. # print(de_file)
  774. return de_file
  775. else:
  776. logger.info(f"状态码{resp.status_code},开始重试")
  777. self.get_v3_report(report_info['groupby'],report_info['columns'],report_info['startDate'],report_info['endDate'],
  778. report_info['reportType'],report_info['timeUnit'],report_info['download'])
  779. def get_v2_report(
  780. self,
  781. record_type: Literal['campaigns', 'adGroups', 'productAds', 'targets', 'asins'],
  782. report_date: str,
  783. metrics: List[str],
  784. segment: Literal['matchedTarget'] = None,
  785. tactic: Literal['T00020', 'T00030'] = None,
  786. download: bool = True
  787. ):
  788. """
  789. @param download: 是否下载文件
  790. @param record_type:
  791. @param report_date: 格式为YYYYMMDD,以请求的卖家市场所对应的时区为准,超过60天的报告不可用
  792. @param metrics:
  793. @param segment:
  794. @param tactic:
  795. T00020: contextual targeting
  796. T00030: audience targeting
  797. @return:
  798. """
  799. url = f"/sd/{record_type}/report"
  800. body = {
  801. "reportDate": report_date,
  802. "metrics": ",".join(metrics),
  803. "tactic": tactic,
  804. "segment": segment
  805. }
  806. ret = self._request(url, method="POST", body=body)
  807. report_id = ret["reportId"]
  808. status = ret["status"]
  809. print(ret)
  810. if status == "FAILURE":
  811. raise Exception(ret)
  812. logger.info(f"创建报告成功:{ret}")
  813. while status == "IN_PROGRESS":
  814. logger.debug(f"报告{report_id}正在处理中...")
  815. time.sleep(4)
  816. try:
  817. ret = self._request(f"/v2/reports/{report_id}")
  818. except:
  819. time.sleep(15)
  820. ret = self._request(f"/v2/reports/{report_id}")
  821. print(ret)
  822. status = ret["status"]
  823. if status == "FAILURE":
  824. raise Exception(ret)
  825. logger.info(f"报告处理完成:{ret}")
  826. if download:
  827. pid = self.profile_id
  828. report_info = {"record_type":record_type,"report_date":report_date,"metrics":metrics,"segment":segment,"tactic":tactic,"download":download}
  829. reportrel= self.download_v2_report(report_info,report_id, f"s3://reportforspsbsd/zosi/us/sd/{str(report_date)}_{record_type}_{tactic}_{segment}_{str(pid)}.gz")
  830. return reportrel
  831. else:
  832. return ret
  833. def download_v2_report(self, report_info,report_id: str, file_path: str, decompress: bool = True) -> str:
  834. url = urljoin(URL_AD_API, f"/v2/reports/{report_id}/download")
  835. resp = requests.get(url, headers=self.auth_headers, stream=True, allow_redirects=True)
  836. # print(resp.status_code)
  837. if resp.status_code in [200,207]:
  838. logger.info(f"开始下载报告:{report_id}")
  839. kwargs = {'region_name': 'us-east-1', 'endpoint_url': "https://s3.amazonaws.com",
  840. 'aws_access_key_id': 'AKIARBAGHTGORIFN44VQ',
  841. 'aws_secret_access_key': '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'}
  842. s3_ = S3FileSystem(client_kwargs=kwargs)
  843. # print()
  844. with s3_.open(file_path, 'wb') as f:
  845. for data in resp.iter_content(chunk_size=10 * 1024):
  846. # print(resp.text)
  847. f.write(data)
  848. logger.info(f"报告{report_id}下载完成:{file_path}")
  849. if not decompress:
  850. return file_path
  851. with s3_.open(file_path, 'rb') as f: # 读取s3数据
  852. data = gzip.GzipFile(fileobj=f, mode='rb')
  853. de_file = json.load(data)
  854. logger.info(f"解压完成:{de_file}")
  855. # print(de_file)
  856. return de_file
  857. else:
  858. logger.info(f"状态码{resp.status_code},开始重试")
  859. self.get_v2_report(report_info['record_type'],report_info['report_date'],report_info['metrics'],
  860. report_info['segment'],report_info['tactic'],report_info['download'])
  861. class Account(BaseClient):
  862. def get_portfolios(self):
  863. url_path = "/v2/portfolios/extended"
  864. return self._request(url_path)
  865. def iter_portfolios(self):
  866. yield from self.get_portfolios()
  867. AccountClient = Account
  868. if __name__ == '__main__':
  869. AWS_CREDENTIALS = {
  870. 'lwa_client_id': 'amzn1.application-oa2-client.ebd701cd07854fb38c37ee49ec4ba109',
  871. 'refresh_token': "Atzr|IwEBIL4ur8kbcwRyxVu_srprAAoTYzujnBvA6jU-0SMxkRgOhGjYJSUNGKvw24EQwJa1jG5RM76mQD2P22AKSq8qSD94LddoXGdKDO74eQVYl0RhuqOMFqdrEZpp1p4bIR6_N8VeSJDHr7UCuo8FiabkSHrkq7tsNvRP-yI-bnpQv4EayPBh7YwHVX3hYdRbhxaBvgJENgCuiEPb35Q2-Z6w6ujjiKUAK2VSbCFpENlEfcHNsjDeY7RCvFlwlCoHj1IeiNIaFTE9yXFu3aEWlExe3LzHv6PZyunEi88QJSXKSh56Um0e0eEg05rMv-VBM83cAqc5POmZnTP1vUdZO8fQv3NFLZ-xU6e1WQVxVPi5Cyqk4jYhGf1Y9t98N654y0tVvw74qNIsTrB-8bGS0Uhfe24oBEWmzObvBY3zhtT1d42myGUJv4pMTU6yPoS83zhPKm3LbUDEpBA1hvvc_09jHk7vUEAuFB-UAZzlht2C1yklzQ",
  872. 'lwa_client_secret': 'cbf0514186db4df91e04a8905f0a91b605eae4201254ced879d8bb90df4b474d',
  873. 'profile_id': "3006125408623189"
  874. }
  875. # sp = SPClient(**AWS_CREDENTIALS)
  876. # print(sp.get_keyword_bidrecommendation(
  877. # adGroupId="119753215871672",
  878. # keyword=["8mp security camera system","8mp security camera system"],
  879. # matchType=["broad","exact"]))
  880. sb = SDClient(**AWS_CREDENTIALS)
  881. # sb.get_v3_report(groupby=["campaign"],columns=["impressions","date"],startDate="2023-10-28",endDate="2023-10-28",reportType="sdCampaigns",timeUnit="DAILY")
  882. # print(list(sb.iter_budgetrecommendation(['147691704431878', '60271227965408', '218847416529861', '31392773399718', '262035857074479', '198816492559645', '142984587494761', '56817060975858', '25915710734770', '129767234792339', '157524678908837', '49318230260950', '108960112862154', '62467550507341', '251456127331427', '191378898823474', '120392223446402', '247773821977107', '280377532791660', '184950658810783', '164014978241334', '123919526909988', '130880921982647', '126821145840591', '64491219404125', '38337331569585', '116386348992750', '225036289048302', '156661647012185', '144776397901336', '135236490839193', '169772039957761', '80512764023579', '34385013996533', '120697809283682', '246829191189710', '209056522772792', '232902564083581', '3932823953797', '279860689033033', '148947795709523', '97303001527474', '273793489896841', '166028784537215', '39300113616652', '170015910542201', '273987458393412', '171417956384778', '179418644846675', '93362211056849', '183219549121837', '92109046525620', '209610737481328', '64551171635322', '104421263246026', '107129361457199', '180558158817613', '196152550852335', '274383816868050', '153176862530690', '202668080336520', '139339123933891', '216562245832724', '44126930182871', '158793558529257', '163377434993147', '129760407316857', '259206875182868', '254478421786009', '113623886210537', '123264383028090', '114995189965872', '126327624499318', '111423039176174', '218824374284099', '268581491758278', '221198600183221', '252229497958387', '25969190496597', '147213408548844', '120873185867848', '121127240307802', '149837836567172', '244566073561396', '170971926269997', '50598109145873', '94661978287830', '172459323917375', '79239463046520', '161538254020266', '122877215020077', '19390096189319', '84671881754842', '56035811352399', '127311973972957', '99871114075939', '216044477480148', '212849641880903', '96517925473279', '140235725339419', '141444557365069', '2246672580150', '268090880021982', '94042224869659', '49225748581620', '32725632149299', '40908978446751', '146895818081903', '119917780655231', '146458404145911', '118070860722352', '52022434573022', '272598548490634', '16095673755650', '172664047341294', '256277301042945', '196836379347636', '155301894644769', '274223966125410', '248132117356305', '57660814157093', '50226094984793', '28888390047403', '102108408531327', '30079789635585', '223870235275688', '184420890768530', '29862567915039', '39565569376716', '5130702823897', '193159277950903', '155681125793546', '147688196248459', '246947393077054', '31511403651405', '102765494942726', '37856299838393', '231975314936513', '241347601457737', '59068161406145', '195628702544128', '225472432237809', '247179558013703', '114349375196504', '247845805126534', '50893503004023', '21955509658149', '66932634825184', '281441197839505', '255188169284953', '127289804144979', '168277283490230', '67066099773166', '14095630154088', '14246020074735', '272850527640233', '107029528476360', '174968043962800', '221862209153056', '116700370772313', '104020924872149', '97962041521658', '173158222344275', '185251252451477', '74364105565262', '210932538222999', '63297563429758', '276793053074864', '69715214871405', '95970937392401', '32561213134196', '33142883019280', '79216188082798', '59643351291635', '76327385728260', '116202606958791', '167409692560139', '171577662027638', '41421288964538', '150057207183605', '154717958881150', '114788039648066', '107895719250913', '264030622380992', '172196786467310', '197934421091867', '278388674751612', '45379253028975', '122119878478906', '4533827464348', '130815119156456', '137297479866563', '250767198691934', '139253473761708', '112336508594346', '132816688267091', '57709263376416', '249118660318975', '196638816684572', '92891389830198', '208322940506617', '186709333461759', '185422701325753', '270743781107196', '150694054972074', '193071867166263', '279972459852484', '119252950300630', '124990749954236', '266097963051545', '234638916539784', '20605146781735', '70768902397459', '154986591065926', '159501779813069', '162103896356568', '460593835599747', '410212175498241', '557726418497539', '500143110670441', '355416669121026', '431780944586985', '404895228651677', '388038108990987', '464118197356560', '521980826814295', '469329788053854', '326468917818944', '552954913725893', '542546450513278', '325523075677132', '392468912549092', '527886948735997', '329385433739622', '485978120289879', '325681364822312', '555651735003802', '303081891898256', '290254871141701', '485921949741399', '403591067235790', '499317623967079', '293479361575582', '495483931857167', '559648871033557', '336635682793609', '471666122828785', '537556291117721', '523813506535076', '497250432979707', '442860218481252', '448893131612583', '281694643275712', '433377458106838', '442612845985546', '338829853627078', '392584497224138', '496308094986157', '418568230991874', '378377235258783', '478649462904301', '369874181350855', '420881761417240', '360520776254966', '479975368002715', '390542812871179', '395983847529920', '390131732881184', '383692739831773', '444758167627464', '453566341896795', '496145304023336', '427176120966955', '310665337034129', '477718522757455', '535119828991333', '375580793899192', '464596753380107', '327731861279726', '294079129793082', '360342567843115', '310372350013643', '235356299301347', '265522205087323', '228344360452595', '264342228103465', '122100911164709', '232859308822086', '45261323402141', '65662811257626', '234144786032766', '219449168841685', '128805550207790', '52164236957007', '120413947884525', '247101003653911', '22887864793841', '66188387334596', '102602610195475', '14284441019308', '125676608715613', '208268030289407', '237637021820467', '83135684198446', '154786784931406', '34854308519962', '8788604610896', '46715264141017', '139624985843170', '220476381163737', '109100736225759', '56729203013140', '88373528260613', '3312332033772', '37834945448724', '94120697716118', '193841489621149', '176450446850186', '166966792230306', '168345583834458', '63645396093211', '21250682621308', '43999955361946', '91246816921037', '256738400830651', '141723939372941', '2925041027398', '209321026962101', '260728226512366', '139367584185686', '264968517312373', '46556032756649', '55837615659599', '6217208882388', '212383697581072', '13384963604097', '220553842518820', '216909098017681', '164205523483164', '140262174434441', '204728710865941', '257987760095645', '74010928042922', '218130080477762', '40247744447213', '243863605109393', '160210698808714', '23666771936859', '209074182215992', '183440625321762', '10008326799771', '251040907550889', '50479853741108', '98121074839083', '182666986837920', '75110020202471', '277378379542982', '137193580729176', '84426787297480', '238858183340317', '270834245781899']
  883. # )))
  884. # print(list(sb.iter_campaigns(**{"stateFilter": {
  885. # "include": ["ARCHIVED","PAUSED","ENABLED"]
  886. # }})))
  887. # print(sb.get_keyword_bidrecommendation(**{'campaignId': 27333596383941, 'keywords': [
  888. # {"matchType": 'broad', "keywordText": "4k security camera system"}]}))
  889. # a = list(sd.iter_targets(**{"campaignIdFilter":"257424912382921"})) #list(sd.iter_targets())#
  890. # print(a,len(a))
  891. # sb = SBClient(**AWS_CREDENTIALS)
  892. # print(sd.get_v2_report(record_type="campaigns",report_date="20231020",tactic="T00030",metrics=['impressions']))