Resync all running ubottu code to bzr branch, they should now be in sync again

This commit is contained in:
Terence Simpson
2009-10-12 19:26:35 +01:00
parent 46a4ea34d2
commit 7791f6e416
39 changed files with 4983 additions and 440 deletions

182
Webcal/cal.ical Normal file
View File

@ -0,0 +1,182 @@
BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-WR-CALNAME:The Fridge | October 17\, 2008 - December 16\, 2008
PRODID:-//strange bird labs//Drupal iCal API//EN
BEGIN:VEVENT
DTSTAMP;VALUE=DATE:20081017T202549Z
DTSTART;VALUE=DATE-TIME:20081017T190000Z
DTEND;VALUE=DATE-TIME:20081017T210000Z
UID:http://fridge.ubuntu.com/node/1656
URL;VALUE=URI:http://fridge.ubuntu.com/node/1656
SUMMARY:Tunisian LoCo Team IRC Meeting
DESCRIPTION:<p>Location\: #ubuntu-tn<br />
Agenda\: Team participation to SFD Tunisia 2008.</p>
END:VEVENT
BEGIN:VEVENT
DTSTAMP;VALUE=DATE:20081017T202549Z
DTSTART;VALUE=DATE-TIME:20081018T130000Z
DTEND;VALUE=DATE-TIME:20081018T150000Z
UID:http://fridge.ubuntu.com/node/1571
URL;VALUE=URI:http://fridge.ubuntu.com/node/1571
SUMMARY:Xubuntu Community Meeting
DESCRIPTION:<p>Location\: #ubuntu-meeting<br />
Agenda\: https\://wiki.ubuntu.com/Xubuntu/Meetings</p>
END:VEVENT
BEGIN:VEVENT
DTSTAMP;VALUE=DATE:20081017T202549Z
DTSTART;VALUE=DATE-TIME:20081021T110000Z
DTEND;VALUE=DATE-TIME:20081021T130000Z
UID:http://fridge.ubuntu.com/node/1558
URL;VALUE=URI:http://fridge.ubuntu.com/node/1558
SUMMARY:Community Council Meeting
DESCRIPTION:<p>Location\: #ubuntu-meeting<br />
Agenda\: <a href=\\"https\://wiki.ubuntu.com/CommunityCouncilAgenda\\">https\://wiki.ubuntu.com/CommunityCouncilAgenda</a></p>
END:VEVENT
BEGIN:VEVENT
DTSTAMP;VALUE=DATE:20081017T202549Z
DTSTART;VALUE=DATE-TIME:20081021T110000Z
DTEND;VALUE=DATE-TIME:20081021T120000Z
UID:http://fridge.ubuntu.com/node/1678
URL;VALUE=URI:http://fridge.ubuntu.com/node/1678
SUMMARY:Asia Oceania Membership Board Meeting
DESCRIPTION:<p>Location\: #ubuntu-meeting</p>
END:VEVENT
BEGIN:VEVENT
DTSTAMP;VALUE=DATE:20081017T202549Z
DTSTART;VALUE=DATE-TIME:20081021T140000Z
DTEND;VALUE=DATE-TIME:20081021T160000Z
UID:http://fridge.ubuntu.com/node/1662
URL;VALUE=URI:http://fridge.ubuntu.com/node/1662
SUMMARY:Technical Board Meeting
DESCRIPTION:<ul>
<li><strong>Agenda\:</strong> <a href=\\"https\://wiki.ubuntu.com/TechnicalBoardAgenda\\">https\://wiki.ubuntu.com/TechnicalBoardAgenda</a></li>
<li><strong>Location\:</strong> #ubuntu-meeting</li>
</ul>
END:VEVENT
BEGIN:VEVENT
DTSTAMP;VALUE=DATE:20081017T202549Z
DTSTART;VALUE=DATE-TIME:20081021T150000Z
DTEND;VALUE=DATE-TIME:20081021T160000Z
UID:http://fridge.ubuntu.com/node/1681
URL;VALUE=URI:http://fridge.ubuntu.com/node/1681
SUMMARY:Server Team Meeting
DESCRIPTION:<p>Location\: #ubuntu-meeting on IRC<br />
Agenda\: https\://wiki.ubuntu.com/ServerTeam/Meeting</p>
END:VEVENT
BEGIN:VEVENT
DTSTAMP;VALUE=DATE:20081017T202549Z
DTSTART;VALUE=DATE-TIME:20081021T170000Z
DTEND;VALUE=DATE-TIME:20081021T180000Z
UID:http://fridge.ubuntu.com/node/1683
URL;VALUE=URI:http://fridge.ubuntu.com/node/1683
SUMMARY:Kernel Team Meeting
DESCRIPTION:<p>Location\: #ubuntu-meeting in IRC<br />
Agenda\: Not listed as of publication</p>
END:VEVENT
BEGIN:VEVENT
DTSTAMP;VALUE=DATE:20081017T202549Z
DTSTART;VALUE=DATE-TIME:20081022T230000Z
DTEND;VALUE=DATE-TIME:20081023T000000Z
UID:http://fridge.ubuntu.com/node/1667
URL;VALUE=URI:http://fridge.ubuntu.com/node/1667
SUMMARY:Forum Council Meeting
DESCRIPTION:<p>Location\: #ubuntu-meeting</p>
END:VEVENT
BEGIN:VEVENT
DTSTAMP;VALUE=DATE:20081017T202549Z
DTSTART;VALUE=DATE-TIME:20081028T160000Z
DTEND;VALUE=DATE-TIME:20081028T170000Z
UID:http://fridge.ubuntu.com/node/1682
URL;VALUE=URI:http://fridge.ubuntu.com/node/1682
SUMMARY:Server Team Meeting
DESCRIPTION:<p>Location\: #ubuntu-meeting on IRC<br />
Agenda\: https\://wiki.ubuntu.com/ServerTeam/Meeting</p>
END:VEVENT
BEGIN:VEVENT
DTSTAMP;VALUE=DATE:20081017T202549Z
DTSTART;VALUE=DATE-TIME:20081028T170000Z
DTEND;VALUE=DATE-TIME:20081028T180000Z
UID:http://fridge.ubuntu.com/node/1684
URL;VALUE=URI:http://fridge.ubuntu.com/node/1684
SUMMARY:Kernel Team Meeting
DESCRIPTION:<p>Location\: #ubuntu-meeting in IRC<br />
Agenda\: Not listed as of publication</p>
END:VEVENT
BEGIN:VEVENT
DTSTAMP;VALUE=DATE:20081017T202549Z
DTSTART;VALUE=DATE-TIME:20081104T140000Z
DTEND;VALUE=DATE-TIME:20081104T140000Z
UID:http://fridge.ubuntu.com/node/1663
URL;VALUE=URI:http://fridge.ubuntu.com/node/1663
SUMMARY:Technical Board Meeting
DESCRIPTION:<ul>
<li><strong>Agenda\:</strong> <a href=\\"https\://wiki.ubuntu.com/TechnicalBoardAgenda\\">https\://wiki.ubuntu.com/TechnicalBoardAgenda</a></li>
<li><strong>Location\:</strong> #ubuntu-meeting</li>
</ul>
END:VEVENT
BEGIN:VEVENT
DTSTAMP;VALUE=DATE:20081017T202549Z
DTSTART;VALUE=DATE-TIME:20081104T210000Z
DTEND;VALUE=DATE-TIME:20081104T230000Z
UID:http://fridge.ubuntu.com/node/1553
URL;VALUE=URI:http://fridge.ubuntu.com/node/1553
SUMMARY:Community Council Meeting
DESCRIPTION:<p>Location\: #ubuntu-meeting<br />
Agenda\: <a href=\\"https\://wiki.ubuntu.com/CommunityCouncilAgenda\\">https\://wiki.ubuntu.com/CommunityCouncilAgenda</a></p>
END:VEVENT
BEGIN:VEVENT
DTSTAMP;VALUE=DATE:20081017T202549Z
DTSTART;VALUE=DATE-TIME:20081106T000000Z
DTEND;VALUE=DATE-TIME:20081106T010000Z
UID:http://fridge.ubuntu.com/node/1547
URL;VALUE=URI:http://fridge.ubuntu.com/node/1547
SUMMARY:Maryland LoCo IRC Meeting
DESCRIPTION:<p>Location\: #ubuntu-us-md</p>
END:VEVENT
BEGIN:VEVENT
DTSTAMP;VALUE=DATE:20081017T202549Z
DTSTART;VALUE=DATE-TIME:20081118T110000Z
DTEND;VALUE=DATE-TIME:20081118T130000Z
UID:http://fridge.ubuntu.com/node/1559
URL;VALUE=URI:http://fridge.ubuntu.com/node/1559
SUMMARY:Community Council Meeting
DESCRIPTION:<p>Location\: #ubuntu-meeting<br />
Agenda\: <a href=\\"https\://wiki.ubuntu.com/CommunityCouncilAgenda\\">https\://wiki.ubuntu.com/CommunityCouncilAgenda</a></p>
END:VEVENT
BEGIN:VEVENT
DTSTAMP;VALUE=DATE:20081017T202549Z
DTSTART;VALUE=DATE-TIME:20081202T210000Z
DTEND;VALUE=DATE-TIME:20081202T230000Z
UID:http://fridge.ubuntu.com/node/1554
URL;VALUE=URI:http://fridge.ubuntu.com/node/1554
SUMMARY:Community Council Meeting
DESCRIPTION:<p>Location\: #ubuntu-meeting<br />
Agenda\: <a href=\\"https\://wiki.ubuntu.com/CommunityCouncilAgenda\\">https\://wiki.ubuntu.com/CommunityCouncilAgenda</a></p>
END:VEVENT
BEGIN:VEVENT
DTSTAMP;VALUE=DATE:20081017T202549Z
DTSTART;VALUE=DATE-TIME:20081204T000000Z
DTEND;VALUE=DATE-TIME:20081204T010000Z
UID:http://fridge.ubuntu.com/node/1548
URL;VALUE=URI:http://fridge.ubuntu.com/node/1548
SUMMARY:Maryland LoCo IRC Meeting
DESCRIPTION:<p>Location\: #ubuntu-us-md</p>
END:VEVENT
END:VCALENDAR

View File

