#!/usr/bin/python # Ccal - Curses Calendar and todo-list manager. # Copyright (C) 2004 Jamie Hillman. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # The GNU GPL is contained in /usr/doc/copyright/GPL on a Debian # system and in the file COPYING in the Linux kernel source. # # Changes: # # 0.2: added color and fixed the go-to date function made cursor invisible when # drawing, made things a bit flickery # # # Changes: # 0.3: added support for network-based storage of data - via cgi script # much better handling of terminal resizing - works with sizes other than # just vt100 now # # 0.4: now has an UpNext mode - triggered with the u # key also changed the default colours to get rid of the dark blue that's a # problem on some terminals # added todo-list summary to main page # cut-n-paste added (y or d to copy/cut and p to paste) # added ical import (thanks to python-pdi - http://savannah.nongnu.org/projects/python-pdi) # this code is dumped into this one file as I didn't want to ship lots of separate files or have # to require people to download extra libraries. I also did some hacking about to make the parser more # tolerant of deviations from the standard. # imported entries can be prefixed with a string of the users choosing # in order to distinguish them from other entries # added help feature - triggered with ? # warning - i key isn't toggle storage any more, it's import ical. toggle storage is now assigned to the z key # # 0.5 # * colour coded entries to indicate priority etc. press 0-3 to colour code # * upnext has friendly entries for dates - how far they are away (thanks to Nick Blundell) # * can now press x to get a postscript file (to print) summarising forthcoming appointments (thanks to Nick Blundell) (requires latex and dvips) # * now have (e)xtended mode for entries. # when adding an entry enter only e and an editor will be launched. # the first line of the text file is the title and the rest will be stored under # that entry. entries with extra text appear with "(e)" after them. pressing e over these entries (in entry or todo mode) will bring the editor up again so you can edit the entry. # you can also edit existing entries by pressing e. # to set a different editor (default is vim) or different tmp path (default is /tmp) add something like the following to your config file: # # [editing] # temppath=/tmppath # editor=myeditor # export is now on E as this feature has stolen e from it. # # # 0.6 # * fixed some weird behaviour with cut 'n' paste. # * added email-import to todo list (only tested with mutt). you can pipe an email in mutt to ccal and it will be imported as a todo list item with the subject as the title and the message as the extended text of the item. import curses import imaplib,getpass import copy import email import curses.textpad import time import threading import shelve import calendar import datetime import os import sys import httplib, urllib, pickle, urllib2 import ConfigParser from optparse import OptionParser class CursesCal: def __init__(self): parser=OptionParser() parser.add_option("-c","--cal",dest="cal",action="store_true",default=False,help="print todays appointment to the command line and quit") parser.add_option("-t","--todo",dest="todo",action="store_true",default=False,help="print todo list to the command line and quit") parser.add_option("-m","--mutt",dest="mutt",action="store_true",default=False,help="used to pipe a message from mutt to be added to the todo list") (options, args) = parser.parse_args() self.useServerStorage=False rcpath=os.path.expanduser("~")+"/.ccalrc" cp=ConfigParser.ConfigParser() cp.read(rcpath) self.temppath="/tmp" self.editor="vim" self.newcount=0 try: self.temppath=cp.get("editing","temppath") self.editor=cp.get("editing","editor") self.cgiURL=cp.get("database","cgiURL") self.useServerStorage=True except Exception,e: pass if options.cal or options.todo: self.printTodaysAppointments(todo=options.todo,cal=options.cal) sys.exit() self._cal=Calendar(self) self._cal.loadDatabase() if options.mutt: pipeinput="" line=sys.stdin.read() while (line!=""): pipeinput+=line line=sys.stdin.read() msg=email.message_from_string(pipeinput) title=msg['subject'] body="" for part in msg.walk(): if part.get_content_type()=="text/plain": body+=part.get_payload() body=body.split("\n") if not title==None and not title=="": self._cal.addToImportList(ccalItem(title,fullentry=body)) sys.exit(0) else: print "Couldn't parse email!" sys.exit(1) self.scr=curses.initscr() rows, cols = self.scr.getmaxyx() if rows<24 or cols<80: print "Your terminal is not big enough!" sys.exit() curses.start_color() try: curses.curs_set(0) except: pass curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK) # date, title and NextUp dates curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK) curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_WHITE) curses.noecho() curses.cbreak() self.scr.keypad(1) self.mode=0 # 0,1 or 2 - - 0 for browsing dates, 1 for manipulating entries, 2 for todo list self.previousMode=0 self.selected=0 #selected diary entry self.pasteText="" self.running=True self.scr.timeout(5000) def printTodaysAppointments(self,todo=False,cal=False): self._cal=Calendar(self) self._cal.loadDatabase(withcurses=False) print "ccal entries:\n" if cal: print "Calendar:\n" for item in self._cal.getItems(): if not str(item.__class__)=="__main__.ccalItem": item=ccalItem(item) print item.entry print "\n" if todo: print "Todo list:\n" entries=self._cal.getItems("todolist") if entries!=None: for entry in entries: if not str(entry.__class__)=="__main__.ccalItem": entry=ccalItem(entry) print entry.entry def destroy(self): self.running=False curses.nocbreak() self.scr.keypad(0) self._cal.destroy() curses.echo() curses.endwin() def setItemTypeForSelected(self,type): entries=self._cal.getItems() if entries==None: return try: self._cal.setItemType(self.selected,type) except exception,e: pass def errorMessage(self,message): rows, cols = self.scr.getmaxyx() self.addstr(rows-2,1,message) self.scr.refresh() curses.beep() time.sleep(1) def addstr(self,y,x,string,*args): try: if len(args)!=0: self.scr.addstr(y,x,string,args[0]) else: self.scr.addstr(y,x,string) except Exception,e : self.handleException(e) def handleException(self,e): if str(e.__class__)!="_curses.error": curses.nocbreak() self.scr.keypad(0) curses.echo() curses.endwin() print e sys.exit() def drawTitleBar(self): self.addstr(0,1,"CursesCal - by Jamie Hillman",curses.color_pair(2)) rows, cols = self.scr.getmaxyx() self.addstr(0,cols-len(self._cal.currentdatestring),str(self._cal.currentdatestring),curses.color_pair(2)) def drawFooter(self): rows, cols = self.scr.getmaxyx() self.addstr(rows-1,0," "+str(self.newcount)+" new messages in inbox") def drawCurrentDayTitle(self): self.addstr(1,1,"Currently Viewing: "+self._cal.viewdatestring,curses.color_pair(1)) def drawMode(self): rows, cols = self.scr.getmaxyx() if self.mode==0: self.addstr(rows-1,cols-12,"Mode: Date") elif self.mode==1: self.addstr(rows-1,cols-12,"Mode: Entry") elif self.mode==2: self.addstr(rows-1,cols-12,"Mode: To-do") elif self.mode==3: self.addstr(rows-1,cols-12,"Mode: NextUp") if self._cal.useServerStorage: self.addstr(1,1,"Using internet database") else: self.addstr(1,1,"Using local database") def drawCalendar(self): rows, cols = self.scr.getmaxyx() startx=cols-23 ypos=6 xpos=startx caltext=calendar.month(self._cal.viewtime[0],self._cal.viewtime[1]).split("\n") self.addstr(4,startx,caltext[0]) self.addstr(5,startx,caltext[1]) for line in caltext[2:]: day=str(int(self._cal.viewtime[2])) offset=0 for i in range(7): item=line[int(offset):int(offset+2)] offset+=3 dayval=0 try: dayval=int(item) except: pass if dayval==int(day): self.addstr(ypos,xpos,item,curses.color_pair(4)) else: if dayval!=0 and self._cal.hasItems(dayval,self._cal.viewtime[1],self._cal.viewtime[0]): self.drawEntry(ypos,xpos,item,True) else: self.drawEntry(ypos,xpos,item,False) xpos+=3 xpos=startx ypos+=1 def main(self): while 1: #render self.scr.clear() self.drawTitleBar() #self.drawFooter() self.drawEntries() self.drawMode() if self.mode!=2: self.drawCalendar() self.drawTodoSidebar() try: self.scr.move(23,0) except Exception,e: self.handleException(e) #refresh self.scr.refresh() #get and handle keypress char=-1 rows, cols = self.scr.getmaxyx() prevrows=rows prevcols=cols while char==-1: char=self.scr.getch() if self._cal.readImports(): char=1 self._cal.updateCurrentTime() if self.handleKey(char)!=None: break def selectDOWN(self): entries=self._cal.getItems() if entries==None: return size=len(entries) if (self.selected+1)==size: return self.selected+=1 def selectUP(self): entries=self._cal.getItems() if entries==None: return if self.selected==0: return self.selected-=1 def deleteSelected(self): entries=self._cal.getItems() if entries==None: return try: self.pasteText=entries[self.selected] self._cal.deleteItem(self.selected) except exception,e: pass self.selected-=1 if self.selected<0: self.selected=0 # Nick def friendlyDateTimeDelta(self, dateTimeDelta) : daysLimit = 1*7 weeksLimit = 8*7 # Calculate a friendly time delta string. friendlyTimeDelta = "" if dateTimeDelta.days == 0 : friendlyTimeDelta = "today" elif dateTimeDelta.days == 1 : friendlyTimeDelta = "tommorow" elif dateTimeDelta.days == 2 : friendlyTimeDelta = "day after tommorow" elif dateTimeDelta.days > 2 and dateTimeDelta.days < daysLimit : friendlyTimeDelta = "in " + str(dateTimeDelta.days) + " days" elif dateTimeDelta.days >= daysLimit and dateTimeDelta.days < weeksLimit : weeks = dateTimeDelta.days / 7 days = dateTimeDelta.days - 7*weeks if weeks == 1 : friendlyTimeDelta = "in "+str(weeks)+" week" elif weeks > 1 : friendlyTimeDelta = "in "+str(weeks)+" weeks" if days == 1 : friendlyTimeDelta += " and "+str(days)+" day" elif days > 1 : friendlyTimeDelta += " and "+str(days)+" days" elif dateTimeDelta.days > weeksLimit : weeks = dateTimeDelta.days / 7 friendlyTimeDelta = "in "+str(weeks)+" weeks" return friendlyTimeDelta def drawEntries(self) : if self.mode == 3 : self.drawNextUpEntries(120) else: self.drawTodaysEntries() def createEntriesPostscriptFile(self, nextNDays=360): # Check we have latex and dvips installed. # TODO: Maybe allow a simple text file to be written. if os.system("which latex &> /dev/null") > 1 or os.system("which dvips &> /dev/null") > 1 : self.errorMessage("Couldn't find latex and/or dvips") return # Set a temporary filename for the latex file. tempDiaryFile = "ccal-diary-temp" # Hardcoded latex template. latexTemplate = "\\documentclass[a4paper,10pt,twocolumn]{article}\n\ \\topmargin=-1.75cm\n\ \\textheight=25.5cm\n\ \\textwidth=13.7cm\n\ \\oddsidemargin=1cm\n\ \\evensidemargin=1cm\n\ \\date{Entries from: "+self._cal.currentdatestring+"}\n\ \\title{Diary Entries (produced by ccal\\footnote{ccal by Jamie Hillman}~)}\n\ \\begin{document}\n\ \\maketitle\n\ \\begin{scriptsize}\n\ \\begin{raggedright}\n\ \n\ \\end{raggedright}\n\ \\end{scriptsize}\n\ \\end{document}" # Initialise diary entries. diaryEntries = "" # Add the diary entries to the Latex template. for i in range(nextNDays) : viewtime = (datetime.datetime(self._cal.viewtime[0],self._cal.viewtime[1],self._cal.viewtime[2])+datetime.timedelta(days=i)).timetuple() entries=self._cal.getItems(viewtime) if entries!=None and len(entries) > 0: dateString = time.strftime(self._cal.dateformat,viewtime) diaryEntries += "\\textbf{"+dateString+"\\\\}\n" for entry in entries: if not str(entry.__class__)=="__main__.ccalItem": entry=ccalItem(entry) diaryEntries += "~~"+entry.entry+"\\\\" diaryEntries += "\\ \\\\" # If there were no entries, say so. if diaryEntries == "" : diaryEntries = "No diary entries!" # Write the latex file. latexFile = open(tempDiaryFile+".tex", "w") latexFile.write(latexTemplate.replace("", diaryEntries)) latexFile.close() # Compile the latex file. os.system("latex "+tempDiaryFile+".tex &> /dev/null") # Generate postscript file in the user's home directory. os.system("dvips -f "+tempDiaryFile+".dvi 1> "+"~/"+tempDiaryFile[0:tempDiaryFile.rfind("-temp")]+".ps" " 2> /dev/null") # Test it with xdvi. #os.system("xdvi "+tempDiaryFile+".dvi &> /dev/null") # Delete temporary files. os.system("rm "+tempDiaryFile+"* &> /dev/null") #self.scr.getch() self.errorMessage("File written!") def drawNextUpEntries(self, nextNDays=0): rows, cols = self.scr.getmaxyx() xpos=2 ypos=4 for i in range(nextNDays) : if ypos >= rows-3: break viewtime = (datetime.datetime(self._cal.viewtime[0],self._cal.viewtime[1],self._cal.viewtime[2])+datetime.timedelta(days=i)).timetuple() entries=self._cal.getItems(viewtime) if entries!=None and len(entries) > 0: dateString = time.strftime(self._cal.dateformat,viewtime) dateString += " ("+self.friendlyDateTimeDelta(datetime.datetime(viewtime[0], viewtime[1], viewtime[2]) - datetime.datetime(self._cal.localtime[0],self._cal.localtime[1],self._cal.localtime[2]))+")" if i==0: self.addstr(ypos,xpos,dateString+":",curses.color_pair(2)) else: self.addstr(ypos,xpos,dateString+":",curses.color_pair(3)) ypos+=1 for entry in entries: self.drawEntry(ypos,xpos+1,entry,False) ypos+=1 ypos+=1 if ypos >= rows-3: break def drawEntry(self,ypos,xpos,entry,selected,maxEntryLength=0): rows, cols = self.scr.getmaxyx() if not str(entry.__class__)=="__main__.ccalItem": entry=ccalItem(entry) if maxEntryLength==0: maxEntryLength = cols - 30 decoration=curses.A_NORMAL try: entryDisplay = entry.entry[0:maxEntryLength] except: entryDisplay=entry.entry if hasattr(entry,"fullentry") and entry.fullentry!="": entryDisplay+=" (e)" if len(entry.entry) > maxEntryLength : entryDisplay += ".." if selected: decoration=decoration+curses.A_BOLD if entry.type==1: decoration=decoration+curses.color_pair(1) elif entry.type==2: decoration=decoration+curses.color_pair(2) elif entry.type==3: decoration=decoration+curses.color_pair(3) elif entry.type==4: decoration=decoration+curses.A_BLINK self.addstr(ypos,xpos,str(entryDisplay),decoration) def drawTodaysEntries(self): rows, cols = self.scr.getmaxyx() if self.mode==2: dateString="ToDo" else: dateString = time.strftime(self._cal.dateformat,self._cal.viewtime) self.addstr(4,2,dateString+":",curses.color_pair(2)) entries=self._cal.getItems() xpos=2 ypos=5 maxEntryLength = cols - 30 num=0 if entries!=None: for entry in entries: selected=False if num==self.selected and self.mode!=0: selected=True self.drawEntry(ypos,xpos+1,entry,selected) ypos+=1 num+=1 def drawTodoSidebar(self): rows, cols = self.scr.getmaxyx() entries=self._cal.getItems("todolist") xpos=cols-23 ypos=15 self.addstr(ypos-1,xpos,"Todo:",curses.A_BOLD) if entries!=None: for entry in entries: self.drawEntry(ypos,xpos,entry,False,maxEntryLength=16) ypos+=1 if (ypos==(rows-2)): return def handleKey(self,char): if self.mode==0 or self.mode ==3: if char==ord("/"): self.setNewDate() if char==ord("a"): self.addNewEntry() if char==ord("n"): self._cal.setViewTimeToCurrent() if char==ord("p"): if self.pasteText!="": if str(self.pasteText.__class__)=="__main__.ccalItem": self._cal.addItem(copy.deepcopy(self.pasteText)) else: self._cal.addItem(ccalItem(self.pasteText)) if self.mode==0: if char==curses.KEY_LEFT: self._cal.setViewToPreviousDay() self.selected=0 if char==curses.KEY_RIGHT: self._cal.setViewToNextDay() self.selected=0 if char==curses.KEY_UP: self._cal.setViewToPreviousMonth() self.selected=0 if char==curses.KEY_DOWN: self._cal.setViewToNextMonth() self.selected=0 if char==ord("t"): self.previousMode=self.mode self.mode=2 self.selected=0 self._cal.todoList() if char==ord("u"): self.mode=3 self.previousMode=self.mode elif self.mode == 3: if char==curses.KEY_UP: self._cal.setViewToPreviousUsedDay() self.selected=0 if char==curses.KEY_DOWN: self._cal.setViewToNextUsedDay() self.selected=0 if char==ord("u"): self.mode=0 if char==ord("t"): self.previousMode=self.mode self.mode=2 self._cal.todoList() else: if char==curses.KEY_UP: self.selectUP() if char==ord("a"): self.addNewEntry() if char==curses.KEY_DOWN: self.selectDOWN() if char==ord("d"): self.deleteSelected() if char==ord("0"): self.setItemTypeForSelected(0) if char==ord("1"): self.setItemTypeForSelected(1) if char==ord("2"): self.setItemTypeForSelected(2) if char==ord("3"): self.setItemTypeForSelected(3) if char==ord("4"): self.setItemTypeForSelected(4) if char==ord("e"): self.editSelected() if char==ord("y"): curses.beep() entries=self._cal.getItems() self.pasteText=entries[self.selected] if char==ord("p"): if self.pasteText!="": if str(self.pasteText.__class__)=="__main__.ccalItem": self._cal.addItem(copy.deepcopy(self.pasteText)) else: self._cal.addItem(ccalItem(self.pasteText)) if self.mode==2 and char==ord("t"): self.mode=self.previousMode self._cal.restorePreviousDate() if char==ord("q"): self.destroy() return 1 if char==ord("s"): self._cal.saveDatabase() if char==ord("l"): self._cal.loadDatabase() if char==ord("b"): self._cal.toggleStorage() if char==ord("?"): self.displayHelp() if char==ord("i"): curses.echo() rows, cols = self.scr.getmaxyx() self.addstr(rows-2,1,"Enter filename of iCal to import:") try: curses.curs_set(1) except: pass filename=str(self.scr.getstr(rows-1,1,50)) self.addstr(rows-2,1,"Enter string to prepend in front of entries:") self.addstr(rows-1,1," ") prepend=str(self.scr.getstr(rows-1,1,50)) try: curses.curs_set(0) except: pass curses.noecho() try: self._cal.importIcal(prepend,filename) except Exception,e: self.addstr(rows-3,1,"Error importing iCal file - "+str(e)) curses.beep() self.scr.refresh() time.sleep(2) if char==ord("E"): curses.echo() rows, cols = self.scr.getmaxyx() self.addstr(rows-2,1,"Enter filename to export to:") try: curses.curs_set(1) except: pass filename=str(self.scr.getstr(rows-1,1,50)) result=self._cal.exportIcal(filename) if result!=None: curses.beep() self.addstr(rows-3,1,str(result)) try: curses.curs_set(0) except: pass curses.noecho() if char==ord("x"): self.createEntriesPostscriptFile() if char==10: if self.mode==1: self.mode=self.previousMode else: if self.mode==2: self.mode=0 self.mode=self.previousMode self._cal.restorePreviousDate() else: self.previousMode=self.mode self.mode=1 def displayHelp(self): self.scr.clear() rows, cols = self.scr.getmaxyx() self.addstr(1,1,"Global commands:",curses.color_pair(2)) self.addstr(2,1,"s - saves database") self.addstr(3,1,"l - loads database") self.addstr(4,1,"b - changes backing store") self.addstr(5,1,"i - imports external calendar") self.addstr(6,1,"E - exports calendar") self.addstr(7,1,"q - quits") self.addstr(1,40,"Mode switches:",curses.color_pair(2)) self.addstr(2,40,"t - toggles todo mode") self.addstr(3,40,"u - toggles up next mode") self.addstr(4,40,"Enter - toggles Entry/date mode") self.addstr(9,1,"Date mode keys:",curses.color_pair(2)) self.addstr(10,1,"left, right - previous,next day") self.addstr(11,1,"up, down - previous, next month") self.addstr(12,1,"n - move calendar to now") self.addstr(13,1,"/ - move to specific date") self.addstr(14,1,"a - add an entry to the current date") self.addstr(15,1,"p - paste to the current date") self.addstr(9,40,"Up-next mode keys:",curses.color_pair(2)) self.addstr(10,40,"up,down - scroll up/down through entries") self.addstr(11,40,"enter - edit selected day's appointments") self.addstr(12,40,"n - move view to now") self.addstr(13,40,"/ - move to specific date") self.addstr(14,40,"a - add an entry to the current date") self.addstr(15,40,"p - paste to the current date") self.addstr(17,1,"Entry & Todo mode keys:",curses.color_pair(2)) self.addstr(18,1,"p - paste") self.addstr(19,1,"y - copy selected entry") self.addstr(20,1,"d - cut/delete selected entry") self.addstr(20,40,"e - edit selected entry in editor") self.addstr(21,1,"a - add an entry") self.addstr(21,40,"0-3 - change colour of entry") self.addstr(rows-1,(cols/2)-13,"Press any key to continue") self.scr.refresh() char=self.scr.getch() def addNewEntry(self): curses.echo() rows, cols = self.scr.getmaxyx() self.addstr(rows-2,1,"Please enter an item to add:") try: curses.curs_set(1) except: pass entry=str(self.scr.getstr(rows-1,1,50)) fullentry="" # if it's a longer editor-based entry then launch an editor import time temptime=str(int(time.time())) if entry=="e": try: curses.curs_set(0) except: pass os.system(self.editor+" "+self.temppath+"/"+temptime) tempfile=open(self.temppath+"/"+temptime) tempstring=tempfile.read() templines=tempstring.split("\n") entry=templines[0] #re-initialise stuff as vim seems to mess it up self.scr=curses.initscr() self.scr.keypad(1) self.scr.clear() self.drawTitleBar() self.drawEntries() self.drawMode() if self.mode!=2: self.drawCalendar() self.drawTodoSidebar() try: self.scr.move(23,0) except Exception,e: self.handleException(e) #refresh self.scr.refresh() fullentry=templines[1:] try: curses.curs_set(0) except: pass if entry!="": self._cal.addItem(ccalItem(entry,fullentry=fullentry)) curses.noecho() def setNewDate(self): rows, cols = self.scr.getmaxyx() self.addstr(rows-2,1,"Enter date in DD/MM/YYYY Format") curses.echo() date=self.scr.getstr(rows-1,1,10) try: day=int(date[0:2]) month=int(date[3:5]) year=int(date[6:10]) curses.noecho() self._cal.setViewTime(day,month,year) except Exception: pass def editSelected(self): #if it has text already then try: entry=self._cal.getItems()[self.selected] if not str(entry.__class__)=="__main__.ccalItem": entry=ccalItem(entry) import time temptime=str(int(time.time())) tempfile=open(self.temppath+"/"+temptime,"w") tempfile.write(entry.entry+"\n") if hasattr(entry,"fullentry") and entry.fullentry!="": for line in entry.fullentry: tempfile.write(str(line)+"\n") tempfile.close() try: curses.curs_set(1) except: pass os.system(self.editor+" "+self.temppath+"/"+temptime) tempfile=open(self.temppath+"/"+temptime) tempstring=tempfile.read() templines=tempstring.split("\n") entry.entry=templines[0] if templines[len(templines)-1]=="": entry.fullentry=templines[1:-1] else: entry.fullentry=templines[1:] if entry.fullentry==[]: entry.fullentry="" self.scr=curses.initscr() try: curses.curs_set(1) curses.curs_set(0) except: pass self.scr.keypad(1) self.scr.clear() except: self.errorMessage("Can't edit this entry!") class ccalItem: def __init__(self,entry,time=0,duration=0,fullentry=""): self.entry=entry self.type=0 self.time=time self.duration=0 self.fullentry=fullentry class Calendar: def __init__(self,parent): self.parent=parent self.path=os.path.expanduser("~")+"/.ccaldb" self.store={} self.dateformat="%A %d %b %Y" self.updateCurrentTime() self.setViewTimeToCurrent() self.previous=None self.useServerStorage=self.parent.useServerStorage def toggleStorage(self): self.useServerStorage=not self.useServerStorage def loadDatabase(self,withcurses=True): if self.useServerStorage: if withcurses: self.parent.errorMessage("Loading remote database..") params = urllib.urlencode({'command':'get'}) try: f=urllib2.urlopen(self.parent.cgiURL, params) data = f.read() pic=str(data).strip() if pic=="": self.store={} else: obj=pickle.loads(pic) self.store=obj except Exception: if withcurses: self.parent.errorMessage("Couldn't load remote database") else: print "Couldn't load remote database" self.toggleStorage() self.loadDatabase() return else: shelf=shelve.open(self.path) self.store={} for key in shelf.keys(): self.store[key]=shelf[key] shelf.close() def importIcal(self,prepend,file): #dict=parseIcalFile("/home/hillman/dev/pdi/Bank32Holidays.ics") dict=parseIcalFile(file) for key in dict: self.viewtime=key self.addItem(ccalItem(prepend+" "+str(dict[key]))) self.setViewTimeToCurrent() def exportIcal(self,filename): try: file=open(filename,"w") except Exception,e: return e ical=ICalendar() ical.addProperties([UnknownProperty('PRODID', '1234-50'), UnknownProperty('VERSION', '2.0'), UnknownProperty('CALSCALE', 'GREGORIAN')]) for date in self.store: entry=self.store[date] date=date.strip("()") tuple=date.split(",") pos=0 try: for item in tuple: item=item.strip() item=int(item) tuple[pos]=item pos+=1 timestring=time.strftime("%Y%m%d",tuple) except Exception,e: pass for item in entry: event = ical.addComponent(VEvent()) event.addProperties([UnknownProperty("DTSTART",timestring,value="DATE"),UnknownProperty("SUMMARY",item)]) if self.store.has_key("todolist"): for todoitem in self.store["todolist"]: todo=ical.addComponent(VTodo()) todo.addProperties([UnknownProperty("SUMMARY",todoitem),]) ical.validate() file.write(str(ical)) file.close() def saveDatabase(self): if self.useServerStorage: self.parent.errorMessage("Saving database remotely..") params = urllib.urlencode({'command':'store','pickle':pickle.dumps(self.store)}) try: f=urllib2.urlopen(self.parent.cgiURL, params) except Exception: self.parent.errorMessage("Couldn't save remote database") self.toggleStorage() self.loadDatabase() return data = f.read() if data.strip()!="DONE": raise Exception("database wasn't saved by cgi script") self.parent.errorMessage("Saved ") else: shelf=shelve.open(self.path) for key in shelf.keys(): del shelf[key] for key in self.store.keys(): shelf[key]=self.store[key] shelf.close() def todoList(self): self.previous=self.viewtime self.viewdatestring="To-do list" self.viewtime="todolist" def addToImportList(self,item): shelf=shelve.open(self.path+"Imports") if shelf.has_key("importlist"): importlist=shelf["importlist"] importlist.append(item) shelf["importlist"]=importlist else: list=[item,] shelf["importlist"]=list shelf.close() def restorePreviousDate(self): self.viewtime=self.previous self.updateViewTimeString() def updateCurrentTime(self): self.localtime=time.localtime() self.currentdatestring=time.strftime(self.dateformat) def setViewTime(self,day,month,year): dt=datetime.datetime(year,month,day) self.viewtime=dt.timetuple() self.updateViewTimeString() def updateViewTimeString(self): self.viewdatestring=time.strftime(self.dateformat,self.viewtime) def setViewToNextDay(self): self.viewtime=(datetime.datetime(self.viewtime[0],self.viewtime[1],self.viewtime[2])+datetime.timedelta(days=1)).timetuple() self.updateViewTimeString() def setViewToNextUsedDay(self): count=0 viewtime=(datetime.datetime(self.viewtime[0],self.viewtime[1],self.viewtime[2])+datetime.timedelta(days=1)).timetuple() while not self.store.has_key(str(viewtime)) or len(self.store[str(viewtime)])==0: count+=1 if count==100: curses.beep() break viewtime=(datetime.datetime(viewtime[0],viewtime[1],viewtime[2])+datetime.timedelta(days=1)).timetuple() if count!=100: self.viewtime=viewtime self.updateViewTimeString() def setViewToPreviousUsedDay(self): count=0 viewtime=(datetime.datetime(self.viewtime[0],self.viewtime[1],self.viewtime[2])-datetime.timedelta(days=1)).timetuple() while not self.store.has_key(str(viewtime)) or len(self.store[str(viewtime)])==0: count+=1 if count==100: curses.beep() break viewtime=(datetime.datetime(viewtime[0],viewtime[1],viewtime[2])-datetime.timedelta(days=1)).timetuple() if count!=100: self.viewtime=viewtime self.updateViewTimeString() def setViewToPreviousDay(self): self.viewtime=(datetime.datetime(self.viewtime[0],self.viewtime[1],self.viewtime[2])-datetime.timedelta(days=1)).timetuple() self.updateViewTimeString() def setViewToNextMonth(self): self.viewtime=(datetime.datetime(self.viewtime[0],self.viewtime[1],self.viewtime[2])+datetime.timedelta(days=30)).timetuple() self.updateViewTimeString() def setViewToPreviousMonth(self): self.viewtime=(datetime.datetime(self.viewtime[0],self.viewtime[1],self.viewtime[2])-datetime.timedelta(days=30)).timetuple() self.updateViewTimeString() def setViewTimeToCurrent(self): self.setViewTime(self.localtime[2],self.localtime[1],self.localtime[0]) def destroy(self): self.saveDatabase() def update(self,viewtime=0): if viewtime==0: viewtime=self.viewtime self.store[str(viewtime)]=self.store[str(viewtime)] def readImports(self): path=os.path.expanduser("~")+"/.ccaldbImports" shelf=shelve.open(path) foundentry=False if shelf.has_key("importlist"): importlist=shelf["importlist"] while len(importlist)!=0: foundentry=True item=importlist.pop() if not self.store.has_key("todolist"): self.store["todolist"]=[] list=self.store["todolist"] list.append(item) self.store["todolist"]=list shelf["importlist"]=importlist shelf.close() return foundentry def addItem(self,item): if self.store.has_key(str(self.viewtime)): list=self.store[str(self.viewtime)] list.append(item) self.store[str(self.viewtime)]=list else: self.store[str(self.viewtime)]=[item,] def getItems(self, viewtime=0): if viewtime == 0: viewtime = self.viewtime if self.store.has_key(str(viewtime)): newlist=[] for item in self.store[str(viewtime)]: if str(item.__class__)!="__main__.ccalItem": item=ccalItem(item) newlist.append(item) self.store[str(viewtime)]=newlist return self.store[str(viewtime)] else: return None def hasItems(self,day,month,year): try: dt=datetime.datetime(year,month,day) key=dt.timetuple() return (self.store.has_key(str(key)) and len(self.store[str(key)])!=0) except Exception,e: print e sys.exit() def deleteItem(self,pos): try: if not self.store.has_key(str(self.viewtime)): return list=self.store[str(self.viewtime)] del list[pos] self.store[str(self.viewtime)]=list except Exception: pass def setItemType(self,pos,type): try: if not self.store.has_key(str(self.viewtime)): return list=self.store[str(self.viewtime)] list[pos].type=type self.store[str(self.viewtime)]=list except Exception: pass CRLF = "\n" RULE_MUST = 10 RULE_MAY = 20 RULE_RECOMMENDED = 30 RULE_NOT = 40 class ParseError(Exception): """If a parse error occurs there's a real problem in the data.""" def __init__(self, compName, message, lineNumber): """ @type compName: string @param compName: A string describing in what component this error occured. @type message: string @param message: A brief message explaining what went wrong. @type lineNumber: number @param lineNumber: On what line number the problem occured. This value can be fetched from a L{pdi.core.ParseError}. """ Exception.__init__(self, "Parse error, %s, in component '%s' starting on line %s"%(message, compName, lineNumber)) self.lineNumber = lineNumber class ComponentError(Exception): """General component error class.""" def __init__(self, compName, message, lineNumber): """ @type compName: string @param compName: A string describing in what component this error occured. @type message: string @param message: A brief message explaining what went wrong. @type lineNumber: number @param lineNumber: On what line number the problem occured. This value can be fetched from a L{pdi.core.ParseError}. """ Exception.__init__(self, "%s '%s' on line %s"%(message, compName, lineNumber)) self.lineNumber = lineNumber class MissingComponentError(ComponentError): """A mandatory component is missing.""" def __init__(self, compName, subCompName, lineStart, lineEnd): """ @type compName: string @param compName: A string describing in what component this error occured. @param lineStart: The component that raised this exception starts on this line number. @param lineEnd: The component that raised this exception ends on this line number. """ Exception.__init__(self, "Mandatory component '%s' is missing in component '%s' on line %s to %s" %(subCompName, compName, lineStart, lineEnd) ) self.lineNUmber = lineStart class InvalidComponentError(ComponentError): """A component has been placed where it's not allowed to be.""" def __init__(self, compName, lineNumber): """ @type compName: string @param compName: A string describing in what component this error occured. @type lineNumber: number @param lineNumber: On what line number the problem occured. This value can be fetched from a L{pdi.core.ParseError}. """ ComponentError.__init__(self, compName, "Invalid component", lineNumber) class PropertyError(Exception): """All property related errrors should inherit from this class.""" def __init__(self, message): """ @type message: string @param message: A brief message describing what went wrong. """ Exception.__init__(self, message) class MissingPropertyError(PropertyError): """A mandatory property is missing.""" def __init__(self, propertyName, compName, lineStart, lineEnd): """ @type propertyName: string @param propertyName: A string describing what property is missing. @type compName: string @param compName: A string describing in what component this error occured. """ PropertyError.__init__(self, "Missing mandatory property '%s' in component '%s' on lines %s to %s" %(propertyName, compName, lineStart, lineEnd)) class PropertyValueError(PropertyError): """The value of the property is malformed or of unknown type. Either way, the value is invalid.""" def __init__(self, property): """ @type property: L{pdi.core.Property} @param property: The property that raised this exception. """ PropertyError.__init__(self, "Invalid value for property '%s' on line %s"%(property.name, property.lineNumber)) class InvalidPropertyError(PropertyError): """The property is not allowed in that component.""" def __init__(self, property): """ @type property: L{pdi.core.Property} @param property: The property that raised this exception. """ PropertyError.__init__(self, "Invalid property '%s' on line %s"%(property.name, property.lineNumber)) class InvalidPropertyValueError(PropertyError): """You tried to set an invalid value for the property.""" def __init__(self, property, value): """ @type property: L{pdi.core.Property} @param property: The property that raised this exception. @type value: string @param value: The invalid value. """ PropertyError.__init__(self, "Invalid value '%s' for property '%s' on line %s"%(value, property.name, property.lineNumber)) class InvalidComponentWarning(UserWarning): """The component is not mandatory, recommended nor presumed (may).""" class MissingComponentWarning(UserWarning): """The component is when recommended components are not found.""" class MissingPropertyWarning(UserWarning): """A recommended property is missing.""" class InvalidPropertyWarning(UserWarning): """A property that is not mandatory, recommended nor presumed (may) has been found or it has invalid syntax.""" class InvalidPropertyTypeWarning(UserWarning): """A property has a defined type that isn't registered.""" class InvalidPropertyContentWarning(UserWarning): """The content of the property is invalid according to the property's validation method.""" class Property(object): """All properties must inherit from this class. An instance of this class should be treated as an unknown property.""" def __init__(self, name, content, value="", encoding="", type="", lineNumber = None): """ @type name: string @param name: The name of this property (not type). @type content: string @param content: The raw content fetched directly from the input data. @type encoding: string @param encoding: The encoding used to store the raw content. @type type: string @param type: Type of content. @type lineNumber: number @param lineNumber: The line on which this property was found. """ self.name = name self.content = content self.encoding = encoding self.type = type self.value = value self.lineNumber = lineNumber self.validate() def _validate(self, content): """ Internal method for validation. Override this for your own validation. You should issue warnings in this method. Exceptions are disliked, if your value is really out of whack you may set it to None. @type content: string @param content: The raw content fetched from data. @rtype: boolean @return: None or zero if invalid value. """ return 1 def validate(self): """ Validates the property content. Issues warnings. @rtype: boolean @return: None or zero if invalid content. """ return self._validate(self.content) def invalidContent(self, message, content): """ Issue warning. @type message: string @param message: Explaining why the content is invalid. @type content: string @param content: The invalid content. """ warnings.warn("Invalid content '%s' for type '%s' in property '%s' on line %s: %s" %(content, self.getType(), self.name, self.lineNumber, message), InvalidPropertyContentWarning) def getContent(self): """ Use this to fetch the content. @return: The content in it's correct form and type. """ return self.content def setContent(self, content): """ Use this to set the content. The content will be validated. @type content: string @param content: The data string content. """ if not self._validate(content): raise InvalidPropertyContentError(property, content) self.content = content def __str__(self): """Serializes a property back to raw data.""" ret = self.name if self.encoding: ret += ";ENCODING=" + self.encoding if self.type: ret += ";TYPE=" + self.type if self.value: ret += ";VALUE=" + self.value ret += ":" + self.content return ret class UnknownProperty(Property): """Unknown type. All properties that doesn't specify a VALUE=TYPE are instansiated as this object.""" class TextProperty(Property): """A text-type property.""" class DateProperty(Property): """A date-type property. It should be an 8 digit number.""" def _validate(self, content): """Makes sure this is readable UTF-8 text.""" if len(content) != 8: self.invalidContent("must be 8 digit number", content) return None try: timestamp = int(self.content) except ContentError: self.invalidContent("may only contain numbers", content) return None return 1 class Component(object): """ The mama of all components. Inherit from this class only if you intend to create a completely new standard. @ivar begin: The line number where this component started. None if it never was started == very bad! @ivar end: The line number where this component ended. None if it never was ended == very bad, will raise an exception. @ivar properties: A dictionary of all properties where the uppercased name is the key and an instance of the property is the value. @ivar propertiesMay: Internal list for keeping track of properties that may occur. @ivar propertiesMust: Internal list for keeping track of mandatory properties. @ivar propertiesNot: Internal list for keeping track of disallowed properties. @ivar components: A list of all subcomponents under this component. @ivar componentsMust: Internal list for keeping track of mandatory components. @ivar componentsMay: Internal list for keeping track components that may occur. @ivar componentsRecommended: Internal list for keeping track of recommended compoennts. @ivar componentsNot: Internal list for keeping track of disallowed components. @ivar parent: Internal instance of a L{pdi.core.Component} for keeping track of the parent. @ivar ignoreWarnings: Set this baby to true to supress warnings. @ivar classes: Internal dictionary for keeping track valid classes for components and properties. @ivar componentTracker: Internal dictionary for keeping track of what subcomponent classes has been added. @ivar propertiesRecommended: Internal list for keeping track of recommended properties. """ def __init__(self, parent = None): """ @type parent: L{pdi.core.Component} @param parent: The parent component, if any. May be omitted or None if it is a top-level component. """ self.begin = None self.end = None self.properties = {} self.propertiesMay = [] self.propertiesMust = [] self.propertiesRecommended = [] self.propertiesNot = [] self.components = [] self.componentsMust = [] self.componentsMay = [] self.componentsRecommended = [] self.componentsNot = [] self.parent = parent self.ignoreWarnings = True self.classes = {} self.componentTracker = [] self.registerPropertyTypes({'UNKNOWN' : UnknownProperty, 'DATE' : DateProperty, 'TEXT' : TextProperty}) def getName(self): """ Return the name of this component. This is the uppercased class name unless an unknown component. @rtype: string @return: The components name (usually the uppercased class name, but not always). It has to be uppercased. """ return self.__class__.__name__.upper() def parseLine(self, data, lineNumber): """ This parses one line of data. @type data: string @param data: A line of data. @type lineNumber: number @param lineNumber: The line we are currently parsing. Needed for exceptions, warnings and debugging in general. @rtype: L{pdi.core.Component} @return: The next component that needs parsing. Can be a child, parent or self. @raise ParseError: Raised if parsed data is whack! @raise ComponentError: Raised if a component is invalid. This probably indicates an internal error. @raise InvalidComponentError: Raised if a disallowed component is found. @raise MissingComponentError: Raised if a mandatory component is not found. @raise InvalidPropertyError: Raised if a disallowed property is found. @raise MissingPropertyError: Raised if a mandatory property is not found. @raise PropertyValueError: Raised if a property fails to validate itself. """ if data[0] in ' \t': if self.lastProperty: self.properties[self.lastProperty.name].value = self.properties[self.lastProperty.name].getContent() + CRLF + data[0:-1] return self else: raise ParseError(self.getName(), "displaced content line", lineNumber) unpack = data.split(":") #if len(unpack) != 2: # raise ParseError(self.getName(), "invalid data", lineNumber) try: key, value = unpack except Exception,e: key,value,other=unpack return self.interpret(key.upper().strip(), value.strip(), lineNumber) def interpret(self, key, value, lineNumber = -1): """Interprets a symbol and value.""" if key == "BEGIN": if value.upper() == self.getName() and not self.begin: self.begin = lineNumber return self subcomponent = None for available in self.componentsMay + self.componentsMust + self.componentsRecommended: if value.upper() == available: if self.classes.has_key(available): subcomponent = self.classes[available](self) else: self.ComponentError("Internal error, component not found in class dictionary", available, lineNumber) if not subcomponent: subcomponent = VUnknown(value, self) subcomponent.interpret(key, value) self.addComponent(subcomponent, lineNumber) return subcomponent elif key == "END": if value.upper() == self.getName(): self.end = lineNumber self._validate(None, lineNumber) return self.parent raise ParseError(self.getName(), "expected 'END:" + self.getName() + "', but found 'END:" + value + "'", lineNumber) else: unpack = key.split(";") propertyClass = UnknownProperty propertyName = key.upper() propertyType = None propertyValue = None propertyEncoding = None if len(unpack) > 1: propertyName = unpack[0].upper() del unpack[0] for unpacked in unpack: unpack2 = unpacked.split("=") if len(unpack2) == 2: if unpack2[0].upper() == "VALUE": if self.classes.has_key("TYPE_" + unpack2[1].upper()): propertyClass = self.classes["TYPE_" + unpack2[1].upper()] propertyValue = unpack2[1].upper() elif unpack2[0].upper() == "ENCODING": propertyEncoding = unpack2[1] elif unpack2[0].upper() == "TYPE": propertyType = unpack2[1] else: propertyName += ";" + unpacked.upper() self.addProperty(propertyClass(propertyName, value, propertyValue, propertyEncoding, propertyType, lineNumber), lineNumber) if self.begin: return self return self.parent def registerComponents(self, componentList, rule = RULE_MAY): """ Register all valid (or invalid, depending on rule) components. All components that are supposed to be valid should be registered with this method, otherwise they become instances of UnknownComponent. @type componentList: list @param componentList: A list with L{pdi.core.Component} derived classes that are valid (or invalid). @type rule: number @param rule: A pdi.core.RULE_MUST, pdi.core.RULE_MAY, pdi.core.RULE_RECOMMEND or pdi.core.RULE_NOT. """ for component in componentList: if rule != RULE_NOT: self.classes[component.__name__.upper()] = component if rule == RULE_MUST: self.componentsMust.append(component.__name__.upper()) elif rule == RULE_MAY: self.componentsMay.append(component.__name__.upper()) elif rule == RULE_RECOMMENDED: self.componentsRecommended.append(component.__name__.upper()) elif rule == RULE_NOT: self.componentsNot.append(component.__name__.upper()) else: raise ValueError("Second argument must be RULE_ value") def registerProperties(self, propertyList, rule = RULE_MAY): """ Register valid (or invalid, depending on rule) properties. @type propertyList: list @param propertyList: A list of strings containing the uppercased names of the properties. @type rule: number @param rule: A pdi.core.RULE_MUST, pdi.core.RULE_MAY, pdi.core.RULE_RECOMMEND or pdi.core.RULE_NOT. """ for property in propertyList: if rule == RULE_MUST: self.propertiesMust.append(property.upper()) elif rule == RULE_MAY: self.propertiesMay.append(property.upper()) elif rule == RULE_RECOMMENDED: self.propertiesRecommended.append(property.upper()) elif rule == RULE_NOT: self.propertiesNot.append(property.upper()) else: raise ValueError("Second argument must be RULE_ value") def registerPropertyTypes(self, propertyTypes): """ Register available property types. Properties without a type or a type that has not been registered will be instansiated as UnknownProperty. @type propertyTypes: list @param propertyTypes: A list of L{pdi.core.Property} derived classes. """ for propertyType in propertyTypes.keys(): self.classes["TYPE_" + propertyType] = propertyTypes[propertyType] def addComponents(self, componentList): """ Candy method for adding several components at one time. @type componentList: list @param componentList: A list of L{pdi.core.Component} instances. @raise InvalidComponentError: If any subcomponent you tried to add is not valid for the component. """ for comp in componentList: self.addComponent(comp) def addProperties(self, propertyList): """ Candy method for adding several properties at one time. @type propertyList: list @param propertyList: A list of L{pdi.core.Property} instances. @raise InvalidPropertyError: If any property you tried to add is not valid for the component. """ for prop in propertyList: self.addProperty(prop) def addComponent(self, component, lineNumber = None): """ Add a subcomponent to this component. @type component: L{pdi.core.Component} @param component: The subcomponent to add. @type lineNumber: number @param lineNumber: The line currently parsed. Used internally when parsing files. May be omitted or None. @rtype: L{pdi.core.Component} @return: The subcomponent you just added. @raise InvalidComponentError: If the subcomponent you tried to add is not valid for the component. """ self.components.append(component) if not component.getName() in self.componentTracker: self.componentTracker.append(component.getName()) for comp in self.componentsNot: if comp in self.componentTracker: raise InvalidComponentError(prop, lineNumber) return component def addProperty(self, property, lineNumber = None): """ Add a property to this component. The property will also be validated and warnings issued as the property implementation sees fit. @type property: L{pdi.core.Property} @param property: The property to add. @type lineNumber: number @param lineNumber: The line currently parsed. Used internally when parsing files. May be omitted or None. @rtype: L{pdi.core.Property} @return: The property you just added. @raise InvalidPropertyError: If the property you tried to add is not valid for the component. """ self.properties[property.name.upper()] = property property.validate() self.lastProperty = property for prop in self.properties.keys(): if prop in self.propertiesNot: raise InvalidPropertyError(prop, lineNumber) return property def validate(self, lineNumber = None): """ This will make sure that all mandatory components and properties are present. The validation is recursive, so you only need to call it for the top component. @type lineNumber: number @param lineNumber: The line currently parsed. Used internally when parsing files. May be omitted or None. @raise MissingComponentError: A mandatory component is missing. @raise MissingPropertyError: A mandatory property is missing. """ self._validate(1, lineNumber) def _validate(self, recursive, lineNumber = None): """ Internal method for validating components. @type recursive: boolean @param recursive: If true, itterate recursevly over subcomponents as well. @type lineNumber: number @param lineNumber: The line currently parsed. Used internally when parsing files. May be omitted or None. @raise MissingComponentError: A mandatory component is missing. @raise MissingPropertyError: A mandatory property is missing. """ for comp in self.componentsMust: if not comp in self.componentTracker: raise MissingComponentError(self.getName(), comp, self.begin, self.end) for prop in self.propertiesMust: if not self.properties.has_key(prop): raise MissingPropertyError(prop, self.getName(), self.begin, self.end) if recursive: for comp in self.components: comp._validate(recursive, lineNumber) def __str__(self): """Serialize this component as well as it's properties and subcomponents.""" ret = "BEGIN:" + self.getName() + CRLF for key in self.properties.keys(): ret = ret + self.properties[key].__str__() + CRLF for child in self.components: ret = ret + child.__str__() ret = ret + "END:" + self.getName() + CRLF return ret class VUnknown(Component): """ This class is used for all components that do not have their own classes registered. """ def __init__(self, name, parent = None): """ @type name: string @param name: The name of the component is required since it doesn't provide it through the class name. @type parent: L{pdi.core.Component} @param parent: The parent component, if any. May be omitted or None if it is a top-level component. """ super(VUnknown, self).__init__(parent) self.name = name def getName(self): """ Returns the name provided on instansiation rather than the uppercased class name due to the fact that the component is of unknown type. @rtype: string @return: The components name (usually the uppercased class name, but not always). """ return self.name def fromFile(fileName, inObject, bufferSize = 4096): """ Opens a file and parses it line by line. If nothing unexpected happens the inObject will be populated and returned. @param fileName: The name of the file to read from. @param inObject: An instance of a L{pdi.core.Component} to populate. @param bufferSize: The read buffer and maximum size per line. Default is 4k. @rtype: L{pdi.core.Component} @return: The same instance of pdi.core.Component passed in as the second argument, only populated. """ currentObject = inObject fObj = open(fileName) line = fObj.readline(bufferSize) lineNum = 0 while line and currentObject: lineNum += 1 currentObject = currentObject.parseLine(line, lineNum) line = fObj.readline(bufferSize) fObj.close() return inObject def fromStrings(list, inObject): """ Itterates over a list of strings and parses them. If nothing unexpected happens the inObject will be populated and returned. @param list: A list with strings. @param inObject: An instance of a L{pdi.core.Component} to populate. @rtype: L{pdi.core.Component} @return: The same instance of pdi.core.Component passed in as the second argument, only populated. """ currentObject = inObject lineNum = 0 for line in list: lineNum += 1 currentObject = currentObject.parseLine(line, lineNum) return inObject def fromString(data, inObject, crlf = "\n"): """ Splits a string on CRLF's and parses them. If nothing unexpected happens the inObject will be populated and returned. @type data: string @param data: A string containing the data. @type inObject: L{pdi.core.Component} @param inObject: An instance of a pdi.core.Component. @type crlf: string @param crlf: Split on this substring. @rtype: L{pdi.core.Component} @return: The same instance of pdi.core.Component passed in as the second argument, only populated. """ unpack = data.split(crlf) return fromStrings(unpack) class VCalendar(Component): """The base of ICalendar component. Use ICalendar instead if you want RFC2445 compliancy!""" def __init__(self, parent = None): """ @type parent: L{pdi.core.Component} @param parent: The parent component, if any. May be omitted or None if it is a top-level component. """ super(VCalendar, self).__init__(parent) self.registerComponents([VEvent, VTodo, VJournal], RULE_MAY) self.registerComponents([VCalendar], RULE_NOT) self.registerProperties(['PRODID', 'VERSION'], RULE_MUST) class VEvent(Component): """Event component, sub-component to VCalendar.""" def __init__(self, parent = None): """ @type parent: L{pdi.core.Component} @param parent: The parent component, if any. May be omitted or None if it is a top-level component. """ super(VEvent, self).__init__(parent) #self.registerProperties(['UID', 'SUMMARY'], RULE_MUST) self.registerProperties(['SUMMARY'], RULE_MUST) self.registerComponents([VEvent], RULE_NOT) self.registerProperties(['DTSTAMP', 'DTSTART', 'RRULE', 'DTEND' ], RULE_MAY) class VTodo(Component): """Todo component, sub-component to VCalendar.""" def __init__(self, parent = None): """ @type parent: L{pdi.core.Component} @param parent: The parent component, if any. May be omitted or None if it is a top-level component. """ super(VTodo, self).__init__(parent) self.registerComponents([VTodo], RULE_NOT) class VJournal(Component): """Journal component, sub-component to VCalendar. This is not a stand-alone component!""" def __init__(self, parent = None): """ @type parent: L{pdi.core.Component} @param parent: The parent component, if any. May be omitted or None if it is a top-level component. """ super(VJournal, self).__init__(parent) self.registerComponents([VJournal], RULE_NOT) class ICalendar(VCalendar): """This is supposed to be the RFC2445 compliant iCalendar component.""" def __init__(self, parent = None): """ @type parent: L{pdi.core.Component} @param parent: The parent component, if any. May be omitted or None if it is a top-level component. """ super(ICalendar, self).__init__(parent) self.registerComponents([VJournal], RULE_MAY) self.registerComponents([ICalendar], RULE_NOT) self.registerProperties(['CALSCALE'], RULE_MUST) self.registerProperties(['X-WR-CALNAME', 'X-WR-RELCALID', 'X-WR-TIMEZONE' ], RULE_MAY) def getName(self): """ Because of the crappy iCalendar standard still uses VCALENDAR sections, pretend the name is VCALENDAR. @rtype: string @return: A constant string 'VCALENDAR'. """ return "VCALENDAR" def parseIcalFile(file): timetupledict={} calendar=fromFile(file,ICalendar()) for event in calendar.components: if event.getName()=="VEVENT": start=event.properties["DTSTART"].getContent() #end=event.properties["DTEND"] summary=event.properties["SUMMARY"].getContent() timeparts=start.split("T") date=timeparts[0] if len(timeparts)==2: time=timeparts[1] summary+=" at "+time[0:4] year=int(date[0:4]) month=int(date[4:6]) day=int(date[6:8]) timetupledict[datetime.datetime(year,month,day).timetuple()]=summary return timetupledict #try: c=CursesCal() c.main() #except Exception,e: #try: #curses.nocbreak() #curses.echo() #curses.endwin() #if str(e.__class__)!="exceptions.KeyboardInterrupt": # print repr(e) #except: # print repr(e)