38305b574d7a7fc90dc876d7a866ea089a529621
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.12 2001-12-01 07:17:50 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 if not cldb: db.close()
138 cache[nodeid] = res
139 return res
141 def hasnode(self, classname, nodeid, cldb=None):
142 ''' add the specified node to its class's db
143 '''
144 # try the cache
145 cache = self.cache.setdefault(classname, {})
146 if cache.has_key(nodeid):
147 return 1
149 # not in the cache - check the database
150 db = cldb or self.getclassdb(classname)
151 res = db.has_key(nodeid)
152 if not cldb: db.close()
153 return res
155 def countnodes(self, classname, cldb=None):
156 # include the new nodes not saved to the DB yet
157 count = len(self.newnodes.get(classname, {}))
159 # and count those in the DB
160 db = cldb or self.getclassdb(classname)
161 count = count + len(db.keys())
162 if not cldb: db.close()
163 return count
165 def getnodeids(self, classname, cldb=None):
166 # start off with the new nodes
167 res = self.newnodes.get(classname, {}).keys()
169 db = cldb or self.getclassdb(classname)
170 res = res + db.keys()
171 if not cldb: db.close()
172 return res
174 #
175 # Journal
176 #
177 def addjournal(self, classname, nodeid, action, params):
178 ''' Journal the Action
179 'action' may be:
181 'create' or 'set' -- 'params' is a dictionary of property values
182 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
183 'retire' -- 'params' is None
184 '''
185 self.transactions.append((self._doSaveJournal, (classname, nodeid,
186 action, params)))
188 def getjournal(self, classname, nodeid):
189 ''' get the journal for id
190 '''
191 # attempt to open the journal - in some rare cases, the journal may
192 # not exist
193 try:
194 db = anydbm.open(os.path.join(self.dir, 'journals.%s'%classname),
195 'r')
196 except anydbm.open, error:
197 if error.args[0] != 2: raise
198 return []
199 journal = marshal.loads(db[nodeid])
200 res = []
201 for entry in journal:
202 (nodeid, date_stamp, self.journaltag, action, params) = entry
203 date_obj = date.Date(date_stamp)
204 res.append((nodeid, date_obj, self.journaltag, action, params))
205 db.close()
206 return res
208 def close(self):
209 ''' Close the Database.
211 Commit all data to the database and release circular refs so
212 the database is closed cleanly.
213 '''
214 self.classes = {}
217 #
218 # Basic transaction support
219 #
220 def commit(self):
221 ''' Commit the current transactions.
222 '''
223 # lock the DB
224 for method, args in self.transactions:
225 print method.__name__, args
226 # TODO: optimise this, duh!
227 method(*args)
228 # unlock the DB
230 # all transactions committed, back to normal
231 self.cache = {}
232 self.dirtynodes = {}
233 self.newnodes = {}
234 self.transactions = []
236 def _doSaveNode(self, classname, nodeid, node):
237 db = self.getclassdb(classname, 'c')
238 # now save the marshalled data
239 db[nodeid] = marshal.dumps(node)
240 db.close()
242 def _doSaveJournal(self, classname, nodeid, action, params):
243 entry = (nodeid, date.Date().get_tuple(), self.journaltag, action,
244 params)
245 db = anydbm.open(os.path.join(self.dir, 'journals.%s'%classname), 'c')
246 if db.has_key(nodeid):
247 s = db[nodeid]
248 l = marshal.loads(db[nodeid])
249 l.append(entry)
250 else:
251 l = [entry]
252 db[nodeid] = marshal.dumps(l)
253 db.close()
255 def rollback(self):
256 ''' Reverse all actions from the current transaction.
257 '''
258 self.cache = {}
259 self.dirtynodes = {}
260 self.newnodes = {}
261 self.transactions = []
263 #
264 #$Log: not supported by cvs2svn $
265 #Revision 1.11 2001/11/21 02:34:18 richard
266 #Added a target version field to the extended issue schema
267 #
268 #Revision 1.10 2001/10/09 23:58:10 richard
269 #Moved the data stringification up into the hyperdb.Class class' get, set
270 #and create methods. This means that the data is also stringified for the
271 #journal call, and removes duplication of code from the backends. The
272 #backend code now only sees strings.
273 #
274 #Revision 1.9 2001/10/09 07:25:59 richard
275 #Added the Password property type. See "pydoc roundup.password" for
276 #implementation details. Have updated some of the documentation too.
277 #
278 #Revision 1.8 2001/09/29 13:27:00 richard
279 #CGI interfaces now spit up a top-level index of all the instances they can
280 #serve.
281 #
282 #Revision 1.7 2001/08/12 06:32:36 richard
283 #using isinstance(blah, Foo) now instead of isFooType
284 #
285 #Revision 1.6 2001/08/07 00:24:42 richard
286 #stupid typo
287 #
288 #Revision 1.5 2001/08/07 00:15:51 richard
289 #Added the copyright/license notice to (nearly) all files at request of
290 #Bizar Software.
291 #
292 #Revision 1.4 2001/07/30 01:41:36 richard
293 #Makes schema changes mucho easier.
294 #
295 #Revision 1.3 2001/07/25 01:23:07 richard
296 #Added the Roundup spec to the new documentation directory.
297 #
298 #Revision 1.2 2001/07/23 08:20:44 richard
299 #Moved over to using marshal in the bsddb and anydbm backends.
300 #roundup-admin now has a "freshen" command that'll load/save all nodes (not
301 # retired - mod hyperdb.Class.list() so it lists retired nodes)
302 #
303 #