vobject-0.8.1c/0000755000076500001200000000000011151624231012556 5ustar chandleadminvobject-0.8.1c/ACKNOWLEDGEMENTS.txt0000644000076500001200000000027111017337614015660 0ustar chandleadminEnormous thanks to: Gustavo Niemeyer, for all his work on dateutil Dave Cridland, for helping talk about vobject and working on vcard TJ Gabbour, for putting his heart into parsing vobject-0.8.1c/ez_setup/0000755000076500001200000000000011151624231014414 5ustar chandleadminvobject-0.8.1c/ez_setup/__init__.py0000755000076500001200000002276411126240453016545 0ustar chandleadmin#!python """Bootstrap setuptools installation If you want to use setuptools in your package's setup.py, just include this file in the same directory with it, and add this to the top of your setup.py:: from ez_setup import use_setuptools use_setuptools() If you want to require a specific version of setuptools, set a download mirror, or use an alternate download directory, you can do so by supplying the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ import sys DEFAULT_VERSION = "0.6c9" DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] md5_data = { 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', } import sys, os try: from hashlib import md5 except ImportError: from md5 import md5 def _validate_md5(egg_name, data): if egg_name in md5_data: digest = md5(data).hexdigest() if digest != md5_data[egg_name]: print >>sys.stderr, ( "md5 validation of %s failed! (Possible download problem?)" % egg_name ) sys.exit(2) return data def use_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, download_delay=15 ): """Automatically find/download setuptools and make it available on sys.path `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where setuptools will be downloaded, if it is not already available. If `download_delay` is specified, it should be the number of seconds that will be paused before initiating a download, should one be required. If an older version of setuptools is installed, this routine will print a message to ``sys.stderr`` and raise SystemExit in an attempt to abort the calling script. """ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules def do_download(): egg = download_setuptools(version, download_base, to_dir, download_delay) sys.path.insert(0, egg) import setuptools; setuptools.bootstrap_install_from = egg try: import pkg_resources except ImportError: return do_download() try: pkg_resources.require("setuptools>="+version); return except pkg_resources.VersionConflict, e: if was_imported: print >>sys.stderr, ( "The required version of setuptools (>=%s) is not available, and\n" "can't be installed while this script is running. Please install\n" " a more recent version first, using 'easy_install -U setuptools'." "\n\n(Currently using %r)" ) % (version, e.args[0]) sys.exit(2) else: del pkg_resources, sys.modules['pkg_resources'] # reload ok return do_download() except pkg_resources.DistributionNotFound: return do_download() def download_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay = 15 ): """Download setuptools from a specified location and return its filename `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. """ import urllib2, shutil egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) url = download_base + egg_name saveto = os.path.join(to_dir, egg_name) src = dst = None if not os.path.exists(saveto): # Avoid repeated downloads try: from distutils import log if delay: log.warn(""" --------------------------------------------------------------------------- This script requires setuptools version %s to run (even to display help). I will attempt to download it for you (from %s), but you may need to enable firewall access for this script first. I will start the download in %d seconds. (Note: if this machine does not have network access, please obtain the file %s and place it in this directory before rerunning this script.) ---------------------------------------------------------------------------""", version, download_base, delay, url ); from time import sleep; sleep(delay) log.warn("Downloading %s", url) src = urllib2.urlopen(url) # Read/write all in one block, so we don't create a corrupt file # if the download is interrupted. data = _validate_md5(egg_name, src.read()) dst = open(saveto,"wb"); dst.write(data) finally: if src: src.close() if dst: dst.close() return os.path.realpath(saveto) def main(argv, version=DEFAULT_VERSION): """Install or upgrade setuptools and EasyInstall""" try: import setuptools except ImportError: egg = None try: egg = download_setuptools(version, delay=0) sys.path.insert(0,egg) from setuptools.command.easy_install import main return main(list(argv)+[egg]) # we're done here finally: if egg and os.path.exists(egg): os.unlink(egg) else: if setuptools.__version__ == '0.0.1': print >>sys.stderr, ( "You have an obsolete version of setuptools installed. Please\n" "remove it from your system entirely before rerunning this script." ) sys.exit(2) req = "setuptools>="+version import pkg_resources try: pkg_resources.require(req) except pkg_resources.VersionConflict: try: from setuptools.command.easy_install import main except ImportError: from easy_install import main main(list(argv)+[download_setuptools(delay=0)]) sys.exit(0) # try to force an exit else: if argv: from setuptools.command.easy_install import main main(argv) else: print "Setuptools version",version,"or greater has been installed." print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' def update_md5(filenames): """Update our built-in md5 registry""" import re for name in filenames: base = os.path.basename(name) f = open(name,'rb') md5_data[base] = md5(f.read()).hexdigest() f.close() data = [" %r: %r,\n" % it for it in md5_data.items()] data.sort() repl = "".join(data) import inspect srcfile = inspect.getsourcefile(sys.modules[__name__]) f = open(srcfile, 'rb'); src = f.read(); f.close() match = re.search("\nmd5_data = {\n([^}]+)}", src) if not match: print >>sys.stderr, "Internal error!" sys.exit(2) src = src[:match.start(1)] + repl + src[match.end(1):] f = open(srcfile,'w') f.write(src) f.close() if __name__=='__main__': if len(sys.argv)>2 and sys.argv[1]=='--md5update': update_md5(sys.argv[2:]) else: main(sys.argv[1:]) vobject-0.8.1c/ez_setup/README.txt0000755000076500001200000000114611017337616016130 0ustar chandleadminThis directory exists so that Subversion-based projects can share a single copy of the ``ez_setup`` bootstrap module for ``setuptools``, and have it automatically updated in their projects when ``setuptools`` is updated. For your convenience, you may use the following svn:externals definition:: ez_setup svn://svn.eby-sarna.com/svnroot/ez_setup You can set this by executing this command in your project directory:: svn propedit svn:externals . And then adding the line shown above to the file that comes up for editing. Then, whenever you update your project, ``ez_setup`` will be updated as well. vobject-0.8.1c/LICENSE-2.0.txt0000644000076500001200000002613610161362341014707 0ustar chandleadmin 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. vobject-0.8.1c/PKG-INFO0000644000076500001200000000516111151624231013656 0ustar chandleadminMetadata-Version: 1.0 Name: vobject Version: 0.8.1c Summary: VObject: module for reading vCard and vCalendar files Home-page: http://vobject.skyhouseconsulting.com Author: Jeffrey Harris Author-email: jeffrey@osafoundation.org License: Apache Description: Description ----------- Parses iCalendar and vCard files into Python data structures, decoding the relevant encodings. Also serializes vobject data structures to iCalendar, vCard, or (experimentally) hCalendar unicode strings. Requirements ------------ Requires python 2.4 or later, dateutil (http://labix.org/python-dateutil) 1.1 or later. Recent changes -------------- - Make change_tz.py compatible with python 2.4, so the entire package stays compatible - Fall back to default (the most recent standard) behavior if a VCARD or VCALENDAR doesn't have a recognized VERSION - Fixed a bad performance bug when parsing large text bodies, thanks to Morgen Sagen at Apple - Changed license to Apache 2.0 from Apache 1.1 - Worked around an issue with Apple Address Book's vcard PHOTO parser - Added change_tz module and script for quickly changing event timezones for an ics file. Requires PyICU. - Add support for BYMONTHDAY=-1 (days before the end of the month) when setting rrules from a dateutil rrule - Tolerate a Ruby iCalendar library escaping semi-colons in RRULEs - Make vobjects pickle-able - Add introspection help for IPython so tab completion works with vobject's custom __getattr__ - Allow Outlook's technically illegal use of commas in TZIDs - Allow unicode names for TZIDs - Worked around Lotus Notes use of underscores in names by just silently replacing with dashes - When allowing quoted-printable data, honor CHARSET for each line, defaulting to iso-8859-1 - Simplified directory layout, unit tests are now available via setup.py test For older changes, see - http://vobject.skyhouseconsulting.com/history.html or - http://websvn.osafoundation.org/listing.php?repname=vobject&path=/trunk/ Platform: any Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: License :: OSI Approved :: BSD License Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Programming Language :: Python Classifier: Operating System :: OS Independent Classifier: Topic :: Text Processing vobject-0.8.1c/README.txt0000644000076500001200000001361311037176453014273 0ustar chandleadmin======= VObject ======= VObject simplifies the process of parsing and creating iCalendar and vCard objects. -------------- Installation -------------- To install vobject, run:: python setup.py install vobject requires the dateutil package, which can be installed via easy_install or downloaded from http://labix.org/python-dateutil --------------- Running tests --------------- Unit tests live in doctests throughout the source code, to run all tests, use:: python tests/tests.py ------- Usage ------- Creating iCalendar objects .......................... vobject has a basic datastructure for working with iCalendar-like syntaxes. Additionally, it defines specialized behaviors for many of the commonly used iCalendar objects. To create an object that already has a behavior defined, run: >>> import vobject >>> cal = vobject.newFromBehavior('vcalendar') >>> cal.behavior Convenience functions exist to create iCalendar and vCard objects: >>> cal = vobject.iCalendar() >>> cal.behavior >>> card = vobject.vCard() >>> card.behavior Once you have an object, you can use the add method to create children: >>> cal.add('vevent') >>> cal.vevent.add('summary').value = "This is a note" >>> cal.prettyPrint() VCALENDAR VEVENT SUMMARY: This is a note Note that summary is a little different from vevent, it's a ContentLine, not a Component. It can't have children, and it has a special value attribute. ContentLines can also have parameters. They can be accessed with regular attribute names with _param appended: >>> cal.vevent.summary.x_random_param = 'Random parameter' >>> cal.prettyPrint() VCALENDAR VEVENT SUMMARY: This is a note params for SUMMARY: X-RANDOM ['Random parameter'] There are a few things to note about this example * The underscore in x_random is converted to a dash (dashes are legal in iCalendar, underscores legal in Python) * X-RANDOM's value is a list. If you want to access the full list of parameters, not just the first, use _paramlist: >>> cal.vevent.summary.x_random_paramlist ['Random parameter'] >>> cal.vevent.summary.x_random_paramlist.append('Other param') >>> cal.vevent.summary Similar to parameters, If you want to access more than just the first child of a Component, you can access the full list of children of a given name by appending _list to the attribute name: >>> cal.add('vevent').add('summary').value = "Second VEVENT" >>> for ev in cal.vevent_list: ... print ev.summary.value This is a note Second VEVENT The interaction between the del operator and the hiding of the underlying list is a little tricky, del cal.vevent and del cal.vevent_list both delete all vevent children: >>> first_ev = cal.vevent >>> del cal.vevent >>> cal >>> cal.vevent = first_ev vobject understands Python's datetime module and tzinfo classes. >>> import datetime >>> utc = vobject.icalendar.utc >>> start = cal.vevent.add('dtstart') >>> start.value = datetime.datetime(2006, 2, 16, tzinfo = utc) >>> first_ev.prettyPrint() VEVENT DTSTART: 2006-02-16 00:00:00+00:00 SUMMARY: This is a note params for SUMMARY: X-RANDOM ['Random parameter', 'Other param'] Components and ContentLines have serialize methods: >>> cal.vevent.add('uid').value = 'Sample UID' >>> icalstream = cal.serialize() >>> print icalstream BEGIN:VCALENDAR VERSION:2.0 PRODID:-//PYVOBJECT//NONSGML Version 1//EN BEGIN:VEVENT UID:Sample UID DTSTART:20060216T000000Z SUMMARY;X-RANDOM=Random parameter,Other param:This is a note END:VEVENT END:VCALENDAR Observe that serializing adds missing required lines like version and prodid. A random UID would be generated, too, if one didn't exist. If dtstart's tzinfo had been something other than UTC, an appropriate vtimezone would be created for it. Parsing iCalendar objects ......................... To parse one top level component from an existing iCalendar stream or string, use the readOne function: >>> parsedCal = vobject.readOne(icalstream) >>> parsedCal.vevent.dtstart.value datetime.datetime(2006, 2, 16, 0, 0, tzinfo=tzutc()) Similarly, readComponents is a generator yielding one top level component at a time from a stream or string. >>> vobject.readComponents(icalstream).next().vevent.dtstart.value datetime.datetime(2006, 2, 16, 0, 0, tzinfo=tzutc()) More examples can be found in source code doctests. vCards ...... Making vCards proceeds in much the same way. Note that the 'N' and 'FN' attributes are required. >>> j = vobject.vCard() >>> j.add('n') >>> j.n.value = vobject.vcard.Name( family='Harris', given='Jeffrey' ) >>> j.add('fn') >>> j.fn.value ='Jeffrey Harris' >>> j.add('email') >>> j.email.value = 'jeffrey@osafoundation.org' >>> j.email.type_param = 'INTERNET' >>> j.prettyPrint() VCARD EMAIL: jeffrey@osafoundation.org params for EMAIL: TYPE ['INTERNET'] FN: Jeffrey Harris N: Jeffrey Harris serializing will add any required computable attributes (like 'VERSION') >>> j.serialize() 'BEGIN:VCARD\r\nVERSION:3.0\r\nEMAIL;TYPE=INTERNET:jeffrey@osafoundation.org\r\nFN:Jeffrey Harris\r\nN:Harris;Jeffrey;;;\r\nEND:VCARD\r\n' >>> j.prettyPrint() VCARD VERSION: 3.0 EMAIL: jeffrey@osafoundation.org params for EMAIL: TYPE ['INTERNET'] FN: Jeffrey Harris N: Jeffrey Harris Parsing vCards .............. >>> s = """ ... BEGIN:VCARD ... VERSION:3.0 ... EMAIL;TYPE=INTERNET:jeffrey@osafoundation.org ... FN:Jeffrey Harris ... N:Harris;Jeffrey;;; ... END:VCARD ... """ >>> v = vobject.readOne( s ) >>> v.prettyPrint() VCARD VERSION: 3.0 EMAIL: jeffrey@osafoundation.org params for EMAIL: TYPE [u'INTERNET'] FN: Jeffrey Harris N: Jeffrey Harris >>> v.n.value.family u'Harris'vobject-0.8.1c/setup.cfg0000644000076500001200000000007311151624231014377 0ustar chandleadmin[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 vobject-0.8.1c/setup.py0000755000076500001200000000602511151602405014275 0ustar chandleadmin"""VObject: module for reading vCard and vCalendar files Description ----------- Parses iCalendar and vCard files into Python data structures, decoding the relevant encodings. Also serializes vobject data structures to iCalendar, vCard, or (experimentally) hCalendar unicode strings. Requirements ------------ Requires python 2.4 or later, dateutil (http://labix.org/python-dateutil) 1.1 or later. Recent changes -------------- - Make change_tz.py compatible with python 2.4, so the entire package stays compatible - Fall back to default (the most recent standard) behavior if a VCARD or VCALENDAR doesn't have a recognized VERSION - Fixed a bad performance bug when parsing large text bodies, thanks to Morgen Sagen at Apple - Changed license to Apache 2.0 from Apache 1.1 - Worked around an issue with Apple Address Book's vcard PHOTO parser - Added change_tz module and script for quickly changing event timezones for an ics file. Requires PyICU. - Add support for BYMONTHDAY=-1 (days before the end of the month) when setting rrules from a dateutil rrule - Tolerate a Ruby iCalendar library escaping semi-colons in RRULEs - Make vobjects pickle-able - Add introspection help for IPython so tab completion works with vobject's custom __getattr__ - Allow Outlook's technically illegal use of commas in TZIDs - Allow unicode names for TZIDs - Worked around Lotus Notes use of underscores in names by just silently replacing with dashes - When allowing quoted-printable data, honor CHARSET for each line, defaulting to iso-8859-1 - Simplified directory layout, unit tests are now available via setup.py test For older changes, see - http://vobject.skyhouseconsulting.com/history.html or - http://websvn.osafoundation.org/listing.php?repname=vobject&path=/trunk/ """ from ez_setup import use_setuptools use_setuptools() from setuptools import setup, find_packages doclines = __doc__.splitlines() setup(name = "vobject", version = "0.8.1c", author = "Jeffrey Harris", author_email = "jeffrey@osafoundation.org", license = "Apache", zip_safe = True, url = "http://vobject.skyhouseconsulting.com", entry_points = { 'console_scripts': ['ics_diff = vobject.ics_diff:main', 'change_tz = vobject.change_tz:main']}, include_package_data = True, test_suite = "test_vobject", install_requires = ['python-dateutil >= 1.1'], platforms = ["any"], packages = find_packages(), description = doclines[0], long_description = "\n".join(doclines[2:]), classifiers = """ Development Status :: 5 - Production/Stable Environment :: Console License :: OSI Approved :: BSD License Intended Audience :: Developers Natural Language :: English Programming Language :: Python Operating System :: OS Independent Topic :: Text Processing""".strip().splitlines() ) vobject-0.8.1c/test_files/0000755000076500001200000000000011151624231014717 5ustar chandleadminvobject-0.8.1c/test_files/more_tests.txt0000644000076500007650000000576111132762034020205 0ustar chandlechandle Unicode in vCards ................. >>> import vobject >>> card = vobject.vCard() >>> card.add('fn').value = u'Hello\u1234 World!' >>> card.add('n').value = vobject.vcard.Name('World', u'Hello\u1234') >>> card.add('adr').value = vobject.vcard.Address(u'5\u1234 Nowhere, Apt 1', 'Berkeley', 'CA', '94704', 'USA') >>> card , , ]> >>> card.serialize().decode("utf-8") u'BEGIN:VCARD\r\nVERSION:3.0\r\nADR:;;5\u1234 Nowhere\\, Apt 1;Berkeley;CA;94704;USA\r\nFN:Hello\u1234 World!\r\nN:World;Hello\u1234;;;\r\nEND:VCARD\r\n' >>> print card.serialize() BEGIN:VCARD VERSION:3.0 ADR:;;5ሴ Nowhere\, Apt 1;Berkeley;CA;94704;USA FN:Helloሴ World! N:World;Helloሴ;;; END:VCARD Helper function ............... >>> from pkg_resources import resource_stream >>> def get_stream(path): ... try: ... return resource_stream(__name__, 'test_files/' + path) ... except: # different paths, depending on whether doctest is run directly ... return resource_stream(__name__, path) Unicode in TZID ............... >>> f = get_stream("tzid_8bit.ics") >>> cal = vobject.readOne(f) >>> print cal.vevent.dtstart.value 2008-05-30 15:00:00+06:00 >>> print cal.vevent.dtstart.serialize() DTSTART;TZID=Екатеринбург:20080530T150000 Commas in TZID .............. >>> f = get_stream("ms_tzid.ics") >>> cal = vobject.readOne(f) >>> print cal.vevent.dtstart.value 2008-05-30 15:00:00+10:00 Equality in vCards .................. >>> card.adr.value == vobject.vcard.Address('Just a street') False >>> card.adr.value == vobject.vcard.Address(u'5\u1234 Nowhere, Apt 1', 'Berkeley', 'CA', '94704', 'USA') True Organization (org) .................. >>> card.add('org').value = ["Company, Inc.", "main unit", "sub-unit"] >>> print card.org.serialize() ORG:Company\, Inc.;main unit;sub-unit Ruby escapes semi-colons in rrules .................................. >>> f = get_stream("ruby_rrule.ics") >>> cal = vobject.readOne(f) >>> iter(cal.vevent.rruleset).next() datetime.datetime(2003, 1, 1, 7, 0) quoted-printable ................ >>> vcf = 'BEGIN:VCARD\nVERSION:2.1\nN;ENCODING=QUOTED-PRINTABLE:;=E9\nFN;ENCODING=QUOTED-PRINTABLE:=E9\nTEL;HOME:0111111111\nEND:VCARD\n\n' >>> vcf = vobject.readOne(vcf) >>> vcf.n.value >>> vcf.n.value.given u'\xe9' >>> vcf.serialize() 'BEGIN:VCARD\r\nVERSION:2.1\r\nFN:\xc3\xa9\r\nN:;\xc3\xa9;;;\r\nTEL:0111111111\r\nEND:VCARD\r\n' >>> vcs = 'BEGIN:VCALENDAR\r\nPRODID:-//OpenSync//NONSGML OpenSync vformat 0.3//EN\r\nVERSION:1.0\r\nBEGIN:VEVENT\r\nDESCRIPTION;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:foo =C3=A5=0Abar =C3=A4=\r\n=0Abaz =C3=B6\r\nUID:20080406T152030Z-7822\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n' >>> vcs = vobject.readOne(vcs, allowQP = True) >>> vcs.serialize() 'BEGIN:VCALENDAR\r\nVERSION:1.0\r\nPRODID:-//OpenSync//NONSGML OpenSync vformat 0.3//EN\r\nBEGIN:VEVENT\r\nUID:20080406T152030Z-7822\r\nDESCRIPTION:foo \xc3\xa5\\nbar \xc3\xa4\\nbaz \xc3\xb6\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n' vobject-0.8.1c/test_files/ms_tzid.ics0000644000076500001200000000166511037216632017105 0ustar chandleadminBEGIN:VCALENDAR PRODID:-//Microsoft Corporation//Outlook 12.0 MIMEDIR//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:Canberra, Melbourne, Sydney BEGIN:STANDARD DTSTART:20010325T020000 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3;UNTIL=20050327T070000Z TZOFFSETFROM:+1100 TZOFFSETTO:+1000 TZNAME:Standard Time END:STANDARD BEGIN:STANDARD DTSTART:20060402T020000 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=4;UNTIL=20060402T070000Z TZOFFSETFROM:+1100 TZOFFSETTO:+1000 TZNAME:Standard Time END:STANDARD BEGIN:STANDARD DTSTART:20070325T020000 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 TZOFFSETFROM:+1100 TZOFFSETTO:+1000 TZNAME:Standard Time END:STANDARD BEGIN:DAYLIGHT DTSTART:20001029T020000 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 TZOFFSETFROM:+1000 TZOFFSETTO:+1100 TZNAME:Daylight Savings Time END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT UID:CommaTest DTSTART;TZID="Canberra, Melbourne, Sydney":20080530T150000 END:VEVENT END:VCALENDARvobject-0.8.1c/test_files/recurrence.ics0000644000076500001200000000104611017337701017561 0ustar chandleadminBEGIN:VCALENDAR VERSION :2.0 PRODID :-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN BEGIN:VEVENT CREATED :20060327T214227Z LAST-MODIFIED :20060313T080829Z DTSTAMP :20060116T231602Z UID :70922B3051D34A9E852570EC00022388 SUMMARY :Monthly - All Hands Meeting with Joe Smith STATUS :CONFIRMED CLASS :PUBLIC RRULE :FREQ=MONTHLY;UNTIL=20061228;INTERVAL=1;BYDAY=4TH DTSTART :20060126T230000Z DTEND :20060127T000000Z DESCRIPTION :Repeat Meeting: - Occurs every 4th Thursday of each month END:VEVENT END:VCALENDAR vobject-0.8.1c/test_files/ruby_rrule.ics0000644000076500001200000000051011037262325017612 0ustar chandleadminBEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN METHOD:PUBLISH PRODID:-//LinkeSOFT GmbH//NONSGML DIMEX//EN BEGIN:VEVENT SEQUENCE:0 RRULE:FREQ=DAILY\;COUNT=10 DTEND:20030101T080000 UID:2008-05-29T17:31:42+02:00_865561242 CATEGORIES:Unfiled SUMMARY:Something DTSTART:20030101T070000 DTSTAMP:20080529T152100 END:VEVENT END:VCALENDARvobject-0.8.1c/test_files/tzid_8bit.ics0000644000076500001200000000077611020076367017337 0ustar chandleadminBEGIN:VCALENDAR PRODID:-//Microsoft Corporation//Outlook 12.0 MIMEDIR//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:Екатеринбург BEGIN:STANDARD DTSTART:16011028T030000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 TZOFFSETFROM:+0600 TZOFFSETTO:+0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:16010325T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 TZOFFSETFROM:+0500 TZOFFSETTO:+0600 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT UID:CyrillicTest DTSTART;TZID=Екатеринбург:20080530T150000 END:VEVENT END:VCALENDAR vobject-0.8.1c/test_files/utf8_test.ics0000644000076500001200000000370411151623252017352 0ustar chandleadminBEGIN:VCALENDAR METHOD:PUBLISH CALSCALE:GREGORIAN PRODID:-//EVDB//www.evdb.com//EN VERSION:2.0 X-WR-CALNAME:EVDB Event Feed BEGIN:VEVENT DTSTART:20060922T000100Z DTEND:20060922T050100Z DTSTAMP:20050914T163414Z SUMMARY:The title こんにちはキティ DESCRIPTION:hello\nHere is a description\n\n\nこんにちはキティ \n\n\n\nZwei Java-schwere Entwicklerpositionen und irgendeine Art sond erbar-klingende Netzsichtbarmachungöffnung\, an einer interessanten F irma im Gebäude\, in dem ich angerufenen Semantic Research bearbeite. 1. Zauberer Des Semantica Software Engineer 2. Älterer Semantica Sof tware-Englisch-3. Graph/Semantica Netz-Visualization/Navigation Sie ei ngestufte Software-Entwicklung für die Regierung. Die Firma ist stark und die Projekte sind sehr kühl und schließen irgendeinen Spielraum ein. Wenn ich Ihnen irgendwie mehr erkläre\, muß ich Sie töten. Ps . Tat schnell -- jemand ist\, wenn es hier interviewt\, wie ich dieses schreibe. Er schaut intelligent (er trägt Kleidhosen) Semantica Soft ware Engineer FIRMA: Semantische Forschung\, Inc. REPORTS ZU: Vizeprä sident\, Produkt-Entwicklung POSITION: San Diego (Pint Loma) WEB SITE: www.semanticresearch.com email: dorie@semanticresearch.com FIRMA-HINT ERGRUND Semantische Forschung ist der führende Versorger der semantis cher Netzwerkanschluß gegründeten nicht linearen Wissen Darstellung Werkzeuge. Die Firma stellt diese Werkzeuge zum Intel\, zur reg.\, zum EDU und zu den kommerziellen Märkten zur Verfügung. BRINGEN SIE ZUS AMMENFASSUNG IN POSITION Semantische Forschung\, Inc. basiert in San D iego\, Ca im alten realen Weltsan Diego Haus...\, das wir den Weltbest en Platz haben zum zu arbeiten. Wir suchen nach Superstarentwicklern\, um uns in der fortfahrenden Entwicklung unserer Semantica Produktseri e zu unterstützen. LOCATION:こんにちはキティ SEQUENCE:0 UID:E0-001-000276068-2 END:VEVENT END:VCALENDAR vobject-0.8.1c/test_vobject.py0000644000076500001200000005741311126240452015636 0ustar chandleadmin"""Long or boring tests for vobjects.""" import vobject from vobject import base, icalendar, behavior, vcard, hcalendar import StringIO, re, dateutil.tz, datetime import doctest, test_vobject, unittest from pkg_resources import resource_stream base.logger.setLevel(base.logging.FATAL) #------------------- Testing and running functions ----------------------------- # named additional_tests for setuptools def additional_tests(): flags = doctest.NORMALIZE_WHITESPACE | doctest.REPORT_ONLY_FIRST_FAILURE | doctest.ELLIPSIS suite = unittest.TestSuite() for module in base, test_vobject, icalendar, vobject, vcard: suite.addTest(doctest.DocTestSuite(module, optionflags=flags)) suite.addTest(doctest.DocFileSuite( 'README.txt', 'test_files/more_tests.txt', package='__main__', optionflags=flags )) return suite if __name__ == '__main__': runner = unittest.TextTestRunner() runner.run(additional_tests()) testSilly=""" sillyname:name profile:sillyprofile stuff:folded line """ + "morestuff;asinine:this line is not folded, \ but in practice probably ought to be, as it is exceptionally long, \ and moreover demonstratively stupid" icaltest=r"""BEGIN:VCALENDAR CALSCALE:GREGORIAN X-WR-TIMEZONE;VALUE=TEXT:US/Pacific METHOD:PUBLISH PRODID:-//Apple Computer\, Inc//iCal 1.0//EN X-WR-CALNAME;VALUE=TEXT:Example VERSION:2.0 BEGIN:VEVENT SEQUENCE:5 DTSTART;TZID=US/Pacific:20021028T140000 RRULE:FREQ=Weekly;COUNT=10 DTSTAMP:20021028T011706Z SUMMARY:Coffee with Jason UID:EC9439B1-FF65-11D6-9973-003065F99D04 DTEND;TZID=US/Pacific:20021028T150000 BEGIN:VALARM TRIGGER;VALUE=DURATION:-P1D ACTION:DISPLAY DESCRIPTION:Event reminder\, with comma\nand line feed END:VALARM END:VEVENT BEGIN:VTIMEZONE X-LIC-LOCATION:Random location TZID:US/Pacific LAST-MODIFIED:19870101T000000Z BEGIN:STANDARD DTSTART:19671029T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 TZOFFSETFROM:-0700 TZOFFSETTO:-0800 TZNAME:PST END:STANDARD BEGIN:DAYLIGHT DTSTART:19870405T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 TZOFFSETFROM:-0800 TZOFFSETTO:-0700 TZNAME:PDT END:DAYLIGHT END:VTIMEZONE END:VCALENDAR""" badDtStartTest="""BEGIN:VCALENDAR METHOD:PUBLISH VERSION:2.0 BEGIN:VEVENT DTSTART:20021028 DTSTAMP:20021028T011706Z SUMMARY:Coffee with Jason UID:EC9439B1-FF65-11D6-9973-003065F99D04 END:VEVENT END:VCALENDAR""" badLineTest="""BEGIN:VCALENDAR METHOD:PUBLISH VERSION:2.0 BEGIN:VEVENT DTSTART:19870405T020000 X-BAD/SLASH:TRUE X-BAD_UNDERSCORE:TRUE UID:EC9439B1-FF65-11D6-9973-003065F99D04 END:VEVENT END:VCALENDAR""" vcardtest =r"""BEGIN:VCARD VERSION:3.0 FN:Daffy Duck Knudson (with Bugs Bunny and Mr. Pluto) N:Knudson;Daffy Duck (with Bugs Bunny and Mr. Pluto) NICKNAME:gnat and gnu and pluto BDAY;value=date:02-10 TEL;type=HOME:+01-(0)2-765.43.21 TEL;type=CELL:+01-(0)5-555.55.55 ACCOUNT;type=HOME:010-1234567-05 ADR;type=HOME:;;Haight Street 512\;\nEscape\, Test;Novosibirsk;;80214;Gnuland TEL;type=HOME:+01-(0)2-876.54.32 ORG:University of Novosibirsk\, Department of Octopus Parthenogenesis END:VCARD""" vcardWithGroups = r"""home.begin:vcard version:3.0 source:ldap://cn=Meister%20Berger,o=Universitaet%20Goerlitz,c=DE name:Meister Berger fn:Meister Berger n:Berger;Meister bday;value=date:1963-09-21 o:Universit=E6t G=F6rlitz title:Mayor title;language=de;value=text:Burgermeister note:The Mayor of the great city of Goerlitz in the great country of Germany.\nNext line. email;internet:mb@goerlitz.de home.tel;type=fax,voice;type=msg:+49 3581 123456 home.label:Hufenshlagel 1234\n 02828 Goerlitz\n Deutschland END:VCARD""" lowercaseComponentNames = r"""begin:vcard fn:Anders Bobo n:Bobo;Anders org:Bobo A/S;Vice President, Technical Support adr:Rockfeller Center;;Mekastreet;Bobocity;;2100;Myworld email;internet:bobo@example.com tel;work:+123455 tel;fax:+123456 tel;cell:+123457 x-mozilla-html:FALSE url:http://www.example.com version:2.1 end:vcard""" icalWeirdTrigger = r"""BEGIN:VCALENDAR CALSCALE:GREGORIAN X-WR-TIMEZONE;VALUE=TEXT:US/Pacific METHOD:PUBLISH PRODID:-//Apple Computer\, Inc//iCal 1.0//EN X-WR-CALNAME;VALUE=TEXT:Example VERSION:2.0 BEGIN:VEVENT DTSTART:20021028T140000Z BEGIN:VALARM TRIGGER:20021028T120000Z ACTION:DISPLAY DESCRIPTION:This trigger is a date-time without a VALUE=DATE-TIME parameter END:VALARM END:VEVENT END:VCALENDAR""" badstream = r"""BEGIN:VCALENDAR CALSCALE:GREGORIAN X-WR-TIMEZONE;VALUE=TEXT:US/Pacific METHOD:PUBLISH PRODID:-//Apple Computer\, Inc//iCal 1.0//EN X-WR-CALNAME;VALUE=TEXT:Example VERSION:2.0 BEGIN:VEVENT DTSTART:20021028T140000Z BEGIN:VALARM TRIGGER:a20021028120000 ACTION:DISPLAY DESCRIPTION:This trigger has a nonsensical value END:VALARM END:VEVENT END:VCALENDAR""" timezones = r""" BEGIN:VTIMEZONE TZID:US/Pacific BEGIN:STANDARD DTSTART:19671029T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 TZOFFSETFROM:-0700 TZOFFSETTO:-0800 TZNAME:PST END:STANDARD BEGIN:DAYLIGHT DTSTART:19870405T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 TZOFFSETFROM:-0800 TZOFFSETTO:-0700 TZNAME:PDT END:DAYLIGHT END:VTIMEZONE BEGIN:VTIMEZONE TZID:US/Eastern BEGIN:STANDARD DTSTART:19671029T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:EST END:STANDARD BEGIN:DAYLIGHT DTSTART:19870405T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT END:VTIMEZONE BEGIN:VTIMEZONE TZID:Santiago BEGIN:STANDARD DTSTART:19700314T000000 TZOFFSETFROM:-0300 TZOFFSETTO:-0400 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SA TZNAME:Pacific SA Standard Time END:STANDARD BEGIN:DAYLIGHT DTSTART:19701010T000000 TZOFFSETFROM:-0400 TZOFFSETTO:-0300 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=2SA TZNAME:Pacific SA Daylight Time END:DAYLIGHT END:VTIMEZONE BEGIN:VTIMEZONE TZID:W. Europe BEGIN:STANDARD DTSTART:19701025T030000 TZOFFSETFROM:+0200 TZOFFSETTO:+0100 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU TZNAME:W. Europe Standard Time END:STANDARD BEGIN:DAYLIGHT DTSTART:19700329T020000 TZOFFSETFROM:+0100 TZOFFSETTO:+0200 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU TZNAME:W. Europe Daylight Time END:DAYLIGHT END:VTIMEZONE BEGIN:VTIMEZONE TZID:US/Fictitious-Eastern LAST-MODIFIED:19870101T000000Z BEGIN:STANDARD DTSTART:19671029T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:EST END:STANDARD BEGIN:DAYLIGHT DTSTART:19870405T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20050403T070000Z TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT END:VTIMEZONE BEGIN:VTIMEZONE TZID:America/Montreal LAST-MODIFIED:20051013T233643Z BEGIN:DAYLIGHT DTSTART:20050403T070000 TZOFFSETTO:-0400 TZOFFSETFROM:+0000 TZNAME:EDT END:DAYLIGHT BEGIN:STANDARD DTSTART:20051030T020000 TZOFFSETTO:-0500 TZOFFSETFROM:-0400 TZNAME:EST END:STANDARD END:VTIMEZONE """ __test__ = { "Test readOne" : r""" >>> silly = base.readOne(testSilly, findBegin=False) >>> silly , , ]> >>> silly.stuff >>> original = silly.serialize() >>> f3 = StringIO.StringIO(original.decode("utf-8")) >>> silly2 = base.readOne(f3) >>> silly2.serialize()==original True >>> s3 = StringIO.StringIO('cn:Babs Jensen\r\ncn:Barbara J Jensen\r\nsn:Jensen\r\nemail:babs@umich.edu\r\nphone:+1 313 747-4454\r\nx-id:1234567890\r\n') >>> ex1 = base.readOne(s3, findBegin=False) >>> ex1 <*unnamed*| [, , , , , ]> >>> ex1.serialize() 'CN:Babs Jensen\r\nCN:Barbara J Jensen\r\nEMAIL:babs@umich.edu\r\nPHONE:+1 313 747-4454\r\nSN:Jensen\r\nX-ID:1234567890\r\n' """, "Import icaltest" : r""" >>> c = base.readOne(icaltest, validate=True) >>> c.vevent.valarm.trigger >>> c.vevent.dtstart.value datetime.datetime(2002, 10, 28, 14, 0, tzinfo=) >>> c.vevent.dtend.value datetime.datetime(2002, 10, 28, 15, 0, tzinfo=) >>> c.vevent.dtstamp.value datetime.datetime(2002, 10, 28, 1, 17, 6, tzinfo=tzutc()) >>> c.vevent.valarm.description.value u'Event reminder, with comma\nand line feed' >>> c.vevent.valarm.description.serialize() 'DESCRIPTION:Event reminder\\, with comma\\nand line feed\r\n' >>> vevent = c.vevent.transformFromNative() >>> vevent.rrule """, "Parsing tests" : """ >>> parseRDate = icalendar.MultiDateBehavior.transformToNative >>> icalendar.stringToTextValues('') [''] >>> icalendar.stringToTextValues('abcd,efgh') ['abcd', 'efgh'] >>> icalendar.stringToPeriod("19970101T180000Z/19970102T070000Z") (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()), datetime.datetime(1997, 1, 2, 7, 0, tzinfo=tzutc())) >>> icalendar.stringToPeriod("19970101T180000Z/PT1H") (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()), datetime.timedelta(0, 3600)) >>> parseRDate(base.textLineToContentLine("RDATE;VALUE=DATE:19970304,19970504,19970704,19970904")) >>> parseRDate(base.textLineToContentLine("RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H")) """, "read failure" : """ >>> vevent = base.readOne(badstream) Traceback (most recent call last): ... ParseError: At line 11: TRIGGER with no VALUE not recognized as DURATION or as DATE-TIME >>> cal = base.readOne(badLineTest) Traceback (most recent call last): ... ParseError: At line 6: Failed to parse line: X-BAD/SLASH:TRUE >>> cal = base.readOne(badLineTest, ignoreUnreadable=True) >>> cal.vevent.x_bad_slash Traceback (most recent call last): ... AttributeError: x_bad_slash >>> cal.vevent.x_bad_underscore """, "ical trigger workaround" : """ >>> badical = base.readOne(icalWeirdTrigger) >>> badical.vevent.valarm.description.value u'This trigger is a date-time without a VALUE=DATE-TIME parameter' >>> badical.vevent.valarm.trigger.value datetime.datetime(2002, 10, 28, 12, 0, tzinfo=tzutc()) """, "unicode test" : r""" >>> f = resource_stream(__name__, 'test_files/utf8_test.ics') >>> vevent = base.readOne(f).vevent >>> vevent.summary.value u'The title \u3053\u3093\u306b\u3061\u306f\u30ad\u30c6\u30a3' >>> summary = vevent.summary.value >>> test = str(vevent.serialize()), """, # make sure date valued UNTILs in rrules are in a reasonable timezone, # and include that day (12/28 in this test) "recurrence test" : r""" >>> f = resource_stream(__name__, 'test_files/recurrence.ics') >>> cal = base.readOne(f) >>> dates = list(cal.vevent.rruleset) >>> dates[0] datetime.datetime(2006, 1, 26, 23, 0, tzinfo=tzutc()) >>> dates[1] datetime.datetime(2006, 2, 23, 23, 0, tzinfo=tzutc()) >>> dates[-1] datetime.datetime(2006, 12, 28, 23, 0, tzinfo=tzutc()) """, "regular expression test" : """ >>> re.findall(base.patterns['name'], '12foo-bar:yay') ['12foo-bar', 'yay'] >>> re.findall(base.patterns['safe_char'], 'a;b"*,cd') ['a', 'b', '*', 'c', 'd'] >>> re.findall(base.patterns['qsafe_char'], 'a;b"*,cd') ['a', ';', 'b', '*', ',', 'c', 'd'] >>> re.findall(base.patterns['param_value'], '"quoted";not-quoted;start"after-illegal-quote', re.VERBOSE) ['"quoted"', '', 'not-quoted', '', 'start', '', 'after-illegal-quote', ''] >>> match = base.line_re.match('TEST;ALTREP="http://www.wiz.org":value:;"') >>> match.group('value') 'value:;"' >>> match.group('name') 'TEST' >>> match.group('params') ';ALTREP="http://www.wiz.org"' """, "VTIMEZONE creation test:" : """ >>> f = StringIO.StringIO(timezones) >>> tzs = dateutil.tz.tzical(f) >>> tzs.get("US/Pacific") >>> icalendar.TimezoneComponent(_) > >>> pacific = _ >>> print pacific.serialize() BEGIN:VTIMEZONE TZID:US/Pacific BEGIN:STANDARD DTSTART:20001029T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:20000402T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT END:VTIMEZONE >>> (_) > >>> santiago = icalendar.TimezoneComponent(tzs.get('Santiago')) >>> ser = santiago.serialize() >>> print ser BEGIN:VTIMEZONE TZID:Santiago BEGIN:STANDARD DTSTART:20000311T000000 RRULE:FREQ=YEARLY;BYDAY=2SA;BYMONTH=3 TZNAME:Pacific SA Standard Time TZOFFSETFROM:-0300 TZOFFSETTO:-0400 END:STANDARD BEGIN:DAYLIGHT DTSTART:20001014T000000 RRULE:FREQ=YEARLY;BYDAY=2SA;BYMONTH=10 TZNAME:Pacific SA Daylight Time TZOFFSETFROM:-0400 TZOFFSETTO:-0300 END:DAYLIGHT END:VTIMEZONE >>> roundtrip = dateutil.tz.tzical(StringIO.StringIO(str(ser))).get() >>> for year in range(2001, 2010): ... for month in (2, 9): ... dt = datetime.datetime(year, month, 15, tzinfo = roundtrip) ... if dt.replace(tzinfo=tzs.get('Santiago')) != dt: ... print "Failed for:", dt >>> fict = icalendar.TimezoneComponent(tzs.get('US/Fictitious-Eastern')) >>> print fict.serialize() BEGIN:VTIMEZONE TZID:US/Fictitious-Eastern BEGIN:STANDARD DTSTART:20001029T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:20000402T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20050403T070000Z TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT END:VTIMEZONE """, "Create iCalendar from scratch" : """ >>> cal = base.newFromBehavior('vcalendar', '2.0') >>> cal.add('vevent') >>> cal.vevent.add('dtstart').value = datetime.datetime(2006, 5, 9) >>> cal.vevent.add('description').value = "Test event" >>> pacific = dateutil.tz.tzical(StringIO.StringIO(timezones)).get('US/Pacific') >>> cal.vevent.add('created').value = datetime.datetime(2006, 1, 1, 10, tzinfo=pacific) >>> cal.vevent.add('uid').value = "Not very random UID" >>> print cal.serialize() BEGIN:VCALENDAR VERSION:2.0 PRODID:-//PYVOBJECT//NONSGML Version 1//EN BEGIN:VEVENT UID:Not very random UID DTSTART:20060509T000000 CREATED:20060101T180000Z DESCRIPTION:Test event END:VEVENT END:VCALENDAR """, "Serializing with timezones test" : """ >>> from dateutil.rrule import rrule, rruleset, WEEKLY, MONTHLY >>> pacific = dateutil.tz.tzical(StringIO.StringIO(timezones)).get('US/Pacific') >>> cal = base.Component('VCALENDAR') >>> cal.setBehavior(icalendar.VCalendar2_0) >>> ev = cal.add('vevent') >>> ev.add('dtstart').value = datetime.datetime(2005, 10, 12, 9, tzinfo = pacific) >>> set = rruleset() >>> set.rrule(rrule(WEEKLY, interval=2, byweekday=[2,4], until=datetime.datetime(2005, 12, 15, 9))) >>> set.rrule(rrule(MONTHLY, bymonthday=[-1,-5])) >>> set.exdate(datetime.datetime(2005, 10, 14, 9, tzinfo = pacific)) >>> ev.rruleset = set >>> ev.add('duration').value = datetime.timedelta(hours=1) >>> print cal.serialize() BEGIN:VCALENDAR VERSION:2.0 PRODID:-//PYVOBJECT//NONSGML Version 1//EN BEGIN:VTIMEZONE TZID:US/Pacific BEGIN:STANDARD DTSTART:20001029T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:20000402T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT UID:... DTSTART;TZID=US/Pacific:20051012T090000 DURATION:PT1H EXDATE;TZID=US/Pacific:20051014T090000 RRULE:FREQ=WEEKLY;BYDAY=WE,FR;INTERVAL=2;UNTIL=20051215T090000 RRULE:FREQ=MONTHLY;BYMONTHDAY=-1,-5 END:VEVENT END:VCALENDAR >>> apple = dateutil.tz.tzical(StringIO.StringIO(timezones)).get('America/Montreal') >>> ev.dtstart.value = datetime.datetime(2005, 10, 12, 9, tzinfo = apple) >>> print cal.serialize() BEGIN:VCALENDAR VERSION:2.0 PRODID:-//PYVOBJECT//NONSGML Version 1//EN BEGIN:VTIMEZONE TZID:US/Pacific BEGIN:STANDARD DTSTART:20001029T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:20000402T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT END:VTIMEZONE BEGIN:VTIMEZONE TZID:America/Montreal BEGIN:STANDARD DTSTART:20000101T000000 RRULE:FREQ=YEARLY;BYMONTH=1;UNTIL=20040101T050000Z TZNAME:EST TZOFFSETFROM:-0500 TZOFFSETTO:-0500 END:STANDARD BEGIN:STANDARD DTSTART:20051030T020000 RRULE:FREQ=YEARLY;BYDAY=5SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:20050403T070000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20050403T120000Z TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT UID:... DTSTART;TZID=America/Montreal:20051012T090000 DURATION:PT1H EXDATE;TZID=US/Pacific:20051014T090000 RRULE:FREQ=WEEKLY;BYDAY=WE,FR;INTERVAL=2;UNTIL=20051215T090000 RRULE:FREQ=MONTHLY;BYMONTHDAY=-1,-5 END:VEVENT END:VCALENDAR """, "Handling DATE without a VALUE=DATE" : """ >>> cal = base.readOne(badDtStartTest) >>> cal.vevent.dtstart.value datetime.date(2002, 10, 28) """, "Serializing iCalendar to hCalendar" : """ >>> cal = base.newFromBehavior('hcalendar') >>> cal.behavior >>> pacific = dateutil.tz.tzical(StringIO.StringIO(timezones)).get('US/Pacific') >>> cal.add('vevent') >>> cal.vevent.add('summary').value = "this is a note" >>> cal.vevent.add('url').value = "http://microformats.org/code/hcalendar/creator" >>> cal.vevent.add('dtstart').value = datetime.date(2006,2,27) >>> cal.vevent.add('location').value = "a place" >>> cal.vevent.add('dtend').value = datetime.date(2006,2,27) + datetime.timedelta(days = 2) >>> event2 = cal.add('vevent') >>> event2.add('summary').value = "Another one" >>> event2.add('description').value = "The greatest thing ever!" >>> event2.add('dtstart').value = datetime.datetime(1998, 12, 17, 16, 42, tzinfo = pacific) >>> event2.add('location').value = "somewhere else" >>> event2.add('dtend').value = event2.dtstart.value + datetime.timedelta(days = 6) >>> hcal = cal.serialize() >>> print hcal this is a note: Monday, February 27 - Tuesday, February 28 at a place Another one: Thursday, December 17, 16:42 - Wednesday, December 23, 16:42 at somewhere else
The greatest thing ever!
""", "Generate UIDs automatically test:" : """ >>> cal = base.newFromBehavior('vcalendar') >>> cal.add('vevent').add('dtstart').value = datetime.datetime(2006,2,2,10) >>> ser = cal.serialize() >>> len(cal.vevent.uid_list) 1 """, "VCARD 3.0 parse test:" : r""" >>> card = base.readOne(vcardtest) >>> card.adr.value >>> print card.adr.value Haight Street 512; Escape, Test Novosibirsk, 80214 Gnuland >>> card.org.value [u'University of Novosibirsk, Department of Octopus Parthenogenesis'] >>> print card.serialize() BEGIN:VCARD VERSION:3.0 ACCOUNT;TYPE=HOME:010-1234567-05 ADR;TYPE=HOME:;;Haight Street 512\;\nEscape\, Test;Novosibirsk;;80214;Gnul and BDAY;VALUE=date:02-10 FN:Daffy Duck Knudson (with Bugs Bunny and Mr. Pluto) N:Knudson;Daffy Duck (with Bugs Bunny and Mr. Pluto);;; NICKNAME:gnat and gnu and pluto ORG:University of Novosibirsk\, Department of Octopus Parthenogenesis TEL;TYPE=HOME:+01-(0)2-765.43.21 TEL;TYPE=CELL:+01-(0)5-555.55.55 TEL;TYPE=HOME:+01-(0)2-876.54.32 END:VCARD """, "Multi-text serialization test:" : """ >>> category = base.newFromBehavior('categories') >>> category.value = ['Random category'] >>> print category.serialize().strip() CATEGORIES:Random category >>> category.value.append('Other category') >>> print category.serialize().strip() CATEGORIES:Random category,Other category """, "Semi-colon separated multi-text serialization test:" : """ >>> requestStatus = base.newFromBehavior('request-status') >>> requestStatus.value = ['5.1', 'Service unavailable'] >>> print requestStatus.serialize().strip() REQUEST-STATUS:5.1;Service unavailable """, "vCard groups test:" : """ >>> card = base.readOne(vcardWithGroups) >>> card.group u'home' >>> card.tel.group u'home' >>> card.group = card.tel.group = 'new' >>> card.tel.serialize().strip() 'new.TEL;TYPE=fax,voice,msg:+49 3581 123456' >>> card.serialize().splitlines()[0] 'new.BEGIN:VCARD' >>> dtstart = base.newFromBehavior('dtstart') >>> dtstart.group = "badgroup" >>> dtstart.serialize() Traceback (most recent call last): ... VObjectError: " has a group, but this object doesn't support groups" """, "Lowercase components test:" : """ >>> card = base.readOne(lowercaseComponentNames) >>> card.version """, "Default behavior test" : """ >>> card = base.readOne(vcardWithGroups) >>> base.getBehavior('note') == None True >>> card.note.behavior >>> print card.note.value The Mayor of the great city of Goerlitz in the great country of Germany. Next line. """ } vobject-0.8.1c/vobject/0000755000076500001200000000000011151624231014212 5ustar chandleadminvobject-0.8.1c/vobject/__init__.py0000644000076500001200000000623611017337652016343 0ustar chandleadmin""" 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 """ import base, icalendar, vcard from base import readComponents, readOne, newFromBehavior def iCalendar(): return newFromBehavior('vcalendar', '2.0') def vCard(): return newFromBehavior('vcard', '3.0')vobject-0.8.1c/vobject/base.py0000644000076500001200000012364711132767303015522 0ustar chandleadmin"""vobject module for reading vCard and vCalendar files.""" import copy import re import sys import logging import StringIO, cStringIO import string import exceptions import codecs #------------------------------------ Logging ---------------------------------- logger = logging.getLogger(__name__) if not logging.getLogger().handlers: handler = logging.StreamHandler() formatter = logging.Formatter('%(name)s %(levelname)s %(message)s') handler.setFormatter(formatter) logger.addHandler(handler) logger.setLevel(logging.ERROR) # Log errors DEBUG = False # Don't waste time on debug calls #----------------------------------- Constants --------------------------------- CR = '\r' LF = '\n' CRLF = CR + LF SPACE = ' ' TAB = '\t' SPACEORTAB = SPACE + TAB #-------------------------------- Useful modules ------------------------------- # use doctest, it kills two birds with one stone and docstrings often become # more readable to boot (see parseLine's docstring). # use logging, then when debugging we can just set our verbosity. # use epydoc syntax for documenting code, please document every class and non- # trivial method (see http://epydoc.sourceforge.net/epytext.html # and http://epydoc.sourceforge.net/fields.html). Also, please # follow http://www.python.org/peps/pep-0257.html for docstrings. #------------------------------------------------------------------------------- #--------------------------------- Main classes -------------------------------- class VBase(object): """Base class for ContentLine and Component. @ivar behavior: The Behavior class associated with this object, which controls validation, transformations, and encoding. @ivar parentBehavior: The object's parent's behavior, or None if no behaviored parent exists. @ivar isNative: Boolean describing whether this component is a Native instance. @ivar group: An optional group prefix, should be used only to indicate sort order in vCards, according to RFC2426 """ def __init__(self, group=None, *args, **kwds): super(VBase, self).__init__(*args, **kwds) self.group = group self.behavior = None self.parentBehavior = None self.isNative = False def copy(self, copyit): self.group = copyit.group self.behavior = copyit.behavior self.parentBehavior = copyit.parentBehavior self.isNative = copyit.isNative def validate(self, *args, **kwds): """Call the behavior's validate method, or return True.""" if self.behavior: return self.behavior.validate(self, *args, **kwds) else: return True def getChildren(self): """Return an iterable containing the contents of the object.""" return [] def clearBehavior(self, cascade=True): """Set behavior to None. Do for all descendants if cascading.""" self.behavior=None if cascade: self.transformChildrenFromNative() def autoBehavior(self, cascade=False): """Set behavior if name is in self.parentBehavior.knownChildren. If cascade is True, unset behavior and parentBehavior for all descendants, then recalculate behavior and parentBehavior. """ parentBehavior = self.parentBehavior if parentBehavior is not None: knownChildTup = parentBehavior.knownChildren.get(self.name, None) if knownChildTup is not None: behavior = getBehavior(self.name, knownChildTup[2]) if behavior is not None: self.setBehavior(behavior, cascade) if isinstance(self, ContentLine) and self.encoded: self.behavior.decode(self) elif isinstance(self, ContentLine): self.behavior = parentBehavior.defaultBehavior if self.encoded and self.behavior: self.behavior.decode(self) def setBehavior(self, behavior, cascade=True): """Set behavior. If cascade is True, autoBehavior all descendants.""" self.behavior=behavior if cascade: for obj in self.getChildren(): obj.parentBehavior=behavior obj.autoBehavior(True) def transformToNative(self): """Transform this object into a custom VBase subclass. transformToNative should always return a representation of this object. It may do so by modifying self in place then returning self, or by creating a new object. """ if self.isNative or not self.behavior or not self.behavior.hasNative: return self else: try: return self.behavior.transformToNative(self) except Exception, e: # wrap errors in transformation in a ParseError lineNumber = getattr(self, 'lineNumber', None) if isinstance(e, ParseError): if lineNumber is not None: e.lineNumber = lineNumber raise else: msg = "In transformToNative, unhandled exception: %s: %s" msg = msg % (sys.exc_info()[0], sys.exc_info()[1]) new_error = ParseError(msg, lineNumber) raise ParseError, new_error, sys.exc_info()[2] def transformFromNative(self): """Return self transformed into a ContentLine or Component if needed. May have side effects. If it does, transformFromNative and transformToNative MUST have perfectly inverse side effects. Allowing such side effects is convenient for objects whose transformations only change a few attributes. Note that it isn't always possible for transformFromNative to be a perfect inverse of transformToNative, in such cases transformFromNative should return a new object, not self after modifications. """ if self.isNative and self.behavior and self.behavior.hasNative: try: return self.behavior.transformFromNative(self) except Exception, e: # wrap errors in transformation in a NativeError lineNumber = getattr(self, 'lineNumber', None) if isinstance(e, NativeError): if lineNumber is not None: e.lineNumber = lineNumber raise else: msg = "In transformFromNative, unhandled exception: %s: %s" msg = msg % (sys.exc_info()[0], sys.exc_info()[1]) new_error = NativeError(msg, lineNumber) raise NativeError, new_error, sys.exc_info()[2] else: return self def transformChildrenToNative(self): """Recursively replace children with their native representation.""" pass def transformChildrenFromNative(self, clearBehavior=True): """Recursively transform native children to vanilla representations.""" pass def serialize(self, buf=None, lineLength=75, validate=True, behavior=None): """Serialize to buf if it exists, otherwise return a string. Use self.behavior.serialize if behavior exists. """ if not behavior: behavior = self.behavior if behavior: if DEBUG: logger.debug("serializing %s with behavior" % self.name) return behavior.serialize(self, buf, lineLength, validate) else: if DEBUG: logger.debug("serializing %s without behavior" % self.name) return defaultSerialize(self, buf, lineLength) def ascii(s): """Turn s into a printable string. Won't work for 8-bit ASCII.""" return unicode(s).encode('ascii', 'replace') def toVName(name, stripNum = 0, upper = False): """ Turn a Python name into an iCalendar style name, optionally uppercase and with characters stripped off. """ if upper: name = name.upper() if stripNum != 0: name = name[:-stripNum] return name.replace('_', '-') class ContentLine(VBase): """Holds one content line for formats like vCard and vCalendar. For example:: @ivar name: The uppercased name of the contentline. @ivar params: A dictionary of parameters and associated lists of values (the list may be empty for empty parameters). @ivar value: The value of the contentline. @ivar singletonparams: A list of parameters for which it's unclear if the string represents the parameter name or the parameter value. In vCard 2.1, "The value string can be specified alone in those cases where the value is unambiguous". This is crazy, but we have to deal with it. @ivar encoded: A boolean describing whether the data in the content line is encoded. Generally, text read from a serialized vCard or vCalendar should be considered encoded. Data added programmatically should not be encoded. @ivar lineNumber: An optional line number associated with the contentline. """ def __init__(self, name, params, value, group=None, encoded=False, isNative=False, lineNumber = None, *args, **kwds): """Take output from parseLine, convert params list to dictionary.""" # group is used as a positional argument to match parseLine's return super(ContentLine, self).__init__(group, *args, **kwds) self.name = name.upper() self.value = value self.encoded = encoded self.params = {} self.singletonparams = [] self.isNative = isNative self.lineNumber = lineNumber def updateTable(x): if len(x) == 1: self.singletonparams += x else: paramlist = self.params.setdefault(x[0].upper(), []) paramlist.extend(x[1:]) map(updateTable, params) qp = False if 'ENCODING' in self.params: if 'QUOTED-PRINTABLE' in self.params['ENCODING']: qp = True self.params['ENCODING'].remove('QUOTED-PRINTABLE') if 0==len(self.params['ENCODING']): del self.params['ENCODING'] if 'QUOTED-PRINTABLE' in self.singletonparams: qp = True self.singletonparams.remove('QUOTED-PRINTABLE') if qp: self.value = str(self.value).decode('quoted-printable') # self.value should be unicode for iCalendar, but if quoted-printable # is used, or if the quoted-printable state machine is used, text may be # encoded if type(self.value) is str: charset = 'iso-8859-1' if 'CHARSET' in self.params: charsets = self.params.pop('CHARSET') if charsets: charset = charsets[0] self.value = unicode(self.value, charset) @classmethod def duplicate(clz, copyit): newcopy = clz('', {}, '') newcopy.copy(copyit) return newcopy def copy(self, copyit): super(ContentLine, self).copy(copyit) self.name = copyit.name self.value = copy.copy(copyit.value) self.encoded = self.encoded self.params = copy.copy(copyit.params) self.singletonparams = copy.copy(copyit.singletonparams) self.lineNumber = copyit.lineNumber def __eq__(self, other): try: return (self.name == other.name) and (self.params == other.params) and (self.value == other.value) except: return False def _getAttributeNames(self): """Return a list of attributes of the object. Python 2.6 will add __dir__ to customize what attributes are returned by dir, for now copy PyCrust so that IPython can accurately do completion. """ keys = self.params.keys() params = [param + '_param' for param in keys] params.extend(param + '_paramlist' for param in keys) return params def __getattr__(self, name): """Make params accessible via self.foo_param or self.foo_paramlist. Underscores, legal in python variable names, are converted to dashes, which are legal in IANA tokens. """ try: if name.endswith('_param'): return self.params[toVName(name, 6, True)][0] elif name.endswith('_paramlist'): return self.params[toVName(name, 10, True)] else: raise exceptions.AttributeError, name except KeyError: raise exceptions.AttributeError, name def __setattr__(self, name, value): """Make params accessible via self.foo_param or self.foo_paramlist. Underscores, legal in python variable names, are converted to dashes, which are legal in IANA tokens. """ if name.endswith('_param'): if type(value) == list: self.params[toVName(name, 6, True)] = value else: self.params[toVName(name, 6, True)] = [value] elif name.endswith('_paramlist'): if type(value) == list: self.params[toVName(name, 10, True)] = value else: raise VObjectError("Parameter list set to a non-list") else: prop = getattr(self.__class__, name, None) if isinstance(prop, property): prop.fset(self, value) else: object.__setattr__(self, name, value) def __delattr__(self, name): try: if name.endswith('_param'): del self.params[toVName(name, 6, True)] elif name.endswith('_paramlist'): del self.params[toVName(name, 10, True)] else: object.__delattr__(self, name) except KeyError: raise exceptions.AttributeError, name def valueRepr( self ): """transform the representation of the value according to the behavior, if any""" v = self.value if self.behavior: v = self.behavior.valueRepr( self ) return ascii( v ) def __str__(self): return "<"+ascii(self.name)+ascii(self.params)+self.valueRepr()+">" def __repr__(self): return self.__str__().replace('\n', '\\n') def prettyPrint(self, level = 0, tabwidth=3): pre = ' ' * level * tabwidth print pre, self.name + ":", self.valueRepr() if self.params: lineKeys= self.params.keys() print pre, "params for ", self.name +':' for aKey in lineKeys: print pre + ' ' * tabwidth, aKey, ascii(self.params[aKey]) class Component(VBase): """A complex property that can contain multiple ContentLines. For our purposes, a component must start with a BEGIN:xxxx line and end with END:xxxx, or have a PROFILE:xxx line if a top-level component. @ivar contents: A dictionary of lists of Component or ContentLine instances. The keys are the lowercased names of child ContentLines or Components. Note that BEGIN and END ContentLines are not included in contents. @ivar name: Uppercase string used to represent this Component, i.e VCARD if the serialized object starts with BEGIN:VCARD. @ivar useBegin: A boolean flag determining whether BEGIN: and END: lines should be serialized. """ def __init__(self, name=None, *args, **kwds): super(Component, self).__init__(*args, **kwds) self.contents = {} if name: self.name=name.upper() self.useBegin = True else: self.name = '' self.useBegin = False self.autoBehavior() @classmethod def duplicate(clz, copyit): newcopy = clz() newcopy.copy(copyit) return newcopy def copy(self, copyit): super(Component, self).copy(copyit) # deep copy of contents self.contents = {} for key, lvalue in copyit.contents.items(): newvalue = [] for value in lvalue: newitem = value.duplicate(value) newvalue.append(newitem) self.contents[key] = newvalue self.name = copyit.name self.useBegin = copyit.useBegin def setProfile(self, name): """Assign a PROFILE to this unnamed component. Used by vCard, not by vCalendar. """ if self.name or self.useBegin: if self.name == name: return raise VObjectError("This component already has a PROFILE or uses BEGIN.") self.name = name.upper() def _getAttributeNames(self): """Return a list of attributes of the object. Python 2.6 will add __dir__ to customize what attributes are returned by dir, for now copy PyCrust so that IPython can accurately do completion. """ names = self.contents.keys() names.extend(name + '_list' for name in self.contents.keys()) return names 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 exceptions.AttributeError, name normal_attributes = ['contents','name','behavior','parentBehavior','group'] def __setattr__(self, name, value): """For convenience, make self.contents directly accessible. Underscores, legal in python variable names, are converted to dashes, which are legal in IANA tokens. """ if name not in self.normal_attributes and name.lower()==name: if type(value) == list: if name.endswith('_list'): name = name[:-5] self.contents[toVName(name)] = value elif name.endswith('_list'): raise VObjectError("Component list set to a non-list") else: self.contents[toVName(name)] = [value] else: prop = getattr(self.__class__, name, None) if isinstance(prop, property): prop.fset(self, value) else: object.__setattr__(self, name, value) def __delattr__(self, name): try: if name not in self.normal_attributes and name.lower()==name: if name.endswith('_list'): del self.contents[toVName(name, 5)] else: del self.contents[toVName(name)] else: object.__delattr__(self, name) except KeyError: raise exceptions.AttributeError, name def getChildValue(self, childName, default = None, childNumber = 0): """Return a child's value (the first, by default), or None.""" child = self.contents.get(toVName(childName)) if child is None: return default else: return child[childNumber].value def add(self, objOrName, group = None): """Add objOrName to contents, set behavior if it can be inferred. If objOrName is a string, create an empty component or line based on behavior. If no behavior is found for the object, add a ContentLine. group is an optional prefix to the name of the object (see RFC 2425). """ if isinstance(objOrName, VBase): obj = objOrName if self.behavior: obj.parentBehavior = self.behavior obj.autoBehavior(True) else: name = objOrName.upper() try: id=self.behavior.knownChildren[name][2] behavior = getBehavior(name, id) if behavior.isComponent: obj = Component(name) else: obj = ContentLine(name, [], '', group) obj.parentBehavior = self.behavior obj.behavior = behavior obj = obj.transformToNative() except (KeyError, AttributeError): obj = ContentLine(objOrName, [], '', group) if obj.behavior is None and self.behavior is not None: if isinstance(obj, ContentLine): obj.behavior = self.behavior.defaultBehavior self.contents.setdefault(obj.name.lower(), []).append(obj) return obj def remove(self, obj): """Remove obj from contents.""" named = self.contents.get(obj.name.lower()) if named: try: named.remove(obj) if len(named) == 0: del self.contents[obj.name.lower()] except ValueError: pass; def getChildren(self): """Return an iterable of all children.""" for objList in self.contents.values(): for obj in objList: yield obj def components(self): """Return an iterable of all Component children.""" return (i for i in self.getChildren() if isinstance(i, Component)) def lines(self): """Return an iterable of all ContentLine children.""" return (i for i in self.getChildren() if isinstance(i, ContentLine)) def sortChildKeys(self): try: first = [s for s in self.behavior.sortFirst if s in self.contents] except: first = [] return first + sorted(k for k in self.contents.keys() if k not in first) def getSortedChildren(self): return [obj for k in self.sortChildKeys() for obj in self.contents[k]] def setBehaviorFromVersionLine(self, versionLine): """Set behavior if one matches name, versionLine.value.""" v=getBehavior(self.name, versionLine.value) if v: self.setBehavior(v) def transformChildrenToNative(self): """Recursively replace children with their native representation.""" #sort to get dependency order right, like vtimezone before vevent for childArray in (self.contents[k] for k in self.sortChildKeys()): for i in xrange(len(childArray)): childArray[i]=childArray[i].transformToNative() childArray[i].transformChildrenToNative() def transformChildrenFromNative(self, clearBehavior=True): """Recursively transform native children to vanilla representations.""" for childArray in self.contents.values(): for i in xrange(len(childArray)): childArray[i]=childArray[i].transformFromNative() childArray[i].transformChildrenFromNative(clearBehavior) if clearBehavior: childArray[i].behavior = None childArray[i].parentBehavior = None def __str__(self): if self.name: return "<" + self.name + "| " + str(self.getSortedChildren()) + ">" else: return '<' + '*unnamed*' + '| ' + str(self.getSortedChildren()) + '>' def __repr__(self): return self.__str__() def prettyPrint(self, level = 0, tabwidth=3): pre = ' ' * level * tabwidth print pre, self.name if isinstance(self, Component): for line in self.getChildren(): line.prettyPrint(level + 1, tabwidth) print class VObjectError(Exception): def __init__(self, message, lineNumber=None): self.message = message if lineNumber is not None: self.lineNumber = lineNumber def __str__(self): if hasattr(self, 'lineNumber'): return "At line %s: %s" % \ (self.lineNumber, self.message) else: return repr(self.message) class ParseError(VObjectError): pass class ValidateError(VObjectError): pass class NativeError(VObjectError): pass #-------------------------- Parsing functions ---------------------------------- # parseLine regular expressions patterns = {} # Note that underscore is not legal for names, it's included because # Lotus Notes uses it patterns['name'] = '[a-zA-Z0-9\-_]+' patterns['safe_char'] = '[^";:,]' patterns['qsafe_char'] = '[^"]' # the combined Python string replacement and regex syntax is a little confusing; # remember that %(foobar)s is replaced with patterns['foobar'], so for instance # param_value is any number of safe_chars or any number of qsaf_chars surrounded # by double quotes. patterns['param_value'] = ' "%(qsafe_char)s * " | %(safe_char)s * ' % patterns # get a tuple of two elements, one will be empty, the other will have the value patterns['param_value_grouped'] = """ " ( %(qsafe_char)s * )" | ( %(safe_char)s + ) """ % patterns # get a parameter and its values, without any saved groups patterns['param'] = r""" ; (?: %(name)s ) # parameter name (?: (?: = (?: %(param_value)s ) )? # 0 or more parameter values, multiple (?: , (?: %(param_value)s ) )* # parameters are comma separated )* """ % patterns # get a parameter, saving groups for name and value (value still needs parsing) patterns['params_grouped'] = r""" ; ( %(name)s ) (?: = ( (?: (?: %(param_value)s ) )? # 0 or more parameter values, multiple (?: , (?: %(param_value)s ) )* # parameters are comma separated ) )? """ % patterns # get a full content line, break it up into group, name, parameters, and value patterns['line'] = r""" ^ ((?P %(name)s)\.)?(?P %(name)s) # name group (?P (?: %(param)s )* ) # params group (may be empty) : (?P .* )$ # value group """ % patterns ' "%(qsafe_char)s*" | %(safe_char)s* ' param_values_re = re.compile(patterns['param_value_grouped'], re.VERBOSE) params_re = re.compile(patterns['params_grouped'], re.VERBOSE) line_re = re.compile(patterns['line'], re.DOTALL | re.VERBOSE) begin_re = re.compile('BEGIN', re.IGNORECASE) def parseParams(string): """ >>> parseParams(';ALTREP="http://www.wiz.org"') [['ALTREP', 'http://www.wiz.org']] >>> parseParams('') [] >>> parseParams(';ALTREP="http://www.wiz.org;;",Blah,Foo;NEXT=Nope;BAR') [['ALTREP', 'http://www.wiz.org;;', 'Blah', 'Foo'], ['NEXT', 'Nope'], ['BAR']] """ all = params_re.findall(string) allParameters = [] for tup in all: paramList = [tup[0]] # tup looks like (name, valuesString) for pair in param_values_re.findall(tup[1]): # pair looks like ('', value) or (value, '') if pair[0] != '': paramList.append(pair[0]) else: paramList.append(pair[1]) allParameters.append(paramList) return allParameters def parseLine(line, lineNumber = None): """ >>> parseLine("BLAH:") ('BLAH', [], '', None) >>> parseLine("RDATE:VALUE=DATE:19970304,19970504,19970704,19970904") ('RDATE', [], 'VALUE=DATE:19970304,19970504,19970704,19970904', None) >>> parseLine('DESCRIPTION;ALTREP="http://www.wiz.org":The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA') ('DESCRIPTION', [['ALTREP', 'http://www.wiz.org']], 'The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA', None) >>> parseLine("EMAIL;PREF;INTERNET:john@nowhere.com") ('EMAIL', [['PREF'], ['INTERNET']], 'john@nowhere.com', None) >>> parseLine('EMAIL;TYPE="blah",hah;INTERNET="DIGI",DERIDOO:john@nowhere.com') ('EMAIL', [['TYPE', 'blah', 'hah'], ['INTERNET', 'DIGI', 'DERIDOO']], 'john@nowhere.com', None) >>> parseLine('item1.ADR;type=HOME;type=pref:;;Reeperbahn 116;Hamburg;;20359;') ('ADR', [['type', 'HOME'], ['type', 'pref']], ';;Reeperbahn 116;Hamburg;;20359;', 'item1') >>> parseLine(":") Traceback (most recent call last): ... ParseError: 'Failed to parse line: :' """ match = line_re.match(line) if match is None: raise ParseError("Failed to parse line: %s" % line, lineNumber) # Underscores are replaced with dash to work around Lotus Notes return (match.group('name').replace('_','-'), parseParams(match.group('params')), match.group('value'), match.group('group')) # logical line regular expressions patterns['lineend'] = r'(?:\r\n|\r|\n|$)' patterns['wrap'] = r'%(lineend)s [\t ]' % patterns patterns['logicallines'] = r""" ( (?: [^\r\n] | %(wrap)s )* %(lineend)s ) """ % patterns patterns['wraporend'] = r'(%(wrap)s | %(lineend)s )' % patterns wrap_re = re.compile(patterns['wraporend'], re.VERBOSE) logical_lines_re = re.compile(patterns['logicallines'], re.VERBOSE) testLines=""" Line 0 text , Line 0 continued. Line 1;encoding=quoted-printable:this is an evil= evil= format. Line 2 is a new line, it does not start with whitespace. """ def getLogicalLines(fp, allowQP=True, findBegin=False): """Iterate through a stream, yielding one logical line at a time. Because many applications still use vCard 2.1, we have to deal with the quoted-printable encoding for long lines, as well as the vCard 3.0 and vCalendar line folding technique, a whitespace character at the start of the line. Quoted-printable data will be decoded in the Behavior decoding phase. >>> import StringIO >>> f=StringIO.StringIO(testLines) >>> for n, l in enumerate(getLogicalLines(f)): ... print "Line %s: %s" % (n, l[0]) ... Line 0: Line 0 text, Line 0 continued. Line 1: Line 1;encoding=quoted-printable:this is an evil= evil= format. Line 2: Line 2 is a new line, it does not start with whitespace. """ if not allowQP: bytes = fp.read(-1) if len(bytes) > 0: if type(bytes[0]) == unicode: val = bytes elif not findBegin: val = bytes.decode('utf-8') else: for encoding in 'utf-8', 'utf-16-LE', 'utf-16-BE', 'iso-8859-1': try: val = bytes.decode(encoding) if begin_re.search(val) is not None: break except UnicodeDecodeError: pass else: raise ParseError, 'Could not find BEGIN when trying to determine encoding' else: val = bytes # strip off any UTF8 BOMs which Python's UTF8 decoder leaves val = val.lstrip( unicode( codecs.BOM_UTF8, "utf8" ) ) lineNumber = 1 for match in logical_lines_re.finditer(val): line, n = wrap_re.subn('', match.group()) if line != '': yield line, lineNumber lineNumber += n else: quotedPrintable=False newbuffer = StringIO.StringIO logicalLine = newbuffer() lineNumber = 0 lineStartNumber = 0 while True: line = fp.readline() if line == '': break else: line = line.rstrip(CRLF) lineNumber += 1 if line.rstrip() == '': if logicalLine.pos > 0: yield logicalLine.getvalue(), lineStartNumber lineStartNumber = lineNumber logicalLine = newbuffer() quotedPrintable=False continue if quotedPrintable and allowQP: logicalLine.write('\n') logicalLine.write(line) quotedPrintable=False elif line[0] in SPACEORTAB: logicalLine.write(line[1:]) elif logicalLine.pos > 0: yield logicalLine.getvalue(), lineStartNumber lineStartNumber = lineNumber logicalLine = newbuffer() logicalLine.write(line) else: logicalLine = newbuffer() logicalLine.write(line) # hack to deal with the fact that vCard 2.1 allows parameters to be # encoded without a parameter name. False positives are unlikely, but # possible. val = logicalLine.getvalue() if val[-1]=='=' and val.lower().find('quoted-printable') >= 0: quotedPrintable=True if logicalLine.pos > 0: yield logicalLine.getvalue(), lineStartNumber def textLineToContentLine(text, n=None): return ContentLine(*parseLine(text, n), **{'encoded':True, 'lineNumber' : n}) def dquoteEscape(param): """Return param, or "param" if ',' or ';' or ':' is in param.""" if param.find('"') >= 0: raise VObjectError("Double quotes aren't allowed in parameter values.") for char in ',;:': if param.find(char) >= 0: return '"'+ param + '"' return param def foldOneLine(outbuf, input, lineLength = 75): # Folding line procedure that ensures multi-byte utf-8 sequences are not broken # across lines if len(input) < lineLength: # Optimize for unfolded line case outbuf.write(input) else: # Look for valid utf8 range and write that out start = 0 written = 0 while written < len(input): # Start max length -1 chars on from where we are offset = start + lineLength - 1 if offset >= len(input): line = input[start:] outbuf.write(line) written = len(input) else: # Check whether next char is valid utf8 lead byte while (input[offset] > 0x7F) and ((ord(input[offset]) & 0xC0) == 0x80): # Step back until we have a valid char offset -= 1 line = input[start:offset] outbuf.write(line) outbuf.write("\r\n ") written += offset - start start = offset outbuf.write("\r\n") def defaultSerialize(obj, buf, lineLength): """Encode and fold obj and its children, write to buf or return a string.""" outbuf = buf or cStringIO.StringIO() if isinstance(obj, Component): if obj.group is None: groupString = '' else: groupString = obj.group + '.' if obj.useBegin: foldOneLine(outbuf, str(groupString + u"BEGIN:" + obj.name), lineLength) for child in obj.getSortedChildren(): #validate is recursive, we only need to validate once child.serialize(outbuf, lineLength, validate=False) if obj.useBegin: foldOneLine(outbuf, str(groupString + u"END:" + obj.name), lineLength) elif isinstance(obj, ContentLine): startedEncoded = obj.encoded if obj.behavior and not startedEncoded: obj.behavior.encode(obj) s=codecs.getwriter('utf-8')(cStringIO.StringIO()) #unfolded buffer if obj.group is not None: s.write(obj.group + '.') s.write(obj.name.upper()) for key, paramvals in obj.params.iteritems(): s.write(';' + key + '=' + ','.join(dquoteEscape(p) for p in paramvals)) s.write(':' + obj.value) if obj.behavior and not startedEncoded: obj.behavior.decode(obj) foldOneLine(outbuf, s.getvalue(), lineLength) return buf or outbuf.getvalue() testVCalendar=""" BEGIN:VCALENDAR BEGIN:VEVENT SUMMARY;blah=hi!:Bastille Day Party END:VEVENT END:VCALENDAR""" class Stack: def __init__(self): self.stack = [] def __len__(self): return len(self.stack) def top(self): if len(self) == 0: return None else: return self.stack[-1] def topName(self): if len(self) == 0: return None else: return self.stack[-1].name def modifyTop(self, item): top = self.top() if top: top.add(item) else: new = Component() self.push(new) new.add(item) #add sets behavior for item and children def push(self, obj): self.stack.append(obj) def pop(self): return self.stack.pop() def readComponents(streamOrString, validate=False, transform=True, findBegin=True, ignoreUnreadable=False, allowQP=False): """Generate one Component at a time from a stream. >>> import StringIO >>> f = StringIO.StringIO(testVCalendar) >>> cal=readComponents(f).next() >>> cal ]>]> >>> cal.vevent.summary """ if isinstance(streamOrString, basestring): stream = StringIO.StringIO(streamOrString) else: stream = streamOrString try: stack = Stack() versionLine = None n = 0 for line, n in getLogicalLines(stream, allowQP, findBegin): if ignoreUnreadable: try: vline = textLineToContentLine(line, n) except VObjectError, e: if e.lineNumber is not None: msg = "Skipped line %(lineNumber)s, message: %(msg)s" else: msg = "Skipped a line, message: %(msg)s" logger.error(msg % {'lineNumber' : e.lineNumber, 'msg' : e.message}) continue else: vline = textLineToContentLine(line, n) if vline.name == "VERSION": versionLine = vline stack.modifyTop(vline) elif vline.name == "BEGIN": stack.push(Component(vline.value, group=vline.group)) elif vline.name == "PROFILE": if not stack.top(): stack.push(Component()) stack.top().setProfile(vline.value) elif vline.name == "END": if len(stack) == 0: err = "Attempted to end the %s component, \ but it was never opened" % vline.value raise ParseError(err, n) if vline.value.upper() == stack.topName(): #START matches END if len(stack) == 1: component=stack.pop() if versionLine is not None: component.setBehaviorFromVersionLine(versionLine) else: behavior = getBehavior(component.name) if behavior: component.setBehavior(behavior) if validate: component.validate(raiseException=True) if transform: component.transformChildrenToNative() yield component #EXIT POINT else: stack.modifyTop(stack.pop()) else: err = "%s component wasn't closed" raise ParseError(err % stack.topName(), n) else: stack.modifyTop(vline) #not a START or END line if stack.top(): if stack.topName() is None: logger.warning("Top level component was never named") elif stack.top().useBegin: raise ParseError("Component %s was never closed" % (stack.topName()), n) yield stack.pop() except ParseError, e: e.input = streamOrString raise def readOne(stream, validate=False, transform=True, findBegin=True, ignoreUnreadable=False, allowQP=False): """Return the first component from stream.""" return readComponents(stream, validate, transform, findBegin, ignoreUnreadable, allowQP).next() #--------------------------- version registry ---------------------------------- __behaviorRegistry={} def registerBehavior(behavior, name=None, default=False, id=None): """Register the given behavior. If default is True (or if this is the first version registered with this name), the version will be the default if no id is given. """ if not name: name=behavior.name.upper() if id is None: id=behavior.versionString if name in __behaviorRegistry: if default: __behaviorRegistry[name].insert(0, (id, behavior)) else: __behaviorRegistry[name].append((id, behavior)) else: __behaviorRegistry[name]=[(id, behavior)] def getBehavior(name, id=None): """Return a matching behavior if it exists, or None. If id is None, return the default for name. """ name=name.upper() if name in __behaviorRegistry: if id: for n, behavior in __behaviorRegistry[name]: if n==id: return behavior return __behaviorRegistry[name][0][1] return None def newFromBehavior(name, id=None): """Given a name, return a behaviored ContentLine or Component.""" name = name.upper() behavior = getBehavior(name, id) if behavior is None: raise VObjectError("No behavior found named %s" % name) if behavior.isComponent: obj = Component(name) else: obj = ContentLine(name, [], '') obj.behavior = behavior obj.isNative = False return obj #--------------------------- Helper function ----------------------------------- def backslashEscape(s): s=s.replace("\\","\\\\").replace(";","\;").replace(",","\,") return s.replace("\r\n", "\\n").replace("\n","\\n").replace("\r","\\n") #------------------- Testing and running functions ----------------------------- if __name__ == '__main__': import tests tests._test() vobject-0.8.1c/vobject/behavior.py0000644000076500001200000001426411017337652016403 0ustar chandleadmin"""Behavior (validation, encoding, and transformations) for vobjects.""" import base #------------------------ Abstract class for behavior -------------------------- class Behavior(object): """Abstract class to describe vobject options, requirements and encodings. Behaviors are used for root components like VCALENDAR, for subcomponents like VEVENT, and for individual lines in components. Behavior subclasses are not meant to be instantiated, all methods should be classmethods. @cvar name: The uppercase name of the object described by the class, or a generic name if the class defines behavior for many objects. @cvar description: A brief excerpt from the RFC explaining the function of the component or line. @cvar versionString: The string associated with the component, for instance, 2.0 if there's a line like VERSION:2.0, an empty string otherwise. @cvar knownChildren: A dictionary with uppercased component/property names as keys and a tuple (min, max, id) as value, where id is the id used by L{registerBehavior}, min and max are the limits on how many of this child must occur. None is used to denote no max or no id. @cvar quotedPrintable: A boolean describing whether the object should be encoded and decoded using quoted printable line folding and character escaping. @cvar defaultBehavior: Behavior to apply to ContentLine children when no behavior is found. @cvar hasNative: A boolean describing whether the object can be transformed into a more Pythonic object. @cvar isComponent: A boolean, True if the object should be a Component. @cvar sortFirst: The lower-case list of children which should come first when sorting. @cvar allowGroup: Whether or not vCard style group prefixes are allowed. """ name='' description='' versionString='' knownChildren = {} quotedPrintable = False defaultBehavior = None hasNative= False isComponent = False allowGroup = False forceUTC = False sortFirst = [] def __init__(self): err="Behavior subclasses are not meant to be instantiated" raise base.VObjectError(err) @classmethod def validate(cls, obj, raiseException=False, complainUnrecognized=False): """Check if the object satisfies this behavior's requirements. @param obj: The L{ContentLine} 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 = str(obj) + " has a group, but this object doesn't support groups" raise base.VObjectError(err) if isinstance(obj, base.ContentLine): return cls.lineValidate(obj, raiseException, complainUnrecognized) elif isinstance(obj, base.Component): count = {} for child in obj.getChildren(): if not child.validate(raiseException, complainUnrecognized): return False name=child.name.upper() count[name] = count.get(name, 0) + 1 for key, val in cls.knownChildren.iteritems(): if count.get(key,0) < val[0]: if raiseException: m = "%s components must contain at least %i %s" raise base.ValidateError(m % (cls.name, val[0], key)) return False if val[1] and count.get(key,0) > val[1]: if raiseException: m = "%s components cannot contain more than %i %s" raise base.ValidateError(m % (cls.name, val[1], key)) return False return True else: err = str(obj) + " is not a Component or Contentline" raise base.VObjectError(err) @classmethod def lineValidate(cls, line, raiseException, complainUnrecognized): """Examine a line's parameters and values, return True if valid.""" return True @classmethod def decode(cls, line): if line.encoded: line.encoded=0 @classmethod def encode(cls, line): if not line.encoded: line.encoded=1 @classmethod def transformToNative(cls, obj): """Turn a ContentLine or Component into a Python-native representation. If appropriate, turn dates or datetime strings into Python objects. Components containing VTIMEZONEs turn into VtimezoneComponents. """ return obj @classmethod def transformFromNative(cls, obj): """Inverse of transformToNative.""" raise base.NativeError("No transformFromNative defined") @classmethod def generateImplicitParameters(cls, obj): """Generate any required information that don't yet exist.""" pass @classmethod def serialize(cls, obj, buf, lineLength, validate=True): """Set implicit parameters, do encoding, return unicode string. If validate is True, raise VObjectError if the line doesn't validate after implicit parameters are generated. Default is to call base.defaultSerialize. """ cls.generateImplicitParameters(obj) if validate: cls.validate(obj, raiseException=True) if obj.isNative: transformed = obj.transformFromNative() undoTransform = True else: transformed = obj undoTransform = False out = base.defaultSerialize(transformed, buf, lineLength) if undoTransform: obj.transformToNative() return out @classmethod def valueRepr( cls, line ): """return the representation of the given content line value""" return line.valuevobject-0.8.1c/vobject/change_tz.py0000644000076500007650000000514011151622553017060 0ustar chandlechandle"""Translate an ics file's events to a different timezone.""" from optparse import OptionParser from vobject import icalendar, base import sys try: import PyICU except: PyICU = None from datetime import datetime def change_tz(cal, new_timezone, default, utc_only=False, utc_tz=icalendar.utc): for vevent in getattr(cal, 'vevent_list', []): start = getattr(vevent, 'dtstart', None) end = getattr(vevent, 'dtend', None) for node in (start, end): if node: dt = node.value if (isinstance(dt, datetime) and (not utc_only or dt.tzinfo == utc_tz)): if dt.tzinfo is None: dt = dt.replace(tzinfo = default) node.value = dt.astimezone(new_timezone) def main(): options, args = get_options() if PyICU is None: print "Failure. change_tz requires PyICU, exiting" elif options.list: for tz_string in PyICU.TimeZone.createEnumeration(): print tz_string elif args: utc_only = options.utc if utc_only: which = "only UTC" else: which = "all" print "Converting %s events" % which ics_file = args[0] if len(args) > 1: timezone = PyICU.ICUtzinfo.getInstance(args[1]) else: timezone = PyICU.ICUtzinfo.default print "... Reading %s" % ics_file cal = base.readOne(file(ics_file)) change_tz(cal, timezone, PyICU.ICUtzinfo.default, utc_only) out_name = ics_file + '.converted' print "... Writing %s" % out_name out = file(out_name, 'wb') cal.serialize(out) print "Done" version = "0.1" def get_options(): ##### Configuration options ##### usage = """usage: %prog [options] ics_file [timezone]""" parser = OptionParser(usage=usage, version=version) parser.set_description("change_tz will convert the timezones in an ics file. ") parser.add_option("-u", "--only-utc", dest="utc", action="store_true", default=False, help="Only change UTC events.") parser.add_option("-l", "--list", dest="list", action="store_true", default=False, help="List available timezones") (cmdline_options, args) = parser.parse_args() if not args and not cmdline_options.list: print "error: too few arguments given" print print parser.format_help() return False, False return cmdline_options, args if __name__ == "__main__": try: main() except KeyboardInterrupt: print "Aborted" vobject-0.8.1c/vobject/hcalendar.py0000644000076500001200000001015511017337652016520 0ustar chandleadmin""" hCalendar: A microformat for serializing iCalendar data (http://microformats.org/wiki/hcalendar) Here is a sample event in an iCalendar: BEGIN:VCALENDAR PRODID:-//XYZproduct//EN VERSION:2.0 BEGIN:VEVENT URL:http://www.web2con.com/ DTSTART:20051005 DTEND:20051008 SUMMARY:Web 2.0 Conference LOCATION:Argent Hotel\, San Francisco\, CA END:VEVENT END:VCALENDAR and an equivalent event in hCalendar format with various elements optimized appropriately. Web 2.0 Conference: October 5- 7, at the Argent Hotel, San Francisco, CA """ from base import foldOneLine, CRLF, registerBehavior from icalendar import VCalendar2_0 from datetime import date, datetime, timedelta import StringIO class HCalendar(VCalendar2_0): name = 'HCALENDAR' @classmethod def serialize(cls, obj, buf=None, lineLength=None, validate=True): """ Serialize iCalendar to HTML using the hCalendar microformat (http://microformats.org/wiki/hcalendar) """ outbuf = buf or StringIO.StringIO() level = 0 # holds current indentation level tabwidth = 3 def indent(): return ' ' * level * tabwidth def out(s): outbuf.write(indent()) outbuf.write(s) # not serializing optional vcalendar wrapper vevents = obj.vevent_list for event in vevents: out('' + CRLF) level += 1 # URL url = event.getChildValue("url") if url: out('' + CRLF) level += 1 # SUMMARY summary = event.getChildValue("summary") if summary: out('' + summary + ':' + CRLF) # DTSTART dtstart = event.getChildValue("dtstart") if dtstart: if type(dtstart) == date: timeformat = "%A, %B %e" machine = "%Y%m%d" elif type(dtstart) == datetime: timeformat = "%A, %B %e, %H:%M" machine = "%Y%m%dT%H%M%S%z" #TODO: Handle non-datetime formats? #TODO: Spec says we should handle when dtstart isn't included out('%s\r\n' % (dtstart.strftime(machine), dtstart.strftime(timeformat))) # DTEND dtend = event.getChildValue("dtend") if not dtend: duration = event.getChildValue("duration") if duration: dtend = duration + dtstart # TODO: If lacking dtend & duration? if dtend: human = dtend # TODO: Human readable part could be smarter, excluding repeated data if type(dtend) == date: human = dtend - timedelta(days=1) out('- %s\r\n' % (dtend.strftime(machine), human.strftime(timeformat))) # LOCATION location = event.getChildValue("location") if location: out('at ' + location + '' + CRLF) description = event.getChildValue("description") if description: out('
' + description + '
' + CRLF) if url: level -= 1 out('
' + CRLF) level -= 1 out('
' + CRLF) # close vevent return buf or outbuf.getvalue() registerBehavior(HCalendar)vobject-0.8.1c/vobject/icalendar.py0000644000076500001200000022442611131217603016517 0ustar chandleadmin"""Definitions and behavior for iCalendar, also known as vCalendar 2.0""" import string import behavior import dateutil.rrule import dateutil.tz import StringIO, cStringIO import datetime import socket, random #for generating a UID import itertools from base import (VObjectError, NativeError, ValidateError, ParseError, VBase, Component, ContentLine, logger, defaultSerialize, registerBehavior, backslashEscape, foldOneLine, newFromBehavior, CRLF, LF, ascii) #------------------------------- Constants ------------------------------------- DATENAMES = ("rdate", "exdate") RULENAMES = ("exrule", "rrule") DATESANDRULES = ("exrule", "rrule", "rdate", "exdate") PRODID = u"-//PYVOBJECT//NONSGML Version 1//EN" WEEKDAYS = "MO", "TU", "WE", "TH", "FR", "SA", "SU" FREQUENCIES = ('YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY') zeroDelta = datetime.timedelta(0) twoHours = datetime.timedelta(hours=2) #---------------------------- TZID registry ------------------------------------ __tzidMap={} def toUnicode(s): """Take a string or unicode, turn it into unicode, decoding as utf-8""" if isinstance(s, str): s = s.decode('utf-8') return s def registerTzid(tzid, tzinfo): """Register a tzid -> tzinfo mapping.""" __tzidMap[toUnicode(tzid)]=tzinfo def getTzid(tzid): """Return the tzid if it exists, or None.""" return __tzidMap.get(toUnicode(tzid), None) utc = dateutil.tz.tzutc() registerTzid("UTC", utc) #-------------------- Helper subclasses ---------------------------------------- class TimezoneComponent(Component): """A VTIMEZONE object. VTIMEZONEs are parsed by dateutil.tz.tzical, the resulting datetime.tzinfo subclass is stored in self.tzinfo, self.tzid stores the TZID associated with this timezone. @ivar name: The uppercased name of the object, in this case always 'VTIMEZONE'. @ivar tzinfo: A datetime.tzinfo subclass representing this timezone. @ivar tzid: The string used to refer to this timezone. """ def __init__(self, tzinfo=None, *args, **kwds): """Accept an existing Component or a tzinfo class.""" super(TimezoneComponent, self).__init__(*args, **kwds) self.isNative=True # hack to make sure a behavior is assigned if self.behavior is None: self.behavior = VTimezone if tzinfo is not None: self.tzinfo = tzinfo if not hasattr(self, 'name') or self.name == '': self.name = 'VTIMEZONE' self.useBegin = True @classmethod def registerTzinfo(obj, tzinfo): """Register tzinfo if it's not already registered, return its tzid.""" tzid = obj.pickTzid(tzinfo) if tzid and not getTzid(tzid): registerTzid(tzid, tzinfo) return tzid def gettzinfo(self): # workaround for dateutil failing to parse some experimental properties good_lines = ('rdate', 'rrule', 'dtstart', 'tzname', 'tzoffsetfrom', 'tzoffsetto', 'tzid') # serialize encodes as utf-8, cStringIO will leave utf-8 alone buffer = cStringIO.StringIO() # allow empty VTIMEZONEs if len(self.contents) == 0: return None def customSerialize(obj): if isinstance(obj, Component): foldOneLine(buffer, u"BEGIN:" + obj.name) for child in obj.lines(): if child.name.lower() in good_lines: child.serialize(buffer, 75, validate=False) for comp in obj.components(): customSerialize(comp) foldOneLine(buffer, u"END:" + obj.name) customSerialize(self) buffer.seek(0) # tzical wants to read a stream return dateutil.tz.tzical(buffer).get() def settzinfo(self, tzinfo, start=2000, end=2030): """Create appropriate objects in self to represent tzinfo. Collapse DST transitions to rrules as much as possible. Assumptions: - DST <-> Standard transitions occur on the hour - never within a month of one another - twice or fewer times a year - never in the month of December - DST always moves offset exactly one hour later - tzinfo classes dst method always treats times that could be in either offset as being in the later regime """ def fromLastWeek(dt): """How many weeks from the end of the month dt is, starting from 1.""" weekDelta = datetime.timedelta(weeks=1) n = 1 current = dt + weekDelta while current.month == dt.month: n += 1 current += weekDelta return n # lists of dictionaries defining rules which are no longer in effect completed = {'daylight' : [], 'standard' : []} # dictionary defining rules which are currently in effect working = {'daylight' : None, 'standard' : None} # rule may be based on the nth week of the month or the nth from the last for year in xrange(start, end + 1): newyear = datetime.datetime(year, 1, 1) for transitionTo in 'daylight', 'standard': transition = getTransition(transitionTo, year, tzinfo) oldrule = working[transitionTo] if transition == newyear: # transitionTo is in effect for the whole year rule = {'end' : None, 'start' : newyear, 'month' : 1, 'weekday' : None, 'hour' : None, 'plus' : None, 'minus' : None, 'name' : tzinfo.tzname(newyear), 'offset' : tzinfo.utcoffset(newyear), 'offsetfrom' : tzinfo.utcoffset(newyear)} if oldrule is None: # transitionTo was not yet in effect working[transitionTo] = rule else: # transitionTo was already in effect if (oldrule['offset'] != tzinfo.utcoffset(newyear)): # old rule was different, it shouldn't continue oldrule['end'] = year - 1 completed[transitionTo].append(oldrule) working[transitionTo] = rule elif transition is None: # transitionTo is not in effect if oldrule is not None: # transitionTo used to be in effect oldrule['end'] = year - 1 completed[transitionTo].append(oldrule) working[transitionTo] = None else: # an offset transition was found old_offset = tzinfo.utcoffset(transition - twoHours) rule = {'end' : None, # None, or an integer year 'start' : transition, # the datetime of transition 'month' : transition.month, 'weekday' : transition.weekday(), 'hour' : transition.hour, 'name' : tzinfo.tzname(transition), 'plus' : (transition.day - 1)/ 7 + 1,#nth week of the month 'minus' : fromLastWeek(transition), #nth from last week 'offset' : tzinfo.utcoffset(transition), 'offsetfrom' : old_offset} if oldrule is None: working[transitionTo] = rule else: plusMatch = rule['plus'] == oldrule['plus'] minusMatch = rule['minus'] == oldrule['minus'] truth = plusMatch or minusMatch for key in 'month', 'weekday', 'hour', 'offset': truth = truth and rule[key] == oldrule[key] if truth: # the old rule is still true, limit to plus or minus if not plusMatch: oldrule['plus'] = None if not minusMatch: oldrule['minus'] = None else: # the new rule did not match the old oldrule['end'] = year - 1 completed[transitionTo].append(oldrule) working[transitionTo] = rule for transitionTo in 'daylight', 'standard': if working[transitionTo] is not None: completed[transitionTo].append(working[transitionTo]) self.tzid = [] self.daylight = [] self.standard = [] self.add('tzid').value = self.pickTzid(tzinfo, True) old = None for transitionTo in 'daylight', 'standard': for rule in completed[transitionTo]: comp = self.add(transitionTo) dtstart = comp.add('dtstart') dtstart.value = rule['start'] if rule['name'] is not None: comp.add('tzname').value = rule['name'] line = comp.add('tzoffsetto') line.value = deltaToOffset(rule['offset']) line = comp.add('tzoffsetfrom') line.value = deltaToOffset(rule['offsetfrom']) if rule['plus'] is not None: num = rule['plus'] elif rule['minus'] is not None: num = -1 * rule['minus'] else: num = None if num is not None: dayString = ";BYDAY=" + str(num) + WEEKDAYS[rule['weekday']] else: dayString = "" if rule['end'] is not None: if rule['hour'] is None: # all year offset, with no rule endDate = datetime.datetime(rule['end'], 1, 1) else: weekday = dateutil.rrule.weekday(rule['weekday'], num) du_rule = dateutil.rrule.rrule(dateutil.rrule.YEARLY, bymonth = rule['month'],byweekday = weekday, dtstart = datetime.datetime( rule['end'], 1, 1, rule['hour']) ) endDate = du_rule[0] endDate = endDate.replace(tzinfo = utc) - rule['offsetfrom'] endString = ";UNTIL="+ dateTimeToString(endDate) else: endString = '' rulestring = "FREQ=YEARLY%s;BYMONTH=%s%s" % \ (dayString, str(rule['month']), endString) comp.add('rrule').value = rulestring tzinfo = property(gettzinfo, settzinfo) # prevent Component's __setattr__ from overriding the tzinfo property normal_attributes = Component.normal_attributes + ['tzinfo'] @staticmethod def pickTzid(tzinfo, allowUTC=False): """ Given a tzinfo class, use known APIs to determine TZID, or use tzname. """ if tzinfo is None or (not allowUTC and tzinfo_eq(tzinfo, utc)): #If tzinfo is UTC, we don't need a TZID return None # try PyICU's tzid key if hasattr(tzinfo, 'tzid'): return toUnicode(tzinfo.tzid) # try pytz zone key if hasattr(tzinfo, 'zone'): return toUnicode(tzinfo.zone) # try tzical's tzid key elif hasattr(tzinfo, '_tzid'): return toUnicode(tzinfo._tzid) else: # return tzname for standard (non-DST) time notDST = datetime.timedelta(0) for month in xrange(1,13): dt = datetime.datetime(2000, month, 1) if tzinfo.dst(dt) == notDST: return toUnicode(tzinfo.tzname(dt)) # there was no standard time in 2000! raise VObjectError("Unable to guess TZID for tzinfo %s" % str(tzinfo)) def __str__(self): return "" def __repr__(self): return self.__str__() def prettyPrint(self, level, tabwidth): pre = ' ' * level * tabwidth print pre, self.name print pre, "TZID:", self.tzid print class RecurringComponent(Component): """A vCalendar component like VEVENT or VTODO which may recur. Any recurring component can have one or multiple RRULE, RDATE, EXRULE, or EXDATE lines, and one or zero DTSTART lines. It can also have a variety of children that don't have any recurrence information. In the example below, note that dtstart is included in the rruleset. This is not the default behavior for dateutil's rrule implementation unless dtstart would already have been a member of the recurrence rule, and as a result, COUNT is wrong. This can be worked around when getting rruleset by adjusting count down by one if an rrule has a count and dtstart isn't in its result set, but by default, the rruleset property doesn't do this work around, to access it getrruleset must be called with addRDate set True. >>> import dateutil.rrule, datetime >>> vevent = RecurringComponent(name='VEVENT') >>> vevent.add('rrule').value =u"FREQ=WEEKLY;COUNT=2;INTERVAL=2;BYDAY=TU,TH" >>> vevent.add('dtstart').value = datetime.datetime(2005, 1, 19, 9) When creating rrule's programmatically it should be kept in mind that count doesn't necessarily mean what rfc2445 says. >>> list(vevent.rruleset) [datetime.datetime(2005, 1, 20, 9, 0), datetime.datetime(2005, 2, 1, 9, 0)] >>> list(vevent.getrruleset(addRDate=True)) [datetime.datetime(2005, 1, 19, 9, 0), datetime.datetime(2005, 1, 20, 9, 0)] Also note that dateutil will expand all-day events (datetime.date values) to datetime.datetime value with time 0 and no timezone. >>> vevent.dtstart.value = datetime.date(2005,3,18) >>> list(vevent.rruleset) [datetime.datetime(2005, 3, 29, 0, 0), datetime.datetime(2005, 3, 31, 0, 0)] >>> list(vevent.getrruleset(True)) [datetime.datetime(2005, 3, 18, 0, 0), datetime.datetime(2005, 3, 29, 0, 0)] @ivar rruleset: A U{rruleset}. """ def __init__(self, *args, **kwds): super(RecurringComponent, self).__init__(*args, **kwds) self.isNative=True #self.clobberedRDates=[] def getrruleset(self, addRDate = False): """Get an rruleset created from self. If addRDate is True, add an RDATE for dtstart if it's not included in an RRULE, and count is decremented if it exists. Note that for rules which don't match DTSTART, DTSTART may not appear in list(rruleset), although it should. By default, an RDATE is not created in these cases, and count isn't updated, so dateutil may list a spurious occurrence. """ rruleset = None for name in DATESANDRULES: addfunc = None for line in self.contents.get(name, ()): # don't bother creating a rruleset unless there's a rule if rruleset is None: rruleset = dateutil.rrule.rruleset() if addfunc is None: addfunc=getattr(rruleset, name) if name in DATENAMES: if type(line.value[0]) == datetime.datetime: map(addfunc, line.value) elif type(line.value[0]) == datetime.date: for dt in line.value: addfunc(datetime.datetime(dt.year, dt.month, dt.day)) else: # ignore RDATEs with PERIOD values for now pass elif name in RULENAMES: try: dtstart = self.dtstart.value except AttributeError, KeyError: # Special for VTODO - try DUE property instead try: if self.name == "VTODO": dtstart = self.due.value else: # if there's no dtstart, just return None return None except AttributeError, KeyError: # if there's no due, just return None return None # rrulestr complains about unicode, so cast to str # a Ruby iCalendar library escapes semi-colons in rrules, # so also remove any backslashes value = str(line.value).replace('\\', '') rule = dateutil.rrule.rrulestr(value, dtstart=dtstart) until = rule._until if until is not None and \ isinstance(dtstart, datetime.datetime) and \ (until.tzinfo != dtstart.tzinfo): # dateutil converts the UNTIL date to a datetime, # check to see if the UNTIL parameter value was a date vals = dict(pair.split('=') for pair in line.value.upper().split(';')) if len(vals.get('UNTIL', '')) == 8: until = datetime.datetime.combine(until.date(), dtstart.time()) # While RFC2445 says UNTIL MUST be UTC, Chandler allows # floating recurring events, and uses floating UNTIL values. # Also, some odd floating UNTIL but timezoned DTSTART values # have shown up in the wild, so put floating UNTIL values # DTSTART's timezone if until.tzinfo is None: until = until.replace(tzinfo=dtstart.tzinfo) if dtstart.tzinfo is not None: until = until.astimezone(dtstart.tzinfo) rule._until = until # add the rrule or exrule to the rruleset addfunc(rule) if name == 'rrule' and addRDate: try: # dateutils does not work with all-day (datetime.date) items # so we need to convert to a datetime.datetime # (which is what dateutils does internally) if not isinstance(dtstart, datetime.datetime): adddtstart = datetime.datetime.fromordinal(dtstart.toordinal()) else: adddtstart = dtstart if rruleset._rrule[-1][0] != adddtstart: rruleset.rdate(adddtstart) added = True else: added = False except IndexError: # it's conceivable that an rrule might have 0 datetimes added = False if added and rruleset._rrule[-1]._count != None: rruleset._rrule[-1]._count -= 1 return rruleset def setrruleset(self, rruleset): # Get DTSTART from component (or DUE if no DTSTART in a VTODO) try: dtstart = self.dtstart.value except AttributeError, KeyError: if self.name == "VTODO": dtstart = self.due.value else: raise isDate = datetime.date == type(dtstart) if isDate: dtstart = datetime.datetime(dtstart.year,dtstart.month, dtstart.day) untilSerialize = dateToString else: # make sure to convert time zones to UTC untilSerialize = lambda x: dateTimeToString(x, True) for name in DATESANDRULES: if hasattr(self.contents, name): del self.contents[name] setlist = getattr(rruleset, '_' + name) if name in DATENAMES: setlist = list(setlist) # make a copy of the list if name == 'rdate' and dtstart in setlist: setlist.remove(dtstart) if isDate: setlist = [dt.date() for dt in setlist] if len(setlist) > 0: self.add(name).value = setlist elif name in RULENAMES: for rule in setlist: buf = StringIO.StringIO() buf.write('FREQ=') buf.write(FREQUENCIES[rule._freq]) values = {} if rule._interval != 1: values['INTERVAL'] = [str(rule._interval)] if rule._wkst != 0: # wkst defaults to Monday values['WKST'] = [WEEKDAYS[rule._wkst]] if rule._bysetpos is not None: values['BYSETPOS'] = [str(i) for i in rule._bysetpos] if rule._count is not None: values['COUNT'] = [str(rule._count)] elif rule._until is not None: values['UNTIL'] = [untilSerialize(rule._until)] days = [] if (rule._byweekday is not None and ( dateutil.rrule.WEEKLY != rule._freq or len(rule._byweekday) != 1 or rule._dtstart.weekday() != rule._byweekday[0])): # ignore byweekday if freq is WEEKLY and day correlates # with dtstart because it was automatically set by # dateutil days.extend(WEEKDAYS[n] for n in rule._byweekday) if rule._bynweekday is not None: days.extend(str(n) + WEEKDAYS[day] for day, n in rule._bynweekday) if len(days) > 0: values['BYDAY'] = days if rule._bymonthday is not None and len(rule._bymonthday) > 0: if not (rule._freq <= dateutil.rrule.MONTHLY and len(rule._bymonthday) == 1 and rule._bymonthday[0] == rule._dtstart.day): # ignore bymonthday if it's generated by dateutil values['BYMONTHDAY'] = [str(n) for n in rule._bymonthday] if rule._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 == dateutil.rrule.YEARLY and len(rule._bymonth) == 1 and rule._bymonth[0] == rule._dtstart.month)): # ignore bymonth if it's generated by dateutil values['BYMONTH'] = [str(n) for n in rule._bymonth] if rule._byyearday is not None: values['BYYEARDAY'] = [str(n) for n in rule._byyearday] if rule._byweekno is not None: values['BYWEEKNO'] = [str(n) for n in rule._byweekno] # byhour, byminute, bysecond are always ignored for now for key, paramvals in values.iteritems(): buf.write(';') buf.write(key) buf.write('=') buf.write(','.join(paramvals)) self.add(name).value = buf.getvalue() rruleset = property(getrruleset, setrruleset) def __setattr__(self, name, value): """For convenience, make self.contents directly accessible.""" if name == 'rruleset': self.setrruleset(value) else: super(RecurringComponent, self).__setattr__(name, value) class TextBehavior(behavior.Behavior): """Provide backslash escape encoding/decoding for single valued properties. TextBehavior also deals with base64 encoding if the ENCODING parameter is explicitly set to BASE64. """ base64string = 'BASE64' # vCard uses B @classmethod def decode(cls, line): """Remove backslash escaping from line.value.""" if line.encoded: encoding = getattr(line, 'encoding_param', None) if encoding and encoding.upper() == cls.base64string: line.value = line.value.decode('base64') else: line.value = stringToTextValues(line.value)[0] line.encoded=False @classmethod def encode(cls, line): """Backslash escape line.value.""" if not line.encoded: encoding = getattr(line, 'encoding_param', None) if encoding and encoding.upper() == cls.base64string: line.value = line.value.encode('base64').replace('\n', '') else: line.value = backslashEscape(line.value) line.encoded=True class VCalendarComponentBehavior(behavior.Behavior): defaultBehavior = TextBehavior isComponent = True class RecurringBehavior(VCalendarComponentBehavior): """Parent Behavior for components which should be RecurringComponents.""" hasNative = True @staticmethod def transformToNative(obj): """Turn a recurring Component into a RecurringComponent.""" if not obj.isNative: object.__setattr__(obj, '__class__', RecurringComponent) obj.isNative = True return obj @staticmethod def transformFromNative(obj): if obj.isNative: object.__setattr__(obj, '__class__', Component) obj.isNative = False return obj @staticmethod def generateImplicitParameters(obj): """Generate a UID if one does not exist. This is just a dummy implementation, for now. """ if not hasattr(obj, 'uid'): rand = str(int(random.random() * 100000)) now = datetime.datetime.now(utc) now = dateTimeToString(now) host = socket.gethostname() obj.add(ContentLine('UID', [], now + '-' + rand + '@' + host)) class DateTimeBehavior(behavior.Behavior): """Parent Behavior for ContentLines containing one DATE-TIME.""" hasNative = True @staticmethod def transformToNative(obj): """Turn obj.value into a datetime. RFC2445 allows times without time zone information, "floating times" in some properties. Mostly, this isn't what you want, but when parsing a file, real floating times are noted by setting to 'TRUE' the X-VOBJ-FLOATINGTIME-ALLOWED parameter. """ if obj.isNative: return obj obj.isNative = True if obj.value == '': return obj obj.value=str(obj.value) #we're cheating a little here, parseDtstart allows DATE obj.value=parseDtstart(obj) if obj.value.tzinfo is None: obj.params['X-VOBJ-FLOATINGTIME-ALLOWED'] = ['TRUE'] if obj.params.get('TZID'): # Keep a copy of the original TZID around obj.params['X-VOBJ-ORIGINAL-TZID'] = [obj.params['TZID']] del obj.params['TZID'] return obj @classmethod def transformFromNative(cls, obj): """Replace the datetime in obj.value with an ISO 8601 string.""" if obj.isNative: obj.isNative = False tzid = TimezoneComponent.registerTzinfo(obj.value.tzinfo) obj.value = dateTimeToString(obj.value, cls.forceUTC) if not cls.forceUTC and tzid is not None: obj.tzid_param = tzid if obj.params.get('X-VOBJ-ORIGINAL-TZID'): if not hasattr(obj, 'tzid_param'): obj.tzid_param = obj.x_vobj_original_tzid_param del obj.params['X-VOBJ-ORIGINAL-TZID'] return obj class UTCDateTimeBehavior(DateTimeBehavior): """A value which must be specified in UTC.""" forceUTC = True class DateOrDateTimeBehavior(behavior.Behavior): """Parent Behavior for ContentLines containing one DATE or DATE-TIME.""" hasNative = True @staticmethod def transformToNative(obj): """Turn obj.value into a date or datetime.""" if obj.isNative: return obj obj.isNative = True if obj.value == '': return obj obj.value=str(obj.value) obj.value=parseDtstart(obj, allowSignatureMismatch=True) if getattr(obj, 'value_param', 'DATE-TIME').upper() == 'DATE-TIME': if hasattr(obj, 'tzid_param'): # Keep a copy of the original TZID around obj.params['X-VOBJ-ORIGINAL-TZID'] = [obj.tzid_param] del obj.tzid_param return obj @staticmethod def transformFromNative(obj): """Replace the date or datetime in obj.value with an ISO 8601 string.""" if type(obj.value) == datetime.date: obj.isNative = False obj.value_param = 'DATE' obj.value = dateToString(obj.value) return obj else: return DateTimeBehavior.transformFromNative(obj) class MultiDateBehavior(behavior.Behavior): """ Parent Behavior for ContentLines containing one or more DATE, DATE-TIME, or PERIOD. """ hasNative = True @staticmethod def transformToNative(obj): """ Turn obj.value into a list of dates, datetimes, or (datetime, timedelta) tuples. """ if obj.isNative: return obj obj.isNative = True if obj.value == '': obj.value = [] return obj tzinfo = getTzid(getattr(obj, 'tzid_param', None)) valueParam = getattr(obj, 'value_param', "DATE-TIME").upper() valTexts = obj.value.split(",") if valueParam == "DATE": obj.value = [stringToDate(x) for x in valTexts] elif valueParam == "DATE-TIME": obj.value = [stringToDateTime(x, tzinfo) for x in valTexts] elif valueParam == "PERIOD": obj.value = [stringToPeriod(x, tzinfo) for x in valTexts] return obj @staticmethod def transformFromNative(obj): """ Replace the date, datetime or period tuples in obj.value with appropriate strings. """ if obj.value and type(obj.value[0]) == datetime.date: obj.isNative = False obj.value_param = 'DATE' obj.value = ','.join([dateToString(val) for val in obj.value]) return obj # Fixme: handle PERIOD case else: if obj.isNative: obj.isNative = False transformed = [] tzid = None for val in obj.value: if tzid is None and type(val) == datetime.datetime: tzid = TimezoneComponent.registerTzinfo(val.tzinfo) if tzid is not None: obj.tzid_param = tzid transformed.append(dateTimeToString(val)) obj.value = ','.join(transformed) return obj class MultiTextBehavior(behavior.Behavior): """Provide backslash escape encoding/decoding of each of several values. After transformation, value is a list of strings. """ listSeparator = "," @classmethod def decode(cls, line): """Remove backslash escaping from line.value, then split on commas.""" if line.encoded: line.value = stringToTextValues(line.value, listSeparator=cls.listSeparator) line.encoded=False @classmethod def encode(cls, line): """Backslash escape line.value.""" if not line.encoded: line.value = cls.listSeparator.join(backslashEscape(val) for val in line.value) line.encoded=True class SemicolonMultiTextBehavior(MultiTextBehavior): listSeparator = ";" #------------------------ Registered Behavior subclasses ----------------------- class VCalendar2_0(VCalendarComponentBehavior): """vCalendar 2.0 behavior. With added VAVAILABILITY support.""" name = 'VCALENDAR' description = 'vCalendar 2.0, also known as iCalendar.' versionString = '2.0' sortFirst = ('version', 'calscale', 'method', 'prodid', 'vtimezone') knownChildren = {'CALSCALE': (0, 1, None),#min, max, behaviorRegistry id 'METHOD': (0, 1, None), 'VERSION': (0, 1, None),#required, but auto-generated 'PRODID': (1, 1, None), 'VTIMEZONE': (0, None, None), 'VEVENT': (0, None, None), 'VTODO': (0, None, None), 'VJOURNAL': (0, None, None), 'VFREEBUSY': (0, None, None), 'VAVAILABILITY': (0, None, None), } @classmethod def generateImplicitParameters(cls, obj): """Create PRODID, VERSION, and VTIMEZONEs if needed. VTIMEZONEs will need to exist whenever TZID parameters exist or when datetimes with tzinfo exist. """ for comp in obj.components(): if comp.behavior is not None: comp.behavior.generateImplicitParameters(comp) if not hasattr(obj, 'prodid'): obj.add(ContentLine('PRODID', [], PRODID)) if not hasattr(obj, 'version'): obj.add(ContentLine('VERSION', [], cls.versionString)) tzidsUsed = {} def findTzids(obj, table): if isinstance(obj, ContentLine) and (obj.behavior is None or not obj.behavior.forceUTC): if getattr(obj, 'tzid_param', None): table[obj.tzid_param] = 1 else: if type(obj.value) == list: for item in obj.value: tzinfo = getattr(obj.value, 'tzinfo', None) tzid = TimezoneComponent.registerTzinfo(tzinfo) if tzid: table[tzid] = 1 else: tzinfo = getattr(obj.value, 'tzinfo', None) tzid = TimezoneComponent.registerTzinfo(tzinfo) if tzid: table[tzid] = 1 for child in obj.getChildren(): if obj.name != 'VTIMEZONE': findTzids(child, table) findTzids(obj, tzidsUsed) oldtzids = [toUnicode(x.tzid.value) for x in getattr(obj, 'vtimezone_list', [])] for tzid in tzidsUsed.keys(): tzid = toUnicode(tzid) if tzid != u'UTC' and tzid not in oldtzids: obj.add(TimezoneComponent(tzinfo=getTzid(tzid))) registerBehavior(VCalendar2_0) class VTimezone(VCalendarComponentBehavior): """Timezone behavior.""" name = 'VTIMEZONE' hasNative = True description = 'A grouping of component properties that defines a time zone.' sortFirst = ('tzid', 'last-modified', 'tzurl', 'standard', 'daylight') knownChildren = {'TZID': (1, 1, None),#min, max, behaviorRegistry id 'LAST-MODIFIED':(0, 1, None), 'TZURL': (0, 1, None), 'STANDARD': (0, None, None),#NOTE: One of Standard or 'DAYLIGHT': (0, None, None) # Daylight must appear } @classmethod def validate(cls, obj, raiseException, *args): if not hasattr(obj, 'tzid') or obj.tzid.value is None: if raiseException: m = "VTIMEZONE components must contain a valid TZID" raise ValidateError(m) return False if obj.contents.has_key('standard') or obj.contents.has_key('daylight'): return super(VTimezone, cls).validate(obj, raiseException, *args) else: if raiseException: m = "VTIMEZONE components must contain a STANDARD or a DAYLIGHT\ component" raise ValidateError(m) return False @staticmethod def transformToNative(obj): if not obj.isNative: object.__setattr__(obj, '__class__', TimezoneComponent) obj.isNative = True obj.registerTzinfo(obj.tzinfo) return obj @staticmethod def transformFromNative(obj): return obj registerBehavior(VTimezone) class TZID(behavior.Behavior): """Don't use TextBehavior for TZID. RFC2445 only allows TZID lines to be paramtext, so they shouldn't need any encoding or decoding. Unfortunately, some Microsoft products use commas in TZIDs which should NOT be treated as a multi-valued text property, nor do we want to escape them. Leaving them alone works for Microsoft's breakage, and doesn't affect compliant iCalendar streams. """ registerBehavior(TZID) class DaylightOrStandard(VCalendarComponentBehavior): hasNative = False knownChildren = {'DTSTART': (1, 1, None),#min, max, behaviorRegistry id 'RRULE': (0, 1, None)} registerBehavior(DaylightOrStandard, 'STANDARD') registerBehavior(DaylightOrStandard, 'DAYLIGHT') class VEvent(RecurringBehavior): """Event behavior.""" name='VEVENT' sortFirst = ('uid', 'recurrence-id', 'dtstart', 'duration', 'dtend') description='A grouping of component properties, and possibly including \ "VALARM" calendar components, that represents a scheduled \ amount of time on a calendar.' knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id 'CLASS': (0, 1, None), 'CREATED': (0, 1, None), 'DESCRIPTION': (0, 1, None), 'GEO': (0, 1, None), 'LAST-MODIFIED':(0, 1, None), 'LOCATION': (0, 1, None), 'ORGANIZER': (0, 1, None), 'PRIORITY': (0, 1, None), 'DTSTAMP': (0, 1, None), 'SEQUENCE': (0, 1, None), 'STATUS': (0, 1, None), 'SUMMARY': (0, 1, None), 'TRANSP': (0, 1, None), 'UID': (1, 1, None), 'URL': (0, 1, None), 'RECURRENCE-ID':(0, 1, None), 'DTEND': (0, 1, None), #NOTE: Only one of DtEnd or 'DURATION': (0, 1, None), # Duration can appear 'ATTACH': (0, None, None), 'ATTENDEE': (0, None, None), 'CATEGORIES': (0, None, None), 'COMMENT': (0, None, None), 'CONTACT': (0, None, None), 'EXDATE': (0, None, None), 'EXRULE': (0, None, None), 'REQUEST-STATUS': (0, None, None), 'RELATED-TO': (0, None, None), 'RESOURCES': (0, None, None), 'RDATE': (0, None, None), 'RRULE': (0, None, None), 'VALARM': (0, None, None) } @classmethod def validate(cls, obj, raiseException, *args): if obj.contents.has_key('dtend') and obj.contents.has_key('duration'): if raiseException: m = "VEVENT components cannot contain both DTEND and DURATION\ components" raise ValidateError(m) return False else: return super(VEvent, cls).validate(obj, raiseException, *args) registerBehavior(VEvent) class VTodo(RecurringBehavior): """To-do behavior.""" name='VTODO' description='A grouping of component properties and possibly "VALARM" \ calendar components that represent an action-item or \ assignment.' knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id 'CLASS': (0, 1, None), 'COMPLETED': (0, 1, None), 'CREATED': (0, 1, None), 'DESCRIPTION': (0, 1, None), 'GEO': (0, 1, None), 'LAST-MODIFIED':(0, 1, None), 'LOCATION': (0, 1, None), 'ORGANIZER': (0, 1, None), 'PERCENT': (0, 1, None), 'PRIORITY': (0, 1, None), 'DTSTAMP': (0, 1, None), 'SEQUENCE': (0, 1, None), 'STATUS': (0, 1, None), 'SUMMARY': (0, 1, None), 'UID': (0, 1, None), 'URL': (0, 1, None), 'RECURRENCE-ID':(0, 1, None), 'DUE': (0, 1, None), #NOTE: Only one of Due or 'DURATION': (0, 1, None), # Duration can appear 'ATTACH': (0, None, None), 'ATTENDEE': (0, None, None), 'CATEGORIES': (0, None, None), 'COMMENT': (0, None, None), 'CONTACT': (0, None, None), 'EXDATE': (0, None, None), 'EXRULE': (0, None, None), 'REQUEST-STATUS': (0, None, None), 'RELATED-TO': (0, None, None), 'RESOURCES': (0, None, None), 'RDATE': (0, None, None), 'RRULE': (0, None, None), 'VALARM': (0, None, None) } @classmethod def validate(cls, obj, raiseException, *args): if obj.contents.has_key('due') and obj.contents.has_key('duration'): if raiseException: m = "VTODO components cannot contain both DUE and DURATION\ components" raise ValidateError(m) return False else: return super(VTodo, cls).validate(obj, raiseException, *args) registerBehavior(VTodo) class VJournal(RecurringBehavior): """Journal entry behavior.""" name='VJOURNAL' knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id 'CLASS': (0, 1, None), 'CREATED': (0, 1, None), 'DESCRIPTION': (0, 1, None), 'LAST-MODIFIED':(0, 1, None), 'ORGANIZER': (0, 1, None), 'DTSTAMP': (0, 1, None), 'SEQUENCE': (0, 1, None), 'STATUS': (0, 1, None), 'SUMMARY': (0, 1, None), 'UID': (0, 1, None), 'URL': (0, 1, None), 'RECURRENCE-ID':(0, 1, None), 'ATTACH': (0, None, None), 'ATTENDEE': (0, None, None), 'CATEGORIES': (0, None, None), 'COMMENT': (0, None, None), 'CONTACT': (0, None, None), 'EXDATE': (0, None, None), 'EXRULE': (0, None, None), 'REQUEST-STATUS': (0, None, None), 'RELATED-TO': (0, None, None), 'RDATE': (0, None, None), 'RRULE': (0, None, None) } registerBehavior(VJournal) class VFreeBusy(VCalendarComponentBehavior): """Free/busy state behavior. >>> vfb = newFromBehavior('VFREEBUSY') >>> vfb.add('uid').value = 'test' >>> vfb.add('dtstart').value = datetime.datetime(2006, 2, 16, 1, tzinfo=utc) >>> vfb.add('dtend').value = vfb.dtstart.value + twoHours >>> vfb.add('freebusy').value = [(vfb.dtstart.value, twoHours / 2)] >>> vfb.add('freebusy').value = [(vfb.dtstart.value, vfb.dtend.value)] >>> print vfb.serialize() BEGIN:VFREEBUSY UID:test DTSTART:20060216T010000Z DTEND:20060216T030000Z FREEBUSY:20060216T010000Z/PT1H FREEBUSY:20060216T010000Z/20060216T030000Z END:VFREEBUSY """ name='VFREEBUSY' description='A grouping of component properties that describe either a \ request for free/busy time, describe a response to a request \ for free/busy time or describe a published set of busy time.' sortFirst = ('uid', 'dtstart', 'duration', 'dtend') knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id 'CONTACT': (0, 1, None), 'DTEND': (0, 1, None), 'DURATION': (0, 1, None), 'ORGANIZER': (0, 1, None), 'DTSTAMP': (0, 1, None), 'UID': (0, 1, None), 'URL': (0, 1, None), 'ATTENDEE': (0, None, None), 'COMMENT': (0, None, None), 'FREEBUSY': (0, None, None), 'REQUEST-STATUS': (0, None, None) } registerBehavior(VFreeBusy) class VAlarm(VCalendarComponentBehavior): """Alarm behavior.""" name='VALARM' description='Alarms describe when and how to provide alerts about events \ and to-dos.' knownChildren = {'ACTION': (1, 1, None),#min, max, behaviorRegistry id 'TRIGGER': (1, 1, None), 'DURATION': (0, 1, None), 'REPEAT': (0, 1, None), 'DESCRIPTION': (0, 1, None) } @staticmethod def generateImplicitParameters(obj): """Create default ACTION and TRIGGER if they're not set.""" try: obj.action except AttributeError: obj.add('action').value = 'AUDIO' try: obj.trigger except AttributeError: obj.add('trigger').value = datetime.timedelta(0) @classmethod def validate(cls, obj, raiseException, *args): """ #TODO audioprop = 2*( ; 'action' and 'trigger' are both REQUIRED, ; but MUST NOT occur more than once action / trigger / ; 'duration' and 'repeat' are both optional, ; and MUST NOT occur more than once each, ; but if one occurs, so MUST the other duration / repeat / ; the following is optional, ; but MUST NOT occur more than once attach / dispprop = 3*( ; the following are all REQUIRED, ; but MUST NOT occur more than once action / description / trigger / ; 'duration' and 'repeat' are both optional, ; and MUST NOT occur more than once each, ; but if one occurs, so MUST the other duration / repeat / emailprop = 5*( ; the following are all REQUIRED, ; but MUST NOT occur more than once action / description / trigger / summary ; the following is REQUIRED, ; and MAY occur more than once attendee / ; 'duration' and 'repeat' are both optional, ; and MUST NOT occur more than once each, ; but if one occurs, so MUST the other duration / repeat / procprop = 3*( ; the following are all REQUIRED, ; but MUST NOT occur more than once action / attach / trigger / ; 'duration' and 'repeat' are both optional, ; and MUST NOT occur more than once each, ; but if one occurs, so MUST the other duration / repeat / ; 'description' is optional, ; and MUST NOT occur more than once description / if obj.contents.has_key('dtend') and obj.contents.has_key('duration'): if raiseException: m = "VEVENT components cannot contain both DTEND and DURATION\ components" raise ValidateError(m) return False else: return super(VEvent, cls).validate(obj, raiseException, *args) """ return True registerBehavior(VAlarm) class VAvailability(VCalendarComponentBehavior): """Availability state behavior. >>> vav = newFromBehavior('VAVAILABILITY') >>> vav.add('uid').value = 'test' >>> vav.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) >>> vav.add('dtstart').value = datetime.datetime(2006, 2, 16, 0, tzinfo=utc) >>> vav.add('dtend').value = datetime.datetime(2006, 2, 17, 0, tzinfo=utc) >>> vav.add('busytype').value = "BUSY" >>> av = newFromBehavior('AVAILABLE') >>> av.add('uid').value = 'test1' >>> av.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) >>> av.add('dtstart').value = datetime.datetime(2006, 2, 16, 9, tzinfo=utc) >>> av.add('dtend').value = datetime.datetime(2006, 2, 16, 12, tzinfo=utc) >>> av.add('summary').value = "Available in the morning" >>> ignore = vav.add(av) >>> print vav.serialize() BEGIN:VAVAILABILITY UID:test DTSTART:20060216T000000Z DTEND:20060217T000000Z BEGIN:AVAILABLE UID:test1 DTSTART:20060216T090000Z DTEND:20060216T120000Z DTSTAMP:20060215T000000Z SUMMARY:Available in the morning END:AVAILABLE BUSYTYPE:BUSY DTSTAMP:20060215T000000Z END:VAVAILABILITY """ name='VAVAILABILITY' description='A component used to represent a user\'s available time slots.' sortFirst = ('uid', 'dtstart', 'duration', 'dtend') knownChildren = {'UID': (1, 1, None),#min, max, behaviorRegistry id 'DTSTAMP': (1, 1, None), 'BUSYTYPE': (0, 1, None), 'CREATED': (0, 1, None), 'DTSTART': (0, 1, None), 'LAST-MODIFIED': (0, 1, None), 'ORGANIZER': (0, 1, None), 'SEQUENCE': (0, 1, None), 'SUMMARY': (0, 1, None), 'URL': (0, 1, None), 'DTEND': (0, 1, None), 'DURATION': (0, 1, None), 'CATEGORIES': (0, None, None), 'COMMENT': (0, None, None), 'CONTACT': (0, None, None), 'AVAILABLE': (0, None, None), } @classmethod def validate(cls, obj, raiseException, *args): if obj.contents.has_key('dtend') and obj.contents.has_key('duration'): if raiseException: m = "VAVAILABILITY components cannot contain both DTEND and DURATION\ components" raise ValidateError(m) return False else: return super(VAvailability, cls).validate(obj, raiseException, *args) registerBehavior(VAvailability) class Available(RecurringBehavior): """Event behavior.""" name='AVAILABLE' sortFirst = ('uid', 'recurrence-id', 'dtstart', 'duration', 'dtend') description='Defines a period of time in which a user is normally available.' knownChildren = {'DTSTAMP': (1, 1, None),#min, max, behaviorRegistry id 'DTSTART': (1, 1, None), 'UID': (1, 1, None), 'DTEND': (0, 1, None), #NOTE: One of DtEnd or 'DURATION': (0, 1, None), # Duration must appear, but not both 'CREATED': (0, 1, None), 'LAST-MODIFIED':(0, 1, None), 'RECURRENCE-ID':(0, 1, None), 'RRULE': (0, 1, None), 'SUMMARY': (0, 1, None), 'CATEGORIES': (0, None, None), 'COMMENT': (0, None, None), 'CONTACT': (0, None, None), 'EXDATE': (0, None, None), 'RDATE': (0, None, None), } @classmethod def validate(cls, obj, raiseException, *args): has_dtend = obj.contents.has_key('dtend') has_duration = obj.contents.has_key('duration') if has_dtend and has_duration: if raiseException: m = "AVAILABLE components cannot contain both DTEND and DURATION\ properties" raise ValidateError(m) return False elif not (has_dtend or has_duration): if raiseException: m = "AVAILABLE components must contain one of DTEND or DURATION\ properties" raise ValidateError(m) return False else: return super(Available, cls).validate(obj, raiseException, *args) registerBehavior(Available) class Duration(behavior.Behavior): """Behavior for Duration ContentLines. Transform to datetime.timedelta.""" name = 'DURATION' hasNative = True @staticmethod def transformToNative(obj): """Turn obj.value into a datetime.timedelta.""" if obj.isNative: return obj obj.isNative = True obj.value=str(obj.value) if obj.value == '': return obj else: deltalist=stringToDurations(obj.value) #When can DURATION have multiple durations? For now: if len(deltalist) == 1: obj.value = deltalist[0] return obj else: raise ParseError("DURATION must have a single duration string.") @staticmethod def transformFromNative(obj): """Replace the datetime.timedelta in obj.value with an RFC2445 string. """ if not obj.isNative: return obj obj.isNative = False obj.value = timedeltaToString(obj.value) return obj registerBehavior(Duration) class Trigger(behavior.Behavior): """DATE-TIME or DURATION""" name='TRIGGER' description='This property specifies when an alarm will trigger.' hasNative = True forceUTC = True @staticmethod def transformToNative(obj): """Turn obj.value into a timedelta or datetime.""" if obj.isNative: return obj value = getattr(obj, 'value_param', 'DURATION').upper() if hasattr(obj, 'value_param'): del obj.value_param if obj.value == '': obj.isNative = True return obj elif value == 'DURATION': try: return Duration.transformToNative(obj) except ParseError: logger.warn("TRIGGER not recognized as DURATION, trying " "DATE-TIME, because iCal sometimes exports " "DATE-TIMEs without setting VALUE=DATE-TIME") try: obj.isNative = False dt = DateTimeBehavior.transformToNative(obj) return dt except: msg = "TRIGGER with no VALUE not recognized as DURATION " \ "or as DATE-TIME" raise ParseError(msg) elif value == 'DATE-TIME': #TRIGGERs with DATE-TIME values must be in UTC, we could validate #that fact, for now we take it on faith. return DateTimeBehavior.transformToNative(obj) else: raise ParseError("VALUE must be DURATION or DATE-TIME") @staticmethod def transformFromNative(obj): if type(obj.value) == datetime.datetime: obj.value_param = 'DATE-TIME' return UTCDateTimeBehavior.transformFromNative(obj) elif type(obj.value) == datetime.timedelta: return Duration.transformFromNative(obj) else: raise NativeError("Native TRIGGER values must be timedelta or datetime") registerBehavior(Trigger) class PeriodBehavior(behavior.Behavior): """A list of (date-time, timedelta) tuples. >>> line = ContentLine('test', [], '', isNative=True) >>> line.behavior = PeriodBehavior >>> line.value = [(datetime.datetime(2006, 2, 16, 10), twoHours)] >>> line.transformFromNative().value '20060216T100000/PT2H' >>> line.transformToNative().value [(datetime.datetime(2006, 2, 16, 10, 0), datetime.timedelta(0, 7200))] >>> line.value.append((datetime.datetime(2006, 5, 16, 10), twoHours)) >>> print line.serialize().strip() TEST:20060216T100000/PT2H,20060516T100000/PT2H """ hasNative = True @staticmethod def transformToNative(obj): """Convert comma separated periods into tuples.""" if obj.isNative: return obj obj.isNative = True if obj.value == '': obj.value = [] return obj tzinfo = getTzid(getattr(obj, 'tzid_param', None)) obj.value = [stringToPeriod(x, tzinfo) for x in obj.value.split(",")] return obj @classmethod def transformFromNative(cls, obj): """Convert the list of tuples in obj.value to strings.""" if obj.isNative: obj.isNative = False transformed = [] for tup in obj.value: transformed.append(periodToString(tup, cls.forceUTC)) if len(transformed) > 0: tzid = TimezoneComponent.registerTzinfo(tup[0].tzinfo) if not cls.forceUTC and tzid is not None: obj.tzid_param = tzid obj.value = ','.join(transformed) return obj class FreeBusy(PeriodBehavior): """Free or busy period of time, must be specified in UTC.""" name = 'FREEBUSY' forceUTC = True registerBehavior(FreeBusy) class RRule(behavior.Behavior): """ Dummy behavior to avoid having RRULEs being treated as text lines (and thus having semi-colons inaccurately escaped). """ registerBehavior(RRule, 'RRULE') registerBehavior(RRule, 'EXRULE') #------------------------ Registration of common classes ----------------------- utcDateTimeList = ['LAST-MODIFIED', 'CREATED', 'COMPLETED', 'DTSTAMP'] map(lambda x: registerBehavior(UTCDateTimeBehavior, x), utcDateTimeList) dateTimeOrDateList = ['DTEND', 'DTSTART', 'DUE', 'RECURRENCE-ID'] map(lambda x: registerBehavior(DateOrDateTimeBehavior, x), dateTimeOrDateList) registerBehavior(MultiDateBehavior, 'RDATE') registerBehavior(MultiDateBehavior, 'EXDATE') textList = ['CALSCALE', 'METHOD', 'PRODID', 'CLASS', 'COMMENT', 'DESCRIPTION', 'LOCATION', 'STATUS', 'SUMMARY', 'TRANSP', 'CONTACT', 'RELATED-TO', 'UID', 'ACTION', 'BUSYTYPE'] map(lambda x: registerBehavior(TextBehavior, x), textList) multiTextList = ['CATEGORIES', 'RESOURCES'] map(lambda x: registerBehavior(MultiTextBehavior, x), multiTextList) 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 rfc2445 DURATION.""" if delta.days == 0: sign = 1 else: sign = delta.days / abs(delta.days) delta = abs(delta) days = delta.days hours = delta.seconds / 3600 minutes = (delta.seconds % 3600) / 60 seconds = delta.seconds % 60 out = '' if sign == -1: out = '-' out += 'P' if days: out += str(days) + 'D' if hours or minutes or seconds: out += 'T' elif not days: #Deal with zero duration out += 'T0S' if hours: out += str(hours) + 'H' if minutes: out += str(minutes) + 'M' if seconds: out += str(seconds) + 'S' return out def timeToString(dateOrDateTime): """ Wraps dateToString and dateTimeToString, returning the results of either based on the type of the argument """ # Didn't use isinstance here as date and datetime sometimes evalutes as both if (type(dateOrDateTime) == datetime.date): return dateToString(dateOrDateTime) elif(type(dateOrDateTime) == datetime.datetime): return dateTimeToString(dateOrDateTime) def dateToString(date): year = numToDigits( date.year, 4 ) month = numToDigits( date.month, 2 ) day = numToDigits( date.day, 2 ) return year + month + day def dateTimeToString(dateTime, convertToUTC=False): """Ignore tzinfo unless convertToUTC. Output string.""" if dateTime.tzinfo and convertToUTC: dateTime = dateTime.astimezone(utc) if tzinfo_eq(dateTime.tzinfo, utc): utcString = "Z" else: utcString = "" year = numToDigits( dateTime.year, 4 ) month = numToDigits( dateTime.month, 2 ) day = numToDigits( dateTime.day, 2 ) hour = numToDigits( dateTime.hour, 2 ) mins = numToDigits( dateTime.minute, 2 ) secs = numToDigits( dateTime.second, 2 ) return year + month + day + "T" + hour + mins + secs + utcString def deltaToOffset(delta): absDelta = abs(delta) hours = absDelta.seconds / 3600 hoursString = numToDigits(hours, 2) minutesString = '00' if absDelta == delta: signString = "+" else: signString = "-" return signString + hoursString + minutesString def periodToString(period, convertToUTC=False): txtstart = dateTimeToString(period[0], convertToUTC) if isinstance(period[1], datetime.timedelta): txtend = timedeltaToString(period[1]) else: txtend = dateTimeToString(period[1], convertToUTC) return txtstart + "/" + txtend #----------------------- Parsing functions ------------------------------------- def isDuration(s): s = string.upper(s) return (string.find(s, "P") != -1) and (string.find(s, "P") < 2) def stringToDate(s): year = int( s[0:4] ) month = int( s[4:6] ) day = int( s[6:8] ) return datetime.date(year,month,day) def stringToDateTime(s, tzinfo=None): """Returns datetime.datetime object.""" try: year = int( s[0:4] ) month = int( s[4:6] ) day = int( s[6:8] ) hour = int( s[9:11] ) minute = int( s[11:13] ) second = int( s[13:15] ) if len(s) > 15: if s[15] == 'Z': tzinfo = utc except: raise ParseError("'%s' is not a valid DATE-TIME" % s) return datetime.datetime(year, month, day, hour, minute, second, 0, tzinfo) # 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: #logger.error(msg) print msg #vars which control state machine charIterator = enumerate(s) state = "read normal" current = [] results = [] while True: try: charIndex, char = charIterator.next() except: char = "eof" if state == "read normal": if char == '\\': state = "read escaped char" elif char == listSeparator: state = "read normal" 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("error: unknown state: '%s' reached in %s" % (state, s)) def stringToDurations(s, strict=False): """Returns list of timedelta objects.""" def makeTimedelta(sign, week, day, hour, minute, sec): if sign == "-": sign = -1 else: sign = 1 week = int(week) day = int(day) hour = int(hour) minute = int(minute) sec = int(sec) return sign * datetime.timedelta(weeks=week, days=day, hours=hour, minutes=minute, seconds=sec) def error(msg): if strict: raise ParseError(msg) else: raise ParseError(msg) #logger.error(msg) #vars which control state machine charIterator = enumerate(s) state = "start" durations = [] current = "" sign = None week = 0 day = 0 hour = 0 minute = 0 sec = 0 while True: try: charIndex, char = charIterator.next() except: charIndex += 1 char = "eof" if state == "start": if char == '+': state = "start" sign = char elif char == '-': state = "start" sign = char elif char.upper() == 'P': state = "read field" elif char == "eof": state = "error" error("got end-of-line while reading in duration: " + s) elif char in string.digits: state = "read field" current = current + char #update this part when updating "read field" else: state = "error" print "got unexpected character %s reading in duration: %s" % (char, s) error("got unexpected character %s reading in duration: %s" % (char, s)) elif state == "read field": if (char in string.digits): state = "read field" current = current + char #update part above when updating "read field" elif char.upper() == 'T': state = "read field" elif char.upper() == 'W': state = "read field" week = current current = "" elif char.upper() == 'D': state = "read field" day = current current = "" elif char.upper() == 'H': state = "read field" hour = current current = "" elif char.upper() == 'M': state = "read field" minute = current current = "" elif char.upper() == 'S': state = "read field" sec = current current = "" elif char == ",": state = "start" durations.append( makeTimedelta(sign, week, day, hour, minute, sec) ) current = "" sign = None week = None day = None hour = None minute = None sec = None elif char == "eof": state = "end" else: state = "error" error("got unexpected character reading in duration: " + s) elif state == "end": #an end state #print "stuff: %s, durations: %s" % ([current, sign, week, day, hour, minute, sec], durations) if (sign or week or day or hour or minute or sec): durations.append( makeTimedelta(sign, week, day, hour, minute, sec) ) return durations elif state == "error": #an end state error("in error state") return durations else: state = "error" error("error: unknown state: '%s' reached in %s" % (state, s)) def parseDtstart(contentline, allowSignatureMismatch=False): """Convert a contentline's value into a date or date-time. A variety of clients don't serialize dates with the appropriate VALUE parameter, so rather than failing on these (technically invalid) lines, if allowSignatureMismatch is True, try to parse both varieties. """ tzinfo = getTzid(getattr(contentline, 'tzid_param', None)) valueParam = getattr(contentline, 'value_param', 'DATE-TIME').upper() if valueParam == "DATE": return stringToDate(contentline.value) elif valueParam == "DATE-TIME": try: return stringToDateTime(contentline.value, tzinfo) except: if allowSignatureMismatch: return stringToDate(contentline.value) else: raise def stringToPeriod(s, tzinfo=None): values = string.split(s, "/") start = stringToDateTime(values[0], tzinfo) valEnd = values[1] if isDuration(valEnd): #period-start = date-time "/" dur-value delta = stringToDurations(valEnd)[0] return (start, delta) else: return (start, stringToDateTime(valEnd, tzinfo)) def getTransition(transitionTo, year, tzinfo): """Return the datetime of the transition to/from DST, or None.""" def firstTransition(iterDates, test): """ Return the last date not matching test, or None if all tests matched. """ success = None for dt in iterDates: if not test(dt): success = dt else: if success is not None: return success return success # may be None def generateDates(year, month=None, day=None): """Iterate over possible dates with unspecified values.""" months = range(1, 13) days = range(1, 32) hours = range(0, 24) if month is None: for month in months: yield datetime.datetime(year, month, 1) elif day is None: for day in days: try: yield datetime.datetime(year, month, day) except ValueError: pass else: for hour in hours: yield datetime.datetime(year, month, day, hour) assert transitionTo in ('daylight', 'standard') if transitionTo == 'daylight': def test(dt): return tzinfo.dst(dt) != zeroDelta elif transitionTo == 'standard': def test(dt): return tzinfo.dst(dt) == zeroDelta newyear = datetime.datetime(year, 1, 1) monthDt = firstTransition(generateDates(year), test) if monthDt is None: return newyear elif monthDt.month == 12: return None else: # there was a good transition somewhere in a non-December month month = monthDt.month day = firstTransition(generateDates(year, month), test).day uncorrected = firstTransition(generateDates(year, month, day), test) if transitionTo == 'standard': # assuming tzinfo.dst returns a new offset for the first # possible hour, we need to add one hour for the offset change # and another hour because firstTransition returns the hour # before the transition return uncorrected + datetime.timedelta(hours=2) else: return uncorrected + datetime.timedelta(hours=1) def tzinfo_eq(tzinfo1, tzinfo2, startYear = 2000, endYear=2020): """Compare offsets and DST transitions from startYear to endYear.""" if tzinfo1 == tzinfo2: return True elif tzinfo1 is None or tzinfo2 is None: return False def dt_test(dt): if dt is None: return True return tzinfo1.utcoffset(dt) == tzinfo2.utcoffset(dt) if not dt_test(datetime.datetime(startYear, 1, 1)): return False for year in xrange(startYear, endYear): for transitionTo in 'daylight', 'standard': t1=getTransition(transitionTo, year, tzinfo1) t2=getTransition(transitionTo, year, tzinfo2) if t1 != t2 or not dt_test(t1): return False return True #------------------- Testing and running functions ----------------------------- if __name__ == '__main__': import tests tests._test() vobject-0.8.1c/vobject/ics_diff.py0000644000076500001200000001762011017337652016351 0ustar chandleadmin"""Compare VTODOs and VEVENTs in two iCalendar sources.""" from base import Component, getBehavior, newFromBehavior def getSortKey(component): def getUID(component): return component.getChildValue('uid', '') # it's not quite as simple as getUID, need to account for recurrenceID and # sequence def getSequence(component): sequence = component.getChildValue('sequence', 0) return "%05d" % int(sequence) def getRecurrenceID(component): recurrence_id = component.getChildValue('recurrence_id', None) if recurrence_id is None: return '0000-00-00' else: return recurrence_id.isoformat() return getUID(component) + getSequence(component) + getRecurrenceID(component) def sortByUID(components): return sorted(components, key=getSortKey) def deleteExtraneous(component, ignore_dtstamp=False): """ Recursively walk the component's children, deleting extraneous details like X-VOBJ-ORIGINAL-TZID. """ for comp in component.components(): deleteExtraneous(comp, ignore_dtstamp) for line in component.lines(): if line.params.has_key('X-VOBJ-ORIGINAL-TZID'): del line.params['X-VOBJ-ORIGINAL-TZID'] if ignore_dtstamp and hasattr(component, 'dtstamp_list'): del component.dtstamp_list def diff(left, right): """ Take two VCALENDAR components, compare VEVENTs and VTODOs in them, return a list of object pairs containing just UID and the bits that didn't match, using None for objects that weren't present in one version or the other. When there are multiple ContentLines in one VEVENT, for instance many DESCRIPTION lines, such lines original order is assumed to be meaningful. Order is also preserved when comparing (the unlikely case of) multiple parameters of the same type in a ContentLine """ def processComponentLists(leftList, rightList): output = [] rightIndex = 0 rightListSize = len(rightList) for comp in leftList: if rightIndex >= rightListSize: output.append((comp, None)) else: leftKey = getSortKey(comp) rightComp = rightList[rightIndex] rightKey = getSortKey(rightComp) while leftKey > rightKey: output.append((None, rightComp)) rightIndex += 1 if rightIndex >= rightListSize: output.append((comp, None)) break else: rightComp = rightList[rightIndex] rightKey = getSortKey(rightComp) if leftKey < rightKey: output.append((comp, None)) elif leftKey == rightKey: rightIndex += 1 matchResult = processComponentPair(comp, rightComp) if matchResult is not None: output.append(matchResult) return output def newComponent(name, body): if body is None: return None else: c = Component(name) c.behavior = getBehavior(name) c.isNative = True return c def processComponentPair(leftComp, rightComp): """ Return None if a match, or a pair of components including UIDs and any differing children. """ leftChildKeys = leftComp.contents.keys() rightChildKeys = rightComp.contents.keys() differentContentLines = [] differentComponents = {} for key in leftChildKeys: rightList = rightComp.contents.get(key, []) if isinstance(leftComp.contents[key][0], Component): compDifference = processComponentLists(leftComp.contents[key], rightList) if len(compDifference) > 0: differentComponents[key] = compDifference elif leftComp.contents[key] != rightList: differentContentLines.append((leftComp.contents[key], rightList)) for key in rightChildKeys: if key not in leftChildKeys: if isinstance(rightComp.contents[key][0], Component): differentComponents[key] = ([], rightComp.contents[key]) else: differentContentLines.append(([], rightComp.contents[key])) if len(differentContentLines) == 0 and len(differentComponents) == 0: return None else: left = newFromBehavior(leftComp.name) right = newFromBehavior(leftComp.name) # add a UID, if one existed, despite the fact that they'll always be # the same uid = leftComp.getChildValue('uid') if uid is not None: left.add( 'uid').value = uid right.add('uid').value = uid for name, childPairList in differentComponents.iteritems(): leftComponents, rightComponents = zip(*childPairList) if len(leftComponents) > 0: # filter out None left.contents[name] = filter(None, leftComponents) if len(rightComponents) > 0: # filter out None right.contents[name] = filter(None, rightComponents) for leftChildLine, rightChildLine in differentContentLines: nonEmpty = leftChildLine or rightChildLine name = nonEmpty[0].name if leftChildLine is not None: left.contents[name] = leftChildLine if rightChildLine is not None: right.contents[name] = rightChildLine return left, right vevents = processComponentLists(sortByUID(getattr(left, 'vevent_list', [])), sortByUID(getattr(right, 'vevent_list', []))) vtodos = processComponentLists(sortByUID(getattr(left, 'vtodo_list', [])), sortByUID(getattr(right, 'vtodo_list', []))) return vevents + vtodos def prettyDiff(leftObj, rightObj): for left, right in diff(leftObj, rightObj): print "<<<<<<<<<<<<<<<" if left is not None: left.prettyPrint() print "===============" if right is not None: right.prettyPrint() print ">>>>>>>>>>>>>>>" print from optparse import OptionParser import icalendar, base import os import codecs def main(): options, args = getOptions() if args: ignore_dtstamp = options.ignore ics_file1, ics_file2 = args cal1 = base.readOne(file(ics_file1)) cal2 = base.readOne(file(ics_file2)) deleteExtraneous(cal1, ignore_dtstamp=ignore_dtstamp) deleteExtraneous(cal2, ignore_dtstamp=ignore_dtstamp) prettyDiff(cal1, cal2) version = "0.1" def getOptions(): ##### Configuration options ##### usage = "usage: %prog [options] ics_file1 ics_file2" parser = OptionParser(usage=usage, version=version) parser.set_description("ics_diff will print a comparison of two iCalendar files ") parser.add_option("-i", "--ignore-dtstamp", dest="ignore", action="store_true", default=False, help="ignore DTSTAMP lines [default: False]") (cmdline_options, args) = parser.parse_args() if len(args) < 2: print "error: too few arguments given" print print parser.format_help() return False, False return cmdline_options, args if __name__ == "__main__": try: main() except KeyboardInterrupt: print "Aborted" vobject-0.8.1c/vobject/vcard.py0000644000076500001200000002412711126240511015666 0ustar chandleadmin"""Definitions and behavior for vCard 3.0""" import behavior import itertools from base import VObjectError, NativeError, ValidateError, ParseError, \ VBase, Component, ContentLine, logger, defaultSerialize, \ registerBehavior, backslashEscape, ascii from icalendar import stringToTextValues #------------------------ vCard structs ---------------------------------------- class Name(object): def __init__(self, family = '', given = '', additional = '', prefix = '', suffix = ''): """Each name attribute can be a string or a list of strings.""" self.family = family self.given = given self.additional = additional self.prefix = prefix self.suffix = suffix @staticmethod def toString(val): """Turn a string or array value into a string.""" if type(val) in (list, tuple): return ' '.join(val) return val def __str__(self): eng_order = ('prefix', 'given', 'additional', 'family', 'suffix') out = ' '.join(self.toString(getattr(self, val)) for val in eng_order) return ascii(out) def __repr__(self): return "" % self.__str__() def __eq__(self, other): try: return (self.family == other.family and self.given == other.given and self.additional == other.additional and self.prefix == other.prefix and self.suffix == other.suffix) except: return False class Address(object): def __init__(self, street = '', city = '', region = '', code = '', country = '', box = '', extended = ''): """Each name attribute can be a string or a list of strings.""" self.box = box self.extended = extended self.street = street self.city = city self.region = region self.code = code self.country = country @staticmethod def toString(val, join_char='\n'): """Turn a string or array value into a string.""" if type(val) in (list, tuple): return join_char.join(val) return val lines = ('box', 'extended', 'street') one_line = ('city', 'region', 'code') def __str__(self): lines = '\n'.join(self.toString(getattr(self, val)) for val in self.lines if getattr(self, val)) one_line = tuple(self.toString(getattr(self, val), ' ') for val in self.one_line) lines += "\n%s, %s %s" % one_line if self.country: lines += '\n' + self.toString(self.country) return ascii(lines) def __repr__(self): return "" % repr(str(self))[1:-1] def __eq__(self, other): try: return (self.box == other.box and self.extended == other.extended and self.street == other.street and self.city == other.city and self.region == other.region and self.code == other.code and self.country == other.country) except: False #------------------------ Registered Behavior subclasses ----------------------- class VCardTextBehavior(behavior.Behavior): """Provide backslash escape encoding/decoding for single valued properties. TextBehavior also deals with base64 encoding if the ENCODING parameter is explicitly set to BASE64. """ allowGroup = True base64string = 'B' @classmethod def decode(cls, line): """Remove backslash escaping from line.valueDecode line, either to remove backslash espacing, or to decode base64 encoding. The content line should contain a ENCODING=b for base64 encoding, but Apple Addressbook seems to export a singleton parameter of 'BASE64', which does not match the 3.0 vCard spec. If we encouter that, then we transform the parameter to ENCODING=b""" if line.encoded: if 'BASE64' in line.singletonparams: line.singletonparams.remove('BASE64') line.encoding_param = cls.base64string encoding = getattr(line, 'encoding_param', None) if encoding: line.value = line.value.decode('base64') else: line.value = stringToTextValues(line.value)[0] line.encoded=False @classmethod def encode(cls, line): """Backslash escape line.value.""" if not line.encoded: encoding = getattr(line, 'encoding_param', None) if encoding and encoding.upper() == cls.base64string: line.value = line.value.encode('base64').replace('\n', '') else: line.value = backslashEscape(line.value) line.encoded=True class VCardBehavior(behavior.Behavior): allowGroup = True defaultBehavior = VCardTextBehavior class VCard3_0(VCardBehavior): """vCard 3.0 behavior.""" name = 'VCARD' description = 'vCard 3.0, defined in rfc2426' versionString = '3.0' isComponent = True sortFirst = ('version', 'prodid', 'uid') knownChildren = {'N': (1, 1, None),#min, max, behaviorRegistry id 'FN': (1, 1, None), 'VERSION': (1, 1, None),#required, auto-generated 'PRODID': (0, 1, None), 'LABEL': (0, None, None), 'UID': (0, None, None), 'ADR': (0, None, None), 'ORG': (0, None, None), 'PHOTO': (0, None, None), 'CATEGORIES':(0, None, None) } @classmethod def generateImplicitParameters(cls, obj): """Create PRODID, VERSION, and VTIMEZONEs if needed. VTIMEZONEs will need to exist whenever TZID parameters exist or when datetimes with tzinfo exist. """ if not hasattr(obj, 'version'): obj.add(ContentLine('VERSION', [], cls.versionString)) registerBehavior(VCard3_0, default=True) class FN(VCardTextBehavior): name = "FN" description = 'Formatted name' registerBehavior(FN) class Label(VCardTextBehavior): name = "Label" description = 'Formatted address' registerBehavior(Label) wacky_apple_photo_serialize = True REALLY_LARGE = 1E50 class Photo(VCardTextBehavior): name = "Photo" description = 'Photograph' @classmethod def valueRepr( cls, line ): return " (BINARY PHOTO DATA at 0x%s) " % 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') class NameBehavior(VCardBehavior): """A structured name.""" hasNative = True @staticmethod def transformToNative(obj): """Turn obj.value into a Name.""" if obj.isNative: return obj obj.isNative = True obj.value = Name(**dict(zip(NAME_ORDER, splitFields(obj.value)))) return obj @staticmethod def transformFromNative(obj): """Replace the Name in obj.value with a string.""" obj.isNative = False obj.value = serializeFields(obj.value, NAME_ORDER) return obj registerBehavior(NameBehavior, 'N') ADDRESS_ORDER = ('box', 'extended', 'street', 'city', 'region', 'code', 'country') class AddressBehavior(VCardBehavior): """A structured address.""" hasNative = True @staticmethod def transformToNative(obj): """Turn obj.value into an Address.""" if obj.isNative: return obj obj.isNative = True obj.value = Address(**dict(zip(ADDRESS_ORDER, splitFields(obj.value)))) return obj @staticmethod def transformFromNative(obj): """Replace the Address in obj.value with a string.""" obj.isNative = False obj.value = serializeFields(obj.value, ADDRESS_ORDER) return obj registerBehavior(AddressBehavior, 'ADR') class OrgBehavior(VCardBehavior): """A list of organization values and sub-organization values.""" hasNative = True @staticmethod def transformToNative(obj): """Turn obj.value into a list.""" if obj.isNative: return obj obj.isNative = True obj.value = splitFields(obj.value) return obj @staticmethod def transformFromNative(obj): """Replace the list in obj.value with a string.""" if not obj.isNative: return obj obj.isNative = False obj.value = serializeFields(obj.value) return obj registerBehavior(OrgBehavior, 'ORG') vobject-0.8.1c/vobject/win32tz.py0000644000076500001200000001321111017337652016113 0ustar chandleadminimport _winreg import struct import datetime handle=_winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE) tzparent=_winreg.OpenKey(handle, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones") parentsize=_winreg.QueryInfoKey(tzparent)[0] localkey=_winreg.OpenKey(handle, "SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation") WEEKS=datetime.timedelta(7) def list_timezones(): """Return a list of all time zones known to the system.""" l=[] for i in xrange(parentsize): l.append(_winreg.EnumKey(tzparent, i)) return l class win32tz(datetime.tzinfo): """tzinfo class based on win32's timezones available in the registry. >>> local = win32tz('Central Standard Time') >>> oct1 = datetime.datetime(month=10, year=2004, day=1, tzinfo=local) >>> dec1 = datetime.datetime(month=12, year=2004, day=1, tzinfo=local) >>> oct1.dst() datetime.timedelta(0, 3600) >>> dec1.dst() datetime.timedelta(0) >>> braz = win32tz('E. South America Standard Time') >>> braz.dst(oct1) datetime.timedelta(0) >>> braz.dst(dec1) datetime.timedelta(0, 3600) """ def __init__(self, name): self.data=win32tz_data(name) def utcoffset(self, dt): if self._isdst(dt): return datetime.timedelta(minutes=self.data.dstoffset) else: return datetime.timedelta(minutes=self.data.stdoffset) def dst(self, dt): if self._isdst(dt): minutes = self.data.dstoffset - self.data.stdoffset return datetime.timedelta(minutes=minutes) else: return datetime.timedelta(0) def tzname(self, dt): if self._isdst(dt): return self.data.dstname else: return self.data.stdname def _isdst(self, dt): dat=self.data dston = pickNthWeekday(dt.year, dat.dstmonth, dat.dstdayofweek, dat.dsthour, dat.dstminute, dat.dstweeknumber) dstoff = pickNthWeekday(dt.year, dat.stdmonth, dat.stddayofweek, dat.stdhour, dat.stdminute, dat.stdweeknumber) if dston < dstoff: if dston <= dt.replace(tzinfo=None) < dstoff: return True else: return False else: if dstoff <= dt.replace(tzinfo=None) < dston: return False else: return True def __repr__(self): return "" % self.data.display def pickNthWeekday(year, month, dayofweek, hour, minute, whichweek): """dayofweek == 0 means Sunday, whichweek > 4 means last instance""" first = datetime.datetime(year=year, month=month, hour=hour, minute=minute, day=1) weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7 + 1)) for n in xrange(whichweek - 1, -1, -1): dt=weekdayone + n * WEEKS if dt.month == month: return dt class win32tz_data(object): """Read a registry key for a timezone, expose its contents.""" def __init__(self, path): """Load path, or if path is empty, load local time.""" if path: keydict=valuesToDict(_winreg.OpenKey(tzparent, path)) self.display = keydict['Display'] self.dstname = keydict['Dlt'] self.stdname = keydict['Std'] #see http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm tup = struct.unpack('=3l16h', keydict['TZI']) self.stdoffset = -tup[0]-tup[1] #Bias + StandardBias * -1 self.dstoffset = self.stdoffset - tup[2] # + DaylightBias * -1 offset=3 self.stdmonth = tup[1 + offset] self.stddayofweek = tup[2 + offset] #Sunday=0 self.stdweeknumber = tup[3 + offset] #Last = 5 self.stdhour = tup[4 + offset] self.stdminute = tup[5 + offset] offset=11 self.dstmonth = tup[1 + offset] self.dstdayofweek = tup[2 + offset] #Sunday=0 self.dstweeknumber = tup[3 + offset] #Last = 5 self.dsthour = tup[4 + offset] self.dstminute = tup[5 + offset] else: keydict=valuesToDict(localkey) self.stdname = keydict['StandardName'] self.dstname = keydict['DaylightName'] sourcekey=_winreg.OpenKey(tzparent, self.stdname) self.display = valuesToDict(sourcekey)['Display'] self.stdoffset = -keydict['Bias']-keydict['StandardBias'] self.dstoffset = self.stdoffset - keydict['DaylightBias'] #see http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm tup = struct.unpack('=8h', keydict['StandardStart']) offset=0 self.stdmonth = tup[1 + offset] self.stddayofweek = tup[2 + offset] #Sunday=0 self.stdweeknumber = tup[3 + offset] #Last = 5 self.stdhour = tup[4 + offset] self.stdminute = tup[5 + offset] tup = struct.unpack('=8h', keydict['DaylightStart']) self.dstmonth = tup[1 + offset] self.dstdayofweek = tup[2 + offset] #Sunday=0 self.dstweeknumber = tup[3 + offset] #Last = 5 self.dsthour = tup[4 + offset] self.dstminute = tup[5 + offset] def valuesToDict(key): """Convert a registry key's values to a dictionary.""" dict={} size=_winreg.QueryInfoKey(key)[1] for i in xrange(size): dict[_winreg.EnumValue(key, i)[0]]=_winreg.EnumValue(key, i)[1] return dict def _test(): import win32tz, doctest doctest.testmod(win32tz, verbose=0) if __name__ == '__main__': _test()vobject-0.8.1c/vobject.egg-info/0000755000076500001200000000000011151624231015704 5ustar chandleadminvobject-0.8.1c/vobject.egg-info/dependency_links.txt0000644000076500001200000000000111151624231021752 0ustar chandleadmin vobject-0.8.1c/vobject.egg-info/entry_points.txt0000644000076500001200000000012711151624231021202 0ustar chandleadmin[console_scripts] change_tz = vobject.change_tz:main ics_diff = vobject.ics_diff:main vobject-0.8.1c/vobject.egg-info/PKG-INFO0000644000076500001200000000516111151624231017004 0ustar chandleadminMetadata-Version: 1.0 Name: vobject Version: 0.8.1c Summary: VObject: module for reading vCard and vCalendar files Home-page: http://vobject.skyhouseconsulting.com Author: Jeffrey Harris Author-email: jeffrey@osafoundation.org License: Apache Description: Description ----------- Parses iCalendar and vCard files into Python data structures, decoding the relevant encodings. Also serializes vobject data structures to iCalendar, vCard, or (experimentally) hCalendar unicode strings. Requirements ------------ Requires python 2.4 or later, dateutil (http://labix.org/python-dateutil) 1.1 or later. Recent changes -------------- - Make change_tz.py compatible with python 2.4, so the entire package stays compatible - Fall back to default (the most recent standard) behavior if a VCARD or VCALENDAR doesn't have a recognized VERSION - Fixed a bad performance bug when parsing large text bodies, thanks to Morgen Sagen at Apple - Changed license to Apache 2.0 from Apache 1.1 - Worked around an issue with Apple Address Book's vcard PHOTO parser - Added change_tz module and script for quickly changing event timezones for an ics file. Requires PyICU. - Add support for BYMONTHDAY=-1 (days before the end of the month) when setting rrules from a dateutil rrule - Tolerate a Ruby iCalendar library escaping semi-colons in RRULEs - Make vobjects pickle-able - Add introspection help for IPython so tab completion works with vobject's custom __getattr__ - Allow Outlook's technically illegal use of commas in TZIDs - Allow unicode names for TZIDs - Worked around Lotus Notes use of underscores in names by just silently replacing with dashes - When allowing quoted-printable data, honor CHARSET for each line, defaulting to iso-8859-1 - Simplified directory layout, unit tests are now available via setup.py test For older changes, see - http://vobject.skyhouseconsulting.com/history.html or - http://websvn.osafoundation.org/listing.php?repname=vobject&path=/trunk/ Platform: any Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: License :: OSI Approved :: BSD License Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Programming Language :: Python Classifier: Operating System :: OS Independent Classifier: Topic :: Text Processing vobject-0.8.1c/vobject.egg-info/requires.txt0000644000076500001200000000002611151624231020302 0ustar chandleadminpython-dateutil >= 1.1vobject-0.8.1c/vobject.egg-info/SOURCES.txt0000644000076500001200000000121511151624231017567 0ustar chandleadminACKNOWLEDGEMENTS.txt LICENSE-2.0.txt README.txt setup.py test_vobject.py ez_setup/README.txt ez_setup/__init__.py test_files/more_tests.txt test_files/ms_tzid.ics test_files/recurrence.ics test_files/ruby_rrule.ics test_files/tzid_8bit.ics test_files/utf8_test.ics vobject/__init__.py vobject/base.py vobject/behavior.py vobject/change_tz.py vobject/hcalendar.py vobject/icalendar.py vobject/ics_diff.py vobject/vcard.py vobject/win32tz.py vobject.egg-info/PKG-INFO vobject.egg-info/SOURCES.txt vobject.egg-info/dependency_links.txt vobject.egg-info/entry_points.txt vobject.egg-info/requires.txt vobject.egg-info/top_level.txt vobject.egg-info/zip-safevobject-0.8.1c/vobject.egg-info/top_level.txt0000644000076500001200000000001011151624231020425 0ustar chandleadminvobject vobject-0.8.1c/vobject.egg-info/zip-safe0000644000076500001200000000000111017353150017334 0ustar chandleadmin