[Checkins] SVN: z3c.metrics/trunk/ Initial import

Ross Patterson me at rpatterson.net
Wed Apr 16 03:05:06 EDT 2008


Log message for revision 85434:
  Initial import
  

Changed:
  A   z3c.metrics/trunk/README.txt
  A   z3c.metrics/trunk/docs/
  A   z3c.metrics/trunk/docs/HISTORY.txt
  A   z3c.metrics/trunk/setup.py
  A   z3c.metrics/trunk/z3c/
  A   z3c.metrics/trunk/z3c/__init__.py
  A   z3c.metrics/trunk/z3c/metrics/
  A   z3c.metrics/trunk/z3c/metrics/README.txt
  A   z3c.metrics/trunk/z3c/metrics/__init__.py
  A   z3c.metrics/trunk/z3c/metrics/bbb.py
  A   z3c.metrics/trunk/z3c/metrics/configure.zcml
  A   z3c.metrics/trunk/z3c/metrics/container.zcml
  A   z3c.metrics/trunk/z3c/metrics/dispatch.py
  A   z3c.metrics/trunk/z3c/metrics/dispatch.txt
  A   z3c.metrics/trunk/z3c/metrics/dispatch.zcml
  A   z3c.metrics/trunk/z3c/metrics/engine.py
  A   z3c.metrics/trunk/z3c/metrics/event.txt
  A   z3c.metrics/trunk/z3c/metrics/index.py
  A   z3c.metrics/trunk/z3c/metrics/index.txt
  A   z3c.metrics/trunk/z3c/metrics/interfaces.py
  A   z3c.metrics/trunk/z3c/metrics/meta.py
  A   z3c.metrics/trunk/z3c/metrics/meta.txt
  A   z3c.metrics/trunk/z3c/metrics/meta.zcml
  A   z3c.metrics/trunk/z3c/metrics/metric.py
  A   z3c.metrics/trunk/z3c/metrics/scale.py
  A   z3c.metrics/trunk/z3c/metrics/scale.txt
  A   z3c.metrics/trunk/z3c/metrics/subscription.py
  A   z3c.metrics/trunk/z3c/metrics/testing.py
  A   z3c.metrics/trunk/z3c/metrics/testing.zcml
  A   z3c.metrics/trunk/z3c/metrics/tests.py
  A   z3c.metrics/trunk/z3c/metrics/verify.txt
  A   z3c.metrics/trunk/z3c/metrics/zope2/
  A   z3c.metrics/trunk/z3c/metrics/zope2/__init__.py
  A   z3c.metrics/trunk/z3c/metrics/zope2/at.zcml
  A   z3c.metrics/trunk/z3c/metrics/zope2/catalog.txt
  A   z3c.metrics/trunk/z3c/metrics/zope2/configure.zcml
  A   z3c.metrics/trunk/z3c/metrics/zope2/container.zcml
  A   z3c.metrics/trunk/z3c/metrics/zope2/creator.py
  A   z3c.metrics/trunk/z3c/metrics/zope2/discussion.py
  A   z3c.metrics/trunk/z3c/metrics/zope2/discussion.zcml
  A   z3c.metrics/trunk/z3c/metrics/zope2/dispatch.zcml
  A   z3c.metrics/trunk/z3c/metrics/zope2/index.py
  A   z3c.metrics/trunk/z3c/metrics/zope2/index.zcml
  A   z3c.metrics/trunk/z3c/metrics/zope2/meta.py
  A   z3c.metrics/trunk/z3c/metrics/zope2/meta.zcml
  A   z3c.metrics/trunk/z3c/metrics/zope2/ofs.py
  A   z3c.metrics/trunk/z3c/metrics/zope2/scale.py
  A   z3c.metrics/trunk/z3c/metrics/zope2/teamspace.py
  A   z3c.metrics/trunk/z3c/metrics/zope2/teamspace.zcml
  A   z3c.metrics/trunk/z3c/metrics/zope2/testing.py
  A   z3c.metrics/trunk/z3c/metrics/zope2/testing.zcml
  A   z3c.metrics/trunk/z3c/metrics/zope2/tests.py

-=-
Added: z3c.metrics/trunk/README.txt
===================================================================
--- z3c.metrics/trunk/README.txt	                        (rev 0)
+++ z3c.metrics/trunk/README.txt	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1 @@
+see z3c/metrics/README.txt

Added: z3c.metrics/trunk/docs/HISTORY.txt
===================================================================
--- z3c.metrics/trunk/docs/HISTORY.txt	                        (rev 0)
+++ z3c.metrics/trunk/docs/HISTORY.txt	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,8 @@
+Changelog
+=========
+
+0.1 - Unreleased
+----------------
+
+* Initial release
+

Added: z3c.metrics/trunk/setup.py
===================================================================
--- z3c.metrics/trunk/setup.py	                        (rev 0)
+++ z3c.metrics/trunk/setup.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,38 @@
+from setuptools import setup, find_packages
+import os
+
+version = '0.1'
+
+setup(name='z3c.metrics',
+      version=version,
+      description="Index arbitrary values as scores for object metrics.",
+      long_description=(
+          open(os.path.join("z3c", "metrics", "README.txt")).read() +
+          "\n" + open(os.path.join("docs", "HISTORY.txt")).read()),
+      # Get more strings from http://www.python.org/pypi?%3Aaction=list_classifiers
+      classifiers=[
+        "Programming Language :: Python",
+        "Topic :: Software Development :: Libraries :: Python Modules",
+        ],
+      keywords='zope zop3 index',
+      author='Ross Patterson',
+      author_email='me at rpatterson.net',
+      url='http://pypi.python.org/pypi/z3c.metrics',
+      license='ZPL',
+      packages=find_packages(exclude=['ez_setup']),
+      namespace_packages=['z3c'],
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=[
+          'setuptools',
+          # -*- Extra requirements: -*-
+          'zope.interface',
+          'zope.component',
+          'ZODB3',
+          'z3c.persistentfactory',
+          'grouparchy.schema',
+      ],
+      entry_points="""
+      # -*- Entry points: -*-
+      """,
+      )

Added: z3c.metrics/trunk/z3c/__init__.py
===================================================================
--- z3c.metrics/trunk/z3c/__init__.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/__init__.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,6 @@
+# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
+try:
+    __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+    from pkgutil import extend_path
+    __path__ = extend_path(__path__, __name__)

Added: z3c.metrics/trunk/z3c/metrics/README.txt
===================================================================
--- z3c.metrics/trunk/z3c/metrics/README.txt	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/README.txt	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,455 @@
+;-*-Doctest-*-
+
+===========
+z3c.metrics
+===========
+
+Create two document indexes.
+
+    >>> from z3c.metrics import testing
+    >>> foo_doc_index = testing.Index()
+    >>> bar_doc_index = testing.Index()
+
+Create one creator index.
+
+    >>> creator_index = testing.Index()
+
+Set the scales for the indexes.  The defaults for scales is a half
+life of one unit.  In the case of a datetime scale, the half life is
+one year.
+
+    >>> from z3c.metrics import scale
+    >>> one_year_scale = scale.ExponentialDatetimeScale()
+    >>> foo_doc_index.scale = one_year_scale
+    >>> creator_index.scale = one_year_scale
+
+Specify a half life of two years for the second document index.
+
+    >>> two_year_scale = scale.ExponentialDatetimeScale(
+    ...     scale_unit=scale.one_year*2)
+    >>> bar_doc_index.scale = two_year_scale
+
+Create a self metric that scores the creation dates of the object
+itself.
+
+    >>> from z3c.metrics import interfaces, metric
+    >>> self_metric = metric.SelfMetric(
+    ...     field_name="created", interface=interfaces.ICreated)
+
+Register the self metric event handlers so that they are run on
+documents themselves for their own scores.
+
+    >>> from zope import component
+    >>> component.provideHandler(
+    ...     factory=self_metric.initSelfScore,
+    ...     adapts=[testing.IDocument, interfaces.IAddValueEvent])
+    >>> component.provideHandler(
+    ...     factory=self_metric.removeSelfScore,
+    ...     adapts=[testing.IDocument, interfaces.IRemoveValueEvent])
+
+Create an other metric that scores creation dates of descendants.
+
+    >>> desc_metric = metric.OtherMetric(
+    ...     interface=interfaces.ICreated,
+    ...     field_name="created", field_callable=True)
+
+Register the other metric event handlers so that they are run on
+descendants of documents for document scores.
+
+    >>> component.provideHandler(
+    ...     factory=desc_metric.addOtherValue,
+    ...     adapts=[testing.IDescendant,
+    ...             interfaces.IAddValueEvent,
+    ...             testing.IDocument])
+    >>> component.provideHandler(
+    ...     factory=desc_metric.removeOtherValue,
+    ...     adapts=[testing.IDescendant,
+    ...             interfaces.IRemoveValueEvent,
+    ...             testing.IDocument])
+
+Creat an init metric that initializes the score for new creators.
+
+    >>> from zope.app.security import interfaces as security_ifaces
+    >>> init_metric = metric.InitMetric()
+
+Register the init metric event handlers so that they are run when
+creators are added and removed.
+
+    >>> component.provideHandler(
+    ...     factory=init_metric.initSelfScore,
+    ...     adapts=[security_ifaces.IPrincipal,
+    ...             interfaces.IInitScoreEvent])
+    >>> component.provideHandler(
+    ...     factory=init_metric.removeSelfScore,
+    ...     adapts=[security_ifaces.IPrincipal,
+    ...             interfaces.IRemoveValueEvent])
+
+Register the other metric event handlers so that they are run on
+documents for creators' scores.
+
+    >>> other_metric = metric.OtherMetric(
+    ...     field_name="created", interface=interfaces.ICreated)
+
+    >>> from zope.app.security import interfaces as security_ifaces
+    >>> component.provideHandler(
+    ...     factory=other_metric.addOtherValue,
+    ...     adapts=[testing.IDocument,
+    ...             interfaces.IAddValueEvent,
+    ...             security_ifaces.IPrincipal])
+    >>> component.provideHandler(
+    ...     factory=other_metric.removeOtherValue,
+    ...     adapts=[testing.IDocument,
+    ...             interfaces.IRemoveValueEvent,
+    ...             security_ifaces.IPrincipal])
+
+Register the other metric event handlers so that they are run on
+descendants of documents for creators' scores.
+
+    >>> component.provideHandler(
+    ...     factory=desc_metric.addOtherValue,
+    ...     adapts=[testing.IDescendant,
+    ...             interfaces.IAddValueEvent,
+    ...             security_ifaces.IPrincipal])
+    >>> component.provideHandler(
+    ...     factory=desc_metric.removeOtherValue,
+    ...     adapts=[testing.IDescendant,
+    ...             interfaces.IRemoveValueEvent,
+    ...             security_ifaces.IPrincipal])
+
+Create a principal as a creator.
+
+    >>> from z3c.metrics import testing
+    >>> authentication = component.getUtility(
+    ...     security_ifaces.IAuthentication)
+    >>> baz_creator = testing.Principal()
+    >>> authentication['baz_creator'] = baz_creator
+
+Create a root container.
+
+    >>> root = testing.setUpRoot()
+
+Create one document before any metrics are added to any indexes.
+
+    >>> foo_doc = testing.Document()
+    >>> foo_doc.created = scale.epoch
+    >>> foo_doc.creators = ('baz_creator',)
+    >>> root['foo_doc'] = foo_doc
+
+Create a descendant of the document that will be included in the score
+for the document.
+
+    >>> now = scale.epoch+scale.one_year*2
+    >>> foo_desc = testing.Descendant()
+    >>> foo_desc.created = now
+    >>> foo_desc.creators = ('baz_creator',)
+    >>> foo_doc['foo_desc'] = foo_desc
+
+The indexes have no metrics yet, so they have no scores for the
+documents.
+
+    >>> foo_doc_index.getScoreFor(foo_doc)
+    Traceback (most recent call last):
+    KeyError: ...
+    >>> bar_doc_index.getScoreFor(foo_doc)
+    Traceback (most recent call last):
+    KeyError: ...
+    >>> creator_index.getScoreFor(baz_creator)
+    Traceback (most recent call last):
+    KeyError: ...
+
+Add the self metric to the first document index with the default
+weight.
+
+    >>> from z3c.metrics import subscription
+    >>> foo_self_sub = subscription.LocalWeightedSubscription(
+    ...     foo_doc_index)
+    >>> component.provideSubscriptionAdapter(
+    ...     factory=foo_self_sub.getChangeScoreEngine,
+    ...     adapts=[interfaces.IMetric, testing.IDocument,
+    ...             interfaces.IChangeScoreEvent])
+    >>> component.provideSubscriptionAdapter(
+    ...     factory=foo_self_sub.getBuildScoreEngine,
+    ...     adapts=[interfaces.IMetric, testing.IDocument,
+    ...             interfaces.IBuildScoreEvent])
+
+Add the self metric to the other document index but with a weight of
+two.
+
+    >>> bar_self_sub = subscription.LocalWeightedSubscription(
+    ...     bar_doc_index)
+    >>> bar_self_sub.weight = 2.0
+    >>> component.provideSubscriptionAdapter(
+    ...     factory=bar_self_sub.getChangeScoreEngine,
+    ...     adapts=[interfaces.IMetric,
+    ...             testing.IDocument,
+    ...             interfaces.IChangeScoreEvent])
+    >>> component.provideSubscriptionAdapter(
+    ...     factory=bar_self_sub.getBuildScoreEngine,
+    ...     adapts=[interfaces.IMetric,
+    ...             testing.IDocument,
+    ...             interfaces.IBuildScoreEvent])
+
+Also add the other metric to this index for descendants of documents.
+
+    >>> bar_desc_sub = subscription.LocalWeightedSubscription(
+    ...     bar_doc_index)
+    >>> component.provideSubscriptionAdapter(
+    ...     factory=bar_desc_sub.getChangeScoreEngine,
+    ...     adapts=[interfaces.IMetric,
+    ...             testing.IDocument,
+    ...             interfaces.IChangeScoreEvent,
+    ...             testing.IDescendant])
+    >>> component.provideSubscriptionAdapter(
+    ...     factory=bar_desc_sub.getBuildScoreEngine,
+    ...     adapts=[interfaces.IMetric,
+    ...             testing.IDocument,
+    ...             interfaces.IBuildScoreEvent,
+    ...             testing.IDescendant])
+
+Add the init metric to the creator index for creators.
+
+    >>> creator_init_sub = subscription.LocalWeightedSubscription(
+    ...     creator_index)
+    >>> component.provideSubscriptionAdapter(
+    ...     factory=creator_init_sub.getChangeScoreEngine,
+    ...     adapts=[interfaces.IMetric,
+    ...             security_ifaces.IPrincipal,
+    ...             interfaces.IChangeScoreEvent])
+    >>> component.provideSubscriptionAdapter(
+    ...     factory=creator_init_sub.getBuildScoreEngine,
+    ...     adapts=[interfaces.IMetric,
+    ...             security_ifaces.IPrincipal,
+    ...             interfaces.IBuildScoreEvent])
+
+Add the other metric to the creator index for document creators with a
+weight of two.
+
+    >>> creator_doc_sub = subscription.LocalWeightedSubscription(
+    ...     creator_index)
+    >>> creator_doc_sub.weight = 2.0
+    >>> component.provideSubscriptionAdapter(
+    ...     factory=creator_doc_sub.getChangeScoreEngine,
+    ...     adapts=[interfaces.IMetric,
+    ...             security_ifaces.IPrincipal,
+    ...             interfaces.IChangeScoreEvent,
+    ...             testing.IDocument])
+    >>> component.provideSubscriptionAdapter(
+    ...     factory=creator_doc_sub.getBuildScoreEngine,
+    ...     adapts=[interfaces.IMetric, security_ifaces.IPrincipal,
+    ...             interfaces.IBuildScoreEvent, testing.IDocument])
+
+Add the other metric to the creator index for document descendant
+creators with the default weight.
+
+    >>> creator_desc_sub = subscription.LocalWeightedSubscription(
+    ...     creator_index)
+    >>> component.provideSubscriptionAdapter(
+    ...     factory=creator_desc_sub.getChangeScoreEngine,
+    ...     adapts=[interfaces.IMetric,
+    ...             security_ifaces.IPrincipal,
+    ...             interfaces.IChangeScoreEvent,
+    ...             testing.IDescendant])
+    >>> component.provideSubscriptionAdapter(
+    ...     factory=creator_desc_sub.getBuildScoreEngine,
+    ...     adapts=[interfaces.IMetric,
+    ...             security_ifaces.IPrincipal,
+    ...             interfaces.IBuildScoreEvent,
+    ...             testing.IDescendant])
+
+Build scores for the document.
+
+    >>> foo_doc_index.buildScoreFor(foo_doc)
+    >>> bar_doc_index.buildScoreFor(foo_doc)
+
+Now the document has different scores in both indexes.
+
+    >>> foo_doc_index.getScoreFor(foo_doc, query=now)
+    0.25
+    >>> bar_doc_index.getScoreFor(foo_doc, query=now)
+    2.0
+
+Build the score for the creator.
+
+    >>> creator_index.buildScoreFor(baz_creator)
+
+Now the creators have scores in the creator index.
+
+    >>> creator_index.getScoreFor(baz_creator, query=now)
+    1.5
+
+Add a new creator.
+
+    >>> qux_creator = testing.Principal()
+    >>> authentication['qux_creator'] = qux_creator
+
+The new creator now also has the correct score
+
+    >>> creator_index.getScoreFor(qux_creator, query=now)
+    0.0
+
+Create a new document with two creators.
+
+    >>> bar_doc = testing.Document()
+    >>> bar_doc.created = now
+    >>> bar_doc.creators = ('baz_creator', 'qux_creator')
+    >>> root['bar_doc'] = bar_doc
+
+The indexes have scores for the new document.
+
+    >>> foo_doc_index.getScoreFor(bar_doc, query=now)
+    1.0
+    >>> bar_doc_index.getScoreFor(bar_doc, query=now)
+    2.0
+    >>> creator_index.getScoreFor(baz_creator, query=now)
+    3.5
+    >>> creator_index.getScoreFor(qux_creator, query=now)
+    2.0
+
+The scores are the same if rebuilt.
+
+    >>> foo_doc_index.buildScoreFor(bar_doc)
+    >>> bar_doc_index.buildScoreFor(bar_doc)
+    >>> creator_index.buildScoreFor(baz_creator)
+    >>> creator_index.buildScoreFor(qux_creator)
+
+    >>> foo_doc_index.getScoreFor(bar_doc, query=now)
+    1.0
+    >>> bar_doc_index.getScoreFor(bar_doc, query=now)
+    2.0
+    >>> creator_index.getScoreFor(baz_creator, query=now)
+    3.5
+    >>> creator_index.getScoreFor(qux_creator, query=now)
+    2.0
+
+Later, add two descendants for this document.
+
+    >>> now = scale.epoch+scale.one_year*4
+    >>> bar_desc = testing.Descendant()
+    >>> bar_desc.created = now
+    >>> bar_doc['bar_desc'] = bar_desc
+    >>> baz_desc = testing.Descendant()
+    >>> baz_desc.created = now
+    >>> bar_doc['baz_desc'] = baz_desc
+
+The scores reflect the addtions.
+
+    >>> foo_doc_index.getScoreFor(bar_doc, query=now)
+    0.25
+    >>> bar_doc_index.getScoreFor(bar_doc, query=now)
+    3.0
+
+The scores for the other document also reflect the advance of time.
+
+    >>> foo_doc_index.getScoreFor(foo_doc, query=now)
+    0.0625
+    >>> bar_doc_index.getScoreFor(foo_doc, query=now)
+    1.0
+    >>> creator_index.getScoreFor(baz_creator, query=now)
+    0.875
+    >>> creator_index.getScoreFor(qux_creator, query=now)
+    0.5
+
+The scores are the same if rebuilt.
+
+    >>> foo_doc_index.buildScoreFor(foo_doc)
+    >>> bar_doc_index.buildScoreFor(foo_doc)
+    >>> foo_doc_index.buildScoreFor(bar_doc)
+    >>> bar_doc_index.buildScoreFor(bar_doc)
+    >>> creator_index.buildScoreFor(baz_creator)
+    >>> creator_index.buildScoreFor(qux_creator)
+
+    >>> foo_doc_index.getScoreFor(foo_doc, query=now)
+    0.0625
+    >>> bar_doc_index.getScoreFor(foo_doc, query=now)
+    1.0
+    >>> foo_doc_index.getScoreFor(bar_doc, query=now)
+    0.25
+    >>> bar_doc_index.getScoreFor(bar_doc, query=now)
+    3.0
+    >>> creator_index.getScoreFor(baz_creator, query=now)
+    0.875
+    >>> creator_index.getScoreFor(qux_creator, query=now)
+    0.5
+
+Remove one of the descendants.
+
+    >>> del bar_doc['bar_desc']
+
+The scores reflect the deletion of the descendant.
+
+    >>> foo_doc_index.getScoreFor(bar_doc, query=now)
+    0.25
+    >>> bar_doc_index.getScoreFor(bar_doc, query=now)
+    2.0
+
+The scores are the same if rebuilt.
+
+    >>> foo_doc_index.buildScoreFor(bar_doc)
+    >>> bar_doc_index.buildScoreFor(bar_doc)
+
+    >>> foo_doc_index.getScoreFor(bar_doc, query=now)
+    0.25
+    >>> bar_doc_index.getScoreFor(bar_doc, query=now)
+    2.0
+
+Remove one of the documents.
+
+    >>> del root['bar_doc']
+
+The document indexes no longer have scores for the document.
+
+    >>> foo_doc_index.getScoreFor(bar_doc)
+    Traceback (most recent call last):
+    KeyError: ...
+    >>> bar_doc_index.getScoreFor(bar_doc)
+    Traceback (most recent call last):
+    KeyError: ...
+
+The creator indexes reflect the change.
+
+    >>> creator_index.getScoreFor(baz_creator, query=now)
+    0.375
+    >>> creator_index.getScoreFor(qux_creator, query=now)
+    0.0
+
+XXX
+===
+
+For example, a metric may collect the date the object itself was
+created.  While another metric might collect the dates certain kinds
+of descendants were created.  Another yet might collect rating values
+from certain kinds of descendants.
+
+An index uses one or more metrics to provide efficient lookup of
+normailized values for objects.  One common use for such values is
+sorting a set of objects.  The score an index stores for an object is
+the sum of the scores determined for each metric.
+
+XXX Metrics
+===========
+
+Metrics define the values that constitute the score for an object in a
+given metric index.  Metrics update an object's score incrementally
+and as such can only use values whose both previous and new values can
+be retrieved on change.
+
+For example, one value may be the creation date of a descendant.  When
+such a value changes, the metric can assume there was no previous
+value.  Likewise, when such an object is deleted, the metric must be
+able to retrieve the creation date from the object before it is
+deleted in order to make the incremental adjustment.
+
+This is mostly a concern if a metric's values are mutable, then the
+metric must be informed whenever that value changes in such a way that
+it has access to both the preveious and new values.  This should most
+commonly be done using events to which the metric subscribes handlers.
+
+A metric is a component that knows how to look up metric values for a
+given object.
+
+Note that if we don't count on event order, then building an object
+score from scratch requires explicitly initializing the index and
+ensuring that none of the event handlers will initialize the socre for
+the build score event.  Otherwise, it's possible that the initializing
+event handler will be called after other add value events and negate
+their effect.

