Mercurial > p > roundup > code
comparison roundup/backends/back_anydbm.py @ 4075:b87d022a2f40
Uniformly use """...""" instead of '''...''' for comments.
| author | Stefan Seefeld <stefan@seefeld.name> |
|---|---|
| date | Tue, 24 Feb 2009 02:12:23 +0000 |
| parents | 27a9906cd8d1 |
| children | 94c992852f12 |
comparison
equal
deleted
inserted
replaced
| 4074:e039f3cbbb96 | 4075:b87d022a2f40 |
|---|---|
| 13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS | 13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS |
| 14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" | 14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" |
| 15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, | 15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, |
| 16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. | 16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. |
| 17 # | 17 # |
| 18 #$Id: back_anydbm.py,v 1.211 2008-08-07 05:53:14 richard Exp $ | 18 """This module defines a backend that saves the hyperdatabase in a |
| 19 '''This module defines a backend that saves the hyperdatabase in a | |
| 20 database chosen by anydbm. It is guaranteed to always be available in python | 19 database chosen by anydbm. It is guaranteed to always be available in python |
| 21 versions >2.1.1 (the dumbdbm fallback in 2.1.1 and earlier has several | 20 versions >2.1.1 (the dumbdbm fallback in 2.1.1 and earlier has several |
| 22 serious bugs, and is not available) | 21 serious bugs, and is not available) |
| 23 ''' | 22 """ |
| 24 __docformat__ = 'restructuredtext' | 23 __docformat__ = 'restructuredtext' |
| 25 | 24 |
| 26 try: | 25 try: |
| 27 import anydbm, sys | 26 import anydbm, sys |
| 28 # dumbdbm only works in python 2.1.2+ | 27 # dumbdbm only works in python 2.1.2+ |
| 60 | 59 |
| 61 # | 60 # |
| 62 # Now the database | 61 # Now the database |
| 63 # | 62 # |
| 64 class Database(FileStorage, hyperdb.Database, roundupdb.Database): | 63 class Database(FileStorage, hyperdb.Database, roundupdb.Database): |
| 65 '''A database for storing records containing flexible data types. | 64 """A database for storing records containing flexible data types. |
| 66 | 65 |
| 67 Transaction stuff TODO: | 66 Transaction stuff TODO: |
| 68 | 67 |
| 69 - check the timestamp of the class file and nuke the cache if it's | 68 - check the timestamp of the class file and nuke the cache if it's |
| 70 modified. Do some sort of conflict checking on the dirty stuff. | 69 modified. Do some sort of conflict checking on the dirty stuff. |
| 71 - perhaps detect write collisions (related to above)? | 70 - perhaps detect write collisions (related to above)? |
| 72 ''' | 71 """ |
| 73 def __init__(self, config, journaltag=None): | 72 def __init__(self, config, journaltag=None): |
| 74 '''Open a hyperdatabase given a specifier to some storage. | 73 """Open a hyperdatabase given a specifier to some storage. |
| 75 | 74 |
| 76 The 'storagelocator' is obtained from config.DATABASE. | 75 The 'storagelocator' is obtained from config.DATABASE. |
| 77 The meaning of 'storagelocator' depends on the particular | 76 The meaning of 'storagelocator' depends on the particular |
| 78 implementation of the hyperdatabase. It could be a file name, | 77 implementation of the hyperdatabase. It could be a file name, |
| 79 a directory path, a socket descriptor for a connection to a | 78 a directory path, a socket descriptor for a connection to a |
| 82 The 'journaltag' is a token that will be attached to the journal | 81 The 'journaltag' is a token that will be attached to the journal |
| 83 entries for any edits done on the database. If 'journaltag' is | 82 entries for any edits done on the database. If 'journaltag' is |
| 84 None, the database is opened in read-only mode: the Class.create(), | 83 None, the database is opened in read-only mode: the Class.create(), |
| 85 Class.set(), Class.retire(), and Class.restore() methods are | 84 Class.set(), Class.retire(), and Class.restore() methods are |
| 86 disabled. | 85 disabled. |
| 87 ''' | 86 """ |
| 88 FileStorage.__init__(self, config.UMASK) | 87 FileStorage.__init__(self, config.UMASK) |
| 89 self.config, self.journaltag = config, journaltag | 88 self.config, self.journaltag = config, journaltag |
| 90 self.dir = config.DATABASE | 89 self.dir = config.DATABASE |
| 91 self.classes = {} | 90 self.classes = {} |
| 92 self.cache = {} # cache of nodes loaded or created | 91 self.cache = {} # cache of nodes loaded or created |
| 105 self.lockfile = locking.acquire_lock(lockfilenm) | 104 self.lockfile = locking.acquire_lock(lockfilenm) |
| 106 self.lockfile.write(str(os.getpid())) | 105 self.lockfile.write(str(os.getpid())) |
| 107 self.lockfile.flush() | 106 self.lockfile.flush() |
| 108 | 107 |
| 109 def post_init(self): | 108 def post_init(self): |
| 110 '''Called once the schema initialisation has finished. | 109 """Called once the schema initialisation has finished. |
| 111 ''' | 110 """ |
| 112 # reindex the db if necessary | 111 # reindex the db if necessary |
| 113 if self.indexer.should_reindex(): | 112 if self.indexer.should_reindex(): |
| 114 self.reindex() | 113 self.reindex() |
| 115 | 114 |
| 116 def refresh_database(self): | 115 def refresh_database(self): |
| 144 | 143 |
| 145 # | 144 # |
| 146 # Classes | 145 # Classes |
| 147 # | 146 # |
| 148 def __getattr__(self, classname): | 147 def __getattr__(self, classname): |
| 149 '''A convenient way of calling self.getclass(classname).''' | 148 """A convenient way of calling self.getclass(classname).""" |
| 150 if self.classes.has_key(classname): | 149 if self.classes.has_key(classname): |
| 151 return self.classes[classname] | 150 return self.classes[classname] |
| 152 raise AttributeError, classname | 151 raise AttributeError, classname |
| 153 | 152 |
| 154 def addclass(self, cl): | 153 def addclass(self, cl): |
| 164 description="User is allowed to edit "+cn) | 163 description="User is allowed to edit "+cn) |
| 165 self.security.addPermission(name="View", klass=cn, | 164 self.security.addPermission(name="View", klass=cn, |
| 166 description="User is allowed to access "+cn) | 165 description="User is allowed to access "+cn) |
| 167 | 166 |
| 168 def getclasses(self): | 167 def getclasses(self): |
| 169 '''Return a list of the names of all existing classes.''' | 168 """Return a list of the names of all existing classes.""" |
| 170 l = self.classes.keys() | 169 l = self.classes.keys() |
| 171 l.sort() | 170 l.sort() |
| 172 return l | 171 return l |
| 173 | 172 |
| 174 def getclass(self, classname): | 173 def getclass(self, classname): |
| 175 '''Get the Class object representing a particular class. | 174 """Get the Class object representing a particular class. |
| 176 | 175 |
| 177 If 'classname' is not a valid class name, a KeyError is raised. | 176 If 'classname' is not a valid class name, a KeyError is raised. |
| 178 ''' | 177 """ |
| 179 try: | 178 try: |
| 180 return self.classes[classname] | 179 return self.classes[classname] |
| 181 except KeyError: | 180 except KeyError: |
| 182 raise KeyError, 'There is no class called "%s"'%classname | 181 raise KeyError, 'There is no class called "%s"'%classname |
| 183 | 182 |
| 184 # | 183 # |
| 185 # Class DBs | 184 # Class DBs |
| 186 # | 185 # |
| 187 def clear(self): | 186 def clear(self): |
| 188 '''Delete all database contents | 187 """Delete all database contents |
| 189 ''' | 188 """ |
| 190 logging.getLogger('hyperdb').info('clear') | 189 logging.getLogger('hyperdb').info('clear') |
| 191 for cn in self.classes.keys(): | 190 for cn in self.classes.keys(): |
| 192 for dummy in 'nodes', 'journals': | 191 for dummy in 'nodes', 'journals': |
| 193 path = os.path.join(self.dir, 'journals.%s'%cn) | 192 path = os.path.join(self.dir, 'journals.%s'%cn) |
| 194 if os.path.exists(path): | 193 if os.path.exists(path): |
| 201 os.remove(path) | 200 os.remove(path) |
| 202 elif os.path.exists(path+'.db'): # dbm appends .db | 201 elif os.path.exists(path+'.db'): # dbm appends .db |
| 203 os.remove(path+'.db') | 202 os.remove(path+'.db') |
| 204 | 203 |
| 205 def getclassdb(self, classname, mode='r'): | 204 def getclassdb(self, classname, mode='r'): |
| 206 ''' grab a connection to the class db that will be used for | 205 """ grab a connection to the class db that will be used for |
| 207 multiple actions | 206 multiple actions |
| 208 ''' | 207 """ |
| 209 return self.opendb('nodes.%s'%classname, mode) | 208 return self.opendb('nodes.%s'%classname, mode) |
| 210 | 209 |
| 211 def determine_db_type(self, path): | 210 def determine_db_type(self, path): |
| 212 ''' determine which DB wrote the class file | 211 """ determine which DB wrote the class file |
| 213 ''' | 212 """ |
| 214 db_type = '' | 213 db_type = '' |
| 215 if os.path.exists(path): | 214 if os.path.exists(path): |
| 216 db_type = whichdb.whichdb(path) | 215 db_type = whichdb.whichdb(path) |
| 217 if not db_type: | 216 if not db_type: |
| 218 raise hyperdb.DatabaseError, \ | 217 raise hyperdb.DatabaseError, \ |
| 222 # anydbm says it's dbhash or not! | 221 # anydbm says it's dbhash or not! |
| 223 db_type = 'dbm' | 222 db_type = 'dbm' |
| 224 return db_type | 223 return db_type |
| 225 | 224 |
| 226 def opendb(self, name, mode): | 225 def opendb(self, name, mode): |
| 227 '''Low-level database opener that gets around anydbm/dbm | 226 """Low-level database opener that gets around anydbm/dbm |
| 228 eccentricities. | 227 eccentricities. |
| 229 ''' | 228 """ |
| 230 # figure the class db type | 229 # figure the class db type |
| 231 path = os.path.join(os.getcwd(), self.dir, name) | 230 path = os.path.join(os.getcwd(), self.dir, name) |
| 232 db_type = self.determine_db_type(path) | 231 db_type = self.determine_db_type(path) |
| 233 | 232 |
| 234 # new database? let anydbm pick the best dbm | 233 # new database? let anydbm pick the best dbm |
| 251 | 250 |
| 252 # | 251 # |
| 253 # Node IDs | 252 # Node IDs |
| 254 # | 253 # |
| 255 def newid(self, classname): | 254 def newid(self, classname): |
| 256 ''' Generate a new id for the given class | 255 """ Generate a new id for the given class |
| 257 ''' | 256 """ |
| 258 # open the ids DB - create if if doesn't exist | 257 # open the ids DB - create if if doesn't exist |
| 259 db = self.opendb('_ids', 'c') | 258 db = self.opendb('_ids', 'c') |
| 260 if db.has_key(classname): | 259 if db.has_key(classname): |
| 261 newid = db[classname] = str(int(db[classname]) + 1) | 260 newid = db[classname] = str(int(db[classname]) + 1) |
| 262 else: | 261 else: |
| 265 db[classname] = newid | 264 db[classname] = newid |
| 266 db.close() | 265 db.close() |
| 267 return newid | 266 return newid |
| 268 | 267 |
| 269 def setid(self, classname, setid): | 268 def setid(self, classname, setid): |
| 270 ''' Set the id counter: used during import of database | 269 """ Set the id counter: used during import of database |
| 271 ''' | 270 """ |
| 272 # open the ids DB - create if if doesn't exist | 271 # open the ids DB - create if if doesn't exist |
| 273 db = self.opendb('_ids', 'c') | 272 db = self.opendb('_ids', 'c') |
| 274 db[classname] = str(setid) | 273 db[classname] = str(setid) |
| 275 db.close() | 274 db.close() |
| 276 | 275 |
| 277 # | 276 # |
| 278 # Nodes | 277 # Nodes |
| 279 # | 278 # |
| 280 def addnode(self, classname, nodeid, node): | 279 def addnode(self, classname, nodeid, node): |
| 281 ''' add the specified node to its class's db | 280 """ add the specified node to its class's db |
| 282 ''' | 281 """ |
| 283 # we'll be supplied these props if we're doing an import | 282 # we'll be supplied these props if we're doing an import |
| 284 if not node.has_key('creator'): | 283 if not node.has_key('creator'): |
| 285 # add in the "calculated" properties (dupe so we don't affect | 284 # add in the "calculated" properties (dupe so we don't affect |
| 286 # calling code's node assumptions) | 285 # calling code's node assumptions) |
| 287 node = node.copy() | 286 node = node.copy() |
| 292 self.newnodes.setdefault(classname, {})[nodeid] = 1 | 291 self.newnodes.setdefault(classname, {})[nodeid] = 1 |
| 293 self.cache.setdefault(classname, {})[nodeid] = node | 292 self.cache.setdefault(classname, {})[nodeid] = node |
| 294 self.savenode(classname, nodeid, node) | 293 self.savenode(classname, nodeid, node) |
| 295 | 294 |
| 296 def setnode(self, classname, nodeid, node): | 295 def setnode(self, classname, nodeid, node): |
| 297 ''' change the specified node | 296 """ change the specified node |
| 298 ''' | 297 """ |
| 299 self.dirtynodes.setdefault(classname, {})[nodeid] = 1 | 298 self.dirtynodes.setdefault(classname, {})[nodeid] = 1 |
| 300 | 299 |
| 301 # can't set without having already loaded the node | 300 # can't set without having already loaded the node |
| 302 self.cache[classname][nodeid] = node | 301 self.cache[classname][nodeid] = node |
| 303 self.savenode(classname, nodeid, node) | 302 self.savenode(classname, nodeid, node) |
| 304 | 303 |
| 305 def savenode(self, classname, nodeid, node): | 304 def savenode(self, classname, nodeid, node): |
| 306 ''' perform the saving of data specified by the set/addnode | 305 """ perform the saving of data specified by the set/addnode |
| 307 ''' | 306 """ |
| 308 if __debug__: | 307 if __debug__: |
| 309 logging.getLogger('hyperdb').debug('save %s%s %r'%(classname, nodeid, node)) | 308 logging.getLogger('hyperdb').debug('save %s%s %r'%(classname, nodeid, node)) |
| 310 self.transactions.append((self.doSaveNode, (classname, nodeid, node))) | 309 self.transactions.append((self.doSaveNode, (classname, nodeid, node))) |
| 311 | 310 |
| 312 def getnode(self, classname, nodeid, db=None, cache=1): | 311 def getnode(self, classname, nodeid, db=None, cache=1): |
| 313 ''' get a node from the database | 312 """ get a node from the database |
| 314 | 313 |
| 315 Note the "cache" parameter is not used, and exists purely for | 314 Note the "cache" parameter is not used, and exists purely for |
| 316 backward compatibility! | 315 backward compatibility! |
| 317 ''' | 316 """ |
| 318 # try the cache | 317 # try the cache |
| 319 cache_dict = self.cache.setdefault(classname, {}) | 318 cache_dict = self.cache.setdefault(classname, {}) |
| 320 if cache_dict.has_key(nodeid): | 319 if cache_dict.has_key(nodeid): |
| 321 if __debug__: | 320 if __debug__: |
| 322 logging.getLogger('hyperdb').debug('get %s%s cached'%(classname, nodeid)) | 321 logging.getLogger('hyperdb').debug('get %s%s cached'%(classname, nodeid)) |
| 353 self.stats['get_items'] += (time.time() - start_t) | 352 self.stats['get_items'] += (time.time() - start_t) |
| 354 | 353 |
| 355 return res | 354 return res |
| 356 | 355 |
| 357 def destroynode(self, classname, nodeid): | 356 def destroynode(self, classname, nodeid): |
| 358 '''Remove a node from the database. Called exclusively by the | 357 """Remove a node from the database. Called exclusively by the |
| 359 destroy() method on Class. | 358 destroy() method on Class. |
| 360 ''' | 359 """ |
| 361 logging.getLogger('hyperdb').info('destroy %s%s'%(classname, nodeid)) | 360 logging.getLogger('hyperdb').info('destroy %s%s'%(classname, nodeid)) |
| 362 | 361 |
| 363 # remove from cache and newnodes if it's there | 362 # remove from cache and newnodes if it's there |
| 364 if (self.cache.has_key(classname) and | 363 if (self.cache.has_key(classname) and |
| 365 self.cache[classname].has_key(nodeid)): | 364 self.cache[classname].has_key(nodeid)): |
| 379 # add the destroy commit action | 378 # add the destroy commit action |
| 380 self.transactions.append((self.doDestroyNode, (classname, nodeid))) | 379 self.transactions.append((self.doDestroyNode, (classname, nodeid))) |
| 381 self.transactions.append((FileStorage.destroy, (self, classname, nodeid))) | 380 self.transactions.append((FileStorage.destroy, (self, classname, nodeid))) |
| 382 | 381 |
| 383 def serialise(self, classname, node): | 382 def serialise(self, classname, node): |
| 384 '''Copy the node contents, converting non-marshallable data into | 383 """Copy the node contents, converting non-marshallable data into |
| 385 marshallable data. | 384 marshallable data. |
| 386 ''' | 385 """ |
| 387 properties = self.getclass(classname).getprops() | 386 properties = self.getclass(classname).getprops() |
| 388 d = {} | 387 d = {} |
| 389 for k, v in node.items(): | 388 for k, v in node.items(): |
| 390 if k == self.RETIRED_FLAG: | 389 if k == self.RETIRED_FLAG: |
| 391 d[k] = v | 390 d[k] = v |
| 407 else: | 406 else: |
| 408 d[k] = v | 407 d[k] = v |
| 409 return d | 408 return d |
| 410 | 409 |
| 411 def unserialise(self, classname, node): | 410 def unserialise(self, classname, node): |
| 412 '''Decode the marshalled node data | 411 """Decode the marshalled node data |
| 413 ''' | 412 """ |
| 414 properties = self.getclass(classname).getprops() | 413 properties = self.getclass(classname).getprops() |
| 415 d = {} | 414 d = {} |
| 416 for k, v in node.items(): | 415 for k, v in node.items(): |
| 417 # if the property doesn't exist, or is the "retired" flag then | 416 # if the property doesn't exist, or is the "retired" flag then |
| 418 # it won't be in the properties dict | 417 # it won't be in the properties dict |
| 434 else: | 433 else: |
| 435 d[k] = v | 434 d[k] = v |
| 436 return d | 435 return d |
| 437 | 436 |
| 438 def hasnode(self, classname, nodeid, db=None): | 437 def hasnode(self, classname, nodeid, db=None): |
| 439 ''' determine if the database has a given node | 438 """ determine if the database has a given node |
| 440 ''' | 439 """ |
| 441 # try the cache | 440 # try the cache |
| 442 cache = self.cache.setdefault(classname, {}) | 441 cache = self.cache.setdefault(classname, {}) |
| 443 if cache.has_key(nodeid): | 442 if cache.has_key(nodeid): |
| 444 return 1 | 443 return 1 |
| 445 | 444 |
| 472 # | 471 # |
| 473 # Journal | 472 # Journal |
| 474 # | 473 # |
| 475 def addjournal(self, classname, nodeid, action, params, creator=None, | 474 def addjournal(self, classname, nodeid, action, params, creator=None, |
| 476 creation=None): | 475 creation=None): |
| 477 ''' Journal the Action | 476 """ Journal the Action |
| 478 'action' may be: | 477 'action' may be: |
| 479 | 478 |
| 480 'create' or 'set' -- 'params' is a dictionary of property values | 479 'create' or 'set' -- 'params' is a dictionary of property values |
| 481 'link' or 'unlink' -- 'params' is (classname, nodeid, propname) | 480 'link' or 'unlink' -- 'params' is (classname, nodeid, propname) |
| 482 'retire' -- 'params' is None | 481 'retire' -- 'params' is None |
| 483 | 482 |
| 484 'creator' -- the user performing the action, which defaults to | 483 'creator' -- the user performing the action, which defaults to |
| 485 the current user. | 484 the current user. |
| 486 ''' | 485 """ |
| 487 if __debug__: | 486 if __debug__: |
| 488 logging.getLogger('hyperdb').debug('addjournal %s%s %s %r %s %r'%(classname, | 487 logging.getLogger('hyperdb').debug('addjournal %s%s %s %r %s %r'%(classname, |
| 489 nodeid, action, params, creator, creation)) | 488 nodeid, action, params, creator, creation)) |
| 490 if creator is None: | 489 if creator is None: |
| 491 creator = self.getuid() | 490 creator = self.getuid() |
| 492 self.transactions.append((self.doSaveJournal, (classname, nodeid, | 491 self.transactions.append((self.doSaveJournal, (classname, nodeid, |
| 493 action, params, creator, creation))) | 492 action, params, creator, creation))) |
| 494 | 493 |
| 495 def setjournal(self, classname, nodeid, journal): | 494 def setjournal(self, classname, nodeid, journal): |
| 496 '''Set the journal to the "journal" list.''' | 495 """Set the journal to the "journal" list.""" |
| 497 if __debug__: | 496 if __debug__: |
| 498 logging.getLogger('hyperdb').debug('setjournal %s%s %r'%(classname, | 497 logging.getLogger('hyperdb').debug('setjournal %s%s %r'%(classname, |
| 499 nodeid, journal)) | 498 nodeid, journal)) |
| 500 self.transactions.append((self.doSetJournal, (classname, nodeid, | 499 self.transactions.append((self.doSetJournal, (classname, nodeid, |
| 501 journal))) | 500 journal))) |
| 502 | 501 |
| 503 def getjournal(self, classname, nodeid): | 502 def getjournal(self, classname, nodeid): |
| 504 ''' get the journal for id | 503 """ get the journal for id |
| 505 | 504 |
| 506 Raise IndexError if the node doesn't exist (as per history()'s | 505 Raise IndexError if the node doesn't exist (as per history()'s |
| 507 API) | 506 API) |
| 508 ''' | 507 """ |
| 509 # our journal result | 508 # our journal result |
| 510 res = [] | 509 res = [] |
| 511 | 510 |
| 512 # add any journal entries for transactions not committed to the | 511 # add any journal entries for transactions not committed to the |
| 513 # database | 512 # database |
| 552 for nodeid, date_stamp, user, action, params in journal: | 551 for nodeid, date_stamp, user, action, params in journal: |
| 553 res.append((nodeid, date.Date(date_stamp), user, action, params)) | 552 res.append((nodeid, date.Date(date_stamp), user, action, params)) |
| 554 return res | 553 return res |
| 555 | 554 |
| 556 def pack(self, pack_before): | 555 def pack(self, pack_before): |
| 557 ''' Delete all journal entries except "create" before 'pack_before'. | 556 """ Delete all journal entries except "create" before 'pack_before'. |
| 558 ''' | 557 """ |
| 559 pack_before = pack_before.serialise() | 558 pack_before = pack_before.serialise() |
| 560 for classname in self.getclasses(): | 559 for classname in self.getclasses(): |
| 561 packed = 0 | 560 packed = 0 |
| 562 # get the journal db | 561 # get the journal db |
| 563 db_name = 'journals.%s'%classname | 562 db_name = 'journals.%s'%classname |
| 592 | 591 |
| 593 # | 592 # |
| 594 # Basic transaction support | 593 # Basic transaction support |
| 595 # | 594 # |
| 596 def commit(self, fail_ok=False): | 595 def commit(self, fail_ok=False): |
| 597 ''' Commit the current transactions. | 596 """ Commit the current transactions. |
| 598 | 597 |
| 599 Save all data changed since the database was opened or since the | 598 Save all data changed since the database was opened or since the |
| 600 last commit() or rollback(). | 599 last commit() or rollback(). |
| 601 | 600 |
| 602 fail_ok indicates that the commit is allowed to fail. This is used | 601 fail_ok indicates that the commit is allowed to fail. This is used |
| 603 in the web interface when committing cleaning of the session | 602 in the web interface when committing cleaning of the session |
| 604 database. We don't care if there's a concurrency issue there. | 603 database. We don't care if there's a concurrency issue there. |
| 605 | 604 |
| 606 The only backend this seems to affect is postgres. | 605 The only backend this seems to affect is postgres. |
| 607 ''' | 606 """ |
| 608 logging.getLogger('hyperdb').info('commit %s transactions'%( | 607 logging.getLogger('hyperdb').info('commit %s transactions'%( |
| 609 len(self.transactions))) | 608 len(self.transactions))) |
| 610 | 609 |
| 611 # keep a handle to all the database files opened | 610 # keep a handle to all the database files opened |
| 612 self.databases = {} | 611 self.databases = {} |
| 643 self.newnodes = {} | 642 self.newnodes = {} |
| 644 self.destroyednodes = {} | 643 self.destroyednodes = {} |
| 645 self.transactions = [] | 644 self.transactions = [] |
| 646 | 645 |
| 647 def getCachedClassDB(self, classname): | 646 def getCachedClassDB(self, classname): |
| 648 ''' get the class db, looking in our cache of databases for commit | 647 """ get the class db, looking in our cache of databases for commit |
| 649 ''' | 648 """ |
| 650 # get the database handle | 649 # get the database handle |
| 651 db_name = 'nodes.%s'%classname | 650 db_name = 'nodes.%s'%classname |
| 652 if not self.databases.has_key(db_name): | 651 if not self.databases.has_key(db_name): |
| 653 self.databases[db_name] = self.getclassdb(classname, 'c') | 652 self.databases[db_name] = self.getclassdb(classname, 'c') |
| 654 return self.databases[db_name] | 653 return self.databases[db_name] |
| 661 | 660 |
| 662 # return the classname, nodeid so we reindex this content | 661 # return the classname, nodeid so we reindex this content |
| 663 return (classname, nodeid) | 662 return (classname, nodeid) |
| 664 | 663 |
| 665 def getCachedJournalDB(self, classname): | 664 def getCachedJournalDB(self, classname): |
| 666 ''' get the journal db, looking in our cache of databases for commit | 665 """ get the journal db, looking in our cache of databases for commit |
| 667 ''' | 666 """ |
| 668 # get the database handle | 667 # get the database handle |
| 669 db_name = 'journals.%s'%classname | 668 db_name = 'journals.%s'%classname |
| 670 if not self.databases.has_key(db_name): | 669 if not self.databases.has_key(db_name): |
| 671 self.databases[db_name] = self.opendb(db_name, 'c') | 670 self.databases[db_name] = self.opendb(db_name, 'c') |
| 672 return self.databases[db_name] | 671 return self.databases[db_name] |
| 724 db = self.getCachedJournalDB(classname) | 723 db = self.getCachedJournalDB(classname) |
| 725 if db.has_key(nodeid): | 724 if db.has_key(nodeid): |
| 726 del db[nodeid] | 725 del db[nodeid] |
| 727 | 726 |
| 728 def rollback(self): | 727 def rollback(self): |
| 729 ''' Reverse all actions from the current transaction. | 728 """ Reverse all actions from the current transaction. |
| 730 ''' | 729 """ |
| 731 logging.getLogger('hyperdb').info('rollback %s transactions'%( | 730 logging.getLogger('hyperdb').info('rollback %s transactions'%( |
| 732 len(self.transactions))) | 731 len(self.transactions))) |
| 733 | 732 |
| 734 for method, args in self.transactions: | 733 for method, args in self.transactions: |
| 735 # delete temporary files | 734 # delete temporary files |
| 740 self.newnodes = {} | 739 self.newnodes = {} |
| 741 self.destroyednodes = {} | 740 self.destroyednodes = {} |
| 742 self.transactions = [] | 741 self.transactions = [] |
| 743 | 742 |
| 744 def close(self): | 743 def close(self): |
| 745 ''' Nothing to do | 744 """ Nothing to do |
| 746 ''' | 745 """ |
| 747 if self.lockfile is not None: | 746 if self.lockfile is not None: |
| 748 locking.release_lock(self.lockfile) | 747 locking.release_lock(self.lockfile) |
| 749 self.lockfile.close() | 748 self.lockfile.close() |
| 750 self.lockfile = None | 749 self.lockfile = None |
| 751 | 750 |
| 752 _marker = [] | 751 _marker = [] |
| 753 class Class(hyperdb.Class): | 752 class Class(hyperdb.Class): |
| 754 '''The handle to a particular class of nodes in a hyperdatabase.''' | 753 """The handle to a particular class of nodes in a hyperdatabase.""" |
| 755 | 754 |
| 756 def enableJournalling(self): | 755 def enableJournalling(self): |
| 757 '''Turn journalling on for this class | 756 """Turn journalling on for this class |
| 758 ''' | 757 """ |
| 759 self.do_journal = 1 | 758 self.do_journal = 1 |
| 760 | 759 |
| 761 def disableJournalling(self): | 760 def disableJournalling(self): |
| 762 '''Turn journalling off for this class | 761 """Turn journalling off for this class |
| 763 ''' | 762 """ |
| 764 self.do_journal = 0 | 763 self.do_journal = 0 |
| 765 | 764 |
| 766 # Editing nodes: | 765 # Editing nodes: |
| 767 | 766 |
| 768 def create(self, **propvalues): | 767 def create(self, **propvalues): |
| 769 '''Create a new node of this class and return its id. | 768 """Create a new node of this class and return its id. |
| 770 | 769 |
| 771 The keyword arguments in 'propvalues' map property names to values. | 770 The keyword arguments in 'propvalues' map property names to values. |
| 772 | 771 |
| 773 The values of arguments must be acceptable for the types of their | 772 The values of arguments must be acceptable for the types of their |
| 774 corresponding properties or a TypeError is raised. | 773 corresponding properties or a TypeError is raised. |
| 782 If an id in a link or multilink property does not refer to a valid | 781 If an id in a link or multilink property does not refer to a valid |
| 783 node, an IndexError is raised. | 782 node, an IndexError is raised. |
| 784 | 783 |
| 785 These operations trigger detectors and can be vetoed. Attempts | 784 These operations trigger detectors and can be vetoed. Attempts |
| 786 to modify the "creation" or "activity" properties cause a KeyError. | 785 to modify the "creation" or "activity" properties cause a KeyError. |
| 787 ''' | 786 """ |
| 788 self.fireAuditors('create', None, propvalues) | 787 self.fireAuditors('create', None, propvalues) |
| 789 newid = self.create_inner(**propvalues) | 788 newid = self.create_inner(**propvalues) |
| 790 self.fireReactors('create', newid, None) | 789 self.fireReactors('create', newid, None) |
| 791 return newid | 790 return newid |
| 792 | 791 |
| 793 def create_inner(self, **propvalues): | 792 def create_inner(self, **propvalues): |
| 794 ''' Called by create, in-between the audit and react calls. | 793 """ Called by create, in-between the audit and react calls. |
| 795 ''' | 794 """ |
| 796 if propvalues.has_key('id'): | 795 if propvalues.has_key('id'): |
| 797 raise KeyError, '"id" is reserved' | 796 raise KeyError, '"id" is reserved' |
| 798 | 797 |
| 799 if self.db.journaltag is None: | 798 if self.db.journaltag is None: |
| 800 raise hyperdb.DatabaseError, _('Database open read-only') | 799 raise hyperdb.DatabaseError, _('Database open read-only') |
| 924 self.db.addjournal(self.classname, newid, 'create', {}) | 923 self.db.addjournal(self.classname, newid, 'create', {}) |
| 925 | 924 |
| 926 return newid | 925 return newid |
| 927 | 926 |
| 928 def get(self, nodeid, propname, default=_marker, cache=1): | 927 def get(self, nodeid, propname, default=_marker, cache=1): |
| 929 '''Get the value of a property on an existing node of this class. | 928 """Get the value of a property on an existing node of this class. |
| 930 | 929 |
| 931 'nodeid' must be the id of an existing node of this class or an | 930 'nodeid' must be the id of an existing node of this class or an |
| 932 IndexError is raised. 'propname' must be the name of a property | 931 IndexError is raised. 'propname' must be the name of a property |
| 933 of this class or a KeyError is raised. | 932 of this class or a KeyError is raised. |
| 934 | 933 |
| 935 'cache' exists for backward compatibility, and is not used. | 934 'cache' exists for backward compatibility, and is not used. |
| 936 | 935 |
| 937 Attempts to get the "creation" or "activity" properties should | 936 Attempts to get the "creation" or "activity" properties should |
| 938 do the right thing. | 937 do the right thing. |
| 939 ''' | 938 """ |
| 940 if propname == 'id': | 939 if propname == 'id': |
| 941 return nodeid | 940 return nodeid |
| 942 | 941 |
| 943 # get the node's dict | 942 # get the node's dict |
| 944 d = self.db.getnode(self.classname, nodeid) | 943 d = self.db.getnode(self.classname, nodeid) |
| 1024 return d[propname][:] | 1023 return d[propname][:] |
| 1025 | 1024 |
| 1026 return d[propname] | 1025 return d[propname] |
| 1027 | 1026 |
| 1028 def set(self, nodeid, **propvalues): | 1027 def set(self, nodeid, **propvalues): |
| 1029 '''Modify a property on an existing node of this class. | 1028 """Modify a property on an existing node of this class. |
| 1030 | 1029 |
| 1031 'nodeid' must be the id of an existing node of this class or an | 1030 'nodeid' must be the id of an existing node of this class or an |
| 1032 IndexError is raised. | 1031 IndexError is raised. |
| 1033 | 1032 |
| 1034 Each key in 'propvalues' must be the name of a property of this | 1033 Each key in 'propvalues' must be the name of a property of this |
| 1043 If the value of a Link or Multilink property contains an invalid | 1042 If the value of a Link or Multilink property contains an invalid |
| 1044 node id, a ValueError is raised. | 1043 node id, a ValueError is raised. |
| 1045 | 1044 |
| 1046 These operations trigger detectors and can be vetoed. Attempts | 1045 These operations trigger detectors and can be vetoed. Attempts |
| 1047 to modify the "creation" or "activity" properties cause a KeyError. | 1046 to modify the "creation" or "activity" properties cause a KeyError. |
| 1048 ''' | 1047 """ |
| 1049 self.fireAuditors('set', nodeid, propvalues) | 1048 self.fireAuditors('set', nodeid, propvalues) |
| 1050 oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid)) | 1049 oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid)) |
| 1051 for name,prop in self.getprops(protected=0).items(): | 1050 for name,prop in self.getprops(protected=0).items(): |
| 1052 if oldvalues.has_key(name): | 1051 if oldvalues.has_key(name): |
| 1053 continue | 1052 continue |
| 1058 propvalues = self.set_inner(nodeid, **propvalues) | 1057 propvalues = self.set_inner(nodeid, **propvalues) |
| 1059 self.fireReactors('set', nodeid, oldvalues) | 1058 self.fireReactors('set', nodeid, oldvalues) |
| 1060 return propvalues | 1059 return propvalues |
| 1061 | 1060 |
| 1062 def set_inner(self, nodeid, **propvalues): | 1061 def set_inner(self, nodeid, **propvalues): |
| 1063 ''' Called by set, in-between the audit and react calls. | 1062 """ Called by set, in-between the audit and react calls. |
| 1064 ''' | 1063 """ |
| 1065 if not propvalues: | 1064 if not propvalues: |
| 1066 return propvalues | 1065 return propvalues |
| 1067 | 1066 |
| 1068 if propvalues.has_key('creation') or propvalues.has_key('activity'): | 1067 if propvalues.has_key('creation') or propvalues.has_key('activity'): |
| 1069 raise KeyError, '"creation" and "activity" are reserved' | 1068 raise KeyError, '"creation" and "activity" are reserved' |
| 1255 self.db.addjournal(self.classname, nodeid, 'set', journalvalues) | 1254 self.db.addjournal(self.classname, nodeid, 'set', journalvalues) |
| 1256 | 1255 |
| 1257 return propvalues | 1256 return propvalues |
| 1258 | 1257 |
| 1259 def retire(self, nodeid): | 1258 def retire(self, nodeid): |
| 1260 '''Retire a node. | 1259 """Retire a node. |
| 1261 | 1260 |
| 1262 The properties on the node remain available from the get() method, | 1261 The properties on the node remain available from the get() method, |
| 1263 and the node's id is never reused. | 1262 and the node's id is never reused. |
| 1264 | 1263 |
| 1265 Retired nodes are not returned by the find(), list(), or lookup() | 1264 Retired nodes are not returned by the find(), list(), or lookup() |
| 1266 methods, and other nodes may reuse the values of their key properties. | 1265 methods, and other nodes may reuse the values of their key properties. |
| 1267 | 1266 |
| 1268 These operations trigger detectors and can be vetoed. Attempts | 1267 These operations trigger detectors and can be vetoed. Attempts |
| 1269 to modify the "creation" or "activity" properties cause a KeyError. | 1268 to modify the "creation" or "activity" properties cause a KeyError. |
| 1270 ''' | 1269 """ |
| 1271 if self.db.journaltag is None: | 1270 if self.db.journaltag is None: |
| 1272 raise hyperdb.DatabaseError, _('Database open read-only') | 1271 raise hyperdb.DatabaseError, _('Database open read-only') |
| 1273 | 1272 |
| 1274 self.fireAuditors('retire', nodeid, None) | 1273 self.fireAuditors('retire', nodeid, None) |
| 1275 | 1274 |
| 1280 self.db.addjournal(self.classname, nodeid, 'retired', None) | 1279 self.db.addjournal(self.classname, nodeid, 'retired', None) |
| 1281 | 1280 |
| 1282 self.fireReactors('retire', nodeid, None) | 1281 self.fireReactors('retire', nodeid, None) |
| 1283 | 1282 |
| 1284 def restore(self, nodeid): | 1283 def restore(self, nodeid): |
| 1285 '''Restpre a retired node. | 1284 """Restpre a retired node. |
| 1286 | 1285 |
| 1287 Make node available for all operations like it was before retirement. | 1286 Make node available for all operations like it was before retirement. |
| 1288 ''' | 1287 """ |
| 1289 if self.db.journaltag is None: | 1288 if self.db.journaltag is None: |
| 1290 raise hyperdb.DatabaseError, _('Database open read-only') | 1289 raise hyperdb.DatabaseError, _('Database open read-only') |
| 1291 | 1290 |
| 1292 node = self.db.getnode(self.classname, nodeid) | 1291 node = self.db.getnode(self.classname, nodeid) |
| 1293 # check if key property was overrided | 1292 # check if key property was overrided |
| 1307 self.db.addjournal(self.classname, nodeid, 'restored', None) | 1306 self.db.addjournal(self.classname, nodeid, 'restored', None) |
| 1308 | 1307 |
| 1309 self.fireReactors('restore', nodeid, None) | 1308 self.fireReactors('restore', nodeid, None) |
| 1310 | 1309 |
| 1311 def is_retired(self, nodeid, cldb=None): | 1310 def is_retired(self, nodeid, cldb=None): |
| 1312 '''Return true if the node is retired. | 1311 """Return true if the node is retired. |
| 1313 ''' | 1312 """ |
| 1314 node = self.db.getnode(self.classname, nodeid, cldb) | 1313 node = self.db.getnode(self.classname, nodeid, cldb) |
| 1315 if node.has_key(self.db.RETIRED_FLAG): | 1314 if node.has_key(self.db.RETIRED_FLAG): |
| 1316 return 1 | 1315 return 1 |
| 1317 return 0 | 1316 return 0 |
| 1318 | 1317 |
| 1319 def destroy(self, nodeid): | 1318 def destroy(self, nodeid): |
| 1320 '''Destroy a node. | 1319 """Destroy a node. |
| 1321 | 1320 |
| 1322 WARNING: this method should never be used except in extremely rare | 1321 WARNING: this method should never be used except in extremely rare |
| 1323 situations where there could never be links to the node being | 1322 situations where there could never be links to the node being |
| 1324 deleted | 1323 deleted |
| 1325 | 1324 |
| 1329 | 1328 |
| 1330 WARNING: really, use retire() instead | 1329 WARNING: really, use retire() instead |
| 1331 | 1330 |
| 1332 Well, I think that's enough warnings. This method exists mostly to | 1331 Well, I think that's enough warnings. This method exists mostly to |
| 1333 support the session storage of the cgi interface. | 1332 support the session storage of the cgi interface. |
| 1334 ''' | 1333 """ |
| 1335 if self.db.journaltag is None: | 1334 if self.db.journaltag is None: |
| 1336 raise hyperdb.DatabaseError, _('Database open read-only') | 1335 raise hyperdb.DatabaseError, _('Database open read-only') |
| 1337 self.db.destroynode(self.classname, nodeid) | 1336 self.db.destroynode(self.classname, nodeid) |
| 1338 | 1337 |
| 1339 def history(self, nodeid): | 1338 def history(self, nodeid): |
| 1340 '''Retrieve the journal of edits on a particular node. | 1339 """Retrieve the journal of edits on a particular node. |
| 1341 | 1340 |
| 1342 'nodeid' must be the id of an existing node of this class or an | 1341 'nodeid' must be the id of an existing node of this class or an |
| 1343 IndexError is raised. | 1342 IndexError is raised. |
| 1344 | 1343 |
| 1345 The returned list contains tuples of the form | 1344 The returned list contains tuples of the form |
| 1346 | 1345 |
| 1347 (nodeid, date, tag, action, params) | 1346 (nodeid, date, tag, action, params) |
| 1348 | 1347 |
| 1349 'date' is a Timestamp object specifying the time of the change and | 1348 'date' is a Timestamp object specifying the time of the change and |
| 1350 'tag' is the journaltag specified when the database was opened. | 1349 'tag' is the journaltag specified when the database was opened. |
| 1351 ''' | 1350 """ |
| 1352 if not self.do_journal: | 1351 if not self.do_journal: |
| 1353 raise ValueError, 'Journalling is disabled for this class' | 1352 raise ValueError, 'Journalling is disabled for this class' |
| 1354 return self.db.getjournal(self.classname, nodeid) | 1353 return self.db.getjournal(self.classname, nodeid) |
| 1355 | 1354 |
| 1356 # Locating nodes: | 1355 # Locating nodes: |
| 1357 def hasnode(self, nodeid): | 1356 def hasnode(self, nodeid): |
| 1358 '''Determine if the given nodeid actually exists | 1357 """Determine if the given nodeid actually exists |
| 1359 ''' | 1358 """ |
| 1360 return self.db.hasnode(self.classname, nodeid) | 1359 return self.db.hasnode(self.classname, nodeid) |
| 1361 | 1360 |
| 1362 def setkey(self, propname): | 1361 def setkey(self, propname): |
| 1363 '''Select a String property of this class to be the key property. | 1362 """Select a String property of this class to be the key property. |
| 1364 | 1363 |
| 1365 'propname' must be the name of a String property of this class or | 1364 'propname' must be the name of a String property of this class or |
| 1366 None, or a TypeError is raised. The values of the key property on | 1365 None, or a TypeError is raised. The values of the key property on |
| 1367 all existing nodes must be unique or a ValueError is raised. If the | 1366 all existing nodes must be unique or a ValueError is raised. If the |
| 1368 property doesn't exist, KeyError is raised. | 1367 property doesn't exist, KeyError is raised. |
| 1369 ''' | 1368 """ |
| 1370 prop = self.getprops()[propname] | 1369 prop = self.getprops()[propname] |
| 1371 if not isinstance(prop, hyperdb.String): | 1370 if not isinstance(prop, hyperdb.String): |
| 1372 raise TypeError, 'key properties must be String' | 1371 raise TypeError, 'key properties must be String' |
| 1373 self.key = propname | 1372 self.key = propname |
| 1374 | 1373 |
| 1375 def getkey(self): | 1374 def getkey(self): |
| 1376 '''Return the name of the key property for this class or None.''' | 1375 """Return the name of the key property for this class or None.""" |
| 1377 return self.key | 1376 return self.key |
| 1378 | 1377 |
| 1379 # TODO: set up a separate index db file for this? profile? | 1378 # TODO: set up a separate index db file for this? profile? |
| 1380 def lookup(self, keyvalue): | 1379 def lookup(self, keyvalue): |
| 1381 '''Locate a particular node by its key property and return its id. | 1380 """Locate a particular node by its key property and return its id. |
| 1382 | 1381 |
| 1383 If this class has no key property, a TypeError is raised. If the | 1382 If this class has no key property, a TypeError is raised. If the |
| 1384 'keyvalue' matches one of the values for the key property among | 1383 'keyvalue' matches one of the values for the key property among |
| 1385 the nodes in this class, the matching node's id is returned; | 1384 the nodes in this class, the matching node's id is returned; |
| 1386 otherwise a KeyError is raised. | 1385 otherwise a KeyError is raised. |
| 1387 ''' | 1386 """ |
| 1388 if not self.key: | 1387 if not self.key: |
| 1389 raise TypeError, 'No key property set for class %s'%self.classname | 1388 raise TypeError, 'No key property set for class %s'%self.classname |
| 1390 cldb = self.db.getclassdb(self.classname) | 1389 cldb = self.db.getclassdb(self.classname) |
| 1391 try: | 1390 try: |
| 1392 for nodeid in self.getnodeids(cldb): | 1391 for nodeid in self.getnodeids(cldb): |
| 1402 raise KeyError, 'No key (%s) value "%s" for "%s"'%(self.key, | 1401 raise KeyError, 'No key (%s) value "%s" for "%s"'%(self.key, |
| 1403 keyvalue, self.classname) | 1402 keyvalue, self.classname) |
| 1404 | 1403 |
| 1405 # change from spec - allows multiple props to match | 1404 # change from spec - allows multiple props to match |
| 1406 def find(self, **propspec): | 1405 def find(self, **propspec): |
| 1407 '''Get the ids of nodes in this class which link to the given nodes. | 1406 """Get the ids of nodes in this class which link to the given nodes. |
| 1408 | 1407 |
| 1409 'propspec' consists of keyword args propname=nodeid or | 1408 'propspec' consists of keyword args propname=nodeid or |
| 1410 propname={nodeid:1, } | 1409 propname={nodeid:1, } |
| 1411 'propname' must be the name of a property in this class, or a | 1410 'propname' must be the name of a property in this class, or a |
| 1412 KeyError is raised. That property must be a Link or | 1411 KeyError is raised. That property must be a Link or |
| 1415 Any node in this class whose 'propname' property links to any of | 1414 Any node in this class whose 'propname' property links to any of |
| 1416 the nodeids will be returned. Examples:: | 1415 the nodeids will be returned. Examples:: |
| 1417 | 1416 |
| 1418 db.issue.find(messages='1') | 1417 db.issue.find(messages='1') |
| 1419 db.issue.find(messages={'1':1,'3':1}, files={'7':1}) | 1418 db.issue.find(messages={'1':1,'3':1}, files={'7':1}) |
| 1420 ''' | 1419 """ |
| 1421 propspec = propspec.items() | 1420 propspec = propspec.items() |
| 1422 for propname, itemids in propspec: | 1421 for propname, itemids in propspec: |
| 1423 # check the prop is OK | 1422 # check the prop is OK |
| 1424 prop = self.properties[propname] | 1423 prop = self.properties[propname] |
| 1425 if not isinstance(prop, hyperdb.Link) and not isinstance(prop, hyperdb.Multilink): | 1424 if not isinstance(prop, hyperdb.Link) and not isinstance(prop, hyperdb.Multilink): |
| 1462 finally: | 1461 finally: |
| 1463 cldb.close() | 1462 cldb.close() |
| 1464 return l | 1463 return l |
| 1465 | 1464 |
| 1466 def stringFind(self, **requirements): | 1465 def stringFind(self, **requirements): |
| 1467 '''Locate a particular node by matching a set of its String | 1466 """Locate a particular node by matching a set of its String |
| 1468 properties in a caseless search. | 1467 properties in a caseless search. |
| 1469 | 1468 |
| 1470 If the property is not a String property, a TypeError is raised. | 1469 If the property is not a String property, a TypeError is raised. |
| 1471 | 1470 |
| 1472 The return is a list of the id of all nodes that match. | 1471 The return is a list of the id of all nodes that match. |
| 1473 ''' | 1472 """ |
| 1474 for propname in requirements.keys(): | 1473 for propname in requirements.keys(): |
| 1475 prop = self.properties[propname] | 1474 prop = self.properties[propname] |
| 1476 if not isinstance(prop, hyperdb.String): | 1475 if not isinstance(prop, hyperdb.String): |
| 1477 raise TypeError, "'%s' not a String property"%propname | 1476 raise TypeError, "'%s' not a String property"%propname |
| 1478 requirements[propname] = requirements[propname].lower() | 1477 requirements[propname] = requirements[propname].lower() |
| 1493 finally: | 1492 finally: |
| 1494 cldb.close() | 1493 cldb.close() |
| 1495 return l | 1494 return l |
| 1496 | 1495 |
| 1497 def list(self): | 1496 def list(self): |
| 1498 ''' Return a list of the ids of the active nodes in this class. | 1497 """ Return a list of the ids of the active nodes in this class. |
| 1499 ''' | 1498 """ |
| 1500 l = [] | 1499 l = [] |
| 1501 cn = self.classname | 1500 cn = self.classname |
| 1502 cldb = self.db.getclassdb(cn) | 1501 cldb = self.db.getclassdb(cn) |
| 1503 try: | 1502 try: |
| 1504 for nodeid in self.getnodeids(cldb): | 1503 for nodeid in self.getnodeids(cldb): |
| 1510 cldb.close() | 1509 cldb.close() |
| 1511 l.sort() | 1510 l.sort() |
| 1512 return l | 1511 return l |
| 1513 | 1512 |
| 1514 def getnodeids(self, db=None, retired=None): | 1513 def getnodeids(self, db=None, retired=None): |
| 1515 ''' Return a list of ALL nodeids | 1514 """ Return a list of ALL nodeids |
| 1516 | 1515 |
| 1517 Set retired=None to get all nodes. Otherwise it'll get all the | 1516 Set retired=None to get all nodes. Otherwise it'll get all the |
| 1518 retired or non-retired nodes, depending on the flag. | 1517 retired or non-retired nodes, depending on the flag. |
| 1519 ''' | 1518 """ |
| 1520 res = [] | 1519 res = [] |
| 1521 | 1520 |
| 1522 # start off with the new nodes | 1521 # start off with the new nodes |
| 1523 if self.db.newnodes.has_key(self.classname): | 1522 if self.db.newnodes.has_key(self.classname): |
| 1524 res += self.db.newnodes[self.classname].keys() | 1523 res += self.db.newnodes[self.classname].keys() |
| 1809 if __debug__: | 1808 if __debug__: |
| 1810 self.db.stats['filtering'] += (time.time() - start_t) | 1809 self.db.stats['filtering'] += (time.time() - start_t) |
| 1811 return matches | 1810 return matches |
| 1812 | 1811 |
| 1813 def count(self): | 1812 def count(self): |
| 1814 '''Get the number of nodes in this class. | 1813 """Get the number of nodes in this class. |
| 1815 | 1814 |
| 1816 If the returned integer is 'numnodes', the ids of all the nodes | 1815 If the returned integer is 'numnodes', the ids of all the nodes |
| 1817 in this class run from 1 to numnodes, and numnodes+1 will be the | 1816 in this class run from 1 to numnodes, and numnodes+1 will be the |
| 1818 id of the next node to be created in this class. | 1817 id of the next node to be created in this class. |
| 1819 ''' | 1818 """ |
| 1820 return self.db.countnodes(self.classname) | 1819 return self.db.countnodes(self.classname) |
| 1821 | 1820 |
| 1822 # Manipulating properties: | 1821 # Manipulating properties: |
| 1823 | 1822 |
| 1824 def getprops(self, protected=1): | 1823 def getprops(self, protected=1): |
| 1825 '''Return a dictionary mapping property names to property objects. | 1824 """Return a dictionary mapping property names to property objects. |
| 1826 If the "protected" flag is true, we include protected properties - | 1825 If the "protected" flag is true, we include protected properties - |
| 1827 those which may not be modified. | 1826 those which may not be modified. |
| 1828 | 1827 |
| 1829 In addition to the actual properties on the node, these | 1828 In addition to the actual properties on the node, these |
| 1830 methods provide the "creation" and "activity" properties. If the | 1829 methods provide the "creation" and "activity" properties. If the |
| 1831 "protected" flag is true, we include protected properties - those | 1830 "protected" flag is true, we include protected properties - those |
| 1832 which may not be modified. | 1831 which may not be modified. |
| 1833 ''' | 1832 """ |
| 1834 d = self.properties.copy() | 1833 d = self.properties.copy() |
| 1835 if protected: | 1834 if protected: |
| 1836 d['id'] = hyperdb.String() | 1835 d['id'] = hyperdb.String() |
| 1837 d['creation'] = hyperdb.Date() | 1836 d['creation'] = hyperdb.Date() |
| 1838 d['activity'] = hyperdb.Date() | 1837 d['activity'] = hyperdb.Date() |
| 1839 d['creator'] = hyperdb.Link('user') | 1838 d['creator'] = hyperdb.Link('user') |
| 1840 d['actor'] = hyperdb.Link('user') | 1839 d['actor'] = hyperdb.Link('user') |
| 1841 return d | 1840 return d |
| 1842 | 1841 |
| 1843 def addprop(self, **properties): | 1842 def addprop(self, **properties): |
| 1844 '''Add properties to this class. | 1843 """Add properties to this class. |
| 1845 | 1844 |
| 1846 The keyword arguments in 'properties' must map names to property | 1845 The keyword arguments in 'properties' must map names to property |
| 1847 objects, or a TypeError is raised. None of the keys in 'properties' | 1846 objects, or a TypeError is raised. None of the keys in 'properties' |
| 1848 may collide with the names of existing properties, or a ValueError | 1847 may collide with the names of existing properties, or a ValueError |
| 1849 is raised before any properties have been added. | 1848 is raised before any properties have been added. |
| 1850 ''' | 1849 """ |
| 1851 for key in properties.keys(): | 1850 for key in properties.keys(): |
| 1852 if self.properties.has_key(key): | 1851 if self.properties.has_key(key): |
| 1853 raise ValueError, key | 1852 raise ValueError, key |
| 1854 self.properties.update(properties) | 1853 self.properties.update(properties) |
| 1855 | 1854 |
| 1856 def index(self, nodeid): | 1855 def index(self, nodeid): |
| 1857 ''' Add (or refresh) the node to search indexes ''' | 1856 """ Add (or refresh) the node to search indexes """ |
| 1858 # find all the String properties that have indexme | 1857 # find all the String properties that have indexme |
| 1859 for prop, propclass in self.getprops().items(): | 1858 for prop, propclass in self.getprops().items(): |
| 1860 if isinstance(propclass, hyperdb.String) and propclass.indexme: | 1859 if isinstance(propclass, hyperdb.String) and propclass.indexme: |
| 1861 # index them under (classname, nodeid, property) | 1860 # index them under (classname, nodeid, property) |
| 1862 try: | 1861 try: |
| 1868 | 1867 |
| 1869 # | 1868 # |
| 1870 # import / export support | 1869 # import / export support |
| 1871 # | 1870 # |
| 1872 def export_list(self, propnames, nodeid): | 1871 def export_list(self, propnames, nodeid): |
| 1873 ''' Export a node - generate a list of CSV-able data in the order | 1872 """ Export a node - generate a list of CSV-able data in the order |
| 1874 specified by propnames for the given node. | 1873 specified by propnames for the given node. |
| 1875 ''' | 1874 """ |
| 1876 properties = self.getprops() | 1875 properties = self.getprops() |
| 1877 l = [] | 1876 l = [] |
| 1878 for prop in propnames: | 1877 for prop in propnames: |
| 1879 proptype = properties[prop] | 1878 proptype = properties[prop] |
| 1880 value = self.get(nodeid, prop) | 1879 value = self.get(nodeid, prop) |
| 1893 l.append(repr(self.is_retired(nodeid))) | 1892 l.append(repr(self.is_retired(nodeid))) |
| 1894 | 1893 |
| 1895 return l | 1894 return l |
| 1896 | 1895 |
| 1897 def import_list(self, propnames, proplist): | 1896 def import_list(self, propnames, proplist): |
| 1898 ''' Import a node - all information including "id" is present and | 1897 """ Import a node - all information including "id" is present and |
| 1899 should not be sanity checked. Triggers are not triggered. The | 1898 should not be sanity checked. Triggers are not triggered. The |
| 1900 journal should be initialised using the "creator" and "created" | 1899 journal should be initialised using the "creator" and "created" |
| 1901 information. | 1900 information. |
| 1902 | 1901 |
| 1903 Return the nodeid of the node imported. | 1902 Return the nodeid of the node imported. |
| 1904 ''' | 1903 """ |
| 1905 if self.db.journaltag is None: | 1904 if self.db.journaltag is None: |
| 1906 raise hyperdb.DatabaseError, _('Database open read-only') | 1905 raise hyperdb.DatabaseError, _('Database open read-only') |
| 1907 properties = self.getprops() | 1906 properties = self.getprops() |
| 1908 | 1907 |
| 1909 # make the new node's property map | 1908 # make the new node's property map |
| 1947 # add the node and journal | 1946 # add the node and journal |
| 1948 self.db.addnode(self.classname, newid, d) | 1947 self.db.addnode(self.classname, newid, d) |
| 1949 return newid | 1948 return newid |
| 1950 | 1949 |
| 1951 def export_journals(self): | 1950 def export_journals(self): |
| 1952 '''Export a class's journal - generate a list of lists of | 1951 """Export a class's journal - generate a list of lists of |
| 1953 CSV-able data: | 1952 CSV-able data: |
| 1954 | 1953 |
| 1955 nodeid, date, user, action, params | 1954 nodeid, date, user, action, params |
| 1956 | 1955 |
| 1957 No heading here - the columns are fixed. | 1956 No heading here - the columns are fixed. |
| 1958 ''' | 1957 """ |
| 1959 properties = self.getprops() | 1958 properties = self.getprops() |
| 1960 r = [] | 1959 r = [] |
| 1961 for nodeid in self.getnodeids(): | 1960 for nodeid in self.getnodeids(): |
| 1962 for nodeid, date, user, action, params in self.history(nodeid): | 1961 for nodeid, date, user, action, params in self.history(nodeid): |
| 1963 date = date.get_tuple() | 1962 date = date.get_tuple() |
| 1987 l = [nodeid, date, user, action, params] | 1986 l = [nodeid, date, user, action, params] |
| 1988 r.append(map(repr, l)) | 1987 r.append(map(repr, l)) |
| 1989 return r | 1988 return r |
| 1990 | 1989 |
| 1991 def import_journals(self, entries): | 1990 def import_journals(self, entries): |
| 1992 '''Import a class's journal. | 1991 """Import a class's journal. |
| 1993 | 1992 |
| 1994 Uses setjournal() to set the journal for each item.''' | 1993 Uses setjournal() to set the journal for each item.""" |
| 1995 properties = self.getprops() | 1994 properties = self.getprops() |
| 1996 d = {} | 1995 d = {} |
| 1997 for l in entries: | 1996 for l in entries: |
| 1998 l = map(eval, l) | 1997 l = map(eval, l) |
| 1999 nodeid, jdate, user, action, params = l | 1998 nodeid, jdate, user, action, params = l |
| 2019 | 2018 |
| 2020 for nodeid, l in d.items(): | 2019 for nodeid, l in d.items(): |
| 2021 self.db.setjournal(self.classname, nodeid, l) | 2020 self.db.setjournal(self.classname, nodeid, l) |
| 2022 | 2021 |
| 2023 class FileClass(hyperdb.FileClass, Class): | 2022 class FileClass(hyperdb.FileClass, Class): |
| 2024 '''This class defines a large chunk of data. To support this, it has a | 2023 """This class defines a large chunk of data. To support this, it has a |
| 2025 mandatory String property "content" which is typically saved off | 2024 mandatory String property "content" which is typically saved off |
| 2026 externally to the hyperdb. | 2025 externally to the hyperdb. |
| 2027 | 2026 |
| 2028 The default MIME type of this data is defined by the | 2027 The default MIME type of this data is defined by the |
| 2029 "default_mime_type" class attribute, which may be overridden by each | 2028 "default_mime_type" class attribute, which may be overridden by each |
| 2030 node if the class defines a "type" String property. | 2029 node if the class defines a "type" String property. |
| 2031 ''' | 2030 """ |
| 2032 def __init__(self, db, classname, **properties): | 2031 def __init__(self, db, classname, **properties): |
| 2033 '''The newly-created class automatically includes the "content" | 2032 """The newly-created class automatically includes the "content" |
| 2034 and "type" properties. | 2033 and "type" properties. |
| 2035 ''' | 2034 """ |
| 2036 if not properties.has_key('content'): | 2035 if not properties.has_key('content'): |
| 2037 properties['content'] = hyperdb.String(indexme='yes') | 2036 properties['content'] = hyperdb.String(indexme='yes') |
| 2038 if not properties.has_key('type'): | 2037 if not properties.has_key('type'): |
| 2039 properties['type'] = hyperdb.String() | 2038 properties['type'] = hyperdb.String() |
| 2040 Class.__init__(self, db, classname, **properties) | 2039 Class.__init__(self, db, classname, **properties) |
| 2041 | 2040 |
| 2042 def create(self, **propvalues): | 2041 def create(self, **propvalues): |
| 2043 ''' Snarf the "content" propvalue and store in a file | 2042 """ Snarf the "content" propvalue and store in a file |
| 2044 ''' | 2043 """ |
| 2045 # we need to fire the auditors now, or the content property won't | 2044 # we need to fire the auditors now, or the content property won't |
| 2046 # be in propvalues for the auditors to play with | 2045 # be in propvalues for the auditors to play with |
| 2047 self.fireAuditors('create', None, propvalues) | 2046 self.fireAuditors('create', None, propvalues) |
| 2048 | 2047 |
| 2049 # now remove the content property so it's not stored in the db | 2048 # now remove the content property so it's not stored in the db |
| 2063 self.fireReactors('create', newid, None) | 2062 self.fireReactors('create', newid, None) |
| 2064 | 2063 |
| 2065 return newid | 2064 return newid |
| 2066 | 2065 |
| 2067 def get(self, nodeid, propname, default=_marker, cache=1): | 2066 def get(self, nodeid, propname, default=_marker, cache=1): |
| 2068 ''' Trap the content propname and get it from the file | 2067 """ Trap the content propname and get it from the file |
| 2069 | 2068 |
| 2070 'cache' exists for backwards compatibility, and is not used. | 2069 'cache' exists for backwards compatibility, and is not used. |
| 2071 ''' | 2070 """ |
| 2072 poss_msg = 'Possibly an access right configuration problem.' | 2071 poss_msg = 'Possibly an access right configuration problem.' |
| 2073 if propname == 'content': | 2072 if propname == 'content': |
| 2074 try: | 2073 try: |
| 2075 return self.db.getfile(self.classname, nodeid, None) | 2074 return self.db.getfile(self.classname, nodeid, None) |
| 2076 except IOError, (strerror): | 2075 except IOError, (strerror): |
| 2081 return Class.get(self, nodeid, propname, default) | 2080 return Class.get(self, nodeid, propname, default) |
| 2082 else: | 2081 else: |
| 2083 return Class.get(self, nodeid, propname) | 2082 return Class.get(self, nodeid, propname) |
| 2084 | 2083 |
| 2085 def set(self, itemid, **propvalues): | 2084 def set(self, itemid, **propvalues): |
| 2086 ''' Snarf the "content" propvalue and update it in a file | 2085 """ Snarf the "content" propvalue and update it in a file |
| 2087 ''' | 2086 """ |
| 2088 self.fireAuditors('set', itemid, propvalues) | 2087 self.fireAuditors('set', itemid, propvalues) |
| 2089 | 2088 |
| 2090 # create the oldvalues dict - fill in any missing values | 2089 # create the oldvalues dict - fill in any missing values |
| 2091 oldvalues = copy.deepcopy(self.db.getnode(self.classname, itemid)) | 2090 oldvalues = copy.deepcopy(self.db.getnode(self.classname, itemid)) |
| 2092 for name,prop in self.getprops(protected=0).items(): | 2091 for name,prop in self.getprops(protected=0).items(): |
| 2119 # fire reactors | 2118 # fire reactors |
| 2120 self.fireReactors('set', itemid, oldvalues) | 2119 self.fireReactors('set', itemid, oldvalues) |
| 2121 return propvalues | 2120 return propvalues |
| 2122 | 2121 |
| 2123 def index(self, nodeid): | 2122 def index(self, nodeid): |
| 2124 ''' Add (or refresh) the node to search indexes. | 2123 """ Add (or refresh) the node to search indexes. |
| 2125 | 2124 |
| 2126 Use the content-type property for the content property. | 2125 Use the content-type property for the content property. |
| 2127 ''' | 2126 """ |
| 2128 # find all the String properties that have indexme | 2127 # find all the String properties that have indexme |
| 2129 for prop, propclass in self.getprops().items(): | 2128 for prop, propclass in self.getprops().items(): |
| 2130 if prop == 'content' and propclass.indexme: | 2129 if prop == 'content' and propclass.indexme: |
| 2131 mime_type = self.get(nodeid, 'type', self.default_mime_type) | 2130 mime_type = self.get(nodeid, 'type', self.default_mime_type) |
| 2132 self.db.indexer.add_text((self.classname, nodeid, 'content'), | 2131 self.db.indexer.add_text((self.classname, nodeid, 'content'), |
| 2142 | 2141 |
| 2143 # deviation from spec - was called ItemClass | 2142 # deviation from spec - was called ItemClass |
| 2144 class IssueClass(Class, roundupdb.IssueClass): | 2143 class IssueClass(Class, roundupdb.IssueClass): |
| 2145 # Overridden methods: | 2144 # Overridden methods: |
| 2146 def __init__(self, db, classname, **properties): | 2145 def __init__(self, db, classname, **properties): |
| 2147 '''The newly-created class automatically includes the "messages", | 2146 """The newly-created class automatically includes the "messages", |
| 2148 "files", "nosy", and "superseder" properties. If the 'properties' | 2147 "files", "nosy", and "superseder" properties. If the 'properties' |
| 2149 dictionary attempts to specify any of these properties or a | 2148 dictionary attempts to specify any of these properties or a |
| 2150 "creation" or "activity" property, a ValueError is raised. | 2149 "creation" or "activity" property, a ValueError is raised. |
| 2151 ''' | 2150 """ |
| 2152 if not properties.has_key('title'): | 2151 if not properties.has_key('title'): |
| 2153 properties['title'] = hyperdb.String(indexme='yes') | 2152 properties['title'] = hyperdb.String(indexme='yes') |
| 2154 if not properties.has_key('messages'): | 2153 if not properties.has_key('messages'): |
| 2155 properties['messages'] = hyperdb.Multilink("msg") | 2154 properties['messages'] = hyperdb.Multilink("msg") |
| 2156 if not properties.has_key('files'): | 2155 if not properties.has_key('files'): |
