Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/sugar_network/toolkit
diff options
context:
space:
mode:
Diffstat (limited to 'sugar_network/toolkit')
-rw-r--r--sugar_network/toolkit/__init__.py154
-rw-r--r--sugar_network/toolkit/coroutine.py109
-rw-r--r--sugar_network/toolkit/http.py4
-rw-r--r--sugar_network/toolkit/i18n.py134
-rw-r--r--sugar_network/toolkit/languages.py.in16
-rw-r--r--sugar_network/toolkit/router.py351
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):