Added: z3c.metrics/trunk/z3c/metrics/__init__.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/__init__.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/__init__.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1 @@
+"""Metrics"""

Added: z3c.metrics/trunk/z3c/metrics/bbb.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/bbb.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/bbb.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,4 @@
+try:
+    from zope.component import eventtesting
+except ImportError:
+    from zope.app.event.tests import placelesssetup as eventtesting

Added: z3c.metrics/trunk/z3c/metrics/configure.zcml
===================================================================
--- z3c.metrics/trunk/z3c/metrics/configure.zcml	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/configure.zcml	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,8 @@
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   i18n_domain="zope">
+
+  <include file="container.zcml" />
+  <include file="dispatch.zcml" />
+
+</configure>

Added: z3c.metrics/trunk/z3c/metrics/container.zcml
===================================================================
--- z3c.metrics/trunk/z3c/metrics/container.zcml	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/container.zcml	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,17 @@
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   i18n_domain="zope">
+
+  <class class="zope.app.container.contained.ObjectMovedEvent">
+    <implements interface=".interfaces.IChangeScoreEvent"  />
+  </class>  
+
+  <class class="zope.app.container.contained.ObjectAddedEvent">
+    <implements interface=".interfaces.IInitScoreEvent"  />
+  </class>  
+
+  <class class="zope.app.container.contained.ObjectRemovedEvent">
+    <implements interface=".interfaces.IRemoveValueEvent"  />
+  </class>  
+
+</configure>

Added: z3c.metrics/trunk/z3c/metrics/dispatch.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/dispatch.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/dispatch.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,111 @@
+from zope import interface, component
+from zope.app.component import hooks
+from zope.app.location import interfaces as location_ifaces
+from zope.app.container import interfaces as container_ifaces
+from zope.app.security import interfaces as security_ifaces
+
+from z3c.metrics import interfaces
+
+class IAncestors(interface.Interface):
+    """Iterate over the ancestors of a contained object."""
+
+ at component.adapter(container_ifaces.IContained)
+ at interface.implementer(IAncestors)
+def getAncestors(contained):
+    ancestor = contained
+    while ancestor is not None:
+        yield ancestor
+        ancestor = ancestor.__parent__
+
+ at component.adapter(container_ifaces.IContained,
+                   interfaces.IChangeScoreEvent)
+def dispatchToAncestors(obj, event):
+    new_ancestors = []
+    if event.newParent is not None:
+        new_ancestors = list(IAncestors(event.newParent))
+
+    old_ancestors = []
+    if event.oldParent is not None:
+        old_ancestors = list(IAncestors(event.oldParent))
+
+    for new_idx in xrange(len(new_ancestors)):
+        new_ancestor = new_ancestors[new_idx]
+        if new_ancestor in old_ancestors:
+            old_idx = old_ancestors.index(new_ancestor)
+            new_ancestors = new_ancestors[:new_idx]
+            old_ancestors = old_ancestors[:old_idx]
+            break
+
+    event.newAncestors = new_ancestors
+    event.oldAncestors = old_ancestors
+
+    for ancestor in new_ancestors + old_ancestors:
+        for _ in component.subscribers(
+            [obj, event, ancestor], None):
+            pass # Just make sure the handlers run
+
+ at component.adapter(container_ifaces.IContainer,
+                   interfaces.IBuildScoreEvent,
+                   container_ifaces.IContainer)
+def dispatchToDescendants(descendant, event, obj=None):
+    if obj is None:
+        obj = descendant
+    subs = location_ifaces.ISublocations(descendant, None)
+    if subs is not None:
+        for sub in subs.sublocations():
+            for ignored in component.subscribers(
+                (sub, event, obj), None):
+                pass # They do work in the adapter fetch
+
+class CreatorLookup(object):
+    interface.implements(interfaces.ICreatorLookup)
+    component.adapts(interfaces.ICreated)
+
+    def __init__(self, context):
+        self.authentication = component.getUtility(
+            security_ifaces.IAuthentication, context=context)
+
+    def __call__(self, creator_id):
+        return self.authentication.getPrincipal(creator_id)
+
+ at component.adapter(interfaces.ICreated,
+                   interfaces.IChangeScoreEvent)
+def dispatchToCreators(obj, event):
+    creator_lookup = component.getAdapter(
+        obj, interfaces.ICreatorLookup)
+    for creator_id in interfaces.ICreated(obj).creators:
+        for _ in component.subscribers(
+            [obj, event, creator_lookup(creator_id)], None):
+            pass # Just make sure the handlers run
+
+class ICreatedDispatchEvent(interface.Interface):
+    """Dispatched to subloacations for matching on creators."""
+
+    event = interface.Attribute('Event')
+    creators = interface.Attribute('Creators')
+
+class CreatedDispatchEvent(object):
+    interface.implements(ICreatedDispatchEvent)
+
+    def __init__(self, creators):
+        self.creators = creators
+
+ at component.adapter(security_ifaces.IPrincipal,
+                   interfaces.IBuildScoreEvent)
+def dispatchToSiteCreated(creator, event):
+    dispatched = CreatedDispatchEvent(set([creator.id]))
+    for _ in component.subscribers(
+        [hooks.getSite(), event, dispatched], None):
+        pass # Just make sure the handlers run
+
+ at component.adapter(interfaces.ICreated,
+                   interfaces.IBuildScoreEvent,
+                   ICreatedDispatchEvent)
+def dispatchToCreated(obj, event, dispatched):
+    creator_lookup = component.getAdapter(
+        obj, interfaces.ICreatorLookup)
+    for creator_id in dispatched.creators.intersection(
+        interfaces.ICreated(obj).creators):
+        for _ in component.subscribers(
+            [obj, event, creator_lookup(creator_id)], None):
+            pass # Just make sure the handlers run

