Code

156857e1bd9af090e196b1b9d220bf4d68debf09
[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.13 2001-12-02 05:06:16 richard Exp $
20 import anydbm, os, marshal
21 from roundup import hyperdb, date, password
23 #
24 # Now the database
25 #
26 class Database(hyperdb.Database):
27     """A database for storing records containing flexible data types.
29     Transaction stuff TODO:
30         . check the timestamp of the class file and nuke the cache if it's
31           modified. Do some sort of conflict checking on the dirty stuff.
32         . perhaps detect write collisions (related to above)?
34     """
36     def __init__(self, storagelocator, journaltag=None):
37         """Open a hyperdatabase given a specifier to some storage.
39         The meaning of 'storagelocator' depends on the particular
40         implementation of the hyperdatabase.  It could be a file name,
41         a directory path, a socket descriptor for a connection to a
42         database over the network, etc.
44         The 'journaltag' is a token that will be attached to the journal
45         entries for any edits done on the database.  If 'journaltag' is
46         None, the database is opened in read-only mode: the Class.create(),
47         Class.set(), and Class.retire() methods are disabled.
48         """
49         self.dir, self.journaltag = storagelocator, journaltag
50         self.classes = {}
51         self.cache = {}         # cache of nodes loaded or created
52         self.dirtynodes = {}    # keep track of the dirty nodes by class
53         self.newnodes = {}      # keep track of the new nodes by class
54         self.transactions = []
55     #
56     # Classes
57     #
58     def __getattr__(self, classname):
59         """A convenient way of calling self.getclass(classname)."""
60         return self.classes[classname]
62     def addclass(self, cl):
63         cn = cl.classname
64         if self.classes.has_key(cn):
65             raise ValueError, cn
66         self.classes[cn] = cl
68     def getclasses(self):
69         """Return a list of the names of all existing classes."""
70         l = self.classes.keys()
71         l.sort()
72         return l
74     def getclass(self, classname):
75         """Get the Class object representing a particular class.
77         If 'classname' is not a valid class name, a KeyError is raised.
78         """
79         return self.classes[classname]
81     #
82     # Class DBs
83     #
84     def clear(self):
85         for cn in self.classes.keys():
86             db = os.path.join(self.dir, 'nodes.%s'%cn)
87             anydbm.open(db, 'n')
88             db = os.path.join(self.dir, 'journals.%s'%cn)
89             anydbm.open(db, 'n')
91     def getclassdb(self, classname, mode='r'):
92         ''' grab a connection to the class db that will be used for
93             multiple actions
94         '''
95         path = os.path.join(os.getcwd(), self.dir, 'nodes.%s'%classname)
96         if os.path.exists(path):
97             return anydbm.open(path, mode)
98         else:
99             return anydbm.open(path, 'n')
101     #
102     # Nodes
103     #
104     def addnode(self, classname, nodeid, node):
105         ''' add the specified node to its class's db
106         '''
107         self.newnodes.setdefault(classname, {})[nodeid] = 1
108         self.cache.setdefault(classname, {})[nodeid] = node
109         self.savenode(classname, nodeid, node)
111     def setnode(self, classname, nodeid, node):
112         ''' change the specified node
113         '''
114         self.dirtynodes.setdefault(classname, {})[nodeid] = 1
115         # can't set without having already loaded the node
116         self.cache[classname][nodeid] = node
117         self.savenode(classname, nodeid, node)
119     def savenode(self, classname, nodeid, node):
120         ''' perform the saving of data specified by the set/addnode
121         '''
122         self.transactions.append((self._doSaveNode, (classname, nodeid, node)))
124     def getnode(self, classname, nodeid, cldb=None):
125         ''' add the specified node to its class's db
126         '''
127         # try the cache
128         cache = self.cache.setdefault(classname, {})
129         if cache.has_key(nodeid):
130             return cache[nodeid]
132         # get from the database and save in the cache
133         db = cldb or self.getclassdb(classname)
134         if not db.has_key(nodeid):
135             raise IndexError, nodeid
136         res = marshal.loads(db[nodeid])
137         cache[nodeid] = res
138         return res
140     def hasnode(self, classname, nodeid, cldb=None):
141         ''' add the specified node to its class's db
142         '''
143         # try the cache
144         cache = self.cache.setdefault(classname, {})
145         if cache.has_key(nodeid):
146             return 1
148         # not in the cache - check the database
149         db = cldb or self.getclassdb(classname)
150         res = db.has_key(nodeid)
151         return res
153     def countnodes(self, classname, cldb=None):
154         # include the new nodes not saved to the DB yet
155         count = len(self.newnodes.get(classname, {}))
157         # and count those in the DB
158         db = cldb or self.getclassdb(classname)
159         count = count + len(db.keys())
160         return count
162     def getnodeids(self, classname, cldb=None):
163         # start off with the new nodes
164         res = self.newnodes.get(classname, {}).keys()
166         db = cldb or self.getclassdb(classname)
167         res = res + db.keys()
168         return res
170     #
171     # Journal
172     #
173     def addjournal(self, classname, nodeid, action, params):
174         ''' Journal the Action
175         'action' may be:
177             'create' or 'set' -- 'params' is a dictionary of property values
178             'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
179             'retire' -- 'params' is None
180         '''
181         self.transactions.append((self._doSaveJournal, (classname, nodeid,
182             action, params)))
184     def getjournal(self, classname, nodeid):
185         ''' get the journal for id
186         '''
187         # attempt to open the journal - in some rare cases, the journal may
188         # not exist
189         try:
190             db = anydbm.open(os.path.join(self.dir, 'journals.%s'%classname),
191                 'r')
192         except anydbm.open, error:
193             if error.args[0] != 2: raise
194             return []
195         journal = marshal.loads(db[nodeid])
196         res = []
197         for entry in journal:
198             (nodeid, date_stamp, self.journaltag, action, params) = entry
199             date_obj = date.Date(date_stamp)
200             res.append((nodeid, date_obj, self.journaltag, action, params))
201         return res
204     #
205     # Basic transaction support
206     #
207     def commit(self):
208         ''' Commit the current transactions.
209         '''
210         # lock the DB
211         for method, args in self.transactions:
212             # TODO: optimise this, duh!
213             method(*args)
214         # unlock the DB
216         # all transactions committed, back to normal
217         self.cache = {}
218         self.dirtynodes = {}
219         self.newnodes = {}
220         self.transactions = []
222     def _doSaveNode(self, classname, nodeid, node):
223         db = self.getclassdb(classname, 'c')
224         # now save the marshalled data
225         db[nodeid] = marshal.dumps(node)
226         db.close()
228     def _doSaveJournal(self, classname, nodeid, action, params):
229         entry = (nodeid, date.Date().get_tuple(), self.journaltag, action,
230             params)
231         db = anydbm.open(os.path.join(self.dir, 'journals.%s'%classname), 'c')
232         if db.has_key(nodeid):
233             s = db[nodeid]
234             l = marshal.loads(db[nodeid])
235             l.append(entry)
236         else:
237             l = [entry]
238         db[nodeid] = marshal.dumps(l)
239         db.close()
241     def rollback(self):
242         ''' Reverse all actions from the current transaction.
243         '''
244         self.cache = {}
245         self.dirtynodes = {}
246         self.newnodes = {}
247         self.transactions = []
250 #$Log: not supported by cvs2svn $
251 #Revision 1.12  2001/12/01 07:17:50  richard
252 #. We now have basic transaction support! Information is only written to
253 #  the database when the commit() method is called. Only the anydbm
254 #  backend is modified in this way - neither of the bsddb backends have been.
255 #  The mail, admin and cgi interfaces all use commit (except the admin tool
256 #  doesn't have a commit command, so interactive users can't commit...)
257 #. Fixed login/registration forwarding the user to the right page (or not,
258 #  on a failure)
260 #Revision 1.11  2001/11/21 02:34:18  richard
261 #Added a target version field to the extended issue schema
263 #Revision 1.10  2001/10/09 23:58:10  richard
264 #Moved the data stringification up into the hyperdb.Class class' get, set
265 #and create methods. This means that the data is also stringified for the
266 #journal call, and removes duplication of code from the backends. The
267 #backend code now only sees strings.
269 #Revision 1.9  2001/10/09 07:25:59  richard
270 #Added the Password property type. See "pydoc roundup.password" for
271 #implementation details. Have updated some of the documentation too.
273 #Revision 1.8  2001/09/29 13:27:00  richard
274 #CGI interfaces now spit up a top-level index of all the instances they can
275 #serve.
277 #Revision 1.7  2001/08/12 06:32:36  richard
278 #using isinstance(blah, Foo) now instead of isFooType
280 #Revision 1.6  2001/08/07 00:24:42  richard
281 #stupid typo
283 #Revision 1.5  2001/08/07 00:15:51  richard
284 #Added the copyright/license notice to (nearly) all files at request of
285 #Bizar Software.
287 #Revision 1.4  2001/07/30 01:41:36  richard
288 #Makes schema changes mucho easier.
290 #Revision 1.3  2001/07/25 01:23:07  richard
291 #Added the Roundup spec to the new documentation directory.
293 #Revision 1.2  2001/07/23 08:20:44  richard
294 #Moved over to using marshal in the bsddb and anydbm backends.
295 #roundup-admin now has a "freshen" command that'll load/save all nodes (not
296 # retired - mod hyperdb.Class.list() so it lists retired nodes)