|
@@ -1,4 +1,6 @@
|
|
import requests
|
|
import requests
|
|
|
|
+from urllib3.util.retry import Retry
|
|
|
|
+from requests.adapters import HTTPAdapter
|
|
import json
|
|
import json
|
|
import time
|
|
import time
|
|
from cachetools import TTLCache
|
|
from cachetools import TTLCache
|
|
@@ -6,14 +8,20 @@ from urllib.parse import urljoin
|
|
from typing import List, Literal, Iterable, Iterator
|
|
from typing import List, Literal, Iterable, Iterator
|
|
import gzip
|
|
import gzip
|
|
from pathlib import Path
|
|
from pathlib import Path
|
|
-from logging import getLogger
|
|
|
|
|
|
+
|
|
|
|
+import logging
|
|
|
|
|
|
URL_AUTH = "https://api.amazon.com/auth/o2/token"
|
|
URL_AUTH = "https://api.amazon.com/auth/o2/token"
|
|
URL_AD_API = "https://advertising-api.amazon.com"
|
|
URL_AD_API = "https://advertising-api.amazon.com"
|
|
|
|
|
|
cache = TTLCache(maxsize=10, ttl=3200)
|
|
cache = TTLCache(maxsize=10, ttl=3200)
|
|
|
|
|
|
-logger = getLogger(__name__)
|
|
|
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+class RateLimitError(Exception):
|
|
|
|
+ def __init__(self, retry_after: str = None):
|
|
|
|
+ self.retry_after = retry_after
|
|
|
|
|
|
|
|
|
|
def gz_decompress(file_path: str, chunk_size: int = 1024 * 1024):
|
|
def gz_decompress(file_path: str, chunk_size: int = 1024 * 1024):
|
|
@@ -41,6 +49,19 @@ class BaseClient:
|
|
if not self.data_path.exists():
|
|
if not self.data_path.exists():
|
|
self.data_path.mkdir(parents=True)
|
|
self.data_path.mkdir(parents=True)
|
|
|
|
|
|
|
|
+ retry_strategy = Retry(
|
|
|
|
+ total=5, # 重试次数
|
|
|
|
+ allowed_methods=["GET", "POST"],
|
|
|
|
+ # 强制重试的状态码,在method_whitelist中的请求方法才会重试
|
|
|
|
+ status_forcelist=[429, 500, 502, 503, 504],
|
|
|
|
+ raise_on_status=False, # 在status_forcelist中的状态码达到重试次数后是否抛出异常
|
|
|
|
+ # backoff_factor * (2 ** (retry_time-1)), 即间隔1s, 2s, 4s, 8s, ...
|
|
|
|
+ backoff_factor=1,
|
|
|
|
+ )
|
|
|
|
+ adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
|
|
+ self.session = requests.session()
|
|
|
|
+ self.session.mount("https://", adapter)
|
|
|
|
+
|
|
@property
|
|
@property
|
|
def access_token(self) -> str:
|
|
def access_token(self) -> str:
|
|
try:
|
|
try:
|
|
@@ -72,15 +93,19 @@ class BaseClient:
|
|
head = self.auth_headers
|
|
head = self.auth_headers
|
|
if headers:
|
|
if headers:
|
|
head.update(headers)
|
|
head.update(headers)
|
|
- resp = requests.request(
|
|
|
|
|
|
+ resp = self.session.request(
|
|
method=method,
|
|
method=method,
|
|
url=urljoin(URL_AD_API, url_path),
|
|
url=urljoin(URL_AD_API, url_path),
|
|
headers=head,
|
|
headers=head,
|
|
params=params,
|
|
params=params,
|
|
json=body,
|
|
json=body,
|
|
)
|
|
)
|
|
- js = resp.json()
|
|
|
|
- return js
|
|
|
|
|
|
+ if resp.status_code == 429:
|
|
|
|
+ raise RateLimitError(resp.headers.get("Retry-After"))
|
|
|
|
+ if resp.status_code >= 400:
|
|
|
|
+ raise Exception(resp.text)
|
|
|
|
+
|
|
|
|
+ return resp.json()
|
|
|
|
|
|
|
|
|
|
class SPClient(BaseClient):
|
|
class SPClient(BaseClient):
|
|
@@ -149,6 +174,7 @@ class SPClient(BaseClient):
|
|
"Content-Type": "application/vnd.spKeyword.v3+json"
|
|
"Content-Type": "application/vnd.spKeyword.v3+json"
|
|
}
|
|
}
|
|
return self._request(url_path, method="POST", body=body, headers=headers)
|
|
return self._request(url_path, method="POST", body=body, headers=headers)
|
|
|
|
+
|
|
def iter_keywords(self, **body) -> Iterator[dict]:
|
|
def iter_keywords(self, **body) -> Iterator[dict]:
|
|
if "maxResults" not in body:
|
|
if "maxResults" not in body:
|
|
body["maxResults"] = 100
|
|
body["maxResults"] = 100
|
|
@@ -186,26 +212,30 @@ class SPClient(BaseClient):
|
|
}
|
|
}
|
|
return self._request(url_path, method="POST", body=body)
|
|
return self._request(url_path, method="POST", body=body)
|
|
|
|
|
|
- def get_adgroup_bidrecommendation(self,campaignId:str,adGroupId:str,targetingExpressions:list,recommendationType:str="BIDS_FOR_EXISTING_AD_GROUP"):
|
|
|
|
|
|
+ def get_adgroup_bidrecommendation(
|
|
|
|
+ self, campaignId: str, adGroupId: str, targetingExpressions: list,
|
|
|
|
+ recommendationType: str = "BIDS_FOR_EXISTING_AD_GROUP"):
|
|
url_path = "/sp/targets/bid/recommendations"
|
|
url_path = "/sp/targets/bid/recommendations"
|
|
headers = {
|
|
headers = {
|
|
"Accept": "application/vnd.spthemebasedbidrecommendation.v3+json",
|
|
"Accept": "application/vnd.spthemebasedbidrecommendation.v3+json",
|
|
"Content-Type": "application/vnd.spthemebasedbidrecommendation.v3+json"
|
|
"Content-Type": "application/vnd.spthemebasedbidrecommendation.v3+json"
|
|
}
|
|
}
|
|
- body = {"campaignId":campaignId,
|
|
|
|
- "adGroupId":adGroupId,
|
|
|
|
- "recommendationType":recommendationType,
|
|
|
|
- "targetingExpressions":targetingExpressions}
|
|
|
|
|
|
+ body = {
|
|
|
|
+ "campaignId": campaignId,
|
|
|
|
+ "adGroupId": adGroupId,
|
|
|
|
+ "recommendationType": recommendationType,
|
|
|
|
+ "targetingExpressions": targetingExpressions
|
|
|
|
+ }
|
|
return self._request(url_path, method="POST", body=body, headers=headers)
|
|
return self._request(url_path, method="POST", body=body, headers=headers)
|
|
|
|
|
|
- def get_keyword_bidrecommendation(self,adGroupId:str,keyword:list,matchType:list):
|
|
|
|
- keywords = list(map(lambda x:{"keyword":x[0],"matchType":x[1]},list(zip(keyword,matchType))))
|
|
|
|
- print(keywords)
|
|
|
|
|
|
+ def get_keyword_bidrecommendation(self, adGroupId: str, keyword: list, matchType: list):
|
|
|
|
+ keywords = list(map(lambda x: {"keyword": x[0], "matchType": x[1]}, list(zip(keyword, matchType))))
|
|
url_path = "/v2/sp/keywords/bidRecommendations"
|
|
url_path = "/v2/sp/keywords/bidRecommendations"
|
|
- body = {"adGroupId":adGroupId,
|
|
|
|
- "keywords":keywords}
|
|
|
|
|
|
+ body = {"adGroupId": adGroupId,
|
|
|
|
+ "keywords": keywords}
|
|
return self._request(url_path, method="POST", body=body)
|
|
return self._request(url_path, method="POST", body=body)
|
|
|
|
|
|
|
|
+
|
|
class SBClient(BaseClient):
|
|
class SBClient(BaseClient):
|
|
def get_campaigns(self, **body):
|
|
def get_campaigns(self, **body):
|
|
url_path = "/sb/v4/campaigns/list"
|
|
url_path = "/sb/v4/campaigns/list"
|
|
@@ -226,33 +256,34 @@ class SBClient(BaseClient):
|
|
body["nextToken"] = info["nextToken"]
|
|
body["nextToken"] = info["nextToken"]
|
|
# logger.info(f"总共数量:{info['totalResults']}")
|
|
# logger.info(f"总共数量:{info['totalResults']}")
|
|
|
|
|
|
- def get_ad_groups(self,**body):
|
|
|
|
|
|
+ def get_ad_groups(self, **body):
|
|
url_path = "/sb/v4/adGroups/list"
|
|
url_path = "/sb/v4/adGroups/list"
|
|
headers = {
|
|
headers = {
|
|
'Content-Type': "application/vnd.sbadgroupresource.v4+json",
|
|
'Content-Type': "application/vnd.sbadgroupresource.v4+json",
|
|
'Accept': "application/vnd.sbadgroupresource.v4+json"
|
|
'Accept': "application/vnd.sbadgroupresource.v4+json"
|
|
}
|
|
}
|
|
- return self._request(url_path, method="POST", headers=headers,body=body)
|
|
|
|
|
|
+ return self._request(url_path, method="POST", headers=headers, body=body)
|
|
|
|
|
|
def iter_adGroups(self, **body) -> Iterator[dict]:
|
|
def iter_adGroups(self, **body) -> Iterator[dict]:
|
|
if "maxResults" not in body:
|
|
if "maxResults" not in body:
|
|
body["maxResults"] = 100
|
|
body["maxResults"] = 100
|
|
while True:
|
|
while True:
|
|
info: dict = self.get_ad_groups(**body)
|
|
info: dict = self.get_ad_groups(**body)
|
|
- print(info)
|
|
|
|
|
|
+ # print(info)
|
|
yield from info["adGroups"]
|
|
yield from info["adGroups"]
|
|
if not info.get("nextToken"):
|
|
if not info.get("nextToken"):
|
|
break
|
|
break
|
|
body["nextToken"] = info["nextToken"]
|
|
body["nextToken"] = info["nextToken"]
|
|
|
|
|
|
- def get_ads(self,**body):
|
|
|
|
|
|
+ def get_ads(self, **body):
|
|
url_path = "/sb/v4/ads/list"
|
|
url_path = "/sb/v4/ads/list"
|
|
- headers = {'Content-Type': "application/vnd.sbadresource.v4+json",
|
|
|
|
- 'Accept': "application/vnd.sbadresource.v4+json"
|
|
|
|
- }
|
|
|
|
- return self._request(url_path, method="POST", headers=headers,body=body)
|
|
|
|
|
|
+ headers = {
|
|
|
|
+ 'Content-Type': "application/vnd.sbadresource.v4+json",
|
|
|
|
+ 'Accept': "application/vnd.sbadresource.v4+json"
|
|
|
|
+ }
|
|
|
|
+ return self._request(url_path, method="POST", headers=headers, body=body)
|
|
|
|
|
|
- def iter_ads(self,**body):
|
|
|
|
|
|
+ def iter_ads(self, **body):
|
|
if "maxResults" not in body:
|
|
if "maxResults" not in body:
|
|
body["maxResults"] = 100
|
|
body["maxResults"] = 100
|
|
while True:
|
|
while True:
|
|
@@ -262,15 +293,16 @@ class SBClient(BaseClient):
|
|
if not info.get("nextToken"):
|
|
if not info.get("nextToken"):
|
|
break
|
|
break
|
|
body["nextToken"] = info["nextToken"]
|
|
body["nextToken"] = info["nextToken"]
|
|
|
|
+
|
|
def get_keywords(self):
|
|
def get_keywords(self):
|
|
url_path = "/sb/keywords"
|
|
url_path = "/sb/keywords"
|
|
return self._request(url_path, method="GET")
|
|
return self._request(url_path, method="GET")
|
|
|
|
|
|
- def get_targets(self,**body):
|
|
|
|
|
|
+ def get_targets(self, **body):
|
|
url_path = "/sb/targets/list"
|
|
url_path = "/sb/targets/list"
|
|
return self._request(url_path, method="POST", body=body)
|
|
return self._request(url_path, method="POST", body=body)
|
|
|
|
|
|
- def iter_targets(self,**body):
|
|
|
|
|
|
+ def iter_targets(self, **body):
|
|
if "maxResults" not in body:
|
|
if "maxResults" not in body:
|
|
body["maxResults"] = 100
|
|
body["maxResults"] = 100
|
|
while True:
|
|
while True:
|
|
@@ -281,11 +313,12 @@ class SBClient(BaseClient):
|
|
break
|
|
break
|
|
body["nextToken"] = info["nextToken"]
|
|
body["nextToken"] = info["nextToken"]
|
|
|
|
|
|
- def get_budget(self,campaignIds:list):
|
|
|
|
|
|
+ def get_budget(self, campaignIds: list):
|
|
url_path = "/sb/campaigns/budget/usage"
|
|
url_path = "/sb/campaigns/budget/usage"
|
|
- body = {"campaignIds":campaignIds}
|
|
|
|
- return self._request(url_path, method="POST",body=body)
|
|
|
|
- def get_keyword_bidrecommendation(self,**body):
|
|
|
|
|
|
+ body = {"campaignIds": campaignIds}
|
|
|
|
+ return self._request(url_path, method="POST", body=body)
|
|
|
|
+
|
|
|
|
+ def get_keyword_bidrecommendation(self, **body):
|
|
url_path = "/sb/recommendations/bids"
|
|
url_path = "/sb/recommendations/bids"
|
|
return self._request(url_path, method="POST", body=body)
|
|
return self._request(url_path, method="POST", body=body)
|
|
|
|
|
|
@@ -340,7 +373,7 @@ class SBClient(BaseClient):
|
|
|
|
|
|
def download_report(self, report_id: str, file_path: str, decompress: bool = True) -> str:
|
|
def download_report(self, report_id: str, file_path: str, decompress: bool = True) -> str:
|
|
url = urljoin(URL_AD_API, f"/v2/reports/{report_id}/download")
|
|
url = urljoin(URL_AD_API, f"/v2/reports/{report_id}/download")
|
|
- resp = requests.get(url, headers=self.auth_headers, stream=True)
|
|
|
|
|
|
+ resp = requests.get(url, headers=self.auth_headers, stream=True, allow_redirects=True)
|
|
logger.info(f"开始下载报告:{report_id}")
|
|
logger.info(f"开始下载报告:{report_id}")
|
|
with open(file_path, "wb") as file:
|
|
with open(file_path, "wb") as file:
|
|
for data in resp.iter_content(chunk_size=10 * 1024):
|
|
for data in resp.iter_content(chunk_size=10 * 1024):
|
|
@@ -358,11 +391,17 @@ class SDClient(BaseClient):
|
|
url_path = "/sd/campaigns"
|
|
url_path = "/sd/campaigns"
|
|
return self._request(url_path, params=params)
|
|
return self._request(url_path, params=params)
|
|
|
|
|
|
|
|
+
|
|
class Account(BaseClient):
|
|
class Account(BaseClient):
|
|
- def get_portfolio(self):
|
|
|
|
|
|
+ def get_portfolios(self):
|
|
url_path = "/v2/portfolios/extended"
|
|
url_path = "/v2/portfolios/extended"
|
|
return self._request(url_path)
|
|
return self._request(url_path)
|
|
|
|
|
|
|
|
+ def iter_portfolios(self):
|
|
|
|
+ yield from self.get_portfolios()
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+AccountClient = Account
|
|
|
|
|
|
if __name__ == '__main__':
|
|
if __name__ == '__main__':
|
|
AWS_CREDENTIALS = {
|
|
AWS_CREDENTIALS = {
|
|
@@ -372,43 +411,47 @@ if __name__ == '__main__':
|
|
'profile_id': "3006125408623189"
|
|
'profile_id': "3006125408623189"
|
|
}
|
|
}
|
|
# sp = SPClient(**AWS_CREDENTIALS)
|
|
# sp = SPClient(**AWS_CREDENTIALS)
|
|
- # print(sp.get_keyword_bidrecommendation(adGroupId="119753215871672",keyword=["8mp security camera system","8mp security camera system"],matchType=["broad","exact"]))
|
|
|
|
|
|
+ # print(sp.get_keyword_bidrecommendation(
|
|
|
|
+ # adGroupId="119753215871672",
|
|
|
|
+ # keyword=["8mp security camera system","8mp security camera system"],
|
|
|
|
+ # matchType=["broad","exact"]))
|
|
sb = SBClient(**AWS_CREDENTIALS)
|
|
sb = SBClient(**AWS_CREDENTIALS)
|
|
# print(list(sb.iter_targets()))
|
|
# print(list(sb.iter_targets()))
|
|
- print(sb.get_keyword_bidrecommendation(**{'campaignId': 27333596383941,'keywords':[{"matchType":'broad',"keywordText":"4k security camera system"}]}))
|
|
|
|
|
|
+ print(sb.get_keyword_bidrecommendation(**{'campaignId': 27333596383941, 'keywords': [
|
|
|
|
+ {"matchType": 'broad', "keywordText": "4k security camera system"}]}))
|
|
print(sb.get_budget([27333596383941]))
|
|
print(sb.get_budget([27333596383941]))
|
|
# sd = SDClient(**AWS_CREDENTIALS)
|
|
# sd = SDClient(**AWS_CREDENTIALS)
|
|
# print(sd.get_campaigns(startIndex=10, count=10))
|
|
# print(sd.get_campaigns(startIndex=10, count=10))
|
|
|
|
|
|
- # sb = SBClient(**AWS_CREDENTIALS)
|
|
|
|
- # metrics = [
|
|
|
|
- # 'applicableBudgetRuleId',
|
|
|
|
- # 'applicableBudgetRuleName',
|
|
|
|
- # 'attributedConversions14d',
|
|
|
|
- # 'attributedConversions14dSameSKU',
|
|
|
|
- # 'attributedDetailPageViewsClicks14d',
|
|
|
|
- # 'attributedOrderRateNewToBrand14d',
|
|
|
|
- # 'attributedOrdersNewToBrand14d',
|
|
|
|
- # 'attributedOrdersNewToBrandPercentage14d',
|
|
|
|
- # 'attributedSales14d',
|
|
|
|
- # 'attributedSales14dSameSKU',
|
|
|
|
- # 'attributedSalesNewToBrand14d',
|
|
|
|
- # 'attributedSalesNewToBrandPercentage14d',
|
|
|
|
- # 'attributedUnitsOrderedNewToBrand14d',
|
|
|
|
- # 'attributedUnitsOrderedNewToBrandPercentage14d',
|
|
|
|
- # 'campaignBudget',
|
|
|
|
- # 'campaignBudgetType',
|
|
|
|
- # 'campaignId',
|
|
|
|
- # 'campaignName',
|
|
|
|
- # 'campaignRuleBasedBudget',
|
|
|
|
- # 'campaignStatus',
|
|
|
|
- # 'clicks',
|
|
|
|
- # 'cost',
|
|
|
|
- # 'dpv14d',
|
|
|
|
- # 'impressions',
|
|
|
|
- # 'unitsSold14d',
|
|
|
|
- # 'attributedBrandedSearches14d',
|
|
|
|
- # 'topOfSearchImpressionShare']
|
|
|
|
|
|
+ sb = SBClient(**AWS_CREDENTIALS)
|
|
|
|
+ metrics = [
|
|
|
|
+ 'applicableBudgetRuleId',
|
|
|
|
+ 'applicableBudgetRuleName',
|
|
|
|
+ 'attributedConversions14d',
|
|
|
|
+ 'attributedConversions14dSameSKU',
|
|
|
|
+ 'attributedDetailPageViewsClicks14d',
|
|
|
|
+ 'attributedOrderRateNewToBrand14d',
|
|
|
|
+ 'attributedOrdersNewToBrand14d',
|
|
|
|
+ 'attributedOrdersNewToBrandPercentage14d',
|
|
|
|
+ 'attributedSales14d',
|
|
|
|
+ 'attributedSales14dSameSKU',
|
|
|
|
+ 'attributedSalesNewToBrand14d',
|
|
|
|
+ 'attributedSalesNewToBrandPercentage14d',
|
|
|
|
+ 'attributedUnitsOrderedNewToBrand14d',
|
|
|
|
+ 'attributedUnitsOrderedNewToBrandPercentage14d',
|
|
|
|
+ 'campaignBudget',
|
|
|
|
+ 'campaignBudgetType',
|
|
|
|
+ 'campaignId',
|
|
|
|
+ 'campaignName',
|
|
|
|
+ 'campaignRuleBasedBudget',
|
|
|
|
+ 'campaignStatus',
|
|
|
|
+ 'clicks',
|
|
|
|
+ 'cost',
|
|
|
|
+ 'dpv14d',
|
|
|
|
+ 'impressions',
|
|
|
|
+ 'unitsSold14d',
|
|
|
|
+ 'attributedBrandedSearches14d',
|
|
|
|
+ 'topOfSearchImpressionShare']
|
|
# sb.get_report(
|
|
# sb.get_report(
|
|
# record_type="campaigns",
|
|
# record_type="campaigns",
|
|
# report_date="20231008",
|
|
# report_date="20231008",
|