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