Introduction
Database wrapper implementation. This wrapper can be used to have easy access to roundup's hyper database without having to know too much knowledge about the class structures and (multi) links.
This document contains a few examples. In those cases
you will see objects like db, dbw, cl, clw
and nodew. These object are:
db : hyperdb.Database
cl : hyperdb.Class (or FileClass or IssueClass)
dbw : dbwrapper.DBWrapper
clw : dbwrapper.ClassWrapper
nodew : dbwrapper.NodeClass
lw : dbwrapper.ListWrapper
Features
Addressing
Addressing a specific node in a class is easier with this
wrapper. It almost has the looks and feels of the wrapper
used in the TAL templates. Also with this wrapper you don't
really need to use get methods to get the data. Just address
the data as if they are member of a tree with the database
object as root. The class objects as limbs and the class
properties as leaves. What's left is how to address the right
node. Well that can be done with indexing the class (sorry
but I don't know were to put that in a tree). As index you
can either use the node id as integer or string, but you
may also use a key value (if the class has a key column).
Here are some examples:
- dbw.issue[1].title
dbw.issue['1'].title
Both these examples will return the title of issue 1.
- dbw.priority['urgent'].name
This example will return 'urgent'. Not really
meaningful, but it's only meant as an example.
- dbw.status['unread'].id
Will return the nodeid of status node 'unread'.
In general the benefit of this wrapper is that you don't
really have to know any of the node ids. All set or indexing
methods will resolve key values into id's. Even so will all
get methods return the key values and not the id's when it
comes to linked items.
Searching
The wrapper is capable of handling regular expression searches.
It will not only return you the found nodes, but it also will
return the found matches per node.
Usage
To use the wrapper you need to place it somewhere. In our case
we placed it in a sub-folder of roundup's core code which remains
in the site-packages folder of the Python library tree.
In this /lib/site-packages/roundup we created a folder named
extensions. This is about conform the standard that Richard uses
for additional stuff in tracker templates (we look at the wrapper
as additional stuff for the roundup core code). In other words
the complete path to the wrapper will be:
/lib/site-packages/roundup/extensions/dbwrapper.py
It will be easiest if the wrapper can be imported as sub package.
This will require a __init__.py file in that directory to. The
content of that file will only be this line:
__all__ = [ dbwrapper ]
What's left is importing the wrapper in your Python sources (e.g.
detectors and action handlers). You do that by adding the line:
from roundup.extensions import dbwrapper
to all sources in which you want to use the wrapper.
There's only one comment left. As you can see, the wrapper has a
lot of documentation strings inside. Therefor we suggest to run
all scripts with the -OO option passed to Python's interpreter.
This will prevent that your scripts will load these documentation
strings too. If you're not sure, then please read section
6.1.2 Compiled Python files of the tutorial in the Python
documentation.
Detectors
The wrapper can easily be used in a detector without having to
change anything to roundup's core code.
This is what you do in an auditor:
def auditor(db, cl, nodeid, newvalues):
dbw = dbwrapper.DBWrapper(db)
clw = dbwrapper.ClassWrapper(dbw, cl)
nodew = dbwrapper.NodeWrapper(clw, nodeid)
And this in a reactor:
def reactor(db, cl, nodeid, oldvalues):
dbw = dbwrapper.DBWrapper(db)
clw = dbwrapper.ClassWrapper(dbw, cl)
nodew = dbwrapper.NodeWrapper(clw, nodeid)
As you can see, they are both the same.
In both cases dbw will be the root object of the tree.
clw will be a wrapper around hyperdb object cl. In most cases
you don't really need that one, but it is needed to create the
node wrapper nodew.
In case of an auditor, nodeid can be None if the auditor was
fired by a create operation. In that case nodew will be a
phantom node which doesn't have any properties (except id which
will be None). A test like nodew is None will return True on
phantom nodes. It will return False if the node isn't a phantom
node but a real living one (auditor fired by set operation).
Action handlers
The wrapper is almost used the same way in an action handler as it
is used in a detector.
This is what you do:
def handler(self):
dbw = dbwrapper.DBWrapper(self.db)
clw = dbwrapper.ClassWrapper(dbw, self.cl)
nodew = dbwrapper.NodeWrapper(clw, self.nodeid)
Below you find some more examples and of course the source code for the wrapper.
Best regards,
Marlon van den Berg
PS: The wrapper has been tested with 0.7.12 and 1.1.0.
Examples
Here are a few more examples what dbwrapper can do for you:
- Reading a linked property:
- With roundup's hyperdb: unread_id = db.status.lookup('unread') db.status.get(unread_id, 'order') - With dbwrapper: dbw.status['unread'].order - Changing a linked property:
- With roundup's hyperdb: unread_id = db.status.lookup('unread') db.issue.set('1', status=unread_id) - With dbwrapper: dbw.issue[1].status = 'unread' - Adding a link to a multi linked property:
- With roundup's hyperdb: topics = db.issue.get('1', 'topic') keyword_id = db.keyword.lookup('DBWrapper') topics.append(keyword_id) db.issue.set('1', topic=topics) - With dbwrapper: dbw.issue[1].topic += 'DBWrapper' or: dbw.issue[1].topic += ['DBWrapper'] - Removing a link from a multi linked property:
- With roundup's hyperdb: topics = db.issue.get('1', 'topic') keyword_id = db.keyword.lookup('DBWrapper') topics.remove(keyword_id) db.issue.set('1', topic=topics) - With dbwrapper: dbw.issue[1].topic -= 'DBWrapper' or: dbw.issue[1].topic -= ['DBWrapper'] - Duplicating an issue with dbwrapper:
- With dbwrapper: issue = db.issue[1].properties issue['title'] = 'Copy of %s'%issue['title'] db.issue += issue - Retiring a node:
- With roundup's hyperdb: nodeid = db.status.lookup('chatting') db.status.retire(nodeid) - With dbwrapper: del dbw.status['chatting'] - Filtering:
- With roundup's hyperdb: nodeids = [ db.status.lookup(name) for name in ['chatting', 'unread'] ] db.filter(None, {'status':nodeids}) - With dbwrapper: dbw.filter(None, {'status':['chatting', 'unread']}) - Regular expression search:
- With dbwrapper: To lookup all issues of which the title starts witch 'roundup': dbw.issue.re.search('^roundup', dbwrapper.IGNORECASE) - Combination RE search:
- With dbwrapper: To lookup all issues of which the title starts witch 'roundup' and have status 'unread': lw = dbw.issue.filter(None, {'status':'unread'}) lw.re.search('^roundup', dbwrapper.IGNORECASE) The next will have the same result but takes longer: lw = dbw.issue.re.search('^roundup', dbwrapper.IGNORECASE) lw.issue.filter(None, {'status':'unread'}) - More search possibilities:
- With dbwrapper: Lookup all issues that have at least one message attached which starts with the word 'roundup': flags = dbwrapper.IGNORECASE + dbwrapper.MULTILINE lw = dbw.msg.re.search('\Aroundup', flags, column='content') dbw.issue.find(messages=lw) - Iteration:
- With dbwrapper: The next code will step trough all nodes in all classes and print the content on the screen. Please don't try this on your 10,000 issue tracker. If you do, well I guess you will be spending your day next to the coffee machine. for clw in dbw: print clw.classname for nodew in clw: print " %s"%nodew.plain() for property in nodew: print " | %s"%property
The Source
And finally the source code:
"""\
Introduction
============
Database wrapper implementation.
This wrapper can be used to have easy access to RoundUp's
hyper database without having to know too much knowledge
about the class structures and (multi) links.
This document contains a few examples. In those cases
you will see objects like 'db', 'dbw', 'cl', 'clw'
and 'nodew'. These object are:
db : hyperdb.Database
cl : hyperdb.Class (or FileClass or IssueClass)
dbw : dbwrapper.DBWrapper
clw : dbwrapper.ClassWrapper
nodew : dbwrapper.NodeClass
lw : dbwrapper.ListWrapper
Features
========
Addressing
----------
Addressing a specific node in a class is easier with this
wrapper. It almost has the looks and feels of the wrapper
used in the TAL templates. Also with this wrapper you don't
really need to use 'get' methods to get the data. Just address
the data as if they are member of a tree with the database
object as root. The class objects as limbs and the class
properties as leaves. What's left is how to address the right
node. Well that can be done with indexing the class (sorry
but I don't know were to put that in a tree). As index you
can either use the node id as integer or string, but you
may also use a key value (if the class has a key column).
Here are some examples:
- dbw.issue[1].title
dbw.issue['1'].title
Both these examples will return the title of issue 1.
- dbw.priority['urgent'].name
This example will return 'urgent'. Not really
meaningful, but it's only meant as an example.
- dbw.status['unread'].id
Will return the nodeid of status node 'unread'.
In general the benefit of this wrapper is that you don't
really have to know any of the node ids. All 'set' or indexing
methods will resolve key values into id's. Even so will all
'get' methods return the key values and not the id's when it
comes to linked items.
Searching
---------
The wrapper is capable of handling regular expression searches.
It will not only return you the found nodes, but it also will
return the found matches per node.
Usage
=====
To use the wrapper you need to place it somewhere. In our case
we placed it in a sub-folder of RoundUp's core code which remains
in the 'site-packages' folder of the Python library tree.
In this /lib/site-packages/roundup we created a folder named
'extensions'. This is about conform the standard that Richard uses
for additional stuff in tracker templates (we look at the wrapper
as additional stuff for the RoundUp core code). In other words
the complete path to the wrapper will be:
/lib/site-packages/roundup/extensions/dbwrapper.py
It will be easiest if the wrapper can be imported as sub package.
This will require a '__init__.py' file in that directory to. The
content of that file will only be this line:
__all__ = [ 'dbwrapper' ]
What's left is importing the wrapper in your Python sources (e.g.
detectors and action handlers). You do that by adding the line:
from roundup.extensions import dbwrapper
to all sources in which you want to use the wrapper.
There's only one comment left. As you can see, the wrapper has a
lot of documentation strings inside. Therefor we suggest to run
all scripts with the '-OO' option passed to Python's interpreter.
This will prevent that your scripts will load these documentation
strings too. If you're not sure, then please read section
'6.1.2 "Compiled" Python files' of the tutorial in the Python
documentation.
Detectors
---------
The wrapper can easily be used in a detector without having to
change anything to RoundUp's core code.
This is what you do in an auditor:
def auditor(db, cl, nodeid, newvalues):
dbw = dbwrapper.DBWrapper(db)
clw = dbwrapper.ClassWrapper(dbw, cl)
nodew = dbwrapper.NodeWrapper(clw, nodeid)
And this in a reactor:
def reactor(db, cl, nodeid, oldvalues):
dbw = dbwrapper.DBWrapper(db)
clw = dbwrapper.ClassWrapper(dbw, cl)
nodew = dbwrapper.NodeWrapper(clw, nodeid)
As you can see, they are both the same.
In both cases 'dbw' will be the root object of the tree.
'clw' will be a wrapper around hyperdb object 'cl'. In most cases
you don't really need that one, but it is needed to create the
node wrapper 'nodew'.
In case of an auditor, 'nodeid' can be 'None' if the auditor was
fired by a 'create' operation. In that case 'nodew' will be a
phantom node which doesn't have any properties (except 'id' which
will be 'None'). A test like:
nodew is None
will return True on phantom nodes. It will return False if the node
isn't a phantom node but a real living one (auditor fired by 'set'
operation).
Action handlers
---------------
The wrapper is almost used the same way in an action handler as it
is used in a detector.
This is what you do:
def handler(self):
dbw = dbwrapper.DBWrapper(self.db)
clw = dbwrapper.ClassWrapper(dbw, self.cl)
nodew = dbwrapper.NodeWrapper(clw, self.nodeid)
Examples
========
Here are a few more examples what dbwrapper can do for you:
1. Reading a linked property:
- With RoundUp's hyperdb:
unread_id = db.status.lookup('unread')
db.status.get(unread_id, 'order')
- With dbwrapper:
dbw.status['unread'].order
2. Changing a linked property:
- With RoundUp's hyperdb:
unread_id = db.status.lookup('unread')
db.issue.set('1', status=unread_id)
- With dbwrapper:
dbw.issue[1].status = 'unread'
3. Adding a link to a multi linked property:
- With RoundUp's hyperdb:
topics = db.issue.get('1', 'topic')
keyword_id = db.keyword.lookup('DBWrapper')
topics.append(keyword_id)
db.issue.set('1', topic=topics)
- With dbwrapper:
dbw.issue[1].topic += 'DBWrapper'
or:
dbw.issue[1].topic += ['DBWrapper']
4. Removing a link from a multi linked property:
- With RoundUp's hyperdb:
topics = db.issue.get('1', 'topic')
keyword_id = db.keyword.lookup('DBWrapper')
topics.remove(keyword_id)
db.issue.set('1', topic=topics)
- With dbwrapper:
dbw.issue[1].topic -= 'DBWrapper'
or:
dbw.issue[1].topic -= ['DBWrapper']
5. Duplicating an issue with dbwrapper:
- With dbwrapper:
issue = db.issue[1].properties
issue['title'] = 'Copy of %s'%issue['title']
db.issue += issue
6. Retiring a node:
- With RoundUp's hyperdb:
nodeid = db.status.lookup('chatting')
db.status.retire(nodeid)
- With dbwrapper:
del dbw.status['chatting']
7. Filtering:
- With RoundUp's hyperdb:
nodeids = [ db.status.lookup(name) for name in ['chatting', 'unread'] ]
db.filter(None, {'status':nodeids})
- With dbwrapper:
dbw.filter(None, {'status':['chatting', 'unread']})
8. Regular expression search:
- With dbwrapper:
To lookup all issues of which the title starts witch 'RoundUp':
dbw.issue.re.search('^RoundUp', dbwrapper.IGNORECASE)
9. Combination RE search:
- With dbwrapper:
To lookup all issues of which the title starts witch 'RoundUp'
and have status 'unread':
lw = dbw.issue.filter(None, {'status':'unread'})
lw.re.search('^RoundUp', dbwrapper.IGNORECASE)
The next will have the same result but takes longer:
lw = dbw.issue.re.search('^RoundUp', dbwrapper.IGNORECASE)
lw.issue.filter(None, {'status':'unread'})
10. More search possibilities:
- With dbwrapper:
Lookup all issues that have at least one message attached which
starts with the word 'RoundUp':
flags = dbwrapper.IGNORECASE + dbwrapper.MULTILINE
lw = dbw.msg.re.search('\ARoundUp', flags, column='content')
dbw.issue.find(messages=lw)
11. Iteration:
- With dbwrapper:
The next code will step trough all nodes in all classes and print
the content on the screen. Please don't try this on your 10,000
issue tracker. If you do, well I guess you will be spending your
day next to the coffee machine.
for clw in dbw:
print clw.classname
for nodew in clw:
print " %s"%nodew.plain()
for property in nodew:
print " | %s"%property
"""
__docformat__ = 'restructuredtext'
import types, os
from re import IGNORECASE, DOTALL, LOCALE, MULTILINE, UNICODE, VERBOSE
from re import I, S, L, M, U, X
from roundup import instance, hyperdb, date
isHyperDB = lambda object: isinstance(object, hyperdb.Database)
isHyperDBClass = lambda object: isinstance(object, hyperdb.Class)
isDB = lambda object: isinstance(object, DBWrapper)
isClass = lambda object: isinstance(object, ClassWrapper)
isNode = lambda object: isinstance(object, NodeWrapper)
def to_hyperdb(*nodes):
"""\
Converts a list (or single node) of dbwrapper nodes to a list of
hyperdb compliant nodes.
"""
result = []
if nodes:
if len(nodes) == 1 \
and (isinstance(nodes[0], types.ListType) \
or isinstance(nodes[0], types.TupleType)):
nodes = nodes[0]
for node in nodes:
if isinstance(node, types.TupleType) \
and len(node) == 2:
# regular expression list
node = node[0]
if not isNode(node):
raise ValueError, \
"Node '%s' isn't an instance of 'NodeWrapper'."%node
result.append(str(node._id))
return result
class DBWrapper:
"""\
A wrapper around RoundUp's hyperdb.Database.
Initialization
==============
There are three methods to initialize the DBWrapper:
1. dbw = dbwrapper.DBWrapper(<tracker home>)
In this case the wrapper will open the database
belonging to <tracker_home>. On destruction it
will always close the database.
This form is useful in separate scripts which
need to access the RoundUp database.
2. dbw = dbwrapper.DBWrapper()
The wrapper will open the database for you if
the environment has a variable TRACKER_HOME
which points to a valid tracker home folder.
On destruction the database will be closed.
This form is useful in separate scripts which
need to access the RoundUp database.
3. dbw = dbwrapper.DBWrapper(<hyperdb.Dtabase object>)
The wrapper will not open the database, but will
use an already opened database. The database
object is passed as parameter to the wrapper and
should be an opened hyperdb.Database instance.
The wrapper will never close the database.
This form is useful in detectors and action
handlers.
Usage
=====
You have full access to the database as soon as the
DBWrapper is initialized. To access a class, just access
it as being a member of the wrapper (same as with hyperdb).
Syntax:
<DBWrapper>.<class name>
The returned object will be an initialized ClassWrapper
object.
Examples:
dbw.issue
dbw.status
Transparency
============
Members defined in hyperdb.Database, but not
defined in this 'DBWrapper' class can still be
used as if they are member of this class.
"""
def __init__(self, arg=None, *user):
"""\
Initiate a wrapper around RoundUp's hyperdb.Database.
"arg" is used to specify the database.
It can have three different types:
1 - an instance of an open RoundUp hyperdb.Database.
This is the most common usage if the wrapper is
used within any detector or action handler.
2 - a valid tracker home path.
This allows us to use the wrapper in any external
script without having to open a tracker instance
and the database before using the wrapper.
If used like this, the destructor of this class
will close the database to prevent pending open
database links.
3 - not specified.
This is almost similar with type 2 except that
environment variable TRACKER_HOME will be used
to open the database.
"user" has only meaning if 'arg' is of type 2 or 3.
In those cases the database will be opened as being 'user'.
Default it opens the database as 'admin'.
"""
if isHyperDB(arg):
# arg is a roundup database instance
# no need to open the database
self._instance = None
self._db = arg
else:
if isinstance(arg, types.StringType):
# arg is a string instance
# we assume it is a valid tracker_home location
# and we will use it to open the database
tracker_home = arg
elif arg is None:
# arg is None -> no argument was passed
# user TRACKER_HOME environment variable
# to open the database
tracker_home = os.getenv('TRACKER_HOME')
else:
# and what now?
raise TypeError, \
"No 'hyperdb' to access"
if not user:
user = ('admin',)
self._instance = instance.open(tracker_home)
self._db = self._instance.open(user[0])
# create storage for classes
self._classes = {}
for classname in self._db.getclasses():
self._classes[classname] = {}
def __del__(self):
"""\
Closes the RoundUp database if it was opened by the wrapper
it selves (initiated with an 'arg' of type 2 or 3)
"""
# restore possible turned off detectors
for classname in self._db.getclasses():
# restore auditors
if self._classes[classname].has_key('auditors'):
self._db.getclass(classname).auditors = \
self._classes[classname]['auditors']
# restore reactors
if self._classes[classname].has_key('reactors'):
self._db.getclass(classname).reactors = \
self._classes[classname]['reactors']
# make sure we close the database if we did open it ourselves
if not self._instance is None:
self._db.close()
def __repr__(self):
"""\
Slightly more useful representation
"""
return '''<dbwrapper.DBWrapper '%s'>'''%self._db
def __str__(self):
"""\
Slightly more useful representation
"""
return self.__repr__()
def __getattr__(self, attr):
"""\
Grant access to ClassWrapper objects of all
classes like they are member of this class.
It also creates transparency to hyperdb.Database
members if they aren't defined in the DBWrapper
class.
"""
# If 'attr' is a member, then access that member
if self.__dict__.has_key(attr):
value = self.__dict__[attr]
# If 'attr' is a hyperdatabse class, wrap it in
# a 'ClassWrapper'
elif attr in self._db.getclasses():
value = ClassWrapper(self, attr)
# If 'attr' is a member of class 'Database', return
# the address of that member to create the transparency
else:
value = getattr(self._db, attr)
return value
def __setattr__(self, attr, value):
"""\
Prevent that attributes which refer to ClassWrapper
objects are overwritten by a wrong assign statement.
"""
# If 'attr' is a member or member to be,
# then access that member
if attr in ['_instance', '_db', '_classes'] \
or self.__dict__.has_key(attr):
self.__dict__[attr] = value
# If 'attr' is a hyperdatabse class, wrap it in
# a 'ClassWrapper'
elif attr in self._db.getclasses():
# Check if it isn't the result of 'ClassWrapper.__iadd__'
if not isClass(value) or value._cl.classname != attr:
raise ValueError, \
"'%s' can't be assigned any value"%attr
# If 'attr' is a member of class 'Database', return
# the address of that member to create the transparency
else:
setattr(self._db, attr, value)
def __iter__(self):
"""\
Create iteration of all classes as ClassWrapper's.
"""
return iter( [ClassWrapper(self, classname) \
for classname in self._db.getclasses()] )
def getclass(self, classname):
"""\
Get the ClassWrapper object representing a particular class.
"""
return self.__getattr__(classname)
class ClassWrapper:
"""\
A wrapper around a RoundUp hyperdb.Class.
Introduction
============
In most cases you don't need to initialize a
ClassWrapper because the DBWrapper will do that
for you where needed. But sometimes it can be
more convenient to initialize one yourself.
Initialization
==============
There are three methods to initialize a ClassWrapper:
1. clw = dbwrapper.ClassWrapper(dbw, <class name>)
A wrapper will be created for class <class name>.
2. clw = dbwrapper.ClassWrapper(dbw, <hyperdb.Class>)
A wrapper will be created for the hyperdb.Class object.
3. clw = dbwrapper.ClassWrapper(dbw, <ClassWrapper>)
A wrapper will be created for the ClassWrapper object.
This won't make much sense because the wrapper was already there.
Usage
=====
The wrapper behaves like a dictionary, meaning you can access the
nodes by indexing them as dictionary items.
Syntax:
<DBWrapper>.<class name>[<key>|<nodeid>|<NodeWrapper>]
As index you can either use the node id as integer or string,
a key value (if the class has a key column) or an instance of
NodeWrapper.
The returned value is a NodeWrapper object.
Examples:
dbw.status['chatting']
dbw.issue[1]
dbw.priority['3']
dbw.msg[nodew]
Some of the methods in this class do have a "key"
parameter. In all these cases this parameter can
have one of the next types:
1 - an integer
The 'key' will be treated as the id of the node
and used for the indexing.
2 - a string
The 'key' will be treated as key argument of the
node and used to lookup the node id.
If the lookup action raises a KeyError and 'key'
only contains digits, the integer value of 'key'
will be used as the node id.
In both cases the node id is used for the indexing.
3 - an instance of NodeWrapper
The node id will be obtained from the NodeWrapper
class and used for the indexing.
Transparency
============
Members defined in the hyperdb.Class, but not
defined in this 'ClassWrapper' class can still
be used as if they are member of this class.
"""
class RegExpClass:
"""\
A class with regular expression methods that will filter nodes
from hyperdb.Class. The methods in this class are one to one
with the regular expression methods in python module 're'.
"""
def __init__(self, owner):
"""\
Initialize class
"owner" must be of type 'ClassWrapper'
"""
import re
if not isClass(owner):
raise TypeError, \
"Owner object isn't an instance of 'ClassWrapper'"
self._clw = owner
self.__re = re
def __repr__(self):
"""\
Slightly more useful representation
"""
return '''<dbwrapper.ClassWrapper.RegExpClass '%s'>'''% \
self._clw._cl
def __str__(self):
"""\
Slightly more useful representation
"""
return self.__repr__()
def __inner_re(self, re_func, column, nodelist):
"""\
Handles the main reg. expression search.
"re_func" must contain one of the search methods
from standard python module 're'.
"column" can be used to perform a search on an
other column than the column returned by
ClassWrapper.getlabel().
The return value is a list object containing tuples
pairs.
The first index of each tuple contains a NodeWrapper
object to a node matching the search.
The second index of the tuples holds the result object
of the corresponding regular expression method passed
in "re_func".
"nodelist" can be used to limit the search within the list
of nodes. 'nodelist' may be a list of ids or any list created
by any of the search methods in this module.
"""
if not column:
column = self._clw.getlabel()
if nodelist is None:
list = self._clw._cl.list()
else:
list = ListWrapper(self._clw, nodelist).getnodeids()
result = []
for nodeid in list:
value = self._clw._cl.get(nodeid, column)
if value is None:
value = ''
res = re_func(value)
if res:
result.append( (NodeWrapper(self._clw, nodeid, False), res) )
return ListWrapper(self._clw, result)
def match(self, pattern, flags=0, column=None, nodelist=None):
"""\
Find all nodes in the class that have a result when
applying the "pattern" at the start of the string in
"column", returning a list object with tuples pairs.
The first index of each tuple contains a
dbwraper.NodeWrapper object of a node that matches
with "pattern".
The second index in the tuples holds the match object.
"flags" may have the same flags as used/defined in
python module 're'. These flags are also available
as members of module 'dbwrapper'.
(e.g. dbwrapper.IGNORECASE)
"column" can be used to perform a search on an
other column than the column returned by
ClassWrapper.getlabel().
"nodelist" can be used to limit the search within the list
of nodes. 'nodelist' may be a list of ids or any list created
by any of the search methods in this module.
"""
reg = self.__re.compile(pattern, flags)
return self.__inner_re(reg.match, column, nodelist)
def search(self, pattern, flags=0, column=None, nodelist=None):
"""\
Find all nodes in the class that have a result when
scanning through the string in "column" looking for a
match to the "pattern", returning a list object with
tuples pairs.
The first index of each tuple contains a
dbwraper.NodeWrapper object of a node that matches
with "pattern".
The second index in the tuples holds the search object.
"flags" may have the same flags as used/defined in
python module 're'. These flags are also available
as members of module 'dbwrapper'.
(e.g. dbwrapper.IGNORECASE)
"column" can be used to perform a search on an
other column than the column returned by
ClassWrapper.getlabel().
"nodelist" can be used to limit the search within the list
of nodes. 'nodelist' may be a list of ids or any list created
by any of the search methods in this module.
"""
reg = self.__re.compile(pattern, flags)
return self.__inner_re(reg.search, column, nodelist)
def findall(self, pattern, flags=0, column=None, nodelist=None):
"""\
Return all nodes in class that have "pattern" matches
in the strings in "column". Returned is a list of tuple
pairs.
The first tuple index contains a NodeWrapper object of
a matching node.
The second tuple index contains a list of all
non-overlapping matches in the node's "column" string.
If one or more groups are present in the "pattern", the
second tuple index will contain a list of groups; this
will be a list of tuples if the "pattern" has more than
one group.
"flags" may have the same flags as used/defined in
python module 're'. These flags are also available
as members of module 'dbwrapper'.
(e.g. dbwrapper.IGNORECASE)
"column" can be used to perform a search on an
other column than the column returned by
ClassWrapper.getlabel().
"nodelist" can be used to limit the search within the list
of nodes. 'nodelist' may be a list of ids or any list created
by any of the search methods in this module.
"""
reg = self.__re.compile(pattern, flags)
return self.__inner_re(reg.findall, column, nodelist)
def __init__(self, parent, cl):
"""\
Initiate a wrapper around a hyperdb.Class.
"parent" is the DBWrapper that owns this ClassWrapper.
"cl" is used to specify the class.
It can have three different types:
1 - an instance of a hyperdb.Class.
2 - an instance of a ClassWrapper.
3 - the class name.
"""
if not isDB(parent):
raise TypeError, \
"Parent object isn't an instance of 'DBWrapper'"
self._dbw = parent
self._db = parent._db
if isHyperDBClass(cl):
self._cl = cl
elif isClass(cl):
self._cl = cl._cl
else:
self._cl = self._db.getclass(cl)
self.re = self.RegExpClass(self)
self.__lastid = None
def __repr__(self):
"""\
Slightly more useful representation
"""
return '''<dbwrapper.ClassWrapper '%s'>'''%self._cl
def __str__(self):
"""\
Slightly more useful representation
"""
return self.__repr__()
def __call__(self, *nodeids):
"""\
Converts a list (or single id) of hyperdb node ids to a list of
dbwrapper compliant nodes.
"""
result = ListWrapper(self)
if nodeids:
if len(nodeids) == 1 \
and (isinstance(nodeids[0], types.ListType) \
or isinstance(nodeids[0], types.TupleType)):
nodeids = nodeids[0]
for nodeid in nodeids:
result.append(NodeWrapper(self, int(nodeid)))
return result
def __getattr__(self, attr):
"""\
Create transparency to hyperdb.Class members
if they aren't defined in the ClassWrapper
class.
"""
if self.__dict__.has_key(attr):
value = self.__dict__[attr]
else:
value = getattr(self._cl, attr)
return value
def __len__(self):
"""\
Number of nodes in this class (excluding retired nodes).
"""
return len(self._cl.list())
def __contains__(self, key):
"""\
Determine if the class has a given node.
"""
return self.has_key(key)
def __getitem__(self, key):
"""\
Grant get access to all class nodes by indexing them as
dictionary items and wrapping them in a NodeWrapper.
"""
return NodeWrapper(self, key, False)
def __setitem__(self, key, columns):
"""\
Grant set access to all class nodes by indexing them as
dictionary items and wrapping them in a NodeWrapper.
"columns" must be a dictionary containing class properties
as the keys. The values in the dictionary are used as
values for the properties.
"""
if isinstance(columns, types.DictType):
node = NodeWrapper(self, key, False)
for key in columns.keys():
setattr(node, key, columns[key])
else:
raise TypeError, \
"Item can only be assigned a dictionary"
def __delitem__(self, key):
"""\
Retire a class node by using the python 'del' command and
indexing the node as dictionary item.
"""
self._cl.retire( str(self.getid(key)) )
def __iadd__(self, columns):
"""\
Add a new node to the class.
"columns" must be a dictionary containing class properties
as the keys. The values in the dictionary are used as
values for the properties.
"""
if isinstance(columns, types.DictType):
self.create(**columns)
else:
raise TypeError, \
"Item can only be assigned a dictionary"
return self
def __iter__(self):
"""\
Create iteration of all nodes as NodeWrapper's.
"""
return iter(self.list())
def getid(self, key):
"""\
Get the node id of the node that matches the key.
"key" can have three different types:
1 - an integer
The 'key' will be treated as the id of the node
and used for the indexing.
2 - a string
The 'key' will be treated as key argument of the
node and used to lookup the node id.
If the lookup action raises a KeyError and 'key'
only contains digits, the integer value of 'key'
will be used as the node id.
In both cases the node id is used for the indexing.
3 - an instance of NodeWrapper
The node id will be obtained from the NodeWrapper
class and used for the indexing.
"""
if isinstance(key, types.IntType):
nodeid = key
elif isNode(key):
nodeid = key._id
else:
try:
nodeid = self._cl.lookup(key)
except (KeyError, TypeError):
if isinstance(key, types.StringType) \
and key.isdigit() \
and self._cl.hasnode(key):
nodeid = int(key)
else:
raise IndexError, \
"Class '%s' doesn't have a node that can be \
indexed with '%s'"%(self._cl.classname, key)
else:
nodeid = int(nodeid)
return nodeid
def auditors(self, enable):
"""\
Enable/Disable auditors for this class.
"""
p = self._dbw._classes[self._cl.classname]
if enable:
if p.has_key('auditors'):
self._cl.auditors = p['auditors']
del p['auditors']
else:
if not p.has_key('auditors'):
p['auditors'] = dict(self._cl.auditors)
self._cl.auditors = {
'create': [],
'set': [],
'retire': [],
'restore': []
}
def reactors(self, enable):
"""\
Enable/Disable reactors for this class.
"""
p = self._dbw._classes[self._cl.classname]
if enable:
if p.has_key('reactors'):
self._cl.reactors = p['reactors']
del p['reactors']
else:
if not p.has_key('reactors'):
p['reactors'] = dict(self._cl.reactors)
self._cl.reactors = {
'create': [],
'set': [],
'retire': [],
'restore': []
}
def setlabel(self, label=None):
"""\
Set an alternative label to be used as label property.
If label is None, the result of hyperdb 'labelprop()' will be
used as label.
"""
if not label is None:
props = self._cl.getprops()
if label in props.keys():
if isinstance(props[label], hyperdb.String):
self._dbw._classes[self._cl.classname]['label'] = label
else:
raise TypeError, \
"'label' should be a string type column"
else:
raise AttributeError, \
"Class '%s' doesn't have a property '%s'"% \
(self._cl.classname, label)
else:
del self._dbw._classes[self._cl.classname]['label']
def getlabel(self):
"""\
Get the label to be used as label property.
If an alternative label is set, it will be returned.
"""
p = self._dbw._classes[self._cl.classname]
return p.get('label', self._cl.labelprop())
def create(self, **propvalues):
"""\
Create a new node of this class and return its id.
The keyword arguments in 'propvalues' map property names to values.
The values of arguments must be acceptable for the types of their
corresponding properties or a TypeError is raised.
If this class has a key property, it must be present and its value
must not collide with other key strings or a ValueError is raised.
Any other properties on this class that are missing from the
'propvalues' dictionary are set to None.
If an id in a link or multilink property does not refer to a valid
node, an IndexError is raised. Valid are node ids (as integer or
as string), key values (for links to classes that have a key column)
or a NodeWrapper object.
"""
props = self._cl.getprops()
creator = {}
for column, value in propvalues.items():
if props.has_key(column):
if isinstance(props[column], hyperdb.Link):
cl = self._db.getclass(props[column].classname)
wrapper = ClassWrapper(self._dbw, cl)
creator[column] = str(wrapper.getid(value))
elif isinstance(props[column], hyperdb.Multilink):
cl = self._db.getclass(props[column].classname)
wrapper = ClassWrapper(self._dbw, cl)
if not isinstance(value, types.ListType) \
and not isinstance(value, types.TupleType):
value = [value]
creator[column] = [ str(wrapper.getid(key)) for key in value ]
elif isinstance(props[column], hyperdb.Interval) \
and not isinstance(value, date.Interval):
creator[column] = date.Interval(value)
elif isinstance(props[column], hyperdb.Date) \
and not isinstance(value, date.Date):
creator[column] = date.Date(value)
else:
creator[column] = value
else:
creator[column] = value
self.__lastid = self._cl.create(**creator)
return self.__lastid
def get(self, key, propname):
"""\
Get the value of a property on an existing node of this class.
'propname' must be the name of a property of this class or a
KeyError is raised.
"""
node = self.__getitem__(key)
return getattr(node, propname)
def set(self, key, **propvalues):
"""\
Modify a property on an existing node of this class.
Each key in 'propvalues' must be the name of a property of this
class or a KeyError is raised.
All values in 'propvalues' must be acceptable types for their
corresponding properties or a TypeError is raised.
If the value of the key property is set, it must not collide with
other key strings or a ValueError is raised.
If the value of a Link or Multilink property contains an invalid
node id, a ValueError is raised.
"""
self.__setitem__(key, propvalues)
def list(self):
"""\
Return a list of NodeWrappers of the active nodes in this class.
"""
id_list = [ int(nodeid) for nodeid in self._cl.list() ]
id_list.sort()
return ListWrapper(self,
[ NodeWrapper(self, int(nodeid), False) for nodeid in id_list ] )
def filter(self, search_matches, filterspec, sort=(None,None),
group=(None,None), nodelist=None):
"""\
Return a list of the NodeWrappers of the active nodes in this class
that match the 'filter' spec, sorted by the group spec and then the
sort spec.
"filterspec" is {propname: value(s)}
'value(s)' may be any node id as integer or string or a key value
(for classes that have a key column).
"sort" and "group" are (dir, prop) where dir is '+', '-' or None
and prop is a prop name or None
"search_matches" is {nodeid: marker}
"nodelist" can be used to limit the search within the list
of nodes. 'nodelist' may be a list of ids or any list created
by any of the search methods in this module.
The filter must match all properties specified - but if the
property value to match is a list, any one of the values in the
list may match for that property to match.
"""
props = self._cl.getprops()
for column in filterspec.keys():
if isinstance(props[column], hyperdb.Link):
cl = self._db.getclass(props[column].classname)
parent = ClassWrapper(self._dbw, cl)
node = NodeWrapper(parent, filterspec[column])
filterspec[column] = str(node._id)
elif isinstance(props[column], hyperdb.Multilink):
cl = self._db.getclass(props[column].classname)
parent = ClassWrapper(self._dbw, cl)
list = ListWrapper(parent, filterspec[column])
filterspec[column] = list.getnodeids()
if not nodelist is None:
filterspec['id'] = ListWrapper(self, nodelist).getnodeids()
return ListWrapper(self,
[ NodeWrapper(self, int(nodeid), False) for nodeid in self._cl.filter(search_matches, filterspec, sort, group) ])
def find(self, **propspec):
"""\
Get the NodeWrappers of items in this class which link to the given
items.
'propspec' consists of keyword args propname=itemid or
propname=key
'propname' must be the name of a property in this class, or a
KeyError is raised. That property must be a Link or
Multilink property, or a TypeError is raised.
"propspec" can also be passed a list with results from anu of the
search methods in this wrapper (also regular expression search
results can be passed 1-to-1).
Any item in this class whose 'propname' property links to any of the
itemids will be returned. Used by the full text indexing, which knows
that "foo" occurs in msg1, msg3 and file7, so we have hits on these
issues:
db.issue.find(messages=[1,3], files=[7])
"""
for column, values in propspec.items():
if isinstance(values, ListWrapper):
values.simple()
elif not isinstance(values, types.ListType) \
and not isinstance(values, types.TupleType):
values = [values]
links = {}
for key in values:
links[str(self.getid(key))] = 1
propspec[column] = dict(links)
id_list = [ int(nodeid) for nodeid in self._cl.find(**propspec) ]
id_list.sort()
return ListWrapper(self,
[ NodeWrapper(self, nodeid, False) for nodeid in id_list ])
def newid(self):
"""\
Generate a new id for this class.
"""
return self._db.newid(self._cl.classname)
def newnodeid(self):
"""\
Return the id of the last created node.
"""
return self.__lastid
def getnodeids(self, retired=None):
"""\
Retrieve all NodeWrappers of the nodes for a particular Class.
if 'retired' is True, all retired nodes will be returned.
"""
id_list = [ int(nodeid) for nodeid in self._cl.getnodeids(retired) ]
id_list.sort()
return id_list
def hasnode(self, key):
"""\
Determine if the class has a given node (also true on retired nodes).
"""
try:
nodeid = self.getid(key)
except:
label = self._cl.getkey()
if label \
and isinstance(key, types.StringType):
value = None
for nodeid in self._cl.getnodeids(True):
value = self._cl.get(nodeid, label)
if value == key:
break
if not value is None \
and value == key:
result = True
else:
result = False
else:
result = False
else:
result = self._cl.hasnode( str(nodeid) )
return result
def lookup(self, keyvalue):
"""\
Locate a particular node by its key property and return it in a
NodeClass object.
If this class has no key property, a TypeError is raised. If the
'keyvalue' matches one of the values for the key property among
the nodes in this class, the matching node's id is returned;
otherwise a KeyError is raised.
"""
return NodeWrapper(self, int(self._cl.lookup(keyvalue)), False)
def search(self, value, column=None, nodelist=None):
"""\
Lookup all nodes that have a 100% match with 'value' and
return them in a list.
"value" is the pattern to look for.
"column" can be set to any string property. A TypeError is
raised if 'column' isn't a string property. The label column
will be used if 'column' is omitted.
"nodelist" can be used to limit the search within the list
of nodes. 'nodelist' may be a list of ids or any list created
by any of the search methods in this module.
"""
if not isinstance(value, types.StringType):
raise TypeError, \
"'value' should be a string"
if not column:
column = self.getlabel()
props = self._cl.getprops()
if not isinstance(props[column], hyperdb.String):
raise TypeError, \
"'column' should be a string type column"
if nodelist is None:
list = self._cl.filter(None, {column:value})
else:
list = ListWrapper(self, nodelist).getnodeids()
list.sort()
value = value.lower()
result = []
for nodeid in list:
if self._cl.get(nodeid, column).lower() == value:
result.append( NodeWrapper(self, int(nodeid), False) )
return ListWrapper(self, result)
def is_retired(self, key):
"""\
Return true if the node is retired.
"""
return self._cl.is_retired( str(self.getid(key)) )
def safeget(self, key, propname, default=None):
"""\
Safely get the value of a property on an existing node of this class.
Return 'default' if the node doesn't exist.
"""
value = getattr(NodeWrapper(self, key), propname, default)
if value is None \
or (isinstance(value, types.ListType) and not value):
value = default
return value
def has_key(self, key):
"""\
Determine if the class has a given node, but then in
a dictionary known method (false on retired nodes).
"""
try:
nodeid = self.getid(key)
except (KeyError, TypeError, IndexError):
result = False
else:
result = str(nodeid) in self._cl.list()
return result
def keys(self):
"""\
Get a list with all key values of this class. If no key is defined
for this class, a list of ids will be returned.
"""
key = self._cl.getkey()
if key:
keys = [ self._cl.get(nodeid, key) for nodeid in self._cl.list() ]
else:
keys = [ int(nodeid) for nodeid in self._cl.list() ]
return keys
def items(self):
"""\
Get a list with tuples containing all keys with there NodeWrapper kept
as pairs.
"""
return [ (key, NodeWrapper(self, key, False)) for key in self.keys() ]
def pop(self, key):
"""\
Get node 'key' and remove (retire) it from this class.
"""
value = self.__getitem__(key)
self.__delitem__(key)
return value
def resurrect(self, key):
"""\
Bring any deleted (retired) node back alive.
"""
retireds = self._cl.getnodeids(retired=1)
if isinstance(key, types.IntType):
nodeid = str(key)
else:
prop = self._cl.getkey()
nodeid = None
for retired_id in retireds:
value = self._cl.get(retired_id, prop)
if value == key:
nodeid = retired_id
break;
if nodeid and nodeid in retireds:
self._cl.restore(nodeid)
else:
raise KeyError, \
"No retired node '%s' in class '%s'"%(key, self._cl.classname)
class NodeWrapper:
"""\
A wrapper around one specific node in a
RoundUp hyperdb.Class.
Introduction
============
In most cases you don't need to initialize a
NodeWrapper because the ClassWrapper will do that
for you where needed. But sometimes it can be
more convenient to initialize one yourself.
Initialization
==============
There are three methods to initialize a NodeWrapper:
1. nodew = dbwrapper.NodeWrapper(clw, <nodeid>)
A wrapper will be created for node <nodeid>.
<nodeid> may be either an integer or a string.
2. nodew = dbwrapper.NodeWrapper(clw, <key>)
For classes that have a key column, a key value can be
used to create a wrapper for the node indexed by <key>.
3. nodew = dbwrapper.NodeWrapper(clw, <NodeWrapper>)
A wrapper will be created for the NodeWrapper object.
This won't make much sense because the wrapper was already there.
4. nodew = dbwrapper.NodeWrapper(clw, None)
This is a phantom object and can be used to test against None.
This might be convenient in auditors where there isn't a node
if they are fired by the create event.
Usage
=====
All node properties can be accessed as if they are members of this
wrapper object.
The syntax is:
<NodeWrapper>.<property>
The returned type depends on the requested property. In most cases
a simple type is returned (like string and integer), but in case of
a linked property, a NodeWrapper object will be returned for the
node to which the property is linked. In case of multi linked
properties, a ListWrapper object will be returned containing
NodeWrapper objects for all nodes to which the property is linked.
Examples:
nodew.title
nodew.name
nodew.id (read only)
nodew.nodeid (read only) (same as str(nodew.id))
It is also possible to set a property by accessing them as wrapper
members.
The syntax is:
<NodeWrapper>.<property> = <value>
The assigned value depends on the property type. In case of the
simple types (String, Number, Boolean) value will be of the
corresponding Python type (string, integer, boolean).
In case of a linked type, the value can have more than one type:
1. node id as integer
2. node id as string
3. key value (for classes that have a key column)
4. NodeWrapper instance
In case of multi linked type, value must be a list (either Python's
list object or dbwrapper's ListWrapper object) containing node
specifications like mentioned for link type properties (see above).
Examples:
nodew.title = 'My new title'
nodew.status = 'chatting'
nodew.topic = ['Keyword 1', 'Keyword 2', 3, '4']
In case of multi linked properties, new links to other nodes can be
created by simply adding them to the property. Even so can links be
removed by subtracting them from the property.
Examples:
Add:
nodew.topic += 'Keyword 3'
nodew.topic += ['Keyword 4', 'Keyword 5']
Remove:
nodew.topic -= 'Keyword 3'
nodew.topic -= ['Keyword 4', 'Keyword 5']
"""
def __init__(self, parent, key, readonly=True):
"""\
Initiate a wrapper around a hyperdb.Class for one
specific node.
"parent" is the ClassWrapper that owns this NodeWrapper.
"key" identifies the node.
It can have one of the next types:
1 - an integer
The 'key' will be treated as the id of the node
and used for the indexing.
2 - a string
The 'key' will be treated as key argument of the
node and used to lookup the node id.
If the lookup action raises a KeyError and 'key'
only contains digits, the integer value of 'key'
will be used as the node id.
In both cases the node id is used for the indexing.
3 - an instance of NodeWrapper
The node id will be obtained from the NodeWrapper
class and used for the indexing.
4 - None type
This is a special option implemented to make it
easy usable in detectors.
"readonly" can be set to False to allow set operations
on any of the properties of this node.
"""
if not isClass(parent):
raise TypeError, \
"Parent object isn't an instance of 'ClassWrapper'"
self._clw = parent
self._db = parent._db
self._cl = parent._cl
self._readonly = readonly
if key is None:
self._id = None
else:
self._id = parent.getid(key)
if not self._cl.hasnode(str(self._id)):
raise IndexError, \
"Class '%s' doesn't have a node with id '%s'"% \
(self._cl.classname, self._id)
def __repr__(self):
"""\
Return a resolved property.
"""
import re
if self._id is None:
value = "<dbwrapper.NodeWrapper 'Phantom'>"
else:
value = self.plain()
if isinstance(value, types.StringType):
value = "'%s'"%re.sub("'", "\\'", value)
return str(value)
def __str__(self):
"""\
Slightly more useful representation
"""
if self._id is None:
value = "<dbwrapper.NodeWrapper 'Phantom'>"
else:
value = str(self.plain())
return value
def __getattr__(self, attr):
"""\
Grant easy get access to all properties of this node.
"""
# is it a NoneClass member?
if self.__dict__.has_key(attr):
value = self.__dict__[attr]
elif attr == 'id':
value = self._id
elif attr == 'nodeid':
if self._id is None:
value = None
else:
value = str(self._id)
elif attr in ['properties', 'values']:
if self._id is None:
value = None
else:
internals = ('id', 'actor', 'activity', 'creator', 'creation')
props = self._cl.getprops()
value = {}
for prop in props.keys():
if not prop in internals:
data = self._cl.get(str(self._id), prop)
if not data is None \
and (not isinstance(data, types.ListType) or data):
if attr == 'properties':
if isinstance(props[prop], hyperdb.Link):
if data:
cl = self._db.getclass(props[prop].classname)
key = cl.getkey()
if key:
value[prop] = cl.get(data, key)
else:
value[prop] = int(data)
elif isinstance(props[prop], hyperdb.Multilink):
cl = self._db.getclass(props[prop].classname)
key = cl.getkey()
if key:
value[prop] = [ cl.get(nodeid, key) for nodeid in data ]
else:
value[prop] = [ int(nodeid) for nodeid in data ]
elif not isinstance(props[prop], hyperdb.String):
value[prop] = str(data)
else:
value[prop] = data
else:
if isinstance(props[prop], hyperdb.Link):
if data:
value[prop] = int(data)
elif isinstance(props[prop], hyperdb.Multilink):
value[prop] = [ int(nodeid) for nodeid in data ]
else:
value[prop] = data
else:
if self._id is None:
raise ValueError, \
"Phantom nodes do not have an attribute '%s'"%attr
else:
props = self._cl.getprops()
# is it a class property?
if props.has_key(attr):
if isinstance(props[attr], hyperdb.Link):
elementid = self._cl.get(str(self._id), attr)
if elementid:
cl = self._db.getclass(props[attr].classname)
parent = ClassWrapper(self._clw._dbw, cl)
value = NodeWrapper(parent, elementid)
else:
value = None
elif isinstance(props[attr], hyperdb.Multilink):
elements = self._cl.get(str(self._id), attr)
cl = self._db.getclass(props[attr].classname)
parent = ClassWrapper(self._clw._dbw, cl)
value = ListWrapper(parent, True, elements)
else:
if attr == 'id':
value = int(self._cl.get(str(self._id), attr))
else:
value = self._cl.get(str(self._id), attr)
# I don't know you
else:
raise AttributeError, \
"'%s' is not a member of object 'NodeWrapper'"%attr
return value
def __setattr__(self, attr, value):
"""\
Grant easy set access to all properties of this node.
If "read only" than all set operations will raise exception
"ValueError".
"""
# test if we are a member of NodeClass or if we will become
# a member of NodeClass
if attr in ['_clw', '_db', '_cl', '_id', '_readonly'] \
or self.__dict__.has_key(attr):
self.__dict__[attr] = value
elif attr in ['properties', 'values']:
raise ValueError, \
"'properties' can't be assigned any value"
else:
if self._id is None:
raise ValueError, \
"Phantom nodes do not have an attribute '%s'"%attr
else:
props = self._cl.getprops()
# is it a class property?
if props.has_key(attr):
if not self._readonly:
if isinstance(props[attr], hyperdb.Link):
cl = self._db.getclass(props[attr].classname)
parent = ClassWrapper(self._clw._dbw, cl)
value = str(parent.getid(value))
elif isinstance(props[attr], hyperdb.Multilink):
cl = self._db.getclass(props[attr].classname)
parent = ClassWrapper(self._clw._dbw, cl)
if isinstance(value, types.StringType):
value = [value]
value = ListWrapper(parent, value)
value = [ str(nodeid) for nodeid in value.getnodeids() ]
elif isinstance(props[attr], hyperdb.Interval) \
and not isinstance(value, date.Interval):
value = date.Interval(value)
elif isinstance(props[attr], hyperdb.Date) \
and not isinstance(value, date.Date):
value = date.Date(value)
self._cl.set(str(self._id), **{attr:value})
else:
raise ValueError, \
"This node is marked as readonly and can't \
be assigned and value"
# not a member or class property
else:
raise AttributeError, \
"'%s' is not a member of object 'NodeWrapper'"%attr
def __iter__(self):
"""\
Create iteration of all properties in this node. The iteration contains
tuples with the property name on the first index and the value on the
second index. For linked properties an instance to a NodeWrapper is
used and not the value.
"""
return iter( [(prop, self.__getattr__(prop)) for prop in self._cl.getprops()] )
def __hash__(self):
"""\
Node identifier.
"""
return self._id
def __eq__(self, other):
"""\
Are these nodes equal?
"""
same_class = isinstance(other, NodeWrapper) \
and self._cl.classname == other._cl.classname
if not other is None:
other = self._clw.getid(other)
return same_class and (other == self._id)
def __ne__(self, other):
"""\
Are these nodes different?
"""
return not self.__eq__(other)
def __nonzero__(self):
"""\
Truth value testing.
"""
return not self._id is None
def plain(self, property=None):
"""\
Return the property in a nice way. Links are resolved.
"""
if not property is None:
value = self.__getattr__(property)
if isinstance(value, types.ListType):
list = []
for one in value:
label = one._clw.getlabel()
if isNode(one):
list.append( getattr(one, label) )
else:
list.append(one)
value = ', '.join( [str(item) for item in list] )
elif isinstance(value, types.NoneType):
value = ''
elif isNode(value):
label = value._clw.getlabel()
value = getattr(value, label)
else:
label = self._clw.getlabel()
value = self.__getattr__(label)
if value is None:
value = ''
return value
def get(self, propname):
"""\
Get the value of a property on an existing node of this class.
'propname' must be the name of a property of this class or a
KeyError is raised.
"""
if self._id is None:
raise NotImplementedError, \
"This method is not implemented for phantom nodes"
else:
return self._clw.get(self._id, propname)
def set(self, **propvalues):
"""\
Modify a property on an existing node of this class.
Each key in 'propvalues' must be the name of a property of this
class or a KeyError is raised.
All values in 'propvalues' must be acceptable types for their
corresponding properties or a TypeError is raised.
If the value of the key property is set, it must not collide with
other key strings or a ValueError is raised.
If the value of a Link or Multilink property contains an invalid
node id, a ValueError is raised.
"""
if self._id is None:
raise NotImplementedError, \
"This method is not implemented for phantom nodes"
else:
self._clw.set(self._id, propvalues)
def is_retired(self):
"""\
Return true if the node is retired.
"""
if self._id is None:
raise NotImplementedError, \
"This method is not implemented for phantom nodes"
else:
return self._clw.is_retired(self._id)
def safeget(self, propname, default=None):
"""\
Safely get the value of a property on an existing node of this class.
Return 'default' if the node doesn't exist.
"""
if self._id is None:
raise NotImplementedError, \
"This method is not implemented for phantom nodes"
else:
return self._clw.safeget(self._id, propname, default)
def pop(self):
"""\
Get node 'key' and remove (retire) it from this class.
"""
if self._id is None:
raise NotImplementedError, \
"This method is not implemented for phantom nodes"
else:
return self._clw.pop(self._id)
def resurrect(self):
"""\
Bring any deleted (retired) node back alive.
"""
if self._id is None:
raise NotImplementedError, \
"This method is not implemented for phantom nodes"
else:
self._clw.resurrect(self._id)
class ListWrapper(types.ListType):
"""\
List object to give easier access to NodeWrapper's.
Introduction
============
This wrapper almost fully supports all available operations for
Python's list object. The difference is that it is meant to be used
in ClassWrapper's and NodeWrapper's.
Initialization
==============
The wrapper can be initialized as follows:
1. lw = dbwrapper.ClassWrapper(<ClassWrapper>)
This is the simplest form. The wrapper will be created to hold
only NodeWrapper objects from class <ClassWrapper>.
The list will be empty.
2. lw = dbwrapper.ClassWrapper(<ClassWrapper>, [<key>|<nodeid>|<NodeWrapper>])
With this form you will fill the list with initial data.
The data must be nodes in <ClassWrapper> and may be addressed
by either key (in case the class has a key column), or by node id
(as integer or string), or by a NodeWrapper object.
For both forms an additional parameter can be set to tell the dbwrapper
that all NodeWrapper classes in the ListClass are either read-only or not.
Default they are set to not read-only, meaning all nodes in the list
may be changed.
Several methods in the ClassWrapper and the NodeWrapper do return
ListWrapper objects. In general these methods are all the search
methods in the ClassWrapper and the 'get' method in the NodeWrapper.
Default the list won't accept duplicate nodes. If you try to add a node
which is already present, a ValueError will be raised. This behavior
can be omitted by setting ListWrapper member 'unique' to False.
If False, then duplicate nodes are accepted. Switching member
'unique' back to True while there are duplicate nodes will raise
a ValueError too.
"""
class RegExpClass:
"""\
A class with regular expression methods that will filter nodes
from hyperdb.Class. The methods in this class are one to one
with the regular expression methods in python module 're'.
"""
def __init__(self, owner):
"""\
Initialize class
"owner" must be of type ListWrapper
"""
if not isinstance(owner, ListWrapper):
raise TypeError, \
"Owner object isn't an instance of 'ListWrapper'"
self._lw = owner
def __repr__(self):
"""\
Slightly more useful representation
"""
return '''<dbwrapper.ListWrapper.RegExpClass '%s'>'''% \
self._lw._clw._cl
def __str__(self):
"""\
Slightly more useful representation
"""
return self.__repr__()
def match(self, pattern, flags=0, column=None):
"""\
Find all nodes in the class that have a result when
applying the "pattern" at the start of the string in
"column", returning a list object with tuples pairs.
The first index of each tuple contains a
dbwraper.NodeWrapper object of a node that matches
with "pattern".
The second index in the tuples holds the match object.
"flags" may have the same flags as used/defined in
python module 're'. These flags are also available
as members of module 'dbwrapper'.
(e.g. dbwrapper.IGNORECASE)
"column" can be used to perform a search on an
other column than the column returned by
ClassWrapper.getlabel().
"""
return self._lw._clw.re.match(pattern, flags, column, nodelist=self._lw)
def search(self, pattern, flags=0, column=None):
"""\
Find all nodes in the class that have a result when
scanning through the string in "column" looking for a
match to the "pattern", returning a list object with
tuples pairs.
The first index of each tuple contains a
dbwraper.NodeWrapper object of a node that matches
with "pattern".
The second index in the tuples holds the search object.
"flags" may have the same flags as used/defined in
python module 're'. These flags are also available
as members of module 'dbwrapper'.
(e.g. dbwrapper.IGNORECASE)
"column" can be used to perform a search on an
other column than the column returned by
ClassWrapper.getlabel().
"""
return self._lw._clw.re.search(pattern, flags, column, nodelist=self._lw)
def findall(self, pattern, flags=0, column=None):
"""\
Return all nodes in class that have "pattern" matches
in the strings in "column". Returned is a list of tuple
pairs.
The first tuple index contains a NodeWrapper object of
a matching node.
The second tuple index contains a list of all
non-overlapping matches in the node's "column" string.
If one or more groups are present in the "pattern", the
second tuple index will contain a list of groups; this
will be a list of tuples if the "pattern" has more than
one group.
"flags" may have the same flags as used/defined in
python module 're'. These flags are also available
as members of module 'dbwrapper'.
(e.g. dbwrapper.IGNORECASE)
"column" can be used to perform a search on an
other column than the column returned by
ClassWrapper.getlabel().
"""
return self._lw._clw.re.findall(pattern, flags, column, nodelist=self._lw)
def __init__(self, owner, readonly=False, *list):
"""\
Create a list object for dbwrapper.
"owner" must be a 'ClassWrapper' object. Only nodes present
in 'ClassWrapper' will be allowed in the list.
"readonly" can be set to prevent that properties of the nodes
in the list can be changed. The list content can always be
changed.
"list" can be a list of any node specification like key,
nodeid or NodeClass object.
"""
if not isClass(owner):
raise TypeError, \
"owner object isn't an instance of 'ClassWrapper'"
self.__list = []
self._clw = owner
self.__regexp = False
self.re = self.RegExpClass(self)
self.unique = True
if isinstance(readonly, types.ListType) \
or isinstance(readonly, types.TupleType):
self._readonly = False
list = readonly
else:
self._readonly = readonly
if list:
if len(list) == 1 \
and isinstance(list[0], types.ListType):
list = list[0]
self.__iadd__(list)
def __repr__(self):
"""\
Slightly more useful representation
"""
return str(self.__list)
def __str__(self):
"""\
Slightly more useful representation
"""
return self.__repr__()
def __getitem__(self, index):
"""\
Retrieve an item at position 'index'.
"""
return self.__list[index]
def __setitem__(self, index, value):
"""\
Assign 'value' to the item at position 'index'.
"""
self.__inner_set(index, value)
def __delitem__(self, index):
"""\
Delete the item at position 'index'.
"""
del self.__list[index]
def __setattr__(self, attr, value):
"""\
Set any attribute.
This method prevents that member 'unique' can be set to True
while the content in the list is not unique.
"""
if attr == 'unique' \
and value \
and self.__dict__.has_key(attr) \
and not self.unique:
for index, node1 in enumerate(self.__list):
if self.__regexp:
node1 = node1[0]
for node2 in self.__list[index+1:]:
if self.__regexp:
node2 = node2[0]
if node1 == node2:
raise ValueError, \
"Can't set list to unique. Node '%s' has more \
than one instances in list."%node1
self.__dict__[attr] = value
def __len__(self):
"""\
Number of items in list.
"""
return len(self.__list)
def __isub__(self, other):
"""\
Remove items from list.
"""
if not isinstance(other, types.ListType) \
and not isinstance(other, types.TupleType):
other = [ other ]
for node in other:
self.remove(node)
return self
def __iadd__(self, other):
"""\
Add items to list.
"""
if not isinstance(other, types.ListType) \
and not isinstance(other, types.TupleType):
other = [ other ]
for node in other:
self.append(node)
return self
def __iter__(self):
"""\
Step trough the list.
"""
return iter(self.__list)
def __getslice__(self, start, stop):
"""\
Get a piece of the list.
"""
return self.__list[start:stop]
def __setslice__(self, start, stop, sequence):
"""\
Set a piece of the list.
"""
if isinstance(sequence, types.ListType) \
or isinstance(sequence, types.TupleType):
for index in range(start, stop):
if index < len(sequence):
self.__setitem__(index, sequence[index])
else:
del self.__list[stop-1]
else:
del self.__list[start:stop]
self.insert(start, sequence)
def __delslice__(self, start, stop):
"""\
Delete a piece of the list.
"""
del self.__list[start:stop]
def __contains__(self, value):
"""\
Determine if the list has a given node.
"""
try:
index = self.index(value)
except:
found = False
else:
found = (index > -1)
return found
def __inner_set(self, index, value):
"""\
Handles the 'set' operation of all data changing methods.
"""
regexp = False
if isinstance(value, types.TupleType):
if len(value) == 2:
# regulare expression list
node = value[0]
regexp = True
else:
node = None
else:
node = value
if not isNode(node):
try:
node = NodeWrapper(self._clw, node, self._readonly)
except:
raise TypeError, \
"Value isn't an instance of 'NodeWrapper'"
if node._cl.classname != self._clw._cl.classname:
raise TypeError, \
"Node '%s' isn't member of class '%s'"% \
(node, self._clw._cl.classname)
if self.unique:
try:
list_index = self.index(node)
except ValueError:
pass
else:
if list_index != index:
raise ValueError, \
"Node '%s' already present in unique list."%node
if len(self.__list) == 0 \
or (len(self.__list) == 1 and self.__list[index] == None):
self.__regexp = regexp
if self.__regexp:
if regexp:
self.__list[index] = (node, value[1])
else:
self.__list[index] = (node, None)
else:
if regexp:
self.__list[index] = node
else:
self.__list[index] = node
def append(self, value):
"""\
Append a node to the list.
"""
self.insert(len(self.__list), value)
def insert(self, index, value):
"""\
Insert a node at position 'index'.
"""
self.__list.insert(index, None)
try:
self.__inner_set(index, value)
except:
del self.__list[index]
raise
def extend(self, other):
"""\
Extend the list with the content of an other list.
"""
self.__iadd__(other)
def remove(self, value):
"""\
Remove the first node in the list that matches value.
"""
index = self.index(value)
del self.__list[index]
def count(self, value):
"""\
Return the number of occurrences of value.
"""
value = NodeWrapper(self._clw, value)
if not isNode(value):
raise TypeError, \
"Value isn't an instance of 'NodeWrapper'"
occurrens = 0
for index, node in enumerate(self.__list):
if self.__regexp:
# regulare expression list
node = node[0]
if node == value:
occurrens += 1
return occurrens
def pop(self, index):
"""\
Return the node at position 'index' and remove it from the list.
"""
value = self.__list[index]
del self.__list[index]
return value
def sort(self):
"""\
Sort all nodes in the list. The sorting will be done on column
'order'. If the class has no column 'order', then the node id
is used.
"""
def inner_sort(self, prop, index1, index2):
if self.__regexp:
# regulare expression list
node1 = self.__list[index1][0]
node2 = self.__list[index2][0]
else:
node1 = self.__list[index1]
node2 = self.__list[index2]
value1 = int(getattr(node1, prop))
value2 = int(getattr(node2, prop))
if value1 > value2:
temp = self.__list[index2]
self.__list[index2] = self.__list[index1]
self.__list[index1] = temp
return True
else:
return False
if self.__list:
props = self._clw._cl.getprops()
if props.has_key('order'):
prop = 'order'
else:
prop = 'id'
swap = True
while swap:
swap = False
for index in range(0, len(self.__list)-1):
swap = swap or inner_sort(self, prop, index, index+1)
def reverse(self):
"""\
Reverse all nodes in the list.
"""
self.__list.reverse()
def index(self, value, start=None, stop=None):
"""\
Return the index position of the first node that matches value.
'start' and 'stop' can be used to search trough a slice of the list.
"""
try:
value = NodeWrapper(self._clw, value)
except IndexError:
raise ValueError, \
"index(%s): %s not in list"%(value, value)
if not isNode(value):
raise TypeError, \
"Value isn't an instance of 'NodeWrapper'"
if start is None:
start = 0
if stop is None:
stop = len(self.__list)
index = 0
for index, node in enumerate(self.__list[start:stop]):
if self.__regexp \
and isinstance(node, types.TupleType):
# regulare expression list
node = node[0]
if value == node:
return index
raise ValueError, \
"index(%s): %s not in list"%(value, value)
def get(self, key):
"""\
Return a node indexed by 'key'.
"key" identifies the node.
It can have one of the next types:
1 - an integer
The 'key' will be treated as the id of the node
and used for the indexing.
2 - a string
The 'key' will be treated as key argument of the
node and used to lookup the node id.
If the lookup action raises a KeyError and 'key'
only contains digits, the integer value of 'key'
will be used as the node id.
In both cases the node id is used for the indexing.
3 - an instance of NodeWrapper
The node id will be obtained from the NodeWrapper
class and used for the indexing.
4 - None type
This is a special option implemented to make it
easy usable in detectors.
"""
index = self.index(key)
return self.__list[index]
def getnodeids(self):
"""\
Return a (normal) list containing all node ids (as integer) of all
nodes in this list class.
"""
ids = []
for node in self.__list:
if self.__regexp:
# regulare expression list
node = node[0]
ids.append(node._id)
return ids
def filter(self, search_matches, filterspec, sort=(None,None), group=(None,None)):
"""\
Return a list of the NodeWrappers of the active nodes in this class
that match the 'filter' spec, sorted by the group spec and then the
sort spec.
"filterspec" is {propname: value(s)}
"sort" and "group" are (dir, prop) where dir is '+', '-' or None
and prop is a prop name or None
"search_matches" is {nodeid: marker}
The filter must match all properties specified - but if the
property value to match is a list, any one of the values in the
list may match for that property to match.
"""
return self._clw.filter(search_matches, filterspec, sort, group, nodelist=self)
def search(self, value, column=None):
"""\
Lookup all nodes that have a 100% match with 'value' and
return them in a list.
"value" is the pattern to look for.
"column" can be set to any string property. A TypeError is
raised if 'column' isn't a string property. The label column
will be used if 'column' is omitted.
"""
return self._clw.search(value, column, nodelist=self)
def simple(self):
"""\
Converts the list content to the simple format. This method can be
used to get rid of the regular expression object when a list was
created by a regular expression search.
"""
if self.__regexp:
for index, node in enumerate(self.__list):
self.__list[index] = node[0]
self.__regexp = False