Added: z3c.metrics/trunk/z3c/metrics/dispatch.txt
===================================================================
--- z3c.metrics/trunk/z3c/metrics/dispatch.txt	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/dispatch.txt	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,346 @@
+;-*-Doctest-*-
+
+==============
+Event Dispatch
+==============
+
+Metrics frequently require handling one event but only when that event
+happens in relation to some other context.  For example, one metric
+might concern comments but only when added as a descendant of a
+particular type of content and not when added as a descendant of other
+types of content.  As such, metrics frequently require dispatching
+events to other contexts.
+
+Containers
+==========
+
+IObjectMovedEvents are dispatched to containers of the moved object
+that have changed as a result of the move.  If the object moved
+contains other objects, the event is only dispatched to the ancestors
+of the moved container itself and is not dispatched to descendants of
+the moved container.  If the object is moved from one container to
+another, the event is only dispatches to the ancestors that have
+changed.
+
+Events are dispatched to the ancestors by the
+dispatch.dispatchToAncestors event handler.
+
+Create a root container and site.
+
+    >>> from z3c.metrics import testing
+    >>> root = testing.setUpRoot()
+
+Use an event handler to capture events for inspection.
+
+    >>> from zope import interface, component
+    >>> from z3c.metrics import interfaces
+    >>> ancestor_events = []
+    >>> def captureEvent(obj, event, ancestor):
+    ...     ancestor_events.append((obj, event, ancestor))
+    >>> component.provideHandler(
+    ...     factory=captureEvent,
+    ...     adapts=[interface.Interface,
+    ...             interfaces.IChangeScoreEvent,
+    ...             interface.Interface])
+
+Add an object to the root.
+
+    >>> from zope.app.folder import folder
+    >>> foo = folder.Folder()
+    >>> root['foo'] = foo
+
+The event is dispatched to the root but not to the object added
+itself.
+
+    >>> obj, event, ancestor = ancestor_events.pop()
+    >>> obj is foo
+    True
+    >>> ancestor is root
+    True
+
+No other events have been dispatched.
+    
+    >>> len(ancestor_events)
+    0
+
+Add another object as a child of the child of root.
+
+    >>> bar = folder.Folder()
+    >>> foo['bar'] = bar
+
+Since the object has two ancestors, the event is dispatched to both.
+
+    >>> obj, event, ancestor = ancestor_events.pop(0)
+    >>> obj is bar
+    True
+
+The first event dispatched is for the immediate parent.
+
+    >>> ancestor is foo
+    True
+
+The last event is dispatched to the parent closest to root.
+
+    >>> obj, event, ancestor = ancestor_events.pop()
+    >>> obj is bar
+    True
+    >>> ancestor is root
+    True
+
+No more events have been dispatched.
+    
+    >>> len(ancestor_events)
+    0
+
+Move an object from one container to another.
+
+    >>> root['bar'] = bar
+
+The event is dispatched only to the ancestors that have changed.
+
+    >>> obj, event, ancestor = ancestor_events.pop()
+    >>> obj is bar
+    True
+
+The only ancestor that has changed is the one that previously
+contained the object since root already contained it both before and
+after the move.
+
+    >>> ancestor is foo
+    True
+
+No more events have been dispatched.
+    
+    >>> len(ancestor_events)
+    0
+
+Removing an object will dispatch the event to all ancestors.
+
+    >>> del root['bar']
+
+The event is dispatched to the root.
+
+    >>> obj, event, ancestor = ancestor_events.pop()
+    >>> obj is bar
+    True
+    >>> ancestor is root
+    True
+
+No more events have been dispatched.
+    
+    >>> len(ancestor_events)
+    0
+
+A rename should dispatch no events at all since no ancestors have
+changed.
+
+    >>> root['bar'] = foo
+    >>> len(ancestor_events)
+    0
+
+Descendants
+===========
+
+For rebuilding scores, IBuildScoreEvents are dispatched to
+sublocations of the object as an inverse of descendant dispatch.
+
+Events are dispatched to the creators by the
+dispatch.dispatchToDescendants event handler.
+
+Use an event handler to capture events dispatched to creators.
+
+    >>> descendant_events = []
+    >>> def captureDescendantEvent(obj, event, descendant):
+    ...     descendant_events.append((obj, event, descendant))
+    >>> component.provideHandler(
+    ...     factory=captureDescendantEvent,
+    ...     adapts=[interface.Interface,
+    ...             interfaces.IBuildScoreEvent,
+    ...             interface.Interface])
+
+Create a heirarchy of contained objects.
+
+    >>> baz = folder.Folder()
+    >>> foo['baz'] = baz
+
+    >>> bah = folder.Folder()
+    >>> baz['bah'] = bah
+
+    >>> qux = folder.Folder()
+    >>> root['qux'] = qux
+
+No events have been dispatched before the event is notified.
+
+    >>> len(descendant_events)
+    0
+
+Notify the IBuildScoreEvent on the root container.
+
+    >>> import zope.event
+    >>> from z3c.metrics import index
+    >>> zope.event.notify(index.BuildScoreEvent(foo))
+
+The event is dispatched to the contained objects.
+    
+    >>> len(descendant_events)
+    3
+    >>> descendant_events[0][2] is foo
+    True
+    >>> descendant_events[1][2] is foo
+    True
+    >>> descendant_events[2][2] is foo
+    True
+    >>> descendants = [event[0] for event in descendant_events]
+    >>> foo in descendants
+    False
+    >>> bar in descendants
+    True
+    >>> baz in descendants
+    True
+    >>> bah in descendants
+    True
+    >>> qux in descendants
+    False
+
+Creators
+========
+
+IObjectMovedEvents are dispatched to the creators of the moved object.
+Events are dispatched to the creators by the
+dispatch.dispatchToCreators event handler.
+
+Use an event handler to capture events dispatched to creators.
+
+    >>> from zope.app.security import interfaces as security_ifaces
+    >>> creator_events = []
+    >>> def captureCreatorEvent(obj, event, ancestor):
+    ...     creator_events.append((obj, event, ancestor))
+    >>> component.provideHandler(
+    ...     factory=captureCreatorEvent,
+    ...     adapts=[interface.Interface,
+    ...             interfaces.IChangeScoreEvent,
+    ...             security_ifaces.IPrincipal])
+
+Create principals to be the creators.
+
+    >>> from z3c.metrics import testing
+    >>> authentication = component.getUtility(
+    ...     security_ifaces.IAuthentication)
+    >>> baz = testing.Principal()
+    >>> authentication['baz'] = baz
+
+    >>> qux = testing.Principal()
+    >>> authentication['qux'] = qux
+
+Create a root container.
+
+    >>> root = testing.setUpRoot()
+
+Set the creators for an object.
+
+    >>> bah = folder.Folder()
+    >>> interface.alsoProvides(bah, interfaces.ICreated)
+    >>> bah.creators = ['baz', 'qux']
+
+Before we add the created object to the container, no events have been
+dispatched.
+
+    >>> len(creator_events)
+    0
+
+Add the created object ot the container.
+
+    >>> root['bah'] = bah
+
+    >>> obj, event, creator = creator_events.pop(0)
+    >>> obj is bah
+    True
+    >>> creator is baz
+    True
+
+    >>> obj, event, creator = creator_events.pop(0)
+    >>> obj is bah
+    True
+    >>> creator is qux
+    True
+
+    >>> len(creator_events)
+    0
+
+Events for objects with no creators are not dispatched to creators,
+only to ancestors.
+
+    >>> quux = folder.Folder()
+    >>> root['quux'] = quux
+
+    >>> len(creator_events)
+    0
+
+TODO: Add support for tracking changes to the creators of an object
+using a grouparchy.schema.event field.
+
+Created
+=======
+
+For rebuilding scores, IBuildScoreEvents are dispatched to objects
+created by the creator as an inverse of creator dispatch.  Events are
+dispatched to the created objects by the dispatch.dispatchToCreated
+event handler.
+
+Use an event handler to capture events dispatched to creators.
+
+    >>> from zope.app.security import interfaces as security_ifaces
+    >>> created_events = []
+    >>> def captureCreatedEvent(obj, event, ancestor):
+    ...     created_events.append((obj, event, ancestor))
+    >>> component.provideHandler(
+    ...     factory=captureCreatedEvent,
+    ...     adapts=[interface.Interface,
+    ...             interfaces.IBuildScoreEvent,
+    ...             security_ifaces.IPrincipal])
+
+Add a principal as a creator of more than one object.
+
+    >>> foo = folder.Folder()
+    >>> interface.alsoProvides(foo, interfaces.ICreated)
+    >>> foo.creators = ['qux']
+    >>> root['foo'] = foo
+
+Notify the IBuildScoreEvent for a creator with one created object.
+
+    >>> import zope.event
+    >>> from z3c.metrics import index
+    >>> zope.event.notify(index.BuildScoreEvent(baz))
+
+The event is dispatched to the created objects.
+
+    >>> created, event, creator = created_events.pop(0)
+    >>> creator is baz
+    True
+    >>> created is bah
+    True
+    >>> len(created_events)
+    0
+
+Notify the IBuildScoreEvent for a creator with two created objects.
+
+    >>> import zope.event
+    >>> from z3c.metrics import index
+    >>> zope.event.notify(index.BuildScoreEvent(qux))
+
+The event is dispatched to the created objects.
+
+    >>> created, event, creator = created_events.pop(0)
+    >>> creator is qux
+    True
+    >>> created is bah
+    True
+
+    >>> created, event, creator = created_events.pop(0)
+    >>> creator is qux
+    True
+    >>> created is foo
+    True
+
+    >>> len(created_events)
+    0

Added: z3c.metrics/trunk/z3c/metrics/dispatch.zcml
===================================================================
--- z3c.metrics/trunk/z3c/metrics/dispatch.zcml	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/dispatch.zcml	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,37 @@
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   i18n_domain="zope">
+
+  <adapter factory=".dispatch.getAncestors" />
+
+  <subscriber 
+     handler=".dispatch.dispatchToAncestors" 
+     />
+  <subscriber 
+     handler=".dispatch.dispatchToDescendants" 
+     />
+  <subscriber 
+     handler=".dispatch.dispatchToDescendants" 
+     for="zope.app.container.interfaces.IContainer
+          .interfaces.IBuildScoreEvent"
+     />
+
+  <adapter factory=".dispatch.CreatorLookup" />
+
+  <subscriber
+     handler=".dispatch.dispatchToCreators" 
+     />
+  <subscriber
+     handler=".dispatch.dispatchToSiteCreated" 
+     />
+  <subscriber 
+     handler=".dispatch.dispatchToDescendants" 
+     for="zope.app.container.interfaces.IContainer
+          .interfaces.IBuildScoreEvent
+          .dispatch.ICreatedDispatchEvent"
+     />
+  <subscriber
+     handler=".dispatch.dispatchToCreated" 
+     />
+
+</configure>

Added: z3c.metrics/trunk/z3c/metrics/engine.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/engine.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/engine.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,43 @@
+from zope import interface, component
+
+from z3c.metrics import interfaces
+
+class Engine(object):
+    interface.implements(interfaces.IEngine)
+    component.adapts(interfaces.IMetric,
+                     interfaces.ISubscription,
+                     interface.Interface)
+
+    def __init__(self, metric, subscription, context):
+        self.metric = metric
+        self.subscription = subscription
+        self.index = subscription.getIndex(context)
+        self.scale = self.index.scale
+        self.context = context
+
+    def initScore(self):
+        self.index.initScoreFor(self.context)
+
+    def removeScore(self):
+        self.index.removeScoreFor(self.context)
+
+class WeightedEngine(Engine):
+    component.adapts(interfaces.IMetric,
+                     interfaces.IWeightedSubscription,
+                     interface.Interface)
+        
+    def addValue(self, value):
+        scaled = self.scale.fromValue(value)
+        weighted = self.subscription.weight*scaled
+        self.index.changeScoreFor(self.context, weighted)
+        
+    def changeValue(self, previous, current):
+        scaled = (self.scale.fromValue(current) -
+                  self.scale.fromValue(previous))
+        weighted = self.subscription.weight*scaled
+        self.index.changeScoreFor(self.context, weighted)
+        
+    def removeValue(self, value):
+        scaled = self.scale.fromValue(value)
+        weighted = self.subscription.weight*scaled
+        self.index.changeScoreFor(self.context, -weighted)

Added: z3c.metrics/trunk/z3c/metrics/event.txt
===================================================================
--- z3c.metrics/trunk/z3c/metrics/event.txt	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/event.txt	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,130 @@
+;-*-Doctest-*-
+
+======
+Events
+======
+    >>> from zope.interface import verify
+    >>> from zope.app.container import contained, sample
+    >>> from z3c.metrics import interfaces, index
+
+Build Score
+===========
+
+    >>> verify.verifyClass(
+    ...     interfaces.IBuildScoreEvent, index.BuildScoreEvent)
+    True
+    >>> event = index.BuildScoreEvent(None)
+    >>> verify.verifyObject(interfaces.IBuildScoreEvent, event)
+    True
+    >>> interfaces.IChangeScoreEvent.implementedBy(
+    ...     index.BuildScoreEvent)
+    False
+    >>> interfaces.IChangeScoreEvent.providedBy(event)
+    False
+    >>> verify.verifyClass(
+    ...     interfaces.IAddValueEvent, index.BuildScoreEvent)
+    True
+    >>> verify.verifyObject(interfaces.IAddValueEvent, event)
+    True
+    >>> interfaces.IInitScoreEvent.implementedBy(index.BuildScoreEvent)
+    False
+    >>> interfaces.IInitScoreEvent.providedBy(event)
+    False
+    >>> interfaces.IRemoveValueEvent.implementedBy(
+    ...     index.BuildScoreEvent)
+    False
+    >>> interfaces.IRemoveValueEvent.providedBy(event)
+    False
+
+Object Added
+============
+
+    >>> verify.verifyClass(
+    ...     interfaces.IChangeScoreEvent, contained.ObjectAddedEvent)
+    True
+    >>> event = contained.ObjectAddedEvent(contained.Contained())
+    >>> verify.verifyObject(interfaces.IChangeScoreEvent, event)
+    True
+    >>> interfaces.IBuildScoreEvent.implementedBy(
+    ...     contained.ObjectAddedEvent)
+    False
+    >>> interfaces.IBuildScoreEvent.providedBy(event)
+    False
+    >>> verify.verifyClass(
+    ...     interfaces.IAddValueEvent, contained.ObjectAddedEvent)
+    True
+    >>> verify.verifyObject(interfaces.IAddValueEvent, event)
+    True
+    >>> verify.verifyClass(
+    ...     interfaces.IInitScoreEvent, contained.ObjectAddedEvent)
+    True
+    >>> verify.verifyObject(interfaces.IInitScoreEvent, event)
+    True
+    >>> interfaces.IRemoveValueEvent.implementedBy(
+    ...     contained.ObjectAddedEvent)
+    False
+    >>> interfaces.IRemoveValueEvent.providedBy(event)
+    False
+
+Object Removed
+==============
+
+    >>> verify.verifyClass(
+    ...     interfaces.IChangeScoreEvent, contained.ObjectRemovedEvent)
+    True
+    >>> event = contained.ObjectRemovedEvent(contained.Contained())
+    >>> verify.verifyObject(interfaces.IChangeScoreEvent, event)
+    True
+    >>> interfaces.IBuildScoreEvent.implementedBy(
+    ...     contained.ObjectRemovedEvent)
+    False
+    >>> interfaces.IBuildScoreEvent.providedBy(event)
+    False
+    >>> verify.verifyClass(
+    ...     interfaces.IRemoveValueEvent, contained.ObjectRemovedEvent)
+    True
+    >>> verify.verifyObject(interfaces.IRemoveValueEvent, event)
+    True
+    >>> interfaces.IInitScoreEvent.implementedBy(
+    ...     contained.ObjectRemovedEvent)
+    False
+    >>> interfaces.IInitScoreEvent.providedBy(event)
+    False
+    >>> interfaces.IAddValueEvent.implementedBy(
+    ...     contained.ObjectRemovedEvent)
+    False
+    >>> interfaces.IAddValueEvent.providedBy(event)
+    False
+
+Object Moved
+============
+
+    >>> verify.verifyClass(
+    ...     interfaces.IChangeScoreEvent, contained.ObjectMovedEvent)
+    True
+    >>> container = sample.SampleContainer()
+    >>> event = contained.ObjectMovedEvent(
+    ...     object=contained.Contained(), oldParent=container,
+    ...     oldName='', newParent=container, newName='')
+    >>> verify.verifyObject(interfaces.IChangeScoreEvent, event)
+    True
+    >>> interfaces.IBuildScoreEvent.implementedBy(
+    ...     contained.ObjectMovedEvent)
+    False
+    >>> interfaces.IBuildScoreEvent.providedBy(event)
+    False
+    >>> interfaces.IInitScoreEvent.implementedBy(
+    ...     contained.ObjectMovedEvent)
+    False
+    >>> interfaces.IInitScoreEvent.providedBy(event)
+    False
+    >>> interfaces.IAddValueEvent.implementedBy(
+    ...     contained.ObjectMovedEvent)
+    False
+    >>> interfaces.IAddValueEvent.providedBy(event)
+    False
+    >>> interfaces.IRemoveValueEvent.implementedBy(
+    ...     contained.ObjectMovedEvent)
+    False
+    >>> interfaces.IRemoveValueEvent.providedBy(event)
+    False

