Browse Source

- mehrere E-Mail Adressen mit unterschiedlichen Typparametern funktionieren
- Serialisierung funktioniert




git-svn-id: svn://svn.siningsoft.de/Sailfish_Contacts_Restore@5 9ea9dde1-eeb2-4aae-9f27-8a0df3aa35ee

devnull 4 years ago
parent
commit
7c31248d16
67 changed files with 13026 additions and 2 deletions
  1. 2 1
      SQL/Abfragen.sql
  2. BIN
      Testdata/system/Contacts/qtcontacts-sqlite/contacts.db-shm
  3. 0 0
      Testdata/system/Contacts/qtcontacts-sqlite/contacts.db-wal
  4. 10 0
      src/vobject/.gitignore
  5. 10 0
      src/vobject/.travis.yml
  6. 8 0
      src/vobject/ACKNOWLEDGEMENTS.txt
  7. 202 0
      src/vobject/LICENSE-2.0.txt
  8. 2 0
      src/vobject/MANIFEST.in
  9. 277 0
      src/vobject/README.md
  10. 86 0
      src/vobject/docs/build/lib/vobject/__init__.py
  11. 1191 0
      src/vobject/docs/build/lib/vobject/base.py
  12. 170 0
      src/vobject/docs/build/lib/vobject/behavior.py
  13. 97 0
      src/vobject/docs/build/lib/vobject/change_tz.py
  14. 130 0
      src/vobject/docs/build/lib/vobject/hcalendar.py
  15. 1957 0
      src/vobject/docs/build/lib/vobject/icalendar.py
  16. 221 0
      src/vobject/docs/build/lib/vobject/ics_diff.py
  17. 356 0
      src/vobject/docs/build/lib/vobject/vcard.py
  18. 156 0
      src/vobject/docs/build/lib/vobject/win32tz.py
  19. BIN
      src/vobject/docs/dist/vobject-0.9.3-py2.7.egg
  20. 1008 0
      src/vobject/docs/index.html
  21. 3 0
      src/vobject/docs/params.json
  22. 124 0
      src/vobject/docs/stylesheets/github-light.css
  23. 424 0
      src/vobject/docs/stylesheets/normalize.css
  24. 245 0
      src/vobject/docs/stylesheets/stylesheet.css
  25. 45 0
      src/vobject/docs/vobject.egg-info/PKG-INFO
  26. 40 0
      src/vobject/docs/vobject.egg-info/SOURCES.txt
  27. 1 0
      src/vobject/docs/vobject.egg-info/dependency_links.txt
  28. 4 0
      src/vobject/docs/vobject.egg-info/entry_points.txt
  29. 1 0
      src/vobject/docs/vobject.egg-info/requires.txt
  30. 1 0
      src/vobject/docs/vobject.egg-info/top_level.txt
  31. 1 0
      src/vobject/docs/vobject.egg-info/zip-safe
  32. 67 0
      src/vobject/setup.py
  33. 14 0
      src/vobject/test_files/availablity.ics
  34. 10 0
      src/vobject/test_files/badline.ics
  35. 16 0
      src/vobject/test_files/badstream.ics
  36. 8 0
      src/vobject/test_files/freebusy.ics
  37. 15 0
      src/vobject/test_files/journal.ics
  38. 85 0
      src/vobject/test_files/more_tests.txt
  39. 39 0
      src/vobject/test_files/ms_tzid.ics
  40. 9 0
      src/vobject/test_files/recurrence-offset-naive.ics
  41. 9 0
      src/vobject/test_files/recurrence-without-tz.ics
  42. 30 0
      src/vobject/test_files/recurrence.ics
  43. 16 0
      src/vobject/test_files/ruby_rrule.ics
  44. 5 0
      src/vobject/test_files/silly_test.ics
  45. 12 0
      src/vobject/test_files/simple_2_0_test.ics
  46. 13 0
      src/vobject/test_files/simple_3_0_test.ics
  47. 5 0
      src/vobject/test_files/simple_test.ics
  48. 41 0
      src/vobject/test_files/standard_test.ics
  49. 107 0
      src/vobject/test_files/timezones.ics
  50. 31 0
      src/vobject/test_files/tz_us_eastern.ics
  51. 23 0
      src/vobject/test_files/tzid_8bit.ics
  52. 39 0
      src/vobject/test_files/utf8_test.ics
  53. 18 0
      src/vobject/test_files/vcard_with_groups.ics
  54. 13 0
      src/vobject/test_files/vtodo.ics
  55. 946 0
      src/vobject/tests.py
  56. 88 0
      src/vobject/vobject/__init__.py
  57. 1220 0
      src/vobject/vobject/base.py
  58. 174 0
      src/vobject/vobject/behavior.py
  59. 101 0
      src/vobject/vobject/change_tz.py
  60. 130 0
      src/vobject/vobject/hcalendar.py
  61. 2078 0
      src/vobject/vobject/icalendar.py
  62. 227 0
      src/vobject/vobject/ics_diff.py
  63. 370 0
      src/vobject/vobject/vcard.py
  64. 162 0
      src/vobject/vobject/win32tz.py
  65. 3 1
      test/vcfExport_1.py
  66. 65 0
      test/vcfExport_2_children.py
  67. 65 0
      test/vcfExport_PhoneNumbers.py

+ 2 - 1
SQL/Abfragen.sql

@@ -1,2 +1,3 @@
 SELECT * FROM Contacts;
-SELECT * FROM Contacts JOIN EmailAddresses ON Contacts.contactId = EmailAddresses.contactId;
+SELECT * FROM Contacts JOIN EmailAddresses ON Contacts.contactId = EmailAddresses.contactId;
+

BIN
Testdata/system/Contacts/qtcontacts-sqlite/contacts.db-shm


+ 0 - 0
Testdata/system/Contacts/qtcontacts-sqlite/contacts.db-wal


+ 10 - 0
src/vobject/.gitignore

