Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksey Lim <alsroot@sugarlabs.org>2012-03-01 05:09:56 (GMT)
committer Aleksey Lim <alsroot@sugarlabs.org>2012-03-01 05:09:56 (GMT)
commit80452d74721f88ad35c8d9d7ad9f7a911fb156fe (patch)
treeb661f9d4852eebc92d853e3d8f833d2971ea1b01
parentfb19a86a3d3d8e53fc13c22dfb2af7bf7d8d48ed (diff)
Adapt rd code to recent ad changes
-rw-r--r--restful_document/__init__.py2
-rw-r--r--restful_document/document.py14
-rw-r--r--restful_document/env.py18
-rw-r--r--restful_document/metadata.py128
-rw-r--r--restful_document/router.py109
-rw-r--r--restful_document/server.py32
-rw-r--r--restful_document/user.py13
-rw-r--r--tests/__init__.py13
-rwxr-xr-xtests/units/document.py54
-rwxr-xr-xtests/units/user.py18
10 files changed, 210 insertions, 191 deletions
diff --git a/restful_document/__init__.py b/restful_document/__init__.py
index 4ceff8f..83e0199 100644
--- a/restful_document/__init__.py
+++ b/restful_document/__init__.py
@@ -15,7 +15,7 @@
from active_document import util
from restful_document.document import Document, restful_method
-from restful_document.metadata import Metadata, Method
from restful_document.router import Router
+from restful_document.server import Server
from restful_document.user import User
from restful_document.env import principal, request
diff --git a/restful_document/document.py b/restful_document/document.py
index d3cae55..497eeaf 100644
--- a/restful_document/document.py
+++ b/restful_document/document.py
@@ -19,7 +19,16 @@ import active_document as ad
enforce = ad.util.enforce
from restful_document import env
-from restful_document.metadata import restful_method
+
+
+def restful_method(**kwargs):
+
+ def decorate(func):
+ func.is_restful_method = True
+ func.restful_cls_kwargs = kwargs
+ return func
+
+ return decorate
class Document(ad.Document):
@@ -67,7 +76,8 @@ class Document(ad.Document):
if prop is None:
reply = []
for name, prop in self.metadata.items():
- if prop.is_trait:
+ if isinstance(prop, ad.BrowsableProperty) and \
+ prop.permissions & ad.ACCESS_READ:
reply.append(name)
return self.all_properties(reply)
elif isinstance(self.metadata[prop], ad.BlobProperty):
diff --git a/restful_document/env.py b/restful_document/env.py
index 8b9bec0..070ea8a 100644
--- a/restful_document/env.py
+++ b/restful_document/env.py
@@ -91,12 +91,6 @@ class HTTPError(Exception):
headers = {}
-class Forbidden(HTTPError):
-
- status = '403 Forbidden'
- headers = {}
-
-
class BadRequest(HTTPError):
status = '400 Bad Request'
@@ -109,6 +103,18 @@ class Unauthorized(HTTPError):
headers = {'WWW-Authenticate': 'Sugar'}
+class Forbidden(HTTPError):
+
+ status = '403 Forbidden'
+ headers = {}
+
+
+class NotFound(HTTPError):
+
+ status = '404 Not Found'
+ headers = {}
+
+
class Request(threading.local):
environ = None
diff --git a/restful_document/metadata.py b/restful_document/metadata.py
deleted file mode 100644
index 8cd9e47..0000000
--- a/restful_document/metadata.py
+++ /dev/null
@@ -1,128 +0,0 @@
-# Copyright (C) 2012, Aleksey Lim
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU 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 General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import types
-from gettext import gettext as _
-
-import active_document as ad
-enforce = ad.util.enforce
-util = ad.util
-
-from restful_document import env
-
-
-def restful_method(**kwargs):
-
- def decorate(func):
- func.is_restful_method = True
- func.restful_cls_kwargs = kwargs
- return func
-
- return decorate
-
-
-class Metadata(object):
-
- def __init__(self, classes):
- self._methods = [{}, {}, {}]
-
- for meth in _list_methods(classes):
- methods = self._methods[meth.scope].setdefault(meth.document, {})
- if meth.cmd:
- enforce(meth.cmd not in methods,
- _('Method "%s" already exists in "%s"'),
- meth.cmd, methods.get(meth.cmd))
- methods[meth.cmd] = meth
- else:
- enforce(meth.method not in methods,
- _('"%s" method already exists in "%s"'),
- meth.method, methods.get(meth.method))
- methods[meth.method] = meth
-
- def get_method(self):
- enforce(len(env.request.path) <= 3, env.BadRequest,
- _('Requested path consists of more than three parts'))
- if len(env.request.path) == 3:
- env.request.query['prop'] = env.request.path.pop()
-
- scope = len(env.request.path)
- if scope == 0:
- document = None
- else:
- document = env.request.path[0]
- enforce(document in self._methods[scope], env.BadRequest,
- _('Unknown document type, "%s"'), document)
-
- if 'cmd' in env.request.query:
- method_name = env.request.query.pop('cmd')
- else:
- method_name = env.request.method
-
- return self._methods[scope][document].get(method_name)
-
-
-class Method(object):
-
- def __init__(self, cls, scope, cb,
- method, cmd=None, mime_type='application/json'):
- self.cls = cls
- self.document = cls.__name__.lower() if scope else None
- self.scope = scope
- self.method = method
- self.cmd = cmd
- self.mime_type = mime_type
- self._cb = cb
-
- def __str__(self):
- return str(self._cb)
-
- def __call__(self):
- try:
- result = self._call()
- except TypeError:
- util.exception()
- raise env.BadRequest(_('Inappropriate arguments'))
- env.responce.setdefault('Content-Type', self.mime_type)
- return result
-
- def _call(self):
- return self._cb(**env.request.query)
-
-
-class _ObjectMethod(Method):
-
- def _call(self):
- guid = env.request.path[1]
- doc = self.cls(guid)
- return self._cb(doc, **env.request.query)
-
-
-def _list_methods(classes):
- for cls in classes:
- for attr in [getattr(cls, i) for i in dir(cls)]:
- if not hasattr(attr, 'is_restful_method'):
- continue
- method_cls = Method
- if isinstance(attr, types.FunctionType):
- slot = 0
- elif isinstance(attr, types.MethodType):
- if attr.im_self is not None:
- slot = 1
- else:
- method_cls = _ObjectMethod
- slot = 2
- else:
- raise RuntimeError(_('Incorrect RESTful method for %r') % attr)
- yield method_cls(cls, slot, attr, **attr.restful_cls_kwargs)
diff --git a/restful_document/router.py b/restful_document/router.py
index a9cebe5..c143a9b 100644
--- a/restful_document/router.py
+++ b/restful_document/router.py
@@ -24,7 +24,6 @@ enforce = ad.util.enforce
util = ad.util
from restful_document import env
-from restful_document.metadata import Metadata
_logger = logging.getLogger('rd.router')
@@ -32,9 +31,8 @@ _logger = logging.getLogger('rd.router')
class Router(object):
- def __init__(self, classes):
- ad.init(classes)
- self.metadata = Metadata(classes)
+ def __init__(self, node):
+ self._metadata = _Metadata(node.documents)
if 'SSH_ASKPASS' in os.environ:
# Otherwise ssh-keygen will popup auth dialogs on registeration
@@ -48,7 +46,7 @@ class Router(object):
env.request.url, env.request.content)
try:
- method = self.metadata.get_method()
+ method = self._metadata.get_method()
enforce(method is not None and \
method.method == env.request.method, env.BadRequest,
_('No way to handle the request'))
@@ -78,3 +76,104 @@ class Router(object):
if env.responce['Content-Type'] == 'application/json':
result = json.dumps(result)
yield result
+
+
+class _Metadata(object):
+
+ def __init__(self, classes):
+ self._methods = [{}, {}, {}]
+
+ for meth in _list_methods(classes):
+ methods = self._methods[meth.scope].setdefault(meth.document, {})
+ if meth.cmd:
+ enforce(meth.cmd not in methods,
+ _('Method "%s" already exists in "%s"'),
+ meth.cmd, methods.get(meth.cmd))
+ methods[meth.cmd] = meth
+ else:
+ enforce(meth.method not in methods,
+ _('"%s" method already exists in "%s"'),
+ meth.method, methods.get(meth.method))
+ methods[meth.method] = meth
+
+ def get_method(self):
+ enforce(len(env.request.path) <= 3, env.BadRequest,
+ _('Requested path consists of more than three parts'))
+ if len(env.request.path) == 3:
+ env.request.query['prop'] = env.request.path.pop()
+
+ scope = len(env.request.path)
+ if scope == 0:
+ document = None
+ else:
+ document = env.request.path[0]
+ enforce(document in self._methods[scope], env.BadRequest,
+ _('Unknown document type, "%s"'), document)
+
+ if 'cmd' in env.request.query:
+ method_name = env.request.query.pop('cmd')
+ else:
+ method_name = env.request.method
+
+ enforce(document in self._methods[scope], env.NotFound,
+ _('Not supported path'))
+ methods = self._methods[scope][document]
+ enforce(method_name in methods, env.NotFound,
+ _('Not supported method'))
+
+ return methods[method_name]
+
+
+class _Method(object):
+
+ def __init__(self, cls, scope, cb,
+ method, cmd=None, mime_type='application/json'):
+ self.cls = cls
+ self.document = cls.__name__.lower() if scope else None
+ self.scope = scope
+ self.method = method
+ self.cmd = cmd
+ self.mime_type = mime_type
+ self._cb = cb
+
+ def __str__(self):
+ return str(self._cb)
+
+ def __call__(self):
+ try:
+ result = self._call()
+ except TypeError:
+ util.exception()
+ raise env.BadRequest(_('Inappropriate arguments'))
+ env.responce.setdefault('Content-Type', self.mime_type)
+ return result
+
+ def _call(self):
+ return self._cb(**env.request.query)
+
+
+class _ObjectMethod(_Method):
+
+ def _call(self):
+ guid = env.request.path[1]
+ doc = self.cls(guid)
+ return self._cb(doc, **env.request.query)
+
+
+def _list_methods(classes):
+ for cls in classes:
+ for attr in [getattr(cls, i) for i in dir(cls)]:
+ if not hasattr(attr, 'is_restful_method'):
+ continue
+ method_cls = _Method
+ if isinstance(attr, types.FunctionType):
+ slot = 0
+ elif isinstance(attr, types.MethodType):
+ if attr.im_self is not None:
+ slot = 1
+ else:
+ method_cls = _ObjectMethod
+ slot = 2
+ else:
+ raise RuntimeError(_('Incorrect RESTful method for %r') % attr)
+ yield method_cls(cls, slot, attr, **attr.restful_cls_kwargs)
diff --git a/restful_document/server.py b/restful_document/server.py
index ec69c50..e1affdf 100644
--- a/restful_document/server.py
+++ b/restful_document/server.py
@@ -33,9 +33,22 @@ from restful_document import env, printf
class Server(object):
+ """Serve active_document documents from WSGI server."""
def __init__(self, name, description, version, homepage):
- self._classes = []
+ """Configure server.
+
+ :param name:
+ id string to use for cases like configure file names
+ :param description:
+ server description string
+ :param version:
+ server version
+ :param homepage:
+ home page for the server project
+
+ """
+ self._node = None
self._name = name
self._description = description
self._version = version
@@ -65,8 +78,17 @@ class Server(object):
self._options, self._args = self._parser.parse_args()
util.Option.merge(self._options)
- def serve_forever(self, classes, **ssl_args):
- self._classes = classes
+ def serve_forever(self, node, **ssl_args):
+ """Start server.
+
+ :param node:
+ either `active_document.Node` or active_document.Master` object
+ with documents to serve
+ :param ssl_args:
+ arguments to pass to `ssl.wrap_socket` to enable SSL connections
+
+ """
+ self._node = node
if [i for i in ssl_args.values() if i is not None]:
self._ssl_args = ssl_args
@@ -165,7 +187,7 @@ class Server(object):
self._name, env.host.value, env.port.value)
httpd = WSGIServer((env.host.value, env.port.value),
- rd.Router(self._classes), **self._ssl_args)
+ rd.Router(self._node), **self._ssl_args)
def sigterm_cb(signum, frame):
logging.info(_('Got signal %s, will stop %s'), signum, self._name)
@@ -183,7 +205,7 @@ class Server(object):
httpd.serve_forever()
finally:
httpd.stop()
- ad.close()
+ self._node.close()
def _check_for_instance(self):
pid = None
diff --git a/restful_document/user.py b/restful_document/user.py
index 2fcbd27..6832e65 100644
--- a/restful_document/user.py
+++ b/restful_document/user.py
@@ -24,8 +24,7 @@ enforce = ad.util.enforce
util = ad.util
from restful_document import env
-from restful_document.metadata import restful_method
-from restful_document.document import Document
+from restful_document.document import Document, restful_method
_logger = logging.getLogger('sugar_stats')
@@ -60,15 +59,15 @@ class User(Document):
enforce('pubkey' in props,
_('Property "pubkey" is required for user registeration'))
guid, props['pubkey'] = _load_pubkey(props['pubkey'].strip())
- doc = cls(raw=['guid'], **props)
- doc['guid'] = guid
+ doc = cls(**props)
+ doc.set('guid', guid, raw=True)
doc.post()
return {'guid': guid}
@classmethod
def verify(cls, guid, signature):
try:
- pubkey = cls(guid, raw=['pubkey'])['pubkey']
+ pubkey = cls(guid).get('pubkey', raw=True)
except ad.NotFound:
raise env.Unauthorized(_('Principal user does not exist'))
if env.trust_users.value:
@@ -80,10 +79,10 @@ class User(Document):
env.Forbidden, _('Wrong principal credentials'))
@classmethod
- def init(cls, final_cls=None):
+ def init(cls, index_class, final_class=None):
# `restful_document.User` is the final class
# for all possible user documents
- Document.init(User)
+ Document.init(index_class, final_class=User)
def _load_pubkey(pubkey):
diff --git a/tests/__init__.py b/tests/__init__.py
index eb848b0..eb78f41 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -54,12 +54,12 @@ class Test(unittest.TestCase):
_env.index_flush_timeout.value = 0
_env.index_flush_threshold.value = 1
_env.find_limit.value = 1024
- _env.index_write_queue.value = 0
+ _env.index_write_queue.value = 10
_env.LAYOUT_VERSION = 1
ad.data_root.value = tmpdir + '/db'
ad.index_flush_timeout.value = 0
- ad.index_flush_threshold.value = 0
+ ad.index_flush_threshold.value = 1
self.httpd_seqno = 0
@@ -118,17 +118,20 @@ class Test(unittest.TestCase):
from gevent.wsgi import WSGIServer
import restful_document as rd
- httpd = WSGIServer(('localhost', port), rd.Router(classes))
+ node = ad.Master(classes)
+ httpd = WSGIServer(('localhost', port), rd.Router(node))
+
try:
httpd.serve_forever()
finally:
httpd.stop()
- ad.close()
+ node.close()
def httpdown(self, port):
pid = Test.httpd_pids[port]
del Test.httpd_pids[port]
- os.kill(pid, signal.SIGTERM)
+ os.kill(pid, signal.SIGINT)
+ sys.stdout.flush()
os.waitpid(pid, 0)
diff --git a/tests/units/document.py b/tests/units/document.py
index 912bb8c..83fca1d 100755
--- a/tests/units/document.py
+++ b/tests/units/document.py
@@ -15,7 +15,7 @@ class Vote(ad.AggregatorProperty):
@property
def value(self):
- return -1
+ return 'vote'
class Document(rd.Document):
@@ -59,9 +59,9 @@ class DocumentTest(tests.Test):
del reply['mtime']
self.assertEqual(
{'stored': 'stored',
- 'vote': '0',
+ 'vote': False,
'term': 'term',
- 'counter': '0',
+ 'counter': 0,
'guid': guid_1,
},
reply)
@@ -78,35 +78,39 @@ class DocumentTest(tests.Test):
del reply['mtime']
self.assertEqual(
{'stored': 'stored2',
- 'vote': '0',
+ 'vote': False,
'term': 'term2',
- 'counter': '0',
+ 'counter': 0,
'guid': guid_2,
},
reply)
+ reply = json.loads(rest.get('/document', reply='guid,stored,term,vote,counter').body_string())
+ self.assertEqual(2, reply['total'])
self.assertEqual(
- {'total': 2,
- 'documents': sorted([
- {'guid': guid_1, 'stored': 'stored', 'term': 'term', 'vote': '0', 'counter': '0'},
- {'guid': guid_2, 'stored': 'stored2', 'term': 'term2', 'vote': '0', 'counter': '0'},
- ])},
- json.loads(rest.get('/document', reply='guid,stored,term,vote,counter').body_string()))
+ sorted([
+ {'guid': guid_1, 'stored': 'stored', 'term': 'term', 'vote': False, 'counter': 0},
+ {'guid': guid_2, 'stored': 'stored2', 'term': 'term2', 'vote': False, 'counter': 0},
+ ]),
+ sorted(reply['documents']))
rest.put('/document/' + guid_2, payload=json.dumps({
- 'vote': '1',
+ 'vote': True,
'stored': 'stored3',
'term': 'term3',
}))
+ # Let server process commit and change `counter` property
+ time.sleep(3)
+
reply = json.loads(rest.get('/document/' + guid_2).body_string())
del reply['ctime']
del reply['mtime']
self.assertEqual(
{'stored': 'stored3',
- 'vote': '1',
+ 'vote': True,
'term': 'term3',
- 'counter': '1',
+ 'counter': 1,
'guid': guid_2,
},
reply)
@@ -114,17 +118,20 @@ class DocumentTest(tests.Test):
self.assertEqual(
{'total': 2,
'documents': sorted([
- {'guid': guid_1, 'stored': 'stored', 'term': 'term', 'vote': '0', 'counter': '0'},
- {'guid': guid_2, 'stored': 'stored3', 'term': 'term3', 'vote': '1', 'counter': '1'},
+ {'guid': guid_1, 'stored': 'stored', 'term': 'term', 'vote': False, 'counter': 0},
+ {'guid': guid_2, 'stored': 'stored3', 'term': 'term3', 'vote': True, 'counter': 1},
])},
json.loads(rest.get('/document', reply='guid,stored,term,vote,counter').body_string()))
rest.delete('/document/' + guid_1)
+ # Let server process commit and change `counter` property
+ time.sleep(3)
+
self.assertEqual(
{'total': 1,
'documents': sorted([
- {'guid': guid_2, 'stored': 'stored3', 'term': 'term3', 'vote': '1', 'counter': '1'},
+ {'guid': guid_2, 'stored': 'stored3', 'term': 'term3', 'vote': True, 'counter': 1},
])},
json.loads(rest.get('/document', reply='guid,stored,term,vote,counter').body_string()))
@@ -144,14 +151,15 @@ class DocumentTest(tests.Test):
rest.delete('/document/' + guid_2)
+ # Let server process commit and change `counter` property
+ time.sleep(3)
+
self.assertEqual(
{'total': 0,
'documents': sorted([])},
json.loads(rest.get('/document', reply='guid,stored,term,vote,counter').body_string()))
def test_ServerCrash(self):
- ad.index_write_queue.value = 10
-
self.httpd(8000, [Document])
rest = Resource('http://localhost:8000')
@@ -173,8 +181,8 @@ class DocumentTest(tests.Test):
reply = json.loads(rest.get('/document', reply='guid,stored,term,vote').body_string())
self.assertEqual(2, reply['total'])
self.assertEqual(
- sorted([{'guid': guid_1, 'stored': 'stored', 'term': 'term', 'vote': '0'},
- {'guid': guid_2, 'stored': 'stored2', 'term': 'term2', 'vote': '1'},
+ sorted([{'guid': guid_1, 'stored': 'stored', 'term': 'term', 'vote': False},
+ {'guid': guid_2, 'stored': 'stored2', 'term': 'term2', 'vote': True},
]),
sorted(reply['documents']))
@@ -185,8 +193,8 @@ class DocumentTest(tests.Test):
reply = json.loads(rest.get('/document', reply='guid,stored,term,vote,counter').body_string())
self.assertEqual(2, reply['total'])
self.assertEqual(
- sorted([{'guid': guid_1, 'stored': 'stored', 'term': 'term', 'vote': '0', 'counter': '0'},
- {'guid': guid_2, 'stored': 'stored2', 'term': 'term2', 'vote': '1', 'counter': '1'},
+ sorted([{'guid': guid_1, 'stored': 'stored', 'term': 'term', 'vote': False, 'counter': 0},
+ {'guid': guid_2, 'stored': 'stored2', 'term': 'term2', 'vote': True, 'counter': 1},
]),
sorted(reply['documents']))
diff --git a/tests/units/user.py b/tests/units/user.py
index 21d2dea..d41d1d8 100755
--- a/tests/units/user.py
+++ b/tests/units/user.py
@@ -44,7 +44,6 @@ class UserTest(tests.Test):
self.assertEqual(UID_FOR_PUBKEY, reply['guid'])
def test_Authenticate(self):
- rd.User.init()
def set_headers(user):
key_path = util.TempFilePath(text=DSS_PRIVKEY)
@@ -53,12 +52,6 @@ class UserTest(tests.Test):
env.request.environ['HTTP_SUGAR_USER'] = user
env.request.environ['HTTP_SUGAR_USER_SIGNATURE'] = signature
- set_headers('foo')
- self.assertRaises(env.Unauthorized, lambda: env.principal.user)
-
- self.httpd(8000, [rd.User])
- rest = Resource('http://localhost:8000')
-
props = {
'nickname': 'foo',
'color': '',
@@ -66,10 +59,17 @@ class UserTest(tests.Test):
'machine_uuid': 'machine_uuid',
'pubkey': VALID_DSS_PUBKEY,
}
+ self.httpd(8888, [rd.User])
+ rest = Resource('http://localhost:8888')
rest.post('/user', payload=json.dumps(props))
+ self.httpdown(8888)
+
+ with ad.Master([rd.User]):
+ set_headers('foo')
+ self.assertRaises(env.Unauthorized, lambda: env.principal.user)
- set_headers(UID_FOR_PUBKEY)
- self.assertEqual(UID_FOR_PUBKEY, env.principal.user)
+ set_headers(UID_FOR_PUBKEY)
+ self.assertEqual(UID_FOR_PUBKEY, env.principal.user)
VALID_DSS_PUBKEY = """\