[Checkins] SVN: lovely.viewcache/trunk/ Initial import of the view cache package.

Jürgen Kartnaller juergen at kartnaller.at
Fri Feb 2 04:32:22 EST 2007


Log message for revision 72319:
  Initial import of the view cache package.
  

Changed:
  A   lovely.viewcache/trunk/
  A   lovely.viewcache/trunk/src/
  A   lovely.viewcache/trunk/src/lovely/
  A   lovely.viewcache/trunk/src/lovely/viewcache/
  A   lovely.viewcache/trunk/src/lovely/viewcache/HOWTOUSE.txt
  A   lovely.viewcache/trunk/src/lovely/viewcache/README.txt
  A   lovely.viewcache/trunk/src/lovely/viewcache/TODO.txt
  A   lovely.viewcache/trunk/src/lovely/viewcache/__init__.py
  A   lovely.viewcache/trunk/src/lovely/viewcache/browser/
  A   lovely.viewcache/trunk/src/lovely/viewcache/browser/__init__.py
  A   lovely.viewcache/trunk/src/lovely/viewcache/browser/configure.zcml
  A   lovely.viewcache/trunk/src/lovely/viewcache/configurator.py
  A   lovely.viewcache/trunk/src/lovely/viewcache/configure.zcml
  A   lovely.viewcache/trunk/src/lovely/viewcache/event.py
  A   lovely.viewcache/trunk/src/lovely/viewcache/ftesting.zcml
  A   lovely.viewcache/trunk/src/lovely/viewcache/ftests.py
  A   lovely.viewcache/trunk/src/lovely/viewcache/i18n.py
  A   lovely.viewcache/trunk/src/lovely/viewcache/interfaces.py
  A   lovely.viewcache/trunk/src/lovely/viewcache/lovely.viewcache-configure.zcml
  A   lovely.viewcache/trunk/src/lovely/viewcache/manager.py
  A   lovely.viewcache/trunk/src/lovely/viewcache/manager.txt
  A   lovely.viewcache/trunk/src/lovely/viewcache/mixin.py
  A   lovely.viewcache/trunk/src/lovely/viewcache/ram.py
  A   lovely.viewcache/trunk/src/lovely/viewcache/ram.txt
  A   lovely.viewcache/trunk/src/lovely/viewcache/stats/
  A   lovely.viewcache/trunk/src/lovely/viewcache/stats/README.txt
  A   lovely.viewcache/trunk/src/lovely/viewcache/stats/__init__.py
  A   lovely.viewcache/trunk/src/lovely/viewcache/stats/browser.py
  A   lovely.viewcache/trunk/src/lovely/viewcache/stats/configure.zcml
  A   lovely.viewcache/trunk/src/lovely/viewcache/stats/interfaces.py
  A   lovely.viewcache/trunk/src/lovely/viewcache/stats/stats.pt
  A   lovely.viewcache/trunk/src/lovely/viewcache/tests.py
  A   lovely.viewcache/trunk/src/lovely/viewcache/view.py
  A   lovely.viewcache/trunk/src/lovely/viewcache/zodb.py
  A   lovely.viewcache/trunk/src/lovely/viewcache/zodb.txt