@ -1,88 +1,101 @@
#!/usr/bin/python
# Slightly modified version of the iCal module found at
# http://www.random-ideas.net/Software/iCal_Module
# Original file doesn't come with a license but is public domain according to
# above website
import os
import os.path
import re
import sys, os
sys.path.append(os.path.dirname(__file__))
from icalendar import Calendar, cal, prop
from dateutil import tz as tzmod
from cStringIO import StringIO
import pytz
import urllib2
import datetime
import time
import pytz # pytz can be found on http://pytz.sourceforge.net
import rruler
DEB_OBJ = None
SECONDS_PER_DAY=24*60*60
def seconds(timediff):
return SECONDS_PER_DAY * timediff.days + timediff.seconds
class ICalReader:
def toTz(date, tz):
assert isinstance(tz, datetime.tzinfo), "tz must be a tzinfo type"
if isinstance(date, datetime.datetime):
try:
return date.astimezone(tz)
except:
return datetime.datetime.combine(date.date(), datetime.time(date.time().hour, date.time().minute, date.time().second, tzinfo=tz))
elif isinstance(datetime.date):
return datetime.datetime.combine(date, datetime.time(0, 0, 0, tzinfo=tz))
class ICalReader:
def __init__(self, data):
self.events = []
self.timezones = {}
self.raw_data = data
self.readEvents()
def readEvents(self):
self.events = []
lines = self.raw_data.split('\n')
inEvent = False
eventLines = []
stRegex = re.compile("^BEGIN:VEVENT")
enRegex = re.compile("^END:VEVENT")
for line in lines:
if stRegex.match(line):
inEvent = True
eventLines = []
if inEvent:
eventLines.append(line)
if enRegex.match(line):
self.events.append(self.parseEvent(eventLines))
self.timezones = {}
parser = Calendar.from_string(self.raw_data)
tzs = parser.walk("vtimezone")
self.parseTzs(tzs)
events = parser.walk("vevent")
for event in events:
res = self.parseEvent(event)
if res:
self.events.append(res)
return self.events
def parseTzs(self, tzs):
if not tzs:
return
for tz in tzs:
if 'X-LIC-LOCATION' in tz:
del tz['X-LIC-LOCATION']
data = ''.join([str(i) for i in tzs])
data = '\r\n'.join([i for i in data.splitlines() if i.strip()])
fd = StringIO(data)
times = tzmod.tzical(fd)
for tz in times.keys():
self.timezones[tz] = times.get(tz)
def parseEvent(self, e):
for k in ["dtstart", "dtend", "summary"]:
if not k in e:
return
if not isinstance(e['dtstart'].dt, datetime.datetime):
return
return ICalEvent.from_event(e, self)
startDate = endDate = rule = summary = None
startDate = self.parseDate(e.get("dtstart"))
endDate = self.parseDate(e.get("dtend"))
rule = e.get("RRULE")
summary = e.get("summary")
if e.get("exdate"):
event.addExceptionDate(e['EXDATE'].ical()[7:])
if not startDate or not endDate or not summary: # Bad event
return
def parseEvent(self, lines):
event = ICalEvent()
event.raw_data = "\n".join(lines)
startDate = None
rule = None
endDate = None
for line in lines:
if re.compile("^SUMMARY:(.*)").match(line):
event.summary = re.compile("^SUMMARY:(.*)").match(line).group(1)
elif re.compile("^DTSTART;.*:(.*).*").match(line):
startDate = self.parseDate(re.compile("^DTSTART;.*:(.*).*").match(line).group(1))
elif re.compile("^DTEND;.*:(.*).*").match(line):
endDate = self.parseDate(re.compile("^DTEND;.*:(.*).*").match(line).group(1))
elif re.compile("^EXDATE.*:(.*)").match(line):
event.addExceptionDate(parseDate(re.compile("^EXDATE.*:(.*)").match(line).group(1)))
elif re.compile("^RRULE:(.*)").match(line):
rule = re.compile("^RRULE:(.*)").match(line).group(1)
event.raw_data = str(e)
event.summary = summary
event.startDate = startDate
event.endDate = endDate
if rule:
event.addRecurrenceRule(rule)
return event
def parseDate(self, dateStr):
year = int(dateStr[0:4])
if year < 1970:
year = 1970
month = int(dateStr[4:4+2])
day = int(dateStr[6:6+2])
try:
hour = int(dateStr[9:9+2])
minute = int(dateStr[11:11+2])
except:
hour = 0
minute = 0
return datetime.datetime(year, month, day, hour, minute, tzinfo=pytz.UTC)
def parseDate(self, date):
if not date:
return
tz = pytz.UTC
if 'tzid' in date.params:
tz = self.timezones[date.params['tzid']]
for attr in ['hour', 'minute', 'second']:
if not hasattr(date.dt, attr):
return
return toTz(date.dt, tz)
# return datetime.datetime(date.dt.year, date.dt.month, date.dt.day, date.dt.hour, date.dt.minute, date.dt.second, tzinfo=tz)
def selectEvents(self, selectFunction):
note = datetime.datetime.today()
self.events.sort()
events = filter(selectFunction, self.events)
return events
@ -94,25 +107,68 @@ class ICalReader:
return event.startsTomorrow()
def eventsFor(self, date):
note = datetime.datetime.today()
self.events.sort()
ret = []
for event in self.events:
if event.startsOn(date):
ret.append(event)
return ret
return re
class ICalEvent:
def __init__(self):
#class ICalEvent:
# def __init__(self):
# self.exceptionDates = []
# self.dateSet = None
#
# def __str__(self):
# return "%s (%s - %s)" % (self.summary, self.startDate, self.endDate)
class ICalEvent(cal.Event):
def __init__(self, *args, **kwargs):
self.exceptionDates = []
self.dateSet = None
self.__parent = super(ICalEvent, self)
self.__parent.__init__(self, *args, **kwargs)
@classmethod
def from_event(cls, event, parent):
global DEB_OBJ
x = cls(**dict(event))
x.__dict__ = event.__dict__
x.exceptionDates = []
x.dateSet = None
x.summary = x['summary']
x.timezone = x['dtstart'].dt.tzinfo
x.startDate = parent.parseDate(x['dtstart'])
x.endDate = parent.parseDate(x['dtend'])
if not x.timezone:
x.timezone = pytz.UTC
x.startDate = parent.parseDate(x['dtstart'])
x.endDate = parent.parseDate(x['dtend'])
x.raw_data = str(x)
if 'rrule' in event:
x.addRecurrenceRule(event['rrule'])
if x.summary == "Server Team Meeting":
DEB_OBJ = x
return x
def __str__(self):
return self.summary
return "%s (%s - %s)" % (self.summary, self.startDate, self.endDate)
def __eq__(self, otherEvent):
return self.startDate == otherEvent.startDate
return self.startTime() == otherEvent.startTime()
def __lt__(self, otherEvent):
return self.startTime() < otherEvent.startTime()
def __gt__(self, otherEvent):
return self.startTime() > otherEvent.startTime()
def __ge__(self, otherEvent):
return self.startTime() >= otherEvent.startTime()
def __le__(self, otherEvent):
return self.startTime() <= otherEvent.startTime()
def addExceptionDate(self, date):
self.exceptionDates.append(date)
@ -124,7 +180,8 @@ class ICalEvent:
return self.startsOn(datetime.datetime.today())
def startsTomorrow(self):
tomorrow = datetime.datetime.fromtimestamp(time.time() + SECONDS_PER_DAY)
tomorrow = datetime.datetime.today() + datetime.timedelta(1)
# tomorrow = datetime.datetime.fromtimestamp(time.time() + SECONDS_PER_DAY)
return self.startsOn(tomorrow)
def startsOn(self, date):
@ -134,29 +191,47 @@ class ICalEvent:
(self.dateSet and self.dateSet.includes(date)))
def startTime(self):
now = datetime.datetime.now(pytz.UTC)
if self.dateSet and self.startDate < now:
dates = self.dateSet.getRecurrence()
for date in dates:
if date.date() >= now.date():
if date.date() > now.date() or (date.date() == now.date and date.astimezone(pytz.UTC).time() >= now.time()):
return toTz(datetime.datetime.combine(date,self.startDate.time()), self.startDate.tzinfo)
return self.startDate
def endTime(self):
now = datetime.datetime.now(pytz.UTC).date()
if self.dateSet and self.endDate.date() < now:
return toTz(datetime.datetime.combine(self.startTime().date(), self.endDate.time()), self.startDate.tzinfo)
return self.endDate
def schedule(self, timezone=None):
if not timezone:
return "%s UTC: %s" % (self.startDate.strftime("%d %b %H:%M"), self.summary.replace('Meeting','').strip())
return "%s: %s" % (self.startDate.astimezone(pytz.timezone(timezone)).strftime("%d %b %H:%M"), self.summary.replace('Meeting','').strip())
return "%s UTC: %s" % (self.startTime().astimezone(pytz.UTC).strftime("%d %b %H:%M"), self.summary.replace('Meeting','').strip())
if isinstance(timezone, basestring):
return "%s: %s" % (self.startTime().astimezone(pytz.timezone(timezone)).strftime("%d %b %H:%M"), self.summary.replace('Meeting','').strip())
return "%s: %s" % (self.startTime().astimezone(timezone).strftime("%d %b %H:%M"), self.summary.replace('Meeting','').strip())
def is_on(self):
return self.startDate < datetime.datetime.now(pytz.UTC) and self.endDate > datetime.datetime.now(pytz.UTC)
now = datetime.datetime.now(pytz.UTC)
return self.startTime() >= now and self.endTime() < now
def has_passed(self):
if self.dateSet:
return toTz(datetime.datetime.combine(self.startTime().date(), self.endDate.time()), self.startDate.tzinfo) < datetime.datetime.now(pytz.UTC)
return self.endDate < datetime.datetime.now(pytz.UTC)
def seconds_to_go(self):
return seconds(self.startDate - datetime.datetime.now(pytz.UTC))
return seconds(self.startTime() - datetime.datetime.now(pytz.UTC))
def seconds_ago(self):
return seconds(datetime.datetime.now(pytz.UTC) - self.endDate)
return seconds(datetime.datetime.now(pytz.UTC) - self.endTime())
def time_to_go(self):
if self.endDate < datetime.datetime.now(pytz.UTC):
if self.endTime() < datetime.datetime.now(pytz.UTC):
return False
delta = self.startDate - datetime.datetime.now(pytz.UTC)
delta = self.startTime() - datetime.datetime.now(pytz.UTC)
s = ''
if delta.days:
if delta.days != 1:
@ -182,69 +257,25 @@ class DateSet:
self.untilDate = None
self.byMonth = None
self.byDate = None
self.dates = None
self.parseRecurrenceRule(rule)
def parseRecurrenceRule(self, rule):
if re.compile("FREQ=(.*?);").match(rule) :
self.frequency = re.compile("FREQ=(.*?);").match(rule).group(1)
if re.compile("COUNT=(\d*)").match(rule) :
self.count = int(re.compile("COUNT=(\d*)").match(rule).group(1))
if re.compile("UNTIL=(.*?);").match(rule) :
self.untilDate = DateParser.parse(re.compile("UNTIL=(.*?);").match(rule).group(1))
if re.compile("INTERVAL=(\d*)").match(rule) :
self.interval = int(re.compile("INTERVAL=(\d*)").match(rule).group(1))
freq = rruler.rrule_map[rule.pop('freq')[0]]
now = datetime.datetime.now(self.startDate.tzinfo)
rule['dtstart'] = now
rule['until'] = now + datetime.timedelta(60)
self.recurrence = rruler.rrule_wrapper(freq, **rule)
if re.compile("BYMONTH=(.*?);").match(rule) :
self.byMonth = re.compile("BYMONTH=(.*?);").match(rule).group(1)
def getRecurrence(self):
if not self.dates:
self.dates = []
for x in list(self.recurrence):
self.dates.append(toTz(x, self.startDate.tzinfo))
self.dates.append(self.startDate)
return self.dates
if re.compile("BYDAY=(.*?);").match(rule) :
self.byDay = re.compile("BYDAY=(.*?);").match(rule).group(1)
def includes(self, date):
if date == self.startDate:
return True
if self.untilDate and date > self.untilDate:
return False
if self.frequency == 'DAILY':
increment = 1
if self.interval:
increment = self.interval
d = self.startDate
counter = 0
while(d < date):
if self.count:
counter += 1
if counter >= self.count:
return False
d = d.replace(day=d.day+1)
if (d.day == date.day and
d.year == date.year and
d.month == date.month):
return True
elif self.frequency == 'WEEKLY':
if self.startDate.weekday() == date.weekday():
return True
else:
if self.endDate:
for n in range(0, self.endDate.day - self.startDate.day):
newDate = self.startDate.replace(day=self.startDate.day+n)
if newDate.weekday() == date.weekday():
return True
elif self.frequency == 'MONTHLY':
pass
elif self.frequency == 'YEARLY':
pass
return False
if isinstance(date, datetime.datetime):
date = date.date()
return date in [x.date() for x in self.getRecurrence()]

299
Webcal/ical.py.bak Normal file
View File

