From 190d655ad4a63bb26aba90e88ae2b5e9da9652e3 Mon Sep 17 00:00:00 2001 From: denisneuf Date: Tue, 9 Nov 2021 23:25:19 +0800 Subject: [PATCH] version 0.1.0 --- README.md | 13 ++- ad_api/api/sb/bid_recommendations.py | 1 + ad_api/api/sb/product_targeting.py | 4 +- ad_api/api/sd/__init__.py | 4 +- ad_api/api/sd/ad_groups.py | 153 +++++++++++++++++++++++++++ ad_api/api/sd/campaigns.py | 138 +++++++++++++++++++++++- ad_api/base/base_client.py | 5 +- ad_api/base/client.py | 83 ++++++++++++--- ad_api/version.py | 1 + 9 files changed, 376 insertions(+), 26 deletions(-) create mode 100644 ad_api/api/sd/ad_groups.py create mode 100644 ad_api/version.py diff --git a/README.md b/README.md index 57290de..d2993ce 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ back to `%HOME%\AppData\Roaming` if undefined ### Modules Available Sponsored Brands * Campaigns -* AdGroups +* Ad Groups * Keywords * Negative Keywords * Product Targeting @@ -130,8 +130,13 @@ back to `%HOME%\AppData\Roaming` if undefined * Brands * Moderation +### Modules Available Sponsored Display + +* Campaigns +* Ad Groups + -### Usage Campaigns +### Example Usage Campaigns ```python import logging @@ -158,8 +163,8 @@ try: logging.info(len(campaigns)) -except AdvertisingApiException as ex: - print(ex) +except AdvertisingApiException as error: + logging.info(error) ``` diff --git a/ad_api/api/sb/bid_recommendations.py b/ad_api/api/sb/bid_recommendations.py index 29a57b7..470412b 100644 --- a/ad_api/api/sb/bid_recommendations.py +++ b/ad_api/api/sb/bid_recommendations.py @@ -29,4 +29,5 @@ def get_bid_recommendations(self, **kwargs) -> ApiResponse: ApiResponse """ + return self._request(kwargs.pop('path'), data=kwargs.pop('body'), params=kwargs) diff --git a/ad_api/api/sb/product_targeting.py b/ad_api/api/sb/product_targeting.py index a525ccb..8a79b33 100644 --- a/ad_api/api/sb/product_targeting.py +++ b/ad_api/api/sb/product_targeting.py @@ -27,7 +27,9 @@ def list_products_targets(self, **kwargs) -> ApiResponse: ApiResponse """ - return self._request(kwargs.pop('path'), data=kwargs.pop('body'), params=kwargs) + contentType = 'application/vnd.sblisttargetsrequest.v3.0+json' + headers = {'Content-Type': contentType} + return self._request(kwargs.pop('path'), data=kwargs.pop('body'), params=kwargs, headers=headers) @sp_endpoint('/sb/targets', method='PUT') diff --git a/ad_api/api/sd/__init__.py b/ad_api/api/sd/__init__.py index 6ad39b4..a291940 100644 --- a/ad_api/api/sd/__init__.py +++ b/ad_api/api/sd/__init__.py @@ -1,4 +1,6 @@ from .campaigns import Campaigns +from .ad_groups import AdGroups __all__ = [ - "Campaigns" + "Campaigns", + "AdGroups" ] diff --git a/ad_api/api/sd/ad_groups.py b/ad_api/api/sd/ad_groups.py new file mode 100644 index 0000000..9cc25b9 --- /dev/null +++ b/ad_api/api/sd/ad_groups.py @@ -0,0 +1,153 @@ +from ad_api.base import Client, sp_endpoint, fill_query_params, ApiResponse + +class AdGroups(Client): + + @sp_endpoint('/sd/adGroups', method='GET') + def list_ad_groups(self, **kwargs) -> ApiResponse: + r""" + list_ad_groups(self, \*\*kwargs) -> ApiResponse + + Gets an array of AdGroup objects for a requested set of Sponsored Display ad groups. Note that the AdGroup object is designed for performance, and includes a small set of commonly used fields to reduce size. If the extended set of fields is required, use the ad group operations that return the AdGroupResponseEx object. + + query **startIndex**:*integer* | Optional. Sets a cursor into the requested set of campaigns. Use in conjunction with the count parameter to control pagination of the returned array. 0-indexed record offset for the result set, defaults to 0. + + query **count**:*integer* | Optional. Sets the number of AdGroup objects in the returned array. Use in conjunction with the startIndex parameter to control pagination. For example, to return the first ten ad groups set startIndex=0 and count=10. To return the next ten ad groups, set startIndex=10 and count=10, and so on. Defaults to max page size. + + query **stateFilter**:*string* | Optional. The returned array is filtered to include only ad groups with state set to one of the values in the specified comma-delimited list. Available values : enabled, paused, archived, enabled, paused, enabled, archived, paused, archived, enabled, paused, archived Default value : enabled, paused, archived + + query **campaignIdFilter**:*string* | Optional. The returned array is filtered to include only ad groups associated with the campaign identifiers in the specified comma-delimited list. + + query **adGroupIdFilter**:*string* | Optional. The returned array is filtered to include only ad groups with an identifier specified in the comma-delimited list. + + query **name**:*string* | Optional. The returned array includes only ad groups with the specified name. + + + + Returns: + + ApiResponse + + """ + return self._request(kwargs.pop('path'), params=kwargs) + + @sp_endpoint('/sd/adGroups', method='PUT') + def edit_ad_groups(self, **kwargs) -> ApiResponse: + r""" + edit_ad_group(self, \*\*kwargs) -> ApiResponse + + Updates one or more ad groups. + + body: | REQUIRED {'description': 'An array of ad groups.}' + + | '**adGroupId**': *number*, {'description': 'The identifier of the ad group.'} + | '**name**': *string*, {'description': 'The name of the ad group.'} + | '**defaultBid**': *number($float)*, {'description': 'The bid value used when no bid is specified for keywords in the ad group.', 'minimum': '0.02'} + | '**state**': *string*, {'description': 'The current resource state', 'Enum': '[ enabled, paused, archived ]'} + + Returns: + + ApiResponse + + """ + return self._request(kwargs.pop('path'), data=kwargs.pop('body'), params=kwargs) + + @sp_endpoint('/sd/adGroups', method='POST') + def create_ad_groups(self, **kwargs) -> ApiResponse: + r""" + create_ad_groups(self, \*\*kwargs) -> ApiResponse + + Creates one or more ad groups. + + body: | REQUIRED {'description': 'An array of ad groups.}' + + | '**name**': *string*, {'description': 'A name for the ad group'} + | '**campaignId**': *number*, {'description': 'An existing campaign to which the ad group is associated'} + | '**defaultBid**': *number($float)*, {'description': 'A bid value for use when no bid is specified for keywords in the ad group', 'minimum': '0.02'} + | '**state**': *string*, {'description': 'A name for the ad group', 'Enum': '[ enabled, paused, archived ]'} + + + Returns: + + ApiResponse + + """ + return self._request(kwargs.pop('path'), data=kwargs.pop('body'), params=kwargs) + + + @sp_endpoint('/sd/adGroups/{}', method='GET') + def get_ad_group(self, adGroupId, **kwargs) -> ApiResponse: + r""" + + get_ad_group(self, adGroupId, \*\*kwargs) -> ApiResponse + + Gets an ad group specified by identifier. + + path **adGroupId**:*number* | Required. The identifier of an existing ad group. + + Returns: + + ApiResponse + + """ + return self._request(fill_query_params(kwargs.pop('path'), adGroupId), params=kwargs) + + + @sp_endpoint('/sd/adGroups/{}', method='DELETE') + def delete_ad_group(self, adGroupId, **kwargs) -> ApiResponse: + r""" + + delete_ad_group(self, adGroupId, \*\*kwargs) -> ApiResponse + + Sets the ad group status to archived. Archived entities cannot be made active again. See developer notes for more information. + + path **adGroupId**:*number* | Required. The identifier of an existing ad group. + + Returns: + + ApiResponse + + """ + return self._request(fill_query_params(kwargs.pop('path'), adGroupId), params=kwargs) + + @sp_endpoint('/sd/adGroups/extended', method='GET') + def list_ad_groups_extended(self, **kwargs) -> ApiResponse: + r""" + list_ad_groups_extended(self, \*\*kwargs) -> ApiResponse + + Gets an array of AdGroup objects for a requested set of Sponsored Display ad groups. Note that the AdGroup object is designed for performance, and includes a small set of commonly used fields to reduce size. If the extended set of fields is required, use the ad group operations that return the AdGroupResponseEx object. + + query: **startIndex**:*integer* | Optional. Sets a cursor into the requested set of campaigns. Use in conjunction with the count parameter to control pagination of the returned array. 0-indexed record offset for the result set, defaults to 0. + + query **count**:*integer* | Optional. Sets the number of AdGroup objects in the returned array. Use in conjunction with the startIndex parameter to control pagination. For example, to return the first ten ad groups set startIndex=0 and count=10. To return the next ten ad groups, set startIndex=10 and count=10, and so on. Defaults to max page size. + + query **stateFilter**:*string* | Optional. The returned array is filtered to include only ad groups with state set to one of the values in the specified comma-delimited list. Available values : enabled, paused, archived, enabled, paused, enabled, archived, paused, archived, enabled, paused, archived Default value : enabled, paused, archived + + query **campaignIdFilter**:*string* | Optional. The returned array is filtered to include only ad groups associated with the campaign identifiers in the specified comma-delimited list. + + query **adGroupIdFilter**:*string* | Optional. The returned array is filtered to include only ad groups with an identifier specified in the comma-delimited list. + + query **name**:*string* | Optional. The returned array includes only ad groups with the specified name. + + Returns: + + ApiResponse + + """ + return self._request(kwargs.pop('path'), params=kwargs) + + @sp_endpoint('/sd/adGroups/extended/{}', method='GET') + def get_ad_group_extended(self, adGroupId, **kwargs) -> ApiResponse: + r""" + + get_ad_group_extended(self, adGroupId, \*\*kwargs) -> ApiResponse + + Gets an ad group that has extended data fields. + + path **adGroupId**:*number* | Required. The identifier of an existing ad group. + + Returns: + + ApiResponse + + """ + return self._request(fill_query_params(kwargs.pop('path'), adGroupId), params=kwargs) diff --git a/ad_api/api/sd/campaigns.py b/ad_api/api/sd/campaigns.py index a47c667..5a3a64b 100644 --- a/ad_api/api/sd/campaigns.py +++ b/ad_api/api/sd/campaigns.py @@ -2,7 +2,7 @@ class Campaigns(Client): - @sp_endpoint('/v2/sd/campaigns', method='GET') + @sp_endpoint('/sd/campaigns', method='GET') def list_campaigns(self, **kwargs) -> ApiResponse: r""" list_campaigns(self, **kwargs) -> ApiResponse @@ -27,3 +27,139 @@ def list_campaigns(self, **kwargs) -> ApiResponse: """ return self._request(kwargs.pop('path'), params=kwargs) + + @sp_endpoint('/sd/campaigns', method='PUT') + def edit_campaigns(self, **kwargs) -> ApiResponse: + r""" + edit_campaigns(self, **kwargs) -> ApiResponse + + Updates one or more campaigns. + + body: | REQUIRED {'description': 'An array of ad groups.}' + + | '**campaignId**': *number*, {'description': 'The identifier of an existing campaign to update.'} + | '**portfolioId**': *number*, {'description': 'The identifier of an existing portfolio to which the campaign is associated'} + | '**name**': *string*, {'description': 'The name for the campaign'} + | '**tags**': *CampaignTags*, {'description': 'A list of advertiser-specified custom identifiers for the campaign. Each customer identifier is a key-value pair. You can specify a maximum of 50 identifiers.'} + | '**state**': *string*, {'description': 'The current resource state.', 'Enum': '[ enabled, paused, archived ]'} + | '**dailyBudget**': *number($float)*, {'description': 'The daily budget for the campaign.'} + | '**startDate**': *string*, {'description': 'The starting date for the campaign to go live. The format of the date is YYYYMMDD.'} + | '**endDate**': *string* nullable: true, {'description': 'The ending date for the campaign to stop running. The format of the date is YYYYMMDD.'} + | '**premiumBidAdjustment**': *boolean*, {'description': 'If set to true, Amazon increases the default bid for ads that are eligible to appear in this placement. See developer notes for more information.'} + | '**bidding**': *Bidding*, {'strategy': 'string', 'Enum': '[ legacyForSales, autoForSales, manual ]', 'adjustments': '{...}'} + + Returns: + + ApiResponse + + """ + return self._request(kwargs.pop('path'), data=kwargs.pop('body'), params=kwargs) + + @sp_endpoint('/sd/campaigns', method='POST') + def create_campaigns(self, **kwargs) -> ApiResponse: + r""" + create_campaigns(self, **kwargs) -> ApiResponse + + Creates one or more campaigns. + + body: | REQUIRED {'description': 'An array of ad groups.}' + + | '**portfolioId**': *number*, {'description': 'The identifier of an existing portfolio to which the campaign is associated'} + | '**name**': *string*, {'description': 'A name for the campaign'} + | '**tags**': *string*, {'description': 'A list of advertiser-specified custom identifiers for the campaign. Each customer identifier is a key-value pair. You can specify a maximum of 50 identifiers.'} + | '**campaignType**': *string*, {'description': 'The advertising product managed by this campaign', 'Enum': '[ sponsoredProducts ]'} + | '**targetingType**': *string*, {'description': 'The type of targeting for the campaign.', 'Enum': '[ manual, auto ]'} + | '**state**': *string*, {'description': 'The current resource state.', 'Enum': '[ enabled, paused, archived ]'} + | '**dailyBudget**': *number($float)*, {'description': 'A daily budget for the campaign.'} + | '**startDate**': *string*, {'description': 'A starting date for the campaign to go live. The format of the date is YYYYMMDD.'} + | '**endDate**': *string* nullable: true, {'description': 'An ending date for the campaign to stop running. The format of the date is YYYYMMDD.'} + | '**premiumBidAdjustment**': *boolean*, {'description': 'If set to true, Amazon increases the default bid for ads that are eligible to appear in this placement. See developer notes for more information.'} + | '**bidding**': *Bidding*, {'strategy': 'string', 'Enum': '[ legacyForSales, autoForSales, manual ]', 'adjustments': '{...}'} + + Returns: + + ApiResponse + + """ + return self._request(kwargs.pop('path'), data=kwargs.pop('body'), params=kwargs) + + + + + + @sp_endpoint('/sd/campaigns/{}', method='GET') + def get_campaign(self, campaignId, **kwargs) -> ApiResponse: + r""" + + get_campaign(self, campaignId, **kwargs) -> ApiResponse + + Gets a campaign specified by identifier. + + path **campaignId**:*number* | Required. The identifier of an existing campaign. + + Returns: + + ApiResponse + + """ + return self._request(fill_query_params(kwargs.pop('path'), campaignId), params=kwargs) + + @sp_endpoint('/sd/campaigns/{}', method='DELETE') + def delete_campaign(self, campaignId, **kwargs) -> ApiResponse: + r""" + + delete_campaign(self, campaignId, **kwargs) -> ApiResponse + + Sets the campaign status to archived. Archived entities cannot be made active again. See developer notes for more information. + + path **campaignId**:*number* | Required. The identifier of an existing campaign. + + Returns: + + ApiResponse + + """ + return self._request(fill_query_params(kwargs.pop('path'), campaignId), params=kwargs) + + @sp_endpoint('/sd/campaigns/extended', method='GET') + def list_campaigns_extended(self, **kwargs) -> ApiResponse: + r""" + list_campaigns_extended(self, **kwargs) -> ApiResponse + + Gets an array of campaigns with extended data fields. + + query **startIndex**:*integer* | Optional. 0-indexed record offset for the result set. Default value : 0 + + query **count**:*integer* | Optional. Number of records to include in the paged response. Defaults to max page size. + + query **stateFilter**:*string* | Optional. The returned array is filtered to include only ad groups with state set to one of the values in the specified comma-delimited list. Available values : enabled, paused, archived, enabled, paused, enabled, archived, paused, archived, enabled, paused, archived Default value : enabled, paused, archived. + + query **name**:*string* | Optional. Restricts results to campaigns with the specified name. + + query **portfolioIdFilter**:*string* | Optional. A comma-delimited list of portfolio identifiers. + + query **campaignIdFilter**:*string* | Optional. A comma-delimited list of campaign identifiers. + + Returns: + + ApiResponse + + """ + return self._request(kwargs.pop('path'), params=kwargs) + + @sp_endpoint('/sd/campaigns/extended/{}', method='GET') + def get_campaign_extended(self, campaignId, **kwargs) -> ApiResponse: + r""" + + get_campaign_extended(self, campaignId, **kwargs) -> ApiResponse + + Gets an array of campaigns with extended data fields. + + path **campaignId**:*number* | Required. The identifier of an existing campaign. + + Returns: + + ApiResponse + + """ + return self._request(fill_query_params(kwargs.pop('path'), campaignId), params=kwargs) diff --git a/ad_api/base/base_client.py b/ad_api/base/base_client.py index 7f17cf3..383be8e 100644 --- a/ad_api/base/base_client.py +++ b/ad_api/base/base_client.py @@ -1,4 +1,5 @@ from ad_api.base.credential_provider import CredentialProvider +import ad_api.version as vd class BaseClient: scheme = 'https://' @@ -6,10 +7,10 @@ class BaseClient: content_type = 'application/x-www-form-urlencoded;charset=UTF-8' user_agent = 'python-ad-api' + def __init__(self, account='default', credentials=None): try: - import pkg_resources - version = pkg_resources.require("python-amazon-ad-api")[0].version + version = vd.__version__ self.user_agent += f'-{version}' except: pass diff --git a/ad_api/base/client.py b/ad_api/base/client.py index adf14d9..f239924 100644 --- a/ad_api/base/client.py +++ b/ad_api/base/client.py @@ -18,6 +18,7 @@ from urllib.parse import urlparse + log = logging.getLogger(__name__) role_cache = TTLCache(maxsize=10, ttl=3600) @@ -28,21 +29,23 @@ def __init__( account='default', marketplace: Marketplaces = Marketplaces.EU, refresh_token=None, - credentials=None + credentials=None, + debug=False ): super().__init__(account, credentials) self.endpoint = marketplace.endpoint + self.debug = debug self._auth = AccessTokenClient(refresh_token=refresh_token, account=account, credentials=credentials) @property def headers(self): return { - 'user-agent': self.user_agent, + 'User-Agent': self.user_agent, 'Amazon-Advertising-API-ClientId': self.credentials.client_id, 'Authorization': 'Bearer %s' % self.auth.access_token, 'Amazon-Advertising-API-Scope': self.credentials.profile_id, - 'content-type': 'application/json' + 'Content-Type': 'application/json' } @property @@ -65,18 +68,18 @@ def _download(self, params: dict = None, headers=None) -> ApiResponse: } next_token = None return ApiResponse(error, next_token, headers=self.headers) - except requests.exceptions.RequestException as e: + except requests.exceptions.ConnectionError as e: error = { 'success': False, - 'code': 503, + 'code': e.status_code, 'response': e } next_token = None return ApiResponse(error, next_token, headers=self.headers) - except requests.exceptions.ConnectionError as e: + except requests.exceptions.RequestException as e: error = { 'success': False, - 'code': e.status_code, + 'code': 503, 'response': e } next_token = None @@ -183,21 +186,31 @@ def _download(self, params: dict = None, headers=None) -> ApiResponse: sys.exit() + def _request(self, + path: str, + data: str = None, + params: dict = None, + headers = None, + ) -> ApiResponse: - - def _request(self, path: str, *, data: str = None, params: dict = None, headers=None, - add_marketplace=True) -> ApiResponse: if params is None: params = {} - if data is None: - data = {} method = params.pop('method') - if method in ('POST', 'PUT', 'PATCH'): + if headers is False: + base_header = self.headers.copy() + base_header.pop("Content-Type") + headers = base_header - if headers is not None: - headers.update(self.headers) + + elif headers is not None: + + base_header = self.headers.copy() + base_header.update(headers) + headers = base_header + + if method in ('POST', 'PUT', 'PATCH'): res = request(method, self.endpoint + path, @@ -211,19 +224,55 @@ def _request(self, path: str, *, data: str = None, params: dict = None, headers= params=params, headers=headers or self.headers) + if self.debug: + logging.info(headers or self.headers) + logging.info(method + " " + self.endpoint + path) + if data is not None: + logging.info(data) + return self._check_response(res) - @staticmethod - def _check_response(res) -> ApiResponse: + # @staticmethod + def _check_response(self, res) -> ApiResponse: + + if self.debug: + logging.info(vars(res)) content = vars(res).get('_content') str_content = content.decode('utf8') + + if type(str_content) is str and str_content[0:50] == '' and vars(res).get('_content_consumed') is True: + + dictionary = {"status_code": vars(res).get('status_code'), "msg": "Unauthorized"} + exception = get_exception_for_content(dictionary) + raise exception(dictionary) + data = json.loads(str_content) if type(data) is dict and data.get('code')=='UNAUTHORIZED': exception = get_exception_for_content(data) raise exception(data) + if type(data) is dict and data.get('message') == 'Unauthorized' and vars(res).get('_content_consumed') is True: + dictionary = {"status_code": vars(res).get('status_code'), "message": "Unauthorized"} + exception = get_exception_for_content(dictionary) + raise exception(dictionary) + + if type(data) is dict and data.get('details') == 'Invalid authorization inputs' and vars(res).get('_content_consumed') is True: + dictionary = {"status_code": vars(res).get('status_code'), "message": "Invalid authorization inputs"} + exception = get_exception_for_content(dictionary) + raise exception(dictionary) + + if type(data) is dict and data.get('message') == 'Missing Authentication Token' and vars(res).get('_content_consumed') is True: + dictionary = {"status_code": vars(res).get('status_code'), "message": "Missing Authentication Token"} + exception = get_exception_for_content(dictionary) + raise exception(dictionary) + + if type(data) is dict and data.get('details') == 'Cannot consume content type' and vars(res).get('_content_consumed') is True: + dictionary = {"status_code": vars(res).get('status_code'), "message": data.get('details')} + exception = get_exception_for_content(dictionary) + raise exception(dictionary) + if vars(res).get('_content') == b'[]' and vars(res).get('_content_consumed') is True: data = json.loads('{"status_code": 200, "msg": "No Data Available"}') diff --git a/ad_api/version.py b/ad_api/version.py new file mode 100644 index 0000000..a68927d --- /dev/null +++ b/ad_api/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" \ No newline at end of file