Commits

Jean-Tiare Le Bigot committed bf8dc1d

add get_item method, test, bugfixes

  • Participants
  • Parent commits 3b1a85e

Comments (0)

Files changed (8)

ddbmock/database/table.py

 
 # All validations are performed on *incomming* data => already done :)
 
+def _filter(item, fields):
+    """
+    Return a dict containing only the keys specified in ``fields``. If
+    ``fields`` evaluates to False (None, empty, ...), the original dict is
+    returned untouched.
+
+    :ivar item: dict to filter
+    :ivar fields: array of name of keys to keep
+    :return: filtered ``item``
+    """
+    if fields:
+        return dict((k, v) for k, v in item.items() if k in fields)
+    return item
+
 class Table(object):
     def __init__(self, name, rt, wt, hash_key, range_key):
         self.name = name
         self.rt = rt
         self.wt = wt
 
-    def _read_primary_key(self, key, item):
+    def _read_primary_key(self, key, item, name=False):
         if key is None:
             return False
-        typename, value = self._key_from_dict((item[key.name]))
+        if name == False:
+            name = key.name
+        typename, value = self._key_from_dict((item[name]))
         if key.typename != typename:
             raise TypeError('Primary key {} is not of type {}. Got {}'.format(key.name, key.typename, typename))
         return value
 
         return old
 
-    def get(self, key):
-        hash = self._key_from_dict(key[u'HashKeyElement'])
-        if u'RangeKeyElement' in range:
-            range = self._key_from_dict(key[u'RangeKeyElement'])
-        else:
-            range = False
+    def get(self, key, fields):
+        try:
+            hash = self._read_primary_key(self.hash_key, key, u'HashKeyElement')
+            range = self._read_primary_key(self.range_key, key, u'RangeKeyElement')
+        except KeyError:
+            raise ValidationException("Either hash, range or both key is missing")
 
-        return self._get(hash, range)
+        item = self._get(hash, range)
+
+        return _filter(item, fields)
 
     @classmethod
     def from_dict(cls, data):

ddbmock/errors.py

 # -*- coding: utf-8 -*-
 
-from voluptuous import Invalid
-
 class DDBError(Exception):
     def to_dict(self):
         name = type(self).__name__
     def wrapped(*args):
         try:
             return func(*args)
-        except (TypeError, ValueError, Invalid) as e:
+        except (TypeError, ValueError) as e:
+            raise
             raise ValidationException(*e.args)
     return wrapped

ddbmock/tests/functional/boto/test_get_item.py