@ -0,0 +1,299 @@
#!/usr/bin/python
import sys, os
sys.path.append(os.path.dirname(__file__))
import icalendar
reload(icalendar)
from icalendar import Calendar, cal, prop
from dateutil import tz as tzmod
from cStringIO import StringIO
import pytz
import urllib2
import datetime, time
import rruler
reload(rruler)
SECONDS_PER_DAY=24*60*60
def seconds(timediff):
return SECONDS_PER_DAY * timediff.days + timediff.seconds
class ICalReader:
def __init__(self, data):
self.events = []
self.timezones = {}
self.raw_data = data
self.readEvents()
def readEvents(self):
self.events = []
self.timezones = {}
parser = Calendar.from_string(self.raw_data)
tzs = parser.walk("vtimezone")
self.parseTzs(tzs)
events = parser.walk("vevent")
for event in events:
res = self.parseEvent(event)
if res:
self.events.append(res)
def parseTzs(self, tzs):
if not tzs:
return
for tz in tzs:
if 'X-LIC-LOCATION' in tz:
del tz['X-LIC-LOCATION']
data = ''.join([str(i) for i in tzs])
data = '\r\n'.join([i for i in data.splitlines() if i.strip()])
fd = StringIO(data)
times = tzmod.tzical(fd)
for tz in times.keys():
self.timezones[tz] = times.get(tz)
def parseEvent(self, e):
for k in ["dtstart", "dtend", "summary"]:
if not k in e:
return
if not isinstance(e['dtstart'].dt, datetime.datetime):
return
return ICalEvent.from_event(e, self)
startDate = endDate = rule = summary = None
startDate = self.parseDate(e.get("dtstart"))
endDate = self.parseDate(e.get("dtend"))
rule = e.get("RRULE")
summary = e.get("summary")
if e.get("exdate"):
event.addExceptionDate(e['EXDATE'].ical()[7:])
if not startDate or not endDate or not summary: # Bad event
return
event = ICalEvent()
event.raw_data = str(e)
event.summary = summary
event.startDate = startDate
event.endDate = endDate
if rule:
event.addRecurrenceRule(rule)
return event
@staticmethod
def toTz(date, tz):
return datetime.datetime(date.year, date.month, date.day, date.hour, date.minute, date.second, tzinfo=tz)
def parseDate(self, date):
if not date:
return
tz = pytz.UTC
if 'tzid' in date.params:
tz = self.timezones[date.params['tzid']]
for attr in ['hour', 'minute', 'second']:
if not hasattr(date.dt, attr):
return
return self.toTz(date.dt, tz)
# return datetime.datetime(date.dt.year, date.dt.month, date.dt.day, date.dt.hour, date.dt.minute, date.dt.second, tzinfo=tz)
def selectEvents(self, selectFunction):
self.events.sort()
events = filter(selectFunction, self.events)
return events
def todaysEvents(self, event):
return event.startsToday()
def tomorrowsEvents(self, event):
return event.startsTomorrow()
def eventsFor(self, date):
self.events.sort()
ret = []
for event in self.events:
if event.startsOn(date):
ret.append(event)
return re
#class ICalEvent:
# def __init__(self):
# self.exceptionDates = []
# self.dateSet = None
#
# def __str__(self):
# return "%s (%s - %s)" % (self.summary, self.startDate, self.endDate)
class ICalEvent(cal.Event):
def __init__(self, *args, **kwargs):
self.exceptionDates = []
self.dateSet = None
self.__parent = super(ICalEvent, self)
self.__parent.__init__(self, *args, **kwargs)
@classmethod
def from_event(cls, event, parent):
x = cls(**dict(event))
x.__dict__ = event.__dict__
x.summary = x['summary']
x.timezone = x['dtstart'].dt.tzinfo
x.startDate = parent.parseDate(x['dtstart'])
x.endDate = parent.parseDate(x['dtend'])
if not x.timezone:
x.timezone = pytz.UTC
x.startDate = parent.parseDate(x['dtstart'])
x.endDate = parent.parseDate(x['dtend'])
x.raw_data = str(x)
if 'rrule' in event:
x.addRecurrenceRule(event['rrule'])
return x
def __str__(self):
return "%s (%s - %s)" % (self.summary, self.startDate, self.endDate)
def __eq__(self, otherEvent):
return self.startDate == otherEvent.startDate
def __lt__(self, otherEvent):
return self.startDate < otherEvent.startDate
def __gt__(self, otherEvent):
return self.startDate > otherEvent.startDate
def __ge__(self, otherEvent):
return self.startDate >= otherEvent.startDate
def __le__(self, otherEvent):
return self.startDate <= otherEvent.startDate
def addExceptionDate(self, date):
self.exceptionDates.append(date)
def addRecurrenceRule(self, rule):
self.dateSet = DateSet(self.startDate, self.endDate, rule)
def startsToday(self):
return self.startsOn(datetime.datetime.today())
def startsTomorrow(self):
tomorrow = datetime.datetime.fromtimestamp(time.time() + SECONDS_PER_DAY)
return self.startsOn(tomorrow)
def startsOn(self, date):
return (self.startDate.year == date.year and
self.startDate.month == date.month and
self.startDate.day == date.day or
(self.dateSet and self.dateSet.includes(date)))
def startTime(self):
return self.startDate
def schedule(self, timezone=None):
if not timezone:
return "%s UTC: %s" % (self.startDate.strftime("%d %b %H:%M"), self.summary.replace('Meeting','').strip())
return "%s: %s" % (self.startDate.astimezone(pytz.timezone(timezone)).strftime("%d %b %H:%M %Z"), self.summary.replace('Meeting','').strip())
def is_on(self):
return self.startDate < datetime.datetime.now(pytz.UTC) and self.endDate > datetime.datetime.now(pytz.UTC)
def has_passed(self):
return self.endDate < datetime.datetime.now(pytz.UTC)
def seconds_to_go(self):
return seconds(self.startDate - datetime.datetime.now(pytz.UTC))
def seconds_ago(self):
return seconds(datetime.datetime.now(pytz.UTC) - self.endDate)
def time_to_go(self):
if self.endDate < datetime.datetime.now(pytz.UTC):
return False
delta = self.startDate - datetime.datetime.now(pytz.UTC)
s = ''
if delta.days:
if delta.days != 1:
s = 's'
return '%d day%s' % (delta.days, s)
h = ''
if delta.seconds > 7200:
s = 's'
if delta.seconds > 3600:
h = '%d hour%s ' % (int(delta.seconds/3600),s)
s = ''
minutes = (delta.seconds % 3600) / 60
if minutes != 1:
s = 's'
return '%s%d minute%s' % (h,minutes,s)
class DateSet:
def __init__(self, startDate, endDate, rule):
self.startDate = startDate
self.endDate = endDate
self.frequency = None
self.count = None
self.untilDate = None
self.byMonth = None
self.byDate = None
self.parseRecurrenceRule(rule)
def parseRecurrenceRule(self, rule):
freq = rruler.rrule_map[rule.pop('freq')[0]]
self.recurrence = rruler.rrule_wrapper(freq, **rule)
# if 'freq' in rule:
# self.frequency = rule['freq']
# if 'count' in rule:
# self.count = rule['count']
# if 'until' in rule:
## self.untilDate = rule['until'][0].strftime("%Y%m%dT%H%M%SZ")
# self.untilDate = rule['until'][0]
# if 'interval' in rule:
# self.interval = rule['interval']
# if 'bymonth' in rule:
# self.myMonth = rule['bymonth']
# if 'byday' in rule:
# self.byDay = rule['byday']
def includes(self, date):
if isinstance(date, datetime.datetime):
date = date.date()
return date in [x.date() for x in list(self.recurrence)] or date == self.startDate.date()
# if date == self.startDate:
# return True
#
# if self.untilDate and date > self.untilDate:
# return False
#
# if self.frequency == 'DAILY':
# increment = 1
# if self.interval:
# increment = self.interval
# d = self.startDate
# counter = 0
# while(d < date):
# if self.count:
# counter += 1
# if counter >= self.count:
# return False
#
# d = d.replace(day=d.day+1)
#
# if (d.day == date.day and
# d.year == date.year and
# d.month == date.month):
# return True
#
# elif self.frequency == 'WEEKLY':
# if self.startDate.weekday() == date.weekday():
# return True
# else:
# if self.endDate:
# for n in range(0, self.endDate.day - self.startDate.day):
# newDate = self.startDate.replace(day=self.startDate.day+n)
# if newDate.weekday() == date.weekday():
# return True
#
# elif self.frequency == 'MONTHLY':
# if self.startDate.month == date.month:
# if self.startDate.weekday() == date.weekday():
# return True
#
# elif self.frequency == 'YEARLY':
# if (self.startDate.month == date.month) and (self.startDate.day == date.day):
# return True
#
# return False

286
Webcal/ical.py.bak.bac2 Normal file
View File