-=-
Added: lovely.viewcache/trunk/src/lovely/viewcache/HOWTOUSE.txt
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/HOWTOUSE.txt	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/HOWTOUSE.txt	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,23 @@
+==================
+How To Use Caching
+==================
+
+
+Cache Utility
+-------------
+
+Caching requires a storage to store the cache entries. The view cache uses an
+unnamed utility implementing IViewCache. If no utility can be found it will
+not cache the results but it will still work.
+
+
+RAM Based Cache Storage
+~~~~~~~~~~~~~~~~~~~~~~~
+
+ZODB Based Cache Storage
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+Turning An Existing View Class Into A Cached Class
+--------------------------------------------------
+


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/HOWTOUSE.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/README.txt
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/README.txt	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/README.txt	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,263 @@
+==========
+View Cache
+==========
+
+
+Test Setup
+----------
+
+Because the view cache uses a utility to store the cached value we provide a
+cache utility here.
+
+  >>> from zope import component
+  >>> from lovely.viewcache.ram import ViewCache
+  >>> from lovely.viewcache.interfaces import IViewCache
+  >>> cache = ViewCache()
+  >>> component.provideUtility(cache, IViewCache)
+
+We need some content to use as context.
+
+  >>> from zope.app.container.contained import Contained
+  >>> from zope import interface
+  >>> class IContent(interface.Interface):
+  ...     pass
+  >>> class Content(Contained):
+  ...     interface.implements(IContent)
+  ...     def __init__(self, name):
+  ...         self.count = 0
+  ...         self.name = name
+  >>> content = Content(u'content 1')
+  >>> root[u'content'] = content
+
+A browser is also needed.
+
+  >>> from zope.publisher.browser import TestRequest
+  >>> request = TestRequest() 
+
+Then we need a view which we try to cache. The view we define here is a normal
+implementation of a view. The view renders a result which depends on a counter
+in it's context. The counter is incremented every time the view is rendered.
+This allows us to check if the result is coming from the cache.
+
+  >>> from zope.publisher.interfaces.browser import IBrowserView
+  >>> from zope.publisher.browser import BrowserView
+  >>> class View(BrowserView):
+  ...     def __call__(self, *args, **kwargs):
+  ...         self.context.count += 1
+  ...         return u'"%s" is rendered %s time(s)'% (
+  ...                           self.context.name, self.context.count)
+
+
+Cached View
+-----------
+
+For the caching of views we provide a special adapter factory for views.
+
+  >>> from lovely.viewcache.view import cachedView
+
+To make the view cached we create a cached view class from the original view
+class.
+
+  >>> CachedView = cachedView(View, dependencies = ('content',))
+
+Instead of registering the original view class we can now use the newly
+created view class.
+
+  >>> from zope.publisher.interfaces.browser import IDefaultBrowserLayer
+  >>> component.provideAdapter(CachedView,
+  ...                          (IContent, IDefaultBrowserLayer),
+  ...                          IBrowserView,
+  ...                          name='cachedView')
+
+If we now lookup our view we get an instance of the view which is proxied by
+the cache manager view.
+
+  >>> view = component.getMultiAdapter((content, request), name='cachedView')
+  >>> view.__name__ = 'cachedView'
+  >>> from lovely.viewcache.view import CachedViewMixin
+  >>> isinstance(view, CachedViewMixin)
+  True
+
+When we render the view by calling it we get the result.
+
+  >>> view()
+  u'"content 1" is rendered 1 time(s)'
+
+Rendering the view again will return the same result because the cached result
+is used.
+
+  >>> view()
+  u'"content 1" is rendered 1 time(s)'
+
+The cachingOn property allows the control of the caching of the view. If
+cachingOn returns False the view is not cached.
+
+  >>> view.cachingOn
+  True
+  >>> view.cachingOn = False
+  >>> view()
+  u'"content 1" is rendered 2 time(s)'
+
+If we switch back we get the old cached value back.
+
+  >>> view.cachingOn = True
+  >>> view()
+  u'"content 1" is rendered 1 time(s)'
+
+We invalidate the cache entry.
+
+  >>> cache = component.queryUtility(IViewCache)
+  >>> cache.invalidate(dependencies=['content'])
+
+And the view will be rendered and cached again.
+
+  >>> view()
+  u'"content 1" is rendered 3 time(s)'
+  >>> view()
+  u'"content 1" is rendered 3 time(s)'
+
+If we request a new view we get the already cached value.
+
+  >>> view = component.getMultiAdapter((content, request), name='cachedView')
+  >>> view.__name__ = 'cachedView'
+  >>> view()
+  u'"content 1" is rendered 3 time(s)'
+
+
+Views On Different Contexts
+---------------------------
+
+If the view is used in another context it creates a new cache entry.
+
+  >>> content2 = Content(u'content 2')
+  >>> root[u'content2'] = content2
+
+  >>> view2 = component.getMultiAdapter((content2, request), name='cachedView')
+  >>> view2()
+  u'"content 2" is rendered 1 time(s)'
+  >>> view2()
+  u'"content 2" is rendered 1 time(s)'
+
+
+Providing static dependencies
+-----------------------------
+
+A cached view provides the static dependencies via the 'staticCachingDeps'
+attribute. The static dependency can be set as a class member or can be
+provided when creating the cached view. It can not be cached during runtime.
+
+  >>> view.staticCachingDeps
+  ('content',)
+  >>> view.staticCachingDeps = ('not possible',)
+  Traceback (most recent call last):
+  ...
+  AttributeError: can't set attribute
+
+
+Providing dynamic dependencies
+------------------------------
+
+The view can provide dynamic dependencies via the 'dynamicCachingDeps'
+attribute.
+
+  >>> view.dynamicCachingDeps
+  ()
+  >>> view.dynamicCachingDeps = ('changeable',)
+  >>> view.dynamicCachingDeps
+  ('changeable',)
+
+
+Using 'minAge'
+--------------
+
+A view with a different name get's a different cache entry. It is also
+possible to provide a minAge for the cache entry.
+
+  >>> AnotherCachedView = cachedView(View,
+  ...                                dependencies = ('content',),
+  ...                                minAge=1)
+  >>> component.provideAdapter(AnotherCachedView,
+  ...                          (IContent, IDefaultBrowserLayer),
+  ...                          IBrowserView,
+  ...                          name='anotherCachedView')
+  >>> view = component.getMultiAdapter((content, request), name='anotherCachedView')
+  >>> view.__name__ = 'anotherCachedView'
+  >>> view()
+  u'"content 1" is rendered 4 time(s)'
+
+Because of the minimum age if we invalidate the the cache the new view is not
+removed from the cache.
+
+  >>> cache.invalidate(dependencies=['content'])
+  >>> view()
+  u'"content 1" is rendered 4 time(s)'
+
+
+Cached Viewlets
+---------------
+
+Caching for viewlets can be used the same as cached views are used.
+
+  >>> from lovely.viewcache.view import cachedViewlet
+
+  >>> from zope.viewlet.viewlet import ViewletBase
+  >>> class Viewlet(ViewletBase):
+  ...     def update(self):
+  ...         self.context.count += 1
+  ...     def render(self):
+  ...         return u'viewlet for context "%s" is rendered %s time(s)'% (
+  ...                           self.context.name, self.context.count)
+
+  >>> CachedViewlet = cachedViewlet(Viewlet, dependencies=('viewlet',))
+
+Now we can build a viewlet instance from the cached viewlet.
+
+  >>> content3 = Content(u'content 3')
+  >>> root[u'content3'] = content3
+  >>> viewlet = CachedViewlet(content3, request, None, None)
+  >>> from lovely.viewcache.view import CachedViewletMixin
+  >>> isinstance(viewlet, CachedViewletMixin)
+  True
+
+  >>> viewlet.update()
+  >>> viewlet.render()
+  u'viewlet for context "content 3" is rendered 1 time(s)'
+
+Because the viewlet is now cached update is not called again. Because the
+update method increments the count in the context we check for a change on the
+count.
+
+  >>> content3.count
+  1
+  >>> viewlet.update()
+  >>> content3.count
+  1
+
+Also rendering the viewlet again will return the cached value.
+
+  >>> viewlet.render()
+  u'viewlet for context "content 3" is rendered 1 time(s)'
+
+  >>> cache.invalidate(dependencies=['viewlet'])
+  >>> viewlet.update()
+  >>> viewlet.render()
+  u'viewlet for context "content 3" is rendered 2 time(s)'
+
+
+Subclassing Cached Views
+------------------------
+
+Subclassing a cached view is possible but if __call__ is used in a derived
+class caching is only done for the values from the base class.
+
+  >>> class DerivedView(CachedView):
+  ...     def __call__(self):
+  ...         return u'Derived was called'
+  >>> cachedDerivedView = cachedView(DerivedView)
+  >>> derived = cachedDerivedView(content, request)
+  >>> derived.__name__ = 'derived'
+  >>> derived()
+  u'Derived was called'
+  >>> derived()
+  u'Derived was called'
+


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/README.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/TODO.txt
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/TODO.txt	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/TODO.txt	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,19 @@
+=====
+TODOs
+=====
+
+- Time based invalidation, min-lifetime, max-lifetime
+
+- cache for views low prio
+
+- ftests (demo package)
+
+- statistics view for zmi
+
+- alternative cache implementations (e.g. inter instance cache,
+  webservice)
+
+- invalidate on multiple instances
+
+- zeo storage
+


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/TODO.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/__init__.py
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/__init__.py	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/__init__.py	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1 @@
+# package


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/browser/__init__.py
===================================================================


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/browser/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/browser/configure.zcml
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/browser/configure.zcml	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/browser/configure.zcml	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,21 @@
+<configure
+    xmlns="http://namespaces.zope.org/browser"
+    i18n_domain="lovely.mount">
+
+  	<addform
+  		schema="lovely.viewcache.interfaces.IZODBViewCache"
+  		content_factory="lovely.viewcache.zodb.ViewCache"
+  		label="Add ZODB ViewCache"
+  		name="addZODBViewCache.html"
+  		permission="zope.ManageContent"
+  		/>
+  	
+  	<editform
+  		schema="lovely.viewcache.interfaces.IZODBViewCache"
+  		label="Edit"
+  		name="editZODBViewCache.html"
+  		menu="zmi_views" title="Edit"
+  		permission="zope.ManageContent"
+  		/>
+
+</configure>


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/browser/configure.zcml
___________________________________________________________________
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/configurator.py
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/configurator.py	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/configurator.py	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,41 @@
+from zope import component
+from zope import event
+
+from zope.security.proxy import removeSecurityProxy
+from zope.app.component.interfaces import ISite
+from zope.lifecycleevent import ObjectCreatedEvent
+
+from z3c import configurator
+
+from lovely.viewcache.interfaces import IViewCache
+from lovely.viewcache.ram import ViewCache as RAMViewCache
+from lovely.viewcache.zodb import ViewCache as ZODBViewCache
+
+
+class RAMViewCacheConfigurator(configurator.ConfigurationPluginBase):
+    component.adapts(ISite)
+
+    def __call__(self, data):
+        sm = removeSecurityProxy(self.context.getSiteManager())
+        default = sm['default']
+
+        if 'view-cache-RAM' not in default:
+            util = RAMViewCache()
+            event.notify(ObjectCreatedEvent(util))
+            default['view-cache-RAM'] = util
+            sm.registerUtility(util, IViewCache)
+
+
+class ZODBViewCacheConfigurator(configurator.ConfigurationPluginBase):
+    component.adapts(ISite)
+
+    def __call__(self, data):
+        sm = removeSecurityProxy(self.context.getSiteManager())
+        default = sm['default']
+
+        if 'view-cache-ZODB' not in default:
+            util = ZODBViewCache()
+            event.notify(ObjectCreatedEvent(util))
+            default['view-cache-ZODB'] = util
+            sm.registerUtility(util, IViewCache)
+


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/configurator.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/configure.zcml
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/configure.zcml	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/configure.zcml	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,64 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    i18n_domain="lovely.viewcache">
+
+  <class
+      class=".ram.ViewCache">
+
+    <require 
+        permission="zope.ManageServices" 
+        interface="zope.app.cache.interfaces.ram.IRAMCache"
+        />
+    <require
+    	permission="zope.ManageServices"
+    	attributes="getExtendedStatistics"
+    	/>
+  </class>
+
+  <class
+      class=".zodb.ViewCache">
+
+    <require 
+        permission="zope.ManageServices" 
+        interface="lovely.viewcache.interfaces.IZODBViewCache"
+        />
+
+    <require 
+        permission="zope.ManageServices" 
+        set_schema="lovely.viewcache.interfaces.IZODBViewCache"
+        />
+  </class>
+
+  <!-- event subscribers for the invalidation based on Object events -->
+  <subscriber
+      handler=".event.invalidateCache"
+      for="zope.interface.Interface
+           zope.lifecycleevent.interfaces.IObjectModifiedEvent"
+      />
+
+  <subscriber
+      handler=".event.invalidateCache"
+      for="zope.interface.Interface
+           zope.app.container.interfaces.IObjectAddedEvent"
+      />
+
+  <subscriber
+      handler=".event.invalidateCache"
+      for="zope.interface.Interface
+           zope.app.container.interfaces.IObjectRemovedEvent"
+      />
+
+  <!-- configurator -->
+  <adapter
+      factory=".configurator.RAMViewCacheConfigurator"
+      name="lovely.viewcache.ram"
+      />
+  
+  <adapter
+      factory=".configurator.ZODBViewCacheConfigurator"
+      name="lovely.viewcache.zodb"
+      />
+  
+  <include package=".stats" />
+  <include package=".browser" />
+</configure>


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/configure.zcml
___________________________________________________________________
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/event.py
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/event.py	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/event.py	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,41 @@
+##############################################################################
+#
+# Copyright (c) 2006 Lovely Systems 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.
+#
+##############################################################################
+"""
+$Id$
+"""
+__docformat__ = 'restructuredtext'
+
+from zope import interface
+from zope import component
+
+from zope.proxy import removeAllProxies
+
+from zope.app.intid.interfaces import IIntIds
+
+from lovely.viewcache.interfaces import IViewCache
+
+
+def invalidateCache(obj, event):
+    cache = component.queryUtility(IViewCache)
+    if cache is None:
+        return
+    deps = [removeAllProxies(iface) for iface in interface.providedBy(obj)]
+    intids = component.queryUtility(IIntIds, context=obj)
+    if intids is not None:
+        uid = intids.queryId(obj)
+        if uid is not None:
+            deps.append(uid)
+    if deps:
+        cache.invalidate(dependencies=deps)
+


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/event.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/ftesting.zcml
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/ftesting.zcml	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/ftesting.zcml	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,92 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           xmlns:browser="http://namespaces.zope.org/browser"
+           xmlns:meta="http://namespaces.zope.org/meta"
+           i18n_domain="zope">
+
+  <include package="zope.app.zcmlfiles" />
+  <include package="zope.app.cache" />
+  <include package="lovely.viewcache" />
+ 
+ 
+dependencies for viewlets and managers
+  <include package="zope.contentprovider" />
+  <include package="zope.viewlet" />
+  <include package="zope.viewlet" file="meta.zcml" />
+  
+   
+to run the deom we need to include the demo package too
+  <include package="lovely.viewcache.demo" />
+
+  
+  <include package="zope.app.securitypolicy" file="meta.zcml" />
+
+  <include package="zope.app.server" />
+  
+  <include package="zope.app.authentication" />
+  <securityPolicy
+    component="zope.app.securitypolicy.zopepolicy.ZopeSecurityPolicy" />
+
+  <include package="zope.app.securitypolicy" />
+
+  <role id="zope.Anonymous" title="Everybody"
+        description="All users have this role implicitly" />
+
+  <role id="zope.Manager" title="Site Manager" />
+
+  
+  <principal
+   id="zope.manager"
+   title="Administrator"
+   login="mgr"
+   password="mgrpw" />
+  <grant
+   role="zope.Manager"
+   principal="zope.manager"
+   />
+  
+  <unauthenticatedPrincipal
+    id="zope.anybody"
+    title="Unauthenticated User" />
+
+  <unauthenticatedGroup
+    id="zope.Anybody"
+    title="Unauthenticated Users" 
+    />
+
+  <authenticatedGroup
+    id="zope.Authenticated"
+    title="Authenticated Users" 
+    />
+
+  <everybodyGroup
+    id="zope.Everybody"
+    title="All Users" 
+    />
+  
+  <include package="zope.app.form.browser" />
+  <include package="zope.formlib" />
+  <include package="z3c.configurator"/>
+
+  <adapter
+      name="z3c.configurator.testing.settitle"
+      factory="z3c.configurator.browser.testing.SetTitle"/>
+
+  <adapter
+      name="z3c.configurator.testing.setdescription"
+      factory="z3c.configurator.browser.testing.SetDescription"/>
+  
+  
+  <grantAll role="zope.Manager" />
+  
+  <grant
+      role="zope.Anonymous"
+      permission="zope.View"
+      />
+
+  <grant
+      role="zope.Anonymous"
+      permission="zope.app.dublincore.view"
+      />  
+     
+         
+</configure>


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/ftesting.zcml
___________________________________________________________________
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/ftests.py
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/ftests.py	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/ftests.py	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,58 @@
+##############################################################################
+#
+# Copyright (c) 2006 Lovely Systems 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.
+#
+##############################################################################
+"""
+$Id$
+"""
+__docformat__ = 'restructuredtext'
+
+import unittest
+from zope.app.testing import functional
+
+functional.defineLayer('TestLayer', 'ftesting.zcml')
+
+
+def setUp(test):
+    """Setup a reasonable environment for the category tests"""
+    pass
+
+def setUpRamCache(test):
+    """runs the configurator for a ram cache.
+    """
+    from zope.app.testing import functional
+    root = functional.getRootFolder()
+    from lovely.viewcache.configurator import RAMViewCacheConfigurator
+    RAMViewCacheConfigurator(root)(None)
+    test.globs['root']=root
+
+
+
+
+def tearDown(test):
+    pass
+
+
+def test_suite():
+    suite = unittest.TestSuite()
+    suites = (
+        functional.FunctionalDocFileSuite('README.txt', package='lovely.viewcache.demo'),
+        functional.FunctionalDocFileSuite('README.txt', package='lovely.viewcache.stats',
+                                          setUp=setUpRamCache),
+        )
+    for s in suites:
+        s.layer=TestLayer
+        suite.addTest(s)
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/ftests.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/i18n.py
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/i18n.py	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/i18n.py	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,22 @@
+##############################################################################
+#
+# Copyright (c) 2006 Lovely Systems 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.
+#
+##############################################################################
+"""
+$Id$
+"""
+__docformat__ = 'restructuredtext'
+
+import zope.i18nmessageid
+
+_ = zope.i18nmessageid.MessageFactory('lovely.viewcache')
+


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/i18n.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/interfaces.py
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/interfaces.py	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/interfaces.py	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,121 @@
+##############################################################################
+#
+# Copyright (c) 2006 Lovely Systems 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.
+#
+##############################################################################
+"""
+$Id$
+"""
+__docformat__ = 'restructuredtext'
+
+from zope import interface
+from zope import schema
+
+from zope.app.cache.interfaces.ram import IRAMCache
+from zope.location.interfaces import ILocation
+
+from lovely.viewcache.i18n import _
+
+
+class ICachedViewletManager(ILocation):
+    """A viewlet manager wich caches the results returned from his viewlets"""
+
+
+class ICacheableView(ILocation):
+    """A marker to make a viewlet cacheable"""
+
+    cachingKey = schema.TextLine(
+            title = u'Cache key',
+            description = u"""
+                The key for the specific viewlet in the cache.
+                None if the viewlet needs no discrimination.
+                Make sure the value is hashable.
+                """,
+            default = None,
+            )
+
+    cachingOn = schema.Bool(
+            title = u'Caching on',
+            description = u"""
+                The view is not cached if this property is False.
+                """,
+            default=True
+            )
+
+    staticCachingDeps = interface.Attribute(
+            "Objects the view depends on",
+            """Usually this is used to define dependencies on class level.
+               This attribute is read only in the CachedView implementation.
+            """)
+
+    dynamicCachingDeps = interface.Attribute(
+            "Objects the view depends on",
+            """"
+                This is used for the dynamic dependencies created by the view
+                at runtime.
+            """)
+
+
+class IViewCache(IRAMCache):
+    """A special cache used for the view cache."""
+
+    def set(data, ob, key=None, dependencies=None):
+        """It is possible to provide dependencies for the cache entry."""
+
+    def invalidate(ob=None, key=None, dependencies=None):
+        """Invalidation also allows to invalidate on the dependencies of a
+           view.
+        """
+
+    def getExtendedStatistics():
+        """return an extended statistics dictionary"""
+
+
+class IZODBViewCache(IViewCache):
+    """A cache which stores it's values in the ZODB using a mountpoint."""
+
+    dbName = schema.Choice(
+            title=_(u'Database Name'),
+            description=_(u"The database to be used for the cache"),
+            vocabulary='Database Names',
+            )
+
+
+class IViewModule(interface.Interface):
+
+    def cachedView(viewClass, dependencies=(), minAge=0, maxAge=None):
+        """Create a cached view class from an existing view class.
+
+        Returns a class which is using the cache. Caching is done when calling
+        the view.
+
+        viewClass : the class which should be turned into a cached class
+        dependencies : used for the 'staticCachingDeps'
+        minAge : the minimum lifetime of a cache entry for this view. If a
+                 view is ivalidated before this time it is marked as
+                 invalidated and removed after this time from the cache.
+        maxAge : allows to overwrite the maxAge of cache entries in the cache
+                 the view is cached. This parameter can not extend the maxAge
+                 defined in the cache!
+        """
+
+    def cachedViewlet(viewletClass, dependencies=(), minAge=0, maxAge=None):
+        """Create a cached view class from an existing view class.
+
+        This is exactly the same an 'cachedView' but allows caching for
+        viewlets.
+
+        If a cached value is present 'update' and 'render' is not called
+        instead the cached value is returned when calling render. The new
+        class is derived from the cachedView class so that it also provides
+        caching when the viewlet is called.
+        """
+


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/interfaces.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/lovely.viewcache-configure.zcml
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/lovely.viewcache-configure.zcml	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/lovely.viewcache-configure.zcml	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1 @@
+<include package="lovely.viewcache" />


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/lovely.viewcache-configure.zcml
___________________________________________________________________
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/manager.py
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/manager.py	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/manager.py	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,84 @@
+##############################################################################
+#
+# Copyright (c) 2006 Lovely Systems 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.
+#
+##############################################################################
+"""
+$Id$
+"""
+__docformat__ = "reStructuredText"
+
+from zope import interface
+from zope import component
+
+from zope.traversing.api import canonicalPath
+from zope.location.interfaces import ILocation
+from zope.viewlet.viewlet import ViewletBase
+from zope.viewlet.manager import ViewletManagerBase
+from zope.viewlet.interfaces import IViewlet
+
+from lovely.viewcache.interfaces import (ICachedViewletManager,
+                                         ICacheableView,
+                                         IViewCache,
+                                        )
+
+
+class CachedViewletManager(ViewletManagerBase):
+    interface.implements(ICachedViewletManager)
+
+    __name__ = u''
+
+    def getCache(self):
+        return component.queryUtility(IViewCache)
+
+    def _getCachePath(self, viewlet):
+        return u'%s/%s/%s'% (canonicalPath(self.context),
+                            unicode(self.__name__),
+                            unicode(viewlet.__name__))
+
+    def _updateViewlets(self):
+        cache = self.getCache()
+        if cache is not None:
+            viewlets = []
+            for viewlet in self.viewlets:
+                # try to get the cached value from the cache
+                if ICacheableView.providedBy(viewlet) and viewlet.cachingOn:
+                    result = cache.query(self._getCachePath(viewlet),
+                                         dict(key=viewlet.key))
+                    if result is not None:
+                        viewlet.__cachedValue__ = result
+                viewlets.append(viewlet)
+            self.viewlets = viewlets
+        for viewlet in self.viewlets:
+            if not hasattr(viewlet, '__cachedValue__'):
+                viewlet.update()
+
+    def render(self):
+        cache = self.getCache()
+        result = []
+        for viewlet in self.viewlets:
+            if not hasattr(viewlet, '__cachedValue__'):
+                viewletResult = viewlet.render()
+                if (    cache is not None
+                    and ICacheableView.providedBy(viewlet)
+                    and viewlet.cachingOn
+                   ):
+                    deps = set(viewlet.staticCachingDeps)
+                    deps.update(viewlet.dynamicCachingDeps)
+                    cache.set(viewletResult,
+                              self._getCachePath(viewlet),
+                              dict(key=viewlet.key),
+                              dependencies=deps)
+                result.append(viewletResult)
+            else:
+                result.append(viewlet.__cachedValue__)
+        return u'\n'.join([r for r in result])
+


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/manager.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/manager.txt
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/manager.txt	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/manager.txt	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,263 @@
+======================
+Cached Viewlet Manager
+======================
+
+We provide a viewlet manager which is able to handle the view cache of his
+viewlets.
+
+  >>> from lovely.viewcache.manager import CachedViewletManager
+
+  >>> from zope.app.container.contained import Contained
+  >>> from zope import interface
+  >>> class IContent(interface.Interface):
+  ...     pass
+  >>> class Content(Contained):
+  ...     interface.implements(IContent)
+  >>> content = Content()
+  >>> root[u'content'] = content
+
+  >>> from zope.publisher.browser import TestRequest
+  >>> request = TestRequest() 
+
+  >>> from zope.publisher.interfaces.browser import IBrowserView
+  >>> class View(object):
+  ...     interface.implements(IBrowserView)
+  ...     def __init__(self, context, request):
+  ...         self.context = context
+  ...         self.request = request
+
+  >>> view = View(content, request)
+
+We use the update/render pattern to render the manager. We get no result
+because no viewlet is registered for the manager.
+
+  >>> manager = CachedViewletManager(content, request, view)
+  >>> manager.update()
+  >>> manager.render()
+  u''
+
+Now we provide a viewlet and register it for out manager.
+
+  >>> count = 0
+  >>> from lovely.viewcache.interfaces import ICachedViewletManager
+  >>> from lovely.viewcache.mixin import CachableViewMixin
+  >>> from zope.viewlet.viewlet import ViewletBase
+  >>> class TestViewlet(ViewletBase, CachableViewMixin):
+  ...     staticCachingDeps = (1,)
+  ...     def render(self):
+  ...         global count
+  ...         count += 1
+  ...         return 'From TestViewlet %s'% count
+  >>> from zope.security.checker import NamesChecker, defineChecker
+  >>> viewletChecker = NamesChecker(('update', 'render'))
+  >>> defineChecker(TestViewlet, viewletChecker)
+
+  >>> from zope import component
+  >>> from zope.viewlet.interfaces import IViewlet
+  >>> from zope.publisher.interfaces.browser import IDefaultBrowserLayer
+  >>> component.provideAdapter(TestViewlet,
+  ...                          adapts=(IContent, IDefaultBrowserLayer,
+  ...                                  IBrowserView, ICachedViewletManager),
+  ...                          provides=IViewlet,
+  ...                          name='test1')
+
+Using the manager again renders the new viewlet.
+
+  >>> manager = CachedViewletManager(content, request, view)
+  >>> manager.update()
+  >>> manager.render()
+  u'From TestViewlet 1'
+
+  >>> manager = CachedViewletManager(content, request, view)
+  >>> manager.update()
+  >>> manager.render()
+  u'From TestViewlet 2'
+
+We still do not get the viewlet from the cache. This is because we didn't
+provide a cache for the manager. We provide a simple ram cache here.
+
+  >>> from lovely.viewcache.ram import ViewCache
+  >>> from lovely.viewcache.interfaces import IViewCache
+  >>> cache = ViewCache()
+  >>> component.provideUtility(cache, IViewCache)
+
+  >>> manager = CachedViewletManager(content, request, view)
+  >>> manager.update()
+  >>> manager.render()
+  u'From TestViewlet 3'
+
+Yipee, we got it.
+
+  >>> manager = CachedViewletManager(content, request, view)
+  >>> manager.update()
+  >>> manager.render()
+  u'From TestViewlet 3'
+
+  >>> component.provideAdapter(TestViewlet,
+  ...                          adapts=(IContent, IDefaultBrowserLayer,
+  ...                                  IBrowserView, ICachedViewletManager),
+  ...                          provides=IViewlet,
+  ...                          name='test2')
+
+  >>> manager = CachedViewletManager(content, request, view)
+  >>> manager.update()
+  >>> print manager.render()
+  From TestViewlet 3
+  From TestViewlet 4
+
+
+Viewlet Providing A Key For The Cache
+-------------------------------------
+
+The viewlet can provide a key to discriminate on internal values.
+
+  >>> class SelectiveViewlet(ViewletBase, CachableViewMixin):
+  ...     @property
+  ...     def key(self):
+  ...         return str(self.request['selector'])
+  ...     def update(self):
+  ...         self.dynamicCachingDeps = (self.request['selector'],)
+  ...     def render(self):
+  ...         global count
+  ...         count += 1
+  ...         return u'%s (selector = %s)'% (count, self.request['selector'])
+  >>> viewletChecker = NamesChecker(('update', 'render'))
+  >>> defineChecker(SelectiveViewlet, viewletChecker)
+  >>> component.provideAdapter(SelectiveViewlet,
+  ...                          adapts=(IContent, IDefaultBrowserLayer,
+  ...                                  IBrowserView, ICachedViewletManager),
+  ...                          provides=IViewlet,
+  ...                          name='selector')
+
+  >>> request.form['selector'] = 1
+  >>> manager = CachedViewletManager(content, request, view)
+  >>> manager.update()
+  >>> print manager.render()
+  5 (selector = 1)
+  ...
+
+  >>> manager = CachedViewletManager(content, request, view)
+  >>> manager.update()
+  >>> print manager.render()
+  5 (selector = 1)
+  ...
+
+Changing the selector must render the viewlet again.
+
+  >>> request.form['selector'] = 2
+  >>> manager = CachedViewletManager(content, request, view)
+  >>> manager.update()
+  >>> print manager.render()
+  6 (selector = 2)
+  ...
+
+The request with selector 1 is still in the cache.
+
+  >>> request.form['selector'] = 1
+  >>> manager = CachedViewletManager(content, request, view)
+  >>> manager.update()
+  >>> print manager.render()
+  5 (selector = 1)
+  ...
+
+
+Invalidating Cache Entries
+--------------------------
+
+  >>> cache.invalidate(dependencies=(1,))
+
+  >>> request.form['selector'] = 1
+  >>> manager = CachedViewletManager(content, request, view)
+  >>> manager.update()
+  >>> print manager.render()
+  7 (selector = 1)
+  ...
+
+
+Dynamically disabling the cache
+-------------------------------
+
+A view can disable caching using the 'cachingOn' property.
+
+  >>> class CacheSwitchingViewlet(ViewletBase, CachableViewMixin):
+  ...     @property
+  ...     def cachingOn(self):
+  ...         return 'cacheMe' in self.request
+  ...     def render(self):
+  ...         global count
+  ...         count += 1
+  ...         return u'%s (cacheMe = %s)'% (
+  ...                       count, self.request.get('cacheMe', False))
+  >>> viewletChecker = NamesChecker(('update', 'render'))
+  >>> defineChecker(CacheSwitchingViewlet, viewletChecker)
+  >>> component.provideAdapter(CacheSwitchingViewlet,
+  ...                          adapts=(IContent, IDefaultBrowserLayer,
+  ...                                  IBrowserView, ICachedViewletManager),
+  ...                          provides=IViewlet,
+  ...                          name='cacheSwitchin')
+
+  >>> manager = CachedViewletManager(content, request, view)
+  >>> manager.update()
+  >>> print manager.render()
+  10 (cacheMe = False)
+  ...
+
+As long as we do not provide 'cacheMe' in the reqeust the viewlet is not
+cached.
+
+  >>> manager = CachedViewletManager(content, request, view)
+  >>> manager.update()
+  >>> print manager.render()
+  11 (cacheMe = False)
+  ...
+
+Now the first call with 'cacheMe' set in the reqeust will render it and stores
+the render view in the cache.
+
+  >>> request.form['cacheMe'] = 1
+  >>> manager = CachedViewletManager(content, request, view)
+  >>> manager.update()
+  >>> print manager.render()
+  12 (cacheMe = 1)
+  ...
+
+Now we get the cached result.
+
+  >>> manager = CachedViewletManager(content, request, view)
+  >>> manager.update()
+  >>> print manager.render()
+  12 (cacheMe = 1)
+  ...
+
+Removing the 'cacheMe' property will render the view.
+
+  >>> del request.form['cacheMe']
+  >>> manager = CachedViewletManager(content, request, view)
+  >>> manager.update()
+  >>> print manager.render()
+  13 (cacheMe = False)
+  ...
+
+Setting 'cacheMe' again takes the existing cache entry.
+
+  >>> request.form['cacheMe'] = 1
+  >>> manager = CachedViewletManager(content, request, view)
+  >>> manager.update()
+  >>> print manager.render()
+  12 (cacheMe = 1)
+  ...
+
+
+Configurator
+------------
+
+We also provide a configurator to register a view cache on a site.
+
+  >>> from lovely.viewcache.configurator import RAMViewCacheConfigurator
+  >>> RAMViewCacheConfigurator(root)(None)
+  >>> sm = root.getSiteManager()
+  >>> 'view-cache-RAM' in sm['default']
+  True
+  >>> component.getUtility(IViewCache) is not None
+  True
+


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/manager.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/mixin.py
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/mixin.py	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/mixin.py	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,33 @@
+##############################################################################
+#
+# Copyright (c) 2006 Lovely Systems 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.
+#
+##############################################################################
+"""
+$Id$
+"""
+__docformat__ = 'restructuredtext'
+
+from zope import interface
+
+from lovely.viewcache.interfaces import ICacheableView
+
+class CachableViewMixin(object):
+    interface.implements(ICacheableView)
+
+    __parent__ = None
+    __name__ = None
+
+    key = None
+    cachingOn = True
+    staticCachingDeps = ()
+    dynamicCachingDeps = ()
+


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/mixin.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/ram.py
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/ram.py	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/ram.py	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,207 @@
+##############################################################################
+#
+# Copyright (c) 2006 Lovely Systems 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.
+#
+##############################################################################
+"""
+$Id$
+"""
+__docformat__ = 'restructuredtext'
+
+import logging
+from time import time
+
+from zope import interface
+from cPickle import dumps
+from zope.app.cache.ram import RAMCache, Storage, writelock, caches
+
+from lovely.viewcache.interfaces import IViewCache
+
+
+class ViewCache(RAMCache):
+    interface.implements(IViewCache)
+
+    def set(self, data, ob, key=None,
+                            dependencies=None,
+                            lifetime=(0, None)):
+        logging.info('Viewcache.set(%r, %r, %r)'% (ob, key, dependencies))
+        s = self._getStorage()
+        key = self._buildKey(key)
+        s.setEntry(ob, key, data, lifetime)
+        if dependencies is not None:
+            for dep in dependencies:
+                try:
+                    obs = s.getEntry(dep, None)
+                    obs += (ob, )
+                except KeyError:
+                    obs = (ob, )
+                s.setEntry(dep, None, obs, lifetime)
+
+    def invalidate(self, ob=None, key=None, dependencies=None):
+        logging.info('Viewcache.invalidate(%r, %r, %r)'% (
+            ob, key, dependencies))
+        if dependencies is not None:
+            s = self._getStorage()
+            if key:
+                key =  self._buildKey(key)
+            for dep in dependencies:
+                try:
+                    obs = s.getEntry(dep, None)
+                    s.invalidate(dep)
+                except KeyError:
+                    obs = ()
+                for ob in obs:
+                    s.invalidate(ob, key)
+        else:
+            #TODO: invalidate dependency-indices
+            super(ViewCache, self).invalidate(ob, key)
+            
+    
+    def _getStorage(self):
+        "Finds or creates a storage object."
+        cacheId = self._cacheId
+        writelock.acquire()
+        try:
+            if cacheId not in caches:
+                caches[cacheId] = LifetimeStorage(self.maxEntries, self.maxAge,
+                                                  self.cleanupInterval)
+            return caches[cacheId]
+        finally:
+            writelock.release()
+            
+            
+    def getExtendedStatistics(self):
+        s = self._getStorage()
+        return s.getExtendedStatistics()
+        
+
+class LifetimeStorage(Storage):
+
+    def setEntry(self, ob, key, value, lifetime=(0, None)):
+        """Stores a value for the object.  Creates the necessary
+        dictionaries."""
+
+        if self.lastCleanup <= time() - self.cleanupInterval:
+            self.cleanup()
+
+        self.writelock.acquire()
+        try:
+            if ob not in self._data:
+                self._data[ob] = {}
+
+            timestamp = time()
+            # [data, ctime, access count, lifetime, Invalidated]
+            self._data[ob][key] = [value, timestamp, 0, lifetime, False]
+        finally:
+            self.writelock.release()
+            self._invalidate_queued()
+
+    def _do_invalidate(self, ob, key=None):
+        """This does the actual invalidation, but does not handle the locking.
+
+        This method is supposed to be called from `invalidate`
+        """
+        obEntry = self._data.get(ob, None)
+        if obEntry is not None:
+            keyEntry = obEntry.get(key, None)
+            if keyEntry is not None:
+                minTime = keyEntry[3][0]
+                lifetime = time() - keyEntry[1]
+                if lifetime < minTime:
+                    # minimum lifetime not reached, just mark it for removal
+                    keyEntry[4]=True
+                    return
+        try:
+            if key is None:
+                del self._data[ob]
+                self._misses[ob] = 0
+            else:
+                del self._data[ob][key]
+                if not self._data[ob]:
+                    del self._data[ob]
+        except KeyError:
+            pass
+
+    def removeStaleEntries(self):
+        "Remove the entries older than `maxAge`"
+        punchline = time() - self.maxAge
+        self.writelock.acquire()
+        try:
+            data = self._data
+            for object, dict in data.items():
+                for ob, key in dict.items():
+                    minTime = key[3][0]
+                    lifetime = time() - key[1]
+                    if lifetime < minTime:
+                        # minimum lifetime not reached, do not remove it
+                        continue
+                    lifetime = time() - key[1]
+                    if (   key[4]
+                        or (    self.maxAge > 0
+                            and key[1] < punchline
+                           )
+                        or (   key[3][1] is not None
+                            and key[3][1] < lifetime
+                           )
+                       ):
+                        # invalidation flag set or maxAge reached
+                        del dict[ob]
+                        if not dict:
+                            del data[object]
+        finally:
+            self.writelock.release()
+            self._invalidate_queued()
+
+    def getExtendedStatistics(self):
+        "Basically see IRAMCache"
+        result = []
+        for ob in self._getStatisticObjects():
+            # use min and maxage for first cache entry (one is always present)
+            minage =  self._data[ob].values()[0][3][0] #damn complicating!
+            maxage =  self._data[ob].values()[0][3][1] or self.maxAge #damn complicating!
+            #the size of all cached values of all subkeys as pickeled in zodb
+            totalsize = len(dumps(self._data[ob]))
+            deps = []
+            for dep in self._data.keys():
+                for cacheentry in self._data[dep].values():
+                    if str(ob) in cacheentry[0]:
+                        #dependency cache entries have a list of dependen objects in val[0]
+                        deps.append(dep)
+            hits = sum(entry[2] for entry in self._data[ob].itervalues())
+            result.append({'path': ob,
+                           'key': None,
+                           'misses': self._misses.get(ob, 0),
+                           'size': totalsize,
+                           'entries': len(self._data[ob]),
+                           'hits': hits,
+                           'minage': minage,
+                           'maxage': maxage,
+                           'deps': deps,
+                           'keys': []})
+            pathObj = result[-1]
+
+            for key, value in self._data[ob].items():
+                if key is not None:
+                    pathObj['keys'].append({'path': ob,
+                                   'key': key,
+                                   'misses': '',
+                                   'size': len(dumps(value)),
+                                   'entries': '',
+                                   'hits': value[2],
+                                   'minage': '',
+                                   'maxage': '',
+                                   'deps': None,
+                                   'keys':[]})
+        return tuple(result)
+
+    def _getStatisticObjects(self):
+        return sorted(self._data.keys())
+


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/ram.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/ram.txt
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/ram.txt	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/ram.txt	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,49 @@
+=========
+ViewCache
+=========
+
+
+The viewcache is an extended RAMCache, which can invalidate entries based
+on object-interfaces and intids.
+
+    >>> from lovely.viewcache import ram
+    >>> viewCache = ram.ViewCache()
+
+It is possible to define dependencies on entries, which can be used to
+invalidate specific entries. If we don't specify dependencies the behaviour is
+the same as the normal RAMCache.
+
+    >>> viewCache.set('value1', 'object1', key={'x':1}, dependencies=[1])
+    >>> viewCache.set('value2', 'object2', dependencies=[2])
+    >>> viewCache.set('value3', 'object3', key={'y':1}, dependencies=[1, 2, 3])
+    >>> viewCache.query('object1', key={'x':1})
+    'value1'
+    >>> viewCache.query('object3', key={'y':1})
+    'value3'
+    >>> viewCache.invalidate(dependencies=[1])
+    >>> viewCache.query('object1', key={'x':1}) is None
+    True
+    >>> viewCache.query('object2')
+    'value2'
+    >>> viewCache.query('object3', key={'y':1}) is None
+    True
+
+    >>> viewCache.set('timed',
+    ...               'timed_object1',
+    ...                {'timed': True},
+    ...                dependencies=[10],
+    ...                lifetime=(1, None))
+    >>> viewCache.query('timed_object1', {'timed': True})
+    'timed'
+    >>> viewCache.invalidate(key={'timed': True}, dependencies=[10])
+    >>> viewCache.query('timed_object1', {'timed': True}) is None
+    False
+    >>> from time import sleep
+    >>> sleep(2)
+
+Now we need to explictly call the storage to remove stale entries.
+
+    >>> viewCache._getStorage().removeStaleEntries()
+    >>> viewCache.query('timed_object1', {'timed': True}) is None
+    True
+


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/ram.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/stats/README.txt
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/stats/README.txt	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/stats/README.txt	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,103 @@
+====================
+viewcache statistics
+====================
+
+viewcache statistics are based on the statistics you know from ramcache
+but offer additional functionality to
+
+* `invalidate cache entries`_
+* show the different keys
+* display their min and max lifetime XXX
+* and show their dependencies XXX
+
+
+
+
+
+The CacheView has been registered in the tests setUp method.
+We can access the statistic view using the sitemanager
+
+  >>> from zope.testbrowser.testing import Browser
+  >>> manager = Browser()
+  >>> manager.addHeader('Authorization', 'Basic mgr:mgrpw')
+  >>> manager.handleErrors = False
+  >>> manager.open('http://localhost:8080/++etc++site/default/view-cache-RAM/statistics.html')
+
+
+Now we just add some fake rendered views to our cache. The cache entries are similar to the viewlets
+used in the demo-package. we create a cache-entry for the idlist-viewlet with the key 'asc' and one 
+with the key 'desc'. We create another cache-entry for the metadata-viewlet where we don't specify a key.
+Both viewlet have a dependency on IFolder.
+
+  >>> viewCache = root.getSiteManager()['default']['view-cache-RAM']
+  >>> viewCache
+  <lovely.viewcache.ram.ViewCache object at ...>
+  
+  >>> from zope.app.folder.interfaces import IFolder
+  >>> viewCache.set('<div>some html snippets</div>', 'idlist', key={'key': u'asc'}, dependencies=[IFolder,])
+  >>> viewCache.set('<div>some html snippets</div>', 'idlist', key={'key': u'desc'}, dependencies=[IFolder,])
+  >>> viewCache.set('<div>some html snippets</div>', 'idlist', key={'key': u'desc'}, dependencies=[IFolder,])
+  >>> viewCache.set('<div>some html</div>', 'metadatalist', key={'key': None}, dependencies=[IFolder,])
+
+
+invalidate cache entries
+========================
+
+We want to display the cache-statistics now, and then select the entry with the key 'asc' for invalidation.
+
+  >>> manager.open('http://localhost:8080/++etc++site/default/view-cache-RAM/statistics.html')
+  >>> form = manager.getForm(index=0)
+  >>> controls = form.getControl(name='ids:list')
+  >>> items = controls.mech_control.get_items()
+  >>> items[1].selected = True
+  >>> from pprint import pprint as pp
+  >>> pp([(input.id, input.selected) for input in items])
+  [('idlist-None', False),
+   ("idlist-(('key', u'asc'),)", True),
+   ("idlist-(('key', u'desc'),)", False),
+   ('metadatalist-None', False),
+   ("metadatalist-(('key', None),)", False),
+   ...
+   
+We see, that the checkbox of the 2nd entry has been selected. We click 'Invalidate' to remove this entry
+from our cache
+ 
+ >>> form.getControl(name='form.Invalidate').click()
+ 
+As we open the statistics page again the respective cache-entry should be gone now:
+  
+  >> manager.open('http://localhost:8080/++etc++site/default/view-cache-RAM/statistics.html')
+  >>> form = manager.getForm(index=0)
+  >>> controls = form.getControl(name='ids:list')
+  >>> items = controls.mech_control.get_items()
+  >>> pp([(input.id, input.selected) for input in items])
+  [('idlist-None', False),
+   ("idlist-(('key', u'desc'),)", False),
+   ('metadatalist-None', False),
+   ("metadatalist-(('key', None),)", False),
+   ...
+   
+   
+
+The first row of a new view does not stand for a cache entry but for all cache entries of the respective view.
+It summarizes the sizes, hits and misses, shows the number of cache-entries and the lifetime settings.
+
+
+You can invalidate all cache entries for a view by invalidating this first row.
+
+  >>> controls.controls[0].selected = True
+  >>> form.getControl(name='form.Invalidate').click()
+
+now all cache entries for the view `idlist` got removed from our cache.
+
+  >>> manager.open('http://localhost:8080/++etc++site/default/view-cache-RAM/statistics.html')
+  >>> form = manager.getForm(index=0)
+  >>> controls = form.getControl(name='ids:list')
+  >>> items = controls.mech_control.get_items()
+  >>> pp([(input.id, input.selected) for input in items])
+  [('metadatalist-None', False),
+   ("metadatalist-(('key', None),)", False),
+   ...
+
+   
+XXX show/test invalidateAll
\ No newline at end of file


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/stats/README.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/stats/__init__.py
===================================================================


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/stats/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/stats/browser.py
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/stats/browser.py	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/stats/browser.py	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,32 @@
+from zope.security.proxy import removeSecurityProxy
+from zope.traversing.browser.absoluteurl import absoluteURL
+from lovely.viewcache.interfaces import IViewCache
+from zope.publisher.browser import BrowserView
+
+class StatisticsView(BrowserView):
+    """
+    """
+    __used_for__ = IViewCache
+    
+    separator='---'
+    
+    def invalidateItems(self):
+        if 'form.Invalidate' in self.request:
+            ids = self.request.get('ids', [])
+            context = removeSecurityProxy(self.context)
+            data = context._getStorage()._data
+            for obj in data.keys():  #XXX discuss to get an method like this into viewcache interface
+                for key in data[obj].keys():
+                    if self.getHash(obj, None) in ids: 
+                        #for invalidation of all entries for an object 
+                        self.context.invalidate(obj)
+                    elif self.getHash(obj, key) in ids:
+                        self.context.invalidate(obj, {key[0][0]:key[0][1]})
+        self.request.response.redirect(absoluteURL(self.context, self.request) + '/statistics.html')
+    
+    def invalidateAll(self):
+        self.context.invalidateAll()
+        self.request.response.redirect(absoluteURL(self.context, self.request) + '/statistics.html')
+   
+    def getHash(self, obj, key):
+        return "%s:%s" % (hash(obj), hash(key))
\ No newline at end of file


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/stats/browser.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/stats/configure.zcml
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/stats/configure.zcml	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/stats/configure.zcml	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,32 @@
+<configure
+    xmlns="http://namespaces.zope.org/browser"
+    i18n_domain="lovely.viewcache">
+    
+    <page
+      name="statistics.html"
+      menu="zmi_views"
+      title="ViewCache Statistics"
+      for="..interfaces.IViewCache"
+      class=".browser.StatisticsView"
+      permission="zope.Public"
+      template="stats.pt"
+      />
+      
+    <page
+      name="invalidateitems"
+      for="..interfaces.IViewCache"
+      class=".browser.StatisticsView"
+      attribute="invalidateItems"
+      permission="zope.Public"
+      />
+      
+    <page
+      name="invalidateall"
+      for="..interfaces.IViewCache"
+      class=".browser.StatisticsView"
+      attribute="invalidateAll"
+      permission="zope.Public"
+      />
+     
+      
+</configure>
\ No newline at end of file


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/stats/configure.zcml
___________________________________________________________________
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/stats/interfaces.py
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/stats/interfaces.py	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/stats/interfaces.py	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,8 @@
+from zope import interface
+
+
+class IViewCacheStatistics(interface.Interface):
+    """methods needed for cache statistics.
+    """
+    
+    pass #XXX stub
\ No newline at end of file


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/stats/interfaces.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/stats/stats.pt
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/stats/stats.pt	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/stats/stats.pt	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,96 @@
+<html metal:use-macro="context/@@standard_macros/view"
+    i18n:domain="zope">
+<body>
+<div metal:fill-slot="body">
+
+  <p><span tal:replace="context/zope:name"/>
+    <span i18n:translate="">ViewCache statistics</span></p>
+
+  <div tal:condition="python: options.has_key('errors') and
+                              options['errors']">
+    <span style="font-weight: bold" i18n:translate="">Errors</span>:
+    <div tal:repeat="error options/errors | nothing">
+      <span tal:replace="python: error[0].title" />:
+      <span tal:replace="python: error[1].error_name" />
+    </div>
+  </div>
+  <br />
+  
+  <form name="viewCacheContentsForm" method="post" action="."
+          tal:attributes="action string:invalidateitems">
+  
+	  <table  id="sortable" class="listing" summary="Content listing"
+	         cellpadding="2" cellspacing="0" >
+	    <thead>
+	    	<tr>
+		      <th i18n:translate="">Invalidate</th>
+		      <th i18n:translate="" colspan="2">Path</th>
+		      <th i18n:translate="">Hits</th>
+		      <th i18n:translate="">Misses</th>
+		      <th i18n:translate="">Size, bytes</th>
+		      <th i18n:translate="">Entries</th>
+		      <th i18n:translate="">Minage</th>
+		      <th i18n:translate="">Maxage</th>
+		      <th i18n:translate="">Dependencies</th>
+		    </tr>
+	    </thead>
+	    <tbody tal:repeat="data context/getExtendedStatistics">
+	      <tr  bgcolor="#d3bc99">
+          <td>
+	         	<input type="checkbox" class="noborder" name="ids:list" id="#" value="#"
+	                 tal:define="id python:view.getHash(data['path'],data['key'])"
+	                 tal:attributes="value id;
+	                                 id string:${data/path}-${data/key}"
+	                 />
+	        </td>
+          <td colspan="2">
+            <span tal:condition="not: data/key" tal:replace="data/path">/url/manager/view</span>
+          </td>
+	        <td tal:content="data/hits">1</td>
+	        <td tal:content="data/misses">0</td>
+	        <td tal:content="data/size">1.7</td>
+	        <td tal:content="data/entries">2</td>
+	        <td tal:content="data/minage">1000</td>
+	        <td tal:content="data/maxage">3000</td>
+	        <td>
+	        	<div tal:repeat="dep data/deps">
+	       			<div tal:content="dep">&lt;InterfaceClass zope.app.folder.interfaces.IFolder&gt;</div>
+	       		</div>
+	       	</td>
+	      </tr>
+        <tr tal:condition="data/keys"
+            tal:repeat="item data/keys">
+	        <td>
+	         	<input type="checkbox" class="noborder" name="ids:list" id="#" value="#"
+	                 tal:define="id python:view.getHash(item['path'],item['key'])"
+	                 tal:attributes="value id;
+	                                 id string:${item/path}-${item/key}"
+	                 />
+	        </td>
+          <td width="20px"></td>
+	        <td><span tal:replace="item/key">/url/manager/view</span></td>
+	        <td tal:content="item/hits">1</td>
+	        <td tal:content="item/misses">0</td>
+	        <td tal:content="item/size">1.7</td>
+	        <td tal:content="item/entries">2</td>
+	        <td tal:content="item/minage">1000</td>
+	        <td tal:content="item/maxage">3000</td>
+	        <td>
+	        	<div tal:repeat="dep item/deps">
+	       			<div tal:replace="dep">&lt;InterfaceClass zope.app.folder.interfaces.IFolder&gt;</div>
+	       		</div>
+	       	</td>
+	      </tr>
+	    </tbody>
+	  </table>
+	  <input type="submit" name="form.Invalidate" value="Invalidate" />
+	</form>
+	<form name="viewCacheContentsForm" method="post" action="."
+          tal:attributes="action string:invalidateall">
+	  	<input type="submit" name="form.InvalidateAll" value="Invalidate all" />
+	</form>
+  <div tal:content="options/message|nothing" i18n:translate="" />
+</div>
+</body>
+
+</html>


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/stats/stats.pt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/tests.py
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/tests.py	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/tests.py	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,90 @@
+##############################################################################
+#
+# Copyright (c) 2004 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.
+#
+##############################################################################
+"""
+$Id$
+"""
+__docformat__ = 'restructuredtext'
+
+import unittest
+from zope import component
+import zope.interface
+import zope.security
+from zope.testing import doctest
+from zope.app.folder import rootFolder
+from zope.app.publication.zopepublication import ZopePublication
+from zope.testing.doctestunit import DocTestSuite, DocFileSuite
+from zope.app.testing import setup
+import ZODB.tests.util, transaction
+from ZODB.interfaces import IDatabase
+from zope.schema.interfaces import IVocabularyFactory
+from lovely.mount.vocabulary import DatabaseVocabulary
+from zope.app.schema import vocabulary
+
+from zope.app.testing.setup import (placefulSetUp,
+                                    placefulTearDown)
+
+
+def setUp(test):
+    root = placefulSetUp(site=True)
+    test.globs['root'] = root
+
+def setUpZODB(test):
+    setUp(test)
+    databases = {}
+    db1 = ZODB.tests.util.DB(databases=databases, database_name='1')
+    db2 = ZODB.tests.util.DB(databases=databases, database_name='2')
+    test.db1 = db1
+    test.db2 = db2
+    cx = db1.open()
+    root = cx.root()
+    test.root_folder = rootFolder()
+    root[ZopePublication.root_name] = test.root_folder
+    test.globs['root'] = test.root_folder
+    transaction.commit()
+    vocabulary._clear()
+    component.provideUtility(DatabaseVocabulary, IVocabularyFactory, 
+                             name="Database Names")
+    test.globs['db'] = db1
+
+def tearDown(test):
+    placefulTearDown()
+
+def tearDownZODB(test):
+    test.db1.close()
+    test.db2.close()
+    tearDown(test)
+
+
+def test_suite():
+    return unittest.TestSuite((
+        DocFileSuite('README.txt',
+                     setUp=setUp, tearDown=tearDown,
+                     optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+                     ),
+        DocFileSuite('manager.txt',
+                     setUp=setUp, tearDown=tearDown,
+                     optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+                     ),
+        DocFileSuite('ram.txt',
+                     setUp=setUp, tearDown=tearDown,
+                     optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+                     ),
+        DocFileSuite('zodb.txt',
+                     setUp=setUpZODB, tearDown=tearDownZODB,
+                     optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+                     ),
+        ))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/tests.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/view.py
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/view.py	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/view.py	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,150 @@
+##############################################################################
+#
+# Copyright (c) 2006 Lovely Systems 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.
+#
+##############################################################################
+"""
+$Id$
+"""
+__docformat__ = "reStructuredText"
+
+from zope import component
+from zope import interface
+
+from zope.traversing.api import canonicalPath
+from zope.publisher.browser import BrowserView
+from zope.traversing.browser.absoluteurl import absoluteURL
+
+from lovely.viewcache.interfaces import IViewCache, ICacheableView
+
+
+class CacheMixinBase(object):
+    interface.implements(ICacheableView)
+
+    _cachingOn = True
+    __cachedValue__ = None
+    _dynamicCachingDeps = ()
+
+    @apply
+    def cachingOn():
+        def get(self):
+            return getattr(super(CacheMixinBase, self),
+                           'cachingOn',
+                           self._cachingOn)
+        def set(self, value):
+            self._cachingOn = value
+        return property(get, set)
+
+    @property
+    def cachingKey(self):
+        return getattr(super(CacheMixinBase, self),
+                       'cachingKey',
+                       self.__name__)
+
+    @property
+    def staticCachingDeps(self):
+        return getattr(super(CacheMixinBase, self),
+                       'staticCachingDeps',
+                       self._staticCachingDeps)
+
+    @apply
+    def dynamicCachingDeps():
+        def get(self):
+            return getattr(super(CacheMixinBase, self),
+                                 'dynamicCachingDeps',
+                                 self._dynamicCachingDeps)
+        def set(self, value):
+            self._dynamicCachingDeps = value
+        return property(get, set)
+
+    def getCache(self):
+        return component.queryUtility(IViewCache)
+
+    def _getCachePath(self):
+        url = absoluteURL(self.context, self.request)
+        return '/'.join(url.split('/')[3:])
+
+    def _getCachedResult(self):
+        self.__cachedValue__ = None
+        if self.cachingOn:
+            cache = self.getCache()
+            if cache is not None:
+                result = cache.query(self._getCachePath(),
+                                     dict(key=self.cachingKey))
+                if result is not None:
+                    self.__cachedValue__ = result
+        return self.__cachedValue__ is not None
+
+    def _setCachedResult(self, value):
+        self.__cachedValue__ = value
+        if self.cachingOn:
+            cache = self.getCache()
+            if cache is not None:
+                deps = set(self.staticCachingDeps)
+                deps.update(self.dynamicCachingDeps)
+                cache.set(value,
+                          self._getCachePath(),
+                          dict(key=self.cachingKey),
+                          lifetime=self.lifetime,
+                          dependencies=deps)
+
+
+class CachedViewMixin(CacheMixinBase):
+
+    def __call__(self, *args, **kwargs):
+        if not self._getCachedResult():
+            result = super(CacheMixinBase, self).__call__(*args, **kwargs)
+            self._setCachedResult(result)
+        return self.__cachedValue__
+
+
+def cachedView(ViewClass, dependencies=(), minAge=0, maxAge=None):
+    """A factory to provide a view which is possibly in the view cache."""
+    klass = ViewClass
+    if ICacheableView not in interface.implementedBy(klass):
+        attrs = dict(_staticCachingDeps=dependencies,
+                     lifetime = (minAge, maxAge),
+                     __name__=None,
+                    )
+        klass = type('<ViewCache for %s>'% ViewClass.__name__,
+                     (CachedViewMixin, ViewClass, ),
+                     attrs)
+    return klass
+
+
+class CachedViewletMixin(CacheMixinBase):
+
+    def update(self):
+        if not self._getCachedResult():
+            super(CachedViewletMixin, self).update()
+
+    def render(self):
+        if self.__cachedValue__ is None:
+            if not self._getCachedResult():
+                result = super(CachedViewletMixin, self).render()
+                self._setCachedResult(result)
+        return self.__cachedValue__
+
+
+def cachedViewlet(ViewClass, dependencies=(), minAge=0, maxAge=None):
+    """A factory to provide a viewlet which is possibly in the view cache."""
+    klass = ViewClass
+    if ICacheableView not in interface.implementedBy(klass):
+        # our class is not cached, so make it a cached class
+        attrs = dict(_staticCachingDeps=dependencies,
+                     lifetime = (minAge, maxAge),
+                     __name__=None,
+                    )
+        klass = type('<ViewletCache for %s>'% ViewClass.__name__,
+                     (CachedViewletMixin, ViewClass, ),
+                     attrs)
+    return klass
+


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/view.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/zodb.py
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/zodb.py	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/zodb.py	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,264 @@
+##############################################################################
+#
+# Copyright (c) 2006 Lovely Systems 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.
+#
+##############################################################################
+"""
+$Id$
+"""
+__docformat__ = 'restructuredtext'
+
+from cPickle import dumps
+from time import time
+from threading import Lock
+
+import persistent
+from persistent.list import PersistentList
+from BTrees.OOBTree import OOBTree
+from ZODB.interfaces import IDatabase
+
+from zope import interface
+from zope import component
+
+from zope.app.cache.ram import Storage
+
+from ram import ViewCache as RAMViewCache
+
+from lovely.mount.root import DBRoot
+from lovely.viewcache.interfaces import IZODBViewCache
+
+
+class ViewCache(RAMViewCache):
+    interface.implements(IZODBViewCache)
+
+    mountpoint = None
+
+    def __init__(self, dbName=''):
+        self.dbName = dbName
+        super(ViewCache, self).__init__()
+
+    def _getStorage(self):
+        "Finds or creates a storage object."
+        if self.mountpoint is None:
+            self.mountpoint = DBRoot(str(self.dbName))
+        if self.dbName != self.mountpoint.dbName:
+            self.mountpoint.dbName = str(self.dbName)
+        if self.__name__ not in self.mountpoint:
+            storage = PersistentStorage(
+                self.maxEntries, self.maxAge, self.cleanupInterval)
+            self.mountpoint[self.__name__] = storage
+        return self.mountpoint[self.__name__]
+
+
+class PersistentStorage(persistent.Persistent):
+    """A storage for ViewCache using ZODB.
+
+    Storage keeps the count and does the aging and cleanup of cached
+    entries.
+
+    This object is shared between threads.  It corresponds to a single
+    persistent `RAMCache` object.  Storage does the locking necessary
+    for thread safety.
+    """
+
+    def __init__(self, maxEntries=1000, maxAge=3600, cleanupInterval=300):
+        self.invalidateAll()
+        self.maxEntries = maxEntries
+        self.maxAge = maxAge
+        self.cleanupInterval = cleanupInterval
+        self.lastCleanup = time()
+
+    def update(self, maxEntries=None, maxAge=None, cleanupInterval=None):
+        """Set the registration options.
+
+        ``None`` values are ignored.
+        """
+        if maxEntries is not None:
+            self.maxEntries = maxEntries
+        if maxAge is not None:
+            self.maxAge = maxAge
+        if cleanupInterval is not None:
+            self.cleanupInterval = cleanupInterval
+
+    def getEntry(self, ob, key):
+        if self.lastCleanup <= time() - self.cleanupInterval:
+            self.cleanup()
+        try:
+            data = self._data[ob][key]
+        except KeyError:
+            if ob not in self._misses:
+                self._misses[ob] = 0
+            self._misses[ob] += 1
+            raise
+        else:
+            data[2] += 1                    # increment access count
+            return data[0]
+
+    def setEntry(self, ob, key, value, lifetime=(0, None)):
+        """Stores a value for the object.  Creates the necessary
+        dictionaries."""
+        if self.lastCleanup <= time() - self.cleanupInterval:
+            self.cleanup()
+        if ob not in self._data:
+            self._data[ob] = OOBTree()
+        timestamp = time()
+        # [data, ctime, access count, lifetime, Invalidated]
+        self._data[ob][key] = PersistentList(
+                [value, timestamp, 0, lifetime, False])
+
+    def invalidate(self, ob, key=None):
+        """Drop the cached values.
+
+        Drop all the values for an object if no key is provided or
+        just one entry if the key is provided.
+        """
+        try:
+            if key is None:
+                del self._data[ob]
+                self._misses[ob] = 0
+            else:
+                del self._data[ob][key]
+                if not self._data[ob]:
+                    del self._data[ob]
+        except KeyError:
+            pass
+
+    def invalidateAll(self):
+        """Drop all the cached values.
+        """
+        self._data = OOBTree()
+        self._misses = OOBTree()
+
+    def removeStaleEntries(self):
+        "Remove the entries older than `maxAge`"
+        punchline = time() - self.maxAge
+        data = self._data
+        for object, dict in data.items():
+            for ob, key in dict.items():
+                minTime = key[3][0]
+                lifetime = time() - key[1]
+                if lifetime < minTime:
+                    # minimum lifetime not reached, do not remove it
+                    continue
+                lifetime = time() - key[1]
+                if (   key[4]
+                    or (    self.maxAge > 0
+                        and key[1] < punchline
+                       )
+                    or (   key[3][1] is not None
+                        and key[3][1] < lifetime
+                       )
+                   ):
+                    # invalidation flag set or maxAge reached
+                    del dict[ob]
+                    if not dict:
+                        del data[object]
+
+    def cleanup(self):
+        "Cleanup the data"
+        self.removeStaleEntries()
+        self.removeLeastAccessed()
+
+    def removeLeastAccessed(self):
+        ""
+        data = self._data
+        keys = [(ob, k) for ob, v in data.iteritems() for k in v]
+
+        if len(keys) > self.maxEntries:
+            def getKey(item):
+                ob, key = item
+                return data[ob][key]
+            sort([v for v in keys], key=getKey)
+
+            ob, key = keys[self.maxEntries]
+            maxDropCount = data[ob][key][2]
+
+            keys.reverse()
+
+            for ob, key in keys:
+                if data[ob][key][2] <= maxDropCount:
+                    del data[ob][key]
+                    if not data[ob]:
+                        del data[ob]
+
+            self._clearAccessCounters()
+
+    def _clearAccessCounters(self):
+        for dict in self._data.itervalues():
+            for val in dict.itervalues():
+                val[2] = 0
+        for k in self._misses:
+            self._misses[k] = 0
+
+    def getKeys(self, object):
+        return self._data[object].keys()
+
+    def getStatistics(self):
+        "Basically see IRAMCache"
+        objects = list(self._data.keys())
+        objects.sort()
+        result = []
+
+        for ob in objects:
+            size = len(dumps(self._data[ob]))
+            hits = sum(entry[2] for entry in self._data[ob].itervalues())
+            result.append({'path': ob,
+                           'hits': hits,
+                           'misses': self._misses[ob],
+                           'size': size,
+                           'entries': len(self._data[ob])})
+        return tuple(result)
+
+    def getExtendedStatistics(self):
+        "Basically see IRAMCache"
+        result = []
+        for ob in self._getStatisticObjects():
+            # use min and maxage for first cache entry (one is always present)
+            minage =  self._data[ob].values()[0][3][0] #damn complicating!
+            maxage =  self._data[ob].values()[0][3][1] or self.maxAge #damn complicating!
+            #the size of all cached values of all subkeys as pickeled in zodb
+            totalsize = len(dumps(self._data[ob]))
+            deps = []
+            for dep in self._data.keys():
+                for cacheentry in self._data[dep].values():
+                    if str(ob) in cacheentry[0]:
+                        #dependency cache entries have a list of dependen objects in val[0]
+                        deps.append(dep)
+            hits = sum(entry[2] for entry in self._data[ob].itervalues())
+            result.append({'path': ob,
+                           'key': None,
+                           'misses': self._misses.get(ob, 0),
+                           'size': totalsize,
+                           'entries': len(self._data[ob]),
+                           'hits': hits,
+                           'minage': minage,
+                           'maxage': maxage,
+                           'deps': deps,
+                           'keys': []})
+            pathObj = result[-1]
+
+            for key, value in self._data[ob].items():
+                if key is not None:
+                    pathObj['keys'].append({'path': ob,
+                                   'key': key,
+                                   'misses': '',
+                                   'size': len(dumps(value)),
+                                   'entries': '',
+                                   'hits': value[2],
+                                   'minage': '',
+                                   'maxage': '',
+                                   'deps': None,
+                                   'keys':[]})
+        return tuple(result)
+
+    def _getStatisticObjects(self):
+        return sorted(list(self._data.keys()))
+


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/zodb.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: lovely.viewcache/trunk/src/lovely/viewcache/zodb.txt
===================================================================
--- lovely.viewcache/trunk/src/lovely/viewcache/zodb.txt	2007-02-02 09:25:53 UTC (rev 72318)
+++ lovely.viewcache/trunk/src/lovely/viewcache/zodb.txt	2007-02-02 09:32:20 UTC (rev 72319)
@@ -0,0 +1,53 @@
+=========
+ViewCache
+=========
+
+The viewcache is an extended RAMCache, which can invalidate entries based
+on object-interfaces and intids.
+
+    >>> from lovely.viewcache.zodb import ViewCache
+    >>> viewCache = ViewCache('1')
+    >>> root['cache'] = viewCache
+
+
+It is possible to define dependencies on entries, which can be used to
+invalidate specific entries. If we don't specify dependencies the behaviour is
+the same as the normal RAMCache.
+
+    >>> viewCache.set('value1', 'object1', key={'x':1}, dependencies=[1])
+    >>> viewCache.set('value2', 'object2', dependencies=[2])
+    >>> viewCache.set('value3', 'object3', key={'y':1}, dependencies=[1, 2, 3])
+    >>> viewCache.query('object1', key={'x':1})
+    'value1'
+    >>> viewCache.invalidate(dependencies=[1])
+    >>> viewCache.query('object3') is None
+    True
+    >>> viewCache.query('object1', key={'x':1}) is None
+    True
+    >>> viewCache.query('object2')
+    'value2'
+    >>> viewCache.query('object3') is None
+    True
+
+Storage
+=======
+
+    >>> s = viewCache._getStorage()
+    >>> import random, time
+    >>> def w():
+    ...     s.setEntry(random.randint(1,100), 'object1', None)
+    ...     import transaction
+    ...     time.sleep(random.randint(0,100)/100.0)
+    ...     if random.randint(0,1):
+    ...         s.invalidate('object1')
+    ...     transaction.commit()
+    >>> import threading
+    >>> t = []
+    >>> for i in range(100):
+    ...     thread = threading.Thread(target=w)
+    ...     thread.start()
+    ...     t.append(thread)
+    >>> for thread in t:
+    ...     thread.join()
+	
+


Property changes on: lovely.viewcache/trunk/src/lovely/viewcache/zodb.txt
___________________________________________________________________
Name: svn:eol-style
   + native



More information about the Checkins mailing list