6a90a1d9404b5c0a24c8d1cc240c04c1f6251342
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.14 2001-12-10 22:20:01 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 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 for cn in self.classes.keys():
92 db = os.path.join(self.dir, 'nodes.%s'%cn)
93 anydbm.open(db, 'n')
94 db = os.path.join(self.dir, 'journals.%s'%cn)
95 anydbm.open(db, 'n')
97 def getclassdb(self, classname, mode='r'):
98 ''' grab a connection to the class db that will be used for
99 multiple actions
100 '''
101 path = os.path.join(os.getcwd(), self.dir, 'nodes.%s'%classname)
102 if os.path.exists(path):
103 return anydbm.open(path, mode)
104 else:
105 return anydbm.open(path, 'n')
107 #
108 # Nodes
109 #
110 def addnode(self, classname, nodeid, node):
111 ''' add the specified node to its class's db
112 '''
113 self.newnodes.setdefault(classname, {})[nodeid] = 1
114 self.cache.setdefault(classname, {})[nodeid] = node
115 self.savenode(classname, nodeid, node)
117 def setnode(self, classname, nodeid, node):
118 ''' change the specified node
119 '''
120 self.dirtynodes.setdefault(classname, {})[nodeid] = 1
121 # can't set without having already loaded the node
122 self.cache[classname][nodeid] = node
123 self.savenode(classname, nodeid, node)
125 def savenode(self, classname, nodeid, node):
126 ''' perform the saving of data specified by the set/addnode
127 '''
128 self.transactions.append((self._doSaveNode, (classname, nodeid, node)))
130 def getnode(self, classname, nodeid, cldb=None):
131 ''' add the specified node to its class's db
132 '''
133 # try the cache
134 cache = self.cache.setdefault(classname, {})
135 if cache.has_key(nodeid):
136 return cache[nodeid]
138 # get from the database and save in the cache
139 db = cldb or self.getclassdb(classname)
140 if not db.has_key(nodeid):
141 raise IndexError, nodeid
142 res = marshal.loads(db[nodeid])
143 cache[nodeid] = res
144 return res
146 def hasnode(self, classname, nodeid, cldb=None):
147 ''' add the specified node to its class's db
148 '''
149 # try the cache
150 cache = self.cache.setdefault(classname, {})
151 if cache.has_key(nodeid):
152 return 1
154 # not in the cache - check the database
155 db = cldb or self.getclassdb(classname)
156 res = db.has_key(nodeid)
157 return res
159 def countnodes(self, classname, cldb=None):
160 # include the new nodes not saved to the DB yet
161 count = len(self.newnodes.get(classname, {}))
163 # and count those in the DB
164 db = cldb or self.getclassdb(classname)
165 count = count + len(db.keys())
166 return count
168 def getnodeids(self, classname, cldb=None):
169 # start off with the new nodes
170 res = self.newnodes.get(classname, {}).keys()
172 db = cldb or self.getclassdb(classname)
173 res = res + db.keys()
174 return res
176 #
177 # Journal
178 #
179 def addjournal(self, classname, nodeid, action, params):
180 ''' Journal the Action
181 'action' may be:
183 'create' or 'set' -- 'params' is a dictionary of property values
184 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
185 'retire' -- 'params' is None
186 '''
187 self.transactions.append((self._doSaveJournal, (classname, nodeid,
188 action, params)))
190 def getjournal(self, classname, nodeid):
191 ''' get the journal for id
192 '''
193 # attempt to open the journal - in some rare cases, the journal may
194 # not exist
195 try:
196 db = anydbm.open(os.path.join(self.dir, 'journals.%s'%classname),
197 'r')
198 except anydbm.open, error:
199 if error.args[0] != 2: raise
200 return []
201 journal = marshal.loads(db[nodeid])
202 res = []
203 for entry in journal:
204 (nodeid, date_stamp, self.journaltag, action, params) = entry
205 date_obj = date.Date(date_stamp)
206 res.append((nodeid, date_obj, self.journaltag, action, params))
207 return res
210 #
211 # Basic transaction support
212 #
213 def commit(self):
214 ''' Commit the current transactions.
215 '''
216 # lock the DB
217 for method, args in self.transactions:
218 # TODO: optimise this, duh!
219 method(*args)
220 # unlock the DB
222 # all transactions committed, back to normal
223 self.cache = {}
224 self.dirtynodes = {}
225 self.newnodes = {}
226 self.transactions = []
228 def _doSaveNode(self, classname, nodeid, node):
229 db = self.getclassdb(classname, 'c')
230 # now save the marshalled data
231 db[nodeid] = marshal.dumps(node)
232 db.close()
234 def _doSaveJournal(self, classname, nodeid, action, params):
235 entry = (nodeid, date.Date().get_tuple(), self.journaltag, action,
236 params)
237 db = anydbm.open(os.path.join(self.dir, 'journals.%s'%classname), 'c')
238 if db.has_key(nodeid):
239 s = db[nodeid]
240 l = marshal.loads(db[nodeid])
241 l.append(entry)
242 else:
243 l = [entry]
244 db[nodeid] = marshal.dumps(l)
245 db.close()
247 def rollback(self):
248 ''' Reverse all actions from the current transaction.
249 '''
250 self.cache = {}
251 self.dirtynodes = {}
252 self.newnodes = {}
253 self.transactions = []
255 #
256 #$Log: not supported by cvs2svn $
257 #Revision 1.13 2001/12/02 05:06:16 richard
258 #. We now use weakrefs in the Classes to keep the database reference, so
259 # the close() method on the database is no longer needed.
260 # I bumped the minimum python requirement up to 2.1 accordingly.
261 #. #487480 ] roundup-server
262 #. #487476 ] INSTALL.txt
263 #
264 #I also cleaned up the change message / post-edit stuff in the cgi client.
265 #There's now a clearly marked "TODO: append the change note" where I believe
266 #the change note should be added there. The "changes" list will obviously
267 #have to be modified to be a dict of the changes, or somesuch.
268 #
269 #More testing needed.
270 #
271 #Revision 1.12 2001/12/01 07:17:50 richard
272 #. We now have basic transaction support! Information is only written to
273 # the database when the commit() method is called. Only the anydbm
274 # backend is modified in this way - neither of the bsddb backends have been.
275 # The mail, admin and cgi interfaces all use commit (except the admin tool
276 # doesn't have a commit command, so interactive users can't commit...)
277 #. Fixed login/registration forwarding the user to the right page (or not,
278 # on a failure)
279 #
280 #Revision 1.11 2001/11/21 02:34:18 richard
281 #Added a target version field to the extended issue schema
282 #
283 #Revision 1.10 2001/10/09 23:58:10 richard
284 #Moved the data stringification up into the hyperdb.Class class' get, set
285 #and create methods. This means that the data is also stringified for the
286 #journal call, and removes duplication of code from the backends. The
287 #backend code now only sees strings.
288 #
289 #Revision 1.9 2001/10/09 07:25:59 richard
290 #Added the Password property type. See "pydoc roundup.password" for
291 #implementation details. Have updated some of the documentation too.
292 #
293 #Revision 1.8 2001/09/29 13:27:00 richard
294 #CGI interfaces now spit up a top-level index of all the instances they can
295 #serve.
296 #
297 #Revision 1.7 2001/08/12 06:32:36 richard
298 #using isinstance(blah, Foo) now instead of isFooType
299 #
300 #Revision 1.6 2001/08/07 00:24:42 richard
301 #stupid typo
302 #
303 #Revision 1.5 2001/08/07 00:15:51 richard
304 #Added the copyright/license notice to (nearly) all files at request of
305 #Bizar Software.
306 #
307 #Revision 1.4 2001/07/30 01:41:36 richard
308 #Makes schema changes mucho easier.
309 #
310 #Revision 1.3 2001/07/25 01:23:07 richard
311 #Added the Roundup spec to the new documentation directory.
312 #
313 #Revision 1.2 2001/07/23 08:20:44 richard
314 #Moved over to using marshal in the bsddb and anydbm backends.
315 #roundup-admin now has a "freshen" command that'll load/save all nodes (not
316 # retired - mod hyperdb.Class.list() so it lists retired nodes)
317 #
318 #