@ -0,0 +1,286 @@
#!/usr/bin/python
# Slightly modified version of the iCal module found at
# http://www.devoesquared.com/Software/iCal_Module
import os
import os.path
import re
import datetime
import time
import pytz # pytz can be found on http://pytz.sourceforge.net
parent = None
def log(x):
if not parent:
return
parent.log.info(x)
SECONDS_PER_DAY=24*60*60
def seconds(timediff):
return SECONDS_PER_DAY * timediff.days + timediff.seconds
class ICalReader:
def __init__(self, data):
self.events = []
self.raw_data = data.replace('\r','')
self.readEvents()
def readEvents(self):
self.events = []
lines = self.raw_data.split('\n')
inEvent = False
eventLines = []
stRegex = re.compile("^BEGIN:VEVENT")
enRegex = re.compile("^END:VEVENT")
for line in lines:
if stRegex.match(line):
inEvent = True
eventLines = []
if inEvent:
eventLines.append(line)
if enRegex.match(line):
inEvent = False
event = self.parseEvent(eventLines)
if event:
self.events.append(event)
self.events.sort()
return self.events
def parseEvent(self, lines):
event = ICalEvent()
event.raw_data = "\n".join(lines)
startDate = None
rule = None
endDate = None
reSummary = re.compile("^SUMMARY:(.*)")
reDstart = re.compile("^DTSTART(.*):([0-9]+T[0-9]+)")
reDend = re.compile("^DTEND(.*):([0-9]+T[0-9]+)")
reExdata = re.compile("^EXDATE:([0-9]+T[0-9]+)")
reRrule = re.compile("^RRULE:(.*)")
for line in lines:
match = False
if reSummary.match(line):
event.summary = reSummary.match(line).group(1)
elif reDstart.match(line):
startDate = self.parseDate(*reDstart.match(line).groups())
elif reDend.match(line):
endDate = self.parseDate(*reDend.match(line).groups())
elif reExdata.match(line):
event.addExceptionDate(reExdate.match(line).group(1))
elif reRrule.match(line):
rule = reRrule.match(line).group(1)
event.startDate = startDate
event.endDate = endDate
if rule:
event.addRecurrenceRule(rule)
if not startDate or not endDate:
return None
return event
def parseDate(self, tz, dateStr):
year = int(dateStr[0:4])
if year < 1970:
year = 1970
month = int(dateStr[4:4+2])
day = int(dateStr[6:6+2])
try:
hour = int(dateStr[9:9+2])
minute = int(dateStr[11:11+2])
except:
hour = 0
minute = 0
if tz:
return datetime.datetime(year, month, day, hour, minute, tzinfo=pytz.timezone(tz[6:]))
return datetime.datetime(year, month, day, hour, minute, tzinfo=pytz.UTC)
def selectEvents(self, selectFunction):
note = datetime.datetime.today()
self.events.sort()
events = filter(selectFunction, self.events)
return events
def todaysEvents(self, event):
return event.startsToday()
def tomorrowsEvents(self, event):
return event.startsTomorrow()
def eventsFor(self, date):
note = datetime.datetime.today()
self.events.sort()
ret = []
for event in self.events:
if event.startsOn(date):
ret.append(event)
return ret
class ICalEvent:
def __init__(self):
self.exceptionDates = []
self.dateSet = None
def __str__(self):
return "%s (%s - %s)" % (self.summary, self.startDate, self.endDate)
def __eq__(self, otherEvent):
return self.startDate == otherEvent.startDate
def __lt__(self, otherEvent):
return self.startDate < otherEvent.startDate
def __gt__(self, otherEvent):
return self.startDate > otherEvent.startDate
def __ge__(self, otherEvent):
return self.startDate >= otherEvent.startDate
def __le__(self, otherEvent):
return self.startDate <= otherEvent.startDate
def addExceptionDate(self, date):
self.exceptionDates.append(date)
def addRecurrenceRule(self, rule):
self.dateSet = DateSet(self.startDate, self.endDate, rule)
def startsToday(self):
return self.startsOn(datetime.datetime.today())
def startsTomorrow(self):
tomorrow = datetime.datetime.fromtimestamp(time.time() + SECONDS_PER_DAY)
return self.startsOn(tomorrow)
def startsOn(self, date):
return (self.startDate.year == date.year and
self.startDate.month == date.month and
self.startDate.day == date.day or
(self.dateSet and self.dateSet.includes(date)))
def startTime(self):
return self.startDate
def schedule(self, timezone=None):
if not timezone:
return "%s UTC: %s" % (self.startDate.strftime("%d %b %H:%M"), self.summary.replace('Meeting','').strip())
return "%s: %s" % (self.startDate.astimezone(pytz.timezone(timezone)).strftime("%d %b %H:%M"), self.summary.replace('Meeting','').strip())
def is_on(self):
return self.startDate < datetime.datetime.now(pytz.UTC) and self.endDate > datetime.datetime.now(pytz.UTC)
def has_passed(self):
return self.endDate < datetime.datetime.now(pytz.UTC)
def seconds_to_go(self):
return seconds(self.startDate - datetime.datetime.now(pytz.UTC))
def seconds_ago(self):
return seconds(datetime.datetime.now(pytz.UTC) - self.endDate)
def time_to_go(self):
if self.endDate < datetime.datetime.now(pytz.UTC):
return False
delta = self.startDate - datetime.datetime.now(pytz.UTC)
s = ''
if delta.days:
if delta.days != 1:
s = 's'
return '%d day%s' % (delta.days, s)
h = ''
if delta.seconds > 7200:
s = 's'
if delta.seconds > 3600:
h = '%d hour%s ' % (int(delta.seconds/3600),s)
s = ''
minutes = (delta.seconds % 3600) / 60
if minutes != 1:
s = 's'
return '%s%d minute%s' % (h,minutes,s)
class DateSet:
def __init__(self, startDate, endDate, rule):
self.startDate = startDate
self.endDate = endDate
self.frequency = None
self.count = None
self.untilDate = None
self.byMonth = None
self.byDate = None
self.parseRecurrenceRule(rule)
def parseRecurrenceRule(self, rule):
if re.compile("FREQ=(.*?);").match(rule) :
self.frequency = re.compile("FREQ=(.*?);").match(rule).group(1)
if re.compile("COUNT=(\d*)").match(rule) :
self.count = int(re.compile("COUNT=(\d*)").match(rule).group(1))
if re.compile("UNTIL=(.*?);").match(rule) :
# self.untilDate = DateParser.parse(re.compile("UNTIL=(.*?);").match(rule).group(1))
self.untilDate = re.compile("UNTIL=(.*?);").match(rule).group(1)
if re.compile("INTERVAL=(\d*)").match(rule) :
self.interval = int(re.compile("INTERVAL=(\d*)").match(rule).group(1))
if re.compile("BYMONTH=(.*?);").match(rule) :
self.byMonth = re.compile("BYMONTH=(.*?);").match(rule).group(1)
if re.compile("BYDAY=(.*?);").match(rule) :
self.byDay = re.compile("BYDAY=(.*?);").match(rule).group(1)
def includes(self, date):
if date == self.startDate:
return True
if self.untilDate and date > self.untilDate:
return False
if self.frequency == 'DAILY':
increment = 1
if self.interval:
increment = self.interval
d = self.startDate
counter = 0
while(d < date):
if self.count:
counter += 1
if counter >= self.count:
return False
d = d.replace(day=d.day+1)
if (d.day == date.day and
d.year == date.year and
d.month == date.month):
return True
elif self.frequency == 'WEEKLY':
if self.startDate.weekday() == date.weekday():
return True
else:
if self.endDate:
for n in range(0, self.endDate.day - self.startDate.day):
newDate = self.startDate.replace(day=self.startDate.day+n)
if newDate.weekday() == date.weekday():
return True
elif self.frequency == 'MONTHLY':
if self.startDate.month == date.month:
if self.startDate.weekday() == date.weekday():
return True
elif self.frequency == 'YEARLY':
if (self.startDate.month == date.month) and (self.startDate.day == date.day):
return True
return False

View File

@ -0,0 +1,16 @@
# Components
from icalendar.cal import Calendar, Event, Todo, Journal
from icalendar.cal import FreeBusy, Timezone, Alarm, ComponentFactory
# Property Data Value Types
from icalendar.prop import vBinary, vBoolean, vCalAddress, vDatetime, vDate, \
vDDDTypes, vDuration, vFloat, vInt, vPeriod, \
vWeekday, vFrequency, vRecur, vText, vTime, vUri, \
vGeo, vUTCOffset, TypesFactory
# useful tzinfo subclasses
from icalendar.prop import FixedOffset, UTC, LocalTimezone
# Parameters and helper methods for splitting and joining string with escaped
# chars.
from icalendar.parser import Parameters, q_split, q_join

534
Webcal/icalendar/cal.py Normal file
View File

