Code

...except of course it's nice to use valid Python syntax
[roundup.git] / roundup / backends / back_anydbm.py
1 #
2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
11 #
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
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"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17
18 #$Id: back_anydbm.py,v 1.43 2002-07-10 06:30:30 richard Exp $
19 '''
20 This module defines a backend that saves the hyperdatabase in a database
21 chosen by anydbm. It is guaranteed to always be available in python
22 versions >2.1.1 (the dumbdbm fallback in 2.1.1 and earlier has several
23 serious bugs, and is not available)
24 '''
26 import whichdb, anydbm, os, marshal
27 from roundup import hyperdb, date
28 from blobfiles import FileStorage
29 from roundup.indexer import Indexer
30 from locking import acquire_lock, release_lock
32 #
33 # Now the database
34 #
35 class Database(FileStorage, hyperdb.Database):
36     """A database for storing records containing flexible data types.
38     Transaction stuff TODO:
39         . check the timestamp of the class file and nuke the cache if it's
40           modified. Do some sort of conflict checking on the dirty stuff.
41         . perhaps detect write collisions (related to above)?
43     """
44     def __init__(self, config, journaltag=None):
45         """Open a hyperdatabase given a specifier to some storage.
47         The 'storagelocator' is obtained from config.DATABASE.
48         The meaning of 'storagelocator' depends on the particular
49         implementation of the hyperdatabase.  It could be a file name,
50         a directory path, a socket descriptor for a connection to a
51         database over the network, etc.
53         The 'journaltag' is a token that will be attached to the journal
54         entries for any edits done on the database.  If 'journaltag' is
55         None, the database is opened in read-only mode: the Class.create(),
56         Class.set(), and Class.retire() methods are disabled.
57         """
58         self.config, self.journaltag = config, journaltag
59         self.dir = config.DATABASE
60         self.classes = {}
61         self.cache = {}         # cache of nodes loaded or created
62         self.dirtynodes = {}    # keep track of the dirty nodes by class
63         self.newnodes = {}      # keep track of the new nodes by class
64         self.transactions = []
65         self.indexer = Indexer(self.dir)
66         # ensure files are group readable and writable
67         os.umask(0002)
69     def post_init(self):
70         """Called once the schema initialisation has finished."""
71         # reindex the db if necessary
72         if self.indexer.should_reindex():
73             self.reindex()
75     def reindex(self):
76         for klass in self.classes.values():
77             for nodeid in klass.list():
78                 klass.index(nodeid)
79         self.indexer.save_index()
81     def __repr__(self):
82         return '<back_anydbm instance at %x>'%id(self) 
84     #
85     # Classes
86     #
87     def __getattr__(self, classname):
88         """A convenient way of calling self.getclass(classname)."""
89         if self.classes.has_key(classname):
90             if __debug__:
91                 print >>hyperdb.DEBUG, '__getattr__', (self, classname)
92             return self.classes[classname]
93         raise AttributeError, classname
95     def addclass(self, cl):
96         if __debug__:
97             print >>hyperdb.DEBUG, 'addclass', (self, cl)
98         cn = cl.classname
99         if self.classes.has_key(cn):
100             raise ValueError, cn
101         self.classes[cn] = cl
103     def getclasses(self):
104         """Return a list of the names of all existing classes."""
105         if __debug__:
106             print >>hyperdb.DEBUG, 'getclasses', (self,)
107         l = self.classes.keys()
108         l.sort()
109         return l
111     def getclass(self, classname):
112         """Get the Class object representing a particular class.
114         If 'classname' is not a valid class name, a KeyError is raised.
115         """
116         if __debug__:
117             print >>hyperdb.DEBUG, 'getclass', (self, classname)
118         return self.classes[classname]
120     #
121     # Class DBs
122     #
123     def clear(self):
124         '''Delete all database contents
125         '''
126         if __debug__:
127             print >>hyperdb.DEBUG, 'clear', (self,)
128         for cn in self.classes.keys():
129             for dummy in 'nodes', 'journals':
130                 path = os.path.join(self.dir, 'journals.%s'%cn)
131                 if os.path.exists(path):
132                     os.remove(path)
133                 elif os.path.exists(path+'.db'):    # dbm appends .db
134                     os.remove(path+'.db')
136     def getclassdb(self, classname, mode='r'):
137         ''' grab a connection to the class db that will be used for
138             multiple actions
139         '''
140         if __debug__:
141             print >>hyperdb.DEBUG, 'getclassdb', (self, classname, mode)
142         return self._opendb('nodes.%s'%classname, mode)
144     def _opendb(self, name, mode):
145         '''Low-level database opener that gets around anydbm/dbm
146            eccentricities.
147         '''
148         if __debug__:
149             print >>hyperdb.DEBUG, '_opendb', (self, name, mode)
151         # determine which DB wrote the class file
152         db_type = ''
153         path = os.path.join(os.getcwd(), self.dir, name)
154         if os.path.exists(path):
155             db_type = whichdb.whichdb(path)
156             if not db_type:
157                 raise hyperdb.DatabaseError, "Couldn't identify database type"
158         elif os.path.exists(path+'.db'):
159             # if the path ends in '.db', it's a dbm database, whether
160             # anydbm says it's dbhash or not!
161             db_type = 'dbm'
163         # new database? let anydbm pick the best dbm
164         if not db_type:
165             if __debug__:
166                 print >>hyperdb.DEBUG, "_opendb anydbm.open(%r, 'n')"%path
167             return anydbm.open(path, 'n')
169         # open the database with the correct module
170         try:
171             dbm = __import__(db_type)
172         except ImportError:
173             raise hyperdb.DatabaseError, \
174                 "Couldn't open database - the required module '%s'"\
175                 " is not available"%db_type
176         if __debug__:
177             print >>hyperdb.DEBUG, "_opendb %r.open(%r, %r)"%(db_type, path,
178                 mode)
179         return dbm.open(path, mode)
181     def _lockdb(self, name):
182         ''' Lock a database file
183         '''
184         path = os.path.join(os.getcwd(), self.dir, '%s.lock'%name)
185         return acquire_lock(path)
187     #
188     # Node IDs
189     #
190     def newid(self, classname):
191         ''' Generate a new id for the given class
192         '''
193         # open the ids DB - create if if doesn't exist
194         lock = self._lockdb('_ids')
195         db = self._opendb('_ids', 'c')
196         if db.has_key(classname):
197             newid = db[classname] = str(int(db[classname]) + 1)
198         else:
199             # the count() bit is transitional - older dbs won't start at 1
200             newid = str(self.getclass(classname).count()+1)
201             db[classname] = newid
202         db.close()
203         release_lock(lock)
204         return newid
206     #
207     # Nodes
208     #
209     def addnode(self, classname, nodeid, node):
210         ''' add the specified node to its class's db
211         '''
212         if __debug__:
213             print >>hyperdb.DEBUG, 'addnode', (self, classname, nodeid, node)
214         self.newnodes.setdefault(classname, {})[nodeid] = 1
215         self.cache.setdefault(classname, {})[nodeid] = node
216         self.savenode(classname, nodeid, node)
218     def setnode(self, classname, nodeid, node):
219         ''' change the specified node
220         '''
221         if __debug__:
222             print >>hyperdb.DEBUG, 'setnode', (self, classname, nodeid, node)
223         self.dirtynodes.setdefault(classname, {})[nodeid] = 1
225         # can't set without having already loaded the node
226         self.cache[classname][nodeid] = node
227         self.savenode(classname, nodeid, node)
229     def savenode(self, classname, nodeid, node):
230         ''' perform the saving of data specified by the set/addnode
231         '''
232         if __debug__:
233             print >>hyperdb.DEBUG, 'savenode', (self, classname, nodeid, node)
234         self.transactions.append((self._doSaveNode, (classname, nodeid, node)))
236     def getnode(self, classname, nodeid, db=None, cache=1):
237         ''' get a node from the database
238         '''
239         if __debug__:
240             print >>hyperdb.DEBUG, 'getnode', (self, classname, nodeid, db)
241         if cache:
242             # try the cache
243             cache_dict = self.cache.setdefault(classname, {})
244             if cache_dict.has_key(nodeid):
245                 if __debug__:
246                     print >>hyperdb.TRACE, 'get %s %s cached'%(classname,
247                         nodeid)
248                 return cache_dict[nodeid]
250         if __debug__:
251             print >>hyperdb.TRACE, 'get %s %s'%(classname, nodeid)
253         # get from the database and save in the cache
254         if db is None:
255             db = self.getclassdb(classname)
256         if not db.has_key(nodeid):
257             raise IndexError, "no such %s %s"%(classname, nodeid)
259         # decode
260         res = marshal.loads(db[nodeid])
262         # reverse the serialisation
263         res = self.unserialise(classname, res)
265         # store off in the cache dict
266         if cache:
267             cache_dict[nodeid] = res
269         return res
271     def hasnode(self, classname, nodeid, db=None):
272         ''' determine if the database has a given node
273         '''
274         if __debug__:
275             print >>hyperdb.DEBUG, 'hasnode', (self, classname, nodeid, db)
277         # try the cache
278         cache = self.cache.setdefault(classname, {})
279         if cache.has_key(nodeid):
280             if __debug__:
281                 print >>hyperdb.TRACE, 'has %s %s cached'%(classname, nodeid)
282             return 1
283         if __debug__:
284             print >>hyperdb.TRACE, 'has %s %s'%(classname, nodeid)
286         # not in the cache - check the database
287         if db is None:
288             db = self.getclassdb(classname)
289         res = db.has_key(nodeid)
290         return res
292     def countnodes(self, classname, db=None):
293         if __debug__:
294             print >>hyperdb.DEBUG, 'countnodes', (self, classname, db)
295         # include the new nodes not saved to the DB yet
296         count = len(self.newnodes.get(classname, {}))
298         # and count those in the DB
299         if db is None:
300             db = self.getclassdb(classname)
301         count = count + len(db.keys())
302         return count
304     def getnodeids(self, classname, db=None):
305         if __debug__:
306             print >>hyperdb.DEBUG, 'getnodeids', (self, classname, db)
307         # start off with the new nodes
308         res = self.newnodes.get(classname, {}).keys()
310         if db is None:
311             db = self.getclassdb(classname)
312         res = res + db.keys()
313         return res
316     #
317     # Files - special node properties
318     # inherited from FileStorage
320     #
321     # Journal
322     #
323     def addjournal(self, classname, nodeid, action, params):
324         ''' Journal the Action
325         'action' may be:
327             'create' or 'set' -- 'params' is a dictionary of property values
328             'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
329             'retire' -- 'params' is None
330         '''
331         if __debug__:
332             print >>hyperdb.DEBUG, 'addjournal', (self, classname, nodeid,
333                 action, params)
334         self.transactions.append((self._doSaveJournal, (classname, nodeid,
335             action, params)))
337     def getjournal(self, classname, nodeid):
338         ''' get the journal for id
339         '''
340         if __debug__:
341             print >>hyperdb.DEBUG, 'getjournal', (self, classname, nodeid)
342         # attempt to open the journal - in some rare cases, the journal may
343         # not exist
344         try:
345             db = self._opendb('journals.%s'%classname, 'r')
346         except anydbm.error, error:
347             if str(error) == "need 'c' or 'n' flag to open new db": return []
348             elif error.args[0] != 2: raise
349             return []
350         try:
351             journal = marshal.loads(db[nodeid])
352         except KeyError:
353             db.close()
354             raise KeyError, 'no such %s %s'%(classname, nodeid)
355         db.close()
356         res = []
357         for entry in journal:
358             (nodeid, date_stamp, user, action, params) = entry
359             date_obj = date.Date(date_stamp)
360             res.append((nodeid, date_obj, user, action, params))
361         return res
363     def pack(self, pack_before):
364         ''' delete all journal entries before 'pack_before' '''
365         if __debug__:
366             print >>hyperdb.DEBUG, 'packjournal', (self, pack_before)
368         pack_before = pack_before.get_tuple()
370         classes = self.getclasses()
372         # TODO: factor this out to method - we're already doing it in
373         # _opendb.
374         db_type = ''
375         path = os.path.join(os.getcwd(), self.dir, classes[0])
376         if os.path.exists(path):
377             db_type = whichdb.whichdb(path)
378             if not db_type:
379                 raise hyperdb.DatabaseError, "Couldn't identify database type"
380         elif os.path.exists(path+'.db'):
381             db_type = 'dbm'
383         for classname in classes:
384             db_name = 'journals.%s'%classname
385             db = self._opendb(db_name, 'w')
387             for key in db.keys():
388                 journal = marshal.loads(db[key])
389                 l = []
390                 last_set_entry = None
391                 for entry in journal:
392                     (nodeid, date_stamp, self.journaltag, action, 
393                         params) = entry
394                     if date_stamp > pack_before or action == 'create':
395                         l.append(entry)
396                     elif action == 'set':
397                         # grab the last set entry to keep information on
398                         # activity
399                         last_set_entry = entry
400                 if last_set_entry:
401                     date_stamp = last_set_entry[1]
402                     # if the last set entry was made after the pack date
403                     # then it is already in the list
404                     if date_stamp < pack_before:
405                         l.append(last_set_entry)
406                 db[key] = marshal.dumps(l)
407             if db_type == 'gdbm':
408                 db.reorganize()
409             db.close()
410             
412     #
413     # Basic transaction support
414     #
415     def commit(self):
416         ''' Commit the current transactions.
417         '''
418         if __debug__:
419             print >>hyperdb.DEBUG, 'commit', (self,)
420         # TODO: lock the DB
422         # keep a handle to all the database files opened
423         self.databases = {}
425         # now, do all the transactions
426         reindex = {}
427         for method, args in self.transactions:
428             reindex[method(*args)] = 1
430         # now close all the database files
431         for db in self.databases.values():
432             db.close()
433         del self.databases
434         # TODO: unlock the DB
436         # reindex the nodes that request it
437         for classname, nodeid in filter(None, reindex.keys()):
438             print >>hyperdb.DEBUG, 'commit.reindex', (classname, nodeid)
439             self.getclass(classname).index(nodeid)
441         # save the indexer state
442         self.indexer.save_index()
444         # all transactions committed, back to normal
445         self.cache = {}
446         self.dirtynodes = {}
447         self.newnodes = {}
448         self.transactions = []
450     def _doSaveNode(self, classname, nodeid, node):
451         if __debug__:
452             print >>hyperdb.DEBUG, '_doSaveNode', (self, classname, nodeid,
453                 node)
455         # get the database handle
456         db_name = 'nodes.%s'%classname
457         if self.databases.has_key(db_name):
458             db = self.databases[db_name]
459         else:
460             db = self.databases[db_name] = self.getclassdb(classname, 'c')
462         # now save the marshalled data
463         db[nodeid] = marshal.dumps(self.serialise(classname, node))
465         # return the classname, nodeid so we reindex this content
466         return (classname, nodeid)
468     def _doSaveJournal(self, classname, nodeid, action, params):
469         # serialise first
470         if action in ('set', 'create'):
471             params = self.serialise(classname, params)
473         # create the journal entry
474         entry = (nodeid, date.Date().get_tuple(), self.journaltag, action,
475             params)
477         if __debug__:
478             print >>hyperdb.DEBUG, '_doSaveJournal', entry
480         # get the database handle
481         db_name = 'journals.%s'%classname
482         if self.databases.has_key(db_name):
483             db = self.databases[db_name]
484         else:
485             db = self.databases[db_name] = self._opendb(db_name, 'c')
487         # now insert the journal entry
488         if db.has_key(nodeid):
489             # append to existing
490             s = db[nodeid]
491             l = marshal.loads(s)
492             l.append(entry)
493         else:
494             l = [entry]
496         db[nodeid] = marshal.dumps(l)
498     def rollback(self):
499         ''' Reverse all actions from the current transaction.
500         '''
501         if __debug__:
502             print >>hyperdb.DEBUG, 'rollback', (self, )
503         for method, args in self.transactions:
504             # delete temporary files
505             if method == self._doStoreFile:
506                 self._rollbackStoreFile(*args)
507         self.cache = {}
508         self.dirtynodes = {}
509         self.newnodes = {}
510         self.transactions = []
513 #$Log: not supported by cvs2svn $
514 #Revision 1.42  2002/07/10 06:21:38  richard
515 #Be extra safe
517 #Revision 1.41  2002/07/10 00:21:45  richard
518 #explicit database closing
520 #Revision 1.40  2002/07/09 04:19:09  richard
521 #Added reindex command to roundup-admin.
522 #Fixed reindex on first access.
523 #Also fixed reindexing of entries that change.
525 #Revision 1.39  2002/07/09 03:02:52  richard
526 #More indexer work:
527 #- all String properties may now be indexed too. Currently there's a bit of
528 #  "issue" specific code in the actual searching which needs to be
529 #  addressed. In a nutshell:
530 #  + pass 'indexme="yes"' as a String() property initialisation arg, eg:
531 #        file = FileClass(db, "file", name=String(), type=String(),
532 #            comment=String(indexme="yes"))
533 #  + the comment will then be indexed and be searchable, with the results
534 #    related back to the issue that the file is linked to
535 #- as a result of this work, the FileClass has a default MIME type that may
536 #  be overridden in a subclass, or by the use of a "type" property as is
537 #  done in the default templates.
538 #- the regeneration of the indexes (if necessary) is done once the schema is
539 #  set up in the dbinit.
541 #Revision 1.38  2002/07/08 06:58:15  richard
542 #cleaned up the indexer code:
543 # - it splits more words out (much simpler, faster splitter)
544 # - removed code we'll never use (roundup.roundup_indexer has the full
545 #   implementation, and replaces roundup.indexer)
546 # - only index text/plain and rfc822/message (ideas for other text formats to
547 #   index are welcome)
548 # - added simple unit test for indexer. Needs more tests for regression.
550 #Revision 1.37  2002/06/20 23:52:35  richard
551 #More informative error message
553 #Revision 1.36  2002/06/19 03:07:19  richard
554 #Moved the file storage commit into blobfiles where it belongs.
556 #Revision 1.35  2002/05/25 07:16:24  rochecompaan
557 #Merged search_indexing-branch with HEAD
559 #Revision 1.34  2002/05/15 06:21:21  richard
560 # . node caching now works, and gives a small boost in performance
562 #As a part of this, I cleaned up the DEBUG output and implemented TRACE
563 #output (HYPERDBTRACE='file to trace to') with checkpoints at the start of
564 #CGI requests. Run roundup with python -O to skip all the DEBUG/TRACE stuff
565 #(using if __debug__ which is compiled out with -O)
567 #Revision 1.33  2002/04/24 10:38:26  rochecompaan
568 #All database files are now created group readable and writable.
570 #Revision 1.32  2002/04/15 23:25:15  richard
571 #. node ids are now generated from a lockable store - no more race conditions
573 #We're using the portalocker code by Jonathan Feinberg that was contributed
574 #to the ASPN Python cookbook. This gives us locking across Unix and Windows.
576 #Revision 1.31  2002/04/03 05:54:31  richard
577 #Fixed serialisation problem by moving the serialisation step out of the
578 #hyperdb.Class (get, set) into the hyperdb.Database.
580 #Also fixed htmltemplate after the showid changes I made yesterday.
582 #Unit tests for all of the above written.
584 #Revision 1.30.2.1  2002/04/03 11:55:57  rochecompaan
585 # . Added feature #526730 - search for messages capability
587 #Revision 1.30  2002/02/27 03:40:59  richard
588 #Ran it through pychecker, made fixes
590 #Revision 1.29  2002/02/25 14:34:31  grubert
591 # . use blobfiles in back_anydbm which is used in back_bsddb.
592 #   change test_db as dirlist does not work for subdirectories.
593 #   ATTENTION: blobfiles now creates subdirectories for files.
595 #Revision 1.28  2002/02/16 09:14:17  richard
596 # . #514854 ] History: "User" is always ticket creator
598 #Revision 1.27  2002/01/22 07:21:13  richard
599 #. fixed back_bsddb so it passed the journal tests
601 #... it didn't seem happy using the back_anydbm _open method, which is odd.
602 #Yet another occurrance of whichdb not being able to recognise older bsddb
603 #databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the
604 #process.
606 #Revision 1.26  2002/01/22 05:18:38  rochecompaan
607 #last_set_entry was referenced before assignment
609 #Revision 1.25  2002/01/22 05:06:08  rochecompaan
610 #We need to keep the last 'set' entry in the journal to preserve
611 #information on 'activity' for nodes.
613 #Revision 1.24  2002/01/21 16:33:20  rochecompaan
614 #You can now use the roundup-admin tool to pack the database
616 #Revision 1.23  2002/01/18 04:32:04  richard
617 #Rollback was breaking because a message hadn't actually been written to the file. Needs
618 #more investigation.
620 #Revision 1.22  2002/01/14 02:20:15  richard
621 # . changed all config accesses so they access either the instance or the
622 #   config attriubute on the db. This means that all config is obtained from
623 #   instance_config instead of the mish-mash of classes. This will make
624 #   switching to a ConfigParser setup easier too, I hope.
626 #At a minimum, this makes migration a _little_ easier (a lot easier in the
627 #0.5.0 switch, I hope!)
629 #Revision 1.21  2002/01/02 02:31:38  richard
630 #Sorry for the huge checkin message - I was only intending to implement #496356
631 #but I found a number of places where things had been broken by transactions:
632 # . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
633 #   for _all_ roundup-generated smtp messages to be sent to.
634 # . the transaction cache had broken the roundupdb.Class set() reactors
635 # . newly-created author users in the mailgw weren't being committed to the db
637 #Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
638 #on when I found that stuff :):
639 # . #496356 ] Use threading in messages
640 # . detectors were being registered multiple times
641 # . added tests for mailgw
642 # . much better attaching of erroneous messages in the mail gateway
644 #Revision 1.20  2001/12/18 15:30:34  rochecompaan
645 #Fixed bugs:
646 # .  Fixed file creation and retrieval in same transaction in anydbm
647 #    backend
648 # .  Cgi interface now renders new issue after issue creation
649 # .  Could not set issue status to resolved through cgi interface
650 # .  Mail gateway was changing status back to 'chatting' if status was
651 #    omitted as an argument
653 #Revision 1.19  2001/12/17 03:52:48  richard
654 #Implemented file store rollback. As a bonus, the hyperdb is now capable of
655 #storing more than one file per node - if a property name is supplied,
656 #the file is called designator.property.
657 #I decided not to migrate the existing files stored over to the new naming
658 #scheme - the FileClass just doesn't specify the property name.
660 #Revision 1.18  2001/12/16 10:53:38  richard
661 #take a copy of the node dict so that the subsequent set
662 #operation doesn't modify the oldvalues structure
664 #Revision 1.17  2001/12/14 23:42:57  richard
665 #yuck, a gdbm instance tests false :(
666 #I've left the debugging code in - it should be removed one day if we're ever
667 #_really_ anal about performace :)
669 #Revision 1.16  2001/12/12 03:23:14  richard
670 #Cor blimey this anydbm/whichdb stuff is yecchy. Turns out that whichdb
671 #incorrectly identifies a dbm file as a dbhash file on my system. This has
672 #been submitted to the python bug tracker as issue #491888:
673 #https://sourceforge.net/tracker/index.php?func=detail&aid=491888&group_id=5470&atid=105470
675 #Revision 1.15  2001/12/12 02:30:51  richard
676 #I fixed the problems with people whose anydbm was using the dbm module at the
677 #backend. It turns out the dbm module modifies the file name to append ".db"
678 #and my check to determine if we're opening an existing or new db just
679 #tested os.path.exists() on the filename. Well, no longer! We now perform a
680 #much better check _and_ cope with the anydbm implementation module changing
681 #too!
682 #I also fixed the backends __init__ so only ImportError is squashed.
684 #Revision 1.14  2001/12/10 22:20:01  richard
685 #Enabled transaction support in the bsddb backend. It uses the anydbm code
686 #where possible, only replacing methods where the db is opened (it uses the
687 #btree opener specifically.)
688 #Also cleaned up some change note generation.
689 #Made the backends package work with pydoc too.
691 #Revision 1.13  2001/12/02 05:06:16  richard
692 #. We now use weakrefs in the Classes to keep the database reference, so
693 #  the close() method on the database is no longer needed.
694 #  I bumped the minimum python requirement up to 2.1 accordingly.
695 #. #487480 ] roundup-server
696 #. #487476 ] INSTALL.txt
698 #I also cleaned up the change message / post-edit stuff in the cgi client.
699 #There's now a clearly marked "TODO: append the change note" where I believe
700 #the change note should be added there. The "changes" list will obviously
701 #have to be modified to be a dict of the changes, or somesuch.
703 #More testing needed.
705 #Revision 1.12  2001/12/01 07:17:50  richard
706 #. We now have basic transaction support! Information is only written to
707 #  the database when the commit() method is called. Only the anydbm
708 #  backend is modified in this way - neither of the bsddb backends have been.
709 #  The mail, admin and cgi interfaces all use commit (except the admin tool
710 #  doesn't have a commit command, so interactive users can't commit...)
711 #. Fixed login/registration forwarding the user to the right page (or not,
712 #  on a failure)
714 #Revision 1.11  2001/11/21 02:34:18  richard
715 #Added a target version field to the extended issue schema
717 #Revision 1.10  2001/10/09 23:58:10  richard
718 #Moved the data stringification up into the hyperdb.Class class' get, set
719 #and create methods. This means that the data is also stringified for the
720 #journal call, and removes duplication of code from the backends. The
721 #backend code now only sees strings.
723 #Revision 1.9  2001/10/09 07:25:59  richard
724 #Added the Password property type. See "pydoc roundup.password" for
725 #implementation details. Have updated some of the documentation too.
727 #Revision 1.8  2001/09/29 13:27:00  richard
728 #CGI interfaces now spit up a top-level index of all the instances they can
729 #serve.
731 #Revision 1.7  2001/08/12 06:32:36  richard
732 #using isinstance(blah, Foo) now instead of isFooType
734 #Revision 1.6  2001/08/07 00:24:42  richard
735 #stupid typo
737 #Revision 1.5  2001/08/07 00:15:51  richard
738 #Added the copyright/license notice to (nearly) all files at request of
739 #Bizar Software.
741 #Revision 1.4  2001/07/30 01:41:36  richard
742 #Makes schema changes mucho easier.
744 #Revision 1.3  2001/07/25 01:23:07  richard
745 #Added the Roundup spec to the new documentation directory.
747 #Revision 1.2  2001/07/23 08:20:44  richard
748 #Moved over to using marshal in the bsddb and anydbm backends.
749 #roundup-admin now has a "freshen" command that'll load/save all nodes (not
750 # retired - mod hyperdb.Class.list() so it lists retired nodes)