1 # $Id: back_metakit.py,v 1.44 2003-03-26 06:36:11 richard 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 value in ('-1', ['-1']):
912 value = []
913 elif type(value) is not _LISTTYPE:
914 value = [value]
915 # transform keys to ids
916 u = []
917 for item in value:
918 try:
919 item = int(item)
920 except (TypeError, ValueError):
921 item = int(self.db.getclass(prop.classname).lookup(item))
922 if item == -1:
923 item = 0
924 u.append(item)
925 mlcriteria[propname] = u
926 elif isinstance(prop, hyperdb.Link):
927 if type(value) is not _LISTTYPE:
928 value = [value]
929 # transform keys to ids
930 u = []
931 for item in value:
932 try:
933 item = int(item)
934 except (TypeError, ValueError):
935 item = int(self.db.getclass(prop.classname).lookup(item))
936 if item == -1:
937 item = 0
938 u.append(item)
939 if len(u) == 1:
940 where[propname] = u[0]
941 else:
942 orcriteria[propname] = u
943 elif isinstance(prop, hyperdb.String):
944 # simple glob searching
945 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', value)
946 v = v.replace('?', '.')
947 v = v.replace('*', '.*?')
948 regexes[propname] = re.compile(v, re.I)
949 elif propname == 'id':
950 where[propname] = int(value)
951 elif isinstance(prop, hyperdb.Boolean):
952 if type(value) is _STRINGTYPE:
953 bv = value.lower() in ('yes', 'true', 'on', '1')
954 else:
955 bv = value
956 where[propname] = bv
957 elif isinstance(prop, hyperdb.Date):
958 try:
959 # Try to filter on range of dates
960 date_rng = Range(value, date.Date, offset=timezone)
961 if date_rng.from_value:
962 t = date_rng.from_value.get_tuple()
963 where[propname] = int(calendar.timegm(t))
964 if date_rng.to_value:
965 t = date_rng.to_value.get_tuple()
966 wherehigh[propname] = int(calendar.timegm(t))
967 else:
968 wherehigh[propname] = None
969 except ValueError:
970 # If range creation fails - ignore that search parameter
971 pass
972 elif isinstance(prop, hyperdb.Interval):
973 where[propname] = str(date.Interval(value))
974 elif isinstance(prop, hyperdb.Number):
975 where[propname] = int(value)
976 else:
977 where[propname] = str(value)
978 v = self.getview()
979 #print "filter start at %s" % time.time()
980 if where:
981 where_higherbound = where.copy()
982 where_higherbound.update(wherehigh)
983 v = v.select(where, where_higherbound)
984 #print "filter where at %s" % time.time()
986 if mlcriteria:
987 # multilink - if any of the nodeids required by the
988 # filterspec aren't in this node's property, then skip it
989 def ff(row, ml=mlcriteria):
990 for propname, values in ml.items():
991 sv = getattr(row, propname)
992 if not values and sv:
993 return 0
994 for id in values:
995 if sv.find(fid=id) == -1:
996 return 0
997 return 1
998 iv = v.filter(ff)
999 v = v.remapwith(iv)
1001 #print "filter mlcrit at %s" % time.time()
1003 if orcriteria:
1004 def ff(row, crit=orcriteria):
1005 for propname, allowed in crit.items():
1006 val = getattr(row, propname)
1007 if val not in allowed:
1008 return 0
1009 return 1
1011 iv = v.filter(ff)
1012 v = v.remapwith(iv)
1014 #print "filter orcrit at %s" % time.time()
1015 if regexes:
1016 def ff(row, r=regexes):
1017 for propname, regex in r.items():
1018 val = str(getattr(row, propname))
1019 if not regex.search(val):
1020 return 0
1021 return 1
1023 iv = v.filter(ff)
1024 v = v.remapwith(iv)
1025 #print "filter regexs at %s" % time.time()
1027 if sort or group:
1028 sortspec = []
1029 rev = []
1030 for dir, propname in group, sort:
1031 if propname is None: continue
1032 isreversed = 0
1033 if dir == '-':
1034 isreversed = 1
1035 try:
1036 prop = getattr(v, propname)
1037 except AttributeError:
1038 print "MK has no property %s" % propname
1039 continue
1040 propclass = self.ruprops.get(propname, None)
1041 if propclass is None:
1042 propclass = self.privateprops.get(propname, None)
1043 if propclass is None:
1044 print "Schema has no property %s" % propname
1045 continue
1046 if isinstance(propclass, hyperdb.Link):
1047 linkclass = self.db.getclass(propclass.classname)
1048 lv = linkclass.getview()
1049 lv = lv.rename('id', propname)
1050 v = v.join(lv, prop, 1)
1051 if linkclass.getprops().has_key('order'):
1052 propname = 'order'
1053 else:
1054 propname = linkclass.labelprop()
1055 prop = getattr(v, propname)
1056 if isreversed:
1057 rev.append(prop)
1058 sortspec.append(prop)
1059 v = v.sortrev(sortspec, rev)[:] #XXX Metakit bug
1060 #print "filter sort at %s" % time.time()
1062 rslt = []
1063 for row in v:
1064 id = str(row.id)
1065 if search_matches is not None:
1066 if search_matches.has_key(id):
1067 rslt.append(id)
1068 else:
1069 rslt.append(id)
1070 return rslt
1072 def hasnode(self, nodeid):
1073 return int(nodeid) < self.maxid
1075 def labelprop(self, default_to_id=0):
1076 ''' Return the property name for a label for the given node.
1078 This method attempts to generate a consistent label for the node.
1079 It tries the following in order:
1080 1. key property
1081 2. "name" property
1082 3. "title" property
1083 4. first property from the sorted property name list
1084 '''
1085 k = self.getkey()
1086 if k:
1087 return k
1088 props = self.getprops()
1089 if props.has_key('name'):
1090 return 'name'
1091 elif props.has_key('title'):
1092 return 'title'
1093 if default_to_id:
1094 return 'id'
1095 props = props.keys()
1096 props.sort()
1097 return props[0]
1099 def stringFind(self, **requirements):
1100 """Locate a particular node by matching a set of its String
1101 properties in a caseless search.
1103 If the property is not a String property, a TypeError is raised.
1105 The return is a list of the id of all nodes that match.
1106 """
1107 for propname in requirements.keys():
1108 prop = self.properties[propname]
1109 if isinstance(not prop, hyperdb.String):
1110 raise TypeError, "'%s' not a String property"%propname
1111 requirements[propname] = requirements[propname].lower()
1112 requirements['_isdel'] = 0
1114 l = []
1115 for row in self.getview().select(requirements):
1116 l.append(str(row.id))
1117 return l
1119 def addjournal(self, nodeid, action, params):
1120 self.db.addjournal(self.classname, nodeid, action, params)
1122 def index(self, nodeid):
1123 ''' Add (or refresh) the node to search indexes '''
1124 # find all the String properties that have indexme
1125 for prop, propclass in self.getprops().items():
1126 if isinstance(propclass, hyperdb.String) and propclass.indexme:
1127 # index them under (classname, nodeid, property)
1128 self.db.indexer.add_text((self.classname, nodeid, prop),
1129 str(self.get(nodeid, prop)))
1131 def export_list(self, propnames, nodeid):
1132 ''' Export a node - generate a list of CSV-able data in the order
1133 specified by propnames for the given node.
1134 '''
1135 properties = self.getprops()
1136 l = []
1137 for prop in propnames:
1138 proptype = properties[prop]
1139 value = self.get(nodeid, prop)
1140 # "marshal" data where needed
1141 if value is None:
1142 pass
1143 elif isinstance(proptype, hyperdb.Date):
1144 value = value.get_tuple()
1145 elif isinstance(proptype, hyperdb.Interval):
1146 value = value.get_tuple()
1147 elif isinstance(proptype, hyperdb.Password):
1148 value = str(value)
1149 l.append(repr(value))
1151 # append retired flag
1152 l.append(self.is_retired(nodeid))
1154 return l
1156 def import_list(self, propnames, proplist):
1157 ''' Import a node - all information including "id" is present and
1158 should not be sanity checked. Triggers are not triggered. The
1159 journal should be initialised using the "creator" and "creation"
1160 information.
1162 Return the nodeid of the node imported.
1163 '''
1164 if self.db.journaltag is None:
1165 raise hyperdb.DatabaseError, 'Database open read-only'
1166 properties = self.getprops()
1168 d = {}
1169 view = self.getview(1)
1170 for i in range(len(propnames)):
1171 value = eval(proplist[i])
1172 if not value:
1173 continue
1175 propname = propnames[i]
1176 if propname == 'id':
1177 newid = value = int(value)
1178 elif propname == 'is retired':
1179 # is the item retired?
1180 if int(value):
1181 d['_isdel'] = 1
1182 continue
1184 prop = properties[propname]
1185 if isinstance(prop, hyperdb.Date):
1186 value = int(calendar.timegm(value))
1187 elif isinstance(prop, hyperdb.Interval):
1188 value = str(date.Interval(value))
1189 elif isinstance(prop, hyperdb.Number):
1190 value = int(value)
1191 elif isinstance(prop, hyperdb.Boolean):
1192 value = int(value)
1193 elif isinstance(prop, hyperdb.Link) and value:
1194 value = int(value)
1195 elif isinstance(prop, hyperdb.Multilink):
1196 # we handle multilinks separately
1197 continue
1198 d[propname] = value
1200 # possibly make a new node
1201 if not d.has_key('id'):
1202 d['id'] = newid = self.maxid
1203 self.maxid += 1
1205 # save off the node
1206 view.append(d)
1208 # fix up multilinks
1209 ndx = view.find(id=newid)
1210 row = view[ndx]
1211 for i in range(len(propnames)):
1212 value = eval(proplist[i])
1213 propname = propnames[i]
1214 if propname == 'is retired':
1215 continue
1216 prop = properties[propname]
1217 if not isinstance(prop, hyperdb.Multilink):
1218 continue
1219 sv = getattr(row, propname)
1220 for entry in value:
1221 sv.append(int(entry))
1223 self.db.dirty = 1
1224 creator = d.get('creator', 0)
1225 creation = d.get('creation', 0)
1226 self.db.addjournal(self.classname, str(newid), _CREATE, {}, creator,
1227 creation)
1228 return newid
1230 # --- used by Database
1231 def _commit(self):
1232 """ called post commit of the DB.
1233 interested subclasses may override """
1234 self.uncommitted = {}
1235 self.rbactions = []
1236 self.idcache = {}
1237 def _rollback(self):
1238 """ called pre rollback of the DB.
1239 interested subclasses may override """
1240 for action in self.rbactions:
1241 action()
1242 self.rbactions = []
1243 self.uncommitted = {}
1244 self.idcache = {}
1245 def _clear(self):
1246 view = self.getview(1)
1247 if len(view):
1248 view[:] = []
1249 self.db.dirty = 1
1250 iv = self.getindexview(1)
1251 if iv:
1252 iv[:] = []
1253 def rollbackaction(self, action):
1254 """ call this to register a callback called on rollback
1255 callback is removed on end of transaction """
1256 self.rbactions.append(action)
1257 # --- internal
1258 def __getview(self):
1259 ''' Find the interface for a specific Class in the hyperdb.
1261 This method checks to see whether the schema has changed and
1262 re-works the underlying metakit structure if it has.
1263 '''
1264 db = self.db._db
1265 view = db.view(self.classname)
1266 mkprops = view.structure()
1268 # if we have structure in the database, and the structure hasn't
1269 # changed
1270 if mkprops and self.db.fastopen:
1271 return view.ordered(1)
1273 # is the definition the same?
1274 for nm, rutyp in self.ruprops.items():
1275 for mkprop in mkprops:
1276 if mkprop.name == nm:
1277 break
1278 else:
1279 mkprop = None
1280 if mkprop is None:
1281 break
1282 if _typmap[rutyp.__class__] != mkprop.type:
1283 break
1284 else:
1285 return view.ordered(1)
1286 # need to create or restructure the mk view
1287 # id comes first, so MK will order it for us
1288 self.db.dirty = 1
1289 s = ["%s[id:I" % self.classname]
1290 for nm, rutyp in self.ruprops.items():
1291 mktyp = _typmap[rutyp.__class__]
1292 s.append('%s:%s' % (nm, mktyp))
1293 if mktyp == 'V':
1294 s[-1] += ('[fid:I]')
1295 s.append('_isdel:I,activity:I,creation:I,creator:I]')
1296 v = self.db._db.getas(','.join(s))
1297 self.db.commit()
1298 return v.ordered(1)
1299 def getview(self, RW=0):
1300 return self.db._db.view(self.classname).ordered(1)
1301 def getindexview(self, RW=0):
1302 return self.db._db.view("_%s" % self.classname).ordered(1)
1304 def _fetchML(sv):
1305 l = []
1306 for row in sv:
1307 if row.fid:
1308 l.append(str(row.fid))
1309 return l
1311 def _fetchPW(s):
1312 ''' Convert to a password.Password unless the password is '' which is
1313 our sentinel for "unset".
1314 '''
1315 if s == '':
1316 return None
1317 p = password.Password()
1318 p.unpack(s)
1319 return p
1321 def _fetchLink(n):
1322 ''' Return None if the link is 0 - otherwise strify it.
1323 '''
1324 return n and str(n) or None
1326 def _fetchDate(n):
1327 ''' Convert the timestamp to a date.Date instance - unless it's 0 which
1328 is our sentinel for "unset".
1329 '''
1330 if n == 0:
1331 return None
1332 return date.Date(time.gmtime(n))
1334 def _fetchInterval(n):
1335 ''' Convert to a date.Interval unless the interval is '' which is our
1336 sentinel for "unset".
1337 '''
1338 if n == '':
1339 return None
1340 return date.Interval(n)
1342 _converters = {
1343 hyperdb.Date : _fetchDate,
1344 hyperdb.Link : _fetchLink,
1345 hyperdb.Multilink : _fetchML,
1346 hyperdb.Interval : _fetchInterval,
1347 hyperdb.Password : _fetchPW,
1348 hyperdb.Boolean : lambda n: n,
1349 hyperdb.Number : lambda n: n,
1350 hyperdb.String : lambda s: s and str(s) or None,
1351 }
1353 class FileName(hyperdb.String):
1354 isfilename = 1
1356 _typmap = {
1357 FileName : 'S',
1358 hyperdb.String : 'S',
1359 hyperdb.Date : 'I',
1360 hyperdb.Link : 'I',
1361 hyperdb.Multilink : 'V',
1362 hyperdb.Interval : 'S',
1363 hyperdb.Password : 'S',
1364 hyperdb.Boolean : 'I',
1365 hyperdb.Number : 'I',
1366 }
1367 class FileClass(Class, hyperdb.FileClass):
1368 ''' like Class but with a content property
1369 '''
1370 default_mime_type = 'text/plain'
1371 def __init__(self, db, classname, **properties):
1372 properties['content'] = FileName()
1373 if not properties.has_key('type'):
1374 properties['type'] = hyperdb.String()
1375 Class.__init__(self, db, classname, **properties)
1377 def get(self, nodeid, propname, default=_marker, cache=1):
1378 x = Class.get(self, nodeid, propname, default, cache)
1379 poss_msg = 'Possibly an access right configuration problem.'
1380 if propname == 'content':
1381 if x.startswith('file:'):
1382 fnm = x[5:]
1383 try:
1384 x = open(fnm, 'rb').read()
1385 except IOError, (strerror):
1386 # XXX by catching this we donot see an error in the log.
1387 return 'ERROR reading file: %s%s\n%s\n%s'%(
1388 self.classname, nodeid, poss_msg, strerror)
1389 return x
1391 def create(self, **propvalues):
1392 self.fireAuditors('create', None, propvalues)
1393 content = propvalues['content']
1394 del propvalues['content']
1395 newid = Class.create_inner(self, **propvalues)
1396 if not content:
1397 return newid
1398 nm = bnm = '%s%s' % (self.classname, newid)
1399 sd = str(int(int(newid) / 1000))
1400 d = os.path.join(self.db.config.DATABASE, 'files', self.classname, sd)
1401 if not os.path.exists(d):
1402 os.makedirs(d)
1403 nm = os.path.join(d, nm)
1404 open(nm, 'wb').write(content)
1405 self.set(newid, content = 'file:'+nm)
1406 mimetype = propvalues.get('type', self.default_mime_type)
1407 self.db.indexer.add_text((self.classname, newid, 'content'), content,
1408 mimetype)
1409 def undo(fnm=nm, action1=os.remove, indexer=self.db.indexer):
1410 action1(fnm)
1411 self.rollbackaction(undo)
1412 return newid
1414 def index(self, nodeid):
1415 Class.index(self, nodeid)
1416 mimetype = self.get(nodeid, 'type')
1417 if not mimetype:
1418 mimetype = self.default_mime_type
1419 self.db.indexer.add_text((self.classname, nodeid, 'content'),
1420 self.get(nodeid, 'content'), mimetype)
1422 class IssueClass(Class, roundupdb.IssueClass):
1423 ''' The newly-created class automatically includes the "messages",
1424 "files", "nosy", and "superseder" properties. If the 'properties'
1425 dictionary attempts to specify any of these properties or a
1426 "creation" or "activity" property, a ValueError is raised.
1427 '''
1428 def __init__(self, db, classname, **properties):
1429 if not properties.has_key('title'):
1430 properties['title'] = hyperdb.String(indexme='yes')
1431 if not properties.has_key('messages'):
1432 properties['messages'] = hyperdb.Multilink("msg")
1433 if not properties.has_key('files'):
1434 properties['files'] = hyperdb.Multilink("file")
1435 if not properties.has_key('nosy'):
1436 # note: journalling is turned off as it really just wastes
1437 # space. this behaviour may be overridden in an instance
1438 properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
1439 if not properties.has_key('superseder'):
1440 properties['superseder'] = hyperdb.Multilink(classname)
1441 Class.__init__(self, db, classname, **properties)
1443 CURVERSION = 2
1445 class Indexer(indexer.Indexer):
1446 disallows = {'THE':1, 'THIS':1, 'ZZZ':1, 'THAT':1, 'WITH':1}
1447 def __init__(self, path, datadb):
1448 self.path = os.path.join(path, 'index.mk4')
1449 self.db = metakit.storage(self.path, 1)
1450 self.datadb = datadb
1451 self.reindex = 0
1452 v = self.db.view('version')
1453 if not v.structure():
1454 v = self.db.getas('version[vers:I]')
1455 self.db.commit()
1456 v.append(vers=CURVERSION)
1457 self.reindex = 1
1458 elif v[0].vers != CURVERSION:
1459 v[0].vers = CURVERSION
1460 self.reindex = 1
1461 if self.reindex:
1462 self.db.getas('ids[tblid:I,nodeid:I,propid:I,ignore:I]')
1463 self.db.getas('index[word:S,hits[pos:I]]')
1464 self.db.commit()
1465 self.reindex = 1
1466 self.changed = 0
1467 self.propcache = {}
1469 def force_reindex(self):
1470 v = self.db.view('ids')
1471 v[:] = []
1472 v = self.db.view('index')
1473 v[:] = []
1474 self.db.commit()
1475 self.reindex = 1
1477 def should_reindex(self):
1478 return self.reindex
1480 def _getprops(self, classname):
1481 props = self.propcache.get(classname, None)
1482 if props is None:
1483 props = self.datadb.view(classname).structure()
1484 props = [prop.name for prop in props]
1485 self.propcache[classname] = props
1486 return props
1488 def _getpropid(self, classname, propname):
1489 return self._getprops(classname).index(propname)
1491 def _getpropname(self, classname, propid):
1492 return self._getprops(classname)[propid]
1494 def add_text(self, identifier, text, mime_type='text/plain'):
1495 if mime_type != 'text/plain':
1496 return
1497 classname, nodeid, property = identifier
1498 tbls = self.datadb.view('tables')
1499 tblid = tbls.find(name=classname)
1500 if tblid < 0:
1501 raise KeyError, "unknown class %r"%classname
1502 nodeid = int(nodeid)
1503 propid = self._getpropid(classname, property)
1504 ids = self.db.view('ids')
1505 oldpos = ids.find(tblid=tblid,nodeid=nodeid,propid=propid,ignore=0)
1506 if oldpos > -1:
1507 ids[oldpos].ignore = 1
1508 self.changed = 1
1509 pos = ids.append(tblid=tblid,nodeid=nodeid,propid=propid)
1511 wordlist = re.findall(r'\b\w{2,25}\b', text.upper())
1512 words = {}
1513 for word in wordlist:
1514 if not self.disallows.has_key(word):
1515 words[word] = 1
1516 words = words.keys()
1518 index = self.db.view('index').ordered(1)
1519 for word in words:
1520 ndx = index.find(word=word)
1521 if ndx < 0:
1522 index.append(word=word)
1523 ndx = index.find(word=word)
1524 index[ndx].hits.append(pos=pos)
1525 self.changed = 1
1527 def find(self, wordlist):
1528 hits = None
1529 index = self.db.view('index').ordered(1)
1530 for word in wordlist:
1531 word = word.upper()
1532 if not 2 < len(word) < 26:
1533 continue
1534 ndx = index.find(word=word)
1535 if ndx < 0:
1536 return {}
1537 if hits is None:
1538 hits = index[ndx].hits
1539 else:
1540 hits = hits.intersect(index[ndx].hits)
1541 if len(hits) == 0:
1542 return {}
1543 if hits is None:
1544 return {}
1545 rslt = {}
1546 ids = self.db.view('ids').remapwith(hits)
1547 tbls = self.datadb.view('tables')
1548 for i in range(len(ids)):
1549 hit = ids[i]
1550 if not hit.ignore:
1551 classname = tbls[hit.tblid].name
1552 nodeid = str(hit.nodeid)
1553 property = self._getpropname(classname, hit.propid)
1554 rslt[i] = (classname, nodeid, property)
1555 return rslt
1557 def save_index(self):
1558 if self.changed:
1559 self.db.commit()
1560 self.changed = 0
1562 def rollback(self):
1563 if self.changed:
1564 self.db.rollback()
1565 self.db = metakit.storage(self.path, 1)
1566 self.changed = 0