summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--qvarn/__init__.py25
-rw-r--r--qvarn/objstore.py148
-rw-r--r--qvarn/objstore_tests.py131
-rw-r--r--without-tests3
4 files changed, 306 insertions, 1 deletions
diff --git a/qvarn/__init__.py b/qvarn/__init__.py
new file mode 100644
index 0000000..237338b
--- /dev/null
+++ b/qvarn/__init__.py
@@ -0,0 +1,25 @@
+# Copyright (C) 2017 Lars Wirzenius
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+from .objstore import (
+ ObjectStoreInterface,
+ MemoryObjectStore,
+ KeyCollision,
+ UnknownKey,
+ KeyValueError,
+ Equal,
+ All,
+)
diff --git a/qvarn/objstore.py b/qvarn/objstore.py
new file mode 100644
index 0000000..f83eb28
--- /dev/null
+++ b/qvarn/objstore.py
@@ -0,0 +1,148 @@
+# Copyright (C) 2017 Lars Wirzenius
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+class ObjectStoreInterface(object): # pragma: no cover
+
+ '''Store and retrieve JSON-like objects.
+
+ A JSON-like object is a Python dict whose keys are strings, and
+ values corresponding to they keys can serialised into JSON, so
+ they're strings, integers, booleans, or JSON-like objects, or
+ lists of such values. JSON would support more types, but Qvarn
+ doesn't need them.
+
+ The object store stores the JSON-like object, and a set of keys
+ that identify the object. The caller gets to define the keys. The
+ keys must be strings. There can be any number of keys, but there
+ must be at least one. The caller gets to define the keys and their
+ meaning. The allowed keys (and their types) are set when the store
+ is created, using the create_store method.
+
+ Objects may be retrieved or removed using any subset of keys. All
+ maching objects are retrieved or removed.
+
+ Objects mey be found using conditions, implemented by subclasses
+ of the qvarn.Condition class. Various condidions may be combined
+ arbitrarily.
+
+ This class is for defining the ObjectStore interface. There is an
+ in-memory variant, for use in unit tests, and a version using
+ PostgreSQL for production use.
+
+ '''
+
+ def create_store(self, **keys):
+ raise NotImplementedError()
+
+ def create_object(self, obj, **keys):
+ raise NotImplementedError()
+
+ def remove_objects(self, **keys):
+ raise NotImplementedError()
+
+ def get_objects(self, **keys):
+ raise NotImplementedError()
+
+ def _keys_match(self, got_keys, wanted_keys):
+ raise NotImplementedError()
+
+ def find_objects(self, cond):
+ raise NotImplementedError()
+
+ def find_object_ids(self, cond):
+ raise NotImplementedError()
+
+
+
+class MemoryObjectStore(ObjectStoreInterface):
+
+ def __init__(self):
+ self._objs = []
+ self._known_keys = {}
+
+ def create_store(self, **keys):
+ self._known_keys = keys
+
+ def create_object(self, obj, **keys):
+ for key in keys.keys():
+ if key not in self._known_keys:
+ raise UnknownKey(key=key)
+ if type(keys[key]) is not self._known_keys[key]:
+ raise KeyValueError(key, keys[key])
+
+ for _, k in self._objs:
+ if self._keys_match(k, keys):
+ raise KeyCollision(k)
+ self._objs.append((obj, keys))
+
+ def remove_objects(self, **keys):
+ self._objs = [
+ (o,k) for o, k in self._objs if not self._keys_match(k, keys)]
+
+ def get_objects(self, **keys):
+ return [o for o, k in self._objs if self._keys_match(k, keys)]
+
+ def _keys_match(self, got_keys, wanted_keys):
+ for key in wanted_keys.keys():
+ if got_keys.get(key) != wanted_keys[key]:
+ return False
+ return True
+
+ def find_objects(self, cond):
+ return [obj for obj, _ in self._objs if cond.matches(obj)]
+
+ def find_object_ids(self, cond):
+ return [keys for obj, keys in self._objs if cond.matches(obj)]
+
+
+class KeyCollision(Exception):
+
+ def __init__(self, keys):
+ super().__init__('Cannot add object with same keys: %r' % keys)
+
+
+class UnknownKey(Exception):
+
+ def __init__(self, key):
+ super().__init__('ObjectStore is not prepared for key %r' % key)
+
+
+class KeyValueError(Exception):
+
+ def __init__(self, key, value):
+ super().__init__('Key %r value %r has the wrong type' % (key, value))
+
+
+class Condition(object):
+
+ def matches(self, obj): # pragma: no cover
+ raise NotImplementedError()
+
+
+class Equal(Condition):
+
+ def __init__(self, name, value):
+ self._name = name
+ self._value = value
+
+ def matches(self, obj):
+ return obj.get(self._name) == self._value
+
+
+class All(Condition):
+
+ def matches(self, obj):
+ return True
diff --git a/qvarn/objstore_tests.py b/qvarn/objstore_tests.py
new file mode 100644
index 0000000..7a55e76
--- /dev/null
+++ b/qvarn/objstore_tests.py
@@ -0,0 +1,131 @@
+# Copyright (C) 2017 Lars Wirzenius
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import unittest
+
+import qvarn
+
+
+class ObjectStoreTests(unittest.TestCase):
+
+ def setUp(self):
+ self.obj1 = {
+ 'name': 'this is my object',
+ }
+ self.obj2 = {
+ 'name': 'this is my other object',
+ }
+
+ def create_store(self, **keys):
+ store = qvarn.MemoryObjectStore()
+ store.create_store(**keys)
+ return store
+
+ def get_all_objects(self, store):
+ return store.find_objects(qvarn.All())
+
+ def sorted_dicts(self, dicts):
+ return sorted(dicts, key=lambda d: sorted(d.items()))
+
+ def test_is_initially_empty(self):
+ store = self.create_store(key=str)
+ self.assertEqual(self.get_all_objects(store), [])
+
+ def test_adds_object(self):
+ store = self.create_store(key=str)
+ store.create_object(self.obj1, key='1st')
+ self.assertEqual(self.get_all_objects(store), [self.obj1])
+
+ def test_raises_error_for_surprising_keys(self):
+ store = self.create_store(key=str)
+ with self.assertRaises(qvarn.UnknownKey):
+ store.create_object(self.obj1, surprise='1st')
+
+ def test_raises_error_adding_object_with_existing_keys(self):
+ store = self.create_store(key=str)
+ store.create_object(self.obj1, key='1st')
+ with self.assertRaises(qvarn.KeyCollision):
+ store.create_object(self.obj1, key='1st')
+
+ def test_raises_error_adding_object_with_keys_of_wrong_type(self):
+ store = self.create_store(key=str)
+ with self.assertRaises(qvarn.KeyValueError):
+ store.create_object(self.obj1, key=1)
+
+ def test_adds_objects_with_two_keys_with_one_key_the_same(self):
+ store = self.create_store(key1=str, key2=str)
+ store.create_object(self.obj1, key1='same', key2='1st')
+ store.create_object(self.obj2, key1='same', key2='2nd')
+ self.assertEqual(self.get_all_objects(store), [self.obj1, self.obj2])
+
+ def test_removes_only_object(self):
+ store = self.create_store(key=str)
+ store.create_object(self.obj1, key='1st')
+ store.remove_objects(key='1st')
+ self.assertEqual(self.get_all_objects(store), [])
+
+ def test_gets_objects(self):
+ store = self.create_store(key=str)
+ store.create_object(self.obj1, key='1st')
+ store.create_object(self.obj2, key='2nd')
+ self.assertEqual(store.get_objects(key='1st'), [self.obj1])
+ self.assertEqual(store.get_objects(key='2nd'), [self.obj2])
+
+ def test_gets_objects_using_only_one_key(self):
+ store = self.create_store(key1=str, key2=str)
+ store.create_object(self.obj1, key1='1st', key2='foo')
+ store.create_object(self.obj2, key1='2nd', key2='foo')
+ self.assertEqual(store.get_objects(key1='1st'), [self.obj1])
+ self.assertEqual(store.get_objects(key1='2nd'), [self.obj2])
+ self.assertEqual(
+ self.sorted_dicts(store.get_objects(key2='foo')),
+ self.sorted_dicts([self.obj1, self.obj2]))
+
+ def test_removes_only_one_object(self):
+ store = self.create_store(key=str)
+ store.create_object(self.obj1, key='1st')
+ store.create_object(self.obj2, key='2nd')
+ store.remove_objects(key='1st')
+ self.assertEqual(self.get_all_objects(store), [self.obj2])
+
+ def test_finds_objects(self):
+ store = self.create_store(key=str)
+ store.create_object(self.obj1, key='1st')
+ store.create_object(self.obj2, key='2nd')
+
+ cond = qvarn.Equal('name', self.obj1['name'])
+ objs = store.find_objects(cond)
+ self.assertEqual(objs, [self.obj1])
+
+ def test_finds_ids_of_objects(self):
+ store = self.create_store(key=str)
+ store.create_object(self.obj1, key='1st')
+ store.create_object(self.obj2, key='2nd')
+
+ cond = qvarn.Equal('name', self.obj1['name'])
+ ids = store.find_object_ids(cond)
+ self.assertEqual(ids, [{'key': '1st'}])
+
+ def test_finds_ids_of_multipl_objects(self):
+ store = self.create_store(key=str)
+ store.create_object(self.obj1, key='1st')
+ store.create_object(self.obj2, key='2nd')
+
+ ids = store.find_object_ids(qvarn.All())
+ self.assertEqual(
+ self.sorted_dicts(ids),
+ self.sorted_dicts([{'key': '1st'}, {'key': '2nd'}])
+ )
diff --git a/without-tests b/without-tests
index 7c5d287..03277d8 100644
--- a/without-tests
+++ b/without-tests
@@ -1,7 +1,8 @@
apitest.py
-qvarn.py
apifw/__init__.py
apifw/http.py
apifw/bottleapp.py
apifw/apixface.py
slog/__init__.py
+qvarn-backend.py
+qvarn/__init__.py