summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2017-10-12 16:01:48 +0300
committerLars Wirzenius <liw@liw.fi>2017-10-12 16:01:48 +0300
commit2afe31ec08117274e989c2de927ea9aab2187179 (patch)
tree6769a80a0e5d7469fc7cccdb03c43e338db8f5ad
parentd585b8a513769f445fbc2e8f1b3ee31c9a6f7cd3 (diff)
parent9255a00983e4b270c1901a8399ce58adcd2906ff (diff)
downloadqvisqve-2afe31ec08117274e989c2de927ea9aab2187179.tar.gz
Merge: refactor api.py for cleanliness
-rw-r--r--qvarn/__init__.py29
-rw-r--r--qvarn/api.py773
-rw-r--r--qvarn/api_errors.py45
-rw-r--r--qvarn/api_tests.py19
-rw-r--r--qvarn/file_router.py98
-rw-r--r--qvarn/notification_router.py236
-rw-r--r--qvarn/resource_router.py165
-rw-r--r--qvarn/responses.py97
-rw-r--r--qvarn/router.py23
-rw-r--r--qvarn/subresource_router.py82
-rw-r--r--qvarn/timestamp.py25
-rw-r--r--qvarn/version_router.py42
-rw-r--r--resource_type/listeners.yaml11
-rw-r--r--resource_type/notifications.yaml13
-rw-r--r--without-tests9
15 files changed, 931 insertions, 736 deletions
diff --git a/qvarn/__init__.py b/qvarn/__init__.py
index b2d6bb1..7d35b1a 100644
--- a/qvarn/__init__.py
+++ b/qvarn/__init__.py
@@ -84,4 +84,31 @@ from .collection import (
WrongRevision,
)
-from .api import QvarnAPI, NoSuchResourceType
+from .responses import (
+ bad_request_response,
+ conflict_response,
+ created_response,
+ need_sort_response,
+ no_such_resource_response,
+ ok_response,
+ search_parser_error_response,
+ unknown_search_field_response,
+)
+
+from .api_errors import (
+ IdMismatch,
+ NoSuchResourceType,
+ NotJson,
+ TooManyResources,
+ TooManyResourceTypes,
+)
+
+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
+
+from .api import QvarnAPI
diff --git a/qvarn/api.py b/qvarn/api.py
index b7b8cc7..a2f3e64 100644
--- a/qvarn/api.py
+++ b/qvarn/api.py
@@ -14,71 +14,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import io
-import os
-import time
-
-import yaml
-
-import apifw
import qvarn
-resource_type_spec_yaml = '''
-type: resource_type
-path: /resource_types
-versions:
-
-- version: v0
- prototype:
- type: ""
- id: ""
- revision: ""
- name: ""
- yaml: ""
-'''
-
-listener_spec = {
- 'type': 'listener',
- 'path': '/listeners',
- 'versions': [
- {
- 'version': 'v0',
- 'prototype': {
- 'id': '',
- 'type': '',
- 'revision': '',
- 'notify_of_new': False,
- 'listen_on_all': False,
- 'listen_on': [''],
- },
- 'subpaths': {},
- },
- ],
-}
-
-notification_spec = {
- 'type': 'notification',
- 'path': '/notifications',
- 'versions': [
- {
- 'version': 'v0',
- 'prototype': {
- 'id': '',
- 'type': '',
- 'revision': '',
- 'listener_id': '',
- 'resource_id': '',
- 'resource_revision': '',
- 'resource_change': '',
- 'timestamp': '',
- },
- 'subpaths': {},
- },
- ],
-}
-
-
class QvarnAPI:
def __init__(self):
@@ -93,29 +31,10 @@ class QvarnAPI:
def set_object_store(self, store):
self._store = store
self._store.create_store(obj_id=str, subpath=str)
- self.set_up_resource_types()
-
- def set_up_resource_types(self):
- f = io.StringIO(resource_type_spec_yaml)
- spec = yaml.safe_load(f)
- rt = qvarn.ResourceType()
- rt.from_spec(spec)
- self._rt_coll = qvarn.CollectionAPI()
- self._rt_coll.set_object_store(self._store)
- self._rt_coll.set_resource_type(rt)
-
- for spec in [listener_spec, notification_spec]:
- rt2 = qvarn.ResourceType()
- rt2.from_spec(spec)
- self.add_resource_type(rt2)
def add_resource_type(self, rt):
path = rt.get_path()
- cond1 = qvarn.Equal('path', path)
- cond2 = qvarn.Equal('type', 'resource_type')
- cond = qvarn.All(cond1, cond2)
- results = self._store.find_objects(cond)
- objs = [obj for _, obj in results]
+ objs = self._get_resource_type_given_path(path)
if not objs:
obj = {
'id': rt.get_type(),
@@ -127,55 +46,51 @@ class QvarnAPI:
obj, obj_id=obj['id'], subpath='', auxtable=True)
def get_resource_type(self, path):
- cond1 = qvarn.Equal('path', path)
- cond2 = qvarn.Equal('type', 'resource_type')
- cond = qvarn.All(cond1, cond2)
- results = self._store.find_objects(cond)
- objs = [obj for _, obj in results]
- qvarn.log.log('debug', objs=objs)
+ objs = self._get_resource_type_given_path(path)
if len(objs) == 0:
qvarn.log.log(
'error',
msg_text='There is no resource type for path',
path=path)
- raise NoSuchResourceType(path)
+ raise qvarn.NoSuchResourceType(path)
elif len(objs) > 1: # pragma: no cover
qvarn.log.log(
'error',
msg_text='There are more than one resource types for path',
path=path,
objs=objs)
- raise TooManyResourceTypes(path)
+ raise qvarn.TooManyResourceTypes(path)
rt = qvarn.ResourceType()
rt.from_spec(objs[0]['spec'])
return rt
- def get_listener_resource_type(self):
- cond1 = qvarn.Equal('id', 'listener')
- cond2 = qvarn.Equal('type', 'resource_type')
- cond = qvarn.All(cond1, cond2)
+ def _get_resource_type_given_path(self, path):
+ cond = qvarn.All(
+ qvarn.Equal('path', path),
+ qvarn.Equal('type', 'resource_type'),
+ )
results = self._store.find_objects(cond)
- objs = [obj for _, obj in results]
- qvarn.log.log('debug', objs=objs)
- if len(objs) == 0: # pragma: no cover
- raise NoSuchResourceType('listener')
- elif len(objs) > 1: # pragma: no cover
- raise TooManyResourceTypes('listener')
- rt = qvarn.ResourceType()
- rt.from_spec(objs[0]['spec'])
- return rt
+ return [obj for _, obj in results]
+
+ def get_listener_resource_type(self):
+ return self._get_resource_type_given_type('listener')
def get_notification_resource_type(self): # pragma: no cover
- cond1 = qvarn.Equal('id', 'notification')
- cond2 = qvarn.Equal('type', 'resource_type')
- cond = qvarn.All(cond1, cond2)
+ return self._get_resource_type_given_type('notification')
+
+ def _get_resource_type_given_type(self, type_name):
+ cond = qvarn.All(
+ qvarn.Equal('id', type_name),
+ qvarn.Equal('type', 'resource_type'),
+ )
results = self._store.find_objects(cond)
objs = [obj for _, obj in results]
- qvarn.log.log('debug', objs=objs)
+
if len(objs) == 0: # pragma: no cover
- raise NoSuchResourceType('listener')
+ raise qvarn.NoSuchResourceType(type_name)
elif len(objs) > 1: # pragma: no cover
- raise TooManyResourceTypes('listener')
+ raise qvarn.TooManyResourceTypes(type_name)
+
rt = qvarn.ResourceType()
rt.from_spec(objs[0]['spec'])
return rt
@@ -185,336 +100,52 @@ class QvarnAPI:
if path == '/version':
qvarn.log.log('info', msg_text='Add /version route')
- return self.version_route()
+ v = qvarn.VersionRouter()
+ return v.get_routes()
try:
rt = self.get_resource_type(path)
- except NoSuchResourceType:
+ except qvarn.NoSuchResourceType:
qvarn.log.log('warning', msg_text='No such route', path=path)
return []
routes = self.resource_routes(path, rt)
- loggable_routes = [
- {
- key: repr(r[key])
- for key in r
- }
- for r in routes
- ]
- qvarn.log.log('info', msg_text='Add routes', routes=loggable_routes)
+ qvarn.log.log('info', msg_text='Found missing routes', routes=routes)
return routes
- def version_route(self):
- return [
- {
- 'method': 'GET',
- 'path': '/version',
- 'callback': self.version,
- 'needs-authorization': False,
- },
- ]
-
- def version(self, content_type, body, **kwargs):
- version = {
- 'api': {
- 'version': qvarn.__version__,
- },
- 'implementation': {
- 'name': 'Qvarn',
- 'version': qvarn.__version__,
- },
- }
- return ok_response(version)
-
def resource_routes(self, path, rt): # pragma: no cover
coll = qvarn.CollectionAPI()
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():
if subpath not in files:
- more = self.get_subresource_routes(id_path, coll, subpath)
+ sub_router = qvarn.SubresourceRouter()
+ sub_router.set_subpath(subpath)
+ sub_router.set_parent_collection(coll)
+ more = sub_router.get_routes()
else:
- more = self.get_file_routes(id_path, coll, subpath)
+ file_router = qvarn.FileRouter()
+ file_router.set_subpath(subpath)
+ file_router.set_object_store(self._store)
+ file_router.set_parent_collection(coll)
+ more = file_router.get_routes()
routes.extend(more)
- return routes + self._get_notification_routes(coll, path, id_path)
-
- def get_subresource_routes(
- self, id_path, coll, subpath): # pragma: no cover
- return [
- {
- 'method': 'GET',
- 'path': '{}/{}'.format(id_path, subpath),
- 'callback': self.get_subpath_callback(coll, subpath),
- },
- {
- 'method': 'PUT',
- 'path': '{}/{}'.format(id_path, subpath),
- 'callback': self.put_subpath_callback(coll, subpath),
- },
- ]
-
- def get_file_routes(self, id_path, objcoll, subpath): # pragma: no cover
- return [
- {
- 'method': 'GET',
- 'path': '{}/{}'.format(id_path, subpath),
- 'callback': self.get_file_callback(objcoll, subpath),
- },
- {
- 'method': 'PUT',
- 'path': '{}/{}'.format(id_path, subpath),
- 'callback': self.put_file_callback(objcoll, subpath),
- },
- ]
-
- def _get_notification_routes(self, coll, path, id_path):
- rt = self.get_listener_resource_type()
- listeners = qvarn.CollectionAPI()
- listeners.set_object_store(self._store)
- listeners.set_resource_type(rt)
-
- return [
- {
- 'method': 'POST',
- 'path': path + '/listeners',
- 'callback': self.get_post_listener_callback(coll, listeners),
- },
- {
- 'method': 'GET',
- 'path': path + '/listeners',
- 'callback': self.get_listener_list_callback(listeners),
- },
- {
- 'method': 'GET',
- 'path': path + '/listeners/<id>',
- 'callback': self.get_listener_callback(coll, listeners),
- },
- {
- 'method': 'PUT',
- 'path': path + '/listeners/<id>',
- 'callback': self.put_listener_callback(listeners),
- },
- {
- 'method': 'DELETE',
- 'path': path + '/listeners/<id>',
- 'callback': self.delete_listener_callback(listeners),
- },
- {
- 'method': 'GET',
- 'path': path + '/listeners/<id>/notifications',
- 'callback': self.get_notifications_list_callback(),
- },
- {
- 'method': 'GET',
- 'path': path + '/listeners/<listener_id>/notifications/<id>',
- 'callback': self.get_notification_callback(),
- },
- {
- 'method': 'DELETE',
- 'path': path + '/listeners/<listener_id>/notifications/<id>',
- 'callback': self.delete_notification_callback(),
- },
- ]
-
- def get_post_listener_callback(self, coll, listeners): # pragma: no cover
- def wrapper(content_type, body, **kwargs):
- if content_type != 'application/json':
- raise NotJson(content_type)
-
- rt = listeners.get_type()
- try:
- self._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 bad_request_response(str(e))
-
- if 'type' not in body:
- body['type'] = 'listener'
-
- result_body = listeners.post(body)
- qvarn.log.log(
- 'debug', msg_text='POST a new listener, result',
- body=result_body)
- location = '{}{}/listeners/{}'.format(
- self._baseurl, coll.get_type().get_path(),
- result_body['id'])
- return created_response(result_body, location)
- return wrapper
-
- def get_listener_list_callback(self, listeners): # pragma: no cover
- def wrapper(content_type, body, **kwargs):
- body = listeners.list()
- return ok_response(body)
- return wrapper
-
- def get_listener_callback(self, coll, listeners): # pragma: no cover
- def wrapper(content_type, body, **kwargs):
- try:
- obj = listeners.get(kwargs['id'])
- except qvarn.NoSuchResource as e:
- return no_such_resource_response(str(e))
- return ok_response(obj)
- return wrapper
-
- def put_listener_callback(self, listeners): # pragma: no cover
- def wrapper(content_type, body, **kwargs):
- if content_type != 'application/json':
- raise NotJson(content_type)
-
- if 'type' not in body:
- body['type'] = 'listener'
+ 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)
+ routes.extend(notif_router.get_routes())
- listener_id = kwargs['id']
-
- if 'id' not in body:
- body['id'] = listener_id
-
- try:
- self._validator.validate_resource_update(
- body, listeners.get_type())
- except qvarn.ValidationError as e:
- qvarn.log.log('error', msg_text=str(e), body=body)
- return bad_request_response(str(e))
-
- try:
- result_body = listeners.put(body)
- except qvarn.WrongRevision as e:
- return 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 bad_request_response(str(e))
-
- return ok_response(result_body)
- return wrapper
-
- def get_notifications_list_callback(self): # pragma: no cover
- def timestamp(pair):
- _, obj = pair
- return obj['timestamp']
-
- def wrapper(content_type, body, **kwargs):
- rid = kwargs['id']
- cond = qvarn.All(
- qvarn.Equal('type', 'notification'),
- qvarn.Equal('listener_id', rid)
- )
- pairs = self._store.find_objects(cond)
- ordered = sorted(pairs, key=timestamp)
- qvarn.log.log(
- 'trace', msg_text='Found notifications',
- notifications=ordered)
- body = {
- 'resources': [
- {
- 'id': keys['obj_id']
- }
- for keys, _ in ordered
- ]
- }
- return ok_response(body)
- return wrapper
-
- def get_notification_callback(self): # pragma: no cover
- def wrapper(content_type, body, **kwargs):
- listener_id = kwargs['listener_id']
- notification_id = kwargs['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)
- qvarn.log.log(
- 'trace', msg_text='Found notifications',
- notifications=pairs)
- if len(pairs) == 0:
- return no_such_resource_response(notification_id)
- if len(pairs) > 1:
- raise TooManyResources(notification_id)
- return ok_response(pairs[0][1])
- return wrapper
-
- def delete_listener_callback(self, listeners): # pragma: no cover
- def wrapper(content_type, body, **kwargs):
- listener_id = kwargs['id']
- listeners.delete(listener_id)
- for obj_id in self.find_notifications(listener_id):
- self._store.remove_objects(obj_id=obj_id)
- return ok_response({})
- return wrapper
-
- def delete_notification_callback(self): # pragma: no cover
- def wrapper(content_type, body, **kwargs):
- listener_id = kwargs['listener_id']
- notification_id = kwargs['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 ok_response({})
- return wrapper
-
- def find_notifications(self, listener_id): # pragma: no cover
- 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
+ return routes
def notify(self, rid, rrev, change): # pragma: no cover
rt = self.get_notification_resource_type()
@@ -526,7 +157,7 @@ class QvarnAPI:
'resource_id': rid,
'resource_revision': rrev,
'resource_change': change,
- 'timestamp': self.get_current_timestamp(),
+ 'timestamp': qvarn.get_current_timestamp(),
}
for listener in self.find_listeners(rid, change):
obj['listener_id'] = listener['id']
@@ -550,309 +181,3 @@ class QvarnAPI:
if rid in obj.get('listen_on', []):
return True
return False
-
- def get_current_timestamp(self): # pragma: no cover
- t = time.time()
- tm = time.gmtime(t)
- ss = t - int(t)
- secs = '%f' % ss
- return time.strftime('%Y-%m-%dT%H:%M:%S', tm) + secs[1:]
-
- def get_post_callback(self, coll): # pragma: no cover
- def wrapper(content_type, body, **kwargs):
- if content_type != 'application/json':
- raise 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 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 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 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 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 IdMismatch(body['id'], obj_id)
-
- try:
- result_body = coll.put(body)
- except qvarn.WrongRevision as e:
- return 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 bad_request_response(str(e))
-
- self.notify(
- result_body['id'], result_body['revision'], 'updated')
- return ok_response(result_body)
- return wrapper
-
- def put_subpath_callback(self, coll, subpath): # pragma: no cover
- def wrapper(content_type, body, **kwargs):
- if content_type != 'application/json':
- raise NotJson(content_type)
-
- obj_id = kwargs['id']
- if 'revision' not in body:
- return bad_request_response('must have revision')
- revision = body.pop('revision')
-
- rt = coll.get_type()
- try:
- self._validator.validate_subresource(subpath, rt, body)
- except qvarn.ValidationError as e:
- qvarn.log.log('error', msg_text=str(e), body=body)
- return bad_request_response(str(e))
-
- try:
- result_body = coll.put_subresource(
- body, subpath=subpath, obj_id=obj_id, revision=revision)
- except qvarn.WrongRevision as e:
- return conflict_response(str(e))
- except qvarn.NoSuchResource as e:
- return no_such_resource_response(str(e))
-
- return ok_response(result_body)
- return wrapper
-
- def put_file_callback(self, objcoll, subpath): # pragma: no cover
- def wrapper(content_type, body, **kwargs):
- obj_id = kwargs['id']
-
- # FIXME: add header getting to apifw
- import bottle
- revision = bottle.request.get_header('Revision')
-
- obj = objcoll.get(obj_id)
- if obj['revision'] != revision:
- qvarn.log.log(
- 'error',
- msg_text='Client gave wrong revision',
- revision_from_client=revision,
- current_revision=obj['revision'])
- return conflict_response('Bad revision {}'.format(revision))
-
- sub_obj = objcoll.get_subresource(obj_id, subpath)
- sub_obj['content_type'] = content_type
- new_sub = objcoll.put_subresource(
- sub_obj, subpath=subpath, obj_id=obj_id, revision=revision)
-
- try:
- self._store.remove_blob(subpath=subpath, obj_id=obj_id)
- self._store.create_blob(body, subpath=subpath, obj_id=obj_id)
- except qvarn.NoSuchObject as e:
- return no_such_resource_response(str(e))
-
- headers = {
- 'Revision': new_sub['revision'],
- }
- return ok_response('', headers)
- 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 no_such_resource_response(str(e))
- return ok_response(obj)
- return wrapper
-
- def get_subpath_callback(self, coll, subpath): # pragma: no cover
- def wrapper(content_type, body, **kwargs):
- try:
- obj = coll.get_subresource(kwargs['id'], subpath)
- except qvarn.NoSuchResource as e:
- return no_such_resource_response(str(e))
- return ok_response(obj)
- return wrapper
-
- def get_file_callback(self, coll, subpath): # pragma: no cover
- def wrapper(content_type, body, **kwargs):
- obj_id = kwargs['id']
- try:
- obj = coll.get(obj_id)
- sub_obj = coll.get_subresource(obj_id, subpath)
- blob = self._store.get_blob(obj_id=obj_id, subpath=subpath)
- except (qvarn.NoSuchResource, qvarn.NoSuchObject) as e:
- return no_such_resource_response(str(e))
- headers = {
- 'Content-Type': sub_obj['content_type'],
- 'Revision': obj['revision'],
- }
- return ok_response(blob, headers)
- return wrapper
-
- def get_resource_list_callback(self, coll): # pragma: no cover
- def wrapper(content_type, body, **kwargs):
- body = coll.list()
- return 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 unknown_search_field_response(e)
- except qvarn.NeedSortOperator:
- return need_sort_response()
- except qvarn.SearchParserError as e:
- return search_parser_error_response(e)
- body = {
- 'resources': result,
- }
- return 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 ok_response({})
- return wrapper
-
-
-def response(status, body, headers): # pragma: no cover
- return apifw.Response(
- {
- 'status': status,
- 'body': body,
- 'headers': headers,
- }
- )
-
-
-def ok_response(body, headers=None): # pragma: no cover
- if headers is None:
- headers = {}
- if 'Content-Type' not in headers:
- headers.update({
- 'Content-Type': 'application/json',
- })
- return response(apifw.HTTP_OK, body, headers)
-
-
-def no_such_resource_response(msg): # pragma: no cover
- return response(apifw.HTTP_NOT_FOUND, msg, {})
-
-
-def created_response(body, location): # pragma: no cover
- headers = {
- 'Content-Type': 'application/json',
- 'Location': location,
- }
- return response(apifw.HTTP_CREATED, body, headers)
-
-
-def bad_request_response(body): # pragma: no cover
- headers = {
- 'Content-Type': 'text/plain',
- }
- return response(apifw.HTTP_BAD_REQUEST, body, headers)
-
-
-def need_sort_response(): # pragma: no cover
- headers = {
- 'Content-Type': 'application/json',
- }
- body = {
- 'message': 'LIMIT and OFFSET can only be used with together SORT.',
- 'error_code': 'LimitWithoutSortError',
- }
- return response(apifw.HTTP_BAD_REQUEST, body, headers)
-
-
-def search_parser_error_response(e): # pragma: no cover
- headers = {
- 'Content-Type': 'application/json',
- }
- body = {
- 'message': 'Could not parse search condition',
- 'error_code': 'BadSearchCondition',
- }
- return response(apifw.HTTP_BAD_REQUEST, body, headers)
-
-
-def unknown_search_field_response(e): # pragma: no cover
- headers = {
- 'Content-Type': 'application/json',
- }
- body = {
- 'field': e.field,
- 'message': 'Resource does not contain given field',
- 'error_code': 'FieldNotInResource',
- }
- return response(apifw.HTTP_BAD_REQUEST, body, headers)
-
-
-def conflict_response(body): # pragma: no cover
- headers = {
- 'Content-Type': 'text/plain',
- }
- return response(apifw.HTTP_CONFLICT, body, headers)
-
-
-class NoSuchResourceType(Exception): # pragma: no cover
-
- def __init__(self, path):
- super().__init__('No resource type for path {}'.format(path))
-
-
-class TooManyResourceTypes(Exception): # pragma: no cover
-
- def __init__(self, path):
- super().__init__('Too many resource types for path {}'.format(path))
-
-
-class TooManyResources(Exception): # pragma: no cover
-
- def __init__(self, resource_id):
- super().__init__('Too many resources with id {}'.format(resource_id))
-
-
-class NotJson(Exception): # pragma: no cover
-
- def __init__(self, ct):
- super().__init__('Was expecting application/json, not {}'.format(ct))
-
-
-class IdMismatch(Exception): # pragma: no cover
-
- def __init__(self, obj_id, id_from_path):
- super().__init__(
- 'Resource has id {} but path says {}'.format(obj_id, id_from_path))
diff --git a/qvarn/api_errors.py b/qvarn/api_errors.py
new file mode 100644
index 0000000..368ddc5
--- /dev/null
+++ b/qvarn/api_errors.py
@@ -0,0 +1,45 @@
+# 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 NoSuchResourceType(Exception): # pragma: no cover
+
+ def __init__(self, path):
+ super().__init__('No resource type for path {}'.format(path))
+
+
+class TooManyResourceTypes(Exception): # pragma: no cover
+
+ def __init__(self, path):
+ super().__init__('Too many resource types for path {}'.format(path))
+
+
+class TooManyResources(Exception): # pragma: no cover
+
+ def __init__(self, resource_id):
+ super().__init__('Too many resources with id {}'.format(resource_id))
+
+
+class NotJson(Exception): # pragma: no cover
+
+ def __init__(self, ct):
+ super().__init__('Was expecting application/json, not {}'.format(ct))
+
+
+class IdMismatch(Exception): # pragma: no cover
+
+ def __init__(self, obj_id, id_from_path):
+ super().__init__(
+ 'Resource has id {} but path says {}'.format(obj_id, id_from_path))
diff --git a/qvarn/api_tests.py b/qvarn/api_tests.py
index 29c0e6c..23e6143 100644
--- a/qvarn/api_tests.py
+++ b/qvarn/api_tests.py
@@ -14,6 +14,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import os
import unittest
@@ -26,17 +27,6 @@ class QvarnAPITests(unittest.TestCase):
api = qvarn.QvarnAPI()
self.assertNotEqual(api.find_missing_route('/version'), [])
- def test_version_returns_sensible_data(self):
- api = qvarn.QvarnAPI()
- r = api.version(None, None)
- v = r['body']
- self.assertTrue(isinstance(v, dict))
- self.assertTrue('api' in v)
- self.assertTrue('version' in v['api'])
- self.assertTrue('implementation' in v)
- self.assertTrue('name' in v['implementation'])
- self.assertTrue('version' in v['implementation'])
-
def test_returns_no_routes_for_unknown_resource_type(self):
store = qvarn.MemoryObjectStore()
api = qvarn.QvarnAPI()
@@ -73,6 +63,13 @@ class QvarnAPITests(unittest.TestCase):
api = qvarn.QvarnAPI()
api.set_object_store(store)
api.add_resource_type(rt)
+
+ dirname = os.path.dirname(qvarn.__file__)
+ dirname = os.path.join(dirname, '../resource_type')
+ resource_types = qvarn.load_resource_types(dirname)
+ for rt in resource_types:
+ api.add_resource_type(rt)
+
self.assertNotEqual(api.find_missing_route('/subjects'), [])
def test_get_resource_type_raises_error_for_unknown_path(self):
diff --git a/qvarn/file_router.py b/qvarn/file_router.py
new file mode 100644
index 0000000..b7e4507
--- /dev/null
+++ b/qvarn/file_router.py
@@ -0,0 +1,98 @@
+# 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 FileRouter(qvarn.Router):
+
+ def __init__(self):
+ super().__init__()
+ self._store = None
+ self._parent_coll = None
+ self._subpath = None
+
+ def set_subpath(self, subpath):
+ self._subpath = subpath
+
+ def set_parent_collection(self, parent_coll):
+ self._parent_coll = parent_coll
+
+ def set_object_store(self, store):
+ self._store = store
+
+ def get_routes(self):
+ rt = self._parent_coll.get_type()
+ file_path = '{}/<id>/{}'.format(rt.get_path(), self._subpath)
+ return [
+ {
+ 'method': 'GET',
+ 'path': file_path,
+ 'callback': self._get_file,
+ },
+ {
+ 'method': 'PUT',
+ 'path': file_path,
+ 'callback': self._put_file,
+ },
+ ]
+
+ def _get_file(self, *args, **kwargs):
+ obj_id = kwargs['id']
+ try:
+ obj = self._parent_coll.get(obj_id)
+ sub_obj = self._parent_coll.get_subresource(obj_id, self._subpath)
+ blob = self._store.get_blob(obj_id=obj_id, subpath=self._subpath)
+ except (qvarn.NoSuchResource, qvarn.NoSuchObject) as e:
+ return qvarn.no_such_resource_response(str(e))
+ headers = {
+ 'Content-Type': sub_obj['content_type'],
+ 'Revision': obj['revision'],
+ }
+ return qvarn.ok_response(blob, headers)
+
+ def _put_file(self, content_type, body, *args, **kwargs):
+ obj_id = kwargs['id']
+
+ # FIXME: add header getting to apifw
+ import bottle
+ revision = bottle.request.get_header('Revision')
+
+ obj = self._parent_coll.get(obj_id)
+ if obj['revision'] != revision:
+ qvarn.log.log(
+ 'error',
+ msg_text='Client gave wrong revision',
+ revision_from_client=revision,
+ current_revision=obj['revision'])
+ return qvarn.conflict_response(
+ 'Bad revision {}'.format(revision))
+
+ sub_obj = self._parent_coll.get_subresource(obj_id, self._subpath)
+ sub_obj['content_type'] = content_type
+ new_sub = self._parent_coll.put_subresource(
+ sub_obj, subpath=self._subpath, obj_id=obj_id, revision=revision)
+
+ try:
+ self._store.remove_blob(obj_id=obj_id, subpath=self._subpath)
+ self._store.create_blob(body, obj_id=obj_id, subpath=self._subpath)
+ except qvarn.NoSuchObject as e:
+ return qvarn.no_such_resource_response(str(e))
+
+ headers = {
+ 'Revision': new_sub['revision'],
+ }
+ return qvarn.ok_response('', headers)
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/qvarn/responses.py b/qvarn/responses.py
new file mode 100644
index 0000000..eea78d7
--- /dev/null
+++ b/qvarn/responses.py
@@ -0,0 +1,97 @@
+# 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 apifw
+
+
+def response(status, body, headers):
+ return apifw.Response(
+ {
+ 'status': status,
+ 'body': body,
+ 'headers': headers,
+ }
+ )
+
+
+def ok_response(body, headers=None):
+ if headers is None:
+ headers = {}
+ if 'Content-Type' not in headers:
+ headers.update({
+ 'Content-Type': 'application/json',
+ })
+ return response(apifw.HTTP_OK, body, headers)
+
+
+def no_such_resource_response(msg):
+ return response(apifw.HTTP_NOT_FOUND, msg, {})
+
+
+def created_response(body, location):
+ headers = {
+ 'Content-Type': 'application/json',
+ 'Location': location,
+ }
+ return response(apifw.HTTP_CREATED, body, headers)
+
+
+def bad_request_response(body):
+ headers = {
+ 'Content-Type': 'text/plain',
+ }
+ return response(apifw.HTTP_BAD_REQUEST, body, headers)
+
+
+def need_sort_response():
+ headers = {
+ 'Content-Type': 'application/json',
+ }
+ body = {
+ 'message': 'LIMIT and OFFSET can only be used with together SORT.',
+ 'error_code': 'LimitWithoutSortError',
+ }
+ return response(apifw.HTTP_BAD_REQUEST, body, headers)
+
+
+def search_parser_error_response(e):
+ headers = {
+ 'Content-Type': 'application/json',
+ }
+ body = {
+ 'message': 'Could not parse search condition',
+ 'error_code': 'BadSearchCondition',
+ }
+ return response(apifw.HTTP_BAD_REQUEST, body, headers)
+
+
+def unknown_search_field_response(e):
+ headers = {
+ 'Content-Type': 'application/json',
+ }
+ body = {
+ 'field': e.field,
+ 'message': 'Resource does not contain given field',
+ 'error_code': 'FieldNotInResource',
+ }
+ return response(apifw.HTTP_BAD_REQUEST, body, headers)
+
+
+def conflict_response(body):
+ headers = {
+ 'Content-Type': 'text/plain',
+ }
+ return response(apifw.HTTP_CONFLICT, body, headers)
diff --git a/qvarn/router.py b/qvarn/router.py
new file mode 100644
index 0000000..9f171b0
--- /dev/null
+++ b/qvarn/router.py
@@ -0,0 +1,23 @@
+# 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 Router:
+
+ def __init__(self):
+ pass
+
+ def get_routes(self):
+ raise NotImplementedError()
diff --git a/qvarn/subresource_router.py b/qvarn/subresource_router.py
new file mode 100644
index 0000000..343ee4a
--- /dev/null
+++ b/qvarn/subresource_router.py
@@ -0,0 +1,82 @@
+# 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 SubresourceRouter(qvarn.Router):
+
+ def __init__(self):
+ super().__init__()
+ self._parent_coll = None
+ self._subpath = None
+
+ def set_subpath(self, subpath):
+ self._subpath = subpath
+
+ def set_parent_collection(self, parent_coll):
+ self._parent_coll = parent_coll
+
+ def get_routes(self):
+ rt = self._parent_coll.get_type()
+ path = '{}/<id>/{}'.format(rt.get_path(), self._subpath)
+ return [
+ {
+ 'method': 'GET',
+ 'path': path,
+ 'callback': self._get_subresource,
+ },
+ {
+ 'method': 'PUT',
+ 'path': path,
+ 'callback': self._put_subresource,
+ },
+ ]
+
+ def _get_subresource(self, *args, **kwargs):
+ obj_id = kwargs['id']
+ try:
+ obj = self._parent_coll.get_subresource(obj_id, self._subpath)
+ except qvarn.NoSuchResource as e:
+ return qvarn.no_such_resource_response(str(e))
+ return qvarn.ok_response(obj)
+
+ def _put_subresource(self, content_type, body, *args, **kwargs):
+ if content_type != 'application/json':
+ raise qvarn.NotJson(content_type)
+
+ obj_id = kwargs['id']
+ if 'revision' not in body:
+ return qvarn.bad_request_response('must have revision')
+ revision = body.pop('revision')
+
+ rt = self._parent_coll.get_type()
+ validator = qvarn.Validator()
+ try:
+ validator.validate_subresource(self._subpath, rt, body)
+ 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._parent_coll.put_subresource(
+ body, subpath=self._subpath, obj_id=obj_id, revision=revision)
+ except qvarn.WrongRevision as e:
+ return qvarn.conflict_response(str(e))
+ except qvarn.NoSuchResource as e:
+ return qvarn.no_such_resource_response(str(e))
+
+ return qvarn.ok_response(result_body)
diff --git a/qvarn/timestamp.py b/qvarn/timestamp.py
new file mode 100644
index 0000000..355bee0
--- /dev/null
+++ b/qvarn/timestamp.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/>.
+
+
+import time
+
+
+def get_current_timestamp():
+ t = time.time()
+ tm = time.gmtime(t)
+ ss = t - int(t)
+ secs = '%f' % ss
+ return time.strftime('%Y-%m-%dT%H:%M:%S', tm) + secs[1:]
diff --git a/qvarn/version_router.py b/qvarn/version_router.py
new file mode 100644
index 0000000..b9f1056
--- /dev/null
+++ b/qvarn/version_router.py
@@ -0,0 +1,42 @@
+# 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 VersionRouter(qvarn.Router):
+
+ def get_routes(self):
+ return [
+ {
+ 'method': 'GET',
+ 'path': '/version',
+ 'callback': self._version,
+ 'needs-authorization': False,
+ },
+ ]
+
+ def _version(self, *args, **kwargs):
+ version = {
+ 'api': {
+ 'version': qvarn.__version__,
+ },
+ 'implementation': {
+ 'name': 'Qvarn',
+ 'version': qvarn.__version__,
+ },
+ }
+ return qvarn.ok_response(version)
diff --git a/resource_type/listeners.yaml b/resource_type/listeners.yaml
new file mode 100644
index 0000000..07c2e53
--- /dev/null
+++ b/resource_type/listeners.yaml
@@ -0,0 +1,11 @@
+type: listener
+path: /listeners
+versions:
+ - version: v0
+ prototype:
+ id: ""
+ type: ""
+ revision: ""
+ notify_of_new: false
+ listen_on_all: false
+ listen_on: [""]
diff --git a/resource_type/notifications.yaml b/resource_type/notifications.yaml
new file mode 100644
index 0000000..9660282
--- /dev/null
+++ b/resource_type/notifications.yaml
@@ -0,0 +1,13 @@
+type: notification
+path: /notifications
+versions:
+ - version: v0
+ prototype:
+ id: ""
+ type: ""
+ revision: ""
+ listener_id: ""
+ resource_id: ""
+ resource_revision: ""
+ resource_change: ""
+ timestamp: ""
diff --git a/without-tests b/without-tests
index 1cdc82c..a752d9d 100644
--- a/without-tests
+++ b/without-tests
@@ -1,7 +1,16 @@
setup.py
qvarn/__init__.py
+qvarn/api_errors.py
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
+qvarn/subresource_router.py
+qvarn/timestamp.py
qvarn/version.py
+qvarn/version_router.py
yarns/lib.py