Commits

Sebastian Rahlf committed a989be2 Merge

Automated merge.

  • Participants
  • Parent commits 4a8daf9, 43bcb68

Comments (0)

Files changed (9)

File amazonproduct/api.py

-# Copyright (C) 2009-2011 Sebastian Rahlf <basti at redtoad dot de>
+# Copyright (C) 2009-2013 Sebastian Rahlf <basti at redtoad dot de>
 #
 # This program is release under the BSD License. You can find the full text of
 # the license in the LICENSE file.
 
 from amazonproduct.version import VERSION
 from amazonproduct.errors import *
-from amazonproduct.utils import load_config, running_on_gae, REQUIRED_KEYS
-from amazonproduct.processors import ITEMS_PAGINATOR
+from amazonproduct.utils import load_config, load_class
+from amazonproduct.utils import running_on_gae, REQUIRED_KEYS
+from amazonproduct.processors import ITEMS_PAGINATOR, BaseProcessor
 
 
-# load default processor
-try:
-    from amazonproduct.processors.objectify import Processor
-except ImportError:
-    from amazonproduct.processors.etree import Processor
+# first processors successfully imported is used
+PROCESSORS = [
+    'amazonproduct.processors.objectify.Processor',
+    'amazonproduct.processors.etree.Processor',
+    'amazonproduct.processors.minidom.Processor',
+]
+
 
 USER_AGENT = ('python-amazon-product-api/%s '
     '+http://pypi.python.org/pypi/python-amazon-product-api/' % VERSION)
         self.last_call = datetime(1970, 1, 1)
         self.debug = 0 # set to 1 if you want to see HTTP headers
 
-        if processor is not None:
+        if isinstance(processor, BaseProcessor):
             self.processor = processor
         else:
-            self.processor = Processor()
+            self.processor = self._load_processor(processor)()
+
+    @staticmethod
+    def _load_processor(*names):
+        """
+        Loads result processor. If no processor is given (``None``), the first
+        one is taken that can be successfully imported from the list of default
+        processors (:const:`PROCESSORS`).
+        """
+        if len(names) == 0 or names[0] is None:
+            names = PROCESSORS
+        for name in names:
+            # processor was given as string
+            if isinstance(name, (str, unicode)):
+                try:
+                    pclass = load_class(name)
+                    if issubclass(pclass, BaseProcessor):
+                        return pclass
+                except ImportError:
+                    continue
+            # processor was given as class
+            elif isinstance(name, type) and issubclass(name, BaseProcessor):
+                return name
+        # nothing successfully loaded
+        raise ImportError('No processor class could be imported!')
 
     def __repr__(self):
         return '<API(%s/%s) at %s>' % (self.VERSION, self.locale, hex(id(self)))
     def call(self, **qargs):
         """
         Builds a signed URL for the operation, fetches the result from Amazon
-        and parses the XML. If you want to customise things at any stage, simply
-        override the respective method(s):
+        and parses the XML.
+
+        Example::
+
+            xml = api.call(Operation='ItemLookup', ItemId='B067884223')
+
+        .. note:: If you want to customise things at any stage, simply override the respective method(s):
 
         * ``_build_url(**query_parameters)``
         * ``_fetch(url)``

File amazonproduct/processors/etree.py

+# Copyright (C) 2009-2013 Sebastian Rahlf <basti at redtoad dot de>
+#
+# This program is release under the BSD License. You can find the full text of
+# the license in the LICENSE file.
+
 import re
 from amazonproduct.contrib.cart import Cart, Item
 
 
 class Processor (BaseProcessor):
 
+    """
+    Result processor using ElementTree.
+
+    The first implementation of ElementTree which can be successfully imported
+    will be used. Order of import is:
+
+    * lxml.etree
+    * xml.etree.cElementTree
+    * xml.etree.ElementTree
+    * cElementTree
+    * elementtree.ElementTree
+
+    """
+
     def __init__(self, *args, **kwargs):
         # processor can be told which etree module to use in order to have
-        # multiple processors each using a different implementation 
+        # multiple processors each using a different implementation
         etree_mod = kwargs.pop('module', None)
         try:
             if etree_mod:
         except KeyError:
             return None
 
+
 class XPathPaginator (BaseResultPaginator):
 
     """
     total_results_xpath = './/{}Items/{}TotalResults'
     items = './/{}Items/{}Item'
 
