diff --git a/.argo-ci/master.yaml b/.argo-ci/master.yaml new file mode 100644 index 0000000..4d36d7a --- /dev/null +++ b/.argo-ci/master.yaml @@ -0,0 +1,255 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: amazon-advertising-api-python-master- +spec: + # entrypoint is the name of the template used as the starting point of the workflow + entrypoint: run-check + onExit: exit-handler + arguments: + parameters: + - name: git_url + value: HAS_TO_BE_PASSED_FROM_SENSOR + - name: repo_name + value: HAS_TO_BE_PASSED_FROM_SENSOR + - name: commit_message + value: HAS_TO_BE_PASSED_FROM_SENSOR + - name: sha + value: HAS_TO_BE_PASSED_FROM_SENSOR + # a temporary volume, named workdir, will be used as a working directory + # for this workflow. This volume is passed around from step to step. + volumeClaimTemplates: + - metadata: + name: workdir + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 1Gi + volumes: + - name: sshkey + secret: + secretName: argo-ci-git-ssh-key + items: + - key: id_rsa + mode: 0400 + path: id_rsa + - name: known-hosts + secret: + secretName: known-github + - name: pypirc + secret: + secretName: nexus-pypi + items: + - key: .pypirc + path: .pypirc + + + + templates: + - name: run-check + steps: + - - name: check-commit-messsage + template: check-message-script + + - - name: run-build + template: run-build + when: "{{steps.check-commit-messsage.outputs.result}} != skip" + + + - name: run-build + steps: + - - name: set-pending + template: status-update + arguments: + parameters: + - name: status + value: pending + - name: message + value: Argo CI tests has been started + + - - name: get-package-name + template: generate-name + + - - name: checkout + template: git + arguments: + parameters: + - name: cmd + value: > + cd /workdir && + git clone {{workflow.parameters.git_url}} && + cd /workdir/{{workflow.parameters.repo_name}} && + git checkout master + + - - name: alpha-version-bump + template: python + arguments: + parameters: + - name: cmd + value: > + pip install bump2version && + git config --global user.email 'argo-ci@pepsi.co' && + git config --global user.name 'Argo CI' && + bump2version patch + --verbose + --allow-dirty + --no-commit + + - - name: pack-and-push + template: python + arguments: + parameters: + - name: cmd + value: > + pip install twine && + python3 setup.py sdist bdist_wheel && + twine upload --config-file /etc/pypirc/.pypirc -r pepsico-ecommerce-dev dist/* + + - - name: version-bump + template: python + arguments: + parameters: + - name: cmd + value: > + pip install bump2version && + git config --global user.email 'argo-ci@pepsi.co' && + git config --global user.name 'Argo CI' && + bump2version release + --serialize {major}.{minor}.{patch} + --verbose + --allow-dirty + + - - name: git-push + template: git + arguments: + parameters: + - name: cmd + value: > + cd /workdir/{{workflow.parameters.repo_name}} && + git push origin master + + + + - name: exit-handler + steps: + - - name: success + template: status-update + arguments: + parameters: + - name: status + value: success + - name: message + value: Version bumped and package deployed. + when: "{{workflow.status}} == Succeeded" + - name: failure + template: status-update + arguments: + parameters: + - name: status + value: failure + - name: message + value: Something went wrong, check logs in details. + when: "{{workflow.status}} != Succeeded" + + - name: git + inputs: + parameters: + - name: cmd + container: + image: "alpine/git" + command: ["sh", "-c"] + args: + - > + git config --global user.email 'argo-ci@pepsi.co' && + git config --global user.name 'Argo CI' && + {{inputs.parameters.cmd}} + volumeMounts: + - name: workdir + mountPath: /workdir + - name: sshkey + mountPath: /root/.ssh + - mountPath: /etc/ssh + name: known-hosts + + - name: python + inputs: + parameters: + - name: cmd + container: + image: "python:3.7-buster" + command: ["sh", "-c"] + args: + - > + cd /workdir/{{workflow.parameters.repo_name}} && + {{inputs.parameters.cmd}} + volumeMounts: + - name: workdir + mountPath: /workdir + - name: pypirc + mountPath: /etc/pypirc/ + env: + - name: NEXUS_PYPI_PASSWORD + valueFrom: + secretKeyRef: + name: nexus-pypi-ro-creds + key: password + - name: NEXUS_PYPI_USERNAME + valueFrom: + secretKeyRef: + name: nexus-pypi-ro-creds + key: username + + - name: status-update + inputs: + parameters: + - name: status + - name: message + container: + image: cloudposse/github-status-updater + env: + - name: GITHUB_ACTION + value: update_state + - name: GITHUB_TOKEN + valueFrom: + secretKeyRef: + name: github-access-token + key: token + - name: GITHUB_OWNER + value: pepsico-ecommerce + - name: GITHUB_REPO + value: "{{workflow.parameters.repo_name}}" + - name: GITHUB_REF + value: "{{workflow.parameters.sha}}" + - name: GITHUB_CONTEXT + value: Dev package published + - name: GITHUB_STATE + value: "{{inputs.parameters.status}}" + - name: GITHUB_DESCRIPTION + value: "{{inputs.parameters.message}}" + - name: GITHUB_TARGET_URL + value: "https://argo-wf.pepstaging.com/workflows/argo-events/{{workflow.name}}" + + - name: check-message-script + script: + image: python:alpine3.7 + command: [python] + source: | + message = """ + {{workflow.parameters.commit_message}} + """ + if "Bump version:" in message: + print("skip") + else: + print("Building") + + - name: generate-name + script: + image: python:alpine3.7 + command: [python] + source: | + repo_name = """ + {{workflow.parameters.repo_name}} + """ + package_name = repo_name.strip().replace('"', '').replace("'", "").replace('-', '_') + print(package_name) diff --git a/.argo-ci/pr.yaml b/.argo-ci/pr.yaml new file mode 100644 index 0000000..60a510f --- /dev/null +++ b/.argo-ci/pr.yaml @@ -0,0 +1,254 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: amazon-advertising-api-python-pr- +spec: + # entrypoint is the name of the template used as the starting point of the workflow + entrypoint: run-tests + onExit: exit-handler + arguments: + parameters: + - name: git_url + value: HAS_TO_BE_PASSED_FROM_SENSOR + - name: source_branch + value: HAS_TO_BE_PASSED_FROM_SENSOR + - name: target_branch + value: HAS_TO_BE_PASSED_FROM_SENSOR + - name: repo_name + value: HAS_TO_BE_PASSED_FROM_SENSOR + - name: source_sha + value: HAS_TO_BE_PASSED_FROM_SENSOR + - name: pr_number + value: HAS_TO_BE_PASSED_FROM_SENSOR + # a temporary volume, named workdir, will be used as a working directory + # for this workflow. This volume is passed around from step to step. + volumeClaimTemplates: + - metadata: + name: workdir + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 1Gi + volumes: + - name: sshkey + secret: + secretName: argo-ci-git-ssh-key + items: + - key: id_rsa + mode: 0400 + path: id_rsa + - name: known-hosts + secret: + secretName: known-github + - name: pypirc + secret: + secretName: nexus-pypi + items: + - key: .pypirc + path: .pypirc + + + + templates: + - name: run-tests + steps: + - - name: set-pending + template: status-update + arguments: + parameters: + - name: status + value: pending + - name: message + value: Argo CI tests has been started + + - - name: checkout + template: git + arguments: + parameters: + - name: cmd + value: > + cd /workdir && + git clone {{workflow.parameters.git_url}} && + cd {{workflow.parameters.repo_name}} && + git fetch && + git merge origin/{{workflow.parameters.source_branch}} + + - - name: get-package-name + template: generate-name + + - - name: version-bump + template: python + arguments: + parameters: + - name: cmd + value: > + pip install bump2version && + git config --global user.email 'argo-ci@pepsi.co' && + git config --global user.name 'Argo CI' && + bump2version release --allow-dirty --verbose + --serialize 0.0.0-dev{{workflow.parameters.source_sha}} && + python setup.py -V > /version + + - - name: pack-and-push + template: python + arguments: + parameters: + - name: cmd + value: > + pip install twine && + python3 setup.py sdist bdist_wheel && + twine upload --verbose --config-file /etc/pypirc/.pypirc -r pepsico-ecommerce-dev dist/* + + - - name: comment-pr + template: PR-comment + arguments: + parameters: + - name: message + value: "Looks good to me! and your package is here \ + {{steps.version-bump.outputs.parameters.version}}" + + + + - name: exit-handler + steps: + - - name: success + template: status-update + arguments: + parameters: + - name: status + value: success + - name: message + value: All tests has passed in argo ci + when: "{{workflow.status}} == Succeeded" + - name: failure + template: status-update + arguments: + parameters: + - name: status + value: failure + - name: message + value: Some tests has failed, check details for logs + when: "{{workflow.status}} != Succeeded" + + - name: git + inputs: + parameters: + - name: cmd + container: + image: "alpine/git" + command: ["sh", "-c"] + args: + - > + git config --global user.email 'argo-ci@pepsi.co' && + git config --global user.name 'Argo CI' && + {{inputs.parameters.cmd}} + volumeMounts: + - name: workdir + mountPath: /workdir + - name: sshkey + mountPath: /root/.ssh + - mountPath: /etc/ssh + name: known-hosts + + - name: python + inputs: + parameters: + - name: cmd + outputs: + parameters: + - name: version # name of output parameter + valueFrom: + path: /version + container: + image: "python:3.7-buster" + command: ["sh", "-c"] + args: + - > + touch /version && + cd /workdir/{{workflow.parameters.repo_name}} && + {{inputs.parameters.cmd}} + volumeMounts: + - name: workdir + mountPath: /workdir + - name: pypirc + mountPath: /etc/pypirc/ + env: + - name: NEXUS_PYPI_PASSWORD + valueFrom: + secretKeyRef: + name: nexus-pypi-ro-creds + key: password + - name: NEXUS_PYPI_USERNAME + valueFrom: + secretKeyRef: + name: nexus-pypi-ro-creds + key: username + + - name: status-update + inputs: + parameters: + - name: status + - name: message + container: + image: cloudposse/github-status-updater + env: + - name: GITHUB_ACTION + value: update_state + - name: GITHUB_TOKEN + valueFrom: + secretKeyRef: + name: github-access-token + key: token + - name: GITHUB_OWNER + value: pepsico-ecommerce + - name: GITHUB_REPO + value: "{{workflow.parameters.repo_name}}" + - name: GITHUB_REF + value: "{{workflow.parameters.source_sha}}" + - name: GITHUB_CONTEXT + value: Argo CI Workflow + - name: GITHUB_STATE + value: "{{inputs.parameters.status}}" + - name: GITHUB_DESCRIPTION + value: "{{inputs.parameters.message}}" + - name: GITHUB_TARGET_URL + value: "https://argo-wf.pepstaging.com/workflows/argo-events/{{workflow.name}}" + + - name: PR-comment + inputs: + parameters: + - name: message + container: + image: cloudposse/github-commenter + env: + - name: GITHUB_TOKEN + valueFrom: + secretKeyRef: + name: github-access-token + key: token + - name: GITHUB_OWNER + value: pepsico-ecommerce + - name: GITHUB_REPO + value: "{{workflow.parameters.repo_name}}" + - name: GITHUB_COMMENT_TYPE + value: pr + - name: GITHUB_PR_ISSUE_NUMBER + value: "{{workflow.parameters.pr_number}}" + - name: GITHUB_COMMENT_FORMAT + value: "Argo CI comment: {{.}}" + - name: GITHUB_COMMENT + value: "{{inputs.parameters.message}}" + - name: GITHUB_DELETE_COMMENT_REGEX + value: "^Argo CI comment" + + - name: generate-name + script: + image: python:alpine3.7 + command: [python] + source: | + repo_name = """ + {{workflow.parameters.repo_name}} + """ + package_name = repo_name.strip().replace('"', '').replace("'", "").replace('-', '_') + print(package_name) diff --git a/.argo-ci/release.yaml b/.argo-ci/release.yaml new file mode 100644 index 0000000..76a660b --- /dev/null +++ b/.argo-ci/release.yaml @@ -0,0 +1,154 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: amazon-advertising-api-python-release- +spec: + # entrypoint is the name of the template used as the starting point of the workflow + entrypoint: run-build + # onExit: exit-handler + arguments: + parameters: + - name: git_url + value: HAS_TO_BE_PASSED_FROM_SENSOR + - name: tag_name + value: HAS_TO_BE_PASSED_FROM_SENSOR + - name: repo_name + value: HAS_TO_BE_PASSED_FROM_SENSOR + + # a temporary volume, named workdir, will be used as a working directory + # for this workflow. This volume is passed around from step to step. + volumeClaimTemplates: + - metadata: + name: workdir + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 1Gi + volumes: + - name: sshkey + secret: + secretName: argo-ci-git-ssh-key + items: + - key: id_rsa + mode: 0400 + path: id_rsa + - name: known-hosts + secret: + secretName: known-github + - name: pypirc + secret: + secretName: nexus-pypi + items: + - key: .pypirc + path: .pypirc + + + + templates: + - name: run-build + steps: + - - name: checkout + template: git + arguments: + parameters: + - name: cmd + value: > + cd /workdir && + git clone {{workflow.parameters.git_url}} && + cd /workdir/{{workflow.parameters.repo_name}} && + git checkout tags/{{workflow.parameters.tag_name}} + + - - name: compare-version + template: python + arguments: + parameters: + - name: cmd + value: > + if [ $(python setup.py --version) = {{workflow.parameters.tag_name}} ]; + then + echo OK; + else + exit 1; + fi +### add notification to slack if fail + + - - name: get-package-name + template: generate-name + + - - name: pack-and-push + template: python + arguments: + parameters: + - name: cmd + value: > + pip install twine && + python3 setup.py sdist bdist_wheel && + twine upload --config-file /etc/pypirc/.pypirc -r pepsico-ecommerce-release dist/* + +### slack notification about new version + + + + - name: git + retryStrategy: + limit: 3 + inputs: + parameters: + - name: cmd + container: + image: "alpine/git" + command: ["sh", "-c"] + args: + - > + git config --global user.email 'argo-ci@pepsi.co' && + git config --global user.name 'Argo CI' && + {{inputs.parameters.cmd}} + volumeMounts: + - name: workdir + mountPath: /workdir + - name: sshkey + mountPath: /root/.ssh + - mountPath: /etc/ssh + name: known-hosts + + - name: python + retryStrategy: + limit: 3 + inputs: + parameters: + - name: cmd + container: + image: "python:3.7-buster" + command: ["sh", "-c"] + args: + - > + cd /workdir/{{workflow.parameters.repo_name}} && + {{inputs.parameters.cmd}} + volumeMounts: + - name: workdir + mountPath: /workdir + - name: pypirc + mountPath: /etc/pypirc/ + env: + - name: NEXUS_PYPI_PASSWORD + valueFrom: + secretKeyRef: + name: nexus-pypi-ro-creds + key: password + - name: NEXUS_PYPI_USERNAME + valueFrom: + secretKeyRef: + name: nexus-pypi-ro-creds + key: username + + - name: generate-name + script: + image: python:alpine3.7 + command: [python] + source: | + repo_name = """ + {{workflow.parameters.repo_name}} + """ + package_name = repo_name.strip().replace('"', '').replace("'", "").replace('-', '_') + print(package_name) diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..bc5e17b --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,18 @@ +[bumpversion] +current_version = 0.0.7 +commit = True +tag = False +message = "Bump version: {current_version} → {new_version}" +parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z0-9]{1,10}))? +serialize = + {major}.{minor}.{patch}-{release} + {major}.{minor}.{patch} + +[bumpversion:part:release] +optional_value = dev +first_value = alpha +values = + dev + alpha + +[bumpversion:file:amazon_advertising_api/versions.py] diff --git a/.gitignore b/.gitignore index 772b156..ab7fb31 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,5 @@ localtest.py # Pycharm *.idea/ + +venv/* diff --git a/README.md b/README.md index 159b813..3318beb 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -Unofficial Amazon Sponsored Products Python client library. +# Amazon Advertising API Client + +This library provides support for Amazon's Advertising API. It is a fork of [Amazon Advertising API Python](https://github.com/dbrent-amazon/amazon-advertising-api-python). diff --git a/amazon_advertising_api/advertising_api.py b/amazon_advertising_api/advertising_api.py index 8fb0657..0809454 100644 --- a/amazon_advertising_api/advertising_api.py +++ b/amazon_advertising_api/advertising_api.py @@ -1,5 +1,6 @@ -from amazon_advertising_api.regions import regions +from amazon_advertising_api import versions as v from amazon_advertising_api.versions import versions +from amazon_advertising_api.regions import regions from io import BytesIO try: # Python 3 @@ -13,6 +14,8 @@ import gzip import json +DSP_REPORT = "dsp_report" + class AdvertisingApi(object): @@ -49,8 +52,10 @@ def __init__(self, self.refresh_token = refresh_token self.api_version = versions['api_version'] + self.user_agent = 'AdvertisingAPI Python Client Library v{}'.format( - versions['application_version']) + v.__version__) + self.profile_id = profile_id self.token_url = None @@ -174,42 +179,50 @@ def update_profiles(self, data): interface = 'profiles' return self._operation(interface, data, method='PUT') - def get_campaign(self, campaign_id): + def get_campaign(self, campaign_id, campaign_type='sp'): """ Retrieves a campaign by Id. Note that this call returns the minimal set of campaign fields, but is more efficient than **getCampaignEx**. - :GET: /campaigns/{campaignId} + :GET: {campaignType}/campaigns/{campaignId} :param campaign_id: The Id of the requested campaign. :type campaign_id: string + :param campaign_type: The campaignType of the requested campaign ('sp' or 'sb') + Defaults to 'sp' + :type campaign_type: string :returns: :200: Campaign :401: Unauthorized :404: Campaign not found """ - interface = 'campaigns/{}'. format(campaign_id) + interface = '{}/campaigns/{}'. format(campaign_type, campaign_id) + return self._operation(interface) - def get_campaign_ex(self, campaign_id): + def get_campaign_ex(self, campaign_id, campaign_type='sp'): """ Retrieves a campaign and its extended fields by ID. Note that this call returns the complete set of campaign fields (including serving status and other read-only fields), but is less efficient than **getCampaign**. - :GET: /campaigns/extended/{campaignId} + :GET: {campaignType}/campaigns/extended/{campaignId} :param campaign_id: The Id of the requested campaign. :type campaign_id: string + :param campaign_type: The campaignType of the requested campaign ('sp' or 'sb') + Defaults to 'sp' + :type campaign_type: string :returns: :200: Campaign :401: Unauthorized :404: Campaign not found """ - interface = 'campaigns/extended/{}'. format(campaign_id) + interface = '{}/campaigns/extended/{}'. format(campaign_type, campaign_id) + return self._operation(interface) - def create_campaigns(self, data): + def create_campaigns(self, data, campaign_type='sp'): """ Creates one or more campaigns. Successfully created campaigns will be assigned unique **campaignIds**. @@ -224,10 +237,10 @@ def create_campaigns(self, data): input. :401: Unauthorized """ - interface = 'campaigns' + interface = '{}/campaigns'.format(campaign_type) return self._operation(interface, data, method='POST') - def update_campaigns(self, data): + def update_campaigns(self, data, campaign_type='sp'): """ Updates one or more campaigns. Campaigns are identified using their **campaignIds**. @@ -242,10 +255,22 @@ def update_campaigns(self, data): input :401: Unauthorized """ - interface = 'campaigns' + interface = '{}/campaigns'.format(campaign_type) return self._operation(interface, data, method='PUT') - def archive_campaign(self, campaign_id): + def get_campaigns(self, campaign_type='sp'): + """ + Gets campaigns + + :GET: /campaigns + :returns: + :207: List of **CampaignResponse** + :401: Unauthorized + """ + interface = '{}/campaigns'.format(campaign_type) + return self._operation(interface) + + def archive_campaign(self, campaign_id, campaign_type='sp'): """ Sets the campaign status to archived. This same operation can be performed via an update, but is included for completeness. @@ -258,62 +283,69 @@ def archive_campaign(self, campaign_id): :401: Unauthorized :404: Campaign not found """ - interface = 'campaigns/{}'.format(campaign_id) + interface = '{}/campaigns/{}'.format(campaign_type, campaign_id) return self._operation(interface, method='DELETE') - def list_campaigns(self, data=None): + def list_campaigns(self, data=None, campaign_type='sp'): """ Retrieves a list of campaigns satisfying optional criteria. - :GET: /campaigns + :GET: /{campaignType}/campaigns + :param campaign_type: The campaignType to retrieve campaigns for ('sp' or 'hsa') + Defaults to 'sp' + :type campaign_type: string :param data: Optional, search criteria containing the following parameters. data may contain the following optional parameters: - :param startIndex: 0-indexed record offset for the result set. + :param startIndex: 0-indexed record offset for the result set. Defaults to 0. :type startIndex: Integer - :param count: Number of records to include in the paged response. + :param count: Number of records to include in the paged response. Defaults to max page size. :type count: Integer - :param campaignType: Restricts results to campaigns of a single + :param campaignType: Restricts results to campaigns of a single campaign type. Must be **sponsoredProducts**. :type campaignType: String - :param stateFilter: Restricts results to campaigns with state within - the specified comma-separatedlist. Must be one of **enabled**, + :param stateFilter: Restricts results to campaigns with state within + the specified comma-separatedlist. Must be one of **enabled**, **paused**, **archived**. Default behavior is to include all. :param name: Restricts results to campaigns with the specified name. :type name: String - :param campaignFilterId: Restricts results to campaigns specified in + :param campaignFilterId: Restricts results to campaigns specified in comma-separated list. :type campaignFilterId: String :returns: :200: Success. list of campaign :401: Unauthorized """ - interface = 'campaigns' + interface = '{}/campaigns' .format(campaign_type) + return self._operation(interface, data) - def list_campaigns_ex(self, data=None): + def list_campaigns_ex(self, data=None, campaign_type='sp'): """ Retrieves a list of campaigns with extended fields satisfying optional filtering criteria. - :GET: /campaigns/extended + :GET: /{campaignType}/campaigns/extended + :param campaign_type: campaignType of the requested campaigns ('sp' or 'hsa') + Defaults to 'sp' + :type campaign_type: string :param data: Optional, search criteria containing the following parameters. :type data: JSON string """ - interface = 'campaigns/extended' + interface = '{}/campaigns/extended' .format(campaign_type) return self._operation(interface, data) - def get_ad_group(self, ad_group_id): + def get_ad_group(self, ad_group_id, campaign_type='sp'): """ Retrieves an ad group by Id. Note that this call returns the minimal set of ad group fields, but is more efficient than getAdGroupEx. - :GET: /adGroups/{adGroupId} + :GET: /sp/adGroups/{adGroupId} :param ad_group_id: The Id of the requested ad group. :type ad_group_id: string @@ -322,17 +354,18 @@ def get_ad_group(self, ad_group_id): :401: Unauthorized :404: Ad group not found """ - interface = 'adGroups/{}'.format(ad_group_id) + interface = '{}/adGroups/{}'.format(campaign_type, ad_group_id) + return self._operation(interface) - def get_ad_group_ex(self, ad_group_id): + def get_ad_group_ex(self, ad_group_id, campaign_type='sp'): """ Retrieves an ad group and its extended fields by ID. Note that this call returns the complete set of ad group fields (including serving status and other read-only fields), but is less efficient than getAdGroup. - :GET: /adGroups/extended/{adGroupId} + :GET: /sp/adGroups/extended/{adGroupId} :param ad_group_id: The Id of the requested ad group. :type ad_group_id: string @@ -341,10 +374,10 @@ def get_ad_group_ex(self, ad_group_id): :401: Unauthorized :404: Ad group not found """ - interface = 'adGroups/extended/{}'.format(ad_group_id) + interface = '{}/adGroups/extended/{}'.format(campaign_type, ad_group_id) return self._operation(interface) - def create_ad_groups(self, data): + def create_ad_groups(self, data, campaign_type='sp'): """ Creates one or more ad groups. Successfully created ad groups will be assigned unique adGroupIds. @@ -360,10 +393,10 @@ def create_ad_groups(self, data): order as the input :401: Unauthorized """ - interface = 'adGroups' + interface = '{}/adGroups'.format(campaign_type) return self._operation(interface, data, method='POST') - def update_ad_groups(self, data): + def update_ad_groups(self, data, campaign_type='sp'): """ Updates one or more ad groups. Ad groups are identified using their adGroupIds. @@ -378,10 +411,10 @@ def update_ad_groups(self, data): order as the input :401: Unauthorized """ - interface = 'adGroups' + interface = '{}/adGroups'.format(campaign_type) return self._operation(interface, data, method='PUT') - def archive_ad_group(self, ad_group_id): + def archive_ad_group(self, ad_group_id, campaign_type="sp"): """ Sets the ad group status to archived. This same operation can be performed via an update, but is included for completeness. @@ -395,14 +428,14 @@ def archive_ad_group(self, ad_group_id): :401: Unauthorized :404: Ad group not found """ - interface = 'adGroups/{}'.format(ad_group_id) + interface = '{}/adGroups/{}'.format(campaign_type, ad_group_id) return self._operation(interface, method='DELETE') - def list_ad_groups(self, data=None): + def list_ad_groups(self, data=None, campaign_type="sp"): """ Retrieves a list of ad groups satisfying optional criteria. - :GET: /adGroups + :GET: /sp/adGroups :param data: Parameter list of criteria. data may contain the following optional parameters: @@ -434,14 +467,15 @@ def list_ad_groups(self, data=None): :401: Unauthorized. """ - interface = 'adGroups' + interface = '{}/adGroups'.format(campaign_type) + return self._operation(interface, data) - def list_ad_groups_ex(self, data=None): + def list_ad_groups_ex(self, data=None, campaign_type="sp"): """ Retrieves a list of ad groups satisfying optional criteria. - :GET: /adGroups/extended + :GET: /sp/adGroups/extended :param data: Parameter list of criteria. data may contain the following optional parameters: @@ -472,31 +506,403 @@ def list_ad_groups_ex(self, data=None): :200: Success. List of adGroup. :401: Unauthorized. """ - interface = 'adGroups/extended' + interface = '{}/adGroups/extended'.format(campaign_type) return self._operation(interface, data) - def get_biddable_keyword(self, keyword_id): + def get_target(self, target_id, campaign_type='sp'): + """ + Retrieves an ad group by Id. Note that this call returns the minimal + set of ad group fields, but is more efficient than getAdGroupEx. + + :GET: /sp/targets/{targetId} + :param target_id: The Id of the requested ad group. + :type target_id: string + + :returns: + :200: Success, Target response + :401: Unauthorized + :404: Ad group not found + """ + interface = '{}/targets/{}'.format(campaign_type, target_id) + + return self._operation(interface) + + def get_target_ex(self, target_id): """ - Retrieves a keyword by ID. Note that this call returns the minimal set + Retrieves a target and its extended fields by ID. Note that this + call returns the complete set of target fields (including serving + status and other read-only fields), but is less efficient than + getTarget. + + :GET: /sp/targets/extended/{adGroupId} + :param target_id: The Id of the requested target. + :type target_id: string + + :returns: + :200: Success, Target response + :401: Unauthorized + :404: Target not found + """ + interface = 'sp/targets/extended/{}'.format(target_id) + return self._operation(interface) + + def create_targets(self, data, campaign_type='sp'): + """ + Creates one or more ad groups. Successfully created ad groups will + be assigned unique adGroupIds. + + :POST: /targets + :param data: A list of up to 100 targets to be created. + :type data: List of **Target** + + :returns: + :207: Multi-status. List of AdGroupResponse reflecting the same + order as the input + :401: Unauthorized + """ + interface = '{}/targets'.format(campaign_type) + + return self._operation(interface, data, method='POST') + + def create_targets_list(self, data, campaign_type='sp'): + """ + Creates many targets + + :POST: /targets + :param data: A list of up to 100 targets to be created. + :type data: List of **Target** + + :returns: + :207: Multi-status. List of AdGroupResponse reflecting the same + order as the input + :401: Unauthorized + """ + interface = '{}/targets/list'.format(campaign_type) + + # not tested + return self._operation(interface, data, method='POST') + + def update_targets(self, data, campaign_type='sp'): + """ + Updates one or more targets. Targets are identified using their + targetId. + + :PUT: /targets + :param data: A list of up to 100 updates containing targetIds and the + mutable fields to be modified. + :type data: List of **Target** + + :returns: + :207: Multi-status. List of Targets reflecting the same + order as the input + :401: Unauthorized + """ + interface = '{}/targets'.format(campaign_type) + + return self._operation(interface, data, method='PUT') + + def archive_target(self, ad_group_id, campaign_type='sp'): + """ + Sets the ad group status to archived. This same operation can be + performed via an update, but is included for completeness. + + :DELETE: /targets/{targetId} + :param target_id: The Id of the ad group to be archived. + :type target_id: string + + :returns: + :200: Success. TargetResponse + :401: Unauthorized + :404: Ad group not found + """ + interface = '{}/targets/{}'.format(campaign_type, ad_group_id) + return self._operation(interface, method='DELETE') + + def list_targets(self, data=None): + """ + Retrieves a list of targets satisfying optional criteria. + + :GET: /sp/targets + :param data: Parameter list of criteria. + + data may contain the following optional parameters: + + :param startIndex: 0-indexed record offset for the result + set. Defaults to 0. + :type startIndex: integer + :param count: Number of records to include in the paged response. + Defaults to max page size. + :type count: integer + :param expressionTypeFilter: Restricts results to targets + with expression types within the specified comma-separated list. + Possible filter types are: auto and manual + :type expressionTypeFilter: string + :param expressionTextFilter: Content of the targeting expression + :type expressionTextFilter: string + :param campaignIdFilter: Restricts results to ad groups within + campaigns specified in comma-separated list. + :type campaignIdFilter: string + :param adGroupIdFilter: Restricts results to ad groups specified in + comma-separated list. + :type adGroupIdFilter: string + :param stateFilter: Restricts results to targets with state within the + specified comma-separatedlist. Must be one of enabled, paused, + archived. Default behavior is to include all. + :type stateFilter: string + :returns: + :200: Success. List of Targets. + :401: Unauthorized. + """ + interface = 'sp/targets' + return self._operation(interface, data) + + def list_targets_ex(self, data=None): + """ + Retrieves a list of targets satisfying optional criteria. + + :GET: /sp/targets/extended + :param data: Parameter list of criteria. + + data may contain the following optional parameters: + + :param startIndex: 0-indexed record offset for the result + set. Defaults to 0. + :type startIndex: integer + :param count: Number of records to include in the paged response. + Defaults to max page size. + :type count: integer + :param expressionTypeFilter: Restricts results to targets + with expression types within the specified comma-separated list. + Possible filter types are: auto and manual + :type expressionTypeFilter: string + :param expressionTextFilter: Content of the targeting expression + :type expressionTextFilter: string + :param campaignIdFilter: Restricts results to ad groups within + campaigns specified in comma-separated list. + :type campaignIdFilter: string + :param adGroupIdFilter: Restricts results to ad groups specified in + comma-separated list. + :type adGroupIdFilter: string + :param stateFilter: Restricts results to targets with state within the + specified comma-separatedlist. Must be one of enabled, paused, + archived. Default behavior is to include all. + :type stateFilter: string + :returns: + :200: Success. List of Targets. + :401: Unauthorized. + """ + interface = 'sp/targets/extended' + return self._operation(interface, data) + + def get_negative_target(self, target_id, campaign_type='sb'): + """ + Retrieves an ad group by Id. Note that this call returns the minimal + set of ad group fields, but is more efficient than getAdGroupEx. + + :GET: /sp/negativeTargets/{targetId} + :param target_id: The Id of the requested ad group. + :type target_id: string + + :returns: + :200: Success, Target response + :401: Unauthorized + :404: Ad group not found + """ + interface = '{}/negativeTargets/{}'.format(campaign_type, target_id) + + return self._operation(interface) + + def get_negative_target_ex(self, target_id): + """ + Retrieves a target and its extended fields by ID. Note that this + call returns the complete set of target fields (including serving + status and other read-only fields), but is less efficient than + getTarget. + + :GET: /sp/negativeTargets/extended/{adGroupId} + :param target_id: The Id of the requested target. + :type target_id: string + + :returns: + :200: Success, Target response + :401: Unauthorized + :404: Target not found + """ + interface = 'sp/negativeTargets/extended/{}'.format(target_id) + return self._operation(interface) + + def create_negative_targets(self, data, campaign_type='sp'): + """ + Creates one or more ad groups. Successfully created ad groups will + be assigned unique adGroupIds. + + :POST: /negativeTargets + :param data: A list of up to 100 negativeTargets to be created. + :type data: List of **Target** + + :returns: + :207: Multi-status. List of AdGroupResponse reflecting the same + order as the input + :401: Unauthorized + """ + interface = '{}/negativeTargets'.format(campaign_type) + + return self._operation(interface, data, method='POST') + + def create_negative_targets_list(self, data, campaign_type='sb'): + """ + Creates list of targets + + :POST: /negativeTargets/list + :param data: A list of negativeTargets to be created. + :type data: List of **Target** + + :returns: + :207: Multi-status. List of AdGroupResponse reflecting the same + order as the input + :401: Unauthorized + """ + interface = '{}/negativeTargets/list'.format(campaign_type) + + return self._operation(interface, data, method='POST') + + def update_negative_targets(self, data, campaign_type='sp'): + """ + Updates one or more negativeTargets. negativeTargets are identified using their + targetId. + + :PUT: /negativeTargets + :param data: A list of up to 100 updates containing targetIds and the + mutable fields to be modified. + :type data: List of **Target** + + :returns: + :207: Multi-status. List of negativeTargets reflecting the same + order as the input + :401: Unauthorized + """ + interface = '{}/negativeTargets'.format(campaign_type) + return self._operation(interface, data, method='PUT') + + def archive_negative_target(self, ad_group_id, campaign_type='sp'): + """ + Sets the ad group status to archived. This same operation can be + performed via an update, but is included for completeness. + + :DELETE: /negativeTargets/{targetId} + :param target_id: The Id of the ad group to be archived. + :type target_id: string + + :returns: + :200: Success. TargetResponse + :401: Unauthorized + :404: Ad group not found + """ + interface = '{}/negativeTargets/{}'.format(campaign_type, ad_group_id) + return self._operation(interface, method='DELETE') + + def list_negative_targets(self, data=None): + """ + Retrieves a list of negativeTargets satisfying optional criteria. + + :GET: /sp/negativeTargets + :param data: Parameter list of criteria. + + data may contain the following optional parameters: + + :param startIndex: 0-indexed record offset for the result + set. Defaults to 0. + :type startIndex: integer + :param count: Number of records to include in the paged response. + Defaults to max page size. + :type count: integer + :param expressionTypeFilter: Restricts results to negativeTargets + with expression types within the specified comma-separated list. + Possible filter types are: auto and manual + :type expressionTypeFilter: string + :param expressionTextFilter: Content of the targeting expression + :type expressionTextFilter: string + :param campaignIdFilter: Restricts results to ad groups within + campaigns specified in comma-separated list. + :type campaignIdFilter: string + :param adGroupIdFilter: Restricts results to ad groups specified in + comma-separated list. + :type adGroupIdFilter: string + :param stateFilter: Restricts results to negativeTargets with state within the + specified comma-separatedlist. Must be one of enabled, paused, + archived. Default behavior is to include all. + :type stateFilter: string + :returns: + :200: Success. List of negativeTargets. + :401: Unauthorized. + """ + interface = 'sp/negativeTargets' + return self._operation(interface, data) + + def list_negative_targets_ex(self, data=None): + """ + Retrieves a list of negativeTargets satisfying optional criteria. + + :GET: /sp/negativeTargets/extended + :param data: Parameter list of criteria. + + data may contain the following optional parameters: + + :param startIndex: 0-indexed record offset for the result + set. Defaults to 0. + :type startIndex: integer + :param count: Number of records to include in the paged response. + Defaults to max page size. + :type count: integer + :param expressionTypeFilter: Restricts results to negativeTargets + with expression types within the specified comma-separated list. + Possible filter types are: auto and manual + :type expressionTypeFilter: string + :param expressionTextFilter: Content of the targeting expression + :type expressionTextFilter: string + :param campaignIdFilter: Restricts results to ad groups within + campaigns specified in comma-separated list. + :type campaignIdFilter: string + :param adGroupIdFilter: Restricts results to ad groups specified in + comma-separated list. + :type adGroupIdFilter: string + :param stateFilter: Restricts results to negativeTargets with state within the + specified comma-separatedlist. Must be one of enabled, paused, + archived. Default behavior is to include all. + :type stateFilter: string + :returns: + :200: Success. List of negativeTargets. + :401: Unauthorized. + """ + interface = 'sp/negativeTargets/extended' + return self._operation(interface, data) + + def get_biddable_keyword(self, keyword_id, campaign_type='sp'): + """ + Retrieves a keyword by ID. Note that this call returns the minimal set of keyword fields, but is more efficient than getBiddableKeywordEx. - :GET: /keywords/{keywordId} + :GET: /{campaignType}/keywords/{keywordId} :param keyword_id: The Id of the requested keyword. :type keyword_id: string + :param campaign_type: The campaignType for the requested keyword + Defaults to 'sp' + :type campaign_type: string :returns: :200: Success. Keyword. :401: Unauthorized. :404: Keyword not found. """ - interface = 'keywords/{}'.format(keyword_id) + interface = '{}/keywords/{}'.format(campaign_type, keyword_id) + return self._operation(interface) def get_biddable_keyword_ex(self, keyword_id): """ - Retrieves a keyword and its extended fields by ID. Note that this call - returns the complete set of keyword fields (including serving status - and other read-only fields), but is less efficient than + Retrieves a keyword and its extended fields by ID. Note that this call + returns the complete set of keyword fields (including serving status + and other read-only fields), but is less efficient than getBiddableKeyword. :GET: /keywords/extended/{keywordId} @@ -508,74 +914,76 @@ def get_biddable_keyword_ex(self, keyword_id): :401: Unauthorized. :404: Keyword not found. """ - interface = 'keywords/extended/{}'.format(keyword_id) + interface = 'sp/keywords/extended/{}'.format(keyword_id) return self._operation(interface) - def create_biddable_keywords(self, data): + def create_biddable_keywords(self, data, campaign_type='sp'): """ - Creates one or more keywords. Successfully created keywords will be + Creates one or more keywords. Successfully created keywords will be assigned unique keywordIds. :POST: /keywords - :param data: A list of up to 1000 keywords to be created. Required - fields for keyword creation are campaignId, adGroupId, keywordText, + :param data: A list of up to 1000 keywords to be created. Required + fields for keyword creation are campaignId, adGroupId, keywordText, matchType and state. :type data: List of **Keyword** """ - interface = 'keywords' - return self._operation(interface, data, method='POST') + interface = '{}/keywords'.format(campaign_type) + + return self._operation(interface, data) - def update_biddable_keywords(self, data): - interface = 'keywords' + def update_biddable_keywords(self, data, campaign_type='sp'): + interface = '{}/keywords'.format(campaign_type) return self._operation(interface, data, method='PUT') - def archive_biddable_keyword(self, keyword_id): - interface = 'keywords/{}'.format(keyword_id) + def archive_biddable_keyword(self, keyword_id, campaign_type='sp'): + interface = '{}/keywords/{}'.format(campaign_type, keyword_id) return self._operation(interface, method='DELETE') - def list_biddable_keywords(self, data=None): - interface = 'keywords' + def list_biddable_keywords(self, data=None, campaign_type='sp'): + interface = '{}/keywords'.format(campaign_type) return self._operation(interface, data) def list_biddable_keywords_ex(self, data=None): - interface = 'keywords/extended' + interface = 'sp/keywords/extended' return self._operation(interface, data) - def get_negative_keyword(self, negative_keyword_id): - interface = 'negativeKeywords/{}'.format(negative_keyword_id) + def get_negative_keyword(self, negative_keyword_id, campaign_type='sp'): + interface = '{}/negativeKeywords/{}'.format(campaign_type, negative_keyword_id) return self._operation(interface) def get_negative_keyword_ex(self, negative_keyword_id): - interface = 'negativeKeywords/extended/{}'.format(negative_keyword_id) + interface = 'sp/negativeKeywords/extended/{}'.format(negative_keyword_id) return self._operation(interface) - def create_negative_keywords(self, data): - interface = 'negativeKeywords' + def create_negative_keywords(self, data, campaign_type="sp"): + interface = '{}/negativeKeywords'.format(campaign_type) + return self._operation(interface, data, method='POST') - def update_negative_keywords(self, data): - interface = 'negativeKeywords' + def update_negative_keywords(self, data, campaign_type='sp'): + interface = '{}/negativeKeywords'.format(campaign_type) return self._operation(interface, data, method='PUT') - def archive_negative_keyword(self, negative_keyword_id): - interface = 'negativeKeywords/{}'.format(negative_keyword_id) + def archive_negative_keyword(self, negative_keyword_id, campaign_type='sp'): + interface = '{}/negativeKeywords/{}'.format(campaign_type, negative_keyword_id) return self._operation(interface, method='DELETE') - def list_negative_keywords(self, data=None): - interface = 'negativeKeywords' + def list_negative_keywords(self, data=None, campaign_type='sp'): + interface = '{}/negativeKeywords'.format(campaign_type) return self._operation(interface, data) def list_negative_keywords_ex(self, data=None): - interface = 'negativeKeywords/extended' + interface = 'sp/negativeKeywords/extended' return self._operation(interface, data) def get_campaign_negative_keyword(self, campaign_negative_keyword_id): - interface = 'campaignNegativeKeywords/{}'.format( + interface = 'sp/campaignNegativeKeywords/{}'.format( campaign_negative_keyword_id) return self._operation(interface) def get_campaign_negative_keyword_ex(self, campaign_negative_keyword_id): - interface = 'campaignNegativeKeywords/extended/{}'.format( + interface = 'sp/campaignNegativeKeywords/extended/{}'.format( campaign_negative_keyword_id) return self._operation(interface) @@ -593,19 +1001,19 @@ def remove_campaign_negative_keyword(self, campaign_negative_keyword_id): return self._operation(interface, method='DELETE') def list_campaign_negative_keywords(self, data=None): - interface = 'campaignNegativeKeywords' + interface = 'sp/campaignNegativeKeywords' return self._operation(interface, data) def list_campaign_negative_keywords_ex(self, data=None): - interface = 'campaignNegativeKeywords/extended' + interface = 'sp/campaignNegativeKeywords/extended' return self._operation(interface, data) def get_product_ad(self, product_ad_id): - interface = 'productAds/{}'.format(product_ad_id) + interface = 'sp/productAds/{}'.format(product_ad_id) return self._operation(interface) def get_product_ad_ex(self, product_ad_id): - interface = 'productAds/extended/{}'.format(product_ad_id) + interface = 'sp/productAds/extended/{}'.format(product_ad_id) return self._operation(interface) def create_product_ads(self, data): @@ -619,47 +1027,50 @@ def update_product_ads(self, data): def archive_product_ads(self): pass - def list_product_ads(self, data=None): - interface = 'productAds' + def list_product_ads(self, data=None, campaign_type="sp"): + interface = '{}/productAds'.format(campaign_type) return self._operation(interface, data) - def list_product_ads_ex(self, data=None): - interface = 'productAds/extended' + def list_product_ads_ex(self, data=None, campaign_type="sp"): + interface = '{}/productAds/extended'.format(campaign_type) return self._operation(interface, data) - def request_snapshot(self, record_type=None, snapshot_id=None, data=None): + def create_keyword_recommendations(self, data, campaign_type='sp'): + interface = '{}/recommendations/keyword'.format(campaign_type) + + return self._operation(interface, data, method='POST') + + def request_snapshot(self, record_type=None, snapshot_id=None, data=None, campaign_type='sp'): """ + :POST: /snapshots + Required data: - * :campaignType: The type of campaign for which snapshot should be generated. Must be sponsoredProducts. + :campaign_type: Should be 'hsa' or 'sp' """ - 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) + interface = '{}/{}/snapshot'.format(campaign_type, record_type) return self._operation(interface, data, method='POST') elif snapshot_id is not None: - interface = 'snapshots/{}'.format(snapshot_id) + interface = '{}/snapshots/{}'.format(campaign_type, 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. + def request_report(self, record_type=None, report_id=None, data=None, campaign_type='sp'): """ - if not data: - data = {'campaignType': 'sponsoredProducts'} - elif not data.get('campaignType'): - data['campaignType'] = 'sponsoredProducts' + :POST: /{campaignType}/reports + :param campaign_type: The campaignType to request the report for ('sp' or 'hsa') + Defaults to 'sp' + :type data: string + """ if record_type is not None: - interface = '{}/report'.format(record_type) + if record_type == DSP_REPORT: + interface = 'dsp/reports' + else: + interface = '{}/{}/report'.format(campaign_type, record_type) return self._operation(interface, data, method='POST') elif report_id is not None: interface = 'reports/{}'.format(report_id) @@ -669,15 +1080,36 @@ def request_report(self, record_type=None, report_id=None, data=None): 'code': 0, 'response': 'record_type and report_id are both empty.'} - def get_report(self, report_id): + def get_report(self, report_id, report_type=None): interface = 'reports/{}'.format(report_id) - res = self._operation(interface) - if json.loads(res['response'])['status'] == 'SUCCESS': - res = self._download( - location=json.loads(res['response'])['location']) - return res + if report_type is not None and report_type == DSP_REPORT: + interface = 'dsp/' + interface + res = self._operation(interface, report_id=report_id) + if res['success']: + body = json.loads(res['response']) + if body.get('status') == 'SUCCESS': + # for dsp report, return response with url for the report instead of the huge report data itself + if report_type == DSP_REPORT: + return res + res = self._download(location=body['location']) + return res + + def get_dsp_entity(self, interface, media_type): + """ + :GET: dsp/interface + : + :interface consists of entity_type and query_param, where + : entity_type: advertisers, orders, lineItems, creatives, lineItemCreativeAssociations + : query_param: startIndex=&count=[&=] + : filter: advertiserIdFilter, orderIdFilter, lineItemIdFilter + :media_type example: 'application/vnd.dspbasiclineitems.v3+json' + """ + if interface is None or media_type is None: + return {'success': False, + 'code': 0, + 'response': 'interface and media_type are both empty.'} else: - return res + return self._operation(interface, method='GET', media_type=media_type) def get_snapshot(self, snapshot_id): interface = 'snapshots/{}'.format(snapshot_id) @@ -734,6 +1166,7 @@ def _download(self, location): data = f.read() return {'success': True, 'code': res.code, + 'api_version': versions["api_version"], 'response': json.loads(data.decode('utf-8'))} else: return {'success': False, @@ -748,7 +1181,7 @@ def _download(self, location): 'code': e.code, 'response': '{msg}: {details}'.format(msg=e.msg, details=e.read())} - def _operation(self, interface, params=None, method='GET'): + def _operation(self, interface, params=None, method='GET', report_id=None, media_type=None): """ Makes that actual API call. @@ -756,17 +1189,29 @@ 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', 'PUT', or 'POST' + :param method: Call method. Should be either 'GET', 'PUT', or 'POST'. :type method: string + :param report_id: report_id obtained in previous 'POST' request. + :type report_id: string + :param media_type: Used for dsp entity 'GET' calls, e.g., 'application/vnd.dsporders.v2.2+json' + :type media_type: string """ + api_v3 = interface.startswith('sb') + dsp_report = interface.startswith('dsp') + if self._access_token is None: return {'success': False, 'code': 0, 'response': 'access_token is empty.'} headers = {'Authorization': 'Bearer {}'.format(self._access_token), + 'Amazon-Advertising-API-ClientId': self.client_id, 'Content-Type': 'application/json', 'User-Agent': self.user_agent} + if report_id is not None: + headers['reportId'] = report_id + if media_type is not None: + headers['Accept'] = media_type if self.profile_id is not None and self.profile_id != '': headers['Amazon-Advertising-API-Scope'] = self.profile_id @@ -778,26 +1223,22 @@ def _operation(self, interface, params=None, method='GET'): data = None + if api_v3 or dsp_report: + url = f"https://{self.endpoint}/{interface}" + else: + url = f"https://{self.endpoint}/{self.api_version}/{interface}" + if method == 'GET': if params is not None: p = '?{}'.format(urllib.parse.urlencode(params)) else: p = '' - url = 'https://{host}/{api_version}/{interface}{params}'.format( - host=self.endpoint, - api_version=self.api_version, - interface=interface, - params=p) + url += '{params}'.format(params=p) else: if params is not None: data = json.dumps(params).encode('utf-8') - url = 'https://{host}/{api_version}/{interface}'.format( - host=self.endpoint, - api_version=self.api_version, - interface=interface) - if PYTHON == 3: req = urllib.request.Request(url=url, headers=headers, data=data) else: @@ -806,11 +1247,15 @@ def _operation(self, interface, params=None, method='GET'): try: f = urllib.request.urlopen(req) - return {'success': True, - 'code': f.code, - 'response': f.read().decode('utf-8')} + return { + 'success': True, + 'api_version': self.api_version if not api_v3 else versions['api_version_sb'], + 'code': f.code, + 'response': f.read().decode('utf-8')} + except urllib.error.HTTPError as e: return {'success': False, + 'api_version': self.api_version if not api_v3 else versions['api_version_sb'], 'code': e.code, 'response': '{msg}: {details}'.format(msg=e.msg, details=e.read())} diff --git a/amazon_advertising_api/versions.py b/amazon_advertising_api/versions.py index e1dccf0..b3940da 100644 --- a/amazon_advertising_api/versions.py +++ b/amazon_advertising_api/versions.py @@ -1,3 +1,5 @@ +__version__ = "0.0.7" + versions = { - 'api_version': 'v1', - 'application_version': '1.0'} + 'api_version': 'v2', + 'api_version_sb': 'v3'} diff --git a/setup.py b/setup.py index d2a341d..27ceb13 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,10 @@ from setuptools import setup -import amazon_advertising_api.versions as aa_versions +from amazon_advertising_api import versions setup( name='amazon_advertising_api', packages=['amazon_advertising_api'], - version=aa_versions.versions['application_version'], + version=versions.__version__, description='Unofficial Amazon Sponsored Products Python client library.', - url='https://github.com/sguermond/amazon-advertising-api-python') + url='https://github.com/pepsico-ecommerce/amazon-advertising-api-python')