@@ -0,0 +1,10 @@
+*~
+*.log
+*.pyc
+.DS_Store
+build/**/*
+dist/**/*
+vobject.egg-info/**/*
+venv/**/*
+.idea/*
+*.sublime-*

+ 10 - 0
src/vobject/.travis.yml

@@ -0,0 +1,10 @@
+language: python
+python:
+  - "2.7"
+  - "3.3"
+  - "3.4"
+  - "3.5"
+# command to install dependencies
+install: pip install -e .
+# command to run tests
+script: python tests.py

+ 8 - 0
src/vobject/ACKNOWLEDGEMENTS.txt

@@ -0,0 +1,8 @@
+Enormous thanks to:
+Jeffrey Harris, for his incredible work on the original package
+Tim Baxter, for all his work maintaining vobject over the past few years
+Adieu, for keeping things alive on github
+Kristian Glass, for his enormous help with testing and Python3 matters
+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

+ 202 - 0
src/vobject/LICENSE-2.0.txt

@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 2 - 0
src/vobject/MANIFEST.in

@@ -0,0 +1,2 @@
+include README.md ACKNOWLEDGEMENTS.txt LICENSE-2.0.txt tests.py
+recursive-include test_files *.ics

+ 277 - 0
src/vobject/README.md

@@ -0,0 +1,277 @@
+# VObject [![PyPI version](https://badge.fury.io/py/vobject.svg)](https://pypi.python.org/pypi/vobject) [![PyPI downloads](https://img.shields.io/pypi/dm/vobject.svg)](https://pypi.python.org/pypi/vobject) [![Build Status](https://travis-ci.org/eventable/vobject.svg?branch=master)](https://travis-ci.org/eventable/vobject) [![License](https://img.shields.io/pypi/l/vobject.svg)](http://www.apache.org/licenses/LICENSE-2.0.html) [![Code Issues](https://www.quantifiedcode.com/api/v1/project/bb995082e4d24119956445829b1e960c/badge.svg)](https://www.quantifiedcode.com/app/project/bb995082e4d24119956445829b1e960c)
+
+VObject is intended to be a full-featured Python package for parsing and generating vCard and vCalendar files. It was originally developed in concert with the Open Source Application Foundation's Chandler project by Jeffrey Harris. Many thanks to [all the contributors](https://github.com/eventable/vobject/blob/master/ACKNOWLEDGEMENTS.txt) for their dedication and support. The project is currently being maintained by [Eventable](https://github.com/eventable) and [Sameen Karim](https://github.com/skarim).
+
+Currently, iCalendar files are supported and well tested. vCard 3.0 files are supported, and all data should be imported, but only a few components are understood in a sophisticated way. The [Calendar Server](http://calendarserver.org/) team has added VAVAILABILITY support to VObject's iCalendar parsing. Please report bugs and issues directly on [GitHub](https://github.com/eventable/vobject/issues).
+
+VObject is licensed under the [Apache 2.0 license](http://www.apache.org/licenses/LICENSE-2.0).
+
+Useful scripts included with VObject:
+
+* [ics_diff](https://github.com/eventable/vobject/blob/master/vobject/ics_diff.py): order is irrelevant in iCalendar files, return a diff of meaningful changes between icalendar files
+* [change_tz](https://github.com/eventable/vobject/blob/master/vobject/change_tz.py): Take an iCalendar file with events in the wrong timezone, change all events or just UTC events into one of the timezones PyICU supports. Requires [PyICU](https://pypi.python.org/pypi/PyICU/).
+
+
+# Installation
+
+To install with [pip](https://pypi.python.org/pypi/pip), run:
+
+```
+pip install vobject
+```
+
+
+Or download the package and run:
+
+```
+python setup.py install
+```
+
+VObject requires Python 2.7 or higher, along with the [dateutil](https://pypi.python.org/pypi/python-dateutil/) and [six](https://pypi.python.org/pypi/six) packages.
+
+
+# Running tests
+
+To run all tests, use:
+
+```
+python tests.py
+```
+
+
+# Usage
+
+## iCalendar
+
+#### 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 &lt;paramname&gt;_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
+
+#### Creating vCard objects
+
+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.add('org')
+ <ORG{}>
+>>> j.org.value = ['Open Source Applications Foundation']
+>>> j.prettyPrint()
+ VCARD
+    ORG: ['Open Source Applications Foundation']
+    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()
+'BEGIN:VCARD\r\nVERSION:3.0\r\nEMAIL;TYPE=INTERNET:jeffrey@osafoundation.org\r\nFN:Jeffrey Harris\r\nN:Harris;Jeffrey;;;\r\nORG:Open Source Applications Foundation\r\nEND:VCARD\r\n'
+>>> j.prettyPrint()
+ VCARD
+    ORG: Open Source Applications Foundation
+    VERSION: 3.0
+    EMAIL: jeffrey@osafoundation.org
+    params for  EMAIL:
+       TYPE ['INTERNET']
+    FN: Jeffrey Harris
+    N:  Jeffrey  Harris 
+```
+
+#### Parsing vCard objects
+
+```
+>>> s = """
+... BEGIN:VCARD
+... VERSION:3.0
+... EMAIL;TYPE=INTERNET:jeffrey@osafoundation.org
+... EMAIL;TYPE=INTERNET:jeffery@example.org
+... ORG:Open Source Applications Foundation
+... FN:Jeffrey Harris
+... N:Harris;Jeffrey;;;
+... END:VCARD
+... """
+>>> v = vobject.readOne( s )
+>>> v.prettyPrint()
+ VCARD
+    ORG: Open Source Applications Foundation
+    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'
+>>> v.email_list
+[<EMAIL{'TYPE': ['INTERNET']}jeffrey@osafoundation.org>,
+ <EMAIL{'TYPE': ['INTERNET']}jeffery@example.org>]
+```

+ 86 - 0
src/vobject/docs/build/lib/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
+
+"""
+
+from .base import newFromBehavior, readOne, readComponents
+from . import icalendar, vcard
+
+def iCalendar():
+    return newFromBehavior('vcalendar', '2.0')
+
+def vCard():
+    return newFromBehavior('vcard', '3.0')

+ 1191 - 0
src/vobject/docs/build/lib/vobject/base.py

@@ -0,0 +1,1191 @@
+"""vobject module for reading vCard and vCalendar files."""
+
+from __future__ import print_function
+
+import copy
+import logging
+import re
+import six
+import sys
+import codecs
+
+# ------------------------------------ Python 2/3 compatibility challenges  ----
+# Python 3 no longer has a basestring type, so....
+try:
+    basestring = basestring
+except NameError:
+    basestring = (str,bytes)
+
+# One more problem ... in python2 the str operator breaks on unicode
+# objects containing non-ascii characters
+try:
+    unicode
+    def str_(s):
+        """
+        Return string with correct encoding
+        """
+        if type(s) == unicode:
+            return s.encode('utf-8')
+        else:
+            return str(s)
+except NameError:
+    def str_(s):
+        """
+        Return string
+        """
+        return s.encode('utf-8')
+
+if not isinstance(b'', type('')):
+    unicode_type = str
+else:
+    unicode_type = unicode  # noqa
+
+
+def to_unicode(value):
+    """Converts a string argument to a unicode string.
+
+    If the argument is already a unicode string, it is returned
+    unchanged.  Otherwise it must be a byte string and is decoded as utf8.
+    """
+    if isinstance(value, unicode_type):
+        return value
+
+    return value.decode('utf-8')
+
+
+def to_basestring(s):
+    """Converts a string argument to a byte string.
+
+    If the argument is already a byte string, it is returned unchanged.
+    Otherwise it must be a unicode string and is encoded as utf8.
+    """
+    if isinstance(s, bytes):
+        return s
+
+    return s.encode('utf-8')
+
+# ------------------------------------ Logging ---------------------------------
+logger = logging.getLogger(__name__)
+if not logging.getLogger().handlers:
+    handler = logging.StreamHandler()
+    formatter = logging.Formatter('%(name)s %(levelname)s %(message)s')
+    handler.setFormatter(formatter)
+    logger.addHandler(handler)
+logger.setLevel(logging.ERROR)  # Log errors
+DEBUG = False  # Don't waste time on debug calls
+
+# ----------------------------------- Constants --------------------------------
+CR     = '\r'
+LF     = '\n'
+CRLF   = CR + LF
+SPACE  = ' '
+TAB    = '\t'
+SPACEORTAB = SPACE + TAB
+
+# --------------------------------- 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 spec.
+
+    Current spec: 4.0 (http://tools.ietf.org/html/rfc6350)
+    """
+    def __init__(self, group=None, *args, **kwds):
+        super(VBase, self).__init__(*args, **kwds)
+        self.group = group
+        self.behavior = None
+        self.parentBehavior = None
+        self.isNative = False
+
+    def copy(self, copyit):
+        self.group = copyit.group
+        self.behavior = copyit.behavior
+        self.parentBehavior = copyit.parentBehavior
+        self.isNative = copyit.isNative
+
+    def validate(self, *args, **kwds):
+        """
+        Call the behavior's validate method, or return True.
+        """
+        if self.behavior:
+            return self.behavior.validate(self, *args, **kwds)
+        return True
+
+    def getChildren(self):
+        """
+        Return an iterable containing the contents of the object.
+        """
+        return []
+
+    def clearBehavior(self, cascade=True):
+        """
+        Set behavior to None. Do for all descendants if cascading.
+        """
+        self.behavior=None
+        if cascade:
+            self.transformChildrenFromNative()
+
+    def autoBehavior(self, cascade=False):
+        """
+        Set behavior if name is in self.parentBehavior.knownChildren.
+
+        If cascade is True, unset behavior and parentBehavior for all
+        descendants, then recalculate behavior and parentBehavior.
+        """
+        parentBehavior = self.parentBehavior
+        if parentBehavior is not None:
+            knownChildTup = parentBehavior.knownChildren.get(self.name, None)
+            if knownChildTup is not None:
+                behavior = getBehavior(self.name, knownChildTup[2])
+                if behavior is not None:
+                    self.setBehavior(behavior, cascade)
+                    if isinstance(self, ContentLine) and self.encoded:
+                        self.behavior.decode(self)
+            elif isinstance(self, ContentLine):
+                self.behavior = parentBehavior.defaultBehavior
+                if self.encoded and self.behavior:
+                    self.behavior.decode(self)
+
+    def setBehavior(self, behavior, cascade=True):
+        """
+        Set behavior. If cascade is True, autoBehavior all descendants.
+        """
+        self.behavior = behavior
+        if cascade:
+            for obj in self.getChildren():
+                obj.parentBehavior = behavior
+                obj.autoBehavior(True)
+
+    def transformToNative(self):
+        """
+        Transform this object into a custom VBase subclass.
+
+        transformToNative should always return a representation of this object.
+        It may do so by modifying self in place then returning self, or by
+        creating a new object.
+        """
+        if self.isNative or not self.behavior or not self.behavior.hasNative:
+            return self
+        else:
+            try:
+                return self.behavior.transformToNative(self)
+            except Exception as 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 on " \
+                          "line %s: %s: %s"
+                    msg = msg % (lineNumber, sys.exc_info()[0],
+                                 sys.exc_info()[1])
+                    raise ParseError(msg, lineNumber)
+
+    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 as 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 " \
+                          "on line %s %s: %s"
+                    msg = msg % (lineNumber, sys.exc_info()[0],
+                                 sys.exc_info()[1])
+                    raise NativeError(msg, lineNumber)
+        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 {0!s} with behavior {1!s}".format(self.name, behavior))
+            return behavior.serialize(self, buf, lineLength, validate)
+        else:
+            if DEBUG:
+                logger.debug("serializing {0!s} without behavior".format(self.name))
+            return defaultSerialize(self, buf, lineLength)
+
+
+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.encoded = encoded
+        self.params = {}
+        self.singletonparams = []
+        self.isNative = isNative
+        self.lineNumber = lineNumber
+        self.value = value
+
+        def updateTable(x):
+            if len(x) == 1:
+                self.singletonparams += x
+            else:
+                paramlist = self.params.setdefault(x[0].upper(), [])
+                paramlist.extend(x[1:])
+
+        list(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 = codecs.decode(self.value.encode("utf-8"), "quoted-printable").decode(self.params['ENCODING'])
+
+    @classmethod
+    def duplicate(clz, copyit):
+        newcopy = clz('', {}, '')
+        newcopy.copy(copyit)
+        return newcopy
+
+    def copy(self, copyit):
+        super(ContentLine, self).copy(copyit)
+        self.name = copyit.name
+        self.value = copy.copy(copyit.value)
+        self.encoded = self.encoded
+        self.params = copy.copy(copyit.params)
+        for k, v in self.params.items():
+            self.params[k] = copy.copy(v)
+        self.singletonparams = copy.copy(copyit.singletonparams)
+        self.lineNumber = copyit.lineNumber
+
+    def __eq__(self, other):
+        try:
+            return (self.name == other.name) and (self.params == other.params) \
+                   and (self.value == other.value)
+        except Exception:
+            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 AttributeError(name)
+        except KeyError:
+            raise 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 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 v
+
+    def __str__(self):
+        return "<{0}{1}{2}>".format(self.name, self.params, self.valueRepr())
+
+    def __repr__(self):
+        return self.__str__()
+
+    def prettyPrint(self, level = 0, tabwidth=3):
+        pre = ' ' * level * tabwidth
+        print(pre, self.name + ":", self.valueRepr())
+        if self.params:
+            print(pre, "params for ", self.name + ':')
+            for k in self.params.keys():
+                print(pre + ' ' * tabwidth, k, self.params[k])
+
+
+class Component(VBase):
+    """
+    A complex property that can contain multiple ContentLines.
+
+    For our purposes, a component must start with a BEGIN:xxxx line and end with
+    END:xxxx, or have a PROFILE:xxx line if a top-level component.
+
+    @ivar contents:
+        A dictionary of lists of Component or ContentLine instances. The keys
+        are the lowercased names of child ContentLines or Components.
+        Note that BEGIN and END ContentLines are not included in contents.
+    @ivar name:
+        Uppercase string used to represent this Component, i.e VCARD if the
+        serialized object starts with BEGIN:VCARD.
+    @ivar useBegin:
+        A boolean flag determining whether BEGIN: and END: lines should
+        be serialized.
+    """
+    def __init__(self, name=None, *args, **kwds):
+        super(Component, self).__init__(*args, **kwds)
+        self.contents  = {}
+        if name:
+            self.name=name.upper()
+            self.useBegin = True
+        else:
+            self.name = ''
+            self.useBegin = False
+
+        self.autoBehavior()
+
+    @classmethod
+    def duplicate(clz, copyit):
+        newcopy = clz()
+        newcopy.copy(copyit)
+        return newcopy
+
+    def copy(self, copyit):
+        super(Component, self).copy(copyit)
+
+        # deep copy of contents
+        self.contents = {}
+        for key, lvalue in copyit.contents.items():
+            newvalue = []
+            for value in lvalue:
+                newitem = value.duplicate(value)
+                newvalue.append(newitem)
+            self.contents[key] = newvalue
+
+        self.name = copyit.name
+        self.useBegin = copyit.useBegin
+
+    def setProfile(self, name):
+        """
+        Assign a PROFILE to this unnamed component.
+
+        Used by vCard, not by vCalendar.
+        """
+        if self.name or self.useBegin:
+            if self.name == name:
+                return
+            raise VObjectError("This component already has a PROFILE or "
+                               "uses BEGIN.")
+        self.name = name.upper()
+
+    def __getattr__(self, name):
+        """
+        For convenience, make self.contents directly accessible.
+
+        Underscores, legal in python variable names, are converted to dashes,
+        which are legal in IANA tokens.
+        """
+        # if the object is being re-created by pickle, self.contents may not
+        # be set, don't get into an infinite loop over the issue
+        if name == 'contents':
+            return object.__getattribute__(self, name)
+        try:
+            if name.endswith('_list'):
+                return self.contents[toVName(name, 5)]
+            else:
+                return self.contents[toVName(name)][0]
+        except KeyError:
+            raise AttributeError(name)
+
+    normal_attributes = ['contents','name','behavior','parentBehavior','group']
+    def __setattr__(self, name, value):
+        """
+        For convenience, make self.contents directly accessible.
+
+        Underscores, legal in python variable names, are converted to dashes,
+        which are legal in IANA tokens.
+        """
+        if name not in self.normal_attributes and name.lower()==name:
+            if type(value) == list:
+                if name.endswith('_list'):
+                    name = name[:-5]
+                self.contents[toVName(name)] = value
+            elif name.endswith('_list'):
+                raise VObjectError("Component list set to a non-list")
+            else:
+                self.contents[toVName(name)] = [value]
+        else:
+            prop = getattr(self.__class__, name, None)
+            if isinstance(prop, property):
+                prop.fset(self, value)
+            else:
+                object.__setattr__(self, name, value)
+
+    def __delattr__(self, name):
+        try:
+            if name not in self.normal_attributes and name.lower()==name:
+                if name.endswith('_list'):
+                    del self.contents[toVName(name, 5)]
+                else:
+                    del self.contents[toVName(name)]
+            else:
+                object.__delattr__(self, name)
+        except KeyError:
+            raise AttributeError(name)
+
+    def getChildValue(self, childName, default = None, childNumber = 0):
+        """
+        Return a child's value (the first, by default), or None.
+        """
+        child = self.contents.get(toVName(childName))
+        if child is None:
+            return default
+        else:
+            return child[childNumber].value
+
+    def add(self, objOrName, group = None):
+        """
+        Add objOrName to contents, set behavior if it can be inferred.
+
+        If objOrName is a string, create an empty component or line based on
+        behavior. If no behavior is found for the object, add a ContentLine.
+
+        group is an optional prefix to the name of the object (see RFC 2425).
+        """
+        if isinstance(objOrName, VBase):
+            obj = objOrName
+            if self.behavior:
+                obj.parentBehavior = self.behavior
+                obj.autoBehavior(True)
+        else:
+            name = objOrName.upper()
+            try:
+                id=self.behavior.knownChildren[name][2]
+                behavior = getBehavior(name, id)
+                if behavior.isComponent:
+                    obj = Component(name)
+                else:
+                    obj = ContentLine(name, [], '', group)
+                obj.parentBehavior = self.behavior
+                obj.behavior = behavior
+                obj = obj.transformToNative()
+            except (KeyError, AttributeError):
+                obj = ContentLine(objOrName, [], '', group)
+            if obj.behavior is None and self.behavior is not None:
+                if isinstance(obj, ContentLine):
+                    obj.behavior = self.behavior.defaultBehavior
+        self.contents.setdefault(obj.name.lower(), []).append(obj)
+        return obj
+
+    def remove(self, obj):
+        """
+        Remove obj from contents.
+        """
+        named = self.contents.get(obj.name.lower())
+        if named:
+            try:
+                named.remove(obj)
+                if len(named) == 0:
+                    del self.contents[obj.name.lower()]
+            except ValueError:
+                pass;
+
+    def getChildren(self):
+        """
+        Return an iterable of all children.
+        """
+        for objList in self.contents.values():
+            for obj in objList: yield obj
+
+    def components(self):
+        """
+        Return an iterable of all Component children.
+        """
+        return (i for i in self.getChildren() if isinstance(i, Component))
+
+    def lines(self):
+        """
+        Return an iterable of all ContentLine children.
+        """
+        return (i for i in self.getChildren() if isinstance(i, ContentLine))
+
+    def sortChildKeys(self):
+        try:
+            first = [s for s in self.behavior.sortFirst if s in self.contents]
+        except Exception:
+            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 child in childArray:
+                child = child.transformToNative()
+                child.transformChildrenToNative()
+
+    def transformChildrenFromNative(self, clearBehavior=True):
+        """
+        Recursively transform native children to vanilla representations.
+        """
+        for childArray in self.contents.values():
+            for child in childArray:
+                child = child.transformFromNative()
+                child.transformChildrenFromNative(clearBehavior)
+                if clearBehavior:
+                    child.behavior = None
+                    child.parentBehavior = None
+
+    def __str__(self):
+        if self.name:
+            return "<{0}| {1}>".format(self.name, self.getSortedChildren())
+        else:
+            return u'<*unnamed*| {0}>'.format(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)
+
+
+class VObjectError(Exception):
+    def __init__(self, msg, lineNumber=None):
+        self.msg = msg
+        if lineNumber is not None:
+            self.lineNumber = lineNumber
+
+    def __str__(self):
+        if hasattr(self, 'lineNumber'):
+            return "At line {0!s}: {1!s}".format(self.lineNumber, self.msg)
+        else:
+            return repr(self.msg)
+
+
+class ParseError(VObjectError):
+    pass
+
+
+class ValidateError(VObjectError):
+    pass
+
+
+class NativeError(VObjectError):
+    pass
+
+
+# --------- Parsing functions and parseLine regular expressions ----------------
+
+patterns = {}
+
+# Note that underscore is not legal for names, it's included because
+# Lotus Notes uses it
+patterns['name'] = '[a-zA-Z0-9\-_]+'
+patterns['safe_char'] = '[^";:,]'
+patterns['qsafe_char'] = '[^"]'
+
+# the combined Python string replacement and regex syntax is a little confusing;
+# remember that %(foobar)s is replaced with patterns['foobar'], so for instance
+# param_value is any number of safe_chars or any number of qsaf_chars surrounded
+# by double quotes.
+
+patterns['param_value'] = ' "{qsafe_char!s} * " | {safe_char!s} * '.format(**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} + )
+""".format(**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
+)*
+""".format(**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
+    )
+)?
+""".format(**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
+""".format(**patterns)
+
+' "%(qsafe_char)s*" | %(safe_char)s* '
+
+param_values_re = re.compile(patterns['param_value_grouped'], re.VERBOSE)
+params_re       = re.compile(patterns['params_grouped'],      re.VERBOSE)
+line_re         = re.compile(patterns['line'],    re.DOTALL | re.VERBOSE)
+begin_re        = re.compile('BEGIN', re.IGNORECASE)
+
+
+def parseParams(string):
+    """
+    Parse parameters
+    """
+    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):
+    """
+    Parse line
+    """
+    match = line_re.match(line)
+    if match is None:
+        raise ParseError("Failed to parse line: {0!s}".format(line), lineNumber)
+    # Underscores are replaced with dash to work around Lotus Notes
+    return (match.group('name').replace('_','-'),
+            parseParams(match.group('params')),
+            match.group('value'), match.group('group'))
+
+# logical line regular expressions
+
+patterns['lineend'] = r'(?:\r\n|\r|\n|$)'
+patterns['wrap'] = r'{lineend!s} [\t ]'.format(**patterns)
+patterns['logicallines'] = r"""
+(
+   (?: [^\r\n] | {wrap!s} )*
+   {lineend!s}
+)
+""".format(**patterns)
+
+patterns['wraporend'] = r'({wrap!s} | {lineend!s} )'.format(**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.
+
+    # We're leaving this test in for awhile, because the unittest was ugly and dumb.
+    >>> from six import StringIO
+    >>> f=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 != '':
+                yield line, lineNumber
+            lineNumber += n
+
+    else:
+        quotedPrintable = False
+        newbuffer = six.StringIO
+        logicalLine = newbuffer()
+        lineNumber = 0
+        lineStartNumber = 0
+        while True:
+            line = fp.readline()
+            if line == '':
+                break
+            else:
+                line = line.rstrip(CRLF)
+                lineNumber += 1
+            if line.rstrip() == '':
+                if logicalLine.tell() > 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.tell() > 0:
+                yield logicalLine.getvalue(), lineStartNumber
+                lineStartNumber = lineNumber
+                logicalLine = newbuffer()
+                logicalLine.write(line)
+            else:
+                logicalLine = newbuffer()
+                logicalLine.write(line)
+
+            # 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.tell() > 0:
+            yield logicalLine.getvalue(), lineStartNumber
+
+
+def textLineToContentLine(text, n=None):
+    return ContentLine(*parseLine(text, n), **{'encoded':True,
+                                               'lineNumber' : n})
+
+
+def dquoteEscape(param):
+    """
+    Return param, or "param" if ',' or ';' or ':' is in param.
+    """
+    if param.find('"') >= 0:
+        raise VObjectError("Double quotes aren't allowed in parameter values.")
+    for char in ',;:':
+        if param.find(char) >= 0:
+            return '"'+ param + '"'
+    return param
+
+def foldOneLine(outbuf, input, lineLength = 75):
+    """
+    Folding line procedure that ensures multi-byte utf-8 sequences are not
+    broken across lines
+
+    TO-DO: This all seems odd. Is it still needed, especially in python3?
+    """
+    if len(input) < lineLength:
+        # Optimize for unfolded line case
+        try:
+            outbuf.write(bytes(input, 'UTF-8'))
+        except Exception:
+            # fall back on py2 syntax
+            outbuf.write(str_(input))
+
+    else:
+        # Look for valid utf8 range and write that out
+        start = 0
+        written = 0
+        counter = 0  # counts line size in bytes
+        decoded = to_unicode(input)
+        length = len(to_basestring(input))
+        while written < length:
+            s = decoded[start]  # take one char
+            size = len(to_basestring(s))  # calculate it's size in bytes
+            if counter + size > lineLength:
+                try:
+                    outbuf.write(bytes("\r\n ", 'UTF-8'))
+                except Exception:
+                    # fall back on py2 syntax
+                    outbuf.write("\r\n ")
+
+                counter = 1  # one for space
+
+            if str is unicode_type:
+                outbuf.write(to_unicode(s))
+            else:
+                # fall back on py2 syntax
+                outbuf.write(s.encode('utf-8'))
+
+            written += size
+            counter += size
+            start += 1
+    try:
+        outbuf.write(bytes("\r\n", 'UTF-8'))
+    except Exception:
+        # fall back on py2 syntax
+        outbuf.write("\r\n")
+
+
+def defaultSerialize(obj, buf, lineLength):
+    """
+    Encode and fold obj and its children, write to buf or return a string.
+    """
+    outbuf = buf or six.StringIO()
+
+    if isinstance(obj, Component):
+        if obj.group is None:
+            groupString = ''
+        else:
+            groupString = obj.group + '.'
+        if obj.useBegin:
+            foldOneLine(outbuf, "{0}BEGIN:{1}".format(groupString, 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, "{0}END:{1}".format(groupString, obj.name),
+                        lineLength)
+
+    elif isinstance(obj, ContentLine):
+        startedEncoded = obj.encoded
+        if obj.behavior and not startedEncoded:
+            obj.behavior.encode(obj)
+
+        s = six.StringIO()
+
+        if obj.group is not None:
+            s.write(obj.group + '.')
+        s.write(obj.name.upper())
+        keys = sorted(obj.params.keys())
+        for key in keys:
+            paramstr = ','.join(dquoteEscape(p) for p in obj.params[key])
+            s.write(";{0}={1}".format(key, paramstr))
+        s.write(":{0}".format(str_(obj.value)))
+        if obj.behavior and not startedEncoded:
+            obj.behavior.decode(obj)
+        foldOneLine(outbuf, s.getvalue(), lineLength)
+
+    return buf or outbuf.getvalue()
+
+
+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,
+                   ignoreUnreadable=False, allowQP=False):
+    """
+    Generate one Component at a time from a stream.
+    """
+    if isinstance(streamOrString, basestring):
+        stream = six.StringIO(str_(streamOrString))
+    else:
+        stream = streamOrString
+
+    try:
+        stack = Stack()
+        versionLine = None
+        n = 0
+        for line, n in getLogicalLines(stream, allowQP):
+            if ignoreUnreadable:
+                try:
+                    vline = textLineToContentLine(line, n)
+                except VObjectError as e:
+                    if e.lineNumber is not None:
+                        msg = "Skipped line %(lineNumber)s, message: %(msg)s"
+                    else:
+                        msg = "Skipped a line, message: %(msg)s"
+                    logger.error(msg % {'lineNumber' : e.lineNumber,
+                                        'msg' : str(e)})
+                    continue
+            else:
+                vline = textLineToContentLine(line, n)
+            if vline.name == "VERSION":
+                versionLine = vline
+                stack.modifyTop(vline)
+            elif vline.name == "BEGIN":
+                stack.push(Component(vline.value, group=vline.group))
+            elif vline.name == "PROFILE":
+                if not stack.top():
+                    stack.push(Component())
+                stack.top().setProfile(vline.value)
+            elif vline.name == "END":
+                if len(stack) == 0:
+                    err = "Attempted to end the %s component but it was " \
+                          "never opened" % vline.value
+                    raise ParseError(err, n)
+
+                if vline.value.upper() == stack.topName(): # START matches END
+                    if len(stack) == 1:
+                        component = stack.pop()
+                        if versionLine is not None:
+                            component.setBehaviorFromVersionLine(versionLine)
+                        else:
+                            behavior = getBehavior(component.name)
+                            if behavior:
+                                component.setBehavior(behavior)
+                        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 {0!s} was never closed".format(
+                                 (stack.topName())), n)
+            yield stack.pop()
+
+    except ParseError as e:
+        e.input = streamOrString
+        raise
+
+
+def readOne(stream, validate=False, transform=True, ignoreUnreadable=False,
+            allowQP=False):
+    """
+    Return the first component from stream.
+    """
+    return next(readComponents(stream, validate, transform, ignoreUnreadable,
+                               allowQP))
+
+
+# --------------------------- 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
+
+        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 {0!s}".format(name))
+    if behavior.isComponent:
+        obj = Component(name)
+    else:
+        obj = ContentLine(name, [], '')
+    obj.behavior = behavior
+    obj.isNative = False
+    return obj
+
+
+# --------------------------- Helper function ----------------------------------
+def backslashEscape(s):
+    s = s.replace("\\","\\\\").replace(";","\;").replace(",","\,")
+    return s.replace("\r\n", "\\n").replace("\n","\\n").replace("\r","\\n")

+ 170 - 0
src/vobject/docs/build/lib/vobject/behavior.py

@@ -0,0 +1,170 @@
+from . import base
+
+#------------------------ Abstract class for behavior --------------------------
+class Behavior(object):
+    """
+    Behavior (validation, encoding, and transformations) for vobjects.
+
+    Abstract class to describe vobject options, requirements and encodings.
+
+    Behaviors are used for root components like VCALENDAR, for subcomponents
+    like VEVENT, and for individual lines in components.
+
+    Behavior subclasses are not meant to be instantiated, all methods should
+    be classmethods.
+
+    @cvar name:
+        The uppercase name of the object described by the class, or a generic
+        name if the class defines behavior for many objects.
+    @cvar description:
+        A brief excerpt from the RFC explaining the function of the component or
+        line.
+    @cvar versionString:
+        The string associated with the component, for instance, 2.0 if there's a
+        line like VERSION:2.0, an empty string otherwise.
+    @cvar knownChildren:
+        A dictionary with uppercased component/property names as keys and a
+        tuple (min, max, id) as value, where id is the id used by
+        L{registerBehavior}, min and max are the limits on how many of this child
+        must occur.  None is used to denote no max or no id.
+    @cvar quotedPrintable:
+        A boolean describing whether the object should be encoded and decoded
+        using quoted printable line folding and character escaping.
+    @cvar defaultBehavior:
+        Behavior to apply to ContentLine children when no behavior is found.
+    @cvar hasNative:
+        A boolean describing whether the object can be transformed into a more
+        Pythonic object.
+    @cvar isComponent:
+        A boolean, True if the object should be a Component.
+    @cvar sortFirst:
+        The lower-case list of children which should come first when sorting.
+    @cvar allowGroup:
+        Whether or not vCard style group prefixes are allowed.
+    """
+    name=''
+    description=''
+    versionString=''
+    knownChildren = {}
+    quotedPrintable = False
+    defaultBehavior = None
+    hasNative= False
+    isComponent = False
+    allowGroup = False
+    forceUTC = False
+    sortFirst = []
+
+    def __init__(self):
+        err="Behavior subclasses are not meant to be instantiated"
+        raise base.VObjectError(err)
+
+    @classmethod
+    def validate(cls, obj, raiseException=False, complainUnrecognized=False):
+        """Check if the object satisfies this behavior's requirements.
+
+        @param obj:
+            The L{ContentLine<base.ContentLine>} or
+            L{Component<base.Component>} to be validated.
+        @param raiseException:
+            If True, raise a L{base.ValidateError} on validation failure.
+            Otherwise return a boolean.
+        @param complainUnrecognized:
+            If True, fail to validate if an uncrecognized parameter or child is
+            found.  Otherwise log the lack of recognition.
+
+        """
+        if not cls.allowGroup and obj.group is not None:
+            err = "{0} has a group, but this object doesn't support groups".format(obj)
+            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.items():
+                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 = "{0} is not a Component or Contentline".format(obj)
+            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

+ 97 - 0
src/vobject/docs/build/lib/vobject/change_tz.py

@@ -0,0 +1,97 @@
+"""Translate an ics file's events to a different timezone."""
+
+from optparse import OptionParser
+from vobject import icalendar, base
+
+try:
+    import PyICU
+except:
+    PyICU = None
+
+from datetime import datetime
+
+def change_tz(cal, new_timezone, default, utc_only=False, utc_tz=icalendar.utc):
+    """
+    Change the timezone of the specified component.
+
+    Args:
+        cal (Component): the component to change
+        new_timezone (tzinfo): the timezone to change to
+        default (tzinfo): a timezone to assume if the dtstart or dtend in cal 
+            doesn't have an existing timezone
+        utc_only (bool): only convert dates that are in utc
+        utc_tz (tzinfo): the tzinfo to compare to for UTC when processing 
+            utc_only=True
+    """
+
+    for vevent in getattr(cal, 'vevent_list', []):
+        start = getattr(vevent, 'dtstart', None)
+        end   = getattr(vevent, 'dtend',   None)
+        for node in (start, end):
+            if node:
+                dt = node.value
+                if (isinstance(dt, datetime) and
+                        (not utc_only or dt.tzinfo == utc_tz)):
+                    if dt.tzinfo is None:
+                        dt = dt.replace(tzinfo = default)
+                    node.value = dt.astimezone(new_timezone)
+
+def main():
+    options, args = get_options()
+    if PyICU is None:
+        print("Failure. change_tz requires PyICU, exiting")
+    elif options.list:
+        for tz_string in PyICU.TimeZone.createEnumeration():
+            print(tz_string)
+    elif args:
+        utc_only = options.utc
+        if utc_only:
+            which = "only UTC"
+        else:
+            which = "all"
+        print("Converting {0!s} events".format(which))
+        ics_file = args[0]
+        if len(args) > 1:
+            timezone = PyICU.ICUtzinfo.getInstance(args[1])
+        else:
+            timezone = PyICU.ICUtzinfo.default
+        print("... Reading {0!s}".format(ics_file))
+        cal = base.readOne(open(ics_file))
+        change_tz(cal, timezone, PyICU.ICUtzinfo.default, utc_only)
+
+        out_name = ics_file + '.converted'
+        print("... Writing {0!s}".format(out_name))
+
+        out = file(out_name, 'wb')
+        cal.serialize(out)
+        print("Done")
+
+
+version = "0.1"
+
+def get_options():
+    # Configuration options
+
+    usage = """usage: %prog [options] ics_file [timezone]"""
+    parser = OptionParser(usage=usage, version=version)
+    parser.set_description("change_tz will convert the timezones in an ics file. ")
+
+    parser.add_option("-u", "--only-utc", dest="utc", action="store_true",
+                      default=False, help="Only change UTC events.")
+    parser.add_option("-l", "--list", dest="list", action="store_true",
+                      default=False, help="List available timezones")
+
+    (cmdline_options, args) = parser.parse_args()
+    if not args and not cmdline_options.list:
+        print("error: too few arguments given")
+        print
+        print(parser.format_help())
+        return False, False
+
+    return cmdline_options, args
+
+if __name__ == "__main__":
+    try:
+        main()
+    except KeyboardInterrupt:
+        print("Aborted")

+ 130 - 0
src/vobject/docs/build/lib/vobject/hcalendar.py

@@ -0,0 +1,130 @@
+"""
+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>
+"""
+
+import six
+
+from datetime import date, datetime, timedelta
+
+from .base import CRLF, registerBehavior
+from .icalendar import VCalendar2_0
+
+
+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 six.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="{0!s}">{1!s}</abbr>\r\n'
+                    .format(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="{0!s}">{1!s}</abbr>\r\n'
+                        .format(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)

+ 1957 - 0
src/vobject/docs/build/lib/vobject/icalendar.py

@@ -0,0 +1,1957 @@
+"""Definitions and behavior for iCalendar, also known as vCalendar 2.0"""
+
+from __future__ import print_function
+
+import codecs
+import datetime
+import logging
+import random  # for generating a UID
+import socket
+import string
+
+from dateutil import rrule, tz
+import six
+
+try:
+    import pytz
+except ImportError:
+    class Pytz:
+        """fake pytz module (pytz is not required)"""
+
+        class AmbiguousTimeError(Exception):
+            """pytz error for ambiguous times
+               during transition daylight->standard"""
+
+        class NonExistentTimeError(Exception):
+            """pytz error for non-existent times
+               during transition standard->daylight"""
+
+    pytz = Pytz  # keeps quantifiedcode happy
+
+from . import behavior
+from .base import (VObjectError, NativeError, ValidateError, ParseError,
+                   Component, ContentLine, logger, registerBehavior,
+                   backslashEscape, foldOneLine, str_)
+
+
+# ------------------------------- Constants ------------------------------------
+DATENAMES = ("rdate", "exdate")
+RULENAMES = ("exrule", "rrule")
+DATESANDRULES = ("exrule", "rrule", "rdate", "exdate")
+PRODID = u"-//PYVOBJECT//NONSGML Version 1//EN"
+
+WEEKDAYS = "MO", "TU", "WE", "TH", "FR", "SA", "SU"
+FREQUENCIES = ('YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY',
+               'SECONDLY')
+
+zeroDelta = datetime.timedelta(0)
+twoHours  = datetime.timedelta(hours=2)
+
+
+# ---------------------------- TZID registry -----------------------------------
+__tzidMap={}
+
+def toUnicode(s):
+    """
+    Take a string or unicode, turn it into unicode, decoding as utf-8
+    """
+    if isinstance(s, six.binary_type):
+        s = s.decode('utf-8')
+    return s
+
+def registerTzid(tzid, tzinfo):
+    """
+    Register a tzid -> tzinfo mapping.
+    """
+    __tzidMap[toUnicode(tzid)]=tzinfo
+
+def getTzid(tzid, smart=True):
+    """
+    Return the tzid if it exists, or None.
+    """
+    tz = __tzidMap.get(toUnicode(tzid), None)
+    if smart and tzid and not tz:
+        try:
+            from pytz import timezone, UnknownTimeZoneError
+            try:
+                tz = timezone(tzid)
+                registerTzid(toUnicode(tzid), tz)
+            except UnknownTimeZoneError as e:
+                logging.error(e)
+        except ImportError as e:
+            logging.error(e)
+    return tz
+
+utc = tz.tzutc()
+registerTzid("UTC", utc)
+
+
+# -------------------- Helper subclasses ---------------------------------------
+class TimezoneComponent(Component):
+    """
+    A VTIMEZONE object.
+
+    VTIMEZONEs are parsed by 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, False):
+            registerTzid(tzid, tzinfo)
+        return tzid
+
+    def gettzinfo(self):
+        # workaround for dateutil failing to parse some experimental properties
+        good_lines = ('rdate', 'rrule', 'dtstart', 'tzname', 'tzoffsetfrom',
+                      'tzoffsetto', 'tzid')
+        # serialize encodes as utf-8, cStringIO will leave utf-8 alone
+        buffer = six.StringIO()
+        # allow empty VTIMEZONEs
+        if len(self.contents) == 0:
+            return None
+        def customSerialize(obj):
+            if isinstance(obj, Component):
+                foldOneLine(buffer, u"BEGIN:" + obj.name)
+                for child in obj.lines():
+                    if child.name.lower() in good_lines:
+                        child.serialize(buffer, 75, validate=False)
+                for comp in obj.components():
+                    customSerialize(comp)
+                foldOneLine(buffer, u"END:" + obj.name)
+        customSerialize(self)
+        buffer.seek(0)  # tzical wants to read a stream
+        return tz.tzical(buffer).get()
+
+    def settzinfo(self, tzinfo, start=2000, end=2030):
+        """
+        Create appropriate objects in self to represent tzinfo.
+
+        Collapse DST transitions to rrules as much as possible.
+
+        Assumptions:
+        - DST <-> Standard transitions occur on the hour
+        - never within a month of one another
+        - twice or fewer times a year
+        - never in the month of December
+        - DST always moves offset exactly one hour later
+        - tzinfo classes dst method always treats times that could be in either
+          offset as being in the later regime
+        """
+        def fromLastWeek(dt):
+            """
+            How many weeks from the end of the month dt is, starting from 1.
+            """
+            weekDelta = datetime.timedelta(weeks=1)
+            n = 1
+            current = dt + weekDelta
+            while current.month == dt.month:
+                n += 1
+                current += weekDelta
+            return n
+
+        # lists of dictionaries defining rules which are no longer in effect
+        completed = {'daylight' : [], 'standard' : []}
+
+        # dictionary defining rules which are currently in effect
+        working   = {'daylight' : None, 'standard' : None}
+
+        # rule may be based on nth week of the month or the nth from the last
+        for year in range(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
+                    try:
+                        old_offset = tzinfo.utcoffset(transition - twoHours)
+                        name = tzinfo.tzname(transition)
+                        offset = tzinfo.utcoffset(transition)
+                    except (pytz.AmbiguousTimeError, pytz.NonExistentTimeError):
+                        # guaranteed that tzinfo is a pytz timezone
+                        is_dst = (transitionTo == "daylight")
+                        old_offset = tzinfo.utcoffset(transition - twoHours, is_dst=is_dst)
+                        name = tzinfo.tzname(transition, is_dst=is_dst)
+                        offset = tzinfo.utcoffset(transition, is_dst=is_dst)
+                    rule = {'end'     : None,  # None, or an integer year
+                            'start'   : transition,  # the datetime of transition
+                            'month'   : transition.month,
+                            'weekday' : transition.weekday(),
+                            'hour'    : transition.hour,
+                            'name'    : name,
+                            'plus'    : int(
+                                (transition.day - 1)/ 7 + 1),  # nth week of the month
+                            'minus'   : fromLastWeek(transition),  # nth from last week
+                            'offset'  : offset,
+                            'offsetfrom' : old_offset}
+
+                    if oldrule is None:
+                        working[transitionTo] = rule
+                    else:
+                        plusMatch  = rule['plus']  == oldrule['plus']
+                        minusMatch = rule['minus'] == oldrule['minus']
+                        truth = plusMatch or minusMatch
+                        for key in 'month', 'weekday', 'hour', 'offset':
+                            truth = truth and rule[key] == oldrule[key]
+                        if truth:
+                            # the old rule is still true, limit to plus or minus
+                            if not plusMatch:
+                                oldrule['plus'] = None
+                            if not minusMatch:
+                                oldrule['minus'] = None
+                        else:
+                            # the new rule did not match the old
+                            oldrule['end'] = year - 1
+                            completed[transitionTo].append(oldrule)
+                            working[transitionTo] = rule
+
+        for transitionTo in 'daylight', 'standard':
+            if working[transitionTo] is not None:
+                completed[transitionTo].append(working[transitionTo])
+
+        self.tzid = []
+        self.daylight = []
+        self.standard = []
+
+        self.add('tzid').value = self.pickTzid(tzinfo, True)
+
+        # old = None # unused?
+        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 = rrule.weekday(rule['weekday'], num)
+                        du_rule = rrule.rrule(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 = ''
+                new_rule = "FREQ=YEARLY{0!s};BYMONTH={1!s}{2!s}"\
+                    .format(dayString, rule['month'], endString)
+
+                comp.add('rrule').value = new_rule
+
+    tzinfo = property(gettzinfo, settzinfo)
+    # prevent Component's __setattr__ from overriding the tzinfo property
+    normal_attributes = Component.normal_attributes + ['tzinfo']
+
+    @staticmethod
+    def pickTzid(tzinfo, allowUTC=False):
+        """
+        Given a tzinfo class, use known APIs to determine TZID, or use tzname.
+        """
+        if tzinfo is None or (not allowUTC and tzinfo_eq(tzinfo, utc)):
+            # If tzinfo is UTC, we don't need a TZID
+            return None
+        # try PyICU's tzid key
+        if hasattr(tzinfo, 'tzid'):
+            return toUnicode(tzinfo.tzid)
+
+        # try pytz zone key
+        if hasattr(tzinfo, 'zone'):
+            return toUnicode(tzinfo.zone)
+
+        # try tzical's tzid key
+        elif hasattr(tzinfo, '_tzid'):
+            return toUnicode(tzinfo._tzid)
+        else:
+            # return tzname for standard (non-DST) time
+            notDST = datetime.timedelta(0)
+            for month in range(1, 13):
+                dt = datetime.datetime(2000, month, 1)
+                if tzinfo.dst(dt) == notDST:
+                    return toUnicode(tzinfo.tzname(dt))
+        # there was no standard time in 2000!
+        raise VObjectError("Unable to guess TZID for tzinfo {0!s}"
+                           .format(tzinfo))
+
+    def __str__(self):
+        return "<VTIMEZONE | {0}>".format(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.
+
+    @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
+
+    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 = rrule.rruleset()
+                if addfunc is None:
+                    addfunc = getattr(rruleset, name)
+
+                if name in DATENAMES:
+                    if type(line.value[0]) == datetime.datetime:
+                        map(addfunc, line.value)
+                    elif type(line.value[0]) == datetime.date:
+                        for dt in line.value:
+                            addfunc(datetime.datetime(dt.year, dt.month, dt.day))
+                    else:
+                        # ignore RDATEs with PERIOD values for now
+                        pass
+                elif name in RULENAMES:
+                    try:
+                        dtstart = self.dtstart.value
+                    except (AttributeError, KeyError):
+                        # Special for VTODO - try DUE property instead
+                        try:
+                            if self.name == "VTODO":
+                                dtstart = self.due.value
+                            else:
+                                # if there's no dtstart, just return None
+                                logging.error('failed to get dtstart with VTODO')
+                                return None
+                        except (AttributeError, KeyError):
+                            # if there's no due, just return None
+                            logging.error('failed to find DUE at all.')
+                            return None
+
+                    # a Ruby iCalendar library escapes semi-colons in rrules,
+                    # so also remove any backslashes
+                    value = str_(line.value).replace('\\', '')
+                    rule = rrule.rrulestr(value, dtstart=dtstart)
+                    until = rule._until
+
+                    if until is not None and isinstance(dtstart,
+                                                        datetime.datetime) and \
+                            (until.tzinfo != dtstart.tzinfo):
+                        # dateutil converts the UNTIL date to a datetime,
+                        # check to see if the UNTIL parameter value was a date
+                        vals = dict(pair.split('=') for pair in
+                                    line.value.upper().split(';'))
+                        if len(vals.get('UNTIL', '')) == 8:
+                            until = datetime.datetime.combine(until.date(),
+                                                              dtstart.time())
+                        # While RFC2445 says UNTIL MUST be UTC, Chandler allows
+                        # floating recurring events, and uses floating UNTIL
+                        # values. Also, some odd floating UNTIL but timezoned
+                        # DTSTART values have shown up in the wild, so put
+                        # floating UNTIL values DTSTART's timezone
+                        if until.tzinfo is None:
+                            until = until.replace(tzinfo=dtstart.tzinfo)
+
+                        if dtstart.tzinfo is not None:
+                            until = until.astimezone(dtstart.tzinfo)
+
+                        # RFC2445 actually states that UNTIL must be a UTC
+                        # value. Whilst the changes above work OK, one problem
+                        # case is if DTSTART is floating but UNTIL is properly
+                        # specified as UTC (or with a TZID). In that case
+                        # dateutil will fail datetime comparisons. There is no
+                        # easy solution to this as there is no obvious timezone
+                        # (at this point) to do proper floating time offset
+                        # comparisons. The best we can do is treat the UNTIL
+                        # value as floating. This could mean incorrect
+                        # determination of the last instance. The better
+                        # solution here is to encourage clients to use COUNT
+                        # rather than UNTIL when DTSTART is floating.
+                        if dtstart.tzinfo is None:
+                            until = until.replace(tzinfo=None)
+
+                        rule._until = until
+
+                    # add the rrule or exrule to the rruleset
+                    addfunc(rule)
+
+                    if name == 'rrule' and addRDate:
+                        try:
+                            # dateutils does not work with all-day
+                            # (datetime.date) items so we need to convert to a
+                            # datetime.datetime (which is what dateutils
+                            # does internally)
+                            if not isinstance(dtstart, datetime.datetime):
+                                adddtstart = datetime.datetime.fromordinal(dtstart.toordinal())
+                            else:
+                                adddtstart = dtstart
+                            if rruleset._rrule[-1][0] != adddtstart:
+                                rruleset.rdate(adddtstart)
+                                added = True
+                            else:
+                                added = False
+                        except IndexError:
+                            # it's conceivable that an rrule has 0 datetimes
+                            added = False
+                        if added and rruleset._rrule[-1]._count != None:
+                            rruleset._rrule[-1]._count -= 1
+        return rruleset
+
+    def setrruleset(self, rruleset):
+        # Get DTSTART from component (or DUE if no DTSTART in a VTODO)
+        try:
+            dtstart = self.dtstart.value
+        except (AttributeError, KeyError):
+            if self.name == "VTODO":
+                dtstart = self.due.value
+            else:
+                raise
+
+        isDate = datetime.date == type(dtstart)
+        if isDate:
+            dtstart = datetime.datetime(dtstart.year,dtstart.month, dtstart.day)
+            untilSerialize = dateToString
+        else:
+            # make sure to convert time zones to UTC
+            untilSerialize = lambda x: dateTimeToString(x, True)
+
+        for name in DATESANDRULES:
+            if name in self.contents:
+                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 = six.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 (
+                                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(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 <= 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._bynmonthday is not None and len(rule._bynmonthday) > 0:
+                        values.setdefault('BYMONTHDAY', []).extend(str(n) for n in rule._bynmonthday)
+
+                    if rule._bymonth is not None and len(rule._bymonth) > 0:
+                        if (rule._byweekday is not None or
+                            len(rule._bynweekday or ()) > 0 or
+                            not (rule._freq == 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.items():
+                        buf.write(';')
+                        buf.write(key)
+                        buf.write('=')
+                        buf.write(','.join(paramvals))
+
+                    self.add(name).value = buf.getvalue()
+
+    rruleset = property(getrruleset, setrruleset)
+
+    def __setattr__(self, name, value):
+        """
+        For convenience, make self.contents directly accessible.
+        """
+        if name == 'rruleset':
+            self.setrruleset(value)
+        else:
+            super(RecurringComponent, self).__setattr__(name, value)
+
+
+class TextBehavior(behavior.Behavior):
+    """
+    Provide backslash escape encoding/decoding for single valued properties.
+
+    TextBehavior also deals with base64 encoding if the ENCODING parameter is
+    explicitly set to BASE64.
+    """
+    base64string = 'BASE64' # vCard uses B
+
+    @classmethod
+    def decode(cls, line):
+        """
+        Remove backslash escaping from line.value.
+        """
+        if line.encoded:
+            encoding = getattr(line, 'encoding_param', None)
+            if encoding and encoding.upper() == cls.base64string:
+                line.value = codecs.decode(self.value.encode("utf-8"), "base64").decode(encoding)
+            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 = codecs.encode(self.value.encode(encoding), "base64").decode("utf-8")
+            else:
+                line.value = backslashEscape(str_(line.value))
+            line.encoded=True
+
+
+class VCalendarComponentBehavior(behavior.Behavior):
+    defaultBehavior = TextBehavior
+    isComponent = True
+
+
+class RecurringBehavior(VCalendarComponentBehavior):
+    """
+    Parent Behavior for components which should be RecurringComponents.
+    """
+    hasNative = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """
+        Turn a recurring Component into a RecurringComponent.
+        """
+        if not obj.isNative:
+            object.__setattr__(obj, '__class__', RecurringComponent)
+            obj.isNative = True
+        return obj
+
+    @staticmethod
+    def transformFromNative(obj):
+        if obj.isNative:
+            object.__setattr__(obj, '__class__', Component)
+            obj.isNative = False
+        return obj
+
+    @staticmethod
+    def generateImplicitParameters(obj):
+        """
+        Generate a UID if one does not exist.
+
+        This is just a dummy implementation, for now.
+        """
+        if not hasattr(obj, 'uid'):
+            rand = int(random.random() * 100000)
+            now = datetime.datetime.now(utc)
+            now = dateTimeToString(now)
+            host = socket.gethostname()
+            obj.add(ContentLine('UID', [], "{0} - {1}@{2}".format(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=obj.value
+        # we're cheating a little here, parseDtstart allows DATE
+        obj.value=parseDtstart(obj)
+        if obj.value.tzinfo is None:
+            obj.params['X-VOBJ-FLOATINGTIME-ALLOWED'] = ['TRUE']
+        if obj.params.get('TZID'):
+            # Keep a copy of the original TZID around
+            obj.params['X-VOBJ-ORIGINAL-TZID'] = [obj.params['TZID']]
+            del obj.params['TZID']
+        return obj
+
+    @classmethod
+    def transformFromNative(cls, obj):
+        """
+        Replace the datetime in obj.value with an ISO 8601 string.
+        """
+        if obj.isNative:
+            obj.isNative = False
+            tzid = TimezoneComponent.registerTzinfo(obj.value.tzinfo)
+            obj.value = dateTimeToString(obj.value, cls.forceUTC)
+            if not cls.forceUTC and tzid is not None:
+                obj.tzid_param = tzid
+            if obj.params.get('X-VOBJ-ORIGINAL-TZID'):
+                if not hasattr(obj, 'tzid_param'):
+                    obj.tzid_param = obj.x_vobj_original_tzid_param
+                del obj.params['X-VOBJ-ORIGINAL-TZID']
+
+        return obj
+
+
+class UTCDateTimeBehavior(DateTimeBehavior):
+    """
+    A value which must be specified in UTC.
+    """
+    forceUTC = True
+
+
+class DateOrDateTimeBehavior(behavior.Behavior):
+    """
+    Parent Behavior for ContentLines containing one DATE or DATE-TIME.
+    """
+    hasNative = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """
+        Turn obj.value into a date or datetime.
+        """
+        if obj.isNative:
+            return obj
+        obj.isNative = True
+        if obj.value == '':
+            return obj
+        obj.value=obj.value
+        obj.value=parseDtstart(obj, allowSignatureMismatch=True)
+        if getattr(obj, 'value_param', 'DATE-TIME').upper() == 'DATE-TIME':
+            if hasattr(obj, 'tzid_param'):
+                # Keep a copy of the original TZID around
+                obj.params['X-VOBJ-ORIGINAL-TZID'] = [obj.tzid_param]
+                del obj.tzid_param
+        return obj
+
+    @staticmethod
+    def transformFromNative(obj):
+        """
+        Replace the date or datetime in obj.value with an ISO 8601 string.
+        """
+        if type(obj.value) == datetime.date:
+            obj.isNative = False
+            obj.value_param = 'DATE'
+            obj.value = dateToString(obj.value)
+            return obj
+        else:
+            return DateTimeBehavior.transformFromNative(obj)
+
+
+class MultiDateBehavior(behavior.Behavior):
+    """
+    Parent Behavior for ContentLines containing one or more DATE, DATE-TIME, or
+    PERIOD.
+    """
+    hasNative = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """
+        Turn obj.value into a list of dates, datetimes, or
+        (datetime, timedelta) tuples.
+        """
+        if obj.isNative:
+            return obj
+        obj.isNative = True
+        if obj.value == '':
+            obj.value = []
+            return obj
+        tzinfo = getTzid(getattr(obj, 'tzid_param', None))
+        valueParam = getattr(obj, 'value_param', "DATE-TIME").upper()
+        valTexts = obj.value.split(",")
+        if valueParam == "DATE":
+            obj.value = [stringToDate(x) for x in valTexts]
+        elif valueParam == "DATE-TIME":
+            obj.value = [stringToDateTime(x, tzinfo) for x in valTexts]
+        elif valueParam == "PERIOD":
+            obj.value = [stringToPeriod(x, tzinfo) for x in valTexts]
+        return obj
+
+    @staticmethod
+    def transformFromNative(obj):
+        """
+        Replace the date, datetime or period tuples in obj.value with
+        appropriate strings.
+        """
+        if obj.value and type(obj.value[0]) == datetime.date:
+            obj.isNative = False
+            obj.value_param = 'DATE'
+            obj.value = ','.join([dateToString(val) for val in obj.value])
+            return obj
+        # Fixme: handle PERIOD case
+        else:
+            if obj.isNative:
+                obj.isNative = False
+                transformed = []
+                tzid = None
+                for val in obj.value:
+                    if tzid is None and type(val) == datetime.datetime:
+                        tzid = TimezoneComponent.registerTzinfo(val.tzinfo)
+                        if tzid is not None:
+                            obj.tzid_param = tzid
+                    transformed.append(dateTimeToString(val))
+                obj.value = ','.join(transformed)
+            return obj
+
+
+class MultiTextBehavior(behavior.Behavior):
+    """
+    Provide backslash escape encoding/decoding of each of several values.
+
+    After transformation, value is a list of strings.
+    """
+    listSeparator = ","
+
+    @classmethod
+    def decode(cls, line):
+        """
+        Remove backslash escaping from line.value, then split on commas.
+        """
+        if line.encoded:
+            line.value = stringToTextValues(line.value,
+                listSeparator=cls.listSeparator)
+            line.encoded=False
+
+    @classmethod
+    def encode(cls, line):
+        """
+        Backslash escape line.value.
+        """
+        if not line.encoded:
+            line.value = cls.listSeparator.join(backslashEscape(val)
+                                                for val in line.value)
+            line.encoded=True
+
+
+class SemicolonMultiTextBehavior(MultiTextBehavior):
+    listSeparator = ";"
+
+
+# ------------------------ Registered Behavior subclasses ----------------------
+class VCalendar2_0(VCalendarComponentBehavior):
+    """
+    vCalendar 2.0 behavior. With added VAVAILABILITY support.
+    """
+    name = 'VCALENDAR'
+    description = 'vCalendar 2.0, also known as iCalendar.'
+    versionString = '2.0'
+    sortFirst = ('version', 'calscale', 'method', 'prodid', 'vtimezone')
+    knownChildren = {'CALSCALE':      (0, 1, None),  # min, max, behaviorRegistry id
+                     'METHOD':        (0, 1, None),
+                     'VERSION':       (0, 1, None),  # required, but auto-generated
+                     'PRODID':        (1, 1, None),
+                     'VTIMEZONE':     (0, None, None),
+                     'VEVENT':        (0, None, None),
+                     'VTODO':         (0, None, None),
+                     'VJOURNAL':      (0, None, None),
+                     'VFREEBUSY':     (0, None, None),
+                     'VAVAILABILITY': (0, None, None),
+                    }
+
+    @classmethod
+    def generateImplicitParameters(cls, obj):
+        """
+        Create PRODID, VERSION, and VTIMEZONEs if needed.
+
+        VTIMEZONEs will need to exist whenever TZID parameters exist or when
+        datetimes with tzinfo exist.
+        """
+        for comp in obj.components():
+            if comp.behavior is not None:
+                comp.behavior.generateImplicitParameters(comp)
+        if not hasattr(obj, 'prodid'):
+            obj.add(ContentLine('PRODID', [], PRODID))
+        if not hasattr(obj, 'version'):
+            obj.add(ContentLine('VERSION', [], cls.versionString))
+        tzidsUsed = {}
+
+        def findTzids(obj, table):
+            if isinstance(obj, ContentLine) and (obj.behavior is None or
+                                                 not obj.behavior.forceUTC):
+                if getattr(obj, 'tzid_param', None):
+                    table[obj.tzid_param] = 1
+                else:
+                    if type(obj.value) == list:
+                        for item in obj.value:
+                            tzinfo = getattr(obj.value, 'tzinfo', None)
+                            tzid = TimezoneComponent.registerTzinfo(tzinfo)
+                            if tzid:
+                                table[tzid] = 1
+                    else:
+                        tzinfo = getattr(obj.value, 'tzinfo', None)
+                        tzid = TimezoneComponent.registerTzinfo(tzinfo)
+                        if tzid:
+                            table[tzid] = 1
+            for child in obj.getChildren():
+                if obj.name != 'VTIMEZONE':
+                    findTzids(child, table)
+
+        findTzids(obj, tzidsUsed)
+        oldtzids = [toUnicode(x.tzid.value) for x in getattr(obj, 'vtimezone_list', [])]
+        for tzid in tzidsUsed.keys():
+            tzid = toUnicode(tzid)
+            if tzid != u'UTC' and tzid not in oldtzids:
+                obj.add(TimezoneComponent(tzinfo=getTzid(tzid)))
+registerBehavior(VCalendar2_0)
+
+
+class VTimezone(VCalendarComponentBehavior):
+    """
+    Timezone behavior.
+    """
+    name = 'VTIMEZONE'
+    hasNative = True
+    description = 'A grouping of component properties that defines a time zone.'
+    sortFirst = ('tzid', 'last-modified', 'tzurl', 'standard', 'daylight')
+    knownChildren = {'TZID':         (1, 1, None),  # min, max, behaviorRegistry id
+                     'LAST-MODIFIED':(0, 1, None),
+                     'TZURL':        (0, 1, None),
+                     'STANDARD':     (0, None, None),  # NOTE: One of Standard or
+                     'DAYLIGHT':     (0, None, None)  # Daylight must appear
+                    }
+
+    @classmethod
+    def validate(cls, obj, raiseException, *args):
+        if not hasattr(obj, 'tzid') or obj.tzid.value is None:
+            if raiseException:
+                m = "VTIMEZONE components must contain a valid TZID"
+                raise ValidateError(m)
+            return False
+        if 'standard' in obj.contents or 'daylight' in obj.contents:
+            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 TZID(behavior.Behavior):
+    """
+    Don't use TextBehavior for TZID.
+
+    RFC2445 only allows TZID lines to be paramtext, so they shouldn't need any
+    encoding or decoding.  Unfortunately, some Microsoft products use commas
+    in TZIDs which should NOT be treated as a multi-valued text property, nor
+    do we want to escape them.  Leaving them alone works for Microsoft's breakage,
+    and doesn't affect compliant iCalendar streams.
+    """
+registerBehavior(TZID)
+
+
+class DaylightOrStandard(VCalendarComponentBehavior):
+    hasNative = False
+    knownChildren = {'DTSTART':      (1, 1, None),  # min, max, behaviorRegistry id
+                     'RRULE':        (0, 1, None)}
+
+registerBehavior(DaylightOrStandard, 'STANDARD')
+registerBehavior(DaylightOrStandard, 'DAYLIGHT')
+
+
+class VEvent(RecurringBehavior):
+    """
+    Event behavior.
+    """
+    name='VEVENT'
+    sortFirst = ('uid', 'recurrence-id', 'dtstart', 'duration', 'dtend')
+
+    description='A grouping of component properties, and possibly including \
+                 "VALARM" calendar components, that represents a scheduled \
+                 amount of time on a calendar.'
+    knownChildren = {'DTSTART':      (0, 1, None),  # min, max, behaviorRegistry id
+                     'CLASS':        (0, 1, None),
+                     'CREATED':      (0, 1, None),
+                     'DESCRIPTION':  (0, 1, None),
+                     'GEO':          (0, 1, None),
+                     'LAST-MODIFIED':(0, 1, None),
+                     'LOCATION':     (0, 1, None),
+                     'ORGANIZER':    (0, 1, None),
+                     'PRIORITY':     (0, 1, None),
+                     'DTSTAMP':      (0, 1, None),
+                     'SEQUENCE':     (0, 1, None),
+                     'STATUS':       (0, 1, None),
+                     'SUMMARY':      (0, 1, None),
+                     'TRANSP':       (0, 1, None),
+                     'UID':          (1, 1, None),
+                     'URL':          (0, 1, None),
+                     'RECURRENCE-ID':(0, 1, None),
+                     'DTEND':        (0, 1, None),  # NOTE: Only one of DtEnd or
+                     'DURATION':     (0, 1, None),  # Duration can appear
+                     'ATTACH':       (0, None, None),
+                     'ATTENDEE':     (0, None, None),
+                     'CATEGORIES':   (0, None, None),
+                     'COMMENT':      (0, None, None),
+                     'CONTACT':      (0, None, None),
+                     'EXDATE':       (0, None, None),
+                     'EXRULE':       (0, None, None),
+                     'REQUEST-STATUS': (0, None, None),
+                     'RELATED-TO':   (0, None, None),
+                     'RESOURCES':    (0, None, None),
+                     'RDATE':        (0, None, None),
+                     'RRULE':        (0, None, None),
+                     'VALARM':       (0, None, None)
+                    }
+
+    @classmethod
+    def validate(cls, obj, raiseException, *args):
+        if 'dtend' in obj.contents and 'duration' in obj.contents:
+            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 'due' in obj.contents and 'duration' in obj.contents:
+            if raiseException:
+                m = "VTODO components cannot contain both DUE and DURATION\
+                     components"
+                raise ValidateError(m)
+            return False
+        else:
+            return super(VTodo, cls).validate(obj, raiseException, *args)
+
+registerBehavior(VTodo)
+
+
+class VJournal(RecurringBehavior):
+    """
+    Journal entry behavior.
+    """
+    name='VJOURNAL'
+    knownChildren = {'DTSTART':      (0, 1, None),  # min, max, behaviorRegistry id
+                     'CLASS':        (0, 1, None),
+                     'CREATED':      (0, 1, None),
+                     'DESCRIPTION':  (0, 1, None),
+                     'LAST-MODIFIED':(0, 1, None),
+                     'ORGANIZER':    (0, 1, None),
+                     'DTSTAMP':      (0, 1, None),
+                     'SEQUENCE':     (0, 1, None),
+                     'STATUS':       (0, 1, None),
+                     'SUMMARY':      (0, 1, None),
+                     'UID':          (0, 1, None),
+                     'URL':          (0, 1, None),
+                     'RECURRENCE-ID':(0, 1, None),
+                     'ATTACH':       (0, None, None),
+                     'ATTENDEE':     (0, None, None),
+                     'CATEGORIES':   (0, None, None),
+                     'COMMENT':      (0, None, None),
+                     'CONTACT':      (0, None, None),
+                     'EXDATE':       (0, None, None),
+                     'EXRULE':       (0, None, None),
+                     'REQUEST-STATUS': (0, None, None),
+                     'RELATED-TO':   (0, None, None),
+                     'RDATE':        (0, None, None),
+                     'RRULE':        (0, None, None)
+                    }
+registerBehavior(VJournal)
+
+
+class VFreeBusy(VCalendarComponentBehavior):
+    """
+    Free/busy state behavior.
+    """
+    name='VFREEBUSY'
+    description='A grouping of component properties that describe either a \
+                 request for free/busy time, describe a response to a request \
+                 for free/busy time or describe a published set of busy time.'
+    sortFirst = ('uid', 'dtstart', 'duration', 'dtend')
+    knownChildren = {'DTSTART':      (0, 1, None),  # min, max, behaviorRegistry id
+                     'CONTACT':      (0, 1, None),
+                     'DTEND':        (0, 1, None),
+                     'DURATION':     (0, 1, None),
+                     'ORGANIZER':    (0, 1, None),
+                     'DTSTAMP':      (0, 1, None),
+                     'UID':          (0, 1, None),
+                     'URL':          (0, 1, None),
+                     'ATTENDEE':     (0, None, None),
+                     'COMMENT':      (0, None, None),
+                     'FREEBUSY':     (0, None, None),
+                     'REQUEST-STATUS': (0, None, None)
+                    }
+registerBehavior(VFreeBusy)
+
+
+class VAlarm(VCalendarComponentBehavior):
+    """
+    Alarm behavior.
+    """
+    name='VALARM'
+    description='Alarms describe when and how to provide alerts about events \
+                 and to-dos.'
+    knownChildren = {'ACTION':       (1, 1, None),  # min, max, behaviorRegistry id
+                     'TRIGGER':      (1, 1, None),
+                     'DURATION':     (0, 1, None),
+                     'REPEAT':       (0, 1, None),
+                     'DESCRIPTION':  (0, 1, None)
+                    }
+
+    @staticmethod
+    def generateImplicitParameters(obj):
+        """
+        Create default ACTION and TRIGGER if they're not set.
+        """
+        try:
+            obj.action
+        except AttributeError:
+            obj.add('action').value = 'AUDIO'
+        try:
+            obj.trigger
+        except AttributeError:
+            obj.add('trigger').value = datetime.timedelta(0)
+
+
+    @classmethod
+    def validate(cls, obj, raiseException, *args):
+        """
+        # TODO
+        if obj.contents.has_key('dtend') and obj.contents.has_key('duration'):
+            if raiseException:
+                m = "VEVENT components cannot contain both DTEND and DURATION\
+                     components"
+                raise ValidateError(m)
+            return False
+        else:
+            return super(VEvent, cls).validate(obj, raiseException, *args)
+        """
+        return True
+registerBehavior(VAlarm)
+
+
+class VAvailability(VCalendarComponentBehavior):
+    """
+    Availability state behavior.
+
+    Used to represent user's available time slots.
+    """
+    name='VAVAILABILITY'
+    description='A component used to represent a user\'s available time slots.'
+    sortFirst = ('uid', 'dtstart', 'duration', 'dtend')
+    knownChildren = {'UID':           (1, 1, None),  # min, max, behaviorRegistry id
+                     'DTSTAMP':       (1, 1, None),
+                     'BUSYTYPE':      (0, 1, None),
+                     'CREATED':       (0, 1, None),
+                     'DTSTART':       (0, 1, None),
+                     'LAST-MODIFIED': (0, 1, None),
+                     'ORGANIZER':     (0, 1, None),
+                     'SEQUENCE':      (0, 1, None),
+                     'SUMMARY':       (0, 1, None),
+                     'URL':           (0, 1, None),
+                     'DTEND':         (0, 1, None),
+                     'DURATION':      (0, 1, None),
+                     'CATEGORIES':    (0, None, None),
+                     'COMMENT':       (0, None, None),
+                     'CONTACT':       (0, None, None),
+                     'AVAILABLE':     (0, None, None),
+                    }
+
+    @classmethod
+    def validate(cls, obj, raiseException, *args):
+        if 'dtend' in obj.contents and 'duration' in obj.contents:
+            if raiseException:
+                m = "VAVAILABILITY components cannot contain both DTEND and DURATION components"
+                raise ValidateError(m)
+            return False
+        else:
+            return super(VAvailability, cls).validate(obj, raiseException, *args)
+registerBehavior(VAvailability)
+
+
+class Available(RecurringBehavior):
+    """
+    Event behavior.
+    """
+    name='AVAILABLE'
+    sortFirst = ('uid', 'recurrence-id', 'dtstart', 'duration', 'dtend')
+
+    description='Defines a period of time in which a user is normally available.'
+    knownChildren = {'DTSTAMP':      (1, 1, None),  # min, max, behaviorRegistry id
+                     'DTSTART':      (1, 1, None),
+                     'UID':          (1, 1, None),
+                     'DTEND':        (0, 1, None),  # NOTE: One of DtEnd or
+                     'DURATION':     (0, 1, None),  # Duration must appear, but not both
+                     'CREATED':      (0, 1, None),
+                     'LAST-MODIFIED':(0, 1, None),
+                     'RECURRENCE-ID':(0, 1, None),
+                     'RRULE':        (0, 1, None),
+                     'SUMMARY':      (0, 1, None),
+                     'CATEGORIES':   (0, None, None),
+                     'COMMENT':      (0, None, None),
+                     'CONTACT':      (0, None, None),
+                     'EXDATE':       (0, None, None),
+                     'RDATE':        (0, None, None),
+                    }
+
+    @classmethod
+    def validate(cls, obj, raiseException, *args):
+        has_dtend = 'dtend' in obj.contents
+        has_duration = 'duration' in obj.contents
+        if has_dtend and has_duration:
+            if raiseException:
+                m = "AVAILABLE components cannot contain both DTEND and DURATION\
+                     properties"
+                raise ValidateError(m)
+            return False
+        elif not (has_dtend or has_duration):
+            if raiseException:
+                m = "AVAILABLE components must contain one of DTEND or DURATION\
+                     properties"
+                raise ValidateError(m)
+            return False
+        else:
+            return super(Available, cls).validate(obj, raiseException, *args)
+registerBehavior(Available)
+
+
+class Duration(behavior.Behavior):
+    """
+    Behavior for Duration ContentLines.  Transform to datetime.timedelta.
+    """
+    name = 'DURATION'
+    hasNative = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """
+        Turn obj.value into a datetime.timedelta.
+        """
+        if obj.isNative:
+            return obj
+        obj.isNative = True
+        obj.value=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.warning("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.
+    """
+    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, 'FREEBUSY')
+
+
+class RRule(behavior.Behavior):
+    """
+    Dummy behavior to avoid having RRULEs being treated as text lines (and thus
+    having semi-colons inaccurately escaped).
+    """
+registerBehavior(RRule, 'RRULE')
+registerBehavior(RRule, 'EXRULE')
+
+
+# ------------------------ Registration of common classes ----------------------
+utcDateTimeList = ['LAST-MODIFIED', 'CREATED', 'COMPLETED', 'DTSTAMP']
+list(map(lambda x: registerBehavior(UTCDateTimeBehavior, x), utcDateTimeList))
+
+dateTimeOrDateList = ['DTEND', 'DTSTART', 'DUE', 'RECURRENCE-ID']
+list(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', 'BUSYTYPE']
+list(map(lambda x: registerBehavior(TextBehavior, x), textList))
+
+list(map(lambda x: registerBehavior(MultiTextBehavior, x), ['CATEGORIES',
+                                                            'RESOURCES']))
+registerBehavior(SemicolonMultiTextBehavior, 'REQUEST-STATUS')
+
+
+# ------------------------ 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 ical DURATION.
+    """
+    if delta.days == 0:
+        sign = 1
+    else:
+        sign = delta.days / abs(delta.days)
+    delta = abs(delta)
+    days = delta.days
+    hours = int(delta.seconds / 3600)
+    minutes = int((delta.seconds % 3600) / 60)
+    seconds = int(delta.seconds % 60)
+
+    output = ''
+    if sign == -1:
+        output += '-'
+    output += 'P'
+    if days:
+        output += '{}D'.format(days)
+    if hours or minutes or seconds:
+        output += 'T'
+    elif not days:  # Deal with zero duration
+        output += 'T0S'
+    if hours:
+        output += '{}H'.format(hours)
+    if minutes:
+        output += '{}M'.format(minutes)
+    if seconds:
+        output += '{}S'.format(seconds)
+    return output
+
+def timeToString(dateOrDateTime):
+    """
+    Wraps dateToString and dateTimeToString, returning the results
+    of either based on the type of the argument
+    """
+    if hasattr(dateOrDateTime, 'hour'):
+        return dateTimeToString(dateOrDateTime)
+    return dateToString(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)
+
+    datestr = "{0}{1}{2}T{3}{4}{5}".format(
+        numToDigits( dateTime.year, 4),
+        numToDigits( dateTime.month, 2),
+        numToDigits( dateTime.day, 2),
+        numToDigits( dateTime.hour, 2),
+        numToDigits( dateTime.minute, 2),
+        numToDigits( dateTime.second, 2),
+    )
+    if tzinfo_eq(dateTime.tzinfo, utc):
+        datestr += "Z"
+    return datestr
+
+def deltaToOffset(delta):
+    absDelta = abs(delta)
+    hours = int(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 = s.upper()
+    return (s.find("P") != -1) and (s.find("P") < 2)
+
+def stringToDate(s):
+    year  = int( s[0:4] )
+    month = int( s[4:6] )
+    day   = int( s[6:8] )
+    return datetime.date(year,month,day)
+
+def stringToDateTime(s, tzinfo=None):
+    """
+    Returns datetime.datetime object.
+    """
+    try:
+        year   = int( s[0:4] )
+        month  = int( s[4:6] )
+        day    = int( s[6:8] )
+        hour   = int( s[9:11] )
+        minute = int( s[11:13] )
+        second = int( s[13:15] )
+        if len(s) > 15:
+            if s[15] == 'Z':
+                tzinfo = utc
+    except:
+        raise ParseError("'{0!s}' is not a valid DATE-TIME".format(s))
+    year = year and year or 2000
+    return datetime.datetime(year, month, day, hour, minute, second, 0, tzinfo)
+
+
+# DQUOTE included to work around iCal's penchant for backslash escaping it,
+# although it isn't actually supposed to be escaped according to rfc2445 TEXT
+escapableCharList = '\\;,Nn"'
+
+def stringToTextValues(s, listSeparator=',', charList=None, strict=False):
+    """
+    Returns list of strings.
+    """
+    if charList is None:
+        charList = escapableCharList
+
+    def escapableChar (c):
+        return c in charList
+
+    def error(msg):
+        if strict:
+            raise ParseError(msg)
+        else:
+            logging.error(msg)
+
+    # vars which control state machine
+    charIterator = enumerate(s)
+    state        = "read normal"
+
+    current = []
+    results = []
+
+    while True:
+        try:
+            charIndex, char = next(charIterator)
+        except:
+            char = "eof"
+
+        if state == "read normal":
+            if char == '\\':
+                state = "read escaped char"
+            elif char == listSeparator:
+                state = "read normal"
+                current = "".join(current)
+                results.append(current)
+                current = []
+            elif char == "eof":
+                state = "end"
+            else:
+                state = "read normal"
+                current.append(char)
+
+        elif state == "read escaped char":
+            if escapableChar(char):
+                state = "read normal"
+                if char in 'nN':
+                    current.append('\n')
+                else:
+                    current.append(char)
+            else:
+                state = "read normal"
+                # leave unrecognized escaped characters for later passes
+                current.append('\\' + char)
+
+        elif state == "end":  # an end state
+            if len(current) or len(results) == 0:
+                current = "".join(current)
+                results.append(current)
+            return results
+
+        elif state == "error":  # an end state
+            return results
+
+        else:
+            state = "error"
+            error("unknown state: '{0!s}' reached in {1!s}".format(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)
+
+    # 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 = next(charIterator)
+        except:
+            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"
+                error("got unexpected character {0} reading in duration: {1}"
+                      .format(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
+            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("unknown state: '{0!s}' reached in {1!s}".format(state, s))
+
+
+def parseDtstart(contentline, allowSignatureMismatch=False):
+    """
+    Convert a contentline's value into a date or date-time.
+
+    A variety of clients don't serialize dates with the appropriate VALUE
+    parameter, so rather than failing on these (technically invalid) lines,
+    if allowSignatureMismatch is True, try to parse both varieties.
+    """
+    tzinfo = getTzid(getattr(contentline, 'tzid_param', None))
+    valueParam = getattr(contentline, 'value_param', 'DATE-TIME').upper()
+    if valueParam == "DATE":
+        return stringToDate(contentline.value)
+    elif valueParam == "DATE-TIME":
+        try:
+            return stringToDateTime(contentline.value, tzinfo)
+        except:
+            if allowSignatureMismatch:
+                return stringToDate(contentline.value)
+            else:
+                raise
+
+def stringToPeriod(s, tzinfo=None):
+    values = s.split("/")
+    start = stringToDateTime(values[0], tzinfo)
+    valEnd   = values[1]
+    if isDuration(valEnd):  # period-start = date-time "/" dur-value
+        delta = stringToDurations(valEnd)[0]
+        return (start, delta)
+    else:
+        return (start, stringToDateTime(valEnd, tzinfo))
+
+
+def getTransition(transitionTo, year, tzinfo):
+    """
+    Return the datetime of the transition to/from DST, or None.
+    """
+    def firstTransition(iterDates, test):
+        """
+        Return the last date not matching test, or None if all tests matched.
+        """
+        success = None
+        for dt in iterDates:
+            if not test(dt):
+                success = dt
+            else:
+                if success is not None:
+                    return success
+        return success # may be None
+
+    def generateDates(year, month=None, day=None):
+        """
+        Iterate over possible dates with unspecified values.
+        """
+        months = range(1, 13)
+        days   = range(1, 32)
+        hours  = range(0, 24)
+        if month is None:
+            for month in months:
+                yield datetime.datetime(year, month, 1)
+        elif day is None:
+            for day in days:
+                try:
+                    yield datetime.datetime(year, month, day)
+                except ValueError:
+                    pass
+        else:
+            for hour in hours:
+                yield datetime.datetime(year, month, day, hour)
+
+    assert transitionTo in ('daylight', 'standard')
+    if transitionTo == 'daylight':
+        def test(dt):
+            try:
+                return tzinfo.dst(dt) != zeroDelta
+            except pytz.NonExistentTimeError:
+                return True  # entering daylight time
+            except pytz.AmbiguousTimeError:
+                return False  # entering standard time
+    elif transitionTo == 'standard':
+        def test(dt):
+            try:
+                return tzinfo.dst(dt) == zeroDelta
+            except pytz.NonExistentTimeError:
+                return False  # entering daylight time
+            except pytz.AmbiguousTimeError:
+                return True  # entering standard time
+    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 range(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()

+ 221 - 0
src/vobject/docs/build/lib/vobject/ics_diff.py

@@ -0,0 +1,221 @@
+from __future__ import print_function
+
+from optparse import OptionParser
+
+from .base import Component, getBehavior, newFromBehavior, readOne
+
+"""
+Compare VTODOs and VEVENTs in two iCalendar sources.
+"""
+
+def getSortKey(component):
+    def getUID(component):
+        return component.getChildValue('uid', '')
+
+    # it's not quite as simple as getUID, need to account for recurrenceID and
+    # sequence
+
+    def getSequence(component):
+        sequence = component.getChildValue('sequence', 0)
+        return "{0:05d}".format(int(sequence))
+
+    def getRecurrenceID(component):
+        recurrence_id = component.getChildValue('recurrence_id', None)
+        if recurrence_id is None:
+            return '0000-00-00'
+        else:
+            return recurrence_id.isoformat()
+
+    return getUID(component) + getSequence(component) + getRecurrenceID(component)
+
+def sortByUID(components):
+    return sorted(components, key=getSortKey)
+
+def deleteExtraneous(component, ignore_dtstamp=False):
+    """
+    Recursively walk the component's children, deleting extraneous details like
+    X-VOBJ-ORIGINAL-TZID.
+    """
+    for comp in component.components():
+        deleteExtraneous(comp, ignore_dtstamp)
+    for line in component.lines():
+        if line.params.has_key('X-VOBJ-ORIGINAL-TZID'):
+            del line.params['X-VOBJ-ORIGINAL-TZID']
+    if ignore_dtstamp and hasattr(component, 'dtstamp_list'):
+        del component.dtstamp_list
+
+def diff(left, right):
+    """
+    Take two VCALENDAR components, compare VEVENTs and VTODOs in them,
+    return a list of object pairs containing just UID and the bits
+    that didn't match, using None for objects that weren't present in one
+    version or the other.
+
+    When there are multiple ContentLines in one VEVENT, for instance many
+    DESCRIPTION lines, such lines original order is assumed to be
+    meaningful.  Order is also preserved when comparing (the unlikely case
+    of) multiple parameters of the same type in a ContentLine
+
+    """
+
+    def processComponentLists(leftList, rightList):
+        output = []
+        rightIndex = 0
+        rightListSize = len(rightList)
+
+        for comp in leftList:
+            if rightIndex >= rightListSize:
+                output.append((comp, None))
+            else:
+                leftKey  = getSortKey(comp)
+                rightComp = rightList[rightIndex]
+                rightKey = getSortKey(rightComp)
+                while leftKey > rightKey:
+                    output.append((None, rightComp))
+                    rightIndex += 1
+                    if rightIndex >= rightListSize:
+                        output.append((comp, None))
+                        break
+                    else:
+                        rightComp = rightList[rightIndex]
+                        rightKey = getSortKey(rightComp)
+
+                if leftKey < rightKey:
+                    output.append((comp, None))
+                elif leftKey == rightKey:
+                    rightIndex += 1
+                    matchResult = processComponentPair(comp, rightComp)
+                    if matchResult is not None:
+                        output.append(matchResult)
+
+        return output
+
+    def newComponent(name, body):
+        if body is None:
+            return None
+        else:
+            c = Component(name)
+            c.behavior = getBehavior(name)
+            c.isNative = True
+            return c
+
+    def processComponentPair(leftComp, rightComp):
+        """
+        Return None if a match, or a pair of components including UIDs and
+        any differing children.
+
+        """
+        leftChildKeys = leftComp.contents.keys()
+        rightChildKeys = rightComp.contents.keys()
+
+        differentContentLines = []
+        differentComponents = {}
+
+        for key in leftChildKeys:
+            rightList = rightComp.contents.get(key, [])
+            if isinstance(leftComp.contents[key][0], Component):
+                compDifference = processComponentLists(leftComp.contents[key],
+                                                       rightList)
+                if len(compDifference) > 0:
+                    differentComponents[key] = compDifference
+
+            elif leftComp.contents[key] != rightList:
+                differentContentLines.append((leftComp.contents[key],
+                                              rightList))
+
+        for key in rightChildKeys:
+            if key not in leftChildKeys:
+                if isinstance(rightComp.contents[key][0], Component):
+                    differentComponents[key] = ([], rightComp.contents[key])
+                else:
+                    differentContentLines.append(([], rightComp.contents[key]))
+
+        if len(differentContentLines) == 0 and len(differentComponents) == 0:
+            return None
+        else:
+            left  = newFromBehavior(leftComp.name)
+            right = newFromBehavior(leftComp.name)
+            # add a UID, if one existed, despite the fact that they'll always be
+            # the same
+            uid = leftComp.getChildValue('uid')
+            if uid is not None:
+                left.add( 'uid').value = uid
+                right.add('uid').value = uid
+
+            for name, childPairList in differentComponents.items():
+                leftComponents, rightComponents = zip(*childPairList)
+                if len(leftComponents) > 0:
+                    # filter out None
+                    left.contents[name] = filter(None, leftComponents)
+                if len(rightComponents) > 0:
+                    # filter out None
+                    right.contents[name] = filter(None, rightComponents)
+
+            for leftChildLine, rightChildLine in differentContentLines:
+                nonEmpty = leftChildLine or rightChildLine
+                name = nonEmpty[0].name
+                if leftChildLine is not None:
+                    left.contents[name] = leftChildLine
+                if rightChildLine is not None:
+                    right.contents[name] = rightChildLine
+
+            return left, right
+
+
+    vevents = processComponentLists(sortByUID(getattr(left, 'vevent_list', [])),
+                                    sortByUID(getattr(right, 'vevent_list', [])))
+
+    vtodos = processComponentLists(sortByUID(getattr(left, 'vtodo_list', [])),
+                                   sortByUID(getattr(right, 'vtodo_list', [])))
+
+    return vevents + vtodos
+
+def prettyDiff(leftObj, rightObj):
+    for left, right in diff(leftObj, rightObj):
+        print("<<<<<<<<<<<<<<<")
+        if left is not None:
+            left.prettyPrint()
+        print("===============")
+        if right is not None:
+            right.prettyPrint()
+        print(">>>>>>>>>>>>>>>")
+        print
+
+
+def main():
+    options, args = getOptions()
+    if args:
+        ignore_dtstamp = options.ignore
+        ics_file1, ics_file2 = args
+        cal1 = readOne(file(ics_file1))
+        cal2 = readOne(file(ics_file2))
+        deleteExtraneous(cal1, ignore_dtstamp=ignore_dtstamp)
+        deleteExtraneous(cal2, ignore_dtstamp=ignore_dtstamp)
+        prettyDiff(cal1, cal2)
+
+version = "0.1"
+
+def getOptions():
+    ##### Configuration options #####
+
+    usage = "usage: %prog [options] ics_file1 ics_file2"
+    parser = OptionParser(usage=usage, version=version)
+    parser.set_description("ics_diff will print a comparison of two iCalendar files ")
+
+    parser.add_option("-i", "--ignore-dtstamp", dest="ignore", action="store_true",
+                      default=False, help="ignore DTSTAMP lines [default: False]")
+
+    (cmdline_options, args) = parser.parse_args()
+    if len(args) < 2:
+        print("error: too few arguments given")
+        print
+        print(parser.format_help())
+        return False, False
+
+    return cmdline_options, args
+
+if __name__ == "__main__":
+    try:
+        main()
+    except KeyboardInterrupt:
+        print("Aborted")

+ 356 - 0
src/vobject/docs/build/lib/vobject/vcard.py

@@ -0,0 +1,356 @@
+"""Definitions and behavior for vCard 3.0"""
+
+import codecs
+
+from . import behavior
+
+from .base import ContentLine, registerBehavior, backslashEscape
+from .icalendar import stringToTextValues
+
+
+# Python 3 no longer has a basestring type, so....
+try:
+    basestring = basestring
+except NameError:
+    basestring = (str,bytes)
+
+# ------------------------ vCard structs ---------------------------------------
+
+class Name(object):
+    def __init__(self, family = '', given = '', additional = '', prefix = '',
+                 suffix = ''):
+        """
+        Each name attribute can be a string or a list of strings.
+        """
+        self.family     = family
+        self.given      = given
+        self.additional = additional
+        self.prefix     = prefix
+        self.suffix     = suffix
+
+    @staticmethod
+    def toString(val):
+        """
+        Turn a string or array value into a string.
+        """
+        if type(val) in (list, tuple):
+            return ' '.join(val)
+        return val
+
+    def __str__(self):
+        eng_order = ('prefix', 'given', 'additional', 'family', 'suffix')
+        out = ' '.join(self.toString(getattr(self, val)) for val in eng_order)
+        return out
+
+    def __repr__(self):
+        return "<Name: {0!s}>".format(self.__str__())
+
+    def __eq__(self, other):
+        try:
+            return (self.family == other.family and
+                    self.given == other.given and
+                    self.additional == other.additional and
+                    self.prefix == other.prefix and
+                    self.suffix == other.suffix)
+        except:
+            return False
+
+
+class Address(object):
+    def __init__(self, street = '', city = '', region = '', code = '',
+                 country = '', box = '', extended = ''):
+        """
+        Each name attribute can be a string or a list of strings.
+        """
+        self.box      = box
+        self.extended = extended
+        self.street   = street
+        self.city     = city
+        self.region   = region
+        self.code     = code
+        self.country  = country
+
+    @staticmethod
+    def toString(val, join_char='\n'):
+        """
+        Turn a string or array value into a string.
+        """
+        if type(val) in (list, tuple):
+            return join_char.join(val)
+        return val
+
+    lines = ('box', 'extended', 'street')
+    one_line = ('city', 'region', 'code')
+
+    def __str__(self):
+        lines = '\n'.join(self.toString(getattr(self, val))
+                          for val in self.lines if getattr(self, val))
+        one_line = tuple(self.toString(getattr(self, val), ' ')
+                         for val in self.one_line)
+        lines += "\n{0!s}, {1!s} {2!s}".format(*one_line)
+        if self.country:
+            lines += '\n' + self.toString(self.country)
+        return lines
+
+    def __repr__(self):
+        return "<Address: {0!s}>".format(self)
+
+    def __eq__(self, other):
+        try:
+            return (self.box == other.box and
+                    self.extended == other.extended and
+                    self.street == other.street and
+                    self.city == other.city and
+                    self.region == other.region and
+                    self.code == other.code and
+                    self.country == other.country)
+        except:
+            return False
+
+
+# ------------------------ Registered Behavior subclasses ----------------------
+
+class VCardTextBehavior(behavior.Behavior):
+    """
+    Provide backslash escape encoding/decoding for single valued properties.
+
+    TextBehavior also deals with base64 encoding if the ENCODING parameter is
+    explicitly set to BASE64.
+    """
+    allowGroup = True
+    base64string = 'B'
+
+    @classmethod
+    def decode(cls, line):
+        """
+        Remove backslash escaping from line.valueDecode line, either to remove
+        backslash espacing, or to decode base64 encoding. The content line should
+        contain a ENCODING=b for base64 encoding, but Apple Addressbook seems to
+        export a singleton parameter of 'BASE64', which does not match the 3.0
+        vCard spec. If we encouter that, then we transform the parameter to
+        ENCODING=b
+        """
+        if line.encoded:
+            if 'BASE64' in line.singletonparams:
+                line.singletonparams.remove('BASE64')
+                line.encoding_param = cls.base64string
+            encoding = getattr(line, 'encoding_param', None)
+            if encoding:
+                line.value = codecs.decode(line.value.encode("utf-8"), "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 = codecs.encode(line.value.encode(coding), "base64").decode("utf-8")
+            else:
+                line.value = backslashEscape(line.value)
+            line.encoded=True
+
+
+class VCardBehavior(behavior.Behavior):
+    allowGroup = True
+    defaultBehavior = VCardTextBehavior
+
+
+class VCard3_0(VCardBehavior):
+    """
+    vCard 3.0 behavior.
+    """
+    name = 'VCARD'
+    description = 'vCard 3.0, defined in rfc2426'
+    versionString = '3.0'
+    isComponent = True
+    sortFirst = ('version', 'prodid', 'uid')
+    knownChildren = {'N':         (1, 1, None),  # min, max, behaviorRegistry id
+                     'FN':        (1, 1, None),
+                     'VERSION':   (1, 1, None),  # required, auto-generated
+                     'PRODID':    (0, 1, None),
+                     'LABEL':     (0, None, None),
+                     'UID':       (0, None, None),
+                     'ADR':       (0, None, None),
+                     'ORG':       (0, None, None),
+                     'PHOTO':     (0, None, None),
+                     'CATEGORIES':(0, None, None)
+                    }
+
+    @classmethod
+    def generateImplicitParameters(cls, obj):
+        """
+        Create PRODID, VERSION, and VTIMEZONEs if needed.
+
+        VTIMEZONEs will need to exist whenever TZID parameters exist or when
+        datetimes with tzinfo exist.
+        """
+        if not hasattr(obj, 'version'):
+            obj.add(ContentLine('VERSION', [], cls.versionString))
+registerBehavior(VCard3_0, default=True)
+
+
+class FN(VCardTextBehavior):
+    name = "FN"
+    description = 'Formatted name'
+registerBehavior(FN)
+
+class Label(VCardTextBehavior):
+    name = "Label"
+    description = 'Formatted address'
+registerBehavior(Label)
+
+wacky_apple_photo_serialize = True
+REALLY_LARGE = 1E50
+
+
+class Photo(VCardTextBehavior):
+    name = "Photo"
+    description = 'Photograph'
+    @classmethod
+    def valueRepr( cls, line ):
+        return " (BINARY PHOTO DATA at 0x{0!s}) ".format(id( line.value ))
+
+    @classmethod
+    def serialize(cls, obj, buf, lineLength, validate):
+        """
+        Apple's Address Book is *really* weird with images, it expects
+        base64 data to have very specific whitespace.  It seems Address Book
+        can handle PHOTO if it's not wrapped, so don't wrap it.
+        """
+        if wacky_apple_photo_serialize:
+            lineLength = REALLY_LARGE
+        VCardTextBehavior.serialize(obj, buf, lineLength, validate)
+
+registerBehavior(Photo)
+
+def toListOrString(string):
+    stringList = stringToTextValues(string)
+    if len(stringList) == 1:
+        return stringList[0]
+    else:
+        return stringList
+
+def splitFields(string):
+    """
+    Return a list of strings or lists from a Name or Address.
+    """
+    return [toListOrString(i) for i in
+            stringToTextValues(string, listSeparator=';', charList=';')]
+
+def toList(stringOrList):
+    if isinstance(stringOrList, basestring):
+        return [stringOrList]
+    return stringOrList
+
+def serializeFields(obj, order=None):
+    """
+    Turn an object's fields into a ';' and ',' seperated string.
+
+    If order is None, obj should be a list, backslash escape each field and
+    return a ';' separated string.
+    """
+    fields = []
+    if order is None:
+        fields = [backslashEscape(val) for val in obj]
+    else:
+        for field in order:
+            escapedValueList = [backslashEscape(val) for val in
+                                toList(getattr(obj, field))]
+            fields.append(','.join(escapedValueList))
+    return ';'.join(fields)
+
+
+NAME_ORDER = ('family', 'given', 'additional', 'prefix', 'suffix')
+ADDRESS_ORDER = ('box', 'extended', 'street', 'city', 'region', 'code',
+                 'country')
+
+
+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')
+
+
+class AddressBehavior(VCardBehavior):
+    """
+    A structured address.
+    """
+    hasNative = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """
+        Turn obj.value into an Address.
+        """
+        if obj.isNative:
+            return obj
+        obj.isNative = True
+        obj.value = Address(**dict(zip(ADDRESS_ORDER, splitFields(obj.value))))
+        return obj
+
+    @staticmethod
+    def transformFromNative(obj):
+        """
+        Replace the Address in obj.value with a string.
+        """
+        obj.isNative = False
+        obj.value = serializeFields(obj.value, ADDRESS_ORDER)
+        return obj
+registerBehavior(AddressBehavior, 'ADR')
+
+
+class OrgBehavior(VCardBehavior):
+    """
+    A list of organization values and sub-organization values.
+    """
+    hasNative = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """
+        Turn obj.value into a list.
+        """
+        if obj.isNative:
+            return obj
+        obj.isNative = True
+        obj.value = splitFields(obj.value)
+        return obj
+
+    @staticmethod
+    def transformFromNative(obj):
+        """
+        Replace the list in obj.value with a string.
+        """
+        if not obj.isNative:
+            return obj
+        obj.isNative = False
+        obj.value = serializeFields(obj.value)
+        return obj
+registerBehavior(OrgBehavior, 'ORG')

+ 156 - 0
src/vobject/docs/build/lib/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 - {0!s}>".format(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()

BIN
src/vobject/docs/dist/vobject-0.9.3-py2.7.egg


+ 1008 - 0
src/vobject/docs/index.html

@@ -0,0 +1,1008 @@
+<!DOCTYPE html>
+<html lang="en-us">
+  <head>
+    <meta charset="UTF-8">
+    <title>VObject by eventable</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <link rel="stylesheet" type="text/css" href="stylesheets/normalize.css" media="screen">
+    <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700' rel='stylesheet' type='text/css'>
+    <link rel="stylesheet" type="text/css" href="stylesheets/stylesheet.css" media="screen">
+    <link rel="stylesheet" type="text/css" href="stylesheets/github-light.css" media="screen">
+  </head>
+  <body>
+    <section class="page-header">
+      <h1 class="project-name">VObject</h1>
+      <h2 class="project-tagline">A full-featured Python package for parsing and creating iCalendar and vCard files</h2>
+      <a href="https://github.com/eventable/vobject" class="btn">View on GitHub</a>
+      <a href="https://github.com/eventable/vobject/zipball/master" class="btn">Download .zip</a>
+      <a href="https://github.com/eventable/vobject/tarball/master" class="btn">Download .tar.gz</a>
+    </section>
+
+    <section class="main-content">
+      <h1>
+<a id="about-vobject" class="anchor" href="#about-vobject" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>About VObject</h1>
+
+<p>VObject is intended to be a full-featured Python package for parsing and generating vCard and vCalendar files. It was originally developed in concert with the Open Source Application Foundation's Chandler project by Jeffrey Harris. Many thanks to <a href="https://github.com/eventable/vobject/blob/master/ACKNOWLEDGEMENTS.txt">all the contributors</a> for their dedication and support. The project is currently being maintained by <a href="https://github.com/eventable">Eventable</a> and <a href="https://github.com/skarim">Sameen Karim</a>.</p>
+
+<p>Currently, iCalendar files are supported and well tested. vCard 3.0 files are supported, and all data should be imported, but only a few components are understood in a sophisticated way. The <a href="http://calendarserver.org/">Calendar Server</a> team has added VAVAILABILITY support to VObject's iCalendar parsing. Please report bugs and issues directly on <a href="https://github.com/eventable/vobject/issues">GitHub</a>.</p>
+
+<p>VObject is licensed under the <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache 2.0 license</a>. <a href="http://www.apache.org/licenses/LICENSE-2.0.html"><img src="https://img.shields.io/pypi/l/vobject.svg" alt="License"></a></p>
+
+<p>Useful scripts included with VObject:</p>
+
+<ul>
+<li>
+<a href="https://github.com/eventable/vobject/blob/master/vobject/ics_diff.py">ics_diff</a>: order is irrelevant in iCalendar files, return a diff of meaningful changes between icalendar files</li>
+<li>
+<a href="https://github.com/eventable/vobject/blob/master/vobject/change_tz.py">change_tz</a>: Take an iCalendar file with events in the wrong timezone, change all events or just UTC events into one of the timezones PyICU supports. Requires <a href="https://pypi.python.org/pypi/PyICU/">PyICU</a>.</li>
+</ul>
+
+<h1>
+<a id="installation-" class="anchor" href="#installation-" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Installation <a href="https://pypi.python.org/pypi/vobject"><img src="https://badge.fury.io/py/vobject.svg" alt="PyPI version"></a>
+</h1>
+
+<p>To install with <a href="https://pypi.python.org/pypi/pip">pip</a>, run:</p>
+
+<pre><code>pip install vobject
+</code></pre>
+
+<p>Or download the package and run:</p>
+
+<pre><code>python setup.py install
+</code></pre>
+
+<p>VObject requires Python 2.7 or higher, along with the <a href="https://pypi.python.org/pypi/python-dateutil/">dateutil</a> and <a href="https://pypi.python.org/pypi/six">six</a> packages.</p>
+
+<h1>
+<a id="running-tests-" class="anchor" href="#running-tests-" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Running tests <a href="https://travis-ci.org/eventable/vobject"><img src="https://travis-ci.org/eventable/vobject.svg?branch=master" alt="Build Status"></a>
+</h1>
+
+<p>To run all tests, use:</p>
+
+<pre><code>python tests/tests.py
+</code></pre>
+
+<h1>
+<a id="usage" class="anchor" href="#usage" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Usage</h1>
+
+<h2>
+<a id="icalendar" class="anchor" href="#icalendar" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>iCalendar</h2>
+
+<h4>
+<a id="creating-icalendar-objects" class="anchor" href="#creating-icalendar-objects" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Creating iCalendar objects</h4>
+
+<p>VObject has a basic datastructure for working with iCalendar-like
+syntaxes.  Additionally, it defines specialized behaviors for many of
+the commonly used iCalendar objects.</p>
+
+<p>To create an object that already has a behavior defined, run:</p>
+
+<pre><code>&gt;&gt;&gt; import vobject
+&gt;&gt;&gt; cal = vobject.newFromBehavior('vcalendar')
+&gt;&gt;&gt; cal.behavior
+&lt;class 'vobject.icalendar.VCalendar2_0'&gt;
+</code></pre>
+
+<p>Convenience functions exist to create iCalendar and vCard objects:</p>
+
+<pre><code>&gt;&gt;&gt; cal = vobject.iCalendar()
+&gt;&gt;&gt; cal.behavior
+&lt;class 'vobject.icalendar.VCalendar2_0'&gt;
+&gt;&gt;&gt; card = vobject.vCard()
+&gt;&gt;&gt; card.behavior
+&lt;class 'vobject.vcard.VCard3_0'&gt;
+</code></pre>
+
+<p>Once you have an object, you can use the add method to create
+children:</p>
+
+<pre><code>&gt;&gt;&gt; cal.add('vevent')
+&lt;VEVENT| []&gt;
+&gt;&gt;&gt; cal.vevent.add('summary').value = "This is a note"
+&gt;&gt;&gt; cal.prettyPrint()
+ VCALENDAR
+    VEVENT
+       SUMMARY: This is a note
+</code></pre>
+
+<p>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.</p>
+
+<p>ContentLines can also have parameters.  They can be accessed with
+regular attribute names with <code>_param</code> appended:</p>
+
+<pre><code>&gt;&gt;&gt; cal.vevent.summary.x_random_param = 'Random parameter'
+&gt;&gt;&gt; cal.prettyPrint()
+ VCALENDAR
+    VEVENT
+       SUMMARY: This is a note
+       params for  SUMMARY:
+          X-RANDOM ['Random parameter']
+</code></pre>
+
+<p>There are a few things to note about this example</p>
+
+<ul>
+<li>The underscore in x_random is converted to a dash (dashes are
+legal in iCalendar, underscores legal in Python)</li>
+<li>X-RANDOM's value is a list.</li>
+</ul>
+
+<p>If you want to access the full list of parameters, not just the first,
+use &lt;paramname&gt;_paramlist:</p>
+
+<pre><code>&gt;&gt;&gt; cal.vevent.summary.x_random_paramlist
+['Random parameter']
+&gt;&gt;&gt; cal.vevent.summary.x_random_paramlist.append('Other param')
+&gt;&gt;&gt; cal.vevent.summary
+&lt;SUMMARY{'X-RANDOM': ['Random parameter', 'Other param']}This is a note&gt;
+</code></pre>
+
+<p>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:</p>
+
+<pre><code>&gt;&gt;&gt; cal.add('vevent').add('summary').value = "Second VEVENT"
+&gt;&gt;&gt; for ev in cal.vevent_list:
+...     print ev.summary.value
+This is a note
+Second VEVENT
+</code></pre>
+
+<p>The interaction between the del operator and the hiding of the
+underlying list is a little tricky, both <code>del cal.vevent</code> and
+<code>del cal.vevent_list</code> delete all vevent children:</p>
+
+<pre><code>&gt;&gt;&gt; first_ev = cal.vevent
+&gt;&gt;&gt; del cal.vevent
+&gt;&gt;&gt; cal
+&lt;VCALENDAR| []&gt;
+&gt;&gt;&gt; cal.vevent = first_ev
+</code></pre>
+
+<p>VObject understands Python's datetime module and tzinfo classes.</p>
+
+<pre><code>&gt;&gt;&gt; import datetime
+&gt;&gt;&gt; utc = vobject.icalendar.utc
+&gt;&gt;&gt; start = cal.vevent.add('dtstart')
+&gt;&gt;&gt; start.value = datetime.datetime(2006, 2, 16, tzinfo = utc)
+&gt;&gt;&gt; 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']
+</code></pre>
+
+<p>Components and ContentLines have serialize methods:</p>
+
+<pre><code>&gt;&gt;&gt; cal.vevent.add('uid').value = 'Sample UID'
+&gt;&gt;&gt; icalstream = cal.serialize()
+&gt;&gt;&gt; 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
+</code></pre>
+
+<p>Observe that serializing adds missing required lines like version and
+prodid.  A random UID would be generated, too, if one didn't exist.</p>
+
+<p>If dtstart's tzinfo had been something other than UTC, an appropriate
+vtimezone would be created for it.</p>
+
+<h4>
+<a id="parsing-icalendar-objects" class="anchor" href="#parsing-icalendar-objects" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Parsing iCalendar objects</h4>
+
+<p>To parse one top level component from an existing iCalendar stream or
+string, use the readOne function:</p>
+
+<pre><code>&gt;&gt;&gt; parsedCal = vobject.readOne(icalstream)
+&gt;&gt;&gt; parsedCal.vevent.dtstart.value
+datetime.datetime(2006, 2, 16, 0, 0, tzinfo=tzutc())
+</code></pre>
+
+<p>Similarly, readComponents is a generator yielding one top level component at a time from a stream or string.</p>
+
+<pre><code>&gt;&gt;&gt; vobject.readComponents(icalstream).next().vevent.dtstart.value
+datetime.datetime(2006, 2, 16, 0, 0, tzinfo=tzutc())
+</code></pre>
+
+<p>More examples can be found in source code doctests.</p>
+
+<h2>
+<a id="vcards" class="anchor" href="#vcards" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>vCards</h2>
+
+<h4>
+<a id="creating-vcard-objects" class="anchor" href="#creating-vcard-objects" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Creating vCard objects</h4>
+
+<p>Making vCards proceeds in much the same way. Note that the 'FN' attribute is required.</p>
+
+<pre><code>&gt;&gt;&gt; j = vobject.vCard()
+&gt;&gt;&gt; j.add('n')
+ &lt;N{}    &gt;
+&gt;&gt;&gt; j.n.value = vobject.vcard.Name( family='Harris', given='Jeffrey' )
+&gt;&gt;&gt; j.add('fn')
+ &lt;FN{}&gt;
+&gt;&gt;&gt; j.fn.value ='Jeffrey Harris'
+&gt;&gt;&gt; j.add('email')
+ &lt;EMAIL{}&gt;
+&gt;&gt;&gt; j.email.value = 'jeffrey@osafoundation.org'
+&gt;&gt;&gt; j.email.type_param = 'INTERNET'
+&gt;&gt;&gt; j.prettyPrint()
+ VCARD
+    EMAIL: jeffrey@osafoundation.org
+    params for  EMAIL:
+       TYPE ['INTERNET']
+    FN: Jeffrey Harris
+    N:  Jeffrey  Harris
+</code></pre>
+
+<p>serializing will add any required computable attributes (like 'VERSION')</p>
+
+<pre><code>&gt;&gt;&gt; j.serialize()
+'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'
+&gt;&gt;&gt; j.prettyPrint()
+ VCARD
+    VERSION: 3.0
+    EMAIL: jeffrey@osafoundation.org
+    params for  EMAIL:
+       TYPE ['INTERNET']
+    FN: Jeffrey Harris
+    N:  Jeffrey  Harris 
+</code></pre>
+
+<h4>
+<a id="parsing-vcard-objects" class="anchor" href="#parsing-vcard-objects" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Parsing vCard objects</h4>
+
+<pre><code>&gt;&gt;&gt; s = """
+... BEGIN:VCARD
+... VERSION:3.0
+... EMAIL;TYPE=INTERNET:jeffrey@osafoundation.org
+... FN:Jeffrey Harris
+... N:Harris;Jeffrey;;;
+... END:VCARD
+... """
+&gt;&gt;&gt; v = vobject.readOne( s )
+&gt;&gt;&gt; v.prettyPrint()
+ VCARD
+    VERSION: 3.0
+    EMAIL: jeffrey@osafoundation.org
+    params for  EMAIL:
+       TYPE [u'INTERNET']
+    FN: Jeffrey Harris
+    N:  Jeffrey  Harris
+&gt;&gt;&gt; v.n.value.family
+u'Harris'
+</code></pre>
+
+<h2>
+<a id="multi-value-props" class="anchor" href="#multi-value-props" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Multi-Value Properties</h2>
+
+<p>Multi-value properties are properties of a component that can contain multiple values. An easy example would be email. A vCard can contain both someone's work email address and their home email address.
+When dealing with multi-value properties, the getting and setting of values happens slightly differentely than singleton properties.</p>
+
+<h4>
+<a id="multi-value-add" class="anchor" href="#multi-value-add" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Adding Multi-Value Properties</h4>
+
+<p>You should add multi-value properties by creating multiple ContentLines and giving each a value. You can also set the property list explicitly, but this is not advised.</p>
+<pre><code>&gt;&gt;&gt; v = vobject.vCard()
+&gt;&gt;&gt; v.add('fn').value = "Jeffery Harris"
+&gt;&gt;&gt; v.add('email').value = 'jeffrey@osafoundation.org'
+&gt;&gt;&gt; v.add('email').value = 'jeffrey@example.com'
+&gt;&gt;&gt; print v.email_list
+ [&lt;EMAIL{}jeffrey@osafoundation.org&gt;, &lt;EMAIL{}jeffery@example.com&gt;]
+&gt;&gt;&gt; print v.serialize()
+ BEGIN:VCARD
+ VERSION:3.0
+ EMAIL:jeffrey@osafoundation.org
+ EMAIL:jeffery@example.com
+ FN:Jeffery Harris
+ END:VCARD
+</code></pre>
+
+<h4>
+<a id="multi-value-access" class="anchor" href="#multi-value-access" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Accessing Multi-Value Properties</h4>
+
+<p>You can access multi-value entries either by grabbing the relevent entry in the `contents` dictionary or by directly accessing the attribute and appending <code>_list</code> to the property name.</p>
+<pre><code>&gt;&gt;&gt; s = """
+... BEGIN:VCARD
+... VERSION:3.0
+... EMAIL;TYPE=INTERNET:jeffrey@osafoundation.org
+... FN:Jeffrey Harris
+... N:Harris;Jeffrey;;;
+... TEL;TYPE=WORK,VOICE:(111) 555-1212
+... TEL;TYPE=HOME,VOICE:(404) 555-1212
+... END:VCARD
+... """
+&gt;&gt;&gt; v = vobject.readOne( s )
+&gt;&gt;&gt; for tel in v.contents['tel']:
+...     print tel
+ &lt;TEL{u'TYPE': [u'WORK', u'VOICE']}(111) 555-1212&gt;
+ &lt;TEL{u'TYPE': [u'HOME', u'VOICE']}(404) 555-1212&gt;
+&gt;&gt;&gt; for tel in v.contents['tel']:
+...     print tel.value
+ (111) 555-1212
+ (404) 555-1212
+&gt;&gt;&gt; v.tel_list
+ [&lt;TEL{'TYPE': ['WORK', 'VOICE']}(111) 555-1212&gt;,
+  &lt;TEL{'TYPE': ['HOME', 'VOICE']}(404) 555-1212&gt;]
+</code></pre>
+
+<h1>
+<a id="release-history" class="anchor" href="#release-history" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release History</h1>
+
+<h3>
+<a id="7-july-2018" class="anchor" href="#7-july-2018" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>7 July 2018</h3>
+
+<p><strong>vobject 0.9.6</strong> released (<a href="https://github.com/eventable/vobject/releases/tag/0.9.6">view</a>).</p>
+
+<p>To install, use <code>pip install vobject</code>, or download the archive and untar, run python setup.py install. Tests can be run via python setup.py test. <a href="https://pypi.org/project/python-dateutil/">dateutil</a> and <a href="https://pypi.org/project/six">six</a> are required. Python 2.7 or higher is required.</p>
+
+<h5>
+<a id="release-notes" class="anchor" href="#release-notes" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Correctly order calendar properties before calendar components</li>
+<li>  Correctly serialize timestamp values (i.e. `REV`)</li>
+<li>  Pass correct formatting string to logger</li>
+<li>  RRULE: Fix floating UNTIL with dateutil > 2.6.1</li>
+<li>  Encode params if necessary in serialization</li>
+<li>  Ignore escaped semi-colons in UNTIL value</li>
+<li>  RRULE: Fix VTODO without DTSTART</li>
+<li>  Fixed regexp for VCF Version 2.1</li>
+<li>  repr() changed for datetime.timedelta in python 3.7</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="29-june-2017" class="anchor" href="#29-june-2017" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>29 June 2017</h3>
+
+<p><strong>vobject 0.9.5</strong> released (<a href="https://github.com/eventable/vobject/releases/tag/0.9.5">view</a>).</p>
+
+<p>To install, use <code>pip install vobject</code>, or download the archive and untar, run python setup.py install. Tests can be run via python setup.py test. <a href="https://pypi.python.org/pypi/python-dateutil/">dateutil</a> and <a href="https://pypi.python.org/pypi/six">six</a> are required. Python 2.7 or higher is required.</p>
+
+<h5>
+<a id="release-notes" class="anchor" href="#release-notes" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Make ics_diff.py work with Python 3</li>
+<li>  Huge changes to text encoding for Python 2/3 compatibility</li>
+<li>  Autogenerate DTSTAMP if not provided</li>
+<li>  Fix getrruleset() for Python 3 and in the case that addRDate=True</li>
+<li>  Update vCard property validation to match specifications</li>
+<li>  Handle offset-naive and offset-aware datetimes in recurrence rules</li>
+<li>  Improved documentation for multi-value properties</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="22-january-2017" class="anchor" href="#22-january-2017" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>22 January 2017</h3>
+
+<p><strong>vobject 0.9.4.1</strong> released (<a href="https://github.com/eventable/vobject/releases/tag/0.9.4.1">view</a>).</p>
+
+<p>To install, use <code>pip install vobject</code>, or download the archive and untar, run python setup.py install. Tests can be run via python setup.py test. <a href="https://pypi.python.org/pypi/python-dateutil/">dateutil</a> and <a href="https://pypi.python.org/pypi/six">six</a> are required. Python 2.7 or higher is required.</p>
+
+<h5>
+<a id="release-notes" class="anchor" href="#release-notes" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Pickling/deepcopy hotfix</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="20-january-2017" class="anchor" href="#20-january-2017" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>20 January 2017</h3>
+
+<p><strong>vobject 0.9.4</strong> released (<a href="https://github.com/eventable/vobject/releases/tag/0.9.4">view</a>).</p>
+
+<p>To install, use <code>pip install vobject</code>, or download the archive and untar, run python setup.py install. Tests can be run via python setup.py test. <a href="https://pypi.python.org/pypi/python-dateutil/">dateutil</a> and <a href="https://pypi.python.org/pypi/six">six</a> are required. Python 2.7 or higher is required.</p>
+
+<h5>
+<a id="release-notes" class="anchor" href="#release-notes" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Improved PEP8 compliance</li>
+<li>  Improved Python 3 compatibility</li>
+<li>  Improved encoding/decoding</li>
+<li>  Correct handling of pytz timezones</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="26-august-2016" class="anchor" href="#26-august-2016" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>26 August 2016</h3>
+
+<p><strong>vobject 0.9.3</strong> released (<a href="https://github.com/eventable/vobject/releases/tag/0.9.3">view</a>).</p>
+
+<p>To install, use <code>pip install vobject</code>, or download the archive and untar, run python setup.py install. Tests can be run via python setup.py test. <a href="https://pypi.python.org/pypi/python-dateutil/">dateutil</a> and <a href="https://pypi.python.org/pypi/six">six</a> are required. Python 2.7 or higher is required.</p>
+
+<h5>
+<a id="release-notes" class="anchor" href="#release-notes" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Fixed use of doc in setup.py for -OO mode</li>
+<li>  Added python3 compatibility for base64 encoding</li>
+<li>  Fixed ORG fields with multiple components</li>
+<li>  Handle pytz timezones in iCalendar serialization</li>
+<li>  Use logging instead of printing to stdout</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="13-march-2016" class="anchor" href="#13-march-2016" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>13 March 2016</h3>
+
+<p><strong>vobject 0.9.2</strong> released (<a href="https://github.com/eventable/vobject/releases/tag/0.9.2">view</a>).</p>
+
+<p>To install, use <code>pip install vobject</code>, or download the archive and untar, run python setup.py install. Tests can be run via python setup.py test. <a href="https://pypi.python.org/pypi/python-dateutil/">dateutil</a> and <a href="https://pypi.python.org/pypi/six">six</a> are required. Python 2.7 or higher is required.</p>
+
+<h5>
+<a id="release-notes-1" class="anchor" href="#release-notes-1" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Better line folding for utf-8 strings</li>
+<li>  Convert unicode to utf-8 to be StringIO compatible</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="16-february-2016" class="anchor" href="#16-february-2016" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>16 February 2016</h3>
+
+<p><strong>vobject 0.9.1</strong> released (<a href="https://github.com/eventable/vobject/releases/tag/0.9.1">view</a>).</p>
+
+<p>To install, use <code>pip install vobject</code>, or download the archive and untar, run python setup.py install. Tests can be run via python setup.py test. <a href="https://pypi.python.org/pypi/python-dateutil/">dateutil</a> and <a href="https://pypi.python.org/pypi/six">six</a> are required. Python 2.7 or higher is now required.</p>
+
+<h5>
+<a id="release-notes-2" class="anchor" href="#release-notes-2" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Removed lock on dateutil version (&gt;=2.4.0 now works)</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="3-february-2016" class="anchor" href="#3-february-2016" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>3 February 2016</h3>
+
+<p><strong>vobject 0.9.0</strong> released (<a href="https://github.com/eventable/vobject/releases/tag/0.9.0">view</a>).</p>
+
+<p>To install, use <code>pip install vobject</code>, or download the archive and untar, run python setup.py install. Tests can be run via python setup.py test. <a href="https://pypi.python.org/pypi/python-dateutil/">dateutil 2.4.0</a> and <a href="https://pypi.python.org/pypi/six">six</a> are required. Python 2.7 or higher is now required.</p>
+
+<h5>
+<a id="release-notes-3" class="anchor" href="#release-notes-3" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Python 3 compatible</li>
+<li>  Updated version of dateutil (2.4.0)</li>
+<li>  More comprehensive unit tests available in tests.py</li>
+<li>  Performance improvements in iteration</li>
+<li>  Test files are included in PyPI download package</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="28-january-2016" class="anchor" href="#28-january-2016" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>28 January 2016</h3>
+
+<p><strong>vobject 0.8.2</strong> released (<a href="https://github.com/eventable/vobject/releases/tag/0.8.2">view</a>).</p>
+
+<p>To install, use <code>pip install vobject</code>, or download the archive and untar, run python setup.py install. Tests can be run via python setup.py test. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 1.1 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-4" class="anchor" href="#release-notes-4" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Removed unnecessary ez_setup call from setup.py</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="27-february-2009" class="anchor" href="#27-february-2009" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>27 February 2009</h3>
+
+<p><strong>vobject 0.8.1c</strong> released (SVN revision 217).</p>
+
+<p>To install, use easy_install, or download the archive and untar, run python setup.py install. Tests can be run via python setup.py test. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 1.1 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-5" class="anchor" href="#release-notes-5" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Tweaked change_tz.py to keep it 2.4 compatible</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="12-january-2009" class="anchor" href="#12-january-2009" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>12 January 2009</h3>
+
+<p><strong>vobject 0.8.1b</strong> released (SVN revision 216).</p>
+
+<p>To install, use easy_install, or download the archive and untar, run python setup.py install. Tests can be run via python setup.py test. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 1.1 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-6" class="anchor" href="#release-notes-6" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Change behavior when import a VCALENDAR or VCARD with an older or absent VERSION line, now the most recent behavior (i.e., VCARD 3.0 and iCalendar, VCALENDAR 2.0) is used</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="29-december-2008" class="anchor" href="#29-december-2008" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>29 December 2008</h3>
+
+<p><strong>vobject 0.8.0</strong> released (SVN revision 213).</p>
+
+<p>To install, use easy_install, or download the archive and untar, run python setup.py install. Tests can be run via python setup.py test. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 1.1 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-7" class="anchor" href="#release-notes-7" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Changed license to Apache 2.0 from Apache 1.1</li>
+<li>  Fixed a major performance bug in backslash decoding large text bodies</li>
+<li>  Added workaround for strange Apple Address Book parsing of vcard PHOTO, don't wrap PHOTO by default. To disable this behavior, set vobject.vcard.wacky_apple_photo_serialize to False.</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="25-july-2008" class="anchor" href="#25-july-2008" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>25 July 2008</h3>
+
+<p><strong>vobject 0.7.1</strong> released (SVN revision 208).</p>
+
+<p>To install, use easy_install, or download the archive and untar, run python setup.py install. Tests can be run via python setup.py test. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 1.1 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-8" class="anchor" href="#release-notes-8" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Add change_tz script for converting timezones in iCalendar files</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="16-july-2008" class="anchor" href="#16-july-2008" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>16 July 2008</h3>
+
+<p><strong>vobject 0.7.0</strong> released (SVN revision 206).</p>
+
+<p>To install, use easy_install, or download the archive and untar, run python setup.py install. Tests can be run via python setup.py test. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 1.1 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-9" class="anchor" href="#release-notes-9" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Allow Outlook's technically illegal use of commas in TZIDs</li>
+<li>  Added introspection help for IPython so tab completion works with vobject's custom <strong>getattr</strong>
+</li>
+<li>  Made vobjects pickle-able</li>
+<li>  Added tolerance for the escaped semi-colons in RRULEs a Ruby iCalendar library generates</li>
+<li>  Fixed <a href="https://bugzilla.osafoundation.org/show_bug.cgi?id=12245">Bug 12245</a>, setting an rrule from a dateutil instance missed BYMONTHDAY when the number used is negative</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="30-may-2008" class="anchor" href="#30-may-2008" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>30 May 2008</h3>
+
+<p><strong>vobject 0.6.6</strong> released (SVN revision 201).</p>
+
+<p>To install, use easy_install, or download the archive and untar, run python setup.py install. Tests can be run via python setup.py test. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 1.1 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-10" class="anchor" href="#release-notes-10" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Fixed <a href="https://bugzilla.osafoundation.org/show_bug.cgi?id=12120">bug 12120</a>, unicode TZIDs were failing to parse.</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="28-may-2008" class="anchor" href="#28-may-2008" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>28 May 2008</h3>
+
+<p><strong>vobject 0.6.5</strong> released (SVN revision 200).</p>
+
+<p>To install, use easy_install, or download the archive and untar, run python setup.py install. Tests can be run via python setup.py test. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 1.1 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-11" class="anchor" href="#release-notes-11" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Fixed <a href="https://bugzilla.osafoundation.org/show_bug.cgi?id=9814">bug 9814</a>, quoted-printable data wasn't being decoded into unicode, thanks to Ilpo Nyyssönen for the fix.</li>
+<li>  Fixed <a href="https://bugzilla.osafoundation.org/show_bug.cgi?id=12008">bug 12008</a>, silently translate buggy Lotus Notes names with underscores into dashes.</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="21-february-2008" class="anchor" href="#21-february-2008" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>21 February 2008</h3>
+
+<p><strong>vobject 0.6.0</strong> released (SVN revision 193).</p>
+
+<p>To install, use easy_install, or download the archive and untar, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 1.1 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-12" class="anchor" href="#release-notes-12" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Added VAVAILABILITY support, thanks to the Calendar Server team.</li>
+<li>  Improved unicode line folding.</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="14-january-2008" class="anchor" href="#14-january-2008" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>14 January 2008</h3>
+
+<p><strong>vobject 0.5.0</strong> released (SVN revision 189).</p>
+
+<p>To install, use easy_install, or download the archive and untar, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 1.1 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-13" class="anchor" href="#release-notes-13" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Updated to more recent ez_setup, vobject wasn't successfully installing.</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="19-november-2007" class="anchor" href="#19-november-2007" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>19 November 2007</h3>
+
+<p><strong>vobject 0.4.9</strong> released (SVN revision 187).</p>
+
+<p>To install, use easy_install, or download the archive and untar, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 1.1 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-14" class="anchor" href="#release-notes-14" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Tolerate invalid UNTIL values for recurring events</li>
+<li>  Minor improvements to logging and tracebacks</li>
+<li>  Fix serialization of zero-delta durations</li>
+<li>  Treat different tzinfo classes that represent UTC as equal</li>
+<li>  Added ORG behavior to vCard handling, native value for ORG is now a list.</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="7-january-2007" class="anchor" href="#7-january-2007" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>7 January 2007</h3>
+
+<p><strong>vobject 0.4.8</strong> released (SVN revision 180).</p>
+
+<p>To install, use easy_install, or download the archive and untar, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 1.1 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-15" class="anchor" href="#release-notes-15" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Fixed problem with the UNTIL time used when creating a dateutil rruleset.</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="21-december-2006" class="anchor" href="#21-december-2006" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>21 December 2006</h3>
+
+<p><strong>vobject 0.4.7</strong> released (SVN revision 172), hot on the heals of yesterday's 0.4.6.</p>
+
+<p>To install, use easy_install, or download the archive and untar, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 1.1 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-16" class="anchor" href="#release-notes-16" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Fixed a problem causing DATE valued RDATEs and EXDATEs to be ignored when interpreting recurrence rules</li>
+<li>  And, from the short lived vobject 0.4.6, added an ics_diff module and an ics_diff command line script for comparing similar iCalendar files</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="20-december-2006" class="anchor" href="#20-december-2006" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>20 December 2006</h3>
+
+<p><strong>vobject 0.4.6</strong> released (SVN revision 171)</p>
+
+<p>To install, use easy_install, or download the archive and untar, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 1.1 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-17" class="anchor" href="#release-notes-17" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Added an ics_diff module and an ics_diff command line script for comparing similar iCalendar files</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="8-december-2006" class="anchor" href="#8-december-2006" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>8 December 2006</h3>
+
+<p><strong>vobject 0.4.5</strong> released (SVN revision 168)</p>
+
+<p>To install, use easy_install, or download the archive and untar, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 1.1 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-18" class="anchor" href="#release-notes-18" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Added ignoreUnreadable flag to readOne and readComponents</li>
+<li>  Tolerate date-time or date fields incorrectly failing to set VALUE=DATE for date values</li>
+<li>  Cause unrecognized lines to default to use a text behavior, so commas, carriage returns, and semi-colons are escaped properly in unrecognized lines</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="9-october-2006" class="anchor" href="#9-october-2006" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>9 October 2006</h3>
+
+<p><strong>vobject 0.4.4</strong> released (SVN revision 159)</p>
+
+<p>To install, use easy_install, or download the archive and untar, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 1.1 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-19" class="anchor" href="#release-notes-19" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Merged in Apple CalendarServer patches as of CalendarServer-r191</li>
+<li>  Added copy and duplicate code to base module</li>
+<li>  Improved recurring VTODO handling</li>
+<li>  Save TZIDs when parsed and use them as back up TZIDs when serializing</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="22-september-2006" class="anchor" href="#22-september-2006" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>22 September 2006</h3>
+
+<p><strong>vobject 0.4.3</strong> released (SVN revision 157)</p>
+
+<p>To install, use easy_install, or download the archive and untar, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 0.9 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-20" class="anchor" href="#release-notes-20" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Added support for PyTZ tzinfo classes.</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="29-august-2006" class="anchor" href="#29-august-2006" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>29 August 2006</h3>
+
+<p><strong>vobject 0.4.2</strong> released (SVN revision 153)</p>
+
+<p>To install, use easy_install, or download the archive and untar, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 0.9 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-21" class="anchor" href="#release-notes-21" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Updated ez_setup.py to use the latest setuptools.</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="4-august-2006" class="anchor" href="#4-august-2006" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>4 August 2006</h3>
+
+<p><strong>vobject 0.4.1</strong> released (SVN revision 152)</p>
+
+<p>To install, use easy_install, or download the archive and untar, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 0.9 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-22" class="anchor" href="#release-notes-22" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  When vobject encounters ascii, it now tries utf-8, then utf-16 with either LE or BE byte orders, searching for BEGIN in the decoded string to determine if it's found an encoding match. readOne and readComponents will no longer work on arbitrary Versit style ascii streams unless the optional findBegin flag is set to False</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="2-august-2006" class="anchor" href="#2-august-2006" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>2 August 2006</h3>
+
+<p><strong>vobject 0.4.0</strong> released (SVN revision 151)</p>
+
+<p>To install, use easy_install, or download the archive and untar, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 0.9 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-23" class="anchor" href="#release-notes-23" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Workarounds for common invalid files produced by Apple's iCal and AddressBook</li>
+<li>  Added getChildValue convenience method</li>
+<li>  Added experimental hCalendar serialization</li>
+<li>  Handle DATE valued EXDATE and RRULEs better</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="17-february-2006" class="anchor" href="#17-february-2006" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>17 February 2006</h3>
+
+<p><strong>vobject 0.3.0</strong> released (SVN revision 129)</p>
+
+<p>To install, untar the archive, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 0.9 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-24" class="anchor" href="#release-notes-24" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Changed API for accessing children and parameters, attributes now return the first child or parameter, not a list. See <a href="usage.html">usage</a> for examples</li>
+<li>  Added support for groups, a vcard feature</li>
+<li>  Added behavior for FREEBUSY lines</li>
+<li>  Worked around problem with dateutil's treatment of experimental properties (bug 4978)</li>
+<li>  Fixed bug 4992, problem with rruleset when addRDate is set</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="9-january-2006" class="anchor" href="#9-january-2006" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>9 January 2006</h3>
+
+<p><strong>vobject 0.2.3</strong> released (SVN revision 104)</p>
+
+<p>To install, untar the archive, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 0.9 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-25" class="anchor" href="#release-notes-25" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Added VERSION line back into native iCalendar objects</li>
+<li>  Added a first stab at a vcard module, parsing of vCard 3.0 files now gives structured values for N and ADR properties</li>
+<li>  Fix bug in regular expression causing the '^' character to not parse</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="4-november-2005" class="anchor" href="#4-november-2005" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>4 November 2005</h3>
+
+<p><strong>vobject 0.2.2</strong> released (SVN revision 101)</p>
+
+<p>To install, untar the archive, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 0.9 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-26" class="anchor" href="#release-notes-26" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Fixed problem with add('duration')</li>
+<li>  Fixed serialization of EXDATEs which are dates or have floating timezone</li>
+<li>  Fixed problem serializing timezones with no daylight savings time</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="10-october-2005" class="anchor" href="#10-october-2005" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>10 October 2005</h3>
+
+<p><strong>vobject 0.2.0</strong> released (SVN revision 97)</p>
+
+<p>To install, untar the archive, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 0.9 or later is required. Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-27" class="anchor" href="#release-notes-27" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Added serialization of arbitrary tzinfo classes as VTIMEZONEs</li>
+<li>  Removed unused methods</li>
+<li>  Changed getLogicalLines to use regular expressions, dramatically speeding it up</li>
+<li>  Changed rruleset behavior to use a property for rruleset</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="30-september-2005" class="anchor" href="#30-september-2005" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>30 September 2005</h3>
+
+<p><strong>vobject 0.1.4</strong> released (SVN revision 93)</p>
+
+<p>To install, untar the archive, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 0.9 or later is required. As of this release, Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-28" class="anchor" href="#release-notes-28" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Changed parseLine to use regular expression instead of a state machine, reducing parse time dramatically</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="1-july-2005" class="anchor" href="#1-july-2005" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>1 July 2005</h3>
+
+<p><strong>vobject 0.1.3</strong> released (SVN revision 88)</p>
+
+<p>To install, untar the archive, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> 0.9 or later is required. As of this release, Python 2.4 is also required.</p>
+
+<h5>
+<a id="release-notes-29" class="anchor" href="#release-notes-29" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Added license and acknowledgements.</li>
+<li>  Fixed the fact that defaultSerialize wasn't escaping linefeeds</li>
+<li>  Updated backslashEscape to encode CRLF's and bare CR's as linefeeds, which seems to be what RFC2445 requires</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="24-march-2005" class="anchor" href="#24-march-2005" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>24 March 2005</h3>
+
+<p><strong>vobject 0.1.2</strong> released (SVN revision 83)</p>
+
+<p>To install, untar the archive, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> is required. You'll need to apply this <a href="dateutil-0.5-tzoffset-bug.patch">patch</a> to be able to read certain VTIMEZONEs exported by Apple iCal, or if you happen to be in Europe!</p>
+
+<p>patch -R $PYTHONLIB/site-packages/dateutil/tz.py dateutil-0.5-tzoffset-bug.patch</p>
+
+<h5>
+<a id="release-notes-30" class="anchor" href="#release-notes-30" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Fixed printing of non-ascii unicode.</li>
+<li>  Fixed bug preventing content lines with empty contents from parsing.</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="25-january-2005" class="anchor" href="#25-january-2005" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>25 January 2005</h3>
+
+<p><strong>vobject 0.1.1</strong> released (SVN revision 82)</p>
+
+<p>To install, untar the archive, run python setup.py install. <a href="http://labix.org/python-dateutil#head-2f49784d6b27bae60cde1cff6a535663cf87497b">dateutil</a> is required. You'll need to apply this <a href="dateutil-0.5-tzoffset-bug.patch">patch</a> to be able to read certain VTIMEZONEs exported by Apple iCal, or if you happen to be in Europe!</p>
+
+<p>patch -R $PYTHONLIB/site-packages/dateutil/tz.py dateutil-0.5-tzoffset-bug.patch</p>
+
+<h5>
+<a id="release-notes-31" class="anchor" href="#release-notes-31" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Various bug fixes involving recurrence.</li>
+<li>  TRIGGER and VALARM behaviors set up.</li>
+</ul>
+
+<hr>
+
+<h3>
+<a id="13-december-2004" class="anchor" href="#13-december-2004" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>13 December 2004</h3>
+
+<p><strong>vobject 0.1</strong> released (SVN revision 70)</p>
+
+<h5>
+<a id="release-notes-32" class="anchor" href="#release-notes-32" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Release Notes</h5>
+
+<ul>
+<li>  Parsing all iCalendar files should be working, please <a href="bugs/">file a bug</a> if you can't read one!</li>
+<li>  Timezones can be set for datetimes, but currently they'll be converted to UTC for serializing, because VTIMEZONE serialization isn't yet working.</li>
+<li>  RRULEs can be parsed, but when they're serialized, they'll be converted to a maximum of 500 RDATEs, because RRULE serialization isn't yet working.</li>
+<li>  To parse unicode, see <a href="http://vobject.skyhouseconsulting.com/bugs/issue4">issue 4</a>.</li>
+<li>  Much more testing is needed, of course!</li>
+</ul>
+
+      <footer class="site-footer">
+        <span class="site-footer-owner"><a href="https://github.com/eventable/vobject">VObject</a> is maintained by <a href="https://github.com/eventable">eventable</a>.</span>
+
+        <span class="site-footer-credits">This page was generated by <a href="https://pages.github.com">GitHub Pages</a> using the <a href="https://github.com/jasonlong/cayman-theme">Cayman theme</a> by <a href="https://twitter.com/jasonlong">Jason Long</a>.</span>
+      </footer>
+
+    </section>
+
+  
+  </body>
+</html>

File diff suppressed because it is too large
+ 3 - 0
src/vobject/docs/params.json


+ 124 - 0
src/vobject/docs/stylesheets/github-light.css

@@ -0,0 +1,124 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2016 GitHub, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+*/
+
+.pl-c /* comment */ {
+  color: #969896;
+}
+
+.pl-c1 /* constant, variable.other.constant, support, meta.property-name, support.constant, support.variable, meta.module-reference, markup.raw, meta.diff.header */,
+.pl-s .pl-v /* string variable */ {
+  color: #0086b3;
+}
+
+.pl-e /* entity */,
+.pl-en /* entity.name */ {
+  color: #795da3;
+}
+
+.pl-smi /* variable.parameter.function, storage.modifier.package, storage.modifier.import, storage.type.java, variable.other */,
+.pl-s .pl-s1 /* string source */ {
+  color: #333;
+}
+
+.pl-ent /* entity.name.tag */ {
+  color: #63a35c;
+}
+
+.pl-k /* keyword, storage, storage.type */ {
+  color: #a71d5d;
+}
+
+.pl-s /* string */,
+.pl-pds /* punctuation.definition.string, string.regexp.character-class */,
+.pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */,
+.pl-sr /* string.regexp */,
+.pl-sr .pl-cce /* string.regexp constant.character.escape */,
+.pl-sr .pl-sre /* string.regexp source.ruby.embedded */,
+.pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */ {
+  color: #183691;
+}
+
+.pl-v /* variable */ {
+  color: #ed6a43;
+}
+
+.pl-id /* invalid.deprecated */ {
+  color: #b52a1d;
+}
+
+.pl-ii /* invalid.illegal */ {
+  color: #f8f8f8;
+  background-color: #b52a1d;
+}
+
+.pl-sr .pl-cce /* string.regexp constant.character.escape */ {
+  font-weight: bold;
+  color: #63a35c;
+}
+
+.pl-ml /* markup.list */ {
+  color: #693a17;
+}
+
+.pl-mh /* markup.heading */,
+.pl-mh .pl-en /* markup.heading entity.name */,
+.pl-ms /* meta.separator */ {
+  font-weight: bold;
+  color: #1d3e81;
+}
+
+.pl-mq /* markup.quote */ {
+  color: #008080;
+}
+
+.pl-mi /* markup.italic */ {
+  font-style: italic;
+  color: #333;
+}
+
+.pl-mb /* markup.bold */ {
+  font-weight: bold;
+  color: #333;
+}
+
+.pl-md /* markup.deleted, meta.diff.header.from-file */ {
+  color: #bd2c00;
+  background-color: #ffecec;
+}
+
+.pl-mi1 /* markup.inserted, meta.diff.header.to-file */ {
+  color: #55a532;
+  background-color: #eaffea;
+}
+
+.pl-mdr /* meta.diff.range */ {
+  font-weight: bold;
+  color: #795da3;
+}
+
+.pl-mo /* meta.output */ {
+  color: #1d3e81;
+}
+

+ 424 - 0
src/vobject/docs/stylesheets/normalize.css

@@ -0,0 +1,424 @@
+/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
+
+/**
+ * 1. Set default font family to sans-serif.
+ * 2. Prevent iOS text size adjust after orientation change, without disabling
+ *    user zoom.
+ */
+
+html {
+  font-family: sans-serif; /* 1 */
+  -ms-text-size-adjust: 100%; /* 2 */
+  -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/**
+ * Remove default margin.
+ */
+
+body {
+  margin: 0;
+}
+
+/* HTML5 display definitions
+   ========================================================================== */
+
+/**
+ * Correct `block` display not defined for any HTML5 element in IE 8/9.
+ * Correct `block` display not defined for `details` or `summary` in IE 10/11
+ * and Firefox.
+ * Correct `block` display not defined for `main` in IE 11.
+ */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+menu,
+nav,
+section,
+summary {
+  display: block;
+}
+
+/**
+ * 1. Correct `inline-block` display not defined in IE 8/9.
+ * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
+ */
+
+audio,
+canvas,
+progress,
+video {
+  display: inline-block; /* 1 */
+  vertical-align: baseline; /* 2 */
+}
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+audio:not([controls]) {
+  display: none;
+  height: 0;
+}
+
+/**
+ * Address `[hidden]` styling not present in IE 8/9/10.
+ * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
+ */
+
+[hidden],
+template {
+  display: none;
+}
+
+/* Links
+   ========================================================================== */
+
+/**
+ * Remove the gray background color from active links in IE 10.
+ */
+
+a {
+  background-color: transparent;
+}
+
+/**
+ * Improve readability when focused and also mouse hovered in all browsers.
+ */
+
+a:active,
+a:hover {
+  outline: 0;
+}
+
+/* Text-level semantics
+   ========================================================================== */
+
+/**
+ * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
+ */
+
+abbr[title] {
+  border-bottom: 1px dotted;
+}
+
+/**
+ * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
+ */
+
+b,
+strong {
+  font-weight: bold;
+}
+
+/**
+ * Address styling not present in Safari and Chrome.
+ */
+
+dfn {
+  font-style: italic;
+}
+
+/**
+ * Address variable `h1` font-size and margin within `section` and `article`
+ * contexts in Firefox 4+, Safari, and Chrome.
+ */
+
+h1 {
+  font-size: 2em;
+  margin: 0.67em 0;
+}
+
+/**
+ * Address styling not present in IE 8/9.
+ */
+
+mark {
+  background: #ff0;
+  color: #000;
+}
+
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+
+small {
+  font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+
+sup {
+  top: -0.5em;
+}
+
+sub {
+  bottom: -0.25em;
+}
+
+/* Embedded content
+   ========================================================================== */
+
+/**
+ * Remove border when inside `a` element in IE 8/9/10.
+ */
+
+img {
+  border: 0;
+}
+
+/**
+ * Correct overflow not hidden in IE 9/10/11.
+ */
+
+svg:not(:root) {
+  overflow: hidden;
+}
+
+/* Grouping content
+   ========================================================================== */
+
+/**
+ * Address margin not present in IE 8/9 and Safari.
+ */
+
+figure {
+  margin: 1em 40px;
+}
+
+/**
+ * Address differences between Firefox and other browsers.
+ */
+
+hr {
+  box-sizing: content-box;
+  height: 0;
+}
+
+/**
+ * Contain overflow in all browsers.
+ */
+
+pre {
+  overflow: auto;
+}
+
+/**
+ * Address odd `em`-unit font size rendering in all browsers.
+ */
+
+code,
+kbd,
+pre,
+samp {
+  font-family: monospace, monospace;
+  font-size: 1em;
+}
+
+/* Forms
+   ========================================================================== */
+
+/**
+ * Known limitation: by default, Chrome and Safari on OS X allow very limited
+ * styling of `select`, unless a `border` property is set.
+ */
+
+/**
+ * 1. Correct color not being inherited.
+ *    Known issue: affects color of disabled elements.
+ * 2. Correct font properties not being inherited.
+ * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+  color: inherit; /* 1 */
+  font: inherit; /* 2 */
+  margin: 0; /* 3 */
+}
+
+/**
+ * Address `overflow` set to `hidden` in IE 8/9/10/11.
+ */
+
+button {
+  overflow: visible;
+}
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
+ * Correct `select` style inheritance in Firefox.
+ */
+
+button,
+select {
+  text-transform: none;
+}
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ *    and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ *    `input` and others.
+ */
+
+button,
+html input[type="button"], /* 1 */
+input[type="reset"],
+input[type="submit"] {
+  -webkit-appearance: button; /* 2 */
+  cursor: pointer; /* 3 */
+}
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+
+button[disabled],
+html input[disabled] {
+  cursor: default;
+}
+
+/**
+ * Remove inner padding and border in Firefox 4+.
+ */
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+  border: 0;
+  padding: 0;
+}
+
+/**
+ * Address Firefox 4+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+input {
+  line-height: normal;
+}
+
+/**
+ * It's recommended that you don't attempt to style these elements.
+ * Firefox's implementation doesn't respect box-sizing, padding, or width.
+ *
+ * 1. Address box sizing set to `content-box` in IE 8/9/10.
+ * 2. Remove excess padding in IE 8/9/10.
+ */
+
+input[type="checkbox"],
+input[type="radio"] {
+  box-sizing: border-box; /* 1 */
+  padding: 0; /* 2 */
+}
+
+/**
+ * Fix the cursor style for Chrome's increment/decrement buttons. For certain
+ * `font-size` values of the `input`, it causes the cursor style of the
+ * decrement button to change from `default` to `text`.
+ */
+
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}
+
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
+ *    (include `-moz` to future-proof).
+ */
+
+input[type="search"] {
+  -webkit-appearance: textfield; /* 1 */ /* 2 */
+  box-sizing: content-box;
+}
+
+/**
+ * Remove inner padding and search cancel button in Safari and Chrome on OS X.
+ * Safari (but not Chrome) clips the cancel button when the search input has
+ * padding (and `textfield` appearance).
+ */
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+
+fieldset {
+  border: 1px solid #c0c0c0;
+  margin: 0 2px;
+  padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct `color` not being inherited in IE 8/9/10/11.
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
+ */
+
+legend {
+  border: 0; /* 1 */
+  padding: 0; /* 2 */
+}
+
+/**
+ * Remove default vertical scrollbar in IE 8/9/10/11.
+ */
+
+textarea {
+  overflow: auto;
+}
+
+/**
+ * Don't inherit the `font-weight` (applied by a rule above).
+ * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
+ */
+
+optgroup {
+  font-weight: bold;
+}
+
+/* Tables
+   ========================================================================== */
+
+/**
+ * Remove most spacing between table cells.
+ */
+
+table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+
+td,
+th {
+  padding: 0;
+}

+ 245 - 0
src/vobject/docs/stylesheets/stylesheet.css

@@ -0,0 +1,245 @@
+* {
+  box-sizing: border-box; }
+
+body {
+  padding: 0;
+  margin: 0;
+  font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-size: 16px;
+  line-height: 1.5;
+  color: #606c71; }
+
+a {
+  color: #1e6bb8;
+  text-decoration: none; }
+  a:hover {
+    text-decoration: underline; }
+
+.btn {
+  display: inline-block;
+  margin-bottom: 1rem;
+  color: rgba(255, 255, 255, 0.7);
+  background-color: rgba(255, 255, 255, 0.08);
+  border-color: rgba(255, 255, 255, 0.2);
+  border-style: solid;
+  border-width: 1px;
+  border-radius: 0.3rem;
+  transition: color 0.2s, background-color 0.2s, border-color 0.2s; }
+  .btn + .btn {
+    margin-left: 1rem; }
+
+.btn:hover {
+  color: rgba(255, 255, 255, 0.8);
+  text-decoration: none;
+  background-color: rgba(255, 255, 255, 0.2);
+  border-color: rgba(255, 255, 255, 0.3); }
+
+@media screen and (min-width: 64em) {
+  .btn {
+    padding: 0.75rem 1rem; } }
+
+@media screen and (min-width: 42em) and (max-width: 64em) {
+  .btn {
+    padding: 0.6rem 0.9rem;
+    font-size: 0.9rem; } }
+
+@media screen and (max-width: 42em) {
+  .btn {
+    display: block;
+    width: 100%;
+    padding: 0.75rem;
+    font-size: 0.9rem; }
+    .btn + .btn {
+      margin-top: 1rem;
+      margin-left: 0; } }
+
+.page-header {
+  color: #fff;
+  text-align: center;
+  background-color: #159957;
+  background-image: linear-gradient(120deg, #155799, #159957); }
+
+@media screen and (min-width: 64em) {
+  .page-header {
+    padding: 5rem 6rem; } }
+
+@media screen and (min-width: 42em) and (max-width: 64em) {
+  .page-header {
+    padding: 3rem 4rem; } }
+
+@media screen and (max-width: 42em) {
+  .page-header {
+    padding: 2rem 1rem; } }
+
+.project-name {
+  margin-top: 0;
+  margin-bottom: 0.1rem; }
+
+@media screen and (min-width: 64em) {
+  .project-name {
+    font-size: 3.25rem; } }
+
+@media screen and (min-width: 42em) and (max-width: 64em) {
+  .project-name {
+    font-size: 2.25rem; } }
+
+@media screen and (max-width: 42em) {
+  .project-name {
+    font-size: 1.75rem; } }
+
+.project-tagline {
+  margin-bottom: 2rem;
+  font-weight: normal;
+  opacity: 0.7; }
+
+@media screen and (min-width: 64em) {
+  .project-tagline {
+    font-size: 1.25rem; } }
+
+@media screen and (min-width: 42em) and (max-width: 64em) {
+  .project-tagline {
+    font-size: 1.15rem; } }
+
+@media screen and (max-width: 42em) {
+  .project-tagline {
+    font-size: 1rem; } }
+
+.main-content :first-child {
+  margin-top: 0; }
+.main-content img {
+  max-width: 100%; }
+.main-content h1, .main-content h2, .main-content h3, .main-content h4, .main-content h5, .main-content h6 {
+  margin-top: 2rem;
+  margin-bottom: 1rem;
+  font-weight: normal;
+  color: #159957; }
+.main-content p {
+  margin-bottom: 1em; }
+.main-content code {
+  padding: 2px 4px;
+  font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
+  font-size: 0.9rem;
+  color: #383e41;
+  background-color: #f3f6fa;
+  border-radius: 0.3rem; }
+.main-content pre {
+  padding: 0.8rem;
+  margin-top: 0;
+  margin-bottom: 1rem;
+  font: 1rem Consolas, "Liberation Mono", Menlo, Courier, monospace;
+  color: #567482;
+  word-wrap: normal;
+  background-color: #f3f6fa;
+  border: solid 1px #dce6f0;
+  border-radius: 0.3rem; }
+  .main-content pre > code {
+    padding: 0;
+    margin: 0;
+    font-size: 0.9rem;
+    color: #567482;
+    word-break: normal;
+    white-space: pre;
+    background: transparent;
+    border: 0; }
+.main-content .highlight {
+  margin-bottom: 1rem; }
+  .main-content .highlight pre {
+    margin-bottom: 0;
+    word-break: normal; }
+.main-content .highlight pre, .main-content pre {
+  padding: 0.8rem;
+  overflow: auto;
+  font-size: 0.9rem;
+  line-height: 1.45;
+  border-radius: 0.3rem; }
+.main-content pre code, .main-content pre tt {
+  display: inline;
+  max-width: initial;
+  padding: 0;
+  margin: 0;
+  overflow: initial;
+  line-height: inherit;
+  word-wrap: normal;
+  background-color: transparent;
+  border: 0; }
+  .main-content pre code:before, .main-content pre code:after, .main-content pre tt:before, .main-content pre tt:after {
+    content: normal; }
+.main-content ul, .main-content ol {
+  margin-top: 0; }
+.main-content blockquote {
+  padding: 0 1rem;
+  margin-left: 0;
+  color: #819198;
+  border-left: 0.3rem solid #dce6f0; }
+  .main-content blockquote > :first-child {
+    margin-top: 0; }
+  .main-content blockquote > :last-child {
+    margin-bottom: 0; }
+.main-content table {
+  display: block;
+  width: 100%;
+  overflow: auto;
+  word-break: normal;
+  word-break: keep-all; }
+  .main-content table th {
+    font-weight: bold; }
+  .main-content table th, .main-content table td {
+    padding: 0.5rem 1rem;
+    border: 1px solid #e9ebec; }
+.main-content dl {
+  padding: 0; }
+  .main-content dl dt {
+    padding: 0;
+    margin-top: 1rem;
+    font-size: 1rem;
+    font-weight: bold; }
+  .main-content dl dd {
+    padding: 0;
+    margin-bottom: 1rem; }
+.main-content hr {
+  height: 2px;
+  padding: 0;
+  margin: 1rem 0;
+  background-color: #eff0f1;
+  border: 0; }
+
+@media screen and (min-width: 64em) {
+  .main-content {
+    max-width: 64rem;
+    padding: 2rem 6rem;
+    margin: 0 auto;
+    font-size: 1.1rem; } }
+
+@media screen and (min-width: 42em) and (max-width: 64em) {
+  .main-content {
+    padding: 2rem 4rem;
+    font-size: 1.1rem; } }
+
+@media screen and (max-width: 42em) {
+  .main-content {
+    padding: 2rem 1rem;
+    font-size: 1rem; } }
+
+.site-footer {
+  padding-top: 2rem;
+  margin-top: 2rem;
+  border-top: solid 1px #eff0f1; }
+
+.site-footer-owner {
+  display: block;
+  font-weight: bold; }
+
+.site-footer-credits {
+  color: #819198; }
+
+@media screen and (min-width: 64em) {
+  .site-footer {
+    font-size: 1rem; } }
+
+@media screen and (min-width: 42em) and (max-width: 64em) {
+  .site-footer {
+    font-size: 1rem; } }
+
+@media screen and (max-width: 42em) {
+  .site-footer {
+    font-size: 0.9rem; } }

+ 45 - 0
src/vobject/docs/vobject.egg-info/PKG-INFO

@@ -0,0 +1,45 @@
+Metadata-Version: 1.1
+Name: vobject
+Version: 0.9.3
+Summary: A full-featured Python package for parsing and creating iCalendar and vCard files
+Home-page: http://eventable.github.io/vobject/
+Author: Sameen Karim
+Author-email: sameen@eventable.com
+License: Apache
+Download-URL: https://github.com/eventable/vobject/tarball/0.9.3
+Description: 
+        Description
+        -----------
+        
+        Parses iCalendar and vCard files into Python data structures, decoding the
+        relevant encodings. Also serializes vobject data structures to iCalendar, vCard,
+        or (experimentally) hCalendar unicode strings.
+        
+        Requirements
+        ------------
+        
+        Requires python 2.7 or later, dateutil 2.4.0 or later.
+        
+        Recent changes
+        --------------
+           - Python 3 compatible
+           - Updated version of dateutil (2.4.0 and above)
+           - More comprehensive unit tests available in tests.py
+           - Performance improvements in iteration
+           - Test files are included in PyPI download package
+        
+        For older changes, see
+           - http://eventable.github.io/vobject/#release-history or
+           - http://vobject.skyhouseconsulting.com/history.html
+Keywords: vobject,icalendar,vcard,ics,vcs,hcalendar
+Platform: any
+Classifier: Development Status :: 5 - Production/Stable
+Classifier:       Environment :: Console
+Classifier:       Intended Audience :: Developers
+Classifier:       License :: OSI Approved :: Apache Software License
+Classifier:       Natural Language :: English
+Classifier:       Operating System :: OS Independent
+Classifier:       Programming Language :: Python
+Classifier:       Programming Language :: Python :: 2.7
+Classifier:       Programming Language :: Python :: 3
+Classifier:       Topic :: Text Processing

+ 40 - 0
src/vobject/docs/vobject.egg-info/SOURCES.txt

@@ -0,0 +1,40 @@
+ACKNOWLEDGEMENTS.txt
+LICENSE-2.0.txt
+MANIFEST.in
+README.md
+setup.py
+test_files/availablity.ics
+test_files/badline.ics
+test_files/badstream.ics
+test_files/freebusy.ics
+test_files/journal.ics
+test_files/ms_tzid.ics
+test_files/recurrence.ics
+test_files/ruby_rrule.ics
+test_files/silly_test.ics
+test_files/simple_2_0_test.ics
+test_files/simple_3_0_test.ics
+test_files/simple_test.ics
+test_files/standard_test.ics
+test_files/timezones.ics
+test_files/tz_us_eastern.ics
+test_files/tzid_8bit.ics
+test_files/utf8_test.ics
+test_files/vcard_with_groups.ics
+test_files/vtodo.ics
+vobject/__init__.py
+vobject/base.py
+vobject/behavior.py
+vobject/change_tz.py
+vobject/hcalendar.py
+vobject/icalendar.py
+vobject/ics_diff.py
+vobject/vcard.py
+vobject/win32tz.py
+vobject.egg-info/PKG-INFO
+vobject.egg-info/SOURCES.txt
+vobject.egg-info/dependency_links.txt
+vobject.egg-info/entry_points.txt
+vobject.egg-info/requires.txt
+vobject.egg-info/top_level.txt
+vobject.egg-info/zip-safe

+ 1 - 0
src/vobject/docs/vobject.egg-info/dependency_links.txt

@@ -0,0 +1 @@
+

+ 4 - 0
src/vobject/docs/vobject.egg-info/entry_points.txt

@@ -0,0 +1,4 @@
+[console_scripts]
+change_tz = vobject.change_tz:main
+ics_diff = vobject.ics_diff:main
+

+ 1 - 0
src/vobject/docs/vobject.egg-info/requires.txt

@@ -0,0 +1 @@
+python-dateutil >= 2.4.0

+ 1 - 0
src/vobject/docs/vobject.egg-info/top_level.txt

@@ -0,0 +1 @@
+vobject

+ 1 - 0
src/vobject/docs/vobject.egg-info/zip-safe

@@ -0,0 +1 @@
+

+ 67 - 0
src/vobject/setup.py

@@ -0,0 +1,67 @@
+"""
+VObject: module for reading vCard and vCalendar files
+
+Description
+-----------
+
+Parses iCalendar and vCard files into Python data structures, decoding the
+relevant encodings. Also serializes vobject data structures to iCalendar, vCard,
+or (experimentally) hCalendar unicode strings.
+
+Requirements
+------------
+
+Requires python 2.7 or later, dateutil 2.4.0 or later.
+
+Recent changes
+--------------
+    - Revert too-strict serialization of timestamp values - broke too many other
+       implementations
+
+For older changes, see
+   - http://eventable.github.io/vobject/#release-history or
+   - http://vobject.skyhouseconsulting.com/history.html
+"""
+
+from setuptools import setup, find_packages
+
+doclines = (__doc__ or '').splitlines()
+
+setup(name = "vobject",
+      version = "0.9.6.1",
+      author = "Jeffrey Harris",
+      author_email = "jeffrey@osafoundation.org",
+      maintainer = "Sameen Karim",
+      maintainer_email="sameen@eventable.com",
+      license = "Apache",
+      zip_safe = True,
+      url = "http://eventable.github.io/vobject/",
+      download_url = 'https://github.com/eventable/vobject/tarball/0.9.6.1',
+      bugtrack_url = "https://github.com/eventable/vobject/issues",
+      entry_points = {
+            'console_scripts': [
+                  'ics_diff = vobject.ics_diff:main',
+                  'change_tz = vobject.change_tz:main'
+            ]
+      },
+      include_package_data = True,
+      install_requires = ['python-dateutil >= 2.4.0'],
+      platforms = ["any"],
+      packages = find_packages(),
+      description = "A full-featured Python package for parsing and creating "
+                    "iCalendar and vCard files",
+      long_description = "\n".join(doclines[2:]),
+      keywords = ['vobject', 'icalendar', 'vcard', 'ics', 'vcs', 'hcalendar'],
+      test_suite="tests",
+      classifiers =  """
+      Development Status :: 5 - Production/Stable
+      Environment :: Console
+      Intended Audience :: Developers
+      License :: OSI Approved :: Apache Software License
+      Natural Language :: English
+      Operating System :: OS Independent
+      Programming Language :: Python
+      Programming Language :: Python :: 2.7
+      Programming Language :: Python :: 3
+      Topic :: Text Processing""".strip().splitlines()
+      )

+ 14 - 0
src/vobject/test_files/availablity.ics

@@ -0,0 +1,14 @@
+BEGIN:VAVAILABILITY
+UID:test
+DTSTART:20060216T000000Z
+DTEND:20060217T000000Z
+BEGIN:AVAILABLE
+UID:test1
+DTSTART:20060216T090000Z
+DTEND:20060216T120000Z
+DTSTAMP:20060215T000000Z
+SUMMARY:Available in the morning
+END:AVAILABLE
+BUSYTYPE:BUSY
+DTSTAMP:20060215T000000Z
+END:VAVAILABILITY

+ 10 - 0
src/vobject/test_files/badline.ics

@@ -0,0 +1,10 @@
+BEGIN:VCALENDAR
+METHOD:PUBLISH
+VERSION:2.0
+BEGIN:VEVENT
+DTSTART:19870405T020000
+X-BAD/SLASH:TRUE
+X-BAD_UNDERSCORE:TRUE
+UID:EC9439B1-FF65-11D6-9973-003065F99D04
+END:VEVENT
+END:VCALENDAR

+ 16 - 0
src/vobject/test_files/badstream.ics

@@ -0,0 +1,16 @@
+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

+ 8 - 0
src/vobject/test_files/freebusy.ics

@@ -0,0 +1,8 @@
+BEGIN:VFREEBUSY
+UID:test
+DTSTART:20060216T010000Z
+DTEND:20060216T030000Z
+DTSTAMP:20060215T000000Z
+FREEBUSY:20060216T010000Z/PT1H
+FREEBUSY:20060216T010000Z/20060216T030000Z
+END:VFREEBUSY

+ 15 - 0
src/vobject/test_files/journal.ics

@@ -0,0 +1,15 @@
+BEGIN:VJOURNAL
+UID:19970901T130000Z-123405@example.com
+DTSTAMP:19970901T130000Z
+DTSTART;VALUE=DATE:19970317
+SUMMARY:Staff meeting minutes
+DESCRIPTION:1. Staff meeting: Participants include Joe\,
+  Lisa\, and Bob. Aurora project plans were reviewed.
+  There is currently no budget reserves for this project.
+  Lisa will escalate to management. Next meeting on Tuesday.\n
+ 2. Telephone Conference: ABC Corp. sales representative
+  called to discuss new printer. Promised to get us a demo by
+  Friday.\n3. Henry Miller (Handsoff Insurance): Car was
+  totaled by tree. Is looking into a loaner car. 555-2323
+  (tel).
+END:VJOURNAL

+ 85 - 0
src/vobject/test_files/more_tests.txt

@@ -0,0 +1,85 @@
+
+Unicode in vCards
+.................
+
+>>> import vobject
+>>> card = vobject.vCard()
+>>> card.add('fn').value = u'Hello\u1234 World!'
+>>> card.add('n').value = vobject.vcard.Name('World', u'Hello\u1234')
+>>> card.add('adr').value = vobject.vcard.Address(u'5\u1234 Nowhere, Apt 1', 'Berkeley', 'CA', '94704', 'USA')
+>>> card
+<VCARD| [<ADR{}5? Nowhere, Apt 1\nBerkeley, CA 94704\nUSA>, <FN{}Hello? World!>, <N{} Hello?  World >]>
+>>> card.serialize()
+u'BEGIN:VCARD\r\nVERSION:3.0\r\nADR:;;5\u1234 Nowhere\\, Apt 1;Berkeley;CA;94704;USA\r\nFN:Hello\u1234 World!\r\nN:World;Hello\u1234;;;\r\nEND:VCARD\r\n'
+>>> print(card.serialize())
+BEGIN:VCARD
+VERSION:3.0
+ADR:;;5ሴ Nowhere\, Apt 1;Berkeley;CA;94704;USA
+FN:Helloሴ World!
+N:World;Helloሴ;;;
+END:VCARD
+
+Helper function
+...............
+>>> from pkg_resources import resource_stream
+>>> def get_stream(path):
+...     try:
+...         return resource_stream(__name__, 'test_files/' + path)
+...     except: # different paths, depending on whether doctest is run directly
+...         return resource_stream(__name__, path)
+
+Unicode in TZID
+...............
+>>> f = get_stream("tzid_8bit.ics")
+>>> cal = vobject.readOne(f)
+>>> print(cal.vevent.dtstart.value)
+2008-05-30 15:00:00+06:00
+>>> print(cal.vevent.dtstart.serialize())
+DTSTART;TZID=Екатеринбург:20080530T150000
+
+Commas in TZID
+..............
+>>> f = get_stream("ms_tzid.ics")
+>>> cal = vobject.readOne(f)
+>>> print(cal.vevent.dtstart.value)
+2008-05-30 15:00:00+10:00
+
+Equality in vCards
+..................
+
+>>> card.adr.value == vobject.vcard.Address('Just a street')
+False
+>>> card.adr.value == vobject.vcard.Address(u'5\u1234 Nowhere, Apt 1', 'Berkeley', 'CA', '94704', 'USA')
+True
+
+Organization (org)
+..................
+
+>>> card.add('org').value = ["Company, Inc.", "main unit", "sub-unit"]
+>>> print(card.org.serialize())
+ORG:Company\, Inc.;main unit;sub-unit
+
+Ruby escapes semi-colons in rrules
+..................................
+
+>>> f = get_stream("ruby_rrule.ics")
+>>> cal = vobject.readOne(f)
+>>> iter(cal.vevent.rruleset).next()
+datetime.datetime(2003, 1, 1, 7, 0)
+
+quoted-printable
+................
+
+>>> vcf = 'BEGIN:VCARD\nVERSION:2.1\nN;ENCODING=QUOTED-PRINTABLE:;=E9\nFN;ENCODING=QUOTED-PRINTABLE:=E9\nTEL;HOME:0111111111\nEND:VCARD\n\n'
+>>> vcf = vobject.readOne(vcf)
+>>> vcf.n.value
+<Name:  ?   >
+>>> vcf.n.value.given
+u'\xe9'
+>>> vcf.serialize()
+'BEGIN:VCARD\r\nVERSION:2.1\r\nFN:\xc3\xa9\r\nN:;\xc3\xa9;;;\r\nTEL:0111111111\r\nEND:VCARD\r\n'
+
+>>> vcs = 'BEGIN:VCALENDAR\r\nPRODID:-//OpenSync//NONSGML OpenSync vformat 0.3//EN\r\nVERSION:1.0\r\nBEGIN:VEVENT\r\nDESCRIPTION;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:foo =C3=A5=0Abar =C3=A4=\r\n=0Abaz =C3=B6\r\nUID:20080406T152030Z-7822\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n'
+>>> vcs = vobject.readOne(vcs, allowQP = True)
+>>> vcs.serialize()
+'BEGIN:VCALENDAR\r\nVERSION:1.0\r\nPRODID:-//OpenSync//NONSGML OpenSync vformat 0.3//EN\r\nBEGIN:VEVENT\r\nUID:20080406T152030Z-7822\r\nDESCRIPTION:foo \xc3\xa5\\nbar \xc3\xa4\\nbaz \xc3\xb6\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n'

+ 39 - 0
src/vobject/test_files/ms_tzid.ics

@@ -0,0 +1,39 @@
+BEGIN:VCALENDAR
+PRODID:-//Microsoft Corporation//Outlook 12.0 MIMEDIR//EN
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:Canberra, Melbourne, Sydney
+BEGIN:STANDARD
+DTSTART:20010325T020000
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3;UNTIL=20050327T070000Z
+TZOFFSETFROM:+1100
+TZOFFSETTO:+1000
+TZNAME:Standard Time
+END:STANDARD
+BEGIN:STANDARD
+DTSTART:20060402T020000
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=4;UNTIL=20060402T070000Z
+TZOFFSETFROM:+1100
+TZOFFSETTO:+1000
+TZNAME:Standard Time
+END:STANDARD
+BEGIN:STANDARD
+DTSTART:20070325T020000
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3
+TZOFFSETFROM:+1100
+TZOFFSETTO:+1000
+TZNAME:Standard Time
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:20001029T020000
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10
+TZOFFSETFROM:+1000
+TZOFFSETTO:+1100
+TZNAME:Daylight Savings Time
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+UID:CommaTest
+DTSTART;TZID="Canberra, Melbourne, Sydney":20080530T150000
+END:VEVENT
+END:VCALENDAR

+ 9 - 0
src/vobject/test_files/recurrence-offset-naive.ics

@@ -0,0 +1,9 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20130117
+DTEND;VALUE=DATE:20130118
+RRULE:FREQ=WEEKLY;UNTIL=20130330T230000Z;BYDAY=TH
+SUMMARY:Meeting
+END:VEVENT
+END:VCALENDAR

+ 9 - 0
src/vobject/test_files/recurrence-without-tz.ics

@@ -0,0 +1,9 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20130117
+DTEND;VALUE=DATE:20130118
+RRULE:FREQ=WEEKLY;UNTIL=20130330;BYDAY=TH
+SUMMARY:Meeting
+END:VEVENT
+END:VCALENDAR

+ 30 - 0
src/vobject/test_files/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

+ 16 - 0
src/vobject/test_files/ruby_rrule.ics

@@ -0,0 +1,16 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:PUBLISH
+PRODID:-//LinkeSOFT GmbH//NONSGML DIMEX//EN
+BEGIN:VEVENT
+SEQUENCE:0
+RRULE:FREQ=DAILY\;COUNT=10
+DTEND:20030101T080000
+UID:2008-05-29T17:31:42+02:00_865561242
+CATEGORIES:Unfiled
+SUMMARY:Something
+DTSTART:20030101T070000
+DTSTAMP:20080529T152100
+END:VEVENT
+END:VCALENDAR

+ 5 - 0
src/vobject/test_files/silly_test.ics

@@ -0,0 +1,5 @@
+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

+ 12 - 0
src/vobject/test_files/simple_2_0_test.ics

@@ -0,0 +1,12 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:Not very random UID
+DTSTART:20060509T000000
+ATTENDEE;CN=Fröhlich:mailto:froelich@example.com
+CREATED:20060101T180000Z
+DESCRIPTION:Test event
+DTSTAMP:20170626T000000Z
+END:VEVENT
+END:VCALENDAR

+ 13 - 0
src/vobject/test_files/simple_3_0_test.ics

@@ -0,0 +1,13 @@
+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
+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\;\nEscape\, Test;Novosibirsk;;80214;Gnuland
+TEL;type=HOME:+01-(0)2-876.54.32
+ORG:University of Novosibirsk;Department of Octopus Parthenogenesis
+END:VCARD

+ 5 - 0
src/vobject/test_files/simple_test.ics

@@ -0,0 +1,5 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+SUMMARY;blah=hi!:Bastille Day Party
+END:VEVENT
+END:VCALENDAR

+ 41 - 0
src/vobject/test_files/standard_test.ics

@@ -0,0 +1,41 @@
+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

+ 107 - 0
src/vobject/test_files/timezones.ics

@@ -0,0 +1,107 @@
+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

+ 31 - 0
src/vobject/test_files/tz_us_eastern.ics

@@ -0,0 +1,31 @@
+BEGIN:VTIMEZONE
+TZID:US/Eastern
+BEGIN:STANDARD
+DTSTART:20001029T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10;UNTIL=20061029T060000Z
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:STANDARD
+DTSTART:20071104T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:20000402T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20060402T070000Z
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:20070311T020000
+RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+END:VTIMEZONE

+ 23 - 0
src/vobject/test_files/tzid_8bit.ics

@@ -0,0 +1,23 @@
+BEGIN:VCALENDAR
+PRODID:-//Microsoft Corporation//Outlook 12.0 MIMEDIR//EN
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:Екатеринбург
+BEGIN:STANDARD
+DTSTART:16011028T030000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZOFFSETFROM:+0600
+TZOFFSETTO:+0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:16010325T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
+TZOFFSETFROM:+0500
+TZOFFSETTO:+0600
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+UID:CyrillicTest
+DTSTART;TZID=Екатеринбург:20080530T150000
+END:VEVENT
+END:VCALENDAR

+ 39 - 0
src/vobject/test_files/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

+ 18 - 0
src/vobject/test_files/vcard_with_groups.ics

@@ -0,0 +1,18 @@
+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.\nNext line.
+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

+ 13 - 0
src/vobject/test_files/vtodo.ics

@@ -0,0 +1,13 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Example Corp.//CalDAV Client//EN
+BEGIN:VTODO
+UID:20070313T123432Z-456553@example.com
+DTSTAMP:20070313T123432Z
+DUE;VALUE=DATE:20070501
+SUMMARY:Submit Quebec Income Tax Return for 2006
+CLASS:CONFIDENTIAL
+CATEGORIES:FAMILY,FINANCE
+STATUS:NEEDS-ACTION
+END:VTODO
+END:VCALENDAR

+ 946 - 0
src/vobject/tests.py

@@ -0,0 +1,946 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import print_function
+
+import datetime
+import dateutil
+import re
+import sys
+import unittest
+import json
+
+from dateutil.tz import tzutc
+from dateutil.rrule import rrule, rruleset, WEEKLY, MONTHLY
+
+from vobject import base, iCalendar
+from vobject import icalendar
+
+from vobject.base import __behaviorRegistry as behavior_registry
+from vobject.base import ContentLine, parseLine, ParseError
+from vobject.base import readComponents, textLineToContentLine
+
+from vobject.change_tz import change_tz
+
+from vobject.icalendar import MultiDateBehavior, PeriodBehavior, \
+    RecurringComponent, utc
+from vobject.icalendar import parseDtstart, stringToTextValues, \
+    stringToPeriod, timedeltaToString
+
+two_hours = datetime.timedelta(hours=2)
+
+
+def get_test_file(path):
+    """
+    Helper function to open and read test files.
+    """
+    filepath = "test_files/{}".format(path)
+    if sys.version_info[0] < 3:
+        # On python 2, this library operates on bytes.
+        f = open(filepath, 'r')
+    else:
+        # On python 3, it operates on unicode. We need to specify an encoding
+        # for systems for which the preferred encoding isn't utf-8 (e.g windows)
+        f = open(filepath, 'r', encoding='utf-8')
+    text = f.read()
+    f.close()
+    return text
+
+
+class TestCalendarSerializing(unittest.TestCase):
+    """
+    Test creating an iCalendar file
+    """
+    max_diff = None
+
+    def test_scratchbuild(self):
+        """
+        CreateCalendar 2.0 format from scratch
+        """
+        test_cal = get_test_file("simple_2_0_test.ics")
+        cal = base.newFromBehavior('vcalendar', '2.0')
+        cal.add('vevent')
+        cal.vevent.add('dtstart').value = datetime.datetime(2006, 5, 9)
+        cal.vevent.add('description').value = "Test event"
+        cal.vevent.add('created').value = \
+            datetime.datetime(2006, 1, 1, 10,
+                              tzinfo=dateutil.tz.tzical(
+                                  "test_files/timezones.ics").get('US/Pacific'))
+        cal.vevent.add('uid').value = "Not very random UID"
+        cal.vevent.add('dtstamp').value = datetime.datetime(2017, 6, 26, 0, tzinfo=tzutc())
+
+        cal.vevent.add('attendee').value = 'mailto:froelich@example.com'
+        cal.vevent.attendee.params['CN'] = ['Fröhlich']
+
+        # Note we're normalizing line endings, because no one got time for that.
+        self.assertEqual(
+            cal.serialize().replace('\r\n', '\n'),
+            test_cal.replace('\r\n', '\n')
+        )
+
+    def test_unicode(self):
+        """
+        Test unicode characters
+        """
+        test_cal = get_test_file("utf8_test.ics")
+        vevent = base.readOne(test_cal).vevent
+        vevent2 = base.readOne(vevent.serialize())
+        self.assertEqual(str(vevent), str(vevent2))
+
+        self.assertEqual(
+            vevent.summary.value,
+            'The title こんにちはキティ'
+        )
+
+        if sys.version_info[0] < 3:
+            test_cal = test_cal.decode('utf-8')
+            vevent = base.readOne(test_cal).vevent
+            vevent2 = base.readOne(vevent.serialize())
+            self.assertEqual(str(vevent), str(vevent2))
+            self.assertEqual(
+                vevent.summary.value,
+                u'The title こんにちはキティ'
+            )
+
+    def test_wrapping(self):
+        """
+        Should support input file with a long text field covering multiple lines
+        """
+        test_journal = get_test_file("journal.ics")
+        vobj = base.readOne(test_journal)
+        vjournal = base.readOne(vobj.serialize())
+        self.assertTrue('Joe, Lisa, and Bob' in vjournal.description.value)
+        self.assertTrue('Tuesday.\n2.' in vjournal.description.value)
+
+    def test_multiline(self):
+        """
+        Multi-text serialization test
+        """
+        category = base.newFromBehavior('categories')
+        category.value = ['Random category']
+        self.assertEqual(
+            category.serialize().strip(),
+            "CATEGORIES:Random category"
+        )
+
+        category.value.append('Other category')
+        self.assertEqual(
+            category.serialize().strip(),
+            "CATEGORIES:Random category,Other category"
+        )
+
+    def test_semicolon_separated(self):
+        """
+        Semi-colon separated multi-text serialization test
+        """
+        request_status = base.newFromBehavior('request-status')
+        request_status.value = ['5.1', 'Service unavailable']
+        self.assertEqual(
+            request_status.serialize().strip(),
+            "REQUEST-STATUS:5.1;Service unavailable"
+        )
+
+    @staticmethod
+    def test_unicode_multiline():
+        """
+        Test multiline unicode characters
+        """
+        cal = iCalendar()
+        cal.add('method').value = 'REQUEST'
+        cal.add('vevent')
+        cal.vevent.add('created').value = datetime.datetime.now()
+        cal.vevent.add('summary').value = 'Классное событие'
+        cal.vevent.add('description').value = ('Классное событие Классное событие Классное событие Классное событие '
+                                               'Классное событие Классsdssdное событие')
+
+        # json tries to encode as utf-8 and it would break if some chars could not be encoded
+        json.dumps(cal.serialize())
+
+    @staticmethod
+    def test_ical_to_hcal():
+        """
+        Serializing iCalendar to hCalendar.
+
+        Since Hcalendar is experimental and the behavior doesn't seem to want to load,
+        This test will have to wait.
+
+
+        tzs = dateutil.tz.tzical("test_files/timezones.ics")
+        cal = base.newFromBehavior('hcalendar')
+        self.assertEqual(
+            str(cal.behavior),
+            "<class 'vobject.hcalendar.HCalendar'>"
+        )
+        cal.add('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 = tzs.get('US/Pacific'))
+        event2.add('location').value = "somewhere else"
+        event2.add('dtend').value = event2.dtstart.value + datetime.timedelta(days = 6)
+        hcal = cal.serialize()
+        """
+        #self.assertEqual(
+        #    str(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>
+        #    """
+        #)
+
+
+class TestBehaviors(unittest.TestCase):
+    """
+    Test Behaviors
+    """
+    def test_general_behavior(self):
+        """
+        Tests for behavior registry, getting and creating a behavior.
+        """
+        # Check expected behavior registry.
+        self.assertEqual(
+            sorted(behavior_registry.keys()),
+            ['', 'ACTION', 'ADR', 'AVAILABLE', 'BUSYTYPE', 'CALSCALE',
+             'CATEGORIES', 'CLASS', 'COMMENT', 'COMPLETED', 'CONTACT',
+             'CREATED', 'DAYLIGHT', 'DESCRIPTION', 'DTEND', 'DTSTAMP',
+             'DTSTART', 'DUE', 'DURATION', 'EXDATE', 'EXRULE', 'FN', 'FREEBUSY',
+             'LABEL', 'LAST-MODIFIED', 'LOCATION', 'METHOD', 'N', 'ORG',
+             'PHOTO', 'PRODID', 'RDATE', 'RECURRENCE-ID', 'RELATED-TO',
+             'REQUEST-STATUS', 'RESOURCES', 'RRULE', 'STANDARD', 'STATUS',
+             'SUMMARY', 'TRANSP', 'TRIGGER', 'UID', 'VALARM', 'VAVAILABILITY',
+             'VCALENDAR', 'VCARD', 'VEVENT', 'VFREEBUSY', 'VJOURNAL',
+             'VTIMEZONE', 'VTODO']
+        )
+
+        # test get_behavior
+        behavior = base.getBehavior('VCALENDAR')
+        self.assertEqual(
+            str(behavior),
+            "<class 'vobject.icalendar.VCalendar2_0'>"
+        )
+        self.assertTrue(behavior.isComponent)
+
+        self.assertEqual(
+            base.getBehavior("invalid_name"),
+            None
+        )
+        # test for ContentLine (not a component)
+        non_component_behavior = base.getBehavior('RDATE')
+        self.assertFalse(non_component_behavior.isComponent)
+
+    def test_MultiDateBehavior(self):
+        """
+        Test MultiDateBehavior
+        """
+        parseRDate = MultiDateBehavior.transformToNative
+        self.assertEqual(
+            str(parseRDate(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)]>"
+        )
+        self.assertEqual(
+            str(parseRDate(textLineToContentLine("RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H"))),
+            "<RDATE{'VALUE': ['PERIOD']}[(datetime.datetime(1996, 4, 3, 2, 0, tzinfo=tzutc()), datetime.datetime(1996, 4, 3, 4, 0, tzinfo=tzutc())), (datetime.datetime(1996, 4, 4, 1, 0, tzinfo=tzutc()), " + ("datetime.timedelta(0, 10800)" if sys.version_info < (3,7) else "datetime.timedelta(seconds=10800)") + ")]>"
+        )
+
+    def test_periodBehavior(self):
+        """
+        Test PeriodBehavior
+        """
+        line = ContentLine('test', [], '', isNative=True)
+        line.behavior = PeriodBehavior
+        line.value = [(datetime.datetime(2006, 2, 16, 10), two_hours)]
+
+        self.assertEqual(
+            line.transformFromNative().value,
+            '20060216T100000/PT2H'
+        )
+        self.assertEqual(
+            line.transformToNative().value,
+            [(datetime.datetime(2006, 2, 16, 10, 0),
+              datetime.timedelta(0, 7200))]
+        )
+
+        line.value.append((datetime.datetime(2006, 5, 16, 10), two_hours))
+
+        self.assertEqual(
+            line.serialize().strip(),
+            'TEST:20060216T100000/PT2H,20060516T100000/PT2H'
+        )
+
+
+class TestVTodo(unittest.TestCase):
+    """
+    VTodo Tests
+    """
+    def test_vtodo(self):
+        """
+        Test VTodo
+        """
+        vtodo = get_test_file("vtodo.ics")
+        obj = base.readOne(vtodo)
+        obj.vtodo.add('completed')
+        obj.vtodo.completed.value = datetime.datetime(2015,5,5,13,30)
+        self.assertEqual(obj.vtodo.completed.serialize()[0:23],
+                         'COMPLETED:20150505T1330')
+        obj = base.readOne(obj.serialize())
+        self.assertEqual(obj.vtodo.completed.value,
+                         datetime.datetime(2015,5,5,13,30))
+
+
+class TestVobject(unittest.TestCase):
+    """
+    VObject Tests
+    """
+    max_diff = None
+
+    @classmethod
+    def setUpClass(cls):
+        """
+        Method for setting up class fixture before running tests in the class.
+        Fetches test file.
+        """
+        cls.simple_test_cal = get_test_file("simple_test.ics")
+
+    def test_readComponents(self):
+        """
+        Test if reading components correctly
+        """
+        cal = next(readComponents(self.simple_test_cal))
+
+        self.assertEqual(str(cal), "<VCALENDAR| [<VEVENT| [<SUMMARY{'BLAH': ['hi!']}Bastille Day Party>]>]>")
+        self.assertEqual(str(cal.vevent.summary), "<SUMMARY{'BLAH': ['hi!']}Bastille Day Party>")
+
+    def test_parseLine(self):
+        """
+        Test line parsing
+        """
+        self.assertEqual(parseLine("BLAH:"), ('BLAH', [], '', None))
+        self.assertEqual(
+            parseLine("RDATE:VALUE=DATE:19970304,19970504,19970704,19970904"),
+            ('RDATE', [], 'VALUE=DATE:19970304,19970504,19970704,19970904', None)
+        )
+        self.assertEqual(
+            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)
+        )
+        self.assertEqual(
+            parseLine("EMAIL;PREF;INTERNET:john@nowhere.com"),
+            ('EMAIL', [['PREF'], ['INTERNET']], 'john@nowhere.com', None)
+        )
+        self.assertEqual(
+            parseLine('EMAIL;TYPE="blah",hah;INTERNET="DIGI",DERIDOO:john@nowhere.com'),
+            ('EMAIL', [['TYPE', 'blah', 'hah'], ['INTERNET', 'DIGI', 'DERIDOO']], 'john@nowhere.com', None)
+        )
+        self.assertEqual(
+            parseLine('item1.ADR;type=HOME;type=pref:;;Reeperbahn 116;Hamburg;;20359;'),
+            ('ADR', [['type', 'HOME'], ['type', 'pref']], ';;Reeperbahn 116;Hamburg;;20359;', 'item1')
+        )
+        self.assertRaises(ParseError, parseLine, ":")
+
+
+class TestGeneralFileParsing(unittest.TestCase):
+    """
+    General tests for parsing ics files.
+    """
+    def test_readOne(self):
+        """
+        Test reading first component of ics
+        """
+        cal = get_test_file("silly_test.ics")
+        silly = base.readOne(cal)
+        self.assertEqual(
+            str(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>]>"
+        )
+        self.assertEqual(
+            str(silly.stuff),
+            "<STUFF{}foldedline>"
+        )
+
+    def test_importing(self):
+        """
+        Test importing ics
+        """
+        cal = get_test_file("standard_test.ics")
+        c = base.readOne(cal, validate=True)
+        self.assertEqual(
+            str(c.vevent.valarm.trigger),
+            "<TRIGGER{}-1 day, 0:00:00>"
+        )
+
+        self.assertEqual(
+            str(c.vevent.dtstart.value),
+            "2002-10-28 14:00:00-08:00"
+        )
+        self.assertTrue(
+            isinstance(c.vevent.dtstart.value, datetime.datetime)
+        )
+        self.assertEqual(
+            str(c.vevent.dtend.value),
+            "2002-10-28 15:00:00-08:00"
+        )
+        self.assertTrue(
+            isinstance(c.vevent.dtend.value, datetime.datetime)
+        )
+        self.assertEqual(
+            c.vevent.dtstamp.value,
+            datetime.datetime(2002, 10, 28, 1, 17, 6, tzinfo=tzutc())
+        )
+
+        vevent = c.vevent.transformFromNative()
+        self.assertEqual(
+            str(vevent.rrule),
+            "<RRULE{}FREQ=Weekly;COUNT=10>"
+        )
+
+    def test_bad_stream(self):
+        """
+        Test bad ics stream
+        """
+        cal = get_test_file("badstream.ics")
+        self.assertRaises(ParseError, base.readOne, cal)
+
+    def test_bad_line(self):
+        """
+        Test bad line in ics file
+        """
+        cal = get_test_file("badline.ics")
+        self.assertRaises(ParseError, base.readOne, cal)
+
+        newcal = base.readOne(cal, ignoreUnreadable=True)
+        self.assertEqual(
+            str(newcal.vevent.x_bad_underscore),
+            '<X-BAD-UNDERSCORE{}TRUE>'
+        )
+
+    def test_parseParams(self):
+        """
+        Test parsing parameters
+        """
+        self.assertEqual(
+            base.parseParams(';ALTREP="http://www.wiz.org"'),
+            [['ALTREP', 'http://www.wiz.org']]
+        )
+        self.assertEqual(
+            base.parseParams(';ALTREP="http://www.wiz.org;;",Blah,Foo;NEXT=Nope;BAR'),
+            [['ALTREP', 'http://www.wiz.org;;', 'Blah', 'Foo'],
+             ['NEXT', 'Nope'], ['BAR']]
+        )
+
+
+class TestVcards(unittest.TestCase):
+    """
+    Test VCards
+    """
+    @classmethod
+    def setUpClass(cls):
+        """
+        Method for setting up class fixture before running tests in the class.
+        Fetches test file.
+        """
+        cls.test_file = get_test_file("vcard_with_groups.ics")
+        cls.card = base.readOne(cls.test_file)
+
+    def test_vcard_creation(self):
+        """
+        Test creating a vCard
+        """
+        vcard = base.newFromBehavior('vcard', '3.0')
+        self.assertEqual(
+            str(vcard),
+            "<VCARD| []>"
+        )
+
+    def test_default_behavior(self):
+        """
+        Default behavior test.
+        """
+        card = self.card
+        self.assertEqual(
+            base.getBehavior('note'),
+            None
+        )
+        self.assertEqual(
+            str(card.note.value),
+            "The Mayor of the great city of Goerlitz in the great country of Germany.\nNext line."
+        )
+
+    def test_with_groups(self):
+        """
+        vCard groups test
+        """
+        card = self.card
+        self.assertEqual(
+            str(card.group),
+            'home'
+        )
+        self.assertEqual(
+            str(card.tel.group),
+            'home'
+        )
+
+        card.group = card.tel.group = 'new'
+        self.assertEqual(
+            str(card.tel.serialize().strip()),
+            'new.TEL;TYPE=fax,voice,msg:+49 3581 123456'
+        )
+        self.assertEqual(
+            str(card.serialize().splitlines()[0]),
+            'new.BEGIN:VCARD'
+        )
+
+
+    def test_vcard_3_parsing(self):
+        """
+        VCARD 3.0 parse test
+        """
+        test_file = get_test_file("simple_3_0_test.ics")
+        card = base.readOne(test_file)
+        # value not rendering correctly?
+        #self.assertEqual(
+        #    card.adr.value,
+        #    "<Address: Haight Street 512;\nEscape, Test\nNovosibirsk,  80214\nGnuland>"
+        #)
+        self.assertEqual(
+            card.org.value,
+            ["University of Novosibirsk", "Department of Octopus Parthenogenesis"]
+        )
+
+        for _ in range(3):
+            new_card = base.readOne(card.serialize())
+            self.assertEqual(new_card.org.value, card.org.value)
+            card = new_card
+
+
+class TestIcalendar(unittest.TestCase):
+    """
+    Tests for icalendar.py
+    """
+    max_diff = None
+    def test_parseDTStart(self):
+        """
+        Should take a content line and return a datetime object.
+        """
+        self.assertEqual(
+            parseDtstart(textLineToContentLine("DTSTART:20060509T000000")),
+            datetime.datetime(2006, 5, 9, 0, 0)
+        )
+
+    def test_regexes(self):
+        """
+        Test regex patterns
+        """
+        self.assertEqual(
+            re.findall(base.patterns['name'], '12foo-bar:yay'),
+            ['12foo-bar', 'yay']
+        )
+        self.assertEqual(
+            re.findall(base.patterns['safe_char'], 'a;b"*,cd'),
+            ['a', 'b', '*', 'c', 'd']
+        )
+        self.assertEqual(
+            re.findall(base.patterns['qsafe_char'], 'a;b"*,cd'),
+            ['a', ';', 'b', '*', ',', 'c', 'd']
+        )
+        self.assertEqual(
+            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:;"')
+        self.assertEqual(
+            match.group('value'),
+            'value:;"'
+        )
+        self.assertEqual(
+            match.group('name'),
+            'TEST'
+        )
+        self.assertEqual(
+            match.group('params'),
+            ';ALTREP="http://www.wiz.org"'
+        )
+
+    def test_stringToTextValues(self):
+        """
+        Test string lists
+        """
+        self.assertEqual(
+            stringToTextValues(''),
+            ['']
+        )
+        self.assertEqual(
+            stringToTextValues('abcd,efgh'),
+            ['abcd', 'efgh']
+        )
+
+    def test_stringToPeriod(self):
+        """
+        Test datetime strings
+        """
+        self.assertEqual(
+            stringToPeriod("19970101T180000Z/19970102T070000Z"),
+            (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()),
+             datetime.datetime(1997, 1, 2, 7, 0, tzinfo=tzutc()))
+        )
+        self.assertEqual(
+            stringToPeriod("19970101T180000Z/PT1H"),
+            (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()),
+             datetime.timedelta(0, 3600))
+        )
+
+    def test_timedeltaToString(self):
+        """
+        Test timedelta strings
+        """
+        self.assertEqual(
+            timedeltaToString(two_hours),
+            'PT2H'
+        )
+        self.assertEqual(
+            timedeltaToString(datetime.timedelta(minutes=20)),
+            'PT20M'
+        )
+
+    def test_vtimezone_creation(self):
+        """
+        Test timezones
+        """
+        tzs = dateutil.tz.tzical("test_files/timezones.ics")
+        pacific = icalendar.TimezoneComponent(tzs.get('US/Pacific'))
+        self.assertEqual(
+            str(pacific),
+            "<VTIMEZONE | <TZID{}US/Pacific>>"
+        )
+        santiago = icalendar.TimezoneComponent(tzs.get('Santiago'))
+        self.assertEqual(
+            str(santiago),
+            "<VTIMEZONE | <TZID{}Santiago>>"
+        )
+        for year in range(2001, 2010):
+            for month in (2, 9):
+                dt = datetime.datetime(year, month, 15,
+                                       tzinfo=tzs.get('Santiago'))
+                self.assertTrue(dt.replace(tzinfo=tzs.get('Santiago')), dt)
+
+    @staticmethod
+    def test_timezone_serializing():
+        """
+        Serializing with timezones test
+        """
+        tzs = dateutil.tz.tzical("test_files/timezones.ics")
+        pacific = tzs.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)
+        evruleset = rruleset()
+        evruleset.rrule(rrule(WEEKLY, interval=2, byweekday=[2,4],
+                              until=datetime.datetime(2005, 12, 15, 9)))
+        evruleset.rrule(rrule(MONTHLY, bymonthday=[-1,-5]))
+        evruleset.exdate(datetime.datetime(2005, 10, 14, 9, tzinfo=pacific))
+        ev.rruleset = evruleset
+        ev.add('duration').value = datetime.timedelta(hours=1)
+
+        apple = tzs.get('America/Montreal')
+        ev.dtstart.value = datetime.datetime(2005, 10, 12, 9, tzinfo=apple)
+
+    def test_pytz_timezone_serializing(self):
+        """
+        Serializing with timezones from pytz test
+        """
+        try:
+            import pytz
+        except ImportError:
+            return self.skipTest("pytz not installed")  # NOQA
+
+        # Avoid conflicting cached tzinfo from other tests
+        def unregister_tzid(tzid):
+            """Clear tzid from icalendar TZID registry"""
+            if icalendar.getTzid(tzid, False):
+                icalendar.registerTzid(tzid, None)
+
+        unregister_tzid('US/Eastern')
+        eastern = pytz.timezone('US/Eastern')
+        cal = base.Component('VCALENDAR')
+        cal.setBehavior(icalendar.VCalendar2_0)
+        ev = cal.add('vevent')
+        ev.add('dtstart').value = eastern.localize(
+            datetime.datetime(2008, 10, 12, 9))
+        serialized = cal.serialize()
+
+        expected_vtimezone = get_test_file("tz_us_eastern.ics")
+        self.assertIn(
+            expected_vtimezone.replace('\r\n', '\n'),
+            serialized.replace('\r\n', '\n')
+        )
+
+        # Exhaustively test all zones (just looking for no errors)
+        for tzname in pytz.all_timezones:
+            unregister_tzid(tzname)
+            tz = icalendar.TimezoneComponent(tzinfo=pytz.timezone(tzname))
+            tz.serialize()
+
+    def test_freeBusy(self):
+        """
+        Test freebusy components
+        """
+        test_cal = get_test_file("freebusy.ics")
+
+        vfb = base.newFromBehavior('VFREEBUSY')
+        vfb.add('uid').value = 'test'
+        vfb.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc)
+        vfb.add('dtstart').value = datetime.datetime(2006, 2, 16, 1, tzinfo=utc)
+        vfb.add('dtend').value   = vfb.dtstart.value + two_hours
+        vfb.add('freebusy').value = [(vfb.dtstart.value, two_hours / 2)]
+        vfb.add('freebusy').value = [(vfb.dtstart.value, vfb.dtend.value)]
+
+        self.assertEqual(
+            vfb.serialize().replace('\r\n', '\n'),
+            test_cal.replace('\r\n', '\n')
+        )
+
+    def test_availablity(self):
+        """
+        Test availability components
+        """
+        test_cal = get_test_file("availablity.ics")
+
+        vcal = base.newFromBehavior('VAVAILABILITY')
+        vcal.add('uid').value = 'test'
+        vcal.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc)
+        vcal.add('dtstart').value = datetime.datetime(2006, 2, 16, 0, tzinfo=utc)
+        vcal.add('dtend').value   = datetime.datetime(2006, 2, 17, 0, tzinfo=utc)
+        vcal.add('busytype').value = "BUSY"
+
+        av = base.newFromBehavior('AVAILABLE')
+        av.add('uid').value = 'test1'
+        av.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc)
+        av.add('dtstart').value = datetime.datetime(2006, 2, 16, 9, tzinfo=utc)
+        av.add('dtend').value   = datetime.datetime(2006, 2, 16, 12, tzinfo=utc)
+        av.add('summary').value = "Available in the morning"
+
+        vcal.add(av)
+
+        self.assertEqual(
+            vcal.serialize().replace('\r\n', '\n'),
+            test_cal.replace('\r\n', '\n')
+        )
+
+    def test_recurrence(self):
+        """
+        Ensure date valued UNTILs in rrules are in a reasonable timezone,
+        and include that day (12/28 in this test)
+        """
+        test_file = get_test_file("recurrence.ics")
+        cal = base.readOne(test_file)
+        dates = list(cal.vevent.getrruleset())
+        self.assertEqual(
+            dates[0],
+            datetime.datetime(2006, 1, 26, 23, 0, tzinfo=tzutc())
+        )
+        self.assertEqual(
+            dates[1],
+            datetime.datetime(2006, 2, 23, 23, 0, tzinfo=tzutc())
+        )
+        self.assertEqual(
+            dates[-1],
+            datetime.datetime(2006, 12, 28, 23, 0, tzinfo=tzutc())
+        )
+
+    def test_recurring_component(self):
+        """
+        Test recurring events
+        """
+        vevent = RecurringComponent(name='VEVENT')
+
+        # init
+        self.assertTrue(vevent.isNative)
+
+        # rruleset should be None at this point.
+        # No rules have been passed or created.
+        self.assertEqual(vevent.rruleset, None)
+
+        # Now add start and rule for recurring event
+        vevent.add('dtstart').value = datetime.datetime(2005, 1, 19, 9)
+        vevent.add('rrule').value =u"FREQ=WEEKLY;COUNT=2;INTERVAL=2;BYDAY=TU,TH"
+        self.assertEqual(
+            list(vevent.rruleset),
+            [datetime.datetime(2005, 1, 20, 9, 0), datetime.datetime(2005, 2, 1, 9, 0)]
+        )
+        self.assertEqual(
+            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)
+        self.assertEqual(
+            list(vevent.rruleset),
+            [datetime.datetime(2005, 3, 29, 0, 0), datetime.datetime(2005, 3, 31, 0, 0)]
+        )
+        self.assertEqual(
+            list(vevent.getrruleset(True)),
+            [datetime.datetime(2005, 3, 18, 0, 0), datetime.datetime(2005, 3, 29, 0, 0)]
+        )
+
+    def test_recurrence_without_tz(self):
+        """
+        Test recurring vevent missing any time zone definitions.
+        """
+        test_file = get_test_file("recurrence-without-tz.ics")
+        cal = base.readOne(test_file)
+        dates = list(cal.vevent.getrruleset())
+        self.assertEqual(dates[0], datetime.datetime(2013, 1, 17, 0, 0))
+        self.assertEqual(dates[1], datetime.datetime(2013, 1, 24, 0, 0))
+        self.assertEqual(dates[-1], datetime.datetime(2013, 3, 28, 0, 0))
+
+    def test_recurrence_offset_naive(self):
+        """
+        Ensure recurring vevent missing some time zone definitions is
+        parsing. See isseu #75.
+        """
+        test_file = get_test_file("recurrence-offset-naive.ics")
+        cal = base.readOne(test_file)
+        dates = list(cal.vevent.getrruleset())
+        self.assertEqual(dates[0], datetime.datetime(2013, 1, 17, 0, 0))
+        self.assertEqual(dates[1], datetime.datetime(2013, 1, 24, 0, 0))
+        self.assertEqual(dates[-1], datetime.datetime(2013, 3, 28, 0, 0))
+
+
+class TestChangeTZ(unittest.TestCase):
+    """
+    Tests for change_tz.change_tz
+    """
+    class StubCal(object):
+        class StubEvent(object):
+            class Node(object):
+                def __init__(self, value):
+                    self.value = value
+
+            def __init__(self, dtstart, dtend):
+                self.dtstart = self.Node(dtstart)
+                self.dtend = self.Node(dtend)
+
+        def __init__(self, dates):
+            """
+            dates is a list of tuples (dtstart, dtend)
+            """
+            self.vevent_list = [self.StubEvent(*d) for d in dates]
+
+    def test_change_tz(self):
+        """
+        Change the timezones of events in a component to a different
+        timezone
+        """
+
+        # Setup - create a stub vevent list
+        old_tz = dateutil.tz.gettz('UTC')  # 0:00
+        new_tz = dateutil.tz.gettz('America/Chicago')  # -5:00
+
+        dates = [
+            (datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=old_tz),
+             datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=old_tz)),
+            (datetime.datetime(2010, 12, 31, 23, 59, 59, 0, tzinfo=old_tz),
+             datetime.datetime(2011, 1, 2, 3, 0, 0, 0, tzinfo=old_tz))]
+
+        cal = self.StubCal(dates)
+
+        # Exercise - change the timezone
+        change_tz(cal, new_tz, dateutil.tz.gettz('UTC'))
+
+        # Test - that the tzs were converted correctly
+        expected_new_dates = [
+            (datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz),
+             datetime.datetime(1999, 12, 31, 18, 0, 0, 0, tzinfo=new_tz)),
+            (datetime.datetime(2010, 12, 31, 17, 59, 59, 0, tzinfo=new_tz),
+             datetime.datetime(2011, 1, 1, 21, 0, 0, 0, tzinfo=new_tz))]
+
+        for vevent, expected_datepair in zip(cal.vevent_list,
+                                             expected_new_dates):
+            self.assertEqual(vevent.dtstart.value, expected_datepair[0])
+            self.assertEqual(vevent.dtend.value, expected_datepair[1])
+
+    def test_change_tz_utc_only(self):
+        """
+        Change any UTC timezones of events in a component to a different
+        timezone
+        """
+
+        # Setup - create a stub vevent list
+        utc_tz = dateutil.tz.gettz('UTC')  # 0:00
+        non_utc_tz = dateutil.tz.gettz('America/Santiago')  # -4:00
+        new_tz = dateutil.tz.gettz('America/Chicago')  # -5:00
+
+        dates = [
+            (datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=utc_tz),
+             datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=non_utc_tz))]
+
+        cal = self.StubCal(dates)
+
+        # Exercise - change the timezone passing utc_only=True
+        change_tz(cal, new_tz, dateutil.tz.gettz('UTC'), utc_only=True)
+
+        # Test - that only the utc item has changed
+        expected_new_dates = [
+            (datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz),
+             dates[0][1])]
+
+        for vevent, expected_datepair in zip(cal.vevent_list,
+                                             expected_new_dates):
+            self.assertEqual(vevent.dtstart.value, expected_datepair[0])
+            self.assertEqual(vevent.dtend.value, expected_datepair[1])
+
+    def test_change_tz_default(self):
+        """
+        Change the timezones of events in a component to a different
+        timezone, passing a default timezone that is assumed when the events
+        don't have one
+        """
+
+        # Setup - create a stub vevent list
+        new_tz = dateutil.tz.gettz('America/Chicago')  # -5:00
+
+        dates = [
+            (datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=None),
+             datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=None))]
+
+        cal = self.StubCal(dates)
+
+        # Exercise - change the timezone
+        change_tz(cal, new_tz, dateutil.tz.gettz('UTC'))
+
+        # Test - that the tzs were converted correctly
+        expected_new_dates = [
+            (datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz),
+             datetime.datetime(1999, 12, 31, 18, 0, 0, 0, tzinfo=new_tz))]
+
+        for vevent, expected_datepair in zip(cal.vevent_list,
+                                             expected_new_dates):
+            self.assertEqual(vevent.dtstart.value, expected_datepair[0])
+            self.assertEqual(vevent.dtend.value, expected_datepair[1])
+
+
+if __name__ == '__main__':
+    unittest.main()

