You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1582 lines
54 KiB
Python

#!/usr/bin/env python
# ** The MIT License **
#
# Copyright (c) 2007 Eric Davis (aka Insanum)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Dude... just buy me a beer. :-)
#
#
# Home: http://code.google.com/p/gcalcli
#
# Author: Eric Davis <http://www.insanum.com>
#
# Requirements:
# - Python 2
# http://www.python.org
# - Google's GData Python module (for Python 2)
# http://code.google.com/p/gdata-python-client
# - dateutil Python module
# http://www.labix.org/python-dateutil
# - vobject Python module (optional, needed for importing ics/vcal files)
# http://vobject.skyhouseconsulting.com
#
__program__ = 'gcalcli'
__version__ = 'v2.2'
__author__ = 'Eric Davis'
import inspect
import sys, os, re, urllib, getopt, shlex, subprocess
import codecs, locale, csv, threading, getpass
from Queue import Queue
from ConfigParser import RawConfigParser
from gdata.calendar.service import *
from datetime import *
from dateutil.tz import *
from dateutil.parser import *
from dateutil.rrule import *
from unicodedata import east_asian_width
def Version():
sys.stdout.write(__program__+' '+__version__+' ('+__author__+')\n')
sys.exit(1)
def Usage():
sys.stdout.write('''
Usage:
gcalcli [options] command [command args]
Options:
--help this usage text
--version version information
--config <file> config file to read (default is '~/.gcalclirc')
--user <username> google username
--pw <password> password
--cals [all, 'calendars' to work with (default is all calendars)
default, - default (your default main calendar)
owner, - owner (your owned calendars)
editor, - editor (editable calendar)
contributor, - contributor (non-owner but able to edit)
read, - read (read only calendars)
freebusy] - freebusy (only free/busy info visible)
--cal <name>[#color] 'calendar' to work with (default is all calendars)
- you can specify a calendar by name or by substring
which can match multiple calendars
- you can use multiple '--cal' arguments on the
command line
- in the config file specify multiple calendars in
quotes separated by commas as:
cal: "foo", "bar", "my cal"
- an optional color override can be specified per
calendar using the ending hashtag:
--cal "Eric Davis"#green --cal foo#red
or via the config file:
cal: "foo"#red, "bar"#yellow, "my cal"#green
--24hr show all dates in 24 hour format
--details show all event details (i.e. length, location,
reminders, contents)
--ignore-started ignore old or already started events
- when used with the 'agenda' command, ignore events
that have already started and are in-progress with
respect to the specified [start] time
- when used with the 'search' command, ignore events
that have already occurred and only show future
events
--width the number of characters to use for each column in
the 'calw' and 'calm' command outputs (default is 10)
--mon week begins with Monday for 'calw' and 'calm' command
outputs (default is Sunday)
--nc don't use colors
--cal-owner-color specify the colors used for the calendars and dates
--cal-editor-color each of these argument requires a <color> argument
--cal-contributor-color which must be one of [ default, black, brightblack,
--cal-read-color red, brightred, green, brightgreen, yellow,
--cal-freebusy-color brightyellow, blue, brightblue, magenta,
--date-color brightmagenta, cyan, brightcyan, white,
--border-color brightwhite ]
--tsv tab-separated output for 'agenda'. Format is:
'date' 'start' 'end' 'title' 'location' 'description'
Commands:
list list all calendars
search <text> search for events
- only matches whole words
agenda [start] [end] get an agenda for a time period
- start time default is 12am today
- end time default is 5 days from start
- example time strings:
'9/24/2007'
'Sep 24 2007 3:30pm'
'2007-09-24T15:30'
'2007-09-24T15:30-8:00'
'20070924T15'
'8am'
calw <weeks> [start] get a week based agenda in a nice calendar format
- weeks is the number of weeks to display
- start time default is beginning of this week
- note that all events for the week(s) are displayed
calm [start] get a month agenda in a nice calendar format
- start time default is the beginning of this month
- note that all events for the month are displayed
and only one month will be displayed
quick <text> quick add an event to a calendar
- if a --cal is not specified then the event is
added to the default calendar
- example:
'Dinner with Eric 7pm tomorrow'
'5pm 10/31 Trick or Treat'
import [-v] [file] import an ics/vcal file to a calendar
- if a --cal is not specified then the event is
added to the default calendar
- if a file is not specified then the data is read
from standard input
- if -v is given then each event in the file is
displayed and you're given the option to import
or skip it, by default everything is imported
quietly without any interaction
remind <mins> <command> execute command if event occurs within <mins>
minutes time ('%s' in <command> is replaced with
event start time and title text)
- <mins> default is 10
- default command:
'notify-send -u critical -a gcalcli %s'
''')
sys.exit(1)
class CLR:
useColor = True
def __str__(self):
if self.useColor: return self.color
else: return ""
class CLR_NRM(CLR): color = "\033[0m"
class CLR_BLK(CLR): color = "\033[0;30m"
class CLR_BRBLK(CLR): color = "\033[30;1m"
class CLR_RED(CLR): color = "\033[0;31m"
class CLR_BRRED(CLR): color = "\033[31;1m"
class CLR_GRN(CLR): color = "\033[0;32m"
class CLR_BRGRN(CLR): color = "\033[32;1m"
class CLR_YLW(CLR): color = "\033[0;33m"
class CLR_BRYLW(CLR): color = "\033[33;1m"
class CLR_BLU(CLR): color = "\033[0;34m"
class CLR_BRBLU(CLR): color = "\033[34;1m"
class CLR_MAG(CLR): color = "\033[0;35m"
class CLR_BRMAG(CLR): color = "\033[35;1m"
class CLR_CYN(CLR): color = "\033[0;36m"
class CLR_BRCYN(CLR): color = "\033[36;1m"
class CLR_WHT(CLR): color = "\033[0;37m"
class CLR_BRWHT(CLR): color = "\033[37;1m"
def PrintErrMsg(msg):
if CLR.useColor:
sys.stdout.write(str(CLR_BRRED()))
sys.stdout.write(msg)
sys.stdout.write(str(CLR_NRM()))
else:
sys.stdout.write(msg)
def PrintMsg(color, msg):
if CLR.useColor:
sys.stdout.write(str(color))
sys.stdout.write(msg)
sys.stdout.write(str(CLR_NRM()))
else:
sys.stdout.write(msg)
def DebugPrint(msg):
return
sys.stdout.write(str(CLR_YLW()))
sys.stdout.write(msg)
sys.stdout.write(str(CLR_NRM()))
class gcalcli:
gcal = None
allCals = None
cals = []
now = datetime.now(tzlocal())
feedPrefix = 'https://www.google.com/calendar/feeds/'
agendaLength = 5
username = None
password = None
access = ''
military = False
details = False
ignoreStarted = False
calWidth = 10
calMonday = False
command = 'notify-send -u critical -a gcalcli %s'
tsv = False
calOwnerColor = CLR_CYN()
calEditorColor = CLR_NRM()
calContributorColor = CLR_NRM()
calReadColor = CLR_MAG()
calFreeBusyColor = CLR_NRM()
dateColor = CLR_YLW()
borderColor = CLR_WHT()
ACCESS_ALL = 'all' # non-google access level
ACCESS_DEFAULT = 'default' # non-google access level
ACCESS_CONTRIBUTOR = 'contributor'
ACCESS_EDITOR = 'editor'
ACCESS_FREEBUSY = 'freebusy'
ACCESS_NONE = 'none'
ACCESS_OVERRIDE = 'override'
ACCESS_OWNER = 'owner'
ACCESS_READ = 'read'
ACCESS_RESPOND = 'respond'
ACCESS_ROOT = 'root'
def __init__(self,
username=None,
password=None,
access='all',
calNames=[],
calNameColors=[],
military=False,
details=False,
ignoreStarted=False,
calWidth=10,
calMonday=False,
calOwnerColor=CLR_CYN(),
calEditorColor=CLR_GRN(),
calContributorColor=CLR_NRM(),
calReadColor=CLR_MAG(),
calFreeBusyColor=CLR_NRM(),
dateColor=CLR_GRN(),
borderColor=CLR_WHT(),
tsv=False):
self.gcal = CalendarService()
self.gcal.ssl = True
self.username = username
self.password = password
self.access = access
self.military = military
self.details = details
self.ignoreStarted = ignoreStarted
self.calWidth = calWidth
self.calMonday = calMonday
self.tsv = tsv
self.calOwnerColor = calOwnerColor
self.calEditorColor = calEditorColor
self.calContributorColor = calContributorColor
self.calReadColor = calReadColor
self.calFreeBusyColor = calFreeBusyColor
self.dateColor = dateColor
self.borderColor = borderColor
# authenticate and login to google calendar
try:
self.gcal.ClientLogin(
username=self.username,
password=self.password,
service='cl',
source=__author__+'-'+__program__+'-'+__version__)
except Exception, e:
PrintErrMsg("Error: " + str(e) + "!\n")
sys.exit(1)
# get the list of calendars
self.allCals = self.gcal.GetAllCalendarsFeed()
# gcalcli defined way to order calendars XXX
order = { self.ACCESS_OWNER : 1,
self.ACCESS_EDITOR : 2,
self.ACCESS_ROOT : 3,
self.ACCESS_CONTRIBUTOR : 4,
self.ACCESS_OVERRIDE : 5,
self.ACCESS_RESPOND : 6,
self.ACCESS_FREEBUSY : 7,
self.ACCESS_READ : 8,
self.ACCESS_NONE : 9 }
self.allCals.entry.sort(lambda x, y:
cmp(order[x.access_level.value],
order[y.access_level.value]))
for cal in self.allCals.entry:
cal.gcalcli_altLink = cal.GetAlternateLink().href
match = re.match('^' + self.feedPrefix + '(.*?)/(.*?)/(.*)$',
cal.gcalcli_altLink)
cal.gcalcli_username = urllib.unquote(match.group(1))
cal.gcalcli_visibility = urllib.unquote(match.group(2))
cal.gcalcli_projection = urllib.unquote(match.group(3))
if len(calNames):
for i in xrange(len(calNames)):
if re.search(calNames[i].lower(),
cal.title.text.lower()):
self.cals.append(cal)
cal.colorSpec = calNameColors[i]
else:
self.cals.append(cal)
cal.colorSpec = None
def _CalendarWithinAccess(self, cal):
if self.access == self.ACCESS_ALL:
return True
elif self.access == self.ACCESS_DEFAULT and \
cal.gcalcli_username == self.username:
return True
elif self.access != cal.access_level.value:
return False
else:
return True
def _CalendarColor(self, cal):
if cal == None:
return CLR_NRM()
elif hasattr(cal, 'colorSpec') and cal.colorSpec != None:
return cal.colorSpec
elif cal.access_level.value == self.ACCESS_OWNER:
return self.calOwnerColor
elif cal.access_level.value == self.ACCESS_EDITOR:
return self.calEditorColor
elif cal.access_level.value == self.ACCESS_CONTRIBUTOR:
return self.calContributorColor
elif cal.access_level.value == self.ACCESS_FREEBUSY:
return self.calFreeBusyColor
elif cal.access_level.value == self.ACCESS_READ:
return self.calReadColor
else:
return CLR_NRM()
def _TargetCalendar(self):
if len(self.cals) == 1:
match = re.match('^https://www.google.com(.*)$',
self.cals[0].gcalcli_altLink)
return match.group(1)
else:
return '/calendar/feeds/default/private/full'
def _ValidTitle(self, title):
if title == None:
return "(No title)"
else:
return title
def _GetWeekEventStrings(self, cmd, curMonth,
startDateTime, endDateTime, eventList):
weekEventStrings = [ '', '', '', '', '', '', '' ]
for event in eventList:
if cmd == 'calm' and curMonth != event.s.strftime("%b"):
continue
dayNum = int(event.s.strftime("%w"))
if self.calMonday:
dayNum -= 1
if dayNum < 0:
dayNum = 6
if event.s >= startDateTime and event.s < endDateTime:
if event.s.hour == 0 and event.s.minute == 0 and \
event.e.hour == 0 and event.e.minute == 0:
tmpTimeStr = ''
elif self.military:
tmpTimeStr = event.s.strftime("%H:%M")
else:
tmpTimeStr = \
event.s.strftime("%I:%M").lstrip('0') + \
event.s.strftime('%p').lower()
# newline and empty string are the keys to turn off coloring
weekEventStrings[dayNum] += \
"\n" + \
str(self._CalendarColor(event.gcalcli_cal)) + \
tmpTimeStr.strip() + \
" " + \
self._ValidTitle(event.title.text).strip()
return weekEventStrings
UNIWIDTH = {'W': 2, 'F': 2, 'N': 1, 'Na': 1, 'H': 1, 'A': 1}
def _PrintLen(self, string):
printLen = 0
for tmpChar in string:
printLen += self.UNIWIDTH[east_asian_width(tmpChar)]
return printLen
# return print length before cut, cut index, and force cut flag
def _NextCut(self, string, curPrintLen):
idx = 0
printLen = 0
for tmpChar in string:
if (curPrintLen + printLen) >= self.calWidth:
return (printLen, idx, True)
if tmpChar in (' ', '\n'):
return (printLen, idx, False)
idx += 1
printLen += self.UNIWIDTH[east_asian_width(tmpChar)]
return (printLen, -1, False)
def _GetCutIndex(self, eventString):
printLen = self._PrintLen(eventString)
if printLen <= self.calWidth:
DebugPrint("------ printLen=%d (end of string)\n" % printLen)
return (printLen, len(eventString))
cutWidth, cut, forceCut = self._NextCut(eventString, 0)
DebugPrint("------ cutWidth=%d cut=%d \"%s\"\n" %
(cutWidth, cut, eventString))
if forceCut:
DebugPrint("--- forceCut cutWidth=%d cut=%d\n" % (cutWidth, cut))
return (cutWidth, cut)
DebugPrint("--- looping\n")
while cutWidth < self.calWidth:
DebugPrint("--- cutWidth=%d cut=%d \"%s\"\n" %
(cutWidth, cut, eventString[cut:]))
while cut < self.calWidth and \
cut < printLen and \
eventString[cut] == ' ':
DebugPrint("-> skipping space <-\n")
cutWidth += 1
cut += 1
DebugPrint("--- cutWidth=%d cut=%d \"%s\"\n" %
(cutWidth, cut, eventString[cut:]))
nextCutWidth, nextCut, forceCut = \
self._NextCut(eventString[cut:], cutWidth)
if forceCut:
DebugPrint("--- forceCut cutWidth=%d cut=%d\n" % (cutWidth, cut))
break
cutWidth += nextCutWidth
cut += nextCut
if eventString[cut] == '\n':
break
DebugPrint("--- loop cutWidth=%d cut=%d\n" % (cutWidth, cut))
return (cutWidth, cut)
def _GraphEvents(self, cmd, startDateTime, count, eventList):
# ignore started events (i.e. that start previous day and end start day)
while (len(eventList) and eventList[0].s < startDateTime):
eventList = eventList[1:]
dayDivider = ''
for i in xrange(self.calWidth):
dayDivider += '-'
weekDivider = ''
for i in xrange(7):
weekDivider += '+'
weekDivider += dayDivider
weekDivider += '+'
weekDivider = str(self.borderColor) + weekDivider + str(CLR_NRM())
empty = ''
for i in xrange(self.calWidth):
empty += ' '
dayFormat = '%-' + str(self.calWidth) + '.' + str(self.calWidth) + 's'
# Get the localized day names... January 1, 2001 was a Monday
dayNames = [ date(2001, 1, i+1).strftime('%A') for i in range(7) ]
dayNames = dayNames[6:] + dayNames[:6]
dayHeader = str(self.borderColor) + '|' + str(CLR_NRM())
for i in xrange(7):
if self.calMonday:
if i == 6:
dayName = dayFormat % (dayNames[0])
else:
dayName = dayFormat % (dayNames[i+1])
else:
dayName = dayFormat % (dayNames[i])
dayHeader += str(self.dateColor) + dayName + str(CLR_NRM())
dayHeader += str(self.borderColor) + '|' + str(CLR_NRM())
PrintMsg(CLR_NRM(), "\n" + weekDivider + "\n")
if cmd == 'calm':
m = startDateTime.strftime('%B %Y')
mw = str((self.calWidth * 7) + 6)
mwf = '%-' + mw + '.' + mw + 's'
PrintMsg(CLR_NRM(),
str(self.borderColor) + '|' + str(CLR_NRM()) +
str(self.dateColor) + mwf % (m) + str(CLR_NRM()) +
str(self.borderColor) + '|' + str(CLR_NRM()) + '\n')
PrintMsg(CLR_NRM(), weekDivider + "\n")
PrintMsg(CLR_NRM(), dayHeader + "\n")
PrintMsg(CLR_NRM(), weekDivider + "\n")
curMonth = startDateTime.strftime("%b")
# get date range objects for the first week
if cmd == 'calm':
dayNum = int(startDateTime.strftime("%w"))
if self.calMonday:
dayNum -= 1
if dayNum < 0:
dayNum = 6
startDateTime = (startDateTime - timedelta(days=dayNum))
startWeekDateTime = startDateTime
endWeekDateTime = (startWeekDateTime + timedelta(days=7))
for i in xrange(count):
# create/print date line
line = str(self.borderColor) + '|' + str(CLR_NRM())
for j in xrange(7):
if cmd == 'calw':
d = (startWeekDateTime +
timedelta(days=j)).strftime("%d %b")
else: # (cmd == 'calm'):
d = (startWeekDateTime +
timedelta(days=j)).strftime("%d")
if curMonth != (startWeekDateTime + \
timedelta(days=j)).strftime("%b"):
d = ''
todayMarker = ''
if self.now.strftime("%d%b%Y") == \
(startWeekDateTime + timedelta(days=j)).strftime("%d%b%Y"):
todayMarker = " **"
line += str(self.dateColor) + \
dayFormat % (d + todayMarker) + \
str(CLR_NRM()) + \
str(self.borderColor) + \
'|' + \
str(CLR_NRM())
PrintMsg(CLR_NRM(), line + "\n")
weekColorStrings = [ '', '', '', '', '', '', '' ]
weekEventStrings = self._GetWeekEventStrings(cmd, curMonth,
startWeekDateTime,
endWeekDateTime,
eventList)
# convert the strings to unicode for various string ops
for j in xrange(7):
weekEventStrings[j] = unicode(weekEventStrings[j],
locale.getpreferredencoding())
# get date range objects for the next week
startWeekDateTime = endWeekDateTime
endWeekDateTime = (endWeekDateTime + timedelta(days=7))
while 1:
done = True
line = str(self.borderColor) + '|' + str(CLR_NRM())
for j in xrange(7):
if weekEventStrings[j] == '':
weekColorStrings[j] = ''
line += empty + \
str(self.borderColor) + '|' + str(CLR_NRM())
continue
if weekEventStrings[j][0] == '\033':
# get/skip over color sequence
weekColorStrings[j] = ''
while (weekEventStrings[j][0] != 'm'):
weekColorStrings[j] += weekEventStrings[j][0]
weekEventStrings[j] = weekEventStrings[j][1:]
weekColorStrings[j] += weekEventStrings[j][0]
weekEventStrings[j] = weekEventStrings[j][1:]
if weekEventStrings[j][0] == '\n':
weekColorStrings[j] = ''
weekEventStrings[j] = weekEventStrings[j][1:]
line += empty + \
str(self.borderColor) + '|' + str(CLR_NRM())
done = False
continue
weekEventStrings[j] = weekEventStrings[j].lstrip()
printLen, cut = self._GetCutIndex(weekEventStrings[j])
padding = ' ' * (self.calWidth - printLen)
line += weekColorStrings[j] + \
weekEventStrings[j][:cut] + \
padding + \
str(CLR_NRM())
weekEventStrings[j] = weekEventStrings[j][cut:]
done = False
line += str(self.borderColor) + '|' + str(CLR_NRM())
if done:
break
PrintMsg(CLR_NRM(), line + "\n")
PrintMsg(CLR_NRM(), weekDivider + "\n")
def _tsv(self, startDateTime, eventList):
# tab-separated output for easier shellscripting.
# Format:
# "Date" "start" "end" "Event-Title" "Location" "Eventdescription"
dayFormat = '%F'
for event in eventList:
tmpDayStr = event.s.strftime(dayFormat)
tmpTimeStr = event.s.strftime("%H:%M")
tmpTimeStp = event.e.strftime("%H:%M")
str = "%s\t%s\t%s\t%s\t%s\t%s" % (tmpDayStr, tmpTimeStr, tmpTimeStp, self._ValidTitle(event.title.text).strip(), event.where[0].value_string, event.content.text )
str2 = "%s\n" % str.replace('\n', '''\\n''')
sys.stdout.write(str2)
def _PrintEvents(self, startDateTime, eventList):
if len(eventList) == 0:
PrintMsg(CLR_YLW(), "\nNo Events Found...\n")
return
dayFormat = '\n%a %b %d' # 10 chars for day
indent = ' ' # 10 spaces
detailsIndent = ' ' # 19 spaces
day = ''
for event in eventList:
if self.ignoreStarted and (event.s < startDateTime):
continue
tmpDayStr = event.s.strftime(dayFormat)
if self.military:
timeFormat = '%-5s'
tmpTimeStr = event.s.strftime("%H:%M")
else:
timeFormat = '%-7s'
tmpTimeStr = \
event.s.strftime("%I:%M").lstrip('0').rjust(5) + \
event.s.strftime('%p').lower()
prefix = indent
if tmpDayStr != day: day = prefix = tmpDayStr
PrintMsg(self.dateColor, prefix)
if event.s.hour == 0 and event.s.minute == 0 and \
event.e.hour == 0 and event.e.minute == 0:
fmt = ' ' + timeFormat + ' %s\n'
PrintMsg(self._CalendarColor(event.gcalcli_cal), fmt %
('', self._ValidTitle(event.title.text).strip()))
else:
fmt = ' ' + timeFormat + ' %s\n'
PrintMsg(self._CalendarColor(event.gcalcli_cal), fmt %
(tmpTimeStr, self._ValidTitle(event.title.text).strip()))
if self.details:
clr = CLR_NRM()
if event.where[0].value_string:
str = "%s Location: %s\n" % (detailsIndent,
event.where[0].value_string)
PrintMsg(clr, str)
diffDateTime = (event.e - event.s)
str = "%s Length: %s\n" % (detailsIndent, diffDateTime)
PrintMsg(clr, str)
# XXX Why does accessing event.when[0].reminder[0] fail?
for rem in event.when[0].reminder:
remStr = ''
if rem.days:
remStr += "%s Days" % (rem.days)
if rem.hours:
if remStr != '': remStr += ' '
remStr += "%s Hours" % (rem.hours)
if rem.minutes:
if remStr != '': remStr += ' '
remStr += "%s Minutes" % (rem.minutes)
str = "%s Reminder: %s\n" % (detailsIndent, remStr)
PrintMsg(clr, str)
if event.content.text:
str = "%s Content: %s\n" % (detailsIndent,
event.content.text)
PrintMsg(clr, str)
def _GetAllEvents(self, cal, feed, end):
eventList = []
while 1:
next = feed.GetNextLink()
for event in feed.entry:
event.gcalcli_cal = cal
event.s = parse(event.when[0].start_time)
if event.s.tzinfo == None:
event.s = event.s.replace(tzinfo=tzlocal())
event.e = parse(event.when[0].end_time)
if event.e.tzinfo == None:
event.e = event.e.replace(tzinfo=tzlocal())
# For all-day events, Google seems to assume that the event time
# is based in the UTC instead of the local timezone. Here we
# filter out those events start beyond a specified end time.
if end and (event.s >= end):
continue
# http://en.wikipedia.org/wiki/Year_2038_problem
# Catch the year 2038 problem here as the python dateutil module
# can choke throwing a ValueError exception. If either the start
# or end time for an event has a year '>= 2038' dump it.
if event.s.year >= 2038 or event.e.year >= 2039:
continue
eventList.append(event)
if not next:
break
feed = self.gcal.GetCalendarEventFeed(next.href)
return eventList
def _SearchForCalEvents(self, start, end, searchText):
eventList = []
queue = Queue()
threads = []
def worker(cal, query):
feed = self.gcal.CalendarQuery(query)
queue.put((cal, feed))
for cal in self.cals:
if not self._CalendarWithinAccess(cal):
continue
# see http://code.google.com/apis/calendar/reference.html
if not searchText:
query = CalendarEventQuery(cal.gcalcli_username,
cal.gcalcli_visibility,
cal.gcalcli_projection)
query.start_min = start.isoformat()
query.start_max = end.isoformat()
else:
query = CalendarEventQuery(cal.gcalcli_username,
cal.gcalcli_visibility,
cal.gcalcli_projection,
searchText)
if start: # flagged by --ignore-started
# weeds out old but still pulls in started events
query.futureevents = 'true'
query.singleevents = 'true'
# we sort later after getting events from all calendars
#query.orderby = 'starttime'
#query.sortorder = 'ascending'
th = threading.Thread(target=worker, args=(cal, query))
threads.append(th)
th.start()
for th in threads:
th.join()
while not queue.empty():
cal, feed = queue.get()
eventList.extend(self._GetAllEvents(cal, feed, end))
eventList.sort(lambda x, y: cmp(x.s, y.s))
return eventList
def ListAllCalendars(self):
accessLen = 0
for cal in self.allCals.entry:
length = len(cal.access_level.value)
if length > accessLen: accessLen = length
if accessLen < len('Access'): accessLen = len('Access')
format = ' %0' + str(accessLen) + 's %s\n'
PrintMsg(CLR_BRYLW(), "\n" + format % ('Access', 'Title'))
PrintMsg(CLR_BRYLW(), format % ('------', '-----'))
for cal in self.allCals.entry:
PrintMsg(self._CalendarColor(cal),
format % (cal.access_level.value, cal.title.text))
def TextQuery(self, searchText=''):
# the empty string would get *ALL* events...
if searchText == '':
return
if self.ignoreStarted:
start = self.now # flags gdata futureevents to true
else:
start = None
# convert now to midnight this morning and use for default
defaultDateTime = self.now.replace(hour=0,
minute=0,
second=0,
microsecond=0)
eventList = \
self._SearchForCalEvents(start, None, searchText)
self._PrintEvents(self.now, eventList)
def AgendaQuery(self, startText='', endText=''):
if self.ignoreStarted:
defaultDateTime = self.now
else:
# convert now to midnight this morning and use for default
defaultDateTime = self.now.replace(hour=0,
minute=0,
second=0,
microsecond=0)
if startText == '':
start = defaultDateTime
else:
try:
start = parse(startText, default=defaultDateTime)
except:
PrintErrMsg('Error: failed to parse start time\n')
return
if endText == '':
end = (start + timedelta(days=self.agendaLength))
else:
try:
end = parse(endText, default=defaultDateTime)
except:
PrintErrMsg('Error: failed to parse end time\n')
return
eventList = self._SearchForCalEvents(start, end, None)
if self.tsv:
self._tsv(start, eventList)
else:
self._PrintEvents(start, eventList)
def CalQuery(self, cmd, startText='', count=1):
# convert now to midnight this morning and use for default
defaultDateTime = self.now.replace(hour=0,
minute=0,
second=0,
microsecond=0)
if startText == '':
start = defaultDateTime
else:
try:
start = parse(startText, default=defaultDateTime)
start = start.replace(hour=0, minute=0, second=0, microsecond=0)
except:
PrintErrMsg('Error: failed to parse start time\n')
return
# convert start date to the beginning of the week or month
if cmd == 'calw':
dayNum = int(start.strftime("%w"))
if self.calMonday:
dayNum -= 1
if dayNum < 0:
dayNum = 6
start = (start - timedelta(days=dayNum))
end = (start + timedelta(days=(count * 7)))
else: # cmd == 'calm':
start = (start - timedelta(days=(start.day - 1)))
endMonth = (start.month + 1)
endYear = start.year
if endMonth == 13:
endMonth = 1
endYear += 1
end = start.replace(month=endMonth, year=endYear)
daysInMonth = (end - start).days
offsetDays = int(start.strftime('%w'))
if self.calMonday:
offsetDays -= 1
if offsetDays < 0:
offsetDays = 6
totalDays = (daysInMonth + offsetDays)
count = (totalDays / 7)
if totalDays % 7:
count += 1
eventList = self._SearchForCalEvents(start, end, None)
self._GraphEvents(cmd, start, count, eventList)
def QuickAdd(self, eventText):
if eventText == '':
return
quickEvent = gdata.calendar.CalendarEventEntry()
quickEvent.content = atom.Content(text=eventText)
quickEvent.quick_add = gdata.calendar.QuickAdd(value='true')
try:
self.gcal.InsertEvent(quickEvent, self._TargetCalendar())
except Exception, e:
PrintErrMsg("Error: " + e["reason"] + "\n")
sys.exit(1)
def Remind(self, minutes=10, command=None):
if command == None:
command = self.command
# perform a date query for now + minutes + slip
start = self.now
end = (start + timedelta(minutes=(minutes + 5)))
eventList = self._SearchForCalEvents(start, end, None)
message = ''
for event in eventList:
# skip this event if it already started
# XXX maybe add a 2+ minute grace period here...
if event.s < self.now:
continue
if self.military:
tmpTimeStr = event.s.strftime('%H:%M')
else:
tmpTimeStr = \
event.s.strftime('%I:%M').lstrip('0') + \
event.s.strftime('%p').lower()
message += '%s %s\n' % \
(tmpTimeStr, self._ValidTitle(event.title.text).strip())
if message == '':
return
cmd = shlex.split(command)
for i, a in zip(xrange(len(cmd)), cmd):
if a == '%s':
cmd[i] = message
pid = os.fork()
if not pid:
os.execvp(cmd[0], cmd)
def ImportICS(self, verbose=False, icsFile=None):
try:
import vobject
except:
PrintErrMsg('Python vobject module not installed!\n')
sys.exit(1)
f = sys.stdin
if icsFile:
try:
f = file(icsFile)
except Exception, e:
PrintErrMsg("Error: " + str(e) + "!\n")
sys.exit(1)
while True:
try:
v = vobject.readComponents(f).next()
except StopIteration:
break
ve = v.vevent
event = gdata.calendar.CalendarEventEntry()
if hasattr(ve, 'summary'):
DebugPrint("SUMMARY: %s\n" % ve.summary.value)
if verbose:
print "Event........%s" % ve.summary.value
event.title = atom.Title(text=ve.summary.value)
if hasattr(ve, 'location'):
DebugPrint("LOCATION: %s\n" % ve.location.value)
if verbose:
print "Location.....%s" % ve.location.value
event.where = gdata.calendar.Where(value_string=ve.location.value)
if not hasattr(ve, 'dtstart') or not hasattr(ve, 'dtend'):
PrintErrMsg("Error: event does not have a dtstart and dtend!\n")
continue
DebugPrint("DTSTART: %s\n" % ve.dtstart.value.isoformat())
DebugPrint("DTEND: %s\n" % ve.dtend.value.isoformat())
if verbose:
print "Start........%s" % ve.dtstart.value.isoformat(' ')
print "End..........%s" % ve.dtend.value.isoformat(' ')
print "Local Start..%s" % ve.dtstart.value.astimezone(tzlocal())
print "Local End....%s" % ve.dtend.value.astimezone(tzlocal())
if hasattr(ve, 'rrule'):
DebugPrint("RRULE: %s\n" % ve.rrule.value)
if verbose:
print "Recurrence...%s" % ve.rrule.value
# XXX Need to print a NICE recurrence string
#rr = rrulestr(ve.rrule.value)
#print dir(rr)
#
# In order to add an RRULE using a DTSTART and DTEND in the
# local timezone, there needs to be a TIMEZONE section in the
# recurrence field. Since that is a pain and I'm lazy... as
# a workaround I convert the DTSTART and DTEND to UTC. Google
# handles this properly and keys off the timezone setting of
# the calendar being added to. The event will be shown at the
# correct local time. :-)
#
if False:
# A TIMEZONE section is needed for this to work XXX
recurrence = \
"DTSTART;TZID=" + \
ve.dtstart.value.tzinfo._tzid + ":" + \
ve.dtstart.value.strftime('%Y%m%dT%H%M%S') + \
'\r\n' + \
"DTEND;TZID=" + \
ve.dtend.value.tzinfo._tzid + ":" + \
ve.dtend.value.strftime('%Y%m%dT%H%M%S') + \
'\r\n' + \
"RRULE:" + ve.rrule.value + '\r\n'
else:
ve.dtstart.value -= ve.dtstart.value.utcoffset()
ve.dtstart.value = ve.dtstart.value.replace(tzinfo=None)
ve.dtend.value -= ve.dtend.value.utcoffset()
ve.dtend.value = ve.dtend.value.replace(tzinfo=None)
recurrence = \
"DTSTART:" + \
ve.dtstart.value.strftime('%Y%m%dT%H%M%S') + \
'\r\n' + \
"DTEND:" + \
ve.dtend.value.strftime('%Y%m%dT%H%M%S') + \
'\r\n' + \
"RRULE:" + ve.rrule.value + '\r\n'
DebugPrint("RECURRENCE:\n%s\n" % recurrence)
event.recurrence = \
gdata.calendar.Recurrence(text=recurrence)
elif hasattr(ve, 'dtstart') and hasattr(ve, 'dtend'):
start = ve.dtstart.value.isoformat()
end = ve.dtend.value.isoformat()
event.when = gdata.calendar.When(start_time=start,
end_time=end)
if hasattr(ve, 'description'):
DebugPrint("DESCRIPTION: %s\n" % ve.description.value)
if verbose:
print "Description:\n%s" % ve.description.value
event.content = atom.Content(text=ve.description.value)
if not verbose:
try:
self.gcal.InsertEvent(event, self._TargetCalendar())
except Exception, e:
PrintErrMsg("Error: " + e["reason"] + "\n")
sys.exit(1)
continue
PrintMsg(CLR_YLW(), "[i]mport [s]kip [q]uit: ")
val = raw_input()
if val == 'i':
try:
self.gcal.InsertEvent(event, self._TargetCalendar())
except Exception, e:
PrintErrMsg("Error: " + e["reason"] + "\n")
sys.exit(1)
elif val == 's':
continue
elif val == 'q':
sys.exit(0)
else:
PrintErrMsg('Error: invalid input\n')
sys.exit(1)
def LoadConfig(configFile):
config = RawConfigParser()
config.read(os.path.expanduser(configFile))
return config
def GetConfig(config, key, default):
try:
value = config.get('gcalcli', key)
except:
value = default
if value and value.startswith('`'):
# Value is a shell command
cmd = value.strip()[1:-1]
parts = []
for part in cmd.split():
parts.append(os.path.expanduser(part))
value = subprocess.check_output(parts).strip()
return value
def GetConfigMultiple(config, key, default):
try:
values = config.get('gcalcli', key)
except:
values = default
if values == None:
return [ None ]
valueList = csv.reader([ values ],
delimiter=',',
quotechar='"',
skipinitialspace=True).next()
return valueList
def GetTrueFalse(value):
if value.lower() == 'false': return False
else: return True
def GetColor(value, exitFlag):
colors = { 'default' : CLR_NRM(),
'black' : CLR_BLK(),
'brightblack' : CLR_BRBLK(),
'red' : CLR_RED(),
'brightred' : CLR_BRRED(),
'green' : CLR_GRN(),
'brightgreen' : CLR_BRGRN(),
'yellow' : CLR_YLW(),
'brightyellow' : CLR_BRYLW(),
'blue' : CLR_BLU(),
'brightblue' : CLR_BRBLU(),
'magenta' : CLR_MAG(),
'brightmagenta' : CLR_BRMAG(),
'cyan' : CLR_CYN(),
'brightcyan' : CLR_BRCYN(),
'white' : CLR_WHT(),
'brightwhite' : CLR_BRWHT() }
try:
return colors[value]
except:
if exitFlag:
PrintErrMsg('Error: invalid color name\n')
sys.exit(1)
else:
return None
def GetCalColors(calNames):
calNameColors = []
for idx in xrange(len(calNames)):
i = calNames[idx].rfind('#')
if i != -1:
c = GetColor(calNames[idx][(i+1):], False)
if c:
calNameColors.append(c)
calNames[idx] = calNames[idx][:i]
else:
calNameColors.append(None)
else:
calNameColors.append(None)
return calNames, calNameColors
def BowChickaWowWow():
try:
opts, args = getopt.getopt(sys.argv[1:], "",
["help",
"version",
"config=",
"user=",
"pw=",
"cals=",
"cal=",
"24hr",
"details",
"tsv",
"ignore-started",
"width=",
"mon",
"nc",
"cal-owner-color=",
"cal-editor-color=",
"cal-contributor-color=",
"cal-read-color=",
"cal-freebusy-color=",
"date-color=",
"border-color="])
except getopt.error:
sys.exit(1)
configFile = '~/.gcalclirc'
# look for config file override then load the config file
# we do this first because command line args take precedence
for opt, arg in opts:
if opt == "--config": configFile = arg
cfg = LoadConfig(configFile)
usr = GetConfig(cfg, 'user', None)
pwd = GetConfig(cfg, 'pw', None)
access = GetConfig(cfg, 'cals', 'all')
calNames = GetConfigMultiple(cfg, 'cal', None)
military = GetTrueFalse(GetConfig(cfg, '24hr', 'false'))
details = GetTrueFalse(GetConfig(cfg, 'details', 'false'))
ignoreStarted = GetTrueFalse(GetConfig(cfg, 'ignore-started', 'false'))
calWidth = int(GetConfig(cfg, 'width', '10'))
calMonday = GetTrueFalse(GetConfig(cfg, 'mon', 'false'))
tsv = GetTrueFalse(GetConfig(cfg, 'tsv', 'false'))
calOwnerColor = \
GetColor(GetConfig(cfg, 'cal-owner-color', 'cyan'), True)
calEditorColor = \
GetColor(GetConfig(cfg, 'cal-editor-color', 'green'), True)
calContributorColor = \
GetColor(GetConfig(cfg, 'cal-contributor-color', 'default'), True)
calReadColor = \
GetColor(GetConfig(cfg, 'cal-read-color', 'magenta'), True)
calFreeBusyColor = \
GetColor(GetConfig(cfg, 'cal-freebusy-color', 'default'), True)
dateColor = \
GetColor(GetConfig(cfg, 'date-color', 'yellow'), True)
borderColor = \
GetColor(GetConfig(cfg, 'border-color', 'white'), True)
# fix wokCalNames when not specified in config file
if len(calNames) == 1 and calNames[0] == None:
calNames = []
calNameColors = []
# Process options
for opt, arg in opts:
if opt == "--help":
Usage()
if opt == "--version":
Version()
elif opt == "--user":
usr = arg
elif opt == "--pw":
pwd = arg
elif opt == "--cals":
access = arg
elif opt == "--cal":
calNames.append(arg)
elif opt == "--24hr":
military = True
elif opt == "--details":
details = True
elif opt == "--ignore-started":
ignoreStarted = True
elif opt == "--width":
calWidth = int(arg)
elif opt == "--mon":
calMonday = True
elif opt == "--nc":
CLR.useColor = False
elif opt == "--cal-owner-color":
calOwnerColor = GetColor(arg, True)
elif opt == "--cal-editor-color":
calEditorColor = GetColor(arg, True)
elif opt == "--cal-contributor-color":
calContributorColor = GetColor(arg, True)
elif opt == "--cal-read-color":
calReadColor = GetColor(arg, True)
elif opt == "--cal-freebusy-color":
calFreeBusyColor = GetColor(arg, True)
elif opt == "--date-color":
dateColor = GetColor(arg, True)
elif opt == "--border-color":
borderColor = GetColor(arg, True)
elif opt == "--tsv":
tsv = True
if usr == None:
PrintErrMsg('Error: must specify a username\n')
sys.exit(1)
try:
if pwd == None:
pwd = getpass.getpass("Password: ")
except Exception, e:
PrintErrMsg("Error: " + str(e) + "!\n")
sys.exit(1)
if pwd == None or pwd == '':
PrintErrMsg('Error: must specify a password\n')
sys.exit(1)
if len(args) == 0:
PrintErrMsg('Error: no command (--help)\n')
sys.exit(1)
calNames, calNameColors = GetCalColors(calNames)
gcal = gcalcli(username=usr,
password=pwd,
access=access,
calNames=calNames,
calNameColors=calNameColors,
military=military,
details=details,
ignoreStarted=ignoreStarted,
calWidth=calWidth,
calMonday=calMonday,
calOwnerColor=calOwnerColor,
calEditorColor=calEditorColor,
calContributorColor=calContributorColor,
calReadColor=calReadColor,
calFreeBusyColor=calFreeBusyColor,
dateColor=dateColor,
borderColor=borderColor,
tsv=tsv)
if args[0] == 'list':
gcal.ListAllCalendars()
elif args[0] == 'search':
if len(args) != 2:
PrintErrMsg('Error: invalid search string\n')
sys.exit(1)
# allow unicode strings for input
gcal.TextQuery(unicode(args[1], locale.getpreferredencoding()))
sys.stdout.write('\n')
elif args[0] == 'agenda':
if len(args) == 3: # start and end
gcal.AgendaQuery(startText=args[1], endText=args[2])
elif len(args) == 2: # start
gcal.AgendaQuery(startText=args[1])
elif len(args) == 1: # defaults
gcal.AgendaQuery()
else:
PrintErrMsg('Error: invalid agenda arguments\n')
sys.exit(1)
if not tsv:
sys.stdout.write('\n')
elif args[0] == 'calw':
if not calWidth:
PrintErrMsg('Error: invalid width, don\'t be an idiot!\n')
sys.exit(1)
if len(args) >= 2:
try:
count = int(args[1])
except:
PrintErrMsg('Error: invalid calw arguments\n')
sys.exit(1)
if len(args) == 3: # weeks and start
gcal.CalQuery(args[0], count=int(args[1]), startText=args[2])
elif len(args) == 2: # weeks
gcal.CalQuery(args[0], count=int(args[1]))
elif len(args) == 1: # defaults
gcal.CalQuery(args[0])
else:
PrintErrMsg('Error: invalid calw arguments\n')
sys.exit(1)
sys.stdout.write('\n')
elif args[0] == 'calm':
if not calWidth:
PrintErrMsg('Error: invalid width, don\'t be an idiot!\n')
sys.exit(1)
if len(args) == 2: # start
gcal.CalQuery(args[0], startText=args[1])
elif len(args) == 1: # defaults
gcal.CalQuery(args[0])
else:
PrintErrMsg('Error: invalid calm arguments\n')
sys.exit(1)
sys.stdout.write('\n')
elif args[0] == 'quick':
if len(args) != 2:
PrintErrMsg('Error: invalid event text\n')
sys.exit(1)
# allow unicode strings for input
gcal.QuickAdd(unicode(args[1], locale.getpreferredencoding()))
elif args[0] == 'remind':
if len(args) == 3: # minutes and command
gcal.Remind(int(args[1]), args[2])
elif len(args) == 2: # minutes
gcal.Remind(int(args[1]))
elif len(args) == 1: # defaults
gcal.Remind()
else:
PrintErrMsg('Error: invalid remind arguments\n')
sys.exit(1)
elif args[0] == 'import':
args = args[1:]
verbose = False
if len(args) >= 1 and args[0] == "-v":
verbose = True
args = args[1:]
if len(args) == 0: # stdin
gcal.ImportICS(verbose)
elif len(args) == 1: # ics file
gcal.ImportICS(verbose, args[0])
else:
PrintErrMsg('Error: invalid import arguments\n')
sys.exit(1)
else:
PrintErrMsg('Error: unknown command (--help)\n')
sys.exit(1)
if __name__ == '__main__':
BowChickaWowWow()