1. Alexis Metaireau
  2. antistress

Commits

Gael Pasgrimaud  committed d55f426

first step

  • Participants
  • Branches default

Comments (0)

Files changed (8)

File .hgignore

View file
+syntax: glob
+
+bin
+dist
+eggs
+parts
+var/
+develop-eggs
+docs/_build/
+cache/
+.installed.cfg
+*.egg-info
+*.swp
+*.pyc
+*.pyo
+*.log
+

File antistress/__init__.py

View file
+#

File antistress/cache.py

View file
+# -*- coding: utf-8 -*-
+from beaker.cache import CacheManager
+from webob import Request, Response, exc
+from fnmatch import fnmatch
+import re
+
+
+
+class Cache(object):
+
+    def __init__(self, app, paths, debug=False, **kwargs):
+        self.application = app
+        self.manager = CacheManager(**kwargs)
+        self.paths = paths
+        self.debug = debug
+
+    def cache_key(self, req):
+        """return the cache key. You may subclass it to take care of cookies or whatever
+        """
+        return req.path_info
+
+    def must_cache(self, req):
+        """return (namespace, cache_option) if the request should be cached
+        """
+        cache = False
+        path_info = req.path_info
+        for path, opts in self.paths:
+            if isinstance(path, tuple):
+                if path[1].match(path_info):
+                    return path[0], opts
+            elif fnmatch(path_info, path):
+                return (path, opts)
+
+    def update_response(self, req, resp, opts):
+        """update expire/cache headers
+        """
+        if 'expire' in opts and not resp.expires:
+            resp.cache_expires(opts.get('expire'))
+
+    def purge(self, req, key, namespace, opts):
+        cache = self.manager.get_cache(namespace, **opts)
+        values = []
+        try:
+            keys = cache.namespace.keys()
+        except Exception, e:
+            cache.clear()
+            values.append(namespace)
+        else:
+            if keys:
+                path_info = req.path_info
+                for k in keys:
+                    if k.startswith(path_info):
+                        del cache[k]
+                        values.append(k)
+            else:
+                cache.clear()
+                values.append(namespace)
+        req.environ['http_cache.purged'] = values
+        resp = Response()
+        resp.body = '\n'.join(sorted(values))
+        return resp
+
+    def cache(self, req, key, namespace, opts):
+        cache = self.manager.get_cache(namespace,  **opts)
+        try:
+            resp = cache.get(key)
+        except KeyError:
+            resp = None
+
+        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
+                dbtype = self.manager.kwargs.get('type', opts.get('type'))
+                if dbtype and dbtype not in ('memory',):
+                    value = resp.headers, resp.body
+                else:
+                    value = resp
+                try:
+                    cache.set_value(key, value)
+                except TypeError:
+                    pass
+            else:
+                return resp
+
+        if isinstance(resp, Response):
+            return resp.conditional_response_app
+        elif 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)
+
+    def __call__(self, environ, start_response):
+
+        def purge_from_app(*args):
+            req = Request(environ)
+            args = list(args)
+            args.insert(0, req.path_info)
+            values = []
+            for path in args:
+                req.path_info = path
+                must_cache = self.must_cache(req)
+                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'])
+            return values
+
+        environ['http_cache.purge'] = purge_from_app
+
+
+        if self.debug:
+            return self.application(environ, start_response)
+
+        req = Request(environ)
+        method = req.method
+
+        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':
+            return self.purge(req, key, *must_cache)(environ, start_response)
+
+        if method not in ['GET', 'HEAD']:
+            self.purge(req, key, *must_cache)
+            return self.application(environ, start_response)
+
+        return self.cache(req, key, *must_cache)(environ, start_response)
+
+def deserialize_path(line):
+    values = line.split()
+    path = values.pop(0)
+    if path.startswith('^/'):
+        path = (path, re.compile(path))
+    cache_opts = {}
+    for v in values:
+        k, v = v.split(':')
+        if k == 'expire':
+            v = int(v)
+        cache_opts[k] = v
+    return [path, cache_opts]
+
+def make_cache(app, global_conf, **local_conf):
+    paths = local_conf.pop('cache_paths')
+    paths = [deserialize_path(p.strip()) for p in paths.split('\n') if p.strip()]
+    return Cache(app, paths=paths, **local_conf)
+

