Commits

Jean-Tiare Le Bigot committed 1210901

packaging + first functional test

  • Participants
  • Parent commits bab3f60

Comments (0)

Files changed (14)

 syntax: glob
 *.pyc
 *.egg*
+.coverage
+ddbmock/.noseids
+ddbmock/coverage.xml
+nosetests.xml
+                   GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+  This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+  0. Additional Definitions.
+
+  As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+  "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+  An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+  A "Combined Work" is a work produced by combining or linking an
+Application with the Library.  The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+  The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+  The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+  1. Exception to Section 3 of the GNU GPL.
+
+  You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+  2. Conveying Modified Versions.
+
+  If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+   a) under this License, provided that you make a good faith effort to
+   ensure that, in the event an Application does not supply the
+   function or data, the facility still operates, and performs
+   whatever part of its purpose remains meaningful, or
+
+   b) under the GNU GPL, with none of the additional permissions of
+   this License applicable to that copy.
+
+  3. Object Code Incorporating Material from Library Header Files.
+
+  The object code form of an Application may incorporate material from
+a header file that is part of the Library.  You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+   a) Give prominent notice with each copy of the object code that the
+   Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the object code with a copy of the GNU GPL and this license
+   document.
+
+  4. Combined Works.
+
+  You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+   a) Give prominent notice with each copy of the Combined Work that
+   the Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the Combined Work with a copy of the GNU GPL and this license
+   document.
+
+   c) For a Combined Work that displays copyright notices during
+   execution, include the copyright notice for the Library among
+   these notices, as well as a reference directing the user to the
+   copies of the GNU GPL and this license document.
+
+   d) Do one of the following:
+
+       0) Convey the Minimal Corresponding Source under the terms of this
+       License, and the Corresponding Application Code in a form
+       suitable for, and under terms that permit, the user to
+       recombine or relink the Application with a modified version of
+       the Linked Version to produce a modified Combined Work, in the
+       manner specified by section 6 of the GNU GPL for conveying
+       Corresponding Source.
+
+       1) Use a suitable shared library mechanism for linking with the
+       Library.  A suitable mechanism is one that (a) uses at run time
+       a copy of the Library already present on the user's computer
+       system, and (b) will operate properly with a modified version
+       of the Library that is interface-compatible with the Linked
+       Version.
+
+   e) Provide Installation Information, but only if you would otherwise
+   be required to provide such information under section 6 of the
+   GNU GPL, and only to the extent that such information is
+   necessary to install and execute a modified version of the
+   Combined Work produced by recombining or relinking the
+   Application with a modified version of the Linked Version. (If
+   you use option 4d0, the Installation Information must accompany
+   the Minimal Corresponding Source and Corresponding Application
+   Code. If you use option 4d1, you must provide the Installation
+   Information in the manner specified by section 6 of the GNU GPL
+   for conveying Corresponding Source.)
+
+  5. Combined Libraries.
+
+  You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+   a) Accompany the combined library with a copy of the same work based
+   on the Library, uncombined with any other library facilities,
+   conveyed under the terms of this License.
+
+   b) Give prominent notice with the combined library that part of it
+   is a work based on the Library, and explaining where to find the
+   accompanying uncombined form of the same work.
+
+  6. Revised Versions of the GNU Lesser General Public License.
+
+  The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+  Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+  If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
 
 ::
 
-    import ddbmock, boto
+    import boto
+    from ddbmock import connect_boto
 
     # Wire-up boto and ddbmock together
-    ddbmock.connect_boto()
+    db = connect_boto()
 
     # Done ! just use it wherever in your project as usual.
     db = boto.connect_dynamodb()

File ddbmock/__init__.py

 # Monkey patch magic, required for the Boto entry point
 # Request hijacking Yeah !
 def connect_boto():
+    import boto
     from boto.dynamodb.layer1 import Layer1
     Layer1.make_request = _boto_make_request
+    return boto.connect_dynamodb()
 
 # Wrap the exception handling logic
 def _do_request(action, post):
     try:
-        import importlib.import_module as _import
+        from importlib import import_module
         target = routes[action]
-        dest = _import('ddbmock.views.{dest}._{dest}'.format(dest=target))
-        return (200, json.dumps(dest(post)))
+        mod = import_module('ddbmock.views.{}'.format(target))
+        func = getattr(mod, '_{}'.format(target))
+        return (200, json.dumps(func(post)))
     except KeyError:
         err = InternalFailure("Method: {} does not exist".format(action))
     except ImportError:
     except DDBError as e:
         err = e
 
-    return (err.status, json.dumps(err.to_dict()))
+    return err.status, json.dumps(err.to_dict())
 
 # Boto lib version entry point
 def _boto_make_request(self, action, body='', object_hook=None):
     # - request ID
     # - simulate retry/throughput errors ?
     # FIXME: dump followed by load... can be better...
-    import boto # do not make boto a global dependancy
+    import boto  # do not make boto a global dependancy
 
     target = '%s_%s.%s' % (self.ServiceName, self.Version, action)
     start = time.time()
