1 # $Id: back_metakit.py,v 1.51 2003-10-07 11:58:57 anthonybaxter Exp $
2 '''
3 Metakit backend for Roundup, originally by Gordon McMillan.
5 Notes by Richard:
7 This backend has some behaviour specific to metakit:
9 - there's no concept of an explicit "unset" in metakit, so all types
10 have some "unset" value:
12 ========= ===== ====================================================
13 Type Value Action when fetching from mk
14 ========= ===== ====================================================
15 Strings '' convert to None
16 Date 0 (seconds since 1970-01-01.00:00:00) convert to None
17 Interval '' convert to None
18 Number 0 ambiguious :( - do nothing
19 Boolean 0 ambiguious :( - do nothing
20 Link 0 convert to None
21 Multilink [] actually, mk can handle this one ;)
22 Passowrd '' convert to None
23 ========= ===== ====================================================
25 The get/set routines handle these values accordingly by converting
26 to/from None where they can. The Number/Boolean types are not able
27 to handle an "unset" at all, so they default the "unset" to 0.
29 - probably a bunch of stuff that I'm not aware of yet because I haven't
30 fully read through the source. One of these days....
31 '''
32 from roundup import hyperdb, date, password, roundupdb, security
33 import metakit
34 from sessions import Sessions, OneTimeKeys
35 import re, marshal, os, sys, weakref, time, calendar
36 from roundup import indexer
37 import locking
38 from roundup.date import Range
40 _dbs = {}
42 def Database(config, journaltag=None):
43 ''' Only have a single instance of the Database class for each instance
44 '''
45 db = _dbs.get(config.DATABASE, None)
46 if db is None or db._db is None:
47 db = _Database(config, journaltag)
48 _dbs[config.DATABASE] = db
49 else:
50 db.journaltag = journaltag
51 return db
53 class _Database(hyperdb.Database, roundupdb.Database):
54 def __init__(self, config, journaltag=None):
55 self.config = config
56 self.journaltag = journaltag
57 self.classes = {}
58 self.dirty = 0
59 self.lockfile = None
60 self._db = self.__open()
61 self.indexer = Indexer(self.config.DATABASE, self._db)
62 self.sessions = Sessions(self.config)
63 self.otks = OneTimeKeys(self.config)
64 self.security = security.Security(self)
66 os.umask(0002)
68 def post_init(self):
69 if self.indexer.should_reindex():
70 self.reindex()
72 def refresh_database(self):
73 # XXX handle refresh
74 self.reindex()
76 def reindex(self):
77 for klass in self.classes.values():
78 for nodeid in klass.list():
79 klass.index(nodeid)
80 self.indexer.save_index()
82 # --- defined in ping's spec
83 def __getattr__(self, classname):
84 if classname == 'transactions':
85 return self.dirty
86 # fall back on the classes
87 return self.getclass(classname)
88 def getclass(self, classname):
89 try:
90 return self.classes[classname]
91 except KeyError:
92 raise KeyError, 'There is no class called "%s"'%classname
93 def getclasses(self):
94 return self.classes.keys()
95 # --- end of ping's spec
97 # --- exposed methods
98 def commit(self):
99 if self.dirty:
100 self._db.commit()
101 for cl in self.classes.values():
102 cl._commit()
103 self.indexer.save_index()
104 self.dirty = 0
105 def rollback(self):
106 if self.dirty:
107 for cl in self.classes.values():
108 cl._rollback()
109 self._db.rollback()
110 self._db = None
111 self._db = metakit.storage(self.dbnm, 1)
112 self.hist = self._db.view('history')
113 self.tables = self._db.view('tables')
114 self.indexer.rollback()
115 self.indexer.datadb = self._db
116 self.dirty = 0
117 def clearCache(self):
118 for cl in self.classes.values():
119 cl._commit()
120 def clear(self):
121 for cl in self.classes.values():
122 cl._clear()
123 def hasnode(self, classname, nodeid):
124 return self.getclass(classname).hasnode(nodeid)
125 def pack(self, pack_before):
126 mindate = int(calendar.timegm(pack_before.get_tuple()))
127 i = 0
128 while i < len(self.hist):
129 if self.hist[i].date < mindate and self.hist[i].action != _CREATE:
130 self.hist.delete(i)
131 else:
132 i = i + 1
133 def addclass(self, cl):
134 self.classes[cl.classname] = cl
135 if self.tables.find(name=cl.classname) < 0:
136 self.tables.append(name=cl.classname)
137 def addjournal(self, tablenm, nodeid, action, params, creator=None,
138 creation=None):
139 tblid = self.tables.find(name=tablenm)
140 if tblid == -1:
141 tblid = self.tables.append(name=tablenm)
142 if creator is None:
143 creator = self.getuid()
144 else:
145 try:
146 creator = int(creator)
147 except TypeError:
148 creator = int(self.getclass('user').lookup(creator))
149 if creation is None:
150 creation = int(time.time())
151 elif isinstance(creation, date.Date):
152 creation = int(calendar.timegm(creation.get_tuple()))
153 # tableid:I,nodeid:I,date:I,user:I,action:I,params:B
154 self.hist.append(tableid=tblid,
155 nodeid=int(nodeid),
156 date=creation,
157 action=action,
158 user = creator,
159 params = marshal.dumps(params))
160 def getjournal(self, tablenm, nodeid):
161 rslt = []
162 tblid = self.tables.find(name=tablenm)
163 if tblid == -1:
164 return rslt
165 q = self.hist.select(tableid=tblid, nodeid=int(nodeid))
166 if len(q) == 0:
167 raise IndexError, "no history for id %s in %s" % (nodeid, tablenm)
168 i = 0
169 #userclass = self.getclass('user')
170 for row in q:
171 try:
172 params = marshal.loads(row.params)
173 except ValueError:
174 print "history couldn't unmarshal %r" % row.params
175 params = {}
176 #usernm = userclass.get(str(row.user), 'username')
177 dt = date.Date(time.gmtime(row.date))
178 #rslt.append((nodeid, dt, usernm, _actionnames[row.action], params))
179 rslt.append((nodeid, dt, str(row.user), _actionnames[row.action],
180 params))
181 return rslt
183 def destroyjournal(self, tablenm, nodeid):
184 nodeid = int(nodeid)
185 tblid = self.tables.find(name=tablenm)
186 if tblid == -1:
187 return
188 i = 0
189 hist = self.hist
190 while i < len(hist):
191 if hist[i].tableid == tblid and hist[i].nodeid == nodeid:
192 hist.delete(i)
193 else:
194 i = i + 1
195 self.dirty = 1
197 def close(self):
198 for cl in self.classes.values():
199 cl.db = None
200 self._db = None
201 if self.lockfile is not None:
202 locking.release_lock(self.lockfile)
203 if _dbs.has_key(self.config.DATABASE):
204 del _dbs[self.config.DATABASE]
205 if self.lockfile is not None:
206 self.lockfile.close()
207 self.lockfile = None
208 self.classes = {}
209 self.indexer = None
211 # --- internal
212 def __open(self):
213 ''' Open the metakit database
214 '''
215 # make the database dir if it doesn't exist
216 if not os.path.exists(self.config.DATABASE):
217 os.makedirs(self.config.DATABASE)
219 # figure the file names
220 self.dbnm = db = os.path.join(self.config.DATABASE, 'tracker.mk4')
221 lockfilenm = db[:-3]+'lck'
223 # get the database lock
224 self.lockfile = locking.acquire_lock(lockfilenm)
225 self.lockfile.write(str(os.getpid()))
226 self.lockfile.flush()
228 # see if the schema has changed since last db access
229 self.fastopen = 0
230 if os.path.exists(db):
231 dbtm = os.path.getmtime(db)
232 pkgnm = self.config.__name__.split('.')[0]
233 schemamod = sys.modules.get(pkgnm+'.dbinit', None)
234 if schemamod:
235 if os.path.exists(schemamod.__file__):
236 schematm = os.path.getmtime(schemamod.__file__)
237 if schematm < dbtm:
238 # found schema mod - it's older than the db
239 self.fastopen = 1
240 else:
241 # can't find schemamod - must be frozen
242 self.fastopen = 1
244 # open the db
245 db = metakit.storage(db, 1)
246 hist = db.view('history')
247 tables = db.view('tables')
248 if not self.fastopen:
249 # create the database if it's brand new
250 if not hist.structure():
251 hist = db.getas('history[tableid:I,nodeid:I,date:I,user:I,action:I,params:B]')
252 if not tables.structure():
253 tables = db.getas('tables[name:S]')
254 db.commit()
256 # we now have an open, initialised database
257 self.tables = tables
258 self.hist = hist
259 return db
261 def setid(self, classname, maxid):
262 ''' No-op in metakit
263 '''
264 pass
266 _STRINGTYPE = type('')
267 _LISTTYPE = type([])
268 _CREATE, _SET, _RETIRE, _LINK, _UNLINK, _RESTORE = range(6)
270 _actionnames = {
271 _CREATE : 'create',
272 _SET : 'set',
273 _RETIRE : 'retire',
274 _RESTORE : 'restore',
275 _LINK : 'link',
276 _UNLINK : 'unlink',
277 }
279 _marker = []
281 _ALLOWSETTINGPRIVATEPROPS = 0
283 class Class:
284 privateprops = None
285 def __init__(self, db, classname, **properties):
286 #self.db = weakref.proxy(db)
287 self.db = db
288 self.classname = classname
289 self.keyname = None
290 self.ruprops = properties
291 self.privateprops = { 'id' : hyperdb.String(),
292 'activity' : hyperdb.Date(),
293 'creation' : hyperdb.Date(),
294 'creator' : hyperdb.Link('user') }
296 # event -> list of callables
297 self.auditors = {'create': [], 'set': [], 'retire': [], 'restore': []}
298 self.reactors = {'create': [], 'set': [], 'retire': [], 'restore': []}
300 view = self.__getview()
301 self.maxid = 1
302 if view:
303 self.maxid = view[-1].id + 1
304 self.uncommitted = {}
305 self.rbactions = []
307 # people reach inside!!
308 self.properties = self.ruprops
309 self.db.addclass(self)
310 self.idcache = {}
312 # default is to journal changes
313 self.do_journal = 1
315 def enableJournalling(self):
316 '''Turn journalling on for this class
317 '''
318 self.do_journal = 1
320 def disableJournalling(self):
321 '''Turn journalling off for this class
322 '''
323 self.do_journal = 0
325 # --- the roundup.Class methods
326 def audit(self, event, detector):
327 l = self.auditors[event]
328 if detector not in l:
329 self.auditors[event].append(detector)
330 def fireAuditors(self, action, nodeid, newvalues):
331 for audit in self.auditors[action]:
332 audit(self.db, self, nodeid, newvalues)
333 def fireReactors(self, action, nodeid, oldvalues):
334 for react in self.reactors[action]:
335 react(self.db, self, nodeid, oldvalues)
336 def react(self, event, detector):
337 l = self.reactors[event]
338 if detector not in l:
339 self.reactors[event].append(detector)
341 # --- the hyperdb.Class methods
342 def create(self, **propvalues):
343 self.fireAuditors('create', None, propvalues)
344 newid = self.create_inner(**propvalues)
345 # self.set() (called in self.create_inner()) does reactors)
346 return newid
348 def create_inner(self, **propvalues):
349 rowdict = {}
350 rowdict['id'] = newid = self.maxid
351 self.maxid += 1
352 ndx = self.getview(1).append(rowdict)
353 propvalues['#ISNEW'] = 1
354 try:
355 self.set(str(newid), **propvalues)
356 except Exception:
357 self.maxid -= 1
358 raise
359 return str(newid)
361 def get(self, nodeid, propname, default=_marker, cache=1):
362 '''
363 'cache' exists for backwards compatibility, and is not used.
364 '''
366 view = self.getview()
367 id = int(nodeid)
368 if cache == 0:
369 oldnode = self.uncommitted.get(id, None)
370 if oldnode and oldnode.has_key(propname):
371 return oldnode[propname]
372 ndx = self.idcache.get(id, None)
373 if ndx is None:
374 ndx = view.find(id=id)
375 if ndx < 0:
376 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
377 self.idcache[id] = ndx
378 try:
379 raw = getattr(view[ndx], propname)
380 except AttributeError:
381 raise KeyError, propname
382 rutyp = self.ruprops.get(propname, None)
383 if rutyp is None:
384 rutyp = self.privateprops[propname]
385 converter = _converters.get(rutyp.__class__, None)
386 if converter:
387 raw = converter(raw)
388 return raw
390 def set(self, nodeid, **propvalues):
391 isnew = 0
392 if propvalues.has_key('#ISNEW'):
393 isnew = 1
394 del propvalues['#ISNEW']
395 if not isnew:
396 self.fireAuditors('set', nodeid, propvalues)
397 if not propvalues:
398 return propvalues
399 if propvalues.has_key('id'):
400 raise KeyError, '"id" is reserved'
401 if self.db.journaltag is None:
402 raise hyperdb.DatabaseError, 'Database open read-only'
403 view = self.getview(1)
405 # node must exist & not be retired
406 id = int(nodeid)
407 ndx = view.find(id=id)
408 if ndx < 0:
409 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
410 row = view[ndx]
411 if row._isdel:
412 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
413 oldnode = self.uncommitted.setdefault(id, {})
414 changes = {}
416 for key, value in propvalues.items():
417 # this will raise the KeyError if the property isn't valid
418 # ... we don't use getprops() here because we only care about
419 # the writeable properties.
420 if _ALLOWSETTINGPRIVATEPROPS:
421 prop = self.ruprops.get(key, None)
422 if not prop:
423 prop = self.privateprops[key]
424 else:
425 prop = self.ruprops[key]
426 converter = _converters.get(prop.__class__, lambda v: v)
427 # if the value's the same as the existing value, no sense in
428 # doing anything
429 oldvalue = converter(getattr(row, key))
430 if value == oldvalue:
431 del propvalues[key]
432 continue
434 # check to make sure we're not duplicating an existing key
435 if key == self.keyname:
436 iv = self.getindexview(1)
437 ndx = iv.find(k=value)
438 if ndx == -1:
439 iv.append(k=value, i=row.id)
440 if not isnew:
441 ndx = iv.find(k=oldvalue)
442 if ndx > -1:
443 iv.delete(ndx)
444 else:
445 raise ValueError, 'node with key "%s" exists'%value
447 # do stuff based on the prop type
448 if isinstance(prop, hyperdb.Link):
449 link_class = prop.classname
450 # must be a string or None
451 if value is not None and not isinstance(value, type('')):
452 raise ValueError, 'property "%s" link value be a string'%(
453 key)
454 # Roundup sets to "unselected" by passing None
455 if value is None:
456 value = 0
457 # if it isn't a number, it's a key
458 try:
459 int(value)
460 except ValueError:
461 try:
462 value = self.db.getclass(link_class).lookup(value)
463 except (TypeError, KeyError):
464 raise IndexError, 'new property "%s": %s not a %s'%(
465 key, value, prop.classname)
467 if (value is not None and
468 not self.db.getclass(link_class).hasnode(value)):
469 raise IndexError, '%s has no node %s'%(link_class, value)
471 setattr(row, key, int(value))
472 changes[key] = oldvalue
474 if self.do_journal and prop.do_journal:
475 # register the unlink with the old linked node
476 if oldvalue:
477 self.db.addjournal(link_class, oldvalue, _UNLINK,
478 (self.classname, str(row.id), key))
480 # register the link with the newly linked node
481 if value:
482 self.db.addjournal(link_class, value, _LINK,
483 (self.classname, str(row.id), key))
485 elif isinstance(prop, hyperdb.Multilink):
486 if value is not None and type(value) != _LISTTYPE:
487 raise TypeError, 'new property "%s" not a list of ids'%key
488 link_class = prop.classname
489 l = []
490 if value is None:
491 value = []
492 for entry in value:
493 if type(entry) != _STRINGTYPE:
494 raise ValueError, 'new property "%s" link value ' \
495 'must be a string'%key
496 # if it isn't a number, it's a key
497 try:
498 int(entry)
499 except ValueError:
500 try:
501 entry = self.db.getclass(link_class).lookup(entry)
502 except (TypeError, KeyError):
503 raise IndexError, 'new property "%s": %s not a %s'%(
504 key, entry, prop.classname)
505 l.append(entry)
506 propvalues[key] = value = l
508 # handle removals
509 rmvd = []
510 for id in oldvalue:
511 if id not in value:
512 rmvd.append(id)
513 # register the unlink with the old linked node
514 if self.do_journal and prop.do_journal:
515 self.db.addjournal(link_class, id, _UNLINK,
516 (self.classname, str(row.id), key))
518 # handle additions
519 adds = []
520 for id in value:
521 if id not in oldvalue:
522 if not self.db.getclass(link_class).hasnode(id):
523 raise IndexError, '%s has no node %s'%(
524 link_class, id)
525 adds.append(id)
526 # register the link with the newly linked node
527 if self.do_journal and prop.do_journal:
528 self.db.addjournal(link_class, id, _LINK,
529 (self.classname, str(row.id), key))
531 # perform the modifications on the actual property value
532 sv = getattr(row, key)
533 i = 0
534 while i < len(sv):
535 if str(sv[i].fid) in rmvd:
536 sv.delete(i)
537 else:
538 i += 1
539 for id in adds:
540 sv.append(fid=int(id))
542 # figure the journal entry
543 l = []
544 if adds:
545 l.append(('+', adds))
546 if rmvd:
547 l.append(('-', rmvd))
548 if l:
549 changes[key] = tuple(l)
550 #changes[key] = oldvalue
552 if not rmvd and not adds:
553 del propvalues[key]
555 elif isinstance(prop, hyperdb.String):
556 if value is not None and type(value) != _STRINGTYPE:
557 raise TypeError, 'new property "%s" not a string'%key
558 if value is None:
559 value = ''
560 setattr(row, key, value)
561 changes[key] = oldvalue
562 if hasattr(prop, 'isfilename') and prop.isfilename:
563 propvalues[key] = os.path.basename(value)
564 if prop.indexme:
565 self.db.indexer.add_text((self.classname, nodeid, key),
566 value, 'text/plain')
568 elif isinstance(prop, hyperdb.Password):
569 if value is not None and not isinstance(value, password.Password):
570 raise TypeError, 'new property "%s" not a Password'% key
571 if value is None:
572 value = ''
573 setattr(row, key, str(value))
574 changes[key] = str(oldvalue)
575 propvalues[key] = str(value)
577 elif isinstance(prop, hyperdb.Date):
578 if value is not None and not isinstance(value, date.Date):
579 raise TypeError, 'new property "%s" not a Date'% key
580 if value is None:
581 setattr(row, key, 0)
582 else:
583 setattr(row, key, int(calendar.timegm(value.get_tuple())))
584 changes[key] = str(oldvalue)
585 propvalues[key] = str(value)
587 elif isinstance(prop, hyperdb.Interval):
588 if value is not None and not isinstance(value, date.Interval):
589 raise TypeError, 'new property "%s" not an Interval'% key
590 if value is None:
591 setattr(row, key, '')
592 else:
593 # kedder: we should store interval values serialized
594 setattr(row, key, value.serialise())
595 changes[key] = str(oldvalue)
596 propvalues[key] = str(value)
598 elif isinstance(prop, hyperdb.Number):
599 if value is None:
600 value = 0
601 try:
602 v = int(value)
603 except ValueError:
604 raise TypeError, "%s (%s) is not numeric"%(key, repr(value))
605 setattr(row, key, v)
606 changes[key] = oldvalue
607 propvalues[key] = value
609 elif isinstance(prop, hyperdb.Boolean):
610 if value is None:
611 bv = 0
612 elif value not in (0,1):
613 raise TypeError, "%s (%s) is not boolean"%(key, repr(value))
614 else:
615 bv = value
616 setattr(row, key, bv)
617 changes[key] = oldvalue
618 propvalues[key] = value
620 oldnode[key] = oldvalue
622 # nothing to do?
623 if not propvalues:
624 return propvalues
625 if not propvalues.has_key('activity'):
626 row.activity = int(time.time())
627 if isnew:
628 if not row.creation:
629 row.creation = int(time.time())
630 if not row.creator:
631 row.creator = self.db.getuid()
633 self.db.dirty = 1
634 if self.do_journal:
635 if isnew:
636 self.db.addjournal(self.classname, nodeid, _CREATE, {})
637 self.fireReactors('create', nodeid, None)
638 else:
639 self.db.addjournal(self.classname, nodeid, _SET, changes)
640 self.fireReactors('set', nodeid, oldnode)
642 return propvalues
644 def retire(self, nodeid):
645 if self.db.journaltag is None:
646 raise hyperdb.DatabaseError, 'Database open read-only'
647 self.fireAuditors('retire', nodeid, None)
648 view = self.getview(1)
649 ndx = view.find(id=int(nodeid))
650 if ndx < 0:
651 raise KeyError, "nodeid %s not found" % nodeid
653 row = view[ndx]
654 oldvalues = self.uncommitted.setdefault(row.id, {})
655 oldval = oldvalues['_isdel'] = row._isdel
656 row._isdel = 1
658 if self.do_journal:
659 self.db.addjournal(self.classname, nodeid, _RETIRE, {})
660 if self.keyname:
661 iv = self.getindexview(1)
662 ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
663 if ndx > -1:
664 iv.delete(ndx)
665 self.db.dirty = 1
666 self.fireReactors('retire', nodeid, None)
668 def restore(self, nodeid):
669 '''Restpre a retired node.
671 Make node available for all operations like it was before retirement.
672 '''
673 if self.db.journaltag is None:
674 raise hyperdb.DatabaseError, 'Database open read-only'
676 # check if key property was overrided
677 key = self.getkey()
678 keyvalue = self.get(nodeid, key)
679 try:
680 id = self.lookup(keyvalue)
681 except KeyError:
682 pass
683 else:
684 raise KeyError, "Key property (%s) of retired node clashes with \
685 existing one (%s)" % (key, keyvalue)
686 # Now we can safely restore node
687 self.fireAuditors('restore', nodeid, None)
688 view = self.getview(1)
689 ndx = view.find(id=int(nodeid))
690 if ndx < 0:
691 raise KeyError, "nodeid %s not found" % nodeid
693 row = view[ndx]
694 oldvalues = self.uncommitted.setdefault(row.id, {})
695 oldval = oldvalues['_isdel'] = row._isdel
696 row._isdel = 0
698 if self.do_journal:
699 self.db.addjournal(self.classname, nodeid, _RESTORE, {})
700 if self.keyname:
701 iv = self.getindexview(1)
702 ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
703 if ndx > -1:
704 iv.delete(ndx)
705 self.db.dirty = 1
706 self.fireReactors('restore', nodeid, None)
708 def is_retired(self, nodeid):
709 view = self.getview(1)
710 # node must exist & not be retired
711 id = int(nodeid)
712 ndx = view.find(id=id)
713 if ndx < 0:
714 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
715 row = view[ndx]
716 return row._isdel
718 def history(self, nodeid):
719 if not self.do_journal:
720 raise ValueError, 'Journalling is disabled for this class'
721 return self.db.getjournal(self.classname, nodeid)
723 def setkey(self, propname):
724 if self.keyname:
725 if propname == self.keyname:
726 return
727 raise ValueError, "%s already indexed on %s"%(self.classname,
728 self.keyname)
729 prop = self.properties.get(propname, None)
730 if prop is None:
731 prop = self.privateprops.get(propname, None)
732 if prop is None:
733 raise KeyError, "no property %s" % propname
734 if not isinstance(prop, hyperdb.String):
735 raise TypeError, "%s is not a String" % propname
737 # first setkey for this run
738 self.keyname = propname
739 iv = self.db._db.view('_%s' % self.classname)
740 if self.db.fastopen and iv.structure():
741 return
743 # very first setkey ever
744 self.db.dirty = 1
745 iv = self.db._db.getas('_%s[k:S,i:I]' % self.classname)
746 iv = iv.ordered(1)
747 for row in self.getview():
748 iv.append(k=getattr(row, propname), i=row.id)
749 self.db.commit()
751 def getkey(self):
752 return self.keyname
754 def lookup(self, keyvalue):
755 if type(keyvalue) is not _STRINGTYPE:
756 raise TypeError, "%r is not a string" % keyvalue
757 iv = self.getindexview()
758 if iv:
759 ndx = iv.find(k=keyvalue)
760 if ndx > -1:
761 return str(iv[ndx].i)
762 else:
763 view = self.getview()
764 ndx = view.find({self.keyname:keyvalue, '_isdel':0})
765 if ndx > -1:
766 return str(view[ndx].id)
767 raise KeyError, keyvalue
769 def destroy(self, id):
770 view = self.getview(1)
771 ndx = view.find(id=int(id))
772 if ndx > -1:
773 if self.keyname:
774 keyvalue = getattr(view[ndx], self.keyname)
775 iv = self.getindexview(1)
776 if iv:
777 ivndx = iv.find(k=keyvalue)
778 if ivndx > -1:
779 iv.delete(ivndx)
780 view.delete(ndx)
781 self.db.destroyjournal(self.classname, id)
782 self.db.dirty = 1
784 def find(self, **propspec):
785 """Get the ids of nodes in this class which link to the given nodes.
787 'propspec' consists of keyword args propname={nodeid:1,}
788 'propname' must be the name of a property in this class, or a
789 KeyError is raised. That property must be a Link or
790 Multilink property, or a TypeError is raised.
792 Any node in this class whose propname property links to any of the
793 nodeids will be returned. Used by the full text indexing, which knows
794 that "foo" occurs in msg1, msg3 and file7; so we have hits on these
795 issues:
797 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
799 """
800 propspec = propspec.items()
801 for propname, nodeid in propspec:
802 # check the prop is OK
803 prop = self.ruprops[propname]
804 if (not isinstance(prop, hyperdb.Link) and
805 not isinstance(prop, hyperdb.Multilink)):
806 raise TypeError, "'%s' not a Link/Multilink property"%propname
808 vws = []
809 for propname, ids in propspec:
810 if type(ids) is _STRINGTYPE:
811 ids = {int(ids):1}
812 elif ids is None:
813 ids = {0:1}
814 else:
815 d = {}
816 for id in ids.keys():
817 d[int(id)] = 1
818 ids = d
819 prop = self.ruprops[propname]
820 view = self.getview()
821 if isinstance(prop, hyperdb.Multilink):
822 def ff(row, nm=propname, ids=ids):
823 sv = getattr(row, nm)
824 for sr in sv:
825 if ids.has_key(sr.fid):
826 return 1
827 return 0
828 else:
829 def ff(row, nm=propname, ids=ids):
830 return ids.has_key(getattr(row, nm))
831 ndxview = view.filter(ff)
832 vws.append(ndxview.unique())
834 # handle the empty match case
835 if not vws:
836 return []
838 ndxview = vws[0]
839 for v in vws[1:]:
840 ndxview = ndxview.union(v)
841 view = self.getview().remapwith(ndxview)
842 rslt = []
843 for row in view:
844 rslt.append(str(row.id))
845 return rslt
848 def list(self):
849 l = []
850 for row in self.getview().select(_isdel=0):
851 l.append(str(row.id))
852 return l
854 def getnodeids(self):
855 l = []
856 for row in self.getview():
857 l.append(str(row.id))
858 return l
860 def count(self):
861 return len(self.getview())
863 def getprops(self, protected=1):
864 # protected is not in ping's spec
865 allprops = self.ruprops.copy()
866 if protected and self.privateprops is not None:
867 allprops.update(self.privateprops)
868 return allprops
870 def addprop(self, **properties):
871 for key in properties.keys():
872 if self.ruprops.has_key(key):
873 raise ValueError, "%s is already a property of %s"%(key,
874 self.classname)
875 self.ruprops.update(properties)
876 # Class structure has changed
877 self.db.fastopen = 0
878 view = self.__getview()
879 self.db.commit()
880 # ---- end of ping's spec
882 def filter(self, search_matches, filterspec, sort=(None,None),
883 group=(None,None)):
884 # search_matches is None or a set (dict of {nodeid: {propname:[nodeid,...]}})
885 # filterspec is a dict {propname:value}
886 # sort and group are (dir, prop) where dir is '+', '-' or None
887 # and prop is a prop name or None
889 timezone = self.db.getUserTimezone()
891 where = {'_isdel':0}
892 wherehigh = {}
893 mlcriteria = {}
894 regexes = {}
895 orcriteria = {}
896 for propname, value in filterspec.items():
897 prop = self.ruprops.get(propname, None)
898 if prop is None:
899 prop = self.privateprops[propname]
900 if isinstance(prop, hyperdb.Multilink):
901 if value in ('-1', ['-1']):
902 value = []
903 elif type(value) is not _LISTTYPE:
904 value = [value]
905 # transform keys to ids
906 u = []
907 for item in value:
908 try:
909 item = int(item)
910 except (TypeError, ValueError):
911 item = int(self.db.getclass(prop.classname).lookup(item))
912 if item == -1:
913 item = 0
914 u.append(item)
915 mlcriteria[propname] = u
916 elif isinstance(prop, hyperdb.Link):
917 if type(value) is not _LISTTYPE:
918 value = [value]
919 # transform keys to ids
920 u = []
921 for item in value:
922 try:
923 item = int(item)
924 except (TypeError, ValueError):
925 item = int(self.db.getclass(prop.classname).lookup(item))
926 if item == -1:
927 item = 0
928 u.append(item)
929 if len(u) == 1:
930 where[propname] = u[0]
931 else:
932 orcriteria[propname] = u
933 elif isinstance(prop, hyperdb.String):
934 if type(value) is not type([]):
935 value = [value]
936 m = []
937 for v in value:
938 # simple glob searching
939 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
940 v = v.replace('?', '.')
941 v = v.replace('*', '.*?')
942 m.append(v)
943 regexes[propname] = re.compile('(%s)'%('|'.join(m)), re.I)
944 elif propname == 'id':
945 where[propname] = int(value)
946 elif isinstance(prop, hyperdb.Boolean):
947 if type(value) is _STRINGTYPE:
948 bv = value.lower() in ('yes', 'true', 'on', '1')
949 else:
950 bv = value
951 where[propname] = bv
952 elif isinstance(prop, hyperdb.Date):
953 try:
954 # Try to filter on range of dates
955 date_rng = Range(value, date.Date, offset=timezone)
956 if date_rng.from_value:
957 t = date_rng.from_value.get_tuple()
958 where[propname] = int(calendar.timegm(t))
959 else:
960 # use minimum possible value to exclude items without
961 # 'prop' property
962 where[propname] = 0
963 if date_rng.to_value:
964 t = date_rng.to_value.get_tuple()
965 wherehigh[propname] = int(calendar.timegm(t))
966 else:
967 wherehigh[propname] = None
968 except ValueError:
969 # If range creation fails - ignore that search parameter
970 pass
971 elif isinstance(prop, hyperdb.Interval):
972 try:
973 # Try to filter on range of intervals
974 date_rng = Range(value, date.Interval)
975 if date_rng.from_value:
976 #t = date_rng.from_value.get_tuple()
977 where[propname] = date_rng.from_value.serialise()
978 else:
979 # use minimum possible value to exclude items without
980 # 'prop' property
981 where[propname] = '-99999999999999'
982 if date_rng.to_value:
983 #t = date_rng.to_value.get_tuple()
984 wherehigh[propname] = date_rng.to_value.serialise()
985 else:
986 wherehigh[propname] = None
987 except ValueError:
988 # If range creation fails - ignore that search parameter
989 pass
990 elif isinstance(prop, hyperdb.Number):
991 where[propname] = int(value)
992 else:
993 where[propname] = str(value)
994 v = self.getview()
995 #print "filter start at %s" % time.time()
996 if where:
997 where_higherbound = where.copy()
998 where_higherbound.update(wherehigh)
999 v = v.select(where, where_higherbound)
1000 #print "filter where at %s" % time.time()
1002 if mlcriteria:
1003 # multilink - if any of the nodeids required by the
1004 # filterspec aren't in this node's property, then skip it
1005 def ff(row, ml=mlcriteria):
1006 for propname, values in ml.items():
1007 sv = getattr(row, propname)
1008 if not values and sv:
1009 return 0
1010 for id in values:
1011 if sv.find(fid=id) == -1:
1012 return 0
1013 return 1
1014 iv = v.filter(ff)
1015 v = v.remapwith(iv)
1017 #print "filter mlcrit at %s" % time.time()
1019 if orcriteria:
1020 def ff(row, crit=orcriteria):
1021 for propname, allowed in crit.items():
1022 val = getattr(row, propname)
1023 if val not in allowed:
1024 return 0
1025 return 1
1027 iv = v.filter(ff)
1028 v = v.remapwith(iv)
1030 #print "filter orcrit at %s" % time.time()
1031 if regexes:
1032 def ff(row, r=regexes):
1033 for propname, regex in r.items():
1034 val = str(getattr(row, propname))
1035 if not regex.search(val):
1036 return 0
1037 return 1
1039 iv = v.filter(ff)
1040 v = v.remapwith(iv)
1041 #print "filter regexs at %s" % time.time()
1043 if sort or group:
1044 sortspec = []
1045 rev = []
1046 for dir, propname in group, sort:
1047 if propname is None: continue
1048 isreversed = 0
1049 if dir == '-':
1050 isreversed = 1
1051 try:
1052 prop = getattr(v, propname)
1053 except AttributeError:
1054 print "MK has no property %s" % propname
1055 continue
1056 propclass = self.ruprops.get(propname, None)
1057 if propclass is None:
1058 propclass = self.privateprops.get(propname, None)
1059 if propclass is None:
1060 print "Schema has no property %s" % propname
1061 continue
1062 if isinstance(propclass, hyperdb.Link):
1063 linkclass = self.db.getclass(propclass.classname)
1064 lv = linkclass.getview()
1065 lv = lv.rename('id', propname)
1066 v = v.join(lv, prop, 1)
1067 if linkclass.getprops().has_key('order'):
1068 propname = 'order'
1069 else:
1070 propname = linkclass.labelprop()
1071 prop = getattr(v, propname)
1072 if isreversed:
1073 rev.append(prop)
1074 sortspec.append(prop)
1075 v = v.sortrev(sortspec, rev)[:] #XXX Metakit bug
1076 #print "filter sort at %s" % time.time()
1078 rslt = []
1079 for row in v:
1080 id = str(row.id)
1081 if search_matches is not None:
1082 if search_matches.has_key(id):
1083 rslt.append(id)
1084 else:
1085 rslt.append(id)
1086 return rslt
1088 def hasnode(self, nodeid):
1089 return int(nodeid) < self.maxid
1091 def labelprop(self, default_to_id=0):
1092 ''' Return the property name for a label for the given node.
1094 This method attempts to generate a consistent label for the node.
1095 It tries the following in order:
1096 1. key property
1097 2. "name" property
1098 3. "title" property
1099 4. first property from the sorted property name list
1100 '''
1101 k = self.getkey()
1102 if k:
1103 return k
1104 props = self.getprops()
1105 if props.has_key('name'):
1106 return 'name'
1107 elif props.has_key('title'):
1108 return 'title'
1109 if default_to_id:
1110 return 'id'
1111 props = props.keys()
1112 props.sort()
1113 return props[0]
1115 def stringFind(self, **requirements):
1116 """Locate a particular node by matching a set of its String
1117 properties in a caseless search.
1119 If the property is not a String property, a TypeError is raised.
1121 The return is a list of the id of all nodes that match.
1122 """
1123 for propname in requirements.keys():
1124 prop = self.properties[propname]
1125 if isinstance(not prop, hyperdb.String):
1126 raise TypeError, "'%s' not a String property"%propname
1127 requirements[propname] = requirements[propname].lower()
1128 requirements['_isdel'] = 0
1130 l = []
1131 for row in self.getview().select(requirements):
1132 l.append(str(row.id))
1133 return l
1135 def addjournal(self, nodeid, action, params):
1136 self.db.addjournal(self.classname, nodeid, action, params)
1138 def index(self, nodeid):
1139 ''' Add (or refresh) the node to search indexes '''
1140 # find all the String properties that have indexme
1141 for prop, propclass in self.getprops().items():
1142 if isinstance(propclass, hyperdb.String) and propclass.indexme:
1143 # index them under (classname, nodeid, property)
1144 self.db.indexer.add_text((self.classname, nodeid, prop),
1145 str(self.get(nodeid, prop)))
1147 def export_list(self, propnames, nodeid):
1148 ''' Export a node - generate a list of CSV-able data in the order
1149 specified by propnames for the given node.
1150 '''
1151 properties = self.getprops()
1152 l = []
1153 for prop in propnames:
1154 proptype = properties[prop]
1155 value = self.get(nodeid, prop)
1156 # "marshal" data where needed
1157 if value is None:
1158 pass
1159 elif isinstance(proptype, hyperdb.Date):
1160 value = value.get_tuple()
1161 elif isinstance(proptype, hyperdb.Interval):
1162 value = value.get_tuple()
1163 elif isinstance(proptype, hyperdb.Password):
1164 value = str(value)
1165 l.append(repr(value))
1167 # append retired flag
1168 l.append(self.is_retired(nodeid))
1170 return l
1172 def import_list(self, propnames, proplist):
1173 ''' Import a node - all information including "id" is present and
1174 should not be sanity checked. Triggers are not triggered. The
1175 journal should be initialised using the "creator" and "creation"
1176 information.
1178 Return the nodeid of the node imported.
1179 '''
1180 if self.db.journaltag is None:
1181 raise hyperdb.DatabaseError, 'Database open read-only'
1182 properties = self.getprops()
1184 d = {}
1185 view = self.getview(1)
1186 for i in range(len(propnames)):
1187 value = eval(proplist[i])
1188 if not value:
1189 continue
1191 propname = propnames[i]
1192 if propname == 'id':
1193 newid = value = int(value)
1194 elif propname == 'is retired':
1195 # is the item retired?
1196 if int(value):
1197 d['_isdel'] = 1
1198 continue
1199 elif value is None:
1200 d[propname] = None
1201 continue
1203 prop = properties[propname]
1204 if isinstance(prop, hyperdb.Date):
1205 value = int(calendar.timegm(value))
1206 elif isinstance(prop, hyperdb.Interval):
1207 value = date.Interval(value).serialise()
1208 elif isinstance(prop, hyperdb.Number):
1209 value = int(value)
1210 elif isinstance(prop, hyperdb.Boolean):
1211 value = int(value)
1212 elif isinstance(prop, hyperdb.Link) and value:
1213 value = int(value)
1214 elif isinstance(prop, hyperdb.Multilink):
1215 # we handle multilinks separately
1216 continue
1217 d[propname] = value
1219 # possibly make a new node
1220 if not d.has_key('id'):
1221 d['id'] = newid = self.maxid
1222 self.maxid += 1
1224 # save off the node
1225 view.append(d)
1227 # fix up multilinks
1228 ndx = view.find(id=newid)
1229 row = view[ndx]
1230 for i in range(len(propnames)):
1231 value = eval(proplist[i])
1232 propname = propnames[i]
1233 if propname == 'is retired':
1234 continue
1235 prop = properties[propname]
1236 if not isinstance(prop, hyperdb.Multilink):
1237 continue
1238 sv = getattr(row, propname)
1239 for entry in value:
1240 sv.append(int(entry))
1242 self.db.dirty = 1
1243 creator = d.get('creator', 0)
1244 creation = d.get('creation', 0)
1245 self.db.addjournal(self.classname, str(newid), _CREATE, {}, creator,
1246 creation)
1247 return newid
1249 # --- used by Database
1250 def _commit(self):
1251 """ called post commit of the DB.
1252 interested subclasses may override """
1253 self.uncommitted = {}
1254 self.rbactions = []
1255 self.idcache = {}
1256 def _rollback(self):
1257 """ called pre rollback of the DB.
1258 interested subclasses may override """
1259 for action in self.rbactions:
1260 action()
1261 self.rbactions = []
1262 self.uncommitted = {}
1263 self.idcache = {}
1264 def _clear(self):
1265 view = self.getview(1)
1266 if len(view):
1267 view[:] = []
1268 self.db.dirty = 1
1269 iv = self.getindexview(1)
1270 if iv:
1271 iv[:] = []
1272 def rollbackaction(self, action):
1273 """ call this to register a callback called on rollback
1274 callback is removed on end of transaction """
1275 self.rbactions.append(action)
1276 # --- internal
1277 def __getview(self):
1278 ''' Find the interface for a specific Class in the hyperdb.
1280 This method checks to see whether the schema has changed and
1281 re-works the underlying metakit structure if it has.
1282 '''
1283 db = self.db._db
1284 view = db.view(self.classname)
1285 mkprops = view.structure()
1287 # if we have structure in the database, and the structure hasn't
1288 # changed
1289 if mkprops and self.db.fastopen:
1290 return view.ordered(1)
1292 # is the definition the same?
1293 for nm, rutyp in self.ruprops.items():
1294 for mkprop in mkprops:
1295 if mkprop.name == nm:
1296 break
1297 else:
1298 mkprop = None
1299 if mkprop is None:
1300 break
1301 if _typmap[rutyp.__class__] != mkprop.type:
1302 break
1303 else:
1304 return view.ordered(1)
1305 # need to create or restructure the mk view
1306 # id comes first, so MK will order it for us
1307 self.db.dirty = 1
1308 s = ["%s[id:I" % self.classname]
1309 for nm, rutyp in self.ruprops.items():
1310 mktyp = _typmap[rutyp.__class__]
1311 s.append('%s:%s' % (nm, mktyp))
1312 if mktyp == 'V':
1313 s[-1] += ('[fid:I]')
1314 s.append('_isdel:I,activity:I,creation:I,creator:I]')
1315 v = self.db._db.getas(','.join(s))
1316 self.db.commit()
1317 return v.ordered(1)
1318 def getview(self, RW=0):
1319 return self.db._db.view(self.classname).ordered(1)
1320 def getindexview(self, RW=0):
1321 return self.db._db.view("_%s" % self.classname).ordered(1)
1323 def _fetchML(sv):
1324 l = []
1325 for row in sv:
1326 if row.fid:
1327 l.append(str(row.fid))
1328 return l
1330 def _fetchPW(s):
1331 ''' Convert to a password.Password unless the password is '' which is
1332 our sentinel for "unset".
1333 '''
1334 if s == '':
1335 return None
1336 p = password.Password()
1337 p.unpack(s)
1338 return p
1340 def _fetchLink(n):
1341 ''' Return None if the link is 0 - otherwise strify it.
1342 '''
1343 return n and str(n) or None
1345 def _fetchDate(n):
1346 ''' Convert the timestamp to a date.Date instance - unless it's 0 which
1347 is our sentinel for "unset".
1348 '''
1349 if n == 0:
1350 return None
1351 return date.Date(time.gmtime(n))
1353 def _fetchInterval(n):
1354 ''' Convert to a date.Interval unless the interval is '' which is our
1355 sentinel for "unset".
1356 '''
1357 if n == '':
1358 return None
1359 return date.Interval(n)
1361 _converters = {
1362 hyperdb.Date : _fetchDate,
1363 hyperdb.Link : _fetchLink,
1364 hyperdb.Multilink : _fetchML,
1365 hyperdb.Interval : _fetchInterval,
1366 hyperdb.Password : _fetchPW,
1367 hyperdb.Boolean : lambda n: n,
1368 hyperdb.Number : lambda n: n,
1369 hyperdb.String : lambda s: s and str(s) or None,
1370 }
1372 class FileName(hyperdb.String):
1373 isfilename = 1
1375 _typmap = {
1376 FileName : 'S',
1377 hyperdb.String : 'S',
1378 hyperdb.Date : 'I',
1379 hyperdb.Link : 'I',
1380 hyperdb.Multilink : 'V',
1381 hyperdb.Interval : 'S',
1382 hyperdb.Password : 'S',
1383 hyperdb.Boolean : 'I',
1384 hyperdb.Number : 'I',
1385 }
1386 class FileClass(Class, hyperdb.FileClass):
1387 ''' like Class but with a content property
1388 '''
1389 default_mime_type = 'text/plain'
1390 def __init__(self, db, classname, **properties):
1391 properties['content'] = FileName()
1392 if not properties.has_key('type'):
1393 properties['type'] = hyperdb.String()
1394 Class.__init__(self, db, classname, **properties)
1396 def get(self, nodeid, propname, default=_marker, cache=1):
1397 x = Class.get(self, nodeid, propname, default)
1398 poss_msg = 'Possibly an access right configuration problem.'
1399 if propname == 'content':
1400 if x.startswith('file:'):
1401 fnm = x[5:]
1402 try:
1403 x = open(fnm, 'rb').read()
1404 except IOError, (strerror):
1405 # XXX by catching this we donot see an error in the log.
1406 return 'ERROR reading file: %s%s\n%s\n%s'%(
1407 self.classname, nodeid, poss_msg, strerror)
1408 return x
1410 def create(self, **propvalues):
1411 self.fireAuditors('create', None, propvalues)
1412 content = propvalues['content']
1413 del propvalues['content']
1414 newid = Class.create_inner(self, **propvalues)
1415 if not content:
1416 return newid
1417 nm = bnm = '%s%s' % (self.classname, newid)
1418 sd = str(int(int(newid) / 1000))
1419 d = os.path.join(self.db.config.DATABASE, 'files', self.classname, sd)
1420 if not os.path.exists(d):
1421 os.makedirs(d)
1422 nm = os.path.join(d, nm)
1423 open(nm, 'wb').write(content)
1424 self.set(newid, content = 'file:'+nm)
1425 mimetype = propvalues.get('type', self.default_mime_type)
1426 self.db.indexer.add_text((self.classname, newid, 'content'), content,
1427 mimetype)
1428 def undo(fnm=nm, action1=os.remove, indexer=self.db.indexer):
1429 action1(fnm)
1430 self.rollbackaction(undo)
1431 return newid
1433 def index(self, nodeid):
1434 Class.index(self, nodeid)
1435 mimetype = self.get(nodeid, 'type')
1436 if not mimetype:
1437 mimetype = self.default_mime_type
1438 self.db.indexer.add_text((self.classname, nodeid, 'content'),
1439 self.get(nodeid, 'content'), mimetype)
1441 class IssueClass(Class, roundupdb.IssueClass):
1442 ''' The newly-created class automatically includes the "messages",
1443 "files", "nosy", and "superseder" properties. If the 'properties'
1444 dictionary attempts to specify any of these properties or a
1445 "creation" or "activity" property, a ValueError is raised.
1446 '''
1447 def __init__(self, db, classname, **properties):
1448 if not properties.has_key('title'):
1449 properties['title'] = hyperdb.String(indexme='yes')
1450 if not properties.has_key('messages'):
1451 properties['messages'] = hyperdb.Multilink("msg")
1452 if not properties.has_key('files'):
1453 properties['files'] = hyperdb.Multilink("file")
1454 if not properties.has_key('nosy'):
1455 # note: journalling is turned off as it really just wastes
1456 # space. this behaviour may be overridden in an instance
1457 properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
1458 if not properties.has_key('superseder'):
1459 properties['superseder'] = hyperdb.Multilink(classname)
1460 Class.__init__(self, db, classname, **properties)
1462 CURVERSION = 2
1464 class Indexer(indexer.Indexer):
1465 disallows = {'THE':1, 'THIS':1, 'ZZZ':1, 'THAT':1, 'WITH':1}
1466 def __init__(self, path, datadb):
1467 self.path = os.path.join(path, 'index.mk4')
1468 self.db = metakit.storage(self.path, 1)
1469 self.datadb = datadb
1470 self.reindex = 0
1471 v = self.db.view('version')
1472 if not v.structure():
1473 v = self.db.getas('version[vers:I]')
1474 self.db.commit()
1475 v.append(vers=CURVERSION)
1476 self.reindex = 1
1477 elif v[0].vers != CURVERSION:
1478 v[0].vers = CURVERSION
1479 self.reindex = 1
1480 if self.reindex:
1481 self.db.getas('ids[tblid:I,nodeid:I,propid:I,ignore:I]')
1482 self.db.getas('index[word:S,hits[pos:I]]')
1483 self.db.commit()
1484 self.reindex = 1
1485 self.changed = 0
1486 self.propcache = {}
1488 def force_reindex(self):
1489 v = self.db.view('ids')
1490 v[:] = []
1491 v = self.db.view('index')
1492 v[:] = []
1493 self.db.commit()
1494 self.reindex = 1
1496 def should_reindex(self):
1497 return self.reindex
1499 def _getprops(self, classname):
1500 props = self.propcache.get(classname, None)
1501 if props is None:
1502 props = self.datadb.view(classname).structure()
1503 props = [prop.name for prop in props]
1504 self.propcache[classname] = props
1505 return props
1507 def _getpropid(self, classname, propname):
1508 return self._getprops(classname).index(propname)
1510 def _getpropname(self, classname, propid):
1511 return self._getprops(classname)[propid]
1513 def add_text(self, identifier, text, mime_type='text/plain'):
1514 if mime_type != 'text/plain':
1515 return
1516 classname, nodeid, property = identifier
1517 tbls = self.datadb.view('tables')
1518 tblid = tbls.find(name=classname)
1519 if tblid < 0:
1520 raise KeyError, "unknown class %r"%classname
1521 nodeid = int(nodeid)
1522 propid = self._getpropid(classname, property)
1523 ids = self.db.view('ids')
1524 oldpos = ids.find(tblid=tblid,nodeid=nodeid,propid=propid,ignore=0)
1525 if oldpos > -1:
1526 ids[oldpos].ignore = 1
1527 self.changed = 1
1528 pos = ids.append(tblid=tblid,nodeid=nodeid,propid=propid)
1530 wordlist = re.findall(r'\b\w{2,25}\b', text.upper())
1531 words = {}
1532 for word in wordlist:
1533 if not self.disallows.has_key(word):
1534 words[word] = 1
1535 words = words.keys()
1537 index = self.db.view('index').ordered(1)
1538 for word in words:
1539 ndx = index.find(word=word)
1540 if ndx < 0:
1541 index.append(word=word)
1542 ndx = index.find(word=word)
1543 index[ndx].hits.append(pos=pos)
1544 self.changed = 1
1546 def find(self, wordlist):
1547 hits = None
1548 index = self.db.view('index').ordered(1)
1549 for word in wordlist:
1550 word = word.upper()
1551 if not 2 < len(word) < 26:
1552 continue
1553 ndx = index.find(word=word)
1554 if ndx < 0:
1555 return {}
1556 if hits is None:
1557 hits = index[ndx].hits
1558 else:
1559 hits = hits.intersect(index[ndx].hits)
1560 if len(hits) == 0:
1561 return {}
1562 if hits is None:
1563 return {}
1564 rslt = {}
1565 ids = self.db.view('ids').remapwith(hits)
1566 tbls = self.datadb.view('tables')
1567 for i in range(len(ids)):
1568 hit = ids[i]
1569 if not hit.ignore:
1570 classname = tbls[hit.tblid].name
1571 nodeid = str(hit.nodeid)
1572 property = self._getpropname(classname, hit.propid)
1573 rslt[i] = (classname, nodeid, property)
1574 return rslt
1576 def save_index(self):
1577 if self.changed:
1578 self.db.commit()
1579 self.changed = 0
1581 def rollback(self):
1582 if self.changed:
1583 self.db.rollback()
1584 self.db = metakit.storage(self.path, 1)
1585 self.changed = 0