diff options
Diffstat (limited to 'sugar_network/toolkit')
-rw-r--r-- | sugar_network/toolkit/__init__.py | 154 | ||||
-rw-r--r-- | sugar_network/toolkit/coroutine.py | 109 | ||||
-rw-r--r-- | sugar_network/toolkit/http.py | 4 | ||||
-rw-r--r-- | sugar_network/toolkit/i18n.py | 134 | ||||
-rw-r--r-- | sugar_network/toolkit/languages.py.in | 16 | ||||
-rw-r--r-- | sugar_network/toolkit/router.py | 351 |
6 files changed, 459 insertions, 309 deletions
diff --git a/sugar_network/toolkit/__init__.py b/sugar_network/toolkit/__init__.py index a32d87f..4088e07 100644 --- a/sugar_network/toolkit/__init__.py +++ b/sugar_network/toolkit/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2013 Aleksey Lim +# Copyright (C) 2011-2014 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 @@ -114,84 +114,12 @@ def exception(*args): logger.debug('\n'.join(tb)) -def default_lang(): - """Default language to fallback for localized strings. - - :returns: - string in format of HTTP's Accept-Language - - """ - return default_langs()[0] - - -def default_langs(): - """Default languages list, i.e., including all secondory languages. - - :returns: - list of strings in format of HTTP's Accept-Language - - """ - global _default_langs - - if _default_langs is None: - locales = os.environ.get('LANGUAGE') - if locales: - locales = [i for i in locales.split(':') if i.strip()] - else: - from locale import getdefaultlocale - locales = [getdefaultlocale()[0]] - if not locales: - _default_langs = ['en'] - else: - _default_langs = [] - for locale in locales: - lang = locale.strip().split('.')[0].lower() - if lang == 'c': - lang = 'en' - elif '_' in lang: - lang, region = lang.split('_') - if lang != region: - lang = '-'.join([lang, region]) - _default_langs.append(lang) - _logger.info('Default languages are %r', _default_langs) - - return _default_langs - - -def gettext(value, accept_language=None): - if not value: - return '' - if not isinstance(value, dict): - return value - - if accept_language is None: - accept_language = [default_lang()] - elif isinstance(accept_language, basestring): - accept_language = [accept_language] - accept_language.append('en') - - stripped_value = None - for lang in accept_language: - result = value.get(lang) - if result is not None: - return result - - prime_lang = lang.split('-')[0] - if prime_lang != lang: - result = value.get(prime_lang) - if result is not None: - return result - - if stripped_value is None: - stripped_value = {} - for k, v in value.items(): - if '-' in k: - stripped_value[k.split('-', 1)[0]] = v - result = stripped_value.get(prime_lang) - if result is not None: - return result - - return value[min(value.keys())] +def ascii(value): + if not isinstance(value, basestring): + return str(value) + if isinstance(value, unicode): + return value.encode('utf8') + return value def uuid(): @@ -484,12 +412,12 @@ def unique_filename(root, filename): class mkdtemp(str): - def __new__(cls, **kwargs): - if cachedir.value and 'dir' not in kwargs: - if not exists(cachedir.value): - os.makedirs(cachedir.value) + def __new__(cls, *args, **kwargs): + if 'dir' not in kwargs: kwargs['dir'] = cachedir.value - result = tempfile.mkdtemp(**kwargs) + if not exists(kwargs['dir']): + os.makedirs(kwargs['dir']) + result = tempfile.mkdtemp(*args, **kwargs) return str.__new__(cls, result) def __enter__(self): @@ -522,21 +450,60 @@ def svg_to_png(data, w, h): return result +class File(dict): + + AWAY = None + + def __init__(self, path=None, meta=None, digest=None): + self.path = path + self.digest = digest + dict.__init__(self, meta or {}) + self._stat = None + self._name = self.get('filename') + + @property + def size(self): + if self._stat is None: + self._stat = os.stat(self.path) + return self._stat.st_size + + @property + def mtime(self): + if self._stat is None: + self._stat = os.stat(self.path) + return int(self._stat.st_mtime) + + @property + def name(self): + if self._name is None: + self._name = self.get('name') or self.digest or 'blob' + mime_type = self.get('mime_type') + if mime_type: + import mimetypes + if not mimetypes.inited: + mimetypes.init() + self._name += mimetypes.guess_extension(mime_type) or '' + return self._name + + def __repr__(self): + return '<File path=%r digest=%r>' % (self.path, self.digest) + + def TemporaryFile(*args, **kwargs): - if cachedir.value and 'dir' not in kwargs: - if not exists(cachedir.value): - os.makedirs(cachedir.value) + if 'dir' not in kwargs: kwargs['dir'] = cachedir.value + if not exists(kwargs['dir']): + os.makedirs(kwargs['dir']) return tempfile.TemporaryFile(*args, **kwargs) class NamedTemporaryFile(object): def __init__(self, *args, **kwargs): - if cachedir.value and 'dir' not in kwargs: - if not exists(cachedir.value): - os.makedirs(cachedir.value) + if 'dir' not in kwargs: kwargs['dir'] = cachedir.value + if not exists(kwargs['dir']): + os.makedirs(kwargs['dir']) self._file = tempfile.NamedTemporaryFile(*args, **kwargs) def close(self): @@ -567,11 +534,9 @@ class Seqno(object): """ self._path = path self._value = 0 - if exists(path): with file(path) as f: self._value = int(f.read().strip()) - self._orig_value = self._value @property @@ -610,7 +575,7 @@ class Sequence(list): """List of sorted and non-overlapping ranges. List items are ranges, [`start`, `stop']. If `start` or `stop` - is `None`, it means the beginning or ending of the entire scale. + is `None`, it means the beginning or ending of the entire sequence. """ @@ -880,5 +845,4 @@ def _nb_read(stream): fcntl.fcntl(fd, fcntl.F_SETFL, orig_flags) -_default_lang = None -_default_langs = None +File.AWAY = File() diff --git a/sugar_network/toolkit/coroutine.py b/sugar_network/toolkit/coroutine.py index 170f445..1913bda 100644 --- a/sugar_network/toolkit/coroutine.py +++ b/sugar_network/toolkit/coroutine.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2013 Aleksey Lim +# Copyright (C) 2012-2014 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 @@ -23,6 +23,7 @@ import logging import gevent import gevent.pool import gevent.hub +from gevent.queue import Empty from sugar_network.toolkit import enforce @@ -36,27 +37,27 @@ sleep = gevent.sleep #: Wait for the spawned events to finish. joinall = gevent.joinall +#: Access to greenlet-local storage +this = None + gevent.hub.Hub.resolver_class = 'gevent.resolver_ares.Resolver' -_group = gevent.pool.Group() +_all_jobs = None _logger = logging.getLogger('coroutine') _wsgi_logger = logging.getLogger('wsgi') def spawn(*args, **kwargs): - return _group.spawn(*args, **kwargs) + return _all_jobs.spawn(*args, **kwargs) def spawn_later(seconds, *args, **kwargs): - job = _group.greenlet_class(*args, **kwargs) - job.start_later(seconds) - _group.add(job) - return job + return _all_jobs.spawn_later(*args, **kwargs) def shutdown(): - _group.kill() - return _group.join() + _all_jobs.kill() + return _all_jobs.join() def reset_resolver(): @@ -168,10 +169,6 @@ class ThreadResult(object): return self._value -class Empty(Exception): - pass - - class AsyncQueue(object): def __init__(self): @@ -216,30 +213,30 @@ class AsyncQueue(object): self._queue.put(*args, **kwargs) def _get(self): - from Queue import Empty as empty - try: - return self._queue.get_nowait() - except empty: - raise Empty() + return self._queue.get_nowait() class Pool(gevent.pool.Pool): def spawn(self, *args, **kwargs): - job = gevent.pool.Pool.spawn(self, *args, **kwargs) - _group.add(job) + job = self.greenlet_class(*args, **kwargs) + job.local = _Local() + if self is not _all_jobs: + _all_jobs.add(job) + self.start(job) return job def spawn_later(self, seconds, *args, **kwargs): job = self.greenlet_class(*args, **kwargs) + job.local = _Local() + if self is not _all_jobs: + _all_jobs.add(job) job.start_later(seconds) self.add(job) - _group.add(job) return job # pylint: disable-msg=W0221 def kill(self, *args, **kwargs): - from gevent.queue import Empty try: gevent.pool.Pool.kill(self, *args, **kwargs) except Empty: @@ -253,6 +250,71 @@ class Pool(gevent.pool.Pool): self.kill() +class Spooler(object): + """One-producer many-consumers events delivery. + + The delivery process supports lossless events feeding with guaranty that + every consumer proccessed every event producer pushed. + + """ + + def __init__(self): + self._value = None + self._waiters = 0 + self._ready = Event() + self._notifying_done = Event() + self._notifying_done.set() + + @property + def waiters(self): + return self._waiters + + def wait(self): + self._notifying_done.wait() + self._waiters += 1 + try: + self._ready.wait() + value = self._value + finally: + self._waiters -= 1 + if self._waiters == 0: + self._ready.clear() + self._notifying_done.set() + return value + + def notify_all(self, value=None): + while not self._notifying_done.is_set(): + self._notifying_done.wait() + if not self._waiters: + return + self._notifying_done.clear() + self._value = value + self._ready.set() + + +class _Local(object): + + def __init__(self): + self.attrs = set() + + if hasattr(gevent.getcurrent(), 'local'): + current = gevent.getcurrent().local + for attr in current.attrs: + self.attrs.add(attr) + setattr(self, attr, getattr(current, attr)) + + +class _LocalAccess(object): + + def __getattr__(self, name): + return getattr(gevent.getcurrent().local, name) + + def __setattr__(self, name, value): + local = gevent.getcurrent().local + local.attrs.add(name) + return setattr(local, name, value) + + class _Child(object): def __init__(self, pid): @@ -317,4 +379,7 @@ def _print_exception(context, klass, value, tb): _logger.error('\n'.join([error, context, tb_repr])) +_all_jobs = Pool() gevent.hub.get_hub().print_exception = _print_exception +gevent.getcurrent().local = gevent.get_hub().local = _Local() +this = _LocalAccess() diff --git a/sugar_network/toolkit/http.py b/sugar_network/toolkit/http.py index d1b2fe7..8d913ae 100644 --- a/sugar_network/toolkit/http.py +++ b/sugar_network/toolkit/http.py @@ -22,7 +22,7 @@ import logging from os.path import join, dirname, exists, expanduser, abspath from sugar_network import toolkit -from sugar_network.toolkit import enforce +from sugar_network.toolkit import i18n, enforce _REDIRECT_CODES = frozenset([301, 302, 303, 307, 308]) @@ -316,7 +316,7 @@ class Connection(object): self._session = Connection._Session() self._session.headers['accept-language'] = \ - ','.join(toolkit.default_langs()) + ','.join(i18n.default_langs()) for arg, value in self._session_args.items(): setattr(self._session, arg, value) self._session.stream = True diff --git a/sugar_network/toolkit/i18n.py b/sugar_network/toolkit/i18n.py new file mode 100644 index 0000000..86d3cae --- /dev/null +++ b/sugar_network/toolkit/i18n.py @@ -0,0 +1,134 @@ +# Copyright (C) 2014 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 os +import logging +from gettext import translation + + +# To let `encode()` working properly, avoid msgids gettext'izing +# but still populate .po files parsing the source code +_ = lambda x: x + +_logger = logging.getLogger('i18n') +_i18n = {} + + +def default_lang(): + """Default language to fallback for localized strings. + + :returns: + string in format of HTTP's Accept-Language + + """ + return default_langs()[0] + + +def default_langs(): + """Default languages list, i.e., including all secondory languages. + + :returns: + list of strings in format of HTTP's Accept-Language + + """ + global _default_langs + + if _default_langs is None: + locales = os.environ.get('LANGUAGE') + if locales: + locales = [i for i in locales.split(':') if i.strip()] + else: + from locale import getdefaultlocale + locales = [getdefaultlocale()[0]] + if not locales: + _default_langs = ['en'] + else: + _default_langs = [] + for locale in locales: + lang = locale.strip().split('.')[0].lower() + if lang == 'c': + lang = 'en' + elif '_' in lang: + lang, region = lang.split('_') + if lang != region: + lang = '-'.join([lang, region]) + _default_langs.append(lang) + _logger.info('Default languages are %r', _default_langs) + + return _default_langs + + +def decode(value, accept_language=None): + if not value: + return '' + if not isinstance(value, dict): + return value + + if accept_language is None: + accept_language = default_langs() + elif isinstance(accept_language, basestring): + accept_language = [accept_language] + accept_language.append('en') + + stripped_value = None + for lang in accept_language: + result = value.get(lang) + if result is not None: + return result + + prime_lang = lang.split('-')[0] + if prime_lang != lang: + result = value.get(prime_lang) + if result is not None: + return result + + if stripped_value is None: + stripped_value = {} + for k, v in value.items(): + if '-' in k: + stripped_value[k.split('-', 1)[0]] = v + result = stripped_value.get(prime_lang) + if result is not None: + return result + + return value[min(value.keys())] + + +def encode(msgid, *args, **kwargs): + if not _i18n: + from sugar_network.toolkit.languages import LANGUAGES + for lang in LANGUAGES: + _i18n[lang] = translation('sugar-network', languages=[lang]) + result = {} + + for lang, trans in _i18n.items(): + msgstr = trans.gettext(msgid) + if args: + msgargs = [] + for arg in args: + msgargs.append(decode(arg, lang)) + msgstr = msgstr % tuple(msgargs) + elif kwargs: + msgargs = {} + for key, value in kwargs.items(): + msgargs[key] = decode(value, lang) + msgstr = msgstr % msgargs + result[lang] = msgstr + + return result + + +_default_lang = None +_default_langs = None diff --git a/sugar_network/toolkit/languages.py.in b/sugar_network/toolkit/languages.py.in new file mode 100644 index 0000000..2542821 --- /dev/null +++ b/sugar_network/toolkit/languages.py.in @@ -0,0 +1,16 @@ +# Copyright (C) 2014 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/>. + +LANGUAGES = [%LANGUAGES%] diff --git a/sugar_network/toolkit/router.py b/sugar_network/toolkit/router.py index df57ff3..b37eee4 100644 --- a/sugar_network/toolkit/router.py +++ b/sugar_network/toolkit/router.py @@ -20,16 +20,16 @@ import time import types import logging import calendar -import mimetypes from base64 import b64decode from bisect import bisect_left from urllib import urlencode from urlparse import parse_qsl, urlsplit from email.utils import parsedate, formatdate -from os.path import isfile, split, splitext +from os.path import isfile from sugar_network import toolkit -from sugar_network.toolkit import http, coroutine, enforce +from sugar_network.toolkit.coroutine import this +from sugar_network.toolkit import i18n, http, coroutine, enforce _SIGNATURE_LIFETIME = 600 @@ -84,14 +84,15 @@ class ACL(object): DELETE = 1 << 5 INSERT = 1 << 6 REMOVE = 1 << 7 + REPLACE = 1 << 8 PUBLIC = CREATE | WRITE | READ | DELETE | INSERT | REMOVE - AUTH = 1 << 8 - AUTHOR = 1 << 9 - SUPERUSER = 1 << 10 + AUTH = 1 << 10 + AUTHOR = 1 << 11 + SUPERUSER = 1 << 12 - LOCAL = 1 << 11 - CALC = 1 << 12 + LOCAL = 1 << 13 + CALC = 1 << 14 NAMES = { CREATE: 'Create', @@ -100,6 +101,7 @@ class ACL(object): DELETE: 'Delete', INSERT: 'Insert', REMOVE: 'Remove', + REPLACE: 'Replace', } @@ -114,18 +116,16 @@ class Unauthorized(http.Unauthorized): class Request(dict): - principal = None - subcall = lambda *args: enforce(False) - def __init__(self, environ=None, method=None, path=None, cmd=None, content=None, content_stream=None, content_type=None, session=None, - **kwargs): + principal=None, **kwargs): dict.__init__(self) self.path = [] self.cmd = None self.environ = {} self.session = session or {} + self.principal = principal self._content = _NOT_SET self._dirty_query = False @@ -252,6 +252,11 @@ class Request(dict): return self.path[2] @property + def key(self): + if len(self.path) > 3: + return self.path[3] + + @property def static_prefix(self): http_host = self.environ.get('HTTP_HOST') if http_host: @@ -326,23 +331,6 @@ class Request(dict): else: existing_value = self[key] = [existing_value, value] - def call(self, response=None, **kwargs): - environ = {} - for key in ('HTTP_HOST', - 'HTTP_ACCEPT_LANGUAGE', - 'HTTP_ACCEPT_ENCODING', - 'HTTP_IF_MODIFIED_SINCE', - 'HTTP_AUTHORIZATION', - ): - if key in self.environ: - environ[key] = self.environ[key] - request = Request(environ, **kwargs) - if response is None: - response = Response() - request.principal = self.principal - request.subcall = self.subcall - return self.subcall(request, response) - def ensure_content(self): if self._content is not _NOT_SET: return @@ -400,9 +388,9 @@ class Response(dict): for key, value in dict.items(self): if type(value) in (list, tuple): for i in value: - result.append((_to_ascii(key), _to_ascii(i))) + result.append((toolkit.ascii(key), toolkit.ascii(i))) else: - result.append((_to_ascii(key), _to_ascii(value))) + result.append((toolkit.ascii(key), toolkit.ascii(value))) return result def __repr__(self): @@ -428,10 +416,6 @@ class Response(dict): dict.__delitem__(self, key) -class Blob(dict): - pass - - class Router(object): def __init__(self, routes_model, allow_spawn=False): @@ -441,8 +425,8 @@ class Router(object): self._invalid_origins = set() self._host = None self._routes = _Routes() - self._preroutes = set() - self._postroutes = set() + self._preroutes = [] + self._postroutes = [] processed = set() cls = type(routes_model) @@ -452,10 +436,14 @@ class Router(object): if name in processed: continue if hasattr(attr, 'is_preroute'): - self._preroutes.add(getattr(routes_model, name)) + route_ = getattr(routes_model, name) + if route_ not in self._preroutes: + self._preroutes.append(route_) continue elif hasattr(attr, 'is_postroute'): - self._postroutes.add(getattr(routes_model, name)) + route_ = getattr(routes_model, name) + if route_ not in self._postroutes: + self._postroutes.append(route_) continue elif not hasattr(attr, 'route'): continue @@ -481,44 +469,75 @@ class Router(object): processed.add(name) cls = cls.__base__ - def call(self, request, response): - request.subcall = self.call - result = self._call_route(request, response) - - if isinstance(result, Blob): - if 'url' in result: - raise http.Redirect(result['url']) - - path = result['blob'] - enforce(isfile(path), 'No such file') - - mtime = result.get('mtime') or int(os.stat(path).st_mtime) - if request.if_modified_since and mtime and \ - mtime <= request.if_modified_since: - raise http.NotModified() - response.last_modified = mtime - - response.content_type = result.get('mime_type') or \ - 'application/octet-stream' - - filename = result.get('filename') - if not filename: - filename = _filename(result.get('name') or - splitext(split(path)[-1])[0], - response.content_type) - response['Content-Disposition'] = \ - 'attachment; filename="%s"' % filename - - result = file(path, 'rb') - - if hasattr(result, 'read'): - if hasattr(result, 'fileno'): - response.content_length = os.fstat(result.fileno()).st_size - elif hasattr(result, 'seek'): - result.seek(0, 2) - response.content_length = result.tell() - result.seek(0) - result = _stream_reader(result) + this.call = self.call + + def call(self, request=None, response=None, environ=None, principal=None, + **kwargs): + if request is None: + if this.request is not None: + if not environ: + environ = {} + for key in ('HTTP_HOST', + 'HTTP_ACCEPT_LANGUAGE', + 'HTTP_ACCEPT_ENCODING', + 'HTTP_IF_MODIFIED_SINCE', + 'HTTP_AUTHORIZATION', + ): + if key in this.request.environ: + environ[key] = this.request.environ[key] + if not principal: + principal = this.request.principal + request = Request(environ=environ, principal=principal, **kwargs) + if response is None: + response = Response() + + route_ = self._resolve_route(request) + + for arg, cast in route_.arguments.items(): + value = request.get(arg) + if value is None: + if not hasattr(cast, '__call__'): + request[arg] = cast + continue + if not hasattr(cast, '__call__'): + cast = type(cast) + try: + request[arg] = _typecast(cast, value) + except Exception, error: + raise http.BadRequest( + 'Cannot typecast %r argument: %s' % (arg, error)) + kwargs = {} + for arg in route_.kwarg_names: + if arg == 'request': + kwargs[arg] = request + elif arg == 'response': + kwargs[arg] = response + elif arg not in kwargs: + kwargs[arg] = request.get(arg) + + for i in self._preroutes: + i(route_, request, response) + result = None + exception = None + try: + result = route_.callback(**kwargs) + if route_.mime_type == 'text/event-stream' and \ + self._allow_spawn and 'spawn' in request: + _logger.debug('Spawn event stream for %r', request) + request.ensure_content() + coroutine.spawn(self._event_stream, request, result) + result = None + except Exception, exception: + raise + else: + if not response.content_type: + if isinstance(result, toolkit.File): + response.content_type = result.get('mime_type') + if not response.content_type: + response.content_type = route_.mime_type + finally: + for i in self._postroutes: + i(request, response, result, exception) return result @@ -533,7 +552,7 @@ class Router(object): if 'callback' in request: js_callback = request.pop('callback') - result = None + content = None try: if 'HTTP_ORIGIN' in request.environ: enforce(self._assert_origin(request.environ), http.Forbidden, @@ -541,7 +560,34 @@ class Router(object): request.environ['HTTP_ORIGIN']) response['Access-Control-Allow-Origin'] = \ request.environ['HTTP_ORIGIN'] + result = self.call(request, response) + + if isinstance(result, toolkit.File): + if 'url' in result: + raise http.Redirect(result['url']) + enforce(isfile(result.path), 'No such file') + if request.if_modified_since and result.mtime and \ + result.mtime <= request.if_modified_since: + raise http.NotModified() + response.last_modified = result.mtime + response.content_type = result.get('mime_type') or \ + 'application/octet-stream' + response['Content-Disposition'] = \ + 'attachment; filename="%s"' % result.name + result = file(result.path, 'rb') + + if not hasattr(result, 'read'): + content = result + else: + if hasattr(result, 'fileno'): + response.content_length = os.fstat(result.fileno()).st_size + elif hasattr(result, 'seek'): + result.seek(0, 2) + response.content_length = result.tell() + result.seek(0) + content = _stream_reader(result) + except http.StatusPass, error: response.status = error.status if error.headers: @@ -557,100 +603,46 @@ class Router(object): if request.method == 'HEAD': response.meta['error'] = str(error) else: - result = {'error': str(error), - 'request': request.url, - } + content = {'error': str(error), 'request': request.url} response.content_type = 'application/json' - result_streamed = isinstance(result, types.GeneratorType) + streamed_content = isinstance(content, types.GeneratorType) if request.method == 'HEAD': - result_streamed = False - result = None + streamed_content = False + content = None elif js_callback: - if result_streamed: - result = ''.join(result) - result_streamed = False - result = '%s(%s);' % (js_callback, json.dumps(result)) - response.content_length = len(result) - elif not result_streamed: + if streamed_content: + content = ''.join(content) + streamed_content = False + content = '%s(%s);' % (js_callback, json.dumps(content)) + response.content_length = len(content) + elif not streamed_content: if response.content_type == 'application/json': - result = json.dumps(result) + content = json.dumps(content) if 'content-length' not in response: - response.content_length = len(result) if result else 0 + response.content_length = len(content) if content else 0 for key, value in response.meta.items(): - response.set('X-SN-%s' % _to_ascii(key), json.dumps(value)) + response.set('X-SN-%s' % toolkit.ascii(key), json.dumps(value)) - if request.method == 'HEAD' and result is not None: + if request.method == 'HEAD' and content is not None: _logger.warning('Content from HEAD response is ignored') - result = None + content = None - _logger.trace('%s call: request=%s response=%r result=%r', - self, request.environ, response, repr(result)[:256]) + _logger.trace('%s call: request=%s response=%r content=%r', + self, request.environ, response, repr(content)[:256]) start_response(response.status, response.items()) - if result_streamed: + if streamed_content: if response.content_type == 'text/event-stream': - for event in _event_stream(request, result): + for event in _event_stream(request, content): yield 'data: %s\n\n' % json.dumps(event) else: - for i in result: + for i in content: yield i - elif result is not None: - yield result - - def _call_route(self, request, response): - route_ = self._resolve_route(request) - request.routes = self._routes_model - - for arg, cast in route_.arguments.items(): - value = request.get(arg) - if value is None: - if not hasattr(cast, '__call__'): - request[arg] = cast - continue - if not hasattr(cast, '__call__'): - cast = type(cast) - try: - request[arg] = _typecast(cast, value) - except Exception, error: - raise http.BadRequest( - 'Cannot typecast %r argument: %s' % (arg, error)) - kwargs = {} - for arg in route_.kwarg_names: - if arg == 'request': - kwargs[arg] = request - elif arg == 'response': - kwargs[arg] = response - elif arg not in kwargs: - kwargs[arg] = request.get(arg) - - for i in self._preroutes: - i(route_, request, response) - result = None - exception = None - try: - result = route_.callback(**kwargs) - if route_.mime_type == 'text/event-stream' and \ - self._allow_spawn and 'spawn' in request: - _logger.debug('Spawn event stream for %r', request) - request.ensure_content() - coroutine.spawn(self._event_stream, request, result) - result = None - except Exception, exception: - raise - else: - if not response.content_type: - if isinstance(result, Blob): - response.content_type = result.get('mime_type') - if not response.content_type: - response.content_type = route_.mime_type - finally: - for i in self._postroutes: - i(request, response, result, exception) - - return result + elif content is not None: + yield content def _resolve_route(self, request): found_path = [False] @@ -695,9 +687,19 @@ class Router(object): commons['guid'] = request.guid if request.prop: commons['prop'] = request.prop - for event in _event_stream(request, stream): + try: + for event in _event_stream(request, stream): + event.update(commons) + this.localcast(event) + except Exception, error: + _logger.exception('Event stream %r failed', request) + event = {'event': 'failure', + 'exception': type(error).__name__, + 'error': str(error), + } + event.update(request.session) event.update(commons) - self._routes_model.broadcast(event) + this.localcast(event) def _assert_origin(self, environ): origin = environ['HTTP_ORIGIN'] @@ -747,22 +749,6 @@ class _ContentStream(object): return result -def _filename(names, mime_type): - if type(names) not in (list, tuple): - names = [names] - parts = [] - for name in names: - if isinstance(name, dict): - name = toolkit.gettext(name) - parts.append(''.join([i.capitalize() for i in name.split()])) - result = '-'.join(parts) - if mime_type: - if not mimetypes.inited: - mimetypes.init() - result += mimetypes.guess_extension(mime_type) or '' - return result.replace(os.sep, '') - - def _stream_reader(stream): try: while True: @@ -783,15 +769,8 @@ def _event_stream(request, stream): event[0].update(i) event = event[0] yield event - except Exception, error: - _logger.exception('Event stream %r failed', request) - event = {'event': 'failure', - 'exception': type(error).__name__, - 'error': str(error), - } - event.update(request.session) - yield event - _logger.debug('Event stream %r exited', request) + finally: + _logger.debug('Event stream %r exited', request) def _typecast(cast, value): @@ -817,7 +796,7 @@ def _typecast(cast, value): def _parse_accept_language(value): if not value: - return [toolkit.default_lang()] + return [i18n.default_lang()] langs = [] qualities = [] for chunk in value.split(','): @@ -836,14 +815,6 @@ def _parse_accept_language(value): return langs -def _to_ascii(value): - if not isinstance(value, basestring): - return str(value) - if isinstance(value, unicode): - return value.encode('utf8') - return value - - class _Routes(dict): def __init__(self, parent=None): |