Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

working approval process and ical view of approved events

  • Loading branch information...
commit fcf760297b2bfc002a66b5c86aaa16dfc95034a0 1 parent 9b632dd
Jeff Lindsay progrium authored
12 TODO
View
@@ -0,0 +1,12 @@
+get ical of events
+
+expire events after 30 days
+send email notifications
+past events
+allow owner actions: cancel, see owned
+allow admin actions: cancel, hold
+edit event
+get rss of events
+
+get json of event/events
+see previous(all),canceled(staff,owner),expired(admin,owner) events
16 icalendar/__init__.py
View
@@ -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 icalendar/cal.py
View
@@ -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 icalendar/caselessdict.py
View
@@ -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 icalendar/interfaces.py
View
@@ -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 icalendar/parser.py
View
@@ -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 æ ø å'))
+ >>> 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')
1,430 icalendar/prop.py
View
@@ -0,0 +1,1430 @@
+# -*- 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 æ ø å \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'
+
+ 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 = "-"
+ 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 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 æøå ÆØÅ ü')
+ >>> 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 æ ø å')
+ >>> 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'
+