1
2 """
3 A simple interface to work with a database saved on the hard disk.
4
5 Author: Robin Lombaert
6
7 """
8
9 import os
10 import cPickle
11 import time
12
13
15
16 '''
17 A database class.
18
19 The class creates and manages a dictionary saved to the hard disk.
20
21 It functions as a python dictionary with the extra option of synchronizing
22 the database instance with the dictionary saved on the hard disk.
23
24 No changes will be made to the hard disk copy, unless Database.sync() is
25 called.
26
27 Note that changes made on a deeper level than the (key,value) pairs of the
28 Database (for instance in the case where value is a dict() type itself)
29 will not be automatically taken into account when calling the sync()
30 method. The key for which the value has been changed on a deeper level has
31 to be added to the Database.__changed list by calling addChangedKey(key)
32 manually.
33
34 Running the Database.sync() method will not read the database from the hard
35 disk if no changes were made or if changes were made on a deeper level
36 only. In order to get the most recent version of the Database, without
37 having made any changes, use the .read() method. Note that if changes were
38 made on a deeper level, they will be lost.
39
40 Example:
41
42 >>> import os
43 >>> from ivs.inout import database
44 >>> filename = 'mytest.db'
45 >>> db = database.Database(filename)
46 No database present at mytest.db. Creating a new one.
47 >>> db['test'] = 1
48 >>> db['test2'] = 'robin'
49 >>> db.sync()
50 >>> db2 = database.Database(filename)
51 >>> print db2['test']
52 1
53 >>> print db2['test2']
54 robin
55 >>> db2['test'] = 2
56 >>> db2.sync()
57 >>> db.sync()
58 >>> print db['test']
59 1
60 >>> db.read()
61 >>> print db['test']
62 2
63 >>> del db2['test2']
64 >>> db2.sync()
65 >>> print db['test2']
66 robin
67 >>> db.read()
68 >>> print db['test2']
69 Traceback (most recent call last):
70 File "<stdin>", line 1, in <module>
71 KeyError: 'test2'
72 >>> test_dict = dict()
73 >>> db['test'] = test_dict
74 >>> db.sync()
75 >>> db2.read()
76 >>> print db2['test']
77 {}
78 >>> db['test']['test'] = 1
79 >>> db.sync()
80 >>> db2.read()
81 >>> print db2['test']
82 {}
83 >>> db.addChangedKey('test')
84 >>> db.sync()
85 >>> db2.read()
86 >>> print db2['test']
87 {'test': 1}
88 >>> db.setdefault('test','defkey')
89 {'test': 1}
90 >>> db.setdefault('test3','defval')
91 'defval'
92 >>> db.sync()
93 >>> db2.read()
94 >>> print db2['test3']
95 defval
96 >>> os.system('rm %s'%filename)
97 0
98 '''
99
100
102
103 '''
104 Initializing a Database class.
105
106 Upon initialization, the class will read the dictionary saved at the
107 db_path given as a dictionary.
108
109 Note that cPickle is used to write and read these dictionaries.
110
111 If no database exists at db_path, a new dictionary will be created.
112
113 @param db_path: The path to the database on the hard disk.
114 @type db_path: string
115
116 '''
117
118 super(Database, self).__init__()
119 self.db_path = db_path
120 self.read()
121 self.__changed = []
122 self.__deleted = []
123
124
125
127
128 '''
129 Delete a key from the database.
130
131 This deletion is also done in the hard disk version of the database
132 when the sync() method is called.
133
134 This method can be called by using syntax:
135 del db[key]
136
137 @param key: a dict key that will be deleted from the Database in memory
138 @type key: a type valid for a dict key
139
140 '''
141
142 self.__deleted.append(key)
143 return super(Database,self).__delitem__(key)
144
145
146
147
149
150 '''
151 Set a dict key with value.
152
153 This change is only added to the database saved on the hard disk when
154 the sync() method is called.
155
156 The key is added to the Database.__changed list.
157
158 This method can be called by using syntax:
159 db[key] = value
160
161 @param key: a dict key that will be added to the Database in memory
162 @type key: a type valid for a dict key
163 @param key: value of the key to be added
164 @type value: any
165
166 '''
167
168 self.__changed.append(key)
169 return super(Database,self).__setitem__(key,value)
170
171
172
174
175 '''
176 Return key's value, if present. Otherwise add key with value default
177 and return.
178
179 Database.__changed is updated with the key if it is not present yet.
180
181 @param key: the key to be returned and/or added.
182 @type key: any valid dict() key
183 @param args: A default value added to the dict() if the key is not
184 present. If not specified, default defaults to None.
185 @type args: any type
186 @return: key's value or default
187
188 '''
189
190 if not self.has_key(key):
191 self.__changed.append(key)
192 return super(Database,self).setdefault(key,*args)
193
194
195
196 - def pop(self,key,*args):
197
198 '''
199 If database has key, remove it from the database and return it, else
200 return default.
201
202 If both default is not given and key is not in the database, a KeyError
203 is raised.
204
205 If deletion is successful, this change is only added to the database
206 saved on the hard disk when the sync() method is called.
207
208 The key is added to the Database.__deleted list, if present originally.
209
210 @param key: a dict key that will be removed from the Database in memory
211 @type key: a type valid for a dict key
212 @param args: value of the key to be returned if key not in Database
213 @type args: any
214 @return: value for key, or default
215
216 '''
217
218 if self.has_key(key):
219 self.__deleted.append(key)
220 return super(Database,self).pop(key,*args)
221
222
224
225 '''
226 Remove and return an arbitrary (key, value) pair from the database.
227
228 A KeyError is raised if the database has an empty dictionary.
229
230 If removal is successful, this change is only added to the database
231 saved on the hard disk when the sync() method is called.
232
233 The removed key is added to the Database.__deleted list.
234
235 @return: (key, value) pair from Database
236
237 '''
238
239 (key,value) = super(Database,self).popitem()
240 self.__deleted.append(key)
241 return (key,value)
242
243
244
245 - def update(self,*args,**kwargs):
246
247 '''
248
249 Update the database with new entries, as with a dictionary.
250
251 This update is not synched to the hard disk! Instead Database.__changed
252 includes the changed keys so that the next sync will save these changes
253 to the hard disk.
254
255 @param args: A dictionary type object to update the Database.
256 @type args: dict()
257 @keyword kwargs: Any extra keywords are added as keys with their values.
258 @type kwargs: any type that is allowed as a dict key type.
259
260 '''
261
262 self.__changed.extend(kwargs.keys())
263 self.__changed.extend(args[0].keys())
264 return super(Database,self).update(*args,**kwargs)
265
266
267
269
270 '''
271 Read the database from the hard disk.
272
273 Whenever called, the database in memory is updated with the version
274 saved on the hard disk.
275
276 Any changes made outside the session of this Database() instance will
277 be applied to the database in memory!
278
279 Any changes made to existing keys in current memory before calling
280 read() will be undone! Use sync() instead of read if you want to keep
281 current changes inside the session.
282
283 If no database is present at the path given to Database() upon
284 initialisation, a new Database is made by saving an empty dict() at the
285 requested location.
286
287 Reading and saving of the database is done by cPickle-ing the dict().
288
289 '''
290
291 try:
292 dbfile = open(self.db_path,'r')
293 while True:
294 try:
295 db = cPickle.load(dbfile)
296 break
297 except ValueError:
298 print 'Loading database failed: ValueError ~ insecure '+\
299 'string pickle. Waiting 10 seconds and trying again.'
300 time.sleep(10)
301 dbfile.close()
302 self.clear()
303 super(Database,self).update(db)
304 except IOError:
305 print 'No database present at %s. Creating a new one.'%self.db_path
306 self.__save()
307
308
309
311
312 '''
313
314 Update the database on the harddisk and in the memory.
315
316 The database is read anew, ie updated with the hard disk version to
317 account for any changes made by a different program. Next, the changes
318 made to the database in memory are applied, before saving the database
319 to the hard disk again.
320
321 Any items deleted from the database in memory will also be deleted from
322 the version saved on the hard disk!
323
324 The keys that are changed explicitly are all listed in self.__changed,
325 to which entries can be added manually using the addChangedKey method,
326 or automatically by calling .update(), .__setitem__() or .setdefault().
327
328 '''
329
330 if self.__changed or self.__deleted:
331 current_db = dict([(k,v)
332 for k,v in self.items()
333 if k in set(self.__changed)])
334 self.read()
335 self.__deleted = list(set(self.__deleted))
336 while self.__deleted:
337 try:
338 super(Database,self).__delitem__(self.__deleted.pop())
339 except KeyError:
340 pass
341 super(Database,self).update(current_db)
342 self.__save()
343 self.__changed = []
344
345
346
348
349 '''
350
351 Save a database.
352
353 Only called by Database() internally. Use sync() to save the Database
354 to the hard disk.
355
356 Reading and saving of the database is done by cPickle-ing the dict().
357
358 '''
359
360 dbfile = open(self.db_path,'w')
361 cPickle.dump(self,dbfile)
362 dbfile.close()
363
364
365
367
368 '''
369 Add a key to the list of changed keys in the database.
370
371 This is useful if a change was made to an entry on a deeper level,
372 meaning that the __set__() method of Database() is not called directly.
373
374 If the key is not added to this list manually, it will not make it into
375 the database on the hard disk when calling the sync() method.
376
377 @param key: the key you want to include in the next sync() call.
378 @type key: string
379
380 '''
381
382 self.__changed.append(key)
383
384
385
387
388 '''
389 Return a list of all keys that have been deleted from the database in
390 memory.
391
392 @return: list of keys
393 @rtype: list
394 '''
395
396 return self.__deleted
397
398
399
401
402 '''
403 Return a list of all keys that have been changed in the database in
404 memory.
405
406 @return: list of keys
407 @rtype: list
408 '''
409
410 return self.__changed
411
412 if __name__ == "__main__":
413 import doctest
414 doctest.testmod()
415