Skip to content

Commit

Permalink
added sp reports, snapshots
Browse files Browse the repository at this point in the history
  • Loading branch information
denisneuf committed Sep 29, 2021
1 parent 26ca7ac commit 12257f4
Show file tree
Hide file tree
Showing 12 changed files with 428 additions and 14 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## PYTHON-AMAZON-AD-API [AMAZON ADVERTISING]

![CodeQL](https://img.shields.io/badge/coverage-39%25-yellow)
![CodeQL](https://img.shields.io/badge/coverage-59%25-yellow)
![CodeQL](https://img.shields.io/badge/Docs-sphinx-green)
![CodeQL](https://img.shields.io/github/v/release/denisneuf/python-amazon-ad-api)
[![Documentation Status](https://readthedocs.org/projects/python-amazon-ad-api/badge/?version=latest)](https://python-amazon-ad-api.readthedocs.io/en/latest/?badge=latest)
Expand Down
2 changes: 1 addition & 1 deletion ad_api/api/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
class Profiles(Client):
"""
Profiles AD-API Client
:link:
:link:
With the Profiles.
"""

Expand Down
4 changes: 4 additions & 0 deletions ad_api/api/sd/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .campaigns import Campaigns
__all__ = [
"Campaigns"
]
29 changes: 29 additions & 0 deletions ad_api/api/sd/campaigns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from ad_api.base import Client, sp_endpoint, fill_query_params, ApiResponse

class Campaigns(Client):

@sp_endpoint('/v2/sd/campaigns', method='GET')
def list_campaigns(self, **kwargs) -> ApiResponse:
r"""
list_campaigns(self, **kwargs) -> ApiResponse
Gets an array of campaigns.
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)
52 changes: 50 additions & 2 deletions ad_api/api/sp/reports.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,59 @@
from ad_api.base import Client, sp_endpoint, fill_query_params, ApiResponse

class Reports(Client):

"""
Use the Amazon Advertising API for Sponsored Products for campaign, ad group, keyword, negative keyword, and product ad management operations. For more information about Sponsored Products, see the Sponsored Products Support Center. For onboarding information, see the account setup topic.
"""
@sp_endpoint('/v2/sp/{}/report', method='POST')
def post_report(self, recordType, **kwargs) -> ApiResponse:
r"""
Requests a Sponsored Products report.
Request the creation of a performance report for all entities of a single type which have performance data to report. Record types can be one of campaigns, adGroups, keywords, productAds, asins, and targets. Note that for asin reports, the report currently can not include metrics associated with both keywords and targets. If the targetingId value is set in the request, the report filters on targets and does not return sales associated with keywords. If the targetingId value is not set in the request, the report filters on keywords and does not return sales associated with targets. Therefore, the default behavior filters the report on keywords. Also note that if both keywordId and targetingId values are passed, the report filters on targets only and does not return keywords.
Keyword Args
| path **recordType** (integer): The type of entity for which the report should be generated. Available values : campaigns, adGroups, keywords, productAds, asins, targets [required]
Request body
| **stateFilter** (string): [optional] Filters the response to include reports with state set to one of the values in the comma-delimited list. Note that this filter is only valid for reports of the following type and segment. Asins and targets report types are not supported. Enum [ enabled, paused, archived ].
| **campaignType** (list > string): [required] Enum: The type of campaign. Only required for asins report - don't use with other report types. [sponsoredProducts]
| **segment** (string) Dimension on which the report is segmented. Note that Search-terms report for auto-targeted campaigns created before 11/14/2018 can be accessed from the /v2/sp/keywords/report resource. Search-terms report for auto-targeted campaigns generated on-and-after 11/14/2018 can be accessed from the /v2/sp/targets/report resource. Also, keyword search terms reports only return search terms that have generated at least one click or one sale. Enum [ query, placement ].
| **reportDate** (string): [optional] The date for which to retrieve the performance report in YYYYMMDD format. The time zone is specified by the profile used to request the report. If this date is today, then the performance report may contain partial information. Reports are not available for data older than 60 days. For details on data latency, see the Service Guarantees in the developer notes section.
| **metrics** (string) [optional] A comma-separated list of the metrics to be included in the report. The following tables summarize report metrics which can be requested via the reports interface. Different report types can use different metrics. Note that ASIN reports only return data for either keywords or targets, but not both.
Returns:
ApiResponse
"""
return self._request(fill_query_params(kwargs.pop('path'), recordType), data=kwargs.pop('body'), params=kwargs)

@sp_endpoint('/v2/report/{}', method='GET')
@sp_endpoint('/v2/reports/{}', method='GET')
def get_report(self, reportId, **kwargs) -> ApiResponse:
r"""
Gets a previously requested report specified by identifier.
Keyword Args
| path **reportId** (number): The report identifier. [required]
Returns:
ApiResponse
"""
return self._request(fill_query_params(kwargs.pop('path'), reportId), params=kwargs)

def download_report(self, **kwargs) -> ApiResponse:
r"""
Downloads the report previously get report specified by location (this is not part of the official Amazon Advertising API, is a helper method to download the report). Take in mind that a direct download of location returned in get_report will return 401 - Unauthorized.
kwarg parameter **file** if not provided will take the default amazon name from path download (add a path with slash / if you want a specific folder, do not add extension as the return will provide the right extension based on format choosed if needed)
kwarg parameter **format** if not provided a format will return a url to download the report (this url has a expiration time)
Keyword Args
| **url** (string): The location obatined from get_report [required]
| **file** (string): The path to save the file if mode is download json, zip or gzip. [optional]
| **format** (string): The mode to download the report: data (list), raw, url, json, zip, gzip. Default (url) [optional]
Returns:
ApiResponse
"""
return self._download(self, params=kwargs)
49 changes: 45 additions & 4 deletions ad_api/api/sp/snapshots.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,52 @@
from ad_api.base import Client, sp_endpoint, fill_query_params, ApiResponse

class Snapshots(Client):

@sp_endpoint('/v2/sp/{}/snapshots', method='POST')
"""
Use the Amazon Advertising API for Sponsored Products for campaign, ad group, keyword, negative keyword, and product ad management operations. For more information about Sponsored Products, see the Sponsored Products Support Center. For onboarding information, see the account setup topic.
"""
@sp_endpoint('/v2/sp/{}/snapshot', method='POST')
def post_snapshot(self, recordType, **kwargs) -> ApiResponse:
"""
Request a file-based snapshot of all entities of the specified type in the account satisfying the filtering criteria.
Keyword Args
| path **recordType** (integer): The type of entity for which the snapshot is generated. Available values : campaigns, adGroups, keywords, negativeKeywords, campaignNegativeKeywords, productAds, targets, negativeTargets [required]
Request body
| **stateFilter** (string): [required] [ enabled, paused, archived, enabled, paused, enabled, archived, paused, archived, enabled, paused, archived ].
Returns:
ApiResponse
"""
return self._request(fill_query_params(kwargs.pop('path'), recordType), data=kwargs.pop('body'), params=kwargs)

@sp_endpoint('/v2/snapshots/{}', method='GET')
def get_snapshot(self, reportId, **kwargs) -> ApiResponse:
return self._request(fill_query_params(kwargs.pop('path'), reportId), params=kwargs)
def get_snapshot(self, snapshotId, **kwargs) -> ApiResponse:
r"""
Gets the status of a requested snapshot.
Keyword Args
| path **snapshotId** (number): The snapshot identifier. [required]
Returns:
ApiResponse
"""
return self._request(fill_query_params(kwargs.pop('path'), snapshotId), params=kwargs)

def download_snapshot(self, **kwargs) -> ApiResponse:
r"""
Downloads the snapshot previously get report specified by location (this is not part of the official Amazon Advertising API, is a helper method to download the snapshot). Take in mind that a direct download of location returned in get_snapshot will return 401 - Unauthorized.
kwarg parameter **file** if not provided will take the default amazon name from path download (add a path with slash / if you want a specific folder, do not add extension as the return will provide the right extension based on format choosed if needed)
kwarg parameter **format** if not provided a format will return a url to download the snapshot (this url has a expiration time)
Keyword Args
| **url** (string): The location obatined from get_snapshot [required]
| **file** (string): The path to save the file if mode is download json, zip or gzip. [optional]
| **format** (string): The mode to download the snapshot: data (list), raw, url, json, zip, gzip. Default (url) [optional]
Returns:
ApiResponse
"""
return self._download(self, params=kwargs)
148 changes: 147 additions & 1 deletion ad_api/base/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@
from .exceptions import get_exception_for_code, get_exception_for_content, AdvertisingApiBadRequestException
from .marketplaces import Marketplaces
import sys
import os
# import urllib.request
# import urllib.parse
import requests
from io import BytesIO
import gzip
from zipfile import ZipFile
import zipfile

from urllib.parse import urlparse

log = logging.getLogger(__name__)
role_cache = TTLCache(maxsize=10, ttl=3600)
Expand Down Expand Up @@ -41,6 +51,140 @@ def headers(self):
def auth(self) -> AccessTokenResponse:
return self._auth.get_auth()

@staticmethod
def _download(self, params: dict = None, headers=None) -> ApiResponse:

# logging.info(params)
location = params.get("url")

try:
r = requests.get(location, headers=self.headers, data=None, allow_redirects=True)

except requests.exceptions.InvalidSchema as e:
error = {
'success': False,
'code': 400,
'response': e
}
next_token = None
return ApiResponse(error, next_token, headers=self.headers)
except requests.exceptions.RequestException as e:
error = {
'success': False,
'code': 503,
'response': e
}
next_token = None
return ApiResponse(error, next_token, headers=self.headers)
except requests.exceptions.ConnectionError as e:
error = {
'success': False,
'code': e.status_code,
'response': e
}
next_token = None
return ApiResponse(error, next_token, headers=self.headers)

bytes = r.content
mode = params.get("format")

if mode is None:
mode = "url"

name = params.get("file")

if name is None:
o = urlparse(r.url)
name = o.path[1:o.path.find('.')]


if mode == "raw":

next_token = None
return ApiResponse(bytes, next_token, headers=r.headers)

elif mode == "url":

next_token = None
return ApiResponse(r.url, next_token, headers=r.headers)

elif mode == "data":

if bytes[0:2] == b'\x1f\x8b':
logging.info("Is gzip report")
buf = BytesIO(bytes)
f = gzip.GzipFile(fileobj=buf)
read_data = f.read()
next_token = None
return ApiResponse(json.loads(read_data.decode('utf-8')), next_token, headers=r.headers)

else:
logging.info("Is bytes snapshot")
next_token = None
return ApiResponse(json.loads(r.text), next_token, headers=r.headers)

elif mode == "json":
if bytes[0:2] == b'\x1f\x8b':
buf = BytesIO(bytes)
f = gzip.GzipFile(fileobj=buf)
read_data = f.read()
fo = open(name+".json", 'w')
fo.write(read_data.decode('utf-8'))
fo.close()
next_token = None
return ApiResponse(name+".json", next_token, headers=r.headers)
else:
fo = open(name+".json", 'w')
fo.write(r.text)
fo.close()
next_token = None
return ApiResponse(name+".json", next_token, headers=r.headers)

elif mode == "gzip":
fo = gzip.open(name + ".json.gz", 'wb').write(r.content)
next_token = None
return ApiResponse(name + ".json.gz", next_token, headers=r.headers)

elif mode == "zip":

if bytes[0:2] == b'\x1f\x8b':
buf = BytesIO(bytes)
f = gzip.GzipFile(fileobj=buf)
read_data = f.read()
fo = open(name+".json", 'w')
fo.write(read_data.decode('utf-8'))
fo.close()

zipObj = ZipFile(name+'.zip', 'w', zipfile.ZIP_DEFLATED)
zipObj.write(name + ".json")
zipObj.close()
else:
fo = open(name + ".json", 'w')
fo.write(r.text)
fo.close()

zipObj = ZipFile(name+'.zip', 'w', zipfile.ZIP_DEFLATED)
zipObj.write(name + ".json")
zipObj.close()

if os.path.exists(name + ".json"):
os.remove(name + ".json")

next_token = None
return ApiResponse(name + ".zip", next_token, headers=r.headers)


else:

error = {
'success': False,
'code': 400,
'response': 'The mode "%s" is not supported perhaps you could use "data", "raw", "url", "json", "zip" or "gzip"' % (mode)
}
next_token = None
return ApiResponse(error, next_token, headers=self.headers)


def _request(self, path: str, *, data: str = None, params: dict = None, headers=None,
add_marketplace=True) -> ApiResponse:
if params is None:
Expand All @@ -52,6 +196,9 @@ def _request(self, path: str, *, data: str = None, params: dict = None, headers=

if method in ('POST', 'PUT', 'PATCH'):

if headers is not None:
headers.update(self.headers)

res = request(method,
self.endpoint + path,
params=params,
Expand All @@ -74,7 +221,6 @@ def _check_response(res) -> ApiResponse:
data = json.loads(str_content)

if type(data) is dict and data.get('code')=='UNAUTHORIZED':
print(content)
exception = get_exception_for_content(data)
raise exception(data)

Expand Down
6 changes: 4 additions & 2 deletions docs/sp.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ Sponsored Products

sp/campaigns
sp/ad_groups
sp/product_ads
sp/bid_recommendations
sp/keywords
sp/negative_keywords
sp/campaign_negative_keywords
sp/suggested_keywords
sp/product_ads
sp/product_targeting
sp/negative_product_targeting
sp/negative_product_targeting
sp/reports
sp/snapshots
4 changes: 2 additions & 2 deletions docs/sp/negative_product_targeting.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Negative Targets
================
Negative Product Targeting
==========================

.. autoclass:: ad_api.api.sp.NegativeTargets
:members:
Expand Down
2 changes: 1 addition & 1 deletion docs/sp/product_targeting.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Targets
Product Targeting
=================

.. autoclass:: ad_api.api.sp.Targets
Expand Down
Loading

0 comments on commit 12257f4

Please sign in to comment.