-    (status, ret) = _do_request(action, json.loads(body))
-    elapsed = (time.time() - start)*1000
+    status, ret = _do_request(action, json.loads(body))
+    elapsed = (time.time() - start) * 1000
     request_id = 'STUB'
-    boto.log.debug('RequestId: %s' % request_id)
+    boto.log.debug('RequestId: %s', request_id)
     boto.perflog.info('dynamodb %s: id=%s time=%sms',
                       target, request_id, int(elapsed))
     boto.log.debug(ret)

File ddbmock/database/db.py

+# -*- coding: utf-8 -*-
+
+from .table import Table
+
+class DynamoDB(object):
+    shared_data = {
+        'data': {}
+    }
+
+    def __init__(self):
+        cls = type(self)
+        self.__dict__ = cls.shared_data
+
+    def hard_reset(self):
+        self.data.clear()
+
+    def list_tables(self):
+        return self.data.keys()
+
+    def get_table(self, name):
+        if name in self.data:
+            return self.data[name]
+        return None
+
+    def create_table(self, name, data):
+        if name in self.data:
+            return None
+        self.data[name] = Table.from_dict(data)
+        return self.data[name]
+
+    def delete_table(self, name):
+        if name not in self.data:
+            return None
+        self.data[name].delete()
+        ret = self.data[name].to_dict()
+        del self.data[name]
+        return ret

File ddbmock/database/key.py

+# -*- coding: utf-8 -*-
+
+import re
+
+class Key(object):
+    valid_types = ['N', 'S', 'B', 'NS', 'SS']
+    min_len = 1
+    max_len = 255
+
+    def validate(self, name, typename):
+        cls = type(self)
+        if typename not in cls.valid_types:
+            raise TypeError("Type must be one of {}. Got {}".format(cls.valid_types, typename))
+        l = len(name)
+        if (l < cls.min_len) or (l > cls.max_len):
+            raise TypeError("Name len must be between {} and {}. Got {}".format(cls.min_len, cls.max_len, l))
+
+    def __init__(self, name, typename):
+        self.validate(name, typename)
+        self.name = name
+        self.typename = typename
+
+    def to_dict(self):
+        return {
+            "AttributeName": self.name,
+            "AttributeType": self.typename,
+        }
+
+    @classmethod
+    def from_dict(cls, data):
+        if u'AttributeName' not in data:
+            raise TypeError("No attribute name")
+        if u'AttributeType' not in data:
+            raise TypeError("No attribute type")
+
+        return cls(data[u'AttributeName'], data[u'AttributeType'])
+
+class PrimaryKey(Key):
+    valid_types = ['N', 'S', 'B']

File ddbmock/database/table.py

+# -*- coding: utf-8 -*-
+
+import re
+
+from .key import Key, PrimaryKey
+
+class Table(object):
+    mask = re.compile(r'^[a-zA-Z0-9\-_\.]*$')
+    min_len = 3
+    max_len = 255
+
+    def __init__(self, name, rt, wt, hash_key, range_key):
+        self.name = self._validate_name(name)
+        self.rt = self._validate_throughput_value(rt)
+        self.wt = self._validate_throughput_value(wt)
+        self.hash_key = hash_key
+        self.range_key = range_key
+        self.status = "ACTIVE"
+
+    def _validate_name(self, name):
+        cls = type(self)
+        l = len(name)
+        if (l < cls.min_len) or (l > cls.max_len):
+            raise TypeError("TableName len must be between {} and {}. Got {}".format(cls.min_len, cls.max_len, l))
+        if cls.mask.match(name) is None:
+            raise TypeError("TableName chars must match pattern {}. Got {}".format(cls.mask.pattern, name))
+        return name
+
+    def _validate_throughput_value(self, value):
+        if value < 1 or value > 10000:
+            raise ValueError("Throughput value must be between {} and {}. Got {}".format(1, 10000, value))
+        return value
+
+    def delete(self):
+        #stub
+        self.status = "DELETING"
+
+    def update_throughput(self, rt, wt):
+        # TODO: check update rate
+        self.rt = self._validate_throughput_value(rt)
+        self.wt = self._validate_throughput_value(wt)
+
+    @classmethod
+    def from_dict(cls, data):
+        if u'TableName' not in data:
+            raise TypeError("No table name supplied")
+        if u'ProvisionedThroughput' not in data:
+            raise TypeError("No throughput provisioned")
+        if u'KeySchema' not in data:
+            raise TypeError("No schema")
+        if u'WriteCapacityUnits' not in data[u'ProvisionedThroughput']:
+            raise TypeError("No WRITE throughput provisioned")
+        if u'ReadCapacityUnits' not in data[u'ProvisionedThroughput']:
+            raise TypeError("No READ throughput provisioned")
+
+        if u'HashKeyElement' not in data[u'KeySchema']:
+            raise TypeError("No hash_key")
+        if u'RangeKeyElement' in data[u'KeySchema']:
+            range_key = PrimaryKey.from_dict(data[u'KeySchema'][u'RangeKeyElement'])
+        hash_key = PrimaryKey.from_dict(data[u'KeySchema'][u'HashKeyElement'])
+
+        return cls( data[u'TableName'],
+                    data[u'ProvisionedThroughput'][u'ReadCapacityUnits'],
+                    data[u'ProvisionedThroughput'][u'WriteCapacityUnits'],
+                    hash_key,
+                    range_key,
+                  )
+
+    def to_dict(self):
+        ret = {
+            "CreationDateTime":1.309988345372E9, #stub
+            "ItemCount": 0, # Stub
+            "KeySchema": {
+                "HashKeyElement": self.hash_key.to_dict(),
+            },
+            "ProvisionedThroughput": {
+                "LastIncreaseDateTime": 1.309988345384E9, #stub
+                "ReadCapacityUnits": self.rt,
+                "WriteCapacityUnits": self.wt,
+            },
+            "TableName": self.name,
+            "TableSizeBytes": -1, #STUB
+            "TableStatus": self.status
+        }
+
+        if self.range_key is not None:
+            ret[u'KeySchema'][u'RangeKeyElement'] = self.range_key.to_dict()
+
+        return ret

