Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Usability updates #1

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
abf27b8
EEB-1317 Python 2/3 compatibility
Sep 20, 2017
93e315d
EEB-1317 - Fix reference error
Sep 20, 2017
254e492
EEB-1317 - Add setup.py
Sep 20, 2017
6e7ed74
EEB-1317 - Allow refresh when no access token exists.
Sep 27, 2017
c97d40d
EEB-1317 - Fix error in setup
Sep 28, 2017
461179f
EEB-1317 - Add state check during token refresh
Sep 28, 2017
8300866
EEB-1317 - Fix typo in state check
Sep 28, 2017
515d362
EEB-1317 - Remove state check since it's not returned in access token…
Oct 3, 2017
81f43c2
EEB-1317 - Require setting a profile ID before making most calls
Oct 4, 2017
641b1c8
EEB-1317 - Allow passing profile id at init
Oct 4, 2017
b01e9b4
EEB-1317 - Add error details to HTTPErrors to make them less obscure
Oct 6, 2017
e2af8a2
Clean up response for HTTPErrors while retaining details.
Oct 20, 2017
be3c5f4
Convert to new style class
Oct 20, 2017
7d6e957
Return error on insufficent parameters when requesting snapshots and …
Oct 26, 2017
4e4791a
EEB-1310 - WIP submitter
Nov 9, 2017
a2fa1b5
Implement register_profile, fix Python 2/3 incompatibility issue due …
Nov 7, 2017
e14cf83
EEB-1310 - Fix install issue
Nov 17, 2017
9cf13f9
EEB-1554 - Start adding recommendations to API
Jan 10, 2018
b248308
Add FE endpoint to regions
Jan 20, 2018
0a763da
Remove FE endpoint, wishful thinking.
Jan 20, 2018
fc080e1
Added check for 200 code before trying to download the report
Aug 10, 2018
ff583d3
Added link to Amazon documentation
sguermond Oct 23, 2018
c91dbf7
Update README.md
sguermond Aug 28, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,5 @@ target/

localtest.py

