Commits

Gael Pasgrimaud committed b491983

add utils

Comments (0)

Files changed (4)

antistress/cache.py

 # -*- coding: utf-8 -*-
 from beaker.cache import CacheManager
 from webob import Request, Response, exc
-import fnmatch
+from mimetypes import guess_type
+from utils import DEFAULT_SIZES, HAS_PIL
+from utils import deserialize_path, resize
+import tempfile
 import logging
-import re
+import os
 
 log = logging.getLogger(__name__)
 
+
 class CacheMiddleware(object):
     """A cache middleware.
     You can use ``environ['http_cache.purge'](*paths)`` in your application
     code to purge some cache values
     """
 
-    def __init__(self, app, paths, cache_qs=False, debug=False, **global_cache_options):
+    defaults = {'url': 'localhost:11211'}
+
+    def __init__(self, app, paths=[], images_paths=[],
+                       max_size=0, cache_qs=False, vary_cookies=[], debug=False,
+                       **global_cache_options):
         self.application = app
-        self.manager = CacheManager(**global_cache_options)
+
+        opts = self.defaults
+        opts.update(global_cache_options)
+
+        self.images_prefix = ''
+        if 'images_prefix' in opts:
+            self.images_prefix = opts.pop('images_prefix')
+
+        self.images_paths = images_paths
+        log.debug('images_paths: %s', images_paths)
+
+        self.images_dir = None
+        if 'images_dir' in opts:
+            if not HAS_PIL:
+                raise ImportError('You cant use images_dir whithout PIL')
+            images_dir = opts.pop('images_dir').rstrip(os.sep)
+            self.images_dir = images_dir
+            log.debug('images_prefix: %s', self.images_prefix)
+            log.debug('images_dir: %s', self.images_dir)
+            if opts.get('type') != 'memory' and 'data_dir' not in opts:
+                opts['data_dir'] = images_dir+'_cache'
+
+        self.manager = CacheManager(**opts)
+        log.debug('cache_options: %s', opts)
+
         self.paths = paths
         log.debug('cache_paths: %s', paths)
+
+        self.max_size = int(max_size)
+        log.debug('max_size: %r', self.max_size)
+
         self.cache_qs = cache_qs
+        log.debug('cache_qs: %r', self.cache_qs)
+
+        if isinstance(vary_cookies, basestring):
+            vary_cookies = vary_cookies.split()
+        self.vary_cookies = vary_cookies
+        log.debug('vary_cookies: %r', self.vary_cookies)
+
         self.debug = debug
+        log.debug('debug: %r', self.debug)
 
     def cache_key(self, req):
         """return the cache key. You may subclass it to take care of cookies or
         whatever
         """
         if self.cache_qs and req.query_string:
-            return '%s?%s' % (req.path_info, req.query_string)
+            key = '%s?%s' % (req.path_info, req.query_string)
+        elif req.query_string:
+            return None
         else:
-            return req.path_info
+            key = req.path_info
+        if self.vary_cookies:
+            cookies = req.str_cookies
+            for cookie in self.vary_cookies:
+                if cookie in cookies:
+                    key += '__%s=%s' % (cookie, cookies[cookie])
+        return key
 
     def must_cache(self, req):
