├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── Manifest.in ├── NOTICE ├── README.md ├── README.rst ├── bottlenose ├── __init__.py ├── api.py └── metadata.py ├── meta ├── header.sketch ├── logo.png ├── repo-banner-2.png ├── repo-banner-bottom.png └── repo-banner.png ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | bottlenose.egg-info/ 4 | build/ 5 | dist/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | - "3.7" 5 | - "3.8" 6 | - "3.9" 7 | script: 8 | - make test 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contributing 2 | 3 | * If you need help or would like to ask a general question, use Stack Overflow. Apply the 'bottlenose' tag to your question to get help faster. 4 | 5 | * If you found a bug or have a feature request, open an issue. 6 | 7 | * If you want to contribute, submit a pull request. If it's a big change, please open an issue first to discuss implementation. 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2014-2018 Lionheart Software LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # Usage: make VERSION=0.1.2 16 | 17 | METADATA_FILE := $(shell find . -name "metadata.py" -depth 2) 18 | 19 | all: clean test publish 20 | 21 | clean: 22 | rm -rf dist/ 23 | 24 | test: 25 | python setup.py test 26 | python3 setup.py test 27 | 28 | update_readme: 29 | pandoc --from=markdown --to=rst --output=README.rst README.md 30 | -git reset 31 | -git add README.rst 32 | -git commit -m "update README.rst from README.md" 33 | 34 | update_version: 35 | sed -i "" "s/\(__version__[ ]*=\).*/\1 \"$(VERSION)\"/g" $(METADATA_FILE) 36 | git add . 37 | # - ignores errors in this command 38 | -git commit -m "bump version to $(VERSION)" 39 | # Delete tag if already exists 40 | -git tag -d $(VERSION) 41 | -git push origin master :$(VERSION) 42 | git tag $(VERSION) 43 | git push origin master 44 | git push --tags 45 | 46 | publish: clean update_readme update_version 47 | python setup.py bdist_wheel --universal 48 | python3 setup.py bdist_wheel --universal 49 | gpg --detach-sign -a dist/*.whl 50 | twine upload dist/* 51 | 52 | -------------------------------------------------------------------------------- /Manifest.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include README.md 3 | include LICENSE 4 | include NOTICE 5 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | bottlenose 2 | Copyright 2012-2018 Lionheart Software LLC. 3 | 4 | This product includes software developed by Lionheart Software (https://lionheartsw.com/). 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | ![](meta/repo-banner.png) 18 | [![](meta/repo-banner-bottom.png)][lionheart-url] 19 | 20 | [![Version](https://img.shields.io/travis/lionheart/bottlenose.svg?style=flat)](https://travis-ci.org/lionheart/bottlenose) 21 | [![Version](https://img.shields.io/pypi/v/bottlenose.svg?style=flat)](https://pypi.python.org/pypi/bottlenose) 22 | [![License](https://img.shields.io/pypi/l/bottlenose.svg?style=flat)](LICENSE) 23 | [![Versions](https://img.shields.io/pypi/pyversions/bottlenose.svg?style=flat)](https://pypi.python.org/pypi/bottlenose) 24 | 25 | Bottlenose is a thin, well-tested, maintained, and powerful Python wrapper over the Amazon Product Advertising API. There is practically no overhead, and no magic (unless you add it yourself). 26 | 27 | Before you get started, make sure you have both Amazon Product Advertising and AWS accounts. `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_ASSOCIATE_TAG` are all from your Amazon Associate Account. 28 | 29 | ## Features 30 | 31 | * [x] Compatible with Python versions 2.4 and up 32 | * [x] Support for AU, BR, CA, CN, DE, ES, FR, IN, IT, JP, MX, UK, and US Amazon Product Advertising API endpoints 33 | * [x] No requirements, except simplejson for Python versions before 2.6 34 | * [x] Configurable query parsing 35 | * [x] Configurable throttling for batches of queries 36 | * [x] Configurable query caching 37 | * [x] Configurable error handling and retries 38 | 39 | ## Usage 40 | 41 | ### [pip](https://pip.pypa.io/en/stable/installing/) 42 | 43 | pip install bottlenose 44 | 45 | or 46 | 47 | python3 -m pip install bottlenose 48 | 49 | Then, using your `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_ASSOCIATE_TAG`: 50 | 51 | ```python 52 | import bottlenose 53 | amazon = bottlenose.Amazon(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ASSOCIATE_TAG) 54 | response = amazon.ItemLookup(ItemId="B007OZNUCE") 55 | ``` 56 | 57 | You can then parse the `response` output to view item information. 58 | 59 | ## Troubleshooting 60 | 61 | * If you need help or would like to ask a general question, use [Stack Overflow](http://stackoverflow.com/questions/tagged/bottlenose). Apply the 'bottlenose' tag to your question to get help faster. 62 | * If you found a bug or have a feature request, open an issue. 63 | * If you want to contribute, submit a pull request. If it's a big change, please open an issue first to discuss implementation. 64 | 65 | ## Advanced Usage 66 | 67 | #### 1. Available Search Methods 68 | 69 | ##### Region Endpoint 70 | 71 | The default `Region` is set to `US` (`webservices.amazon.com`). To specify another endpoint, 72 | simply set the `Region` parameter with the request. For example, to specify the French 73 | endpoint (`webservices.amazon.fr`), set the Region parameter to `FR`: 74 | 75 | ```python 76 | amazon = bottlenose.Amazon(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ASSOCIATE_TAG, Region='FR') 77 | ``` 78 | Supported values for the Region parameter are `CA`, `CN`, `DE`, `ES`, `FR`, `IN`, `IT`, `JP`, `UK`, and `US` (default). 79 | 80 | Your Amazon Product Advertising account (`AWS_ASSOCIATE_TAG`) must exist for the given endpoint, otherwise, you'll get an HTTP 400 error ('Bad Request'). 81 | 82 | ##### Search for a Specific Item 83 | 84 | ```python 85 | response = amazon.ItemLookup(ItemId="B007OZNUCE") 86 | ``` 87 | 88 | ##### Search for Items by Keywords 89 | 90 | ```python 91 | response = amazon.ItemSearch(Keywords="Kindle 3G", SearchIndex="All") 92 | ``` 93 | 94 | ##### Search for Images for an item 95 | 96 | ```python 97 | response = amazon.ItemLookup(ItemId="1449372422", ResponseGroup="Images") 98 | ``` 99 | 100 | ##### Search for Similar Items 101 | 102 | ```python 103 | response = amazon.SimilarityLookup(ItemId="B007OZNUCE") 104 | ``` 105 | 106 | #### 2. Available Shopping Related Methods 107 | 108 | ##### Required 109 | 110 | ```python 111 | amazon = bottlenose.Amazon(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ASSOCIATE_TAG) 112 | ``` 113 | 114 | ##### Create a cart 115 | 116 | ```python 117 | response = amazon.CartCreate(...) 118 | ``` 119 | 120 | ##### Adding to a cart 121 | 122 | ```python 123 | response = amazon.CartAdd(CartId, ...) 124 | ``` 125 | 126 | ##### Get a cart by ID 127 | 128 | ```python 129 | response = amazon.CartGet(CartId, ...) 130 | ``` 131 | 132 | ##### Modifying a cart 133 | 134 | ```python 135 | response = amazon.CartModify(ASIN, CartId,...) 136 | ``` 137 | 138 | ##### Clearing a cart 139 | 140 | ```python 141 | response = amazon.CartClear(CartId, ...) 142 | ``` 143 | 144 | #### 3. Sample Code 145 | 146 | ```python 147 | import bottlenose 148 | amazon = bottlenose.Amazon(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ASSOCIATE_TAG) 149 | response = amazon.ItemLookup(ItemId="0596520999", ResponseGroup="Images", 150 | SearchIndex="Books", IdType="ISBN") 151 | print(response) 152 | # 18 | 19 | |image0| |image1| 20 | 21 | |Version| |Version| |License| |Versions| 22 | 23 | Bottlenose is a thin, well-tested, maintained, and powerful Python 24 | wrapper over the Amazon Product Advertising API. There is practically no 25 | overhead, and no magic (unless you add it yourself). 26 | 27 | Before you get started, make sure you have both Amazon Product 28 | Advertising and AWS accounts. ``AWS_ACCESS_KEY_ID``, 29 | ``AWS_SECRET_ACCESS_KEY`` and ``AWS_ASSOCIATE_TAG`` are all from your 30 | Amazon Associate Account. 31 | 32 | Features 33 | -------- 34 | 35 | - [x] Compatible with Python versions 2.4 and up 36 | - [x] Support for AU, BR, CA, CN, DE, ES, FR, IN, IT, JP, MX, UK, and 37 | US Amazon Product Advertising API endpoints 38 | - [x] No requirements, except simplejson for Python versions before 2.6 39 | - [x] Configurable query parsing 40 | - [x] Configurable throttling for batches of queries 41 | - [x] Configurable query caching 42 | - [x] Configurable error handling and retries 43 | 44 | Usage 45 | ----- 46 | 47 | `pip `__ 48 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 49 | 50 | :: 51 | 52 | pip install bottlenose 53 | 54 | or 55 | 56 | :: 57 | 58 | python3 -m pip install bottlenose 59 | 60 | Then, using your ``AWS_ACCESS_KEY_ID``, ``AWS_SECRET_ACCESS_KEY``, and 61 | ``AWS_ASSOCIATE_TAG``: 62 | 63 | .. code:: python 64 | 65 | import bottlenose 66 | amazon = bottlenose.Amazon(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ASSOCIATE_TAG) 67 | response = amazon.ItemLookup(ItemId="B007OZNUCE") 68 | 69 | You can then parse the ``response`` output to view item information. 70 | 71 | Troubleshooting 72 | --------------- 73 | 74 | - If you need help or would like to ask a general question, use `Stack 75 | Overflow `__. 76 | Apply the ‘bottlenose’ tag to your question to get help faster. 77 | - If you found a bug or have a feature request, open an issue. 78 | - If you want to contribute, submit a pull request. If it’s a big 79 | change, please open an issue first to discuss implementation. 80 | 81 | Advanced Usage 82 | -------------- 83 | 84 | 1. Available Search Methods 85 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 86 | 87 | Region Endpoint 88 | ''''''''''''''' 89 | 90 | The default Region is the US (``webservices.amazon.com``). To specify a 91 | different endpoint simply set the Region parameter with the request. For 92 | example to specify the French endpoint (``webservices.amazon.fr``) set 93 | the Region parameter to ‘FR’: 94 | 95 | .. code:: python 96 | 97 | amazon = bottlenose.Amazon(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ASSOCIATE_TAG, Region='FR') 98 | 99 | Supported values for the Region parameter are CA, CN, DE, ES, FR, IN, 100 | IT, JP, UK, and US (default). 101 | 102 | Your Amazon Product Advertising account (AWS_ASSOCIATE_TAG) mut exist 103 | for the given endpoint or you’ll get an HTTP 400 error (‘Bad Request’). 104 | 105 | Search for a Specific Item 106 | '''''''''''''''''''''''''' 107 | 108 | .. code:: python 109 | 110 | response = amazon.ItemLookup(ItemId="B007OZNUCE") 111 | 112 | Search for Items by Keywords 113 | '''''''''''''''''''''''''''' 114 | 115 | .. code:: python 116 | 117 | response = amazon.ItemSearch(Keywords="Kindle 3G", SearchIndex="All") 118 | 119 | Search for Images for an item 120 | ''''''''''''''''''''''''''''' 121 | 122 | .. code:: python 123 | 124 | response = amazon.ItemLookup(ItemId="1449372422", ResponseGroup="Images") 125 | 126 | Search for Similar Items 127 | '''''''''''''''''''''''' 128 | 129 | .. code:: python 130 | 131 | response = amazon.SimilarityLookup(ItemId="B007OZNUCE") 132 | 133 | 2. Available Shopping Related Methods 134 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 135 | 136 | Required 137 | '''''''' 138 | 139 | .. code:: python 140 | 141 | amazon = bottlenose.Amazon(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ASSOCIATE_TAG) 142 | 143 | Create a cart 144 | ''''''''''''' 145 | 146 | .. code:: python 147 | 148 | response = amazon.CartCreate(...) 149 | 150 | Adding to a cart 151 | '''''''''''''''' 152 | 153 | .. code:: python 154 | 155 | response = amazon.CartAdd(CartId, ...) 156 | 157 | Get a cart by ID 158 | '''''''''''''''' 159 | 160 | .. code:: python 161 | 162 | response = amazon.CartGet(CartId, ...) 163 | 164 | Modifying a cart 165 | '''''''''''''''' 166 | 167 | .. code:: python 168 | 169 | response = amazon.CartModify(ASIN, CartId,...) 170 | 171 | Clearing a cart 172 | ''''''''''''''' 173 | 174 | .. code:: python 175 | 176 | response = amazon.CartClear(CartId, ...) 177 | 178 | 3. Sample Code 179 | ^^^^^^^^^^^^^^ 180 | 181 | .. code:: python 182 | 183 | import bottlenose 184 | amazon = bottlenose.Amazon(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ASSOCIATE_TAG) 185 | response = amazon.ItemLookup(ItemId="0596520999", ResponseGroup="Images", 186 | SearchIndex="Books", IdType="ISBN") 187 | print(response) 188 | # `__ 307 | only allows you to cache queries for up to 24 hours. 308 | 309 | Error Handling 310 | -------------- 311 | 312 | Sometimes the Amazon API returns errors; for example, if you have gone 313 | over your query limit, you’ll get a 503. The ``ErrorHandler`` 314 | constructor argument gives you a way to keep track of such errors, and 315 | to retry queries when you receive a transient error. 316 | 317 | ``ErrorHandler`` should be a callable that takes a single argument, a 318 | dictionary with these keys: 319 | 320 | - api_url: the actual URL used to call the API 321 | - cache_url: ``api_url`` minus authentication information 322 | - exception: the exception raised (usually an ``HTTPError`` or 323 | ``URLError``) 324 | 325 | If your ``ErrorHandler`` returns true, the query will be retried. Here’s 326 | some example code that does exponential backoff after throttling: 327 | 328 | .. code:: python 329 | 330 | import random 331 | import time 332 | from urllib2 import HTTPError 333 | 334 | def error_handler(err): 335 | ex = err['exception'] 336 | if isinstance(ex, HTTPError) and ex.code == 503: 337 | time.sleep(random.expovariate(0.1)) 338 | return True 339 | 340 | amazon = bottlenose.Amazon(ErrorHandler=error_handler) 341 | 342 | License 343 | ------- 344 | 345 | Apache License, Version 2.0. See `LICENSE `__ for details. 346 | 347 | .. |image0| image:: meta/repo-banner.png 348 | .. |image1| image:: meta/repo-banner-bottom.png 349 | :target: https://lionheartsw.com/ 350 | .. |Version| image:: https://img.shields.io/travis/lionheart/bottlenose.svg?style=flat 351 | :target: https://travis-ci.org/lionheart/bottlenose 352 | .. |Version| image:: https://img.shields.io/pypi/v/bottlenose.svg?style=flat 353 | :target: https://pypi.python.org/pypi/bottlenose 354 | .. |License| image:: https://img.shields.io/pypi/l/bottlenose.svg?style=flat 355 | :target: LICENSE 356 | .. |Versions| image:: https://img.shields.io/pypi/pyversions/bottlenose.svg?style=flat 357 | :target: https://pypi.python.org/pypi/bottlenose 358 | -------------------------------------------------------------------------------- /bottlenose/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2012-2017 Lionheart Software LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from bottlenose.api import * 18 | 19 | from .metadata import ( 20 | __author__, 21 | __copyright__, 22 | __email__, 23 | __license__, 24 | __maintainer__, 25 | __version__, 26 | ) 27 | -------------------------------------------------------------------------------- /bottlenose/api.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2017 Lionheart Software LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from base64 import b64encode 16 | import gzip 17 | import sys 18 | import urllib 19 | try: 20 | import urllib2 21 | except ImportError: 22 | import urllib.request as urllib2 23 | import hmac 24 | import os 25 | import time 26 | import socket 27 | import logging 28 | 29 | from hashlib import sha256 30 | 31 | try: 32 | from cStringIO import StringIO 33 | except ImportError: 34 | try: 35 | from StringIO import StringIO 36 | except ImportError: 37 | from io import StringIO 38 | 39 | try: 40 | from urllib import quote as urllib_quote 41 | except ImportError: 42 | # Python 3 43 | from urllib.parse import quote as urllib_quote 44 | unicode = str 45 | 46 | 47 | # Python 2.4 compatibility 48 | # http://code.google.com/p/boto/source/detail?r=1011 49 | if sys.version[:3] == "2.4": 50 | # we are using an hmac that expects a .new() method. 51 | class Faker: 52 | def __init__(self, which): 53 | self.which = which 54 | self.digest_size = self.which().digest_size 55 | 56 | def new(self, *args, **kwargs): 57 | return self.which(*args, **kwargs) 58 | 59 | sha256 = Faker(sha256) 60 | 61 | 62 | try: 63 | from exceptions import Exception 64 | except ImportError: 65 | pass 66 | 67 | SERVICE_DOMAINS = { 68 | 'AU': ('webservices.amazon.com.au', 'xml-au.amznxslt.com'), 69 | 'CA': ('webservices.amazon.ca', 'xml-ca.amznxslt.com'), 70 | 'CN': ('webservices.amazon.cn', 'xml-cn.amznxslt.com'), 71 | 'DE': ('webservices.amazon.de', 'xml-de.amznxslt.com'), 72 | 'ES': ('webservices.amazon.es', 'xml-es.amznxslt.com'), 73 | 'FR': ('webservices.amazon.fr', 'xml-fr.amznxslt.com'), 74 | 'IN': ('webservices.amazon.in', 'xml-in.amznxslt.com'), 75 | 'IT': ('webservices.amazon.it', 'xml-it.amznxslt.com'), 76 | 'JP': ('webservices.amazon.co.jp', 'xml-jp.amznxslt.com'), 77 | 'UK': ('webservices.amazon.co.uk', 'xml-uk.amznxslt.com'), 78 | 'US': ('webservices.amazon.com', 'xml-us.amznxslt.com'), 79 | 'BR': ('webservices.amazon.com.br', 'xml-br.amznxslt.com'), 80 | 'MX': ('webservices.amazon.com.mx', 'xml-mx.amznxslt.com') 81 | } 82 | 83 | log = logging.getLogger(__name__) 84 | 85 | 86 | def _quote_query(query): 87 | """Turn a dictionary into a query string in a URL, with keys 88 | in alphabetical order.""" 89 | return "&".join("%s=%s" % ( 90 | k, urllib_quote( 91 | unicode(query[k]).encode('utf-8'), safe='~')) 92 | for k in sorted(query)) 93 | 94 | 95 | class AmazonError(Exception): 96 | pass 97 | 98 | 99 | class AmazonCall(object): 100 | def __init__(self, AWSAccessKeyId=None, AWSSecretAccessKey=None, 101 | AssociateTag=None, Operation=None, Version="2013-08-01", Region=None, 102 | Timeout=None, MaxQPS=None, Parser=None, 103 | CacheReader=None, CacheWriter=None, 104 | ErrorHandler=None, 105 | _last_query_time=None): 106 | 107 | self.AWSAccessKeyId = (AWSAccessKeyId or 108 | os.environ.get('AWS_ACCESS_KEY_ID')) 109 | if self.AWSAccessKeyId is None: 110 | raise TypeError("AWSAccessKeyId is not defined.") 111 | 112 | self.AWSSecretAccessKey = (AWSSecretAccessKey or 113 | os.environ.get('AWS_SECRET_ACCESS_KEY')) 114 | if self.AWSSecretAccessKey is None: 115 | raise TypeError("AWSSecretAccessKey is not defined.") 116 | 117 | self.AssociateTag = (AssociateTag or 118 | os.environ.get('AWS_ASSOCIATE_TAG')) 119 | if self.AssociateTag is None: 120 | raise TypeError("AssociateTag is not defined.") 121 | 122 | self.CacheReader = CacheReader 123 | self.CacheWriter = CacheWriter 124 | self.ErrorHandler = ErrorHandler 125 | self.MaxQPS = MaxQPS 126 | self.Operation = Operation 127 | self.Parser = Parser 128 | self.Version = Version 129 | self.Region = Region 130 | self.Timeout = Timeout 131 | 132 | # put this in a list so it can be shared between instances 133 | self._last_query_time = _last_query_time or [None] 134 | 135 | def signed_request(self): 136 | pass 137 | 138 | def __getattr__(self, k): 139 | try: 140 | return object.__getattr__(self, k) 141 | except: 142 | return AmazonCall(self.AWSAccessKeyId, self.AWSSecretAccessKey, 143 | self.AssociateTag, 144 | Operation=k, Version=self.Version, 145 | Region=self.Region, Timeout=self.Timeout, 146 | MaxQPS=self.MaxQPS, Parser=self.Parser, 147 | CacheReader=self.CacheReader, 148 | CacheWriter=self.CacheWriter, 149 | ErrorHandler=self.ErrorHandler, 150 | _last_query_time=self._last_query_time) 151 | 152 | def _maybe_parse(self, response_text): 153 | if self.Parser: 154 | return self.Parser(response_text) 155 | else: 156 | return response_text 157 | 158 | 159 | def api_url(self, **kwargs): 160 | """The URL for making the given query against the API.""" 161 | query = { 162 | 'Operation': self.Operation, 163 | 'Service': "AWSECommerceService", 164 | 'Timestamp': time.strftime( 165 | "%Y-%m-%dT%H:%M:%SZ", time.gmtime()), 166 | 'Version': self.Version, 167 | } 168 | query.update(kwargs) 169 | 170 | query['AWSAccessKeyId'] = self.AWSAccessKeyId 171 | query['Timestamp'] = time.strftime("%Y-%m-%dT%H:%M:%SZ", 172 | time.gmtime()) 173 | 174 | if self.AssociateTag: 175 | query['AssociateTag'] = self.AssociateTag 176 | 177 | service_domain = SERVICE_DOMAINS[self.Region][0] 178 | quoted_strings = _quote_query(query) 179 | 180 | data = "GET\n" + service_domain + "\n/onca/xml\n" + quoted_strings 181 | 182 | # convert unicode to UTF8 bytes for hmac library 183 | if type(self.AWSSecretAccessKey) is unicode: 184 | self.AWSSecretAccessKey = self.AWSSecretAccessKey.encode('utf-8') 185 | 186 | if type(data) is unicode: 187 | data = data.encode('utf-8') 188 | 189 | # calculate sha256 signature 190 | digest = hmac.new(self.AWSSecretAccessKey, data, sha256).digest() 191 | 192 | # base64 encode and urlencode 193 | if sys.version_info[0] == 3: 194 | signature = urllib.parse.quote(b64encode(digest)) 195 | else: 196 | signature = urllib.quote(b64encode(digest)) 197 | 198 | return ("https://" + service_domain + "/onca/xml?" + 199 | quoted_strings + "&Signature=%s" % signature) 200 | 201 | def cache_url(self, **kwargs): 202 | """A simplified URL to be used for caching the given query.""" 203 | query = { 204 | 'Operation': self.Operation, 205 | 'Service': "AWSECommerceService", 206 | 'Version': self.Version, 207 | } 208 | query.update(kwargs) 209 | 210 | service_domain = SERVICE_DOMAINS[self.Region][0] 211 | 212 | return "https://" + service_domain + "/onca/xml?" + _quote_query(query) 213 | 214 | def _call_api(self, api_url, err_env): 215 | """urlopen(), plus error handling and possible retries. 216 | 217 | err_env is a dict of additional info passed to the error handler 218 | """ 219 | while True: # may retry on error 220 | api_request = urllib2.Request( 221 | api_url, headers={"Accept-Encoding": "gzip"}) 222 | 223 | log.debug("Amazon URL: %s" % api_url) 224 | 225 | try: 226 | if self.Timeout and sys.version[:3] in ["2.4", "2.5"]: 227 | # urllib2.urlopen() doesn't accept timeout until 2.6 228 | old_timeout = socket.getdefaulttimeout() 229 | try: 230 | socket.setdefaulttimeout(self.Timeout) 231 | return urllib2.urlopen(api_request) 232 | finally: 233 | socket.setdefaulttimeout(old_timeout) 234 | else: 235 | # the simple way 236 | return urllib2.urlopen(api_request, timeout=self.Timeout) 237 | except: 238 | if not self.ErrorHandler: 239 | raise 240 | 241 | exception = sys.exc_info()[1] # works in Python 2 and 3 242 | err = {'exception': exception} 243 | err.update(err_env) 244 | if not self.ErrorHandler(err): 245 | raise 246 | 247 | def __call__(self, **kwargs): 248 | if 'Style' in kwargs: 249 | raise AmazonError("The `Style` parameter has been discontinued by" 250 | " AWS. Please remove all references to it and" 251 | " reattempt your request.") 252 | 253 | cache_url = self.cache_url(**kwargs) 254 | 255 | if self.CacheReader: 256 | cached_response_text = self.CacheReader(cache_url) 257 | if cached_response_text is not None: 258 | return self._maybe_parse(cached_response_text) 259 | 260 | api_url = self.api_url(**kwargs) 261 | 262 | # throttle ourselves if need be 263 | if self.MaxQPS: 264 | last_query_time = self._last_query_time[0] 265 | if last_query_time: 266 | wait_time = 1 / self.MaxQPS - (time.time() - last_query_time) 267 | if wait_time > 0: 268 | log.debug('Waiting %.3fs to call Amazon API' % wait_time) 269 | time.sleep(wait_time) 270 | 271 | self._last_query_time[0] = time.time() 272 | 273 | # make the actual API call 274 | response = self._call_api(api_url, 275 | {'api_url': api_url, 'cache_url': cache_url}) 276 | 277 | # decompress the response if need be 278 | if sys.version_info[0] == 3: 279 | if "gzip" in response.info().get("Content-Encoding"): 280 | response_text = gzip.decompress(response.read()) 281 | else: 282 | response_text = response.read() 283 | else: 284 | if "gzip" in response.info().getheader("Content-Encoding"): 285 | gzipped_file = gzip.GzipFile(fileobj=StringIO(response.read())) 286 | response_text = gzipped_file.read() 287 | else: 288 | response_text = response.read() 289 | 290 | # write it back to the cache 291 | if self.CacheWriter: 292 | self.CacheWriter(cache_url, response_text) 293 | 294 | # parse and return it 295 | return self._maybe_parse(response_text) 296 | 297 | 298 | class Amazon(AmazonCall): 299 | def __init__(self, AWSAccessKeyId=None, AWSSecretAccessKey=None, 300 | AssociateTag=None, Operation=None, Version="2013-08-01", 301 | Region="US", Timeout=None, MaxQPS=None, Parser=None, 302 | CacheReader=None, CacheWriter=None, ErrorHandler=None): 303 | """Create an Amazon API object. 304 | 305 | AWSAccessKeyId: Your AWS Access Key, sent with API queries. If not 306 | set, will be automatically read from the environment 307 | variable $AWS_ACCESS_KEY_ID 308 | AWSSecretAccessKey: Your AWS Secret Key, used to sign API queries. If 309 | not set, will be automatically read from the 310 | environment variable $AWS_SECRET_ACCESS_KEY 311 | AssociateTag: Your "username" for the Amazon Affiliate program, 312 | sent with API queries. 313 | Version: API version. The default should work 314 | Region: ccTLD you want to search for products on (e.g. 'UK' 315 | for amazon.co.uk). Must be uppercase. Default is 'US'. 316 | Timeout: optional timeout for queries 317 | MaxQPS: optional maximum queries per second. If we've made an API call 318 | on this object more recently that 1/MaxQPS, we'll wait 319 | before making the call. Useful for making batches of queries. 320 | You generally want to set this a little lower than the 321 | max (so 0.9, not 1.0). 322 | Parser: a function that takes the raw API response (XML in a 323 | bytestring) and returns a more convenient object of 324 | your choice; if set, API calls will pass the response through 325 | this 326 | CacheReader: Called before attempting to make an API call. 327 | A function that takes a single argument, the URL that 328 | would be passed to the API, minus auth information, 329 | and returns a cached version of the (unparsed) response, 330 | or None 331 | CacheWriter: Called after a successful API call. A function that 332 | takes two arguments, the same URL passed to 333 | CacheReader, and the (unparsed) API response. 334 | ErrorHandler: Called after an unsuccessful API call, with a 335 | dictionary containing these values: 336 | exception: the exception (an HTTPError or URLError) 337 | api_url: the url called 338 | cache_url: the url used for caching purposes 339 | (see CacheReader above) 340 | If this returns true, the call will be retried 341 | (you generally want to wait some time before 342 | returning, in this case) 343 | """ 344 | # Operation is for internal use by AmazonCall.__getattr__() 345 | 346 | AmazonCall.__init__(self, AWSAccessKeyId, AWSSecretAccessKey, 347 | AssociateTag, Operation, Version=Version, 348 | Region=Region, Timeout=Timeout, 349 | MaxQPS=MaxQPS, Parser=Parser, 350 | CacheReader=CacheReader, 351 | CacheWriter=CacheWriter, 352 | ErrorHandler=ErrorHandler) 353 | 354 | 355 | __all__ = ["Amazon", "AmazonError"] 356 | -------------------------------------------------------------------------------- /bottlenose/metadata.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2017 Lionheart Software LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __version__ = "1.1.10" 16 | __author__ = "Dan Loewenherz" 17 | __copyright__ = "Copyright 2012-2017 Lionheart Software LLC" 18 | __maintainer__ = "Dan Loewenherz" 19 | __email__ = "dan@lionheartsw.com" 20 | __license__ = "Apache 2.0" 21 | 22 | -------------------------------------------------------------------------------- /meta/header.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lionheart/bottlenose/a8381ec56ab427e08c45b1ac7bcdab30dca10724/meta/header.sketch -------------------------------------------------------------------------------- /meta/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lionheart/bottlenose/a8381ec56ab427e08c45b1ac7bcdab30dca10724/meta/logo.png -------------------------------------------------------------------------------- /meta/repo-banner-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lionheart/bottlenose/a8381ec56ab427e08c45b1ac7bcdab30dca10724/meta/repo-banner-2.png -------------------------------------------------------------------------------- /meta/repo-banner-bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lionheart/bottlenose/a8381ec56ab427e08c45b1ac7bcdab30dca10724/meta/repo-banner-bottom.png -------------------------------------------------------------------------------- /meta/repo-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lionheart/bottlenose/a8381ec56ab427e08c45b1ac7bcdab30dca10724/meta/repo-banner.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [egg_info] 2 | tag_svn_revision = false 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2012-2017 Lionheart Software LLC 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | import sys 19 | import os 20 | 21 | try: 22 | from setuptools import setup 23 | except ImportError: 24 | from distutils.core import setup 25 | 26 | metadata = {} 27 | exec(compile(open("bottlenose/metadata.py").read(), "bottlenose/metadata.py", 'exec'), metadata) 28 | 29 | install_requires = [] 30 | if sys.version_info < (2, 6): 31 | # Python 2.6 was the first version to come bundled with the json module. 32 | install_requires.append("simplejson>=1.7.1") 33 | 34 | # http://pypi.python.org/pypi?:action=list_classifiers 35 | classifiers = [ 36 | "Development Status :: 5 - Production/Stable", 37 | "Environment :: Console", 38 | "Intended Audience :: Developers", 39 | "Natural Language :: English", 40 | "Operating System :: OS Independent", 41 | "Programming Language :: Python", 42 | "Topic :: Software Development :: Libraries :: Python Modules", 43 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries", 44 | "Topic :: Utilities", 45 | "License :: OSI Approved :: Apache Software License", 46 | "Programming Language :: Python :: 2.4", 47 | "Programming Language :: Python :: 2.5", 48 | "Programming Language :: Python :: 2.6", 49 | "Programming Language :: Python :: 2.7", 50 | "Programming Language :: Python :: 3.4", 51 | "Programming Language :: Python :: 3.5", 52 | "Programming Language :: Python :: 3.6" 53 | ] 54 | 55 | setup( 56 | name='bottlenose', 57 | version=metadata['__version__'], 58 | description="A Python hook into the Amazon.com Product Advertising API", 59 | classifiers=classifiers, 60 | keywords='amazon, product advertising, api', 61 | author=metadata['__author__'], 62 | author_email=metadata['__email__'], 63 | url='https://github.com/lionheart/bottlenose', 64 | packages=["bottlenose"], 65 | data_files=[("", ["LICENSE", "README.md"])], 66 | license=metadata['__license__'], 67 | install_requires=install_requires, 68 | ) 69 | --------------------------------------------------------------------------------