Commits

Max Noel committed 3584ac3

* Added support (and tests) for json_list and json_dict datatypes.

Comments (0)

Files changed (2)

dynamodb_mapper/model.py

 import boto
 from boto.dynamodb.item import Item
 from boto.exception import DynamoDBResponseError
+import json
 
 
 log = logging.getLogger(__name__)
     pass
 
 
+class json_list(list):
+    """JSON-serialized list data type for use in your schemas.
+
+    From your object's point of view, it's a list with arbitrary nesting and
+    whatever you want to store in it.
+
+    In DynamoDB itself, though, it's stored as a JSON-serialized string.
+    """
+    def __init__(self, source_str=""):
+        if source_str:
+            super(json_list, self).__init__(json.loads(source_str))
+        else:
+            super(json_list, self).__init__()
+
+
+class json_dict(dict):
+    """JSON-serialized dict data type for use in your schemas.
+
+    From your object's point of view, it's a dict with arbitrary nesting and
+    whatever you want to store in it.
+
+    In DynamoDB itself, though, it's stored as a JSON-serialized string.
+    """
+    def __init__(self, source_str=""):
+        if source_str:
+            super(json_dict, self).__init__(json.loads(source_str))
+        else:
+            super(json_dict, self).__init__()
+
+
 class ConnectionBorg(object):
     """Borg that handles access to DynamoDB.
 
         """
         instance = cls()
         for (name, type_) in cls.__schema__.iteritems():
-            # Fill in missing attributes with default values.
-            setattr(instance, name, d.get(name, type_()))
+            try:
+                value = type_(d[name])
+            except KeyError:
+                # Fill in missing attributes with default values.
+                value = type_()
+            setattr(instance, name, value)
         return instance
 
     @classmethod
     def to_db_dict(self):
         """Return a dict representation of the object according to the class's
         schema, suitable for direct storage in DynamoDB.
-
-        This means the values must all be numbers, strings or sets thereof.
         """
-        return {name: getattr(self, name) for name in self.__schema__}
+        out = {}
+        for (name, type_) in self.__schema__.iteritems():
+            value = getattr(self, name)
+            # Yes, that part is horrible. DynamoDB can't store empty
+            # sets/strings, so we're representing them as missing
+            # attributes on the DB side.
+            if value or value == 0:
+                out[name] = value
+            elif type_ in [json_list, json_dict]:
+                # json serialization hooks for json_* data types.
+                out[name] = json.dumps(value)
+        return out
 
     def to_json_dict(self):
         """Return a dict representation of the object, suitable for JSON
         the operation fails with OverwriteError).
         """
         table = ConnectionBorg().get_table(self.__table__)
-        # Yes, that part is horrible. DynamoDB can't store empty sets/strings,
-        # so we're representing them as missing attributes on the DB side.
-        item_data = {
-            key: value for (key, value) in self.to_db_dict().iteritems() if
-                value or value == 0
-        }
+        item_data = self.to_db_dict()
         item = Item(table, attrs=item_data)
 
         if (self.__schema__[self.__hash_key__] == autoincrement_int and

dynamodb_mapper/tests/test_model.py

 
 
 import mock
-from dynamodb_mapper.model import ConnectionBorg, DynamoDBModel
+from dynamodb_mapper.model import ConnectionBorg, DynamoDBModel, json_list,\
+    json_dict
+import json
 
 
 class TestConnectionBorg(unittest.TestCase):
         self.assertEquals(d.id, 0)
         self.assertEquals(d.name, u"")
 
-        d_dict = d.to_db_dict()
+        # Using to_json_dict because to_db_dict omits empty values
+        d_dict = d.to_json_dict()
         self.assertEquals(d_dict["id"], d.id)
         self.assertEquals(d_dict["name"], d.name)
 
         self.assertEquals(d.name, u"")
         self.assertEquals(d.episodes, set())
 
+    def test_build_from_dict_json_list(self):
+        table_name = "doom_monster"
+        hash_key = "id"
+        schema = {
+            "id": int,
+            "attacks": json_list,
+        }
+
+        attacks = [
+            {'name': 'fireball', 'damage': 10},
+            {'name': 'punch', 'damage': 5}
+        ]
+
+        class DoomMonster(DynamoDBModel):
+            __table__ = table_name
+            __hash_key__ = hash_key
+            __schema__ = schema
+
+        m = DoomMonster.from_dict({
+            "id": 1,
+            "attacks": json.dumps(attacks)
+        })
+
+        self.assertEquals(m.attacks, attacks)
+
+        # Test default values
+        m2 = DoomMonster.from_dict({"id": 1})
+        self.assertEquals(m2.attacks, [])
+
+    def test_build_from_dict_json_dict(self):
+        table_name = "doom_episode"
+        hash_key = "id"
+        schema = {
+            "id": int,
+            "monsters": json_dict,
+        }
+
+        monsters = {
+            "cacodemon": [
+                {"x": 10, "y": 20},
+                {"x": 10, "y": 30}
+            ],
+            "imp": [],
+            "cyberdemon": []
+        }
+
+        class DoomEpisode(DynamoDBModel):
+            __table__ = table_name
+            __hash_key__ = hash_key
+            __schema__ = schema
+
+        e = DoomEpisode.from_dict({
+            "id": 1,
+            "monsters": json.dumps(monsters)
+        })
+        self.assertEquals(e.monsters, monsters)
+
+        e2 = DoomEpisode.from_dict({"id": 1})
+        self.assertEquals(e2.monsters, {})
+
     @mock.patch("dynamodb_mapper.model.Item")
     @mock.patch("dynamodb_mapper.model.boto")
     def test_save_simple_types(self, m_boto, m_item):
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.