Added: z3c.metrics/trunk/z3c/metrics/index.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/index.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/index.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,76 @@
+import persistent
+from BTrees import family64, Length
+
+from zope import interface
+import zope.event
+from zope.app.event import objectevent
+
+from z3c.metrics import interfaces, scale
+
+class ScoreError(Exception):
+    pass
+
+class IndexesScoreEvent(objectevent.ObjectEvent):
+    interface.implements(interfaces.IIndexesScoreEvent)
+
+    def __init__(self, obj, indexes=()):
+        self.object = obj
+        self.indexes = indexes
+
+class BuildScoreEvent(IndexesScoreEvent):
+    interface.implements(interfaces.IBuildScoreEvent,
+                         interfaces.IAddValueEvent)
+
+class Index(persistent.Persistent):
+    interface.implements(interfaces.IIndex)
+
+    family = family64.IF
+
+    def __init__(self, initial=0,
+                 scale=scale.ExponentialDatetimeScale()):
+        self.initial = initial
+        self.scale = scale
+        self.clear()
+
+    def __contains__(self, obj):
+        docid = self._getKeyFor(obj)
+        return docid in self._scores
+
+    def _getKeyFor(self, obj):
+        raise NotImplementedError()
+
+    def clear(self):
+        self._scores = self.family.BTree()
+        self._num_docs = Length.Length(0)
+
+    def getScoreFor(self, obj, query=None):
+        docid = self._getKeyFor(obj)
+        raw = self._scores[docid]
+        return self.scale.normalize(raw, query)
+
+    def initScoreFor(self, obj):
+        docid = self._getKeyFor(obj)
+        self._num_docs.change(1)
+        self._scores[docid] = self.initial
+
+    def buildScoreFor(self, obj):
+        docid = self._getKeyFor(obj)
+        if docid not in self._scores:
+            self._num_docs.change(1)
+        self._scores[docid] = self.initial
+        zope.event.notify(
+            BuildScoreEvent(obj, [self]))
+
+    def changeScoreFor(self, obj, amount):
+        docid = self._getKeyFor(obj)
+        old = self._scores[docid]
+        self._scores[docid] = old + amount
+        if self._scores[docid] == scale.inf:
+            self._scores[docid] = old
+            raise ScoreError('Adding %s to %s for %s is too large' % (
+                amount, old, obj))
+
+    def removeScoreFor(self, obj):
+        docid = self._getKeyFor(obj)
+        del self._scores[docid]
+        self._num_docs.change(-1)

Added: z3c.metrics/trunk/z3c/metrics/index.txt
===================================================================
--- z3c.metrics/trunk/z3c/metrics/index.txt	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/index.txt	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,115 @@
+;-*-Doctest-*-
+
+==============
+Metric Indexes
+==============
+
+Create the index with the scale used to normalize scores at query
+time.
+
+    >>> from z3c.metrics import scale, testing
+    >>> foo_index = testing.Index(scale=scale.ExponentialScale())
+
+Before an object has been added to the score index, retrieving its
+score raises an error.
+
+    >>> foo_index.getScoreFor(0)
+    Traceback (most recent call last):
+    KeyError: ...
+
+Once the object is added, it's score is set the the initial score of
+the index.
+
+    >>> foo_index.buildScoreFor(0)
+    >>> foo_index.getScoreFor(0) == foo_index.initial
+    True
+
+Indexes also report containment.
+
+    >>> 0 in foo_index
+    True
+
+    >>> 1 in foo_index
+    False
+
+Incremental Indexing
+====================
+
+Under normal operation, metric indexes are updated incrementally, adding
+the relevant score for a releveant event to the value stored in the index.
+
+Increment the score.
+
+    >>> foo_index.changeScoreFor(0, 1)
+    >>> foo_index.getScoreFor(0)
+    1.0
+
+The score can be decremented.
+
+    >>> foo_index.changeScoreFor(0, -1)
+    >>> foo_index.getScoreFor(0)
+    0.0
+
+The score can be changed by any value.
+
+    >>> foo_index.changeScoreFor(0, 3)
+    >>> foo_index.getScoreFor(0)
+    3.0
+
+Reindexing
+==========
+
+The score for an individual object can be rebuilt if needed for
+maintenance.  The score is rebuilt using events and there are no
+subscribers for this index, so in this case the score will simply be
+removed.
+set back to the initial value.
+
+After reindexing the object, it has the appropriate value.
+
+    >>> foo_index.buildScoreFor(0)
+    >>> foo_index.getScoreFor(0) == foo_index.initial
+    True
+
+Changing the Scale
+==================
+
+TODO
+
+Scoring Results
+===============
+
+TODO
+
+Infinity
+========
+
+Pickle protocol 1 raises an error on infinite values.
+
+    >>> import pickle
+    >>> foo_index._scores[id(0)] = scale.inf
+    >>> pickle.dumps(foo_index, 1)
+    Traceback (most recent call last):
+    SystemError: frexp() result out of range
+
+As such, trying the change the score by a value that will result in
+the infinite float will raise an error.
+
+    >>> foo_index.buildScoreFor(0)
+    >>> foo_index.changeScoreFor(0, scale.inf)
+    Traceback (most recent call last):
+    ScoreError: Adding inf to 0.0 for 0 is too large
+
+The previous value will be resotred leaving the index pickleable.
+
+    >>> foo_index.getScoreFor(0) == foo_index.initial
+    True
+    >>> _ = pickle.dumps(foo_index, 1)
+
+Removing
+========
+
+Finally, objects can also be removed from the index.
+
+    >>> foo_index.initScoreFor(0)
+    >>> foo_index.removeScoreFor(0)

Added: z3c.metrics/trunk/z3c/metrics/interfaces.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/interfaces.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/interfaces.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,164 @@
+from zope import interface, schema
+from zope.configuration import fields
+from zope.app.event import interfaces as event_ifaces
+
+class IMetric(interface.Interface):
+    """Defines what values are to be collected for an object."""
+
+class IAttributeMetric(IMetric):
+    """Retrieves the metric value from an interface field."""
+
+    interface = fields.GlobalInterface(
+        title=u'The interface to adapt the object to',
+        required=True)
+
+    field_name = fields.PythonIdentifier(
+        title=u'The interface field name of the value',
+        required=True)
+
+    field_callable = fields.Bool(
+        title=u'The interface field name of the value',
+        required=False, default=False)
+
+class ISelfMetric(IAttributeMetric):
+    """Initializes the object score and uses attribute value."""
+
+class IIndex(interface.Interface):
+
+    initial = interface.Attribute('Initial Score')
+    scale = interface.Attribute('Scale')
+
+    def __contains__(obj):
+        """Returns True if the object has a score"""
+
+    def changeScoreFor(obj, amount):
+        """Change the score for the object by the amount"""
+
+    def getScoreFor(obj):
+        """Get the score for an object"""
+
+    def initScoreFor(obj):
+        """Initialize the score for the object"""
+
+    def buildScoreFor(obj):
+        """Build the score for the object from scratch"""
+
+    def changeScoreFor(obj, amount):
+        """Change the score for the object by the amount"""
+
+    def removeScoreFor(obj):
+        """Remove the score for the object from the index"""
+
+class IScale(interface.Interface):
+    """Translates metric values into scaled values for storage in the
+    index.  The scale is also responsible for normalizing raw scores
+    into meaningful normalized scores on query time."""
+
+    def fromValue(value):
+        """Return the scaled value for the metric value"""
+
+    def toValue(scaled):
+        """Return the metric value for the scaled value"""
+
+    def normalize(raw, query=None):
+        """Normalize the raw score acording to the scale.  Some scales
+        may make use of a query."""
+
+class ISubscription(interface.Interface):
+    """Associates a metric with an index and any parameters needed by
+    the engine that are specific to the combination of metric and
+    index."""
+
+    def getIndex(context=None):
+        """Return the index for this subscription.  Some subscriptions
+        may accept a context argument for looking up the index."""
+
+class IWeightedSubscription(interface.Interface):
+    """A subscription that multiplies metric values by the weight."""
+
+    weight = schema.Float(title=u'Weight', required=False, default=1.0)
+
+class IUtilitySubscription(interface.Interface):
+    """The subscribed index is looked up as a utility."""
+
+    utility_interface = fields.GlobalInterface(
+        title=u'Index Utility Interface',
+        required=False, default=IIndex)
+
+class IUtilityWeightedSubscription(IUtilitySubscription,
+                                   IWeightedSubscription):
+    """ZCML directive for subscribing a metric to an index with a
+    weight."""
+
+class IEngine(interface.Interface):
+    """Process a values returned by a metric and update the raw score
+    in the index."""
+
+    metric = interface.Attribute('Metric')
+    subscription = interface.Attribute('Subscription')
+    context = interface.Attribute('Context')
+
+    def initScore():
+        """Initialize the score for the context to the index"""
+        
+    def addValue(value):
+        """Add the value to the score for the context in the index"""
+
+    def changeValue(previous, current):
+        """Add the difference in values to the score for the context
+        in the index"""
+
+    def removeValue(value):
+        """Remove the value from the score for the context in the
+        index"""
+
+    def removeScore():
+        """Remove the score for the context from the index"""
+
+class IChangeScoreEvent(event_ifaces.IObjectEvent):
+    """Change an object's score.
+
+    These events are used under normal operation for incrementally
+    updating a score in response to normal events on the object.
+    These events are dispatched "up" to the scored object."""
+
+class IIndexesScoreEvent(event_ifaces.IObjectEvent):
+    """If indexes is not None, the metrics will only apply the score
+    changes to the indexes listed."""
+
+    indexes = interface.Attribute('Indexes')
+
+class IBuildScoreEvent(IIndexesScoreEvent):
+    """Build an object's score.
+
+    These events are used for maintenance operations to build an
+    object's score from scratch.  These events are dispatched
+    "down" from the scored object to the objects whose values
+    contribute to the score."""
+
+class IAddValueEvent(event_ifaces.IObjectEvent):
+    """Add a value from the object's score.
+
+    These events are handled by the metrics to add values to the
+    object's score and are independent of the direction of dispatch."""
+
+class IInitScoreEvent(IAddValueEvent):
+    """Initialize the object's score only when not building.
+
+    These events are handled by the metrics to initialize the object's
+    score and are independent of the direction of dispatch."""
+
+class IRemoveValueEvent(event_ifaces.IObjectEvent):
+    """Remove a value from the object's score.
+
+    These events are handled by the metrics to remove values from the
+    object's score and are independent of the direction of
+    dispatch."""
+
+class ICreated(interface.Interface):
+    """List the creators of an object."""
+
+    creators = interface.Attribute('Creators')
+
+class ICreatorLookup(interface.Interface):
+    """Lookup creators by id."""

Added: z3c.metrics/trunk/z3c/metrics/meta.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/meta.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/meta.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,88 @@
+from zope import interface
+from zope.configuration import fields, config
+from zope.app.component import metaconfigure
+
+from z3c.metrics import interfaces, metric, subscription
+
+default = object()
+
+class IMetric(interfaces.IMetric):
+    """Defines what values are to be collected for an object."""
+
+    for_ = fields.Tokens(
+        title=u'Interfaces of the objects the metric applied to',
+        required=True, value_type=fields.GlobalObject())
+
+class IAttributeMetric(IMetric, interfaces.IAttributeMetric):
+    """Retrieves the metric value from an interface field."""
+
+class Metric(config.GroupingContextDecorator):
+
+    add_interface = interfaces.IAddValueEvent
+    remove_interface = interfaces.IRemoveValueEvent
+
+    def __init__(self, context, for_, **kw):
+        super(Metric, self).__init__(context, **kw)
+        self.metric = self.metric_factory(**kw)
+        self.object_interface = for_.pop(0)
+        self.for_ = for_
+
+    def before(self):
+        object_iface = self.handler_adapts[0]
+        other_ifaces = self.handler_adapts[1:]
+        metaconfigure.subscriber(
+            _context=self.context,
+            for_=[object_iface, self.add_interface]+other_ifaces,
+            handler=getattr(self.metric, self.add_handler))
+        metaconfigure.subscriber(
+            _context=self.context,
+            for_=[object_iface, self.remove_interface]+other_ifaces,
+            handler=getattr(self.metric, self.remove_handler))
+
+    @property
+    def handler_adapts(self):
+        return [self.object_interface]+self.for_
+
+class InitMetric(Metric):
+
+    metric_factory = metric.InitMetric
+    add_interface = interfaces.IInitScoreEvent
+    add_handler = 'initSelfScore'
+    remove_handler = 'removeSelfScore'
+
+class SelfMetric(Metric):
+
+    metric_factory = metric.SelfMetric
+    add_handler = 'initSelfScore'
+    remove_handler = 'removeSelfScore'
+
+class OtherMetric(Metric):
+
+    metric_factory = metric.OtherMetric
+    add_handler = 'addOtherValue'
+    remove_handler = 'removeOtherValue'
+
+    @property
+    def handler_adapts(self):
+        return self.for_+[self.object_interface]
+
+def weighted(_context, utility_interface, weight=default):
+    sub = subscription.UtilityWeightedSubscription(
+        utility_interface=utility_interface)
+    if weight is not default:
+        sub.weight = weight
+
+    provides, = interface.implementedBy(sub.getChangeScoreEngine)
+    metaconfigure.subscriber(
+        _context=_context, provides=provides,
+        for_=[interfaces.IMetric, _context.object_interface,
+              interfaces.IChangeScoreEvent]+_context.for_,
+        factory=sub.getChangeScoreEngine)
+    provides, = interface.implementedBy(sub.getBuildScoreEngine)
+    metaconfigure.subscriber(
+        _context=_context, provides=provides,
+        for_=[interfaces.IMetric, _context.object_interface,
+              interfaces.IIndexesScoreEvent]+_context.for_,
+        factory=sub.getBuildScoreEngine)
+    
+    

