From 19695858c7ad5506f1b1dfd9421e36f143efddc3 Mon Sep 17 00:00:00 2001 From: John Wehr Date: Tue, 21 Dec 2010 07:00:03 -0500 Subject: [PATCH] Initial commit. --- .gitignore | 2 + AUTHORS | 1 + LICENSE | 20 +++ README.mkd | 0 cassorm/__init__.py | 5 + cassorm/dict.py | 123 +++++++++++++++++ cassorm/list.py | 146 ++++++++++++++++++++ cassorm/model.py | 115 ++++++++++++++++ cassorm/tests/__init__.py | 3 + cassorm/tests/dict.py | 129 +++++++++++++++++ cassorm/tests/list.py | 61 ++++++++ cassorm/tests/model.py | 112 +++++++++++++++ cassorm/tests/randomseq.py | 28 ++++ cassorm/types.py | 39 ++++++ ez_setup.py | 275 +++++++++++++++++++++++++++++++++++++ setup.py | 36 +++++ 16 files changed, 1095 insertions(+) create mode 100644 .gitignore create mode 100644 AUTHORS create mode 100644 LICENSE create mode 100644 README.mkd create mode 100644 cassorm/__init__.py create mode 100644 cassorm/dict.py create mode 100644 cassorm/list.py create mode 100644 cassorm/model.py create mode 100644 cassorm/tests/__init__.py create mode 100644 cassorm/tests/dict.py create mode 100644 cassorm/tests/list.py create mode 100644 cassorm/tests/model.py create mode 100644 cassorm/tests/randomseq.py create mode 100644 cassorm/types.py create mode 100644 ez_setup.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9460dae --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +*.DS_Store* \ No newline at end of file diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..c77f679 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +John Wehr diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c21d443 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Everything else: +http://www.opensource.org/licenses/mit-license.php + +Copyright (c) 2011 John Wehr + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.mkd b/README.mkd new file mode 100644 index 0000000..e69de29 diff --git a/cassorm/__init__.py b/cassorm/__init__.py new file mode 100644 index 0000000..8abbfaa --- /dev/null +++ b/cassorm/__init__.py @@ -0,0 +1,5 @@ +from .list import CassandraList +from .model import CassandraModel +from .dict import CassandraDict +from .types import DateTime, DateTimeString, Float64, FloatString, Int64, IntString, String +from .tests import CassandraListTest, CassandraModelTest, CassandraDictTest diff --git a/cassorm/dict.py b/cassorm/dict.py new file mode 100644 index 0000000..469a7a3 --- /dev/null +++ b/cassorm/dict.py @@ -0,0 +1,123 @@ +from django.conf import settings +import pycassa +from pycassa.system_manager import SystemManager + + +MANAGER_MAPS = {} + + +class CassandraKeyError(KeyError): + pass + + +class CassandraDictManager(pycassa.ColumnFamily): + """Manager for CassandraDict, provides schema creation.""" + + def __init__(self, cls): + self.cls = cls + super(CassandraDictManager, self).__init__( + settings.CASSANDRA_POOL, + self.cls.__name__.lower()) + + +class CassandraMetaDict(type): + """Metaclass for CassandraDict, provides access to CassandraDictManager""" + + @property + def objects(cls): + """CassandraListManager Singletons""" + if cls.__name__ not in MANAGER_MAPS: + MANAGER_MAPS[id(cls)] = CassandraDictManager(cls) + return MANAGER_MAPS[id(cls)] + + def sync(cls, default_validation_class=None, destructive=False): + """Create the Cassandra List schema.""" + if default_validation_class is None: + default_validation_class = cls.default_validation_class + sys = SystemManager(settings.CASSANDRA_SERVERS[0]) + if destructive: + try: + sys.drop_column_family(settings.CASSANDRA_KEYSPACE, cls.__name__.lower()) + except: + pass + sys.create_column_family( + settings.CASSANDRA_KEYSPACE, + cls.__name__.lower(), + comparator_type=pycassa.UTF8_TYPE, + default_validation_class=default_validation_class) + sys.close() + + +class CassandraDict(object): + "A dictionary that stores its data persistantly in Cassandra" + + default_validation_class = pycassa.UTF8_TYPE + + __metaclass__ = CassandraMetaDict + + def __init__(self, row_key): + self.row_key = row_key + self.cf = self.__class__.objects + + def get(self, key, default=None): + try: + return self.cf.get(self.row_key, columns=[key]).values()[0] + except pycassa.NotFoundException, e: + if default is None: + raise CassandraKeyError(key) + else: + return default + + def __getitem__(self, key): + try: + return self.cf.get(self.row_key, columns=[key]).values()[0] + except pycassa.NotFoundException, e: + raise CassandraKeyError(key) + + def __setitem__(self, key, value): + return self.cf.insert(self.row_key, {key:value}) + + def __delitem__(self, key): + return self.cf.remove(self.row_key, columns=[key]) + + def update(self, d): + return self.cf.insert(self.row_key, d) + + def keys(self): + try: + return self.cf.get(self.row_key).keys() + except pycassa.NotFoundException, e: + return [] + + def values(self): + try: + return self.cf.get(self.row_key).values() + except pycassa.NotFoundException, e: + return [] + + def items(self): + try: + return self.cf.get(self.row_key) + except pycassa.NotFoundException, e: + return [] + + def iterkeys(self): + return self.keys() + + def itervalues(self): + return self.values() + + def iteritems(self): + return self.items() + + def has_key(self, key): + return key in dict(self.items()) + + def __contains__(self, key): + return self.has_key(key) + + def __len__(self): + return self.cf.get_count(self.row_key) + + def __del__(self): + self.cf.remove(self.row_key) \ No newline at end of file diff --git a/cassorm/list.py b/cassorm/list.py new file mode 100644 index 0000000..05a7668 --- /dev/null +++ b/cassorm/list.py @@ -0,0 +1,146 @@ +import time +import uuid +from django.conf import settings +import pycassa +from pycassa.util import convert_time_to_uuid, convert_uuid_to_time +from pycassa.system_manager import SystemManager +from .exceptions import CassandraIndexError + +MANAGER_MAPS = {} + +class CassandraIndexError(IndexError): + pass + +class CassandraListManager(pycassa.ColumnFamily): + """Manager for CassandraList.""" + + def __init__(self, cls): + self.cls = cls + super(CassandraListManager, self).__init__( + settings.CASSANDRA_POOL, + self.cls.__name__.lower()) + + +class CassandraMetaList(type): + """Metaclass for CassandraList, provides schema creation, + access to CassandraListManager""" + + @property + def objects(cls): + """CassandraListManager Singletons""" + if cls.__name__ not in MANAGER_MAPS: + MANAGER_MAPS[id(cls)] = CassandraListManager(cls) + return MANAGER_MAPS[id(cls)] + + def sync(cls, default_validation_class=None, destructive=False): + """Create the Cassandra List schema.""" + if default_validation_class is None: + default_validation_class = cls.default_validation_class + sys = SystemManager(settings.CASSANDRA_SERVERS[0]) + if destructive: + try: + sys.drop_column_family(settings.CASSANDRA_KEYSPACE, cls.__name__.lower()) + except: + pass + sys.create_column_family( + settings.CASSANDRA_KEYSPACE, + cls.__name__.lower(), + comparator_type=pycassa.TIME_UUID_TYPE, + default_validation_class=default_validation_class) + sys.close() + + +class CassandraList(object): + + default_validation_class = pycassa.UTF8_TYPE + + __metaclass__ = CassandraMetaList + + def __init__(self, row_key): + self.row_key = row_key + self.cf = self.__class__.objects + + def append(self, x): + self.cf.insert(self.row_key, {time.time():x}) + + def extend(self, seq): + rows = {} + start = uuid.UUID("{%s}" % convert_time_to_uuid(time.time())) + i = 0 + for x in seq: + rows[uuid.UUID(int=start.int + i)] = unicode(x) + i += 10 + self.cf.insert(self.row_key, rows) + + def insert(self, i, x): + try: + seq = self.cf.get(self.row_key, column_count=i + 1) + except pycassa.NotFoundException, e: + self.append(x) + return + if i >= len(seq): + self.append(x) + return + old_key = seq.keys().pop() + old_key_time = convert_uuid_to_time(old_key) + low, high = convert_time_to_uuid(old_key_time, randomize=True), \ + convert_time_to_uuid(old_key_time, randomize=True) + if low > high: + high, low = low, high + while high > old_key: + low, high = convert_time_to_uuid(old_key_time, randomize=True), \ + convert_time_to_uuid(old_key_time, randomize=True) + if low > high: + high, low = low, high + old_value = self.cf.get(self.row_key, columns=[old_key]).values().pop() + self.cf.insert(self.row_key, {high:old_value}) + self.cf.insert(self.row_key, {low:x}) + self.cf.remove(self.row_key, columns=[old_key]) + + def pop(self): + try: + item = self.cf.get(self.row_key, column_count=1, column_reversed=True) + except pycassa.NotFoundException, e: + raise CassandraIndexError("pop from empty list") + self.cf.remove(self.row_key, columns=item.keys()) + return item.values()[0] + + def remove(self, x): + column_start = "" + while 1: + try: + columns = self.cf.get(self.row_key, column_start=column_start, column_count=100) + except pycassa.NotFoundException, e: + return + for key in columns: + if columns[key] == x: + self.cf.remove(self.row_key, columns=[key]) + key = uuid.UUID("{%s}" % key) + column_start = uuid.UUID(int=key.int + 1) + + def delete(self): + self.cf.remove(self.row_key) + + def __len__(self): + return self.cf.get_count(self.row_key) + + def __str__(self): + try: + return str([x[1] for x in self.cf.get(self.row_key).items()]) + except pycassa.NotFoundException, e: + return str([]) + + def __getitem__(self, val): + if isinstance(val, slice): + if (val.step is None or val.step > 0) and val.stop is not None: + seq = [x[1] for x in self.cf.get(self.row_key, column_count=val.stop).items()] + return seq[val.start:val.stop:val.step] + seq = [x[1] for x in self.cf.get(self.row_key).items()] + return seq[val.start:val.stop:val.step] + elif isinstance(val, int): + try: + return self[val:val + 1].pop() + except Exception, e: + raise CassandraIndexError("List index out of range.") + else: + return super(CassandraList, self).__getitem__(val) diff --git a/cassorm/model.py b/cassorm/model.py new file mode 100644 index 0000000..074066e --- /dev/null +++ b/cassorm/model.py @@ -0,0 +1,115 @@ +import time +from django.conf import settings +import pycassa +from pycassa.util import convert_time_to_uuid +from pycassa.system_manager import SystemManager +from .exceptions import CassandraIndexError +from .types import DateTime, DateTimeString, Float64, FloatString, Int64, IntString, String +from pycassa import BYTES_TYPE, LONG_TYPE, INT_TYPE, ASCII_TYPE, UTF8_TYPE, TIME_UUID_TYPE, LEXICAL_UUID_TYPE + + +MANAGER_MAPS = {} + + +class CassandraMetaModel(type): + + @property + def objects(cls): + if cls.__name__ not in MANAGER_MAPS: + MANAGER_MAPS[id(cls)] = CassandraModelManager(cls) + return MANAGER_MAPS[id(cls)] + + def sync(cls, default_validation_class=None, destructive=False): + if default_validation_class is None: + default_validation_class = cls.default_validation_class + columns = [] + indexes = [] + column_family = cls.__name__.lower() + for i in cls.__dict__: + attr = cls.__dict__[i] + if isinstance(attr, DateTime) or \ + isinstance(attr, DateTimeString) or isinstance(attr, Float64) or \ + isinstance(attr, FloatString) or isinstance(attr, Int64) or \ + isinstance(attr, IntString) or isinstance(attr, String): + columns.append(i) + if attr.index: + indexes.append(i) + sys = SystemManager(settings.CASSANDRA_SERVERS[0]) + if destructive: + try: + sys.drop_column_family(settings.CASSANDRA_KEYSPACE, cls.__name__.lower()) + except: + pass + sys.create_column_family( + settings.CASSANDRA_KEYSPACE, + cls.__name__.lower(), + comparator_type=UTF8_TYPE, + default_validation_class=default_validation_class) + for column in columns: + attr = cls.__dict__[column] + if isinstance(attr, DateTime): + type = ASCII_TYPE + elif isinstance(attr, DateTimeString): + type = ASCII_TYPE + elif isinstance(attr, Float64): + type = BYTES_TYPE + elif isinstance(attr, FloatString): + type = ASCII_TYPE + elif isinstance(attr, Int64): + type = BYTES_TYPE + elif isinstance(attr, IntString): + type = ASCII_TYPE + elif isinstance(attr, String): + type = UTF8_TYPE + if column in indexes: + sys.create_index( + settings.CASSANDRA_KEYSPACE, + column_family, + column.lower(), + type, + index_name="%s_index" % column) + else: + sys.alter_column( + settings.CASSANDRA_KEYSPACE, + column_family, + column.lower(), + type) + sys.close() + + +class CassandraModel(object): + + default_validation_class = UTF8_TYPE + + __metaclass__ = CassandraMetaModel + + def __init__(self, **kwargs): + for key in kwargs: + setattr(self, key, kwargs[key]) + + def save(self): + if not hasattr(self, "key"): + self.key = str(convert_time_to_uuid(time.time())) + self.__class__.objects.insert(self) + + +class CassandraModelManager(pycassa.ColumnFamilyMap): + + def __init__(self, cls, create=False): + self.cls = cls + try: + super(CassandraModelManager, self).__init__( + self.cls, + pycassa.ColumnFamily( + settings.CASSANDRA_POOL, + self.cls.__name__.lower())) + except: + pass + + def __getitem__(self, key): + return self.get(key) + + def __setitem__(self, key, value): + value.key = key + self.insert(value) + diff --git a/cassorm/tests/__init__.py b/cassorm/tests/__init__.py new file mode 100644 index 0000000..c1a637f --- /dev/null +++ b/cassorm/tests/__init__.py @@ -0,0 +1,3 @@ +from .list import CassandraListTest +from .model import CassandraModelTest +from .dict import CassandraDictTest \ No newline at end of file diff --git a/cassorm/tests/dict.py b/cassorm/tests/dict.py new file mode 100644 index 0000000..b564ca3 --- /dev/null +++ b/cassorm/tests/dict.py @@ -0,0 +1,129 @@ +from django.utils import unittest +from ..dict import CassandraDict +from .randomseq import random_utf8_seq +from ..dict import CassandraKeyError + + +class TestDict(CassandraDict): + pass + + +class CassandraDictTest(unittest.TestCase): + + def setUp(self): + TestDict.sync(destructive=True) + self.d = TestDict("Test") + + def test_get(self): + key_1 = "".join(random_utf8_seq() for i in range(10)) + key_2 = "".join(random_utf8_seq() for i in range(10)) + value_1 = "".join(random_utf8_seq() for i in range(10)) + value_2 = "".join(random_utf8_seq() for i in range(10)) + self.d[key_1] = value_1 + self.assertEqual(self.d.get(key_1, value_2), value_1) + self.assertEqual(self.d.get(key_2, value_2), value_2) + self.assertNotEqual(self.d.get(key_2, value_2), value_1) + + def test__getitem__(self): + key_1 = "".join(random_utf8_seq() for i in range(10)) + key_2 = "".join(random_utf8_seq() for i in range(10)) + value_1 = "".join(random_utf8_seq() for i in range(10)) + value_2 = "".join(random_utf8_seq() for i in range(10)) + self.d[key_1] = value_1 + self.d[key_2] = value_2 + self.assertEqual(self.d[key_1], value_1) + self.assertEqual(self.d[key_2], value_2) + self.assertNotEqual(self.d[key_1], value_2) + self.assertNotEqual(self.d[key_2], value_1) + + def test__delitem__(self): + key_1 = "".join(random_utf8_seq() for i in range(10)) + key_2 = "".join(random_utf8_seq() for i in range(10)) + value_1 = "".join(random_utf8_seq() for i in range(10)) + value_2 = "".join(random_utf8_seq() for i in range(10)) + self.d[key_1] = value_1 + self.d[key_2] = value_2 + self.assertEqual(self.d[key_1], value_1) + del self.d[key_1] + with self.assertRaises(CassandraKeyError): + x = self.d[key_1] + self.assertEqual(self.d[key_2], value_2) + del self.d[key_2] + with self.assertRaises(CassandraKeyError): + x = self.d[key_2] + + def testupdate(self): + key_1 = "".join(random_utf8_seq() for i in range(10)) + key_2 = "".join(random_utf8_seq() for i in range(10)) + value_1 = "".join(random_utf8_seq() for i in range(10)) + value_2 = "".join(random_utf8_seq() for i in range(10)) + x = {key_1:value_1, key_2:value_2} + self.d.update(x) + self.assertEqual(self.d[key_1], value_1) + self.assertEqual(self.d[key_2], value_2) + + def test_keys(self): + key_1 = "".join(random_utf8_seq() for i in range(10)) + key_2 = "".join(random_utf8_seq() for i in range(10)) + value_1 = "".join(random_utf8_seq() for i in range(10)) + value_2 = "".join(random_utf8_seq() for i in range(10)) + self.d[key_1] = value_1 + self.d[key_2] = value_2 + self.assertTrue(key_1 in self.d.keys()) + self.assertTrue(key_2 in self.d.keys()) + + def test_values(self): + key_1 = "".join(random_utf8_seq() for i in range(10)) + key_2 = "".join(random_utf8_seq() for i in range(10)) + value_1 = "".join(random_utf8_seq() for i in range(10)) + value_2 = "".join(random_utf8_seq() for i in range(10)) + self.d[key_1] = value_1 + self.d[key_2] = value_2 + self.assertTrue(value_1 in self.d.values()) + self.assertTrue(value_2 in self.d.values()) + + def test_items(self): + key_1 = "".join(random_utf8_seq() for i in range(10)) + key_2 = "".join(random_utf8_seq() for i in range(10)) + value_1 = "".join(random_utf8_seq() for i in range(10)) + value_2 = "".join(random_utf8_seq() for i in range(10)) + self.d[key_1] = value_1 + self.d[key_2] = value_2 + x = dict(self.d.items()) + self.assertEqual(x[key_1], value_1) + self.assertEqual(x[key_2], value_2) + + def test_has_key(self): + key_1 = "".join(random_utf8_seq() for i in range(10)) + key_2 = "".join(random_utf8_seq() for i in range(10)) + value_1 = "".join(random_utf8_seq() for i in range(10)) + value_2 = "".join(random_utf8_seq() for i in range(10)) + self.d[key_1] = value_1 + self.d[key_2] = value_2 + self.assertTrue(self.d.has_key(key_1)) + self.assertTrue(self.d.has_key(key_2)) + + def test__contains__(self): + key_1 = "".join(random_utf8_seq() for i in range(10)) + key_2 = "".join(random_utf8_seq() for i in range(10)) + value_1 = "".join(random_utf8_seq() for i in range(10)) + value_2 = "".join(random_utf8_seq() for i in range(10)) + self.d[key_1] = value_1 + self.d[key_2] = value_2 + self.assertTrue(key_1 in self.d) + self.assertTrue(key_2 in self.d) + + def test__len__(self): + key_1 = "".join(random_utf8_seq() for i in range(10)) + key_2 = "".join(random_utf8_seq() for i in range(10)) + value_1 = "".join(random_utf8_seq() for i in range(10)) + value_2 = "".join(random_utf8_seq() for i in range(10)) + self.d[key_1] = value_1 + self.d[key_2] = value_2 + self.assertEqual(len(self.d), 2) + del self.d[key_1] + self.assertEqual(len(self.d), 1) + del self.d[key_2] + self.assertEqual(len(self.d), 0) + + \ No newline at end of file diff --git a/cassorm/tests/list.py b/cassorm/tests/list.py new file mode 100644 index 0000000..29d0bc8 --- /dev/null +++ b/cassorm/tests/list.py @@ -0,0 +1,61 @@ +from django.utils import unittest +from ..list import CassandraList + +class TestList(CassandraList): + pass + +class CassandraListTest(unittest.TestCase): + + def setUp(self): + TestList.sync(destructive=True) + self.seq = TestList("Test") + + def tearDown(self): + self.seq.delete() + + def test_append(self): + self.seq.append("a") + self.seq.append("b") + self.assertEqual(len(self.seq), 2) + self.assertNotEqual(len(self.seq), 9) + self.assertEqual(self.seq.pop(), "b") + self.assertEqual(self.seq.pop(), "a") + self.assertEqual(len(self.seq), 0) + self.seq.append("c") + self.assertEqual(self.seq.pop(), "c") + + def test_extend(self): + self.seq.extend(["a", "b", "c"]) + self.assertEqual(len(self.seq), 3) + self.assertEqual(self.seq.pop(), "c") + self.seq.extend(["d", "e", "f"]) + self.assertEqual(len(self.seq), 5) + self.assertEqual(self.seq.pop(), "f") + + def test_insert(self): + self.seq.insert(0, "a") + self.seq.insert(0, "b") + self.seq.insert(2, "c") + self.assertEqual(len(self.seq), 3) + self.assertEqual(self.seq.pop(), "c") + self.assertEqual(self.seq.pop(), "a") + self.assertEqual(self.seq.pop(), "b") + self.assertEqual(len(self.seq), 0) + + def test_remove(self): + self.seq.extend(["a", "b", "c"]) + self.seq.remove("b") + self.assertEqual(len(self.seq), 2) + self.assertEqual(self.seq.pop(), "c") + self.assertEqual(self.seq.pop(), "a") + + def test_delete(self): + self.seq.delete() + + def test_get(self): + self.seq.append("a") + self.seq.append("b") + self.assertEqual(self.seq[0], "a") + self.assertEqual(self.seq[1], "b") + + diff --git a/cassorm/tests/model.py b/cassorm/tests/model.py new file mode 100644 index 0000000..119ec13 --- /dev/null +++ b/cassorm/tests/model.py @@ -0,0 +1,112 @@ +from datetime import datetime +import time +import random +from django.utils import unittest +from .randomseq import random_utf8_seq +from ..model import CassandraModel +from ..types import * + + +class TestModel(CassandraModel): + a = DateTime() + b = DateTimeString() + c = Float64() + d = FloatString() + e = Int64() + f = IntString() + g = String() + +class CassandraModelTest(unittest.TestCase): + + def setUp(self): + TestModel.sync(destructive=True) + + def test_a(self): + tm_1 = TestModel() + now = datetime.now() + tm_1.a = now + tm_1.save() + tm_2 = TestModel.objects[tm_1.key] + self.assertAlmostEqual( + time.mktime(tm_2.a.timetuple()), + time.mktime(now.timetuple())) + + def test_b(self): + tm_1 = TestModel() + now = datetime.now() + tm_1.b = now + tm_1.save() + tm_2 = TestModel.objects[tm_1.key] + self.assertAlmostEqual( + time.mktime(tm_2.b.timetuple()), + time.mktime(now.timetuple())) + + def test_c(self): + value = random.random() + tm_1 = TestModel() + tm_1.c = value + tm_1.save() + tm_2 = TestModel.objects[tm_1.key] + self.assertAlmostEqual(tm_2.c, value) + + def test_d(self): + value = random.random() + tm_1 = TestModel() + tm_1.d = value + tm_1.save() + tm_2 = TestModel.objects[tm_1.key] + self.assertAlmostEqual(tm_2.d, value) + + def test_e(self): + value = random.randint(0, 100000) + tm_1 = TestModel() + tm_1.e = value + tm_1.save() + tm_2 = TestModel.objects[tm_1.key] + self.assertEqual(tm_2.e, value) + + def test_f(self): + value = random.randint(0, 100000) + tm_1 = TestModel() + tm_1.f = value + tm_1.save() + tm_2 = TestModel.objects[tm_1.key] + self.assertEqual(tm_2.f, value) + + def test_g(self): + value = "".join(random_utf8_seq() for i in range(10)) + tm_1 = TestModel() + tm_1.g = value + tm_1.save() + tm_2 = TestModel.objects[tm_1.key] + self.assertEqual(tm_2.g, value) + + def test_all(self): + now_a = datetime.now() + now_b = datetime.now() + value_c = random.random() + value_d = random.random() + value_e = random.randint(0, 100000) + value_f = random.randint(0, 100000) + value_g = "".join(random_utf8_seq() for i in range(10)) + tm_1 = TestModel( + a=now_a, + b=now_b, + c=value_c, + d=value_d, + e=value_e, + f=value_f, + g=value_g) + tm_1.save() + tm_2 = TestModel.objects[tm_1.key] + self.assertAlmostEqual( + time.mktime(tm_2.a.timetuple()), + time.mktime(now_a.timetuple())) + self.assertAlmostEqual( + time.mktime(tm_2.b.timetuple()), + time.mktime(now_a.timetuple())) + self.assertAlmostEqual(tm_2.c, value_c) + self.assertAlmostEqual(tm_2.d, value_d) + self.assertEqual(tm_2.e, value_e) + self.assertEqual(tm_2.f, value_f) + self.assertEqual(tm_2.g, value_g) diff --git a/cassorm/tests/randomseq.py b/cassorm/tests/randomseq.py new file mode 100644 index 0000000..0a94f1f --- /dev/null +++ b/cassorm/tests/randomseq.py @@ -0,0 +1,28 @@ +import random + +""" From Table 3-7 of the Unicode Standard 5.0.0 """ + +def byte_range(first, last): + return list(range(first, last+1)) + +first_values = byte_range(0x00, 0x7F) + byte_range(0xC2, 0xF4) +trailing_values = byte_range(0x80, 0xBF) + +def random_utf8_seq(): + first = random.choice(first_values) + if first <= 0x7F: + return bytes([first]) + elif first <= 0xDF: + return bytes([first, random.choice(trailing_values)]) + elif first == 0xE0: + return bytes([first, random.choice(byte_range(0xA0, 0xBF)), random.choice(trailing_values)]) + elif first == 0xED: + return bytes([first, random.choice(byte_range(0x80, 0x9F)), random.choice(trailing_values)]) + elif first <= 0xEF: + return bytes([first, random.choice(trailing_values), random.choice(trailing_values)]) + elif first == 0xF0: + return bytes([first, random.choice(byte_range(0x90, 0xBF)), random.choice(trailing_values), random.choice(trailing_values)]) + elif first <= 0xF3: + return bytes([first, random.choice(trailing_values), random.choice(trailing_values), random.choice(trailing_values)]) + elif first == 0xF4: + return bytes([first, random.choice(byte_range(0x80, 0x8F)), random.choice(trailing_values), random.choice(trailing_values)]) \ No newline at end of file diff --git a/cassorm/types.py b/cassorm/types.py new file mode 100644 index 0000000..85b454c --- /dev/null +++ b/cassorm/types.py @@ -0,0 +1,39 @@ +import pycassa + +def _pycassa_type_init(self, *args, **kwargs): + """Stores an index kwarg, then passes to standard pycassa Types""" + if "index" in kwargs: + self.index = kwargs["index"] + del kwargs["index"] + else: + self.index = False + super(self.__class__, self).__init__(*args, **kwargs) + + +class DateTime(pycassa.DateTime): + __init__ = _pycassa_type_init + + +class DateTimeString(pycassa.DateTimeString): + __init__ = _pycassa_type_init + + +class Float64(pycassa.Float64): + __init__ = _pycassa_type_init + + +class FloatString(pycassa.FloatString): + __init__ = _pycassa_type_init + + +class Int64(pycassa.Int64): + __init__ = _pycassa_type_init + + +class IntString(pycassa.IntString): + __init__ = _pycassa_type_init + + +class String(pycassa.String): + __init__ = _pycassa_type_init + \ No newline at end of file diff --git a/ez_setup.py b/ez_setup.py new file mode 100644 index 0000000..4a84fea --- /dev/null +++ b/ez_setup.py @@ -0,0 +1,275 @@ +#!python +"""Bootstrap setuptools installation + +If you want to use setuptools in your package's setup.py, just include this +file in the same directory with it, and add this to the top of your setup.py:: + + from ez_setup import use_setuptools + use_setuptools() + +If you want to require a specific version of setuptools, set a download +mirror, or use an alternate download directory, you can do so by supplying +the appropriate options to ``use_setuptools()``. + +This file can also be run as a script to install or upgrade setuptools. +""" +import sys +DEFAULT_VERSION = "0.6c9" +DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] + +md5_data = { + 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', + 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', + 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', + 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', + 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', + 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', + 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', + 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', + 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', + 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', + 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', + 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', + 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', + 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', + 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', + 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', + 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', + 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', + 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', + 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', + 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', + 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', + 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', + 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', + 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', + 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', + 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', + 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', + 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', + 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', + 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', + 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', + 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', + 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', +} + +import sys, os +try: from hashlib import md5 +except ImportError: from md5 import md5 + +def _validate_md5(egg_name, data): + if egg_name in md5_data: + digest = md5(data).hexdigest() + if digest != md5_data[egg_name]: + print >>sys.stderr, ( + "md5 validation of %s failed! (Possible download problem?)" + % egg_name + ) + sys.exit(2) + return data + +def use_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + download_delay=15 +): + """Automatically find/download setuptools and make it available on sys.path + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end with + a '/'). `to_dir` is the directory where setuptools will be downloaded, if + it is not already available. If `download_delay` is specified, it should + be the number of seconds that will be paused before initiating a download, + should one be required. If an older version of setuptools is installed, + this routine will print a message to ``sys.stderr`` and raise SystemExit in + an attempt to abort the calling script. + """ + was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules + def do_download(): + egg = download_setuptools(version, download_base, to_dir, download_delay) + sys.path.insert(0, egg) + import setuptools; setuptools.bootstrap_install_from = egg + try: + import pkg_resources + except ImportError: + return do_download() + try: + pkg_resources.require("setuptools>="+version); return + except pkg_resources.VersionConflict, e: + if was_imported: + print >>sys.stderr, ( + "The required version of setuptools (>=%s) is not available, and\n" + "can't be installed while this script is running. Please install\n" + " a more recent version first, using 'easy_install -U setuptools'." + "\n\n(Currently using %r)" + ) % (version, e.args[0]) + sys.exit(2) + else: + del pkg_resources, sys.modules['pkg_resources'] # reload ok + return do_download() + except pkg_resources.DistributionNotFound: + return do_download() + +def download_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + delay = 15 +): + """Download setuptools from a specified location and return its filename + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download attempt. + """ + import urllib2, shutil + egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) + url = download_base + egg_name + saveto = os.path.join(to_dir, egg_name) + src = dst = None + if not os.path.exists(saveto): # Avoid repeated downloads + try: + from distutils import log + if delay: + log.warn(""" +--------------------------------------------------------------------------- +This script requires setuptools version %s to run (even to display +help). I will attempt to download it for you (from +%s), but +you may need to enable firewall access for this script first. +I will start the download in %d seconds. + +(Note: if this machine does not have network access, please obtain the file + + %s + +and place it in this directory before rerunning this script.) +---------------------------------------------------------------------------""", + version, download_base, delay, url + ); from time import sleep; sleep(delay) + log.warn("Downloading %s", url) + src = urllib2.urlopen(url) + # Read/write all in one block, so we don't create a corrupt file + # if the download is interrupted. + data = _validate_md5(egg_name, src.read()) + dst = open(saveto,"wb"); dst.write(data) + finally: + if src: src.close() + if dst: dst.close() + return os.path.realpath(saveto) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +def main(argv, version=DEFAULT_VERSION): + """Install or upgrade setuptools and EasyInstall""" + try: + import setuptools + except ImportError: + egg = None + try: + egg = download_setuptools(version, delay=0) + sys.path.insert(0,egg) + from setuptools.command.easy_install import main + return main(list(argv)+[egg]) # we're done here + finally: + if egg and os.path.exists(egg): + os.unlink(egg) + else: + if setuptools.__version__ == '0.0.1': + print >>sys.stderr, ( + "You have an obsolete version of setuptools installed. Please\n" + "remove it from your system entirely before rerunning this script." + ) + sys.exit(2) + + req = "setuptools>="+version + import pkg_resources + try: + pkg_resources.require(req) + except pkg_resources.VersionConflict: + try: + from setuptools.command.easy_install import main + except ImportError: + from easy_install import main + main(list(argv)+[download_setuptools(delay=0)]) + sys.exit(0) # try to force an exit + else: + if argv: + from setuptools.command.easy_install import main + main(argv) + else: + print "Setuptools version",version,"or greater has been installed." + print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' + +def update_md5(filenames): + """Update our built-in md5 registry""" + + import re + + for name in filenames: + base = os.path.basename(name) + f = open(name,'rb') + md5_data[base] = md5(f.read()).hexdigest() + f.close() + + data = [" %r: %r,\n" % it for it in md5_data.items()] + data.sort() + repl = "".join(data) + + import inspect + srcfile = inspect.getsourcefile(sys.modules[__name__]) + f = open(srcfile, 'rb'); src = f.read(); f.close() + + match = re.search("\nmd5_data = {\n([^}]+)}", src) + if not match: + print >>sys.stderr, "Internal error!" + sys.exit(2) + + src = src[:match.start(1)] + repl + src[match.end(1):] + f = open(srcfile,'w') + f.write(src) + f.close() + + +if __name__=='__main__': + if len(sys.argv)>2 and sys.argv[1]=='--md5update': + update_md5(sys.argv[2:]) + else: + main(sys.argv[1:]) + + + + + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..14adaad --- /dev/null +++ b/setup.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# + +from distutils.core import setup + +__version_info__ = (0, 0, 1) +__version__ = '.'.join([str(v) for v in __version_info__]) + +setup( + name = 'cassorm', + version = __version__, + author = 'John Wehr', + author_email = 'johnwehr@gmail.com', + maintainer = 'John Wehr', + maintainer_email = 'johnwehr@gmail.com', + description = 'Django ORM for use with Apache Cassandra', + url = 'https://github.com/wehriam/cassorm/', + download_url = 'https://github.com/wehriam/cassorm/tarball/master', + keywords = 'cassandra client db distributed thrift pycassa django', + packages = ['cassorm'], + py_modules = ['ez_setup'], + requires = ['pycassa'], + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2.4', + 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Topic :: Software Development :: Libraries :: Python Modules' + ] + ) \ No newline at end of file