add new 'link' package for general-purpose link and relation management

git-svn-id: svn://svn.cy55.de/Zope3/src/cybertools/trunk@3678 fd906abe-77d9-0310-91a1-e0d9ade77398
This commit is contained in:
helmutm 2010-01-13 14:00:27 +00:00
parent e82034c31e
commit 24157bb323
5 changed files with 371 additions and 0 deletions

55
link/README.txt Normal file
View file

@ -0,0 +1,55 @@
============================================
General-purpose Link and Relation Management
============================================
($Id$)
Basic setup
-----------
>>> from zope import component
>>> from cybertools.link.tests import IntIdsStub
>>> intids = IntIdsStub()
>>> component.provideUtility(intids)
>>> from cybertools.link.base import LinkManager
>>> links = LinkManager()
Create and link objects
-----------------------
We create a simple class to derive objects from it.
>>> class Page(object):
... pass
>>> p1 = Page()
>>> p2 = Page()
These objects have to be registered with the IntIds utility.
>>> intids.register(p1)
0
>>> intids.register(p2)
1
Now we can create a link from p1 to p2.
Usually the link gets a name that is related to the target.
>>> l01 = links.createLink(name='p2', source=p1, target=p2)
Let's have a look at the newly created link and the default values of some
of its attributes.
>>> (l01.identifier, l01.source, l01.target, l01.name, l01.linkType, l01.state,
... l01.relevance, l01.order)
(1, 0, 1, 'p2', u'link', u'valid', 1.0, 0)
Query for links
---------------
We are now able to query the link manager for links, e.g. using name and
source for finding all corresponding links on a page.
>>> [l.identifier for l in links.query(name='p2', source=p1)]
[1]

3
link/__init__.py Normal file
View file

@ -0,0 +1,3 @@
"""
$Id$
"""

170
link/base.py Normal file
View file

@ -0,0 +1,170 @@
#
# Copyright (c) 2010 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
"""
A simple generic, general-purpose link management framework.
$Id$
"""
from BTrees.IFBTree import intersection, union
from BTrees.IOBTree import IOBTree
from persistent import Persistent
from zope import component
from zope.component import adapts
from zope.index.field import FieldIndex
from zope.interface import implements
from zope.app.intid.interfaces import IIntIds
from cybertools.link.interfaces import ILinkManager, ILink
class LinkManager(Persistent):
""" A manager (storage, registry) for link objects.
"""
implements(ILinkManager)
uid = int
# initialization
def __init__(self):
self.links = IOBTree()
self.setupIndexes()
self.currentId = 0
def setupIndexes(self):
self.indexes = dict(
name=FieldIndex(),
source=FieldIndex(),
target=FieldIndex())
# public methods
def createLink(self, **kw):
if not 'name' in kw or not 'source' in kw:
raise ValueError("At least 'name' and 'source' attributes must "
"be given.")
identifier = self.generateIdentifier()
link = Link(self, identifier)
link.update(**kw)
self.getLinks()[identifier] = link
self.indexLink(link)
return link
def removeLink(self, link):
del self.getLinks()[link.identifier]
def getLink(self, identifier):
return self.getLinks()[identifier]
def query(self, name=None, source=None, target=None, **kw):
source = self.getUniqueId(source)
target = self.getUniqueId(target)
r = None
if name is not None:
r = self.indexes['name'].apply((name, name))
if source is not None:
r = self.intersect(r, self.indexes['source'].apply((source, source)))
if target is not None:
r = self.intersect(r, self.indexes['target'].apply((target, target)))
if r is None:
raise ValueError("At least one critera of 'name', 'source', or "
"'target' must be given.")
result = [self.getLink(id) for id in r]
#for k, v in kw:
# result = [link for link in result if getattr(link, k) == v]
return sorted(result, key=lambda x: (x.order, x.name))
def __iter__(self):
return self.getLinks().values()
# protected methods
def getLinks(self):
return self.links
def generateIdentifier(self):
self.currentId += 1
return self.currentId
def indexLink(self, link):
for attr, idx in self.indexes.items():
value = getattr(link, attr)
if value is None:
idx.unindex_doc(link.identifier)
else:
idx.index_doc(link.identifier, value)
def intersect(self, r1, r2):
return r1 is None and r2 or intersection(r1, r2)
def getUniqueId(self, obj):
if obj is None:
return None
if isinstance(obj, self.uid):
return obj
# TODO: take external objects (referenced by URIs) in to account
return component.getUtility(IIntIds).getId(obj)
def getObject(self, uid):
return component.getUtility(IIntIds).getObject(uid)
class Link(Persistent):
""" A basic link implementation.
"""
implements(ILink)
defaults = dict(target=None,
linkType=u'link',
state=u'valid',
relevance=1.0,
order=0,)
def __init__(self, manager, identifier):
self.manager = self.__parent__ = manager
self.identifier = self.__name__ = identifier
def getManager(self):
return self.manager
def getSource(self):
return self.getManager().getObject(self.source)
def getTarget(self):
return self.getManager().getObject(self.target)
def update(self, **kw):
manager = self.getManager()
for k in ('source', 'target'):
kw[k] = manager.getUniqueId(kw[k])
for k, v in kw.items():
setattr(self, k, v)
for k in manager.indexes:
if k in kw:
manager.indexLink(self)
break
def __getattr__(self, attr):
if attr not in ILink:
raise AttributeError(attr)
value = self.defaults.get(attr, u'')
return value

