From e5e98fdc2f5e8c2e6c0f4ab4d130b811f1a8dd30 Mon Sep 17 00:00:00 2001 From: Jeremy Fincher Date: Wed, 11 Aug 2004 05:14:15 +0000 Subject: [PATCH] Initial checkin. --- plugins/Note.py | 145 ++++++++----------- src/dbi.py | 367 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 422 insertions(+), 90 deletions(-) create mode 100644 src/dbi.py diff --git a/plugins/Note.py b/plugins/Note.py index ee36417f4..d22a6496c 100644 --- a/plugins/Note.py +++ b/plugins/Note.py @@ -43,8 +43,11 @@ import sets import time import getopt import os.path +import operator from itertools import imap +import supybot.dbi as dbi +import supybot.log as log import supybot.conf as conf import supybot.utils as utils import supybot.ircdb as ircdb @@ -76,84 +79,50 @@ class Ignores(registry.SpaceSeparatedListOfStrings): conf.registerUserValue(conf.users.plugins.Note, 'ignores', Ignores([], '')) -class FlatfileNoteDB(plugins.FlatfileDB): - class Note(object): - def __init__(self, L=None, to=None, frm=None, text=None): - if L is not None: - self.frm = int(L[0]) - self.to = int(L[1]) - self.at = float(L[2]) - self.notified = bool(int(L[3])) - self.read = bool(int(L[4])) - self.public = bool(int(L[5])) - self.text = L[6] - else: - self.to = to - self.frm = frm - self.text = text - self.read = False - self.public = True - self.at = time.time() - self.notified = False - - def __str__(self): - return csv.join(map(str, [self.frm, self.to, self.at, - int(self.notified), int(self.read), - int(self.public), self.text])) - - def serialize(self, n): - return str(n) - - def deserialize(self, s): - return self.Note(csv.split(s)) +class DbiNoteDB(dbi.DB): + Mapping = 'flat' + class Record(object): + __metaclass__ = dbi.Record + __fields__ = [ + 'frm', + 'to', + 'at', + 'notified', + 'read', + 'public', + ('text', str) + ] def setRead(self, id): - n = self.getRecord(id) + n = self.get(id) n.read = True n.notified = True - self.setRecord(id, n) + self.set(id, n) def setNotified(self, id): - n = self.getRecord(id) + n = self.get(id) n.notified = True - self.setRecord(id, n) + self.set(id, n) def getUnnotifiedIds(self, to): - L = [] - for (id, note) in self.records(): - if note.to == to and not note.notified: - L.append(id) - return L + def p(note): + return not note.notified and note.to == to + return [note.id for note in self.select(p)] def getUnreadIds(self, to): - L = [] - for (id, note) in self.records(): - if note.to == to and not note.read: - L.append(id) - return L + def p(note): + return not note.read and note.to == to + return [note.id for note in self.select(p)] - def send(self, frm, to, text): - n = self.Note(frm=frm, to=to, text=text) - return self.addRecord(n) - - def get(self, id): - return self.getRecord(id) - - def remove(self, id): - n = self.getRecord(id) - assert not n.read - self.delRecord(id) - - def notes(self, p): - L = [] - for (id, note) in self.records(): - if p(note): - L.append((id, note)) - return L + def send(self, frm, to, public, text): + n = self.Record(frm=frm, to=to, text=text, + at=time.time(), public=public) + return self.add(n) def NoteDB(): - return FlatfileNoteDB(conf.supybot.directories.data.dirize('Note.db')) + # XXX This should eventually be smarter. + return DbiNoteDB(conf.supybot.directories.data.dirize('Note.db')) class Note(callbacks.Privmsg): @@ -249,7 +218,7 @@ class Note(callbacks.Privmsg): return sent = [] for toId in ids: - id = self.db.send(fromId, toId, text) + id = self.db.send(fromId, toId, public, text) name = ircdb.users.getUser(toId).name s = 'note #%s sent to %s' % (id, name) sent.append(s) @@ -362,15 +331,15 @@ class Note(callbacks.Privmsg): except KeyError: irc.errorNoUser() - def _formatNoteId(self, msg, id, frm, public, sent=False): - if public or not ircutils.isChannel(msg.args[0]): - sender = ircdb.users.getUser(frm).name + def _formatNoteId(self, msg, note, sent=False): + if note.public or not ircutils.isChannel(msg.args[0]): + sender = ircdb.users.getUser(note.frm).name if sent: - return '#%s to %s' % (id, sender) + return '#%s to %s' % (note.id, sender) else: - return '#%s from %s' % (id, sender) + return '#%s from %s' % (note.id, sender) else: - return '#%s (private)' % id + return '#%s (private)' % note.id def list(self, irc, msg, args): """[--{old,sent}] [--{from,to} ] @@ -414,16 +383,14 @@ class Note(callbacks.Privmsg): except KeyError: irc.errorNoUser() return - notesAndIds = self.db.notes(p) - notesAndIds.sort() - #notesAndIds.reverse() # Newer notes, higher ids. - if not notesAndIds: + notes = list(self.db.select(p)) + if not notes: irc.reply('You have no unread notes.') else: - L = [self._formatNoteId(msg, id, n.frm, n.public) - for (id, n) in notesAndIds] - L = self._condense(L) - irc.reply(utils.commaAndify(L)) + utils.sortBy(operator.attrgetter('id'), notes) + ids = [self._formatNoteId(msg, note) for note in notes] + ids = self._condense(ids) + irc.reply(utils.commaAndify(ids)) def _condense(self, notes): temp = {} @@ -455,14 +422,13 @@ class Note(callbacks.Privmsg): originalP = p def p(note): return originalP(note) and note.to == receiver - notesAndIds = self.db.notes(p) - notesAndIds.sort() - notesAndIds.reverse() - if not notesAndIds: + notes = list(self.db.select(p)) + if not notes: irc.error('I couldn\'t find any sent notes for your user.') else: - ids = [self._formatNoteId(msg, id, note.to, note.public,sent=True) - for (id, note) in notesAndIds] + utils.sortBy(operator.attrgetter('id'), notes) + notes.reverse() # Most recently sent first. + ids = [self._formatNoteId(msg, note, sent=True) for note in notes] ids = self._condense(ids) irc.reply(utils.commaAndify(ids)) @@ -483,14 +449,13 @@ class Note(callbacks.Privmsg): originalP = p def p(note): return originalP(note) and note.frm == sender - notesAndIds = self.db.notes(p) - notesAndIds.sort() - notesAndIds.reverse() - if not notesAndIds: + notes = list(self.db.select(p)) + if not notes: irc.reply('I couldn\'t find any matching read notes for your user.') else: - ids = [self._formatNoteId(msg, id, note.frm, note.public) - for (id, note) in notesAndIds] + utils.sortBy(operator.attrgetter('id'), notes) + notes.reverse() + ids = [self._formatNoteId(msg, note) for note in notes] ids = self._condense(ids) irc.reply(utils.commaAndify(ids)) diff --git a/src/dbi.py b/src/dbi.py new file mode 100644 index 000000000..f2acc5f27 --- /dev/null +++ b/src/dbi.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python + +### +# Copyright (c) 2002, Jeremiah Fincher +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +""" +Module for some slight database-independence for simple databases. +""" + +__revision__ = "$Id$" + +import supybot.fix as fix + +import csv +import math +import sets + +import supybot.cdb as cdb +import supybot.utils as utils + +class Error(Exception): + """General error for this module.""" + +class MappingInterface(object): + """This is a class to represent the underlying representation of a map + from integer keys to strings.""" + def __init__(self, filename, **kwargs): + """Feel free to ignore the filename.""" + raise NotImplementedError + + def get(id): + """Gets the record matching id. Raises KeyError otherwise.""" + raise NotImplementedError + + def set(id, s): + """Sets the record matching id to s.""" + raise NotImplementedError + + def add(self, s): + """Adds a new record, returning a new id for it.""" + raise NotImplementedError + + def remove(self, id): + "Returns and removes the record with the given id from the database." + raise NotImplementedError + + def __iter__(self): + "Return an iterator over (id, s) pairs. Not required to be ordered." + raise NotImplementedError + + def flush(self): + """Flushes current state to disk.""" + raise NotImplementedError + + def close(self): + """Flushes current state to disk and invalidates the Mapping.""" + raise NotImplementedError + + def vacuum(self): + "Cleans up in the database, if possible. Not required to do anything." + pass + +class FlatfileMapping(MappingInterface): + def __init__(self, filename, maxSize=10**6): + self.filename = filename + try: + fd = file(self.filename) + strId = fd.readline().rstrip() + self.maxSize = len(strId) + try: + self.currentId = int(strId) + except ValueError: + raise Error, 'Invalid file for FlatfileMapping: %s' % filename + except EnvironmentError, e: + # File couldn't be opened. + self.maxSize = int(math.log10(maxSize)) + self.currentId = 0 + self._incrementCurrentId() + + def _canonicalId(self, id): + if id is not None: + return str(id).zfill(self.maxSize) + else: + return '-'*self.maxSize + + def _incrementCurrentId(self, fd=None): + fdWasNone = fd is None + if fdWasNone: + fd = file(self.filename, 'a') + fd.seek(0) + self.currentId += 1 + fd.write(self._canonicalId(self.currentId)) + fd.write('\n') + if fdWasNone: + fd.close() + + def _splitLine(self, line): + line = line.rstrip('\r\n') + (id, s) = line.split(':', 1) + return (id, s) + + def _joinLine(self, id, s): + return '%s:%s\n' % (self._canonicalId(id), s) + + def add(self, s): + line = self._joinLine(self.currentId, s) + try: + fd = file(self.filename, 'r+') + fd.seek(0, 2) # End. + fd.write(line) + return self.currentId + finally: + self._incrementCurrentId(fd) + fd.close() + + def get(self, id): + strId = self._canonicalId(id) + try: + fd = file(self.filename) + fd.readline() # First line, nextId. + for line in fd: + (lineId, s) = self._splitLine(line) + if lineId == strId: + return s + raise KeyError, id + finally: + fd.close() + + def set(self, id, s): + strLine = self._joinLine(id, s) + try: + fd = file(self.filename, 'r+') + self.remove(id, fd) + fd.seek(0, 2) # End. + fd.write(strLine) + finally: + fd.close() + + def remove(self, id, fd=None): + fdWasNone = fd is None + strId = self._canonicalId(id) + try: + if fdWasNone: + fd = file(self.filename, 'r+') + fd.seek(0) + fd.readline() # First line, nextId + pos = fd.tell() + line = fd.readline() + while line: + (lineId, _) = self._splitLine(line) + if lineId == strId: + fd.seek(pos) + fd.write(self._canonicalId(None)) + fd.seek(pos) + fd.readline() # Same line we just rewrote the id for. + pos = fd.tell() + line = fd.readline() + # We should be at the end. + finally: + if fdWasNone: + fd.close() + + def __iter__(self): + fd = file(self.filename) + fd.readline() # First line, nextId. + for line in fd: + (id, s) = self._splitLine(line) + if not id.startswith('-'): + yield (int(id), s) + fd.close() + + def vacuum(self): + infd = file(self.filename) + outfd = utils.transactionalFile(self.filename) + outfd.write(infd.readline()) # First line, nextId. + for line in infd: + if not line.startswith('-'): + outfd.write(line) + infd.close() + outfd.close() + + def flush(self): + pass # No-op, we maintain no open files. + + def close(self): + self.vacuum() # Should we do this? It should be fine. + + +class CdbMapping(MappingInterface): + def __init__(self, filename, **kwargs): + self.filename = filename + self.db = cdb.open(filename, 'c', **kwargs) + if 'nextId' not in self.db: + self.db['nextId'] = '1' + + def _getNextId(self): + i = int(self.db['nextId']) + self.db['nextId'] = str(i+1) + return i + + def get(self, id): + return self.db[str(id)] + + def set(self, id, s): + self.db[str(id)] = s + + def add(self, s): + id = self._getNextId() + self.set(id, s) + return id + + def remove(self, id): + del self.db[str(id)] + + def __iter__(self): + for (id, s) in self.db.iteritems(): + if id != 'nextId': + yield (int(id), s) + + def flush(self): + self.db.flush() + + def close(self): + self.db.close() + + +class DB(object): + Mapping = None + Record = None + def __init__(self, filename, Mapping=None, Record=None): + if Record is not None: + self.Record = Record + if Mapping is not None: + self.Mapping = Mapping + if isinstance(self.Mapping, basestring): + self.Mapping = Mappings[self.Mapping] + self.map = self.Mapping(filename) + + def _newRecord(self, id, s): + record = self.Record(id=id) + record.deserialize(s) + return record + + def get(self, id): + s = self.map.get(id) + return self._newRecord(id, s) + + def set(self, id, record): + s = record.serialize() + self.map.set(id, s) + + def add(self, record): + s = record.serialize() + return self.map.add(s) + + def remove(self, id): + s = self.map.remove(id) + return self._newRecord(id, s) + + def __iter__(self): + for (id, s) in self.map: + # We don't need to yield the id because it's in the record. + yield self._newRecord(id, s) + + def select(self, p): + for record in self: + if p(record): + yield record + + def flush(self): + self.map.flush() + + def close(self): + self.map.close() + +Mappings = { + 'cdb': CdbMapping, + 'flat': FlatfileMapping, + } + + +class Record(type): + """__fields should be a list of two-tuples, (name, converter) or + (name, (converter, default)).""" + def __new__(cls, clsname, bases, dict): + defaults = {} + converters = {} + fields = [] + for name in dict['__fields__']: + if isinstance(name, tuple): + (name, spec) = name + else: + spec = utils.safeEval + assert name != 'convert' and name != 'id' + fields.append(name) + if isinstance(spec, tuple): + (converter, default) = spec + else: + converter = spec + default = None + defaults[name] = default + converters[name] = converter + del dict['__fields__'] + + def __init__(self, id=None, convert=False, **kwargs): + if id is not None: + assert isinstance(id, int), 'id must be an integer.' + self.id = id + set = sets.Set() + for (name, value) in kwargs.iteritems(): + assert name in fields, 'name must be a record value.' + set.add(name) + if convert: + setattr(self, name, converters[name](value)) + else: + setattr(self, name, value) + for name in fields: + if name not in set: + setattr(self, name, defaults[name]) + + def serialize(self): + return csv.join([str(getattr(self, name)) for name in fields]) + + def deserialize(self, s): + unseenRecords = sets.Set(fields) + for (name, strValue) in zip(fields, csv.split(s)): + setattr(self, name, converters[name](strValue)) + unseenRecords.remove(name) + for name in unseenRecords: + setattr(self, record, defaults[record]) + + dict['__init__'] = __init__ + dict['serialize'] = serialize + dict['deserialize'] = deserialize + return type.__new__(cls, clsname, bases, dict) + + + + + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: