summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGuido Guenther <agx@sigxcpu.org>2006-09-09 17:15:23 +0200
committerGuido Guenther <agx@bogon.sigxcpu.org>2006-09-09 17:15:23 +0200
commit0f963fa4c3d05e0f8b297aeb20aa5d719f7ab31d (patch)
tree8a580ddfbfbab89549d8e086f175fd0284652421
Import upstream version 0.0.svn147
-rw-r--r--ACKNOWLEDGEMENTS.txt4
-rw-r--r--LICENSE.txt57
-rw-r--r--README.txt227
-rw-r--r--ez_setup.py221
-rwxr-xr-xsetup.py53
-rw-r--r--src/vobject/__init__.py86
-rw-r--r--src/vobject/base.py978
-rw-r--r--src/vobject/behavior.py161
-rw-r--r--src/vobject/hcalendar.py125
-rw-r--r--src/vobject/icalendar.py1696
-rw-r--r--src/vobject/vcard.py225
-rw-r--r--src/vobject/win32tz.py156
-rw-r--r--tests/recurrence.ics30
-rw-r--r--tests/tests.py711
-rw-r--r--tests/utf8_test.ics39
15 files changed, 4769 insertions, 0 deletions
diff --git a/ACKNOWLEDGEMENTS.txt b/ACKNOWLEDGEMENTS.txt
new file mode 100644
index 0000000..100138a
--- /dev/null
+++ b/ACKNOWLEDGEMENTS.txt
@@ -0,0 +1,4 @@
+Enormous thanks to:
+Gustavo Niemeyer, for all his work on dateutil
+Dave Cridland, for helping talk about vobject and working on vcard
+TJ Gabbour, for putting his heart into parsing
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..bbd7e18
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,57 @@
+/* ====================================================================
+ * The Apache Software License, Version 1.1
+ *
+ * Copyright (c) 2004-2006 Jeffrey Harris. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * 3. The end-user documentation included with the redistribution,
+ * if any, must include the following acknowledgment:
+ * "This product includes software developed by the
+ * Apache Software Foundation (http://www.apache.org/)."
+ * Alternately, this acknowledgment may appear in the software itself,
+ * if and wherever such third-party acknowledgments normally appear.
+ *
+ * 4. The names "Apache" and "Apache Software Foundation" must
+ * not be used to endorse or promote products derived from this
+ * software without prior written permission. For written
+ * permission, please contact apache@apache.org.
+ *
+ * 5. Products derived from this software may not be called "Apache",
+ * nor may "Apache" appear in their name, without prior written
+ * permission of the Apache Software Foundation.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
+ * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ * Portions of this software are based upon public domain software
+ * originally written at the National Center for Supercomputing Applications,
+ * University of Illinois, Urbana-Champaign.
+ */
+
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..bc94dae
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,227 @@
+=======
+VObject
+=======
+
+VObject simplifies the process of parsing and creating iCalendar and
+vCard objects.
+
+--------------
+ Installation
+--------------
+
+To install vobject, run::
+
+ python setup.py install
+
+vobject requires the dateutil package, which can be installed via
+easy_install or downloaded from http://labix.org/python-dateutil
+
+---------------
+ Running tests
+---------------
+
+Unit tests live in doctests throughout the source code, to run all tests, use::
+
+ python tests/tests.py
+
+-------
+ Usage
+-------
+
+Creating iCalendar objects
+..........................
+
+vobject has a basic datastructure for working with iCalendar-like
+syntaxes. Additionally, it defines specialized behaviors for many of
+the commonly used iCalendar objects.
+
+To create an object that already has a behavior defined, run:
+
+>>> import vobject
+>>> cal = vobject.newFromBehavior('vcalendar')
+>>> cal.behavior
+<class 'vobject.icalendar.VCalendar2_0'>
+
+Convenience functions exist to create iCalendar and vCard objects:
+
+>>> cal = vobject.iCalendar()
+>>> cal.behavior
+<class 'vobject.icalendar.VCalendar2_0'>
+>>> card = vobject.vCard()
+>>> card.behavior
+<class 'vobject.vcard.VCard3_0'>
+
+Once you have an object, you can use the add method to create
+children:
+
+>>> cal.add('vevent')
+<VEVENT| []>
+>>> cal.vevent.add('summary').value = "This is a note"
+>>> cal.prettyPrint()
+ VCALENDAR
+ VEVENT
+ SUMMARY: This is a note
+
+Note that summary is a little different from vevent, it's a
+ContentLine, not a Component. It can't have children, and it has a
+special value attribute.
+
+ContentLines can also have parameters. They can be accessed with
+regular attribute names with _param appended:
+
+>>> cal.vevent.summary.x_random_param = 'Random parameter'
+>>> cal.prettyPrint()
+ VCALENDAR
+ VEVENT
+ SUMMARY: This is a note
+ params for SUMMARY:
+ X-RANDOM ['Random parameter']
+
+There are a few things to note about this example
+
+ * The underscore in x_random is converted to a dash (dashes are
+ legal in iCalendar, underscores legal in Python)
+ * X-RANDOM's value is a list.
+
+If you want to access the full list of parameters, not just the first,
+use <paramname>_paramlist:
+
+>>> cal.vevent.summary.x_random_paramlist
+['Random parameter']
+>>> cal.vevent.summary.x_random_paramlist.append('Other param')
+>>> cal.vevent.summary
+<SUMMARY{'X-RANDOM': ['Random parameter', 'Other param']}This is a note>
+
+Similar to parameters, If you want to access more than just the first
+child of a Component, you can access the full list of children of a
+given name by appending _list to the attribute name:
+
+>>> cal.add('vevent').add('summary').value = "Second VEVENT"
+>>> for ev in cal.vevent_list:
+... print ev.summary.value
+This is a note
+Second VEVENT
+
+The interaction between the del operator and the hiding of the
+underlying list is a little tricky, del cal.vevent and del
+cal.vevent_list both delete all vevent children:
+
+>>> first_ev = cal.vevent
+>>> del cal.vevent
+>>> cal
+<VCALENDAR| []>
+>>> cal.vevent = first_ev
+
+vobject understands Python's datetime module and tzinfo classes.
+
+>>> import datetime
+>>> utc = vobject.icalendar.utc
+>>> start = cal.vevent.add('dtstart')
+>>> start.value = datetime.datetime(2006, 2, 16, tzinfo = utc)
+>>> first_ev.prettyPrint()
+ VEVENT
+ DTSTART: 2006-02-16 00:00:00+00:00
+ SUMMARY: This is a note
+ params for SUMMARY:
+ X-RANDOM ['Random parameter', 'Other param']
+
+Components and ContentLines have serialize methods:
+
+>>> cal.vevent.add('uid').value = 'Sample UID'
+>>> icalstream = cal.serialize()
+>>> print icalstream
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:Sample UID
+DTSTART:20060216T000000Z
+SUMMARY;X-RANDOM=Random parameter,Other param:This is a note
+END:VEVENT
+END:VCALENDAR
+
+Observe that serializing adds missing required lines like version and
+prodid. A random UID would be generated, too, if one didn't exist.
+
+If dtstart's tzinfo had been something other than UTC, an appropriate
+vtimezone would be created for it.
+
+
+Parsing iCalendar objects
+.........................
+
+To parse one top level component from an existing iCalendar stream or
+string, use the readOne function:
+
+>>> parsedCal = vobject.readOne(icalstream)
+>>> parsedCal.vevent.dtstart.value
+datetime.datetime(2006, 2, 16, 0, 0, tzinfo=tzutc())
+
+Similarly, readComponents is a generator yielding one top level
+component at a time from a stream or string.
+
+>>> vobject.readComponents(icalstream).next().vevent.dtstart.value
+datetime.datetime(2006, 2, 16, 0, 0, tzinfo=tzutc())
+
+More examples can be found in source code doctests.
+
+vCards
+......
+
+Making vCards proceeds in much the same way. Note that the 'N' and 'FN'
+attributes are required.
+
+>>> j = vobject.vCard()
+>>> j.add('n')
+ <N{} >
+>>> j.n.value = vobject.vcard.Name( family='Harris', given='Jeffrey' )
+>>> j.add('fn')
+ <FN{}>
+>>> j.fn.value ='Jeffrey Harris'
+>>> j.add('email')
+ <EMAIL{}>
+>>> j.email.value = 'jeffrey@osafoundation.org'
+>>> j.email.type_param = 'INTERNET'
+>>> j.prettyPrint()
+ VCARD
+ EMAIL: jeffrey@osafoundation.org
+ params for EMAIL:
+ TYPE ['INTERNET']
+ FN: Jeffrey Harris
+ N: Jeffrey Harris
+
+serializing will add any required computable attributes (like 'VERSION')
+
+>>> j.serialize()
+u'BEGIN:VCARD\r\nVERSION:3.0\r\nEMAIL;TYPE=INTERNET:jeffrey@osafoundation.org\r\nFN:Jeffrey Harris\r\nN:Harris;Jeffrey;;;\r\nEND:VCARD\r\n'
+>>> j.prettyPrint()
+ VCARD
+ VERSION: 3.0
+ EMAIL: jeffrey@osafoundation.org
+ params for EMAIL:
+ TYPE ['INTERNET']
+ FN: Jeffrey Harris
+ N: Jeffrey Harris
+
+Parsing vCards
+..............
+
+>>> s = """
+... BEGIN:VCARD
+... VERSION:3.0
+... EMAIL;TYPE=INTERNET:jeffrey@osafoundation.org
+... FN:Jeffrey Harris
+... N:Harris;Jeffrey;;;
+... END:VCARD
+... """
+>>> v = vobject.readOne( s )
+>>> v.prettyPrint()
+ VCARD
+ VERSION: 3.0
+ EMAIL: jeffrey@osafoundation.org
+ params for EMAIL:
+ TYPE [u'INTERNET']
+ FN: Jeffrey Harris
+ N: Jeffrey Harris
+>>> v.n.value.family
+u'Harris' \ No newline at end of file
diff --git a/ez_setup.py b/ez_setup.py
new file mode 100644
index 0000000..4789e37
--- /dev/null
+++ b/ez_setup.py
@@ -0,0 +1,221 @@
+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+ from ez_setup import use_setuptools
+ use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6a9"
+DEFAULT_URL = "http://cheeseshop.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+ 'setuptools-0.5a13-py2.3.egg': '85edcf0ef39bab66e130d3f38f578c86',
+ 'setuptools-0.5a13-py2.4.egg': 'ede4be600e3890e06d4ee5e0148e092a',
+ 'setuptools-0.6a1-py2.3.egg': 'ee819a13b924d9696b0d6ca6d1c5833d',
+ 'setuptools-0.6a1-py2.4.egg': '8256b5f1cd9e348ea6877b5ddd56257d',
+ 'setuptools-0.6a2-py2.3.egg': 'b98da449da411267c37a738f0ab625ba',
+ 'setuptools-0.6a2-py2.4.egg': 'be5b88bc30aed63fdefd2683be135c3b',
+ 'setuptools-0.6a3-py2.3.egg': 'ee0e325de78f23aab79d33106dc2a8c8',
+ 'setuptools-0.6a3-py2.4.egg': 'd95453d525a456d6c23e7a5eea89a063',
+ 'setuptools-0.6a4-py2.3.egg': 'e958cbed4623bbf47dd1f268b99d7784',
+ 'setuptools-0.6a4-py2.4.egg': '7f33c3ac2ef1296f0ab4fac1de4767d8',
+ 'setuptools-0.6a5-py2.3.egg': '748408389c49bcd2d84f6ae0b01695b1',
+ 'setuptools-0.6a5-py2.4.egg': '999bacde623f4284bfb3ea77941d2627',
+ 'setuptools-0.6a6-py2.3.egg': '7858139f06ed0600b0d9383f36aca24c',
+ 'setuptools-0.6a6-py2.4.egg': 'c10d20d29acebce0dc76219dc578d058',
+ 'setuptools-0.6a7-py2.3.egg': 'cfc4125ddb95c07f9500adc5d6abef6f',
+ 'setuptools-0.6a7-py2.4.egg': 'c6d62dab4461f71aed943caea89e6f20',
+ 'setuptools-0.6a8-py2.3.egg': '2f18eaaa3f544f5543ead4a68f3b2e1a',
+ 'setuptools-0.6a8-py2.4.egg': '799018f2894f14c9f8bcb2b34e69b391',
+ 'setuptools-0.6a9-py2.3.egg': '8e438ad70438b07b0d8f82cae42b278f',
+ 'setuptools-0.6a9-py2.4.egg': '8f6e01fc12fb1cd006dc0d6c04327ec1',
+}
+
+import sys, os
+
+def _validate_md5(egg_name, data):
+ if egg_name in md5_data:
+ from md5 import md5
+ digest = md5(data).hexdigest()
+ if digest != md5_data[egg_name]:
+ print >>sys.stderr, (
+ "md5 validation of %s failed! (Possible download problem?)"
+ % egg_name
+ )
+ sys.exit(2)
+ return data
+
+
+def use_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ download_delay=15
+):
+ """Automatically find/download setuptools and make it available on sys.path
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end with
+ a '/'). `to_dir` is the directory where setuptools will be downloaded, if
+ it is not already available. If `download_delay` is specified, it should
+ be the number of seconds that will be paused before initiating a download,
+ should one be required. If an older version of setuptools is installed,
+ this routine will print a message to ``sys.stderr`` and raise SystemExit in
+ an attempt to abort the calling script.
+ """
+ try:
+ import setuptools
+ if setuptools.__version__ == '0.0.1':
+ print >>sys.stderr, (
+ "You have an obsolete version of setuptools installed. Please\n"
+ "remove it from your system entirely before rerunning this script."
+ )
+ sys.exit(2)
+ except ImportError:
+ egg = download_setuptools(version, download_base, to_dir, download_delay)
+ sys.path.insert(0, egg)
+ import setuptools; setuptools.bootstrap_install_from = egg
+
+ import pkg_resources
+ try:
+ pkg_resources.require("setuptools>="+version)
+
+ except pkg_resources.VersionConflict:
+ # XXX could we install in a subprocess here?
+ print >>sys.stderr, (
+ "The required version of setuptools (>=%s) is not available, and\n"
+ "can't be installed while this script is running. Please install\n"
+ " a more recent version first."
+ ) % version
+ sys.exit(2)
+
+def download_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ delay = 15
+):
+ """Download setuptools from a specified location and return its filename
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end
+ with a '/'). `to_dir` is the directory where the egg will be downloaded.
+ `delay` is the number of seconds to pause before an actual download attempt.
+ """
+ import urllib2, shutil
+ egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+ url = download_base + egg_name
+ saveto = os.path.join(to_dir, egg_name)
+ src = dst = None
+ if not os.path.exists(saveto): # Avoid repeated downloads
+ try:
+ from distutils import log
+ if delay:
+ log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help). I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+---------------------------------------------------------------------------""",
+ version, download_base, delay
+ ); from time import sleep; sleep(delay)
+ log.warn("Downloading %s", url)
+ src = urllib2.urlopen(url)
+ # Read/write all in one block, so we don't create a corrupt file
+ # if the download is interrupted.
+ data = _validate_md5(egg_name, src.read())
+ dst = open(saveto,"wb"); dst.write(data)
+ finally:
+ if src: src.close()
+ if dst: dst.close()
+ return os.path.realpath(saveto)
+
+def main(argv, version=DEFAULT_VERSION):
+ """Install or upgrade setuptools and EasyInstall"""
+
+ try:
+ import setuptools
+ except ImportError:
+ import tempfile, shutil
+ tmpdir = tempfile.mkdtemp(prefix="easy_install-")
+ try:
+ egg = download_setuptools(version, to_dir=tmpdir, delay=0)
+ sys.path.insert(0,egg)
+ from setuptools.command.easy_install import main
+ main(list(argv)+[egg])
+ finally:
+ shutil.rmtree(tmpdir)
+ else:
+ if setuptools.__version__ == '0.0.1':
+ # tell the user to uninstall obsolete version
+ use_setuptools(version)
+
+ req = "setuptools>="+version
+ import pkg_resources
+ try:
+ pkg_resources.require(req)
+ except pkg_resources.VersionConflict:
+ try:
+ from setuptools.command.easy_install import main
+ except ImportError:
+ from easy_install import main
+ main(list(argv)+[download_setuptools(delay=0)])
+ sys.exit(0) # try to force an exit
+ else:
+ if argv:
+ from setuptools.command.easy_install import main
+ main(argv)
+ else:
+ print "Setuptools version",version,"or greater has been installed."
+ print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+
+
+def update_md5(filenames):
+ """Update our built-in md5 registry"""
+
+ import re
+ from md5 import md5
+
+ for name in filenames:
+ base = os.path.basename(name)
+ f = open(name,'rb')
+ md5_data[base] = md5(f.read()).hexdigest()
+ f.close()
+
+ data = [" %r: %r,\n" % it for it in md5_data.items()]
+ data.sort()
+ repl = "".join(data)
+
+ import inspect
+ srcfile = inspect.getsourcefile(sys.modules[__name__])
+ f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+ match = re.search("\nmd5_data = {\n([^}]+)}", src)
+ if not match:
+ print >>sys.stderr, "Internal error!"
+ sys.exit(2)
+
+ src = src[:match.start(1)] + repl + src[match.end(1):]
+ f = open(srcfile,'w')
+ f.write(src)
+ f.close()
+
+
+if __name__=='__main__':
+ if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+ update_md5(sys.argv[2:])
+ else:
+ main(sys.argv[1:])
+
+
+
+
+
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..fd44be0
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+"""VObject: module for reading vCard and vCalendar files
+
+Parses iCalendar and vCard files into Python data structures, decoding the relevant encodings. Also serializes vobject data structures to valid iCalendar or vCard unicode strings.
+
+Requires dateutil (https://moin.conectiva.com.br/DateUtil) 0.9 or later.
+"""
+
+# not using setuptools until Chandler's ready for eggs
+# from ez_setup import use_setuptools
+# use_setuptools()
+
+# from setuptools import setup, find_packages
+
+from distutils.core import setup
+
+# Metadata
+PACKAGE_NAME = "vobject"
+PACKAGE_VERSION = "0.3.0"
+
+ALL_EXTS = ['*.py', '*.ics', '*.txt']
+
+packages = ['vobject']
+
+doclines = __doc__.splitlines()
+
+setup(name = "vobject",
+ version = PACKAGE_VERSION,
+ author = "Jeffrey Harris",
+ author_email = "jeffrey@osafoundation.org",
+ license = "BSD",
+ zip_safe = True,
+ url = "http://vobject.skyhouseconsulting.com",
+
+ package_dir = {'':'src'},
+ package_data = {'': ALL_EXTS},
+
+ install_requires = ['python-dateutil >= 1.1'],
+
+ platforms = ["any"],
+ packages = ["vobject"],
+ description = doclines[0],
+ long_description = "\n".join(doclines[2:]),
+ classifiers = """
+ Development Status :: 3 - Alpha
+ Environment :: Console
+ License :: OSI Approved :: BSD License
+ Intended Audience :: Developers
+ Natural Language :: English
+ Programming Language :: Python
+ Operating System :: OS Independent
+ Topic :: Text Processing""".strip().splitlines()
+ )
diff --git a/src/vobject/__init__.py b/src/vobject/__init__.py
new file mode 100644
index 0000000..d5daf30
--- /dev/null
+++ b/src/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/src/vobject/base.py b/src/vobject/base.py
new file mode 100644
index 0000000..102e38f
--- /dev/null
+++ b/src/vobject/base.py
@@ -0,0 +1,978 @@
+"""vobject module for reading vCard and vCalendar files."""
+
+import re
+import sys
+import logging
+import StringIO
+import string
+import exceptions
+import codecs
+
+#------------------------------------ Logging ----------------------------------
+logger = logging.getLogger('vobject')
+if not logger.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 = unichr(13)
+LF = unichr(10)
+CRLF = CR + LF
+SPACE = unichr(32)
+TAB = unichr(9)
+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 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)
+
+ 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')
+
+ 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()
+
+ 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:
+ 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)
+ 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 = {}
+
+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.VERBOSE)
+
+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)
+ return (match.group('name'),
+ 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):
+ """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:
+ val = fp.read(-1)
+ lineNumber = 1
+ for match in logical_lines_re.finditer(val):
+ line, n = wrap_re.subn('', match.group())
+ if line != '':
+ if type(line[0]) != unicode:
+ line = line.decode('utf-8')
+ 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:
+ if type(line[0]) != unicode:
+ line = line.decode('utf-8')
+ 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):
+ if isinstance(input, basestring): input = StringIO.StringIO(input)
+ input.seek(0)
+ outbuf.write(input.read(lineLength) + CRLF)
+ brokenline = input.read(lineLength - 1)
+ while brokenline:
+ outbuf.write(' ' + brokenline + CRLF)
+ brokenline = input.read(lineLength - 1)
+
+def defaultSerialize(obj, buf, lineLength):
+ """Encode and fold obj and its children, write to buf or return a string."""
+
+ outbuf = buf or StringIO.StringIO()
+
+ if isinstance(obj, Component):
+ if obj.group is None:
+ groupString = ''
+ else:
+ groupString = obj.group + '.'
+ if obj.useBegin:
+ foldOneLine(outbuf, 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, groupString + u"END:" + obj.name, lineLength)
+ if DEBUG: logger.debug("Finished %s" % obj.name.upper())
+
+ elif isinstance(obj, ContentLine):
+ startedEncoded = obj.encoded
+ #TODO: X- lines should be considered TEXT, and should be encoded as such
+ if obj.behavior and not startedEncoded: obj.behavior.encode(obj)
+ s=StringIO.StringIO() #unfolded buffer
+ if obj.group is not None:
+ s.write(obj.group + '.')
+ if DEBUG: logger.debug("Serializing line" + str(obj))
+ s.write(obj.name.upper())
+ for key, paramvals in obj.params.iteritems():
+ s.write(';' + key + '=' + ','.join(map(dquoteEscape, paramvals)))
+ s.write(':' + obj.value)
+ if obj.behavior and not startedEncoded: obj.behavior.decode(obj)
+ foldOneLine(outbuf, s, lineLength)
+ if DEBUG: logger.debug("Finished %s line" % obj.name.upper())
+
+ 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):
+ """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
+ stack = Stack()
+ versionLine = None
+ n = 0
+ for line, n in getLogicalLines(stream, False): # not allowing vCard 2.1
+ 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()
+
+
+def readOne(stream, validate=False, transform=True):
+ """Return the first component from stream."""
+ return readComponents(stream, validate, transform).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 = True
+ 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/src/vobject/behavior.py b/src/vobject/behavior.py
new file mode 100644
index 0000000..e2a2d56
--- /dev/null
+++ b/src/vobject/behavior.py
@@ -0,0 +1,161 @@
+"""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 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
+ 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/src/vobject/hcalendar.py b/src/vobject/hcalendar.py
new file mode 100644
index 0000000..93614ab
--- /dev/null
+++ b/src/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/src/vobject/icalendar.py b/src/vobject/icalendar.py
new file mode 100644
index 0000000..9350131
--- /dev/null
+++ b/src/vobject/icalendar.py
@@ -0,0 +1,1696 @@
+"""Definitions and behavior for iCalendar, also known as vCalendar 2.0"""
+
+import string
+import behavior
+import dateutil.rrule
+import dateutil.tz
+import StringIO
+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
+
+#------------------------------- 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 registerTzid(tzid, tzinfo):
+ """Register a tzid -> tzinfo mapping."""
+ __tzidMap[tzid]=tzinfo
+
+def getTzid(tzid):
+ """Return the tzid if it exists, or None."""
+ return __tzidMap.get(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')
+ buffer = StringIO.StringIO()
+ 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)
+ return dateutil.tz.tzical(StringIO.StringIO(str(buffer.getvalue()))).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)
+
+ 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):
+ """
+ Given a tzinfo class, use known APIs to determine TZID, or use tzname.
+ """
+ if tzinfo is None or 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 tzinfo.tzid
+
+ # try tzical's tzid key
+ elif hasattr(tzinfo, '_tzid'):
+ return 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 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) == 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:
+ # if there's no dtstart, 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 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:
+ # it's not entirely clear, but presumably a date
+ # valued UNTIL should include that date
+ until = datetime.datetime.combine(until.date(),
+ dtstart.time())
+ rule._until = until.replace(tzinfo=dtstart.tzinfo)
+
+ # 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):
+ dtstart = self.dtstart.value
+ 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 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 RecurringBehavior(behavior.Behavior):
+ """Parent Behavior for components which should be RecurringComponents."""
+ hasNative = True
+ isComponent = 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'):
+ 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
+
+ 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)
+ if getattr(obj, 'value_param', 'DATE-TIME').upper() == 'DATE-TIME':
+ if hasattr(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.
+
+ """
+ # Fixme: obj.value should be a list, so this test should never succeed
+ if type(obj.value) == 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 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 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(behavior.Behavior):
+ """vCalendar 2.0 behavior."""
+ name = 'VCALENDAR'
+ description = 'vCalendar 2.0, also known as iCalendar.'
+ versionString = '2.0'
+ isComponent = True
+ 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)
+ }
+
+ @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 = [x.tzid.value for x in getattr(obj, 'vtimezone_list', [])]
+ for tzid in tzidsUsed.keys():
+ if tzid == 'UTC' or tzid in oldtzids: continue
+ obj.add(TimezoneComponent(tzinfo=getTzid(tzid)))
+registerBehavior(VCalendar2_0)
+
+class VTimezone(behavior.Behavior):
+ """Timezone behavior."""
+ name = 'VTIMEZONE'
+ hasNative = True
+ isComponent = 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):
+ return True #TODO: FIXME
+ 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(behavior.Behavior):
+ hasNative = False
+ isComponent = True
+ knownChildren = {'DTSTART': (1, 1, None)}#min, max, behaviorRegistry id
+
+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(behavior.Behavior):
+ """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)]
+ >>> print vfb.serialize()
+ BEGIN:VFREEBUSY
+ UID:test
+ DTSTART:20060216T010000Z
+ DTEND:20060216T030000Z
+ FREEBUSY:20060216T010000Z/PT1H
+ END:VFREEBUSY
+
+ """
+ name='VFREEBUSY'
+ isComponent = True
+ 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(behavior.Behavior):
+ """Alarm behavior."""
+ name='VALARM'
+ isComponent = True
+ 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 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)
+
+#------------------------ 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']
+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 += '0S'
+ 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, tzinfos=None):
+ if tzinfos != None: print "Didn't expect a tzinfos here"
+ 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, strict=False):
+ """Returns list of strings."""
+
+ def escapableChar (c):
+ return c in escapableCharList
+
+ 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 == ',':
+ 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"
+ current = current + char #this is an error, but whatever
+
+ 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):
+ 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":
+ return stringToDateTime(contentline.value, tzinfo)
+
+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) - start)
+
+
+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/src/vobject/vcard.py b/src/vobject/vcard.py
new file mode 100644
index 0000000..2aa30cb
--- /dev/null
+++ b/src/vobject/vcard.py
@@ -0,0 +1,225 @@
+"""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
+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')
+ return ' '.join(self.toString(getattr(self, val)) for val in eng_order)
+
+ def __repr__(self):
+ return "<Name: %s>" % self.__str__()
+
+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 lines
+
+ def __repr__(self):
+ return "<Address: %s>" % repr(str(self))[1:-1]
+
+#------------------------ Registered Behavior subclasses -----------------------
+class VCardBehavior(behavior.Behavior):
+ allowGroup = True
+
+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),
+ '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 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 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):
+ if string.find(',') >= 0:
+ return string.split(',')
+ return string
+
+def splitFields(string):
+ """Return a list of strings or lists from a Name or Address."""
+ return [toListOrString(i) for i in string.split(';')]
+
+def toList(stringOrList):
+ if isinstance(stringOrList, basestring):
+ return [stringOrList]
+ return stringOrList
+
+def serializeFields(obj, order):
+ """Turn an object's fields into a ';' and ',' seperated string."""
+ return ';'.join([','.join(toList(getattr(obj, val))) for val in order])
+
+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')
+
diff --git a/src/vobject/win32tz.py b/src/vobject/win32tz.py
new file mode 100644
index 0000000..35f997b
--- /dev/null
+++ b/src/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
diff --git a/tests/recurrence.ics b/tests/recurrence.ics
new file mode 100644
index 0000000..f592234
--- /dev/null
+++ b/tests/recurrence.ics
@@ -0,0 +1,30 @@
+BEGIN:VCALENDAR
+VERSION
+ :2.0
+PRODID
+ :-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN
+BEGIN:VEVENT
+CREATED
+ :20060327T214227Z
+LAST-MODIFIED
+ :20060313T080829Z
+DTSTAMP
+ :20060116T231602Z
+UID
+ :70922B3051D34A9E852570EC00022388
+SUMMARY
+ :Monthly - All Hands Meeting with Joe Smith
+STATUS
+ :CONFIRMED
+CLASS
+ :PUBLIC
+RRULE
+ :FREQ=MONTHLY;UNTIL=20061228;INTERVAL=1;BYDAY=4TH
+DTSTART
+ :20060126T230000Z
+DTEND
+ :20060127T000000Z
+DESCRIPTION
+ :Repeat Meeting: - Occurs every 4th Thursday of each month
+END:VEVENT
+END:VCALENDAR
diff --git a/tests/tests.py b/tests/tests.py
new file mode 100644
index 0000000..16ef0c2
--- /dev/null
+++ b/tests/tests.py
@@ -0,0 +1,711 @@
+"""Long or boring tests for vobjects."""
+
+# add source directory to front of sys path
+import sys, os
+basepath = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert( 0, os.path.join( basepath, 'src', 'vobject' ) )
+
+import base, icalendar, behavior, vcard, hcalendar
+import StringIO, re, dateutil.tz, datetime
+
+
+#------------------- Testing and running functions -----------------------------
+def _test():
+ import doctest, base, tests, icalendar, __init__, re
+ flags = doctest.NORMALIZE_WHITESPACE | doctest.REPORT_ONLY_FIRST_FAILURE
+ for mod in base, tests, icalendar, __init__, vcard:
+ doctest.testmod(mod, verbose=0, optionflags=flags)
+ try:
+ sys.path.pop()
+ sys.path.insert( 0, os.path.join( basepath, 'src' ) )
+ doctest.testfile('../README.txt', optionflags=flags)
+ except IOError: #allow this test to fail if we can't find README.txt
+ pass
+
+
+if __name__ == '__main__':
+ _test()
+
+
+testSilly="""
+sillyname:name
+profile:sillyprofile
+stuff:folded
+ line
+""" + "morestuff;asinine:this line is not folded, \
+but in practice probably ought to be, as it is exceptionally long, \
+and moreover demonstratively stupid"
+
+icaltest=r"""BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+X-WR-TIMEZONE;VALUE=TEXT:US/Pacific
+METHOD:PUBLISH
+PRODID:-//Apple Computer\, Inc//iCal 1.0//EN
+X-WR-CALNAME;VALUE=TEXT:Example
+VERSION:2.0
+BEGIN:VEVENT
+SEQUENCE:5
+DTSTART;TZID=US/Pacific:20021028T140000
+RRULE:FREQ=Weekly;COUNT=10
+DTSTAMP:20021028T011706Z
+SUMMARY:Coffee with Jason
+UID:EC9439B1-FF65-11D6-9973-003065F99D04
+DTEND;TZID=US/Pacific:20021028T150000
+BEGIN:VALARM
+TRIGGER;VALUE=DURATION:-P1D
+ACTION:DISPLAY
+DESCRIPTION:Event reminder\, with comma\nand line feed
+END:VALARM
+END:VEVENT
+BEGIN:VTIMEZONE
+X-LIC-LOCATION:Random location
+TZID:US/Pacific
+LAST-MODIFIED:19870101T000000Z
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZOFFSETFROM:-0700
+TZOFFSETTO:-0800
+TZNAME:PST
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZOFFSETFROM:-0800
+TZOFFSETTO:-0700
+TZNAME:PDT
+END:DAYLIGHT
+END:VTIMEZONE
+END:VCALENDAR"""
+
+vcardtest = """BEGIN:VCARD
+VERSION:3.0
+FN:Daffy Duck Knudson (with Bugs Bunny and Mr. Pluto)
+N:Knudson;Daffy Duck (with Bugs Bunny and Mr. Pluto)
+NICKNAME:gnat and gnu and pluto
+BDAY;value=date:02-10;11-05;01-01
+TEL;type=HOME:+01-(0)2-765.43.21
+TEL;type=CELL:+01-(0)5-555.55.55
+ACCOUNT;type=HOME:010-1234567-05
+ADR;type=HOME:;;Haight Street 512;Novosibirsk;;80214;Gnuland
+TEL;type=HOME:+01-(0)2-876.54.32
+ORG:University of Novosibirsk, Department of Octopus
+ Parthenogenesis
+END:VCARD"""
+
+vcardWithGroups = r"""home.begin:vcard
+version:3.0
+source:ldap://cn=Meister%20Berger,o=Universitaet%20Goerlitz,c=DE
+name:Meister Berger
+fn:Meister Berger
+n:Berger;Meister
+bday;value=date:1963-09-21
+o:Universit=E6t G=F6rlitz
+title:Mayor
+title;language=de;value=text:Burgermeister
+note:The Mayor of the great city of
+ Goerlitz in the great country of Germany.
+email;internet:mb@goerlitz.de
+home.tel;type=fax,voice;type=msg:+49 3581 123456
+home.label:Hufenshlagel 1234\n
+ 02828 Goerlitz\n
+ Deutschland
+END:VCARD"""
+
+lowercaseComponentNames = r"""begin:vcard
+fn:Anders Bobo
+n:Bobo;Anders
+org:Bobo A/S;Vice President, Technical Support
+adr:Rockfeller Center;;Mekastreet;Bobocity;;2100;Myworld
+email;internet:bobo@example.com
+tel;work:+123455
+tel;fax:+123456
+tel;cell:+123457
+x-mozilla-html:FALSE
+url:http://www.example.com
+version:2.1
+end:vcard"""
+
+icalWeirdTrigger = r"""BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+X-WR-TIMEZONE;VALUE=TEXT:US/Pacific
+METHOD:PUBLISH
+PRODID:-//Apple Computer\, Inc//iCal 1.0//EN
+X-WR-CALNAME;VALUE=TEXT:Example
+VERSION:2.0
+BEGIN:VEVENT
+DTSTART:20021028T140000Z
+BEGIN:VALARM
+TRIGGER:20021028T120000Z
+ACTION:DISPLAY
+DESCRIPTION:This trigger is a date-time without a VALUE=DATE-TIME parameter
+END:VALARM
+END:VEVENT
+END:VCALENDAR"""
+
+badstream = r"""BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+X-WR-TIMEZONE;VALUE=TEXT:US/Pacific
+METHOD:PUBLISH
+PRODID:-//Apple Computer\, Inc//iCal 1.0//EN
+X-WR-CALNAME;VALUE=TEXT:Example
+VERSION:2.0
+BEGIN:VEVENT
+DTSTART:20021028T140000Z
+BEGIN:VALARM
+TRIGGER:a20021028120000
+ACTION:DISPLAY
+DESCRIPTION:This trigger has a nonsensical value
+END:VALARM
+END:VEVENT
+END:VCALENDAR"""
+
+timezones = r"""
+
+BEGIN:VTIMEZONE
+TZID:US/Pacific
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZOFFSETFROM:-0700
+TZOFFSETTO:-0800
+TZNAME:PST
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZOFFSETFROM:-0800
+TZOFFSETTO:-0700
+TZNAME:PDT
+END:DAYLIGHT
+END:VTIMEZONE
+
+BEGIN:VTIMEZONE
+TZID:US/Eastern
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+TZNAME:EST
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+TZNAME:EDT
+END:DAYLIGHT
+END:VTIMEZONE
+
+BEGIN:VTIMEZONE
+TZID:Santiago
+BEGIN:STANDARD
+DTSTART:19700314T000000
+TZOFFSETFROM:-0300
+TZOFFSETTO:-0400
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SA
+TZNAME:Pacific SA Standard Time
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19701010T000000
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0300
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=2SA
+TZNAME:Pacific SA Daylight Time
+END:DAYLIGHT
+END:VTIMEZONE
+
+BEGIN:VTIMEZONE
+TZID:W. Europe
+BEGIN:STANDARD
+DTSTART:19701025T030000
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+TZNAME:W. Europe Standard Time
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19700329T020000
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+TZNAME:W. Europe Daylight Time
+END:DAYLIGHT
+END:VTIMEZONE
+
+BEGIN:VTIMEZONE
+TZID:US/Fictitious-Eastern
+LAST-MODIFIED:19870101T000000Z
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+TZNAME:EST
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20050403T070000Z
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+TZNAME:EDT
+END:DAYLIGHT
+END:VTIMEZONE
+
+BEGIN:VTIMEZONE
+TZID:America/Montreal
+LAST-MODIFIED:20051013T233643Z
+BEGIN:DAYLIGHT
+DTSTART:20050403T070000
+TZOFFSETTO:-0400
+TZOFFSETFROM:+0000
+TZNAME:EDT
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20051030T020000
+TZOFFSETTO:-0500
+TZOFFSETFROM:-0400
+TZNAME:EST
+END:STANDARD
+END:VTIMEZONE
+
+"""
+
+__test__ = { "Test readOne" :
+ r"""
+ >>> silly = base.readOne(testSilly)
+ >>> silly
+ <SILLYPROFILE| [<MORESTUFF{}this line is not folded, but in practice probably ought to be, as it is exceptionally long, and moreover demonstratively stupid>, <SILLYNAME{}name>, <STUFF{}foldedline>]>
+ >>> silly.stuff
+ <STUFF{}foldedline>
+ >>> original = silly.serialize()
+ >>> f3 = StringIO.StringIO(original)
+ >>> silly2 = base.readOne(f3)
+ >>> silly2.serialize()==original
+ True
+ >>> s3 = StringIO.StringIO('cn:Babs Jensen\r\ncn:Barbara J Jensen\r\nsn:Jensen\r\nemail:babs@umich.edu\r\nphone:+1 313 747-4454\r\nx-id:1234567890\r\n')
+ >>> ex1 = base.readOne(s3)
+ >>> ex1
+ <*unnamed*| [<CN{}Babs Jensen>, <CN{}Barbara J Jensen>, <EMAIL{}babs@umich.edu>, <PHONE{}+1 313 747-4454>, <SN{}Jensen>, <X-ID{}1234567890>]>
+ >>> ex1.serialize()
+ u'CN:Babs Jensen\r\nCN:Barbara J Jensen\r\nEMAIL:babs@umich.edu\r\nPHONE:+1 313 747-4454\r\nSN:Jensen\r\nX-ID:1234567890\r\n'
+ """,
+
+ "Import icaltest" :
+ r"""
+ >>> c = base.readOne(icaltest, validate=True)
+ >>> c.vevent.valarm.trigger
+ <TRIGGER{}-1 day, 0:00:00>
+ >>> c.vevent.dtstart.value
+ datetime.datetime(2002, 10, 28, 14, 0, tzinfo=<tzicalvtz 'US/Pacific'>)
+ >>> c.vevent.dtend.value
+ datetime.datetime(2002, 10, 28, 15, 0, tzinfo=<tzicalvtz 'US/Pacific'>)
+ >>> c.vevent.dtstamp.value
+ datetime.datetime(2002, 10, 28, 1, 17, 6, tzinfo=tzutc())
+ >>> c.vevent.valarm.description.value
+ u'Event reminder, with comma\nand line feed'
+ >>> c.vevent.valarm.description.serialize()
+ u'DESCRIPTION:Event reminder\\, with comma\\nand line feed\r\n'
+ >>> vevent = c.vevent.transformFromNative()
+ >>> vevent.rrule
+ <RRULE{}FREQ=Weekly;COUNT=10>
+ """,
+
+ "Parsing tests" :
+ """
+ >>> parseRDate = icalendar.MultiDateBehavior.transformToNative
+ >>> icalendar.stringToTextValues('')
+ ['']
+ >>> icalendar.stringToTextValues('abcd,efgh')
+ ['abcd', 'efgh']
+ >>> icalendar.stringToPeriod("19970101T180000Z/19970102T070000Z")
+ (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()), datetime.timedelta(0, 46800))
+ >>> parseRDate(base.textLineToContentLine("RDATE;VALUE=DATE:19970304,19970504,19970704,19970904"))
+ <RDATE{'VALUE': ['DATE']}[datetime.date(1997, 3, 4), datetime.date(1997, 5, 4), datetime.date(1997, 7, 4), datetime.date(1997, 9, 4)]>
+ >>> parseRDate(base.textLineToContentLine("RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H"))
+ <RDATE{'VALUE': ['PERIOD']}[(datetime.datetime(1996, 4, 3, 2, 0, tzinfo=tzutc()), datetime.timedelta(0, 7200)), (datetime.datetime(1996, 4, 4, 1, 0, tzinfo=tzutc()), datetime.timedelta(0, 10800))]>
+ """,
+
+ "read failure" :
+ """
+ >>> vevent = base.readOne(badstream)
+ Traceback (most recent call last):
+ ...
+ ParseError: At line 11: TRIGGER with no VALUE not recognized as DURATION or as DATE-TIME
+ """,
+
+ "ical trigger workaround" :
+ """
+
+ >>> badical = base.readOne(icalWeirdTrigger)
+ >>> badical.vevent.valarm.description.value
+ u'This trigger is a date-time without a VALUE=DATE-TIME parameter'
+ >>> badical.vevent.valarm.trigger.value
+ datetime.datetime(2002, 10, 28, 12, 0, tzinfo=tzutc())
+ """,
+
+ "unicode test" :
+ r"""
+ >>> f = open(os.path.join(basepath, 'tests', 'utf8_test.ics'))
+ >>> vevent = base.readOne(f).vevent
+ >>> vevent.summary.value
+ u'The title \u3053\u3093\u306b\u3061\u306f\u30ad\u30c6\u30a3'
+ >>> summary = vevent.summary.value
+ """,
+
+ # make sure date valued UNTILs in rrules are in a reasonable timezone,
+ # and include that day (12/28 in this test)
+ "recurrence test" :
+ r"""
+ >>> f = file(os.path.join(basepath, 'tests', 'recurrence.ics'))
+ >>> cal = base.readOne(f)
+ >>> dates = list(cal.vevent.rruleset)
+ >>> dates[0]
+ datetime.datetime(2006, 1, 26, 23, 0, tzinfo=tzutc())
+ >>> dates[1]
+ datetime.datetime(2006, 2, 23, 23, 0, tzinfo=tzutc())
+ >>> dates[-1]
+ datetime.datetime(2006, 12, 28, 23, 0, tzinfo=tzutc())
+ """,
+
+
+ "regular expression test" :
+ """
+ >>> re.findall(base.patterns['name'], '12foo-bar:yay')
+ ['12foo-bar', 'yay']
+ >>> re.findall(base.patterns['safe_char'], 'a;b"*,cd')
+ ['a', 'b', '*', 'c', 'd']
+ >>> re.findall(base.patterns['qsafe_char'], 'a;b"*,cd')
+ ['a', ';', 'b', '*', ',', 'c', 'd']
+ >>> re.findall(base.patterns['param_value'], '"quoted";not-quoted;start"after-illegal-quote', re.VERBOSE)
+ ['"quoted"', '', 'not-quoted', '', 'start', '', 'after-illegal-quote', '']
+ >>> match = base.line_re.match('TEST;ALTREP="http://www.wiz.org":value:;"')
+ >>> match.group('value')
+ 'value:;"'
+ >>> match.group('name')
+ 'TEST'
+ >>> match.group('params')
+ ';ALTREP="http://www.wiz.org"'
+ """,
+
+ "VTIMEZONE creation test:" :
+
+ """
+ >>> f = StringIO.StringIO(timezones)
+ >>> tzs = dateutil.tz.tzical(f)
+ >>> tzs.get("US/Pacific")
+ <tzicalvtz 'US/Pacific'>
+ >>> icalendar.TimezoneComponent(_)
+ <VTIMEZONE | <TZID{}US/Pacific>>
+ >>> pacific = _
+ >>> print pacific.serialize()
+ BEGIN:VTIMEZONE
+ TZID:US/Pacific
+ BEGIN:STANDARD
+ DTSTART:20001029T020000
+ RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+ TZNAME:PST
+ TZOFFSETFROM:-0700
+ TZOFFSETTO:-0800
+ END:STANDARD
+ BEGIN:DAYLIGHT
+ DTSTART:20000402T020000
+ RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+ TZNAME:PDT
+ TZOFFSETFROM:-0800
+ TZOFFSETTO:-0700
+ END:DAYLIGHT
+ END:VTIMEZONE
+ >>> (_)
+ <VTIMEZONE | <TZID{}US/Pacific>>
+ >>> santiago = icalendar.TimezoneComponent(tzs.get('Santiago'))
+ >>> ser = santiago.serialize()
+ >>> print ser
+ BEGIN:VTIMEZONE
+ TZID:Santiago
+ BEGIN:STANDARD
+ DTSTART:20000311T000000
+ RRULE:FREQ=YEARLY;BYDAY=2SA;BYMONTH=3
+ TZNAME:Pacific SA Standard Time
+ TZOFFSETFROM:-0300
+ TZOFFSETTO:-0400
+ END:STANDARD
+ BEGIN:DAYLIGHT
+ DTSTART:20001014T000000
+ RRULE:FREQ=YEARLY;BYDAY=2SA;BYMONTH=10
+ TZNAME:Pacific SA Daylight Time
+ TZOFFSETFROM:-0400
+ TZOFFSETTO:-0300
+ END:DAYLIGHT
+ END:VTIMEZONE
+ >>> roundtrip = dateutil.tz.tzical(StringIO.StringIO(str(ser))).get()
+ >>> for year in range(2001, 2010):
+ ... for month in (2, 9):
+ ... dt = datetime.datetime(year, month, 15, tzinfo = roundtrip)
+ ... if dt.replace(tzinfo=tzs.get('Santiago')) != dt:
+ ... print "Failed for:", dt
+ >>> fict = icalendar.TimezoneComponent(tzs.get('US/Fictitious-Eastern'))
+ >>> print fict.serialize()
+ BEGIN:VTIMEZONE
+ TZID:US/Fictitious-Eastern
+ BEGIN:STANDARD
+ DTSTART:20001029T020000
+ RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+ TZNAME:EST
+ TZOFFSETFROM:-0400
+ TZOFFSETTO:-0500
+ END:STANDARD
+ BEGIN:DAYLIGHT
+ DTSTART:20000402T020000
+ RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20050403T070000Z
+ TZNAME:EDT
+ TZOFFSETFROM:-0500
+ TZOFFSETTO:-0400
+ END:DAYLIGHT
+ END:VTIMEZONE
+ """,
+
+ "Create iCalendar from scratch" :
+
+ """
+ >>> cal = base.newFromBehavior('vcalendar', '2.0')
+ >>> cal.add('vevent')
+ <VEVENT| []>
+ >>> cal.vevent.add('dtstart').value = datetime.datetime(2006, 5, 9)
+ >>> cal.vevent.add('description').value = "Test event"
+ >>> pacific = dateutil.tz.tzical(StringIO.StringIO(timezones)).get('US/Pacific')
+ >>> cal.vevent.add('created').value = datetime.datetime(2006, 1, 1, 10, tzinfo=pacific)
+ >>> cal.vevent.add('uid').value = "Not very random UID"
+ >>> print cal.serialize()
+ BEGIN:VCALENDAR
+ VERSION:2.0
+ PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+ BEGIN:VEVENT
+ UID:Not very random UID
+ DTSTART:20060509T000000
+ CREATED:20060101T180000Z
+ DESCRIPTION:Test event
+ END:VEVENT
+ END:VCALENDAR
+ """,
+
+ "Serializing with timezones test" :
+
+ """
+ >>> from dateutil.rrule import rrule, rruleset, WEEKLY
+ >>> pacific = dateutil.tz.tzical(StringIO.StringIO(timezones)).get('US/Pacific')
+ >>> cal = base.Component('VCALENDAR')
+ >>> cal.setBehavior(icalendar.VCalendar2_0)
+ >>> ev = cal.add('vevent')
+ >>> ev.add('dtstart').value = datetime.datetime(2005, 10, 12, 9, tzinfo = pacific)
+ >>> set = rruleset()
+ >>> set.rrule(rrule(WEEKLY, interval=2, byweekday=[2,4], until=datetime.datetime(2005, 12, 15, 9)))
+ >>> set.exdate(datetime.datetime(2005, 10, 14, 9, tzinfo = pacific))
+ >>> ev.rruleset = set
+ >>> ev.add('uid').value = "uid could be generated but doctest complains"
+ >>> ev.add('duration').value = datetime.timedelta(hours=1)
+ >>> print cal.serialize()
+ BEGIN:VCALENDAR
+ VERSION:2.0
+ PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+ BEGIN:VTIMEZONE
+ TZID:US/Pacific
+ BEGIN:STANDARD
+ DTSTART:20001029T020000
+ RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+ TZNAME:PST
+ TZOFFSETFROM:-0700
+ TZOFFSETTO:-0800
+ END:STANDARD
+ BEGIN:DAYLIGHT
+ DTSTART:20000402T020000
+ RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+ TZNAME:PDT
+ TZOFFSETFROM:-0800
+ TZOFFSETTO:-0700
+ END:DAYLIGHT
+ END:VTIMEZONE
+ BEGIN:VEVENT
+ UID:uid could be generated but doctest complains
+ DTSTART;TZID=US/Pacific:20051012T090000
+ DURATION:PT1H
+ EXDATE;TZID=US/Pacific:20051014T090000
+ RRULE:FREQ=WEEKLY;BYDAY=WE,FR;INTERVAL=2;UNTIL=20051215T090000
+ END:VEVENT
+ END:VCALENDAR
+ >>> apple = dateutil.tz.tzical(StringIO.StringIO(timezones)).get('America/Montreal')
+ >>> ev.dtstart.value = datetime.datetime(2005, 10, 12, 9, tzinfo = apple)
+ >>> print cal.serialize()
+ BEGIN:VCALENDAR
+ VERSION:2.0
+ PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+ BEGIN:VTIMEZONE
+ TZID:US/Pacific
+ BEGIN:STANDARD
+ DTSTART:20001029T020000
+ RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+ TZNAME:PST
+ TZOFFSETFROM:-0700
+ TZOFFSETTO:-0800
+ END:STANDARD
+ BEGIN:DAYLIGHT
+ DTSTART:20000402T020000
+ RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+ TZNAME:PDT
+ TZOFFSETFROM:-0800
+ TZOFFSETTO:-0700
+ END:DAYLIGHT
+ END:VTIMEZONE
+ BEGIN:VTIMEZONE
+ TZID:America/Montreal
+ BEGIN:STANDARD
+ DTSTART:20000101T000000
+ RRULE:FREQ=YEARLY;BYMONTH=1;UNTIL=20040101T050000Z
+ TZNAME:EST
+ TZOFFSETFROM:-0500
+ TZOFFSETTO:-0500
+ END:STANDARD
+ BEGIN:STANDARD
+ DTSTART:20051030T020000
+ RRULE:FREQ=YEARLY;BYDAY=5SU;BYMONTH=10
+ TZNAME:EST
+ TZOFFSETFROM:-0400
+ TZOFFSETTO:-0500
+ END:STANDARD
+ BEGIN:DAYLIGHT
+ DTSTART:20050403T070000
+ RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20050403T120000Z
+ TZNAME:EDT
+ TZOFFSETFROM:-0500
+ TZOFFSETTO:-0400
+ END:DAYLIGHT
+ END:VTIMEZONE
+ BEGIN:VEVENT
+ UID:uid could be generated but doctest complains
+ DTSTART;TZID=America/Montreal:20051012T090000
+ DURATION:PT1H
+ EXDATE;TZID=US/Pacific:20051014T090000
+ RRULE:FREQ=WEEKLY;BYDAY=WE,FR;INTERVAL=2;UNTIL=20051215T090000
+ END:VEVENT
+ END:VCALENDAR
+ """,
+
+ "Serializing iCalendar to hCalendar" :
+
+ """
+ >>> cal = base.newFromBehavior('hcalendar')
+ >>> cal.behavior
+ <class 'hcalendar.HCalendar'>
+ >>> pacific = dateutil.tz.tzical(StringIO.StringIO(timezones)).get('US/Pacific')
+ >>> cal.add('vevent')
+ <VEVENT| []>
+ >>> cal.vevent.add('summary').value = "this is a note"
+ >>> cal.vevent.add('url').value = "http://microformats.org/code/hcalendar/creator"
+ >>> cal.vevent.add('dtstart').value = datetime.date(2006,2,27)
+ >>> cal.vevent.add('location').value = "a place"
+ >>> cal.vevent.add('dtend').value = datetime.date(2006,2,27) + datetime.timedelta(days = 2)
+ >>> event2 = cal.add('vevent')
+ >>> event2.add('summary').value = "Another one"
+ >>> event2.add('description').value = "The greatest thing ever!"
+ >>> event2.add('dtstart').value = datetime.datetime(1998, 12, 17, 16, 42, tzinfo = pacific)
+ >>> event2.add('location').value = "somewhere else"
+ >>> event2.add('dtend').value = event2.dtstart.value + datetime.timedelta(days = 6)
+ >>> hcal = cal.serialize()
+ >>> print hcal
+ <span class="vevent">
+ <a class="url" href="http://microformats.org/code/hcalendar/creator">
+ <span class="summary">this is a note</span>:
+ <abbr class="dtstart", title="20060227">Monday, February 27</abbr>
+ - <abbr class="dtend", title="20060301">Tuesday, February 28</abbr>
+ at <span class="location">a place</span>
+ </a>
+ </span>
+ <span class="vevent">
+ <span class="summary">Another one</span>:
+ <abbr class="dtstart", title="19981217T164200-0800">Thursday, December 17, 16:42</abbr>
+ - <abbr class="dtend", title="19981223T164200-0800">Wednesday, December 23, 16:42</abbr>
+ at <span class="location">somewhere else</span>
+ <div class="description">The greatest thing ever!</div>
+ </span>
+ """,
+
+ "Generate UIDs automatically test:" :
+
+ """
+ >>> cal = base.newFromBehavior('vcalendar')
+ >>> cal.add('vevent').add('dtstart').value = datetime.datetime(2006,2,2,10)
+ >>> ser = cal.serialize()
+ >>> len(cal.vevent.uid_list)
+ 1
+ """,
+
+ "VCARD 3.0 parse test:" :
+
+ r"""
+ >>> card = base.readOne(vcardtest)
+ >>> card.adr.value
+ <Address: Haight Street 512\nNovosibirsk, 80214\nGnuland>
+ >>> print card.adr.value
+ Haight Street 512
+ Novosibirsk, 80214
+ Gnuland
+ >>> card.org.value
+ u'University of Novosibirsk, Department of Octopus Parthenogenesis'
+ >>> print card.serialize()
+ BEGIN:VCARD
+ VERSION:3.0
+ ACCOUNT;TYPE=HOME:010-1234567-05
+ ADR;TYPE=HOME:;;Haight Street 512;Novosibirsk;;80214;Gnuland
+ BDAY;VALUE=date:02-10;11-05;01-01
+ FN:Daffy Duck Knudson (with Bugs Bunny and Mr. Pluto)
+ N:Knudson;Daffy Duck (with Bugs Bunny and Mr. Pluto);;;
+ NICKNAME:gnat and gnu and pluto
+ ORG:University of Novosibirsk, Department of Octopus Parthenogenesis
+ TEL;TYPE=HOME:+01-(0)2-765.43.21
+ TEL;TYPE=CELL:+01-(0)5-555.55.55
+ TEL;TYPE=HOME:+01-(0)2-876.54.32
+ END:VCARD
+ """,
+
+ "Multi-text serialization test:" :
+
+ """
+ >>> category = base.newFromBehavior('categories')
+ >>> category.value = ['Random category']
+ >>> print category.serialize().strip()
+ CATEGORIES:Random category
+ >>> category.value.append('Other category')
+ >>> print category.serialize().strip()
+ CATEGORIES:Random category,Other category
+ """,
+
+ "vCard groups test:" :
+
+ """
+ >>> card = base.readOne(vcardWithGroups)
+ >>> card.group
+ u'home'
+ >>> card.tel.group
+ u'home'
+ >>> card.group = card.tel.group = 'new'
+ >>> card.tel.serialize().strip()
+ u'new.TEL;TYPE=fax,voice,msg:+49 3581 123456'
+ >>> card.serialize().splitlines()[0]
+ u'new.BEGIN:VCARD'
+ >>> dtstart = base.newFromBehavior('dtstart')
+ >>> dtstart.group = "badgroup"
+ >>> dtstart.serialize()
+ Traceback (most recent call last):
+ ...
+ VObjectError: "<DTSTART{}> has a group, but this object doesn't support groups"
+ """,
+
+ "Lowercase components test:" :
+
+ """
+ >>> card = base.readOne(lowercaseComponentNames)
+ >>> card.version
+ <VERSION{}2.1>
+ """
+ }
diff --git a/tests/utf8_test.ics b/tests/utf8_test.ics
new file mode 100644
index 0000000..fbb9ada
--- /dev/null
+++ b/tests/utf8_test.ics
@@ -0,0 +1,39 @@
+BEGIN:VCALENDAR
+METHOD:PUBLISH
+CALSCALE:GREGORIAN
+PRODID:-//EVDB//www.evdb.com//EN
+VERSION:2.0
+X-WR-CALNAME:EVDB Event Feed
+BEGIN:VEVENT
+DTSTART:20060922T000100Z
+DTEND:20060922T050100Z
+DTSTAMP:20050914T163414Z
+SUMMARY:The title こんにちはキティ
+DESCRIPTION:hello\nHere is a description\n\n\nこんにちはキティ
+ \n\n\n\nZwei Java-schwere Entwicklerpositionen und irgendeine Art sond
+ erbar-klingende Netzsichtbarmachungöffnung\, an einer interessanten F
+ irma im Gebäude\, in dem ich angerufenen Semantic Research bearbeite.
+ 1. Zauberer Des Semantica Software Engineer 2. Älterer Semantica Sof
+ tware-Englisch-3. Graph/Semantica Netz-Visualization/Navigation Sie ei
+ ngestufte Software-Entwicklung für die Regierung. Die Firma ist stark
+ und die Projekte sind sehr kühl und schließen irgendeinen Spielraum
+ ein. Wenn ich Ihnen irgendwie mehr erkläre\, muß ich Sie töten. Ps
+ . Tat schnell -- jemand ist\, wenn es hier interviewt\, wie ich dieses
+ schreibe. Er schaut intelligent (er trägt Kleidhosen) Semantica Soft
+ ware Engineer FIRMA: Semantische Forschung\, Inc. REPORTS ZU: Vizeprä
+ sident\, Produkt-Entwicklung POSITION: San Diego (Pint Loma) WEB SITE:
+ www.semanticresearch.com email: dorie@semanticresearch.com FIRMA-HINT
+ ERGRUND Semantische Forschung ist der führende Versorger der semantis
+ cher Netzwerkanschluß gegründeten nicht linearen Wissen Darstellung
+ Werkzeuge. Die Firma stellt diese Werkzeuge zum Intel\, zur reg.\, zum
+ EDU und zu den kommerziellen Märkten zur Verfügung. BRINGEN SIE ZUS
+ AMMENFASSUNG IN POSITION Semantische Forschung\, Inc. basiert in San D
+ iego\, Ca im alten realen Weltsan Diego Haus...\, das wir den Weltbest
+ en Platz haben zum zu arbeiten. Wir suchen nach Superstarentwicklern\,
+ um uns in der fortfahrenden Entwicklung unserer Semantica Produktseri
+ e zu unterstützen.
+LOCATION:こんにちはキティ
+SEQUENCE:0
+UID:E0-001-000276068-2
+END:VEVENT
+END:VCALENDAR \ No newline at end of file