@ -0,0 +1,534 @@
# -*- coding: latin-1 -*-
"""
Calendar is a dictionary like Python object that can render itself as VCAL
files according to rfc2445.
These are the defined components.
"""
# from python
from types import ListType, TupleType
SequenceTypes = (ListType, TupleType)
import re
# from this package
from icalendar.caselessdict import CaselessDict
from icalendar.parser import Contentlines, Contentline, Parameters
from icalendar.parser import q_split, q_join
from icalendar.prop import TypesFactory, vText
######################################
# The component factory
class ComponentFactory(CaselessDict):
"""
All components defined in rfc 2445 are registered in this factory class. To
get a component you can use it like this.
>>> factory = ComponentFactory()
>>> component = factory['VEVENT']
>>> event = component(dtstart='19700101')
>>> event.as_string()
'BEGIN:VEVENT\\r\\nDTSTART:19700101\\r\\nEND:VEVENT\\r\\n'
>>> factory.get('VCALENDAR', Component)
<class 'icalendar.cal.Calendar'>
"""
def __init__(self, *args, **kwargs):
"Set keys to upper for initial dict"
CaselessDict.__init__(self, *args, **kwargs)
self['VEVENT'] = Event
self['VTODO'] = Todo
self['VJOURNAL'] = Journal
self['VFREEBUSY'] = FreeBusy
self['VTIMEZONE'] = Timezone
self['VALARM'] = Alarm
self['VCALENDAR'] = Calendar
# These Properties have multiple property values inlined in one propertyline
# seperated by comma. Use CaselessDict as simple caseless set.
INLINE = CaselessDict(
[(cat, 1) for cat in ('CATEGORIES', 'RESOURCES', 'FREEBUSY')]
)
_marker = []
class Component(CaselessDict):
"""
Component is the base object for calendar, Event and the other components
defined in RFC 2445. normally you will not use this class directy, but
rather one of the subclasses.
A component is like a dictionary with extra methods and attributes.
>>> c = Component()
>>> c.name = 'VCALENDAR'
Every key defines a property. A property can consist of either a single
item. This can be set with a single value
>>> c['prodid'] = '-//max m//icalendar.mxm.dk/'
>>> c
VCALENDAR({'PRODID': '-//max m//icalendar.mxm.dk/'})
or with a list
>>> c['ATTENDEE'] = ['Max M', 'Rasmussen']
if you use the add method you don't have to considder if a value is a list
or not.
>>> c = Component()
>>> c.name = 'VEVENT'
>>> c.add('attendee', 'maxm@mxm.dk')
>>> c.add('attendee', 'test@example.dk')
>>> c
VEVENT({'ATTENDEE': [vCalAddress('maxm@mxm.dk'), vCalAddress('test@example.dk')]})
You can get the values back directly
>>> c.add('prodid', '-//my product//')
>>> c['prodid']
vText(u'-//my product//')
or decoded to a python type
>>> c.decoded('prodid')
u'-//my product//'
With default values for non existing properties
>>> c.decoded('version', 'No Version')
'No Version'
The component can render itself in the RFC 2445 format.
>>> c = Component()
>>> c.name = 'VCALENDAR'
>>> c.add('attendee', 'Max M')
>>> c.as_string()
'BEGIN:VCALENDAR\\r\\nATTENDEE:Max M\\r\\nEND:VCALENDAR\\r\\n'
>>> from icalendar.prop import vDatetime
Components can be nested, so You can add a subcompont. Eg a calendar holds events.
>>> e = Component(summary='A brief history of time')
>>> e.name = 'VEVENT'
>>> e.add('dtend', '20000102T000000', encode=0)
>>> e.add('dtstart', '20000101T000000', encode=0)
>>> e.as_string()
'BEGIN:VEVENT\\r\\nDTEND:20000102T000000\\r\\nDTSTART:20000101T000000\\r\\nSUMMARY:A brief history of time\\r\\nEND:VEVENT\\r\\n'
>>> c.add_component(e)
>>> c.subcomponents
[VEVENT({'DTEND': '20000102T000000', 'DTSTART': '20000101T000000', 'SUMMARY': 'A brief history of time'})]
We can walk over nested componentes with the walk method.
>>> [i.name for i in c.walk()]
['VCALENDAR', 'VEVENT']
We can also just walk over specific component types, by filtering them on
their name.
>>> [i.name for i in c.walk('VEVENT')]
['VEVENT']
>>> [i['dtstart'] for i in c.walk('VEVENT')]
['20000101T000000']
INLINE properties have their values on one property line. Note the double
quoting of the value with a colon in it.
>>> c = Calendar()
>>> c['resources'] = 'Chair, Table, "Room: 42"'
>>> c
VCALENDAR({'RESOURCES': 'Chair, Table, "Room: 42"'})
>>> c.as_string()
'BEGIN:VCALENDAR\\r\\nRESOURCES:Chair, Table, "Room: 42"\\r\\nEND:VCALENDAR\\r\\n'
The inline values must be handled by the get_inline() and set_inline()
methods.
>>> c.get_inline('resources', decode=0)
['Chair', 'Table', 'Room: 42']
These can also be decoded
>>> c.get_inline('resources', decode=1)
[u'Chair', u'Table', u'Room: 42']
You can set them directly
>>> c.set_inline('resources', ['A', 'List', 'of', 'some, recources'], encode=1)
>>> c['resources']
'A,List,of,"some, recources"'
and back again
>>> c.get_inline('resources', decode=0)
['A', 'List', 'of', 'some, recources']
>>> c['freebusy'] = '19970308T160000Z/PT3H,19970308T200000Z/PT1H,19970308T230000Z/19970309T000000Z'
>>> c.get_inline('freebusy', decode=0)
['19970308T160000Z/PT3H', '19970308T200000Z/PT1H', '19970308T230000Z/19970309T000000Z']
>>> freebusy = c.get_inline('freebusy', decode=1)
>>> type(freebusy[0][0]), type(freebusy[0][1])
(<type 'datetime.datetime'>, <type 'datetime.timedelta'>)
"""
name = '' # must be defined in each component
required = () # These properties are required
singletons = () # These properties must only appear once
multiple = () # may occur more than once
exclusive = () # These properties are mutually exclusive
inclusive = () # if any occurs the other(s) MUST occur ('duration', 'repeat')
def __init__(self, *args, **kwargs):
"Set keys to upper for initial dict"
CaselessDict.__init__(self, *args, **kwargs)
# set parameters here for properties that use non-default values
self.subcomponents = [] # Components can be nested.
# def non_complience(self, warnings=0):
# """
# not implemented yet!
# Returns a dict describing non compliant properties, if any.
# If warnings is true it also returns warnings.
#
# If the parser is too strict it might prevent parsing erroneous but
# otherwise compliant properties. So the parser is pretty lax, but it is
# possible to test for non-complience by calling this method.
# """
# nc = {}
# if not getattr(self, 'name', ''):
# nc['name'] = {'type':'ERROR', 'description':'Name is not defined'}
# return nc
#############################
# handling of property values
def _encode(self, name, value, cond=1):
# internal, for conditional convertion of values.
if cond:
klass = types_factory.for_property(name)
return klass(value)
return value
def set(self, name, value, encode=1):
if type(value) == ListType:
self[name] = [self._encode(name, v, encode) for v in value]
else:
self[name] = self._encode(name, value, encode)
def add(self, name, value, encode=1):
"If property exists append, else create and set it"
if name in self:
oldval = self[name]
value = self._encode(name, value, encode)
if type(oldval) == ListType:
oldval.append(value)
else:
self.set(name, [oldval, value], encode=0)
else:
self.set(name, value, encode)
def _decode(self, name, value):
# internal for decoding property values
decoded = types_factory.from_ical(name, value)
return decoded
def decoded(self, name, default=_marker):
"Returns decoded value of property"
if name in self:
value = self[name]
if type(value) == ListType:
return [self._decode(name, v) for v in value]
return self._decode(name, value)
else:
if default is _marker:
raise KeyError, name
else:
return default
########################################################################
# Inline values. A few properties have multiple values inlined in in one
# property line. These methods are used for splitting and joining these.
def get_inline(self, name, decode=1):
"""
Returns a list of values (split on comma).
"""
vals = [v.strip('" ').encode(vText.encoding)
for v in q_split(self[name])]
if decode:
return [self._decode(name, val) for val in vals]
return vals
def set_inline(self, name, values, encode=1):
"""
Converts a list of values into comma seperated string and sets value to
that.
"""
if encode:
values = [self._encode(name, value, 1) for value in values]
joined = q_join(values).encode(vText.encoding)
self[name] = types_factory['inline'](joined)
#########################
# Handling of components
def add_component(self, component):
"add a subcomponent to this component"
self.subcomponents.append(component)
def _walk(self, name):
# private!
result = []
if name is None or self.name == name:
result.append(self)
for subcomponent in self.subcomponents:
result += subcomponent._walk(name)
return result
def walk(self, name=None):
"""
Recursively traverses component and subcomponents. Returns sequence of
same. If name is passed, only components with name will be returned.
"""
if not name is None:
name = name.upper()
return self._walk(name)
#####################
# Generation
def property_items(self):
"""
Returns properties in this component and subcomponents as:
[(name, value), ...]
"""
vText = types_factory['text']
properties = [('BEGIN', vText(self.name).ical())]
property_names = self.keys()
property_names.sort()
for name in property_names:
values = self[name]
if type(values) == ListType:
# normally one property is one line
for value in values:
properties.append((name, value))
else:
properties.append((name, values))
# recursion is fun!
for subcomponent in self.subcomponents:
properties += subcomponent.property_items()
properties.append(('END', vText(self.name).ical()))
return properties
def from_string(st, multiple=False):
"""
Populates the component recursively from a string
"""
stack = [] # a stack of components
comps = []
for line in Contentlines.from_string(st): # raw parsing
if not line:
continue
name, params, vals = line.parts()
uname = name.upper()
# check for start of component
if uname == 'BEGIN':
# try and create one of the components defined in the spec,
# otherwise get a general Components for robustness.
component_name = vals.upper()
component_class = component_factory.get(component_name, Component)
component = component_class()
if not getattr(component, 'name', ''): # for undefined components
component.name = component_name
stack.append(component)
# check for end of event
elif uname == 'END':
# we are done adding properties to this component
# so pop it from the stack and add it to the new top.
component = stack.pop()
if not stack: # we are at the end
comps.append(component)
else:
stack[-1].add_component(component)
# we are adding properties to the current top of the stack
else:
factory = types_factory.for_property(name)
vals = factory(factory.from_ical(vals))
vals.params = params
stack[-1].add(name, vals, encode=0)
if multiple:
return comps
if not len(comps) == 1:
raise ValueError('Found multiple components where '
'only one is allowed')
return comps[0]
from_string = staticmethod(from_string)
def __repr__(self):
return '%s(' % self.name + dict.__repr__(self) + ')'
# def content_line(self, name):
# "Returns property as content line"
# value = self[name]
# params = getattr(value, 'params', Parameters())
# return Contentline.from_parts((name, params, value))
def content_lines(self):
"Converts the Component and subcomponents into content lines"
contentlines = Contentlines()
for name, values in self.property_items():
params = getattr(values, 'params', Parameters())
contentlines.append(Contentline.from_parts((name, params, values)))
contentlines.append('') # remember the empty string in the end
return contentlines
def as_string(self):
return str(self.content_lines())
def __str__(self):
"Returns rendered iCalendar"
return self.as_string()
#######################################
# components defined in RFC 2445
class Event(Component):
name = 'VEVENT'
required = ('UID',)
singletons = (
'CLASS', 'CREATED', 'DESCRIPTION', 'DTSTART', 'GEO',
'LAST-MOD', 'LOCATION', 'ORGANIZER', 'PRIORITY', 'DTSTAMP', 'SEQUENCE',
'STATUS', 'SUMMARY', 'TRANSP', 'URL', 'RECURID', 'DTEND', 'DURATION',
'DTSTART',
)
exclusive = ('DTEND', 'DURATION', )
multiple = (
'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT','CONTACT', 'EXDATE',
'EXRULE', 'RSTATUS', 'RELATED', 'RESOURCES', 'RDATE', 'RRULE'
)
class Todo(Component):
name = 'VTODO'
required = ('UID',)
singletons = (
'CLASS', 'COMPLETED', 'CREATED', 'DESCRIPTION', 'DTSTAMP', 'DTSTART',
'GEO', 'LAST-MOD', 'LOCATION', 'ORGANIZER', 'PERCENT', 'PRIORITY',
'RECURID', 'SEQUENCE', 'STATUS', 'SUMMARY', 'UID', 'URL', 'DUE', 'DURATION',
)
exclusive = ('DUE', 'DURATION',)
multiple = (
'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE',
'EXRULE', 'RSTATUS', 'RELATED', 'RESOURCES', 'RDATE', 'RRULE'
)
class Journal(Component):
name = 'VJOURNAL'
required = ('UID',)
singletons = (
'CLASS', 'CREATED', 'DESCRIPTION', 'DTSTART', 'DTSTAMP', 'LAST-MOD',
'ORGANIZER', 'RECURID', 'SEQUENCE', 'STATUS', 'SUMMARY', 'UID', 'URL',
)
multiple = (
'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE',
'EXRULE', 'RELATED', 'RDATE', 'RRULE', 'RSTATUS',
)
class FreeBusy(Component):
name = 'VFREEBUSY'
required = ('UID',)
singletons = (
'CONTACT', 'DTSTART', 'DTEND', 'DURATION', 'DTSTAMP', 'ORGANIZER',
'UID', 'URL',
)
multiple = ('ATTENDEE', 'COMMENT', 'FREEBUSY', 'RSTATUS',)
class Timezone(Component):
name = 'VTIMEZONE'
required = (
'TZID', 'STANDARDC', 'DAYLIGHTC', 'DTSTART', 'TZOFFSETTO',
'TZOFFSETFROM'
)
singletons = ('LAST-MOD', 'TZURL', 'TZID',)
multiple = ('COMMENT', 'RDATE', 'RRULE', 'TZNAME',)
class Alarm(Component):
name = 'VALARM'
# not quite sure about these ...
required = ('ACTION', 'TRIGGER',)
singletons = ('ATTACH', 'ACTION', 'TRIGGER', 'DURATION', 'REPEAT',)
inclusive = (('DURATION', 'REPEAT',),)
multiple = ('STANDARDC', 'DAYLIGHTC')
class Calendar(Component):
"""
This is the base object for an iCalendar file.
Setting up a minimal calendar component looks like this
>>> cal = Calendar()
Som properties are required to be compliant
>>> cal['prodid'] = '-//My calendar product//mxm.dk//'
>>> cal['version'] = '2.0'
We also need at least one subcomponent for a calendar to be compliant
>>> from datetime import datetime
>>> event = Event()
>>> event['summary'] = 'Python meeting about calendaring'
>>> event['uid'] = '42'
>>> event.set('dtstart', datetime(2005,4,4,8,0,0))
>>> cal.add_component(event)
>>> cal.subcomponents[0].as_string()
'BEGIN:VEVENT\\r\\nDTSTART:20050404T080000\\r\\nSUMMARY:Python meeting about calendaring\\r\\nUID:42\\r\\nEND:VEVENT\\r\\n'
Write to disc
>>> import tempfile, os
>>> directory = tempfile.mkdtemp()
>>> open(os.path.join(directory, 'test.ics'), 'wb').write(cal.as_string())
"""
name = 'VCALENDAR'
required = ('prodid', 'version', )
singletons = ('prodid', 'version', )
multiple = ('calscale', 'method', )
# These are read only singleton, so one instance is enough for the module
types_factory = TypesFactory()
component_factory = ComponentFactory()

View File

@ -0,0 +1,93 @@
# -*- coding: latin-1 -*-
class CaselessDict(dict):
"""
A dictionary that isn't case sensitive, and only use string as keys.
>>> ncd = CaselessDict(key1='val1', key2='val2')
>>> ncd
CaselessDict({'KEY2': 'val2', 'KEY1': 'val1'})
>>> ncd['key1']
'val1'
>>> ncd['KEY1']
'val1'
>>> ncd['KEY3'] = 'val3'
>>> ncd['key3']
'val3'
>>> ncd.setdefault('key3', 'FOUND')
'val3'
>>> ncd.setdefault('key4', 'NOT FOUND')
'NOT FOUND'
>>> ncd['key4']
'NOT FOUND'
>>> ncd.get('key1')
'val1'
>>> ncd.get('key3', 'NOT FOUND')
'val3'
>>> ncd.get('key4', 'NOT FOUND')
'NOT FOUND'
>>> 'key4' in ncd
True
>>> del ncd['key4']
>>> ncd.has_key('key4')
False
>>> ncd.update({'key5':'val5', 'KEY6':'val6', 'KEY5':'val7'})
>>> ncd['key6']
'val6'
>>> keys = ncd.keys()
>>> keys.sort()
>>> keys
['KEY1', 'KEY2', 'KEY3', 'KEY5', 'KEY6']
"""
def __init__(self, *args, **kwargs):
"Set keys to upper for initial dict"
dict.__init__(self, *args, **kwargs)
for k,v in self.items():
k_upper = k.upper()
if k != k_upper:
dict.__delitem__(self, k)
self[k_upper] = v
def __getitem__(self, key):
return dict.__getitem__(self, key.upper())
def __setitem__(self, key, value):
dict.__setitem__(self, key.upper(), value)
def __delitem__(self, key):
dict.__delitem__(self, key.upper())
def __contains__(self, item):
return dict.__contains__(self, item.upper())
def get(self, key, default=None):
return dict.get(self, key.upper(), default)
def setdefault(self, key, value=None):
return dict.setdefault(self, key.upper(), value)
def pop(self, key, default=None):
return dict.pop(self, key.upper(), default)
def popitem(self):
return dict.popitem(self)
def has_key(self, key):
return dict.has_key(self, key.upper())
def update(self, indict):
"""
Multiple keys where key1.upper() == key2.upper() will be lost.
"""
for entry in indict:
self[entry] = indict[entry]
def copy(self):
return CaselessDict(dict.copy(self))
def clear(self):
dict.clear(self)
def __repr__(self):
return 'CaselessDict(' + dict.__repr__(self) + ')'

View File

