Commits

Jean-Tiare Le Bigot  committed e0027ca

Entry point cleanup + Exception logic

- move boto specific part of ddb.__init__ to router
- move routes to... router :)
- better exception logic, now compatible with boto

  • Participants
  • Parent commits ca46689

Comments (0)

Files changed (6)

File ddbmock/__init__.py

 # -*- coding: utf-8 -*-
 
-import json, time
-from ddbmock.errors import *
+from ddbmock.router import routes
 from pyramid.config import Configurator
 
-# src: dest
-routes = {
-    'BatchGetItem':   'batch_get_item',
-    'BatchWriteItem': 'batch_write_item',
-    'CreateTable':    'create_table',
-    'DeleteItem':     'delete_item',
-    'DeleteTable':    'delete_table',
-    'DescribeTable':  'describe_table',
-    'GetItem':        'get_item',
-    'ListTables':     'list_tables',
-    'PutItem':        'put_item',
-    'Query':          'query',
-    'Scan':           'scan',
-    'UpdateItem':     'update_item',
-    'UpdateTable':    'update_table',
-}
-
 # Pyramid entry point
 def main(global_config, **settings):
     """ This function returns a Pyramid WSGI application.
     config.scan()
     return config.make_wsgi_app()
 
+# Regular "over the network" connection wrapper.
 def connect_ddbmock(host='localhost', port=6543):
     import boto
     from boto.regioninfo import RegionInfo
 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:
-        from importlib import import_module
-        target = routes[action]
-        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:
-    #    err = InternalFailure("Method: {} not yet implemented".format(action))
-    except DDBError as e:
-        err = e
-
-    return err.status, json.dumps(err.to_dict())
-
-# Boto lib version entry point
-def _boto_make_request(self, action, body='', object_hook=None):
-    # from an external point of view, this function behaves exactly as the
-    # original version. It only avoids all the HTTP and network overhead.
-    # Even logs are preserved !
-    # route to take is in 'action'
-    # TODO:
-    # - handle auth
-    # - handle route errors (404)
-    # - handle all exceptions
-    # - request ID
-    # - simulate retry/throughput errors ?
-    # FIXME: dump followed by load... can be better...
-    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
-    request_id = 'STUB'
-    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)
-    # TODO: exception handling
-    return json.loads(ret, object_hook=object_hook)
+    from router.botopatch import boto_make_request
+    Layer1.make_request = boto_make_request
+    return boto.connect_dynamodb()

File ddbmock/errors.py

             "message": msg,
         }
 
-class BadRequest(DDBError): status=400
-class ServerError(DDBError): status=500
+class BadRequest(DDBError):  status=400; status_str='Bad Request'
+class ServerError(DDBError): status=500; status_str='InternalServerError'
 
 class AccessDeniedException(BadRequest): pass
 class ConditionalCheckFailedException(BadRequest): pass

File ddbmock/router/__init__.py

-# -*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
+
+# src: dest
+routes = {
+    'BatchGetItem':   'batch_get_item',
+    'BatchWriteItem': 'batch_write_item',
+    'CreateTable':    'create_table',
+    'DeleteItem':     'delete_item',
+    'DeleteTable':    'delete_table',
+    'DescribeTable':  'describe_table',
+    'GetItem':        'get_item',
+    'ListTables':     'list_tables',
+    'PutItem':        'put_item',
+    'Query':          'query',
+    'Scan':           'scan',
+    'UpdateItem':     'update_item',
+    'UpdateTable':    'update_table',
+}

File ddbmock/router/botopatch.py

+# -*- coding: utf-8 -*-
+
+# This module is not imported unless connect_boto is called thus making 'boto'
+# an optional dependancy
+
+import json, time
+import boto
+
+from importlib import import_module
+from boto.exception import BotoServerError
+from boto.dynamodb.exceptions import DynamoDBValidationError as DDBValidationErr
+from ddbmock.router import routes
+from ddbmock.errors import *
+
+# DDB to Boto exception
+def _do_exception(err):
+    if isinstance(err, ValidationException):
+        raise DDBValidationErr(err.status, err.status_str, err.to_dict())
+    else:
+        raise BotoServerError(err.status, err.status_str, err.to_dict())
+
+# Wrap the request logic
+def _do_request(action, post):
+    # handles the request and wrap exceptions
+    try:
+        target = routes[action]
+        mod = import_module('ddbmock.views.{}'.format(target))
+        func = getattr(mod, '_{}'.format(target))
+        return json.dumps(func(post))
+    except KeyError:
+        err = InternalFailure("Method: {} does not exist".format(action))
+    except ImportError:
+        err = InternalFailure("Method: {} not yet implemented".format(action))
+    except DDBError as e:
+        err = e
+
+    return _do_exception(err)
+
+# Boto lib version entry point
+def boto_make_request(self, action, body='', object_hook=None):
+    # from an external point of view, this function behaves exactly as the
+    # original version. It only avoids all the HTTP and network overhead.
+    # Even logs are preserved !
+    # route to take is in 'action'
+    # TODO:
+    # - handle auth
+    # - handle route errors (404)
+    # - handle all exceptions
+    # - request ID
+    # - simulate retry/throughput errors ?
+    # FIXME: dump followed by load... can be better...
+    target = '%s_%s.%s' % (self.ServiceName, self.Version, action)
+    start = time.time()
+
+    try:
+        ret = _do_request(action, json.loads(body))
+    except DDBError as e:
+        raise
+    finally:
+        request_id = 'STUB'
+        elapsed = (time.time() - start) * 1000
+        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)
+    return json.loads(ret, object_hook=object_hook)

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

+# -*- coding: utf-8 -*-
+
+import unittest
+import boto
+
+TABLE_NAME1 = 'Table-1'
+TABLE_NAME2 = 'Table-2'
+TABLE_NAME_INVALID1 = 'Table-invalid 1'
+
+TABLE_SCHEMA1 = {
+    'hash_key_name': 'hash_key',
+    'hash_key_proto_value': int,
+    'range_key_name': 'range_key',
+    'range_key_proto_value': unicode,
+}
+
+TABLE_SCHEMA2 = {
+    'hash_key_name': 'hash_key',
+    'hash_key_proto_value': int,
+}
+
+TABLE_SCHEMA_INVALID1 = {
+    'hash_key_name': 'hash_key',
+    'hash_key_proto_value': unicode,
+}
+
+# Not much can be tested here as most bugs are caught by Boto :)
+
+class TestListTables(unittest.TestCase):
+    def setUp(self):
+        from ddbmock.database.db import DynamoDB
+        DynamoDB().hard_reset()
+
+    def tearDown(self):
+        from ddbmock.database.db import DynamoDB
+        DynamoDB().hard_reset()
+
+    def test_create_table_hash_range(self):
+        from ddbmock import connect_boto
+        db = connect_boto()
+
+        table = db.create_table(
+            name=TABLE_NAME1,
+            schema=db.create_schema(**TABLE_SCHEMA1),
+            read_units=10,
+            write_units=10,
+        )
+
+        self.assertEqual(TABLE_NAME1, table.name)
+
+    def test_create_table_hash(self):
+        from ddbmock import connect_boto
+        db = connect_boto()
+
+        table = db.create_table(
+            name=TABLE_NAME2,
+            schema=db.create_schema(**TABLE_SCHEMA2),
+            read_units=10,
+            write_units=10,
+        )
+
+        self.assertEqual(TABLE_NAME2, table.name)
+
+    def test_create_table_invalid_name(self):
+        from ddbmock import connect_boto
+        from boto.dynamodb.exceptions import DynamoDBValidationError as DDBValidationErr
+
+        db = connect_boto()
+
+        self.assertRaises(DDBValidationErr, db.create_table,
+            name=TABLE_NAME_INVALID1,
+            schema=db.create_schema(**TABLE_SCHEMA_INVALID1),
+            read_units=10,
+            write_units=10,
+        )

File ddbmock/views/errors.py

 
 @view_config(context=DDBError, renderer='json')
 def failed_validation(exc, request):
-    request.response.status = exc.status
+    request.response_status = '{} {}'.format(exc.status, exc.status_str)
     return exc.to_dict()