317bc3a3c9fb3153474032a41b8a15d944b26df3
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.15 2001-12-12 02:30:51 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, password
29 #
30 # Now the database
31 #
32 class Database(hyperdb.Database):
33 """A database for storing records containing flexible data types.
35 Transaction stuff TODO:
36 . check the timestamp of the class file and nuke the cache if it's
37 modified. Do some sort of conflict checking on the dirty stuff.
38 . perhaps detect write collisions (related to above)?
40 """
41 def __init__(self, storagelocator, journaltag=None):
42 """Open a hyperdatabase given a specifier to some storage.
44 The meaning of 'storagelocator' depends on the particular
45 implementation of the hyperdatabase. It could be a file name,
46 a directory path, a socket descriptor for a connection to a
47 database over the network, etc.
49 The 'journaltag' is a token that will be attached to the journal
50 entries for any edits done on the database. If 'journaltag' is
51 None, the database is opened in read-only mode: the Class.create(),
52 Class.set(), and Class.retire() methods are disabled.
53 """
54 self.dir, self.journaltag = storagelocator, journaltag
55 self.classes = {}
56 self.cache = {} # cache of nodes loaded or created
57 self.dirtynodes = {} # keep track of the dirty nodes by class
58 self.newnodes = {} # keep track of the new nodes by class
59 self.transactions = []
61 #
62 # Classes
63 #
64 def __getattr__(self, classname):
65 """A convenient way of calling self.getclass(classname)."""
66 return self.classes[classname]
68 def addclass(self, cl):
69 cn = cl.classname
70 if self.classes.has_key(cn):
71 raise ValueError, cn
72 self.classes[cn] = cl
74 def getclasses(self):
75 """Return a list of the names of all existing classes."""
76 l = self.classes.keys()
77 l.sort()
78 return l
80 def getclass(self, classname):
81 """Get the Class object representing a particular class.
83 If 'classname' is not a valid class name, a KeyError is raised.
84 """
85 return self.classes[classname]
87 #
88 # Class DBs
89 #
90 def clear(self):
91 '''Delete all database contents
92 '''
93 for cn in self.classes.keys():
94 for type in 'nodes', 'journals':
95 path = os.path.join(self.dir, 'journals.%s'%cn)
96 if os.path.exists(path):
97 os.remove(path)
98 elif os.path.exists(path+'.db'): # dbm appends .db
99 os.remove(path+'.db')
101 def getclassdb(self, classname, mode='r'):
102 ''' grab a connection to the class db that will be used for
103 multiple actions
104 '''
105 # determine which DB wrote the class file
106 path = os.path.join(os.getcwd(), self.dir, 'nodes.%s'%classname)
107 db_type = whichdb.whichdb(path)
108 if not db_type:
109 # dbm appends ".db"
110 db_type = whichdb.whichdb(path+'.db')
111 db_type = whichdb.whichdb(path)
113 # if we can't identify it and it exists...
114 if not db_type and os.path.exists(path) or os.path.exists(path+'.db'):
115 raise hyperdb.DatabaseError, \
116 "Couldn't identify the database type"
118 # new database? let anydbm pick the best dbm
119 if not db_type:
120 return anydbm.open(path, 'n')
122 # open the database with the correct module
123 try:
124 dbm = __import__(db_type)
125 except:
126 raise hyperdb.DatabaseError, \
127 "Couldn't open database - the required module '%s'"\
128 "is not available"%db_type
129 return dbm.open(path, mode)
131 #
132 # Nodes
133 #
134 def addnode(self, classname, nodeid, node):
135 ''' add the specified node to its class's db
136 '''
137 self.newnodes.setdefault(classname, {})[nodeid] = 1
138 self.cache.setdefault(classname, {})[nodeid] = node
139 self.savenode(classname, nodeid, node)
141 def setnode(self, classname, nodeid, node):
142 ''' change the specified node
143 '''
144 self.dirtynodes.setdefault(classname, {})[nodeid] = 1
145 # can't set without having already loaded the node
146 self.cache[classname][nodeid] = node
147 self.savenode(classname, nodeid, node)
149 def savenode(self, classname, nodeid, node):
150 ''' perform the saving of data specified by the set/addnode
151 '''
152 self.transactions.append((self._doSaveNode, (classname, nodeid, node)))
154 def getnode(self, classname, nodeid, cldb=None):
155 ''' add the specified node to its class's db
156 '''
157 # try the cache
158 cache = self.cache.setdefault(classname, {})
159 if cache.has_key(nodeid):
160 return cache[nodeid]
162 # get from the database and save in the cache
163 db = cldb or self.getclassdb(classname)
164 if not db.has_key(nodeid):
165 raise IndexError, nodeid
166 res = marshal.loads(db[nodeid])
167 cache[nodeid] = res
168 return res
170 def hasnode(self, classname, nodeid, cldb=None):
171 ''' add the specified node to its class's db
172 '''
173 # try the cache
174 cache = self.cache.setdefault(classname, {})
175 if cache.has_key(nodeid):
176 return 1
178 # not in the cache - check the database
179 db = cldb or self.getclassdb(classname)
180 res = db.has_key(nodeid)
181 return res
183 def countnodes(self, classname, cldb=None):
184 # include the new nodes not saved to the DB yet
185 count = len(self.newnodes.get(classname, {}))
187 # and count those in the DB
188 db = cldb or self.getclassdb(classname)
189 count = count + len(db.keys())
190 return count
192 def getnodeids(self, classname, cldb=None):
193 # start off with the new nodes
194 res = self.newnodes.get(classname, {}).keys()
196 db = cldb or self.getclassdb(classname)
197 res = res + db.keys()
198 return res
200 #
201 # Journal
202 #
203 def addjournal(self, classname, nodeid, action, params):
204 ''' Journal the Action
205 'action' may be:
207 'create' or 'set' -- 'params' is a dictionary of property values
208 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
209 'retire' -- 'params' is None
210 '''
211 self.transactions.append((self._doSaveJournal, (classname, nodeid,
212 action, params)))
214 def getjournal(self, classname, nodeid):
215 ''' get the journal for id
216 '''
217 # attempt to open the journal - in some rare cases, the journal may
218 # not exist
219 try:
220 db = anydbm.open(os.path.join(self.dir, 'journals.%s'%classname),
221 'r')
222 except anydbm.open, error:
223 if error.args[0] != 2: raise
224 return []
225 journal = marshal.loads(db[nodeid])
226 res = []
227 for entry in journal:
228 (nodeid, date_stamp, self.journaltag, action, params) = entry
229 date_obj = date.Date(date_stamp)
230 res.append((nodeid, date_obj, self.journaltag, action, params))
231 return res
234 #
235 # Basic transaction support
236 #
237 def commit(self):
238 ''' Commit the current transactions.
239 '''
240 # lock the DB
241 for method, args in self.transactions:
242 # TODO: optimise this, duh!
243 method(*args)
244 # unlock the DB
246 # all transactions committed, back to normal
247 self.cache = {}
248 self.dirtynodes = {}
249 self.newnodes = {}
250 self.transactions = []
252 def _doSaveNode(self, classname, nodeid, node):
253 db = self.getclassdb(classname, 'c')
254 # now save the marshalled data
255 db[nodeid] = marshal.dumps(node)
256 db.close()
258 def _doSaveJournal(self, classname, nodeid, action, params):
259 entry = (nodeid, date.Date().get_tuple(), self.journaltag, action,
260 params)
261 db = anydbm.open(os.path.join(self.dir, 'journals.%s'%classname), 'c')
262 if db.has_key(nodeid):
263 s = db[nodeid]
264 l = marshal.loads(db[nodeid])
265 l.append(entry)
266 else:
267 l = [entry]
268 db[nodeid] = marshal.dumps(l)
269 db.close()
271 def rollback(self):
272 ''' Reverse all actions from the current transaction.
273 '''
274 self.cache = {}
275 self.dirtynodes = {}
276 self.newnodes = {}
277 self.transactions = []
279 #
280 #$Log: not supported by cvs2svn $
281 #Revision 1.14 2001/12/10 22:20:01 richard
282 #Enabled transaction support in the bsddb backend. It uses the anydbm code
283 #where possible, only replacing methods where the db is opened (it uses the
284 #btree opener specifically.)
285 #Also cleaned up some change note generation.
286 #Made the backends package work with pydoc too.
287 #
288 #Revision 1.13 2001/12/02 05:06:16 richard
289 #. We now use weakrefs in the Classes to keep the database reference, so
290 # the close() method on the database is no longer needed.
291 # I bumped the minimum python requirement up to 2.1 accordingly.
292 #. #487480 ] roundup-server
293 #. #487476 ] INSTALL.txt
294 #
295 #I also cleaned up the change message / post-edit stuff in the cgi client.
296 #There's now a clearly marked "TODO: append the change note" where I believe
297 #the change note should be added there. The "changes" list will obviously
298 #have to be modified to be a dict of the changes, or somesuch.
299 #
300 #More testing needed.
301 #
302 #Revision 1.12 2001/12/01 07:17:50 richard
303 #. We now have basic transaction support! Information is only written to
304 # the database when the commit() method is called. Only the anydbm
305 # backend is modified in this way - neither of the bsddb backends have been.
306 # The mail, admin and cgi interfaces all use commit (except the admin tool
307 # doesn't have a commit command, so interactive users can't commit...)
308 #. Fixed login/registration forwarding the user to the right page (or not,
309 # on a failure)
310 #
311 #Revision 1.11 2001/11/21 02:34:18 richard
312 #Added a target version field to the extended issue schema
313 #
314 #Revision 1.10 2001/10/09 23:58:10 richard
315 #Moved the data stringification up into the hyperdb.Class class' get, set
316 #and create methods. This means that the data is also stringified for the
317 #journal call, and removes duplication of code from the backends. The
318 #backend code now only sees strings.
319 #
320 #Revision 1.9 2001/10/09 07:25:59 richard
321 #Added the Password property type. See "pydoc roundup.password" for
322 #implementation details. Have updated some of the documentation too.
323 #
324 #Revision 1.8 2001/09/29 13:27:00 richard
325 #CGI interfaces now spit up a top-level index of all the instances they can
326 #serve.
327 #
328 #Revision 1.7 2001/08/12 06:32:36 richard
329 #using isinstance(blah, Foo) now instead of isFooType
330 #
331 #Revision 1.6 2001/08/07 00:24:42 richard
332 #stupid typo
333 #
334 #Revision 1.5 2001/08/07 00:15:51 richard
335 #Added the copyright/license notice to (nearly) all files at request of
336 #Bizar Software.
337 #
338 #Revision 1.4 2001/07/30 01:41:36 richard
339 #Makes schema changes mucho easier.
340 #
341 #Revision 1.3 2001/07/25 01:23:07 richard
342 #Added the Roundup spec to the new documentation directory.
343 #
344 #Revision 1.2 2001/07/23 08:20:44 richard
345 #Moved over to using marshal in the bsddb and anydbm backends.
346 #roundup-admin now has a "freshen" command that'll load/save all nodes (not
347 # retired - mod hyperdb.Class.list() so it lists retired nodes)
348 #
349 #