diff options
author | Aleksey Lim <alsroot@sugarlabs.org> | 2012-03-01 05:09:56 (GMT) |
---|---|---|
committer | Aleksey Lim <alsroot@sugarlabs.org> | 2012-03-01 05:09:56 (GMT) |
commit | 80452d74721f88ad35c8d9d7ad9f7a911fb156fe (patch) | |
tree | b661f9d4852eebc92d853e3d8f833d2971ea1b01 | |
parent | fb19a86a3d3d8e53fc13c22dfb2af7bf7d8d48ed (diff) |
Adapt rd code to recent ad changes
-rw-r--r-- | restful_document/__init__.py | 2 | ||||
-rw-r--r-- | restful_document/document.py | 14 | ||||
-rw-r--r-- | restful_document/env.py | 18 | ||||
-rw-r--r-- | restful_document/metadata.py | 128 | ||||
-rw-r--r-- | restful_document/router.py | 109 | ||||
-rw-r--r-- | restful_document/server.py | 32 | ||||
-rw-r--r-- | restful_document/user.py | 13 | ||||
-rw-r--r-- | tests/__init__.py | 13 | ||||
-rwxr-xr-x | tests/units/document.py | 54 | ||||
-rwxr-xr-x | tests/units/user.py | 18 |
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 = """\ |