ubuntu-bots/Webcal/icalendar/prop.py

1514 lines
41 KiB
Python
Raw Blame History

# -*- coding: latin-1 -*-
"""
This module contains the parser/generators (or coders/encoders if you prefer)
for the classes/datatypes that are used in Icalendar:
###########################################################################
# This module defines these property value data types and property parameters
4.2 Defined property parameters are:
ALTREP, CN, CUTYPE, DELEGATED-FROM, DELEGATED-TO, DIR, ENCODING, FMTTYPE,
FBTYPE, LANGUAGE, MEMBER, PARTSTAT, RANGE, RELATED, RELTYPE, ROLE, RSVP,
SENT-BY, TZID, VALUE
4.3 Defined value data types are:
BINARY, BOOLEAN, CAL-ADDRESS, DATE, DATE-TIME, DURATION, FLOAT, INTEGER,
PERIOD, RECUR, TEXT, TIME, URI, UTC-OFFSET
###########################################################################
iCalendar properties has values. The values are strongly typed. This module
defines these types, calling val.ical() on them, Will render them as defined in
rfc2445.
If you pass any of these classes a Python primitive, you will have an object
that can render itself as iCalendar formatted date.
Property Value Data Types starts with a 'v'. they all have an ical() and
from_ical() method. The ical() method generates a text string in the iCalendar
format. The from_ical() method can parse this format and return a primitive
Python datatype. So it should allways be true that:
x == vDataType.from_ical(VDataType(x).ical())
These types are mainly used for parsing and file generation. But you can set
them directly.
"""
# from python >= 2.3
from datetime import datetime, timedelta, time, date, tzinfo
from types import IntType, StringType, UnicodeType, TupleType, ListType
SequenceTypes = [TupleType, ListType]
import re
import time as _time
# from this package
from icalendar.caselessdict import CaselessDict
from icalendar.parser import Parameters
DATE_PART = r'(\d+)D'
TIME_PART = r'T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?'
DATETIME_PART = '(?:%s)?(?:%s)?' % (DATE_PART, TIME_PART)
WEEKS_PART = r'(\d+)W'
DURATION_REGEX = re.compile(r'([-+]?)P(?:%s|%s)$'
% (WEEKS_PART, DATETIME_PART))
WEEKDAY_RULE = re.compile('(?P<signal>[+-]?)(?P<relative>[\d]?)'
'(?P<weekday>[\w]{2})$')
class vBinary:
"""
Binary property values are base 64 encoded
>>> b = vBinary('This is gibberish')
>>> b.ical()
'VGhpcyBpcyBnaWJiZXJpc2g='
>>> b = vBinary.from_ical('VGhpcyBpcyBnaWJiZXJpc2g=')
>>> b
'This is gibberish'
The roundtrip test
>>> x = 'Binary data <20> <20> <20> \x13 \x56'
>>> vBinary(x).ical()
'QmluYXJ5IGRhdGEg5iD4IOUgEyBW'
>>> vBinary.from_ical('QmluYXJ5IGRhdGEg5iD4IOUgEyBW')
'Binary data \\xe6 \\xf8 \\xe5 \\x13 V'
>>> b = vBinary('txt')
>>> b.params
Parameters({'VALUE': 'BINARY', 'ENCODING': 'BASE64'})
"""
def __init__(self, obj):
self.obj = obj
self.params = Parameters(encoding='BASE64', value="BINARY")
def __repr__(self):
return "vBinary(%s)" % str.__repr__(self.obj)
def ical(self):
return self.obj.encode('base-64')[:-1]
def from_ical(ical):
"Parses the data format from ical text format"
try:
return ical.decode('base-64')
except:
raise ValueError, 'Not valid base 64 encoding.'
from_ical = staticmethod(from_ical)
def __str__(self):
return self.ical()
class vBoolean(int):
"""
Returns specific string according to state
>>> bin = vBoolean(True)
>>> bin.ical()
'TRUE'
>>> bin = vBoolean(0)
>>> bin.ical()
'FALSE'
The roundtrip test
>>> x = True
>>> x == vBoolean.from_ical(vBoolean(x).ical())
True
>>> vBoolean.from_ical('true')
True
"""
def __init__(self, *args, **kwargs):
int.__init__(self, *args, **kwargs)
self.params = Parameters()
def ical(self):
if self:
return 'TRUE'
return 'FALSE'
bool_map = CaselessDict(true=True, false=False)
def from_ical(ical):
"Parses the data format from ical text format"
try:
return vBoolean.bool_map[ical]
except:
raise ValueError, "Expected 'TRUE' or 'FALSE'. Got %s" % ical
from_ical = staticmethod(from_ical)
def __str__(self):
return self.ical()
class vCalAddress(str):
"""
This just returns an unquoted string
>>> a = vCalAddress('MAILTO:maxm@mxm.dk')
>>> a.params['cn'] = 'Max M'
>>> a.ical()
'MAILTO:maxm@mxm.dk'
>>> str(a)
'MAILTO:maxm@mxm.dk'
>>> a.params
Parameters({'CN': 'Max M'})
>>> vCalAddress.from_ical('MAILTO:maxm@mxm.dk')
'MAILTO:maxm@mxm.dk'
"""
def __init__(self, *args, **kwargs):
str.__init__(self, *args, **kwargs)
self.params = Parameters()
def __repr__(self):
return u"vCalAddress(%s)" % str.__repr__(self)
def ical(self):
return str(self)
def from_ical(ical):
"Parses the data format from ical text format"
try:
return str(ical)
except:
raise ValueError, 'Expected vCalAddress, got: %s' % ical
from_ical = staticmethod(from_ical)
def __str__(self):
return str.__str__(self)
####################################################
# handy tzinfo classes you can use.
ZERO = timedelta(0)
HOUR = timedelta(hours=1)
STDOFFSET = timedelta(seconds = -_time.timezone)
if _time.daylight:
DSTOFFSET = timedelta(seconds = -_time.altzone)
else:
DSTOFFSET = STDOFFSET
DSTDIFF = DSTOFFSET - STDOFFSET
class FixedOffset(tzinfo):
"""Fixed offset in minutes east from UTC."""
def __init__(self, offset, name):
self.__offset = timedelta(minutes = offset)
self.__name = name
def utcoffset(self, dt):
return self.__offset
def tzname(self, dt):
return self.__name
def dst(self, dt):
return ZERO
class Utc(tzinfo):
"""UTC tzinfo subclass"""
def utcoffset(self, dt):
return ZERO
def tzname(self, dt):
return "UTC"
def dst(self, dt):
return ZERO
UTC = Utc()
class LocalTimezone(tzinfo):
"""
Timezone of the machine where the code is running
"""
def utcoffset(self, dt):
if self._isdst(dt):
return DSTOFFSET
else:
return STDOFFSET
def dst(self, dt):
if self._isdst(dt):
return DSTDIFF
else:
return ZERO
def tzname(self, dt):
return _time.tzname[self._isdst(dt)]
def _isdst(self, dt):
tt = (dt.year, dt.month, dt.day,
dt.hour, dt.minute, dt.second,
dt.weekday(), 0, -1)
stamp = _time.mktime(tt)
tt = _time.localtime(stamp)
return tt.tm_isdst > 0
####################################################
class vDatetime:
"""
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.
>>> d = datetime(2001, 1,1, 12, 30, 0)
>>> dt = vDatetime(d)
>>> dt.ical()
'20010101T123000'
>>> vDatetime.from_ical('20000101T120000')
datetime.datetime(2000, 1, 1, 12, 0)
>>> dutc = datetime(2001, 1,1, 12, 30, 0, tzinfo=UTC)
>>> vDatetime(dutc).ical()
'20010101T123000Z'
>>> vDatetime.from_ical('20010101T000000')
datetime.datetime(2001, 1, 1, 0, 0)
>>> vDatetime.from_ical('20010101T000000A')
Traceback (most recent call last):
...
ValueError: Wrong datetime format: 20010101T000000A
>>> utc = vDatetime.from_ical('20010101T000000Z')
>>> vDatetime(utc).ical()
'20010101T000000Z'
"""
def __init__(self, dt):
self.dt = dt
self.params = Parameters()
def ical(self):
if self.dt.tzinfo:
offset = self.dt.tzinfo.utcoffset(datetime.now())
utc_time = self.dt - self.dt.tzinfo.utcoffset(datetime.now())
return utc_time.strftime("%Y%m%dT%H%M%SZ")
return self.dt.strftime("%Y%m%dT%H%M%S")
def from_ical(ical):
"Parses the data format from ical text format"
try:
timetuple = map(int, ((
ical[:4], # year
ical[4:6], # month
ical[6:8], # day
ical[9:11], # hour
ical[11:13], # minute
ical[13:15], # second
)))
if not ical[15:]:
return datetime(*timetuple)
elif ical[15:16] == 'Z':
timetuple += [0, UTC]
return datetime(*timetuple)
else:
raise ValueError, ical
except:
raise ValueError, 'Wrong datetime format: %s' % ical
from_ical = staticmethod(from_ical)
def __str__(self):
return self.ical()
class vDate:
"""
Render and generates iCalendar date format.
>>> d = date(2001, 1,1)
>>> vDate(d).ical()
'20010101'
>>> vDate.from_ical('20010102')
datetime.date(2001, 1, 2)
>>> vDate('d').ical()
Traceback (most recent call last):
...
ValueError: Value MUST be a date instance
"""
def __init__(self, dt):
if not isinstance(dt, date):
raise ValueError('Value MUST be a date instance')
self.dt = dt
self.params = Parameters()
def ical(self):
return self.dt.strftime("%Y%m%d")
def from_ical(ical):
"Parses the data format from ical text format"
try:
timetuple = map(int, ((
ical[:4], # year
ical[4:6], # month
ical[6:8], # day
)))
return date(*timetuple)
except:
raise ValueError, 'Wrong date format %s' % ical
from_ical = staticmethod(from_ical)
def __str__(self):
return self.ical()
class vDuration:
"""
Subclass of timedelta that renders itself in the iCalendar DURATION format.
>>> vDuration(timedelta(11)).ical()
'P11D'
>>> vDuration(timedelta(-14)).ical()
'-P14D'
>>> vDuration(timedelta(1, 7384)).ical()
'P1DT2H3M4S'
>>> vDuration(timedelta(1, 7380)).ical()
'P1DT2H3M'
>>> vDuration(timedelta(1, 7200)).ical()
'P1DT2H'
>>> vDuration(timedelta(0, 7200)).ical()
'PT2H'
>>> vDuration(timedelta(0, 7384)).ical()
'PT2H3M4S'
>>> vDuration(timedelta(0, 184)).ical()
'PT3M4S'
>>> vDuration(timedelta(0, 22)).ical()
'PT22S'
>>> vDuration(timedelta(0, 3622)).ical()
'PT1H0M22S'
>>> vDuration(timedelta(days=1, hours=5)).ical()
'P1DT5H'
>>> vDuration(timedelta(hours=-5)).ical()
'-PT5H'
>>> vDuration(timedelta(days=-1, hours=-5)).ical()
'-P1DT5H'
How does the parsing work?
>>> vDuration.from_ical('PT1H0M22S')
datetime.timedelta(0, 3622)
>>> vDuration.from_ical('kox')
Traceback (most recent call last):
...
ValueError: Invalid iCalendar duration: kox
>>> vDuration.from_ical('-P14D')
datetime.timedelta(-14)
>>> vDuration(11)
Traceback (most recent call last):
...
ValueError: Value MUST be a timedelta instance
"""
def __init__(self, td):
if not isinstance(td, timedelta):
raise ValueError('Value MUST be a timedelta instance')
self.td = td
self.params = Parameters()
def ical(self):
sign = ""
if self.td.days < 0:
sign = "-"
self.td = -self.td
timepart = ""
if self.td.seconds:
timepart = "T"
hours = self.td.seconds // 3600
minutes = self.td.seconds % 3600 // 60
seconds = self.td.seconds % 60
if hours:
timepart += "%dH" % hours
if minutes or (hours and seconds):
timepart += "%dM" % minutes
if seconds:
timepart += "%dS" % seconds
if self.td.days == 0 and timepart:
return "%sP%s" % (sign, timepart)
else:
return "%sP%dD%s" % (sign, abs(self.td.days), timepart)
def from_ical(ical):
"""
Parses the data format from ical text format.
"""
try:
match = DURATION_REGEX.match(ical)
sign, weeks, days, hours, minutes, seconds = match.groups()
if weeks:
value = timedelta(weeks=int(weeks))
else:
value = timedelta(days=int(days or 0),
hours=int(hours or 0),
minutes=int(minutes or 0),
seconds=int(seconds or 0))
if sign == '-':
value = -value
return value
except:
raise ValueError('Invalid iCalendar duration: %s' % ical)
from_ical = staticmethod(from_ical)
def __str__(self):
return self.ical()
class vFloat(float):
"""
Just a float.
>>> f = vFloat(1.0)
>>> f.ical()
'1.0'
>>> vFloat.from_ical('42')
42.0
>>> vFloat(42).ical()
'42.0'
"""
def __init__(self, *args, **kwargs):
float.__init__(self, *args, **kwargs)
self.params = Parameters()
def ical(self):
return str(self)
def from_ical(ical):
"Parses the data format from ical text format"
try:
return float(ical)
except:
raise ValueError, 'Expected float value, got: %s' % ical
from_ical = staticmethod(from_ical)
class vInt(int):
"""
Just an int.
>>> f = vInt(42)
>>> f.ical()
'42'
>>> vInt.from_ical('13')
13
>>> vInt.from_ical('1s3')
Traceback (most recent call last):
...
ValueError: Expected int, got: 1s3
"""
def __init__(self, *args, **kwargs):
int.__init__(self, *args, **kwargs)
self.params = Parameters()
def ical(self):
return str(self)
def from_ical(ical):
"Parses the data format from ical text format"
try:
return int(ical)
except:
raise ValueError, 'Expected int, got: %s' % ical
from_ical = staticmethod(from_ical)
class vDDDTypes:
"""
A combined Datetime, Date or Duration parser/generator. Their format cannot
be confused, and often values can be of either types. So this is practical.
>>> d = vDDDTypes.from_ical('20010101T123000')
>>> type(d)
<type 'datetime.datetime'>
>>> repr(vDDDTypes.from_ical('20010101T123000Z'))[:65]
'datetime.datetime(2001, 1, 1, 12, 30, tzinfo=<icalendar.prop.Utc '
>>> d = vDDDTypes.from_ical('20010101')
>>> type(d)
<type 'datetime.date'>
>>> vDDDTypes.from_ical('P31D')
datetime.timedelta(31)
>>> vDDDTypes.from_ical('-P31D')
datetime.timedelta(-31)
Bad input
>>> vDDDTypes(42)
Traceback (most recent call last):
...
ValueError: You must use datetime, date or timedelta
"""
def __init__(self, dt):
"Returns vDate from"
wrong_type_used = 1
for typ in (datetime, date, timedelta):
if isinstance(dt, typ):
wrong_type_used = 0
if wrong_type_used:
raise ValueError ('You must use datetime, date or timedelta')
self.dt = dt
def ical(self):
dt = self.dt
if isinstance(dt, datetime):
return vDatetime(dt).ical()
elif isinstance(dt, date):
return vDate(dt).ical()
elif isinstance(dt, timedelta):
return vDuration(dt).ical()
else:
raise ValueEror ('Unknown date type')
def from_ical(ical):
"Parses the data format from ical text format"
u = ical.upper()
if u.startswith('-P') or u.startswith('P'):
return vDuration.from_ical(ical)
try:
return vDatetime.from_ical(ical)
except:
return vDate.from_ical(ical)
from_ical = staticmethod(from_ical)
def __str__(self):
return self.ical()
class vDDDLists:
"""
A list of vDDDTypes values.
>>> dt_list = vDDDLists.from_ical('19960402T010000Z')
>>> type(dt_list)
<type 'list'>
>>> len(dt_list)
1
>>> type(dt_list[0])
<type 'datetime.datetime'>
>>> str(dt_list[0])
'1996-04-02 01:00:00+00:00'
>>> dt_list = vDDDLists.from_ical('19960402T010000Z,19960403T010000Z,19960404T010000Z')
>>> len(dt_list)
3
>>> str(dt_list[0])
'1996-04-02 01:00:00+00:00'
>>> str(dt_list[2])
'1996-04-04 01:00:00+00:00'
>>> dt_list = vDDDLists('19960402T010000Z')
Traceback (most recent call last):
...
ValueError: Value MUST be a list (of date instances)
>>> dt_list = vDDDLists([])
>>> str(dt_list)
''
>>> dt_list = vDDDLists([datetime(2000,1,1)])
>>> str(dt_list)
'20000101T000000'
>>> dt_list = vDDDLists([datetime(2000,1,1), datetime(2000,11,11)])
>>> str(dt_list)
'20000101T000000,20001111T000000'
"""
def __init__(self, dt_list):
if not isinstance(dt_list, list):
raise ValueError('Value MUST be a list (of date instances)')
vDDD = []
for dt in dt_list:
vDDD.append(vDDDTypes(dt))
self.dts = vDDD
def ical(self):
'''
Generates the text string in the iCalendar format.
'''
dts_ical = [dt.ical() for dt in self.dts]
return ",".join(dts_ical)
def from_ical(ical):
'''
Parses the list of data formats from ical text format.
@param ical: ical text format
'''
out = []
ical_dates = ical.split(",")
for ical_dt in ical_dates:
out.append(vDDDTypes.from_ical(ical_dt))
return out
from_ical = staticmethod(from_ical)
def __str__(self):
return self.ical()
class vPeriod:
"""
A precise period of time.
One day in exact datetimes
>>> per = (datetime(2000,1,1), datetime(2000,1,2))
>>> p = vPeriod(per)
>>> p.ical()
'20000101T000000/20000102T000000'
>>> per = (datetime(2000,1,1), timedelta(days=31))
>>> p = vPeriod(per)
>>> p.ical()
'20000101T000000/P31D'
Roundtrip
>>> p = vPeriod.from_ical('20000101T000000/20000102T000000')
>>> p
(datetime.datetime(2000, 1, 1, 0, 0), datetime.datetime(2000, 1, 2, 0, 0))
>>> vPeriod(p).ical()
'20000101T000000/20000102T000000'
>>> vPeriod.from_ical('20000101T000000/P31D')
(datetime.datetime(2000, 1, 1, 0, 0), datetime.timedelta(31))
Roundtrip with absolute time
>>> p = vPeriod.from_ical('20000101T000000Z/20000102T000000Z')
>>> vPeriod(p).ical()
'20000101T000000Z/20000102T000000Z'
And an error
>>> vPeriod.from_ical('20000101T000000/Psd31D')
Traceback (most recent call last):
...
ValueError: Expected period format, got: 20000101T000000/Psd31D
Utc datetime
>>> da_tz = FixedOffset(+1.0, 'da_DK')
>>> start = datetime(2000,1,1, tzinfo=da_tz)
>>> end = datetime(2000,1,2, tzinfo=da_tz)
>>> per = (start, end)
>>> vPeriod(per).ical()
'19991231T235900Z/20000101T235900Z'
>>> p = vPeriod((datetime(2000,1,1, tzinfo=da_tz), timedelta(days=31)))
>>> p.ical()
'19991231T235900Z/P31D'
"""
def __init__(self, per):
start, end_or_duration = per
if not (isinstance(start, datetime) or isinstance(start, date)):
raise ValueError('Start value MUST be a datetime or date instance')
if not (isinstance(end_or_duration, datetime) or
isinstance(end_or_duration, date) or
isinstance(end_or_duration, timedelta)):
raise ValueError('end_or_duration MUST be a datetime, date or timedelta instance')
self.start = start
self.end_or_duration = end_or_duration
self.by_duration = 0
if isinstance(end_or_duration, timedelta):
self.by_duration = 1
self.duration = end_or_duration
self.end = self.start + self.duration
else:
self.end = end_or_duration
self.duration = self.end - self.start
if self.start > self.end:
raise ValueError("Start time is greater than end time")
self.params = Parameters()
def __cmp__(self, other):
if not isinstance(other, vPeriod):
raise NotImplementedError(
'Cannot compare vPeriod with %s' % repr(other))
return cmp((self.start, self.end), (other.start, other.end))
def overlaps(self, other):
if self.start > other.start:
return other.overlaps(self)
if self.start <= other.start < self.end:
return True
return False
def ical(self):
if self.by_duration:
return '%s/%s' % (vDatetime(self.start).ical(), vDuration(self.duration).ical())
return '%s/%s' % (vDatetime(self.start).ical(), vDatetime(self.end).ical())
def from_ical(ical):
"Parses the data format from ical text format"
try:
start, end_or_duration = ical.split('/')
start = vDDDTypes.from_ical(start)
end_or_duration = vDDDTypes.from_ical(end_or_duration)
return (start, end_or_duration)
except:
raise ValueError, 'Expected period format, got: %s' % ical
from_ical = staticmethod(from_ical)
def __str__(self):
return self.ical()
def __repr__(self):
if self.by_duration:
p = (self.start, self.duration)
else:
p = (self.start, self.end)
return 'vPeriod(%s)' % repr(p)
class vWeekday(str):
"""
This returns an unquoted weekday abbrevation
>>> a = vWeekday('mo')
>>> a.ical()
'MO'
>>> a = vWeekday('erwer')
Traceback (most recent call last):
...
ValueError: Expected weekday abbrevation, got: ERWER
>>> vWeekday.from_ical('mo')
'MO'
>>> vWeekday.from_ical('+3mo')
'+3MO'
>>> vWeekday.from_ical('Saturday')
Traceback (most recent call last):
...
ValueError: Expected weekday abbrevation, got: Saturday
>>> a = vWeekday('+mo')
>>> a.ical()
'+MO'
>>> a = vWeekday('+3mo')
>>> a.ical()
'+3MO'
>>> a = vWeekday('-tu')
>>> a.ical()
'-TU'
"""
week_days = CaselessDict({"SU":0, "MO":1, "TU":2, "WE":3,
"TH":4, "FR":5, "SA":6})
def __init__(self, *args, **kwargs):
str.__init__(self, *args, **kwargs)
match = WEEKDAY_RULE.match(self)
if match is None:
raise ValueError, 'Expected weekday abbrevation, got: %s' % self
match = match.groupdict()
sign = match['signal']
weekday = match['weekday']
relative = match['relative']
if not weekday in vWeekday.week_days or sign not in '+-':
raise ValueError, 'Expected weekday abbrevation, got: %s' % self
self.relative = relative and int(relative) or None
self.params = Parameters()
def ical(self):
return self.upper()
def from_ical(ical):
"Parses the data format from ical text format"
try:
return vWeekday(ical.upper())
except:
raise ValueError, 'Expected weekday abbrevation, got: %s' % ical
from_ical = staticmethod(from_ical)
def __str__(self):
return self.ical()
class vFrequency(str):
"""
A simple class that catches illegal values.
>>> f = vFrequency('bad test')
Traceback (most recent call last):
...
ValueError: Expected frequency, got: BAD TEST
>>> vFrequency('daily').ical()
'DAILY'
>>> vFrequency('daily').from_ical('MONTHLY')
'MONTHLY'
"""
frequencies = CaselessDict({
"SECONDLY":"SECONDLY",
"MINUTELY":"MINUTELY",
"HOURLY":"HOURLY",
"DAILY":"DAILY",
"WEEKLY":"WEEKLY",
"MONTHLY":"MONTHLY",
"YEARLY":"YEARLY",
})
def __init__(self, *args, **kwargs):
str.__init__(self, *args, **kwargs)
if not self in vFrequency.frequencies:
raise ValueError, 'Expected frequency, got: %s' % self
self.params = Parameters()
def ical(self):
return self.upper()
def from_ical(ical):
"Parses the data format from ical text format"
try:
return vFrequency(ical.upper())
except:
raise ValueError, 'Expected weekday abbrevation, got: %s' % ical
from_ical = staticmethod(from_ical)
def __str__(self):
return self.ical()
class vRecur(CaselessDict):
"""
Let's see how close we can get to one from the rfc:
FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30
>>> r = dict(freq='yearly', interval=2)
>>> r['bymonth'] = 1
>>> r['byday'] = 'su'
>>> r['byhour'] = [8,9]
>>> r['byminute'] = 30
>>> r = vRecur(r)
>>> r.ical()
'BYHOUR=8,9;BYDAY=SU;BYMINUTE=30;BYMONTH=1;FREQ=YEARLY;INTERVAL=2'
>>> r = vRecur(FREQ='yearly', INTERVAL=2)
>>> r['BYMONTH'] = 1
>>> r['BYDAY'] = 'su'
>>> r['BYHOUR'] = [8,9]
>>> r['BYMINUTE'] = 30
>>> r.ical()
'BYDAY=SU;BYMINUTE=30;BYMONTH=1;INTERVAL=2;FREQ=YEARLY;BYHOUR=8,9'
>>> r = vRecur(freq='DAILY', count=10)
>>> r['bysecond'] = [0, 15, 30, 45]
>>> r.ical()
'COUNT=10;FREQ=DAILY;BYSECOND=0,15,30,45'
>>> r = vRecur(freq='DAILY', until=datetime(2005,1,1,12,0,0))
>>> r.ical()
'FREQ=DAILY;UNTIL=20050101T120000'
How do we fare with regards to parsing?
>>> r = vRecur.from_ical('FREQ=DAILY;INTERVAL=2;COUNT=10')
>>> r
{'COUNT': [10], 'FREQ': ['DAILY'], 'INTERVAL': [2]}
>>> vRecur(r).ical()
'COUNT=10;FREQ=DAILY;INTERVAL=2'
>>> r = vRecur.from_ical('FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=-SU;BYHOUR=8,9;BYMINUTE=30')
>>> r
{'BYHOUR': [8, 9], 'BYDAY': ['-SU'], 'BYMINUTE': [30], 'BYMONTH': [1], 'FREQ': ['YEARLY'], 'INTERVAL': [2]}
>>> vRecur(r).ical()
'BYDAY=-SU;BYMINUTE=30;INTERVAL=2;BYMONTH=1;FREQ=YEARLY;BYHOUR=8,9'
Some examples from the spec
>>> r = vRecur.from_ical('FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1')
>>> vRecur(r).ical()
'BYSETPOS=-1;FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR'
>>> r = vRecur.from_ical('FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30')
>>> vRecur(r).ical()
'BYDAY=SU;BYMINUTE=30;INTERVAL=2;BYMONTH=1;FREQ=YEARLY;BYHOUR=8,9'
and some errors
>>> r = vRecur.from_ical('BYDAY=12')
Traceback (most recent call last):
...
ValueError: Error in recurrence rule: BYDAY=12
"""
frequencies = ["SECONDLY", "MINUTELY", "HOURLY", "DAILY", "WEEKLY",
"MONTHLY", "YEARLY"]
types = CaselessDict({
'COUNT':vInt,
'INTERVAL':vInt,
'BYSECOND':vInt,
'BYMINUTE':vInt,
'BYHOUR':vInt,
'BYMONTHDAY':vInt,
'BYYEARDAY':vInt,
'BYMONTH':vInt,
'UNTIL':vDDDTypes,
'BYSETPOS':vInt,
'WKST':vWeekday,
'BYDAY':vWeekday,
'FREQ':vFrequency
})
def __init__(self, *args, **kwargs):
CaselessDict.__init__(self, *args, **kwargs)
self.params = Parameters()
def ical(self):
# SequenceTypes
result = []
for key, vals in self.items():
typ = self.types[key]
if not type(vals) in SequenceTypes:
vals = [vals]
vals = ','.join([typ(val).ical() for val in vals])
result.append('%s=%s' % (key, vals))
return ';'.join(result)
def parse_type(key, values):
# integers
parser = vRecur.types.get(key, vText)
return [parser.from_ical(v) for v in values.split(',')]
parse_type = staticmethod(parse_type)
def from_ical(ical):
"Parses the data format from ical text format"
try:
recur = vRecur()
for pairs in ical.split(';'):
key, vals = pairs.split('=')
recur[key] = vRecur.parse_type(key, vals)
return dict(recur)
except:
raise ValueError, 'Error in recurrence rule: %s' % ical
from_ical = staticmethod(from_ical)
def __str__(self):
return self.ical()
class vText(unicode):
"""
Simple text
>>> t = vText(u'Simple text')
>>> t.ical()
'Simple text'
Escaped text
>>> t = vText('Text ; with escaped, chars')
>>> t.ical()
'Text \\\\; with escaped\\\\, chars'
Escaped newlines
>>> vText('Text with escaped\N chars').ical()
'Text with escaped\\\\n chars'
If you pass a unicode object, it will be utf-8 encoded. As this is the
(only) standard that RFC 2445 support.
>>> t = vText(u'international chars <20><><EFBFBD> <20><><EFBFBD> <20>')
>>> t.ical()
'international chars \\xc3\\xa6\\xc3\\xb8\\xc3\\xa5 \\xc3\\x86\\xc3\\x98\\xc3\\x85 \\xc3\\xbc'
Unicode is converted to utf-8
>>> t = vText(u'international <20> <20> <20>')
>>> str(t)
'international \\xc3\\xa6 \\xc3\\xb8 \\xc3\\xa5'
and parsing?
>>> vText.from_ical('Text \\; with escaped\\, chars')
u'Text ; with escaped, chars'
>>> print vText.from_ical('A string with\\; some\\\\ characters in\\Nit')
A string with; some\\ characters in
it
"""
encoding = 'utf-8'
def __init__(self, *args, **kwargs):
unicode.__init__(self, *args, **kwargs)
self.params = Parameters()
def escape(self):
"""
Format value according to iCalendar TEXT escaping rules.
"""
return (self.replace('\N', '\n')
.replace('\\', '\\\\')
.replace(';', r'\;')
.replace(',', r'\,')
.replace('\r\n', r'\n')
.replace('\n', r'\n')
)
def __repr__(self):
return u"vText(%s)" % unicode.__repr__(self)
def ical(self):
return self.escape().encode(self.encoding)
def from_ical(ical):
"Parses the data format from ical text format"
try:
ical = (ical.replace(r'\N', r'\n')
.replace(r'\r\n', '\n')
.replace(r'\n', '\n')
.replace(r'\,', ',')
.replace(r'\;', ';')
.replace('\\\\', '\\'))
return ical.decode(vText.encoding)
except:
raise ValueError, 'Expected ical text, got: %s' % ical
from_ical = staticmethod(from_ical)
def __str__(self):
return self.ical()
class vTime(time):
"""
A subclass of datetime, that renders itself in the iCalendar time
format.
>>> dt = vTime(12, 30, 0)
>>> dt.ical()
'123000'
>>> vTime.from_ical('123000')
datetime.time(12, 30)
We should also fail, right?
>>> vTime.from_ical('263000')
Traceback (most recent call last):
...
ValueError: Expected time, got: 263000
"""
def __init__(self, *args, **kwargs):
time.__init__(self, *args, **kwargs)
self.params = Parameters()
def ical(self):
return self.strftime("%H%M%S")
def from_ical(ical):
"Parses the data format from ical text format"
try:
timetuple = map(int, (ical[:2],ical[2:4],ical[4:6]))
return time(*timetuple)
except:
raise ValueError, 'Expected time, got: %s' % ical
from_ical = staticmethod(from_ical)
def __str__(self):
return self.ical()
class vUri(str):
"""
Uniform resource identifier is basically just an unquoted string.
>>> u = vUri('http://www.example.com/')
>>> u.ical()
'http://www.example.com/'
>>> vUri.from_ical('http://www.example.com/') # doh!
'http://www.example.com/'
"""
def __init__(self, *args, **kwargs):
str.__init__(self, *args, **kwargs)
self.params = Parameters()
def ical(self):
return str(self)
def from_ical(ical):
"Parses the data format from ical text format"
try:
return str(ical)
except:
raise ValueError, 'Expected , got: %s' % ical
from_ical = staticmethod(from_ical)
def __str__(self):
return str.__str__(self)
class vGeo:
"""
A special type that is only indirectly defined in the rfc.
>>> g = vGeo((1.2, 3.0))
>>> g.ical()
'1.2;3.0'
>>> g = vGeo.from_ical('37.386013;-122.082932')
>>> g
(37.386012999999998, -122.082932)
>>> vGeo(g).ical()
'37.386013;-122.082932'
>>> vGeo('g').ical()
Traceback (most recent call last):
...
ValueError: Input must be (float, float) for latitude and longitude
"""
def __init__(self, geo):
try:
latitude, longitude = geo
latitude = float(latitude)
longitude = float(longitude)
except:
raise ValueError('Input must be (float, float) for latitude and longitude')
self.latitude = latitude
self.longitude = longitude
self.params = Parameters()
def ical(self):
return '%s;%s' % (self.latitude, self.longitude)
def from_ical(ical):
"Parses the data format from ical text format"
try:
latitude, longitude = ical.split(';')
return (float(latitude), float(longitude))
except:
raise ValueError, "Expected 'float;float' , got: %s" % ical
from_ical = staticmethod(from_ical)
def __str__(self):
return self.ical()
class vUTCOffset:
"""
Renders itself as a utc offset
>>> u = vUTCOffset(timedelta(hours=2))
>>> u.ical()
'+0200'
>>> u = vUTCOffset(timedelta(hours=-5))
>>> u.ical()
'-0500'
>>> u = vUTCOffset(timedelta())
>>> u.ical()
'0000'
>>> u = vUTCOffset(timedelta(minutes=-30))
>>> u.ical()
'-0030'
>>> u = vUTCOffset(timedelta(hours=2, minutes=-30))
>>> u.ical()
'+0130'
>>> u = vUTCOffset(timedelta(hours=1, minutes=30))
>>> u.ical()
'+0130'
Parsing
>>> vUTCOffset.from_ical('0000')
datetime.timedelta(0)
>>> vUTCOffset.from_ical('-0030')
datetime.timedelta(-1, 84600)
>>> vUTCOffset.from_ical('+0200')
datetime.timedelta(0, 7200)
>>> o = vUTCOffset.from_ical('+0230')
>>> vUTCOffset(o).ical()
'+0230'
And a few failures
>>> vUTCOffset.from_ical('+323k')
Traceback (most recent call last):
...
ValueError: Expected utc offset, got: +323k
>>> vUTCOffset.from_ical('+2400')
Traceback (most recent call last):
...
ValueError: Offset must be less than 24 hours, was +2400
"""
def __init__(self, td):
if not isinstance(td, timedelta):
raise ValueError('Offset value MUST be a timedelta instance')
self.td = td
self.params = Parameters()
def ical(self):
td = self.td
day_in_minutes = (td.days * 24 * 60)
seconds_in_minutes = td.seconds // 60
total_minutes = day_in_minutes + seconds_in_minutes
if total_minutes == 0:
sign = '%s'
elif total_minutes < 0:
sign = '-%s'
else:
sign = '+%s'
hours = abs(total_minutes) // 60
minutes = total_minutes % 60
duration = '%02i%02i' % (hours, minutes)
return sign % duration
def from_ical(ical):
"Parses the data format from ical text format"
try:
sign, hours, minutes = (ical[-5:-4], int(ical[-4:-2]), int(ical[-2:]))
offset = timedelta(hours=hours, minutes=minutes)
except:
raise ValueError, 'Expected utc offset, got: %s' % ical
if offset >= timedelta(hours=24):
raise ValueError, 'Offset must be less than 24 hours, was %s' % ical
if sign == '-':
return -offset
return offset
from_ical = staticmethod(from_ical)
def __str__(self):
return self.ical()
class vInline(str):
"""
This is an especially dumb class that just holds raw unparsed text and has
parameters. Conversion of inline values are handled by the Component class,
so no further processing is needed.
>>> vInline('Some text')
'Some text'
>>> vInline.from_ical('Some text')
'Some text'
>>> t2 = vInline('other text')
>>> t2.params['cn'] = 'Test Osterone'
>>> t2.params
Parameters({'CN': 'Test Osterone'})
"""
def __init__(self,obj):
self.obj = obj
self.params = Parameters()
def ical(self):
return str(self)
def from_ical(ical):
return str(ical)
from_ical = staticmethod(from_ical)
def __str__(self):
return str(self.obj)
class TypesFactory(CaselessDict):
"""
All Value types defined in rfc 2445 are registered in this factory class. To
get a type you can use it like this.
>>> factory = TypesFactory()
>>> datetime_parser = factory['date-time']
>>> dt = datetime_parser(datetime(2001, 1, 1))
>>> dt.ical()
'20010101T000000'
A typical use is when the parser tries to find a content type and use text
as the default
>>> value = '20050101T123000'
>>> value_type = 'date-time'
>>> typ = factory.get(value_type, 'text')
>>> typ.from_ical(value)
datetime.datetime(2005, 1, 1, 12, 30)
It can also be used to directly encode property and parameter values
>>> comment = factory.ical('comment', u'by Rasmussen, Max M<>ller')
>>> str(comment)
'by Rasmussen\\\\, Max M\\xc3\\xb8ller'
>>> factory.ical('priority', 1)
'1'
>>> factory.ical('cn', u'Rasmussen, Max M<>ller')
'Rasmussen\\\\, Max M\\xc3\\xb8ller'
>>> factory.from_ical('cn', 'Rasmussen\\\\, Max M\\xc3\\xb8ller')
u'Rasmussen, Max M\\xf8ller'
The value and parameter names don't overlap. So one factory is enough for
both kinds.
"""
def __init__(self, *args, **kwargs):
"Set keys to upper for initial dict"
CaselessDict.__init__(self, *args, **kwargs)
self['binary'] = vBinary
self['boolean'] = vBoolean
self['cal-address'] = vCalAddress
self['date'] = vDDDTypes
self['date-time'] = vDDDTypes
self['duration'] = vDDDTypes
self['float'] = vFloat
self['integer'] = vInt
self['period'] = vPeriod
self['recur'] = vRecur
self['text'] = vText
self['time'] = vTime
self['uri'] = vUri
self['utc-offset'] = vUTCOffset
self['geo'] = vGeo
self['inline'] = vInline
self['date-time-list'] = vDDDLists
#################################################
# Property types
# These are the default types
types_map = CaselessDict({
####################################
# Property valye types
# Calendar Properties
'calscale' : 'text',
'method' : 'text',
'prodid' : 'text',
'version' : 'text',
# Descriptive Component Properties
'attach' : 'uri',
'categories' : 'text',
'class' : 'text',
'comment' : 'text',
'description' : 'text',
'geo' : 'geo',
'location' : 'text',
'percent-complete' : 'integer',
'priority' : 'integer',
'resources' : 'text',
'status' : 'text',
'summary' : 'text',
# Date and Time Component Properties
'completed' : 'date-time',
'dtend' : 'date-time',
'due' : 'date-time',
'dtstart' : 'date-time',
'duration' : 'duration',
'freebusy' : 'period',
'transp' : 'text',
# Time Zone Component Properties
'tzid' : 'text',
'tzname' : 'text',
'tzoffsetfrom' : 'utc-offset',
'tzoffsetto' : 'utc-offset',
'tzurl' : 'uri',
# Relationship Component Properties
'attendee' : 'cal-address',
'contact' : 'text',
'organizer' : 'cal-address',
'recurrence-id' : 'date-time',
'related-to' : 'text',
'url' : 'uri',
'uid' : 'text',
# Recurrence Component Properties
'exdate' : 'date-time-list',
'exrule' : 'recur',
'rdate' : 'date-time-list',
'rrule' : 'recur',
# Alarm Component Properties
'action' : 'text',
'repeat' : 'integer',
'trigger' : 'duration',
# Change Management Component Properties
'created' : 'date-time',
'dtstamp' : 'date-time',
'last-modified' : 'date-time',
'sequence' : 'integer',
# Miscellaneous Component Properties
'request-status' : 'text',
####################################
# parameter types (luckilly there is no name overlap)
'altrep' : 'uri',
'cn' : 'text',
'cutype' : 'text',
'delegated-from' : 'cal-address',
'delegated-to' : 'cal-address',
'dir' : 'uri',
'encoding' : 'text',
'fmttype' : 'text',
'fbtype' : 'text',
'language' : 'text',
'member' : 'cal-address',
'partstat' : 'text',
'range' : 'text',
'related' : 'text',
'reltype' : 'text',
'role' : 'text',
'rsvp' : 'boolean',
'sent-by' : 'cal-address',
'tzid' : 'text',
'value' : 'text',
})
def for_property(self, name):
"Returns a the default type for a property or parameter"
return self[self.types_map.get(name, 'text')]
def ical(self, name, value):
"""
Encodes a named value from a primitive python type to an
icalendar encoded string.
"""
type_class = self.for_property(name)
return type_class(value).ical()
def from_ical(self, name, value):
"""
Decodes a named property or parameter value from an icalendar encoded
string to a primitive python type.
"""
type_class = self.for_property(name)
decoded = type_class.from_ical(str(value))
return decoded