+
 class RelatedItemsPaginator (XPathPaginator):
 
     counter = 'RelatedItemPage'

File amazonproduct/processors/minidom.py

+# Copyright (C) 2009-2013 Sebastian Rahlf <basti at redtoad dot de>
+#
+# This program is release under the BSD License. You can find the full text of
+# the license in the LICENSE file.
+
 import xml.dom.minidom
 
 from amazonproduct.errors import AWSError
 from amazonproduct.processors import BaseProcessor
 
+
 class Processor(BaseProcessor):
 
     """

File amazonproduct/processors/objectify.py

+# Copyright (C) 2009-2013 Sebastian Rahlf <basti at redtoad dot de>
+#
+# This program is release under the BSD License. You can find the full text of
+# the license in the LICENSE file.
 
 from lxml import etree, objectify
 

File amazonproduct/utils.py

+# Copyright (C) 2009-2013 Sebastian Rahlf <basti at redtoad dot de>
+#
+# This program is release under the BSD License. You can find the full text of
+# the license in the LICENSE file.
+
 from ConfigParser import SafeConfigParser
 import os
 import sys
     * Config files ``/etc/amazon-product-api.cfg`` or ``~/.amazon-product-api``
       where the latter may add or replace values of the former.
     * A boto config file [#]_ found in ``/etc/boto.cfg`` or ``~/.boto``.
-    
+
     Whatever is found first counts.
 
     The returned dictionary may look like this::
             'associate_tag': 'redtoad-10',
             'locale': 'uk'
         }
-    
+
     .. _#: http://code.google.com/p/boto/wiki/BotoConfig
     """
     config = load_boto_config()
     """
     return 'Google' in os.environ.get('SERVER_SOFTWARE', '')
 
+
+def load_class(name):
+    """
+    Loads class from string.
+
+    :param name: fully-qualified class name (e.g. ``processors.etree.
+      ItemPaginator``)
+    """
+    module_name, class_name = name.rsplit('.', 1)
+    module = import_module(module_name)
+    return getattr(module, class_name)
 _here = os.path.abspath(os.path.dirname(__file__))
 
 def version():
-    # This rather complicated mechanism is employed to avoid importing any 
-    # yet unfulfilled dependencies, for instance when installing under 
+    # This rather complicated mechanism is employed to avoid importing any
+    # yet unfulfilled dependencies, for instance when installing under
     # Python 2.4 from scratch
     import imp
     path = os.path.join(_here, 'amazonproduct', 'version.py')
     return mod.VERSION
 
 def read(fname):
-    # makes sure that setup can be executed from a different location
-    return open(os.path.join(_here, fname)).read()
+    try:
+        # makes sure that setup can be executed from a different location
+        return open(os.path.join(_here, fname)).read()
+    except IOError:
+        return ''
 
 def readme():
     # substitute all include statements.
         return read(matchobj.group(1))
     return re.sub(r'\.\. include:: (\w+)', insert_include, read('README.rst'))
 
-reqs = []
+extras = {
+    'setup_requires': [],
+}
+
  # for python2.4
 if sys.version_info[:2] < (2, 5):
-    reqs += ['pycrypto']
+    extras['setup_requires'] += ['pycrypto']
 
 class PyTest(Command):
     """
     def finalize_options(self): pass
     def run(self):
         import subprocess
-        errno = subprocess.call([sys.executable, os.path.join(_here, 'tests', 'runtests.py')])
+        errno = subprocess.call([
+            sys.executable, os.path.join(_here, 'tests', 'runtests.py'), '-vl'])
         raise SystemExit(errno)
 
 # make sure that no development version end up on PyPI
 if 'register' in sys.argv or 'upload' in sys.argv:
     version_ = version()
     if '/' in version_ or '+' in version_:
