diff options
-rw-r--r-- | qvarn/__init__.py | 25 | ||||
-rw-r--r-- | qvarn/objstore.py | 148 | ||||
-rw-r--r-- | qvarn/objstore_tests.py | 131 | ||||
-rw-r--r-- | without-tests | 3 |
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 |