@ -0,0 +1,262 @@
try:
from zope.interface import Interface, Attribute
except ImportError:
class Interface:
"""A dummy interface base class"""
class Attribute:
"""A dummy attribute implementation"""
def __init__(self, doc):
self.doc = doc
_marker = object()
class IComponent(Interface):
"""
Component is the base object for calendar, Event and the other
components defined in RFC 2445.
A component is like a dictionary with extra methods and attributes.
"""
# MANIPULATORS
def __setitem__(name, value):
"""Set a property.
name - case insensitive name
value - value of the property to set. This can be either a single
item or a list.
Some iCalendar properties are set INLINE; these properties
have multiple values on one property line in the iCalendar
representation. The list can be supplied as a comma separated
string to __setitem__. If special iCalendar characters exist in
an entry, such as the colon (:) and (,), that comma-separated
entry needs to be quoted with double quotes. For example:
'foo, bar, "baz:hoi"'
See also set_inline() for an easier way to deal with this case.
"""
def set_inline(name, values, encode=1):
"""Set list of INLINE values for property.
Converts a list of values into valid iCalendar comma seperated
string and sets value to that.
name - case insensitive name of property
values - list of values to set
encode - if True, encode Python values as iCalendar types first.
"""
def add(name, value):
"""Add a property. Can be called multiple times to set a list.
name - case insensitive name
value - value of property to set or add to list for this property.
"""
def add_component(component):
"""Add a nested subcomponent to this component.
"""
# static method, can be called on class directly
def from_string(st, multiple=False):
"""Populates the component recursively from a iCalendar string.
Reads the iCalendar string and constructs components and
subcomponents out of it.
"""
# ACCESSORS
def __getitem__(name):
"""Get a property
name - case insensitive name
Returns an iCalendar property object such as vText.
"""
def decoded(name, default=_marker):
"""Get a property as a python object.
name - case insensitive name
default - optional argument. If supplied, will use this if
name cannot be found. If not supplied, decoded will raise a
KeyError if name cannot be found.
Returns python object (such as unicode string, datetime, etc).
"""
def get_inline(name, decode=1):
"""Get list of INLINE values from property.
name - case insensitive name
decode - decode to Python objects.
Returns list of python objects.
"""
def as_string():
"""Render the component in the RFC 2445 (iCalendar) format.
Returns a string in RFC 2445 format.
"""
subcomponents = Attribute("""
A list of all subcomponents of this component,
added using add_component()""")
name = Attribute("""
Name of this component (VEVENT, etc)
""")
def walk(name=None):
"""Recursively traverses component and subcomponents.
name - optional, if given, only return components with that name
Returns sequence of components.
"""
def property_items():
"""Return properties as (name, value) tuples.
Returns all properties in this comopnent and subcomponents as
name, value tuples.
"""
class IEvent(IComponent):
"""A component which conforms to an iCalendar VEVENT.
"""
class ITodo(IComponent):
"""A component which conforms to an iCalendar VTODO.
"""
class IJournal(IComponent):
"""A component which conforms to an iCalendar VJOURNAL.
"""
class IFreeBusy(IComponent):
"""A component which conforms to an iCalendar VFREEBUSY.
"""
class ITimezone(IComponent):
"""A component which conforms to an iCalendar VTIMEZONE.
"""
class IAlarm(IComponent):
"""A component which conforms to an iCalendar VALARM.
"""
class ICalendar(IComponent):
"""A component which conforms to an iCalendar VCALENDAR.
"""
class IPropertyValue(Interface):
"""An iCalendar property value.
iCalendar properties have strongly typed values.
This invariance should always be true:
assert x == vDataType.from_ical(vDataType(x).ical())
"""
def ical():
"""Render property as string, as defined in iCalendar RFC 2445.
"""
# this is a static method
def from_ical(ical):
"""Parse property from iCalendar RFC 2445 text.
Inverse of ical().
"""
class IBinary(IPropertyValue):
"""Binary property values are base 64 encoded
"""
class IBoolean(IPropertyValue):
"""Boolean property.
Also behaves like a python int.
"""
class ICalAddress(IPropertyValue):
"""Email address.
Also behaves like a python str.
"""
class IDateTime(IPropertyValue):
"""Render and generates iCalendar datetime format.
Important: if tzinfo is defined it renders itself as 'date with utc time'
Meaning that it has a 'Z' appended, and is in absolute time.
"""
class IDate(IPropertyValue):
"""Render and generates iCalendar date format.
"""
class IDuration(IPropertyValue):
"""Render and generates timedelta in iCalendar DURATION format.
"""
class IFloat(IPropertyValue):
"""Render and generate floats in iCalendar format.
Also behaves like a python float.
"""
class IInt(IPropertyValue):
"""Render and generate ints in iCalendar format.
Also behaves like a python int.
"""
class IPeriod(IPropertyValue):
"""A precise period of time (datetime, datetime).
"""
class IWeekDay(IPropertyValue):
"""Render and generate weekday abbreviation.
"""
class IFrequency(IPropertyValue):
"""Frequency.
"""
class IRecur(IPropertyValue):
"""Render and generate data based on recurrent event representation.
This acts like a caseless dictionary.
"""
class IText(IPropertyValue):
"""Unicode text.
"""
class ITime(IPropertyValue):
"""Time.
"""
class IUri(IPropertyValue):
"""URI
"""
class IGeo(IPropertyValue):
"""Geographical location.
"""
class IUTCOffset(IPropertyValue):
"""Offset from UTC.
"""
class IInline(IPropertyValue):
"""Inline list.
"""

522
Webcal/icalendar/parser.py Normal file
View File

