Commits

Max Noel  committed cd3ae9c

* Added support for datetime.datetime attributes -- serialized as iso-formatted, UTC strings. You can use them as hash or range keys too. Note that they *must* be timezone-aware -- attempting to save a naive (timezone-less) object will raise ValueError.

  • Participants
  • Parent commits 0794285

Comments (0)

Files changed (2)

File dynamodb_mapper/model.py

-#!/usr/bin/env python
 """Object mapper for Amazon DynamoDB.
 
 Based in part on mongokit's Document interface.
 
 Released under the GNU LGPL, version 3 or later (see COPYING).
 """
+from __future__ import absolute_import
 
+import datetime
 import logging
 import threading
 
 _JSON_TYPES = frozenset([list, dict])
 
 
+class UTC(datetime.tzinfo):
+    """UTC timezone"""
+    def utcoffset(self, dt):
+        return datetime.timedelta(0)
+
+    def tzname(self, dt):
+        return "UTC"
+
+    def dst(self, dt):
+        return datetime.timedelta(0)
+
+
+utc_tz = UTC()
+
+
+def _get_proto_value(schema_type):
+    """Return a prototype value matching what schema_type will be serialized
+    as in DynamoDB:
+
+      - For strings and numbers, an instance of schema_type.
+      - For "special" types implemented at the mapper level (list, dict,
+        datetime), an empty string (this is what they're stored as in the DB).
+    """
+    # Those types must be serialized as strings
+    if schema_type in _JSON_TYPES:
+        return u""
+
+    if schema_type == datetime.datetime:
+        return u""
+
+    # Regular string/number
+    return schema_type()
+
+
 def _python_to_dynamodb(schema_type, value):
     """Convert a Python object to a representation suitable to direct storage
     in DynamoDB, according to a type from a DynamoDBModel schema.
         # json serialization hooks for json_* data types.
         return json.dumps(value, sort_keys=True)
 
+    if schema_type == datetime.datetime:
+        # datetime instances are stored as UTC in the DB itself.
+        # (that way, they become sortable)
+        # datetime objects without tzinfo are not supported.
+        return value.astimezone(utc_tz).strftime("%Y-%m-%dT%H:%M:%S.%f%z")
+
     if value or value == 0:
         return value
 
     if schema_type in _JSON_TYPES:
         return schema_type(json.loads(value))
 
+    if schema_type == datetime.datetime:
+        # Parse TZ-aware isoformat
+
+        # strptime doesn't support timezone parsing (%z flag), so we're forcing
+        # the strings in the database to be UTC (+0000) for now.
+        # TODO Handle arbitrary timezones (with manual parsing).
+        return datetime.datetime.strptime(
+            value, "%Y-%m-%dT%H:%M:%S.%f+0000").replace(tzinfo=utc_tz)
+
     return schema_type(value)
 
 
 
         conn = self._get_connection()
         # It's a prototype/an instance, not a type.
-        hash_key_proto_value = hash_key_type()
+        hash_key_proto_value = _get_proto_value(hash_key_type)
         # None in the case of a hash-only table.
         if range_key_name:
             # We have a range key, its type must be specified.
-            range_key_proto_value = cls.__schema__[range_key_name]()
+            range_key_proto_value = _get_proto_value(
+                cls.__schema__[range_key_name])
         else:
             range_key_proto_value = None
 

File dynamodb_mapper/tests/test_model.py

 
 from dynamodb_mapper.model import (ConnectionBorg, DynamoDBModel,
     autoincrement_int, MaxRetriesExceededError, MAX_RETRIES,
-    ExpectedValueError, _python_to_dynamodb, _dynamodb_to_python)
+    ExpectedValueError, _python_to_dynamodb, _dynamodb_to_python, utc_tz)
 from boto.exception import DynamoDBResponseError
+import datetime
 
 
 # Hash-only primary key
             _dynamodb_to_python(dict, json.dumps(monsters, sort_keys=True)))
         self.assertEquals({}, _dynamodb_to_python(dict, "{}"))
 
+    def test_dynamodb_to_python_datetime(self):
+        self.assertEquals(
+            datetime.datetime(2012, 05, 31, 12, 0, 0, tzinfo=utc_tz),
+            _dynamodb_to_python(datetime.datetime, "2012-05-31T12:00:00.000000+0000"))
+
+    def test_dynamodb_to_python_datetime_notz(self):
+        # Timezone info is mandatory
+        self.assertRaises(
+            ValueError,
+            _dynamodb_to_python, datetime.datetime, "2012-05-31T12:00:00.000000")
+
+    def test_python_to_dynamodb_datetime(self):
+        self.assertEquals(
+            "2012-05-31T12:00:00.000000+0000",
+            _python_to_dynamodb(datetime.datetime, datetime.datetime(2012, 05, 31, 12, 0, 0, tzinfo=utc_tz)))
+
+    def test_python_to_dynamodb_datetime_notz(self):
+        self.assertRaises(
+            ValueError,
+            _python_to_dynamodb, datetime.datetime, datetime.datetime(2012, 05, 31, 12, 0, 0))
+
 
 class TestDynamoDBModel(unittest.TestCase):
     def test_build_default_values(self):