diff --git a/radicale/__init__.py b/radicale/__init__.py index 563c1d4..8582856 100644 --- a/radicale/__init__.py +++ b/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 diff --git a/radicale/storage.py b/radicale/storage.py index f8215c8..c4baf24 100644 --- a/radicale/storage.py +++ b/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 diff --git a/radicale_vobject/ACKNOWLEDGEMENTS.txt b/radicale_vobject/ACKNOWLEDGEMENTS.txt deleted file mode 100644 index 044911b..0000000 --- a/radicale_vobject/ACKNOWLEDGEMENTS.txt +++ /dev/null @@ -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 diff --git a/radicale_vobject/LICENSE-2.0.txt b/radicale_vobject/LICENSE-2.0.txt deleted file mode 100644 index d645695..0000000 --- a/radicale_vobject/LICENSE-2.0.txt +++ /dev/null @@ -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. diff --git a/radicale_vobject/README.md b/radicale_vobject/README.md deleted file mode 100644 index 33daf6f..0000000 --- a/radicale_vobject/README.md +++ /dev/null @@ -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) diff --git a/radicale_vobject/__init__.py b/radicale_vobject/__init__.py deleted file mode 100644 index 416dea7..0000000 --- a/radicale_vobject/__init__.py +++ /dev/null @@ -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}s can be - parsed using L{readComponents}. As each Component - is parsed, vobject will attempt to give it a L{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} classes implement validation for - L{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} 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') - - >>> x - ]> - >>> v = x.vevent - >>> utc = icalendar.utc - >>> v.add('dtstart').value = datetime.datetime(2004, 12, 15, 14, tzinfo = utc) - >>> v - ]> - >>> x - ]>]> - >>> 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') diff --git a/radicale_vobject/base.py b/radicale_vobject/base.py deleted file mode 100644 index 26a0d39..0000000 --- a/radicale_vobject/base.py +++ /dev/null @@ -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:: - - - @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 {name!s})\.)?(?P {name!s}) # name group - (?P (?: {param!s} )* ) # params group (may be empty) -: (?P .* )$ # 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") diff --git a/radicale_vobject/behavior.py b/radicale_vobject/behavior.py deleted file mode 100644 index 0f2cd41..0000000 --- a/radicale_vobject/behavior.py +++ /dev/null @@ -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} or - L{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 diff --git a/radicale_vobject/icalendar.py b/radicale_vobject/icalendar.py deleted file mode 100644 index bcc4fab..0000000 --- a/radicale_vobject/icalendar.py +++ /dev/null @@ -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 "".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}. - """ - 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() diff --git a/radicale_vobject/vcard.py b/radicale_vobject/vcard.py deleted file mode 100644 index e966e97..0000000 --- a/radicale_vobject/vcard.py +++ /dev/null @@ -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 "".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 "".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') diff --git a/setup.cfg b/setup.cfg index 83848de..6c7e202 100644 --- a/setup.cfg +++ b/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 diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 1e97353..54debe2 --- a/setup.py +++ b/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={