summaryrefslogtreecommitdiff
path: root/vobject
diff options
context:
space:
mode:
authorGuido Guenther <agx@sigxcpu.org>2008-06-20 09:11:42 +0200
committerGuido Guenther <agx@sigxcpu.org>2008-06-20 09:11:42 +0200
commitbd308fbf5f91cd2d9f86f9ea437bcf54a120a09c (patch)
tree6361d7bceda4ac2ef9f4b6de0565874f9da0c1a2 /vobject
parent7a6dcb8d42e0611d4d49130d0cb492d9f58e51d6 (diff)
Imported Upstream version 0.6.6
Diffstat (limited to 'vobject')
-rw-r--r--vobject/__init__.py86
-rw-r--r--vobject/base.py1106
-rw-r--r--vobject/behavior.py164
-rw-r--r--vobject/hcalendar.py125
-rw-r--r--vobject/icalendar.py1892
-rw-r--r--vobject/ics_diff.py219
-rw-r--r--vobject/vcard.py289
-rw-r--r--vobject/win32tz.py156
8 files changed, 4037 insertions, 0 deletions
diff --git a/vobject/__init__.py b/vobject/__init__.py
new file mode 100644
index 0000000..d5daf30
--- /dev/null
+++ b/vobject/__init__.py
@@ -0,0 +1,86 @@
+"""
+VObject Overview
+================
+ vobject parses vCard or vCalendar files, returning a tree of Python objects.
+ It also provids an API to create vCard or vCalendar data structures which
+ can then be serialized.
+
+ Parsing existing streams
+ ------------------------
+ Streams containing one or many L{Component<base.Component>}s can be
+ parsed using L{readComponents<base.readComponents>}. As each Component
+ is parsed, vobject will attempt to give it a L{Behavior<behavior.Behavior>}.
+ If an appropriate Behavior is found, any base64, quoted-printable, or
+ backslash escaped data will automatically be decoded. Dates and datetimes
+ will be transformed to datetime.date or datetime.datetime instances.
+ Components containing recurrence information will have a special rruleset
+ attribute (a dateutil.rrule.rruleset instance).
+
+ Validation
+ ----------
+ L{Behavior<behavior.Behavior>} classes implement validation for
+ L{Component<base.Component>}s. To validate, an object must have all
+ required children. There (TODO: will be) a toggle to raise an exception or
+ just log unrecognized, non-experimental children and parameters.
+
+ Creating objects programatically
+ --------------------------------
+ A L{Component<base.Component>} can be created from scratch. No encoding
+ is necessary, serialization will encode data automatically. Factory
+ functions (TODO: will be) available to create standard objects.
+
+ Serializing objects
+ -------------------
+ Serialization:
+ - Looks for missing required children that can be automatically generated,
+ like a UID or a PRODID, and adds them
+ - Encodes all values that can be automatically encoded
+ - Checks to make sure the object is valid (unless this behavior is
+ explicitly disabled)
+ - Appends the serialized object to a buffer, or fills a new
+ buffer and returns it
+
+ Examples
+ --------
+
+ >>> import datetime
+ >>> import dateutil.rrule as rrule
+ >>> x = iCalendar()
+ >>> x.add('vevent')
+ <VEVENT| []>
+ >>> x
+ <VCALENDAR| [<VEVENT| []>]>
+ >>> v = x.vevent
+ >>> utc = icalendar.utc
+ >>> v.add('dtstart').value = datetime.datetime(2004, 12, 15, 14, tzinfo = utc)
+ >>> v
+ <VEVENT| [<DTSTART{}2004-12-15 14:00:00+00:00>]>
+ >>> x
+ <VCALENDAR| [<VEVENT| [<DTSTART{}2004-12-15 14:00:00+00:00>]>]>
+ >>> newrule = rrule.rruleset()
+ >>> newrule.rrule(rrule.rrule(rrule.WEEKLY, count=2, dtstart=v.dtstart.value))
+ >>> v.rruleset = newrule
+ >>> list(v.rruleset)
+ [datetime.datetime(2004, 12, 15, 14, 0, tzinfo=tzutc()), datetime.datetime(2004, 12, 22, 14, 0, tzinfo=tzutc())]
+ >>> v.add('uid').value = "randomuid@MYHOSTNAME"
+ >>> print x.serialize()
+ BEGIN:VCALENDAR
+ VERSION:2.0
+ PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+ BEGIN:VEVENT
+ UID:randomuid@MYHOSTNAME
+ DTSTART:20041215T140000Z
+ RRULE:FREQ=WEEKLY;COUNT=2
+ END:VEVENT
+ END:VCALENDAR
+
+"""
+
+import base, icalendar, vcard
+from base import readComponents, readOne, newFromBehavior
+
+def iCalendar():
+ return newFromBehavior('vcalendar', '2.0')
+
+def vCard():
+ return newFromBehavior('vcard', '3.0') \ No newline at end of file
diff --git a/vobject/base.py b/vobject/base.py
new file mode 100644
index 0000000..107e71c
--- /dev/null
+++ b/vobject/base.py
@@ -0,0 +1,1106 @@
+"""vobject module for reading vCard and vCalendar files."""
+
+import copy
+import re
+import sys
+import logging
+import StringIO, cStringIO
+import string
+import exceptions
+import codecs
+
+#------------------------------------ Logging ----------------------------------
+logger = logging.getLogger(__name__)
+if not logging.getLogger().handlers:
+ handler = logging.StreamHandler()
+ formatter = logging.Formatter('%(name)s %(levelname)s %(message)s')
+ handler.setFormatter(formatter)
+ logger.addHandler(handler)
+logger.setLevel(logging.ERROR) # Log errors
+DEBUG = False # Don't waste time on debug calls
+#----------------------------------- Constants ---------------------------------
+CR = '\r'
+LF = '\n'
+CRLF = CR + LF
+SPACE = ' '
+TAB = '\t'
+SPACEORTAB = SPACE + TAB
+#-------------------------------- Useful modules -------------------------------
+# use doctest, it kills two birds with one stone and docstrings often become
+# more readable to boot (see parseLine's docstring).
+# use logging, then when debugging we can just set our verbosity.
+# use epydoc syntax for documenting code, please document every class and non-
+# trivial method (see http://epydoc.sourceforge.net/epytext.html
+# and http://epydoc.sourceforge.net/fields.html). Also, please
+# follow http://www.python.org/peps/pep-0257.html for docstrings.
+#-------------------------------------------------------------------------------
+
+#--------------------------------- Main classes --------------------------------
+class VBase(object):
+ """Base class for ContentLine and Component.
+
+ @ivar behavior:
+ The Behavior class associated with this object, which controls
+ validation, transformations, and encoding.
+ @ivar parentBehavior:
+ The object's parent's behavior, or None if no behaviored parent exists.
+ @ivar isNative:
+ Boolean describing whether this component is a Native instance.
+ @ivar group:
+ An optional group prefix, should be used only to indicate sort order in
+ vCards, according to RFC2426
+ """
+ def __init__(self, group=None, *args, **kwds):
+ super(VBase, self).__init__(*args, **kwds)
+ self.group = group
+ self.behavior = None
+ self.parentBehavior = None
+ self.isNative = False
+
+ def copy(self, copyit):
+ self.group = copyit.group
+ self.behavior = copyit.behavior
+ self.parentBehavior = copyit.parentBehavior
+ self.isNative = copyit.isNative
+
+ def validate(self, *args, **kwds):
+ """Call the behavior's validate method, or return True."""
+ if self.behavior:
+ return self.behavior.validate(self, *args, **kwds)
+ else: return True
+
+ def getChildren(self):
+ """Return an iterable containing the contents of the object."""
+ return []
+
+ def clearBehavior(self, cascade=True):
+ """Set behavior to None. Do for all descendants if cascading."""
+ self.behavior=None
+ if cascade: self.transformChildrenFromNative()
+
+ def autoBehavior(self, cascade=False):
+ """Set behavior if name is in self.parentBehavior.knownChildren.
+
+ If cascade is True, unset behavior and parentBehavior for all
+ descendants, then recalculate behavior and parentBehavior.
+
+ """
+ parentBehavior = self.parentBehavior
+ if parentBehavior is not None:
+ knownChildTup = parentBehavior.knownChildren.get(self.name, None)
+ if knownChildTup is not None:
+ behavior = getBehavior(self.name, knownChildTup[2])
+ if behavior is not None:
+ self.setBehavior(behavior, cascade)
+ if isinstance(self, ContentLine) and self.encoded:
+ self.behavior.decode(self)
+ elif isinstance(self, ContentLine):
+ self.behavior = parentBehavior.defaultBehavior
+ if self.encoded and self.behavior:
+ self.behavior.decode(self)
+
+ def setBehavior(self, behavior, cascade=True):
+ """Set behavior. If cascade is True, autoBehavior all descendants."""
+ self.behavior=behavior
+ if cascade:
+ for obj in self.getChildren():
+ obj.parentBehavior=behavior
+ obj.autoBehavior(True)
+
+ def transformToNative(self):
+ """Transform this object into a custom VBase subclass.
+
+ transformToNative should always return a representation of this object.
+ It may do so by modifying self in place then returning self, or by
+ creating a new object.
+
+ """
+ if self.isNative or not self.behavior or not self.behavior.hasNative:
+ return self
+ else:
+ try:
+ return self.behavior.transformToNative(self)
+ except Exception, e:
+ # wrap errors in transformation in a ParseError
+ lineNumber = getattr(self, 'lineNumber', None)
+ if isinstance(e, ParseError):
+ if lineNumber is not None:
+ e.lineNumber = lineNumber
+ raise
+ else:
+ msg = "In transformToNative, unhandled exception: %s: %s"
+ msg = msg % (sys.exc_info()[0], sys.exc_info()[1])
+ new_error = ParseError(msg, lineNumber)
+ raise ParseError, new_error, sys.exc_info()[2]
+
+
+ def transformFromNative(self):
+ """Return self transformed into a ContentLine or Component if needed.
+
+ May have side effects. If it does, transformFromNative and
+ transformToNative MUST have perfectly inverse side effects. Allowing
+ such side effects is convenient for objects whose transformations only
+ change a few attributes.
+
+ Note that it isn't always possible for transformFromNative to be a
+ perfect inverse of transformToNative, in such cases transformFromNative
+ should return a new object, not self after modifications.
+
+ """
+ if self.isNative and self.behavior and self.behavior.hasNative:
+ try:
+ return self.behavior.transformFromNative(self)
+ except Exception, e:
+ # wrap errors in transformation in a NativeError
+ lineNumber = getattr(self, 'lineNumber', None)
+ if isinstance(e, NativeError):
+ if lineNumber is not None:
+ e.lineNumber = lineNumber
+ raise
+ else:
+ msg = "In transformFromNative, unhandled exception: %s: %s"
+ msg = msg % (sys.exc_info()[0], sys.exc_info()[1])
+ new_error = NativeError(msg, lineNumber)
+ raise NativeError, new_error, sys.exc_info()[2]
+ else: return self
+
+ def transformChildrenToNative(self):
+ """Recursively replace children with their native representation."""
+ pass
+
+ def transformChildrenFromNative(self, clearBehavior=True):
+ """Recursively transform native children to vanilla representations."""
+ pass
+
+ def serialize(self, buf=None, lineLength=75, validate=True, behavior=None):
+ """Serialize to buf if it exists, otherwise return a string.
+
+ Use self.behavior.serialize if behavior exists.
+
+ """
+ if not behavior:
+ behavior = self.behavior
+
+ if behavior:
+ if DEBUG: logger.debug("serializing %s with behavior" % self.name)
+ return behavior.serialize(self, buf, lineLength, validate)
+ else:
+ if DEBUG: logger.debug("serializing %s without behavior" % self.name)
+ return defaultSerialize(self, buf, lineLength)
+
+def ascii(s):
+ """Turn s into a printable string. Won't work for 8-bit ASCII."""
+ return unicode(s).encode('ascii', 'replace')
+
+def toVName(name, stripNum = 0, upper = False):
+ """
+ Turn a Python name into an iCalendar style name, optionally uppercase and
+ with characters stripped off.
+ """
+ if upper:
+ name = name.upper()
+ if stripNum != 0:
+ name = name[:-stripNum]
+ return name.replace('_', '-')
+
+class ContentLine(VBase):
+ """Holds one content line for formats like vCard and vCalendar.
+
+ For example::
+ <SUMMARY{u'param1' : [u'val1'], u'param2' : [u'val2']}Bastille Day Party>
+
+ @ivar name:
+ The uppercased name of the contentline.
+ @ivar params:
+ A dictionary of parameters and associated lists of values (the list may
+ be empty for empty parameters).
+ @ivar value:
+ The value of the contentline.
+ @ivar singletonparams:
+ A list of parameters for which it's unclear if the string represents the
+ parameter name or the parameter value. In vCard 2.1, "The value string
+ can be specified alone in those cases where the value is unambiguous".
+ This is crazy, but we have to deal with it.
+ @ivar encoded:
+ A boolean describing whether the data in the content line is encoded.
+ Generally, text read from a serialized vCard or vCalendar should be
+ considered encoded. Data added programmatically should not be encoded.
+ @ivar lineNumber:
+ An optional line number associated with the contentline.
+ """
+ def __init__(self, name, params, value, group=None,
+ encoded=False, isNative=False,
+ lineNumber = None, *args, **kwds):
+ """Take output from parseLine, convert params list to dictionary."""
+ # group is used as a positional argument to match parseLine's return
+ super(ContentLine, self).__init__(group, *args, **kwds)
+ self.name = name.upper()
+ self.value = value
+ self.encoded = encoded
+ self.params = {}
+ self.singletonparams = []
+ self.isNative = isNative
+ self.lineNumber = lineNumber
+ def updateTable(x):
+ if len(x) == 1:
+ self.singletonparams += x
+ else:
+ paramlist = self.params.setdefault(x[0].upper(), [])
+ paramlist.extend(x[1:])
+ map(updateTable, params)
+ qp = False
+ if 'ENCODING' in self.params:
+ if 'QUOTED-PRINTABLE' in self.params['ENCODING']:
+ qp = True
+ self.params['ENCODING'].remove('QUOTED-PRINTABLE')
+ if 0==len(self.params['ENCODING']):
+ del self.params['ENCODING']
+ if 'QUOTED-PRINTABLE' in self.singletonparams:
+ qp = True
+ self.singletonparams.remove('QUOTED-PRINTABLE')
+ if qp:
+ self.value = str(self.value).decode('quoted-printable')
+
+ # self.value should be unicode for iCalendar, but if quoted-printable
+ # is used, or if the quoted-printable state machine is used, text may be
+ # encoded
+ if type(self.value) is str:
+ charset = 'iso-8859-1'
+ if 'CHARSET' in self.params:
+ charsets = self.params.pop('CHARSET')
+ if charsets:
+ charset = charsets[0]
+ self.value = unicode(self.value, charset)
+
+ @classmethod
+ def duplicate(clz, copyit):
+ newcopy = clz('', {}, '')
+ newcopy.copy(copyit)
+ return newcopy
+
+ def copy(self, copyit):
+ super(ContentLine, self).copy(copyit)
+ self.name = copyit.name
+ self.value = copy.copy(copyit.value)
+ self.encoded = self.encoded
+ self.params = copy.copy(copyit.params)
+ self.singletonparams = copy.copy(copyit.singletonparams)
+ self.lineNumber = copyit.lineNumber
+
+ def __eq__(self, other):
+ try:
+ return (self.name == other.name) and (self.params == other.params) and (self.value == other.value)
+ except:
+ return False
+
+ def __getattr__(self, name):
+ """Make params accessible via self.foo_param or self.foo_paramlist.
+
+ Underscores, legal in python variable names, are converted to dashes,
+ which are legal in IANA tokens.
+
+ """
+ try:
+ if name.endswith('_param'):
+ return self.params[toVName(name, 6, True)][0]
+ elif name.endswith('_paramlist'):
+ return self.params[toVName(name, 10, True)]
+ else:
+ raise exceptions.AttributeError, name
+ except KeyError:
+ raise exceptions.AttributeError, name
+
+ def __setattr__(self, name, value):
+ """Make params accessible via self.foo_param or self.foo_paramlist.
+
+ Underscores, legal in python variable names, are converted to dashes,
+ which are legal in IANA tokens.
+
+ """
+ if name.endswith('_param'):
+ if type(value) == list:
+ self.params[toVName(name, 6, True)] = value
+ else:
+ self.params[toVName(name, 6, True)] = [value]
+ elif name.endswith('_paramlist'):
+ if type(value) == list:
+ self.params[toVName(name, 10, True)] = value
+ else:
+ raise VObjectError("Parameter list set to a non-list")
+ else:
+ prop = getattr(self.__class__, name, None)
+ if isinstance(prop, property):
+ prop.fset(self, value)
+ else:
+ object.__setattr__(self, name, value)
+
+ def __delattr__(self, name):
+ try:
+ if name.endswith('_param'):
+ del self.params[toVName(name, 6, True)]
+ elif name.endswith('_paramlist'):
+ del self.params[toVName(name, 10, True)]
+ else:
+ object.__delattr__(self, name)
+ except KeyError:
+ raise exceptions.AttributeError, name
+
+ def valueRepr( self ):
+ """transform the representation of the value according to the behavior,
+ if any"""
+ v = self.value
+ if self.behavior:
+ v = self.behavior.valueRepr( self )
+ return ascii( v )
+
+ def __str__(self):
+ return "<"+ascii(self.name)+ascii(self.params)+self.valueRepr()+">"
+
+ def __repr__(self):
+ return self.__str__().replace('\n', '\\n')
+
+ def prettyPrint(self, level = 0, tabwidth=3):
+ pre = ' ' * level * tabwidth
+ print pre, self.name + ":", self.valueRepr()
+ if self.params:
+ lineKeys= self.params.keys()
+ print pre, "params for ", self.name +':'
+ for aKey in lineKeys:
+ print pre + ' ' * tabwidth, aKey, ascii(self.params[aKey])
+
+class Component(VBase):
+ """A complex property that can contain multiple ContentLines.
+
+ For our purposes, a component must start with a BEGIN:xxxx line and end with
+ END:xxxx, or have a PROFILE:xxx line if a top-level component.
+
+ @ivar contents:
+ A dictionary of lists of Component or ContentLine instances. The keys
+ are the lowercased names of child ContentLines or Components.
+ Note that BEGIN and END ContentLines are not included in contents.
+ @ivar name:
+ Uppercase string used to represent this Component, i.e VCARD if the
+ serialized object starts with BEGIN:VCARD.
+ @ivar useBegin:
+ A boolean flag determining whether BEGIN: and END: lines should
+ be serialized.
+
+ """
+ def __init__(self, name=None, *args, **kwds):
+ super(Component, self).__init__(*args, **kwds)
+ self.contents = {}
+ if name:
+ self.name=name.upper()
+ self.useBegin = True
+ else:
+ self.name = ''
+ self.useBegin = False
+
+ self.autoBehavior()
+
+ @classmethod
+ def duplicate(clz, copyit):
+ newcopy = clz()
+ newcopy.copy(copyit)
+ return newcopy
+
+ def copy(self, copyit):
+ super(Component, self).copy(copyit)
+
+ # deep copy of contents
+ self.contents = {}
+ for key, lvalue in copyit.contents.items():
+ newvalue = []
+ for value in lvalue:
+ newitem = value.duplicate(value)
+ newvalue.append(newitem)
+ self.contents[key] = newvalue
+
+ self.name = copyit.name
+ self.useBegin = copyit.useBegin
+
+ def setProfile(self, name):
+ """Assign a PROFILE to this unnamed component.
+
+ Used by vCard, not by vCalendar.
+
+ """
+ if self.name or self.useBegin:
+ if self.name == name: return
+ raise VObjectError("This component already has a PROFILE or uses BEGIN.")
+ self.name = name.upper()
+
+ def __getattr__(self, name):
+ """For convenience, make self.contents directly accessible.
+
+ Underscores, legal in python variable names, are converted to dashes,
+ which are legal in IANA tokens.
+
+ """
+ try:
+ if name.endswith('_list'):
+ return self.contents[toVName(name, 5)]
+ else:
+ return self.contents[toVName(name)][0]
+ except KeyError:
+ raise exceptions.AttributeError, name
+
+ normal_attributes = ['contents','name','behavior','parentBehavior','group']
+ def __setattr__(self, name, value):
+ """For convenience, make self.contents directly accessible.
+
+ Underscores, legal in python variable names, are converted to dashes,
+ which are legal in IANA tokens.
+
+ """
+ if name not in self.normal_attributes and name.lower()==name:
+ if type(value) == list:
+ if name.endswith('_list'):
+ name = name[:-5]
+ self.contents[toVName(name)] = value
+ elif name.endswith('_list'):
+ raise VObjectError("Component list set to a non-list")
+ else:
+ self.contents[toVName(name)] = [value]
+ else:
+ prop = getattr(self.__class__, name, None)
+ if isinstance(prop, property):
+ prop.fset(self, value)
+ else:
+ object.__setattr__(self, name, value)
+
+ def __delattr__(self, name):
+ try:
+ if name not in self.normal_attributes and name.lower()==name:
+ if name.endswith('_list'):
+ del self.contents[toVName(name, 5)]
+ else:
+ del self.contents[toVName(name)]
+ else:
+ object.__delattr__(self, name)
+ except KeyError:
+ raise exceptions.AttributeError, name
+
+ def getChildValue(self, childName, default = None, childNumber = 0):
+ """Return a child's value (the first, by default), or None."""
+ child = self.contents.get(toVName(childName))
+ if child is None:
+ return default
+ else:
+ return child[childNumber].value
+
+ def add(self, objOrName, group = None):
+ """Add objOrName to contents, set behavior if it can be inferred.
+
+ If objOrName is a string, create an empty component or line based on
+ behavior. If no behavior is found for the object, add a ContentLine.
+
+ group is an optional prefix to the name of the object (see
+ RFC 2425).
+ """
+ if isinstance(objOrName, VBase):
+ obj = objOrName
+ if self.behavior:
+ obj.parentBehavior = self.behavior
+ obj.autoBehavior(True)
+ else:
+ name = objOrName.upper()
+ try:
+ id=self.behavior.knownChildren[name][2]
+ behavior = getBehavior(name, id)
+ if behavior.isComponent:
+ obj = Component(name)
+ else:
+ obj = ContentLine(name, [], '', group)
+ obj.parentBehavior = self.behavior
+ obj.behavior = behavior
+ obj = obj.transformToNative()
+ except (KeyError, AttributeError):
+ obj = ContentLine(objOrName, [], '', group)
+ if obj.behavior is None and self.behavior is not None:
+ if isinstance(obj, ContentLine):
+ obj.behavior = self.behavior.defaultBehavior
+ self.contents.setdefault(obj.name.lower(), []).append(obj)
+ return obj
+
+ def remove(self, obj):
+ """Remove obj from contents."""
+ named = self.contents.get(obj.name.lower())
+ if named:
+ try:
+ named.remove(obj)
+ if len(named) == 0:
+ del self.contents[obj.name.lower()]
+ except ValueError:
+ pass;
+
+ def getChildren(self):
+ """Return an iterable of all children."""
+ for objList in self.contents.values():
+ for obj in objList: yield obj
+
+ def components(self):
+ """Return an iterable of all Component children."""
+ return (i for i in self.getChildren() if isinstance(i, Component))
+
+ def lines(self):
+ """Return an iterable of all ContentLine children."""
+ return (i for i in self.getChildren() if isinstance(i, ContentLine))
+
+ def sortChildKeys(self):
+ try:
+ first = [s for s in self.behavior.sortFirst if s in self.contents]
+ except:
+ first = []
+ return first + sorted(k for k in self.contents.keys() if k not in first)
+
+ def getSortedChildren(self):
+ return [obj for k in self.sortChildKeys() for obj in self.contents[k]]
+
+ def setBehaviorFromVersionLine(self, versionLine):
+ """Set behavior if one matches name, versionLine.value."""
+ v=getBehavior(self.name, versionLine.value)
+ if v: self.setBehavior(v)
+
+ def transformChildrenToNative(self):
+ """Recursively replace children with their native representation."""
+ #sort to get dependency order right, like vtimezone before vevent
+ for childArray in (self.contents[k] for k in self.sortChildKeys()):
+ for i in xrange(len(childArray)):
+ childArray[i]=childArray[i].transformToNative()
+ childArray[i].transformChildrenToNative()
+
+ def transformChildrenFromNative(self, clearBehavior=True):
+ """Recursively transform native children to vanilla representations."""
+ for childArray in self.contents.values():
+ for i in xrange(len(childArray)):
+ childArray[i]=childArray[i].transformFromNative()
+ childArray[i].transformChildrenFromNative(clearBehavior)
+ if clearBehavior:
+ childArray[i].behavior = None
+ childArray[i].parentBehavior = None
+
+ def __str__(self):
+ if self.name:
+ return "<" + self.name + "| " + str(self.getSortedChildren()) + ">"
+ else:
+ return '<' + '*unnamed*' + '| ' + str(self.getSortedChildren()) + '>'
+
+ def __repr__(self):
+ return self.__str__()
+
+ def prettyPrint(self, level = 0, tabwidth=3):
+ pre = ' ' * level * tabwidth
+ print pre, self.name
+ if isinstance(self, Component):
+ for line in self.getChildren():
+ line.prettyPrint(level + 1, tabwidth)
+ print
+
+class VObjectError(Exception):
+ def __init__(self, message, lineNumber=None):
+ self.message = message
+ if lineNumber is not None:
+ self.lineNumber = lineNumber
+ def __str__(self):
+ if hasattr(self, 'lineNumber'):
+ return "At line %s: %s" % \
+ (self.lineNumber, self.message)
+ else:
+ return repr(self.message)
+
+class ParseError(VObjectError):
+ pass
+
+class ValidateError(VObjectError):
+ pass
+
+class NativeError(VObjectError):
+ pass
+
+#-------------------------- Parsing functions ----------------------------------
+
+# parseLine regular expressions
+
+patterns = {}
+
+# Note that underscore is not legal for names, it's included because
+# Lotus Notes uses it
+patterns['name'] = '[a-zA-Z0-9\-_]+'
+patterns['safe_char'] = '[^";:,]'
+patterns['qsafe_char'] = '[^"]'
+
+# the combined Python string replacement and regex syntax is a little confusing;
+# remember that %(foobar)s is replaced with patterns['foobar'], so for instance
+# param_value is any number of safe_chars or any number of qsaf_chars surrounded
+# by double quotes.
+
+patterns['param_value'] = ' "%(qsafe_char)s * " | %(safe_char)s * ' % patterns
+
+
+# get a tuple of two elements, one will be empty, the other will have the value
+patterns['param_value_grouped'] = """
+" ( %(qsafe_char)s * )" | ( %(safe_char)s + )
+""" % patterns
+
+# get a parameter and its values, without any saved groups
+patterns['param'] = r"""
+; (?: %(name)s ) # parameter name
+(?:
+ (?: = (?: %(param_value)s ) )? # 0 or more parameter values, multiple
+ (?: , (?: %(param_value)s ) )* # parameters are comma separated
+)*
+""" % patterns
+
+# get a parameter, saving groups for name and value (value still needs parsing)
+patterns['params_grouped'] = r"""
+; ( %(name)s )
+
+(?: =
+ (
+ (?: (?: %(param_value)s ) )? # 0 or more parameter values, multiple
+ (?: , (?: %(param_value)s ) )* # parameters are comma separated
+ )
+)?
+""" % patterns
+
+# get a full content line, break it up into group, name, parameters, and value
+patterns['line'] = r"""
+^ ((?P<group> %(name)s)\.)?(?P<name> %(name)s) # name group
+ (?P<params> (?: %(param)s )* ) # params group (may be empty)
+: (?P<value> .* )$ # value group
+""" % patterns
+
+' "%(qsafe_char)s*" | %(safe_char)s* '
+
+param_values_re = re.compile(patterns['param_value_grouped'], re.VERBOSE)
+params_re = re.compile(patterns['params_grouped'], re.VERBOSE)
+line_re = re.compile(patterns['line'], re.DOTALL | re.VERBOSE)
+begin_re = re.compile('BEGIN', re.IGNORECASE)
+
+
+def parseParams(string):
+ """
+ >>> parseParams(';ALTREP="http://www.wiz.org"')
+ [['ALTREP', 'http://www.wiz.org']]
+ >>> parseParams('')
+ []
+ >>> parseParams(';ALTREP="http://www.wiz.org;;",Blah,Foo;NEXT=Nope;BAR')
+ [['ALTREP', 'http://www.wiz.org;;', 'Blah', 'Foo'], ['NEXT', 'Nope'], ['BAR']]
+ """
+ all = params_re.findall(string)
+ allParameters = []
+ for tup in all:
+ paramList = [tup[0]] # tup looks like (name, valuesString)
+ for pair in param_values_re.findall(tup[1]):
+ # pair looks like ('', value) or (value, '')
+ if pair[0] != '':
+ paramList.append(pair[0])
+ else:
+ paramList.append(pair[1])
+ allParameters.append(paramList)
+ return allParameters
+
+
+def parseLine(line, lineNumber = None):
+ """
+ >>> parseLine("BLAH:")
+ ('BLAH', [], '', None)
+ >>> parseLine("RDATE:VALUE=DATE:19970304,19970504,19970704,19970904")
+ ('RDATE', [], 'VALUE=DATE:19970304,19970504,19970704,19970904', None)
+ >>> parseLine('DESCRIPTION;ALTREP="http://www.wiz.org":The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA')
+ ('DESCRIPTION', [['ALTREP', 'http://www.wiz.org']], 'The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA', None)
+ >>> parseLine("EMAIL;PREF;INTERNET:john@nowhere.com")
+ ('EMAIL', [['PREF'], ['INTERNET']], 'john@nowhere.com', None)
+ >>> parseLine('EMAIL;TYPE="blah",hah;INTERNET="DIGI",DERIDOO:john@nowhere.com')
+ ('EMAIL', [['TYPE', 'blah', 'hah'], ['INTERNET', 'DIGI', 'DERIDOO']], 'john@nowhere.com', None)
+ >>> parseLine('item1.ADR;type=HOME;type=pref:;;Reeperbahn 116;Hamburg;;20359;')
+ ('ADR', [['type', 'HOME'], ['type', 'pref']], ';;Reeperbahn 116;Hamburg;;20359;', 'item1')
+ >>> parseLine(":")
+ Traceback (most recent call last):
+ ...
+ ParseError: 'Failed to parse line: :'
+ """
+
+ match = line_re.match(line)
+ if match is None:
+ raise ParseError("Failed to parse line: %s" % line, lineNumber)
+ # Underscores are replaced with dash to work around Lotus Notes
+ return (match.group('name').replace('_','-'),
+ parseParams(match.group('params')),
+ match.group('value'), match.group('group'))
+
+# logical line regular expressions
+
+patterns['lineend'] = r'(?:\r\n|\r|\n|$)'
+patterns['wrap'] = r'%(lineend)s [\t ]' % patterns
+patterns['logicallines'] = r"""
+(
+ (?: [^\r\n] | %(wrap)s )*
+ %(lineend)s
+)
+""" % patterns
+
+patterns['wraporend'] = r'(%(wrap)s | %(lineend)s )' % patterns
+
+wrap_re = re.compile(patterns['wraporend'], re.VERBOSE)
+logical_lines_re = re.compile(patterns['logicallines'], re.VERBOSE)
+
+testLines="""
+Line 0 text
+ , Line 0 continued.
+Line 1;encoding=quoted-printable:this is an evil=
+ evil=
+ format.
+Line 2 is a new line, it does not start with whitespace.
+"""
+
+def getLogicalLines(fp, allowQP=True, findBegin=False):
+ """Iterate through a stream, yielding one logical line at a time.
+
+ Because many applications still use vCard 2.1, we have to deal with the
+ quoted-printable encoding for long lines, as well as the vCard 3.0 and
+ vCalendar line folding technique, a whitespace character at the start
+ of the line.
+
+ Quoted-printable data will be decoded in the Behavior decoding phase.
+
+ >>> import StringIO
+ >>> f=StringIO.StringIO(testLines)
+ >>> for n, l in enumerate(getLogicalLines(f)):
+ ... print "Line %s: %s" % (n, l[0])
+ ...
+ Line 0: Line 0 text, Line 0 continued.
+ Line 1: Line 1;encoding=quoted-printable:this is an evil=
+ evil=
+ format.
+ Line 2: Line 2 is a new line, it does not start with whitespace.
+
+ """
+ if not allowQP:
+ bytes = fp.read(-1)
+ if len(bytes) > 0:
+ if type(bytes[0]) == unicode:
+ val = bytes
+ elif not findBegin:
+ val = bytes.decode('utf-8')
+ else:
+ for encoding in 'utf-8', 'utf-16-LE', 'utf-16-BE', 'iso-8859-1':
+ try:
+ val = bytes.decode(encoding)
+ if begin_re.search(val) is not None:
+ break
+ except UnicodeDecodeError:
+ pass
+ else:
+ raise ParseError, 'Could not find BEGIN when trying to determine encoding'
+ else:
+ val = bytes
+
+ # strip off any UTF8 BOMs which Python's UTF8 decoder leaves
+
+ val = val.lstrip( unicode( codecs.BOM_UTF8, "utf8" ) )
+
+ lineNumber = 1
+ for match in logical_lines_re.finditer(val):
+ line, n = wrap_re.subn('', match.group())
+ if line != '':
+ yield line, lineNumber
+ lineNumber += n
+
+ else:
+ quotedPrintable=False
+ newbuffer = StringIO.StringIO
+ logicalLine = newbuffer()
+ lineNumber = 0
+ lineStartNumber = 0
+ while True:
+ line = fp.readline()
+ if line == '':
+ break
+ else:
+ line = line.rstrip(CRLF)
+ lineNumber += 1
+ if line.rstrip() == '':
+ if logicalLine.pos > 0:
+ yield logicalLine.getvalue(), lineStartNumber
+ lineStartNumber = lineNumber
+ logicalLine = newbuffer()
+ quotedPrintable=False
+ continue
+
+ if quotedPrintable and allowQP:
+ logicalLine.write('\n')
+ logicalLine.write(line)
+ quotedPrintable=False
+ elif line[0] in SPACEORTAB:
+ logicalLine.write(line[1:])
+ elif logicalLine.pos > 0:
+ yield logicalLine.getvalue(), lineStartNumber
+ lineStartNumber = lineNumber
+ logicalLine = newbuffer()
+ logicalLine.write(line)
+ else:
+ logicalLine = newbuffer()
+ logicalLine.write(line)
+
+ # hack to deal with the fact that vCard 2.1 allows parameters to be
+ # encoded without a parameter name. False positives are unlikely, but
+ # possible.
+ val = logicalLine.getvalue()
+ if val[-1]=='=' and val.lower().find('quoted-printable') >= 0:
+ quotedPrintable=True
+
+ if logicalLine.pos > 0:
+ yield logicalLine.getvalue(), lineStartNumber
+
+
+def textLineToContentLine(text, n=None):
+ return ContentLine(*parseLine(text, n), **{'encoded':True, 'lineNumber' : n})
+
+
+def dquoteEscape(param):
+ """Return param, or "param" if ',' or ';' or ':' is in param."""
+ if param.find('"') >= 0:
+ raise VObjectError("Double quotes aren't allowed in parameter values.")
+ for char in ',;:':
+ if param.find(char) >= 0:
+ return '"'+ param + '"'
+ return param
+
+def foldOneLine(outbuf, input, lineLength = 75):
+ # Folding line procedure that ensures multi-byte utf-8 sequences are not broken
+ # across lines
+
+ if len(input) < lineLength:
+ # Optimize for unfolded line case
+ outbuf.write(input)
+ else:
+ # Look for valid utf8 range and write that out
+ start = 0
+ written = 0
+ while written < len(input):
+ # Start max length -1 chars on from where we are
+ offset = start + lineLength - 1
+ if offset >= len(input):
+ line = input[start:]
+ outbuf.write(line)
+ written = len(input)
+ else:
+ # Check whether next char is valid utf8 lead byte
+ while (input[offset] > 0x7F) and ((ord(input[offset]) & 0xC0) == 0x80):
+ # Step back until we have a valid char
+ offset -= 1
+
+ line = input[start:offset]
+ outbuf.write(line)
+ outbuf.write("\r\n ")
+ written += offset - start
+ start = offset
+ outbuf.write("\r\n")
+
+def defaultSerialize(obj, buf, lineLength):
+ """Encode and fold obj and its children, write to buf or return a string."""
+
+ outbuf = buf or cStringIO.StringIO()
+
+ if isinstance(obj, Component):
+ if obj.group is None:
+ groupString = ''
+ else:
+ groupString = obj.group + '.'
+ if obj.useBegin:
+ foldOneLine(outbuf, str(groupString + u"BEGIN:" + obj.name), lineLength)
+ for child in obj.getSortedChildren():
+ #validate is recursive, we only need to validate once
+ child.serialize(outbuf, lineLength, validate=False)
+ if obj.useBegin:
+ foldOneLine(outbuf, str(groupString + u"END:" + obj.name), lineLength)
+
+ elif isinstance(obj, ContentLine):
+ startedEncoded = obj.encoded
+ if obj.behavior and not startedEncoded: obj.behavior.encode(obj)
+ s=codecs.getwriter('utf-8')(cStringIO.StringIO()) #unfolded buffer
+ if obj.group is not None:
+ s.write(obj.group + '.')
+ s.write(obj.name.upper())
+ for key, paramvals in obj.params.iteritems():
+ s.write(';' + key + '=' + ','.join(dquoteEscape(p) for p in paramvals))
+ s.write(':' + obj.value)
+ if obj.behavior and not startedEncoded: obj.behavior.decode(obj)
+ foldOneLine(outbuf, s.getvalue(), lineLength)
+
+ return buf or outbuf.getvalue()
+
+
+testVCalendar="""
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+SUMMARY;blah=hi!:Bastille Day Party
+END:VEVENT
+END:VCALENDAR"""
+
+class Stack:
+ def __init__(self):
+ self.stack = []
+ def __len__(self):
+ return len(self.stack)
+ def top(self):
+ if len(self) == 0: return None
+ else: return self.stack[-1]
+ def topName(self):
+ if len(self) == 0: return None
+ else: return self.stack[-1].name
+ def modifyTop(self, item):
+ top = self.top()
+ if top:
+ top.add(item)
+ else:
+ new = Component()
+ self.push(new)
+ new.add(item) #add sets behavior for item and children
+ def push(self, obj): self.stack.append(obj)
+ def pop(self): return self.stack.pop()
+
+
+def readComponents(streamOrString, validate=False, transform=True,
+ findBegin=True, ignoreUnreadable=False,
+ allowQP=False):
+ """Generate one Component at a time from a stream.
+
+ >>> import StringIO
+ >>> f = StringIO.StringIO(testVCalendar)
+ >>> cal=readComponents(f).next()
+ >>> cal
+ <VCALENDAR| [<VEVENT| [<SUMMARY{u'BLAH': [u'hi!']}Bastille Day Party>]>]>
+ >>> cal.vevent.summary
+ <SUMMARY{u'BLAH': [u'hi!']}Bastille Day Party>
+
+ """
+ if isinstance(streamOrString, basestring):
+ stream = StringIO.StringIO(streamOrString)
+ else:
+ stream = streamOrString
+
+ try:
+ stack = Stack()
+ versionLine = None
+ n = 0
+ for line, n in getLogicalLines(stream, allowQP, findBegin):
+ if ignoreUnreadable:
+ try:
+ vline = textLineToContentLine(line, n)
+ except VObjectError, e:
+ if e.lineNumber is not None:
+ msg = "Skipped line %(lineNumber)s, message: %(msg)s"
+ else:
+ msg = "Skipped a line, message: %(msg)s"
+ logger.error(msg % {'lineNumber' : e.lineNumber,
+ 'msg' : e.message})
+ continue
+ else:
+ vline = textLineToContentLine(line, n)
+ if vline.name == "VERSION":
+ versionLine = vline
+ stack.modifyTop(vline)
+ elif vline.name == "BEGIN":
+ stack.push(Component(vline.value, group=vline.group))
+ elif vline.name == "PROFILE":
+ if not stack.top(): stack.push(Component())
+ stack.top().setProfile(vline.value)
+ elif vline.name == "END":
+ if len(stack) == 0:
+ err = "Attempted to end the %s component, \
+ but it was never opened" % vline.value
+ raise ParseError(err, n)
+ if vline.value.upper() == stack.topName(): #START matches END
+ if len(stack) == 1:
+ component=stack.pop()
+ if versionLine is not None:
+ component.setBehaviorFromVersionLine(versionLine)
+ if validate: component.validate(raiseException=True)
+ if transform: component.transformChildrenToNative()
+ yield component #EXIT POINT
+ else: stack.modifyTop(stack.pop())
+ else:
+ err = "%s component wasn't closed"
+ raise ParseError(err % stack.topName(), n)
+ else: stack.modifyTop(vline) #not a START or END line
+ if stack.top():
+ if stack.topName() is None:
+ logger.warning("Top level component was never named")
+ elif stack.top().useBegin:
+ raise ParseError("Component %s was never closed" % (stack.topName()), n)
+ yield stack.pop()
+
+ except ParseError, e:
+ e.input = streamOrString
+ raise
+
+
+def readOne(stream, validate=False, transform=True, findBegin=True,
+ ignoreUnreadable=False, allowQP=False):
+ """Return the first component from stream."""
+ return readComponents(stream, validate, transform, findBegin,
+ ignoreUnreadable, allowQP).next()
+
+#--------------------------- version registry ----------------------------------
+__behaviorRegistry={}
+
+def registerBehavior(behavior, name=None, default=False, id=None):
+ """Register the given behavior.
+
+ If default is True (or if this is the first version registered with this
+ name), the version will be the default if no id is given.
+
+ """
+ if not name: name=behavior.name.upper()
+ if id is None: id=behavior.versionString
+ if name in __behaviorRegistry:
+ if default:
+ __behaviorRegistry[name].insert(0, (id, behavior))
+ else:
+ __behaviorRegistry[name].append((id, behavior))
+ else:
+ __behaviorRegistry[name]=[(id, behavior)]
+
+def getBehavior(name, id=None):
+ """Return a matching behavior if it exists, or None.
+
+ If id is None, return the default for name.
+
+ """
+ name=name.upper()
+ if name in __behaviorRegistry:
+ if id:
+ for n, behavior in __behaviorRegistry[name]:
+ if n==id:
+ return behavior
+ else:
+ return __behaviorRegistry[name][0][1]
+ return None
+
+def newFromBehavior(name, id=None):
+ """Given a name, return a behaviored ContentLine or Component."""
+ name = name.upper()
+ behavior = getBehavior(name, id)
+ if behavior is None:
+ raise VObjectError("No behavior found named %s" % name)
+ if behavior.isComponent:
+ obj = Component(name)
+ else:
+ obj = ContentLine(name, [], '')
+ obj.behavior = behavior
+ obj.isNative = False
+ return obj
+
+
+#--------------------------- Helper function -----------------------------------
+def backslashEscape(s):
+ s=s.replace("\\","\\\\").replace(";","\;").replace(",","\,")
+ return s.replace("\r\n", "\\n").replace("\n","\\n").replace("\r","\\n")
+
+#------------------- Testing and running functions -----------------------------
+if __name__ == '__main__':
+ import tests
+ tests._test()
diff --git a/vobject/behavior.py b/vobject/behavior.py
new file mode 100644
index 0000000..226c0cc
--- /dev/null
+++ b/vobject/behavior.py
@@ -0,0 +1,164 @@
+"""Behavior (validation, encoding, and transformations) for vobjects."""
+
+import base
+
+#------------------------ Abstract class for behavior --------------------------
+class Behavior(object):
+ """Abstract class to describe vobject options, requirements and encodings.
+
+ Behaviors are used for root components like VCALENDAR, for subcomponents
+ like VEVENT, and for individual lines in components.
+
+ Behavior subclasses are not meant to be instantiated, all methods should
+ be classmethods.
+
+ @cvar name:
+ The uppercase name of the object described by the class, or a generic
+ name if the class defines behavior for many objects.
+ @cvar description:
+ A brief excerpt from the RFC explaining the function of the component or
+ line.
+ @cvar versionString:
+ The string associated with the component, for instance, 2.0 if there's a
+ line like VERSION:2.0, an empty string otherwise.
+ @cvar knownChildren:
+ A dictionary with uppercased component/property names as keys and a
+ tuple (min, max, id) as value, where id is the id used by
+ L{registerBehavior}, min and max are the limits on how many of this child
+ must occur. None is used to denote no max or no id.
+ @cvar quotedPrintable:
+ A boolean describing whether the object should be encoded and decoded
+ using quoted printable line folding and character escaping.
+ @cvar defaultBehavior:
+ Behavior to apply to ContentLine children when no behavior is found.
+ @cvar hasNative:
+ A boolean describing whether the object can be transformed into a more
+ Pythonic object.
+ @cvar isComponent:
+ A boolean, True if the object should be a Component.
+ @cvar sortFirst:
+ The lower-case list of children which should come first when sorting.
+ @cvar allowGroup:
+ Whether or not vCard style group prefixes are allowed.
+ """
+ name=''
+ description=''
+ versionString=''
+ knownChildren = {}
+ quotedPrintable = False
+ defaultBehavior = None
+ hasNative= False
+ isComponent = False
+ allowGroup = False
+ forceUTC = False
+ sortFirst = []
+
+ def __init__(self):
+ err="Behavior subclasses are not meant to be instantiated"
+ raise base.VObjectError(err)
+
+ @classmethod
+ def validate(cls, obj, raiseException=False, complainUnrecognized=False):
+ """Check if the object satisfies this behavior's requirements.
+
+ @param obj:
+ The L{ContentLine<base.ContentLine>} or
+ L{Component<base.Component>} to be validated.
+ @param raiseException:
+ If True, raise a L{base.ValidateError} on validation failure.
+ Otherwise return a boolean.
+ @param complainUnrecognized:
+ If True, fail to validate if an uncrecognized parameter or child is
+ found. Otherwise log the lack of recognition.
+
+ """
+ if not cls.allowGroup and obj.group is not None:
+ err = str(obj) + " has a group, but this object doesn't support groups"
+ raise base.VObjectError(err)
+ if isinstance(obj, base.ContentLine):
+ return cls.lineValidate(obj, raiseException, complainUnrecognized)
+ elif isinstance(obj, base.Component):
+ count = {}
+ for child in obj.getChildren():
+ if not child.validate(raiseException, complainUnrecognized):
+ return False
+ name=child.name.upper()
+ count[name] = count.get(name, 0) + 1
+ for key, val in cls.knownChildren.iteritems():
+ if count.get(key,0) < val[0]:
+ if raiseException:
+ m = "%s components must contain at least %i %s"
+ raise base.ValidateError(m % (cls.name, val[0], key))
+ return False
+ if val[1] and count.get(key,0) > val[1]:
+ if raiseException:
+ m = "%s components cannot contain more than %i %s"
+ raise base.ValidateError(m % (cls.name, val[1], key))
+ return False
+ return True
+ else:
+ err = str(obj) + " is not a Component or Contentline"
+ raise base.VObjectError(err)
+
+ @classmethod
+ def lineValidate(cls, line, raiseException, complainUnrecognized):
+ """Examine a line's parameters and values, return True if valid."""
+ return True
+
+ @classmethod
+ def decode(cls, line):
+ if line.encoded: line.encoded=0
+
+ @classmethod
+ def encode(cls, line):
+ if not line.encoded: line.encoded=1
+
+ @classmethod
+ def transformToNative(cls, obj):
+ """Turn a ContentLine or Component into a Python-native representation.
+
+ If appropriate, turn dates or datetime strings into Python objects.
+ Components containing VTIMEZONEs turn into VtimezoneComponents.
+
+ """
+ return obj
+
+ @classmethod
+ def transformFromNative(cls, obj):
+ """Inverse of transformToNative."""
+ raise base.NativeError("No transformFromNative defined")
+
+ @classmethod
+ def generateImplicitParameters(cls, obj):
+ """Generate any required information that don't yet exist."""
+ pass
+
+ @classmethod
+ def serialize(cls, obj, buf, lineLength, validate=True):
+ """Set implicit parameters, do encoding, return unicode string.
+
+ If validate is True, raise VObjectError if the line doesn't validate
+ after implicit parameters are generated.
+
+ Default is to call base.defaultSerialize.
+
+ """
+
+ cls.generateImplicitParameters(obj)
+ if validate: cls.validate(obj, raiseException=True)
+
+ if obj.isNative:
+ transformed = obj.transformFromNative()
+ undoTransform = True
+ else:
+ transformed = obj
+ undoTransform = False
+
+ out = base.defaultSerialize(transformed, buf, lineLength)
+ if undoTransform: obj.transformToNative()
+ return out
+
+ @classmethod
+ def valueRepr( cls, line ):
+ """return the representation of the given content line value"""
+ return line.value \ No newline at end of file
diff --git a/vobject/hcalendar.py b/vobject/hcalendar.py
new file mode 100644
index 0000000..93614ab
--- /dev/null
+++ b/vobject/hcalendar.py
@@ -0,0 +1,125 @@
+"""
+hCalendar: A microformat for serializing iCalendar data
+ (http://microformats.org/wiki/hcalendar)
+
+Here is a sample event in an iCalendar:
+
+BEGIN:VCALENDAR
+PRODID:-//XYZproduct//EN
+VERSION:2.0
+BEGIN:VEVENT
+URL:http://www.web2con.com/
+DTSTART:20051005
+DTEND:20051008
+SUMMARY:Web 2.0 Conference
+LOCATION:Argent Hotel\, San Francisco\, CA
+END:VEVENT
+END:VCALENDAR
+
+and an equivalent event in hCalendar format with various elements optimized appropriately.
+
+<span class="vevent">
+ <a class="url" href="http://www.web2con.com/">
+ <span class="summary">Web 2.0 Conference</span>:
+ <abbr class="dtstart" title="2005-10-05">October 5</abbr>-
+ <abbr class="dtend" title="2005-10-08">7</abbr>,
+ at the <span class="location">Argent Hotel, San Francisco, CA</span>
+ </a>
+</span>
+"""
+
+from base import foldOneLine, CRLF, registerBehavior
+from icalendar import VCalendar2_0
+from datetime import date, datetime, timedelta
+import StringIO
+
+class HCalendar(VCalendar2_0):
+ name = 'HCALENDAR'
+
+ @classmethod
+ def serialize(cls, obj, buf=None, lineLength=None, validate=True):
+ """
+ Serialize iCalendar to HTML using the hCalendar microformat (http://microformats.org/wiki/hcalendar)
+ """
+
+ outbuf = buf or StringIO.StringIO()
+ level = 0 # holds current indentation level
+ tabwidth = 3
+
+ def indent():
+ return ' ' * level * tabwidth
+
+ def out(s):
+ outbuf.write(indent())
+ outbuf.write(s)
+
+ # not serializing optional vcalendar wrapper
+
+ vevents = obj.vevent_list
+
+ for event in vevents:
+ out('<span class="vevent">' + CRLF)
+ level += 1
+
+ # URL
+ url = event.getChildValue("url")
+ if url:
+ out('<a class="url" href="' + url + '">' + CRLF)
+ level += 1
+ # SUMMARY
+ summary = event.getChildValue("summary")
+ if summary:
+ out('<span class="summary">' + summary + '</span>:' + CRLF)
+
+ # DTSTART
+ dtstart = event.getChildValue("dtstart")
+ if dtstart:
+ if type(dtstart) == date:
+ timeformat = "%A, %B %e"
+ machine = "%Y%m%d"
+ elif type(dtstart) == datetime:
+ timeformat = "%A, %B %e, %H:%M"
+ machine = "%Y%m%dT%H%M%S%z"
+
+ #TODO: Handle non-datetime formats?
+ #TODO: Spec says we should handle when dtstart isn't included
+
+ out('<abbr class="dtstart", title="%s">%s</abbr>\r\n' %
+ (dtstart.strftime(machine), dtstart.strftime(timeformat)))
+
+ # DTEND
+ dtend = event.getChildValue("dtend")
+ if not dtend:
+ duration = event.getChildValue("duration")
+ if duration:
+ dtend = duration + dtstart
+ # TODO: If lacking dtend & duration?
+
+ if dtend:
+ human = dtend
+ # TODO: Human readable part could be smarter, excluding repeated data
+ if type(dtend) == date:
+ human = dtend - timedelta(days=1)
+
+ out('- <abbr class="dtend", title="%s">%s</abbr>\r\n' %
+ (dtend.strftime(machine), human.strftime(timeformat)))
+
+ # LOCATION
+ location = event.getChildValue("location")
+ if location:
+ out('at <span class="location">' + location + '</span>' + CRLF)
+
+ description = event.getChildValue("description")
+ if description:
+ out('<div class="description">' + description + '</div>' + CRLF)
+
+ if url:
+ level -= 1
+ out('</a>' + CRLF)
+
+ level -= 1
+ out('</span>' + CRLF) # close vevent
+
+ return buf or outbuf.getvalue()
+
+registerBehavior(HCalendar) \ No newline at end of file
diff --git a/vobject/icalendar.py b/vobject/icalendar.py
new file mode 100644
index 0000000..09862c1
--- /dev/null
+++ b/vobject/icalendar.py
@@ -0,0 +1,1892 @@
+"""Definitions and behavior for iCalendar, also known as vCalendar 2.0"""
+
+import string
+import behavior
+import dateutil.rrule
+import dateutil.tz
+import StringIO, cStringIO
+import datetime
+import socket, random #for generating a UID
+import itertools
+
+from base import (VObjectError, NativeError, ValidateError, ParseError,
+ VBase, Component, ContentLine, logger, defaultSerialize,
+ registerBehavior, backslashEscape, foldOneLine,
+ newFromBehavior, CRLF, LF, ascii)
+
+#------------------------------- Constants -------------------------------------
+DATENAMES = ("rdate", "exdate")
+RULENAMES = ("exrule", "rrule")
+DATESANDRULES = ("exrule", "rrule", "rdate", "exdate")
+PRODID = u"-//PYVOBJECT//NONSGML Version 1//EN"
+
+WEEKDAYS = "MO", "TU", "WE", "TH", "FR", "SA", "SU"
+FREQUENCIES = ('YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY',
+ 'SECONDLY')
+
+zeroDelta = datetime.timedelta(0)
+twoHours = datetime.timedelta(hours=2)
+
+#---------------------------- TZID registry ------------------------------------
+__tzidMap={}
+
+def toUnicode(s):
+ """Take a string or unicode, turn it into unicode, decoding as utf-8"""
+ if isinstance(s, str):
+ s = s.decode('utf-8')
+ return s
+
+def registerTzid(tzid, tzinfo):
+ """Register a tzid -> tzinfo mapping."""
+ __tzidMap[toUnicode(tzid)]=tzinfo
+
+def getTzid(tzid):
+ """Return the tzid if it exists, or None."""
+ return __tzidMap.get(toUnicode(tzid), None)
+
+utc = dateutil.tz.tzutc()
+registerTzid("UTC", utc)
+
+#-------------------- Helper subclasses ----------------------------------------
+
+class TimezoneComponent(Component):
+ """A VTIMEZONE object.
+
+ VTIMEZONEs are parsed by dateutil.tz.tzical, the resulting datetime.tzinfo
+ subclass is stored in self.tzinfo, self.tzid stores the TZID associated
+ with this timezone.
+
+ @ivar name:
+ The uppercased name of the object, in this case always 'VTIMEZONE'.
+ @ivar tzinfo:
+ A datetime.tzinfo subclass representing this timezone.
+ @ivar tzid:
+ The string used to refer to this timezone.
+
+ """
+ def __init__(self, tzinfo=None, *args, **kwds):
+ """Accept an existing Component or a tzinfo class."""
+ super(TimezoneComponent, self).__init__(*args, **kwds)
+ self.isNative=True
+ # hack to make sure a behavior is assigned
+ if self.behavior is None:
+ self.behavior = VTimezone
+ if tzinfo is not None:
+ self.tzinfo = tzinfo
+ if not hasattr(self, 'name') or self.name == '':
+ self.name = 'VTIMEZONE'
+ self.useBegin = True
+
+ @classmethod
+ def registerTzinfo(obj, tzinfo):
+ """Register tzinfo if it's not already registered, return its tzid."""
+ tzid = obj.pickTzid(tzinfo)
+ if tzid and not getTzid(tzid):
+ registerTzid(tzid, tzinfo)
+ return tzid
+
+ def gettzinfo(self):
+ # workaround for dateutil failing to parse some experimental properties
+ good_lines = ('rdate', 'rrule', 'dtstart', 'tzname', 'tzoffsetfrom',
+ 'tzoffsetto', 'tzid')
+ # serialize encodes as utf-8, cStringIO will leave utf-8 alone
+ buffer = cStringIO.StringIO()
+ # allow empty VTIMEZONEs
+ if len(self.contents) == 0:
+ return None
+ def customSerialize(obj):
+ if isinstance(obj, Component):
+ foldOneLine(buffer, u"BEGIN:" + obj.name)
+ for child in obj.lines():
+ if child.name.lower() in good_lines:
+ child.serialize(buffer, 75, validate=False)
+ for comp in obj.components():
+ customSerialize(comp)
+ foldOneLine(buffer, u"END:" + obj.name)
+ customSerialize(self)
+ buffer.seek(0) # tzical wants to read a stream
+ return dateutil.tz.tzical(buffer).get()
+
+ def settzinfo(self, tzinfo, start=2000, end=2030):
+ """Create appropriate objects in self to represent tzinfo.
+
+ Collapse DST transitions to rrules as much as possible.
+
+ Assumptions:
+ - DST <-> Standard transitions occur on the hour
+ - never within a month of one another
+ - twice or fewer times a year
+ - never in the month of December
+ - DST always moves offset exactly one hour later
+ - tzinfo classes dst method always treats times that could be in either
+ offset as being in the later regime
+
+ """
+ def fromLastWeek(dt):
+ """How many weeks from the end of the month dt is, starting from 1."""
+ weekDelta = datetime.timedelta(weeks=1)
+ n = 1
+ current = dt + weekDelta
+ while current.month == dt.month:
+ n += 1
+ current += weekDelta
+ return n
+
+ # lists of dictionaries defining rules which are no longer in effect
+ completed = {'daylight' : [], 'standard' : []}
+
+ # dictionary defining rules which are currently in effect
+ working = {'daylight' : None, 'standard' : None}
+
+ # rule may be based on the nth week of the month or the nth from the last
+ for year in xrange(start, end + 1):
+ newyear = datetime.datetime(year, 1, 1)
+ for transitionTo in 'daylight', 'standard':
+ transition = getTransition(transitionTo, year, tzinfo)
+ oldrule = working[transitionTo]
+
+ if transition == newyear:
+ # transitionTo is in effect for the whole year
+ rule = {'end' : None,
+ 'start' : newyear,
+ 'month' : 1,
+ 'weekday' : None,
+ 'hour' : None,
+ 'plus' : None,
+ 'minus' : None,
+ 'name' : tzinfo.tzname(newyear),
+ 'offset' : tzinfo.utcoffset(newyear),
+ 'offsetfrom' : tzinfo.utcoffset(newyear)}
+ if oldrule is None:
+ # transitionTo was not yet in effect
+ working[transitionTo] = rule
+ else:
+ # transitionTo was already in effect
+ if (oldrule['offset'] !=
+ tzinfo.utcoffset(newyear)):
+ # old rule was different, it shouldn't continue
+ oldrule['end'] = year - 1
+ completed[transitionTo].append(oldrule)
+ working[transitionTo] = rule
+ elif transition is None:
+ # transitionTo is not in effect
+ if oldrule is not None:
+ # transitionTo used to be in effect
+ oldrule['end'] = year - 1
+ completed[transitionTo].append(oldrule)
+ working[transitionTo] = None
+ else:
+ # an offset transition was found
+ old_offset = tzinfo.utcoffset(transition - twoHours)
+ rule = {'end' : None, # None, or an integer year
+ 'start' : transition, # the datetime of transition
+ 'month' : transition.month,
+ 'weekday' : transition.weekday(),
+ 'hour' : transition.hour,
+ 'name' : tzinfo.tzname(transition),
+ 'plus' : (transition.day - 1)/ 7 + 1,#nth week of the month
+ 'minus' : fromLastWeek(transition), #nth from last week
+ 'offset' : tzinfo.utcoffset(transition),
+ 'offsetfrom' : old_offset}
+
+ if oldrule is None:
+ working[transitionTo] = rule
+ else:
+ plusMatch = rule['plus'] == oldrule['plus']
+ minusMatch = rule['minus'] == oldrule['minus']
+ truth = plusMatch or minusMatch
+ for key in 'month', 'weekday', 'hour', 'offset':
+ truth = truth and rule[key] == oldrule[key]
+ if truth:
+ # the old rule is still true, limit to plus or minus
+ if not plusMatch:
+ oldrule['plus'] = None
+ if not minusMatch:
+ oldrule['minus'] = None
+ else:
+ # the new rule did not match the old
+ oldrule['end'] = year - 1
+ completed[transitionTo].append(oldrule)
+ working[transitionTo] = rule
+
+ for transitionTo in 'daylight', 'standard':
+ if working[transitionTo] is not None:
+ completed[transitionTo].append(working[transitionTo])
+
+ self.tzid = []
+ self.daylight = []
+ self.standard = []
+
+ self.add('tzid').value = self.pickTzid(tzinfo, True)
+
+ old = None
+ for transitionTo in 'daylight', 'standard':
+ for rule in completed[transitionTo]:
+ comp = self.add(transitionTo)
+ dtstart = comp.add('dtstart')
+ dtstart.value = rule['start']
+ if rule['name'] is not None:
+ comp.add('tzname').value = rule['name']
+ line = comp.add('tzoffsetto')
+ line.value = deltaToOffset(rule['offset'])
+ line = comp.add('tzoffsetfrom')
+ line.value = deltaToOffset(rule['offsetfrom'])
+
+ if rule['plus'] is not None:
+ num = rule['plus']
+ elif rule['minus'] is not None:
+ num = -1 * rule['minus']
+ else:
+ num = None
+ if num is not None:
+ dayString = ";BYDAY=" + str(num) + WEEKDAYS[rule['weekday']]
+ else:
+ dayString = ""
+ if rule['end'] is not None:
+ if rule['hour'] is None:
+ # all year offset, with no rule
+ endDate = datetime.datetime(rule['end'], 1, 1)
+ else:
+ weekday = dateutil.rrule.weekday(rule['weekday'], num)
+ du_rule = dateutil.rrule.rrule(dateutil.rrule.YEARLY,
+ bymonth = rule['month'],byweekday = weekday,
+ dtstart = datetime.datetime(
+ rule['end'], 1, 1, rule['hour'])
+ )
+ endDate = du_rule[0]
+ endDate = endDate.replace(tzinfo = utc) - rule['offsetfrom']
+ endString = ";UNTIL="+ dateTimeToString(endDate)
+ else:
+ endString = ''
+ rulestring = "FREQ=YEARLY%s;BYMONTH=%s%s" % \
+ (dayString, str(rule['month']), endString)
+
+ comp.add('rrule').value = rulestring
+
+ tzinfo = property(gettzinfo, settzinfo)
+ # prevent Component's __setattr__ from overriding the tzinfo property
+ normal_attributes = Component.normal_attributes + ['tzinfo']
+
+ @staticmethod
+ def pickTzid(tzinfo, allowUTC=False):
+ """
+ Given a tzinfo class, use known APIs to determine TZID, or use tzname.
+ """
+ if tzinfo is None or (not allowUTC and tzinfo_eq(tzinfo, utc)):
+ #If tzinfo is UTC, we don't need a TZID
+ return None
+ # try PyICU's tzid key
+ if hasattr(tzinfo, 'tzid'):
+ return toUnicode(tzinfo.tzid)
+
+ # try pytz zone key
+ if hasattr(tzinfo, 'zone'):
+ return toUnicode(tzinfo.zone)
+
+ # try tzical's tzid key
+ elif hasattr(tzinfo, '_tzid'):
+ return toUnicode(tzinfo._tzid)
+ else:
+ # return tzname for standard (non-DST) time
+ notDST = datetime.timedelta(0)
+ for month in xrange(1,13):
+ dt = datetime.datetime(2000, month, 1)
+ if tzinfo.dst(dt) == notDST:
+ return toUnicode(tzinfo.tzname(dt))
+ # there was no standard time in 2000!
+ raise VObjectError("Unable to guess TZID for tzinfo %s" % str(tzinfo))
+
+ def __str__(self):
+ return "<VTIMEZONE | " + str(getattr(self, 'tzid', 'No TZID')) +">"
+
+ def __repr__(self):
+ return self.__str__()
+
+ def prettyPrint(self, level, tabwidth):
+ pre = ' ' * level * tabwidth
+ print pre, self.name
+ print pre, "TZID:", self.tzid
+ print
+
+class RecurringComponent(Component):
+ """A vCalendar component like VEVENT or VTODO which may recur.
+
+ Any recurring component can have one or multiple RRULE, RDATE,
+ EXRULE, or EXDATE lines, and one or zero DTSTART lines. It can also have a
+ variety of children that don't have any recurrence information.
+
+ In the example below, note that dtstart is included in the rruleset.
+ This is not the default behavior for dateutil's rrule implementation unless
+ dtstart would already have been a member of the recurrence rule, and as a
+ result, COUNT is wrong. This can be worked around when getting rruleset by
+ adjusting count down by one if an rrule has a count and dtstart isn't in its
+ result set, but by default, the rruleset property doesn't do this work
+ around, to access it getrruleset must be called with addRDate set True.
+
+ >>> import dateutil.rrule, datetime
+ >>> vevent = RecurringComponent(name='VEVENT')
+ >>> vevent.add('rrule').value =u"FREQ=WEEKLY;COUNT=2;INTERVAL=2;BYDAY=TU,TH"
+ >>> vevent.add('dtstart').value = datetime.datetime(2005, 1, 19, 9)
+
+ When creating rrule's programmatically it should be kept in
+ mind that count doesn't necessarily mean what rfc2445 says.
+
+ >>> list(vevent.rruleset)
+ [datetime.datetime(2005, 1, 20, 9, 0), datetime.datetime(2005, 2, 1, 9, 0)]
+ >>> list(vevent.getrruleset(addRDate=True))
+ [datetime.datetime(2005, 1, 19, 9, 0), datetime.datetime(2005, 1, 20, 9, 0)]
+
+ Also note that dateutil will expand all-day events (datetime.date values) to
+ datetime.datetime value with time 0 and no timezone.
+
+ >>> vevent.dtstart.value = datetime.date(2005,3,18)
+ >>> list(vevent.rruleset)
+ [datetime.datetime(2005, 3, 29, 0, 0), datetime.datetime(2005, 3, 31, 0, 0)]
+ >>> list(vevent.getrruleset(True))
+ [datetime.datetime(2005, 3, 18, 0, 0), datetime.datetime(2005, 3, 29, 0, 0)]
+
+ @ivar rruleset:
+ A U{rruleset<https://moin.conectiva.com.br/DateUtil>}.
+ """
+ def __init__(self, *args, **kwds):
+ super(RecurringComponent, self).__init__(*args, **kwds)
+ self.isNative=True
+ #self.clobberedRDates=[]
+
+
+ def getrruleset(self, addRDate = False):
+ """Get an rruleset created from self.
+
+ If addRDate is True, add an RDATE for dtstart if it's not included in
+ an RRULE, and count is decremented if it exists.
+
+ Note that for rules which don't match DTSTART, DTSTART may not appear
+ in list(rruleset), although it should. By default, an RDATE is not
+ created in these cases, and count isn't updated, so dateutil may list
+ a spurious occurrence.
+
+ """
+ rruleset = None
+ for name in DATESANDRULES:
+ addfunc = None
+ for line in self.contents.get(name, ()):
+ # don't bother creating a rruleset unless there's a rule
+ if rruleset is None:
+ rruleset = dateutil.rrule.rruleset()
+ if addfunc is None:
+ addfunc=getattr(rruleset, name)
+
+ if name in DATENAMES:
+ if type(line.value[0]) == datetime.datetime:
+ map(addfunc, line.value)
+ elif type(line.value[0]) == datetime.date:
+ for dt in line.value:
+ addfunc(datetime.datetime(dt.year, dt.month, dt.day))
+ else:
+ # ignore RDATEs with PERIOD values for now
+ pass
+ elif name in RULENAMES:
+ try:
+ dtstart = self.dtstart.value
+ except AttributeError, KeyError:
+ # Special for VTODO - try DUE property instead
+ try:
+ if self.name == "VTODO":
+ dtstart = self.due.value
+ else:
+ # if there's no dtstart, just return None
+ return None
+ except AttributeError, KeyError:
+ # if there's no due, just return None
+ return None
+
+ # rrulestr complains about unicode, so cast to str
+ rule = dateutil.rrule.rrulestr(str(line.value),
+ dtstart=dtstart)
+ until = rule._until
+ if until is not None and \
+ isinstance(dtstart, datetime.datetime) and \
+ (until.tzinfo != dtstart.tzinfo):
+ # dateutil converts the UNTIL date to a datetime,
+ # check to see if the UNTIL parameter value was a date
+ vals = dict(pair.split('=') for pair in
+ line.value.upper().split(';'))
+ if len(vals.get('UNTIL', '')) == 8:
+ until = datetime.datetime.combine(until.date(),
+ dtstart.time())
+ # While RFC2445 says UNTIL MUST be UTC, Chandler allows
+ # floating recurring events, and uses floating UNTIL values.
+ # Also, some odd floating UNTIL but timezoned DTSTART values
+ # have shown up in the wild, so put floating UNTIL values
+ # DTSTART's timezone
+ if until.tzinfo is None:
+ until = until.replace(tzinfo=dtstart.tzinfo)
+
+ if dtstart.tzinfo is not None:
+ until = until.astimezone(dtstart.tzinfo)
+
+ rule._until = until
+
+ # add the rrule or exrule to the rruleset
+ addfunc(rule)
+
+ if name == 'rrule' and addRDate:
+ try:
+ # dateutils does not work with all-day (datetime.date) items
+ # so we need to convert to a datetime.datetime
+ # (which is what dateutils does internally)
+ if not isinstance(dtstart, datetime.datetime):
+ adddtstart = datetime.datetime.fromordinal(dtstart.toordinal())
+ else:
+ adddtstart = dtstart
+ if rruleset._rrule[-1][0] != adddtstart:
+ rruleset.rdate(adddtstart)
+ added = True
+ else:
+ added = False
+ except IndexError:
+ # it's conceivable that an rrule might have 0 datetimes
+ added = False
+ if added and rruleset._rrule[-1]._count != None:
+ rruleset._rrule[-1]._count -= 1
+ return rruleset
+
+ def setrruleset(self, rruleset):
+
+ # Get DTSTART from component (or DUE if no DTSTART in a VTODO)
+ try:
+ dtstart = self.dtstart.value
+ except AttributeError, KeyError:
+ if self.name == "VTODO":
+ dtstart = self.due.value
+ else:
+ raise
+
+ isDate = datetime.date == type(dtstart)
+ if isDate:
+ dtstart = datetime.datetime(dtstart.year,dtstart.month, dtstart.day)
+ untilSerialize = dateToString
+ else:
+ # make sure to convert time zones to UTC
+ untilSerialize = lambda x: dateTimeToString(x, True)
+
+ for name in DATESANDRULES:
+ if hasattr(self.contents, name):
+ del self.contents[name]
+ setlist = getattr(rruleset, '_' + name)
+ if name in DATENAMES:
+ setlist = list(setlist) # make a copy of the list
+ if name == 'rdate' and dtstart in setlist:
+ setlist.remove(dtstart)
+ if isDate:
+ setlist = [dt.date() for dt in setlist]
+ if len(setlist) > 0:
+ self.add(name).value = setlist
+ elif name in RULENAMES:
+ for rule in setlist:
+ buf = StringIO.StringIO()
+ buf.write('FREQ=')
+ buf.write(FREQUENCIES[rule._freq])
+
+ values = {}
+
+ if rule._interval != 1:
+ values['INTERVAL'] = [str(rule._interval)]
+ if rule._wkst != 0: # wkst defaults to Monday
+ values['WKST'] = [WEEKDAYS[rule._wkst]]
+ if rule._bysetpos is not None:
+ values['BYSETPOS'] = [str(i) for i in rule._bysetpos]
+
+ if rule._count is not None:
+ values['COUNT'] = [str(rule._count)]
+ elif rule._until is not None:
+ values['UNTIL'] = [untilSerialize(rule._until)]
+
+ days = []
+ if (rule._byweekday is not None and (
+ dateutil.rrule.WEEKLY != rule._freq or
+ len(rule._byweekday) != 1 or
+ rule._dtstart.weekday() != rule._byweekday[0])):
+ # ignore byweekday if freq is WEEKLY and day correlates
+ # with dtstart because it was automatically set by
+ # dateutil
+ days.extend(WEEKDAYS[n] for n in rule._byweekday)
+
+ if rule._bynweekday is not None:
+ days.extend(str(n) + WEEKDAYS[day] for day, n in rule._bynweekday)
+
+ if len(days) > 0:
+ values['BYDAY'] = days
+
+ if rule._bymonthday is not None and len(rule._bymonthday) > 0:
+ if not (rule._freq <= dateutil.rrule.MONTHLY and
+ len(rule._bymonthday) == 1 and
+ rule._bymonthday[0] == rule._dtstart.day):
+ # ignore bymonthday if it's generated by dateutil
+ values['BYMONTHDAY'] = [str(n) for n in rule._bymonthday]
+
+ if rule._bymonth is not None and len(rule._bymonth) > 0:
+ if (rule._byweekday is not None or
+ len(rule._bynweekday or ()) > 0 or
+ not (rule._freq == dateutil.rrule.YEARLY and
+ len(rule._bymonth) == 1 and
+ rule._bymonth[0] == rule._dtstart.month)):
+ # ignore bymonth if it's generated by dateutil
+ values['BYMONTH'] = [str(n) for n in rule._bymonth]
+
+ if rule._byyearday is not None:
+ values['BYYEARDAY'] = [str(n) for n in rule._byyearday]
+ if rule._byweekno is not None:
+ values['BYWEEKNO'] = [str(n) for n in rule._byweekno]
+
+ # byhour, byminute, bysecond are always ignored for now
+
+
+ for key, paramvals in values.iteritems():
+ buf.write(';')
+ buf.write(key)
+ buf.write('=')
+ buf.write(','.join(paramvals))
+
+ self.add(name).value = buf.getvalue()
+
+
+
+ rruleset = property(getrruleset, setrruleset)
+
+ def __setattr__(self, name, value):
+ """For convenience, make self.contents directly accessible."""
+ if name == 'rruleset':
+ self.setrruleset(value)
+ else:
+ super(RecurringComponent, self).__setattr__(name, value)
+
+class TextBehavior(behavior.Behavior):
+ """Provide backslash escape encoding/decoding for single valued properties.
+
+ TextBehavior also deals with base64 encoding if the ENCODING parameter is
+ explicitly set to BASE64.
+
+ """
+ base64string = 'BASE64' # vCard uses B
+
+ @classmethod
+ def decode(cls, line):
+ """Remove backslash escaping from line.value."""
+ if line.encoded:
+ encoding = getattr(line, 'encoding_param', None)
+ if encoding and encoding.upper() == cls.base64string:
+ line.value = line.value.decode('base64')
+ else:
+ line.value = stringToTextValues(line.value)[0]
+ line.encoded=False
+
+ @classmethod
+ def encode(cls, line):
+ """Backslash escape line.value."""
+ if not line.encoded:
+ encoding = getattr(line, 'encoding_param', None)
+ if encoding and encoding.upper() == cls.base64string:
+ line.value = line.value.encode('base64').replace('\n', '')
+ else:
+ line.value = backslashEscape(line.value)
+ line.encoded=True
+
+class VCalendarComponentBehavior(behavior.Behavior):
+ defaultBehavior = TextBehavior
+ isComponent = True
+
+class RecurringBehavior(VCalendarComponentBehavior):
+ """Parent Behavior for components which should be RecurringComponents."""
+ hasNative = True
+
+ @staticmethod
+ def transformToNative(obj):
+ """Turn a recurring Component into a RecurringComponent."""
+ if not obj.isNative:
+ object.__setattr__(obj, '__class__', RecurringComponent)
+ obj.isNative = True
+ return obj
+
+ @staticmethod
+ def transformFromNative(obj):
+ if obj.isNative:
+ object.__setattr__(obj, '__class__', Component)
+ obj.isNative = False
+ return obj
+
+ @staticmethod
+ def generateImplicitParameters(obj):
+ """Generate a UID if one does not exist.
+
+ This is just a dummy implementation, for now.
+
+ """
+ if not hasattr(obj, 'uid'):
+ rand = str(int(random.random() * 100000))
+ now = datetime.datetime.now(utc)
+ now = dateTimeToString(now)
+ host = socket.gethostname()
+ obj.add(ContentLine('UID', [], now + '-' + rand + '@' + host))
+
+
+class DateTimeBehavior(behavior.Behavior):
+ """Parent Behavior for ContentLines containing one DATE-TIME."""
+ hasNative = True
+
+ @staticmethod
+ def transformToNative(obj):
+ """Turn obj.value into a datetime.
+
+ RFC2445 allows times without time zone information, "floating times"
+ in some properties. Mostly, this isn't what you want, but when parsing
+ a file, real floating times are noted by setting to 'TRUE' the
+ X-VOBJ-FLOATINGTIME-ALLOWED parameter.
+
+ """
+ if obj.isNative: return obj
+ obj.isNative = True
+ if obj.value == '': return obj
+ obj.value=str(obj.value)
+ #we're cheating a little here, parseDtstart allows DATE
+ obj.value=parseDtstart(obj)
+ if obj.value.tzinfo is None:
+ obj.params['X-VOBJ-FLOATINGTIME-ALLOWED'] = ['TRUE']
+ if obj.params.get('TZID'):
+ # Keep a copy of the original TZID around
+ obj.params['X-VOBJ-ORIGINAL-TZID'] = [obj.params['TZID']]
+ del obj.params['TZID']
+ return obj
+
+ @classmethod
+ def transformFromNative(cls, obj):
+ """Replace the datetime in obj.value with an ISO 8601 string."""
+ if obj.isNative:
+ obj.isNative = False
+ tzid = TimezoneComponent.registerTzinfo(obj.value.tzinfo)
+ obj.value = dateTimeToString(obj.value, cls.forceUTC)
+ if not cls.forceUTC and tzid is not None:
+ obj.tzid_param = tzid
+ if obj.params.get('X-VOBJ-ORIGINAL-TZID'):
+ if not hasattr(obj, 'tzid_param'):
+ obj.tzid_param = obj.x_vobj_original_tzid_param
+ del obj.params['X-VOBJ-ORIGINAL-TZID']
+
+ return obj
+
+class UTCDateTimeBehavior(DateTimeBehavior):
+ """A value which must be specified in UTC."""
+ forceUTC = True
+
+class DateOrDateTimeBehavior(behavior.Behavior):
+ """Parent Behavior for ContentLines containing one DATE or DATE-TIME."""
+ hasNative = True
+
+ @staticmethod
+ def transformToNative(obj):
+ """Turn obj.value into a date or datetime."""
+ if obj.isNative: return obj
+ obj.isNative = True
+ if obj.value == '': return obj
+ obj.value=str(obj.value)
+ obj.value=parseDtstart(obj, allowSignatureMismatch=True)
+ if getattr(obj, 'value_param', 'DATE-TIME').upper() == 'DATE-TIME':
+ if hasattr(obj, 'tzid_param'):
+ # Keep a copy of the original TZID around
+ obj.params['X-VOBJ-ORIGINAL-TZID'] = [obj.tzid_param]
+ del obj.tzid_param
+ return obj
+
+ @staticmethod
+ def transformFromNative(obj):
+ """Replace the date or datetime in obj.value with an ISO 8601 string."""
+ if type(obj.value) == datetime.date:
+ obj.isNative = False
+ obj.value_param = 'DATE'
+ obj.value = dateToString(obj.value)
+ return obj
+ else: return DateTimeBehavior.transformFromNative(obj)
+
+class MultiDateBehavior(behavior.Behavior):
+ """
+ Parent Behavior for ContentLines containing one or more DATE, DATE-TIME, or
+ PERIOD.
+
+ """
+ hasNative = True
+
+ @staticmethod
+ def transformToNative(obj):
+ """
+ Turn obj.value into a list of dates, datetimes, or
+ (datetime, timedelta) tuples.
+
+ """
+ if obj.isNative:
+ return obj
+ obj.isNative = True
+ if obj.value == '':
+ obj.value = []
+ return obj
+ tzinfo = getTzid(getattr(obj, 'tzid_param', None))
+ valueParam = getattr(obj, 'value_param', "DATE-TIME").upper()
+ valTexts = obj.value.split(",")
+ if valueParam == "DATE":
+ obj.value = [stringToDate(x) for x in valTexts]
+ elif valueParam == "DATE-TIME":
+ obj.value = [stringToDateTime(x, tzinfo) for x in valTexts]
+ elif valueParam == "PERIOD":
+ obj.value = [stringToPeriod(x, tzinfo) for x in valTexts]
+ return obj
+
+ @staticmethod
+ def transformFromNative(obj):
+ """
+ Replace the date, datetime or period tuples in obj.value with
+ appropriate strings.
+
+ """
+ if obj.value and type(obj.value[0]) == datetime.date:
+ obj.isNative = False
+ obj.value_param = 'DATE'
+ obj.value = ','.join([dateToString(val) for val in obj.value])
+ return obj
+ # Fixme: handle PERIOD case
+ else:
+ if obj.isNative:
+ obj.isNative = False
+ transformed = []
+ tzid = None
+ for val in obj.value:
+ if tzid is None and type(val) == datetime.datetime:
+ tzid = TimezoneComponent.registerTzinfo(val.tzinfo)
+ if tzid is not None:
+ obj.tzid_param = tzid
+ transformed.append(dateTimeToString(val))
+ obj.value = ','.join(transformed)
+ return obj
+
+class MultiTextBehavior(behavior.Behavior):
+ """Provide backslash escape encoding/decoding of each of several values.
+
+ After transformation, value is a list of strings.
+
+ """
+
+ @staticmethod
+ def decode(line):
+ """Remove backslash escaping from line.value, then split on commas."""
+ if line.encoded:
+ line.value = stringToTextValues(line.value)
+ line.encoded=False
+
+ @staticmethod
+ def encode(line):
+ """Backslash escape line.value."""
+ if not line.encoded:
+ line.value = ','.join(backslashEscape(val) for val in line.value)
+ line.encoded=True
+
+
+#------------------------ Registered Behavior subclasses -----------------------
+class VCalendar2_0(VCalendarComponentBehavior):
+ """vCalendar 2.0 behavior. With added VAVAILABILITY support."""
+ name = 'VCALENDAR'
+ description = 'vCalendar 2.0, also known as iCalendar.'
+ versionString = '2.0'
+ sortFirst = ('version', 'calscale', 'method', 'prodid', 'vtimezone')
+ knownChildren = {'CALSCALE': (0, 1, None),#min, max, behaviorRegistry id
+ 'METHOD': (0, 1, None),
+ 'VERSION': (0, 1, None),#required, but auto-generated
+ 'PRODID': (1, 1, None),
+ 'VTIMEZONE': (0, None, None),
+ 'VEVENT': (0, None, None),
+ 'VTODO': (0, None, None),
+ 'VJOURNAL': (0, None, None),
+ 'VFREEBUSY': (0, None, None),
+ 'VAVAILABILITY': (0, None, None),
+ }
+
+ @classmethod
+ def generateImplicitParameters(cls, obj):
+ """Create PRODID, VERSION, and VTIMEZONEs if needed.
+
+ VTIMEZONEs will need to exist whenever TZID parameters exist or when
+ datetimes with tzinfo exist.
+
+ """
+ for comp in obj.components():
+ if comp.behavior is not None:
+ comp.behavior.generateImplicitParameters(comp)
+ if not hasattr(obj, 'prodid'):
+ obj.add(ContentLine('PRODID', [], PRODID))
+ if not hasattr(obj, 'version'):
+ obj.add(ContentLine('VERSION', [], cls.versionString))
+ tzidsUsed = {}
+
+ def findTzids(obj, table):
+ if isinstance(obj, ContentLine) and (obj.behavior is None or
+ not obj.behavior.forceUTC):
+ if getattr(obj, 'tzid_param', None):
+ table[obj.tzid_param] = 1
+ else:
+ if type(obj.value) == list:
+ for item in obj.value:
+ tzinfo = getattr(obj.value, 'tzinfo', None)
+ tzid = TimezoneComponent.registerTzinfo(tzinfo)
+ if tzid:
+ table[tzid] = 1
+ else:
+ tzinfo = getattr(obj.value, 'tzinfo', None)
+ tzid = TimezoneComponent.registerTzinfo(tzinfo)
+ if tzid:
+ table[tzid] = 1
+ for child in obj.getChildren():
+ if obj.name != 'VTIMEZONE':
+ findTzids(child, table)
+
+ findTzids(obj, tzidsUsed)
+ oldtzids = [toUnicode(x.tzid.value) for x in getattr(obj, 'vtimezone_list', [])]
+ for tzid in tzidsUsed.keys():
+ tzid = toUnicode(tzid)
+ if tzid != u'UTC' and tzid not in oldtzids:
+ obj.add(TimezoneComponent(tzinfo=getTzid(tzid)))
+registerBehavior(VCalendar2_0)
+
+class VTimezone(VCalendarComponentBehavior):
+ """Timezone behavior."""
+ name = 'VTIMEZONE'
+ hasNative = True
+ description = 'A grouping of component properties that defines a time zone.'
+ sortFirst = ('tzid', 'last-modified', 'tzurl', 'standard', 'daylight')
+ knownChildren = {'TZID': (1, 1, None),#min, max, behaviorRegistry id
+ 'LAST-MODIFIED':(0, 1, None),
+ 'TZURL': (0, 1, None),
+ 'STANDARD': (0, None, None),#NOTE: One of Standard or
+ 'DAYLIGHT': (0, None, None) # Daylight must appear
+ }
+
+ @classmethod
+ def validate(cls, obj, raiseException, *args):
+ if not hasattr(obj, 'tzid') or obj.tzid.value is None:
+ if raiseException:
+ m = "VTIMEZONE components must contain a valid TZID"
+ raise ValidateError(m)
+ return False
+ if obj.contents.has_key('standard') or obj.contents.has_key('daylight'):
+ return super(VTimezone, cls).validate(obj, raiseException, *args)
+ else:
+ if raiseException:
+ m = "VTIMEZONE components must contain a STANDARD or a DAYLIGHT\
+ component"
+ raise ValidateError(m)
+ return False
+
+
+ @staticmethod
+ def transformToNative(obj):
+ if not obj.isNative:
+ object.__setattr__(obj, '__class__', TimezoneComponent)
+ obj.isNative = True
+ obj.registerTzinfo(obj.tzinfo)
+ return obj
+
+ @staticmethod
+ def transformFromNative(obj):
+ return obj
+
+
+registerBehavior(VTimezone)
+
+class DaylightOrStandard(VCalendarComponentBehavior):
+ hasNative = False
+ knownChildren = {'DTSTART': (1, 1, None),#min, max, behaviorRegistry id
+ 'RRULE': (0, 1, None)}
+
+registerBehavior(DaylightOrStandard, 'STANDARD')
+registerBehavior(DaylightOrStandard, 'DAYLIGHT')
+
+
+class VEvent(RecurringBehavior):
+ """Event behavior."""
+ name='VEVENT'
+ sortFirst = ('uid', 'recurrence-id', 'dtstart', 'duration', 'dtend')
+
+ description='A grouping of component properties, and possibly including \
+ "VALARM" calendar components, that represents a scheduled \
+ amount of time on a calendar.'
+ knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id
+ 'CLASS': (0, 1, None),
+ 'CREATED': (0, 1, None),
+ 'DESCRIPTION': (0, 1, None),
+ 'GEO': (0, 1, None),
+ 'LAST-MODIFIED':(0, 1, None),
+ 'LOCATION': (0, 1, None),
+ 'ORGANIZER': (0, 1, None),
+ 'PRIORITY': (0, 1, None),
+ 'DTSTAMP': (0, 1, None),
+ 'SEQUENCE': (0, 1, None),
+ 'STATUS': (0, 1, None),
+ 'SUMMARY': (0, 1, None),
+ 'TRANSP': (0, 1, None),
+ 'UID': (1, 1, None),
+ 'URL': (0, 1, None),
+ 'RECURRENCE-ID':(0, 1, None),
+ 'DTEND': (0, 1, None), #NOTE: Only one of DtEnd or
+ 'DURATION': (0, 1, None), # Duration can appear
+ 'ATTACH': (0, None, None),
+ 'ATTENDEE': (0, None, None),
+ 'CATEGORIES': (0, None, None),
+ 'COMMENT': (0, None, None),
+ 'CONTACT': (0, None, None),
+ 'EXDATE': (0, None, None),
+ 'EXRULE': (0, None, None),
+ 'REQUEST-STATUS': (0, None, None),
+ 'RELATED-TO': (0, None, None),
+ 'RESOURCES': (0, None, None),
+ 'RDATE': (0, None, None),
+ 'RRULE': (0, None, None),
+ 'VALARM': (0, None, None)
+ }
+
+ @classmethod
+ def validate(cls, obj, raiseException, *args):
+ if obj.contents.has_key('dtend') and obj.contents.has_key('duration'):
+ if raiseException:
+ m = "VEVENT components cannot contain both DTEND and DURATION\
+ components"
+ raise ValidateError(m)
+ return False
+ else:
+ return super(VEvent, cls).validate(obj, raiseException, *args)
+
+registerBehavior(VEvent)
+
+
+class VTodo(RecurringBehavior):
+ """To-do behavior."""
+ name='VTODO'
+ description='A grouping of component properties and possibly "VALARM" \
+ calendar components that represent an action-item or \
+ assignment.'
+ knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id
+ 'CLASS': (0, 1, None),
+ 'COMPLETED': (0, 1, None),
+ 'CREATED': (0, 1, None),
+ 'DESCRIPTION': (0, 1, None),
+ 'GEO': (0, 1, None),
+ 'LAST-MODIFIED':(0, 1, None),
+ 'LOCATION': (0, 1, None),
+ 'ORGANIZER': (0, 1, None),
+ 'PERCENT': (0, 1, None),
+ 'PRIORITY': (0, 1, None),
+ 'DTSTAMP': (0, 1, None),
+ 'SEQUENCE': (0, 1, None),
+ 'STATUS': (0, 1, None),
+ 'SUMMARY': (0, 1, None),
+ 'UID': (0, 1, None),
+ 'URL': (0, 1, None),
+ 'RECURRENCE-ID':(0, 1, None),
+ 'DUE': (0, 1, None), #NOTE: Only one of Due or
+ 'DURATION': (0, 1, None), # Duration can appear
+ 'ATTACH': (0, None, None),
+ 'ATTENDEE': (0, None, None),
+ 'CATEGORIES': (0, None, None),
+ 'COMMENT': (0, None, None),
+ 'CONTACT': (0, None, None),
+ 'EXDATE': (0, None, None),
+ 'EXRULE': (0, None, None),
+ 'REQUEST-STATUS': (0, None, None),
+ 'RELATED-TO': (0, None, None),
+ 'RESOURCES': (0, None, None),
+ 'RDATE': (0, None, None),
+ 'RRULE': (0, None, None),
+ 'VALARM': (0, None, None)
+ }
+
+ @classmethod
+ def validate(cls, obj, raiseException, *args):
+ if obj.contents.has_key('due') and obj.contents.has_key('duration'):
+ if raiseException:
+ m = "VTODO components cannot contain both DUE and DURATION\
+ components"
+ raise ValidateError(m)
+ return False
+ else:
+ return super(VTodo, cls).validate(obj, raiseException, *args)
+
+registerBehavior(VTodo)
+
+
+class VJournal(RecurringBehavior):
+ """Journal entry behavior."""
+ name='VJOURNAL'
+ knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id
+ 'CLASS': (0, 1, None),
+ 'CREATED': (0, 1, None),
+ 'DESCRIPTION': (0, 1, None),
+ 'LAST-MODIFIED':(0, 1, None),
+ 'ORGANIZER': (0, 1, None),
+ 'DTSTAMP': (0, 1, None),
+ 'SEQUENCE': (0, 1, None),
+ 'STATUS': (0, 1, None),
+ 'SUMMARY': (0, 1, None),
+ 'UID': (0, 1, None),
+ 'URL': (0, 1, None),
+ 'RECURRENCE-ID':(0, 1, None),
+ 'ATTACH': (0, None, None),
+ 'ATTENDEE': (0, None, None),
+ 'CATEGORIES': (0, None, None),
+ 'COMMENT': (0, None, None),
+ 'CONTACT': (0, None, None),
+ 'EXDATE': (0, None, None),
+ 'EXRULE': (0, None, None),
+ 'REQUEST-STATUS': (0, None, None),
+ 'RELATED-TO': (0, None, None),
+ 'RDATE': (0, None, None),
+ 'RRULE': (0, None, None)
+ }
+registerBehavior(VJournal)
+
+
+class VFreeBusy(VCalendarComponentBehavior):
+ """Free/busy state behavior.
+
+ >>> vfb = newFromBehavior('VFREEBUSY')
+ >>> vfb.add('uid').value = 'test'
+ >>> vfb.add('dtstart').value = datetime.datetime(2006, 2, 16, 1, tzinfo=utc)
+ >>> vfb.add('dtend').value = vfb.dtstart.value + twoHours
+ >>> vfb.add('freebusy').value = [(vfb.dtstart.value, twoHours / 2)]
+ >>> vfb.add('freebusy').value = [(vfb.dtstart.value, vfb.dtend.value)]
+ >>> print vfb.serialize()
+ BEGIN:VFREEBUSY
+ UID:test
+ DTSTART:20060216T010000Z
+ DTEND:20060216T030000Z
+ FREEBUSY:20060216T010000Z/PT1H
+ FREEBUSY:20060216T010000Z/20060216T030000Z
+ END:VFREEBUSY
+
+ """
+ name='VFREEBUSY'
+ description='A grouping of component properties that describe either a \
+ request for free/busy time, describe a response to a request \
+ for free/busy time or describe a published set of busy time.'
+ sortFirst = ('uid', 'dtstart', 'duration', 'dtend')
+ knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id
+ 'CONTACT': (0, 1, None),
+ 'DTEND': (0, 1, None),
+ 'DURATION': (0, 1, None),
+ 'ORGANIZER': (0, 1, None),
+ 'DTSTAMP': (0, 1, None),
+ 'UID': (0, 1, None),
+ 'URL': (0, 1, None),
+ 'ATTENDEE': (0, None, None),
+ 'COMMENT': (0, None, None),
+ 'FREEBUSY': (0, None, None),
+ 'REQUEST-STATUS': (0, None, None)
+ }
+registerBehavior(VFreeBusy)
+
+
+class VAlarm(VCalendarComponentBehavior):
+ """Alarm behavior."""
+ name='VALARM'
+ description='Alarms describe when and how to provide alerts about events \
+ and to-dos.'
+ knownChildren = {'ACTION': (1, 1, None),#min, max, behaviorRegistry id
+ 'TRIGGER': (1, 1, None),
+ 'DURATION': (0, 1, None),
+ 'REPEAT': (0, 1, None),
+ 'DESCRIPTION': (0, 1, None)
+ }
+
+ @staticmethod
+ def generateImplicitParameters(obj):
+ """Create default ACTION and TRIGGER if they're not set."""
+ try:
+ obj.action
+ except AttributeError:
+ obj.add('action').value = 'AUDIO'
+ try:
+ obj.trigger
+ except AttributeError:
+ obj.add('trigger').value = datetime.timedelta(0)
+
+
+ @classmethod
+ def validate(cls, obj, raiseException, *args):
+ """
+ #TODO
+ audioprop = 2*(
+
+ ; 'action' and 'trigger' are both REQUIRED,
+ ; but MUST NOT occur more than once
+
+ action / trigger /
+
+ ; 'duration' and 'repeat' are both optional,
+ ; and MUST NOT occur more than once each,
+ ; but if one occurs, so MUST the other
+
+ duration / repeat /
+
+ ; the following is optional,
+ ; but MUST NOT occur more than once
+
+ attach /
+
+ dispprop = 3*(
+
+ ; the following are all REQUIRED,
+ ; but MUST NOT occur more than once
+
+ action / description / trigger /
+
+ ; 'duration' and 'repeat' are both optional,
+ ; and MUST NOT occur more than once each,
+ ; but if one occurs, so MUST the other
+
+ duration / repeat /
+
+ emailprop = 5*(
+
+ ; the following are all REQUIRED,
+ ; but MUST NOT occur more than once
+
+ action / description / trigger / summary
+
+ ; the following is REQUIRED,
+ ; and MAY occur more than once
+
+ attendee /
+
+ ; 'duration' and 'repeat' are both optional,
+ ; and MUST NOT occur more than once each,
+ ; but if one occurs, so MUST the other
+
+ duration / repeat /
+
+ procprop = 3*(
+
+ ; the following are all REQUIRED,
+ ; but MUST NOT occur more than once
+
+ action / attach / trigger /
+
+ ; 'duration' and 'repeat' are both optional,
+ ; and MUST NOT occur more than once each,
+ ; but if one occurs, so MUST the other
+
+ duration / repeat /
+
+ ; 'description' is optional,
+ ; and MUST NOT occur more than once
+
+ description /
+ if obj.contents.has_key('dtend') and obj.contents.has_key('duration'):
+ if raiseException:
+ m = "VEVENT components cannot contain both DTEND and DURATION\
+ components"
+ raise ValidateError(m)
+ return False
+ else:
+ return super(VEvent, cls).validate(obj, raiseException, *args)
+ """
+ return True
+
+registerBehavior(VAlarm)
+
+class VAvailability(VCalendarComponentBehavior):
+ """Availability state behavior.
+
+ >>> vav = newFromBehavior('VAVAILABILITY')
+ >>> vav.add('uid').value = 'test'
+ >>> vav.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc)
+ >>> vav.add('dtstart').value = datetime.datetime(2006, 2, 16, 0, tzinfo=utc)
+ >>> vav.add('dtend').value = datetime.datetime(2006, 2, 17, 0, tzinfo=utc)
+ >>> vav.add('busytype').value = "BUSY"
+ >>> av = newFromBehavior('AVAILABLE')
+ >>> av.add('uid').value = 'test1'
+ >>> av.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc)
+ >>> av.add('dtstart').value = datetime.datetime(2006, 2, 16, 9, tzinfo=utc)
+ >>> av.add('dtend').value = datetime.datetime(2006, 2, 16, 12, tzinfo=utc)
+ >>> av.add('summary').value = "Available in the morning"
+ >>> ignore = vav.add(av)
+ >>> print vav.serialize()
+ BEGIN:VAVAILABILITY
+ UID:test
+ DTSTART:20060216T000000Z
+ DTEND:20060217T000000Z
+ BEGIN:AVAILABLE
+ UID:test1
+ DTSTART:20060216T090000Z
+ DTEND:20060216T120000Z
+ DTSTAMP:20060215T000000Z
+ SUMMARY:Available in the morning
+ END:AVAILABLE
+ BUSYTYPE:BUSY
+ DTSTAMP:20060215T000000Z
+ END:VAVAILABILITY
+
+ """
+ name='VAVAILABILITY'
+ description='A component used to represent a user\'s available time slots.'
+ sortFirst = ('uid', 'dtstart', 'duration', 'dtend')
+ knownChildren = {'UID': (1, 1, None),#min, max, behaviorRegistry id
+ 'DTSTAMP': (1, 1, None),
+ 'BUSYTYPE': (0, 1, None),
+ 'CREATED': (0, 1, None),
+ 'DTSTART': (0, 1, None),
+ 'LAST-MODIFIED': (0, 1, None),
+ 'ORGANIZER': (0, 1, None),
+ 'SEQUENCE': (0, 1, None),
+ 'SUMMARY': (0, 1, None),
+ 'URL': (0, 1, None),
+ 'DTEND': (0, 1, None),
+ 'DURATION': (0, 1, None),
+ 'CATEGORIES': (0, None, None),
+ 'COMMENT': (0, None, None),
+ 'CONTACT': (0, None, None),
+ 'AVAILABLE': (0, None, None),
+ }
+
+ @classmethod
+ def validate(cls, obj, raiseException, *args):
+ if obj.contents.has_key('dtend') and obj.contents.has_key('duration'):
+ if raiseException:
+ m = "VAVAILABILITY components cannot contain both DTEND and DURATION\
+ components"
+ raise ValidateError(m)
+ return False
+ else:
+ return super(VAvailability, cls).validate(obj, raiseException, *args)
+
+registerBehavior(VAvailability)
+
+class Available(RecurringBehavior):
+ """Event behavior."""
+ name='AVAILABLE'
+ sortFirst = ('uid', 'recurrence-id', 'dtstart', 'duration', 'dtend')
+
+ description='Defines a period of time in which a user is normally available.'
+ knownChildren = {'DTSTAMP': (1, 1, None),#min, max, behaviorRegistry id
+ 'DTSTART': (1, 1, None),
+ 'UID': (1, 1, None),
+ 'DTEND': (0, 1, None), #NOTE: One of DtEnd or
+ 'DURATION': (0, 1, None), # Duration must appear, but not both
+ 'CREATED': (0, 1, None),
+ 'LAST-MODIFIED':(0, 1, None),
+ 'RECURRENCE-ID':(0, 1, None),
+ 'RRULE': (0, 1, None),
+ 'SUMMARY': (0, 1, None),
+ 'CATEGORIES': (0, None, None),
+ 'COMMENT': (0, None, None),
+ 'CONTACT': (0, None, None),
+ 'EXDATE': (0, None, None),
+ 'RDATE': (0, None, None),
+ }
+
+ @classmethod
+ def validate(cls, obj, raiseException, *args):
+ has_dtend = obj.contents.has_key('dtend')
+ has_duration = obj.contents.has_key('duration')
+ if has_dtend and has_duration:
+ if raiseException:
+ m = "AVAILABLE components cannot contain both DTEND and DURATION\
+ properties"
+ raise ValidateError(m)
+ return False
+ elif not (has_dtend or has_duration):
+ if raiseException:
+ m = "AVAILABLE components must contain one of DTEND or DURATION\
+ properties"
+ raise ValidateError(m)
+ return False
+ else:
+ return super(Available, cls).validate(obj, raiseException, *args)
+
+registerBehavior(Available)
+
+class Duration(behavior.Behavior):
+ """Behavior for Duration ContentLines. Transform to datetime.timedelta."""
+ name = 'DURATION'
+ hasNative = True
+
+ @staticmethod
+ def transformToNative(obj):
+ """Turn obj.value into a datetime.timedelta."""
+ if obj.isNative: return obj
+ obj.isNative = True
+ obj.value=str(obj.value)
+ if obj.value == '':
+ return obj
+ else:
+ deltalist=stringToDurations(obj.value)
+ #When can DURATION have multiple durations? For now:
+ if len(deltalist) == 1:
+ obj.value = deltalist[0]
+ return obj
+ else:
+ raise ParseError("DURATION must have a single duration string.")
+
+ @staticmethod
+ def transformFromNative(obj):
+ """Replace the datetime.timedelta in obj.value with an RFC2445 string.
+ """
+ if not obj.isNative: return obj
+ obj.isNative = False
+ obj.value = timedeltaToString(obj.value)
+ return obj
+
+registerBehavior(Duration)
+
+class Trigger(behavior.Behavior):
+ """DATE-TIME or DURATION"""
+ name='TRIGGER'
+ description='This property specifies when an alarm will trigger.'
+ hasNative = True
+ forceUTC = True
+
+ @staticmethod
+ def transformToNative(obj):
+ """Turn obj.value into a timedelta or datetime."""
+ if obj.isNative: return obj
+ value = getattr(obj, 'value_param', 'DURATION').upper()
+ if hasattr(obj, 'value_param'):
+ del obj.value_param
+ if obj.value == '':
+ obj.isNative = True
+ return obj
+ elif value == 'DURATION':
+ try:
+ return Duration.transformToNative(obj)
+ except ParseError:
+ logger.warn("TRIGGER not recognized as DURATION, trying "
+ "DATE-TIME, because iCal sometimes exports "
+ "DATE-TIMEs without setting VALUE=DATE-TIME")
+ try:
+ obj.isNative = False
+ dt = DateTimeBehavior.transformToNative(obj)
+ return dt
+ except:
+ msg = "TRIGGER with no VALUE not recognized as DURATION " \
+ "or as DATE-TIME"
+ raise ParseError(msg)
+ elif value == 'DATE-TIME':
+ #TRIGGERs with DATE-TIME values must be in UTC, we could validate
+ #that fact, for now we take it on faith.
+ return DateTimeBehavior.transformToNative(obj)
+ else:
+ raise ParseError("VALUE must be DURATION or DATE-TIME")
+
+ @staticmethod
+ def transformFromNative(obj):
+ if type(obj.value) == datetime.datetime:
+ obj.value_param = 'DATE-TIME'
+ return UTCDateTimeBehavior.transformFromNative(obj)
+ elif type(obj.value) == datetime.timedelta:
+ return Duration.transformFromNative(obj)
+ else:
+ raise NativeError("Native TRIGGER values must be timedelta or datetime")
+
+registerBehavior(Trigger)
+
+class PeriodBehavior(behavior.Behavior):
+ """A list of (date-time, timedelta) tuples.
+
+ >>> line = ContentLine('test', [], '', isNative=True)
+ >>> line.behavior = PeriodBehavior
+ >>> line.value = [(datetime.datetime(2006, 2, 16, 10), twoHours)]
+ >>> line.transformFromNative().value
+ '20060216T100000/PT2H'
+ >>> line.transformToNative().value
+ [(datetime.datetime(2006, 2, 16, 10, 0), datetime.timedelta(0, 7200))]
+ >>> line.value.append((datetime.datetime(2006, 5, 16, 10), twoHours))
+ >>> print line.serialize().strip()
+ TEST:20060216T100000/PT2H,20060516T100000/PT2H
+ """
+ hasNative = True
+
+ @staticmethod
+ def transformToNative(obj):
+ """Convert comma separated periods into tuples."""
+ if obj.isNative:
+ return obj
+ obj.isNative = True
+ if obj.value == '':
+ obj.value = []
+ return obj
+ tzinfo = getTzid(getattr(obj, 'tzid_param', None))
+ obj.value = [stringToPeriod(x, tzinfo) for x in obj.value.split(",")]
+ return obj
+
+ @classmethod
+ def transformFromNative(cls, obj):
+ """Convert the list of tuples in obj.value to strings."""
+ if obj.isNative:
+ obj.isNative = False
+ transformed = []
+ for tup in obj.value:
+ transformed.append(periodToString(tup, cls.forceUTC))
+ if len(transformed) > 0:
+ tzid = TimezoneComponent.registerTzinfo(tup[0].tzinfo)
+ if not cls.forceUTC and tzid is not None:
+ obj.tzid_param = tzid
+
+ obj.value = ','.join(transformed)
+
+ return obj
+
+class FreeBusy(PeriodBehavior):
+ """Free or busy period of time, must be specified in UTC."""
+ name = 'FREEBUSY'
+ forceUTC = True
+registerBehavior(FreeBusy)
+
+class RRule(behavior.Behavior):
+ """
+ Dummy behavior to avoid having RRULEs being treated as text lines (and thus
+ having semi-colons inaccurately escaped).
+ """
+registerBehavior(RRule, 'RRULE')
+registerBehavior(RRule, 'EXRULE')
+
+#------------------------ Registration of common classes -----------------------
+
+utcDateTimeList = ['LAST-MODIFIED', 'CREATED', 'COMPLETED', 'DTSTAMP']
+map(lambda x: registerBehavior(UTCDateTimeBehavior, x), utcDateTimeList)
+
+dateTimeOrDateList = ['DTEND', 'DTSTART', 'DUE', 'RECURRENCE-ID']
+map(lambda x: registerBehavior(DateOrDateTimeBehavior, x),
+ dateTimeOrDateList)
+
+registerBehavior(MultiDateBehavior, 'RDATE')
+registerBehavior(MultiDateBehavior, 'EXDATE')
+
+
+textList = ['CALSCALE', 'METHOD', 'PRODID', 'CLASS', 'COMMENT', 'DESCRIPTION',
+ 'LOCATION', 'STATUS', 'SUMMARY', 'TRANSP', 'CONTACT', 'RELATED-TO',
+ 'UID', 'ACTION', 'REQUEST-STATUS', 'TZID', 'BUSYTYPE']
+map(lambda x: registerBehavior(TextBehavior, x), textList)
+
+multiTextList = ['CATEGORIES', 'RESOURCES']
+map(lambda x: registerBehavior(MultiTextBehavior, x), multiTextList)
+
+#------------------------ Serializing helper functions -------------------------
+
+def numToDigits(num, places):
+ """Helper, for converting numbers to textual digits."""
+ s = str(num)
+ if len(s) < places:
+ return ("0" * (places - len(s))) + s
+ elif len(s) > places:
+ return s[len(s)-places: ]
+ else:
+ return s
+
+def timedeltaToString(delta):
+ """Convert timedelta to an rfc2445 DURATION."""
+ if delta.days == 0: sign = 1
+ else: sign = delta.days / abs(delta.days)
+ delta = abs(delta)
+ days = delta.days
+ hours = delta.seconds / 3600
+ minutes = (delta.seconds % 3600) / 60
+ seconds = delta.seconds % 60
+ out = ''
+ if sign == -1: out = '-'
+ out += 'P'
+ if days: out += str(days) + 'D'
+ if hours or minutes or seconds: out += 'T'
+ elif not days: #Deal with zero duration
+ out += 'T0S'
+ if hours: out += str(hours) + 'H'
+ if minutes: out += str(minutes) + 'M'
+ if seconds: out += str(seconds) + 'S'
+ return out
+
+def timeToString(dateOrDateTime):
+ """
+ Wraps dateToString and dateTimeToString, returning the results
+ of either based on the type of the argument
+ """
+ # Didn't use isinstance here as date and datetime sometimes evalutes as both
+ if (type(dateOrDateTime) == datetime.date):
+ return dateToString(dateOrDateTime)
+ elif(type(dateOrDateTime) == datetime.datetime):
+ return dateTimeToString(dateOrDateTime)
+
+
+def dateToString(date):
+ year = numToDigits( date.year, 4 )
+ month = numToDigits( date.month, 2 )
+ day = numToDigits( date.day, 2 )
+ return year + month + day
+
+def dateTimeToString(dateTime, convertToUTC=False):
+ """Ignore tzinfo unless convertToUTC. Output string."""
+ if dateTime.tzinfo and convertToUTC:
+ dateTime = dateTime.astimezone(utc)
+ if tzinfo_eq(dateTime.tzinfo, utc): utcString = "Z"
+ else: utcString = ""
+
+ year = numToDigits( dateTime.year, 4 )
+ month = numToDigits( dateTime.month, 2 )
+ day = numToDigits( dateTime.day, 2 )
+ hour = numToDigits( dateTime.hour, 2 )
+ mins = numToDigits( dateTime.minute, 2 )
+ secs = numToDigits( dateTime.second, 2 )
+
+ return year + month + day + "T" + hour + mins + secs + utcString
+
+def deltaToOffset(delta):
+ absDelta = abs(delta)
+ hours = absDelta.seconds / 3600
+ hoursString = numToDigits(hours, 2)
+ minutesString = '00'
+ if absDelta == delta:
+ signString = "+"
+ else:
+ signString = "-"
+ return signString + hoursString + minutesString
+
+def periodToString(period, convertToUTC=False):
+ txtstart = dateTimeToString(period[0], convertToUTC)
+ if isinstance(period[1], datetime.timedelta):
+ txtend = timedeltaToString(period[1])
+ else:
+ txtend = dateTimeToString(period[1], convertToUTC)
+ return txtstart + "/" + txtend
+
+#----------------------- Parsing functions -------------------------------------
+
+def isDuration(s):
+ s = string.upper(s)
+ return (string.find(s, "P") != -1) and (string.find(s, "P") < 2)
+
+def stringToDate(s):
+ year = int( s[0:4] )
+ month = int( s[4:6] )
+ day = int( s[6:8] )
+ return datetime.date(year,month,day)
+
+def stringToDateTime(s, tzinfo=None):
+ """Returns datetime.datetime object."""
+ try:
+ year = int( s[0:4] )
+ month = int( s[4:6] )
+ day = int( s[6:8] )
+ hour = int( s[9:11] )
+ minute = int( s[11:13] )
+ second = int( s[13:15] )
+ if len(s) > 15:
+ if s[15] == 'Z':
+ tzinfo = utc
+ except:
+ raise ParseError("'%s' is not a valid DATE-TIME" % s)
+ return datetime.datetime(year, month, day, hour, minute, second, 0, tzinfo)
+
+
+escapableCharList = "\\;,Nn"
+
+def stringToTextValues(s, listSeparator=',', charList=None, strict=False):
+ """Returns list of strings."""
+
+ if charList is None:
+ charList = escapableCharList
+
+ def escapableChar (c):
+ return c in charList
+
+ def error(msg):
+ if strict:
+ raise ParseError(msg)
+ else:
+ #logger.error(msg)
+ print msg
+
+ #vars which control state machine
+ charIterator = enumerate(s)
+ state = "read normal"
+
+ current = ""
+ results = []
+
+ while True:
+ try:
+ charIndex, char = charIterator.next()
+ except:
+ char = "eof"
+
+ if state == "read normal":
+ if char == '\\':
+ state = "read escaped char"
+ elif char == listSeparator:
+ state = "read normal"
+ results.append(current)
+ current = ""
+ elif char == "eof":
+ state = "end"
+ else:
+ state = "read normal"
+ current = current + char
+
+ elif state == "read escaped char":
+ if escapableChar(char):
+ state = "read normal"
+ if char in 'nN':
+ current = current + '\n'
+ else:
+ current = current + char
+ else:
+ state = "read normal"
+ # leave unrecognized escaped characters for later passes
+ current = current + '\\' + char
+
+ elif state == "end": #an end state
+ if current != "" or len(results) == 0:
+ results.append(current)
+ return results
+
+ elif state == "error": #an end state
+ return results
+
+ else:
+ state = "error"
+ error("error: unknown state: '%s' reached in %s" % (state, s))
+
+def stringToDurations(s, strict=False):
+ """Returns list of timedelta objects."""
+ def makeTimedelta(sign, week, day, hour, minute, sec):
+ if sign == "-": sign = -1
+ else: sign = 1
+ week = int(week)
+ day = int(day)
+ hour = int(hour)
+ minute = int(minute)
+ sec = int(sec)
+ return sign * datetime.timedelta(weeks=week, days=day, hours=hour, minutes=minute, seconds=sec)
+
+ def error(msg):
+ if strict:
+ raise ParseError(msg)
+ else:
+ raise ParseError(msg)
+ #logger.error(msg)
+
+ #vars which control state machine
+ charIterator = enumerate(s)
+ state = "start"
+
+ durations = []
+ current = ""
+ sign = None
+ week = 0
+ day = 0
+ hour = 0
+ minute = 0
+ sec = 0
+
+ while True:
+ try:
+ charIndex, char = charIterator.next()
+ except:
+ charIndex += 1
+ char = "eof"
+
+ if state == "start":
+ if char == '+':
+ state = "start"
+ sign = char
+ elif char == '-':
+ state = "start"
+ sign = char
+ elif char.upper() == 'P':
+ state = "read field"
+ elif char == "eof":
+ state = "error"
+ error("got end-of-line while reading in duration: " + s)
+ elif char in string.digits:
+ state = "read field"
+ current = current + char #update this part when updating "read field"
+ else:
+ state = "error"
+ print "got unexpected character %s reading in duration: %s" % (char, s)
+ error("got unexpected character %s reading in duration: %s" % (char, s))
+
+ elif state == "read field":
+ if (char in string.digits):
+ state = "read field"
+ current = current + char #update part above when updating "read field"
+ elif char.upper() == 'T':
+ state = "read field"
+ elif char.upper() == 'W':
+ state = "read field"
+ week = current
+ current = ""
+ elif char.upper() == 'D':
+ state = "read field"
+ day = current
+ current = ""
+ elif char.upper() == 'H':
+ state = "read field"
+ hour = current
+ current = ""
+ elif char.upper() == 'M':
+ state = "read field"
+ minute = current
+ current = ""
+ elif char.upper() == 'S':
+ state = "read field"
+ sec = current
+ current = ""
+ elif char == ",":
+ state = "start"
+ durations.append( makeTimedelta(sign, week, day, hour, minute, sec) )
+ current = ""
+ sign = None
+ week = None
+ day = None
+ hour = None
+ minute = None
+ sec = None
+ elif char == "eof":
+ state = "end"
+ else:
+ state = "error"
+ error("got unexpected character reading in duration: " + s)
+
+ elif state == "end": #an end state
+ #print "stuff: %s, durations: %s" % ([current, sign, week, day, hour, minute, sec], durations)
+
+ if (sign or week or day or hour or minute or sec):
+ durations.append( makeTimedelta(sign, week, day, hour, minute, sec) )
+ return durations
+
+ elif state == "error": #an end state
+ error("in error state")
+ return durations
+
+ else:
+ state = "error"
+ error("error: unknown state: '%s' reached in %s" % (state, s))
+
+def parseDtstart(contentline, allowSignatureMismatch=False):
+ """Convert a contentline's value into a date or date-time.
+
+ A variety of clients don't serialize dates with the appropriate VALUE
+ parameter, so rather than failing on these (technically invalid) lines,
+ if allowSignatureMismatch is True, try to parse both varieties.
+
+ """
+ tzinfo = getTzid(getattr(contentline, 'tzid_param', None))
+ valueParam = getattr(contentline, 'value_param', 'DATE-TIME').upper()
+ if valueParam == "DATE":
+ return stringToDate(contentline.value)
+ elif valueParam == "DATE-TIME":
+ try:
+ return stringToDateTime(contentline.value, tzinfo)
+ except:
+ if allowSignatureMismatch:
+ return stringToDate(contentline.value)
+ else:
+ raise
+
+def stringToPeriod(s, tzinfo=None):
+ values = string.split(s, "/")
+ start = stringToDateTime(values[0], tzinfo)
+ valEnd = values[1]
+ if isDuration(valEnd): #period-start = date-time "/" dur-value
+ delta = stringToDurations(valEnd)[0]
+ return (start, delta)
+ else:
+ return (start, stringToDateTime(valEnd, tzinfo))
+
+
+def getTransition(transitionTo, year, tzinfo):
+ """Return the datetime of the transition to/from DST, or None."""
+
+ def firstTransition(iterDates, test):
+ """
+ Return the last date not matching test, or None if all tests matched.
+ """
+ success = None
+ for dt in iterDates:
+ if not test(dt):
+ success = dt
+ else:
+ if success is not None:
+ return success
+ return success # may be None
+
+ def generateDates(year, month=None, day=None):
+ """Iterate over possible dates with unspecified values."""
+ months = range(1, 13)
+ days = range(1, 32)
+ hours = range(0, 24)
+ if month is None:
+ for month in months:
+ yield datetime.datetime(year, month, 1)
+ elif day is None:
+ for day in days:
+ try:
+ yield datetime.datetime(year, month, day)
+ except ValueError:
+ pass
+ else:
+ for hour in hours:
+ yield datetime.datetime(year, month, day, hour)
+
+ assert transitionTo in ('daylight', 'standard')
+ if transitionTo == 'daylight':
+ def test(dt): return tzinfo.dst(dt) != zeroDelta
+ elif transitionTo == 'standard':
+ def test(dt): return tzinfo.dst(dt) == zeroDelta
+ newyear = datetime.datetime(year, 1, 1)
+ monthDt = firstTransition(generateDates(year), test)
+ if monthDt is None:
+ return newyear
+ elif monthDt.month == 12:
+ return None
+ else:
+ # there was a good transition somewhere in a non-December month
+ month = monthDt.month
+ day = firstTransition(generateDates(year, month), test).day
+ uncorrected = firstTransition(generateDates(year, month, day), test)
+ if transitionTo == 'standard':
+ # assuming tzinfo.dst returns a new offset for the first
+ # possible hour, we need to add one hour for the offset change
+ # and another hour because firstTransition returns the hour
+ # before the transition
+ return uncorrected + datetime.timedelta(hours=2)
+ else:
+ return uncorrected + datetime.timedelta(hours=1)
+
+def tzinfo_eq(tzinfo1, tzinfo2, startYear = 2000, endYear=2020):
+ """Compare offsets and DST transitions from startYear to endYear."""
+ if tzinfo1 == tzinfo2:
+ return True
+ elif tzinfo1 is None or tzinfo2 is None:
+ return False
+
+ def dt_test(dt):
+ if dt is None:
+ return True
+ return tzinfo1.utcoffset(dt) == tzinfo2.utcoffset(dt)
+
+ if not dt_test(datetime.datetime(startYear, 1, 1)):
+ return False
+ for year in xrange(startYear, endYear):
+ for transitionTo in 'daylight', 'standard':
+ t1=getTransition(transitionTo, year, tzinfo1)
+ t2=getTransition(transitionTo, year, tzinfo2)
+ if t1 != t2 or not dt_test(t1):
+ return False
+ return True
+
+
+#------------------- Testing and running functions -----------------------------
+if __name__ == '__main__':
+ import tests
+ tests._test()
diff --git a/vobject/ics_diff.py b/vobject/ics_diff.py
new file mode 100644
index 0000000..4aaaef9
--- /dev/null
+++ b/vobject/ics_diff.py
@@ -0,0 +1,219 @@
+"""Compare VTODOs and VEVENTs in two iCalendar sources."""
+from base import Component, getBehavior, newFromBehavior
+
+def getSortKey(component):
+ def getUID(component):
+ return component.getChildValue('uid', '')
+
+ # it's not quite as simple as getUID, need to account for recurrenceID and
+ # sequence
+
+ def getSequence(component):
+ sequence = component.getChildValue('sequence', 0)
+ return "%05d" % int(sequence)
+
+ def getRecurrenceID(component):
+ recurrence_id = component.getChildValue('recurrence_id', None)
+ if recurrence_id is None:
+ return '0000-00-00'
+ else:
+ return recurrence_id.isoformat()
+
+ return getUID(component) + getSequence(component) + getRecurrenceID(component)
+
+def sortByUID(components):
+ return sorted(components, key=getSortKey)
+
+def deleteExtraneous(component, ignore_dtstamp=False):
+ """
+ Recursively walk the component's children, deleting extraneous details like
+ X-VOBJ-ORIGINAL-TZID.
+ """
+ for comp in component.components():
+ deleteExtraneous(comp, ignore_dtstamp)
+ for line in component.lines():
+ if line.params.has_key('X-VOBJ-ORIGINAL-TZID'):
+ del line.params['X-VOBJ-ORIGINAL-TZID']
+ if ignore_dtstamp and hasattr(component, 'dtstamp_list'):
+ del component.dtstamp_list
+
+def diff(left, right):
+ """
+ Take two VCALENDAR components, compare VEVENTs and VTODOs in them,
+ return a list of object pairs containing just UID and the bits
+ that didn't match, using None for objects that weren't present in one
+ version or the other.
+
+ When there are multiple ContentLines in one VEVENT, for instance many
+ DESCRIPTION lines, such lines original order is assumed to be
+ meaningful. Order is also preserved when comparing (the unlikely case
+ of) multiple parameters of the same type in a ContentLine
+
+ """
+
+ def processComponentLists(leftList, rightList):
+ output = []
+ rightIndex = 0
+ rightListSize = len(rightList)
+
+ for comp in leftList:
+ if rightIndex >= rightListSize:
+ output.append((comp, None))
+ else:
+ leftKey = getSortKey(comp)
+ rightComp = rightList[rightIndex]
+ rightKey = getSortKey(rightComp)
+ while leftKey > rightKey:
+ output.append((None, rightComp))
+ rightIndex += 1
+ if rightIndex >= rightListSize:
+ output.append((comp, None))
+ break
+ else:
+ rightComp = rightList[rightIndex]
+ rightKey = getSortKey(rightComp)
+
+ if leftKey < rightKey:
+ output.append((comp, None))
+ elif leftKey == rightKey:
+ rightIndex += 1
+ matchResult = processComponentPair(comp, rightComp)
+ if matchResult is not None:
+ output.append(matchResult)
+
+ return output
+
+ def newComponent(name, body):
+ if body is None:
+ return None
+ else:
+ c = Component(name)
+ c.behavior = getBehavior(name)
+ c.isNative = True
+ return c
+
+ def processComponentPair(leftComp, rightComp):
+ """
+ Return None if a match, or a pair of components including UIDs and
+ any differing children.
+
+ """
+ leftChildKeys = leftComp.contents.keys()
+ rightChildKeys = rightComp.contents.keys()
+
+ differentContentLines = []
+ differentComponents = {}
+
+ for key in leftChildKeys:
+ rightList = rightComp.contents.get(key, [])
+ if isinstance(leftComp.contents[key][0], Component):
+ compDifference = processComponentLists(leftComp.contents[key],
+ rightList)
+ if len(compDifference) > 0:
+ differentComponents[key] = compDifference
+
+ elif leftComp.contents[key] != rightList:
+ differentContentLines.append((leftComp.contents[key],
+ rightList))
+
+ for key in rightChildKeys:
+ if key not in leftChildKeys:
+ if isinstance(rightComp.contents[key][0], Component):
+ differentComponents[key] = ([], rightComp.contents[key])
+ else:
+ differentContentLines.append(([], rightComp.contents[key]))
+
+ if len(differentContentLines) == 0 and len(differentComponents) == 0:
+ return None
+ else:
+ left = newFromBehavior(leftComp.name)
+ right = newFromBehavior(leftComp.name)
+ # add a UID, if one existed, despite the fact that they'll always be
+ # the same
+ uid = leftComp.getChildValue('uid')
+ if uid is not None:
+ left.add( 'uid').value = uid
+ right.add('uid').value = uid
+
+ for name, childPairList in differentComponents.iteritems():
+ leftComponents, rightComponents = zip(*childPairList)
+ if len(leftComponents) > 0:
+ # filter out None
+ left.contents[name] = filter(None, leftComponents)
+ if len(rightComponents) > 0:
+ # filter out None
+ right.contents[name] = filter(None, rightComponents)
+
+ for leftChildLine, rightChildLine in differentContentLines:
+ nonEmpty = leftChildLine or rightChildLine
+ name = nonEmpty[0].name
+ if leftChildLine is not None:
+ left.contents[name] = leftChildLine
+ if rightChildLine is not None:
+ right.contents[name] = rightChildLine
+
+ return left, right
+
+
+ vevents = processComponentLists(sortByUID(getattr(left, 'vevent_list', [])),
+ sortByUID(getattr(right, 'vevent_list', [])))
+
+ vtodos = processComponentLists(sortByUID(getattr(left, 'vtodo_list', [])),
+ sortByUID(getattr(right, 'vtodo_list', [])))
+
+ return vevents + vtodos
+
+def prettyDiff(leftObj, rightObj):
+ for left, right in diff(leftObj, rightObj):
+ print "<<<<<<<<<<<<<<<"
+ if left is not None:
+ left.prettyPrint()
+ print "==============="
+ if right is not None:
+ right.prettyPrint()
+ print ">>>>>>>>>>>>>>>"
+ print
+
+
+from optparse import OptionParser
+import icalendar, base
+import os
+import codecs
+
+def main():
+ options, args = getOptions()
+ if args:
+ ignore_dtstamp = options.ignore
+ ics_file1, ics_file2 = args
+ cal1 = base.readOne(file(ics_file1))
+ cal2 = base.readOne(file(ics_file2))
+ deleteExtraneous(cal1, ignore_dtstamp=ignore_dtstamp)
+ deleteExtraneous(cal2, ignore_dtstamp=ignore_dtstamp)
+ prettyDiff(cal1, cal2)
+
+version = "0.1"
+
+def getOptions():
+ ##### Configuration options #####
+
+ usage = "usage: %prog [options] ics_file1 ics_file2"
+ parser = OptionParser(usage=usage, version=version)
+ parser.set_description("ics_diff will print a comparison of two iCalendar files ")
+
+ parser.add_option("-i", "--ignore-dtstamp", dest="ignore", action="store_true",
+ default=False, help="ignore DTSTAMP lines [default: False]")
+
+ (cmdline_options, args) = parser.parse_args()
+ if len(args) < 2:
+ print "error: too few arguments given"
+ print
+ print parser.format_help()
+ return False, False
+
+ return cmdline_options, args
+
+if __name__ == "__main__":
+ try:
+ main()
+ except KeyboardInterrupt:
+ print "Aborted"
diff --git a/vobject/vcard.py b/vobject/vcard.py
new file mode 100644
index 0000000..01d1d42
--- /dev/null
+++ b/vobject/vcard.py
@@ -0,0 +1,289 @@
+"""Definitions and behavior for vCard 3.0"""
+
+import behavior
+import itertools
+
+from base import VObjectError, NativeError, ValidateError, ParseError, \
+ VBase, Component, ContentLine, logger, defaultSerialize, \
+ registerBehavior, backslashEscape, ascii
+from icalendar import stringToTextValues
+
+#------------------------ vCard structs ----------------------------------------
+
+class Name(object):
+ def __init__(self, family = '', given = '', additional = '', prefix = '',
+ suffix = ''):
+ """Each name attribute can be a string or a list of strings."""
+ self.family = family
+ self.given = given
+ self.additional = additional
+ self.prefix = prefix
+ self.suffix = suffix
+
+ @staticmethod
+ def toString(val):
+ """Turn a string or array value into a string."""
+ if type(val) in (list, tuple):
+ return ' '.join(val)
+ return val
+
+ def __str__(self):
+ eng_order = ('prefix', 'given', 'additional', 'family', 'suffix')
+ out = ' '.join(self.toString(getattr(self, val)) for val in eng_order)
+ return ascii(out)
+
+ def __repr__(self):
+ return "<Name: %s>" % self.__str__()
+
+ def __eq__(self, other):
+ try:
+ return (self.family == other.family and
+ self.given == other.given and
+ self.additional == other.additional and
+ self.prefix == other.prefix and
+ self.suffix == other.suffix)
+ except:
+ return False
+
+class Address(object):
+ def __init__(self, street = '', city = '', region = '', code = '',
+ country = '', box = '', extended = ''):
+ """Each name attribute can be a string or a list of strings."""
+ self.box = box
+ self.extended = extended
+ self.street = street
+ self.city = city
+ self.region = region
+ self.code = code
+ self.country = country
+
+ @staticmethod
+ def toString(val, join_char='\n'):
+ """Turn a string or array value into a string."""
+ if type(val) in (list, tuple):
+ return join_char.join(val)
+ return val
+
+ lines = ('box', 'extended', 'street')
+ one_line = ('city', 'region', 'code')
+
+ def __str__(self):
+ lines = '\n'.join(self.toString(getattr(self, val)) for val in self.lines if getattr(self, val))
+ one_line = tuple(self.toString(getattr(self, val), ' ') for val in self.one_line)
+ lines += "\n%s, %s %s" % one_line
+ if self.country:
+ lines += '\n' + self.toString(self.country)
+ return ascii(lines)
+
+ def __repr__(self):
+ return "<Address: %s>" % repr(str(self))[1:-1]
+
+ def __eq__(self, other):
+ try:
+ return (self.box == other.box and
+ self.extended == other.extended and
+ self.street == other.street and
+ self.city == other.city and
+ self.region == other.region and
+ self.code == other.code and
+ self.country == other.country)
+ except:
+ False
+
+
+#------------------------ Registered Behavior subclasses -----------------------
+
+class VCardTextBehavior(behavior.Behavior):
+ """Provide backslash escape encoding/decoding for single valued properties.
+
+ TextBehavior also deals with base64 encoding if the ENCODING parameter is
+ explicitly set to BASE64.
+
+ """
+ allowGroup = True
+ base64string = 'B'
+
+ @classmethod
+ def decode(cls, line):
+ """Remove backslash escaping from line.valueDecode line, either to remove
+ backslash espacing, or to decode base64 encoding. The content line should
+ contain a ENCODING=b for base64 encoding, but Apple Addressbook seems to
+ export a singleton parameter of 'BASE64', which does not match the 3.0
+ vCard spec. If we encouter that, then we transform the parameter to
+ ENCODING=b"""
+ if line.encoded:
+ if 'BASE64' in line.singletonparams:
+ line.singletonparams.remove('BASE64')
+ line.encoding_param = cls.base64string
+ encoding = getattr(line, 'encoding_param', None)
+ if encoding:
+ line.value = line.value.decode('base64')
+ else:
+ line.value = stringToTextValues(line.value)[0]
+ line.encoded=False
+
+ @classmethod
+ def encode(cls, line):
+ """Backslash escape line.value."""
+ if not line.encoded:
+ encoding = getattr(line, 'encoding_param', None)
+ if encoding and encoding.upper() == cls.base64string:
+ line.value = line.value.encode('base64').replace('\n', '')
+ else:
+ line.value = backslashEscape(line.value)
+ line.encoded=True
+
+
+class VCardBehavior(behavior.Behavior):
+ allowGroup = True
+ defaultBehavior = VCardTextBehavior
+
+class VCard3_0(VCardBehavior):
+ """vCard 3.0 behavior."""
+ name = 'VCARD'
+ description = 'vCard 3.0, defined in rfc2426'
+ versionString = '3.0'
+ isComponent = True
+ sortFirst = ('version', 'prodid', 'uid')
+ knownChildren = {'N': (1, 1, None),#min, max, behaviorRegistry id
+ 'FN': (1, 1, None),
+ 'VERSION': (1, 1, None),#required, auto-generated
+ 'PRODID': (0, 1, None),
+ 'LABEL': (0, None, None),
+ 'UID': (0, None, None),
+ 'ADR': (0, None, None),
+ 'ORG': (0, None, None),
+ 'PHOTO': (0, None, None),
+ 'CATEGORIES':(0, None, None)
+ }
+
+ @classmethod
+ def generateImplicitParameters(cls, obj):
+ """Create PRODID, VERSION, and VTIMEZONEs if needed.
+
+ VTIMEZONEs will need to exist whenever TZID parameters exist or when
+ datetimes with tzinfo exist.
+
+ """
+ if not hasattr(obj, 'version'):
+ obj.add(ContentLine('VERSION', [], cls.versionString))
+registerBehavior(VCard3_0, default=True)
+
+class FN(VCardTextBehavior):
+ name = "FN"
+ description = 'Formatted name'
+registerBehavior(FN)
+
+class Label(VCardTextBehavior):
+ name = "Label"
+ description = 'Formatted address'
+registerBehavior(FN)
+
+class Photo(VCardTextBehavior):
+ name = "Photo"
+ description = 'Photograph'
+ @classmethod
+ def valueRepr( cls, line ):
+ return " (BINARY PHOTO DATA at 0x%s) " % id( line.value )
+
+registerBehavior(Photo)
+
+def toListOrString(string):
+ stringList = stringToTextValues(string)
+ if len(stringList) == 1:
+ return stringList[0]
+ else:
+ return stringList
+
+def splitFields(string):
+ """Return a list of strings or lists from a Name or Address."""
+ return [toListOrString(i) for i in
+ stringToTextValues(string, listSeparator=';', charList=';')]
+
+def toList(stringOrList):
+ if isinstance(stringOrList, basestring):
+ return [stringOrList]
+ return stringOrList
+
+def serializeFields(obj, order=None):
+ """Turn an object's fields into a ';' and ',' seperated string.
+
+ If order is None, obj should be a list, backslash escape each field and
+ return a ';' separated string.
+ """
+ fields = []
+ if order is None:
+ fields = [backslashEscape(val) for val in obj]
+ else:
+ for field in order:
+ escapedValueList = [backslashEscape(val) for val in
+ toList(getattr(obj, field))]
+ fields.append(','.join(escapedValueList))
+ return ';'.join(fields)
+
+NAME_ORDER = ('family', 'given', 'additional', 'prefix', 'suffix')
+
+class NameBehavior(VCardBehavior):
+ """A structured name."""
+ hasNative = True
+
+ @staticmethod
+ def transformToNative(obj):
+ """Turn obj.value into a Name."""
+ if obj.isNative: return obj
+ obj.isNative = True
+ obj.value = Name(**dict(zip(NAME_ORDER, splitFields(obj.value))))
+ return obj
+
+ @staticmethod
+ def transformFromNative(obj):
+ """Replace the Name in obj.value with a string."""
+ obj.isNative = False
+ obj.value = serializeFields(obj.value, NAME_ORDER)
+ return obj
+registerBehavior(NameBehavior, 'N')
+
+ADDRESS_ORDER = ('box', 'extended', 'street', 'city', 'region', 'code',
+ 'country')
+
+class AddressBehavior(VCardBehavior):
+ """A structured address."""
+ hasNative = True
+
+ @staticmethod
+ def transformToNative(obj):
+ """Turn obj.value into an Address."""
+ if obj.isNative: return obj
+ obj.isNative = True
+ obj.value = Address(**dict(zip(ADDRESS_ORDER, splitFields(obj.value))))
+ return obj
+
+ @staticmethod
+ def transformFromNative(obj):
+ """Replace the Address in obj.value with a string."""
+ obj.isNative = False
+ obj.value = serializeFields(obj.value, ADDRESS_ORDER)
+ return obj
+registerBehavior(AddressBehavior, 'ADR')
+
+class OrgBehavior(VCardBehavior):
+ """A list of organization values and sub-organization values."""
+ hasNative = True
+
+ @staticmethod
+ def transformToNative(obj):
+ """Turn obj.value into a list."""
+ if obj.isNative: return obj
+ obj.isNative = True
+ obj.value = splitFields(obj.value)
+ return obj
+
+ @staticmethod
+ def transformFromNative(obj):
+ """Replace the list in obj.value with a string."""
+ if not obj.isNative: return obj
+ obj.isNative = False
+ obj.value = serializeFields(obj.value)
+ return obj
+registerBehavior(OrgBehavior, 'ORG')
+
diff --git a/vobject/win32tz.py b/vobject/win32tz.py
new file mode 100644
index 0000000..35f997b
--- /dev/null
+++ b/vobject/win32tz.py
@@ -0,0 +1,156 @@
+import _winreg
+import struct
+import datetime
+
+handle=_winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
+tzparent=_winreg.OpenKey(handle,
+ "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones")
+parentsize=_winreg.QueryInfoKey(tzparent)[0]
+
+localkey=_winreg.OpenKey(handle,
+ "SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation")
+WEEKS=datetime.timedelta(7)
+
+def list_timezones():
+ """Return a list of all time zones known to the system."""
+ l=[]
+ for i in xrange(parentsize):
+ l.append(_winreg.EnumKey(tzparent, i))
+ return l
+
+class win32tz(datetime.tzinfo):
+ """tzinfo class based on win32's timezones available in the registry.
+
+ >>> local = win32tz('Central Standard Time')
+ >>> oct1 = datetime.datetime(month=10, year=2004, day=1, tzinfo=local)
+ >>> dec1 = datetime.datetime(month=12, year=2004, day=1, tzinfo=local)
+ >>> oct1.dst()
+ datetime.timedelta(0, 3600)
+ >>> dec1.dst()
+ datetime.timedelta(0)
+ >>> braz = win32tz('E. South America Standard Time')
+ >>> braz.dst(oct1)
+ datetime.timedelta(0)
+ >>> braz.dst(dec1)
+ datetime.timedelta(0, 3600)
+
+ """
+ def __init__(self, name):
+ self.data=win32tz_data(name)
+
+ def utcoffset(self, dt):
+ if self._isdst(dt):
+ return datetime.timedelta(minutes=self.data.dstoffset)
+ else:
+ return datetime.timedelta(minutes=self.data.stdoffset)
+
+ def dst(self, dt):
+ if self._isdst(dt):
+ minutes = self.data.dstoffset - self.data.stdoffset
+ return datetime.timedelta(minutes=minutes)
+ else:
+ return datetime.timedelta(0)
+
+ def tzname(self, dt):
+ if self._isdst(dt): return self.data.dstname
+ else: return self.data.stdname
+
+ def _isdst(self, dt):
+ dat=self.data
+ dston = pickNthWeekday(dt.year, dat.dstmonth, dat.dstdayofweek,
+ dat.dsthour, dat.dstminute, dat.dstweeknumber)
+ dstoff = pickNthWeekday(dt.year, dat.stdmonth, dat.stddayofweek,
+ dat.stdhour, dat.stdminute, dat.stdweeknumber)
+ if dston < dstoff:
+ if dston <= dt.replace(tzinfo=None) < dstoff: return True
+ else: return False
+ else:
+ if dstoff <= dt.replace(tzinfo=None) < dston: return False
+ else: return True
+
+ def __repr__(self):
+ return "<win32tz - %s>" % self.data.display
+
+def pickNthWeekday(year, month, dayofweek, hour, minute, whichweek):
+ """dayofweek == 0 means Sunday, whichweek > 4 means last instance"""
+ first = datetime.datetime(year=year, month=month, hour=hour, minute=minute,
+ day=1)
+ weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7 + 1))
+ for n in xrange(whichweek - 1, -1, -1):
+ dt=weekdayone + n * WEEKS
+ if dt.month == month: return dt
+
+
+class win32tz_data(object):
+ """Read a registry key for a timezone, expose its contents."""
+
+ def __init__(self, path):
+ """Load path, or if path is empty, load local time."""
+ if path:
+ keydict=valuesToDict(_winreg.OpenKey(tzparent, path))
+ self.display = keydict['Display']
+ self.dstname = keydict['Dlt']
+ self.stdname = keydict['Std']
+
+ #see http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
+ tup = struct.unpack('=3l16h', keydict['TZI'])
+ self.stdoffset = -tup[0]-tup[1] #Bias + StandardBias * -1
+ self.dstoffset = self.stdoffset - tup[2] # + DaylightBias * -1
+
+ offset=3
+ self.stdmonth = tup[1 + offset]
+ self.stddayofweek = tup[2 + offset] #Sunday=0
+ self.stdweeknumber = tup[3 + offset] #Last = 5
+ self.stdhour = tup[4 + offset]
+ self.stdminute = tup[5 + offset]
+
+ offset=11
+ self.dstmonth = tup[1 + offset]
+ self.dstdayofweek = tup[2 + offset] #Sunday=0
+ self.dstweeknumber = tup[3 + offset] #Last = 5
+ self.dsthour = tup[4 + offset]
+ self.dstminute = tup[5 + offset]
+
+ else:
+ keydict=valuesToDict(localkey)
+
+ self.stdname = keydict['StandardName']
+ self.dstname = keydict['DaylightName']
+
+ sourcekey=_winreg.OpenKey(tzparent, self.stdname)
+ self.display = valuesToDict(sourcekey)['Display']
+
+ self.stdoffset = -keydict['Bias']-keydict['StandardBias']
+ self.dstoffset = self.stdoffset - keydict['DaylightBias']
+
+ #see http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
+ tup = struct.unpack('=8h', keydict['StandardStart'])
+
+ offset=0
+ self.stdmonth = tup[1 + offset]
+ self.stddayofweek = tup[2 + offset] #Sunday=0
+ self.stdweeknumber = tup[3 + offset] #Last = 5
+ self.stdhour = tup[4 + offset]
+ self.stdminute = tup[5 + offset]
+
+ tup = struct.unpack('=8h', keydict['DaylightStart'])
+ self.dstmonth = tup[1 + offset]
+ self.dstdayofweek = tup[2 + offset] #Sunday=0
+ self.dstweeknumber = tup[3 + offset] #Last = 5
+ self.dsthour = tup[4 + offset]
+ self.dstminute = tup[5 + offset]
+
+def valuesToDict(key):
+ """Convert a registry key's values to a dictionary."""
+ dict={}
+ size=_winreg.QueryInfoKey(key)[1]
+ for i in xrange(size):
+ dict[_winreg.EnumValue(key, i)[0]]=_winreg.EnumValue(key, i)[1]
+ return dict
+
+def _test():
+ import win32tz, doctest
+ doctest.testmod(win32tz, verbose=0)
+
+if __name__ == '__main__':
+ _test() \ No newline at end of file