File antistress/tests.py

View file
+# -*- coding: utf-8 -*-
+from webob import Request, Response, exc
+from webtest import TestApp
+from cache import Cache
+from datetime import datetime
+import tempfile
+import shutil
+import time
+import os
+
+def application(environ, start_response):
+    req = Request(environ)
+    resp = Response()
+    if req.path_info.startswith('/cache'):
+        resp.body = str(datetime.now())
+    elif req.method == 'PURGE':
+        raise ValueError(req.method)
+    elif req.path_info == '/purge':
+        values = req.environ['http_cache.purge']('/cache/')
+        body = 'method: %s\n' % req.method
+        body += 'purged: %s\n' % ', '.join(sorted(values))
+        resp.body = body
+    return resp(environ, start_response)
+
+data_dir = tempfile.mkdtemp(prefix='antistress')
+
+def with_app(**kwargs):
+    def wrapper(func):
+        def wrapped():
+            if 'type' in kwargs:
+                kwargs['data_dir'] = data_dir
+            cache = Cache(application, **kwargs)
+            app = TestApp(cache)
+            func(app, cache)
+        wrapped.func_name = func.func_name
+        return wrapped
+    return wrapper
+
+def _test_cache(app):
+    resp1 = app.get('/cache')
+    time.sleep(.2)
+    resp2 = app.get('/cache')
+    assert resp1.expires is not None
+    assert resp1.body == resp2.body
+
+    resp = app.get('/')
+    assert resp.expires is None
+
+    time.sleep(.8)
+    resp2 = app.get('/cache')
+    assert resp1.body != resp2.body, (resp1.body, resp2.body)
+
+@with_app(paths=[('/cache', dict(expire=1))])
+def test_cache_memory(app, cache):
+    _test_cache(app)
+
+@with_app(paths=[('/cache', dict(expire=1))], type='file')
+def test_cache_file(app, cache):
+    _test_cache(app)
+
+@with_app(paths=[('/cache', dict(expire=1))], type='dbm')
+def test_cache_dbm(app, cache):
+    _test_cache(app)
+
+@with_app(paths=[('/cache/*', dict(expire=100))])
+def test_purge_memory(app, cache):
+    for i in range(3):
+        app.get('/cache/%i' % i)
+    resp = app.get('/purge')
+    resp.mustcontain('purged: /cache/0, /cache/1, /cache/2')
+
+@with_app(paths=[('/cache/*', dict(expire=100))], type='file')
+def test_purge_file(app, cache):
+    for i in range(3):
+        app.get('/cache/%i' % i)
+    resp = app.get('/purge')
+    resp.mustcontain('purged: /cache/*')
+
+@with_app(paths=[('/cache/*', dict(expire=100))], type='dbm')
+def test_purge_dbm(app, cache):
+    for i in range(3):
+        app.get('/cache/%i' % i)
+    resp = app.get('/purge')
+    resp.mustcontain('purged: /cache/*')
+
+@with_app(paths=[('/cache*', dict(expire=100))])
+def test_purge_http(app, cache):
+    for i in range(3):
+        app.get('/cache/%i' % i)
+
+    resp = app._gen_request('PURGE', '/cache')
+    resp.mustcontain('/cache/0', '/cache/1', '/cache/2')
+
+

File bootstrap.py