-        """return (namespace, cache_option) if the request should be cached
+        """return (namespace, cache_options) if the request should be cached
         """
         cache = False
         path_info = req.path_info
-        for (namespace, match, _re), opts in self.paths:
+
+        if self.images_prefix and path_info.startswith(self.images_prefix):
+            paths = self.images_paths
+        else:
+            paths = self.paths
+
+        for (namespace, match, _re), opts in paths:
             result = _re.match(path_info) is not None
             if result is match:
                 return namespace, opts
         resp.body = '\n'.join(sorted(values))
         return resp
 
+    def get_image_response(self, req, size=None, **opts):
+        """Build an image response from ``images_dir``
+        """
+        resp = Response()
+        ctype = guess_type(req.path_info)
+        resp.content_type = ctype[0]
+        path = req.path_info[len(self.images_prefix):]
+        path = path.split('/', 2)[-1]
+        filename = os.path.join(self.images_dir, path)
+        dummy, ext = os.path.splitext(filename)
+        fd = tempfile.NamedTemporaryFile(prefix='aintistress-image', suffix=ext)
+        resize(filename, fd.name, size)
+        fd.seek(0)
+        resp.body = fd.read()
+        fd.close()
+        return resp
+
+    def get_response(self, req, size=None, **opts):
+        """Get application's response
+        """
+        if size is not None and req.path_info.startswith(self.images_prefix):
+            return self.get_image_response(req, size, **opts)
+        return req.get_response(self.application)
+
     def cache(self, req, key, namespace, opts):
         """Get a request from cache or set it if not already cached
         """
         if resp is None:
             # remove conditional headers to get the real content
             req.remove_conditional_headers()
-            resp = req.get_response(self.application)
-            status = resp.status_int
-            if status in (200,):
-                # update headers on 200 OK
-                self.update_response(req, resp, opts)
-            if status in (200, 301, 302, 404):
-                # cache response
-                value = resp.headers, resp.body
-                try:
-                    cache.set_value(key, value)
-                except TypeError, e:
-                    log.debug('set_value("%s-%s", **%s) -> %r', namespace, key, opts, e)
-                else:
-                    log.debug('set_value("%s-%s", **%s) -> OK', namespace, key, opts)
+            resp = self.get_response(req, **opts)
+            if self.max_size == 0 or resp.content_length < self.max_size:
+                status = resp.status_int
+                if status in (200,):
+                    # update headers on 200 OK
+                    self.update_response(req, resp, opts)
+                if status in (200, 301, 302, 404):
+                    # cache response
+                    value = resp.headers, resp.body
+                    try:
+                        cache.set_value(key, value)
+                    except TypeError, e:
+                        log.error('set_value("%s-%s", **%s) -> %r', namespace, key, opts, e)
+                    else:
+                        log.debug('set_value("%s-%s", **%s) -> OK', namespace, key, opts)
             else:
                 return resp
         else:
             log.debug('get(%s-%s, **%s) -> OK', namespace, key, opts)
 
-        if isinstance(resp, Response):
-            return resp.conditional_response_app
-        elif isinstance(resp, tuple):
+        if isinstance(resp, tuple):
             headers, body = resp
             resp = Response(headers=headers)
             resp.body = body
-            return resp.conditional_response_app
-        else:
-            raise ValueError('Invalid response %s' % resp)
+        return resp.conditional_response_app
 
     def __call__(self, environ, start_response):
 
                 if must_cache:
                     namespace, opts = must_cache
                     key = self.cache_key(req)
-                    self.purge(req, key, *must_cache)
-                    values.extend(req.environ['http_cache.purged'])
+                    if key:
+                        self.purge(req, key, *must_cache)
+                        values.extend(req.environ['http_cache.purged'])
             return values
 
         environ['http_cache.purge'] = purge_from_app
         req = Request(environ)
         method = req.method
 
-        key = self.cache_key(req)
         must_cache = self.must_cache(req)
         if not must_cache:
             return self.application(environ, start_response)
 
+        key = self.cache_key(req)
         if not key:
             return self.application(environ, start_response)
 
-        if method == 'PURGE':
+        if method == 'PURGE' and must_cache:
             return self.purge(req, key, *must_cache)(environ, start_response)
 
         if method not in ['GET', 'HEAD']:
 
         return self.cache(req, key, *must_cache)(environ, start_response)
 
-def deserialize_path(line):
-    """Deserialize a path config value:
-
-    .. sourcecode:: py
-
-        >>> deserialize_path('/cache/* expire:10') #doctest: +ELLIPSIS
-        [('/cache/*', True, <_sre.SRE_Pattern ...>), {'expire': 10}]
-
-        >>> deserialize_path('!/cache/* expire:100 type:memcached url:localhost:11211') #doctest: +ELLIPSIS
-        [('!/cache/*', False, <_sre.SRE_Pattern ...>), {'url': 'localhost:11211', 'expire': 100, 'type': 'memcached'}]
-
-    """
-    values = line.split()
-    namespace = path = values.pop(0)
-    if path.startswith('!'):
-        path = path[1:]
-        match = False
-    else:
-        match = True
-    if path.startswith('^/'):
-        path = (namespace, match, re.compile(path))
-    else:
-        path = (namespace, match, re.compile(fnmatch.translate(path)))
-    cache_opts = {}
-    for v in values:
-        k, v = v.split(':', 1)
-        if k == 'expire':
-            v = int(v)
-        cache_opts[k] = v
-    return [path, cache_opts]
-
 def make_cache(app, global_conf, **local_conf):
-    """Paste entry_point"""
+    """Paste entry_point for CacheMiddleware"""
     paths = local_conf.pop('cache_paths')
     paths = [deserialize_path(p.strip()) for p in paths.split('\n') if p.strip()]
-    return CacheMiddleware(app, paths=paths, **local_conf)
+    prefix = local_conf.get('images_prefix', '')
+    if 'images_dir' in local_conf:
+        if 'images_paths' in local_conf:
+            images_paths = local_conf.pop('images_paths')
+        else:
+            images_paths = DEFAULT_SIZES
+        images_paths = [deserialize_path(prefix+p.strip()) for p in images_paths.split('\n') if p.strip()]
+    return CacheMiddleware(app, paths=paths, images_paths=images_paths, **local_conf)
 
+def make_image(global_conf, images_dir=None, **local_conf):
+    """Paste entry_point for Image application"""
+    return CacheMiddleware(StaticURLParser(images_dir), **local_conf)
+

antistress/utils.py

+# -*- coding: utf-8 -*-
+import fnmatch
+import logging
+import re
+
+log = logging.getLogger(__name__)
+
+try:
+    import PIL.Image
+    HAS_PIL = True
+except:
+    log.warn('PIL is not available. AntiStress use it to resize images response')
+    HAS_PIL = False
+
+DEFAULT_SIZES = (
+    '/icon/* size:16',
+    '/tile/* size:50',
+    '/square/* size:100',
+    '/thumb/* size:200',
+    '/medium/* size:300',
+    '/large/* size:500',
+    '/xlarge/* size:800',
+    )
+
+def resize(src, dst, size, mode='r'):
+    """resize an image to size
+    """
+    image = PIL.Image.open(src, mode)
+    image.thumbnail(size, PIL.Image.ANTIALIAS)
+    image.save(dst)
+    return dst
+
+def deserialize_path(line):
+    """Deserialize a path config value:
+
+    .. sourcecode:: py
+
+        >>> deserialize_path('/cache/* expire:10') #doctest: +ELLIPSIS
+        [('/cache/*', True, <_sre.SRE_Pattern ...>), {'expire': 10}]
+
+        >>> deserialize_path('!/cache/* expire:100 type:memcached url:localhost:11211') #doctest: +ELLIPSIS
+        [('!/cache/*', False, <_sre.SRE_Pattern ...>), {'url': 'localhost:11211', 'expire': 100, 'type': 'memcached'}]
+
+    """
+    if not isinstance(line, basestring):
+        return line
+    values = line.split()
+    namespace = path = values.pop(0)
+    if path.startswith('!'):
+        path = path[1:]
+        match = False
+    else:
+        match = True
+    if path.startswith('^/'):
+        path = (namespace, match, re.compile(path))
+    else:
+        path = (namespace, match, re.compile(fnmatch.translate(path)))
+    cache_opts = {}
+    for v in values:
+        k, v = v.split(':', 1)
+        if k in ('expire', 'size'):
+            if not v.isdigit():
+                raise ValueError('%s must be integer. got %s' % (k, line))
+            v = int(v)
+        if k in ('size',):
+            v = (v, v)
+        cache_opts[k] = v
+    if 'size' in cache_opts and 'type' not in cache_opts:
+        cache_opts['type'] = 'file'
+    return [path, cache_opts]
+
+
 eggs =
     WebTest
     AntiStress
+    PIL
     python-memcached
     restkit
     PasteScript
       # -*- Entry points: -*-
       [paste.filter_app_factory]
       main = antistress.cache:make_cache
+      [paste.app_factory]
+      images = antistress.image:make_image
       """,
       )