diff options
author | Aleksey Lim <alsroot@sugarlabs.org> | 2012-01-14 17:15:43 (GMT) |
---|---|---|
committer | Aleksey Lim <alsroot@sugarlabs.org> | 2012-01-14 17:15:43 (GMT) |
commit | a04cc2ed342a0eaa59221332c94979a7ab54e9c3 (patch) | |
tree | 0660bc092afc6864574d4dc6016149b95c4ffa28 | |
parent | 4bfdc7335d0ebcdda04d43632b522ff4dca5bb24 (diff) |
Since rd is eventlet based, move request/responce objects to env module
-rw-r--r-- | restful_document/__init__.py | 2 | ||||
-rw-r--r-- | restful_document/document.py | 32 | ||||
-rw-r--r-- | restful_document/env.py | 127 | ||||
-rw-r--r-- | restful_document/metadata.py | 46 | ||||
-rw-r--r-- | restful_document/router.py | 66 | ||||
-rw-r--r-- | tests/__init__.py | 32 | ||||
-rwxr-xr-x | tests/units/document.py | 44 |
7 files changed, 214 insertions, 135 deletions
diff --git a/restful_document/__init__.py b/restful_document/__init__.py index 3a3770e..9c09d54 100644 --- a/restful_document/__init__.py +++ b/restful_document/__init__.py @@ -15,4 +15,4 @@ from restful_document.document import Document, restful_method from restful_document.metadata import Metadata, Method -from restful_document.router import Router, Responce +from restful_document.router import Router diff --git a/restful_document/document.py b/restful_document/document.py index 395b69e..792f80e 100644 --- a/restful_document/document.py +++ b/restful_document/document.py @@ -18,31 +18,22 @@ from gettext import gettext as _ import active_document as ad from restful_document import env +from restful_document.metadata import restful_method from restful_document.util import enforce -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): """All RESTful document classes need to inherit this one.""" @classmethod @restful_method(method='POST') - def _restful_create(cls, response): - doc = cls.create(response.request) + def _restful_create(cls): + doc = cls.create(env.request.content) return {'guid': doc.guid} @classmethod @restful_method(method='GET') - def _restful_find(cls, response, **kwargs): + def _restful_find(cls, **kwargs): offset = env.pop_int('offset', kwargs, None) limit = env.pop_int('limit', kwargs, None) query = env.pop_str('query', kwargs, None) @@ -55,23 +46,24 @@ class Document(ad.Document): 'documents': [i.all_properties(reply) for i in documents]} @restful_method(method='PUT') - def _restful_update(self, response, prop=None): + def _restful_update(self, prop=None): if prop is None: - self.update(self.guid, response.request) + self.update(self.guid, env.request.content) elif isinstance(self.metadata[prop], ad.BlobProperty): - self.set_blob(prop, response.rfile, response.rsize) + self.set_blob(prop, env.request.content_stream, + env.request.content_length) else: - self[prop] = response.request + self[prop] = env.request.content self.post() @restful_method(method='DELETE') - def _restful_delete(self, response, prop=None): + def _restful_delete(self, prop=None): enforce(prop is None, env.Forbidden, _('Properties cannot be deleted')) self.delete(self.guid) @restful_method(method='GET') - def _restful_get(self, response, prop=None): + def _restful_get(self, prop=None): if prop is None: reply = [] for name, prop in self.metadata.items(): @@ -79,7 +71,7 @@ class Document(ad.Document): reply.append(name) return self.all_properties(reply) elif isinstance(self.metadata[prop], ad.BlobProperty): - response.headers['Content-Type'] = self.metadata[prop].mime_type + env.responce['Content-Type'] = self.metadata[prop].mime_type return self.get_blob(prop) else: return self[prop] diff --git a/restful_document/env.py b/restful_document/env.py index 1d4adca..d0ba7b8 100644 --- a/restful_document/env.py +++ b/restful_document/env.py @@ -13,6 +13,9 @@ # 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 json +import threading +from urlparse import parse_qsl from gettext import gettext as _ from restful_document.util import enforce @@ -21,21 +24,6 @@ from restful_document.util import enforce _default = object() -class HTTPError(Exception): - - status = '500 Internal Server Error' - - -class Forbidden(HTTPError): - - status = '403 Forbidden' - - -class BadRequest(HTTPError): - - status = '400 Bad Request' - - def pop_str(name, kwargs, default=_default): if name in kwargs: return kwargs.pop(name) @@ -61,3 +49,112 @@ def pop_list(name, kwargs, default=_default): # pylint: disable-msg=E1103 value = value.split(',') return value + + +class HTTPError(Exception): + + status = '500 Internal Server Error' + headers = {} + + +class Forbidden(HTTPError): + + status = '403 Forbidden' + headers = {} + + +class BadRequest(HTTPError): + + status = '400 Bad Request' + headers = {} + + +class Unauthorized(HTTPError): + + status = '401 Unauthorized' + headers = {'WWW-Authenticate': 'Sugar'} + + +class Request(threading.local): + + environ = None + method = None + url = None + path = None + query = None + content = None + content_stream = None + content_length = None + + def set(self, environ): + self.environ = environ + + self.method = environ['REQUEST_METHOD'] + self.url = environ['PATH_INFO'] or '/' + self.path = \ + [i for i in environ['PATH_INFO'].strip('/').split('/') if i] + self.query = {} + self.content = None + self.content_stream = environ.get('wsgi.input') + self.content_length = 0 + + for attr, value in parse_qsl(environ.get('QUERY_STRING', '')): + self.query[str(attr)] = value + + if self.query: + self.url += '?' + environ.get('QUERY_STRING') + + content_length = environ.get('CONTENT_LENGTH') + if content_length: + self.content_length = int(content_length) + if environ.get('CONTENT_TYPE', '').lower() == 'application/json': + content = self.read() + if content: + self.content = json.loads(content) + + def read(self, size=None): + if size is None: + size = self.content_length + if not size: + return '' + result = self.content_stream.read(size) + self.content_length -= len(result) + return result + + def __getitem__(self, key): + return self.environ.get('HTTP_%s' % key) + + def __contains__(self, key): + return ('HTTP_%s' % key) in self.environ + + +class Responce(threading.local): + + status = None + _headers = None + + def set(self): + self.status = '200 OK' + self._headers = {} + + def update(self, values): + self._headers.update(values) + + def items(self): + return self._headers.items() + + def setdefault(self, key, value=None): + self._headers.setdefault(key, value) + + def __getitem__(self, key): + return self._headers.get(key) + + def __setitem__(self, key, value): + self._headers[key] = value + + def __contains__(self, key): + return key in self._headers + + +request = Request() +responce = Responce() diff --git a/restful_document/metadata.py b/restful_document/metadata.py index 5791aab..066d32c 100644 --- a/restful_document/metadata.py +++ b/restful_document/metadata.py @@ -20,6 +20,16 @@ from restful_document import util, env from restful_document.util import enforce +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): @@ -31,27 +41,31 @@ class Metadata(object): enforce(meth.cmd not in methods, _('Method %s already exists in %s'), meth.cmd, methods.get(meth.cmd)) - methods[meth.name] = meth + 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, response): - enforce(len(response.path) <= 3, env.BadRequest, + def get_method(self): + enforce(len(env.request.path) <= 3, env.BadRequest, _('Requested path consists of more than three parts')) - if len(response.path) == 3: - response.query['prop'] = response.path.pop() + if len(env.request.path) == 3: + env.request.query['prop'] = env.request.path.pop() - scope = len(response.path) + scope = len(env.request.path) if scope == 0: document = None else: - document = response.path[0] + document = env.request.path[0] enforce(document in self._methods[scope], env.BadRequest, _('Unknown document type, %s'), document) - method_name = response.query.get('cmd') or response.request_method + + 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) @@ -71,25 +85,25 @@ class Method(object): def __str__(self): return str(self._cb) - def __call__(self, response): + def __call__(self): try: - result = self._call(response) + result = self._call() except TypeError: util.exception() raise env.BadRequest(_('Inappropriate arguments')) - response.headers.setdefault('Content-Type', self.mime_type) + env.responce.setdefault('Content-Type', self.mime_type) return result - def _call(self, response): - return self._cb(response, **response.query) + def _call(self): + return self._cb(**env.request.query) class _ObjectMethod(Method): - def _call(self, response): - guid = response.path[1] + def _call(self): + guid = env.request.path[1] doc = self.cls(guid) - return self._cb(doc, response, **response.query) + return self._cb(doc, **env.request.query) def _list_methods(classes): diff --git a/restful_document/router.py b/restful_document/router.py index e361736..f1cd360 100644 --- a/restful_document/router.py +++ b/restful_document/router.py @@ -15,7 +15,6 @@ import json import types -import urlparse from gettext import gettext as _ import active_document as ad @@ -32,62 +31,35 @@ class Router(object): self.metadata = Metadata(classes) def __call__(self, environ, start_response): - response = Responce(environ) + env.request.set(environ) + env.responce.set() + try: - method = self.metadata.get_method(response) + method = self.metadata.get_method() enforce(method is not None and \ - method.method == response.request_method, env.BadRequest, + method.method == env.request.method, env.BadRequest, _('No way to handle the request')) - if environ.get('CONTENT_TYPE', '').lower() == 'application/json': - request = response.read() - if request: - response.request = json.loads(request) - result = method(response) + result = method() + except Exception, error: util.exception(_('Error while processing %s request'), - response.request_url) - if isinstance(error, env.HTTPError): - response.status = error.status + env.request.url) + if isinstance(error, ad.Unauthorized): + env.responce.status = env.Unauthorized.status + env.responce.update(env.Unauthorized.headers) + elif isinstance(error, env.HTTPError): + env.responce.status = error.status + env.responce.update(error.headers) else: - response.status = '500 Internal Server Error' - response.headers['Content-Type'] = 'application/json' - result = {'error': str(error), 'request': response.request_url} + env.responce.status = '500 Internal Server Error' + env.responce['Content-Type'] = 'application/json' + result = {'error': str(error), 'request': env.request.url} - start_response(response.status, response.headers.items()) + start_response(env.responce.status, env.responce.items()) if isinstance(result, types.GeneratorType): for i in result: yield i else: - if response.headers['Content-Type'] == 'application/json': + if env.responce['Content-Type'] == 'application/json': result = json.dumps(result) yield result - - -class Responce(object): - - def __init__(self, environ): - self.status = '200 OK' - self.headers = {} - self.query = dict(urlparse.parse_qsl(environ.get('QUERY_STRING', ''))) - self.path = \ - [i for i in environ['PATH_INFO'].strip('/').split('/') if i] - self.request_method = environ['REQUEST_METHOD'] - self.request = None - self.request_url = environ['PATH_INFO'] or '/' - if self.query: - self.request_url += '?' + environ.get('QUERY_STRING') - self.rfile = environ.get('wsgi.input') - self.rsize = 0 - - rsize = environ.get('CONTENT_LENGTH') - if rsize: - self.rsize = int(rsize) - - def read(self, size=None): - if size is None: - size = self.rsize - if not size: - return '' - result = self.rfile.read(size) - self.rsize -= len(result) - return result diff --git a/tests/__init__.py b/tests/__init__.py index 292590b..010c8c7 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -14,6 +14,7 @@ from os.path import dirname, join, exists, abspath import gobject import dbus.glib import dbus.mainloop.glib +import restkit import active_document as ad import restful_document as rd @@ -136,3 +137,34 @@ class Test(unittest.TestCase): del Test.httpd_pids[port] os.kill(pid, signal.SIGTERM) os.waitpid(pid, 0) + + +class Resource(restkit.Resource): + + def get(self, path=None, headers=None, **kwargs): + if headers is None: + headers = {} + if 'Content-Type' not in headers: + headers['Content-Type'] = 'application/json' + return restkit.Resource.get(self, path, headers=headers, **kwargs) + + def put(self, path=None, headers=None, **kwargs): + if headers is None: + headers = {} + if 'Content-Type' not in headers: + headers['Content-Type'] = 'application/json' + return restkit.Resource.put(self, path, headers=headers, **kwargs) + + def post(self, path=None, headers=None, **kwargs): + if headers is None: + headers = {} + if 'Content-Type' not in headers: + headers['Content-Type'] = 'application/json' + return restkit.Resource.post(self, path, headers=headers, **kwargs) + + def delete(self, path=None, headers=None, **kwargs): + if headers is None: + headers = {} + if 'Content-Type' not in headers: + headers['Content-Type'] = 'application/json' + return restkit.Resource.delete(self, path, headers=headers, **kwargs) diff --git a/tests/units/document.py b/tests/units/document.py index 6c6be72..9e6cc91 100755 --- a/tests/units/document.py +++ b/tests/units/document.py @@ -2,10 +2,10 @@ # sugar-lint: disable import json - -import restkit +import time from __init__ import tests +from tests import Resource import active_document as ad import restful_document as rd @@ -147,6 +147,8 @@ class DocumentTest(tests.Test): 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') @@ -165,14 +167,15 @@ class DocumentTest(tests.Test): )['guid'] - reply = json.loads(rest.get('/document', reply='guid,stored,term,vote,counter').body_string()) + 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', 'counter': '0'}, - {'guid': guid_2, 'stored': 'stored2', 'term': 'term2', 'vote': '1', 'counter': '1'}, + sorted([{'guid': guid_1, 'stored': 'stored', 'term': 'term', 'vote': '0'}, + {'guid': guid_2, 'stored': 'stored2', 'term': 'term2', 'vote': '1'}, ]), sorted(reply['documents'])) + time.sleep(1) self.httpdown(8000) self.httpd(8000, [Document]) @@ -185,36 +188,5 @@ class DocumentTest(tests.Test): sorted(reply['documents'])) -class Resource(restkit.Resource): - - def get(self, path=None, headers=None, **kwargs): - if headers is None: - headers = {} - if 'Content-Type' not in headers: - headers['Content-Type'] = 'application/json' - return restkit.Resource.get(self, path, headers=headers, **kwargs) - - def put(self, path=None, headers=None, **kwargs): - if headers is None: - headers = {} - if 'Content-Type' not in headers: - headers['Content-Type'] = 'application/json' - return restkit.Resource.put(self, path, headers=headers, **kwargs) - - def post(self, path=None, headers=None, **kwargs): - if headers is None: - headers = {} - if 'Content-Type' not in headers: - headers['Content-Type'] = 'application/json' - return restkit.Resource.post(self, path, headers=headers, **kwargs) - - def delete(self, path=None, headers=None, **kwargs): - if headers is None: - headers = {} - if 'Content-Type' not in headers: - headers['Content-Type'] = 'application/json' - return restkit.Resource.delete(self, path, headers=headers, **kwargs) - - if __name__ == '__main__': tests.main() |