git » el » commit fc8628b

Initial commit

author Alberto Bertogli
2010-05-08 23:17:17 UTC
committer Alberto Bertogli
2010-05-08 23:35:36 UTC

Initial commit

Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>

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())
+