author | Alberto Bertogli
<albertito@blitiri.com.ar> 2010-05-08 23:17:17 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2010-05-08 23:35:36 UTC |
config.sample | +13 | -0 |
el | +325 | -0 |
diff --git a/config.sample b/config.sample new file mode 100644 index 0000000..98e48b4 --- /dev/null +++ b/config.sample @@ -0,0 +1,13 @@ + +# Copy this file to ~/.el/config and edit to suit your preferences. + + +[email] +# Section used by the --send-email option + +# Where to send email to. It is required to be able to send email successfuly. +to = me@example.com + +# What to put in the From field in the email. Optional. +#from = el@example.com + diff --git a/el b/el new file mode 100755 index 0000000..9607231 --- /dev/null +++ b/el @@ -0,0 +1,325 @@ +#!/usr/bin/env python + +import sys +import os +import optparse +import time +import datetime +import locale +import calendar +import ConfigParser + +# json is needed but available only in >= 2.6, simplejson is a third-party +# module with compatible API +try: + import json +except ImportError: + import simplejson as json + + +# enable locale settings +locale.setlocale(locale.LC_ALL, ''); + + +# +# Configuration +# + +# A single, global instance so it can be easily accessed when needed. The +# main() function is in charge of loading this with the user-supplied values. +config = ConfigParser.SafeConfigParser() + + + +# +# Event representation (internal and on-disk) +# + +class Event (object): + def __init__(self, date, msg): + self.date = date + self.msg = msg + + def __cmp__(self, other): + return cmp(self.date, other.date) + + def to_dict(self): + return { + 'date': (self.date.year, self.date.month, + self.date.day), + 'msg': self.msg, + } + +def event_from_dict(d): + date = datetime.date(*d['date']) + msg = d['msg'] + return Event(date, msg) + +class DB (object): + def __init__(self, fname = ""): + self.fname = fname + if not self.fname: + if not os.path.isdir(os.environ["HOME"] + "/.el/"): + os.mkdir(os.environ["HOME"] + "/.el/") + self.fname = os.environ["HOME"] + "/.el/events" + self.tmpfname = fname + ".tmp" + self.events = [] + self.has_loaded = False + + def load(self): + if self.has_loaded: + return + if not os.path.exists(self.fname): + return + evts = json.load(open(self.fname)) + self.events = [ event_from_dict(d) for d in evts ] + self.has_loaded = True + + def save(self): + self.load() + self.events.sort() + evts = [ e.to_dict() for e in self.events ] + json.dump(evts, open(self.tmpfname, 'w'), indent = 4) + os.rename(self.tmpfname, self.fname) + + def append(self, evt): + self.events.append(evt) + self.save() + + +# +# Date parsing +# + +def try_strptime(s, *fmts): + """Attempts to apply the different formats, in order, to the given + string. Returns the matching date object (not datetime), or None if + there was no match.""" + for f in fmts: + try: + d = datetime.datetime.strptime(s, f) + except ValueError: + continue + return d.date() + return None + + +def parse_when(w): + "Parses the <when> argument, returns a datetime.date object." + days_abbr = [ x.lower() for x in calendar.day_abbr] + days_names = [ x.lower() for x in calendar.day_name] + + today = datetime.date.today() + + w = w.lower() + + if set(w) == set('+'): + # "+" == tomorrow, "+++" = "3 days from now", and so on + td = datetime.timedelta(days = len(w)) + return today + td + elif w.startswith('+') and len(w) > 1: + # time offset, in days (default), weeks, months or years + if w[-1] in "dwmy": + incr = int(w[1:-1]) + if w[-1] == 'd': + td = datetime.timedelta(days = incr) + if w[-1] == 'w': + td = datetime.timedelta(weeks = incr) + if w[-1] == 'm': + td = datetime.timedelta(months = incr) + if w[-1] == 'y': + td = datetime.timedelta(years = incr) + else: + incr = int(w[1:]) + td = datetime.timedelta(days = incr) + return today + td + elif w.isdigit(): + # a day, from either this month or the next + day = int(w) + if day <= today.day: + # the day of the next month + d = datetime.date(today.year, today.month, day) + d += datetime.timedelta(months = 1) + return d + else: + # the day of this month + return datetime.date(today.year, today.month, day) + elif w in days_abbr or w in days_names: + # next <day of the week> + current = today.weekday() + if w in days_abbr: + dst = days_abbr.index(w) + else: + dst = days_names.index(w) + diff = (dst - current) % 7 + return today + datetime.timedelta(days = diff) + else: + # attempt to parse different formats; note we force the use of + # the month by name to prevent misinterpreting ambiguous + # formats + d = try_strptime(w, + "%d-%b", "%d/%b", "%d %b", # day-month + "%b-%d", "%b/%d", "%b %d") # month-day + if d: + # if the month is before the current one, it refers to + # next year's + if d.month < today.month: + return datetime.date(today.year + 1, d.month, + d.day) + else: + return datetime.date(today.year, d.month, + d.day) + + # no match, try the versions with a year + d = try_strptime(w, + "%d-%b-%Y", "%d/%b/%Y", "%d %b %Y", # day-month-year + "%b-%d-%Y", "%b/%d/%Y", "%b %d %Y", # month-day-year + "%Y-%b-%d", # year-month-day + ) + + return d + + +# +# Email sending +# + +def send_events(evts): + import email + import email.mime.text + import socket + + if not config.has_option('email', 'to'): + print "Error: destination email needed, check config file" + return 1 + + # leave only today's events + today = datetime.date.today() + tomorrow = today + datetime.timedelta(days = 1) + today_evts = [ e for e in evts if e.date == today ] + tomorrow_evts = [ e for e in evts if e.date == tomorrow ] + + if not today_evts and not tomorrow_evts: + # do not send anything when there are no events to report + return 0 + + body = "\n" + if today_evts: + body += "Events for today, %s\n" % today + body += "-----------------------------------\n\n" + for e in today_evts: + body += "%s %s\n" % (e.date, e.msg) + body += "\n" + else: + body += "No events for today, %s\n" % today + + body += "\n" + + if tomorrow_evts: + body += "Events for tomorrow, %s\n" % tomorrow + body += "-----------------------------------\n\n" + for e in tomorrow_evts: + body += "%s %s\n" % (e.date, e.msg) + body += "\n" + else: + body += "No events for tomorrow, %s\n" % tomorrow + + e = email.mime.text.MIMEText("") + e['Subject'] = "Events for %s" % today + e['To'] = config.get("email", "to") + if config.has_option("email", "from"): + e['From'] = config.get("email", "from") + e['Message-Id'] = "<%s.%s.%f@%s>" % (os.environ['USER'], today, + time.time(), socket.gethostname()) + e['X-Sender'] = "el" + e.set_payload(body, 'utf8') + + return sendmail(e) + +def sendmail(e): + # TODO: SMTP via configuration + import subprocess + cmd = subprocess.Popen( ["/usr/sbin/sendmail", "-t"], + stdin = subprocess.PIPE) + cmd.communicate(e.as_string()) + return cmd.wait() + + +# +# Main functions +# + +def main(): + usage = \ +""" + %prog [options] <when> <message> + Add an event + + %prog [options] <when> + List events for today or on the given date + + %prog [options] --list [when] + List events, either all or on the given date + + %prog [options] --send-email + Send today's events via email, using sendmail +""" + parser = optparse.OptionParser(usage = usage) + parser.add_option("", "--send-email", + action = "store_true", default = False, + help = "send events via email") + parser.add_option("-l", "--list", + action = "store_true", default = False, + help = "list events") + parser.add_option("-c", "--config-file", + default = "", + help = "configuration file path [~/.el/config]") + + options, args = parser.parse_args() + + cfname = os.environ['HOME'] + '/.el/config' + if options.config_file: + cfname = options.config_file + if os.path.exists(cfname): + config.read(cfname) + + db = DB() + db.load() + + if options.send_email: + return send_events(db.events) + + elif options.list or len(args) <= 1: + if options.list: + when = None + else: + when = datetime.date.today() + + if args: + when = parse_when(args[0]) + if not when: + print "Error: unknown date format" + return 1 + + for evt in db.events: + if when and evt.date != when: + continue + print evt.date, evt.msg + + else: + if len(args) < 2: + parser.print_help() + return 1 + + when = parse_when(args[0]) + msg = " ".join(args[1:]) + + if not when: + print "Error: unknown date format" + return 1 + + db.append(Event(when, msg)) + +if __name__ == '__main__': + sys.exit(main()) +