Resync all running ubottu code to bzr branch, they should now be in sync again
This commit is contained in:
182
Webcal/cal.ical
Normal file
182
Webcal/cal.ical
Normal 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
|
299
Webcal/ical.py
299
Webcal/ical.py
@ -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
299
Webcal/ical.py.bak
Normal 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
286
Webcal/ical.py.bak.bac2
Normal 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
|
||||
|
16
Webcal/icalendar/__init__.py
Normal file
16
Webcal/icalendar/__init__.py
Normal 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
534
Webcal/icalendar/cal.py
Normal 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()
|
93
Webcal/icalendar/caselessdict.py
Normal file
93
Webcal/icalendar/caselessdict.py
Normal 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) + ')'
|
262
Webcal/icalendar/interfaces.py
Normal file
262
Webcal/icalendar/interfaces.py
Normal 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
522
Webcal/icalendar/parser.py
Normal 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
1513
Webcal/icalendar/prop.py
Normal file
File diff suppressed because it is too large
Load Diff
1
Webcal/icalendar/tests/__init__.py
Normal file
1
Webcal/icalendar/tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# this is a package
|
16
Webcal/icalendar/tests/test_icalendar.py
Normal file
16
Webcal/icalendar/tests/test_icalendar.py
Normal 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
53
Webcal/icalendar/tools.py
Normal 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
50
Webcal/icalendar/util.py
Normal 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))
|
@ -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
61
Webcal/rruler.py
Normal 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
14
Webcal/testical.py
Normal 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])
|
Reference in New Issue
Block a user