@ -0,0 +1,522 @@
# -*- coding: latin-1 -*-
"""
This module parses and generates contentlines as defined in RFC 2445
(iCalendar), but will probably work for other MIME types with similar syntax.
Eg. RFC 2426 (vCard)
It is stupid in the sense that it treats the content purely as strings. No type
conversion is attempted.
Copyright, 2005: Max M <maxm@mxm.dk>
License: GPL (Just contact med if and why you would like it changed)
"""
# from python
from types import TupleType, ListType
SequenceTypes = [TupleType, ListType]
import re
# from this package
from icalendar.caselessdict import CaselessDict
#################################################################
# Property parameter stuff
def paramVal(val):
"Returns a parameter value"
if type(val) in SequenceTypes:
return q_join(val)
return dQuote(val)
# Could be improved
NAME = re.compile('[\w-]+')
UNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7F",:;]')
QUNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7F"]')
FOLD = re.compile('([\r]?\n)+[ \t]{1}')
def validate_token(name):
match = NAME.findall(name)
if len(match) == 1 and name == match[0]:
return
raise ValueError, name
def validate_param_value(value, quoted=True):
validator = UNSAFE_CHAR
if quoted:
validator = QUNSAFE_CHAR
if validator.findall(value):
raise ValueError, value
QUOTABLE = re.compile('[,;:].')
def dQuote(val):
"""
Parameter values containing [,;:] must be double quoted
>>> dQuote('Max')
'Max'
>>> dQuote('Rasmussen, Max')
'"Rasmussen, Max"'
>>> dQuote('name:value')
'"name:value"'
"""
if QUOTABLE.search(val):
return '"%s"' % val
return val
# parsing helper
def q_split(st, sep=','):
"""
Splits a string on char, taking double (q)uotes into considderation
>>> q_split('Max,Moller,"Rasmussen, Max"')
['Max', 'Moller', '"Rasmussen, Max"']
"""
result = []
cursor = 0
length = len(st)
inquote = 0
for i in range(length):
ch = st[i]
if ch == '"':
inquote = not inquote
if not inquote and ch == sep:
result.append(st[cursor:i])
cursor = i + 1
if i + 1 == length:
result.append(st[cursor:])
return result
def q_join(lst, sep=','):
"""
Joins a list on sep, quoting strings with QUOTABLE chars
>>> s = ['Max', 'Moller', 'Rasmussen, Max']
>>> q_join(s)
'Max,Moller,"Rasmussen, Max"'
"""
return sep.join([dQuote(itm) for itm in lst])
class Parameters(CaselessDict):
"""
Parser and generator of Property parameter strings. It knows nothing of
datatypes. It's main concern is textual structure.
Simple parameter:value pair
>>> p = Parameters(parameter1='Value1')
>>> str(p)
'PARAMETER1=Value1'
keys are converted to upper
>>> p.keys()
['PARAMETER1']
Parameters are case insensitive
>>> p['parameter1']
'Value1'
>>> p['PARAMETER1']
'Value1'
Parameter with list of values must be seperated by comma
>>> p = Parameters({'parameter1':['Value1', 'Value2']})
>>> str(p)
'PARAMETER1=Value1,Value2'
Multiple parameters must be seperated by a semicolon
>>> p = Parameters({'RSVP':'TRUE', 'ROLE':'REQ-PARTICIPANT'})
>>> str(p)
'ROLE=REQ-PARTICIPANT;RSVP=TRUE'
Parameter values containing ',;:' must be double quoted
>>> p = Parameters({'ALTREP':'http://www.wiz.org'})
>>> str(p)
'ALTREP="http://www.wiz.org"'
list items must be quoted seperately
>>> p = Parameters({'MEMBER':['MAILTO:projectA@host.com', 'MAILTO:projectB@host.com', ]})
>>> str(p)
'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"'
Now the whole sheebang
>>> p = Parameters({'parameter1':'Value1', 'parameter2':['Value2', 'Value3'],\
'ALTREP':['http://www.wiz.org', 'value4']})
>>> str(p)
'ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;PARAMETER2=Value2,Value3'
We can also parse parameter strings
>>> Parameters.from_string('PARAMETER1=Value 1;param2=Value 2')
Parameters({'PARAMETER1': 'Value 1', 'PARAM2': 'Value 2'})
Including empty strings
>>> Parameters.from_string('param=')
Parameters({'PARAM': ''})
We can also parse parameter strings
>>> Parameters.from_string('MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"')
Parameters({'MEMBER': ['MAILTO:projectA@host.com', 'MAILTO:projectB@host.com']})
We can also parse parameter strings
>>> Parameters.from_string('ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;PARAMETER2=Value2,Value3')
Parameters({'PARAMETER1': 'Value1', 'ALTREP': ['http://www.wiz.org', 'value4'], 'PARAMETER2': ['Value2', 'Value3']})
"""
def params(self):
"""
in rfc2445 keys are called parameters, so this is to be consitent with
the naming conventions
"""
return self.keys()
### Later, when I get more time... need to finish this off now. The last majot thing missing.
### def _encode(self, name, value, cond=1):
### # internal, for conditional convertion of values.
### if cond:
### klass = types_factory.for_property(name)
### return klass(value)
### return value
###
### def add(self, name, value, encode=0):
### "Add a parameter value and optionally encode it."
### if encode:
### value = self._encode(name, value, encode)
### self[name] = value
###
### def decoded(self, name):
### "returns a decoded value, or list of same"
def __repr__(self):
return 'Parameters(' + dict.__repr__(self) + ')'
def __str__(self):
result = []
items = self.items()
items.sort() # To make doctests work
for key, value in items:
value = paramVal(value)
result.append('%s=%s' % (key.upper(), value))
return ';'.join(result)
def from_string(st, strict=False):
"Parses the parameter format from ical text format"
try:
# parse into strings
result = Parameters()
for param in q_split(st, ';'):
key, val = q_split(param, '=')
validate_token(key)
param_values = [v for v in q_split(val, ',')]
# Property parameter values that are not in quoted
# strings are case insensitive.
vals = []
for v in param_values:
if v.startswith('"') and v.endswith('"'):
v = v.strip('"')
validate_param_value(v, quoted=True)
vals.append(v)
else:
validate_param_value(v, quoted=False)
if strict:
vals.append(v.upper())
else:
vals.append(v)
if not vals:
result[key] = val
else:
if len(vals) == 1:
result[key] = vals[0]
else:
result[key] = vals
return result
except:
raise ValueError, 'Not a valid parameter string'
from_string = staticmethod(from_string)
#########################################
# parsing and generation of content lines
class Contentline(str):
"""
A content line is basically a string that can be folded and parsed into
parts.
>>> c = Contentline('Si meliora dies, ut vina, poemata reddit')
>>> str(c)
'Si meliora dies, ut vina, poemata reddit'
A long line gets folded
>>> c = Contentline(''.join(['123456789 ']*10))
>>> str(c)
'123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234\\r\\n 56789 123456789 123456789 '
A folded line gets unfolded
>>> c = Contentline.from_string(str(c))
>>> c
'123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 '
We do not fold within a UTF-8 character:
>>> c = Contentline('This line has a UTF-8 character where it should be folded. Make sure it g\xc3\xabts folded before that character.')
>>> '\xc3\xab' in str(c)
True
Don't fail if we fold a line that is exactly X times 74 characters long:
>>> c = str(Contentline(''.join(['x']*148)))
It can parse itself into parts. Which is a tuple of (name, params, vals)
>>> c = Contentline('dtstart:20050101T120000')
>>> c.parts()
('dtstart', Parameters({}), '20050101T120000')
>>> c = Contentline('dtstart;value=datetime:20050101T120000')
>>> c.parts()
('dtstart', Parameters({'VALUE': 'datetime'}), '20050101T120000')
>>> c = Contentline('ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com')
>>> c.parts()
('ATTENDEE', Parameters({'ROLE': 'REQ-PARTICIPANT', 'CN': 'Max Rasmussen'}), 'MAILTO:maxm@example.com')
>>> str(c)
'ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com'
and back again
>>> parts = ('ATTENDEE', Parameters({'ROLE': 'REQ-PARTICIPANT', 'CN': 'Max Rasmussen'}), 'MAILTO:maxm@example.com')
>>> Contentline.from_parts(parts)
'ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com'
and again
>>> parts = ('ATTENDEE', Parameters(), 'MAILTO:maxm@example.com')
>>> Contentline.from_parts(parts)
'ATTENDEE:MAILTO:maxm@example.com'
A value can also be any of the types defined in PropertyValues
>>> from icalendar.prop import vText
>>> parts = ('ATTENDEE', Parameters(), vText('MAILTO:test@example.com'))
>>> Contentline.from_parts(parts)
'ATTENDEE:MAILTO:test@example.com'
A value can also be unicode
>>> from icalendar.prop import vText
>>> parts = ('SUMMARY', Parameters(), vText(u'INternational char <20> <20> <20>'))
>>> Contentline.from_parts(parts)
'SUMMARY:INternational char \\xc3\\xa6 \\xc3\\xb8 \\xc3\\xa5'
Traversing could look like this.
>>> name, params, vals = c.parts()
>>> name
'ATTENDEE'
>>> vals
'MAILTO:maxm@example.com'
>>> for key, val in params.items():
... (key, val)
('ROLE', 'REQ-PARTICIPANT')
('CN', 'Max Rasmussen')
And the traditional failure
>>> c = Contentline('ATTENDEE;maxm@example.com')
>>> c.parts()
Traceback (most recent call last):
...
ValueError: Content line could not be parsed into parts
Another failure:
>>> c = Contentline(':maxm@example.com')
>>> c.parts()
Traceback (most recent call last):
...
ValueError: Content line could not be parsed into parts
>>> c = Contentline('key;param=:value')
>>> c.parts()
('key', Parameters({'PARAM': ''}), 'value')
>>> c = Contentline('key;param="pvalue":value')
>>> c.parts()
('key', Parameters({'PARAM': 'pvalue'}), 'value')
Should bomb on missing param:
>>> c = Contentline.from_string("k;:no param")
>>> c.parts()
Traceback (most recent call last):
...
ValueError: Content line could not be parsed into parts
>>> c = Contentline('key;param=pvalue:value', strict=False)
>>> c.parts()
('key', Parameters({'PARAM': 'pvalue'}), 'value')
If strict is set to True, uppercase param values that are not
double-quoted, this is because the spec says non-quoted params are
case-insensitive.
>>> c = Contentline('key;param=pvalue:value', strict=True)
>>> c.parts()
('key', Parameters({'PARAM': 'PVALUE'}), 'value')
>>> c = Contentline('key;param="pValue":value', strict=True)
>>> c.parts()
('key', Parameters({'PARAM': 'pValue'}), 'value')
"""
def __new__(cls, st, strict=False):
self = str.__new__(cls, st)
setattr(self, 'strict', strict)
return self
def from_parts(parts):
"Turns a tuple of parts into a content line"
(name, params, values) = [str(p) for p in parts]
try:
if params:
return Contentline('%s;%s:%s' % (name, params, values))
return Contentline('%s:%s' % (name, values))
except:
raise ValueError(
'Property: %s Wrong values "%s" or "%s"' % (repr(name),
repr(params),
repr(values)))
from_parts = staticmethod(from_parts)
def parts(self):
""" Splits the content line up into (name, parameters, values) parts
"""
try:
name_split = None
value_split = None
inquotes = 0
for i in range(len(self)):
ch = self[i]
if not inquotes:
if ch in ':;' and not name_split:
name_split = i
if ch == ':' and not value_split:
value_split = i
if ch == '"':
inquotes = not inquotes
name = self[:name_split]
if not name:
raise ValueError, 'Key name is required'
validate_token(name)
if name_split+1 == value_split:
raise ValueError, 'Invalid content line'
params = Parameters.from_string(self[name_split+1:value_split],
strict=self.strict)
values = self[value_split+1:]
return (name, params, values)
except:
raise ValueError, 'Content line could not be parsed into parts'
def from_string(st, strict=False):
"Unfolds the content lines in an iCalendar into long content lines"
try:
# a fold is carriage return followed by either a space or a tab
return Contentline(FOLD.sub('', st), strict=strict)
except:
raise ValueError, 'Expected StringType with content line'
from_string = staticmethod(from_string)
def __str__(self):
"Long content lines are folded so they are less than 75 characters wide"
l_line = len(self)
new_lines = []
start = 0
end = 74
while True:
if end >= l_line:
end = l_line
else:
# Check that we don't fold in the middle of a UTF-8 character:
# http://lists.osafoundation.org/pipermail/ietf-calsify/2006-August/001126.html
while True:
char_value = ord(self[end])
if char_value < 128 or char_value >= 192:
# This is not in the middle of a UTF-8 character, so we
# can fold here:
break
else:
end -= 1
new_lines.append(self[start:end])
if end == l_line:
# Done
break
start = end
end = start + 74
return '\r\n '.join(new_lines)
class Contentlines(list):
"""
I assume that iCalendar files generally are a few kilobytes in size. Then
this should be efficient. for Huge files, an iterator should probably be
used instead.
>>> c = Contentlines([Contentline('BEGIN:VEVENT\\r\\n')])
>>> str(c)
'BEGIN:VEVENT\\r\\n'
Lets try appending it with a 100 charater wide string
>>> c.append(Contentline(''.join(['123456789 ']*10)+'\\r\\n'))
>>> str(c)
'BEGIN:VEVENT\\r\\n\\r\\n123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234\\r\\n 56789 123456789 123456789 \\r\\n'
Notice that there is an extra empty string in the end of the content lines.
That is so they can be easily joined with: '\r\n'.join(contentlines)).
>>> Contentlines.from_string('A short line\\r\\n')
['A short line', '']
>>> Contentlines.from_string('A faked\\r\\n long line\\r\\n')
['A faked long line', '']
>>> Contentlines.from_string('A faked\\r\\n long line\\r\\nAnd another lin\\r\\n\\te that is folded\\r\\n')
['A faked long line', 'And another line that is folded', '']
"""
def __str__(self):
"Simply join self."
return '\r\n'.join(map(str, self))
def from_string(st):
"Parses a string into content lines"
try:
# a fold is carriage return followed by either a space or a tab
unfolded = FOLD.sub('', st)
lines = [Contentline(line) for line in unfolded.splitlines() if line]
lines.append('') # we need a '\r\n' in the end of every content line
return Contentlines(lines)
except:
raise ValueError, 'Expected StringType with content lines'
from_string = staticmethod(from_string)
# ran this:
# sample = open('./samples/test.ics', 'rb').read() # binary file in windows!
# lines = Contentlines.from_string(sample)
# for line in lines[:-1]:
# print line.parts()
# got this:
#('BEGIN', Parameters({}), 'VCALENDAR')
#('METHOD', Parameters({}), 'Request')
#('PRODID', Parameters({}), '-//My product//mxm.dk/')
#('VERSION', Parameters({}), '2.0')
#('BEGIN', Parameters({}), 'VEVENT')
#('DESCRIPTION', Parameters({}), 'This is a very long description that ...')
#('PARTICIPANT', Parameters({'CN': 'Max M'}), 'MAILTO:maxm@mxm.dk')
#('DTEND', Parameters({}), '20050107T160000')
#('DTSTART', Parameters({}), '20050107T120000')
#('SUMMARY', Parameters({}), 'A second event')
#('END', Parameters({}), 'VEVENT')
#('BEGIN', Parameters({}), 'VEVENT')
#('DTEND', Parameters({}), '20050108T235900')
#('DTSTART', Parameters({}), '20050108T230000')
#('SUMMARY', Parameters({}), 'A single event')
#('UID', Parameters({}), '42')
#('END', Parameters({}), 'VEVENT')
#('END', Parameters({}), 'VCALENDAR')

1513
Webcal/icalendar/prop.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
# this is a package

View File

@ -0,0 +1,16 @@
import unittest, doctest, os
from icalendar import cal, caselessdict, parser, prop
def test_suite():
suite = unittest.TestSuite()
suite.addTest(doctest.DocTestSuite(caselessdict))
suite.addTest(doctest.DocTestSuite(parser))
suite.addTest(doctest.DocTestSuite(prop))
suite.addTest(doctest.DocTestSuite(cal))
doc_dir = '../../../doc'
for docfile in ['example.txt', 'groupscheduled.txt',
'small.txt', 'multiple.txt', 'recurrence.txt']:
suite.addTest(doctest.DocFileSuite(os.path.join(doc_dir, docfile),
optionflags=doctest.ELLIPSIS),)
return suite

53
Webcal/icalendar/tools.py Normal file
View File

@ -0,0 +1,53 @@
from string import ascii_letters, digits
import random
"""
This module contains non-essential tools for iCalendar. Pretty thin so far eh?
"""
class UIDGenerator:
"""
If you are too lazy to create real uid's. Notice, this doctest is disabled!
Automatic semi-random uid
>> g = UIDGenerator()
>> uid = g.uid()
>> uid.ical()
'20050109T153222-7ekDDHKcw46QlwZK@example.com'
You Should at least insert your own hostname to be more complient
>> g = UIDGenerator()
>> uid = g.uid('Example.ORG')
>> uid.ical()
'20050109T153549-NbUItOPDjQj8Ux6q@Example.ORG'
You can also insert a path or similar
>> g = UIDGenerator()
>> uid = g.uid('Example.ORG', '/path/to/content')
>> uid.ical()
'20050109T153415-/path/to/content@Example.ORG'
"""
chars = list(ascii_letters + digits)
def rnd_string(self, length=16):
"Generates a string with random characters of length"
return ''.join([random.choice(self.chars) for i in range(length)])
def uid(self, host_name='example.com', unique=''):
"""
Generates a unique id consisting of:
datetime-uniquevalue@host. Like:
20050105T225746Z-HKtJMqUgdO0jDUwm@example.com
"""
from PropertyValues import vText, vDatetime
unique = unique or self.rnd_string()
return vText('%s-%s@%s' % (vDatetime.today().ical(), unique, host_name))
if __name__ == "__main__":
import os.path, doctest, tools
# import and test this file
doctest.testmod(tools)

50
Webcal/icalendar/util.py Normal file
View File