-        print 'ERROR: Version %r has not been adjusted yet!' % version_
-        sys.exit(1)
+        if 'local' in sys.argv:
+            extras['setup_requires'] += ['hgtools']
+            extras['use_hg_version'] = {'increment': '0.0.1'}
+        else:
+            print 'ERROR: Version %r has not been adjusted yet!' % version_
+            sys.exit(1)
 
 setup(
-    name = 'python-amazon-product-api',
-    version = version(),
-    author = 'Sebastian Rahlf',
-    author_email = 'basti AT redtoad DOT de',
+    name='python-amazon-product-api',
+    version=version(),
+    author='Sebastian Rahlf',
+    author_email='basti AT redtoad DOT de',
     url="http://bitbucket.org/basti/python-amazon-product-api/downloads/",
     license='bsd',
 
-    description = 'A Python wrapper for the Amazon Product Advertising API.',
+    description='A Python wrapper for the Amazon Product Advertising API.',
     long_description=readme(),
-    keywords = 'amazon product advertising api wrapper signed requests',
+    zip_safe=False,  # we want to find README.rst and version.py
+    keywords='amazon product advertising api wrapper signed requests',
 
-    packages = find_packages(_here, exclude=['tests']),
-    install_requires=reqs,
+    packages=find_packages(_here, exclude=['tests']),
 
-    cmdclass = {'test': PyTest},
-    test_requires=['pytest>=2.0.3,<2.2', 'pytest-localserver'],
+    cmdclass={'test': PyTest},
+    tests_require=[
+        'pytest>=2.0.3,<2.3',
+        'pytest-localserver',
+        'lxml',
+        'cElementTree',
+        'elementtree',
+        'tox',
+        'virtualenv<1.8',  # for testing Python 2.4
+    ],
 
-    classifiers = [
+    classifiers=[
         'Operating System :: OS Independent',
         'Development Status :: 3 - Alpha',
         'Intended Audience :: Developers',
         'Programming Language :: Python :: 2.7',
         'Topic :: Internet :: WWW/HTTP',
         'Topic :: Software Development :: Libraries :: Python Modules',
-    ]
+    ],
+
+    **extras
 )

File tests/test_utils.py

+import pytest
+import types
+
 from amazonproduct import utils
+from amazonproduct.processors import etree
 
 def test_load_global_boto_config(configfiles):
     configfiles.add_file('''
     assert cfg['associate_tag'] is None
     assert cfg['locale'] == 'OS VARIABLE'
 
+
+@pytest.mark.parametrize(('txt', 'cls'), [
+    ('amazonproduct.processors.etree.Processor', etree.Processor),
+    ('amazonproduct.processors.etree.XPathPaginator', etree.XPathPaginator),
+    ('amazonproduct.processors.etree.ItemPaginator', etree.ItemPaginator),
+])
+def test_load_class(txt, cls):
+    loaded = utils.load_class(txt)
+    assert isinstance(loaded, types.TypeType)
+    assert loaded == cls

File tests/test_xml_responses.py

     version = request.param['version']
     xml_response = request.param['xml_response']
 
-    processor = TESTABLE_PROCESSORS[request.param['processor']]
-    if isinstance(processor, type):
-        processor = processor()
-
-    api = API(locale=locale, processor=processor)
+    api = API(locale=locale,
+        processor=TESTABLE_PROCESSORS[request.param['processor']])
     api.VERSION = version
     api.REQUESTS_PER_SECOND = 10000 # just for here!
 
         return fnc
 
 
-
 class TestAPICredentials (object):
 
     """
     Check that API will complain about missing credentials.
     """
 
+    # locale CN does not seem to work!
+    locales = ['ca', 'de', 'es', 'fr', 'it', 'jp', 'uk', 'us']
+
     def test_without_credentials_fails(self, api):
         api.access_key = api.secret_key = ''
         pytest.raises(MissingClientTokenId, api.item_lookup, '???')
 [testenv]
 commands =
   py.test -v \
-    --junitxml=junit-{envname}.xml
-deps = 
+    --junitxml=junit-{envname}.xml \
+    tests
+deps =
+  pytest>=2.0.3,<2.3
+  pytest-localserver
   lxml
-  pytest
-  pytest-localserver
+  cElementTree
+  elementtree
+  pycrypto
 
 [testenv:docs]
 basepython = python