#!/usr/bin/env python
import sys
import os
import optparse
import time
import datetime
import locale
import tempfile
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.dirname = os.path.dirname(self.fname)
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 ]
tmpfd, tmpfname = tempfile.mkstemp(dir = self.dirname)
tmpfd = os.fdopen(tmpfd, 'w')
json.dump(evts, tmpfd, indent = 4)
tmpfd.close()
os.rename(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)
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
next_month = (today.month + 1) % 12
year = today.year
if next_month < today.month:
year += 1
return datetime.date(year, next_month, day)
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
import random
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.encode('utf8'))
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.encode('utf8'))
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'] = "<%f.%d.%d@%s>" % (time.time(), os.getpid(),
int(random.random() * 100000), 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
elif not when:
if evt.date < datetime.date.today():
continue
flag = " "
if evt.date == datetime.date.today():
flag = "*"
print flag, 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())