92
link/interfaces.py Normal file
View file

@ -0,0 +1,92 @@
#
# Copyright (c) 2010 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
"""
Interfaces for Wiki functionality.
$Id$
"""
from zope.interface import Interface, Attribute
from zope import schema
class ILinkManager(Interface):
""" Manages (and possibly contains) all kinds of wiki-related links.
"""
def createLink(name, source, target, **kw):
""" Create, register, and return a link record.
Optional attributes are given as keyword arguments.
"""
def removeLink(link):
""" Remove a link.
"""
def getLink(identifier):
""" Return the link record identfied by the identifier given or None if
not present.
"""
def query(name=None, source=None, target=None, **kw):
""" Search for link records matching the criteria given. One of
name, source or target must be given.
"""
def __iter__():
""" Return an iterator of all links.
"""
class ILink(Interface):
""" A directed link (connection, relation) between two local or foreign
objects.
The combination of name and source usually uniquely identfy a link.
"""
identifier = Attribute('An internal identifier of the link record, '
'unique within the link manager.')
name = Attribute('The external identifier for the link, i.e. the '
'string used in the source text to address the link.')
source = Attribute('Identifier of the link\'s source object.')
target = Attribute('Identifier of the link\'s target object or - '
'for external links - the target URI.')
linkType = Attribute('Optional: A short string specifying the type of the '
'link, a sort of predicate; default: "link".')
title = Attribute('Optional: A short text, may be used as the default text for '
'the link or for the alt tag of an image.')
description = Attribute('Optional: some text, may be used as a title attribute.')
state = Attribute('Optional: A short string denoting the state of the link '
'entry; default: "valid"')
relevance = Attribute('Optional: A float number between 0.0 and 1.0 denoting '
'the relevance of the connection between source and target; '
'default: 1.0.')
order = Attribute('Optional: An integer that may be used when providing an '
'ordered listing of links; default: 0.')
targetFragment = Attribute('Optional: an address part leading to a '
'text anchor or the part of an image.')
targetParameters = Attribute('Optional: a dictionary of URI parameters '
'that will have to be appended to the link to the target object.')
creator = Attribute('Optional: a string denoting the creator of the record.')
def getManager():
""" Return the link manager this link is managed by.
"""

51
link/tests.py Executable file
View file

@ -0,0 +1,51 @@
# $Id$
import unittest
from zope.testing.doctestunit import DocFileSuite
from zope.interface.verify import verifyClass
from zope.interface import implements
from zope.app.intid.interfaces import IIntIds
class IntIdsStub(object):
"""A testing stub (mock utility) for IntIds."""
implements(IIntIds)
def __init__(self):
self.objs = []
def getObject(self, uid):
return self.objs[uid]
def register(self, ob):
if ob not in self.objs:
self.objs.append(ob)
return self.objs.index(ob)
getId = register
queryId = getId
def unregister(self, ob):
id = self.getId(ob)
self.objs[id] = None
def __iter__(self):
return iter(xrange(len(self.objs)))
class TestLink(unittest.TestCase):
"Basic tests for the link package."
def testBasics(self):
pass
def test_suite():
return unittest.TestSuite((
unittest.makeSuite(TestLink),
DocFileSuite('README.txt'),
))
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')