+# -*- coding: utf-8 -*-
+
+import unittest
+import boto
+
+TABLE_NAME = 'Table-HR'
+TABLE_NAME2 = 'Table-H'
+TABLE_NAME_404 = 'Waldo'
+TABLE_RT = 45
+TABLE_WT = 123
+TABLE_RT2 = 10
+TABLE_WT2 = 10
+TABLE_HK_NAME = u'hash_key'
+TABLE_HK_TYPE = u'N'
+TABLE_RK_NAME = u'range_key'
+TABLE_RK_TYPE = u'S'
+
+HK_VALUE = u'123'
+RK_VALUE = u'Decode this data if you are a coder'
+
+
+ITEM = {
+    TABLE_HK_NAME: {TABLE_HK_TYPE: HK_VALUE},
+    TABLE_RK_NAME: {TABLE_RK_TYPE: RK_VALUE},
+    u'relevant_data': {u'B': u'THVkaWEgaXMgdGhlIGJlc3QgY29tcGFueSBldmVyIQ=='},
+}
+ITEM2 = {
+    TABLE_HK_NAME: {TABLE_HK_TYPE: HK_VALUE},
+    u'relevant_data': {u'B': u'THVkaWEgaXMgdGhlIGJlc3QgY29tcGFueSBldmVyIQ=='},
+}
+
+class TestGetItem(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(TABLE_HK_NAME, TABLE_HK_TYPE)
+        range_key = PrimaryKey(TABLE_RK_NAME, TABLE_RK_TYPE)
+
+        self.t1 = Table(TABLE_NAME, TABLE_RT, TABLE_WT, hash_key, range_key)
+        self.t2 = Table(TABLE_NAME2, TABLE_RT, TABLE_WT, hash_key, None)
+
+        db.data[TABLE_NAME]  = self.t1
+        db.data[TABLE_NAME2] = self.t2
+
+        self.t1.put(ITEM)
+        self.t2.put(ITEM2)
+
+    def tearDown(self):
+        from ddbmock.database.db import DynamoDB
+        DynamoDB().hard_reset()
+
+    def test_get_hr(self):
+        from ddbmock import connect_boto
+        from ddbmock.database.db import DynamoDB
+
+        db = connect_boto()
+
+        expected = {
+            u'ConsumedCapacityUnits': 0.5,
+            u'Item': {
+                u'hash_key': {u'N': u'123'},
+                u'range_key': {u'S': u'Decode this data if you are a coder'},
+                u'relevant_data': {u'B': u'THVkaWEgaXMgdGhlIGJlc3QgY29tcGFueSBldmVyIQ=='}}
+        }
+
+        key = {
+            u"HashKeyElement":  {TABLE_HK_TYPE: HK_VALUE},
+            u"RangeKeyElement": {TABLE_RK_TYPE: RK_VALUE},
+        }
+
+        self.assertEquals(expected, db.layer1.get_item(TABLE_NAME, key))
+
+    def test_get_hr_consistent(self):
+        from ddbmock import connect_boto
+        from ddbmock.database.db import DynamoDB
+
+        db = connect_boto()
+
+        expected = {
+            u'ConsumedCapacityUnits': 1,
+            u'Item': {
+                u'hash_key': {u'N': u'123'},
+                u'range_key': {u'S': u'Decode this data if you are a coder'},
+                u'relevant_data': {u'B': u'THVkaWEgaXMgdGhlIGJlc3QgY29tcGFueSBldmVyIQ=='}}
+        }
+
+        key = {
+            u"HashKeyElement":  {TABLE_HK_TYPE: HK_VALUE},
+            u"RangeKeyElement": {TABLE_RK_TYPE: RK_VALUE},
+        }
+
+        self.assertEquals(expected, db.layer1.get_item(TABLE_NAME, key,consistent_read=True))
+
+    def test_get_h(self):
+        from ddbmock import connect_boto
+        from ddbmock.database.db import DynamoDB
+
+        db = connect_boto()
+
+        expected = {
+            u'ConsumedCapacityUnits': 0.5,
+            u'Item': {
+                u'hash_key': {u'N': u'123'},
+                u'relevant_data': {u'B': u'THVkaWEgaXMgdGhlIGJlc3QgY29tcGFueSBldmVyIQ=='}}
+        }
+
+        key = {
+            u"HashKeyElement":  {TABLE_HK_TYPE: HK_VALUE},
+        }
+
+        self.assertEquals(expected, db.layer1.get_item(TABLE_NAME2, key))
+
+    def test_get_hr_attr_to_get(self):
+        from ddbmock import connect_boto
+        from ddbmock.database.db import DynamoDB
+
+        db = connect_boto()
+
+        expected = {
+            u'ConsumedCapacityUnits': 0.5,
+            u'Item': {
+                u'relevant_data': {u'B': u'THVkaWEgaXMgdGhlIGJlc3QgY29tcGFueSBldmVyIQ=='}}
+        }
+
+        key = {
+            u"HashKeyElement":  {TABLE_HK_TYPE: HK_VALUE},
+            u"RangeKeyElement": {TABLE_RK_TYPE: RK_VALUE},
+        }
+
+        self.assertEquals(expected, db.layer1.get_item(TABLE_NAME, key, attributes_to_get=[u'relevant_data']))

ddbmock/validators/__init__.py

 # -*- coding: utf-8 -*-
 
-from voluptuous import Schema
+from voluptuous import Schema, Invalid
 from importlib import import_module
+from ddbmock.errors import ValidationException
 
 # Cool function that automatically find and applies a validator for the input
 # Crash if no validator were found (early breakage detection)
         name = api.__name__
         mod = import_module('.{}'.format(name), __name__)
         validate = Schema(getattr(mod, 'post'), required=True)
-        validate(post)
+        try:
+            validate(post)
+        except Invalid as e:
+            raise ValidationException(*e.args)
         return api(post)
     return wrapped

ddbmock/validators/create_table.py

 # -*- coding: utf-8 -*-
 
-from .types import table_name, key_schema, provisioned_throughtput
+from .types import table_name, table_key_schema, provisioned_throughtput
 
 post = {
     u'TableName': table_name,
-    u'KeySchema': key_schema,
+    u'KeySchema': table_key_schema,
     u'ProvisionedThroughput': provisioned_throughtput,
 }

ddbmock/validators/get_item.py

+# -*- coding: utf-8 -*-
+
+from .types import table_name, optional, item_schema, get_key_schema, consistent_read, attributes_to_get_schema
+
+post = {
+    u'TableName': table_name,
+    u'Key': get_key_schema,
+    optional(u'AttributesToGet'): attributes_to_get_schema, #FIXME: handle default
+    optional(u'ConsistentRead'): consistent_read,  #FIXME: handle default
+}

ddbmock/validators/types.py

 )
 
 consistent_read = all(
+    default_to(False),
     boolean(msg="Consistent_read parameter must be a boolean"),
-    default_to(False),
 )
 
 # DynamoDB data types
     u'AttributeType': primary_key_type,
 }
 