+ 88 - 0
src/vobject/vobject/__init__.py

@@ -0,0 +1,88 @@
+"""
+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
+
+"""
+
+from .base import newFromBehavior, readOne, readComponents
+from . import icalendar, vcard
+
+
+def iCalendar():
+    return newFromBehavior('vcalendar', '2.0')
+
+
+def vCard():
+    return newFromBehavior('vcard', '3.0')

+ 1220 - 0
src/vobject/vobject/base.py

@@ -0,0 +1,1220 @@
+"""vobject module for reading vCard and vCalendar files."""
+
+from __future__ import print_function
+
+import copy
+import codecs
+import logging
+import re
+import six
+import sys
+
+# ------------------------------------ Python 2/3 compatibility challenges  ----
+# Python 3 no longer has a basestring type, so....
+try:
+    basestring = basestring
+except NameError:
+    basestring = (str, bytes)
+
+# One more problem ... in python2 the str operator breaks on unicode
+# objects containing non-ascii characters
+try:
+    unicode
+
+    def str_(s):
+        """
+        Return byte string with correct encoding
+        """
+        if type(s) == unicode:
+            return s.encode('utf-8')
+        else:
+            return str(s)
+except NameError:
+    def str_(s):
+        """
+        Return string
+        """
+        return s
+
+if not isinstance(b'', type('')):
+    unicode_type = str
+else:
+    unicode_type = unicode  # noqa
+
+
+def to_unicode(value):
+    """Converts a string argument to a unicode string.
+
+    If the argument is already a unicode string, it is returned
+    unchanged.  Otherwise it must be a byte string and is decoded as utf8.
+    """
+    if isinstance(value, unicode_type):
+        return value
+
+    return value.decode('utf-8')
+
+
+def to_basestring(s):
+    """Converts a string argument to a byte string.
+
+    If the argument is already a byte string, it is returned unchanged.
+    Otherwise it must be a unicode string and is encoded as utf8.
+    """
+    if isinstance(s, bytes):
+        return s
+
+    return s.encode('utf-8')
+
+# ------------------------------------ Logging ---------------------------------
+logger = logging.getLogger(__name__)
+if not logging.getLogger().handlers:
+    handler = logging.StreamHandler()
+    formatter = logging.Formatter('%(name)s %(levelname)s %(message)s')
+    handler.setFormatter(formatter)
+    logger.addHandler(handler)
+logger.setLevel(logging.ERROR)  # Log errors
+DEBUG = False  # Don't waste time on debug calls
+
+# ----------------------------------- Constants --------------------------------
+CR = '\r'
+LF = '\n'
+CRLF = CR + LF
+SPACE = ' '
+TAB = '\t'
+SPACEORTAB = SPACE + TAB
+
+# --------------------------------- 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 spec.
+
+    Current spec: 4.0 (http://tools.ietf.org/html/rfc6350)
+    """
+    def __init__(self, group=None, *args, **kwds):
+        super(VBase, self).__init__(*args, **kwds)
+        self.group = group
+        self.behavior = None
+        self.parentBehavior = None
+        self.isNative = False
+
+    def copy(self, copyit):
+        self.group = copyit.group
+        self.behavior = copyit.behavior
+        self.parentBehavior = copyit.parentBehavior
+        self.isNative = copyit.isNative
+
+    def validate(self, *args, **kwds):
+        """
+        Call the behavior's validate method, or return True.
+        """
+        if self.behavior:
+            return self.behavior.validate(self, *args, **kwds)
+        return True
+
+    def getChildren(self):
+        """
+        Return an iterable containing the contents of the object.
+        """
+        return []
+
+    def clearBehavior(self, cascade=True):
+        """
+        Set behavior to None. Do for all descendants if cascading.
+        """
+        self.behavior = None
+        if cascade:
+            self.transformChildrenFromNative()
+
+    def autoBehavior(self, cascade=False):
+        """
+        Set behavior if name is in self.parentBehavior.knownChildren.
+
+        If cascade is True, unset behavior and parentBehavior for all
+        descendants, then recalculate behavior and parentBehavior.
+        """
+        parentBehavior = self.parentBehavior
+        if parentBehavior is not None:
+            knownChildTup = parentBehavior.knownChildren.get(self.name, None)
+            if knownChildTup is not None:
+                behavior = getBehavior(self.name, knownChildTup[2])
+                if behavior is not None:
+                    self.setBehavior(behavior, cascade)
+                    if isinstance(self, ContentLine) and self.encoded:
+                        self.behavior.decode(self)
+            elif isinstance(self, ContentLine):
+                self.behavior = parentBehavior.defaultBehavior
+                if self.encoded and self.behavior:
+                    self.behavior.decode(self)
+
+    def setBehavior(self, behavior, cascade=True):
+        """
+        Set behavior. If cascade is True, autoBehavior all descendants.
+        """
+        self.behavior = behavior
+        if cascade:
+            for obj in self.getChildren():
+                obj.parentBehavior = behavior
+                obj.autoBehavior(True)
+
+    def transformToNative(self):
+        """
+        Transform this object into a custom VBase subclass.
+
+        transformToNative should always return a representation of this object.
+        It may do so by modifying self in place then returning self, or by
+        creating a new object.
+        """
+        if self.isNative or not self.behavior or not self.behavior.hasNative:
+            return self
+        else:
+            self_orig = copy.copy(self)
+            try:
+                return self.behavior.transformToNative(self)
+            except Exception as 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 on line {0}: {1}: {2}"
+                    msg = msg.format(lineNumber, sys.exc_info()[0], sys.exc_info()[1])
+                    msg = msg + " (" + str(self_orig) + ")"
+                    raise ParseError(msg, lineNumber)
+
+    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 as 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 on line {0} {1}: {2}"
+                    msg = msg.format(lineNumber, sys.exc_info()[0], sys.exc_info()[1])
+                    raise NativeError(msg, lineNumber)
+        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 {0!s} with behavior {1!s}".format(self.name, behavior))
+            return behavior.serialize(self, buf, lineLength, validate)
+        else:
+            if DEBUG:
+                logger.debug("serializing {0!s} without behavior".format(self.name))
+            return defaultSerialize(self, buf, lineLength)
+
+
+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.encoded = encoded
+        self.params = {}
+        self.singletonparams = []
+        self.isNative = isNative
+        self.lineNumber = lineNumber
+        self.value = value
+
+        def updateTable(x):
+            if len(x) == 1:
+                self.singletonparams += x
+            else:
+                paramlist = self.params.setdefault(x[0].upper(), [])
+                paramlist.extend(x[1:])
+
+        list(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 len(self.params['ENCODING']) == 0:
+                    del self.params['ENCODING']
+        if 'QUOTED-PRINTABLE' in self.singletonparams:
+            qp = True
+            self.singletonparams.remove('QUOTED-PRINTABLE')
+        if qp:
+            if 'ENCODING' in self.params:
+                self.value = codecs.decode(self.value.encode("utf-8"), "quoted-printable").decode(self.params['ENCODING'])
+            else:
+                if 'CHARSET' in self.params:
+                    self.value = codecs.decode(self.value.encode("utf-8"), "quoted-printable").decode(self.params['CHARSET'][0])
+                else:
+                    self.value = codecs.decode(self.value.encode("utf-8"), "quoted-printable").decode('utf-8')
+
+    @classmethod
+    def duplicate(clz, copyit):
+        newcopy = clz('', {}, '')
+        newcopy.copy(copyit)
+        return newcopy
+
+    def copy(self, copyit):
+        super(ContentLine, self).copy(copyit)
+        self.name = copyit.name
+        self.value = copy.copy(copyit.value)
+        self.encoded = self.encoded
+        self.params = copy.copy(copyit.params)
+        for k, v in self.params.items():
+            self.params[k] = copy.copy(v)
+        self.singletonparams = copy.copy(copyit.singletonparams)
+        self.lineNumber = copyit.lineNumber
+
+    def __eq__(self, other):
+        try:
+            return (self.name == other.name) and (self.params == other.params) and (self.value == other.value)
+        except Exception:
+            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 AttributeError(name)
+        except KeyError:
+            raise 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 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 v
+
+    def __str__(self):
+        try:
+            return "<{0}{1}{2}>".format(self.name, self.params, self.valueRepr())
+        except UnicodeEncodeError as e:
+            return "<{0}{1}{2}>".format(self.name, self.params, self.valueRepr().encode('utf-8'))
+
+    def __repr__(self):
+        return self.__str__()
+
+    def __unicode__(self):
+        return u"<{0}{1}{2}>".format(self.name, self.params, self.valueRepr())
+
+    def prettyPrint(self, level=0, tabwidth=3):
+        pre = ' ' * level * tabwidth
+        print(pre, self.name + ":", self.valueRepr())
+        if self.params:
+            print(pre, "params for ", self.name + ':')
+            for k in self.params.keys():
+                print(pre + ' ' * tabwidth, k, self.params[k])
+
+
+class Component(VBase):
+    """
+    A complex property that can contain multiple ContentLines.
+
+    For our purposes, a component must start with a BEGIN:xxxx line and end with
+    END:xxxx, or have a PROFILE:xxx line if a top-level component.
+
+    @ivar contents:
+        A dictionary of lists of Component or ContentLine instances. The keys
+        are the lowercased names of child ContentLines or Components.
+        Note that BEGIN and END ContentLines are not included in contents.
+    @ivar name:
+        Uppercase string used to represent this Component, i.e VCARD if the
+        serialized object starts with BEGIN:VCARD.
+    @ivar useBegin:
+        A boolean flag determining whether BEGIN: and END: lines should
+        be serialized.
+    """
+    def __init__(self, name=None, *args, **kwds):
+        super(Component, self).__init__(*args, **kwds)
+        self.contents = {}
+        if name:
+            self.name = name.upper()
+            self.useBegin = True
+        else:
+            self.name = ''
+            self.useBegin = False
+
+        self.autoBehavior()
+
+    @classmethod
+    def duplicate(cls, copyit):
+        newcopy = cls()
+        newcopy.copy(copyit)
+        return newcopy
+
+    def copy(self, copyit):
+        super(Component, self).copy(copyit)
+
+        # deep copy of contents
+        self.contents = {}
+        for key, lvalue in copyit.contents.items():
+            newvalue = []
+            for value in lvalue:
+                newitem = value.duplicate(value)
+                newvalue.append(newitem)
+            self.contents[key] = newvalue
+
+        self.name = copyit.name
+        self.useBegin = copyit.useBegin
+
+    def setProfile(self, name):
+        """
+        Assign a PROFILE to this unnamed component.
+
+        Used by vCard, not by vCalendar.
+        """
+        if self.name or self.useBegin:
+            if self.name == name:
+                return
+            raise VObjectError("This component already has a PROFILE or "
+                               "uses BEGIN.")
+        self.name = name.upper()
+
+    def __getattr__(self, name):
+        """
+        For convenience, make self.contents directly accessible.
+
+        Underscores, legal in python variable names, are converted to dashes,
+        which are legal in IANA tokens.
+        """
+        # if the object is being re-created by pickle, self.contents may not
+        # be set, don't get into an infinite loop over the issue
+        if name == 'contents':
+            return object.__getattribute__(self, name)
+        try:
+            if name.endswith('_list'):
+                return self.contents[toVName(name, 5)]
+            else:
+                return self.contents[toVName(name)][0]
+        except KeyError:
+            raise AttributeError(name)
+
+    normal_attributes = ['contents', 'name', 'behavior', 'parentBehavior', 'group']
+
+    def __setattr__(self, name, value):
+        """
+        For convenience, make self.contents directly accessible.
+
+        Underscores, legal in python variable names, are converted to dashes,
+        which are legal in IANA tokens.
+        """
+        if name not in self.normal_attributes and name.lower() == name:
+            if type(value) == list:
+                if name.endswith('_list'):
+                    name = name[:-5]
+                self.contents[toVName(name)] = value
+            elif name.endswith('_list'):
+                raise VObjectError("Component list set to a non-list")
+            else:
+                self.contents[toVName(name)] = [value]
+        else:
+            prop = getattr(self.__class__, name, None)
+            if isinstance(prop, property):
+                prop.fset(self, value)
+            else:
+                object.__setattr__(self, name, value)
+
+    def __delattr__(self, name):
+        try:
+            if name not in self.normal_attributes and name.lower() == name:
+                if name.endswith('_list'):
+                    del self.contents[toVName(name, 5)]
+                else:
+                    del self.contents[toVName(name)]
+            else:
+                object.__delattr__(self, name)
+        except KeyError:
+            raise AttributeError(name)
+
+    def getChildValue(self, childName, default=None, childNumber=0):
+        """
+        Return a child's value (the first, by default), or None.
+        """
+        child = self.contents.get(toVName(childName))
+        if child is None:
+            return default
+        else:
+            return child[childNumber].value
+
+    def add(self, objOrName, group=None):
+        """
+        Add objOrName to contents, set behavior if it can be inferred.
+
+        If objOrName is a string, create an empty component or line based on
+        behavior. If no behavior is found for the object, add a ContentLine.
+
+        group is an optional prefix to the name of the object (see RFC 2425).
+        """
+        if isinstance(objOrName, VBase):
+            obj = objOrName
+            if self.behavior:
+                obj.parentBehavior = self.behavior
+                obj.autoBehavior(True)
+        else:
+            name = objOrName.upper()
+            try:
+                id = self.behavior.knownChildren[name][2]
+                behavior = getBehavior(name, id)
+                if behavior.isComponent:
+                    obj = Component(name)
+                else:
+                    obj = ContentLine(name, [], '', group)
+                obj.parentBehavior = self.behavior
+                obj.behavior = behavior
+                obj = obj.transformToNative()
+            except (KeyError, AttributeError):
+                obj = ContentLine(objOrName, [], '', group)
+            if obj.behavior is None and self.behavior is not None:
+                if isinstance(obj, ContentLine):
+                    obj.behavior = self.behavior.defaultBehavior
+        self.contents.setdefault(obj.name.lower(), []).append(obj)
+        return obj
+
+    def remove(self, obj):
+        """
+        Remove obj from contents.
+        """
+        named = self.contents.get(obj.name.lower())
+        if named:
+            try:
+                named.remove(obj)
+                if len(named) == 0:
+                    del self.contents[obj.name.lower()]
+            except ValueError:
+                pass
+
+    def getChildren(self):
+        """
+        Return an iterable of all children.
+        """
+        for objList in self.contents.values():
+            for obj in objList:
+                yield obj
+
+    def components(self):
+        """
+        Return an iterable of all Component children.
+        """
+        return (i for i in self.getChildren() if isinstance(i, Component))
+
+    def lines(self):
+        """
+        Return an iterable of all ContentLine children.
+        """
+        return (i for i in self.getChildren() if isinstance(i, ContentLine))
+
+    def sortChildKeys(self):
+        try:
+            first = [s for s in self.behavior.sortFirst if s in self.contents]
+        except Exception:
+            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 child in childArray:
+                child = child.transformToNative()
+                child.transformChildrenToNative()
+
+    def transformChildrenFromNative(self, clearBehavior=True):
+        """
+        Recursively transform native children to vanilla representations.
+        """
+        for childArray in self.contents.values():
+            for child in childArray:
+                child = child.transformFromNative()
+                child.transformChildrenFromNative(clearBehavior)
+                if clearBehavior:
+                    child.behavior = None
+                    child.parentBehavior = None
+
+    def __str__(self):
+        if self.name:
+            return "<{0}| {1}>".format(self.name, self.getSortedChildren())
+        else:
+            return u'<*unnamed*| {0}>'.format(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)
+
+
+class VObjectError(Exception):
+    def __init__(self, msg, lineNumber=None):
+        self.msg = msg
+        if lineNumber is not None:
+            self.lineNumber = lineNumber
+
+    def __str__(self):
+        if hasattr(self, 'lineNumber'):
+            return "At line {0!s}: {1!s}".format(self.lineNumber, self.msg)
+        else:
+            return repr(self.msg)
+
+
+class ParseError(VObjectError):
+    pass
+
+
+class ValidateError(VObjectError):
+    pass
+
+
+class NativeError(VObjectError):
+    pass
+
+
+# --------- Parsing functions and parseLine regular expressions ----------------
+
+patterns = {}
+
+# Note that underscore is not legal for names, it's included because
+# Lotus Notes uses it
+patterns['name'] = '[a-zA-Z0-9\-_]+'
+patterns['safe_char'] = '[^";:,]'
+patterns['qsafe_char'] = '[^"]'
+
+# the combined Python string replacement and regex syntax is a little confusing;
+# remember that {foobar} 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} * '.format(**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} + )
+""".format(**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
+)*
+""".format(**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
+    )
+)?
+""".format(**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
+""".format(**patterns)
+
+' "%(qsafe_char)s*" | %(safe_char)s* '  # what is this line?? - never assigned?
+
+param_values_re = re.compile(patterns['param_value_grouped'], re.VERBOSE)
+params_re = re.compile(patterns['params_grouped'], re.VERBOSE)
+line_re = re.compile(patterns['line'], re.DOTALL | re.VERBOSE)
+begin_re = re.compile('BEGIN', re.IGNORECASE)
+
+
+def parseParams(string):
+    """
+    Parse parameters
+    """
+    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):
+    """
+    Parse line
+    """
+    match = line_re.match(line)
+    if match is None:
+        raise ParseError("Failed to parse line: {0!s}".format(line), lineNumber)
+    # Underscores are replaced with dash to work around Lotus Notes
+    return (match.group('name').replace('_', '-'),
+            parseParams(match.group('params')),
+            match.group('value'), match.group('group'))
+
+# logical line regular expressions
+
+patterns['lineend'] = r'(?:\r\n|\r|\n|$)'
+patterns['wrap'] = r'{lineend!s} [\t ]'.format(**patterns)
+patterns['logicallines'] = r"""
+(
+   (?: [^\r\n] | {wrap!s} )*
+   {lineend!s}
+)
+""".format(**patterns)
+
+patterns['wraporend'] = r'({wrap!s} | {lineend!s} )'.format(**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.
+
+    # We're leaving this test in for awhile, because the unittest was ugly and dumb.
+    >>> from six import StringIO
+    >>> f=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 != '':
+                yield line, lineNumber
+            lineNumber += n
+
+    else:
+        quotedPrintable = False
+        newbuffer = six.StringIO
+        logicalLine = newbuffer()
+        lineNumber = 0
+        lineStartNumber = 0
+        while True:
+            line = fp.readline()
+            if line == '':
+                break
+            else:
+                line = line.rstrip(CRLF)
+                lineNumber += 1
+            if line.rstrip() == '':
+                if logicalLine.tell() > 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.tell() > 0:
+                yield logicalLine.getvalue(), lineStartNumber
+                lineStartNumber = lineNumber
+                logicalLine = newbuffer()
+                logicalLine.write(line)
+            else:
+                logicalLine = newbuffer()
+                logicalLine.write(line)
+
+            # 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.tell() > 0:
+            yield logicalLine.getvalue(), lineStartNumber
+
+
+def textLineToContentLine(text, n=None):
+    return ContentLine(*parseLine(text, n), **{'encoded': True,
+                                               'lineNumber': n})
+
+
+def dquoteEscape(param):
+    """
+    Return param, or "param" if ',' or ';' or ':' is in param.
+    """
+    if param.find('"') >= 0:
+        raise VObjectError("Double quotes aren't allowed in parameter values.")
+    for char in ',;:':
+        if param.find(char) >= 0:
+            return '"' + param + '"'
+    return param
+
+
+def foldOneLine(outbuf, input, lineLength=75):
+    """
+    Folding line procedure that ensures multi-byte utf-8 sequences are not
+    broken across lines
+
+    TO-DO: This all seems odd. Is it still needed, especially in python3?
+    """
+    if len(input) < lineLength:
+        # Optimize for unfolded line case
+        try:
+            outbuf.write(bytes(input, 'UTF-8'))
+        except Exception:
+            # fall back on py2 syntax
+            outbuf.write(input)
+
+    else:
+        # Look for valid utf8 range and write that out
+        start = 0
+        written = 0
+        counter = 0  # counts line size in bytes
+        decoded = to_unicode(input)
+        length = len(to_basestring(input))
+        while written < length:
+            s = decoded[start]  # take one char
+            size = len(to_basestring(s))  # calculate it's size in bytes
+            if counter + size > lineLength:
+                try:
+                    outbuf.write(bytes("\r\n ", 'UTF-8'))
+                except Exception:
+                    # fall back on py2 syntax
+                    outbuf.write("\r\n ")
+
+                counter = 1  # one for space
+
+            if str is unicode_type:
+                outbuf.write(to_unicode(s))
+            else:
+                # fall back on py2 syntax
+                outbuf.write(s.encode('utf-8'))
+
+            written += size
+            counter += size
+            start += 1
+    try:
+        outbuf.write(bytes("\r\n", 'UTF-8'))
+    except Exception:
+        # fall back on py2 syntax
+        outbuf.write("\r\n")
+
+
+def defaultSerialize(obj, buf, lineLength):
+    """
+    Encode and fold obj and its children, write to buf or return a string.
+    """
+    outbuf = buf or six.StringIO()
+
+    if isinstance(obj, Component):
+        if obj.group is None:
+            groupString = ''
+        else:
+            groupString = obj.group + '.'
+        if obj.useBegin:
+            foldOneLine(outbuf, "{0}BEGIN:{1}".format(groupString, 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, "{0}END:{1}".format(groupString, obj.name),
+                        lineLength)
+
+    elif isinstance(obj, ContentLine):
+        startedEncoded = obj.encoded
+        if obj.behavior and not startedEncoded:
+            obj.behavior.encode(obj)
+
+        s = six.StringIO()
+
+        if obj.group is not None:
+            s.write(obj.group + '.')
+        s.write(str_(obj.name.upper()))
+        keys = sorted(obj.params.keys())
+        for key in keys:
+            paramstr = ','.join(dquoteEscape(p) for p in obj.params[key])
+            try:
+                s.write(";{0}={1}".format(key, paramstr))
+            except (UnicodeDecodeError, UnicodeEncodeError):
+                s.write(";{0}={1}".format(key, paramstr.encode('utf-8')))
+        try:
+            s.write(":{0}".format(obj.value))
+        except (UnicodeDecodeError, UnicodeEncodeError):
+            s.write(":{0}".format(obj.value.encode('utf-8')))
+        if obj.behavior and not startedEncoded:
+            obj.behavior.decode(obj)
+        foldOneLine(outbuf, s.getvalue(), lineLength)
+
+    return buf or outbuf.getvalue()
+
+
+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,
+                   ignoreUnreadable=False, allowQP=False):
+    """
+    Generate one Component at a time from a stream.
+    """
+    if isinstance(streamOrString, basestring):
+        stream = six.StringIO(streamOrString)
+    else:
+        stream = streamOrString
+
+    try:
+        stack = Stack()
+        versionLine = None
+        n = 0
+        for line, n in getLogicalLines(stream, allowQP):
+            if ignoreUnreadable:
+                try:
+                    vline = textLineToContentLine(line, n)
+                except VObjectError as e:
+                    if e.lineNumber is not None:
+                        msg = "Skipped line {lineNumber}, message: {msg}"
+                    else:
+                        msg = "Skipped a line, message: {msg}"
+                    logger.error(msg.format(**{'lineNumber': e.lineNumber, 'msg': str(e)}))
+                    continue
+            else:
+                vline = textLineToContentLine(line, n)
+            if vline.name == "VERSION":
+                versionLine = vline
+                stack.modifyTop(vline)
+            elif vline.name == "BEGIN":
+                stack.push(Component(vline.value, group=vline.group))
+            elif vline.name == "PROFILE":
+                if not stack.top():
+                    stack.push(Component())
+                stack.top().setProfile(vline.value)
+            elif vline.name == "END":
+                if len(stack) == 0:
+                    err = "Attempted to end the {0} component but it was never opened"
+                    raise ParseError(err.format(vline.value), 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)
+                        else:
+                            behavior = getBehavior(component.name)
+                            if behavior:
+                                component.setBehavior(behavior)
+                        if validate:
+                            component.validate(raiseException=True)
+                        if transform:
+                            component.transformChildrenToNative()
+                        yield component  # EXIT POINT
+                    else:
+                        stack.modifyTop(stack.pop())
+                else:
+                    err = "{0} component wasn't closed"
+                    raise ParseError(err.format(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 {0!s} was never closed".format(
+                                 (stack.topName())), n)
+            yield stack.pop()
+
+    except ParseError as e:
+        e.input = streamOrString
+        raise
+
+
+def readOne(stream, validate=False, transform=True, ignoreUnreadable=False,
+            allowQP=False):
+    """
+    Return the first component from stream.
+    """
+    return next(readComponents(stream, validate, transform, ignoreUnreadable,
+                               allowQP))
+
+
+# --------------------------- 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
+
+        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 {0!s}".format(name))
+    if behavior.isComponent:
+        obj = Component(name)
+    else:
+        obj = ContentLine(name, [], '')
+    obj.behavior = behavior
+    obj.isNative = False
+    return obj
+
+
+# --------------------------- Helper function ----------------------------------
+def backslashEscape(s):
+    s = s.replace("\\", "\\\\").replace(";", "\;").replace(",", "\,")
+    return s.replace("\r\n", "\\n").replace("\n", "\\n").replace("\r", "\\n")

+ 174 - 0
src/vobject/vobject/behavior.py

@@ -0,0 +1,174 @@
+from . import base
+
+
+#------------------------ Abstract class for behavior --------------------------
+class Behavior(object):
+    """
+    Behavior (validation, encoding, and transformations) for vobjects.
+
+    Abstract class to describe vobject options, requirements and encodings.
+
+    Behaviors are used for root components like VCALENDAR, for subcomponents
+    like VEVENT, and for individual lines in components.
+
+    Behavior subclasses are not meant to be instantiated, all methods should
+    be classmethods.
+
+    @cvar name:
+        The uppercase name of the object described by the class, or a generic
+        name if the class defines behavior for many objects.
+    @cvar description:
+        A brief excerpt from the RFC explaining the function of the component or
+        line.
+    @cvar versionString:
+        The string associated with the component, for instance, 2.0 if there's a
+        line like VERSION:2.0, an empty string otherwise.
+    @cvar knownChildren:
+        A dictionary with uppercased component/property names as keys and a
+        tuple (min, max, id) as value, where id is the id used by
+        L{registerBehavior}, min and max are the limits on how many of this child
+        must occur.  None is used to denote no max or no id.
+    @cvar quotedPrintable:
+        A boolean describing whether the object should be encoded and decoded
+        using quoted printable line folding and character escaping.
+    @cvar defaultBehavior:
+        Behavior to apply to ContentLine children when no behavior is found.
+    @cvar hasNative:
+        A boolean describing whether the object can be transformed into a more
+        Pythonic object.
+    @cvar isComponent:
+        A boolean, True if the object should be a Component.
+    @cvar sortFirst:
+        The lower-case list of children which should come first when sorting.
+    @cvar allowGroup:
+        Whether or not vCard style group prefixes are allowed.
+    """
+    name = ''
+    description = ''
+    versionString = ''
+    knownChildren = {}
+    quotedPrintable = False
+    defaultBehavior = None
+    hasNative = False
+    isComponent = False
+    allowGroup = False
+    forceUTC = False
+    sortFirst = []
+
+    def __init__(self):
+        err = "Behavior subclasses are not meant to be instantiated"
+        raise base.VObjectError(err)
+
+    @classmethod
+    def validate(cls, obj, raiseException=False, complainUnrecognized=False):
+        """Check if the object satisfies this behavior's requirements.
+
+        @param obj:
+            The L{ContentLine<base.ContentLine>} or
+            L{Component<base.Component>} to be validated.
+        @param raiseException:
+            If True, raise a L{base.ValidateError} on validation failure.
+            Otherwise return a boolean.
+        @param complainUnrecognized:
+            If True, fail to validate if an uncrecognized parameter or child is
+            found.  Otherwise log the lack of recognition.
+
+        """
+        if not cls.allowGroup and obj.group is not None:
+            err = "{0} has a group, but this object doesn't support groups".format(obj)
+            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.items():
+                if count.get(key, 0) < val[0]:
+                    if raiseException:
+                        m = "{0} components must contain at least {1} {2}"
+                        raise base.ValidateError(m .format(cls.name, val[0], key))
+                    return False
+                if val[1] and count.get(key, 0) > val[1]:
+                    if raiseException:
+                        m = "{0} components cannot contain more than {1} {2}"
+                        raise base.ValidateError(m.format(cls.name, val[1], key))
+                    return False
+            return True
+        else:
+            err = "{0} is not a Component or Contentline".format(obj)
+            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

+ 101 - 0
src/vobject/vobject/change_tz.py

@@ -0,0 +1,101 @@
+"""Translate an ics file's events to a different timezone."""
+
+from optparse import OptionParser
+from vobject import icalendar, base
+
+try:
+    import PyICU
+except:
+    PyICU = None
+
+from datetime import datetime
+
+
+def change_tz(cal, new_timezone, default, utc_only=False, utc_tz=icalendar.utc):
+    """
+    Change the timezone of the specified component.
+
+    Args:
+        cal (Component): the component to change
+        new_timezone (tzinfo): the timezone to change to
+        default (tzinfo): a timezone to assume if the dtstart or dtend in cal
+            doesn't have an existing timezone
+        utc_only (bool): only convert dates that are in utc
+        utc_tz (tzinfo): the tzinfo to compare to for UTC when processing
+            utc_only=True
+    """
+
+    for vevent in getattr(cal, 'vevent_list', []):
+        start = getattr(vevent, 'dtstart', None)
+        end = getattr(vevent, 'dtend', None)
+        for node in (start, end):
+            if node:
+                dt = node.value
+                if (isinstance(dt, datetime) and
+                        (not utc_only or dt.tzinfo == utc_tz)):
+                    if dt.tzinfo is None:
+                        dt = dt.replace(tzinfo=default)
+                    node.value = dt.astimezone(new_timezone)
+
+
+def main():
+    options, args = get_options()
+    if PyICU is None:
+        print("Failure. change_tz requires PyICU, exiting")
+    elif options.list:
+        for tz_string in PyICU.TimeZone.createEnumeration():
+            print(tz_string)
+    elif args:
+        utc_only = options.utc
+        if utc_only:
+            which = "only UTC"
+        else:
+            which = "all"
+        print("Converting {0!s} events".format(which))
+        ics_file = args[0]
+        if len(args) > 1:
+            timezone = PyICU.ICUtzinfo.getInstance(args[1])
+        else:
+            timezone = PyICU.ICUtzinfo.default
+        print("... Reading {0!s}".format(ics_file))
+        cal = base.readOne(open(ics_file))
+        change_tz(cal, timezone, PyICU.ICUtzinfo.default, utc_only)
+
+        out_name = ics_file + '.converted'
+        print("... Writing {0!s}".format(out_name))
+
+        with open(out_name, 'wb') as out:
+            cal.serialize(out)
+
+        print("Done")
+
+
+version = "0.1"
+
+
+def get_options():
+    # Configuration options
+
+    usage = """usage: %prog [options] ics_file [timezone]"""
+    parser = OptionParser(usage=usage, version=version)
+    parser.set_description("change_tz will convert the timezones in an ics file. ")
+
+    parser.add_option("-u", "--only-utc", dest="utc", action="store_true",
+                      default=False, help="Only change UTC events.")
+    parser.add_option("-l", "--list", dest="list", action="store_true",
+                      default=False, help="List available timezones")
+
+    (cmdline_options, args) = parser.parse_args()
+    if not args and not cmdline_options.list:
+        print("error: too few arguments given")
+        print
+        print(parser.format_help())
+        return False, False
+
+    return cmdline_options, args
+
+if __name__ == "__main__":
+    try:
+        main()
+    except KeyboardInterrupt:
+        print("Aborted")

+ 130 - 0
src/vobject/vobject/hcalendar.py

@@ -0,0 +1,130 @@
+"""
+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>
+"""
+
+import six
+
+from datetime import date, datetime, timedelta
+
+from .base import CRLF, registerBehavior
+from .icalendar import VCalendar2_0
+
+
+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 six.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="{0!s}">{1!s}</abbr>\r\n'
+                    .format(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="{0!s}">{1!s}</abbr>\r\n'
+                        .format(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)

+ 2078 - 0
src/vobject/vobject/icalendar.py

@@ -0,0 +1,2078 @@
+"""Definitions and behavior for iCalendar, also known as vCalendar 2.0"""
+
+from __future__ import print_function
+
+import datetime
+import logging
+import random  # for generating a UID
+import socket
+import string
+import base64
+
+from dateutil import rrule, tz
+import six
+
+try:
+    import pytz
+except ImportError:
+    class Pytz:
+        """fake pytz module (pytz is not required)"""
+
+        class AmbiguousTimeError(Exception):
+            """pytz error for ambiguous times
+               during transition daylight->standard"""
+
+        class NonExistentTimeError(Exception):
+            """pytz error for non-existent times
+               during transition standard->daylight"""
+
+    pytz = Pytz  # keeps quantifiedcode happy
+
+from . import behavior
+from .base import (VObjectError, NativeError, ValidateError, ParseError,
+                   Component, ContentLine, logger, registerBehavior,
+                   backslashEscape, foldOneLine)
+
+
+# ------------------------------- Constants ------------------------------------
+DATENAMES = ("rdate", "exdate")
+RULENAMES = ("exrule", "rrule")
+DATESANDRULES = ("exrule", "rrule", "rdate", "exdate")
+PRODID = u"-//PYVOBJECT//NONSGML Version 1//EN"
+
+WEEKDAYS = "MO", "TU", "WE", "TH", "FR", "SA", "SU"
+FREQUENCIES = ('YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY',
+               'SECONDLY')
+
+zeroDelta = datetime.timedelta(0)
+twoHours = datetime.timedelta(hours=2)
+
+
+# ---------------------------- TZID registry -----------------------------------
+__tzidMap = {}
+
+
+def toUnicode(s):
+    """
+    Take a string or unicode, turn it into unicode, decoding as utf-8
+    """
+    if isinstance(s, six.binary_type):
+        s = s.decode('utf-8')
+    return s
+
+
+def registerTzid(tzid, tzinfo):
+    """
+    Register a tzid -> tzinfo mapping.
+    """
+    __tzidMap[toUnicode(tzid)] = tzinfo
+
+
+def getTzid(tzid, smart=True):
+    """
+    Return the tzid if it exists, or None.
+    """
+    tz = __tzidMap.get(toUnicode(tzid), None)
+    if smart and tzid and not tz:
+        try:
+            from pytz import timezone, UnknownTimeZoneError
+            try:
+                tz = timezone(tzid)
+                registerTzid(toUnicode(tzid), tz)
+            except UnknownTimeZoneError as e:
+                logging.error(e)
+        except ImportError as e:
+            logging.error(e)
+    return tz
+
+utc = tz.tzutc()
+registerTzid("UTC", utc)
+
+
+# -------------------- Helper subclasses ---------------------------------------
+class TimezoneComponent(Component):
+    """
+    A VTIMEZONE object.
+
+    VTIMEZONEs are parsed by 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, False):
+            registerTzid(tzid, tzinfo)
+        return tzid
+
+    def gettzinfo(self):
+        # workaround for dateutil failing to parse some experimental properties
+        good_lines = ('rdate', 'rrule', 'dtstart', 'tzname', 'tzoffsetfrom',
+                      'tzoffsetto', 'tzid')
+        # serialize encodes as utf-8, cStringIO will leave utf-8 alone
+        buffer = six.StringIO()
+        # allow empty VTIMEZONEs
+        if len(self.contents) == 0:
+            return None
+
+        def customSerialize(obj):
+            if isinstance(obj, Component):
+                foldOneLine(buffer, u"BEGIN:" + obj.name)
+                for child in obj.lines():
+                    if child.name.lower() in good_lines:
+                        child.serialize(buffer, 75, validate=False)
+                for comp in obj.components():
+                    customSerialize(comp)
+                foldOneLine(buffer, u"END:" + obj.name)
+        customSerialize(self)
+        buffer.seek(0)  # tzical wants to read a stream
+        return tz.tzical(buffer).get()
+
+    def settzinfo(self, tzinfo, start=2000, end=2030):
+        """
+        Create appropriate objects in self to represent tzinfo.
+
+        Collapse DST transitions to rrules as much as possible.
+
+        Assumptions:
+        - DST <-> Standard transitions occur on the hour
+        - never within a month of one another
+        - twice or fewer times a year
+        - never in the month of December
+        - DST always moves offset exactly one hour later
+        - tzinfo classes dst method always treats times that could be in either
+          offset as being in the later regime
+        """
+        def fromLastWeek(dt):
+            """
+            How many weeks from the end of the month dt is, starting from 1.
+            """
+            weekDelta = datetime.timedelta(weeks=1)
+            n = 1
+            current = dt + weekDelta
+            while current.month == dt.month:
+                n += 1
+                current += weekDelta
+            return n
+
+        # lists of dictionaries defining rules which are no longer in effect
+        completed = {'daylight': [], 'standard': []}
+
+        # dictionary defining rules which are currently in effect
+        working = {'daylight': None, 'standard': None}
+
+        # rule may be based on nth week of the month or the nth from the last
+        for year in range(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
+                    try:
+                        old_offset = tzinfo.utcoffset(transition - twoHours)
+                        name = tzinfo.tzname(transition)
+                        offset = tzinfo.utcoffset(transition)
+                    except (pytz.AmbiguousTimeError, pytz.NonExistentTimeError):
+                        # guaranteed that tzinfo is a pytz timezone
+                        is_dst = (transitionTo == "daylight")
+                        old_offset = tzinfo.utcoffset(transition - twoHours, is_dst=is_dst)
+                        name = tzinfo.tzname(transition, is_dst=is_dst)
+                        offset = tzinfo.utcoffset(transition, is_dst=is_dst)
+                    rule = {'end'     : None,  # None, or an integer year
+                            'start'   : transition,  # the datetime of transition
+                            'month'   : transition.month,
+                            'weekday' : transition.weekday(),
+                            'hour'    : transition.hour,
+                            'name'    : name,
+                            'plus'    : int(
+                                (transition.day - 1)/ 7 + 1),  # nth week of the month
+                            'minus'   : fromLastWeek(transition),  # nth from last week
+                            'offset'  : offset,
+                            'offsetfrom' : old_offset}
+
+                    if oldrule is None:
+                        working[transitionTo] = rule
+                    else:
+                        plusMatch = rule['plus'] == oldrule['plus']
+                        minusMatch = rule['minus'] == oldrule['minus']
+                        truth = plusMatch or minusMatch
+                        for key in 'month', 'weekday', 'hour', 'offset':
+                            truth = truth and rule[key] == oldrule[key]
+                        if truth:
+                            # the old rule is still true, limit to plus or minus
+                            if not plusMatch:
+                                oldrule['plus'] = None
+                            if not minusMatch:
+                                oldrule['minus'] = None
+                        else:
+                            # the new rule did not match the old
+                            oldrule['end'] = year - 1
+                            completed[transitionTo].append(oldrule)
+                            working[transitionTo] = rule
+
+        for transitionTo in 'daylight', 'standard':
+            if working[transitionTo] is not None:
+                completed[transitionTo].append(working[transitionTo])
+
+        self.tzid = []
+        self.daylight = []
+        self.standard = []
+
+        self.add('tzid').value = self.pickTzid(tzinfo, True)
+
+        # old = None # unused?
+        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 = rrule.weekday(rule['weekday'], num)
+                        du_rule = rrule.rrule(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 = ''
+                new_rule = "FREQ=YEARLY{0!s};BYMONTH={1!s}{2!s}"\
+                    .format(dayString, rule['month'], endString)
+
+                comp.add('rrule').value = new_rule
+
+    tzinfo = property(gettzinfo, settzinfo)
+    # prevent Component's __setattr__ from overriding the tzinfo property
+    normal_attributes = Component.normal_attributes + ['tzinfo']
+
+    @staticmethod
+    def pickTzid(tzinfo, allowUTC=False):
+        """
+        Given a tzinfo class, use known APIs to determine TZID, or use tzname.
+        """
+        if tzinfo is None or (not allowUTC and tzinfo_eq(tzinfo, utc)):
+            # If tzinfo is UTC, we don't need a TZID
+            return None
+        # try PyICU's tzid key
+        if hasattr(tzinfo, 'tzid'):
+            return toUnicode(tzinfo.tzid)
+
+        # try pytz zone key
+        if hasattr(tzinfo, 'zone'):
+            return toUnicode(tzinfo.zone)
+
+        # try tzical's tzid key
+        elif hasattr(tzinfo, '_tzid'):
+            return toUnicode(tzinfo._tzid)
+        else:
+            # return tzname for standard (non-DST) time
+            notDST = datetime.timedelta(0)
+            for month in range(1, 13):
+                dt = datetime.datetime(2000, month, 1)
+                if tzinfo.dst(dt) == notDST:
+                    return toUnicode(tzinfo.tzname(dt))
+        # there was no standard time in 2000!
+        raise VObjectError("Unable to guess TZID for tzinfo {0!s}"
+                           .format(tzinfo))
+
+    def __str__(self):
+        return "<VTIMEZONE | {0}>".format(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.
+
+    @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
+
+    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 or RDATE, 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 = rrule.rruleset()
+                if addfunc is None:
+                    addfunc = getattr(rruleset, name)
+
+                try:
+                    dtstart = self.dtstart.value
+                except (AttributeError, KeyError):
+                    # Special for VTODO - try DUE property instead
+                    try:
+                        if self.name == "VTODO":
+                            dtstart = self.due.value
+                        else:
+                            # if there's no dtstart, just return None
+                            logging.error('failed to get dtstart with VTODO')
+                            return None
+                    except (AttributeError, KeyError):
+                        # if there's no due, just return None
+                        logging.error('failed to find DUE at all.')
+                        return None
+
+                if name in DATENAMES:
+                    if type(line.value[0]) == datetime.datetime:
+                        list(map(addfunc, line.value))
+                    elif type(line.value[0]) == datetime.date:
+                        for dt in line.value:
+                            addfunc(datetime.datetime(dt.year, dt.month, dt.day))
+                    else:
+                        # ignore RDATEs with PERIOD values for now
+                        pass
+                elif name in RULENAMES:
+                    # a Ruby iCalendar library escapes semi-colons in rrules,
+                    # so also remove any backslashes
+                    value = line.value.replace('\\', '')
+                    # If dtstart has no time zone, `until`
+                    # shouldn't get one, either:
+                    ignoretz = (not isinstance(dtstart, datetime.datetime) or
+                                dtstart.tzinfo is None)
+                    try:
+                        until = rrule.rrulestr(value, ignoretz=ignoretz)._until
+                    except ValueError:
+                        # WORKAROUND: dateutil<=2.7.2 doesn't set the time zone
+                        # of dtstart
+                        if ignoretz:
+                            raise
+                        utc_now = datetime.datetime.now(datetime.timezone.utc)
+                        until = rrule.rrulestr(value, dtstart=utc_now)._until
+
+                    if until is not None and isinstance(dtstart,
+                                                        datetime.datetime) and \
+                            (until.tzinfo != dtstart.tzinfo):
+                        # dateutil converts the UNTIL date to a datetime,
+                        # check to see if the UNTIL parameter value was a date
+                        vals = dict(pair.split('=') for pair in
+                                    value.upper().split(';'))
+                        if len(vals.get('UNTIL', '')) == 8:
+                            until = datetime.datetime.combine(until.date(),
+                                                              dtstart.time())
+                        # While RFC2445 says UNTIL MUST be UTC, Chandler allows
+                        # floating recurring events, and uses floating UNTIL
+                        # values. Also, some odd floating UNTIL but timezoned
+                        # DTSTART values have shown up in the wild, so put
+                        # floating UNTIL values DTSTART's timezone
+                        if until.tzinfo is None:
+                            until = until.replace(tzinfo=dtstart.tzinfo)
+
+                        if dtstart.tzinfo is not None:
+                            until = until.astimezone(dtstart.tzinfo)
+
+                        # RFC2445 actually states that UNTIL must be a UTC
+                        # value. Whilst the changes above work OK, one problem
+                        # case is if DTSTART is floating but UNTIL is properly
+                        # specified as UTC (or with a TZID). In that case
+                        # dateutil will fail datetime comparisons. There is no
+                        # easy solution to this as there is no obvious timezone
+                        # (at this point) to do proper floating time offset
+                        # comparisons. The best we can do is treat the UNTIL
+                        # value as floating. This could mean incorrect
+                        # determination of the last instance. The better
+                        # solution here is to encourage clients to use COUNT
+                        # rather than UNTIL when DTSTART is floating.
+                        if dtstart.tzinfo is None:
+                            until = until.replace(tzinfo=None)
+
+                    value_without_until = ';'.join(
+                        pair for pair in value.split(';')
+                        if pair.split('=')[0].upper() != 'UNTIL')
+                    rule = rrule.rrulestr(value_without_until,
+                                          dtstart=dtstart, ignoretz=ignoretz)
+                    rule._until = until
+
+                    # add the rrule or exrule to the rruleset
+                    addfunc(rule)
+
+                if (name == 'rrule' or name == 'rdate') and addRDate:
+                    # rlist = rruleset._rrule if name == 'rrule' else rruleset._rdate
+                    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 name == 'rrule':
+                            if rruleset._rrule[-1][0] != adddtstart:
+                                rruleset.rdate(adddtstart)
+                                added = True
+                                if rruleset._rrule[-1]._count is not None:
+                                    rruleset._rrule[-1]._count -= 1
+                            else:
+                                added = False
+                        elif name == 'rdate':
+                            if rruleset._rdate[0] != adddtstart:
+                                rruleset.rdate(adddtstart)
+                                added = True
+                            else:
+                                added = False
+                    except IndexError:
+                        # it's conceivable that an rrule has 0 datetimes
+                        added = False
+
+        return rruleset
+
+    def setrruleset(self, rruleset):
+        # Get DTSTART from component (or DUE if no DTSTART in a VTODO)
+        try:
+            dtstart = self.dtstart.value
+        except (AttributeError, KeyError):
+            if self.name == "VTODO":
+                dtstart = self.due.value
+            else:
+                raise
+
+        isDate = datetime.date == type(dtstart)
+        if isDate:
+            dtstart = datetime.datetime(dtstart.year, dtstart.month, dtstart.day)
+            untilSerialize = dateToString
+        else:
+            # make sure to convert time zones to UTC
+            untilSerialize = lambda x: dateTimeToString(x, True)
+
+        for name in DATESANDRULES:
+            if name in self.contents:
+                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 = six.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 (
+                                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(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 <= 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._bynmonthday is not None and len(rule._bynmonthday) > 0:
+                        values.setdefault('BYMONTHDAY', []).extend(str(n) for n in rule._bynmonthday)
+
+                    if rule._bymonth is not None and len(rule._bymonth) > 0:
+                        if (rule._byweekday is not None or
+                            len(rule._bynweekday or ()) > 0 or
+                            not (rule._freq == 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.items():
+                        buf.write(';')
+                        buf.write(key)
+                        buf.write('=')
+                        buf.write(','.join(paramvals))
+
+                    self.add(name).value = buf.getvalue()
+
+    rruleset = property(getrruleset, setrruleset)
+
+    def __setattr__(self, name, value):
+        """
+        For convenience, make self.contents directly accessible.
+        """
+        if name == 'rruleset':
+            self.setrruleset(value)
+        else:
+            super(RecurringComponent, self).__setattr__(name, value)
+
+
+class TextBehavior(behavior.Behavior):
+    """
+    Provide backslash escape encoding/decoding for single valued properties.
+
+    TextBehavior also deals with base64 encoding if the ENCODING parameter is
+    explicitly set to BASE64.
+    """
+    base64string = 'BASE64'  # vCard uses B
+
+    @classmethod
+    def decode(cls, line):
+        """
+        Remove backslash escaping from line.value.
+        """
+        if line.encoded:
+            encoding = getattr(line, 'encoding_param', None)
+            if encoding and encoding.upper() == cls.base64string:
+                line.value = base64.b64decode(line.value)
+            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 = base64.b64encode(line.value.encode('utf-8')).decode('utf-8').replace('\n', '')
+            else:
+                line.value = backslashEscape(line.value)
+            line.encoded = True
+
+
+class VCalendarComponentBehavior(behavior.Behavior):
+    defaultBehavior = TextBehavior
+    isComponent = True
+
+
+class RecurringBehavior(VCalendarComponentBehavior):
+    """
+    Parent Behavior for components which should be RecurringComponents.
+    """
+    hasNative = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """
+        Turn a recurring Component into a RecurringComponent.
+        """
+        if not obj.isNative:
+            object.__setattr__(obj, '__class__', RecurringComponent)
+            obj.isNative = True
+        return obj
+
+    @staticmethod
+    def transformFromNative(obj):
+        if obj.isNative:
+            object.__setattr__(obj, '__class__', Component)
+            obj.isNative = False
+        return obj
+
+    @staticmethod
+    def generateImplicitParameters(obj):
+        """
+        Generate a UID and DTSTAMP if one does not exist.
+
+        This is just a dummy implementation, for now.
+        """
+        if not hasattr(obj, 'uid'):
+            rand = int(random.random() * 100000)
+            now = datetime.datetime.now(utc)
+            now = dateTimeToString(now)
+            host = socket.gethostname()
+            obj.add(ContentLine('UID', [], "{0} - {1}@{2}".format(now, rand,
+                                                                  host)))
+
+        if not hasattr(obj, 'dtstamp'):
+            now = datetime.datetime.now(utc)
+            obj.add('dtstamp').value = now
+
+
+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 = obj.value
+        # we're cheating a little here, parseDtstart allows DATE
+        obj.value = parseDtstart(obj)
+        if obj.value.tzinfo is None:
+            obj.params['X-VOBJ-FLOATINGTIME-ALLOWED'] = ['TRUE']
+        if obj.params.get('TZID'):
+            # Keep a copy of the original TZID around
+            obj.params['X-VOBJ-ORIGINAL-TZID'] = [obj.params['TZID']]
+            del obj.params['TZID']
+        return obj
+
+    @classmethod
+    def transformFromNative(cls, obj):
+        """
+        Replace the datetime in obj.value with an ISO 8601 string.
+        """
+        if obj.isNative:
+            obj.isNative = False
+            tzid = TimezoneComponent.registerTzinfo(obj.value.tzinfo)
+            obj.value = dateTimeToString(obj.value, cls.forceUTC)
+            if not cls.forceUTC and tzid is not None:
+                obj.tzid_param = tzid
+            if obj.params.get('X-VOBJ-ORIGINAL-TZID'):
+                if not hasattr(obj, 'tzid_param'):
+                    obj.tzid_param = obj.x_vobj_original_tzid_param
+                del obj.params['X-VOBJ-ORIGINAL-TZID']
+
+        return obj
+
+
+class UTCDateTimeBehavior(DateTimeBehavior):
+    """
+    A value which must be specified in UTC.
+    """
+    forceUTC = True
+
+
+class DateOrDateTimeBehavior(behavior.Behavior):
+    """
+    Parent Behavior for ContentLines containing one DATE or DATE-TIME.
+    """
+    hasNative = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """
+        Turn obj.value into a date or datetime.
+        """
+        if obj.isNative:
+            return obj
+        obj.isNative = True
+        if obj.value == '':
+            return obj
+        obj.value = obj.value
+        obj.value = parseDtstart(obj, allowSignatureMismatch=True)
+        if getattr(obj, 'value_param', 'DATE-TIME').upper() == 'DATE-TIME':
+            if hasattr(obj, 'tzid_param'):
+                # Keep a copy of the original TZID around
+                obj.params['X-VOBJ-ORIGINAL-TZID'] = [obj.tzid_param]
+                del obj.tzid_param
+        return obj
+
+    @staticmethod
+    def transformFromNative(obj):
+        """
+        Replace the date or datetime in obj.value with an ISO 8601 string.
+        """
+        if type(obj.value) == datetime.date:
+            obj.isNative = False
+            obj.value_param = 'DATE'
+            obj.value = dateToString(obj.value)
+            return obj
+        else:
+            return DateTimeBehavior.transformFromNative(obj)
+
+
+class MultiDateBehavior(behavior.Behavior):
+    """
+    Parent Behavior for ContentLines containing one or more DATE, DATE-TIME, or
+    PERIOD.
+    """
+    hasNative = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """
+        Turn obj.value into a list of dates, datetimes, or
+        (datetime, timedelta) tuples.
+        """
+        if obj.isNative:
+            return obj
+        obj.isNative = True
+        if obj.value == '':
+            obj.value = []
+            return obj
+        tzinfo = getTzid(getattr(obj, 'tzid_param', None))
+        valueParam = getattr(obj, 'value_param', "DATE-TIME").upper()
+        valTexts = obj.value.split(",")
+        if valueParam == "DATE":
+            obj.value = [stringToDate(x) for x in valTexts]
+        elif valueParam == "DATE-TIME":
+            obj.value = [stringToDateTime(x, tzinfo) for x in valTexts]
+        elif valueParam == "PERIOD":
+            obj.value = [stringToPeriod(x, tzinfo) for x in valTexts]
+        return obj
+
+    @staticmethod
+    def transformFromNative(obj):
+        """
+        Replace the date, datetime or period tuples in obj.value with
+        appropriate strings.
+        """
+        if obj.value and type(obj.value[0]) == datetime.date:
+            obj.isNative = False
+            obj.value_param = 'DATE'
+            obj.value = ','.join([dateToString(val) for val in obj.value])
+            return obj
+        # Fixme: handle PERIOD case
+        else:
+            if obj.isNative:
+                obj.isNative = False
+                transformed = []
+                tzid = None
+                for val in obj.value:
+                    if tzid is None and type(val) == datetime.datetime:
+                        tzid = TimezoneComponent.registerTzinfo(val.tzinfo)
+                        if tzid is not None:
+                            obj.tzid_param = tzid
+                    transformed.append(dateTimeToString(val))
+                obj.value = ','.join(transformed)
+            return obj
+
+
+class MultiTextBehavior(behavior.Behavior):
+    """
+    Provide backslash escape encoding/decoding of each of several values.
+
+    After transformation, value is a list of strings.
+    """
+    listSeparator = ","
+
+    @classmethod
+    def decode(cls, line):
+        """
+        Remove backslash escaping from line.value, then split on commas.
+        """
+        if line.encoded:
+            line.value = stringToTextValues(line.value,
+                                            listSeparator=cls.listSeparator)
+            line.encoded = False
+
+    @classmethod
+    def encode(cls, line):
+        """
+        Backslash escape line.value.
+        """
+        if not line.encoded:
+            line.value = cls.listSeparator.join(backslashEscape(val)
+                                                for val in line.value)
+            line.encoded = True
+
+
+class SemicolonMultiTextBehavior(MultiTextBehavior):
+    listSeparator = ";"
+
+
+# ------------------------ Registered Behavior subclasses ----------------------
+class VCalendar2_0(VCalendarComponentBehavior):
+    """
+    vCalendar 2.0 behavior. With added VAVAILABILITY support.
+    """
+    name = 'VCALENDAR'
+    description = 'vCalendar 2.0, also known as iCalendar.'
+    versionString = '2.0'
+    sortFirst = ('version', 'calscale', 'method', 'prodid', 'vtimezone')
+    knownChildren = {
+        'CALSCALE':      (0, 1, None),  # min, max, behaviorRegistry id
+        'METHOD':        (0, 1, None),
+        'VERSION':       (0, 1, None),  # required, but auto-generated
+        'PRODID':        (1, 1, None),
+        'VTIMEZONE':     (0, None, None),
+        'VEVENT':        (0, None, None),
+        'VTODO':         (0, None, None),
+        'VJOURNAL':      (0, None, None),
+        'VFREEBUSY':     (0, None, None),
+        'VAVAILABILITY': (0, None, None),
+    }
+
+    @classmethod
+    def generateImplicitParameters(cls, obj):
+        """
+        Create PRODID, VERSION and VTIMEZONEs if needed.
+
+        VTIMEZONEs will need to exist whenever TZID parameters exist or when
+        datetimes with tzinfo exist.
+        """
+        for comp in obj.components():
+            if comp.behavior is not None:
+                comp.behavior.generateImplicitParameters(comp)
+        if not hasattr(obj, 'prodid'):
+            obj.add(ContentLine('PRODID', [], PRODID))
+        if not hasattr(obj, 'version'):
+            obj.add(ContentLine('VERSION', [], cls.versionString))
+        tzidsUsed = {}
+
+        def findTzids(obj, table):
+            if isinstance(obj, ContentLine) and (obj.behavior is None or
+                                                 not obj.behavior.forceUTC):
+                if getattr(obj, 'tzid_param', None):
+                    table[obj.tzid_param] = 1
+                else:
+                    if type(obj.value) == list:
+                        for item in obj.value:
+                            tzinfo = getattr(obj.value, 'tzinfo', None)
+                            tzid = TimezoneComponent.registerTzinfo(tzinfo)
+                            if tzid:
+                                table[tzid] = 1
+                    else:
+                        tzinfo = getattr(obj.value, 'tzinfo', None)
+                        tzid = TimezoneComponent.registerTzinfo(tzinfo)
+                        if tzid:
+                            table[tzid] = 1
+            for child in obj.getChildren():
+                if obj.name != 'VTIMEZONE':
+                    findTzids(child, table)
+
+        findTzids(obj, tzidsUsed)
+        oldtzids = [toUnicode(x.tzid.value) for x in getattr(obj, 'vtimezone_list', [])]
+        for tzid in tzidsUsed.keys():
+            tzid = toUnicode(tzid)
+            if tzid != u'UTC' and tzid not in oldtzids:
+                obj.add(TimezoneComponent(tzinfo=getTzid(tzid)))
+
+    @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 = None
+        outbuf = buf or six.StringIO()
+        if obj.group is None:
+            groupString = ''
+        else:
+            groupString = obj.group + '.'
+        if obj.useBegin:
+            foldOneLine(outbuf, "{0}BEGIN:{1}".format(groupString, obj.name),
+                        lineLength)
+
+        try:
+            first_props = [s for s in cls.sortFirst if s in obj.contents \
+                                    and not isinstance(obj.contents[s][0], Component)]
+            first_components = [s for s in cls.sortFirst if s in obj.contents \
+                                    and isinstance(obj.contents[s][0], Component)]
+        except Exception:
+            first_props = first_components = []
+            # first_components = []
+
+        prop_keys = sorted(list(k for k in obj.contents.keys() if k not in first_props \
+                                         and not isinstance(obj.contents[k][0], Component)))
+        comp_keys = sorted(list(k for k in obj.contents.keys() if k not in first_components \
+                                        and isinstance(obj.contents[k][0], Component)))
+
+        sorted_keys = first_props + prop_keys + first_components + comp_keys
+        children = [o for k in sorted_keys for o in obj.contents[k]]
+
+        for child in children:
+            # validate is recursive, we only need to validate once
+            child.serialize(outbuf, lineLength, validate=False)
+        if obj.useBegin:
+            foldOneLine(outbuf, "{0}END:{1}".format(groupString, obj.name),
+                        lineLength)
+        out = buf or outbuf.getvalue()
+        if undoTransform:
+            obj.transformToNative()
+        return out
+registerBehavior(VCalendar2_0)
+
+
+class VTimezone(VCalendarComponentBehavior):
+    """
+    Timezone behavior.
+    """
+    name = 'VTIMEZONE'
+    hasNative = True
+    description = 'A grouping of component properties that defines a time zone.'
+    sortFirst = ('tzid', 'last-modified', 'tzurl', 'standard', 'daylight')
+    knownChildren = {
+        'TZID':          (1, 1, None),  # min, max, behaviorRegistry id
+        'LAST-MODIFIED': (0, 1, None),
+        'TZURL':         (0, 1, None),
+        'STANDARD':      (0, None, None),  # NOTE: One of Standard or
+        'DAYLIGHT':      (0, None, None)  # Daylight must appear
+    }
+
+    @classmethod
+    def validate(cls, obj, raiseException, *args):
+        if not hasattr(obj, 'tzid') or obj.tzid.value is None:
+            if raiseException:
+                m = "VTIMEZONE components must contain a valid TZID"
+                raise ValidateError(m)
+            return False
+        if 'standard' in obj.contents or 'daylight' in obj.contents:
+            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 TZID(behavior.Behavior):
+    """
+    Don't use TextBehavior for TZID.
+
+    RFC2445 only allows TZID lines to be paramtext, so they shouldn't need any
+    encoding or decoding.  Unfortunately, some Microsoft products use commas
+    in TZIDs which should NOT be treated as a multi-valued text property, nor
+    do we want to escape them.  Leaving them alone works for Microsoft's breakage,
+    and doesn't affect compliant iCalendar streams.
+    """
+registerBehavior(TZID)
+
+
+class DaylightOrStandard(VCalendarComponentBehavior):
+    hasNative = False
+    knownChildren = {'DTSTART':      (1, 1, None),  # min, max, behaviorRegistry id
+                     'RRULE':        (0, 1, None)}
+
+registerBehavior(DaylightOrStandard, 'STANDARD')
+registerBehavior(DaylightOrStandard, 'DAYLIGHT')
+
+
+class VEvent(RecurringBehavior):
+    """
+    Event behavior.
+    """
+    name = 'VEVENT'
+    sortFirst = ('uid', 'recurrence-id', 'dtstart', 'duration', 'dtend')
+
+    description = 'A grouping of component properties, and possibly including \
+                   "VALARM" calendar components, that represents a scheduled \
+                   amount of time on a calendar.'
+    knownChildren = {
+        'DTSTART':        (0, 1, None),  # min, max, behaviorRegistry id
+        'CLASS':          (0, 1, None),
+        'CREATED':        (0, 1, None),
+        'DESCRIPTION':    (0, 1, None),
+        'GEO':            (0, 1, None),
+        'LAST-MODIFIED':  (0, 1, None),
+        'LOCATION':       (0, 1, None),
+        'ORGANIZER':      (0, 1, None),
+        'PRIORITY':       (0, 1, None),
+        'DTSTAMP':        (1, 1, None),  # required
+        '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 'dtend' in obj.contents and 'duration' in obj.contents:
+            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':        (1, 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 'due' in obj.contents and 'duration' in obj.contents:
+            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':        (1, 1, None),
+        'SEQUENCE':       (0, 1, None),
+        'STATUS':         (0, 1, None),
+        'SUMMARY':        (0, 1, None),
+        'UID':            (0, 1, None),
+        'URL':            (0, 1, None),
+        'RECURRENCE-ID':  (0, 1, None),
+        'ATTACH':         (0, None, None),
+        'ATTENDEE':       (0, None, None),
+        'CATEGORIES':     (0, None, None),
+        'COMMENT':        (0, None, None),
+        'CONTACT':        (0, None, None),
+        'EXDATE':         (0, None, None),
+        'EXRULE':         (0, None, None),
+        'REQUEST-STATUS': (0, None, None),
+        'RELATED-TO':     (0, None, None),
+        'RDATE':          (0, None, None),
+        'RRULE':          (0, None, None)
+    }
+registerBehavior(VJournal)
+
+
+class VFreeBusy(VCalendarComponentBehavior):
+    """
+    Free/busy state behavior.
+    """
+    name = 'VFREEBUSY'
+    description = 'A grouping of component properties that describe either a \
+                   request for free/busy time, describe a response to a request \
+                   for free/busy time or describe a published set of busy time.'
+    sortFirst = ('uid', 'dtstart', 'duration', 'dtend')
+    knownChildren = {
+        'DTSTART':        (0, 1, None),  # min, max, behaviorRegistry id
+        'CONTACT':        (0, 1, None),
+        'DTEND':          (0, 1, None),
+        'DURATION':       (0, 1, None),
+        'ORGANIZER':      (0, 1, None),
+        'DTSTAMP':        (1, 1, None),
+        'UID':            (0, 1, None),
+        'URL':            (0, 1, None),
+        'ATTENDEE':       (0, None, None),
+        'COMMENT':        (0, None, None),
+        'FREEBUSY':       (0, None, None),
+        'REQUEST-STATUS': (0, None, None)
+    }
+
+registerBehavior(VFreeBusy)
+
+
+class VAlarm(VCalendarComponentBehavior):
+    """
+    Alarm behavior.
+    """
+    name = 'VALARM'
+    description = 'Alarms describe when and how to provide alerts about events \
+                   and to-dos.'
+    knownChildren = {
+        'ACTION':       (1, 1, None),  # min, max, behaviorRegistry id
+        'TRIGGER':      (1, 1, None),
+        'DURATION':     (0, 1, None),
+        'REPEAT':       (0, 1, None),
+        'DESCRIPTION':  (0, 1, None)
+    }
+
+    @staticmethod
+    def generateImplicitParameters(obj):
+        """
+        Create default ACTION and TRIGGER if they're not set.
+        """
+        try:
+            obj.action
+        except AttributeError:
+            obj.add('action').value = 'AUDIO'
+        try:
+            obj.trigger
+        except AttributeError:
+            obj.add('trigger').value = datetime.timedelta(0)
+
+    @classmethod
+    def validate(cls, obj, raiseException, *args):
+        """
+        # TODO
+        if obj.contents.has_key('dtend') and obj.contents.has_key('duration'):
+            if raiseException:
+                m = "VEVENT components cannot contain both DTEND and DURATION\
+                     components"
+                raise ValidateError(m)
+            return False
+        else:
+            return super(VEvent, cls).validate(obj, raiseException, *args)
+        """
+        return True
+
+registerBehavior(VAlarm)
+
+
+class VAvailability(VCalendarComponentBehavior):
+    """
+    Availability state behavior.
+
+    Used to represent user's available time slots.
+    """
+    name = 'VAVAILABILITY'
+    description = 'A component used to represent a user\'s available time slots.'
+    sortFirst = ('uid', 'dtstart', 'duration', 'dtend')
+    knownChildren = {
+        'UID':           (1, 1, None),  # min, max, behaviorRegistry id
+        'DTSTAMP':       (1, 1, None),
+        'BUSYTYPE':      (0, 1, None),
+        'CREATED':       (0, 1, None),
+        'DTSTART':       (0, 1, None),
+        'LAST-MODIFIED': (0, 1, None),
+        'ORGANIZER':     (0, 1, None),
+        'SEQUENCE':      (0, 1, None),
+        'SUMMARY':       (0, 1, None),
+        'URL':           (0, 1, None),
+        'DTEND':         (0, 1, None),
+        'DURATION':      (0, 1, None),
+        'CATEGORIES':    (0, None, None),
+        'COMMENT':       (0, None, None),
+        'CONTACT':       (0, None, None),
+        'AVAILABLE':     (0, None, None),
+    }
+
+    @classmethod
+    def validate(cls, obj, raiseException, *args):
+        if 'dtend' in obj.contents and 'duration' in obj.contents:
+            if raiseException:
+                m = "VAVAILABILITY components cannot contain both DTEND and DURATION components"
+                raise ValidateError(m)
+            return False
+        else:
+            return super(VAvailability, cls).validate(obj, raiseException, *args)
+
+registerBehavior(VAvailability)
+
+
+class Available(RecurringBehavior):
+    """
+    Event behavior.
+    """
+    name = 'AVAILABLE'
+    sortFirst = ('uid', 'recurrence-id', 'dtstart', 'duration', 'dtend')
+    description = 'Defines a period of time in which a user is normally available.'
+    knownChildren = {
+        'DTSTAMP':       (1, 1, None),  # min, max, behaviorRegistry id
+        'DTSTART':       (1, 1, None),
+        'UID':           (1, 1, None),
+        'DTEND':         (0, 1, None),  # NOTE: One of DtEnd or
+        'DURATION':      (0, 1, None),  # Duration must appear, but not both
+        'CREATED':       (0, 1, None),
+        'LAST-MODIFIED': (0, 1, None),
+        'RECURRENCE-ID': (0, 1, None),
+        'RRULE':         (0, 1, None),
+        'SUMMARY':       (0, 1, None),
+        'CATEGORIES':    (0, None, None),
+        'COMMENT':       (0, None, None),
+        'CONTACT':       (0, None, None),
+        'EXDATE':        (0, None, None),
+        'RDATE':         (0, None, None),
+    }
+
+    @classmethod
+    def validate(cls, obj, raiseException, *args):
+        has_dtend = 'dtend' in obj.contents
+        has_duration = 'duration' in obj.contents
+        if has_dtend and has_duration:
+            if raiseException:
+                m = "AVAILABLE components cannot contain both DTEND and DURATION\
+                     properties"
+                raise ValidateError(m)
+            return False
+        elif not (has_dtend or has_duration):
+            if raiseException:
+                m = "AVAILABLE components must contain one of DTEND or DURATION\
+                     properties"
+                raise ValidateError(m)
+            return False
+        else:
+            return super(Available, cls).validate(obj, raiseException, *args)
+
+registerBehavior(Available)
+
+
+class Duration(behavior.Behavior):
+    """
+    Behavior for Duration ContentLines.  Transform to datetime.timedelta.
+    """
+    name = 'DURATION'
+    hasNative = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """
+        Turn obj.value into a datetime.timedelta.
+        """
+        if obj.isNative:
+            return obj
+        obj.isNative = True
+        obj.value = 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.warning("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.
+    """
+    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, 'FREEBUSY')
+
+
+class RRule(behavior.Behavior):
+    """
+    Dummy behavior to avoid having RRULEs being treated as text lines (and thus
+    having semi-colons inaccurately escaped).
+    """
+registerBehavior(RRule, 'RRULE')
+registerBehavior(RRule, 'EXRULE')
+
+
+# ------------------------ Registration of common classes ----------------------
+utcDateTimeList = ['LAST-MODIFIED', 'CREATED', 'COMPLETED', 'DTSTAMP']
+list(map(lambda x: registerBehavior(UTCDateTimeBehavior, x), utcDateTimeList))
+
+dateTimeOrDateList = ['DTEND', 'DTSTART', 'DUE', 'RECURRENCE-ID']
+list(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', 'BUSYTYPE']
+list(map(lambda x: registerBehavior(TextBehavior, x), textList))
+
+list(map(lambda x: registerBehavior(MultiTextBehavior, x), ['CATEGORIES',
+                                                            'RESOURCES']))
+registerBehavior(SemicolonMultiTextBehavior, 'REQUEST-STATUS')
+
+
+# ------------------------ 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 ical DURATION.
+    """
+    if delta.days == 0:
+        sign = 1
+    else:
+        sign = delta.days / abs(delta.days)
+    delta = abs(delta)
+    days = delta.days
+    hours = int(delta.seconds / 3600)
+    minutes = int((delta.seconds % 3600) / 60)
+    seconds = int(delta.seconds % 60)
+
+    output = ''
+    if sign == -1:
+        output += '-'
+    output += 'P'
+    if days:
+        output += '{}D'.format(days)
+    if hours or minutes or seconds:
+        output += 'T'
+    elif not days:  # Deal with zero duration
+        output += 'T0S'
+    if hours:
+        output += '{}H'.format(hours)
+    if minutes:
+        output += '{}M'.format(minutes)
+    if seconds:
+        output += '{}S'.format(seconds)
+    return output
+
+
+def timeToString(dateOrDateTime):
+    """
+    Wraps dateToString and dateTimeToString, returning the results
+    of either based on the type of the argument
+    """
+    if hasattr(dateOrDateTime, 'hour'):
+        return dateTimeToString(dateOrDateTime)
+    return dateToString(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)
+
+    datestr = "{0}{1}{2}T{3}{4}{5}".format(
+        numToDigits(dateTime.year, 4),
+        numToDigits(dateTime.month, 2),
+        numToDigits(dateTime.day, 2),
+        numToDigits(dateTime.hour, 2),
+        numToDigits(dateTime.minute, 2),
+        numToDigits(dateTime.second, 2),
+    )
+    if tzinfo_eq(dateTime.tzinfo, utc):
+        datestr += "Z"
+    return datestr
+
+
+def deltaToOffset(delta):
+    absDelta = abs(delta)
+    hours = int(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 = s.upper()
+    return (s.find("P") != -1) and (s.find("P") < 2)
+
+
+def stringToDate(s):
+    year = int(s[0:4])
+    month = int(s[4:6])
+    day = int(s[6:8])
+    return datetime.date(year, month, day)
+
+
+def stringToDateTime(s, tzinfo=None):
+    """
+    Returns datetime.datetime object.
+    """
+    try:
+        year = int(s[0:4])
+        month = int(s[4:6])
+        day = int(s[6:8])
+        hour = int(s[9:11])
+        minute = int(s[11:13])
+        second = int(s[13:15])
+        if len(s) > 15:
+            if s[15] == 'Z':
+                tzinfo = getTzid('UTC')
+    except:
+        raise ParseError("'{0!s}' is not a valid DATE-TIME".format(s))
+    year = year and year or 2000
+    if tzinfo is not None and hasattr(tzinfo,'localize'):  # PyTZ case
+        return tzinfo.localize(datetime.datetime(year, month, day, hour, minute, second))
+    return datetime.datetime(year, month, day, hour, minute, second, 0, tzinfo)
+
+
+# DQUOTE included to work around iCal's penchant for backslash escaping it,
+# although it isn't actually supposed to be escaped according to rfc2445 TEXT
+escapableCharList = '\\;,Nn"'
+
+
+def stringToTextValues(s, listSeparator=',', charList=None, strict=False):
+    """
+    Returns list of strings.
+    """
+    if charList is None:
+        charList = escapableCharList
+
+    def escapableChar(c):
+        return c in charList
+
+    def error(msg):
+        if strict:
+            raise ParseError(msg)
+        else:
+            logging.error(msg)
+
+    # vars which control state machine
+    charIterator = enumerate(s)
+    state = "read normal"
+
+    current = []
+    results = []
+
+    while True:
+        try:
+            charIndex, char = next(charIterator)
+        except:
+            char = "eof"
+
+        if state == "read normal":
+            if char == '\\':
+                state = "read escaped char"
+            elif char == listSeparator:
+                state = "read normal"
+                current = "".join(current)
+                results.append(current)
+                current = []
+            elif char == "eof":
+                state = "end"
+            else:
+                state = "read normal"
+                current.append(char)
+
+        elif state == "read escaped char":
+            if escapableChar(char):
+                state = "read normal"
+                if char in 'nN':
+                    current.append('\n')
+                else:
+                    current.append(char)
+            else:
+                state = "read normal"
+                # leave unrecognized escaped characters for later passes
+                current.append('\\' + char)
+
+        elif state == "end":  # an end state
+            if len(current) or len(results) == 0:
+                current = "".join(current)
+                results.append(current)
+            return results
+
+        elif state == "error":  # an end state
+            return results
+
+        else:
+            state = "error"
+            error("unknown state: '{0!s}' reached in {1!s}".format(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)
+
+    # 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 = next(charIterator)
+        except:
+            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"
+                error("got unexpected character {0} reading in duration: {1}"
+                      .format(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
+            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("unknown state: '{0!s}' reached in {1!s}".format(state, s))
+
+
+def parseDtstart(contentline, allowSignatureMismatch=False):
+    """
+    Convert a contentline's value into a date or date-time.
+
+    A variety of clients don't serialize dates with the appropriate VALUE
+    parameter, so rather than failing on these (technically invalid) lines,
+    if allowSignatureMismatch is True, try to parse both varieties.
+    """
+    tzinfo = getTzid(getattr(contentline, 'tzid_param', None))
+    valueParam = getattr(contentline, 'value_param', 'DATE-TIME').upper()
+    if valueParam == "DATE":
+        return stringToDate(contentline.value)
+    elif valueParam == "DATE-TIME":
+        try:
+            return stringToDateTime(contentline.value, tzinfo)
+        except:
+            if allowSignatureMismatch:
+                return stringToDate(contentline.value)
+            else:
+                raise
+
+
+def stringToPeriod(s, tzinfo=None):
+    values = s.split("/")
+    start = stringToDateTime(values[0], tzinfo)
+    valEnd = values[1]
+    if isDuration(valEnd):  # period-start = date-time "/" dur-value
+        delta = stringToDurations(valEnd)[0]
+        return (start, delta)
+    else:
+        return (start, stringToDateTime(valEnd, tzinfo))
+
+
+def getTransition(transitionTo, year, tzinfo):
+    """
+    Return the datetime of the transition to/from DST, or None.
+    """
+    def firstTransition(iterDates, test):
+        """
+        Return the last date not matching test, or None if all tests matched.
+        """
+        success = None
+        for dt in iterDates:
+            if not test(dt):
+                success = dt
+            else:
+                if success is not None:
+                    return success
+        return success  # may be None
+
+    def generateDates(year, month=None, day=None):
+        """
+        Iterate over possible dates with unspecified values.
+        """
+        months = range(1, 13)
+        days = range(1, 32)
+        hours = range(0, 24)
+        if month is None:
+            for month in months:
+                yield datetime.datetime(year, month, 1)
+        elif day is None:
+            for day in days:
+                try:
+                    yield datetime.datetime(year, month, day)
+                except ValueError:
+                    pass
+        else:
+            for hour in hours:
+                yield datetime.datetime(year, month, day, hour)
+
+    assert transitionTo in ('daylight', 'standard')
+    if transitionTo == 'daylight':
+        def test(dt):
+            try:
+                return tzinfo.dst(dt) != zeroDelta
+            except pytz.NonExistentTimeError:
+                return True  # entering daylight time
+            except pytz.AmbiguousTimeError:
+                return False  # entering standard time
+    elif transitionTo == 'standard':
+        def test(dt):
+            try:
+                return tzinfo.dst(dt) == zeroDelta
+            except pytz.NonExistentTimeError:
+                return False  # entering daylight time
+            except pytz.AmbiguousTimeError:
+                return True  # entering standard time
+    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 range(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()

+ 227 - 0
src/vobject/vobject/ics_diff.py

@@ -0,0 +1,227 @@
+from __future__ import print_function
+
+from optparse import OptionParser
+
+from .base import Component, getBehavior, newFromBehavior, readOne
+
+"""
+Compare VTODOs and VEVENTs in two iCalendar sources.
+"""
+
+
+def getSortKey(component):
+    def getUID(component):
+        return component.getChildValue('uid', '')
+
+    # it's not quite as simple as getUID, need to account for recurrenceID and
+    # sequence
+
+    def getSequence(component):
+        sequence = component.getChildValue('sequence', 0)
+        return "{0:05d}".format(int(sequence))
+
+    def getRecurrenceID(component):
+        recurrence_id = component.getChildValue('recurrence_id', None)
+        if recurrence_id is None:
+            return '0000-00-00'
+        else:
+            return recurrence_id.isoformat()
+
+    return getUID(component) + getSequence(component) + getRecurrenceID(component)
+
+
+def sortByUID(components):
+    return sorted(components, key=getSortKey)
+
+
+def deleteExtraneous(component, ignore_dtstamp=False):
+    """
+    Recursively walk the component's children, deleting extraneous details like
+    X-VOBJ-ORIGINAL-TZID.
+    """
+    for comp in component.components():
+        deleteExtraneous(comp, ignore_dtstamp)
+    for line in component.lines():
+        if 'X-VOBJ-ORIGINAL-TZID' in line.params:
+            del line.params['X-VOBJ-ORIGINAL-TZID']
+    if ignore_dtstamp and hasattr(component, 'dtstamp_list'):
+        del component.dtstamp_list
+
+
+def diff(left, right):
+    """
+    Take two VCALENDAR components, compare VEVENTs and VTODOs in them,
+    return a list of object pairs containing just UID and the bits
+    that didn't match, using None for objects that weren't present in one
+    version or the other.
+
+    When there are multiple ContentLines in one VEVENT, for instance many
+    DESCRIPTION lines, such lines original order is assumed to be
+    meaningful.  Order is also preserved when comparing (the unlikely case
+    of) multiple parameters of the same type in a ContentLine
+
+    """
+
+    def processComponentLists(leftList, rightList):
+        output = []
+        rightIndex = 0
+        rightListSize = len(rightList)
+
+        for comp in leftList:
+            if rightIndex >= rightListSize:
+                output.append((comp, None))
+            else:
+                leftKey = getSortKey(comp)
+                rightComp = rightList[rightIndex]
+                rightKey = getSortKey(rightComp)
+                while leftKey > rightKey:
+                    output.append((None, rightComp))
+                    rightIndex += 1
+                    if rightIndex >= rightListSize:
+                        output.append((comp, None))
+                        break
+                    else:
+                        rightComp = rightList[rightIndex]
+                        rightKey = getSortKey(rightComp)
+
+                if leftKey < rightKey:
+                    output.append((comp, None))
+                elif leftKey == rightKey:
+                    rightIndex += 1
+                    matchResult = processComponentPair(comp, rightComp)
+                    if matchResult is not None:
+                        output.append(matchResult)
+
+        return output
+
+    def newComponent(name, body):
+        if body is None:
+            return None
+        else:
+            c = Component(name)
+            c.behavior = getBehavior(name)
+            c.isNative = True
+            return c
+
+    def processComponentPair(leftComp, rightComp):
+        """
+        Return None if a match, or a pair of components including UIDs and
+        any differing children.
+
+        """
+        leftChildKeys = leftComp.contents.keys()
+        rightChildKeys = rightComp.contents.keys()
+
+        differentContentLines = []
+        differentComponents = {}
+
+        for key in leftChildKeys:
+            rightList = rightComp.contents.get(key, [])
+            if isinstance(leftComp.contents[key][0], Component):
+                compDifference = processComponentLists(leftComp.contents[key],
+                                                       rightList)
+                if len(compDifference) > 0:
+                    differentComponents[key] = compDifference
+
+            elif leftComp.contents[key] != rightList:
+                differentContentLines.append((leftComp.contents[key],
+                                              rightList))
+
+        for key in rightChildKeys:
+            if key not in leftChildKeys:
+                if isinstance(rightComp.contents[key][0], Component):
+                    differentComponents[key] = ([], rightComp.contents[key])
+                else:
+                    differentContentLines.append(([], rightComp.contents[key]))
+
+        if len(differentContentLines) == 0 and len(differentComponents) == 0:
+            return None
+        else:
+            left = newFromBehavior(leftComp.name)
+            right = newFromBehavior(leftComp.name)
+            # add a UID, if one existed, despite the fact that they'll always be
+            # the same
+            uid = leftComp.getChildValue('uid')
+            if uid is not None:
+                left.add('uid').value = uid
+                right.add('uid').value = uid
+
+            for name, childPairList in differentComponents.items():
+                leftComponents, rightComponents = zip(*childPairList)
+                if len(leftComponents) > 0:
+                    # filter out None
+                    left.contents[name] = filter(None, leftComponents)
+                if len(rightComponents) > 0:
+                    # filter out None
+                    right.contents[name] = filter(None, rightComponents)
+
+            for leftChildLine, rightChildLine in differentContentLines:
+                nonEmpty = leftChildLine or rightChildLine
+                name = nonEmpty[0].name
+                if leftChildLine is not None:
+                    left.contents[name] = leftChildLine
+                if rightChildLine is not None:
+                    right.contents[name] = rightChildLine
+
+            return left, right
+
+    vevents = processComponentLists(sortByUID(getattr(left, 'vevent_list', [])),
+                                    sortByUID(getattr(right, 'vevent_list', [])))
+
+    vtodos = processComponentLists(sortByUID(getattr(left, 'vtodo_list', [])),
+                                   sortByUID(getattr(right, 'vtodo_list', [])))
+
+    return vevents + vtodos
+
+
+def prettyDiff(leftObj, rightObj):
+    for left, right in diff(leftObj, rightObj):
+        print("<<<<<<<<<<<<<<<")
+        if left is not None:
+            left.prettyPrint()
+        print("===============")
+        if right is not None:
+            right.prettyPrint()
+        print(">>>>>>>>>>>>>>>")
+        print
+
+
+def main():
+    options, args = getOptions()
+    if args:
+        ignore_dtstamp = options.ignore
+        ics_file1, ics_file2 = args
+        with open(ics_file1) as f, open(ics_file2) as g:
+            cal1 = readOne(f)
+            cal2 = readOne(g)
+        deleteExtraneous(cal1, ignore_dtstamp=ignore_dtstamp)
+        deleteExtraneous(cal2, ignore_dtstamp=ignore_dtstamp)
+        prettyDiff(cal1, cal2)
+
+version = "0.1"
+
+
+def getOptions():
+    ##### Configuration options #####
+
+    usage = "usage: %prog [options] ics_file1 ics_file2"
+    parser = OptionParser(usage=usage, version=version)
+    parser.set_description("ics_diff will print a comparison of two iCalendar files ")
+
+    parser.add_option("-i", "--ignore-dtstamp", dest="ignore", action="store_true",
+                      default=False, help="ignore DTSTAMP lines [default: False]")
+
+    (cmdline_options, args) = parser.parse_args()
+    if len(args) < 2:
+        print("error: too few arguments given")
+        print
+        print(parser.format_help())
+        return False, False
+
+    return cmdline_options, args
+
+if __name__ == "__main__":
+    try:
+        main()
+    except KeyboardInterrupt:
+        print("Aborted")

+ 370 - 0
src/vobject/vobject/vcard.py

@@ -0,0 +1,370 @@
+"""Definitions and behavior for vCard 3.0"""
+
+import codecs
+
+from . import behavior
+
+from .base import ContentLine, registerBehavior, backslashEscape, str_
+from .icalendar import stringToTextValues
+
+
+# Python 3 no longer has a basestring type, so....
+try:
+    basestring = basestring
+except NameError:
+    basestring = (str, bytes)
+
+# ------------------------ vCard structs ---------------------------------------
+
+
+class Name(object):
+    def __init__(self, family='', given='', additional='', prefix='',
+                 suffix=''):
+        """
+        Each name attribute can be a string or a list of strings.
+        """
+        self.family = family
+        self.given = given
+        self.additional = additional
+        self.prefix = prefix
+        self.suffix = suffix
+
+    @staticmethod
+    def toString(val):
+        """
+        Turn a string or array value into a string.
+        """
+        if type(val) in (list, tuple):
+            return ' '.join(val)
+        return val
+
+    def __str__(self):
+        eng_order = ('prefix', 'given', 'additional', 'family', 'suffix')
+        out = ' '.join(self.toString(getattr(self, val)) for val in eng_order)
+        return str_(out)
+
+    def __repr__(self):
+        return "<Name: {0!s}>".format(self.__str__())
+
+    def __eq__(self, other):
+        try:
+            return (self.family == other.family and
+                    self.given == other.given and
+                    self.additional == other.additional and
+                    self.prefix == other.prefix and
+                    self.suffix == other.suffix)
+        except:
+            return False
+
+
+class Address(object):
+    def __init__(self, street='', city='', region='', code='',
+                 country='', box='', extended=''):
+        """
+        Each name attribute can be a string or a list of strings.
+        """
+        self.box = box
+        self.extended = extended
+        self.street = street
+        self.city = city
+        self.region = region
+        self.code = code
+        self.country = country
+
+    @staticmethod
+    def toString(val, join_char='\n'):
+        """
+        Turn a string or array value into a string.
+        """
+        if type(val) in (list, tuple):
+            return join_char.join(val)
+        return val
+
+    lines = ('box', 'extended', 'street')
+    one_line = ('city', 'region', 'code')
+
+    def __str__(self):
+        lines = '\n'.join(self.toString(getattr(self, val))
+                          for val in self.lines if getattr(self, val))
+        one_line = tuple(self.toString(getattr(self, val), ' ')
+                         for val in self.one_line)
+        lines += "\n{0!s}, {1!s} {2!s}".format(*one_line)
+        if self.country:
+            lines += '\n' + self.toString(self.country)
+        return lines
+
+    def __repr__(self):
+        return "<Address: {0!s}>".format(self)
+
+    def __eq__(self, other):
+        try:
+            return (self.box == other.box and
+                    self.extended == other.extended and
+                    self.street == other.street and
+                    self.city == other.city and
+                    self.region == other.region and
+                    self.code == other.code and
+                    self.country == other.country)
+        except:
+            return False
+
+
+# ------------------------ Registered Behavior subclasses ----------------------
+
+class VCardTextBehavior(behavior.Behavior):
+    """
+    Provide backslash escape encoding/decoding for single valued properties.
+
+    TextBehavior also deals with base64 encoding if the ENCODING parameter is
+    explicitly set to BASE64.
+    """
+    allowGroup = True
+    base64string = 'B'
+
+    @classmethod
+    def decode(cls, line):
+        """
+        Remove backslash escaping from line.valueDecode line, either to remove
+        backslash espacing, or to decode base64 encoding. The content line should
+        contain a ENCODING=b for base64 encoding, but Apple Addressbook seems to
+        export a singleton parameter of 'BASE64', which does not match the 3.0
+        vCard spec. If we encouter that, then we transform the parameter to
+        ENCODING=b
+        """
+        if line.encoded:
+            if 'BASE64' in line.singletonparams:
+                line.singletonparams.remove('BASE64')
+                line.encoding_param = cls.base64string
+            encoding = getattr(line, 'encoding_param', None)
+            if encoding:
+                if isinstance(line.value, bytes):
+                    line.value = codecs.decode(line.value, "base64")
+                else:
+                    line.value = codecs.decode(line.value.encode("utf-8"), "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:
+                if isinstance(line.value, bytes):
+                    line.value = codecs.encode(line.value, "base64").decode("utf-8").replace('\n', '')
+                else:
+                    line.value = codecs.encode(line.value.encode(encoding), "base64").decode("utf-8")
+            else:
+                line.value = backslashEscape(line.value)
+            line.encoded = True
+
+
+class VCardBehavior(behavior.Behavior):
+    allowGroup = True
+    defaultBehavior = VCardTextBehavior
+
+
+class VCard3_0(VCardBehavior):
+    """
+    vCard 3.0 behavior.
+    """
+    name = 'VCARD'
+    description = 'vCard 3.0, defined in rfc2426'
+    versionString = '3.0'
+    isComponent = True
+    sortFirst = ('version', 'prodid', 'uid')
+    knownChildren = {
+        'N':          (0, 1, None),  # min, max, behaviorRegistry id
+        'FN':         (1, None, None),
+        'VERSION':    (1, 1, None),  # required, auto-generated
+        'PRODID':     (0, 1, None),
+        'LABEL':      (0, None, None),
+        'UID':        (0, None, None),
+        'ADR':        (0, None, None),
+        'ORG':        (0, None, None),
+        'PHOTO':      (0, None, None),
+        'CATEGORIES': (0, None, None)
+    }
+
+    @classmethod
+    def generateImplicitParameters(cls, obj):
+        """
+        Create PRODID, VERSION, and VTIMEZONEs if needed.
+
+        VTIMEZONEs will need to exist whenever TZID parameters exist or when
+        datetimes with tzinfo exist.
+        """
+        if not hasattr(obj, 'version'):
+            obj.add(ContentLine('VERSION', [], cls.versionString))
+registerBehavior(VCard3_0, default=True)
+
+
+class FN(VCardTextBehavior):
+    name = "FN"
+    description = 'Formatted name'
+registerBehavior(FN)
+
+
+class Label(VCardTextBehavior):
+    name = "Label"
+    description = 'Formatted address'
+registerBehavior(Label)
+
+wacky_apple_photo_serialize = True
+REALLY_LARGE = 1E50
+
+
+class Photo(VCardTextBehavior):
+    name = "Photo"
+    description = 'Photograph'
+
+    @classmethod
+    def valueRepr(cls, line):
+        return " (BINARY PHOTO DATA at 0x{0!s}) ".format(id(line.value))
+
+    @classmethod
+    def serialize(cls, obj, buf, lineLength, validate):
+        """
+        Apple's Address Book is *really* weird with images, it expects
+        base64 data to have very specific whitespace.  It seems Address Book
+        can handle PHOTO if it's not wrapped, so don't wrap it.
+        """
+        if wacky_apple_photo_serialize:
+            lineLength = REALLY_LARGE
+        VCardTextBehavior.serialize(obj, buf, lineLength, validate)
+
+registerBehavior(Photo)
+
+
+def toListOrString(string):
+    stringList = stringToTextValues(string)
+    if len(stringList) == 1:
+        return stringList[0]
+    else:
+        return stringList
+
+
+def splitFields(string):
+    """
+    Return a list of strings or lists from a Name or Address.
+    """
+    return [toListOrString(i) for i in
+            stringToTextValues(string, listSeparator=';', charList=';')]
+
+
+def toList(stringOrList):
+    if isinstance(stringOrList, basestring):
+        return [stringOrList]
+    return stringOrList
+
+
+def serializeFields(obj, order=None):
+    """
+    Turn an object's fields into a ';' and ',' seperated string.
+
+    If order is None, obj should be a list, backslash escape each field and
+    return a ';' separated string.
+    """
+    fields = []
+    if order is None:
+        fields = [backslashEscape(val) for val in obj]
+    else:
+        for field in order:
+            escapedValueList = [backslashEscape(val) for val in
+                                toList(getattr(obj, field))]
+            fields.append(','.join(escapedValueList))
+    return ';'.join(fields)
+
+
+NAME_ORDER = ('family', 'given', 'additional', 'prefix', 'suffix')
+ADDRESS_ORDER = ('box', 'extended', 'street', 'city', 'region', 'code',
+                 'country')
+
+
+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')
+
+
+class AddressBehavior(VCardBehavior):
+    """
+    A structured address.
+    """
+    hasNative = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """
+        Turn obj.value into an Address.
+        """
+        if obj.isNative:
+            return obj
+        obj.isNative = True
+        obj.value = Address(**dict(zip(ADDRESS_ORDER, splitFields(obj.value))))
+        return obj
+
+    @staticmethod
+    def transformFromNative(obj):
+        """
+        Replace the Address in obj.value with a string.
+        """
+        obj.isNative = False
+        obj.value = serializeFields(obj.value, ADDRESS_ORDER)
+        return obj
+registerBehavior(AddressBehavior, 'ADR')
+
+
+class OrgBehavior(VCardBehavior):
+    """
+    A list of organization values and sub-organization values.
+    """
+    hasNative = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """
+        Turn obj.value into a list.
+        """
+        if obj.isNative:
+            return obj
+        obj.isNative = True
+        obj.value = splitFields(obj.value)
+        return obj
+
+    @staticmethod
+    def transformFromNative(obj):
+        """
+        Replace the list in obj.value with a string.
+        """
+        if not obj.isNative:
+            return obj
+        obj.isNative = False
+        obj.value = serializeFields(obj.value)
+        return obj
+registerBehavior(OrgBehavior, 'ORG')

+ 162 - 0
src/vobject/vobject/win32tz.py

@@ -0,0 +1,162 @@
+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:
+            return (dston <= dt.replace(tzinfo=None) < dstoff)
+        else:
+            return not (dstoff <= dt.replace(tzinfo=None) < dston)
+
+    def __repr__(self):
+        return "<win32tz - {0!s}>".format(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."""
+    d = {}
+    size = _winreg.QueryInfoKey(key)[1]
+    for i in xrange(size):
+        d[_winreg.EnumValue(key, i)[0]] = _winreg.EnumValue(key, i)[1]
+    return d
+
+
+def _test():
+    import win32tz, doctest
+    doctest.testmod(win32tz, verbose=0)
+
+if __name__ == '__main__':
+    _test()

+ 3 - 1
test/vcfExport_1.py

@@ -1,11 +1,13 @@
 # Doc
 #  Script to extract Contacts from Sailfish Contact SQLite DB located at 
 #
-#  
+  
 # Links
 #  FileFormatdescription: https://docs.fileformat.com/email/vcf/#vcf-30-example
 #  Pytho vobject: http://eventable.github.io/vobject/
+
 # ChangeLog
+#  2021-07-30 - first version VObject
 
 import sqlite3
 import vobject

+ 65 - 0
test/vcfExport_2_children.py

@@ -0,0 +1,65 @@
+# Doc
+#  Script to extract Contacts from Sailfish Contact SQLite DB located at 
+#
+#  
+# Links
+#  FileFormatdescription: https://docs.fileformat.com/email/vcf/#vcf-30-example
+#  Pytho vobject: http://eventable.github.io/vobject/
+# ChangeLog
+
+import sqlite3
+import vobject
+import uuid
+
+SQLconn = sqlite3.connect('../Testdata/system/Contacts/qtcontacts-sqlite/contacts.db')
+
+
+try:
+    SQLContCur = SQLconn.cursor()
+    for row in SQLContCur.execute('SELECT * FROM Contacts'):
+            print(row[4] + ' ' + row[6])
+        
+            # contactID abfragen
+            contactID=row[0]
+            
+            # wir erstellen das Objekt
+            vcf = vobject.vCard()
+            
+            vcf.add('uid').value = str(uuid.uuid4())
+            #vcf.add('uid').value = "Testdaten"
+            vcf.add('n').value = vobject.vcard.Name( family=row[6], given=row[4] )
+            vcf.add('fn').value =row[1]
+            
+            SQLEmailCur = SQLconn.cursor()
+            
+            ## Abfragen E-Mail-Adressen
+            for Emailrow in SQLEmailCur.execute('SELECT * from EmailAddresses JOIN Details on Details.detailId= EmailAddresses.detailId where EmailAddresses.contactId = ' + str(contactID)):
+                
+                # debug ausgabe
+                print("debug: " + str(Emailrow[2]) + " at " + str(Emailrow[9]))
+                
+                email = vcf.add('email')
+                email.value = str(Emailrow[2])
+    
+                # nur den Typ einpflegen, wenn das hier nicht none ist
+                if Emailrow[9] != None:
+                    email.type_param = str(Emailrow[9])
+
+
+            
+            print(vcf.serialize())
+
+# hier brauchen wir einige eception handles -> wie bekommen wir die einzelnen exceptions heruas ?
+#except:
+    #print("Error in executing SQL")
+    
+    
+except AttributeError:
+    print("Datatype mismatch")
+    raise
+
+# das generöse Except am Ende    
+except:
+    print("unhandled error")
+    raise
+

+ 65 - 0
test/vcfExport_PhoneNumbers.py

@@ -0,0 +1,65 @@
+# Doc
+#  Script to extract Contacts from Sailfish Contact SQLite DB located at 
+#
+#  
+# Links
+#  FileFormatdescription: https://docs.fileformat.com/email/vcf/#vcf-30-example
+#  Pytho vobject: http://eventable.github.io/vobject/
+# ChangeLog
+
+import sqlite3
+import vobject
+import uuid
+
+SQLconn = sqlite3.connect('../Testdata/system/Contacts/qtcontacts-sqlite/contacts.db')
+
+
+try:
+    SQLContCur = SQLconn.cursor()
+    for row in SQLContCur.execute('SELECT * FROM Contacts'):
+            print(row[4] + ' ' + row[6])
+        
+            # contactID abfragen
+            contactID=row[0]
+            
+            # wir erstellen das Objekt
+            vcf = vobject.vCard()
+            
+            vcf.add('uid').value = str(uuid.uuid4())
+            #vcf.add('uid').value = "Testdaten"
+            vcf.add('n').value = vobject.vcard.Name( family=row[6], given=row[4] )
+            vcf.add('fn').value =row[1]
+            
+            SQLEmailCur = SQLconn.cursor()
+            
+            ## Abfragen E-Mail-Adressen
+            for Emailrow in SQLEmailCur.execute('SELECT * from EmailAddresses JOIN Details on Details.detailId= EmailAddresses.detailId where EmailAddresses.contactId = ' + str(contactID)):
+                
+                # debug ausgabe
+                print("debug: " + str(Emailrow[2]) + " at " + str(Emailrow[9]))
+                
+                email = vcf.add('email')
+                email.value = str(Emailrow[2])
+    
+                # nur den Typ einpflegen, wenn das hier nicht none ist
+                if Emailrow[9] != None:
+                    email.type_param = str(Emailrow[9])
+
+
+            
+            print(vcf.serialize())
+
+# hier brauchen wir einige eception handles -> wie bekommen wir die einzelnen exceptions heruas ?
+#except:
+    #print("Error in executing SQL")
+    
+    
+except AttributeError:
+    print("Datatype mismatch")
+    raise
+
+# das generöse Except am Ende    
+except:
+    print("unhandled error")
+    raise
+

Some files were not shown because too many files changed in this diff