Added: z3c.metrics/trunk/z3c/metrics/meta.txt
===================================================================
--- z3c.metrics/trunk/z3c/metrics/meta.txt	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/meta.txt	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,292 @@
+;-*-Doctest-*-
+
+=========================
+ZCML Metric Configuration
+=========================
+
+Create a root container and site.
+
+    >>> from z3c.metrics import testing
+    >>> root = testing.setUpRoot()
+
+Create a principal as a creator.
+
+    >>> from zope import component
+    >>> from zope.app.security import interfaces as security_ifaces
+    >>> authentication = component.getUtility(
+    ...     security_ifaces.IAuthentication)
+    >>> baz_creator = testing.Principal()
+    >>> authentication['baz_creator'] = baz_creator
+
+Create one document before any metrics are added to any indexes.
+
+    >>> from z3c.metrics import scale
+    >>> foo_doc = testing.Document()
+    >>> foo_doc.created = scale.epoch
+    >>> foo_doc.creators = ('baz_creator',)
+    >>> root['foo_doc'] = foo_doc
+
+Create a descendant of the document that will be included in the score
+for the document.
+
+    >>> now = scale.epoch+scale.one_year*2
+    >>> foo_desc = testing.Descendant()
+    >>> foo_desc.created = now
+    >>> foo_desc.creators = ('baz_creator',)
+    >>> foo_doc['foo_desc'] = foo_desc
+
+    >>> from zope.configuration import xmlconfig
+    >>> context = xmlconfig.string("""
+    ... <configure
+    ...    xmlns="http://namespaces.zope.org/zope"
+    ...    xmlns:metrics="http://namespaces.zope.org/metrics"
+    ...    i18n_domain="zope">
+    ...   <include package="zope.app.component" file="meta.zcml" />
+    ...   <include package="z3c.metrics" file="meta.zcml" />
+    ... 
+    ...   <utility
+    ...      factory="z3c.metrics.testing.FooDocIndex"
+    ...      provides="z3c.metrics.testing.IFooDocIndex" />
+    ...   <utility
+    ...      factory="z3c.metrics.testing.BarDocIndex"
+    ...      provides="z3c.metrics.testing.IBarDocIndex" />
+    ...   <utility
+    ...      factory="z3c.metrics.testing.CreatorIndex"
+    ...      provides="z3c.metrics.testing.ICreatorIndex" />
+    ... 
+    ...   <metrics:self
+    ...      for="z3c.metrics.testing.IDocument"
+    ...      interface="z3c.metrics.interfaces.ICreated"
+    ...      field_name="created">
+    ...     <metrics:weighted
+    ...        utility_interface="z3c.metrics.testing.IFooDocIndex" /> 
+    ...     <metrics:weighted
+    ...        utility_interface="z3c.metrics.testing.IBarDocIndex"
+    ...        weight="2" />
+    ...   </metrics:self>
+    ... 
+    ...   <metrics:other
+    ...      for="z3c.metrics.testing.IDocument
+    ...           z3c.metrics.testing.IDescendant"
+    ...      interface="z3c.metrics.interfaces.ICreated"
+    ...      field_name="created"
+    ...      field_callable="True">
+    ...     <metrics:weighted
+    ...        utility_interface="z3c.metrics.testing.IBarDocIndex" />
+    ...   </metrics:other>
+    ... 
+    ...   <metrics:other
+    ...      for="zope.app.security.interfaces.IPrincipal
+    ...           z3c.metrics.testing.IDocument"
+    ...      interface="z3c.metrics.interfaces.ICreated"
+    ...      field_name="created">
+    ...     <metrics:weighted
+    ...        utility_interface="z3c.metrics.testing.ICreatorIndex"
+    ...        weight="2" />
+    ...   </metrics:other>
+    ... 
+    ...   <metrics:other
+    ...      for="zope.app.security.interfaces.IPrincipal
+    ...           z3c.metrics.testing.IDescendant"
+    ...      interface="z3c.metrics.interfaces.ICreated"
+    ...      field_name="created"
+    ...      field_callable="True">
+    ...     <metrics:weighted
+    ...        utility_interface="z3c.metrics.testing.ICreatorIndex" />
+    ...   </metrics:other>
+    ... 
+    ...   <metrics:init
+    ...     for="zope.app.security.interfaces.IPrincipal">
+    ...     <metrics:weighted
+    ...        utility_interface="z3c.metrics.testing.ICreatorIndex" /> 
+    ...   </metrics:init>
+    ... 
+    ... </configure>
+    ... """)
+
+    >>> foo_doc_index = component.getUtility(testing.IFooDocIndex)
+    >>> bar_doc_index = component.getUtility(testing.IBarDocIndex)
+    >>> creator_index = component.getUtility(testing.ICreatorIndex)
+
+Set the scales for the indexes.  The defaults for scales is a half
+life of one unit.  In the case of a datetime scale, the half life is
+one year.
+
+    >>> one_year_scale = scale.ExponentialDatetimeScale()
+    >>> foo_doc_index.scale = one_year_scale
+    >>> creator_index.scale = one_year_scale
+
+Specify a half life of two years for the second document index.
+
+    >>> two_year_scale = scale.ExponentialDatetimeScale(
+    ...     scale_unit=scale.one_year*2)
+    >>> bar_doc_index.scale = two_year_scale
+
+The indexes have no metrics yet, so they have no scores for the
+documents.
+
+    >>> foo_doc_index.getScoreFor(foo_doc)
+    Traceback (most recent call last):
+    KeyError: ...
+    >>> bar_doc_index.getScoreFor(foo_doc)
+    Traceback (most recent call last):
+    KeyError: ...
+    >>> creator_index.getScoreFor(baz_creator)
+    Traceback (most recent call last):
+    KeyError: ...
+
+Build scores for the document.
+
+    >>> foo_doc_index.buildScoreFor(foo_doc)
+    >>> bar_doc_index.buildScoreFor(foo_doc)
+
+Now the document has different scores in both indexes.
+
+    >>> foo_doc_index.getScoreFor(foo_doc, query=now)
+    0.25
+    >>> bar_doc_index.getScoreFor(foo_doc, query=now)
+    2.0
+
+Build the score for the creator.
+
+    >>> creator_index.buildScoreFor(baz_creator)
+
+Now the creators have scores in the creator index.
+
+    >>> creator_index.getScoreFor(baz_creator, query=now)
+    1.5
+
+Add a new creator.
+
+    >>> qux_creator = testing.Principal()
+    >>> authentication['qux_creator'] = qux_creator
+
+The new creator now also has the correct score
+
+    >>> creator_index.getScoreFor(qux_creator, query=now)
+    0.0
+
+Create a new document with two creators.
+
+    >>> bar_doc = testing.Document()
+    >>> bar_doc.created = now
+    >>> bar_doc.creators = ('baz_creator', 'qux_creator')
+    >>> root['bar_doc'] = bar_doc
+
+The indexes have scores for the new document.
+
+    >>> foo_doc_index.getScoreFor(bar_doc, query=now)
+    1.0
+    >>> bar_doc_index.getScoreFor(bar_doc, query=now)
+    2.0
+    >>> creator_index.getScoreFor(baz_creator, query=now)
+    3.5
+    >>> creator_index.getScoreFor(qux_creator, query=now)
+    2.0
+
+The scores are the same if rebuilt.
+
+    >>> foo_doc_index.buildScoreFor(bar_doc)
+    >>> bar_doc_index.buildScoreFor(bar_doc)
+    >>> creator_index.buildScoreFor(baz_creator)
+    >>> creator_index.buildScoreFor(qux_creator)
+
+    >>> foo_doc_index.getScoreFor(bar_doc, query=now)
+    1.0
+    >>> bar_doc_index.getScoreFor(bar_doc, query=now)
+    2.0
+    >>> creator_index.getScoreFor(baz_creator, query=now)
+    3.5
+    >>> creator_index.getScoreFor(qux_creator, query=now)
+    2.0
+
+Later, add two descendants for this document.
+
+    >>> now = scale.epoch+scale.one_year*4
+    >>> bar_desc = testing.Descendant()
+    >>> bar_desc.created = now
+    >>> bar_doc['bar_desc'] = bar_desc
+    >>> baz_desc = testing.Descendant()
+    >>> baz_desc.created = now
+    >>> bar_doc['baz_desc'] = baz_desc
+
+The scores reflect the addtions.
+
+    >>> foo_doc_index.getScoreFor(bar_doc, query=now)
+    0.25
+    >>> bar_doc_index.getScoreFor(bar_doc, query=now)
+    3.0
+
+The scores for the other document also reflect the advance of time.
+
+    >>> foo_doc_index.getScoreFor(foo_doc, query=now)
+    0.0625
+    >>> bar_doc_index.getScoreFor(foo_doc, query=now)
+    1.0
+    >>> creator_index.getScoreFor(baz_creator, query=now)
+    0.875
+    >>> creator_index.getScoreFor(qux_creator, query=now)
+    0.5
+
+The scores are the same if rebuilt.
+
+    >>> foo_doc_index.buildScoreFor(foo_doc)
+    >>> bar_doc_index.buildScoreFor(foo_doc)
+    >>> foo_doc_index.buildScoreFor(bar_doc)
+    >>> bar_doc_index.buildScoreFor(bar_doc)
+    >>> creator_index.buildScoreFor(baz_creator)
+    >>> creator_index.buildScoreFor(qux_creator)
+
+    >>> foo_doc_index.getScoreFor(foo_doc, query=now)
+    0.0625
+    >>> bar_doc_index.getScoreFor(foo_doc, query=now)
+    1.0
+    >>> foo_doc_index.getScoreFor(bar_doc, query=now)
+    0.25
+    >>> bar_doc_index.getScoreFor(bar_doc, query=now)
+    3.0
+    >>> creator_index.getScoreFor(baz_creator, query=now)
+    0.875
+    >>> creator_index.getScoreFor(qux_creator, query=now)
+    0.5
+
+Remove one of the descendants.
+
+    >>> del bar_doc['bar_desc']
+
+The scores reflect the deletion of the descendant.
+
+    >>> foo_doc_index.getScoreFor(bar_doc, query=now)
+    0.25
+    >>> bar_doc_index.getScoreFor(bar_doc, query=now)
+    2.0
+
+The scores are the same if rebuilt.
+
+    >>> foo_doc_index.buildScoreFor(bar_doc)
+    >>> bar_doc_index.buildScoreFor(bar_doc)
+
+    >>> foo_doc_index.getScoreFor(bar_doc, query=now)
+    0.25
+    >>> bar_doc_index.getScoreFor(bar_doc, query=now)
+    2.0
+
+Remove one of the documents.
+
+    >>> del root['bar_doc']
+
+The document indexes no longer have scores for the document.
+
+    >>> foo_doc_index.getScoreFor(bar_doc)
+    Traceback (most recent call last):
+    KeyError: ...
+    >>> bar_doc_index.getScoreFor(bar_doc)
+    Traceback (most recent call last):
+    KeyError: ...
+
+The creator indexes reflect the change.
+
+    >>> creator_index.getScoreFor(baz_creator, query=now)
+    0.375
+    >>> creator_index.getScoreFor(qux_creator, query=now)
+    0.0

Added: z3c.metrics/trunk/z3c/metrics/meta.zcml
===================================================================
--- z3c.metrics/trunk/z3c/metrics/meta.zcml	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/meta.zcml	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,33 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:meta="http://namespaces.zope.org/meta">
+
+  <meta:directives namespace="http://namespaces.zope.org/metrics">
+    
+    <meta:groupingDirective
+       name="init"
+       schema=".meta.IMetric"
+       handler=".meta.InitMetric" />
+       />
+
+    <meta:groupingDirective
+       name="self"
+       schema=".meta.IAttributeMetric"
+       handler=".meta.SelfMetric" />
+       />
+
+    <meta:groupingDirective
+       name="other"
+       schema=".meta.IAttributeMetric"
+       handler=".meta.OtherMetric" />
+       />
+      
+    <meta:directive
+       name="weighted"
+       schema=".interfaces.IUtilityWeightedSubscription"
+       handler=".meta.weighted"
+       />
+
+  </meta:directives>
+
+</configure>

Added: z3c.metrics/trunk/z3c/metrics/metric.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/metric.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/metric.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,84 @@
+from zope import interface, component
+
+from z3c.metrics import interfaces
+
+class InitMetric(object):
+    interface.implements(interfaces.IMetric)
+
+    @component.adapter(interface.Interface,
+                       interfaces.IInitScoreEvent)
+    def initSelfScore(self, obj, event):
+        for engine in component.subscribers(
+            [self, obj, event], interfaces.IEngine):
+            engine.initScore()
+
+    @component.adapter(interface.Interface,
+                       interfaces.IRemoveValueEvent)
+    def removeSelfScore(self, obj, event):
+        for engine in component.subscribers(
+            [self, obj, event], interfaces.IEngine):
+            engine.removeScore()
+
+class AttributeMetric(object):
+    interface.implements(interfaces.IAttributeMetric)
+
+    default_interface = None
+    default_field_name = None
+
+    def __init__(self, interface=None, field_name=None,
+                 field_callable=False):
+
+        if interface is None and self.default_interface is None:
+            raise ValueError("Must pass an interface")
+        if interface is None:
+            self.interface = self.default_interface
+        else:
+            self.interface = interface
+
+        if field_name is None and self.default_field_name is None:
+            raise ValueError("Must pass a field_name")
+        if field_name is None:
+            self.field_name = self.default_field_name
+        else:
+            self.field_name = field_name
+        self.field_callable = field_callable
+
+    def getValueFor(self, obj):
+        obj = self.interface(obj)
+        value = getattr(obj, self.field_name)
+        if self.field_callable:
+            value = value()
+        return value
+
+class SelfMetric(InitMetric, AttributeMetric):
+
+    @component.adapter(interface.Interface,
+                       interfaces.IAddValueEvent)
+    def initSelfScore(self, obj, event):
+        value = self.getValueFor(obj)
+        init = interfaces.IInitScoreEvent.providedBy(event)
+        for engine in component.subscribers(
+            [self, obj, event], interfaces.IEngine):
+            if init:
+                engine.initScore()
+            engine.addValue(value)
+
+class OtherMetric(AttributeMetric):
+
+    @component.adapter(interface.Interface,
+                       interfaces.IAddValueEvent,
+                       interface.Interface)
+    def addOtherValue(self, other, event, obj):
+        value = self.getValueFor(other)
+        for engine in component.subscribers(
+            [self, obj, event, other], interfaces.IEngine):
+            engine.addValue(value)
+
+    @component.adapter(interface.Interface,
+                       interfaces.IRemoveValueEvent,
+                       interface.Interface)
+    def removeOtherValue(self, other, event, obj):
+        value = self.getValueFor(other)
+        for engine in component.subscribers(
+            [self, obj, event, other], interfaces.IEngine):
+            engine.removeValue(value)

