Просмотр исходного кода

Revert "Fork vobject"

This reverts commit 126a31c82abaf64ff27fc1bb5844a64a3b4e85f7.
Unrud 7 лет назад
Родитель
Сommit
87a68a6ea8

+ 1 - 1
radicale/__init__.py

@@ -49,7 +49,7 @@ from http import client
 from urllib.parse import unquote, urlparse
 from xml.etree import ElementTree as ET
 
-import radicale_vobject as vobject
+import vobject
 
 from radicale import auth, config, log, rights, storage, web, xmlutils
 

+ 1 - 1
radicale/storage.py

@@ -44,7 +44,7 @@ from math import log
 from random import getrandbits
 from tempfile import NamedTemporaryFile, TemporaryDirectory
 
-import radicale_vobject as vobject
+import vobject
 
 if sys.version_info >= (3, 5):
     # HACK: Avoid import cycle for Python < 3.5

+ 0 - 8
radicale_vobject/ACKNOWLEDGEMENTS.txt

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

+ 0 - 202
radicale_vobject/LICENSE-2.0.txt

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

+ 0 - 7
radicale_vobject/README.md

@@ -1,7 +0,0 @@
-# Radicale VObject
-
-Fork of [VObject](https://github.com/eventable/vobject).
-
-Radicale VObject is licensed under the [Apache 2.0 license](http://www.apache.org/licenses/LICENSE-2.0).
-
-Upstream git commit: [c4ae08b767](https://github.com/eventable/vobject/commit/c4ae08b7678bfdeec3c6b2dcbf74d383fd27ed14)

+ 0 - 88
radicale_vobject/__init__.py

@@ -1,88 +0,0 @@
-"""
-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')

+ 0 - 1217
radicale_vobject/base.py

@@ -1,1217 +0,0 @@
-"""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])
-            s.write(";{0}={1}".format(key, paramstr))
-        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")

+ 0 - 174
radicale_vobject/behavior.py

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

+ 0 - 2068
radicale_vobject/icalendar.py

@@ -1,2068 +0,0 @@
-"""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)
-
-                dtstart = self.dtstart.value
-
-                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:
-                    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 = line.value.replace('\\', '')
-                    rule = rrule.rrulestr(
-                        value, dtstart=dtstart,
-                        # If dtstart has no time zone, `until`
-                        # shouldn't get one, either:
-                        ignoretz=isinstance(dtstart, datetime.date))
-                    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' 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()

+ 0 - 377
radicale_vobject/vcard.py

@@ -1,377 +0,0 @@
-"""Definitions and behavior for vCard 3.0"""
-
-import codecs
-
-from . import behavior
-
-from .base import ContentLine, registerBehavior, backslashEscape, str_
-from .icalendar import stringToTextValues, DateOrDateTimeBehavior
-
-
-# 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),
-        'REV':        (0, 1, 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)
-
-
-class REV(DateOrDateTimeBehavior):
-    name = "REV"
-    description = 'Current revision of this vCard'
-registerBehavior(REV)
-
-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')

+ 1 - 1
setup.cfg

@@ -6,4 +6,4 @@ python-tag = py3
 
 [tool:pytest]
 addopts = --flake8 --isort --cov radicale -r s
-norecursedirs = dist .cache .git build Radicale.egg-info .eggs venv radicale_vobject
+norecursedirs = dist .cache .git build Radicale.egg-info .eggs venv

+ 2 - 2
setup.py

@@ -63,10 +63,10 @@ setup(
                   "Radicale-%s.tar.gz" % VERSION),
     license="GNU GPL v3",
     platforms="Any",
-    packages=["radicale", "radicale_vobject"],
+    packages=["radicale"],
     package_data={"radicale": WEB_FILES},
     entry_points={"console_scripts": ["radicale = radicale.__main__:run"]},
-    install_requires=["python-dateutil==2.6.1"],
+    install_requires=["vobject==0.9.5", "python-dateutil==2.6.1"],
     setup_requires=pytest_runner,
     tests_require=tests_require,
     extras_require={