# Pycharm
*.idea/
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
Unofficial Amazon Sponsored Products Python client library.
⚠️ This repository is no longer being maintained. I recommend using [hartjet](https://github.com/hartjet/amazon-advertising-api-python) or [pepsico-ecommerce](https://github.com/pepsico-ecommerce/amazon-advertising-api-python) for more actively maintained versions. ⚠️

# Unofficial [Amazon Sponsored Products Python client library (v1.0)](https://advertising.amazon.com/API/docs/v1/guides/get_started)
132 changes: 115 additions & 17 deletions amazon_advertising_api/advertising_api.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
from amazon_advertising_api.regions import regions
from amazon_advertising_api.versions import versions
from io import BytesIO
import urllib.request
import urllib.parse
try:
# Python 3
import urllib.request
import urllib.parse
PYTHON = 3
except ImportError:
# Python 2
from six.moves import urllib
PYTHON = 2
import gzip
import json


class AdvertisingApi:
class AdvertisingApi(object):

"""Lightweight client library for Amazon Sponsored Products API."""

def __init__(self,
client_id,
client_secret,
region,
profile_id=None,
access_token=None,
refresh_token=None,
sandbox=False):
Expand Down Expand Up @@ -43,7 +51,7 @@ def __init__(self,
self.api_version = versions['api_version']
self.user_agent = 'AdvertisingAPI Python Client Library v{}'.format(
versions['application_version'])
self.profile_id = None
self.profile_id = profile_id
self.token_url = None

if region in regions:
Expand All @@ -53,7 +61,7 @@ def __init__(self,
self.endpoint = regions[region]['prod']
self.token_url = regions[region]['token_url']
else:
raise KeyError('Region {} not found in regions.'.format(regions))
raise KeyError('Region {} not found in regions.'.format(region))

@property
def access_token(self):
Expand All @@ -70,7 +78,8 @@ def do_refresh_token(self):
'code': 0,
'response': 'refresh_token is empty.'}

self._access_token = urllib.parse.unquote(self._access_token)
if self._access_token:
self._access_token = urllib.parse.unquote(self._access_token)
self.refresh_token = urllib.parse.unquote(self.refresh_token)

params = {
Expand Down Expand Up @@ -101,7 +110,24 @@ def do_refresh_token(self):
except urllib.error.HTTPError as e:
return {'success': False,
'code': e.code,
'response': e.msg}
'response': '{msg}: {details}'.format(msg=e.msg, details=e.read())}

def register_profile(self, country_code):
"""
Registers a sandbox profile.

:PUT: /profiles/register
:param country_code: The country in which to register the profile.
Country code can be one of the following:
US, CA, UK, DE, FR, ES, IT, IN, CN, JP
:returns:
:200: Success
:401: Unauthorized
"""
interface = 'profiles/register'
params = {"countryCode": country_code}
method = 'PUT'
return self._operation(interface, params, method)

def get_profiles(self):
"""
Expand Down Expand Up @@ -136,7 +162,7 @@ def update_profiles(self, data):
profileIds.

:PUT: /profiles
:param data: A list of updates containing **proflileId** and the
:param data: A list of updates containing **profileId** and the
mutable fields to be modified. Only daily budgets are mutable at
this time.
:type data: List of **Profile**
Expand Down Expand Up @@ -602,25 +628,51 @@ def list_product_ads_ex(self, data=None):
return self._operation(interface, data)

def request_snapshot(self, record_type=None, snapshot_id=None, data=None):
"""
Required data:
* :campaignType: The type of campaign for which snapshot should be generated. Must be sponsoredProducts.
"""
if not data:
data = {'campaignType': 'sponsoredProducts'}
elif not data.get('campaignType'):
data['campaignType'] = 'sponsoredProducts'

if record_type is not None:
interface = '{}/snapshot'.format(record_type)
return self._operation(interface, data, method='POST')
elif snapshot_id is not None:
interface = 'snapshots/{}'.format(snapshot_id)
return self._operation(interface, data)
else:
return {'success': False,
'code': 0,
'response': 'record_type and snapshot_id are both empty.'}

def request_report(self, record_type=None, report_id=None, data=None):
"""
Required data:
* :campaignType: The type of campaign for which report should be generated. Must be sponsoredProducts.
"""
if not data:
data = {'campaignType': 'sponsoredProducts'}
elif not data.get('campaignType'):
data['campaignType'] = 'sponsoredProducts'

if record_type is not None:
interface = '{}/report'.format(record_type)
return self._operation(interface, data, method='POST')
elif report_id is not None:
interface = 'reports/{}'.format(report_id)
return self._operation(interface)
else:
return {'success': False,
'code': 0,
'response': 'record_type and report_id are both empty.'}

def get_report(self, report_id):
interface = 'reports/{}'.format(report_id)
res = self._operation(interface)
if json.loads(res['response'])['status'] == 'SUCCESS':
if res['code'] == 200 and json.loads(res['response'])['status'] == 'SUCCESS':
res = self._download(
location=json.loads(res['response'])['location'])
return res
Expand All @@ -637,6 +689,26 @@ def get_snapshot(self, snapshot_id):
else:
return res

def get_ad_group_bid_recommendations(self, ad_group_id):
"""Request bid recommendations for specified ad group."""
interface = 'adGroups/{}/bidRecommendations'.format(ad_group_id)
return self._operation(interface)

def get_keyword_bid_recommendations(self, keyword_id=None, keyword_data=None):
"""
Request bid recommendations for:

* a specified keyword
* a list of up to 100 keywords

A list of keywords must be in the KeywordBidRecommendationsData format:

```
int adGroupId: []
```
"""
pass

def _download(self, location):
headers = {'Authorization': 'Bearer {}'.format(self._access_token),
'Content-Type': 'application/json',
Expand Down Expand Up @@ -665,16 +737,16 @@ def _download(self, location):
'response': json.loads(data.decode('utf-8'))}
else:
return {'success': False,
'code': res.code,
'code': response.code,
'response': 'Location is empty.'}
else:
return {'success': False,
'code': res.code,
'code': response.code,
'response': 'Location not found.'}
except urllib.error.HTTPError as e:
return {'success': False,
'code': e.code,
'response': e.msg}
'response': '{msg}: {details}'.format(msg=e.msg, details=e.read())}

def _operation(self, interface, params=None, method='GET'):
"""
Expand All @@ -684,20 +756,25 @@ def _operation(self, interface, params=None, method='GET'):
:type interface: string
:param params: Parameters associated with this call.
:type params: GET: string POST: dictionary
:param method: Call method. Should be either 'GET' or 'POST'
:param method: Call method. Should be either 'GET', 'PUT', or 'POST'
:type method: string
"""
if self._access_token is None:
return {'success': False,
'code': 0,
'response': 'access_token is empty.'}

headers = {'Authorization': 'bearer {}'.format(self._access_token),
headers = {'Authorization': 'Bearer {}'.format(self._access_token),
'Content-Type': 'application/json',
'User-Agent': self.user_agent}

if self.profile_id is not None and self.profile_id != '':
headers['Amazon-Advertising-API-Scope'] = self.profile_id
elif 'profiles' not in interface:
# Profile ID is required for all calls beyond authentication and getting profile info
return {'success': False,
'code': 0,
'response': 'profile_id is empty.'}

data = None

Expand All @@ -721,7 +798,10 @@ def _operation(self, interface, params=None, method='GET'):
api_version=self.api_version,
interface=interface)

req = urllib.request.Request(url=url, headers=headers, data=data)
if PYTHON == 3:
req = urllib.request.Request(url=url, headers=headers, data=data)
else:
req = MethodRequest(url=url, headers=headers, data=data, method=method)
req.method = method

try:
Expand All @@ -732,11 +812,10 @@ def _operation(self, interface, params=None, method='GET'):
except urllib.error.HTTPError as e:
return {'success': False,
'code': e.code,
'response': e.msg}
'response': '{msg}: {details}'.format(msg=e.msg, details=e.read())}


class NoRedirectHandler(urllib.request.HTTPErrorProcessor):

"""Handles report and snapshot redirects."""

def http_response(self, request, response):
Expand All @@ -751,3 +830,22 @@ def http_response(self, request, response):
self, request, response)

https_response = http_response


class MethodRequest(urllib.request.Request):
"""
When not using Python 3 and the requests library.
Source: Ed Marshall, https://gist.github.com/logic/2715756
"""
def __init__(self, *args, **kwargs):
if 'method' in kwargs:
self._method = kwargs['method']
del kwargs['method']
else:
self._method = None
return urllib.request.Request.__init__(self, *args, **kwargs)

def get_method(self, *args, **kwargs):
if self._method is not None:
return self._method
return urllib.request.Request.get_method(self, *args, **kwargs)
1 change: 1 addition & 0 deletions amazon_advertising_api/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
six
10 changes: 10 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from setuptools import setup
import amazon_advertising_api.versions as aa_versions


setup(
name='amazon_advertising_api',
packages=['amazon_advertising_api'],
version=aa_versions.versions['application_version'],
description='Unofficial Amazon Sponsored Products Python client library.',
url='https://github.com/sguermond/amazon-advertising-api-python')