Added: z3c.metrics/trunk/z3c/metrics/scale.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/scale.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/scale.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,119 @@
+import math, datetime, time
+
+from zope import interface
+from zope.cachedescriptors import property
+import persistent
+
+from z3c.metrics import interfaces
+
+inf = 1e1000000
+
+class ExponentialScale(persistent.Persistent):
+    interface.implements(interfaces.IScale)
+
+    origin = 1
+
+    @property.readproperty
+    def default(self):
+        return self.start
+
+    def __init__(self, scale_unit=1, scale_ratio=2,
+                 start=1, min_unit=None):
+        self.scale_unit = self._fromDelta(scale_unit)
+        self.scale_ratio = scale_ratio
+        self.start = start
+        if min_unit is not None:
+            self.min_unit = self._fromDelta(min_unit)
+            self.origin = getOrigin(
+                self.min_unit, self.scale_unit, scale_ratio)
+
+    def _fromDelta(self, delta):
+        return delta
+
+    def _toDelta(self, quantity):
+        return quantity
+
+    def fromValue(self, value):
+        return self.origin*self.scale_ratio**(
+            self._fromDelta(value-self.start)/float(self.scale_unit))
+
+    def toValue(self, scaled):
+        return self.start + self._toDelta(
+            math.log(scaled/float(self.origin),
+                     self.scale_ratio)*self.scale_unit)
+
+    def normalize(self, raw, query=None):
+        if query is None:
+            query = self.default
+        return raw/float(self.fromValue(query))
+
+def getOrigin(min_unit, scale_unit, scale_ratio):
+    """
+    The scale ratio to the power of the proportion of the minimum
+    guaranteed granularity to the scale unit is the proportion of the
+    number after the origin to the origin.
+
+    scale_ratio**(min_unit/scale_unit) == origin+1/origin
+
+    Solve the above for origin.
+
+    scale_ratio**(min_unit/scale_unit) == 1+1/origin
+
+    scale_ratio**(min_unit/scale_unit)-1 == 1/origin
+    """
+    return 1/(scale_ratio**(
+        min_unit/float(scale_unit))-1)
+    
+def getRatio(scaled, scale_units, scale_unit, min_unit=None):
+    """
+    Return the ratio such that the number of units will result in
+    scaled.
+
+    scaled == origin*scale_ratio**units
+
+    scaled == scale_ratio**units/(
+        scale_ratio**(min_unit/scale_unit)-1)
+
+    scaled*(scale_ratio**(
+        min_unit/scale_unit)-1) == scale_ratio**units
+    
+    scale_ratio**(min_unit/scale_unit)-1 == scale_ratio**units/scaled
+
+    ----------------------------
+
+    units == math.log(scaled/origin, scale_ratio)
+
+    units == math.log(
+    scaled*(scale_ratio**(min_unit/scale_unit)-1), scale_ratio)
+
+    1 == math.log(
+    (scaled*(scale_ratio**(min_unit/scale_unit)-1))**(1/unit), scale_ratio)
+
+    scale_ratio**units == scaled*(scale_ratio**(min_unit/scale_unit)-1)
+    """
+    raise NotImplementedError
+
+epoch = datetime.datetime(*time.gmtime(0)[:3])
+one_day = datetime.timedelta(1)
+one_year = one_day*365
+seconds_per_day = 24*60*60
+
+class ExponentialDatetimeScale(ExponentialScale):
+
+    def __init__(self, scale_unit=one_year, scale_ratio=2,
+                 start=epoch, min_unit=None):
+        super(ExponentialDatetimeScale, self).__init__(
+            scale_unit=scale_unit, scale_ratio=scale_ratio,
+            start=start, min_unit=min_unit)
+
+    @property.readproperty
+    def default(self):
+        return datetime.datetime.now()
+
+    def _fromDelta(self, delta):
+        """Convert a time delta into a float of seconds"""
+        return (delta.days*seconds_per_day + delta.seconds +
+                delta.microseconds/float(1000000))
+
+    def _toDelta(self, quantity):
+        return datetime.timedelta(seconds=quantity)

Added: z3c.metrics/trunk/z3c/metrics/scale.txt
===================================================================
--- z3c.metrics/trunk/z3c/metrics/scale.txt	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/scale.txt	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,209 @@
+;-*-Doctest-*-
+
+=============
+Metric Scales
+=============
+
+One of the main uses of the metrics will be sorting.  As such, an
+index that efficiently weights query results for sorting is required.
+The weights, however, will need to change over time.  For example, a
+project with 10 comments today should have a higher score than a
+project with 10 comments last month.
+
+Reindexing all objects on a periodic basis is, however, unscalable,
+especially when the indexing involves touching multiple objects per
+object indexed as will be the case with the metric indexes.  As such,
+the index needs to store a number representing the date and those
+numbers need to increase relative to each other over time on an
+exponential scale.
+
+If a comment today needs to be worth as much as two comments a month
+ago, then a comment a month ago needs to be worth as much as two
+comments two months ago.  Thus a comment today needs to be worth as
+much as 4 comments two months ago.
+
+The scale ratio of 10 and the scale unit of one year means any two
+dates one year apart will always have a ratio of 10.
+
+    >>> import datetime
+    >>> from z3c.metrics import scale
+    >>> one_year = scale.one_day*365
+    >>> datetime_scale = scale.ExponentialDatetimeScale(
+    ...     scale_unit=one_year, scale_ratio=10,
+    ...     min_unit=scale.one_day)
+
+    >>> epoch_num = datetime_scale.fromValue(scale.epoch)
+    >>> datetime_scale.toValue(epoch_num) == scale.epoch
+    True
+
+The minimum guaranteed granularity is one day.
+
+    >>> datetime_scale.fromValue(
+    ...     scale.epoch+scale.one_day) == epoch_num+1
+    True
+    >>> datetime_scale.toValue(
+    ...     epoch_num+1) == scale.epoch+scale.one_day
+    True
+
+The number for a date one year from the epoch will be 10 times the
+number for the epoch.
+
+    >>> datetime_scale.fromValue(scale.epoch+one_year) == epoch_num*10
+    True
+    >>> datetime_scale.toValue(epoch_num*10) == scale.epoch+one_year
+    True
+
+Likewise, the number for a date two years from the epoch will be 10
+times the number for the date one year away and in turn 100 times the
+number for the epoch which is two years away.
+
+    >>> datetime_scale.fromValue(
+    ...     scale.epoch+one_year*2) == epoch_num*100
+    True
+    >>> datetime_scale.toValue(epoch_num*100) == scale.epoch+one_year*2
+    True
+
+Reserved Overhead
+=================
+
+If we're using integer indexes for efficiency, then we also need to
+guarantee a minimum granularity at a beginning or minimum date.  For
+example, a beginning date may be based on the oldest object in the
+application and the minimum granularity may be a day.  IOW, the
+integer representing the begining date must be the integer immediately
+preceding the integer representing the beginning date plus one day.
+Otherwise, two dates a day apart would have the same value and
+weighting.
+
+Multiple metrics will be included in the indexes with corresponding
+weights.  Furthermore, some metrics, such as comments and forums, will
+include multiple dates.  So a given index may have several metrics
+multiplied by many dates multiplied by various weights.  As such,
+there's a certain amount of numeric overhead for which room must be
+reserved in addition to reserving enough room for the exponential
+date scale over the expected maximum life of the project.
+
+A ratio of 10 over a year means a very narrow range of years for
+integer values.
+
+    >>> import sys
+    >>> (datetime_scale.toValue(sys.maxint)-scale.epoch
+    ...  ).days/365
+    7
+
+We can calculate the maximum ratio we can use if we project some
+maximums for the application:
+
+  - maximum total count of dates across all metrics for an object
+    
+    If we assume one metric may record the dates of all posts in a
+    forum, then the maximum for that one metric might be 100,000
+    posts.  Assuming the rest of the metrics record an insignificant
+    number of dates relative to that one metric, we can use 100,000 as
+    our maximum here.
+
+    >>> count_max = 100000
+ 
+  - maximum average metric weight across all the metrics
+
+    If we assume that we have a maximum metric that we need to be 1000
+    times heavier than the minimum metric, then the maximum weight
+    would be 1000.  Assuming that the rest of the metrics are
+    scattered about the lower end, then we can use 100 as our maximum
+    here.
+
+    >>> weight_max_avg = 100
+
+  - maximum range of dates indexed
+
+    We can assume that the appliction will never have record dates in
+    a range wider than 10 years in its current form.
+
+    >>> units_max = 10
+
+By these numbers, the overhead we need to reserve above the maximum
+integer is the maximum count times the maximum average weight.
+
+    >>> reserved = count_max*weight_max_avg
+
+The domain of 32 bit integers doesn't accomodate these estimats even
+with a scale ratio of 1.1.  The first date itself exceeds the domain
+by a factor of almost 18, not to mentention any dates after the first.
+
+    >>> min_num = scale.ExponentialDatetimeScale(
+    ...     scale_unit=one_year, scale_ratio=1.1,
+    ...     min_unit=scale.one_day).fromValue(scale.epoch)
+    >>> min_num/((2**31-1)/float(reserved))
+    17.8...
+
+In order to accomodate 10 years of dates with a guaranteed minimum
+granularity of one day, one would have to reduce the capacity
+estimations by at least 170.  The maximum average weight could, for
+example be restricted to 10 and the maximum total count to 5000.
+
+Even without a minimum guaranteed granularity, for example, that the
+greatest decay of the value of comments a year ago versus comments
+today is such 17 comments that are one year old would have the same
+metric score as 10 comments today.  They could decay slower than that,
+1.5 for example, but decaying any faster than that runs the risk of
+numeric errors.
+
+Without a minimum guaranteed granularity, it works with with a ratio
+of 1.5.
+
+    >>> from BTrees import IIBTree
+    >>> datetime_scale = scale.ExponentialDatetimeScale(
+    ...     scale_unit=one_year, scale_ratio=1.5)
+    >>> max_datetime_num = datetime_scale.fromValue(
+    ...     scale.epoch+one_year*units_max)
+    >>> _ = IIBTree.IIBTree({0: int(
+    ...      max_datetime_num*count_max*weight_max_avg)})
+
+It doesn't, however, work with with a ratio of 2.
+
+    >>> datetime_scale = scale.ExponentialDatetimeScale(
+    ...     scale_unit=one_year, scale_ratio=2)
+    >>> max_datetime_num = datetime_scale.fromValue(
+    ...     scale.epoch+one_year*units_max)
+    >>> _ = IIBTree.IIBTree({0: int(
+    ...      max_datetime_num*count_max*weight_max_avg)})
+    Traceback (most recent call last):
+    TypeError: expected integer value
+
+Furthermore, in the cases above the absense of a minimum guaranteed
+granularity means that dates close to the beginning date will have the
+same value.  In this case, even with a high ratio like 10, even dates
+100 days past the epoch have the same value as the epoch itself.
+
+    >>> datetime_scale = scale.ExponentialDatetimeScale(
+    ...     scale_unit=one_year, scale_ratio=10)
+    >>> int(datetime_scale.fromValue(scale.epoch+scale.one_day*100)
+    ...     ) == datetime_scale.fromValue(scale.epoch)
+    True
+
+Floating Point Numbers
+======================
+
+If we could use floats, such as in ZODB 3.8 with IFBTrees, then the
+range is much larger both because the maximum float is much larger
+than the maximum integer and because there's no need to guarantee any
+granularity as granularity is effectively constant.
+
+Find the largest float.
+
+    >>> i = 2
+    >>> from BTrees import family64
+    >>> btree = family64.IF.BTree({0:0})
+    >>> max_float = 10
+    >>> while 1:
+    ...     btree[0] = max_float*10
+    ...     if btree[0] == scale.inf: break
+    ...     max_float = btree[0]
+
+In this case, reserving room for our estimates, we can see that
+maximum date range is at least 2 times that of our esimtated maximum
+or 20 years even with a ratio of 10.
+
+    >>> max_datetime = datetime_scale.toValue(max_float/reserved)
+    >>> (max_datetime-scale.epoch).days/365 > 2*units_max
+    True

Added: z3c.metrics/trunk/z3c/metrics/subscription.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/subscription.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/subscription.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,70 @@
+from zope import interface, component
+from zope.schema import fieldproperty
+import persistent
+
+from z3c.metrics import interfaces, engine
+
+default = object()
+
+class Subscription(object):
+
+    @component.adapter(interfaces.IMetric,
+                       interface.Interface,
+                       interfaces.IChangeScoreEvent) 
+    @interface.implementer(interfaces.IEngine)
+    def getChangeScoreEngine(self, metric, context, event, *args):
+        return self.engine_factory(metric, self, context)
+
+    @component.adapter(interfaces.IMetric,
+                       interface.Interface,
+                       interfaces.IIndexesScoreEvent) 
+    @interface.implementer(interfaces.IEngine)
+    def getBuildScoreEngine(self, metric, context, event, *args):
+        if self.getIndex(context) in event.indexes:
+            return self.engine_factory(metric, self, context)
+
+class WeightedSubscription(Subscription):
+    interface.implements(interfaces.IWeightedSubscription)
+
+    engine_factory = engine.WeightedEngine
+
+    weight = fieldproperty.FieldProperty(
+        interfaces.IWeightedSubscription['weight'])
+
+class ILocalSubscription(interfaces.ISubscription):
+    """The subscribed index is stored on an attribute."""
+
+    index = interface.Attribute('Index')
+
+class LocalSubscription(persistent.Persistent):
+    interface.implements(ILocalSubscription)
+    component.adapts(interfaces.IIndex)
+
+    def __init__(self, index):
+        self.index = index
+
+    def getIndex(self, context=None):
+        return self.index
+
+class UtilitySubscription(object):
+    interface.implements(interfaces.IUtilitySubscription,
+                         interfaces.ISubscription)
+
+    utility_interface = fieldproperty.FieldProperty(
+        interfaces.IUtilitySubscription['utility_interface'])
+
+    def __init__(self, utility_interface=default):
+        if utility_interface is not default:
+            self.utility_interface = utility_interface
+
+    def getIndex(self, context=None):
+        return component.getUtility(
+            self.utility_interface, context=context)
+
+class UtilityWeightedSubscription(WeightedSubscription,
+                                  UtilitySubscription):
+    pass
+
+class LocalWeightedSubscription(WeightedSubscription,
+                                LocalSubscription):
+    pass

Added: z3c.metrics/trunk/z3c/metrics/testing.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/testing.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/testing.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,67 @@
+from zope import interface
+from zope.app.component import site, hooks
+from zope.app.folder import folder
+from zope.app.container import contained
+from zope.app.security import interfaces as security_ifaces
+
+from z3c.metrics import index, interfaces
+
+class IFooDocIndex(interfaces.IIndex): pass
+class IBarDocIndex(interfaces.IIndex): pass
+class ICreatorIndex(interfaces.IIndex): pass
+
+class IDocument(interface.Interface): pass
+class IDescendant(interface.Interface): pass
+
+def setUpRoot():
+    root = folder.rootFolder()
+    sm = site.LocalSiteManager(root)
+    root.setSiteManager(sm)
+    hooks.setSite(root)
+    return root
+
+class Principal(contained.Contained):
+    interface.implements(security_ifaces.IPrincipal)
+
+    @property
+    def id(self):
+        return self.__name__
+
+class Authentication(folder.Folder):
+    interface.implements(security_ifaces.IAuthentication)
+
+    getPrincipal = folder.Folder.__getitem__
+
+class Index(index.Index):
+
+    def _getKeyFor(self, obj):
+        return id(obj)
+
+class FooDocIndex(Index):
+    interface.implements(IFooDocIndex)
+
+class BarDocIndex(Index):
+    interface.implements(IBarDocIndex)
+
+class CreatorIndex(Index):
+    interface.implements(ICreatorIndex)
+
+class Created(folder.Folder):
+    interface.implements(interfaces.ICreated)
+
+    creators = ()
+
+class Document(Created):
+    interface.implements(IDocument)
+
+class Descendant(Created):
+    interface.implements(IDescendant)
+
+    @apply
+    def created():
+        """Callable created field attribute"""
+        def get(self):
+            return lambda: self.__dict__['created']
+        def set(self, value):
+            self.__dict__['created'] = value
+        return property(get, set)

