diff options
author | Lars Wirzenius <liw@liw.fi> | 2017-10-12 15:23:27 +0300 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2017-10-12 15:23:27 +0300 |
commit | 894afeab0e4cab70c6629a611664cdb922aa0aa3 (patch) | |
tree | 64d68e9e3818039d82baab12a278aa2f27e2c8d9 | |
parent | fc24feb6a85d28ec658abbecea96055add8ccf1d (diff) | |
download | qvisqve-894afeab0e4cab70c6629a611664cdb922aa0aa3.tar.gz |
Refactor: move notifications and resources into their own routers
-rw-r--r-- | qvarn/__init__.py | 1 | ||||
-rw-r--r-- | qvarn/api.py | 146 | ||||
-rw-r--r-- | qvarn/notification_router.py | 236 | ||||
-rw-r--r-- | qvarn/resource_router.py | 165 | ||||
-rw-r--r-- | without-tests | 1 |
5 files changed, 410 insertions, 139 deletions
diff --git a/qvarn/__init__.py b/qvarn/__init__.py index f2f0efb..7d35b1a 100644 --- a/qvarn/__init__.py +++ b/qvarn/__init__.py @@ -106,6 +106,7 @@ from .api_errors import ( from .router import Router from .file_router import FileRouter from .notification_router import NotificationRouter +from .resource_router import ResourceRouter from .subresource_router import SubresourceRouter from .version_router import VersionRouter from .timestamp import get_current_timestamp diff --git a/qvarn/api.py b/qvarn/api.py index 992b52e..6211f14 100644 --- a/qvarn/api.py +++ b/qvarn/api.py @@ -15,7 +15,6 @@ import io -import os import yaml @@ -201,39 +200,10 @@ class QvarnAPI: coll.set_object_store(self._store) coll.set_resource_type(rt) - id_path = os.path.join(path, '<id>') - routes = [ - { - 'method': 'POST', - 'path': path, - 'callback': self.get_post_callback(coll), - }, - { - 'method': 'PUT', - 'path': id_path, - 'callback': self.get_put_callback(coll), - }, - { - 'method': 'GET', - 'path': path, - 'callback': self.get_resource_list_callback(coll), - }, - { - 'method': 'GET', - 'path': id_path, - 'callback': self.get_resource_callback(coll), - }, - { - 'method': 'GET', - 'path': path + '/search/<search_criteria:path>', - 'callback': self.get_search_callback(coll), - }, - { - 'method': 'DELETE', - 'path': id_path, - 'callback': self.delete_resource_callback(coll), - }, - ] + router = qvarn.ResourceRouter() + router.set_collection(coll) + router.set_notifier(self.notify) + routes = router.get_routes() files = rt.get_files() for subpath in rt.get_subpaths(): @@ -250,15 +220,14 @@ class QvarnAPI: more = file_router.get_routes() routes.extend(more) - return routes + self._get_notification_routes(coll, path, id_path) - - def _get_notification_routes(self, coll, path, id_path): listener_rt = self.get_listener_resource_type() notif_router = qvarn.NotificationRouter() notif_router.set_baseurl(self._baseurl) notif_router.set_parent_collection(coll) notif_router.set_object_store(self._store, listener_rt) - return notif_router.get_routes() + routes.extend(notif_router.get_routes()) + + return routes def notify(self, rid, rrev, change): # pragma: no cover rt = self.get_notification_resource_type() @@ -294,104 +263,3 @@ class QvarnAPI: if rid in obj.get('listen_on', []): return True return False - - def get_post_callback(self, coll): # pragma: no cover - def wrapper(content_type, body, **kwargs): - if content_type != 'application/json': - raise qvarn.NotJson(content_type) - if 'type' not in body: - body['type'] = coll.get_type_name() - try: - self._validator.validate_new_resource(body, coll.get_type()) - except qvarn.ValidationError as e: - qvarn.log.log('error', msg_text=str(e), body=body) - return qvarn.bad_request_response(str(e)) - result_body = coll.post(body) - qvarn.log.log( - 'debug', msg_text='POST a new resource, result', - body=result_body) - location = '{}{}/{}'.format( - self._baseurl, coll.get_type().get_path(), result_body['id']) - self.notify(result_body['id'], result_body['revision'], 'created') - return qvarn.created_response(result_body, location) - return wrapper - - def get_put_callback(self, coll): # pragma: no cover - def wrapper(content_type, body, **kwargs): - if content_type != 'application/json': - raise qvarn.NotJson(content_type) - - if 'type' not in body: - body['type'] = coll.get_type_name() - - if 'id' not in body: - body['id'] = kwargs['id'] - - try: - self._validator.validate_resource_update( - body, coll.get_type()) - except qvarn.ValidationError as e: - qvarn.log.log('error', msg_text=str(e), body=body) - return qvarn.bad_request_response(str(e)) - - obj_id = kwargs['id'] - # FIXME: the following test should be enabled once we - # no longer need test-api. - if False and body['id'] != obj_id: - raise qvarn.IdMismatch(body['id'], obj_id) - - try: - result_body = coll.put(body) - except qvarn.WrongRevision as e: - return qvarn.conflict_response(str(e)) - except qvarn.NoSuchResource as e: - # We intentionally say bad request, instead of not found. - # This is to be compatible with old Qvarn. This may get - # changed later. - return qvarn.bad_request_response(str(e)) - - self.notify( - result_body['id'], result_body['revision'], 'updated') - return qvarn.ok_response(result_body) - return wrapper - - def get_resource_callback(self, coll): # pragma: no cover - def wrapper(content_type, body, **kwargs): - try: - obj = coll.get(kwargs['id']) - except qvarn.NoSuchResource as e: - return qvarn.no_such_resource_response(str(e)) - return qvarn.ok_response(obj) - return wrapper - - def get_resource_list_callback(self, coll): # pragma: no cover - def wrapper(content_type, body, **kwargs): - body = coll.list() - return qvarn.ok_response(body) - return wrapper - - def get_search_callback(self, coll): # pragma: no cover - def wrapper(content_type, body, **kwargs): - path = kwargs['raw_uri_path'] - search_criteria = path.split('/search/', 1)[1] - try: - result = coll.search(search_criteria) - except qvarn.UnknownSearchField as e: - return qvarn.unknown_search_field_response(e) - except qvarn.NeedSortOperator: - return qvarn.need_sort_response() - except qvarn.SearchParserError as e: - return qvarn.search_parser_error_response(e) - body = { - 'resources': result, - } - return qvarn.ok_response(body) - return wrapper - - def delete_resource_callback(self, coll): # pragma: no cover - def wrapper(content_type, body, **kwargs): - obj_id = kwargs['id'] - coll.delete(obj_id) - self.notify(obj_id, None, 'deleted') - return qvarn.ok_response({}) - return wrapper diff --git a/qvarn/notification_router.py b/qvarn/notification_router.py new file mode 100644 index 0000000..000c445 --- /dev/null +++ b/qvarn/notification_router.py @@ -0,0 +1,236 @@ +# 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 qvarn + + +class NotificationRouter(qvarn.Router): + + def __init__(self): + super().__init__() + self._baseurl = None + self._store = None + self._parent_coll = None + self._listener_coll = None + + def set_baseurl(self, baseurl): + self._baseurl = baseurl + + def set_parent_collection(self, parent_coll): + self._parent_coll = parent_coll + + def set_object_store(self, store, listener_rt): + self._store = store + listeners = qvarn.CollectionAPI() + listeners.set_object_store(self._store) + listeners.set_resource_type(listener_rt) + self._listener_coll = listeners + + def get_routes(self): + rt = self._parent_coll.get_type() + listeners_path = '{}/listeners'.format(rt.get_path()) + listener_id_path = '{}/<listener_id>'.format(listeners_path) + notifications_path = '{}/notifications'.format(listener_id_path) + notification_id_path = '{}/<notification_id>'.format( + notifications_path) + + return [ + { + 'method': 'POST', + 'path': listeners_path, + 'callback': self._create_listener, + }, + { + 'method': 'GET', + 'path': listeners_path, + 'callback': self._get_listener_list, + }, + { + 'method': 'GET', + 'path': listener_id_path, + 'callback': self._get_a_listener, + }, + { + 'method': 'PUT', + 'path': listener_id_path, + 'callback': self._update_listener, + }, + { + 'method': 'DELETE', + 'path': listener_id_path, + 'callback': self._delete_listener, + }, + { + 'method': 'GET', + 'path': notifications_path, + 'callback': self._get_notifications_list, + }, + { + 'method': 'GET', + 'path': notification_id_path, + 'callback': self._get_a_notification, + }, + { + 'method': 'DELETE', + 'path': notification_id_path, + 'callback': self._delete_notification, + }, + ] + + def _create_listener(self, content_type, body, *args, **kwargs): + if content_type != 'application/json': + raise qvarn.NotJson(content_type) + + rt = self._listener_coll.get_type() + validator = qvarn.Validator() + try: + validator.validate_against_prototype( + rt.get_type(), body, rt.get_latest_prototype()) + except qvarn.ValidationError as e: + qvarn.log.log('error', msg_text=str(e), body=body) + return qvarn.bad_request_response(str(e)) + + if 'type' not in body: + body['type'] = 'listener' + + result_body = self._listener_coll.post(body) + location = self._get_new_resource_location(result_body) + qvarn.log.log( + 'debug', msg_text='POST a new listener, result', + body=result_body, location=location) + return qvarn.created_response(result_body, location) + + def _get_new_resource_location(self, resource): + return '{}{}/listeners/{}'.format( + self._baseurl, self._parent_coll.get_type().get_path(), + resource['id']) + + def _get_listener_list(self, content_type, body, *args, **kwargs): + body = self._listener_coll.list() + return qvarn.ok_response(body) + + def _get_a_listener(self, *args, **kwargs): + try: + obj = self._listener_coll.get(kwargs['listener_id']) + except qvarn.NoSuchResource as e: + return qvarn.no_such_resource_response(str(e)) + return qvarn.ok_response(obj) + + def _update_listener(self, content_type, body, *args, **kwargs): + if content_type != 'application/json': + raise qvarn.NotJson(content_type) + + if 'type' not in body: + body['type'] = 'listener' + + listener_id = kwargs['listener_id'] + if 'id' not in body: + body['id'] = listener_id + + validator = qvarn.Validator() + try: + validator.validate_resource_update( + body, self._listener_coll.get_type()) + except qvarn.ValidationError as e: + qvarn.log.log('error', msg_text=str(e), body=body) + return qvarn.bad_request_response(str(e)) + + try: + result_body = self._listener_coll.put(body) + except qvarn.WrongRevision as e: + return qvarn.conflict_response(str(e)) + except qvarn.NoSuchResource as e: + # We intentionally say bad request, instead of not found. + # This is to be compatible with old Qvarn. This may get + # changed later. + return qvarn.bad_request_response(str(e)) + + return qvarn.ok_response(result_body) + + def _delete_listener(self, *args, **kwargs): + listener_id = kwargs['listener_id'] + self._listener_coll.delete(listener_id) + for obj_id in self._find_notifications(listener_id): + self._store.remove_objects(obj_id=obj_id) + return qvarn.ok_response({}) + + def _find_notifications(self, listener_id): + cond = qvarn.All( + qvarn.Equal('type', 'notification'), + qvarn.Equal('listener_id', listener_id), + ) + obj_ids = [ + keys['obj_id'] + for keys, _ in self._store.find_objects(cond) + ] + qvarn.log.log( + 'trace', msg_text='Found notifications', + notifications=obj_ids) + return obj_ids + + def _get_notifications_list(self, *args, **kwargs): + def timestamp(pair): + _, obj = pair + return obj['timestamp'] + + listener_id = kwargs['listener_id'] + cond = qvarn.All( + qvarn.Equal('type', 'notification'), + qvarn.Equal('listener_id', listener_id) + ) + pairs = self._store.find_objects(cond) + ordered = sorted(pairs, key=timestamp) + body = { + 'resources': [ + { + 'id': keys['obj_id'] + } + for keys, _ in ordered + ] + } + return qvarn.ok_response(body) + + def _get_a_notification(self, *args, **kwargs): + listener_id = kwargs['listener_id'] + notification_id = kwargs['notification_id'] + cond = qvarn.All( + qvarn.Equal('type', 'notification'), + qvarn.Equal('listener_id', listener_id), + qvarn.Equal('id', notification_id), + ) + pairs = self._store.find_objects(cond) + if len(pairs) == 0: + return qvarn.no_such_resource_response(notification_id) + if len(pairs) > 1: + raise qvarn.TooManyResources(notification_id) + return qvarn.ok_response(pairs[0][1]) + + def _delete_notification(self, *args, **kwargs): + listener_id = kwargs['listener_id'] + notification_id = kwargs['notification_id'] + cond = qvarn.All( + qvarn.Equal('type', 'notification'), + qvarn.Equal('listener_id', listener_id), + qvarn.Equal('id', notification_id), + ) + for keys, _ in self._store.find_objects(cond): + values = { + key: keys[key] + for key in keys + if isinstance(keys[key], str) + } + self._store.remove_objects(**values) + return qvarn.ok_response({}) diff --git a/qvarn/resource_router.py b/qvarn/resource_router.py new file mode 100644 index 0000000..ddfabe7 --- /dev/null +++ b/qvarn/resource_router.py @@ -0,0 +1,165 @@ +# 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 qvarn + + +class ResourceRouter(qvarn.Router): + + def __init__(self): + super().__init__() + self._coll = None + self._baseurl = None + self._notify = None + + def set_baseurl(self, baseurl): + self._baseurl = baseurl + + def set_collection(self, coll): + self._coll = coll + + def set_notifier(self, notify): + self._notify = notify + + def get_routes(self): + rt = self._coll.get_type() + path = rt.get_path() + id_path = '{}/<id>'.format(path) + + return [ + { + 'method': 'POST', + 'path': path, + 'callback': self._create, + }, + { + 'method': 'PUT', + 'path': id_path, + 'callback': self._update, + }, + { + 'method': 'GET', + 'path': path, + 'callback': self._list, + }, + { + 'method': 'GET', + 'path': id_path, + 'callback': self._get, + }, + { + 'method': 'GET', + 'path': path + '/search/<search_criteria:path>', + 'callback': self._search, + }, + { + 'method': 'DELETE', + 'path': id_path, + 'callback': self._delete, + }, + ] + + def _create(self, content_type, body, *args, **kwargs): + if content_type != 'application/json': + raise qvarn.NotJson(content_type) + + if 'type' not in body: + body['type'] = self._coll.get_type_name() + + validator = qvarn.Validator() + try: + validator.validate_new_resource(body, self._coll.get_type()) + except qvarn.ValidationError as e: + qvarn.log.log('error', msg_text=str(e), body=body) + return qvarn.bad_request_response(str(e)) + + result_body = self._coll.post(body) + qvarn.log.log( + 'debug', msg_text='POST a new resource, result', + body=result_body) + location = '{}{}/{}'.format( + self._baseurl, self._coll.get_type().get_path(), result_body['id']) + self._notify(result_body['id'], result_body['revision'], 'created') + return qvarn.created_response(result_body, location) + + def _update(self, content_type, body, *args, **kwargs): + if content_type != 'application/json': + raise qvarn.NotJson(content_type) + + if 'type' not in body: + body['type'] = self._coll.get_type_name() + + if 'id' not in body: + body['id'] = kwargs['id'] + + validator = qvarn.Validator() + try: + validator.validate_resource_update(body, self._coll.get_type()) + except qvarn.ValidationError as e: + qvarn.log.log('error', msg_text=str(e), body=body) + return qvarn.bad_request_response(str(e)) + + obj_id = kwargs['id'] + # FIXME: the following test should be enabled once we + # no longer need test-api. + if False and body['id'] != obj_id: + raise qvarn.IdMismatch(body['id'], obj_id) + + try: + result_body = self._coll.put(body) + except qvarn.WrongRevision as e: + return qvarn.conflict_response(str(e)) + except qvarn.NoSuchResource as e: + # We intentionally say bad request, instead of not found. + # This is to be compatible with old Qvarn. This may get + # changed later. + return qvarn.bad_request_response(str(e)) + + self._notify(result_body['id'], result_body['revision'], 'updated') + return qvarn.ok_response(result_body) + + def _list(self, *args, **kwargs): + body = self._coll.list() + return qvarn.ok_response(body) + + def _get(self, *args, **kwargs): + try: + obj = self._coll.get(kwargs['id']) + except qvarn.NoSuchResource as e: + return qvarn.no_such_resource_response(str(e)) + return qvarn.ok_response(obj) + + def _search(self, *args, **kwargs): + path = kwargs['raw_uri_path'] + search_criteria = path.split('/search/', 1)[1] + try: + result = self._coll.search(search_criteria) + except qvarn.UnknownSearchField as e: + return qvarn.unknown_search_field_response(e) + except qvarn.NeedSortOperator: + return qvarn.need_sort_response() + except qvarn.SearchParserError as e: + return qvarn.search_parser_error_response(e) + body = { + 'resources': result, + } + return qvarn.ok_response(body) + + def _delete(self, *args, **kwargs): + obj_id = kwargs['id'] + self._coll.delete(obj_id) + self._notify(obj_id, None, 'deleted') + return qvarn.ok_response({}) diff --git a/without-tests b/without-tests index 09d1efc..a752d9d 100644 --- a/without-tests +++ b/without-tests @@ -5,6 +5,7 @@ qvarn/backend.py qvarn/file_router.py qvarn/logging.py qvarn/notification_router.py +qvarn/resource_router.py qvarn/responses.py qvarn/router.py qvarn/sql.py |