156857e1bd9af090e196b1b9d220bf4d68debf09
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 = []
249 #
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)
259 #
260 #Revision 1.11 2001/11/21 02:34:18 richard
261 #Added a target version field to the extended issue schema
262 #
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.
268 #
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.
272 #
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.
276 #
277 #Revision 1.7 2001/08/12 06:32:36 richard
278 #using isinstance(blah, Foo) now instead of isFooType
279 #
280 #Revision 1.6 2001/08/07 00:24:42 richard
281 #stupid typo
282 #
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.
286 #
287 #Revision 1.4 2001/07/30 01:41:36 richard
288 #Makes schema changes mucho easier.
289 #
290 #Revision 1.3 2001/07/25 01:23:07 richard
291 #Added the Roundup spec to the new documentation directory.
292 #
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)
297 #
298 #