Added: z3c.metrics/trunk/z3c/metrics/testing.zcml
===================================================================
--- z3c.metrics/trunk/z3c/metrics/testing.zcml	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/testing.zcml	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,26 @@
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   i18n_domain="zope">
+
+  <include package="zope.app.component" file="meta.zcml" />
+
+  <adapter
+      for="zope.interface.Interface" 
+      provides="zope.component.interfaces.ISiteManager"
+      factory="zope.app.component.site.SiteManagerAdapter"
+      />
+
+  <adapter
+      provides="zope.app.location.interfaces.ISublocations"
+      for="zope.app.container.interfaces.IReadContainer"
+      factory="zope.app.container.contained.ContainerSublocations"
+      />
+
+  <utility
+     factory=".testing.Authentication"
+     provides="zope.app.security.interfaces.IAuthentication"
+     />
+
+  <include package="." />
+
+</configure>

Added: z3c.metrics/trunk/z3c/metrics/tests.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/tests.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/tests.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,34 @@
+import unittest
+from zope.testing import doctest, cleanup
+
+from zope.configuration import xmlconfig
+
+import z3c.metrics
+from z3c.metrics import bbb
+
+def setUp(test):
+    cleanup.setUp()
+    bbb.eventtesting.PlacelessSetup().setUp()
+    xmlconfig.file('testing.zcml', z3c.metrics)
+
+def tearDown(test):
+    cleanup.tearDown()
+
+def test_suite():
+    return doctest.DocFileSuite(
+        'scale.txt',
+        'index.txt',
+        'verify.txt',
+        'event.txt',
+        'dispatch.txt',
+        'meta.txt',
+        'README.txt',
+        setUp=setUp, tearDown=tearDown,
+        optionflags=(
+            doctest.REPORT_NDIFF|
+            #doctest.REPORT_ONLY_FIRST_FAILURE|
+            doctest.NORMALIZE_WHITESPACE|
+            doctest.ELLIPSIS))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')

Added: z3c.metrics/trunk/z3c/metrics/verify.txt
===================================================================
--- z3c.metrics/trunk/z3c/metrics/verify.txt	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/verify.txt	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,152 @@
+;-*-Doctest-*-
+
+======================
+Verify Implementations
+======================
+
+Verify that the component implementations fulfill their interfaces.
+
+    >>> from zope.interface import verify
+    >>> from z3c.metrics import interfaces
+
+Scale
+=====
+
+    >>> from z3c.metrics import scale
+
+    >>> verify.verifyClass(
+    ...     interfaces.IScale, scale.ExponentialScale)
+    True
+    >>> verify.verifyObject(
+    ...     interfaces.IScale, scale.ExponentialScale())
+    True
+
+    >>> verify.verifyClass(
+    ...     interfaces.IScale, scale.ExponentialDatetimeScale)
+    True
+    >>> verify.verifyObject(
+    ...     interfaces.IScale, scale.ExponentialDatetimeScale())
+    True
+
+Index
+=====
+
+    >>> from z3c.metrics import index
+
+    >>> verify.verifyClass(
+    ...     interfaces.IIndex, index.Index)
+    True
+    >>> verify.verifyObject(
+    ...     interfaces.IIndex, index.Index())
+    True
+
+Metric
+======
+
+    >>> from zope import interface 
+    >>> from z3c.metrics import metric
+
+    >>> verify.verifyClass(
+    ...     interfaces.IMetric, metric.InitMetric)
+    True
+    >>> verify.verifyObject(
+    ...     interfaces.IMetric, metric.InitMetric())
+    True
+
+    >>> verify.verifyClass(
+    ...     interfaces.IMetric, metric.AttributeMetric)
+    True
+    >>> verify.verifyObject(
+    ...     interfaces.IMetric, metric.AttributeMetric(
+    ...         interface=interface.Interface, field_name='foo'))
+    True
+
+    >>> verify.verifyClass(
+    ...     interfaces.IMetric, metric.SelfMetric)
+    True
+    >>> verify.verifyObject(
+    ...     interfaces.IMetric, metric.SelfMetric(
+    ...         interface=interface.Interface, field_name='foo'))
+    True
+
+    >>> verify.verifyClass(
+    ...     interfaces.IMetric, metric.OtherMetric)
+    True
+    >>> verify.verifyObject(
+    ...     interfaces.IMetric, metric.OtherMetric(
+    ...         interface=interface.Interface, field_name='foo'))
+    True
+
+Subscriptions
+=============
+
+    >>> from z3c.metrics import subscription
+
+    >>> verify.verifyClass(
+    ...     interfaces.ISubscription,
+    ...     subscription.UtilitySubscription)
+    True
+    >>> verify.verifyObject(
+    ...     interfaces.ISubscription,
+    ...     subscription.UtilitySubscription(utility_interface=None))
+    True
+
+    >>> verify.verifyClass(
+    ...     interfaces.ISubscription,
+    ...     subscription.LocalSubscription)
+    True
+    >>> verify.verifyObject(
+    ...     interfaces.ISubscription,
+    ...     subscription.LocalSubscription(index=None))
+    True
+
+    >>> verify.verifyClass(
+    ...     interfaces.IWeightedSubscription,
+    ...     subscription.WeightedSubscription)
+    True
+    >>> verify.verifyObject(
+    ...     interfaces.IWeightedSubscription,
+    ...     subscription.WeightedSubscription())
+    True
+
+    >>> verify.verifyClass(
+    ...     interfaces.ISubscription,
+    ...     subscription.UtilityWeightedSubscription)
+    True
+    >>> sub = subscription.UtilityWeightedSubscription(
+    ...         utility_interface=None)
+    >>> verify.verifyObject(interfaces.ISubscription, sub)
+    True
+    >>> verify.verifyClass(
+    ...     interfaces.IWeightedSubscription,
+    ...     subscription.UtilityWeightedSubscription)
+    True
+    >>> verify.verifyObject(interfaces.IWeightedSubscription, sub)
+    True
+
+    >>> verify.verifyClass(
+    ...     interfaces.ISubscription,
+    ...     subscription.LocalWeightedSubscription)
+    True
+    >>> sub = subscription.LocalWeightedSubscription(
+    ...     index=index.Index())
+    >>> verify.verifyObject(interfaces.ISubscription, sub)
+    True
+    >>> verify.verifyClass(
+    ...     interfaces.IWeightedSubscription,
+    ...     subscription.LocalWeightedSubscription)
+    True
+    >>> verify.verifyObject(interfaces.IWeightedSubscription, sub)
+    True
+
+Engines
+=======
+
+    >>> from z3c.metrics import engine
+    >>> verify.verifyClass(
+    ...     interfaces.IEngine, engine.WeightedEngine)
+    True
+    >>> verify.verifyObject(
+    ...     interfaces.IEngine, engine.WeightedEngine(
+    ...         metric=None, subscription=sub, context=None))
+    True

Added: z3c.metrics/trunk/z3c/metrics/zope2/__init__.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/__init__.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/__init__.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1 @@
+"""CMF support for z3c.metrics"""

Added: z3c.metrics/trunk/z3c/metrics/zope2/at.zcml
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/at.zcml	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/at.zcml	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,10 @@
+<configure 
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:i18n="http://namespaces.zope.org/i18n"
+    i18n_domain="thetech.virtual">
+
+  <class class="Products.Archetypes.ExtensibleMetadata.ExtensibleMetadata">
+    <implements interface="Products.CMFCore.interfaces.ICatalogableDublinCore"  />
+  </class>
+
+</configure>

Added: z3c.metrics/trunk/z3c/metrics/zope2/catalog.txt
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/catalog.txt	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/catalog.txt	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,41 @@
+;-*-Doctest-*-
+
+====================
+ZCatalog Integration
+====================
+
+In order to support scoring ZCatalog queries, metric indexes must use
+docids from the ZCatalog.  Since the OFS events that CMF uses to index
+objects in the ZCatalog can't be counted on to run before our events,
+scores are initialized when the object is first indexed in the catalog.
+
+Create a catalog.
+
+    >>> from zope import component
+    >>> from Products.ZCatalog import ZCatalog
+    >>> ZCatalog.manage_addZCatalog(app, 'catalog', 'Catalog')
+    >>> catalog = app.catalog
+
+Add a metrics index to the catalog.
+
+    >>> catalog.addIndex(name='index', type='MetricsIndex')
+
+The index is registered as a utility.
+
+    >>> from z3c.metrics import interfaces
+    >>> index = catalog._catalog.getIndex('index')
+    >>> component.getUtility(interfaces.IIndex) is index.aq_base
+    True
+
+Ctalog the object so that it will have a docid.
+
+    >>> catalog.catalog_object(folder)
+
+Initialize the object's score.
+
+    >>> index.initScoreFor(folder)
+
+Get the object's score.
+
+    >>> index.getScoreFor(folder)
+    0.0

Added: z3c.metrics/trunk/z3c/metrics/zope2/configure.zcml
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/configure.zcml	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/configure.zcml	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,13 @@
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   i18n_domain="zope">
+
+  <adapter factory=".creator.CreatorLookup" />
+  <adapter factory=".creator.Creators" />
+
+  <include file="container.zcml" />
+  <include file="index.zcml" />
+  <include file="discussion.zcml" />
+  <include file="at.zcml" />
+
+</configure>

Added: z3c.metrics/trunk/z3c/metrics/zope2/container.zcml
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/container.zcml	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/container.zcml	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,23 @@
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   i18n_domain="zope">
+
+  <adapter factory=".ofs.getAncestors" />
+
+  <class class="zope.app.container.contained.ObjectAddedEvent">
+    <implements interface="z3c.metrics.interfaces.IAddValueEvent"  />
+  </class>  
+
+  <class class="zope.app.container.contained.ObjectMovedEvent">
+    <implements interface="z3c.metrics.interfaces.IChangeScoreEvent"  />
+  </class>
+
+  <class class="zope.app.container.contained.ObjectRemovedEvent">
+    <implements interface="z3c.metrics.interfaces.IRemoveValueEvent"  />
+  </class>  
+
+  <class class="z3c.metrics.index.BuildScoreEvent">
+    <implements interface=".index.IAddSelfValueEvent"  />
+  </class>  
+
+</configure>

Added: z3c.metrics/trunk/z3c/metrics/zope2/creator.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/creator.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/creator.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,31 @@
+from zope import interface, component
+from zope.cachedescriptors import property
+
+from Products.CMFCore import interfaces as cmf_ifaces
+from Products.CMFCore import utils as cmf_utils
+
+from z3c.metrics import interfaces
+
+class CreatorLookup(object):
+    interface.implements(interfaces.ICreatorLookup)
+    component.adapts(cmf_ifaces.ICatalogableDublinCore)
+
+    def __init__(self, context):
+        self.portal_membership = cmf_utils.getToolByName(
+            context, 'portal_membership')
+
+    def __call__(self, creator_id):
+        return self.portal_membership.getMemberById(creator_id)
+
+class Creators(object):
+    interface.implements(interfaces.ICreated)
+    component.adapts(cmf_ifaces.ICatalogableDublinCore)
+
+    def __init__(self, context):
+        self.context = context
+
+    @property.Lazy
+    def creators(self):
+        return self.context.listCreators()
+
+    

Added: z3c.metrics/trunk/z3c/metrics/zope2/discussion.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/discussion.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/discussion.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,72 @@
+from zope import interface, component
+from zope.testing import cleanup
+import zope.event
+from zope.app.container import contained
+
+import Acquisition
+
+from Products.CMFCore import interfaces as cmf_ifaces
+from Products.CMFCore import utils as cmf_utils
+from Products.CMFDefault import DiscussionItem
+
+from z3c.metrics import interfaces
+
+class ReplyCreatedEvent(contained.ObjectAddedEvent):
+    interface.implementsOnly(interfaces.IChangeScoreEvent,
+                             interfaces.IAddValueEvent)
+
+class ReplyDeletedEvent(contained.ObjectRemovedEvent):
+    interface.implementsOnly(interfaces.IChangeScoreEvent,
+                             interfaces.IRemoveValueEvent)
+    
+def createReply(self, *args, **kw):
+    reply_id = createReply.orig(
+        self, *args, **kw)
+    zope.event.notify(ReplyCreatedEvent(
+        object=self.getReply(reply_id), newParent=self,
+        newName=reply_id))
+    return reply_id
+createReply.orig = DiscussionItem.DiscussionItemContainer.createReply
+    
+def deleteReply(self, reply_id, *args, **kw):
+    reply = self.getReply(reply_id)
+    reply_id = deleteReply.orig(self, reply_id, *args, **kw)
+    zope.event.notify(ReplyDeletedEvent(
+        object=reply, oldParent=self, oldName=reply_id))
+    return reply_id
+deleteReply.orig = DiscussionItem.DiscussionItemContainer.deleteReply
+
+def patch():
+    DiscussionItem.DiscussionItemContainer.createReply = createReply
+    DiscussionItem.DiscussionItemContainer.deleteReply = deleteReply
+
+def unpatch():
+    DiscussionItem.DiscussionItemContainer.createReply = (
+        createReply.orig)
+    DiscussionItem.DiscussionItemContainer.deleteReply = (
+        deleteReply.orig)
+
+patch()
+cleanup.addCleanUp(unpatch)
+
+ at component.adapter(cmf_ifaces.IDiscussionResponse,
+                   interfaces.IChangeScoreEvent)
+def dispatchToDiscussed(obj, event):
+    parent = event.newParent
+    if parent is None:
+        parent = event.oldParent
+    discussed = Acquisition.aq_parent(Acquisition.aq_inner(parent))
+    for _ in component.subscribers(
+        [obj, event, discussed], None):
+        pass # Just make sure the handlers run
+
+ at component.adapter(cmf_ifaces.IDiscussable,
+                   interfaces.IBuildScoreEvent)
+def dispatchToReplies(obj, event):
+    portal_discussion = cmf_utils.getToolByName(
+        obj, 'portal_discussion')
+    for reply in portal_discussion.getDiscussionFor(obj).getReplies():
+        for _ in component.subscribers(
+            [reply, event, obj], None):
+            pass # Just make sure the handlers run
+    

Added: z3c.metrics/trunk/z3c/metrics/zope2/discussion.zcml
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/discussion.zcml	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/discussion.zcml	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,13 @@
+<configure 
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:five="http://namespaces.zope.org/five"
+    xmlns:i18n="http://namespaces.zope.org/i18n"
+    i18n_domain="thetech.virtual">
+
+  <subscriber handler=".discussion.dispatchToDiscussed" />
+
+  <!-- Apply the patches -->
+  <class class=".discussion.ReplyCreatedEvent" />
+
+
+</configure>

Added: z3c.metrics/trunk/z3c/metrics/zope2/dispatch.zcml
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/dispatch.zcml	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/dispatch.zcml	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,24 @@
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   i18n_domain="zope">
+
+  <subscriber 
+     handler="z3c.metrics.dispatch.dispatchToAncestors" 
+     for="OFS.interfaces.IItem
+          z3c.metrics.interfaces.IChangeScoreEvent"
+     />
+  <subscriber 
+     handler="z3c.metrics.dispatch.dispatchToDescendants" 
+     for="OFS.interfaces.IObjectManager
+          z3c.metrics.interfaces.IBuildScoreEvent"
+     />
+  <subscriber 
+     handler="z3c.metrics.dispatch.dispatchToDescendants" 
+     for="OFS.interfaces.IObjectManager
+          z3c.metrics.interfaces.IBuildScoreEvent
+          OFS.interfaces.IObjectManager"
+     />
+
+  <subscriber handler=".discussion.dispatchToReplies" />
+
+</configure>

