summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2017-10-12 15:23:27 +0300
committerLars Wirzenius <liw@liw.fi>2017-10-12 15:23:27 +0300
commit894afeab0e4cab70c6629a611664cdb922aa0aa3 (patch)
tree64d68e9e3818039d82baab12a278aa2f27e2c8d9
parentfc24feb6a85d28ec658abbecea96055add8ccf1d (diff)
downloadqvisqve-894afeab0e4cab70c6629a611664cdb922aa0aa3.tar.gz
Refactor: move notifications and resources into their own routers
-rw-r--r--qvarn/__init__.py1
-rw-r--r--qvarn/api.py146
-rw-r--r--qvarn/notification_router.py236
-rw-r--r--qvarn/resource_router.py165
-rw-r--r--without-tests1
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