@ -0,0 +1,50 @@
from string import ascii_letters, digits
import random
"""
This module contains non-essential tools for iCalendar. Pretty thin so far eh?
"""
class UIDGenerator:
"""
If you are too lazy to create real uids.
NOTE: this doctest is disabled
(only two > instead of three)
Automatic semi-random uid
>> g = UIDGenerator()
>> uid = g.uid()
>> uid.ical()
'20050109T153222-7ekDDHKcw46QlwZK@example.com'
You should at least insert your own hostname to be more compliant
>> g = UIDGenerator()
>> uid = g.uid('Example.ORG')
>> uid.ical()
'20050109T153549-NbUItOPDjQj8Ux6q@Example.ORG'
You can also insert a path or similar
>> g = UIDGenerator()
>> uid = g.uid('Example.ORG', '/path/to/content')
>> uid.ical()
'20050109T153415-/path/to/content@Example.ORG'
"""
chars = list(ascii_letters + digits)
def rnd_string(self, length=16):
"Generates a string with random characters of length"
return ''.join([random.choice(self.chars) for i in range(length)])
def uid(self, host_name='example.com', unique=''):
"""
Generates a unique id consisting of:
datetime-uniquevalue@host. Like:
20050105T225746Z-HKtJMqUgdO0jDUwm@example.com
"""
from PropertyValues import vText, vDatetime
unique = unique or self.rnd_string()
return vText('%s-%s@%s' % (vDatetime.today().ical(), unique, host_name))

View File

@ -19,11 +19,14 @@ import supybot.ircutils as ircutils
import supybot.callbacks as callbacks
import supybot.schedule as schedule
import supybot.ircmsgs as ircmsgs
import supybot.ircdb as ircdb
import supybot.conf as conf
import pytz
import ical
import datetime, shelve, re
import cPickle as pickle
try:
import supybot.plugin as plugin
LoggerWrapper = plugin.loadPluginModule("IRCLog", False).LoggerWrapper
except Exception, e:
def LoggerWrapper(self): return self.log
def checkIgnored(hostmask, recipient='', users=ircdb.users, channels=ircdb.channels):
if ircdb.ignores.checkIgnored(hostmask):
@ -58,25 +61,38 @@ def checkIgnored(hostmask, recipient='', users=ircdb.users, channels=ircdb.chann
else:
return False
import pytz
import ical
reload(ical)
import datetime, shelve, re
import cPickle as pickle
class Webcal(callbacks.Plugin):
"""@schedule <timezone>: display the schedule in your timezone"""
"""@schedule <timezone>
display the schedule in your timezone
"""
threaded = True
noIgnore = True
def __init__(self, irc):
callbacks.Privmsg.__init__(self, irc)
parent = super(Webcal, self)
parent.__init__(irc)
self.log = LoggerWrapper(self)
self.irc = irc
self.cache = {}
self.firstevent = {}
try:
schedule.removeEvent(self.name())
schedule.removeEvent(self.name() + 'b')
except Exception: # Oh well
except AssertionError:
pass
except KeyError:
pass
try:
schedule.addPeriodicEvent(self.refresh_cache, 60 * 20, name=self.name())
schedule.addPeriodicEvent(self.autotopics, 60, name=self.name() + 'b')
except Exception: # Just work
except AssertionError:
pass
self.cache = {}
self.firstevent = {}
def die(self):
try:
@ -116,22 +132,38 @@ class Webcal(callbacks.Plugin):
def filter(self, events, channel):
now = datetime.datetime.now(pytz.UTC)
fword = self.registryValue('filter', channel)
return [x for x in events if fword.lower() in x.raw_data.lower() and x.seconds_ago() < 1800]
ret = [x for x in events if fword.lower() in x.raw_data.lower() and x.seconds_ago() < 1800]
ret = [x for x in ret if self.filterChannel(x)]
ret.sort()
ret.sort()
return ret
def filterChannel(self, event):
desc = event['description']
where = u'#ubuntu-meeting'
if "Location\\:" in desc:
where = desc.split('<')[1].split()[-1]
if where[0] != u'#':
where = u'#ubuntu-meeting'
return where == u'#ubuntu-meeting'
def maketopic(self, c, tz=None, template='%s', num_events=6):
url = self.registryValue('url',c)
if not tz:
tz = 'UTC'
if url not in self.cache.keys():
self.update(url)
now = datetime.datetime.now(pytz.UTC)
events = self.filter(self.cache[url],c)[:num_events]
# events.sort()
preamble = ''
if not len(events):
return template % "No meetings scheduled"
# The standard slack of 30 minutes after the meeting will be an
# error if there are 2 conscutive meetings, so remove the first
# one in that case
if len(events) > 1 and events[1].startDate < now:
if len(events) > 1 and events[1].startTime() < now:
events = events[1:]
ev0 = events[0]
if ev0.seconds_to_go() < 600:
@ -149,6 +181,9 @@ class Webcal(callbacks.Plugin):
# Now the commands
def topic(self, irc, msg, args):
"""No args
Updates the topics in the channel
"""
c = msg.args[0]
url = self.registryValue('url', c)
if not url or not self.registryValue('doTopic',channel=c):
@ -156,14 +191,14 @@ class Webcal(callbacks.Plugin):
self.update(url)
events = self.filter(self.cache[url], c)
if events[0].is_on():
if events and events[0].is_on():
irc.error("Won't update topic while a meeting is in progress")
return
newtopic = self.maketopic(c, template=self.registryValue('topic',c))
if not (newtopic.strip() == irc.state.getTopic(c).strip()):
irc.queueMsg(ircmsgs.topic(c, newtopic))
topic = wrap(topic)
topic = wrap(topic, [('checkCapability', 'admin')])
def _tzfilter(self, tz, ud):
if tz == ud:
@ -186,7 +221,9 @@ class Webcal(callbacks.Plugin):
return False
def schedule(self, irc, msg, args, tz):
""" Retrieve the date/time of scheduled meetings in a specific timezone """
"""[<timezone>]
Retrieve the date/time of scheduled meetings in a specific timezone, defaults to UTC
"""
if not tz:
tz = 'utc'
if irc.isChannel(msg.args[0]):
@ -196,15 +233,18 @@ class Webcal(callbacks.Plugin):
if not c:
return
url = self.registryValue('url', c)
if not url:
c = self.registryValue('defaultChannel')
url = self.registryValue('url', c)
if not url:
return
tzs = filter(lambda x: self._tzfilter(x.lower(),tz.lower()), pytz.all_timezones)
if not tzs:
irc.error('Unknown timezone: %s - Full list: %s' % (tz, self.config.registryValue('tzUrl') or 'Value not set'))
irc.error('Unknown timezone: %s - Full list: %s' % (tz, self.registryValue('tzUrl') or 'Value not set'))
return
newtopic = self.maketopic(c,tz=tzs[0])
events = self.filter(self.cache[url], msg.args[0])
if events[0].is_on(): # FIXME channel filter
if events and events[0].is_on(): # FIXME channel filter
irc.error('Please don\'t use @schedule during a meeting')
irc.reply('Schedule for %s: %s' % (tzs[0], newtopic), private=True)
else:
@ -212,7 +252,9 @@ class Webcal(callbacks.Plugin):
schedule = wrap(schedule, [additional('text')])
def now(self, irc, msg, args, tz):
""" Display the current time """
"""[<timezone>]
Display the current time, <timezone> defaults to UTC
"""
if not tz:
tz = 'utc'
if irc.isChannel(msg.args[0]):
@ -222,11 +264,14 @@ class Webcal(callbacks.Plugin):
if not c:
return
url = self.registryValue('url', c)
if not url:
c = self.registryValue('defaultChannel')
url = self.registryValue('url', c)
if not url:
return
tzs = filter(lambda x: self._tzfilter(x.lower(),tz.lower()), pytz.all_timezones)
if not tzs:
irc.error('Unknown timezone: %s - Full list: %s' % (tz, self.config.registryValue('tzUrl') or 'Value not set'))
irc.error('Unknown timezone: %s - Full list: %s' % (tz, self.registryValue('tzUrl') or 'Value not set'))
return
now = datetime.datetime.now(pytz.UTC)
newtopic = self.maketopic(c,tz=tzs[0],num_events=1)
@ -234,7 +279,7 @@ class Webcal(callbacks.Plugin):
newtopic = 'Current time in %s: %s - %s' % \
(tzs[0], datetime.datetime.now(pytz.UTC).astimezone(pytz.timezone(tzs[0])).strftime("%B %d %Y, %H:%M:%S"), newtopic)
if events[0].is_on(): # Fixme -- channel filter
if events and events[0].is_on(): # Fixme -- channel filter
irc.error('Please don\'t use @schedule during a meeting')
irc.reply(newtopic, private=True)
else:
@ -270,7 +315,7 @@ class Webcal(callbacks.Plugin):
return msg
try:
id = ircdb.users.getUserId(msg.prefix)
user = users.getUser(id)
user = ircdb.users.getUser(id)
return msg
except:
pass
@ -281,6 +326,5 @@ class Webcal(callbacks.Plugin):
tokens = callbacks.tokenize(s, channel=msg.args[0])
self.Proxy(irc, msg, tokens)
return msg
# self._callCommand([cmd], irc, msg, [])
Class = Webcal

61
Webcal/rruler.py Normal file
View File

@ -0,0 +1,61 @@
#!/usr/bin/python
from dateutil import rrule
import re, datetime
#wk_days = ('MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU')
wk_days = re.compile("([0-9]?)([M|T|W|F|S][O|U|E|H|R|A])")
rrule_map = {
'SECONDLY': rrule.SECONDLY,
'MINUTELY': rrule.MINUTELY,
'HOURLY': rrule.HOURLY,
'DAILY': rrule.DAILY,
'WEEKLY': rrule.WEEKLY,
'MONTHLY': rrule.MONTHLY,
'YEARLY': rrule.YEARLY,
'MO': rrule.MO,
'TU': rrule.TU,
'WE': rrule.WE,
'TH': rrule.TH,
'FR': rrule.FR,
'SA': rrule.SA,
'SU': rrule.SU }
def rrule_wrapper(*args, **kwargs):
for k, v in kwargs.iteritems():
if k == 'byday' or k == 'BYDAY':
del kwargs[k]
groups = wk_days.match(v[0]).groups()
if groups[0]:
kwargs['byweekday'] = rrule_map[groups[1]](int(groups[0]))
else:
kwargs['byweekday'] = rrule_map[groups[1]]
else:
del kwargs[k]
k = k.lower()
if isinstance(v, list):
if len(v) > 1:
res = []
for x in v:
if isinstance(x, basestring) and wk_days.match(x):
res.append(rrule_map[wk_days.match(x).group(1)])
elif v in rrule_map:
res.append(rrule_map[x])
elif isinstance(x, datetime.datetime):
res.append(datetime.datetime.fromordinal(x.toordinal()))
else:
res.append(v)
kwargs[k] = tuple(res)
else:
if isinstance(v[0], basestring) and wk_days.match(v[0]):
kwargs[k] = rrule_map[wk_days.match(v[0]).group(0)]
elif v[0] in rrule_map:
kwargs[k] = rrule_map[v[0]]
elif isinstance(v[0], datetime.datetime):
kwargs[k] = datetime.datetime.fromordinal(v[0].toordinal())
else:
kwargs[k] = v[0]
else:
kwargs[k] = v
return rrule.rrule(*args, **kwargs)

14
Webcal/testical.py Normal file
View File

@ -0,0 +1,14 @@
#!/usr/bin/python
import datetime, pytz, urllib2, ical
def filter(events):
ret = [x for x in events if x.seconds_ago() < 1800]
ret.sort()
ret.sort() # Needs this twice for some reason
return ret
data = urllib2.urlopen("http://tinyurl.com/6mzmbr").read()
parser = ical.ICalReader(data)
events = filter(parser.events)
print "\n".join([x.schedule() for x in events])