diff --git a/.gitignore b/.gitignore index b6feec6..c75da77 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ test_settings.py _build _static _templates +.tox/ diff --git a/.travis.yml b/.travis.yml index 1c0a025..e29bfd8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,22 @@ language: python python: - - 2.7 -install: - - pip install . - - pip install pep8 - - pip install -r requirements.txt - - pip install -r test-requirements.txt -script: - - "pep8 --ignore=E501,E225,E128 amazon" - - "sphinx-build -b html -d _build/doctrees ./docs/ _build/html" -sudo: false +- 2.7 +- 3.5 +install: +- pip install -r requirements.txt +- pip install -r test-requirements.txt +- pip install . +script: +- pep8 --ignore=E501,E225,E128 amazon +- sphinx-build -b html -d _build/doctrees ./docs/ _build/html +- nosetests --with-flaky --with-coverage --cover-package=amazon +after_success: +- coveralls +sudo: false +deploy: + provider: pypi + user: "Yoav.Aviram" + password: + secure: FOaqWOnjlWbM/K7MmyAXy1V6zDUYEkfO4FgLYiAOoTXGfNdOKSJGxKD7cb+Wq5CjN3XX/Y1KNXwXZZ4odKRWX463Q80vU4x6wpxMbFYLP36EuwiFNqW63qYLftbSkqDrLWRjp06vENDGmadStdYb9CruboLzyau07mFPpHmuWmg= + on: + tags: true diff --git a/CODESHELTER.md b/CODESHELTER.md new file mode 100644 index 0000000..365ea79 --- /dev/null +++ b/CODESHELTER.md @@ -0,0 +1,25 @@ +# Note to Code Shelter maintainers + +I started this project a while ago and it grow and grow in popularity. Over the +past few years I've hed less and less time to work on it, and that's unfair to +all the people who use. I'd appreciate some help with maintaining the project. +I will generally be available to consult and maybe develop the occasional +feature or bugfix, but my availability is not reliable. + +It would be great if you could help out with issue triage, fixing bugs, +merging pull request, improving documentation and test coverage and +occasionally developing new features that you think are necessary. The project +is mainly feature-complete and the Amazon API is quite stable, so I don't +expect any major deviation and would appreciate being consulted before making +any drastic changes, but other than that, you have free reign. + +I have already added Code Shelter to the project's PyPI page, so feel free to +make any releases necessary. + +The codebase is in reasonable shape. The one major issue which needs sorting out +is that Amazon have terminated my associate account due to inactivity, and won't +let me create a new one (apperently maintaining this library does not qualify as +a reason). The implication is that I do not have credentials to test the library with. +I am working on sorting this one out but any help is welcome. + +Thank you! diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 999a966..0000000 --- a/LICENSE +++ /dev/null @@ -1,13 +0,0 @@ -Copyright (C) 2012 Yoav Aviram. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7c1e6db --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include *.md *.rst *.txt INSTALL LICENSE tox.ini .travis.yml docs/Makefile +include tests.py +recursive-include docs *.py +recursive-include docs *.rst +prune docs/_build diff --git a/README.md b/README.md index 3e77d33..ab4d917 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ Amazon Simple Product API ========================== A simple Python wrapper for the Amazon.com Product Advertising API. + [](http://travis-ci.org/yoavaviram/python-amazon-simple-product-api) [](http://python-amazon-simple-product-api.readthedocs.org/en/latest/?badge=latest) - +[](https://coveralls.io/github/yoavaviram/python-amazon-simple-product-api?branch=master) +[](https://badge.fury.io/py/python-amazon-simple-product-api) + + + +[](https://www.codeshelter.co/) Features -------- @@ -64,16 +70,18 @@ Lookup on amazon.de instead of amazon.com by setting the region: >>> product.price_and_currency (99.0, 'EUR') -Batch lookup requests are also supported: +Bulk lookup requests are also supported: >>> from amazon.api import AmazonAPI >>> amazon = AmazonAPI(AMAZON_ACCESS_KEY, AMAZON_SECRET_KEY, AMAZON_ASSOC_TAG) - >>> products = amazon.lookup(ItemId='B0051QVESA,B005DOK8NW,B005890G8Y,B0051VVOB2,B005890G8O') + >>> products = amazon.lookup(ItemId='B00KC6I06S,B005DOK8NW,B00TSUGXKE') >>> len(products) 5 >>> products[0].asin 'B0051QVESA' +If you'd rather get an empty list intead of exceptions use lookup_bulk() instead. + Search: >>> from amazon.api import AmazonAPI @@ -128,6 +136,29 @@ Browse Node Lookup: >>> bn.name 'eBook Readers' +Create and manipulate Carts: + + >>> from amazon.api import AmazonAPI + >>> amazon = AmazonAPI(AMAZON_ACCESS_KEY, AMAZON_SECRET_KEY, AMAZON_ASSOC_TAG) + >>> product = amazon.lookup(ItemId="B0016J8AOC") + >>> item = {'offer_id': product.offer_id, 'quantity': 1} + >>> cart = amazon.cart_create(item) + >>> fetched_cart = amazon.cart_get(cart.cart_id, cart.hmac) + >>> another_product = amazon.lookup(ItemId='0312098286') + >>> another_item = {'offer_id': another_product.offer_id, 'quantity': 1} + >>> another_cart = amazon.cart_add(another_item, cart.cart_id, cart.hmac) + >>> cart_item_id = None + >>> for item in cart: + >>> cart_item_id = item.cart_item_id + >>> modify_item = {'cart_item_id': cart_item_id, 'quantity': 3} + >>> modified_cart = amazon.cart_modify(item, cart.cart_id, cart.hmac) + >>> cleared_cart = amazon.cart_clear(cart.cart_id, cart.hmac) + +For the 'Books' SearchIndex a [Power Search](https://docs.aws.amazon.com/AWSECommerceService/latest/DG/PowerSearchSyntax.html) option is avaialble: + + >>> products = amazon.search(Power="subject:history and (spain or mexico) and not military and language:spanish",SearchIndex='Books') + + For more information about these calls, please consult the [Product Advertising API Developer Guide](http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html). @@ -139,6 +170,17 @@ To run the test suite please follow these steps: * Create a local file named: `test_settings.py` with the following variables set to the relevant values: `AMAZON_ACCESS_KEY`, `AMAZON_SECRET_KEY`, `AMAZON_ASSOC_TAG` * Run `nosetests` +Pull Requests +-------------- + +* All code should be unit tested +* All tests must pass +* Source code should be PEP8 complient +* Coverage shouldn't decrease +* All Pull Requests should be rebased against master before submitting the PR + +**This project is looking for core contributors. Please message me.** + License ------- diff --git a/amazon/__init__.py b/amazon/__init__.py index e27dcd2..bf8cadb 100644 --- a/amazon/__init__.py +++ b/amazon/__init__.py @@ -6,5 +6,5 @@ Amazon Product Advertising API Client library. """ -__version__ = '2.0.2' +__version__ = '2.2.11' __author__ = 'Yoav Aviram' diff --git a/amazon/api.py b/amazon/api.py index 510d663..99a7ad9 100644 --- a/amazon/api.py +++ b/amazon/api.py @@ -19,10 +19,10 @@ import bottlenose from lxml import objectify, etree import dateutil.parser +from decimal import Decimal # https://kdp.amazon.com/help?topicId=A1CT8LK6UW2FXJ -# CN not listed DOMAINS = { 'CA': 'ca', 'DE': 'de', @@ -33,6 +33,7 @@ 'JP': 'co.jp', 'UK': 'co.uk', 'US': 'com', + 'CN': 'cn' } AMAZON_ASSOCIATES_BASE_URL = 'http://www.amazon.{domain}/dp/' @@ -80,6 +81,13 @@ class NoMorePages(SearchException): pass +class RequestThrottled(AmazonException): + """Exception for when Amazon has throttled a request, per: + http://docs.aws.amazon.com/AWSECommerceService/latest/DG/ErrorNumbers.html + """ + pass + + class SimilartyLookupException(AmazonException): """Similarty Lookup Exception. """ @@ -169,7 +177,7 @@ def lookup(self, ResponseGroup="Large", **kwargs): code = root.Items.Request.Errors.Error.Code msg = root.Items.Request.Errors.Error.Message raise LookupException( - "Amazon Product Lookup Error: '{0}', '{1}'".format(code, msg)) + u"Amazon Product Lookup Error: '{0}', '{1}'".format(code, msg)) if not hasattr(root.Items, 'Item'): raise AsinNotFound("ASIN(s) not found: '{0}'".format( etree.tostring(root, pretty_print=True))) @@ -189,6 +197,27 @@ def lookup(self, ResponseGroup="Large", **kwargs): region=self.region ) + def lookup_bulk(self, ResponseGroup="Large", **kwargs): + """Lookup Amazon Products in bulk. + + Returns all products matching requested ASINs, ignoring invalid + entries. + + :return: + A list of :class:`~.AmazonProduct` instances. + """ + response = self.api.ItemLookup(ResponseGroup=ResponseGroup, **kwargs) + root = objectify.fromstring(response) + if not hasattr(root.Items, 'Item'): + return [] + return list( + AmazonProduct( + item, + self.aws_associate_tag, + self, + region=self.region) for item in root.Items.Item + ) + def similarity_lookup(self, ResponseGroup="Large", **kwargs): """Similarty Lookup. @@ -277,14 +306,12 @@ def cart_create(self, items, **kwargs): if len(items) > 10: raise CartException("You can't add more than 10 items at once") - offer_id_key_template = 'Item.%s.OfferListingId' - quantity_key_template = 'Item.%s.Quantity' - i = 0 + offer_id_key_template = 'Item.{0}.OfferListingId' + quantity_key_template = 'Item.{0}.Quantity' - for item in items: - i += 1 - kwargs[offer_id_key_template % (i, )] = item['offer_id'] - kwargs[quantity_key_template % (i, )] = item['quantity'] + for i, item in enumerate(items): + kwargs[offer_id_key_template.format(i)] = item['offer_id'] + kwargs[quantity_key_template.format(i)] = item['quantity'] response = self.api.CartCreate(**kwargs) root = objectify.fromstring(response) @@ -305,7 +332,7 @@ def cart_add(self, items, CartId=None, HMAC=None, **kwargs): An :class:`~.AmazonCart`. """ if not CartId or not HMAC: - raise CartException('CartId required for CartClear call') + raise CartException('CartId and HMAC required for CartAdd call') if isinstance(items, dict): items = [items] @@ -313,14 +340,12 @@ def cart_add(self, items, CartId=None, HMAC=None, **kwargs): if len(items) > 10: raise CartException("You can't add more than 10 items at once") - offer_id_key_template = 'Item.%s.OfferListingId' - quantity_key_template = 'Item.%s.Quantity' - i = 0 + offer_id_key_template = 'Item.{0}.OfferListingId' + quantity_key_template = 'Item.{0}.Quantity' - for item in items: - i += 1 - kwargs[offer_id_key_template % (i, )] = item['offer_id'] - kwargs[quantity_key_template % (i, )] = item['quantity'] + for i, item in enumerate(items): + kwargs[offer_id_key_template.format(i)] = item['offer_id'] + kwargs[quantity_key_template.format(i)] = item['quantity'] response = self.api.CartAdd(CartId=CartId, HMAC=HMAC, **kwargs) root = objectify.fromstring(response) @@ -383,14 +408,12 @@ def cart_modify(self, items, CartId=None, HMAC=None, **kwargs): if len(items) > 10: raise CartException("You can't add more than 10 items at once") - cart_item_id_key_template = 'Item.%s.CartItemId' - quantity_key_template = 'Item.%s.Quantity' - i = 0 + cart_item_id_key_template = 'Item.{0}.CartItemId' + quantity_key_template = 'Item.{0}.Quantity' - for item in items: - i += 1 - kwargs[cart_item_id_key_template % (i, )] = item['cart_item_id'] - kwargs[quantity_key_template % (i, )] = item['quantity'] + for i, item in enumerate(items): + kwargs[cart_item_id_key_template.format(i)] = item['cart_item_id'] + kwargs[quantity_key_template.format(i)] = item['quantity'] response = self.api.CartModify(CartId=CartId, HMAC=HMAC, **kwargs) root = objectify.fromstring(response) @@ -400,7 +423,8 @@ def cart_modify(self, items, CartId=None, HMAC=None, **kwargs): return new_cart - def _check_for_cart_error(self, cart): + @staticmethod + def _check_for_cart_error(cart): if cart._safe_get_element('Cart.Request.Errors') is not None: error = cart._safe_get_element( 'Cart.Request.Errors.Error.Code').text @@ -503,7 +527,8 @@ def __init__(self, api, aws_associate_tag, **kwargs): An string representing an Amazon Associates tag. """ self.kwargs = kwargs - self.current_page = 1 + self.current_page = 0 + self.is_last_page = False self.api = api self.aws_associate_tag = aws_associate_tag @@ -531,9 +556,9 @@ def iterate_pages(self): Yields lxml root elements. """ try: - while True: - yield self._query(ItemPage=self.current_page, **self.kwargs) + while not self.is_last_page: self.current_page += 1 + yield self._query(ItemPage=self.current_page, **self.kwargs) except NoMorePages: pass @@ -553,9 +578,15 @@ def _query(self, ResponseGroup="Large", **kwargs): msg = root.Items.Request.Errors.Error.Message if code == 'AWS.ParameterOutOfRange': raise NoMorePages(msg) + elif code == 'HTTP Error 503': + raise RequestThrottled( + "Request Throttled Error: '{0}', '{1}'".format(code, msg)) else: raise SearchException( "Amazon Search Error: '{0}', '{1}'".format(code, msg)) + if hasattr(root.Items, 'TotalPages'): + if root.Items.TotalPages == self.current_page: + self.is_last_page = True return root @@ -645,6 +676,13 @@ def __init__(self, item, aws_associate_tag, api, *args, **kwargs): self.parent = None self.region = kwargs.get('region', 'US') + def __str__(self): + """Return redable representation. + + Uses the item's title. + """ + return self.title + @property def price_and_currency(self): """Get Offer Price and Currency. @@ -659,7 +697,7 @@ def price_and_currency(self): :return: A tuple containing: - 1. Float representation of price. + 1. Decimal representation of price. 2. ISO Currency code (string). """ price = self._safe_get_element_text( @@ -679,11 +717,22 @@ def price_and_currency(self): currency = self._safe_get_element_text( 'OfferSummary.LowestNewPrice.CurrencyCode') if price: - fprice = float(price) / 100 if 'JP' not in self.region else price - return fprice, currency + dprice = Decimal( + price) / 100 if 'JP' not in self.region else Decimal(price) + return dprice, currency else: return None, None + @property + def offer_id(self): + """Offer ID + + :return: + Offer ID (string). + """ + return self._safe_get_element( + 'Offers.Offer.OfferListing.OfferListingId') + @property def asin(self): """ASIN (Amazon ID) @@ -702,6 +751,26 @@ def sales_rank(self): """ return self._safe_get_element_text('SalesRank') + @property + def super_saver_shipping(self): + """Super Saver Shipping + + :return: + Super Saver Shipping (boolean). + """ + return self._safe_get_element_text( + 'Offers.Offer.OfferListing.IsEligibleForSuperSaverShipping') + + @property + def prime(self): + """Prime + + :return: + Prime (boolean). + """ + return self._safe_get_element_text( + 'Offers.Offer.OfferListing.IsEligibleForPrime') + @property def offer_url(self): """Offer URL @@ -912,7 +981,7 @@ def reviews(self): """ iframe = self._safe_get_element_text('CustomerReviews.IFrameURL') has_reviews = self._safe_get_element_text('CustomerReviews.HasReviews') - if has_reviews and has_reviews == 'true': + if has_reviews is not None and has_reviews == 'true': has_reviews = True else: has_reviews = False @@ -1078,14 +1147,16 @@ def list_price(self): :return: A tuple containing: - 1. Float representation of price. + 1. Decimal representation of price. 2. ISO Currency code (string). """ price = self._safe_get_element_text('ItemAttributes.ListPrice.Amount') currency = self._safe_get_element_text( 'ItemAttributes.ListPrice.CurrencyCode') if price: - return float(price) / 100, currency + dprice = Decimal( + price) / 100 if 'JP' not in self.region else Decimal(price) + return dprice, currency else: return None, None @@ -1223,6 +1294,148 @@ def directors(self): result.append(director.text) return result + @property + def is_adult(self): + """IsAdultProduct. + + :return: + IsAdultProduct (string) + """ + return self._safe_get_element_text('ItemAttributes.IsAdultProduct') + + @property + def product_group(self): + """ProductGroup. + + :return: + ProductGroup (string) + """ + return self._safe_get_element_text('ItemAttributes.ProductGroup') + + @property + def product_type_name(self): + """ProductTypeName. + + :return: + ProductTypeName (string) + """ + return self._safe_get_element_text('ItemAttributes.ProductTypeName') + + @property + def formatted_price(self): + """FormattedPrice. + + :return: + FormattedPrice (string) + """ + return self._safe_get_element_text( + 'OfferSummary.LowestNewPrice.FormattedPrice') + + @property + def running_time(self): + """RunningTime. + + :return: + RunningTime (string) + """ + return self._safe_get_element_text('ItemAttributes.RunningTime') + + @property + def studio(self): + """Studio. + + :return: + Studio (string) + """ + return self._safe_get_element_text('ItemAttributes.Studio') + + @property + def is_preorder(self): + """IsPreorder (Is Preorder) + + :return: + IsPreorder (string). + """ + return self._safe_get_element_text( + 'Offers.Offer.OfferListing.AvailabilityAttributes.IsPreorder') + + @property + def availability(self): + """Availability + + :return: + Availability (string). + """ + return self._safe_get_element_text( + 'Offers.Offer.OfferListing.Availability') + + @property + def availability_type(self): + """AvailabilityAttributes.AvailabilityType + + :return: + AvailabilityType (string). + """ + return self._safe_get_element_text( + 'Offers.Offer.OfferListing.AvailabilityAttributes.AvailabilityType' + ) + + @property + def availability_min_hours(self): + """AvailabilityAttributes.MinimumHours + + :return: + MinimumHours (string). + """ + return self._safe_get_element_text( + 'Offers.Offer.OfferListing.AvailabilityAttributes.MinimumHours') + + @property + def availability_max_hours(self): + """AvailabilityAttributes.MaximumHours + + :return: + MaximumHours (string). + """ + return self._safe_get_element_text( + 'Offers.Offer.OfferListing.AvailabilityAttributes.MaximumHours') + + @property + def detail_page_url(self): + """DetailPageURL. + + :return: + DetailPageURL (string) + """ + return self._safe_get_element_text('DetailPageURL') + + @property + def number_sellers(self): + """Number of offers - New. + + :return: + Number of offers - New (string)\ + """ + return self._safe_get_element_text('OfferSummary.TotalNew') + + @property + def is_eligible_for_super_saver_shipping(self): + """IsEligibleForSuperSaverShipping + + :return: + IsEligibleForSuperSaverShipping (Bool). + """ + return self._safe_get_element_text('Offers.Offer.OfferListing.IsEligibleForSuperSaverShipping') + + @property + def is_eligible_for_prime(self): + """IsEligibleForPrime + + :return: + IsEligibleForPrime (Bool). + """ + return self._safe_get_element_text('Offers.Offer.OfferListing.IsEligibleForPrime') + class AmazonCart(LXMLWrapper): """Wrapper around Amazon shopping cart. @@ -1274,7 +1487,8 @@ def __getitem__(self, cart_item_id): for item in self: if item.cart_item_id == cart_item_id: return item - raise KeyError('no item found with CartItemId: %s' % (cart_item_id,)) + raise KeyError( + 'no item found with CartItemId: {0}'.format(cart_item_id,)) class AmazonCartItem(LXMLWrapper): diff --git a/requirements.txt b/requirements.txt index 638de05..b390d76 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ lxml bottlenose -python-dateutil +python-dateutil \ No newline at end of file diff --git a/setup.py b/setup.py index 4a41150..1b77dcd 100644 --- a/setup.py +++ b/setup.py @@ -3,9 +3,16 @@ from setuptools import setup, find_packages +try: + long_description=open('READMxE.md', 'r').read() +except IOError: + long_description="" + + setup(name='python-amazon-simple-product-api', version=amazon.__version__, description="A simple Python wrapper for the Amazon.com Product Advertising API", + long_description=long_description, # http://pypi.python.org/pypi?:action=list_classifiers classifiers=[ "Development Status :: 5 - Production/Stable", @@ -14,16 +21,20 @@ "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries", "Topic :: Utilities", - "License :: OSI Approved :: BSD License", + "License :: OSI Approved :: Apache Software License", ], keywords='amazon, product advertising, api', author='Yoav Aviram', - author_email='support@cleverblocks.com', + author_email='yoav.aviram@gmail.com', url='https://github.com/yoavaviram/python-amazon-simple-product-api', - license='BSD', + license='Apache 2.0', packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), include_package_data=True, zip_safe=True, diff --git a/test-requirements.txt b/test-requirements.txt index aba1014..eb0ee24 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1 +1,6 @@ -Sphinx==1.1.3 +nose +Sphinx==1.1.3 +coverage +coveralls +flaky +pep8 \ No newline at end of file diff --git a/tests.py b/tests.py index 63336a4..d8b7c69 100644 --- a/tests.py +++ b/tests.py @@ -1,19 +1,41 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + import unittest -from nose.tools import assert_equals, assert_true, assert_false +from nose.tools import assert_equals, assert_true, assert_false, assert_raises +from flaky import flaky +import time import datetime +from decimal import Decimal from amazon.api import (AmazonAPI, CartException, CartInfoMismatchException, SearchException, + AmazonSearch, AsinNotFound) -from test_settings import (AMAZON_ACCESS_KEY, - AMAZON_SECRET_KEY, - AMAZON_ASSOC_TAG) +_AMAZON_ACCESS_KEY = None +_AMAZON_SECRET_KEY = None +_AMAZON_ASSOC_TAG = None + + +import os +if 'AMAZON_ACCESS_KEY' in os.environ and 'AMAZON_SECRET_KEY' in os.environ and 'AMAZON_ASSOC_TAG' in os.environ: + _AMAZON_ACCESS_KEY = os.environ['AMAZON_ACCESS_KEY'] + _AMAZON_SECRET_KEY = os.environ['AMAZON_SECRET_KEY'] + _AMAZON_ASSOC_TAG = os.environ['AMAZON_ASSOC_TAG'] +else: + from test_settings import (AMAZON_ACCESS_KEY, + AMAZON_SECRET_KEY, + AMAZON_ASSOC_TAG) + _AMAZON_ACCESS_KEY = AMAZON_ACCESS_KEY + _AMAZON_SECRET_KEY = AMAZON_SECRET_KEY + _AMAZON_ASSOC_TAG = AMAZON_ASSOC_TAG -TEST_ASIN = "B007HCCNJU" + +TEST_ASIN = "0312098286" PRODUCT_ATTRIBUTES = [ 'asin', 'author', 'binding', 'brand', 'browse_nodes', 'ean', 'edition', @@ -50,6 +72,10 @@ def cache_clear(): global CACHE CACHE = {} +def delay_rerun(err, *args): + time.sleep(5) + return True + class TestAmazonApi(unittest.TestCase): """Test Amazon API @@ -69,26 +95,27 @@ def setUp(self): Are imported from a custom file named: 'test_settings.py' """ self.amazon = AmazonAPI( - AMAZON_ACCESS_KEY, - AMAZON_SECRET_KEY, - AMAZON_ASSOC_TAG, + _AMAZON_ACCESS_KEY, + _AMAZON_SECRET_KEY, + _AMAZON_ASSOC_TAG, CacheReader=cache_reader, CacheWriter=cache_writer, MaxQPS=0.5 ) + @flaky(max_runs=3, rerun_filter=delay_rerun) def test_lookup(self): """Test Product Lookup. Tests that a product lookup for a kindle returns results and that the main methods are working. """ - product = self.amazon.lookup(ItemId="B00I15SB16") + product = self.amazon.lookup(ItemId="B00ZV9PXP2") assert_true('Kindle' in product.title) - assert_equals(product.ean, '0848719039726') + assert_equals(product.ean, '0848719083774') assert_equals( product.large_image_url, - 'http://ecx.images-amazon.com/images/I/51XGerXeYeL.jpg' + 'https://images-na.ssl-images-amazon.com/images/I/51hrdzXLUHL.jpg' ) assert_equals( product.get_attribute('Publisher'), @@ -96,32 +123,60 @@ def test_lookup(self): ) assert_equals(product.get_attributes( ['ItemDimensions.Width', 'ItemDimensions.Height']), - {'ItemDimensions.Width': '469', 'ItemDimensions.Height': '40'}) + {'ItemDimensions.Width': '450', 'ItemDimensions.Height': '36'}) assert_true(len(product.browse_nodes) > 0) assert_true(product.price_and_currency[0] is not None) assert_true(product.price_and_currency[1] is not None) assert_equals(product.browse_nodes[0].id, 2642129011) assert_equals(product.browse_nodes[0].name, 'eBook Readers') + @flaky(max_runs=3, rerun_filter=delay_rerun) def test_lookup_nonexistent_asin(self): """Test Product Lookup with a nonexistent ASIN. Tests that a product lookup for a nonexistent ASIN raises AsinNotFound. """ - self.assertRaises(AsinNotFound, self.amazon.lookup, ItemId="ABCD1234") + assert_raises(AsinNotFound, self.amazon.lookup, ItemId="ABCD1234") - def test_batch_lookup(self): - """Test Batch Product Lookup. + @flaky(max_runs=3, rerun_filter=delay_rerun) + def test_bulk_lookup(self): + """Test Baulk Product Lookup. - Tests that a batch product lookup request returns multiple results. + Tests that a bulk product lookup request returns multiple results. """ - asins = ['B007HCCNJU', 'B00BWYQ9YE', + asins = [TEST_ASIN, 'B00BWYQ9YE', 'B00BWYRF7E', 'B00D2KJDXA'] products = self.amazon.lookup(ItemId=','.join(asins)) assert_equals(len(products), len(asins)) for i, product in enumerate(products): assert_equals(asins[i], product.asin) + @flaky(max_runs=3, rerun_filter=delay_rerun) + def test_lookup_bulk(self): + """Test Bulk Product Lookup. + + Tests that a bulk product lookup request returns multiple results. + """ + asins = [TEST_ASIN, 'B00BWYQ9YE', + 'B00BWYRF7E', 'B00D2KJDXA'] + products = self.amazon.lookup_bulk(ItemId=','.join(asins)) + assert_equals(len(products), len(asins)) + for i, product in enumerate(products): + assert_equals(asins[i], product.asin) + + @flaky(max_runs=3, rerun_filter=delay_rerun) + def test_lookup_bulk_empty(self): + """Test Bulk Product Lookup With No Results. + + Tests that a bulk product lookup request with no results + returns an empty list. + """ + asins = ['not-an-asin', 'als-not-an-asin'] + products = self.amazon.lookup_bulk(ItemId=','.join(asins)) + assert_equals(type(products), list) + assert_equals(len(products), 0) + + @flaky(max_runs=3, rerun_filter=delay_rerun) def test_search(self): """Test Product Search. @@ -136,6 +191,7 @@ def test_search(self): else: assert_true(False, 'No search results returned.') + @flaky(max_runs=3, rerun_filter=delay_rerun) def test_search_n(self): """Test Product Search N. @@ -149,25 +205,37 @@ def test_search_n(self): ) assert_equals(len(products), 1) + @flaky(max_runs=3, rerun_filter=delay_rerun) + def test_search_iterate_pages(self): + products = self.amazon.search(Keywords='internet of things oreilly', + SearchIndex='Books') + assert_false(products.is_last_page) + for product in products: + pass + assert_true(products.is_last_page) + + + @flaky(max_runs=3, rerun_filter=delay_rerun) def test_search_no_results(self): """Test Product Search with no results. Tests that a product search with that returns no results throws a SearchException. """ - products = self.amazon.search(Title='HarryPotter', + products = self.amazon.search(Title='no-such-thing-on-amazon', SearchIndex='Automotive') - self.assertRaises(SearchException, (x for x in products).next) + assert_raises(SearchException, next, (x for x in products)) def test_amazon_api_defaults_to_US(self): """Test Amazon API defaults to the US store.""" amazon = AmazonAPI( - AMAZON_ACCESS_KEY, - AMAZON_SECRET_KEY, - AMAZON_ASSOC_TAG + _AMAZON_ACCESS_KEY, + _AMAZON_SECRET_KEY, + _AMAZON_ASSOC_TAG ) assert_equals(amazon.api.Region, "US") + @flaky(max_runs=3, rerun_filter=delay_rerun) def test_search_amazon_uk(self): """Test Poduct Search on Amazon UK. @@ -176,9 +244,9 @@ def test_search_amazon_uk(self): results were returned. """ amazon = AmazonAPI( - AMAZON_ACCESS_KEY, - AMAZON_SECRET_KEY, - AMAZON_ASSOC_TAG, + _AMAZON_ACCESS_KEY, + _AMAZON_SECRET_KEY, + _AMAZON_ASSOC_TAG, region="UK" ) assert_equals(amazon.api.Region, "UK", "Region has not been set to UK") @@ -190,6 +258,7 @@ def test_search_amazon_uk(self): is_gbp = 'GBP' in currencies assert_true(is_gbp, "Currency is not GBP, cannot be Amazon UK, though") + @flaky(max_runs=3, rerun_filter=delay_rerun) def test_similarity_lookup(self): """Test Similarity Lookup. @@ -198,6 +267,7 @@ def test_similarity_lookup(self): products = self.amazon.similarity_lookup(ItemId=TEST_ASIN) assert_true(len(products) > 5) + @flaky(max_runs=3, rerun_filter=delay_rerun) def test_product_attributes(self): """Test Product Attributes. @@ -207,6 +277,7 @@ def test_product_attributes(self): for attribute in PRODUCT_ATTRIBUTES: getattr(product, attribute) + @flaky(max_runs=3, rerun_filter=delay_rerun) def test_browse_node_lookup(self): """Test Browse Node Lookup. @@ -218,6 +289,7 @@ def test_browse_node_lookup(self): assert_equals(bn.name, 'eBook Readers') assert_equals(bn.is_category_root, False) + @flaky(max_runs=3, rerun_filter=delay_rerun) def test_obscure_date(self): """Test Obscure Date Formats @@ -228,14 +300,16 @@ def test_obscure_date(self): assert_equals(product.publication_date.month, 5) assert_true(isinstance(product.publication_date, datetime.date)) + @flaky(max_runs=3, rerun_filter=delay_rerun) def test_single_creator(self): """Test a product with a single creator """ product = self.amazon.lookup(ItemId="B00005NZJA") creators = dict(product.creators) assert_equals(creators[u"Jonathan Davis"], u"Narrator") - assert_equals(len(creators.values()), 1) + assert_equals(len(creators.values()), 2) + @flaky(max_runs=3, rerun_filter=delay_rerun) def test_multiple_creators(self): """Test a product with multiple creators """ @@ -245,12 +319,14 @@ def test_multiple_creators(self): assert_equals(creators[u"Colin Azariah-Kribbs"], u"Editor") assert_equals(len(creators.values()), 2) + @flaky(max_runs=3, rerun_filter=delay_rerun) def test_no_creators(self): """Test a product with no creators """ product = self.amazon.lookup(ItemId="8420658537") assert_false(product.creators) + @flaky(max_runs=3, rerun_filter=delay_rerun) def test_single_editorial_review(self): product = self.amazon.lookup(ItemId="1930846258") expected = u'In the title piece, Alan Turing' @@ -258,18 +334,20 @@ def test_single_editorial_review(self): assert_equals(product.editorial_review, product.editorial_reviews[0]) assert_equals(len(product.editorial_reviews), 1) + @flaky(max_runs=3, rerun_filter=delay_rerun) def test_multiple_editorial_reviews(self): - product = self.amazon.lookup(ItemId="B000FBJCJE") - expected = u'Only once in a great' + product = self.amazon.lookup(ItemId="B01HQA6EOC") + expected = u'
Introducing an instant classic—master storyteller' assert_equals(product.editorial_reviews[0][:len(expected)], expected) - expected = u'From the opening line' + expected = u'An Amazon Best Book of February 2017:' assert_equals(product.editorial_reviews[1][:len(expected)], expected) # duplicate data, amazon user data is great... - expected = u'Only once in a great' + expected = u'
Introducing an instant classic—master storyteller' assert_equals(product.editorial_reviews[2][:len(expected)], expected) assert_equals(len(product.editorial_reviews), 3) + @flaky(max_runs=3, rerun_filter=delay_rerun) def test_languages_english(self): """Test Language Data @@ -279,6 +357,7 @@ def test_languages_english(self): assert_true('english' in product.languages) assert_equals(len(product.languages), 1) + @flaky(max_runs=3, rerun_filter=delay_rerun) def test_languages_spanish(self): """Test Language Data @@ -289,24 +368,108 @@ def test_languages_spanish(self): assert_equals(len(product.languages), 1) def test_region(self): - amazon = AmazonAPI(AMAZON_ACCESS_KEY, AMAZON_SECRET_KEY, - AMAZON_ASSOC_TAG) + amazon = AmazonAPI(_AMAZON_ACCESS_KEY, _AMAZON_SECRET_KEY, + _AMAZON_ASSOC_TAG) assert_equals(amazon.region, 'US') # old 'region' parameter - amazon = AmazonAPI(AMAZON_ACCESS_KEY, AMAZON_SECRET_KEY, - AMAZON_ASSOC_TAG, region='UK') + amazon = AmazonAPI(_AMAZON_ACCESS_KEY, _AMAZON_SECRET_KEY, + _AMAZON_ASSOC_TAG, region='UK') assert_equals(amazon.region, 'UK') # kwargs method - amazon = AmazonAPI(AMAZON_ACCESS_KEY, AMAZON_SECRET_KEY, - AMAZON_ASSOC_TAG, Region='UK') + amazon = AmazonAPI(_AMAZON_ACCESS_KEY, _AMAZON_SECRET_KEY, + _AMAZON_ASSOC_TAG, Region='UK') assert_equals(amazon.region, 'UK') + @flaky(max_runs=3, rerun_filter=delay_rerun) + def test_is_adult(self): + product = self.amazon.lookup(ItemId="B01E7P9LEE") + assert_true(product.is_adult is not None) + + @flaky(max_runs=3, rerun_filter=delay_rerun) + def test_product_group(self): + product = self.amazon.lookup(ItemId="B01LXM0S25") + assert_equals(product.product_group, 'DVD') + + product = self.amazon.lookup(ItemId="B01NBTSVDN") + assert_equals(product.product_group, 'Digital Music Album') + + @flaky(max_runs=3, rerun_filter=delay_rerun) + def test_product_type_name(self): + product = self.amazon.lookup(ItemId="B01NBTSVDN") + assert_equals(product.product_type_name, 'DOWNLOADABLE_MUSIC_ALBUM') + + @flaky(max_runs=3, rerun_filter=delay_rerun) + def test_formatted_price(self): + product = self.amazon.lookup(ItemId="B01NBTSVDN") + assert_equals(product.formatted_price, '$12.49') + + @flaky(max_runs=3, rerun_filter=delay_rerun) + def test_price_and_currency(self): + product = self.amazon.lookup(ItemId="B01NBTSVDN") + price, currency = product.price_and_currency + assert_equals(price, Decimal('12.49')) + assert_equals(currency, 'USD') + + @flaky(max_runs=3, rerun_filter=delay_rerun) + def test_list_price(self): + product = self.amazon.lookup(ItemId="B01NBTSVDN") + price, currency = product.list_price + assert_equals(price, Decimal('12.49')) + assert_equals(currency, 'USD') + + @flaky(max_runs=3, rerun_filter=delay_rerun) + def test_running_time(self): + product = self.amazon.lookup(ItemId="B01NBTSVDN") + assert_equals(product.running_time, '3567') + + @flaky(max_runs=3, rerun_filter=delay_rerun) + def test_studio(self): + product = self.amazon.lookup(ItemId="B01NBTSVDN") + assert_equals(product.studio, 'Atlantic Records UK') + + @flaky(max_runs=3, rerun_filter=delay_rerun) + def test_is_preorder(self): + product = self.amazon.lookup(ItemId="B01NBTSVDN") + assert_equals(product.is_preorder , None) + + @flaky(max_runs=3, rerun_filter=delay_rerun) + def test_detail_page_url(self): + product = self.amazon.lookup(ItemId="B01NBTSVDN") + assert_true(product.detail_page_url.startswith('https://www.amazon.com/%C3%B7-Deluxe-Ed-Sheeran/dp/B01NBTSVDN')) + + @flaky(max_runs=3, rerun_filter=delay_rerun) + def test_availability(self): + product = self.amazon.lookup(ItemId="B00ZV9PXP2") + assert_equals(product.availability, 'Usually ships in 24 hours') + + product = self.amazon.lookup(ItemId="1491914254") # pre-order book + assert_equals(product.availability, 'Not yet published') + + @flaky(max_runs=3, rerun_filter=delay_rerun) + def test_availability_type(self): + product = self.amazon.lookup(ItemId="B00ZV9PXP2") + assert_equals(product.availability_type, 'now') + + product = self.amazon.lookup(ItemId="1491914254") # pre-order book + assert_equals(product.availability_type, 'now') + + product = self.amazon.lookup(ItemId="B00ZV9PXP2") # late availability + assert_equals(product.availability_type, 'now') + + @flaky(max_runs=3, rerun_filter=delay_rerun) + def test_availability_min_max_hours(self): + product = self.amazon.lookup(ItemId="B00ZV9PXP2") + assert_equals(product.availability_min_hours, '0') + assert_equals(product.availability_max_hours, '0') + + def test_kwargs(self): - amazon = AmazonAPI(AMAZON_ACCESS_KEY, AMAZON_SECRET_KEY, - AMAZON_ASSOC_TAG, MaxQPS=0.7) + amazon = AmazonAPI(_AMAZON_ACCESS_KEY, _AMAZON_SECRET_KEY, + _AMAZON_ASSOC_TAG, MaxQPS=0.7) + @flaky(max_runs=3, rerun_filter=delay_rerun) def test_images(self): """Test images property @@ -322,27 +485,26 @@ def test_images(self): class TestAmazonCart(unittest.TestCase): def setUp(self): self.amazon = AmazonAPI( - AMAZON_ACCESS_KEY, - AMAZON_SECRET_KEY, - AMAZON_ASSOC_TAG, + _AMAZON_ACCESS_KEY, + _AMAZON_SECRET_KEY, + _AMAZON_ASSOC_TAG, CacheReader=cache_reader, CacheWriter=cache_writer, MaxQPS=0.5 ) def test_cart_clear_required_params(self): - self.assertRaises(CartException, self.amazon.cart_clear, None, None) - self.assertRaises(CartException, self.amazon.cart_clear, 'NotNone', - None) - self.assertRaises(CartException, self.amazon.cart_clear, None, - 'NotNone') + assert_raises(CartException, self.amazon.cart_clear, None, None) + assert_raises(CartException, self.amazon.cart_clear, 'NotNone', + None) + assert_raises(CartException, self.amazon.cart_clear, None, + 'NotNone') def build_cart_object(self): - product = self.amazon.lookup(ItemId="B0016J8AOC") + product = self.amazon.lookup(ItemId="B00ZV9PXP2") return self.amazon.cart_create( { - 'offer_id': product._safe_get_element( - 'Offers.Offer.OfferListing.OfferListingId'), + 'offer_id': product.offer_id, 'quantity': 1 } ) @@ -352,8 +514,8 @@ def test_cart_create_single_item(self): assert_equals(len(cart), 1) def test_cart_create_multiple_item(self): - product1 = self.amazon.lookup(ItemId="B0016J8AOC") - product2 = self.amazon.lookup(ItemId="B007HCCNJU") + product1 = self.amazon.lookup(ItemId="B00ZV9PXP2") + product2 = self.amazon.lookup(ItemId=TEST_ASIN) asins = [product1.asin, product2.asin] cart = self.amazon.cart_create([ @@ -382,8 +544,8 @@ def test_cart_clear_wrong_hmac(self): # never use urlencoded hmac, as library encodes as well. Just in case # hmac = url_encoded_hmac we add some noise hmac = cart.url_encoded_hmac + '%3d' - self.assertRaises(CartInfoMismatchException, self.amazon.cart_clear, - cart.cart_id, hmac) + assert_raises(CartInfoMismatchException, self.amazon.cart_clear, + cart.cart_id, hmac) def test_cart_attributes(self): cart = self.build_cart_object() @@ -411,12 +573,12 @@ def test_cart_get_wrong_hmac(self): # not been used in test_cart_clear cache_clear() cart = self.build_cart_object() - self.assertRaises(CartInfoMismatchException, self.amazon.cart_get, - cart.cart_id, cart.hmac + '%3d') + assert_raises(CartInfoMismatchException, self.amazon.cart_get, + cart.cart_id, cart.hmac + '%3d') def test_cart_add(self): cart = self.build_cart_object() - product = self.amazon.lookup(ItemId="B007HCCNJU") + product = self.amazon.lookup(ItemId=TEST_ASIN) item = { 'offer_id': product._safe_get_element( 'Offers.Offer.OfferListing.OfferListingId'), @@ -441,7 +603,7 @@ def test_cart_delete(self): cart_item_id = item.cart_item_id item = {'cart_item_id': cart_item_id, 'quantity': 0} new_cart = self.amazon.cart_modify(item, cart.cart_id, cart.hmac) - self.assertRaises(KeyError, new_cart.__getitem__, cart_item_id) + assert_raises(KeyError, new_cart.__getitem__, cart_item_id) if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..6e39443 --- /dev/null +++ b/tox.ini @@ -0,0 +1,5 @@ +[tox] +envlist = py27, py33, py34, py35 +[testenv] +deps = nose +commands = nosetests