Added: z3c.metrics/trunk/z3c/metrics/zope2/index.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/index.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/index.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,118 @@
+from zope import interface, component
+from zope.dottedname import resolve
+
+import DateTime
+import Acquisition
+from OFS import SimpleItem
+from Products.ZCatalog import interfaces as zcatalog_ifaces
+from Products.PluginIndexes import interfaces as plugidx_ifaces
+from Products.PluginIndexes.TextIndex import Vocabulary
+
+from z3c.metrics import interfaces, index
+from z3c.metrics.zope2 import scale
+
+class IRemoveScoreEvent(interfaces.IRemoveValueEvent):
+    """Remove the object score from the index."""
+
+class IAddSelfValueEvent(interfaces.IAddValueEvent):
+    """Add self value with special handling for the index."""
+    # This is necessary because for the OFS/CMF/ZCatalog mess we need
+    # the self add handlers to trigger for initial indexing and
+    # rebuilding scores but not on object add
+
+class InitIndexScoreEvent(index.IndexesScoreEvent):
+    interface.implements(interfaces.IInitScoreEvent,
+                         IAddSelfValueEvent)
+
+class RemoveIndexScoreEvent(index.IndexesScoreEvent):
+    interface.implements(IRemoveScoreEvent)
+
+class MetricsIndex(index.Index, SimpleItem.SimpleItem):
+    interface.implements(plugidx_ifaces.IPluggableIndex)
+
+    def __init__(self, id, extra=None, caller=None):
+        self.id = id
+        self.__catalog_path = caller.getPhysicalPath()
+
+        if extra is None:
+            extra = Vocabulary._extra()
+
+        # TODO: the utility registration should be moved to an INode
+        # GS handler to be run after the index is added
+        utility_interface = extra.__dict__.pop(
+            'utility_interface', interfaces.IIndex)
+        utility_name = extra.__dict__.pop('utility_name', '')
+
+        scale_kw = {}
+        if 'start' in extra.__dict__:
+            scale_kw['start'] = DateTime.DateTime(
+                extra.__dict__.pop('start'))
+        if 'scale_unit' in extra.__dict__:
+            scale_kw['scale_unit'] = float(
+                extra.__dict__.pop('scale_unit'))
+        index_scale = scale.ExponentialDateTimeScale(**scale_kw)
+
+        super(MetricsIndex, self).__init__(
+            scale=index_scale, **extra.__dict__)
+
+        if isinstance(utility_interface, (str, unicode)):
+            utility_interface = resolve.resolve(utility_interface)
+        if not utility_interface.providedBy(self):
+            interface.alsoProvides(self, utility_interface)
+        sm = component.getSiteManager(context=caller)
+        reg = getattr(sm, 'registerUtility', None)
+        if reg is None:
+            reg = sm.provideUtility
+        reg(utility_interface, self, utility_name)
+
+    def _getCatalog(self):
+        zcatalog = Acquisition.aq_parent(Acquisition.aq_inner(
+            Acquisition.aq_parent(Acquisition.aq_inner(self))))
+        if not zcatalog_ifaces.IZCatalog.providedBy(zcatalog):
+            return self.restrictedTraverse(self.__catalog_path)
+        return zcatalog
+
+    def _getKeyFor(self, obj):
+        """Get the key from the ZCatalog so that the index may be used
+        to score or sort ZCatalog results."""
+        return self._getCatalog().getrid(
+            '/'.join(obj.getPhysicalPath()))
+
+    def index_object(self, documentId, obj, threshold=None):
+        """Run the initialize score metrics for this index only if
+        this is the first time the object is indexed."""
+        if documentId not in self._scores:
+            obj = self._getCatalog().getobject(documentId)
+            event = InitIndexScoreEvent(obj, [self])
+            component.subscribers([obj, event], None)
+            return True
+        return False
+
+    def unindex_object(self, documentId):
+        """Run the remove value metrics for this index only when the
+        object is unindexed."""
+        obj = self._getCatalog().getobject(documentId)
+        event = RemoveIndexScoreEvent(obj, [self])
+        component.subscribers([obj, event], None)
+
+# XXX Old notes
+
+# This is a tough call.  We can't depend on event ordering
+# adding the object to the catalog before the event that adds
+# the object to the metric index.
+
+# The solution below is to add the object to the catalog
+# without actually indexing it yet so that it has an id in the
+# catalog.
+
+# A more appropriate solution might be to fire an event when
+# the object is added to the catalog and subscribe to that
+# event rather than to IObjectAddedEvent.  This solution would
+# involve, however, monkey pacthing CMFCatalogAware.
+
+# So the choice is between monkey patch or abusing the
+# catalog.  Until there is obvious reason otherwise, we'll
+# abuse the catalog.
+
+# On second thought, let's just make the score index an actual
+# index in the catalog

Added: z3c.metrics/trunk/z3c/metrics/zope2/index.zcml
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/index.zcml	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/index.zcml	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,17 @@
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   xmlns:five="http://namespaces.zope.org/five"
+   i18n_domain="zope">
+
+  <permission
+    id="zope2.AddPluggableIndex"
+    title="Add Pluggable Index"
+    />
+
+  <five:registerClass
+      class=".index.MetricsIndex"
+      meta_type="MetricsIndex"
+      permission="zope2.AddPluggableIndex"
+      />
+
+</configure>

Added: z3c.metrics/trunk/z3c/metrics/zope2/meta.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/meta.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/meta.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,17 @@
+from z3c.metrics import meta
+from z3c.metrics.zope2 import index, ofs
+
+class InitMetric(meta.InitMetric):
+
+    metric_factory = ofs.InitMetric
+    remove_interface = index.IRemoveScoreEvent
+
+class SelfMetric(meta.SelfMetric):
+
+    metric_factory = ofs.SelfMetric
+    add_interface = index.IAddSelfValueEvent
+    remove_interface = index.IRemoveScoreEvent
+
+class OtherMetric(meta.OtherMetric):
+
+    metric_factory = ofs.OtherMetric

Added: z3c.metrics/trunk/z3c/metrics/zope2/meta.zcml
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/meta.zcml	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/meta.zcml	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,33 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:meta="http://namespaces.zope.org/meta">
+
+  <meta:directives namespace="http://namespaces.zope.org/metrics">
+    
+    <meta:groupingDirective
+       name="init"
+       schema="z3c.metrics.meta.IMetric"
+       handler=".meta.InitMetric" />
+       />
+
+    <meta:groupingDirective
+       name="self"
+       schema="z3c.metrics.meta.IAttributeMetric"
+       handler=".meta.SelfMetric" />
+       />
+
+    <meta:groupingDirective
+       name="other"
+       schema="z3c.metrics.meta.IAttributeMetric"
+       handler=".meta.OtherMetric" />
+       />
+      
+    <meta:directive
+       name="weighted"
+       schema="z3c.metrics.interfaces.IUtilityWeightedSubscription"
+       handler="z3c.metrics.meta.weighted"
+       />
+
+  </meta:directives>
+
+</configure>

Added: z3c.metrics/trunk/z3c/metrics/zope2/ofs.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/ofs.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/ofs.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,37 @@
+from zope import interface, component
+
+import Acquisition
+from OFS import interfaces as ofs_ifaces
+
+from z3c.metrics import interfaces, metric, dispatch
+
+ at component.adapter(ofs_ifaces.IItem)
+ at interface.implementer(dispatch.IAncestors)
+def getAncestors(contained):
+    ancestor = contained
+    while ancestor is not None:
+        yield ancestor
+        ancestor = Acquisition.aq_parent(
+            Acquisition.aq_inner(ancestor))
+
+class InitMetric(metric.InitMetric):
+
+    @component.adapter(interface.Interface,
+                       interfaces.IRemoveValueEvent)
+    def removeSelfScore(self, obj, event):
+        if not ofs_ifaces.IObjectWillBeAddedEvent.providedBy(event):
+            super(InitMetric, self).removeSelfScore(obj, event)
+
+class SelfMetric(InitMetric, metric.SelfMetric):
+    pass
+
+class OtherMetric(metric.OtherMetric):
+
+    @component.adapter(interface.Interface,
+                       interfaces.IRemoveValueEvent,
+                       interface.Interface)
+    def removeOtherValue(self, other, event, obj):
+        if not ofs_ifaces.IObjectWillBeAddedEvent.providedBy(event):
+            super(OtherMetric, self).removeOtherValue(
+                other, event, obj)
+

Added: z3c.metrics/trunk/z3c/metrics/zope2/scale.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/scale.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/scale.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,28 @@
+from zope.cachedescriptors import property
+
+from z3c.metrics import scale
+
+import DateTime
+
+epoch = DateTime.DateTime(0)
+one_year = 365
+
+class ExponentialDateTimeScale(scale.ExponentialDatetimeScale):
+
+    def __init__(self, scale_unit=one_year, scale_ratio=2,
+                 start=epoch, min_unit=None):
+        super(ExponentialDateTimeScale, self).__init__(
+            scale_unit=scale_unit, scale_ratio=scale_ratio,
+            start=start, min_unit=min_unit)
+
+    @property.readproperty
+    def default(self):
+        return DateTime.DateTime()
+
+    def _fromDelta(self, delta):
+        """DateTime deltas are just a float of days"""
+        return delta
+
+    def _toDelta(self, quantity):
+        """DateTime deltas are just a float of days"""
+        return quantity

Added: z3c.metrics/trunk/z3c/metrics/zope2/teamspace.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/teamspace.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/teamspace.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,42 @@
+from zope import component
+
+from Products.CMFCore import utils as cmf_utils
+
+from Products.remember import interfaces as remember_ifaces
+from Products.TeamSpace import interfaces as ts_ifaces
+
+from z3c.metrics import interfaces
+
+ at component.adapter(ts_ifaces.ITeamMembership,
+                   interfaces.IChangeScoreEvent)
+def dispatchToSpaces(membership, event):
+    for space in membership.getTeam().getTeamSpaces():
+        for _ in component.subscribers(
+            [membership, event, space], None):
+            pass # Just make sure the handlers run
+
+ at component.adapter(ts_ifaces.ISpace,
+                   interfaces.IBuildScoreEvent)
+def dispatchToTeamMemberships(space, event):
+    for team in space.getSpaceTeams():
+        for membership in team.getMemberships():
+            for _ in component.subscribers(
+                [membership, event, space], None):
+                pass # Just make sure the handlers run
+
+ at component.adapter(ts_ifaces.ITeamMembership,
+                   interfaces.IChangeScoreEvent)
+def dispatchToMember(membership, event):
+    for _ in component.subscribers(
+        [membership, event, membership.getMember()], None):
+        pass # Just make sure the handlers run
+    
+ at component.adapter(remember_ifaces.IReMember,
+                   interfaces.IBuildScoreEvent)
+def dispatchToMemberMemberships(member, event):
+    portal_teams = cmf_utils.getToolByName(member, 'portal_teams')
+    for membership in portal_teams.getTeamMembershipsFor(
+        member.getId()):
+        for _ in component.subscribers(
+            [membership, event, member], None):
+            pass # Just make sure the handlers run

Added: z3c.metrics/trunk/z3c/metrics/zope2/teamspace.zcml
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/teamspace.zcml	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/teamspace.zcml	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,23 @@
+<configure 
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:five="http://namespaces.zope.org/five"
+    xmlns:i18n="http://namespaces.zope.org/i18n"
+    i18n_domain="thetech.virtual">
+
+  <five:bridge
+      zope2="Products.TeamSpace.interfaces.membership.ITeamMembership"
+      package="Products.TeamSpace.interfaces"
+      />
+  <five:bridge
+      zope2="Products.TeamSpace.interfaces.space.ISpace"
+      package="Products.TeamSpace.interfaces"
+      />
+
+  <class class="Products.TeamSpace.membership.TeamMembership">
+    <implements interface="Products.TeamSpace.interfaces.ITeamMembership"  />
+  </class>  
+  <class class="Products.TeamSpace.space.TeamSpaceMixin">
+    <implements interface="Products.TeamSpace.interfaces.ISpace"  />
+  </class>  
+
+</configure>

Added: z3c.metrics/trunk/z3c/metrics/zope2/testing.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/testing.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/testing.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,41 @@
+from zope.testing import cleanup
+
+import DateTime
+
+def freezeATDefaultDate(datetime_, cls, field_name='creation_date'):
+    field = cls.schema[field_name]
+
+    # If this is the first time we've touched the field, keep the
+    # original default method around to restore on cleanup
+    if not hasattr(field, 'default_method_orig'):
+        field.default_method_orig = field.default_method
+        def cleanUp():
+            field.default_method = field.default_method_orig
+            del field.default_method_orig
+        cleanup.addCleanUp(cleanUp)
+
+    field.default_method = lambda: datetime_
+
+def freezeATDefaultDates(datetime_, classes,
+                         field_name='creation_date'):
+    for cls in classes:
+        freezeATDefaultDate(datetime_, cls, field_name)
+
+class FrozenDateTime(DateTime.DateTime):
+
+    def __init__(self, *args, **kw):
+        if not (args or kw):
+            return DateTime.DateTime.__init__(self, self._frozen)
+        return DateTime.DateTime.__init__(self, *args, **kw)
+
+def freezeDateTime(datetime_, module):
+    def cleanUp():
+        if hasattr(module.DateTime, 'orig'):
+            module.DateTime = module.DateTime.orig
+    cleanUp()
+
+    FrozenDateTime.orig = module.DateTime
+    FrozenDateTime._frozen = datetime_
+    module.DateTime = FrozenDateTime
+
+    cleanup.addCleanUp(cleanUp)

Added: z3c.metrics/trunk/z3c/metrics/zope2/testing.zcml
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/testing.zcml	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/testing.zcml	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,9 @@
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   i18n_domain="zope">
+
+  <include package="Products.Five" file="meta.zcml" />
+
+  <include file="index.zcml" />
+
+</configure>

Added: z3c.metrics/trunk/z3c/metrics/zope2/tests.py
===================================================================
--- z3c.metrics/trunk/z3c/metrics/zope2/tests.py	                        (rev 0)
+++ z3c.metrics/trunk/z3c/metrics/zope2/tests.py	2008-04-16 07:05:05 UTC (rev 85434)
@@ -0,0 +1,28 @@
+import unittest
+from zope.testing import doctest, cleanup
+
+from Testing import ZopeTestCase
+
+from zope.configuration import xmlconfig
+
+import z3c.metrics.zope2
+
+def setUp(test):
+    cleanup.setUp()
+    xmlconfig.file('testing.zcml', z3c.metrics.zope2)
+
+def tearDown(test):
+    cleanup.tearDown()
+
+def test_suite():
+    return ZopeTestCase.ZopeDocFileSuite(
+        'catalog.txt',
+        setUp=setUp, tearDown=tearDown,
+        optionflags=(
+            doctest.REPORT_NDIFF|
+            #doctest.REPORT_ONLY_FIRST_FAILURE|
+            doctest.NORMALIZE_WHITESPACE|
+            doctest.ELLIPSIS))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')



More information about the Checkins mailing list