-key_schema = {
+table_key_schema = {
     u'HashKeyElement': primary_key,
     optional(u'RangeKeyElement'): primary_key,
 }
 
 # Fixme: max 1 item
-field_value = {
+key_field_value = {
     optional(u'N'): field_number_value,
     optional(u'S'): field_string_value,
     optional(u'B'): field_binary_value,
+}
+
+field_value = key_field_value.copy()
+field_value.update({
     optional(u'NS'): field_number_set_value,
     optional(u'SS'): field_string_set_value,
     optional(u'BS'): field_binary_set_value,
-}
+})
 
 item_schema = {
     required(field_name): field_value,
 }
 
-attributes_to_get_schema = [unicode]
+get_key_schema = {
+    u'HashKeyElement': key_field_value,
+    optional(u'RangeKeyElement'): key_field_value,
+}
+
+attributes_to_get_schema = all(
+    default_to([]),
+    [unicode],
+)
 
 # TODO
 expected_schema = {extra: object}

ddbmock/views/get_item.py

+# -*- coding: utf-8 -*-
+
+from pyramid.view import view_config
+from ddbmock.database import DynamoDB
+from ddbmock.validators import dynamodb_api_validate
+from ddbmock.errors import wrap_exceptions
+
+# Real work
+@wrap_exceptions
+@dynamodb_api_validate
+def get_item(post):
+    #FIXME: this line is a temp workaround
+    if u'AttributesToGet' not in post:
+        post[u'AttributesToGet'] = []
+    if u'ConsistentRead' not in post:
+        post[u'ConsistentRead'] = False
+
+    # hackish consistency
+    capacity = 1 if post[u'ConsistentRead'] else 0.5
+
+    #TODO: ConsistentRead
+    name = post[u'TableName']
+    table = DynamoDB().get_table(name)
+    if table is None:
+        raise ResourceNotFoundException("Table {} does not exist".format(name))
+
+    return {
+        "ConsumedCapacityUnits": capacity, #FIXME: stub
+        "Item": table.get(post[u'Key'], post[u'AttributesToGet']),
+    }
+
+# Pyramid route wrapper
+@view_config(route_name='pyramid_get_item', renderer='json')
+def pyramid_get_item(request):
+    return pyramid_get_item(request.json)