File ddbmock/errors.py

 def WrapExceptions(func):
     def wrapped(*args):
         try:
-            return func(*args[1:])
+            return func(*args)
         except (TypeError, ValueError) as e:
             raise ValidationException(*e.args)
     return wrapped

File ddbmock/tests/__init__.py

+# -*- coding: utf-8 -*-

File ddbmock/tests/functional/__init__.py

+# -*- coding: utf-8 -*-

File ddbmock/tests/functional/boto/__init__.py

+# -*- coding: utf-8 -*-

File ddbmock/tests/functional/boto/test_list_tables.py

+# -*- coding: utf-8 -*-
+
+import unittest
+import boto
+
+TABLE_NAME1 = 'Table-1'
+TABLE_NAME2 = 'Table-2'
+
+class TestListTables(unittest.TestCase):
+    def setUp(self):
+        from ddbmock.database.db import DynamoDB
+        from ddbmock.database.table import Table
+        from ddbmock.database.key import PrimaryKey
+
+        db = DynamoDB()
+        db.hard_reset()
+
+        hash_key = PrimaryKey('hash_key', 'N')
+        range_key = PrimaryKey('range_key', 'S')
+
+        t1 = Table(TABLE_NAME1, 10, 10, hash_key, range_key)
+        t2 = Table(TABLE_NAME2, 10, 10, hash_key, range_key)
+
+        db.data[TABLE_NAME1] = t1
+        db.data[TABLE_NAME2] = t2
+
+    def tearDown(self):
+        from ddbmock.database.db import DynamoDB
+        DynamoDB().hard_reset()
+
+    def test_list_tables(self):
+        from ddbmock import connect_boto
+        db = connect_boto()
+
+        expected = [TABLE_NAME1, TABLE_NAME2]
+
+        self.assertEqual(expected, db.list_tables())
+
+[metadata]
+name = ddbmock
+version = 0.1.0.dev
+summary = Amazon DynamoDB mock implementation
+description-file = README.rst
+author = Jean-Tiare Le Bigot
+author_email = jtlebigot@socialludia.com
+home_page = https://bitbucket.org/jtlebigot/dynamodb-mock
+classifier =
+    Topic :: Internet
+    Topic :: Database :: Front-Ends
+    Operating System :: OS Independent
+    Intended Audience :: Developers
+    Development Status :: 3 - Alpha
+    Programming Language :: Python
+    Programming Language :: Python :: 2.7
+    License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
+requires-dist =
+
 [nosetests]
+where = ddbmock
 match = ^test
 nocapture = 1
 cover-package = ddbmock
-with-coverage = 1
-cover-erase = 1
+with-xcoverage = 1
+with-xunit = 1
+cover-erase = 1
+verbosity = 3
+with-id = 1
 from setuptools import setup, find_packages
 
 here = os.path.abspath(os.path.dirname(__file__))
-README = open(os.path.join(here, 'README.txt')).read()
-CHANGES = open(os.path.join(here, 'CHANGES.txt')).read()
 
-requires = [
+install_requires = [
+    # d2to1 bootstrap
+    'd2to1',
+
     'pyramid',
     'waitress',
-    ]
+    'webtest'
+]
+
+tests_requires = [
+    # d2to1 bootstrap
+    'd2to1',
+    'pyramid',
+    'boto',
+
+    'nose',
+    'nosexcover',
+    'coverage',
+    'mock',
+]
 
 setup(name='ddbmock',
-      version='0.1',
-      description='ddbmock',
-      long_description=README + '\n\n' +  CHANGES,
-      classifiers=[
-        "Programming Language :: Python",
-        "Framework :: Pylons",
-        "Topic :: Internet :: WWW/HTTP",
-        "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
-        ],
-      author='',
-      author_email='',
-      url='',
-      keywords='web pyramid pylons',
+      d2to1=True,
+      keywords='pyramid dynamodb mock',
       packages=find_packages(),
       include_package_data=True,
       zip_safe=False,
-      install_requires=requires,
-      tests_require=requires,
+      install_requires=install_requires,
+      tests_require=tests_requires,
       test_suite="ddbmock",
       entry_points = """\
       [paste.app_factory]
       main = ddbmock:main
       """,
-      )
+)