others/timeparse.py

This commit is contained in:
Jeremy Fincher 2004-09-17 04:56:38 +00:00
parent 6548085878
commit 0a2802f56e
14 changed files with 328 additions and 71 deletions

3
TODO
View File

@ -1,6 +1,9 @@
Roughly in order of precedence (the closer to the front of the file,
the more likely it'll be done before next release):
* We should have a channel-global value in ircdb.IrcChannel for
ignoring unregistered users.
* It'd be nice to be able to use a backslash to continue lines in the
registry, so we could linewrap long strings or lists.

231
others/timeparse.py Normal file
View File

@ -0,0 +1,231 @@
#!/usr/bin/python
#
# Copyright (c) 2004, Mike Taylor
# 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.
#
__author__ = "Mike Taylor <bear@code-bear.com>"
__copyright__ = "Copyright (c) 2004 Mike Taylor"
__license__ = "BSD"
__revision__ = "$Id$"
import os, string, re, time
RE_SPECIAL = r'(?P<special>^[in|last|next]+)\s+'
RE_UNITS = r'\s+(?P<units>[hour|minute|second|day|week|month|year]+)'
RE_QUNITS = r'(?P<qunits>[0-9]+[hmsdwmy])'
RE_MODIFIER = r'(?P<modifier>[from|before|after|ago|prior]+)\s+'
CRE_SPECIAL = re.compile(RE_SPECIAL, re.IGNORECASE)
CRE_UNITS = re.compile(RE_UNITS, re.IGNORECASE)
CRE_QUNITS = re.compile(RE_QUNITS, re.IGNORECASE)
CRE_MODIFIER = re.compile(RE_MODIFIER, re.IGNORECASE)
# Used to adjust the returned date before/after the source
_Modifiers = {'from': 1,
'before': -1,
'after': 1,
'ago': 1,
'prior': -1}
_Minute = 60
_Hour = 60 * _Minute
_Day = 24 * _Hour
_Week = 7 * _Day
_Month = 30 * _Day
_Year = 365 * _Day
# This looks hokey - but it is a nice simple way to get
# the proper unit value and it has the advantage that
# later I can morph it into something localized.
# Any trailing s will be removed before lookup.
_Units = {'second': 1,
'sec': 1,
's': 1,
'minute': _Minute,
'min': _Minute,
'm': _Minute,
'hour': _Hour,
'hr': _Hour,
'h': _Hour,
'day': _Day,
'dy': _Day,
'd': _Day,
'week': _Week,
'wk': _Week,
'w': _Week,
'month': _Month,
'mth': _Month,
'm': _Month,
'year': _Year,
'yr': _Year,
'y': _Year}
def _buildTime(sourceTime, quantity, modifier, units):
"""Take quantity, modifier and units strings and convert them
into values, calcuate the time and return the adjusted
sourceTime
"""
# print '[%s][%s][%s]' % (quantity, modifier, units)
q = int(quantity)
if _Modifiers.has_key(modifier):
q = q * _Modifiers[modifier]
if units[-1] == 's':
units = units[:-1]
if _Units.has_key(units):
u = _Units[units]
else:
u = 1
# print 'sourceTime [%d]' % sourceTime
# print 'quantity [%d]' % q
# print 'units [%d]' % u
return sourceTime + (q * u)
def parse(timeString, sourceTime=None):
"""Parse timeString and return the number of seconds from sourceTime
that the timeString expression represents.
This version of parse understands only the more basic of expressions
in this form:
<quantity> <units> <modifier> <target>
Example:
5 minutes from now
last week
2 hours before noon
Valid units - hour, minute, second, month, week, day and year
(including their plural forms)
Valid modifiers - from, before, after, ago, prior
"""
if sourceTime == None:
sourceTime = int(time.time())
else:
sourceTime = int(sourceTime)
quantity = ''
units = ''
modifier = ''
target = ''
s = string.strip(string.lower(timeString))
m = CRE_SPECIAL.search(s)
if m <> None:
target = 'now'
if m.group('special') == 'last':
modifier = 'before'
else:
modifier = 'from'
s = s[m.end('special'):]
m = CRE_UNITS.search(s)
if m <> None:
units = m.group('units')
quantity = s[:m.start('units')]
s = s[m.end('units'):]
else:
m = CRE_MODIFIER.search(s)
if m <> None:
modifier = m.group('modifier')
target = s[m.end('modifier'):]
s = s[:m.start('modifier'):]
m = CRE_UNITS.search(s)
if m <> None:
units = m.group('units')
quantity = s[:m.start('units')]
target = s[m.end('units'):]
return _buildTime(sourceTime, quantity, modifier, units)
def _test(text, value):
print text
v = parse(text)
print '\t%s\t%d\t%d' % ((v == value), value, v)
#
# TODO
#
# - make month unit adjustment aware of the actual number of days in each
# month between source and target
# - handle edge case where quantity and unit are merged: 5s for 5 sec
# - handle compound/nested quantites and modifiers
# - bring unit test over from prototype into this file
# - convert 'five' to 5 and also 'twenty five' to 25
if __name__ == '__main__':
start = int(time.time())
tests = { '5 minutes from now': start + 5 * _Minute,
'5 min from now': start + 5 * _Minute,
'in 5 minutes': start + 5 * _Minute,
'5 days from today': start + 5 * _Day,
'5 days before today': start - 5 * _Day,
'5 minutes': start + 5 * _Minute,
'5 min': start + 5 * _Minute,
'30 seconds from now': start + 30,
'30 sec from now': start + 30,
'30 seconds': start + 30,
'30 sec': start + 30,
'1 week': start + _Week,
'1 wk': start + _Week,
'1 week': start + _Week,
'5 days': start + 5 * _Day,
'1 year': start + _Year,
'2 years': start + 2 * _Year}
for test in tests.keys():
_test(test, tests[test])

View File

@ -45,9 +45,12 @@ import time
sys.path.append(os.pardir)
import supybot.conf as conf
import supybot.utils as utils
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs
import supybot.registry as registry
import supybot.schedule as schedule
import supybot.callbacks as callbacks
conf.registerPlugin('Ctcp')
@ -63,8 +66,13 @@ conf.registerGlobalValue(conf.supybot.abuse.flood.ctcp, 'maximum',
conf.registerGlobalValue(conf.supybot.abuse.flood.ctcp, 'punishment',
registry.PositiveInteger(300, """Determines how many seconds the bot will
ignore CTCP messages from users who flood it with CTCP messages."""))
conf.registerGlobalValue(conf.supybot.plugins.Ctcp, 'versionWait',
registry.PositiveInteger(10, """Determines how many seconds the bot will
wait after getting a version command (not a CTCP VERSION, but an actual
call of the command in this plugin named "version") before replying with
the results it has collected."""))
class Ctcp(callbacks.PrivmsgRegexp):
class Ctcp(callbacks.PrivmsgCommandAndRegexp):
public = False
def __init__(self):
self.__parent = super(Ctcp, self)
@ -96,35 +104,77 @@ class Ctcp(callbacks.PrivmsgRegexp):
s = '\x01%s\x01' % s
irc.reply(s, notice=True, private=True, to=msg.nick)
def ping(self, irc, msg, match):
regexps = ('ctcpPing', 'ctcpVersion', 'ctcpUserinfo',
'ctcpTime', 'ctcpFinger', 'ctcpSource')
def ctcpPing(self, irc, msg, match):
"\x01PING (.*)\x01"
self.log.info('Received CTCP PING from %s', msg.prefix)
self._reply(irc, msg, 'PING %s' % match.group(1))
def version(self, irc, msg, match):
def ctcpVersion(self, irc, msg, match):
"\x01VERSION\x01"
self.log.info('Received CTCP VERSION from %s', msg.prefix)
self._reply(irc, msg, 'VERSION Supybot %s' % conf.version)
def userinfo(self, irc, msg, match):
def ctcpUserinfo(self, irc, msg, match):
"\x01USERINFO\x01"
self.log.info('Received CTCP USERINFO from %s', msg.prefix)
self._reply(irc, msg, 'USERINFO')
def time(self, irc, msg, match):
def ctcpTime(self, irc, msg, match):
"\x01TIME\x01"
self.log.info('Received CTCP TIME from %s' % msg.prefix)
self._reply(irc, msg, time.ctime())
def finger(self, irc, msg, match):
def ctcpFinger(self, irc, msg, match):
"\x01FINGER\x01"
self.log.info('Received CTCP FINGER from %s' % msg.prefix)
self._reply(irc, msg, 'Supybot, the best Python IRC bot in existence!')
def source(self, irc, msg, match):
def ctcpSource(self, irc, msg, match):
"\x01SOURCE\x01"
self.log.info('Received CTCP SOURCE from %s' % msg.prefix)
self._reply(irc, msg, 'http://www.sourceforge.net/projects/supybot/')
def doNotice(self, irc, msg):
if ircmsgs.isCtcp(msg):
try:
(version, payload) = msg.args[1][1:-1].split(None, 1)
except ValueError:
return
if version == 'VERSION':
self.versions.setdefault(payload, []).append(msg.nick)
def version(self, irc, msg, args):
"""[<channel>] [--nicks]
Sends a CTCP VERSION to <channel>, returning the various
version strings returned. It waits for 10 seconds before returning
the versions received at that point. If --nicks is given, nicks are
associated with the version strings; otherwise, only the version
strings are given.
"""
self.versions = ircutils.IrcDict()
nicks = False
while '--nicks' in args:
nicks = True
args.remove('--nicks')
channel = privmsgs.getChannel(msg, args)
irc.queueMsg(ircmsgs.privmsg(channel, '\x01VERSION\x01'))
def doReply():
if self.versions:
L = []
for (reply, nicks) in self.versions.iteritems():
if nicks:
L.append('%s responded with %r' %
(utils.commaAndify(nicks), reply))
else:
L.append(reply)
irc.reply(utils.commaAndify(L))
else:
irc.reply('I received no version responses.')
wait = self.registryValue('versionWait')
schedule.addEvent(doReply, time.time()+wait)
Class = Ctcp
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -275,8 +275,7 @@ class Debian(callbacks.Privmsg,
r = utils.perlReToPythonRe(arg)
predicates.append(r.search)
except ValueError:
irc.error('%r is not a valid regexp.' % arg)
return
irc.errorInvalid('regular expression', arg, Raise=True)
elif option == '--arch':
arg = '_%s.' % arg
archPredicate = lambda s, arg=arg: (arg in s)

View File

@ -194,8 +194,7 @@ class Dunno(callbacks.Privmsg):
try:
id = int(id)
except ValueError:
irc.error('Invalid id: %r' % id)
return
irc.error('id', id, Raise=True)
dunno = self.db.get(channel, id)
if by != dunno.by:
cap = ircdb.makeChannelCapability(channel, 'op')
@ -238,8 +237,7 @@ class Dunno(callbacks.Privmsg):
try:
id = int(id)
except ValueError:
irc.error('%r is not a valid dunno id.' % id)
return
irc.errorInvalid('dunno id', id, Raise=True)
try:
dunno = self.db.get(channel, id)
name = ircdb.users.getUser(dunno.by).name
@ -268,18 +266,15 @@ class Dunno(callbacks.Privmsg):
try:
id = int(id)
except ValueError:
irc.error('%r is not a valid dunno id.' % id)
return
irc.errorInvalid('dunno id', id, Raise=True)
try:
_ = self.db.get(channel, id)
except KeyError:
irc.error('There is no dunno #%s.' % id)
return
try:
replacer = utils.perlReToReplacer(regexp)
except:
irc.error('%r is not a valid regular expression.' % regexp)
return
irc.errorInvalid('regular expression', regexp, Raise=True)
self.db.change(channel, id, replacer)
irc.replySuccess()

View File

@ -171,8 +171,7 @@ class Factoids(plugins.ChannelDBHandler, callbacks.Privmsg):
try:
irc.reply(factoids[number-1])
except IndexError:
irc.error('That\'s not a valid number for that key.')
return
irc.errorInvalid('number for that key', number, Raise=True)
else:
intro = self.registryValue('factoidPrefix', channel)
prefix = '%r %s' % (key, intro)
@ -214,8 +213,7 @@ class Factoids(plugins.ChannelDBHandler, callbacks.Privmsg):
try:
number = int(number)
except ValueError:
irc.error('%s is not a valid number.' % number)
return
irc.errorInvalid('number', number, Raise=True)
else:
number = 0
factoids = self._lookupFactoid(channel, key)
@ -293,8 +291,7 @@ class Factoids(plugins.ChannelDBHandler, callbacks.Privmsg):
try:
(_, id) = results[number]
except IndexError:
irc.error('Invalid factoid number.')
return
irc.errorInvalid('factoid number', number, Raise=True)
cursor.execute("DELETE FROM factoids WHERE id=%s", id)
db.commit()
irc.replySuccess()
@ -370,15 +367,13 @@ class Factoids(plugins.ChannelDBHandler, callbacks.Privmsg):
try:
replacer = utils.perlReToReplacer(regexp)
except ValueError, e:
irc.error('Invalid regexp: %s' % e)
return
irc.errorInvalid('regular expression', regexp, str(e), Raise=True)
try:
number = int(number)
if number <= 0:
raise ValueError
except ValueError:
irc.error('Invalid key id.')
return
irc.errorInvalid('id', number, Raise=True)
db = self.getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT factoids.id, factoids.fact

View File

@ -101,14 +101,12 @@ class Format(callbacks.Privmsg):
try:
fg = ircutils.mircColors[fg]
except KeyError:
irc.error('%r is not a valid foreground color.' % fg)
return
irc.errorInvalid('foreground color', fg, Raise=True)
if bg is not None:
try:
bg = ircutils.mircColors[bg]
except KeyError:
irc.error('%r is not a valid background color.' % bg)
return
irc.errorInvalid('background color', bg, Raise=True)
irc.reply(ircutils.mircColor(text, fg=fg, bg=bg))
def join(self, irc, msg, args):
@ -172,7 +170,7 @@ class Format(callbacks.Privmsg):
try:
size = int(size)
except ValueError:
irc.error('%r is not a valid integer.' % size)
irc.errorInvalid('integer', size, Raise=True)
irc.reply(text[:size])
def field(self, irc, msg, args):
@ -187,11 +185,11 @@ class Format(callbacks.Privmsg):
if index > 0:
index -= 1
except ValueError:
irc.error('%r is not a valid integer.' % number)
irc.errorInvalid('integer', number, Raise=True)
try:
irc.reply(text.split()[index])
except IndexError:
irc.error('That\'s not a valid field.')
irc.errorInvalid('field')
def format(self, irc, msg, args):
"""<format string> [<arg> ...]

View File

@ -252,8 +252,7 @@ class Herald(callbacks.Privmsg):
try:
changer = utils.perlReToReplacer(regexp)
except ValueError, e:
irc.error('That\'s not a valid regexp: %s.' % e)
return
irc.errorInvalid('regular expression', regexp, str(e), Raise=True)
s = self.db[channel, id]
newS = changer(s)
self.db[channel, id] = newS

View File

@ -133,8 +133,7 @@ class Lookup(callbacks.Privmsg):
name = privmsgs.getArgs(args)
name = callbacks.canonicalName(name)
if name not in self.lookupDomains:
irc.error('That\'s not a valid lookup to remove.')
return
irc.errorInvalid('lookup', name, Raise=True)
db = self.dbHandler.getDb()
cursor = db.cursor()
try:
@ -249,9 +248,7 @@ class Lookup(callbacks.Privmsg):
try:
r = utils.perlReToPythonRe(arg)
except ValueError, e:
irc.error('%r is not a valid regular expression' %
arg)
return
irc.errorInvalid('regular expression', arg, Raise=True)
def p(s, r=r):
return int(bool(r.search(s)))
db.create_function(predicateName, 1, p)

View File

@ -194,7 +194,7 @@ class Observer(callbacks.Privmsg):
"""
name = privmsgs.getArgs(args)
if name not in self.registryValue('observers'):
irc.error('That\'s not a valid observer.', Raise=True)
irc.errorInvalid('observer', name, Raise=True)
g = self.registryValue('observers.%s' % name, value=False)
regexp = g()
command = g.command()
@ -222,8 +222,9 @@ class Observer(callbacks.Privmsg):
probability = 1.0
(name, regexp, command) = privmsgs.getArgs(args, required=3)
if not registry.isValidRegistryName(name):
irc.error('That\'s not a valid observer name. Please be sure '
'there are no spaces in the name.', Raise=True)
irc.errorInvalid('observer name', name,
'Please be sure there are no spaces in the name.',
Raise=True)
registerObserver(name, regexp, command, probability)
irc.replySuccess()

View File

@ -323,8 +323,7 @@ class Quotes(callbacks.Privmsg):
try:
id = int(id)
except ValueError:
irc.error('Invalid id: %r' % id)
return
irc.errorInvalid('id', id, Raise=True)
try:
quote = self.db.get(channel, id)
irc.reply(str(quote))
@ -342,7 +341,7 @@ class Quotes(callbacks.Privmsg):
try:
id = int(id)
except ValueError:
irc.error('That\'s not a valid id: %r' % id)
irc.errorInvalid('id', id, Raise=True)
try:
self.db.remove(channel, id)
irc.replySuccess()

View File

@ -112,9 +112,7 @@ class Todo(callbacks.Privmsg):
try:
userid = ircdb.users.getUserId(arg)
except KeyError:
irc.error(
'%r is not a valid task id or username' % arg)
return
irc.errorInvalid('task or username', arg, Raise=True)
db = self.dbHandler.getDb()
cursor = db.cursor()
if not userid and not taskid:
@ -151,8 +149,7 @@ class Todo(callbacks.Privmsg):
cursor.execute("""SELECT userid,priority,added_at,task,active
FROM todo WHERE id = %s""", taskid)
if cursor.rowcount == 0:
irc.error('%r is not a valid task id' % taskid)
return
irc.errorInvalid('task id', taskid, Raise=True)
(userid, pri, added_at, task, active) = cursor.fetchone()
# Construct and return the reply
user = ircdb.users.getUser(userid)
@ -191,8 +188,7 @@ class Todo(callbacks.Privmsg):
try:
priority = int(arg)
except ValueError, e:
irc.error('%r is an invalid priority' % arg)
return
irc.errorInvalid('priority', arg, Raise=True)
text = privmsgs.getArgs(rest)
db = self.dbHandler.getDb()
cursor = db.cursor()
@ -272,9 +268,7 @@ class Todo(callbacks.Privmsg):
try:
r = utils.perlReToPythonRe(arg)
except ValueError, e:
irc.error('%r is not a valid regular expression' %
arg)
return
irc.errorInvalid('regular expression', arg, Raise=True)
def p(s, r=r):
return int(bool(r.search(s)))
db.create_function(predicateName, 1, p)
@ -337,19 +331,17 @@ class Todo(callbacks.Privmsg):
try:
replacer = utils.perlReToReplacer(regexp)
except ValueError:
irc.error('%r is not a valid regexp' % regexp)
return
irc.errorInvalid('regular expression', regexp, Raise=True)
db = self.dbHandler.getDb()
cursor = db.cursor()
cursor.execute("""SELECT task FROM todo
WHERE userid = %s AND id = %s
AND active = 1""", userid, taskid)
if cursor.rowcount == 0:
irc.error('%r is not a valid task id' % taskid)
return
irc.errorInvalid('task id', taskid, Raise=True)
newtext = replacer(cursor.fetchone()[0])
cursor.execute("""UPDATE todo SET task = %s
WHERE id = %s""", newtext, taskid)
cursor.execute("""UPDATE todo SET task = %s WHERE id = %s""",
newtext, taskid)
db.commit()
irc.replySuccess()

View File

@ -166,7 +166,7 @@ class Topic(callbacks.Privmsg):
n += len(topics)
return n
except (ValueError, IndexError):
irc.error('That\'s not a valid topic number.', Raise=True)
irc.errorInvalid('topic number', n, Raise=True)
def topic(self, irc, msg, args, channel):
"""[<channel>]
@ -308,14 +308,12 @@ class Topic(callbacks.Privmsg):
(number, regexp) = privmsgs.getArgs(args, required=2)
topics = self._splitTopic(irc.state.getTopic(channel), channel)
if not topics:
irc.error('There are no topics to change.')
return
irc.error('There are no topics to change.', Raise=True)
number = self._topicNumber(irc, number, topics=topics)
try:
replacer = utils.perlReToReplacer(regexp)
except ValueError, e:
irc.error('The regexp wasn\'t valid: %s' % e)
return
irc.errorInvalid('regexp', regexp, Raise=True)
topics[number] = replacer(topics[number])
self._sendTopics(irc, channel, topics)
change = privmsgs.channel(change)

View File

@ -303,10 +303,10 @@ class Words(callbacks.Privmsg):
else:
# User input an invalid character
if len(letter) == 1:
irc.error('That is not a valid letter.')
irc.errorInvalid('letter', letter, Raise=True)
# User input an invalid word (len(try) != len(hidden))
else:
irc.error('That is not a valid word guess.')
irc.errorInvalid('word guess', letter, Raise=True)
# Verify if the user won or lost
if game.guessed and game.tries > 0:
self._hangmanReply(irc, channel,