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_<br>
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.
- This example will return 'urgent'. Not really meaningful, but it's only meant as an example.
- Will return the nodeid of status node 'unread'.
- dbw.issue['1'].title
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_<br> 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:<br> '/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:<br> <code>all = [ 'dbwrapper' ]</code>
What's left is importing the wrapper in your Python sources (e.g. detectors and action handlers). You do that by adding the line:<br> 'from roundup.extensions import dbwrapper'<br> 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_<br> 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)
- dbw = dbwrapper.DBWrapper(db)
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)
- dbw = dbwrapper.DBWrapper(db)
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_<br> 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)
- dbw = dbwrapper.DBWrapper(self.db)
Below you find some more examples and of course the source code for the wrapper.
Best regards,<br> Marlon van den Berg
PS: The wrapper has been tested with 0.7.12 and 1.1.0.
<hr>
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')
- 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)
- 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)
- 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)
- 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)
- 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})
- 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)
- To lookup all issues of which the title starts witch 'roundup':
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)
- lw = dbw.issue.re.search('^roundup', dbwrapper.IGNORECASE) lw.issue.filter(None, {'status':'unread'})
- To lookup all issues of which the title starts witch 'roundup' and have 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)
- Lookup all issues that have at least one message attached which starts with the word 'roundup':
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
- print " %s"%nodew.plain() for property in nodew:
- print clw.classname for nodew in clw:
- for clw in dbw:
- 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.
<hr>
The Source
- And finally the source code
1 """\
2 Introduction
3 ============
4 Database wrapper implementation.
5 This wrapper can be used to have easy access to RoundUp's
6 hyper database without having to know too much knowledge
7 about the class structures and (multi) links.
8
9 This document contains a few examples. In those cases
10 you will see objects like 'db', 'dbw', 'cl', 'clw'
11 and 'nodew'. These object are:
12 db : hyperdb.Database
13 cl : hyperdb.Class (or FileClass or IssueClass)
14 dbw : dbwrapper.DBWrapper
15 clw : dbwrapper.ClassWrapper
16 nodew : dbwrapper.NodeClass
17 lw : dbwrapper.ListWrapper
18
19
20 Features
21 ========
22
23 Addressing
24 ----------
25 Addressing a specific node in a class is easier with this
26 wrapper. It almost has the looks and feels of the wrapper
27 used in the TAL templates. Also with this wrapper you don't
28 really need to use 'get' methods to get the data. Just address
29 the data as if they are member of a tree with the database
30 object as root. The class objects as limbs and the class
31 properties as leaves. What's left is how to address the right
32 node. Well that can be done with indexing the class (sorry
33 but I don't know were to put that in a tree). As index you
34 can either use the node id as integer or string, but you
35 may also use a key value (if the class has a key column).
36 Here are some examples:
37 - dbw.issue[1].title
38 dbw.issue['1'].title
39 Both these examples will return the title of issue 1.
40 - dbw.priority['urgent'].name
41 This example will return 'urgent'. Not really
42 meaningful, but it's only meant as an example.
43 - dbw.status['unread'].id
44 Will return the nodeid of status node 'unread'.
45
46 In general the benefit of this wrapper is that you don't
47 really have to know any of the node ids. All 'set' or indexing
48 methods will resolve key values into id's. Even so will all
49 'get' methods return the key values and not the id's when it
50 comes to linked items.
51
52 Searching
53 ---------
54 The wrapper is capable of handling regular expression searches.
55 It will not only return you the found nodes, but it also will
56 return the found matches per node.
57
58
59 Usage
60 =====
61 To use the wrapper you need to place it somewhere. In our case
62 we placed it in a sub-folder of RoundUp's core code which remains
63 in the 'site-packages' folder of the Python library tree.
64 In this /lib/site-packages/roundup we created a folder named
65 'extensions'. This is about conform the standard that Richard uses
66 for additional stuff in tracker templates (we look at the wrapper
67 as additional stuff for the RoundUp core code). In other words
68 the complete path to the wrapper will be:
69 /lib/site-packages/roundup/extensions/dbwrapper.py
70 It will be easiest if the wrapper can be imported as sub package.
71 This will require a '__init__.py' file in that directory to. The
72 content of that file will only be this line:
73 __all__ = [ 'dbwrapper' ]
74
75 What's left is importing the wrapper in your Python sources (e.g.
76 detectors and action handlers). You do that by adding the line:
77 from roundup.extensions import dbwrapper
78 to all sources in which you want to use the wrapper.
79
80 There's only one comment left. As you can see, the wrapper has a
81 lot of documentation strings inside. Therefor we suggest to run
82 all scripts with the '-OO' option passed to Python's interpreter.
83 This will prevent that your scripts will load these documentation
84 strings too. If you're not sure, then please read section
85 '6.1.2 "Compiled" Python files' of the tutorial in the Python
86 documentation.
87
88 Detectors
89 ---------
90 The wrapper can easily be used in a detector without having to
91 change anything to RoundUp's core code.
92 This is what you do in an auditor:
93 def auditor(db, cl, nodeid, newvalues):
94 dbw = dbwrapper.DBWrapper(db)
95 clw = dbwrapper.ClassWrapper(dbw, cl)
96 nodew = dbwrapper.NodeWrapper(clw, nodeid)
97
98 And this in a reactor:
99 def reactor(db, cl, nodeid, oldvalues):
100 dbw = dbwrapper.DBWrapper(db)
101 clw = dbwrapper.ClassWrapper(dbw, cl)
102 nodew = dbwrapper.NodeWrapper(clw, nodeid)
103
104 As you can see, they are both the same.
105 In both cases 'dbw' will be the root object of the tree.
106 'clw' will be a wrapper around hyperdb object 'cl'. In most cases
107 you don't really need that one, but it is needed to create the
108 node wrapper 'nodew'.
109
110 In case of an auditor, 'nodeid' can be 'None' if the auditor was
111 fired by a 'create' operation. In that case 'nodew' will be a
112 phantom node which doesn't have any properties (except 'id' which
113 will be 'None'). A test like:
114 nodew is None
115 will return True on phantom nodes. It will return False if the node
116 isn't a phantom node but a real living one (auditor fired by 'set'
117 operation).
118
119 Action handlers
120 ---------------
121 The wrapper is almost used the same way in an action handler as it
122 is used in a detector.
123 This is what you do:
124 def handler(self):
125 dbw = dbwrapper.DBWrapper(self.db)
126 clw = dbwrapper.ClassWrapper(dbw, self.cl)
127 nodew = dbwrapper.NodeWrapper(clw, self.nodeid)
128
129
130 Examples
131 ========
132 Here are a few more examples what dbwrapper can do for you:
133 1. Reading a linked property:
134 - With RoundUp's hyperdb:
135 unread_id = db.status.lookup('unread')
136 db.status.get(unread_id, 'order')
137
138 - With dbwrapper:
139 dbw.status['unread'].order
140
141 2. Changing a linked property:
142 - With RoundUp's hyperdb:
143 unread_id = db.status.lookup('unread')
144 db.issue.set('1', status=unread_id)
145
146 - With dbwrapper:
147 dbw.issue[1].status = 'unread'
148
149 3. Adding a link to a multi linked property:
150 - With RoundUp's hyperdb:
151 topics = db.issue.get('1', 'topic')
152 keyword_id = db.keyword.lookup('DBWrapper')
153 topics.append(keyword_id)
154 db.issue.set('1', topic=topics)
155
156 - With dbwrapper:
157 dbw.issue[1].topic += 'DBWrapper'
158 or:
159 dbw.issue[1].topic += ['DBWrapper']
160
161 4. Removing a link from a multi linked property:
162 - With RoundUp's hyperdb:
163 topics = db.issue.get('1', 'topic')
164 keyword_id = db.keyword.lookup('DBWrapper')
165 topics.remove(keyword_id)
166 db.issue.set('1', topic=topics)
167
168 - With dbwrapper:
169 dbw.issue[1].topic -= 'DBWrapper'
170 or:
171 dbw.issue[1].topic -= ['DBWrapper']
172
173 5. Duplicating an issue with dbwrapper:
174 - With dbwrapper:
175 issue = db.issue[1].properties
176 issue['title'] = 'Copy of %s'%issue['title']
177 db.issue += issue
178
179 6. Retiring a node:
180 - With RoundUp's hyperdb:
181 nodeid = db.status.lookup('chatting')
182 db.status.retire(nodeid)
183
184 - With dbwrapper:
185 del dbw.status['chatting']
186
187 7. Filtering:
188 - With RoundUp's hyperdb:
189 nodeids = [ db.status.lookup(name) for name in ['chatting', 'unread'] ]
190 db.filter(None, {'status':nodeids})
191
192 - With dbwrapper:
193 dbw.filter(None, {'status':['chatting', 'unread']})
194
195 8. Regular expression search:
196 - With dbwrapper:
197 To lookup all issues of which the title starts witch 'RoundUp':
198 dbw.issue.re.search('^RoundUp', dbwrapper.IGNORECASE)
199
200 9. Combination RE search:
201 - With dbwrapper:
202 To lookup all issues of which the title starts witch 'RoundUp'
203 and have status 'unread':
204 lw = dbw.issue.filter(None, {'status':'unread'})
205 lw.re.search('^RoundUp', dbwrapper.IGNORECASE)
206 The next will have the same result but takes longer:
207 lw = dbw.issue.re.search('^RoundUp', dbwrapper.IGNORECASE)
208 lw.issue.filter(None, {'status':'unread'})
209
210 10. More search possibilities:
211 - With dbwrapper:
212 Lookup all issues that have at least one message attached which
213 starts with the word 'RoundUp':
214 flags = dbwrapper.IGNORECASE + dbwrapper.MULTILINE
215 lw = dbw.msg.re.search('\ARoundUp', flags, column='content')
216 dbw.issue.find(messages=lw)
217
218 11. Iteration:
219 - With dbwrapper:
220 The next code will step trough all nodes in all classes and print
221 the content on the screen. Please don't try this on your 10,000
222 issue tracker. If you do, well I guess you will be spending your
223 day next to the coffee machine.
224 for clw in dbw:
225 print clw.classname
226 for nodew in clw:
227 print " %s"%nodew.plain()
228 for property in nodew:
229 print " | %s"%property
230 """
231
232 __docformat__ = 'restructuredtext'
233
234 import types, os
235
236 from re import IGNORECASE, DOTALL, LOCALE, MULTILINE, UNICODE, VERBOSE
237 from re import I, S, L, M, U, X
238
239 from roundup import instance, hyperdb, date
240
241
242 isHyperDB = lambda object: isinstance(object, hyperdb.Database)
243 isHyperDBClass = lambda object: isinstance(object, hyperdb.Class)
244 isDB = lambda object: isinstance(object, DBWrapper)
245 isClass = lambda object: isinstance(object, ClassWrapper)
246 isNode = lambda object: isinstance(object, NodeWrapper)
247
248
249
250 def to_hyperdb(*nodes):
251 """\
252 Converts a list (or single node) of dbwrapper nodes to a list of
253 hyperdb compliant nodes.
254 """
255
256 result = []
257
258 if nodes:
259 if len(nodes) == 1 \
and (isinstance(nodes[0], types.ListType) \
or isinstance(nodes[0], types.TupleType)):
260 nodes = nodes[0]
261
262 for node in nodes:
263 if isinstance(node, types.TupleType) \
and len(node) == 2:
264 # regular expression list
265 node = node[0]
266
267 if not isNode(node):
268 raise ValueError, \
"Node '%s' isn't an instance of 'NodeWrapper'."%node
269
270 result.append(str(node._id))
271
272 return result
273
274
275
276 class DBWrapper:
277 """\
278 A wrapper around RoundUp's hyperdb.Database.
279
280 Initialization
281 ==============
282 There are three methods to initialize the DBWrapper:
283 1. dbw = dbwrapper.DBWrapper(<tracker home>)
284 In this case the wrapper will open the database
285 belonging to <tracker_home>. On destruction it
286 will always close the database.
287 This form is useful in separate scripts which
288 need to access the RoundUp database.
289
290 2. dbw = dbwrapper.DBWrapper()
291 The wrapper will open the database for you if
292 the environment has a variable TRACKER_HOME
293 which points to a valid tracker home folder.
294 On destruction the database will be closed.
295 This form is useful in separate scripts which
296 need to access the RoundUp database.
297
298 3. dbw = dbwrapper.DBWrapper(<hyperdb.Dtabase object>)
299 The wrapper will not open the database, but will
300 use an already opened database. The database
301 object is passed as parameter to the wrapper and
302 should be an opened hyperdb.Database instance.
303 The wrapper will never close the database.
304 This form is useful in detectors and action
305 handlers.
306
307 Usage
308 =====
309 You have full access to the database as soon as the
310 DBWrapper is initialized. To access a class, just access
311 it as being a member of the wrapper (same as with hyperdb).
312 Syntax:
313 <DBWrapper>.<class name>
314 The returned object will be an initialized ClassWrapper
315 object.
316
317 Examples:
318 dbw.issue
319 dbw.status
320
321 Transparency
322 ============
323 Members defined in hyperdb.Database, but not
324 defined in this 'DBWrapper' class can still be
325 used as if they are member of this class.
326 """
327
328 def __init__(self, arg=None, *user):
329 """\
330 Initiate a wrapper around RoundUp's hyperdb.Database.
331
332 "arg" is used to specify the database.
333 It can have three different types:
334 1 - an instance of an open RoundUp hyperdb.Database.
335 This is the most common usage if the wrapper is
336 used within any detector or action handler.
337 2 - a valid tracker home path.
338 This allows us to use the wrapper in any external
339 script without having to open a tracker instance
340 and the database before using the wrapper.
341 If used like this, the destructor of this class
342 will close the database to prevent pending open
343 database links.
344 3 - not specified.
345 This is almost similar with type 2 except that
346 environment variable TRACKER_HOME will be used
347 to open the database.
348
349 "user" has only meaning if 'arg' is of type 2 or 3.
350 In those cases the database will be opened as being 'user'.
351 Default it opens the database as 'admin'.
352 """
353
354 if isHyperDB(arg):
355 # arg is a roundup database instance
356 # no need to open the database
357 self._instance = None
358 self._db = arg
359
360 else:
361 if isinstance(arg, types.StringType):
362 # arg is a string instance
363 # we assume it is a valid tracker_home location
364 # and we will use it to open the database
365 tracker_home = arg
366
367 elif arg is None:
368 # arg is None -> no argument was passed
369 # user TRACKER_HOME environment variable
370 # to open the database
371 tracker_home = os.getenv('TRACKER_HOME')
372
373 else:
374 # and what now?
375 raise TypeError, \
"No 'hyperdb' to access"
376
377 if not user:
378 user = ('admin',)
379
380 self._instance = instance.open(tracker_home)
381 self._db = self._instance.open(user[0])
382
383 # create storage for classes
384 self._classes = {}
385 for classname in self._db.getclasses():
386 self._classes[classname] = {}
387
388 def __del__(self):
389 """\
390 Closes the RoundUp database if it was opened by the wrapper
391 it selves (initiated with an 'arg' of type 2 or 3)
392 """
393
394 # restore possible turned off detectors
395 for classname in self._db.getclasses():
396 # restore auditors
397 if self._classes[classname].has_key('auditors'):
398 self._db.getclass(classname).auditors = \
self._classes[classname]['auditors']
399
400 # restore reactors
401 if self._classes[classname].has_key('reactors'):
402 self._db.getclass(classname).reactors = \
self._classes[classname]['reactors']
403
404 # make sure we close the database if we did open it ourselves
405 if not self._instance is None:
406 self._db.close()
407
408 def __repr__(self):
409 """\
410 Slightly more useful representation
411 """
412
413 return '''<dbwrapper.DBWrapper '%s'>'''%self._db
414
415 def __str__(self):
416 """\
417 Slightly more useful representation
418 """
419
420 return self.__repr__()
421
422 def __getattr__(self, attr):
423 """\
424 Grant access to ClassWrapper objects of all
425 classes like they are member of this class.
426
427 It also creates transparency to hyperdb.Database
428 members if they aren't defined in the DBWrapper
429 class.
430 """
431
432 # If 'attr' is a member, then access that member
433 if self.__dict__.has_key(attr):
434 value = self.__dict__[attr]
435
436 # If 'attr' is a hyperdatabse class, wrap it in
437 # a 'ClassWrapper'
438 elif attr in self._db.getclasses():
439 value = ClassWrapper(self, attr)
440
441 # If 'attr' is a member of class 'Database', return
442 # the address of that member to create the transparency
443 else:
444 value = getattr(self._db, attr)
445
446 return value
447
448 def __setattr__(self, attr, value):
449 """\
450 Prevent that attributes which refer to ClassWrapper
451 objects are overwritten by a wrong assign statement.
452 """
453
454 # If 'attr' is a member or member to be,
455 # then access that member
456 if attr in ['_instance', '_db', '_classes'] \
or self.__dict__.has_key(attr):
457 self.__dict__[attr] = value
458
459 # If 'attr' is a hyperdatabse class, wrap it in
460 # a 'ClassWrapper'
461 elif attr in self._db.getclasses():
462 # Check if it isn't the result of 'ClassWrapper.__iadd__'
463 if not isClass(value) or value._cl.classname != attr:
464 raise ValueError, \
"'%s' can't be assigned any value"%attr
465
466 # If 'attr' is a member of class 'Database', return
467 # the address of that member to create the transparency
468 else:
469 setattr(self._db, attr, value)
470
471 def __iter__(self):
472 """\
473 Create iteration of all classes as ClassWrapper's.
474 """
475
476 return iter( [ClassWrapper(self, classname) \
for classname in self._db.getclasses()] )
477
478 def getclass(self, classname):
479 """\
480 Get the ClassWrapper object representing a particular class.
481 """
482
483 return self.__getattr__(classname)
484
485
486
487 class ClassWrapper:
488 """\
489 A wrapper around a RoundUp hyperdb.Class.
490
491 Introduction
492 ============
493 In most cases you don't need to initialize a
494 ClassWrapper because the DBWrapper will do that
495 for you where needed. But sometimes it can be
496 more convenient to initialize one yourself.
497
498 Initialization
499 ==============
500 There are three methods to initialize a ClassWrapper:
501 1. clw = dbwrapper.ClassWrapper(dbw, <class name>)
502 A wrapper will be created for class <class name>.
503
504 2. clw = dbwrapper.ClassWrapper(dbw, <hyperdb.Class>)
505 A wrapper will be created for the hyperdb.Class object.
506
507 3. clw = dbwrapper.ClassWrapper(dbw, <ClassWrapper>)
508 A wrapper will be created for the ClassWrapper object.
509 This won't make much sense because the wrapper was already there.
510
511 Usage
512 =====
513 The wrapper behaves like a dictionary, meaning you can access the
514 nodes by indexing them as dictionary items.
515 Syntax:
516 <DBWrapper>.<class name>[<key>|<nodeid>|<NodeWrapper>]
517 As index you can either use the node id as integer or string,
518 a key value (if the class has a key column) or an instance of
519 NodeWrapper.
520 The returned value is a NodeWrapper object.
521
522 Examples:
523 dbw.status['chatting']
524 dbw.issue[1]
525 dbw.priority['3']
526 dbw.msg[nodew]
527
528 Some of the methods in this class do have a "key"
529 parameter. In all these cases this parameter can
530 have one of the next types:
531 1 - an integer
532 The 'key' will be treated as the id of the node
533 and used for the indexing.
534 2 - a string
535 The 'key' will be treated as key argument of the
536 node and used to lookup the node id.
537 If the lookup action raises a KeyError and 'key'
538 only contains digits, the integer value of 'key'
539 will be used as the node id.
540 In both cases the node id is used for the indexing.
541 3 - an instance of NodeWrapper
542 The node id will be obtained from the NodeWrapper
543 class and used for the indexing.
544
545 Transparency
546 ============
547 Members defined in the hyperdb.Class, but not
548 defined in this 'ClassWrapper' class can still
549 be used as if they are member of this class.
550 """
551
552 class RegExpClass:
553 """\
554 A class with regular expression methods that will filter nodes
555 from hyperdb.Class. The methods in this class are one to one
556 with the regular expression methods in python module 're'.
557 """
558
559 def __init__(self, owner):
560 """\
561 Initialize class
562
563 "owner" must be of type 'ClassWrapper'
564 """
565
566 import re
567
568 if not isClass(owner):
569 raise TypeError, \
"Owner object isn't an instance of 'ClassWrapper'"
570
571 self._clw = owner
572 self.__re = re
573
574 def __repr__(self):
575 """\
576 Slightly more useful representation
577 """
578
579 return '''<dbwrapper.ClassWrapper.RegExpClass '%s'>'''% \
self._clw._cl
580
581 def __str__(self):
582 """\
583 Slightly more useful representation
584 """
585
586 return self.__repr__()
587
588 def __inner_re(self, re_func, column, nodelist):
589 """\
590 Handles the main reg. expression search.
591
592 "re_func" must contain one of the search methods
593 from standard python module 're'.
594
595 "column" can be used to perform a search on an
596 other column than the column returned by
597 ClassWrapper.getlabel().
598
599 The return value is a list object containing tuples
600 pairs.
601
602 The first index of each tuple contains a NodeWrapper
603 object to a node matching the search.
604
605 The second index of the tuples holds the result object
606 of the corresponding regular expression method passed
607 in "re_func".
608
609 "nodelist" can be used to limit the search within the list
610 of nodes. 'nodelist' may be a list of ids or any list created
611 by any of the search methods in this module.
612 """
613
614 if not column:
615 column = self._clw.getlabel()
616
617 if nodelist is None:
618 list = self._clw._cl.list()
619 else:
620 list = ListWrapper(self._clw, nodelist).getnodeids()
621
622 result = []
623 for nodeid in list:
624 value = self._clw._cl.get(nodeid, column)
625
626 if value is None:
627 value = ''
628
629 res = re_func(value)
630
631 if res:
632 result.append( (NodeWrapper(self._clw, nodeid, False), res) )
633
634 return ListWrapper(self._clw, result)
635
636 def match(self, pattern, flags=0, column=None, nodelist=None):
637 """\
638 Find all nodes in the class that have a result when
639 applying the "pattern" at the start of the string in
640 "column", returning a list object with tuples pairs.
641
642 The first index of each tuple contains a
643 dbwraper.NodeWrapper object of a node that matches
644 with "pattern".
645
646 The second index in the tuples holds the match object.
647
648 "flags" may have the same flags as used/defined in
649 python module 're'. These flags are also available
650 as members of module 'dbwrapper'.
651 (e.g. dbwrapper.IGNORECASE)
652
653 "column" can be used to perform a search on an
654 other column than the column returned by
655 ClassWrapper.getlabel().
656
657 "nodelist" can be used to limit the search within the list
658 of nodes. 'nodelist' may be a list of ids or any list created
659 by any of the search methods in this module.
660 """
661
662 reg = self.__re.compile(pattern, flags)
663
664 return self.__inner_re(reg.match, column, nodelist)
665
666 def search(self, pattern, flags=0, column=None, nodelist=None):
667 """\
668 Find all nodes in the class that have a result when
669 scanning through the string in "column" looking for a
670 match to the "pattern", returning a list object with
671 tuples pairs.
672
673 The first index of each tuple contains a
674 dbwraper.NodeWrapper object of a node that matches
675 with "pattern".
676
677 The second index in the tuples holds the search object.
678
679 "flags" may have the same flags as used/defined in
680 python module 're'. These flags are also available
681 as members of module 'dbwrapper'.
682 (e.g. dbwrapper.IGNORECASE)
683
684 "column" can be used to perform a search on an
685 other column than the column returned by
686 ClassWrapper.getlabel().
687
688 "nodelist" can be used to limit the search within the list
689 of nodes. 'nodelist' may be a list of ids or any list created
690 by any of the search methods in this module.
691 """
692
693 reg = self.__re.compile(pattern, flags)
694
695 return self.__inner_re(reg.search, column, nodelist)
696
697 def findall(self, pattern, flags=0, column=None, nodelist=None):
698 """\
699 Return all nodes in class that have "pattern" matches
700 in the strings in "column". Returned is a list of tuple
701 pairs.
702
703 The first tuple index contains a NodeWrapper object of
704 a matching node.
705
706 The second tuple index contains a list of all
707 non-overlapping matches in the node's "column" string.
708
709 If one or more groups are present in the "pattern", the
710 second tuple index will contain a list of groups; this
711 will be a list of tuples if the "pattern" has more than
712 one group.
713
714 "flags" may have the same flags as used/defined in
715 python module 're'. These flags are also available
716 as members of module 'dbwrapper'.
717 (e.g. dbwrapper.IGNORECASE)
718
719 "column" can be used to perform a search on an
720 other column than the column returned by
721 ClassWrapper.getlabel().
722
723 "nodelist" can be used to limit the search within the list
724 of nodes. 'nodelist' may be a list of ids or any list created
725 by any of the search methods in this module.
726 """
727
728 reg = self.__re.compile(pattern, flags)
729
730 return self.__inner_re(reg.findall, column, nodelist)
731
732
733 def __init__(self, parent, cl):
734 """\
735 Initiate a wrapper around a hyperdb.Class.
736
737 "parent" is the DBWrapper that owns this ClassWrapper.
738
739 "cl" is used to specify the class.
740 It can have three different types:
741 1 - an instance of a hyperdb.Class.
742 2 - an instance of a ClassWrapper.
743 3 - the class name.
744 """
745
746 if not isDB(parent):
747 raise TypeError, \
"Parent object isn't an instance of 'DBWrapper'"
748
749 self._dbw = parent
750 self._db = parent._db
751
752 if isHyperDBClass(cl):
753 self._cl = cl
754 elif isClass(cl):
755 self._cl = cl._cl
756 else:
757 self._cl = self._db.getclass(cl)
758
759 self.re = self.RegExpClass(self)
760
761 self.__lastid = None
762
763 def __repr__(self):
764 """\
765 Slightly more useful representation
766 """
767
768 return '''<dbwrapper.ClassWrapper '%s'>'''%self._cl
769
770 def __str__(self):
771 """\
772 Slightly more useful representation
773 """
774
775 return self.__repr__()
776
777 def __call__(self, *nodeids):
778 """\
779 Converts a list (or single id) of hyperdb node ids to a list of
780 dbwrapper compliant nodes.
781 """
782
783 result = ListWrapper(self)
784
785 if nodeids:
786 if len(nodeids) == 1 \
and (isinstance(nodeids[0], types.ListType) \
or isinstance(nodeids[0], types.TupleType)):
787 nodeids = nodeids[0]
788
789 for nodeid in nodeids:
790 result.append(NodeWrapper(self, int(nodeid)))
791
792 return result
793
794 def __getattr__(self, attr):
795 """\
796 Create transparency to hyperdb.Class members
797 if they aren't defined in the ClassWrapper
798 class.
799 """
800
801 if self.__dict__.has_key(attr):
802 value = self.__dict__[attr]
803 else:
804 value = getattr(self._cl, attr)
805
806 return value
807
808 def __len__(self):
809 """\
810 Number of nodes in this class (excluding retired nodes).
811 """
812
813 return len(self._cl.list())
814
815 def __contains__(self, key):
816 """\
817 Determine if the class has a given node.
818 """
819
820 return self.has_key(key)
821
822 def __getitem__(self, key):
823 """\
824 Grant get access to all class nodes by indexing them as
825 dictionary items and wrapping them in a NodeWrapper.
826 """
827
828 return NodeWrapper(self, key, False)
829
830 def __setitem__(self, key, columns):
831 """\
832 Grant set access to all class nodes by indexing them as
833 dictionary items and wrapping them in a NodeWrapper.
834
835 "columns" must be a dictionary containing class properties
836 as the keys. The values in the dictionary are used as
837 values for the properties.
838 """
839
840 if isinstance(columns, types.DictType):
841 node = NodeWrapper(self, key, False)
842
843 for key in columns.keys():
844 setattr(node, key, columns[key])
845
846 else:
847 raise TypeError, \
"Item can only be assigned a dictionary"
848
849 def __delitem__(self, key):
850 """\
851 Retire a class node by using the python 'del' command and
852 indexing the node as dictionary item.
853 """
854
855 self._cl.retire( str(self.getid(key)) )
856
857 def __iadd__(self, columns):
858 """\
859 Add a new node to the class.
860
861 "columns" must be a dictionary containing class properties
862 as the keys. The values in the dictionary are used as
863 values for the properties.
864 """
865
866 if isinstance(columns, types.DictType):
867 self.create(**columns)
868 else:
869 raise TypeError, \
"Item can only be assigned a dictionary"
870
871 return self
872
873 def __iter__(self):
874 """\
875 Create iteration of all nodes as NodeWrapper's.
876 """
877
878 return iter(self.list())
879
880 def getid(self, key):
881 """\
882 Get the node id of the node that matches the key.
883
884 "key" can have three different types:
885 1 - an integer
886 The 'key' will be treated as the id of the node
887 and used for the indexing.
888 2 - a string
889 The 'key' will be treated as key argument of the
890 node and used to lookup the node id.
891 If the lookup action raises a KeyError and 'key'
892 only contains digits, the integer value of 'key'
893 will be used as the node id.
894 In both cases the node id is used for the indexing.
895 3 - an instance of NodeWrapper
896 The node id will be obtained from the NodeWrapper
897 class and used for the indexing.
898 """
899
900 if isinstance(key, types.IntType):
901 nodeid = key
902
903 elif isNode(key):
904 nodeid = key._id
905
906 else:
907 try:
908 nodeid = self._cl.lookup(key)
909
910 except (KeyError, TypeError):
911 if isinstance(key, types.StringType) \
and key.isdigit() \
and self._cl.hasnode(key):
912 nodeid = int(key)
913
914 else:
915 raise IndexError, \
"Class '%s' doesn't have a node that can be \
916 indexed with '%s'"%(self._cl.classname, key)
917
918 else:
919 nodeid = int(nodeid)
920
921 return nodeid
922
923 def auditors(self, enable):
924 """\
925 Enable/Disable auditors for this class.
926 """
927
928 p = self._dbw._classes[self._cl.classname]
929
930 if enable:
931 if p.has_key('auditors'):
932 self._cl.auditors = p['auditors']
933 del p['auditors']
934 else:
935 if not p.has_key('auditors'):
936 p['auditors'] = dict(self._cl.auditors)
937 self._cl.auditors = {
938 'create': [],
939 'set': [],
940 'retire': [],
941 'restore': []
942 }
943
944 def reactors(self, enable):
945 """\
946 Enable/Disable reactors for this class.
947 """
948
949 p = self._dbw._classes[self._cl.classname]
950
951 if enable:
952 if p.has_key('reactors'):
953 self._cl.reactors = p['reactors']
954 del p['reactors']
955 else:
956 if not p.has_key('reactors'):
957 p['reactors'] = dict(self._cl.reactors)
958 self._cl.reactors = {
959 'create': [],
960 'set': [],
961 'retire': [],
962 'restore': []
963 }
964
965 def setlabel(self, label=None):
966 """\
967 Set an alternative label to be used as label property.
968
969 If label is None, the result of hyperdb 'labelprop()' will be
970 used as label.
971 """
972
973 if not label is None:
974 props = self._cl.getprops()
975
976 if label in props.keys():
977 if isinstance(props[label], hyperdb.String):
978 self._dbw._classes[self._cl.classname]['label'] = label
979 else:
980 raise TypeError, \
"'label' should be a string type column"
981
982 else:
983 raise AttributeError, \
"Class '%s' doesn't have a property '%s'"% \
(self._cl.classname, label)
984
985 else:
986 del self._dbw._classes[self._cl.classname]['label']
987
988 def getlabel(self):
989 """\
990 Get the label to be used as label property.
991
992 If an alternative label is set, it will be returned.
993 """
994
995 p = self._dbw._classes[self._cl.classname]
996
997 return p.get('label', self._cl.labelprop())
998
999 def create(self, **propvalues):
1000 """\
1001 Create a new node of this class and return its id.
1002
1003 The keyword arguments in 'propvalues' map property names to values.
1004
1005 The values of arguments must be acceptable for the types of their
1006 corresponding properties or a TypeError is raised.
1007
1008 If this class has a key property, it must be present and its value
1009 must not collide with other key strings or a ValueError is raised.
1010
1011 Any other properties on this class that are missing from the
1012 'propvalues' dictionary are set to None.
1013
1014 If an id in a link or multilink property does not refer to a valid
1015 node, an IndexError is raised. Valid are node ids (as integer or
1016 as string), key values (for links to classes that have a key column)
1017 or a NodeWrapper object.
1018 """
1019
1020 props = self._cl.getprops()
1021
1022 creator = {}
1023 for column, value in propvalues.items():
1024 if props.has_key(column):
1025 if isinstance(props[column], hyperdb.Link):
1026 cl = self._db.getclass(props[column].classname)
1027 wrapper = ClassWrapper(self._dbw, cl)
1028 creator[column] = str(wrapper.getid(value))
1029
1030 elif isinstance(props[column], hyperdb.Multilink):
1031 cl = self._db.getclass(props[column].classname)
1032 wrapper = ClassWrapper(self._dbw, cl)
1033
1034 if not isinstance(value, types.ListType) \
and not isinstance(value, types.TupleType):
1035 value = [value]
1036
1037 creator[column] = [ str(wrapper.getid(key)) for key in value ]
1038
1039 elif isinstance(props[column], hyperdb.Interval) \
and not isinstance(value, date.Interval):
1040 creator[column] = date.Interval(value)
1041
1042 elif isinstance(props[column], hyperdb.Date) \
and not isinstance(value, date.Date):
1043 creator[column] = date.Date(value)
1044
1045 else:
1046 creator[column] = value
1047
1048 else:
1049 creator[column] = value
1050
1051 self.__lastid = self._cl.create(**creator)
1052
1053 return self.__lastid
1054
1055 def get(self, key, propname):
1056 """\
1057 Get the value of a property on an existing node of this class.
1058
1059 'propname' must be the name of a property of this class or a
1060 KeyError is raised.
1061 """
1062
1063 node = self.__getitem__(key)
1064
1065 return getattr(node, propname)
1066
1067 def set(self, key, **propvalues):
1068 """\
1069 Modify a property on an existing node of this class.
1070
1071 Each key in 'propvalues' must be the name of a property of this
1072 class or a KeyError is raised.
1073
1074 All values in 'propvalues' must be acceptable types for their
1075 corresponding properties or a TypeError is raised.
1076
1077 If the value of the key property is set, it must not collide with
1078 other key strings or a ValueError is raised.
1079
1080 If the value of a Link or Multilink property contains an invalid
1081 node id, a ValueError is raised.
1082 """
1083
1084 self.__setitem__(key, propvalues)
1085
1086 def list(self):
1087 """\
1088 Return a list of NodeWrappers of the active nodes in this class.
1089 """
1090
1091 id_list = [ int(nodeid) for nodeid in self._cl.list() ]
1092 id_list.sort()
1093
1094 return ListWrapper(self,
1095 [ NodeWrapper(self, int(nodeid), False) for nodeid in id_list ] )
1096
1097 def filter(self, search_matches, filterspec, sort=(None,None),
1098 group=(None,None), nodelist=None):
1099 """\
1100 Return a list of the NodeWrappers of the active nodes in this class
1101 that match the 'filter' spec, sorted by the group spec and then the
1102 sort spec.
1103
1104 "filterspec" is {propname: value(s)}
1105 'value(s)' may be any node id as integer or string or a key value
1106 (for classes that have a key column).
1107
1108 "sort" and "group" are (dir, prop) where dir is '+', '-' or None
1109 and prop is a prop name or None
1110
1111 "search_matches" is {nodeid: marker}
1112
1113 "nodelist" can be used to limit the search within the list
1114 of nodes. 'nodelist' may be a list of ids or any list created
1115 by any of the search methods in this module.
1116
1117 The filter must match all properties specified - but if the
1118 property value to match is a list, any one of the values in the
1119 list may match for that property to match.
1120 """
1121
1122 props = self._cl.getprops()
1123
1124 for column in filterspec.keys():
1125 if isinstance(props[column], hyperdb.Link):
1126 cl = self._db.getclass(props[column].classname)
1127 parent = ClassWrapper(self._dbw, cl)
1128 node = NodeWrapper(parent, filterspec[column])
1129 filterspec[column] = str(node._id)
1130
1131 elif isinstance(props[column], hyperdb.Multilink):
1132 cl = self._db.getclass(props[column].classname)
1133 parent = ClassWrapper(self._dbw, cl)
1134 list = ListWrapper(parent, filterspec[column])
1135 filterspec[column] = list.getnodeids()
1136
1137 if not nodelist is None:
1138 filterspec['id'] = ListWrapper(self, nodelist).getnodeids()
1139
1140 return ListWrapper(self,
1141 [ NodeWrapper(self, int(nodeid), False) for nodeid in self._cl.filter(search_matches, filterspec, sort, group) ])
1142
1143 def find(self, **propspec):
1144 """\
1145 Get the NodeWrappers of items in this class which link to the given
1146 items.
1147
1148 'propspec' consists of keyword args propname=itemid or
1149 propname=key
1150 'propname' must be the name of a property in this class, or a
1151 KeyError is raised. That property must be a Link or
1152 Multilink property, or a TypeError is raised.
1153
1154 "propspec" can also be passed a list with results from anu of the
1155 search methods in this wrapper (also regular expression search
1156 results can be passed 1-to-1).
1157
1158 Any item in this class whose 'propname' property links to any of the
1159 itemids will be returned. Used by the full text indexing, which knows
1160 that "foo" occurs in msg1, msg3 and file7, so we have hits on these
1161 issues:
1162 db.issue.find(messages=[1,3], files=[7])
1163 """
1164
1165 for column, values in propspec.items():
1166 if isinstance(values, ListWrapper):
1167 values.simple()
1168
1169 elif not isinstance(values, types.ListType) \
and not isinstance(values, types.TupleType):
1170 values = [values]
1171
1172 links = {}
1173 for key in values:
1174 links[str(self.getid(key))] = 1
1175
1176 propspec[column] = dict(links)
1177
1178 id_list = [ int(nodeid) for nodeid in self._cl.find(**propspec) ]
1179 id_list.sort()
1180
1181 return ListWrapper(self,
1182 [ NodeWrapper(self, nodeid, False) for nodeid in id_list ])
1183
1184 def newid(self):
1185 """\
1186 Generate a new id for this class.
1187 """
1188
1189 return self._db.newid(self._cl.classname)
1190
1191 def newnodeid(self):
1192 """\
1193 Return the id of the last created node.
1194 """
1195
1196 return self.__lastid
1197
1198 def getnodeids(self, retired=None):
1199 """\
1200 Retrieve all NodeWrappers of the nodes for a particular Class.
1201
1202 if 'retired' is True, all retired nodes will be returned.
1203 """
1204
1205 id_list = [ int(nodeid) for nodeid in self._cl.getnodeids(retired) ]
1206 id_list.sort()
1207
1208 return id_list
1209
1210 def hasnode(self, key):
1211 """\
1212 Determine if the class has a given node (also true on retired nodes).
1213 """
1214
1215 try:
1216 nodeid = self.getid(key)
1217
1218 except:
1219 label = self._cl.getkey()
1220
1221 if label \
and isinstance(key, types.StringType):
1222 value = None
1223
1224 for nodeid in self._cl.getnodeids(True):
1225 value = self._cl.get(nodeid, label)
1226
1227 if value == key:
1228 break
1229
1230 if not value is None \
and value == key:
1231 result = True
1232 else:
1233 result = False
1234
1235 else:
1236 result = False
1237
1238 else:
1239 result = self._cl.hasnode( str(nodeid) )
1240
1241 return result
1242
1243 def lookup(self, keyvalue):
1244 """\
1245 Locate a particular node by its key property and return it in a
1246 NodeClass object.
1247
1248 If this class has no key property, a TypeError is raised. If the
1249 'keyvalue' matches one of the values for the key property among
1250 the nodes in this class, the matching node's id is returned;
1251 otherwise a KeyError is raised.
1252 """
1253
1254 return NodeWrapper(self, int(self._cl.lookup(keyvalue)), False)
1255
1256 def search(self, value, column=None, nodelist=None):
1257 """\
1258 Lookup all nodes that have a 100% match with 'value' and
1259 return them in a list.
1260
1261 "value" is the pattern to look for.
1262
1263 "column" can be set to any string property. A TypeError is
1264 raised if 'column' isn't a string property. The label column
1265 will be used if 'column' is omitted.
1266
1267 "nodelist" can be used to limit the search within the list
1268 of nodes. 'nodelist' may be a list of ids or any list created
1269 by any of the search methods in this module.
1270 """
1271
1272 if not isinstance(value, types.StringType):
1273 raise TypeError, \
"'value' should be a string"
1274
1275 if not column:
1276 column = self.getlabel()
1277
1278 props = self._cl.getprops()
1279 if not isinstance(props[column], hyperdb.String):
1280 raise TypeError, \
"'column' should be a string type column"
1281
1282 if nodelist is None:
1283 list = self._cl.filter(None, {column:value})
1284 else:
1285 list = ListWrapper(self, nodelist).getnodeids()
1286
1287 list.sort()
1288
1289 value = value.lower()
1290 result = []
1291 for nodeid in list:
1292 if self._cl.get(nodeid, column).lower() == value:
1293 result.append( NodeWrapper(self, int(nodeid), False) )
1294
1295 return ListWrapper(self, result)
1296
1297 def is_retired(self, key):
1298 """\
1299 Return true if the node is retired.
1300 """
1301
1302 return self._cl.is_retired( str(self.getid(key)) )
1303
1304 def safeget(self, key, propname, default=None):
1305 """\
1306 Safely get the value of a property on an existing node of this class.
1307
1308 Return 'default' if the node doesn't exist.
1309 """
1310
1311 value = getattr(NodeWrapper(self, key), propname, default)
1312
1313 if value is None \
or (isinstance(value, types.ListType) and not value):
1314 value = default
1315
1316 return value
1317
1318 def has_key(self, key):
1319 """\
1320 Determine if the class has a given node, but then in
1321 a dictionary known method (false on retired nodes).
1322 """
1323
1324 try:
1325 nodeid = self.getid(key)
1326
1327 except (KeyError, TypeError, IndexError):
1328 result = False
1329
1330 else:
1331 result = str(nodeid) in self._cl.list()
1332
1333 return result
1334
1335 def keys(self):
1336 """\
1337 Get a list with all key values of this class. If no key is defined
1338 for this class, a list of ids will be returned.
1339 """
1340
1341 key = self._cl.getkey()
1342
1343 if key:
1344 keys = [ self._cl.get(nodeid, key) for nodeid in self._cl.list() ]
1345 else:
1346 keys = [ int(nodeid) for nodeid in self._cl.list() ]
1347
1348 return keys
1349
1350 def items(self):
1351 """\
1352 Get a list with tuples containing all keys with there NodeWrapper kept
1353 as pairs.
1354 """
1355
1356 return [ (key, NodeWrapper(self, key, False)) for key in self.keys() ]
1357
1358 def pop(self, key):
1359 """\
1360 Get node 'key' and remove (retire) it from this class.
1361 """
1362
1363 value = self.__getitem__(key)
1364 self.__delitem__(key)
1365
1366 return value
1367
1368 def resurrect(self, key):
1369 """\
1370 Bring any deleted (retired) node back alive.
1371 """
1372
1373 retireds = self._cl.getnodeids(retired=1)
1374
1375 if isinstance(key, types.IntType):
1376 nodeid = str(key)
1377 else:
1378 prop = self._cl.getkey()
1379 nodeid = None
1380 for retired_id in retireds:
1381 value = self._cl.get(retired_id, prop)
1382
1383 if value == key:
1384 nodeid = retired_id
1385 break;
1386
1387 if nodeid and nodeid in retireds:
1388 self._cl.restore(nodeid)
1389 else:
1390 raise KeyError, \
"No retired node '%s' in class '%s'"%(key, self._cl.classname)
1391
1392
1393
1394 class NodeWrapper:
1395 """\
1396 A wrapper around one specific node in a
1397 RoundUp hyperdb.Class.
1398
1399 Introduction
1400 ============
1401 In most cases you don't need to initialize a
1402 NodeWrapper because the ClassWrapper will do that
1403 for you where needed. But sometimes it can be
1404 more convenient to initialize one yourself.
1405
1406 Initialization
1407 ==============
1408 There are three methods to initialize a NodeWrapper:
1409 1. nodew = dbwrapper.NodeWrapper(clw, <nodeid>)
1410 A wrapper will be created for node <nodeid>.
1411 <nodeid> may be either an integer or a string.
1412
1413 2. nodew = dbwrapper.NodeWrapper(clw, <key>)
1414 For classes that have a key column, a key value can be
1415 used to create a wrapper for the node indexed by <key>.
1416
1417 3. nodew = dbwrapper.NodeWrapper(clw, <NodeWrapper>)
1418 A wrapper will be created for the NodeWrapper object.
1419 This won't make much sense because the wrapper was already there.
1420
1421 4. nodew = dbwrapper.NodeWrapper(clw, None)
1422 This is a phantom object and can be used to test against None.
1423 This might be convenient in auditors where there isn't a node
1424 if they are fired by the create event.
1425
1426 Usage
1427 =====
1428 All node properties can be accessed as if they are members of this
1429 wrapper object.
1430 The syntax is:
1431 <NodeWrapper>.<property>
1432 The returned type depends on the requested property. In most cases
1433 a simple type is returned (like string and integer), but in case of
1434 a linked property, a NodeWrapper object will be returned for the
1435 node to which the property is linked. In case of multi linked
1436 properties, a ListWrapper object will be returned containing
1437 NodeWrapper objects for all nodes to which the property is linked.
1438
1439 Examples:
1440 nodew.title
1441 nodew.name
1442 nodew.id (read only)
1443 nodew.nodeid (read only) (same as str(nodew.id))
1444
1445 It is also possible to set a property by accessing them as wrapper
1446 members.
1447 The syntax is:
1448 <NodeWrapper>.<property> = <value>
1449 The assigned value depends on the property type. In case of the
1450 simple types (String, Number, Boolean) value will be of the
1451 corresponding Python type (string, integer, boolean).
1452 In case of a linked type, the value can have more than one type:
1453 1. node id as integer
1454 2. node id as string
1455 3. key value (for classes that have a key column)
1456 4. NodeWrapper instance
1457 In case of multi linked type, value must be a list (either Python's
1458 list object or dbwrapper's ListWrapper object) containing node
1459 specifications like mentioned for link type properties (see above).
1460
1461 Examples:
1462 nodew.title = 'My new title'
1463 nodew.status = 'chatting'
1464 nodew.topic = ['Keyword 1', 'Keyword 2', 3, '4']
1465
1466 In case of multi linked properties, new links to other nodes can be
1467 created by simply adding them to the property. Even so can links be
1468 removed by subtracting them from the property.
1469
1470 Examples:
1471 Add:
1472 nodew.topic += 'Keyword 3'
1473 nodew.topic += ['Keyword 4', 'Keyword 5']
1474 Remove:
1475 nodew.topic -= 'Keyword 3'
1476 nodew.topic -= ['Keyword 4', 'Keyword 5']
1477 """
1478
1479 def __init__(self, parent, key, readonly=True):
1480 """\
1481 Initiate a wrapper around a hyperdb.Class for one
1482 specific node.
1483
1484 "parent" is the ClassWrapper that owns this NodeWrapper.
1485
1486 "key" identifies the node.
1487 It can have one of the next types:
1488 1 - an integer
1489 The 'key' will be treated as the id of the node
1490 and used for the indexing.
1491 2 - a string
1492 The 'key' will be treated as key argument of the
1493 node and used to lookup the node id.
1494 If the lookup action raises a KeyError and 'key'
1495 only contains digits, the integer value of 'key'
1496 will be used as the node id.
1497 In both cases the node id is used for the indexing.
1498 3 - an instance of NodeWrapper
1499 The node id will be obtained from the NodeWrapper
1500 class and used for the indexing.
1501 4 - None type
1502 This is a special option implemented to make it
1503 easy usable in detectors.
1504
1505 "readonly" can be set to False to allow set operations
1506 on any of the properties of this node.
1507 """
1508
1509 if not isClass(parent):
1510 raise TypeError, \
"Parent object isn't an instance of 'ClassWrapper'"
1511
1512 self._clw = parent
1513 self._db = parent._db
1514 self._cl = parent._cl
1515 self._readonly = readonly
1516
1517 if key is None:
1518 self._id = None
1519 else:
1520 self._id = parent.getid(key)
1521
1522 if not self._cl.hasnode(str(self._id)):
1523 raise IndexError, \
"Class '%s' doesn't have a node with id '%s'"% \
(self._cl.classname, self._id)
1524
1525 def __repr__(self):
1526 """\
1527 Return a resolved property.
1528 """
1529
1530 import re
1531
1532 if self._id is None:
1533 value = "<dbwrapper.NodeWrapper 'Phantom'>"
1534
1535 else:
1536 value = self.plain()
1537
1538 if isinstance(value, types.StringType):
1539 value = "'%s'"%re.sub("'", "\\'", value)
1540
1541 return str(value)
1542
1543 def __str__(self):
1544 """\
1545 Slightly more useful representation
1546 """
1547
1548 if self._id is None:
1549 value = "<dbwrapper.NodeWrapper 'Phantom'>"
1550
1551 else:
1552 value = str(self.plain())
1553
1554 return value
1555
1556 def __getattr__(self, attr):
1557 """\
1558 Grant easy get access to all properties of this node.
1559 """
1560
1561 # is it a NoneClass member?
1562 if self.__dict__.has_key(attr):
1563 value = self.__dict__[attr]
1564
1565 elif attr == 'id':
1566 value = self._id
1567
1568 elif attr == 'nodeid':
1569 if self._id is None:
1570 value = None
1571 else:
1572 value = str(self._id)
1573
1574 elif attr in ['properties', 'values']:
1575 if self._id is None:
1576 value = None
1577
1578 else:
1579 internals = ('id', 'actor', 'activity', 'creator', 'creation')
1580
1581 props = self._cl.getprops()
1582
1583 value = {}
1584 for prop in props.keys():
1585 if not prop in internals:
1586 data = self._cl.get(str(self._id), prop)
1587
1588 if not data is None \
and (not isinstance(data, types.ListType) or data):
1589 if attr == 'properties':
1590 if isinstance(props[prop], hyperdb.Link):
1591 if data:
1592 cl = self._db.getclass(props[prop].classname)
1593 key = cl.getkey()
1594
1595 if key:
1596 value[prop] = cl.get(data, key)
1597 else:
1598 value[prop] = int(data)
1599
1600 elif isinstance(props[prop], hyperdb.Multilink):
1601 cl = self._db.getclass(props[prop].classname)
1602 key = cl.getkey()
1603
1604 if key:
1605 value[prop] = [ cl.get(nodeid, key) for nodeid in data ]
1606 else:
1607 value[prop] = [ int(nodeid) for nodeid in data ]
1608
1609 elif not isinstance(props[prop], hyperdb.String):
1610 value[prop] = str(data)
1611
1612 else:
1613 value[prop] = data
1614
1615 else:
1616 if isinstance(props[prop], hyperdb.Link):
1617 if data:
1618 value[prop] = int(data)
1619
1620 elif isinstance(props[prop], hyperdb.Multilink):
1621 value[prop] = [ int(nodeid) for nodeid in data ]
1622
1623 else:
1624 value[prop] = data
1625
1626 else:
1627 if self._id is None:
1628 raise ValueError, \
"Phantom nodes do not have an attribute '%s'"%attr
1629
1630 else:
1631 props = self._cl.getprops()
1632
1633 # is it a class property?
1634 if props.has_key(attr):
1635 if isinstance(props[attr], hyperdb.Link):
1636 elementid = self._cl.get(str(self._id), attr)
1637
1638 if elementid:
1639 cl = self._db.getclass(props[attr].classname)
1640 parent = ClassWrapper(self._clw._dbw, cl)
1641 value = NodeWrapper(parent, elementid)
1642 else:
1643 value = None
1644
1645 elif isinstance(props[attr], hyperdb.Multilink):
1646 elements = self._cl.get(str(self._id), attr)
1647 cl = self._db.getclass(props[attr].classname)
1648 parent = ClassWrapper(self._clw._dbw, cl)
1649
1650 value = ListWrapper(parent, True, elements)
1651
1652 else:
1653 if attr == 'id':
1654 value = int(self._cl.get(str(self._id), attr))
1655 else:
1656 value = self._cl.get(str(self._id), attr)
1657
1658 # I don't know you
1659 else:
1660 raise AttributeError, \
"'%s' is not a member of object 'NodeWrapper'"%attr
1661
1662 return value
1663
1664 def __setattr__(self, attr, value):
1665 """\
1666 Grant easy set access to all properties of this node.
1667
1668 If "read only" than all set operations will raise exception
1669 "ValueError".
1670 """
1671
1672 # test if we are a member of NodeClass or if we will become
1673 # a member of NodeClass
1674 if attr in ['_clw', '_db', '_cl', '_id', '_readonly'] \
or self.__dict__.has_key(attr):
1675 self.__dict__[attr] = value
1676
1677 elif attr in ['properties', 'values']:
1678 raise ValueError, \
"'properties' can't be assigned any value"
1679
1680 else:
1681 if self._id is None:
1682 raise ValueError, \
"Phantom nodes do not have an attribute '%s'"%attr
1683
1684 else:
1685 props = self._cl.getprops()
1686
1687 # is it a class property?
1688 if props.has_key(attr):
1689 if not self._readonly:
1690 if isinstance(props[attr], hyperdb.Link):
1691 cl = self._db.getclass(props[attr].classname)
1692 parent = ClassWrapper(self._clw._dbw, cl)
1693
1694 value = str(parent.getid(value))
1695
1696 elif isinstance(props[attr], hyperdb.Multilink):
1697 cl = self._db.getclass(props[attr].classname)
1698 parent = ClassWrapper(self._clw._dbw, cl)
1699
1700 if isinstance(value, types.StringType):
1701 value = [value]
1702
1703 value = ListWrapper(parent, value)
1704
1705 value = [ str(nodeid) for nodeid in value.getnodeids() ]
1706
1707 elif isinstance(props[attr], hyperdb.Interval) \
and not isinstance(value, date.Interval):
1708 value = date.Interval(value)
1709
1710 elif isinstance(props[attr], hyperdb.Date) \
and not isinstance(value, date.Date):
1711 value = date.Date(value)
1712
1713 self._cl.set(str(self._id), **{attr:value})
1714
1715 else:
1716 raise ValueError, \
"This node is marked as readonly and can't \
1717 be assigned and value"
1718
1719 # not a member or class property
1720 else:
1721 raise AttributeError, \
"'%s' is not a member of object 'NodeWrapper'"%attr
1722
1723 def __iter__(self):
1724 """\
1725 Create iteration of all properties in this node. The iteration contains
1726 tuples with the property name on the first index and the value on the
1727 second index. For linked properties an instance to a NodeWrapper is
1728 used and not the value.
1729 """
1730
1731 return iter( [(prop, self.__getattr__(prop)) for prop in self._cl.getprops()] )
1732
1733 def __hash__(self):
1734 """\
1735 Node identifier.
1736 """
1737
1738 return self._id
1739
1740 def __eq__(self, other):
1741 """\
1742 Are these nodes equal?
1743 """
1744
1745 same_class = isinstance(other, NodeWrapper) \
and self._cl.classname == other._cl.classname
1746
1747 if not other is None:
1748 other = self._clw.getid(other)
1749
1750 return same_class and (other == self._id)
1751
1752 def __ne__(self, other):
1753 """\
1754 Are these nodes different?
1755 """
1756
1757 return not self.__eq__(other)
1758
1759 def __nonzero__(self):
1760 """\
1761 Truth value testing.
1762 """
1763
1764 return not self._id is None
1765
1766 def plain(self, property=None):
1767 """\
1768 Return the property in a nice way. Links are resolved.
1769 """
1770
1771 if not property is None:
1772 value = self.__getattr__(property)
1773
1774 if isinstance(value, types.ListType):
1775 list = []
1776 for one in value:
1777 label = one._clw.getlabel()
1778
1779 if isNode(one):
1780 list.append( getattr(one, label) )
1781 else:
1782 list.append(one)
1783 value = ', '.join( [str(item) for item in list] )
1784
1785 elif isinstance(value, types.NoneType):
1786 value = ''
1787
1788 elif isNode(value):
1789 label = value._clw.getlabel()
1790 value = getattr(value, label)
1791 else:
1792 label = self._clw.getlabel()
1793 value = self.__getattr__(label)
1794
1795 if value is None:
1796 value = ''
1797
1798 return value
1799
1800 def get(self, propname):
1801 """\
1802 Get the value of a property on an existing node of this class.
1803
1804 'propname' must be the name of a property of this class or a
1805 KeyError is raised.
1806 """
1807
1808 if self._id is None:
1809 raise NotImplementedError, \
"This method is not implemented for phantom nodes"
1810
1811 else:
1812 return self._clw.get(self._id, propname)
1813
1814 def set(self, **propvalues):
1815 """\
1816 Modify a property on an existing node of this class.
1817
1818 Each key in 'propvalues' must be the name of a property of this
1819 class or a KeyError is raised.
1820
1821 All values in 'propvalues' must be acceptable types for their
1822 corresponding properties or a TypeError is raised.
1823
1824 If the value of the key property is set, it must not collide with
1825 other key strings or a ValueError is raised.
1826
1827 If the value of a Link or Multilink property contains an invalid
1828 node id, a ValueError is raised.
1829 """
1830
1831 if self._id is None:
1832 raise NotImplementedError, \
"This method is not implemented for phantom nodes"
1833
1834 else:
1835 self._clw.set(self._id, propvalues)
1836
1837 def is_retired(self):
1838 """\
1839 Return true if the node is retired.
1840 """
1841
1842 if self._id is None:
1843 raise NotImplementedError, \
"This method is not implemented for phantom nodes"
1844
1845 else:
1846 return self._clw.is_retired(self._id)
1847
1848 def safeget(self, propname, default=None):
1849 """\
1850 Safely get the value of a property on an existing node of this class.
1851
1852 Return 'default' if the node doesn't exist.
1853 """
1854
1855 if self._id is None:
1856 raise NotImplementedError, \
"This method is not implemented for phantom nodes"
1857
1858 else:
1859 return self._clw.safeget(self._id, propname, default)
1860
1861 def pop(self):
1862 """\
1863 Get node 'key' and remove (retire) it from this class.
1864 """
1865
1866 if self._id is None:
1867 raise NotImplementedError, \
"This method is not implemented for phantom nodes"
1868
1869 else:
1870 return self._clw.pop(self._id)
1871
1872 def resurrect(self):
1873 """\
1874 Bring any deleted (retired) node back alive.
1875 """
1876
1877 if self._id is None:
1878 raise NotImplementedError, \
"This method is not implemented for phantom nodes"
1879
1880 else:
1881 self._clw.resurrect(self._id)
1882
1883
1884
1885 class ListWrapper(types.ListType):
1886 """\
1887 List object to give easier access to NodeWrapper's.
1888
1889 Introduction
1890 ============
1891 This wrapper almost fully supports all available operations for
1892 Python's list object. The difference is that it is meant to be used
1893 in ClassWrapper's and NodeWrapper's.
1894
1895 Initialization
1896 ==============
1897 The wrapper can be initialized as follows:
1898 1. lw = dbwrapper.ClassWrapper(<ClassWrapper>)
1899 This is the simplest form. The wrapper will be created to hold
1900 only NodeWrapper objects from class <ClassWrapper>.
1901 The list will be empty.
1902
1903 2. lw = dbwrapper.ClassWrapper(<ClassWrapper>, [<key>|<nodeid>|<NodeWrapper>])
1904 With this form you will fill the list with initial data.
1905 The data must be nodes in <ClassWrapper> and may be addressed
1906 by either key (in case the class has a key column), or by node id
1907 (as integer or string), or by a NodeWrapper object.
1908
1909 For both forms an additional parameter can be set to tell the dbwrapper
1910 that all NodeWrapper classes in the ListClass are either read-only or not.
1911 Default they are set to not read-only, meaning all nodes in the list
1912 may be changed.
1913
1914 Several methods in the ClassWrapper and the NodeWrapper do return
1915 ListWrapper objects. In general these methods are all the search
1916 methods in the ClassWrapper and the 'get' method in the NodeWrapper.
1917
1918 Default the list won't accept duplicate nodes. If you try to add a node
1919 which is already present, a ValueError will be raised. This behavior
1920 can be omitted by setting ListWrapper member 'unique' to False.
1921 If False, then duplicate nodes are accepted. Switching member
1922 'unique' back to True while there are duplicate nodes will raise
1923 a ValueError too.
1924 """
1925
1926 class RegExpClass:
1927 """\
1928 A class with regular expression methods that will filter nodes
1929 from hyperdb.Class. The methods in this class are one to one
1930 with the regular expression methods in python module 're'.
1931 """
1932
1933 def __init__(self, owner):
1934 """\
1935 Initialize class
1936
1937 "owner" must be of type ListWrapper
1938 """
1939
1940 if not isinstance(owner, ListWrapper):
1941 raise TypeError, \
"Owner object isn't an instance of 'ListWrapper'"
1942
1943 self._lw = owner
1944
1945 def __repr__(self):
1946 """\
1947 Slightly more useful representation
1948 """
1949
1950 return '''<dbwrapper.ListWrapper.RegExpClass '%s'>'''% \
self._lw._clw._cl
1951
1952 def __str__(self):
1953 """\
1954 Slightly more useful representation
1955 """
1956
1957 return self.__repr__()
1958
1959 def match(self, pattern, flags=0, column=None):
1960 """\
1961 Find all nodes in the class that have a result when
1962 applying the "pattern" at the start of the string in
1963 "column", returning a list object with tuples pairs.
1964
1965 The first index of each tuple contains a
1966 dbwraper.NodeWrapper object of a node that matches
1967 with "pattern".
1968
1969 The second index in the tuples holds the match object.
1970
1971 "flags" may have the same flags as used/defined in
1972 python module 're'. These flags are also available
1973 as members of module 'dbwrapper'.
1974 (e.g. dbwrapper.IGNORECASE)
1975
1976 "column" can be used to perform a search on an
1977 other column than the column returned by
1978 ClassWrapper.getlabel().
1979 """
1980
1981 return self._lw._clw.re.match(pattern, flags, column, nodelist=self._lw)
1982
1983 def search(self, pattern, flags=0, column=None):
1984 """\
1985 Find all nodes in the class that have a result when
1986 scanning through the string in "column" looking for a
1987 match to the "pattern", returning a list object with
1988 tuples pairs.
1989
1990 The first index of each tuple contains a
1991 dbwraper.NodeWrapper object of a node that matches
1992 with "pattern".
1993
1994 The second index in the tuples holds the search object.
1995
1996 "flags" may have the same flags as used/defined in
1997 python module 're'. These flags are also available
1998 as members of module 'dbwrapper'.
1999 (e.g. dbwrapper.IGNORECASE)
2000
2001 "column" can be used to perform a search on an
2002 other column than the column returned by
2003 ClassWrapper.getlabel().
2004 """
2005
2006 return self._lw._clw.re.search(pattern, flags, column, nodelist=self._lw)
2007
2008 def findall(self, pattern, flags=0, column=None):
2009 """\
2010 Return all nodes in class that have "pattern" matches
2011 in the strings in "column". Returned is a list of tuple
2012 pairs.
2013
2014 The first tuple index contains a NodeWrapper object of
2015 a matching node.
2016
2017 The second tuple index contains a list of all
2018 non-overlapping matches in the node's "column" string.
2019
2020 If one or more groups are present in the "pattern", the
2021 second tuple index will contain a list of groups; this
2022 will be a list of tuples if the "pattern" has more than
2023 one group.
2024
2025 "flags" may have the same flags as used/defined in
2026 python module 're'. These flags are also available
2027 as members of module 'dbwrapper'.
2028 (e.g. dbwrapper.IGNORECASE)
2029
2030 "column" can be used to perform a search on an
2031 other column than the column returned by
2032 ClassWrapper.getlabel().
2033 """
2034
2035 return self._lw._clw.re.findall(pattern, flags, column, nodelist=self._lw)
2036
2037
2038 def __init__(self, owner, readonly=False, *list):
2039 """\
2040 Create a list object for dbwrapper.
2041
2042 "owner" must be a 'ClassWrapper' object. Only nodes present
2043 in 'ClassWrapper' will be allowed in the list.
2044
2045 "readonly" can be set to prevent that properties of the nodes
2046 in the list can be changed. The list content can always be
2047 changed.
2048
2049 "list" can be a list of any node specification like key,
2050 nodeid or NodeClass object.
2051 """
2052
2053 if not isClass(owner):
2054 raise TypeError, \
"owner object isn't an instance of 'ClassWrapper'"
2055
2056 self.__list = []
2057 self._clw = owner
2058 self.__regexp = False
2059 self.re = self.RegExpClass(self)
2060 self.unique = True
2061
2062 if isinstance(readonly, types.ListType) \
or isinstance(readonly, types.TupleType):
2063 self._readonly = False
2064 list = readonly
2065 else:
2066 self._readonly = readonly
2067
2068 if list:
2069 if len(list) == 1 \
and isinstance(list[0], types.ListType):
2070 list = list[0]
2071
2072