View file
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+
+$Id$
+"""
+
+import os, shutil, sys, tempfile, urllib2
+from optparse import OptionParser
+
+tmpeggs = tempfile.mkdtemp()
+
+is_jython = sys.platform.startswith('java')
+
+# parsing arguments
+parser = OptionParser()
+parser.add_option("-v", "--version", dest="version",
+                          help="use a specific zc.buildout version")
+parser.add_option("-d", "--distribute",
+                   action="store_true", dest="distribute", default=False,
+                   help="Use Distribute rather than Setuptools.")
+
+parser.add_option("-c", None, action="store", dest="config_file",
+                   help=("Specify the path to the buildout configuration "
+                         "file to be used."))
+
+options, args = parser.parse_args()
+
+# if -c was provided, we push it back into args for buildout' main function
+if options.config_file is not None:
+    args += ['-c', options.config_file]
+
+if options.version is not None:
+    VERSION = '==%s' % options.version
+else:
+    VERSION = ''
+
+USE_DISTRIBUTE = options.distribute
+args = args + ['bootstrap']
+
+to_reload = False
+try:
+    import pkg_resources
+    if not hasattr(pkg_resources, '_distribute'):
+        to_reload = True
+        raise ImportError
+except ImportError:
+    ez = {}
+    if USE_DISTRIBUTE:
+        exec urllib2.urlopen('http://python-distribute.org/distribute_setup.py'
+                         ).read() in ez
+        ez['use_setuptools'](to_dir=tmpeggs, download_delay=0, no_fake=True)
+    else:
+        exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
+                             ).read() in ez
+        ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
+
+    if to_reload:
+        reload(pkg_resources)
+    else:
+        import pkg_resources
+
+if sys.platform == 'win32':
+    def quote(c):
+        if ' ' in c:
+            return '"%s"' % c # work around spawn lamosity on windows
+        else:
+            return c
+else:
+    def quote (c):
+        return c
+
+cmd = 'from setuptools.command.easy_install import main; main()'
+ws  = pkg_resources.working_set
+
+if USE_DISTRIBUTE:
+    requirement = 'distribute'
+else:
+    requirement = 'setuptools'
+
+if is_jython:
+    import subprocess
+
+    assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd',
+           quote(tmpeggs), 'zc.buildout' + VERSION],
+           env=dict(os.environ,
+               PYTHONPATH=
+               ws.find(pkg_resources.Requirement.parse(requirement)).location
+               ),
+           ).wait() == 0
+
+else:
+    assert os.spawnle(
+        os.P_WAIT, sys.executable, quote (sys.executable),
+        '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout' + VERSION,
+        dict(os.environ,
+            PYTHONPATH=
+            ws.find(pkg_resources.Requirement.parse(requirement)).location
+            ),
+        ) == 0
+
+ws.add_entry(tmpeggs)
+ws.require('zc.buildout' + VERSION)
+import zc.buildout.buildout
+zc.buildout.buildout.main(args)
+shutil.rmtree(tmpeggs)

File buildout.cfg

View file
+[buildout]
+newest = false
+extensions = gp.vcsdevelop
+#vcs-extend-develop =
+parts = eggs
+develop = .
+
+[eggs]
+recipe = zc.recipe.egg
+eggs =
+    WebTest
+    AntiStress
+    restkit
+    PasteScript
+    Sphinx
+    nose
+

File config.ini

View file
+[DEFAULT]
+cache_file = expire:3600 type:file 
+
+[server:main]
+use = egg:Paste#http
+port = 5000
+
+[pipeline:main]
+pipeline = cache app
+
+[filter:cache]
+use = egg:AntiStress
+data_dir = %(here)s/cache
+cache_paths = 
+    ^/(_.*|.*\.(css|js)) %(cache_file)s
+    /* expire:60 type:dbm
+
+[app:app]
+use = egg:restkit#host_proxy
+uri = http://www.gawel.org/

File setup.py

View file
+from setuptools import setup, find_packages
+import sys, os
+
+version = '0.1'
+
+setup(name='AntiStress',
+      version=version,
+      description="A WSGI Middleware",
+      long_description="""\
+""",
+      classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+      keywords='cache wsgi middleware',
+      author='Gael Pasgrimaud',
+      author_email='gael@gawel.org',
+      url='http://www.gawel.org/docs/AntiStress/index.html',
+      license='MIT',
+      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=[
+          # -*- Extra requirements: -*-
+          'beaker',
+          'webob',
+      ],
+      entry_points="""
+      # -*- Entry points: -*-
+      [paste.filter_app_factory]
+      main = antistress.cache:make_cache
+      """,
+      )