535 lines
17 KiB
Python
535 lines
17 KiB
Python
# -*- 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()
|