icalendar-4.0.3/0000755000076600000240000000000013357326437014265 5ustar mauritsstaff00000000000000icalendar-4.0.3/PKG-INFO0000644000076600000240000006615113357326437015373 0ustar mauritsstaff00000000000000Metadata-Version: 2.1 Name: icalendar Version: 4.0.3 Summary: iCalendar parser/generator Home-page: https://github.com/collective/icalendar Author: Plone Foundation Author-email: plone-developers@lists.sourceforge.net License: BSD Description: ========================================================== Internet Calendaring and Scheduling (iCalendar) for Python ========================================================== The `icalendar`_ package is a `RFC 5545`_ compatible parser/generator for iCalendar files. ---- :Homepage: https://icalendar.readthedocs.io :Code: https://github.com/collective/icalendar :Mailing list: https://github.com/collective/icalendar/issues :Dependencies: `python-dateutil`_ and `pytz`_. :Compatible with: Python 2.7 and 3.4+ :License: `BSD`_ ---- .. image:: https://travis-ci.org/collective/icalendar.svg?branch=master :target: https://travis-ci.org/collective/icalendar .. _`icalendar`: https://pypi.org/project/icalendar/ .. _`RFC 5545`: https://www.ietf.org/rfc/rfc5545.txt .. _`python-dateutil`: https://github.com/dateutil/dateutil/ .. _`pytz`: https://pypi.org/project/pytz/ .. _`BSD`: https://github.com/collective/icalendar/issues/2 Related projects ================ * `icalevents `_. It is built on top of icalendar and allows you to query iCal files and get the events happening on specific dates. It manages recurrent events as well. You want to help and contribute? Perfect! ========================================= These are some contribution examples ------------------------------------ - Reporting issues to the bugtracker. - Submitting pull requests from a forked icalendar repo. - Extending the documentation. - Sponsor a Sprint (http://plone.org/events/sprints/whatis). For pull requests, keep this in mind ------------------------------------ - Add a test which proves your fix and make it pass. - Describe your change in CHANGES.rst - Add yourself to the docs/credits.rst Changelog ========= 4.0.3 (2018-10-10) ------------------ Bug fixes: - Categories are comma separated not 1 per line #265. [cleder] - mark test with mixed timezoneaware and naive datetimes as an expected failure. [cleder] 4.0.2 (2018-06-20) ------------------ Bug fixes: - Update all pypi.python.org URLs to pypi.org [jon.dufresne] 4.0.1 (2018-02-11) ------------------ - Added rudimentary command line interface. [jfjlaros] - Readme, setup and travis updates. [jdufresne, PabloCastellano] 4.0.0 (2017-11-08) ------------------ Breaking changes: - Drop support for Python 2.6 and 3.3. 3.12 (2017-11-07) ----------------- New features: - Accept Windows timezone identifiers as valid. #242 [geier] Bug fixes: - Fix ResourceWarnings in setup.py when Python warnings are enabled. #244 [jdufresne] - Fix invalid escape sequences in string and bytes literals. #245 [jdufresne] - Include license file in the generated wheel package. #243 [jdufresne] - Fix non-ASCII TZID and TZNAME parameter handling. #238 [clivest] - Docs: update install instructions. #240 [Ekran] 3.11.7 (2017-08-27) ------------------- New features: - added vUTCOffset.ignore_exceptions to allow surpressing of failed TZOFFSET parsing (for now this ignores the check for offsets > 24h) [geier] 3.11.6 (2017-08-04) ------------------- Bug fixes: - Fix VTIMEZONEs including RDATEs #234. [geier] 3.11.5 (2017-07-03) ------------------- Bug fixes: - added an assertion that VTIMEZONE sub-components' DTSTART must be of type DATETIME [geier] - Fix handling of VTIMEZONEs with subcomponents with the same DTSTARTs and OFFSETs but which are of different types [geier] 3.11.4 (2017-05-10) ------------------- Bug fixes: - Don't break on parameter values which contain equal signs, e.g. base64 encoded binary data [geier] - Fix handling of VTIMEZONEs with subcomponents with the same DTSTARTs. [geier] 3.11.3 (2017-02-15) ------------------- Bug fixes: - Removed ``setuptools`` as a dependency as it was only required by setup.py and not by the package. - Don't split content lines on the unicode ``LINE SEPARATOR`` character ``\u2028`` but only on ``CRLF`` or ``LF``. 3.11.2 (2017-01-12) ------------------- Bug fixes: - Run tests with python 3.5 and 3.6. [geier] - Allow tests failing with pypy3 on travis.ci. [geier] 3.11.1 (2016-12-19) ------------------- Bug fixes: - Encode error message before adding it to the stack of collected error messages. 3.11 (2016-11-18) ----------------- Fixes: - Successfully test with pypy and pypy3. [gforcada] - Minor documentation update. [tpltnt] 3.10 (2016-05-26) ----------------- New: - Updated components description to better comply with RFC 5545. Refs #183. [stlaz] - Added PERIOD value type to date types. Also fixes incompatibilities described in #184. Refs #189. [stlaz] Fixes: - Fix testsuite for use with ``dateutil>=2.5``. Refs #195. [untitaker] - Reintroduce cal.Component.is_broken that was removed with 3.9.2. Refs #185. [geier] 3.9.2 (2016-02-05) ------------------ New: - Defined ``test_suite`` in setup.py. Now tests can be run via ``python setup.py test``. [geier] Fixes: - Fixed cal.Component.from_ical() representing an unknown component as one of the known. [stlaz] - Fixed possible IndexError exception during parsing of an ical string. [stlaz] - When doing a boolean test on ``icalendar.cal.Component``, always return ``True``. Before it was returning ``False`` due to CaselessDict, if it didn't contain any items. [stlaz] - Fixed date-time being recognized as date or time during parsing. Added better error handling to parsing from ical strings. [stlaz] - Added __version__ attribute to init.py. [TomTry] - Documentation fixes. [TomTry] - Pep 8, UTF 8 headers, dict/list calls to literals. [thet] 3.9.1 (2015-09-08) ------------------ - Fix ``vPeriod.__repr__``. [spacekpe] - Improve foldline() performance. This improves the foldline performance, especially for large strings like base64-encoded inline attachements. In some cases (1MB string) from 7 Minutes to less than 20ms for ASCII data and 500ms for non-ASCII data. Ref: #163. [emfree] 3.9.0 (2015-03-24) ------------------ - Creating timezone objects from VTIMEZONE components. [geier] - Make ``python-dateutil`` a dependency. [geier] - Made RRULE tolerant of trailing semicolons. [sleeper] - Documentation fixes. [t-8ch, thet] 3.8.4 (2014-11-01) ------------------ - Add missing BYWEEKNO to recurrence rules. [russkel] 3.8.3 (2014-08-26) ------------------ - PERCENT property in VTODO renamed to PERCENT-COMPLETE, according to RFC5545. [thomascube] 3.8.2 (2014-07-22) ------------------ - Exclude editor backup files from egg distributions. Fixes #144. [thet] 3.8.1 (2014-07-17) ------------------ - The representation of CaselessDicts in 3.8 changed the name attribute of Components and therefore broke the external API. This has been fixed. [untitaker] 3.8 (2014-07-17) ---------------- - Allow dots in property names (Needed for vCard compatibility). Refs #143. [untitaker] - Change class representation for CaselessDict objects to always include the class name or the class' name attribute, if available. Also show subcomponents for Component objects. [thet] - Don't use data_encode for CaselessDict class representation but use dict's __repr__ method. [t-8ch] - Handle parameters with multiple values, which is needed for vCard 3.0. Refs #142. [t-8ch] 3.7 (2014-06-02) ---------------- - For components with ``ignore_exceptions`` set to ``True``, mark unparseable lines as broken instead rising a ``ValueError``. ``VEVENT`` components have ``ignore_exceptions`` set to ``True`` by default. Ref #131. Fixes #104. [jkiang13] - Make ``python-dateutil`` a soft-dependency. [boltnev] - Add optional ``sorted`` parameter to ``Component.to_ical``. Setting it to false allows the user to preserve the original property and parameter order. Ref #136. Fixes #133. [untitaker] - Fix tests for latest ``pytz``. Don't set ``tzinfo`` directly on datetime objects, but use pytz's ``localize`` function. Ref #138. [untitaker, thet] - Remove incorrect use of __all__. We don't encourage using ``from package import *`` imports. Fixes #129. [eric-wieser] 3.6.2 (2014-04-05) ------------------ - Pep8 and cleanup. [lasudry] 3.6.1 (2014-01-13) ------------------ - Open text files referenced by setup.py as utf-8, no matter what the locale settings are set to. Fixes #122. [sochotnicky] - Add tox.ini to source tarball, which simplifies testing for in distributions. [sochotnicky] 3.6 (2014-01-06) ---------------- - Python3 (3.3+) + Python 2 (2.6+) support [geier] - Made sure to_ical() always returns bytes [geier] - Support adding lists to a component property, which value already was a list and remove the Component.set method, which was only used by the add method. [thet] - Remove ability to add property parameters via a value's params attribute when adding via cal.add (that was only possible for custom value objects and makes up a strange API), but support a parameter attribute on cal.add's method signature to pass a dictionary with property parameter key/value pairs. Fixes #116. [thet] - Backport some of Regebro's changes from his regebro-refactor branch. [thet] - Raise explicit error on another malformed content line case. [hajdbo] - Correctly parse datetime component property values with timezone information when parsed from ical strings. [untitaker] 3.5 (2013-07-03) ---------------- - Let to_unicode be more graceful for non-unicode strings, as like CMFPlone's safe_unicode does it. [thet] 3.4 (2013-04-24) ---------------- - Switch to unicode internally. This should fix all en/decoding errors. [thet] - Support for non-ascii parameter values. Fixes #88. [warvariuc] - Added functions to transform chars in string with '\\' + any of r'\,;:' chars into '%{:02X}' form to avoid splitting on chars escaped with '\\'. [warvariuc] - Allow seconds in vUTCOffset properties. Fixes #55. [thet] - Let ``Component.decode`` better handle vRecur and vDDDLists properties. Fixes #70. [thet] - Don't let ``Component.add`` re-encode already encoded values. This simplifies the API, since there is no need explicitly pass ``encode=False``. Fixes #82. [thet] - Rename tzinfo_from_dt to tzid_from_dt, which is what it does. [thet] - More support for dateutil parsed tzinfo objects. Fixes #89. [leo-naeka] - Remove python-dateutil version fix at all. Current python-dateutil has Py3 and Py2 compatibility. [thet] - Declare the required python-dateutil dependency in setup.py. Fixes #90. [kleink] - Raise test coverage. [thet] - Remove interfaces module, as it is unused. [thet] - Remove ``test_doctests.py``, test suite already created properly in ``test_icalendar.py``. [rnix] - Transformed doctests into unittests, Test fixes and cleanup. [warvariuc] 3.3 (2013-02-08) ---------------- - Drop support for Python < 2.6. [thet] - Allow vGeo to be instantiated with list and not only tuples of geo coordinates. Fixes #83. [thet] - Don't force to pass a list to vDDDLists and allow setting individual RDATE and EXDATE values without having to wrap them in a list. [thet] - Fix encoding function to allow setting RDATE and EXDATE values and not to have bypass encoding with an icalendar property. [thet] - Allow setting of timezone for vDDDLists and support timezone properties for RDATE and EXDATE component properties. [thet] - Move setting of TZID properties to vDDDTypes, where it belongs to. [thet] - Use @staticmethod decorator instead of wrapper function. [warvariuc, thet] - Extend quoting of parameter values to all of those characters: ",;: ’'". This fixes an outlook incompatibility with some characters. Fixes: #79, Fixes: #81. [warvariuc] - Define VTIMETZONE subcomponents STANDARD and DAYLIGHT for RFC5545 compliance. [thet] 3.2 (2012-11-27) ---------------- - Documentation file layout restructuring. [thet] - Fix time support. vTime events can be instantiated with a datetime.time object, and do not inherit from datetime.time itself. [rdunklau] - Correctly handle tzinfo objects parsed with dateutil. Fixes #77. [warvariuc, thet] - Text values are escaped correclty. Fixes #74. [warvariuc] - Returned old folding algorithm, as the current implementation fails in some cases. Fixes #72, Fixes #73. [warvariuc] - Supports to_ical() on date/time properties for dates prior to 1900. [cdevienne] 3.1 (2012-09-05) ---------------- - Make sure parameters to certain properties propagate to the ical output. [kanarip] - Re-include doctests. [rnix] - Ensure correct datatype at instance creation time in ``prop.vCalAddress`` and ``prop.vText``. [rnix] - Apply TZID parameter to datetimes parsed from RECURRENCE-ID [dbstovall] - Localize datetimes for timezones to avoid DST transition errors. [dbstovall] - Allow UTC-OFFSET property value data types in seconds, which follows RFC5545 specification. [nikolaeff] - Remove utctz and normalized_timezone methods to simplify the codebase. The methods were too tiny to be useful and just used at one place. [thet] - When using Component.add() to add icalendar properties, force a value conversion to UTC for CREATED, DTSTART and LAST-MODIFIED. The RFC expects UTC for those properties. [thet] - Removed last occurrences of old API (from_string). [Rembane] - Add 'recursive' argument to property_items() to switch recursive listing. For example when parsing a text/calendar text including multiple components (e.g. a VCALENDAR with 5 VEVENTs), the previous situation required us to look over all properties in VEVENTs even if we just want the properties under the VCALENDAR component (VERSION, PRODID, CALSCALE, METHOD). [dmikurube] - All unit tests fixed. [mikaelfrykholm] 3.0.1b2 (2012-03-01) -------------------- - For all TZID parameters in DATE-TIME properties, use timezone identifiers (e.g. Europe/Vienna) instead of timezone names (e.g. CET), as required by RFC5545. Timezone names are used together with timezone identifiers in the Timezone components. [thet] - Timezone parsing, issues and test fixes. [mikaelfrykholm, garbas, tgecho] - Since we use pytz for timezones, also use UTC tzinfo object from the pytz library instead of own implementation. [thet] 3.0.1b1 (2012-02-24) -------------------- - Update Release information. [thet] 3.0 --- - Add API for proper Timezone support. Allow creating ical DATE-TIME strings with timezone information from Python datetimes with pytz based timezone information and vice versa. [thet] - Unify API to only use to_ical and from_ical and remove string casting as a requirement for Python 3 compatibility: New: to_ical. Old: ical, string, as_string and string casting via __str__ and str. New: from_ical. Old: from_string. [thet] 2.2 (2011-08-24) ---------------- - migration to https://github.com/collective/icalendar using svn2git preserving tags, branches and authors. [garbas] - using tox for testing on python 2.4, 2.5, 2.6, 2.6. [garbas] - fixed tests so they pass also under python 2.7. [garbas] - running tests on https://jenkins.plone.org/job/icalendar (only 2.6 for now) with some other metrics (pylint, clonedigger, coverage). [garbas] - review and merge changes from https://github.com/cozi/icalendar fork. [garbas] - created sphinx documentation and started documenting development and goals. [garbas] - hook out github repository to http://readthedocs.org service so sphinx documentation is generated on each commit (for master). Documentation can be visible on: http://readthedocs.org/docs/icalendar/en/latest/ [garbas] 2.1 (2009-12-14) ---------------- - Fix deprecation warnings about ``object.__init__`` taking no parameters. - Set the VALUE parameter correctly for date values. - Long binary data would be base64 encoded with newlines, which made the iCalendar files incorrect. (This still needs testing). - Correctly handle content lines which include newlines. 2.0.1 (2008-07-11) ------------------ - Made the tests run under Python 2.5+ - Renamed the UTC class to Utc, so it would not clash with the UTC object, since that rendered the UTC object unpicklable. 2.0 (2008-07-11) ---------------- - EXDATE and RDATE now returns a vDDDLists object, which contains a list of vDDDTypes objects. This is do that EXDATE and RDATE can contain lists of dates, as per RFC. ***Note!***: This change is incompatible with earlier behavior, so if you handle EXDATE and RDATE you will need to update your code. - When createing a vDuration of -5 hours (which in itself is nonsensical), the ical output of that was -P1DT19H, which is correct, but ugly. Now it's '-PT5H', which is prettier. 1.2 (2006-11-25) ---------------- - Fixed a string index out of range error in the new folding code. 1.1 (2006-11-23) ---------------- - Fixed a bug in caselessdicts popitem. (thanks to Michael Smith ) - The RFC 2445 was a bit unclear on how to handle line folding when it happened to be in the middle of a UTF-8 character. This has been clarified in the following discussion: http://lists.osafoundation.org/pipermail/ietf-calsify/2006-August/001126.html And this is now implemented in iCalendar. It will not fold in the middle of a UTF-8 character, but may fold in the middle of a UTF-8 composing character sequence. 1.0 (2006-08-03) ---------------- - make get_inline and set_inline support non ascii codes. - Added support for creating a python egg distribution. 0.11 (2005-11-08) ----------------- - Changed component .from_string to use types_factory instead of hardcoding entries to 'inline' - Changed UTC tzinfo to a singleton so the same one is used everywhere - Made the parser more strict by using regular expressions for key name, param name and quoted/unquoted safe char as per the RFC - Added some tests from the schooltool icalendar parser for better coverage - Be more forgiving on the regex for folding lines - Allow for multiple top-level components on .from_string - Fix vWeekdays, wasn't accepting relative param (eg: -3SA vs -SA) - vDDDTypes didn't accept negative period (eg: -P30M) - 'N' is also acceptable as newline on content lines, per RFC 0.10 (2005-04-28) ----------------- - moved code to codespeak.net subversion. - reorganized package structure so that source code is under 'src' directory. Non-package files remain in distribution root. - redid doc/.py files as doc/.txt, using more modern doctest. Before they were .py files with big docstrings. - added test.py testrunner, and tests/test_icalendar.py that picks up all doctests in source code and doc directory, and runs them, when typing:: python2.3 test.py - renamed iCalendar to lower case package name, lowercased, de-pluralized and shorted module names, which are mostly implementation detail. - changed tests so they generate .ics files in a temp directory, not in the structure itself. License ======= Copyright (c) 2012-2013, Plone Foundation All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Keywords: calendar calendaring ical icalendar event todo journal recurring Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Provides-Extra: test icalendar-4.0.3/CONTRIBUTING.rst0000644000076600000240000000105713357326437016731 0ustar mauritsstaff00000000000000You want to help and contribute? Perfect! ========================================= These are some contribution examples ------------------------------------ - Reporting issues to the bugtracker. - Submitting pull requests from a forked icalendar repo. - Extending the documentation. - Sponsor a Sprint (http://plone.org/events/sprints/whatis). For pull requests, keep this in mind ------------------------------------ - Add a test which proves your fix and make it pass. - Describe your change in CHANGES.rst - Add yourself to the docs/credits.rst icalendar-4.0.3/MANIFEST.in0000644000076600000240000000015413357326437016023 0ustar mauritsstaff00000000000000include *.rst tox.ini graft docs recursive-include src/icalendar * recursive-exclude src/icalendar *.pyc *~ icalendar-4.0.3/docs/0000755000076600000240000000000013357326437015215 5ustar mauritsstaff00000000000000icalendar-4.0.3/docs/install.rst0000644000076600000240000000141413357326437017415 0ustar mauritsstaff00000000000000Installing iCalendar ==================== To install the icalendar package, use:: pip install icalendar If installation is successful, you will be able to import the iCalendar package, like this:: >>> import icalendar Building the documentation locally ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To build the documentation follow these steps: .. code-block:: bash $ git clone https://github.com/collective/icalendar.git $ cd icalendar $ virtualenv-2.7 . $ source bin/activate $ pip install -r requirements_docs.txt $ cd docs $ make html You can now open the output from ``_build/html/index.html``. To build the presentation-version use ``make presentation`` instead of ``make html``. You can open the presentation at ``presentation/index.html``. icalendar-4.0.3/docs/index.rst0000644000076600000240000000022613357326437017056 0ustar mauritsstaff00000000000000.. include:: ../README.rst Contents ======== .. toctree:: :maxdepth: 2 about install usage api cli credits license icalendar-4.0.3/docs/Makefile0000644000076600000240000001313513357326437016660 0ustar mauritsstaff00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build LOCALESDIR = _locales # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." manual: *.rst $(SPHINXBUILD) -b html -t manual . manual presentation: *.rst $(SPHINXBUILD) -b html -t presentation . presentation dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/icalendar.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/icalendar.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/icalendar" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/icalendar" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(LOCALESDIR) @echo @echo "Build finished. The message catalogs are in $(LOCALESDIR)." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." icalendar-4.0.3/docs/cli.rst0000644000076600000240000000161513357326437016521 0ustar mauritsstaff00000000000000iCalendar utility ================= To get more information about the command line interface, use the ``-h`` option:: $ icalendar -h view ---- Use the ``view`` subcommand for a human readable summary of an event:: $ icalendar view myfile.ics The output will look something like this:: Organiser: Secretary Attendees: John Doe Randy Summary: Yearly evaluation. When: Tue 14 Mar 2017 11:00-12:00 Location: Randy's office Comment: Reminder. Description: Your yearly evaluation is scheduled for next Tuesday. Please be on time. To use this in terminal based e-mail clients like mutt, add a new mime type (as root):: # cat << EOF > /usr/lib/mime/packages/icalendar text/calendar; icalendar view '%s'; copiousoutput; description=iCalendar text; priority=2 EOF # update-mime icalendar-4.0.3/docs/conf.py0000644000076600000240000000211113357326437016507 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- # icalendar documentation build configuration file import pkg_resources import datetime import os on_rtd = os.environ.get('READTHEDOCS', None) == 'True' try: import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] except ImportError: html_theme = 'default' if not on_rtd: print('-' * 74) print('Warning: sphinx-rtd-theme not installed, building with default ' 'theme.') print('-' * 74) extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.viewcode' ] source_suffix = '.rst' master_doc = 'index' project = u'icalendar' this_year = datetime.date.today().year copyright = u'{}, Plone Foundation'.format(this_year) version = pkg_resources.get_distribution('icalendar').version release = version exclude_patterns = ['_build', 'lib', 'bin', 'include', 'local'] pygments_style = 'sphinx' htmlhelp_basename = 'icalendardoc' man_pages = [ ('index', 'icalendar', u'icalendar Documentation', [u'Plone Foundation'], 1) ] icalendar-4.0.3/docs/usage.rst0000644000076600000240000002253013357326437017055 0ustar mauritsstaff00000000000000iCalendar package ================= This package is used for parsing and generating iCalendar files following the standard in RFC 2445. It should be fully compliant, but it is possible to generate and parse invalid files if you really want to. File structure -------------- An iCalendar file is a text file (utf-8) with a special format. Basically it consists of content lines. Each content line defines a property that has 3 parts (name, parameters, values). Parameters are optional. A simple content line with only name and value could look like this:: BEGIN:VCALENDAR A content line with parameters can look like this:: ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:example@example.com And the parts are:: Name: ATTENDEE Params: CN=Max Rasmussen;ROLE=REQ-PARTICIPANT Value: MAILTO:example@example.com Long content lines are usually "folded" to less than 75 character, but the package takes care of that. Overview -------- On a higher level iCalendar files consists of components. Components can have sub components. The root component is the VCALENDAR:: BEGIN:VCALENDAR ... vcalendar properties ... END:VCALENDAR The most frequent subcomponent to a VCALENDAR is a VEVENT. They are nested like this:: BEGIN:VCALENDAR ... vcalendar properties ... BEGIN:VEVENT ... vevent properties ... END:VEVENT END:VCALENDAR Inside the components there are properties with values. The values have special types. Like integer, text, datetime etc. these values are encoded in a special text format in an iCalendar file. There are methods for converting to and from these encodings in the package. These are the most important imports:: >>> from icalendar import Calendar, Event Components ---------- Components are like (Case Insensitive) dicts. So if you want to set a property you do it like this. The calendar is a component:: >>> cal = Calendar() >>> cal['dtstart'] = '20050404T080000' >>> cal['summary'] = 'Python meeting about calendaring' >>> for k,v in cal.items(): ... k,v (u'DTSTART', '20050404T080000') (u'SUMMARY', 'Python meeting about calendaring') NOTE: the recommended way to add components to the calendar is to use create the subcomponent and add it via Calendar.add! The example above adds a string, but not a vText component. You can generate a string for a file with the to_ical() method:: >>> cal.to_ical() 'BEGIN:VCALENDAR\r\nDTSTART:20050404T080000\r\nSUMMARY:Python meeting about calendaring\r\nEND:VCALENDAR\r\n' The rendered view is easier to read:: BEGIN:VCALENDAR DTSTART:20050404T080000 SUMMARY:Python meeting about calendaring END:VCALENDAR So, let's define a function so we can easily display to_ical() output:: >>> def display(cal): ... return cal.to_ical().replace('\r\n', '\n').strip() You can set multiple properties like this:: >>> cal = Calendar() >>> cal['attendee'] = ['MAILTO:maxm@mxm.dk','MAILTO:test@example.com'] >>> print display(cal) BEGIN:VCALENDAR ATTENDEE:MAILTO:maxm@mxm.dk ATTENDEE:MAILTO:test@example.com END:VCALENDAR If you don't want to care about whether a property value is a list or a single value, just use the add() method. It will automatically convert the property to a list of values if more than one value is added. Here is an example:: >>> cal = Calendar() >>> cal.add('attendee', 'MAILTO:maxm@mxm.dk') >>> cal.add('attendee', 'MAILTO:test@example.com') >>> print display(cal) BEGIN:VCALENDAR ATTENDEE:MAILTO:maxm@mxm.dk ATTENDEE:MAILTO:test@example.com END:VCALENDAR Note: this version doesn't check for compliance, so you should look in the RFC 2445 spec for legal properties for each component, or look in the icalendar/calendar.py file, where it is at least defined for each component. Subcomponents ------------- Any component can have subcomponents. Eg. inside a calendar there can be events. They can be arbitrarily nested. First by making a new component:: >>> event = Event() >>> event['uid'] = '42' >>> event['dtstart'] = '20050404T080000' And then appending it to a "parent":: >>> cal.add_component(event) >>> print display(cal) BEGIN:VCALENDAR ATTENDEE:MAILTO:maxm@mxm.dk ATTENDEE:MAILTO:test@example.com BEGIN:VEVENT DTSTART:20050404T080000 UID:42 END:VEVENT END:VCALENDAR Subcomponents are appended to the subcomponents property on the component:: >>> cal.subcomponents [VEVENT({'DTSTART': '20050404T080000', 'UID': '42'})] Value types ----------- Property values are utf-8 encoded strings. This is impractical if you want to use the data for further computation. The datetime format for example looks like this: '20050404T080000'. But the package makes it simple to parse and generate iCalendar formatted strings. Basically you can make the add() method do the thinking, or you can do it yourself. To add a datetime value, you can use Pythons built in datetime types, and the set the encode parameter to true, and it will convert to the type defined in the spec:: >>> from datetime import datetime >>> cal.add('dtstart', datetime(2005,4,4,8,0,0)) >>> cal['dtstart'].to_ical() '20050404T080000' If that doesn't work satisfactorily for some reason, you can also do it manually. In 'icalendar.prop', all the iCalendar data types are defined. Each type has a class that can parse and encode the type. So if you want to do it manually:: >>> from icalendar import vDatetime >>> now = datetime(2005,4,4,8,0,0) >>> vDatetime(now).to_ical() '20050404T080000' So the drill is to initialise the object with a python built in type, and then call the "to_ical()" method on the object. That will return an ical encoded string. You can do it the other way around too. To parse an encoded string, just call the "from_ical()" method, and it will return an instance of the corresponding Python type:: >>> vDatetime.from_ical('20050404T080000') datetime.datetime(2005, 4, 4, 8, 0) >>> dt = vDatetime.from_ical('20050404T080000Z') >>> repr(dt)[:62] 'datetime.datetime(2005, 4, 4, 8, 0, tzinfo=)' You can also choose to use the decoded() method, which will return a decoded value directly:: >>> cal = Calendar() >>> cal.add('dtstart', datetime(2005,4,4,8,0,0)) >>> cal['dtstart'].to_ical() '20050404T080000' >>> cal.decoded('dtstart') datetime.datetime(2005, 4, 4, 8, 0) Property parameters ------------------- Property parameters are automatically added, depending on the input value. For example, for date/time related properties, the value type and timezone identifier (if applicable) are automatically added here:: >>> event = Event() >>> event.add('dtstart', datetime(2010, 10, 10, 10, 0, 0, ... tzinfo=pytz.timezone("Europe/Vienna"))) >>> lines = event.to_ical().splitlines() >>> self.assertTrue( ... b"DTSTART;TZID=Europe/Vienna;VALUE=DATE-TIME:20101010T100000" ... in lines) You can also add arbitrary property parameters by passing a parameters dictionary to the add method like so:: >>> event = Event() >>> event.add('X-TEST-PROP', 'tryout.', .... parameters={'prop1': 'val1', 'prop2': 'val2'}) >>> lines = event.to_ical().splitlines() >>> self.assertTrue(b"X-TEST-PROP;PROP1=val1;PROP2=val2:tryout." in lines) Example ------- Here is an example generating a complete iCal calendar file with a single event that can be loaded into the Mozilla calendar. Init the calendar:: >>> cal = Calendar() >>> from datetime import datetime Some properties are required to be compliant:: >>> cal.add('prodid', '-//My calendar product//mxm.dk//') >>> cal.add('version', '2.0') We need at least one subcomponent for a calendar to be compliant:: >>> import pytz >>> event = Event() >>> event.add('summary', 'Python meeting about calendaring') >>> event.add('dtstart', datetime(2005,4,4,8,0,0,tzinfo=pytz.utc)) >>> event.add('dtend', datetime(2005,4,4,10,0,0,tzinfo=pytz.utc)) >>> event.add('dtstamp', datetime(2005,4,4,0,10,0,tzinfo=pytz.utc)) A property with parameters. Notice that they are an attribute on the value:: >>> from icalendar import vCalAddress, vText >>> organizer = vCalAddress('MAILTO:noone@example.com') Automatic encoding is not yet implemented for parameter values, so you must use the 'v*' types you can import from the icalendar package (they're defined in ``icalendar.prop``):: >>> organizer.params['cn'] = vText('Max Rasmussen') >>> organizer.params['role'] = vText('CHAIR') >>> event['organizer'] = organizer >>> event['location'] = vText('Odense, Denmark') >>> event['uid'] = '20050115T101010/27346262376@mxm.dk' >>> event.add('priority', 5) >>> attendee = vCalAddress('MAILTO:maxm@example.com') >>> attendee.params['cn'] = vText('Max Rasmussen') >>> attendee.params['ROLE'] = vText('REQ-PARTICIPANT') >>> event.add('attendee', attendee, encode=0) >>> attendee = vCalAddress('MAILTO:the-dude@example.com') >>> attendee.params['cn'] = vText('The Dude') >>> attendee.params['ROLE'] = vText('REQ-PARTICIPANT') >>> event.add('attendee', attendee, encode=0) Add the event to the calendar:: >>> cal.add_component(event) Write to disk:: >>> import tempfile, os >>> directory = tempfile.mkdtemp() >>> f = open(os.path.join(directory, 'example.ics'), 'wb') >>> f.write(cal.to_ical()) >>> f.close() More documentation ================== Have a look at the tests of this package to get more examples. All modules and classes docstrings, which document how they work. icalendar-4.0.3/docs/license.rst0000644000076600000240000000003413357326437017366 0ustar mauritsstaff00000000000000.. include:: ../LICENSE.rst icalendar-4.0.3/docs/api.rst0000644000076600000240000000026113357326437016517 0ustar mauritsstaff00000000000000API Reference ------------- icalendar.cal +++++++++++++ .. automodule:: icalendar.cal :members: icalendar.prop ++++++++++++++ .. automodule:: icalendar.prop :members: icalendar-4.0.3/docs/credits.rst0000644000076600000240000000410713357326437017406 0ustar mauritsstaff00000000000000icalendar contributors ====================== - Johannes Raggam (Maintainer) - Max M (Original author) - Andreas Zeidler - Andrey Nikolaev - Barak Michener - Christian Geier - Christophe de Vienne - Dai MIKURUBE - Dan Stovall - Eric Hanchrow - Eric Wieser - Erik Simmler - George V. Reilly - Jannis Leidel - Jeroen F.J. Laros - Jeroen van Meeuwen (Kolab Systems) - Jordan Kiang - Klaus Klein - Laurent Lasudry - Lennart Regebro - Léo S - Marc Egli - Markus Unterwaditzer - Martijn Faassen - Martin Melin - Michael Smith - Mikael Frykholm - Olivier Grisel - Pavel Repin - Pedro Ferreira - Rembane - Robert Niederreiter - Rok Garbas - Ronan Dunklau - Russ - Sidnei da Silva - Stanislav Láznička - Stanislav Ochotnicky - Stefan Schwarzer - Thomas Bruederli - Thomas Weißschuh - Victor Varvaryuk - Wichert Akkerman - cillianderoiste - fitnr - hajdbo - ilya - spanktar - tgecho - tisto - TomTry - Andreas Ruppen - Clive Stevens Find out who contributed:: $ git shortlog -s -e icalendar-4.0.3/docs/about.rst0000644000076600000240000000127313357326437017064 0ustar mauritsstaff00000000000000About ===== `Max M`_ had often needed to parse and generate iCalendar files. Finally he got tired of writing ad-hoc tools. This package is his attempt at making an iCalendar package for Python. The inspiration has come from the email package in the standard lib, which he thinks is pretty simple, yet efficient and powerful. At the time of writing this, last version was released more then 2 years ago. Since then many things have changes. For one, `RFC 2445`_ was updated by `RFC 5545`_ which makes this package. So in some sense this package became outdated. .. _`Max M`: http://www.mxm.dk .. _`RFC 2445`: https://tools.ietf.org/html/rfc2445 .. _`RFC 5545`: https://tools.ietf.org/html/rfc5545 icalendar-4.0.3/setup.py0000644000076600000240000000402513357326437016000 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- import codecs import setuptools import re import ast _version_re = re.compile(r'__version__\s+=\s+(.*)') with open('src/icalendar/__init__.py', 'rb') as f: version = str(ast.literal_eval(_version_re.search( f.read().decode('utf-8')).group(1))) shortdesc = 'iCalendar parser/generator' longdesc = '' for fname in ['README.rst', 'CONTRIBUTING.rst', 'CHANGES.rst', 'LICENSE.rst']: with codecs.open(fname, encoding='utf-8') as f: longdesc += f.read() longdesc += '\n' tests_require = [] install_requires = [ 'python-dateutil', 'pytz', ] setuptools.setup( name='icalendar', version=version, description=shortdesc, long_description=longdesc, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ], keywords='calendar calendaring ical icalendar event todo journal ' 'recurring', author='Plone Foundation', author_email='plone-developers@lists.sourceforge.net', url='https://github.com/collective/icalendar', license='BSD', packages=setuptools.find_packages('src'), package_dir={'': 'src'}, include_package_data=True, zip_safe=False, python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", install_requires=install_requires, entry_points = {'console_scripts': ['icalendar = icalendar.cli:main']}, extras_require={ 'test': tests_require }, test_suite='icalendar.tests' ) icalendar-4.0.3/tox.ini0000644000076600000240000000071113357326437015577 0ustar mauritsstaff00000000000000# to run for a specific environment, use ``tox -e ENVNAME`` [tox] envlist = py27,py34,py35,py36,pypy,pypy3 [testenv] deps = pytest coverage py{27,34,35,36}: hypothesis>=3.0 .[test] commands = coverage run --source=src/icalendar --omit=*/tests/* --module pytest [] py{27,34,35,36}: coverage run --append --source=src/icalendar --omit=*/tests/* --module pytest [] src/icalendar/tests/hypothesis/ coverage report coverage html icalendar-4.0.3/LICENSE.rst0000644000076600000240000000245613357326437016110 0ustar mauritsstaff00000000000000License ======= Copyright (c) 2012-2013, Plone Foundation All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. icalendar-4.0.3/setup.cfg0000644000076600000240000000053413357326437016110 0ustar mauritsstaff00000000000000[check-manifest] ignore = *.cfg bootstrap.py requirements_docs.txt [zest.releaser] python-file-with-version = src/icalendar/__init__.py create-wheel = yes [bdist_wheel] universal = 1 [metadata] license_file = LICENSE.rst [tool:pytest] norecursedirs = .* env* docs *.egg src/icalendar/tests/hypothesis [egg_info] tag_build = tag_date = 0 icalendar-4.0.3/README.rst0000644000076600000240000000224313357326437015755 0ustar mauritsstaff00000000000000========================================================== Internet Calendaring and Scheduling (iCalendar) for Python ========================================================== The `icalendar`_ package is a `RFC 5545`_ compatible parser/generator for iCalendar files. ---- :Homepage: https://icalendar.readthedocs.io :Code: https://github.com/collective/icalendar :Mailing list: https://github.com/collective/icalendar/issues :Dependencies: `python-dateutil`_ and `pytz`_. :Compatible with: Python 2.7 and 3.4+ :License: `BSD`_ ---- .. image:: https://travis-ci.org/collective/icalendar.svg?branch=master :target: https://travis-ci.org/collective/icalendar .. _`icalendar`: https://pypi.org/project/icalendar/ .. _`RFC 5545`: https://www.ietf.org/rfc/rfc5545.txt .. _`python-dateutil`: https://github.com/dateutil/dateutil/ .. _`pytz`: https://pypi.org/project/pytz/ .. _`BSD`: https://github.com/collective/icalendar/issues/2 Related projects ================ * `icalevents `_. It is built on top of icalendar and allows you to query iCal files and get the events happening on specific dates. It manages recurrent events as well. icalendar-4.0.3/CHANGES.rst0000644000076600000240000004163513357326437016100 0ustar mauritsstaff00000000000000Changelog ========= 4.0.3 (2018-10-10) ------------------ Bug fixes: - Categories are comma separated not 1 per line #265. [cleder] - mark test with mixed timezoneaware and naive datetimes as an expected failure. [cleder] 4.0.2 (2018-06-20) ------------------ Bug fixes: - Update all pypi.python.org URLs to pypi.org [jon.dufresne] 4.0.1 (2018-02-11) ------------------ - Added rudimentary command line interface. [jfjlaros] - Readme, setup and travis updates. [jdufresne, PabloCastellano] 4.0.0 (2017-11-08) ------------------ Breaking changes: - Drop support for Python 2.6 and 3.3. 3.12 (2017-11-07) ----------------- New features: - Accept Windows timezone identifiers as valid. #242 [geier] Bug fixes: - Fix ResourceWarnings in setup.py when Python warnings are enabled. #244 [jdufresne] - Fix invalid escape sequences in string and bytes literals. #245 [jdufresne] - Include license file in the generated wheel package. #243 [jdufresne] - Fix non-ASCII TZID and TZNAME parameter handling. #238 [clivest] - Docs: update install instructions. #240 [Ekran] 3.11.7 (2017-08-27) ------------------- New features: - added vUTCOffset.ignore_exceptions to allow surpressing of failed TZOFFSET parsing (for now this ignores the check for offsets > 24h) [geier] 3.11.6 (2017-08-04) ------------------- Bug fixes: - Fix VTIMEZONEs including RDATEs #234. [geier] 3.11.5 (2017-07-03) ------------------- Bug fixes: - added an assertion that VTIMEZONE sub-components' DTSTART must be of type DATETIME [geier] - Fix handling of VTIMEZONEs with subcomponents with the same DTSTARTs and OFFSETs but which are of different types [geier] 3.11.4 (2017-05-10) ------------------- Bug fixes: - Don't break on parameter values which contain equal signs, e.g. base64 encoded binary data [geier] - Fix handling of VTIMEZONEs with subcomponents with the same DTSTARTs. [geier] 3.11.3 (2017-02-15) ------------------- Bug fixes: - Removed ``setuptools`` as a dependency as it was only required by setup.py and not by the package. - Don't split content lines on the unicode ``LINE SEPARATOR`` character ``\u2028`` but only on ``CRLF`` or ``LF``. 3.11.2 (2017-01-12) ------------------- Bug fixes: - Run tests with python 3.5 and 3.6. [geier] - Allow tests failing with pypy3 on travis.ci. [geier] 3.11.1 (2016-12-19) ------------------- Bug fixes: - Encode error message before adding it to the stack of collected error messages. 3.11 (2016-11-18) ----------------- Fixes: - Successfully test with pypy and pypy3. [gforcada] - Minor documentation update. [tpltnt] 3.10 (2016-05-26) ----------------- New: - Updated components description to better comply with RFC 5545. Refs #183. [stlaz] - Added PERIOD value type to date types. Also fixes incompatibilities described in #184. Refs #189. [stlaz] Fixes: - Fix testsuite for use with ``dateutil>=2.5``. Refs #195. [untitaker] - Reintroduce cal.Component.is_broken that was removed with 3.9.2. Refs #185. [geier] 3.9.2 (2016-02-05) ------------------ New: - Defined ``test_suite`` in setup.py. Now tests can be run via ``python setup.py test``. [geier] Fixes: - Fixed cal.Component.from_ical() representing an unknown component as one of the known. [stlaz] - Fixed possible IndexError exception during parsing of an ical string. [stlaz] - When doing a boolean test on ``icalendar.cal.Component``, always return ``True``. Before it was returning ``False`` due to CaselessDict, if it didn't contain any items. [stlaz] - Fixed date-time being recognized as date or time during parsing. Added better error handling to parsing from ical strings. [stlaz] - Added __version__ attribute to init.py. [TomTry] - Documentation fixes. [TomTry] - Pep 8, UTF 8 headers, dict/list calls to literals. [thet] 3.9.1 (2015-09-08) ------------------ - Fix ``vPeriod.__repr__``. [spacekpe] - Improve foldline() performance. This improves the foldline performance, especially for large strings like base64-encoded inline attachements. In some cases (1MB string) from 7 Minutes to less than 20ms for ASCII data and 500ms for non-ASCII data. Ref: #163. [emfree] 3.9.0 (2015-03-24) ------------------ - Creating timezone objects from VTIMEZONE components. [geier] - Make ``python-dateutil`` a dependency. [geier] - Made RRULE tolerant of trailing semicolons. [sleeper] - Documentation fixes. [t-8ch, thet] 3.8.4 (2014-11-01) ------------------ - Add missing BYWEEKNO to recurrence rules. [russkel] 3.8.3 (2014-08-26) ------------------ - PERCENT property in VTODO renamed to PERCENT-COMPLETE, according to RFC5545. [thomascube] 3.8.2 (2014-07-22) ------------------ - Exclude editor backup files from egg distributions. Fixes #144. [thet] 3.8.1 (2014-07-17) ------------------ - The representation of CaselessDicts in 3.8 changed the name attribute of Components and therefore broke the external API. This has been fixed. [untitaker] 3.8 (2014-07-17) ---------------- - Allow dots in property names (Needed for vCard compatibility). Refs #143. [untitaker] - Change class representation for CaselessDict objects to always include the class name or the class' name attribute, if available. Also show subcomponents for Component objects. [thet] - Don't use data_encode for CaselessDict class representation but use dict's __repr__ method. [t-8ch] - Handle parameters with multiple values, which is needed for vCard 3.0. Refs #142. [t-8ch] 3.7 (2014-06-02) ---------------- - For components with ``ignore_exceptions`` set to ``True``, mark unparseable lines as broken instead rising a ``ValueError``. ``VEVENT`` components have ``ignore_exceptions`` set to ``True`` by default. Ref #131. Fixes #104. [jkiang13] - Make ``python-dateutil`` a soft-dependency. [boltnev] - Add optional ``sorted`` parameter to ``Component.to_ical``. Setting it to false allows the user to preserve the original property and parameter order. Ref #136. Fixes #133. [untitaker] - Fix tests for latest ``pytz``. Don't set ``tzinfo`` directly on datetime objects, but use pytz's ``localize`` function. Ref #138. [untitaker, thet] - Remove incorrect use of __all__. We don't encourage using ``from package import *`` imports. Fixes #129. [eric-wieser] 3.6.2 (2014-04-05) ------------------ - Pep8 and cleanup. [lasudry] 3.6.1 (2014-01-13) ------------------ - Open text files referenced by setup.py as utf-8, no matter what the locale settings are set to. Fixes #122. [sochotnicky] - Add tox.ini to source tarball, which simplifies testing for in distributions. [sochotnicky] 3.6 (2014-01-06) ---------------- - Python3 (3.3+) + Python 2 (2.6+) support [geier] - Made sure to_ical() always returns bytes [geier] - Support adding lists to a component property, which value already was a list and remove the Component.set method, which was only used by the add method. [thet] - Remove ability to add property parameters via a value's params attribute when adding via cal.add (that was only possible for custom value objects and makes up a strange API), but support a parameter attribute on cal.add's method signature to pass a dictionary with property parameter key/value pairs. Fixes #116. [thet] - Backport some of Regebro's changes from his regebro-refactor branch. [thet] - Raise explicit error on another malformed content line case. [hajdbo] - Correctly parse datetime component property values with timezone information when parsed from ical strings. [untitaker] 3.5 (2013-07-03) ---------------- - Let to_unicode be more graceful for non-unicode strings, as like CMFPlone's safe_unicode does it. [thet] 3.4 (2013-04-24) ---------------- - Switch to unicode internally. This should fix all en/decoding errors. [thet] - Support for non-ascii parameter values. Fixes #88. [warvariuc] - Added functions to transform chars in string with '\\' + any of r'\,;:' chars into '%{:02X}' form to avoid splitting on chars escaped with '\\'. [warvariuc] - Allow seconds in vUTCOffset properties. Fixes #55. [thet] - Let ``Component.decode`` better handle vRecur and vDDDLists properties. Fixes #70. [thet] - Don't let ``Component.add`` re-encode already encoded values. This simplifies the API, since there is no need explicitly pass ``encode=False``. Fixes #82. [thet] - Rename tzinfo_from_dt to tzid_from_dt, which is what it does. [thet] - More support for dateutil parsed tzinfo objects. Fixes #89. [leo-naeka] - Remove python-dateutil version fix at all. Current python-dateutil has Py3 and Py2 compatibility. [thet] - Declare the required python-dateutil dependency in setup.py. Fixes #90. [kleink] - Raise test coverage. [thet] - Remove interfaces module, as it is unused. [thet] - Remove ``test_doctests.py``, test suite already created properly in ``test_icalendar.py``. [rnix] - Transformed doctests into unittests, Test fixes and cleanup. [warvariuc] 3.3 (2013-02-08) ---------------- - Drop support for Python < 2.6. [thet] - Allow vGeo to be instantiated with list and not only tuples of geo coordinates. Fixes #83. [thet] - Don't force to pass a list to vDDDLists and allow setting individual RDATE and EXDATE values without having to wrap them in a list. [thet] - Fix encoding function to allow setting RDATE and EXDATE values and not to have bypass encoding with an icalendar property. [thet] - Allow setting of timezone for vDDDLists and support timezone properties for RDATE and EXDATE component properties. [thet] - Move setting of TZID properties to vDDDTypes, where it belongs to. [thet] - Use @staticmethod decorator instead of wrapper function. [warvariuc, thet] - Extend quoting of parameter values to all of those characters: ",;: ’'". This fixes an outlook incompatibility with some characters. Fixes: #79, Fixes: #81. [warvariuc] - Define VTIMETZONE subcomponents STANDARD and DAYLIGHT for RFC5545 compliance. [thet] 3.2 (2012-11-27) ---------------- - Documentation file layout restructuring. [thet] - Fix time support. vTime events can be instantiated with a datetime.time object, and do not inherit from datetime.time itself. [rdunklau] - Correctly handle tzinfo objects parsed with dateutil. Fixes #77. [warvariuc, thet] - Text values are escaped correclty. Fixes #74. [warvariuc] - Returned old folding algorithm, as the current implementation fails in some cases. Fixes #72, Fixes #73. [warvariuc] - Supports to_ical() on date/time properties for dates prior to 1900. [cdevienne] 3.1 (2012-09-05) ---------------- - Make sure parameters to certain properties propagate to the ical output. [kanarip] - Re-include doctests. [rnix] - Ensure correct datatype at instance creation time in ``prop.vCalAddress`` and ``prop.vText``. [rnix] - Apply TZID parameter to datetimes parsed from RECURRENCE-ID [dbstovall] - Localize datetimes for timezones to avoid DST transition errors. [dbstovall] - Allow UTC-OFFSET property value data types in seconds, which follows RFC5545 specification. [nikolaeff] - Remove utctz and normalized_timezone methods to simplify the codebase. The methods were too tiny to be useful and just used at one place. [thet] - When using Component.add() to add icalendar properties, force a value conversion to UTC for CREATED, DTSTART and LAST-MODIFIED. The RFC expects UTC for those properties. [thet] - Removed last occurrences of old API (from_string). [Rembane] - Add 'recursive' argument to property_items() to switch recursive listing. For example when parsing a text/calendar text including multiple components (e.g. a VCALENDAR with 5 VEVENTs), the previous situation required us to look over all properties in VEVENTs even if we just want the properties under the VCALENDAR component (VERSION, PRODID, CALSCALE, METHOD). [dmikurube] - All unit tests fixed. [mikaelfrykholm] 3.0.1b2 (2012-03-01) -------------------- - For all TZID parameters in DATE-TIME properties, use timezone identifiers (e.g. Europe/Vienna) instead of timezone names (e.g. CET), as required by RFC5545. Timezone names are used together with timezone identifiers in the Timezone components. [thet] - Timezone parsing, issues and test fixes. [mikaelfrykholm, garbas, tgecho] - Since we use pytz for timezones, also use UTC tzinfo object from the pytz library instead of own implementation. [thet] 3.0.1b1 (2012-02-24) -------------------- - Update Release information. [thet] 3.0 --- - Add API for proper Timezone support. Allow creating ical DATE-TIME strings with timezone information from Python datetimes with pytz based timezone information and vice versa. [thet] - Unify API to only use to_ical and from_ical and remove string casting as a requirement for Python 3 compatibility: New: to_ical. Old: ical, string, as_string and string casting via __str__ and str. New: from_ical. Old: from_string. [thet] 2.2 (2011-08-24) ---------------- - migration to https://github.com/collective/icalendar using svn2git preserving tags, branches and authors. [garbas] - using tox for testing on python 2.4, 2.5, 2.6, 2.6. [garbas] - fixed tests so they pass also under python 2.7. [garbas] - running tests on https://jenkins.plone.org/job/icalendar (only 2.6 for now) with some other metrics (pylint, clonedigger, coverage). [garbas] - review and merge changes from https://github.com/cozi/icalendar fork. [garbas] - created sphinx documentation and started documenting development and goals. [garbas] - hook out github repository to http://readthedocs.org service so sphinx documentation is generated on each commit (for master). Documentation can be visible on: http://readthedocs.org/docs/icalendar/en/latest/ [garbas] 2.1 (2009-12-14) ---------------- - Fix deprecation warnings about ``object.__init__`` taking no parameters. - Set the VALUE parameter correctly for date values. - Long binary data would be base64 encoded with newlines, which made the iCalendar files incorrect. (This still needs testing). - Correctly handle content lines which include newlines. 2.0.1 (2008-07-11) ------------------ - Made the tests run under Python 2.5+ - Renamed the UTC class to Utc, so it would not clash with the UTC object, since that rendered the UTC object unpicklable. 2.0 (2008-07-11) ---------------- - EXDATE and RDATE now returns a vDDDLists object, which contains a list of vDDDTypes objects. This is do that EXDATE and RDATE can contain lists of dates, as per RFC. ***Note!***: This change is incompatible with earlier behavior, so if you handle EXDATE and RDATE you will need to update your code. - When createing a vDuration of -5 hours (which in itself is nonsensical), the ical output of that was -P1DT19H, which is correct, but ugly. Now it's '-PT5H', which is prettier. 1.2 (2006-11-25) ---------------- - Fixed a string index out of range error in the new folding code. 1.1 (2006-11-23) ---------------- - Fixed a bug in caselessdicts popitem. (thanks to Michael Smith ) - The RFC 2445 was a bit unclear on how to handle line folding when it happened to be in the middle of a UTF-8 character. This has been clarified in the following discussion: http://lists.osafoundation.org/pipermail/ietf-calsify/2006-August/001126.html And this is now implemented in iCalendar. It will not fold in the middle of a UTF-8 character, but may fold in the middle of a UTF-8 composing character sequence. 1.0 (2006-08-03) ---------------- - make get_inline and set_inline support non ascii codes. - Added support for creating a python egg distribution. 0.11 (2005-11-08) ----------------- - Changed component .from_string to use types_factory instead of hardcoding entries to 'inline' - Changed UTC tzinfo to a singleton so the same one is used everywhere - Made the parser more strict by using regular expressions for key name, param name and quoted/unquoted safe char as per the RFC - Added some tests from the schooltool icalendar parser for better coverage - Be more forgiving on the regex for folding lines - Allow for multiple top-level components on .from_string - Fix vWeekdays, wasn't accepting relative param (eg: -3SA vs -SA) - vDDDTypes didn't accept negative period (eg: -P30M) - 'N' is also acceptable as newline on content lines, per RFC 0.10 (2005-04-28) ----------------- - moved code to codespeak.net subversion. - reorganized package structure so that source code is under 'src' directory. Non-package files remain in distribution root. - redid doc/.py files as doc/.txt, using more modern doctest. Before they were .py files with big docstrings. - added test.py testrunner, and tests/test_icalendar.py that picks up all doctests in source code and doc directory, and runs them, when typing:: python2.3 test.py - renamed iCalendar to lower case package name, lowercased, de-pluralized and shorted module names, which are mostly implementation detail. - changed tests so they generate .ics files in a temp directory, not in the structure itself. icalendar-4.0.3/src/0000755000076600000240000000000013357326437015054 5ustar mauritsstaff00000000000000icalendar-4.0.3/src/icalendar.egg-info/0000755000076600000240000000000013357326437020470 5ustar mauritsstaff00000000000000icalendar-4.0.3/src/icalendar.egg-info/PKG-INFO0000644000076600000240000006615113357326437021576 0ustar mauritsstaff00000000000000Metadata-Version: 2.1 Name: icalendar Version: 4.0.3 Summary: iCalendar parser/generator Home-page: https://github.com/collective/icalendar Author: Plone Foundation Author-email: plone-developers@lists.sourceforge.net License: BSD Description: ========================================================== Internet Calendaring and Scheduling (iCalendar) for Python ========================================================== The `icalendar`_ package is a `RFC 5545`_ compatible parser/generator for iCalendar files. ---- :Homepage: https://icalendar.readthedocs.io :Code: https://github.com/collective/icalendar :Mailing list: https://github.com/collective/icalendar/issues :Dependencies: `python-dateutil`_ and `pytz`_. :Compatible with: Python 2.7 and 3.4+ :License: `BSD`_ ---- .. image:: https://travis-ci.org/collective/icalendar.svg?branch=master :target: https://travis-ci.org/collective/icalendar .. _`icalendar`: https://pypi.org/project/icalendar/ .. _`RFC 5545`: https://www.ietf.org/rfc/rfc5545.txt .. _`python-dateutil`: https://github.com/dateutil/dateutil/ .. _`pytz`: https://pypi.org/project/pytz/ .. _`BSD`: https://github.com/collective/icalendar/issues/2 Related projects ================ * `icalevents `_. It is built on top of icalendar and allows you to query iCal files and get the events happening on specific dates. It manages recurrent events as well. You want to help and contribute? Perfect! ========================================= These are some contribution examples ------------------------------------ - Reporting issues to the bugtracker. - Submitting pull requests from a forked icalendar repo. - Extending the documentation. - Sponsor a Sprint (http://plone.org/events/sprints/whatis). For pull requests, keep this in mind ------------------------------------ - Add a test which proves your fix and make it pass. - Describe your change in CHANGES.rst - Add yourself to the docs/credits.rst Changelog ========= 4.0.3 (2018-10-10) ------------------ Bug fixes: - Categories are comma separated not 1 per line #265. [cleder] - mark test with mixed timezoneaware and naive datetimes as an expected failure. [cleder] 4.0.2 (2018-06-20) ------------------ Bug fixes: - Update all pypi.python.org URLs to pypi.org [jon.dufresne] 4.0.1 (2018-02-11) ------------------ - Added rudimentary command line interface. [jfjlaros] - Readme, setup and travis updates. [jdufresne, PabloCastellano] 4.0.0 (2017-11-08) ------------------ Breaking changes: - Drop support for Python 2.6 and 3.3. 3.12 (2017-11-07) ----------------- New features: - Accept Windows timezone identifiers as valid. #242 [geier] Bug fixes: - Fix ResourceWarnings in setup.py when Python warnings are enabled. #244 [jdufresne] - Fix invalid escape sequences in string and bytes literals. #245 [jdufresne] - Include license file in the generated wheel package. #243 [jdufresne] - Fix non-ASCII TZID and TZNAME parameter handling. #238 [clivest] - Docs: update install instructions. #240 [Ekran] 3.11.7 (2017-08-27) ------------------- New features: - added vUTCOffset.ignore_exceptions to allow surpressing of failed TZOFFSET parsing (for now this ignores the check for offsets > 24h) [geier] 3.11.6 (2017-08-04) ------------------- Bug fixes: - Fix VTIMEZONEs including RDATEs #234. [geier] 3.11.5 (2017-07-03) ------------------- Bug fixes: - added an assertion that VTIMEZONE sub-components' DTSTART must be of type DATETIME [geier] - Fix handling of VTIMEZONEs with subcomponents with the same DTSTARTs and OFFSETs but which are of different types [geier] 3.11.4 (2017-05-10) ------------------- Bug fixes: - Don't break on parameter values which contain equal signs, e.g. base64 encoded binary data [geier] - Fix handling of VTIMEZONEs with subcomponents with the same DTSTARTs. [geier] 3.11.3 (2017-02-15) ------------------- Bug fixes: - Removed ``setuptools`` as a dependency as it was only required by setup.py and not by the package. - Don't split content lines on the unicode ``LINE SEPARATOR`` character ``\u2028`` but only on ``CRLF`` or ``LF``. 3.11.2 (2017-01-12) ------------------- Bug fixes: - Run tests with python 3.5 and 3.6. [geier] - Allow tests failing with pypy3 on travis.ci. [geier] 3.11.1 (2016-12-19) ------------------- Bug fixes: - Encode error message before adding it to the stack of collected error messages. 3.11 (2016-11-18) ----------------- Fixes: - Successfully test with pypy and pypy3. [gforcada] - Minor documentation update. [tpltnt] 3.10 (2016-05-26) ----------------- New: - Updated components description to better comply with RFC 5545. Refs #183. [stlaz] - Added PERIOD value type to date types. Also fixes incompatibilities described in #184. Refs #189. [stlaz] Fixes: - Fix testsuite for use with ``dateutil>=2.5``. Refs #195. [untitaker] - Reintroduce cal.Component.is_broken that was removed with 3.9.2. Refs #185. [geier] 3.9.2 (2016-02-05) ------------------ New: - Defined ``test_suite`` in setup.py. Now tests can be run via ``python setup.py test``. [geier] Fixes: - Fixed cal.Component.from_ical() representing an unknown component as one of the known. [stlaz] - Fixed possible IndexError exception during parsing of an ical string. [stlaz] - When doing a boolean test on ``icalendar.cal.Component``, always return ``True``. Before it was returning ``False`` due to CaselessDict, if it didn't contain any items. [stlaz] - Fixed date-time being recognized as date or time during parsing. Added better error handling to parsing from ical strings. [stlaz] - Added __version__ attribute to init.py. [TomTry] - Documentation fixes. [TomTry] - Pep 8, UTF 8 headers, dict/list calls to literals. [thet] 3.9.1 (2015-09-08) ------------------ - Fix ``vPeriod.__repr__``. [spacekpe] - Improve foldline() performance. This improves the foldline performance, especially for large strings like base64-encoded inline attachements. In some cases (1MB string) from 7 Minutes to less than 20ms for ASCII data and 500ms for non-ASCII data. Ref: #163. [emfree] 3.9.0 (2015-03-24) ------------------ - Creating timezone objects from VTIMEZONE components. [geier] - Make ``python-dateutil`` a dependency. [geier] - Made RRULE tolerant of trailing semicolons. [sleeper] - Documentation fixes. [t-8ch, thet] 3.8.4 (2014-11-01) ------------------ - Add missing BYWEEKNO to recurrence rules. [russkel] 3.8.3 (2014-08-26) ------------------ - PERCENT property in VTODO renamed to PERCENT-COMPLETE, according to RFC5545. [thomascube] 3.8.2 (2014-07-22) ------------------ - Exclude editor backup files from egg distributions. Fixes #144. [thet] 3.8.1 (2014-07-17) ------------------ - The representation of CaselessDicts in 3.8 changed the name attribute of Components and therefore broke the external API. This has been fixed. [untitaker] 3.8 (2014-07-17) ---------------- - Allow dots in property names (Needed for vCard compatibility). Refs #143. [untitaker] - Change class representation for CaselessDict objects to always include the class name or the class' name attribute, if available. Also show subcomponents for Component objects. [thet] - Don't use data_encode for CaselessDict class representation but use dict's __repr__ method. [t-8ch] - Handle parameters with multiple values, which is needed for vCard 3.0. Refs #142. [t-8ch] 3.7 (2014-06-02) ---------------- - For components with ``ignore_exceptions`` set to ``True``, mark unparseable lines as broken instead rising a ``ValueError``. ``VEVENT`` components have ``ignore_exceptions`` set to ``True`` by default. Ref #131. Fixes #104. [jkiang13] - Make ``python-dateutil`` a soft-dependency. [boltnev] - Add optional ``sorted`` parameter to ``Component.to_ical``. Setting it to false allows the user to preserve the original property and parameter order. Ref #136. Fixes #133. [untitaker] - Fix tests for latest ``pytz``. Don't set ``tzinfo`` directly on datetime objects, but use pytz's ``localize`` function. Ref #138. [untitaker, thet] - Remove incorrect use of __all__. We don't encourage using ``from package import *`` imports. Fixes #129. [eric-wieser] 3.6.2 (2014-04-05) ------------------ - Pep8 and cleanup. [lasudry] 3.6.1 (2014-01-13) ------------------ - Open text files referenced by setup.py as utf-8, no matter what the locale settings are set to. Fixes #122. [sochotnicky] - Add tox.ini to source tarball, which simplifies testing for in distributions. [sochotnicky] 3.6 (2014-01-06) ---------------- - Python3 (3.3+) + Python 2 (2.6+) support [geier] - Made sure to_ical() always returns bytes [geier] - Support adding lists to a component property, which value already was a list and remove the Component.set method, which was only used by the add method. [thet] - Remove ability to add property parameters via a value's params attribute when adding via cal.add (that was only possible for custom value objects and makes up a strange API), but support a parameter attribute on cal.add's method signature to pass a dictionary with property parameter key/value pairs. Fixes #116. [thet] - Backport some of Regebro's changes from his regebro-refactor branch. [thet] - Raise explicit error on another malformed content line case. [hajdbo] - Correctly parse datetime component property values with timezone information when parsed from ical strings. [untitaker] 3.5 (2013-07-03) ---------------- - Let to_unicode be more graceful for non-unicode strings, as like CMFPlone's safe_unicode does it. [thet] 3.4 (2013-04-24) ---------------- - Switch to unicode internally. This should fix all en/decoding errors. [thet] - Support for non-ascii parameter values. Fixes #88. [warvariuc] - Added functions to transform chars in string with '\\' + any of r'\,;:' chars into '%{:02X}' form to avoid splitting on chars escaped with '\\'. [warvariuc] - Allow seconds in vUTCOffset properties. Fixes #55. [thet] - Let ``Component.decode`` better handle vRecur and vDDDLists properties. Fixes #70. [thet] - Don't let ``Component.add`` re-encode already encoded values. This simplifies the API, since there is no need explicitly pass ``encode=False``. Fixes #82. [thet] - Rename tzinfo_from_dt to tzid_from_dt, which is what it does. [thet] - More support for dateutil parsed tzinfo objects. Fixes #89. [leo-naeka] - Remove python-dateutil version fix at all. Current python-dateutil has Py3 and Py2 compatibility. [thet] - Declare the required python-dateutil dependency in setup.py. Fixes #90. [kleink] - Raise test coverage. [thet] - Remove interfaces module, as it is unused. [thet] - Remove ``test_doctests.py``, test suite already created properly in ``test_icalendar.py``. [rnix] - Transformed doctests into unittests, Test fixes and cleanup. [warvariuc] 3.3 (2013-02-08) ---------------- - Drop support for Python < 2.6. [thet] - Allow vGeo to be instantiated with list and not only tuples of geo coordinates. Fixes #83. [thet] - Don't force to pass a list to vDDDLists and allow setting individual RDATE and EXDATE values without having to wrap them in a list. [thet] - Fix encoding function to allow setting RDATE and EXDATE values and not to have bypass encoding with an icalendar property. [thet] - Allow setting of timezone for vDDDLists and support timezone properties for RDATE and EXDATE component properties. [thet] - Move setting of TZID properties to vDDDTypes, where it belongs to. [thet] - Use @staticmethod decorator instead of wrapper function. [warvariuc, thet] - Extend quoting of parameter values to all of those characters: ",;: ’'". This fixes an outlook incompatibility with some characters. Fixes: #79, Fixes: #81. [warvariuc] - Define VTIMETZONE subcomponents STANDARD and DAYLIGHT for RFC5545 compliance. [thet] 3.2 (2012-11-27) ---------------- - Documentation file layout restructuring. [thet] - Fix time support. vTime events can be instantiated with a datetime.time object, and do not inherit from datetime.time itself. [rdunklau] - Correctly handle tzinfo objects parsed with dateutil. Fixes #77. [warvariuc, thet] - Text values are escaped correclty. Fixes #74. [warvariuc] - Returned old folding algorithm, as the current implementation fails in some cases. Fixes #72, Fixes #73. [warvariuc] - Supports to_ical() on date/time properties for dates prior to 1900. [cdevienne] 3.1 (2012-09-05) ---------------- - Make sure parameters to certain properties propagate to the ical output. [kanarip] - Re-include doctests. [rnix] - Ensure correct datatype at instance creation time in ``prop.vCalAddress`` and ``prop.vText``. [rnix] - Apply TZID parameter to datetimes parsed from RECURRENCE-ID [dbstovall] - Localize datetimes for timezones to avoid DST transition errors. [dbstovall] - Allow UTC-OFFSET property value data types in seconds, which follows RFC5545 specification. [nikolaeff] - Remove utctz and normalized_timezone methods to simplify the codebase. The methods were too tiny to be useful and just used at one place. [thet] - When using Component.add() to add icalendar properties, force a value conversion to UTC for CREATED, DTSTART and LAST-MODIFIED. The RFC expects UTC for those properties. [thet] - Removed last occurrences of old API (from_string). [Rembane] - Add 'recursive' argument to property_items() to switch recursive listing. For example when parsing a text/calendar text including multiple components (e.g. a VCALENDAR with 5 VEVENTs), the previous situation required us to look over all properties in VEVENTs even if we just want the properties under the VCALENDAR component (VERSION, PRODID, CALSCALE, METHOD). [dmikurube] - All unit tests fixed. [mikaelfrykholm] 3.0.1b2 (2012-03-01) -------------------- - For all TZID parameters in DATE-TIME properties, use timezone identifiers (e.g. Europe/Vienna) instead of timezone names (e.g. CET), as required by RFC5545. Timezone names are used together with timezone identifiers in the Timezone components. [thet] - Timezone parsing, issues and test fixes. [mikaelfrykholm, garbas, tgecho] - Since we use pytz for timezones, also use UTC tzinfo object from the pytz library instead of own implementation. [thet] 3.0.1b1 (2012-02-24) -------------------- - Update Release information. [thet] 3.0 --- - Add API for proper Timezone support. Allow creating ical DATE-TIME strings with timezone information from Python datetimes with pytz based timezone information and vice versa. [thet] - Unify API to only use to_ical and from_ical and remove string casting as a requirement for Python 3 compatibility: New: to_ical. Old: ical, string, as_string and string casting via __str__ and str. New: from_ical. Old: from_string. [thet] 2.2 (2011-08-24) ---------------- - migration to https://github.com/collective/icalendar using svn2git preserving tags, branches and authors. [garbas] - using tox for testing on python 2.4, 2.5, 2.6, 2.6. [garbas] - fixed tests so they pass also under python 2.7. [garbas] - running tests on https://jenkins.plone.org/job/icalendar (only 2.6 for now) with some other metrics (pylint, clonedigger, coverage). [garbas] - review and merge changes from https://github.com/cozi/icalendar fork. [garbas] - created sphinx documentation and started documenting development and goals. [garbas] - hook out github repository to http://readthedocs.org service so sphinx documentation is generated on each commit (for master). Documentation can be visible on: http://readthedocs.org/docs/icalendar/en/latest/ [garbas] 2.1 (2009-12-14) ---------------- - Fix deprecation warnings about ``object.__init__`` taking no parameters. - Set the VALUE parameter correctly for date values. - Long binary data would be base64 encoded with newlines, which made the iCalendar files incorrect. (This still needs testing). - Correctly handle content lines which include newlines. 2.0.1 (2008-07-11) ------------------ - Made the tests run under Python 2.5+ - Renamed the UTC class to Utc, so it would not clash with the UTC object, since that rendered the UTC object unpicklable. 2.0 (2008-07-11) ---------------- - EXDATE and RDATE now returns a vDDDLists object, which contains a list of vDDDTypes objects. This is do that EXDATE and RDATE can contain lists of dates, as per RFC. ***Note!***: This change is incompatible with earlier behavior, so if you handle EXDATE and RDATE you will need to update your code. - When createing a vDuration of -5 hours (which in itself is nonsensical), the ical output of that was -P1DT19H, which is correct, but ugly. Now it's '-PT5H', which is prettier. 1.2 (2006-11-25) ---------------- - Fixed a string index out of range error in the new folding code. 1.1 (2006-11-23) ---------------- - Fixed a bug in caselessdicts popitem. (thanks to Michael Smith ) - The RFC 2445 was a bit unclear on how to handle line folding when it happened to be in the middle of a UTF-8 character. This has been clarified in the following discussion: http://lists.osafoundation.org/pipermail/ietf-calsify/2006-August/001126.html And this is now implemented in iCalendar. It will not fold in the middle of a UTF-8 character, but may fold in the middle of a UTF-8 composing character sequence. 1.0 (2006-08-03) ---------------- - make get_inline and set_inline support non ascii codes. - Added support for creating a python egg distribution. 0.11 (2005-11-08) ----------------- - Changed component .from_string to use types_factory instead of hardcoding entries to 'inline' - Changed UTC tzinfo to a singleton so the same one is used everywhere - Made the parser more strict by using regular expressions for key name, param name and quoted/unquoted safe char as per the RFC - Added some tests from the schooltool icalendar parser for better coverage - Be more forgiving on the regex for folding lines - Allow for multiple top-level components on .from_string - Fix vWeekdays, wasn't accepting relative param (eg: -3SA vs -SA) - vDDDTypes didn't accept negative period (eg: -P30M) - 'N' is also acceptable as newline on content lines, per RFC 0.10 (2005-04-28) ----------------- - moved code to codespeak.net subversion. - reorganized package structure so that source code is under 'src' directory. Non-package files remain in distribution root. - redid doc/.py files as doc/.txt, using more modern doctest. Before they were .py files with big docstrings. - added test.py testrunner, and tests/test_icalendar.py that picks up all doctests in source code and doc directory, and runs them, when typing:: python2.3 test.py - renamed iCalendar to lower case package name, lowercased, de-pluralized and shorted module names, which are mostly implementation detail. - changed tests so they generate .ics files in a temp directory, not in the structure itself. License ======= Copyright (c) 2012-2013, Plone Foundation All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Keywords: calendar calendaring ical icalendar event todo journal recurring Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Provides-Extra: test icalendar-4.0.3/src/icalendar.egg-info/not-zip-safe0000644000076600000240000000000113357326437022716 0ustar mauritsstaff00000000000000 icalendar-4.0.3/src/icalendar.egg-info/SOURCES.txt0000644000076600000240000000355513357326437022364 0ustar mauritsstaff00000000000000CHANGES.rst CONTRIBUTING.rst LICENSE.rst MANIFEST.in README.rst setup.cfg setup.py tox.ini docs/Makefile docs/about.rst docs/api.rst docs/cli.rst docs/conf.py docs/credits.rst docs/index.rst docs/install.rst docs/license.rst docs/usage.rst src/icalendar/__init__.py src/icalendar/cal.py src/icalendar/caselessdict.py src/icalendar/cli.py src/icalendar/compat.py src/icalendar/parser.py src/icalendar/parser_tools.py src/icalendar/prop.py src/icalendar/timezone_cache.py src/icalendar/tools.py src/icalendar/windows_to_olson.py src/icalendar.egg-info/PKG-INFO src/icalendar.egg-info/SOURCES.txt src/icalendar.egg-info/dependency_links.txt src/icalendar.egg-info/entry_points.txt src/icalendar.egg-info/not-zip-safe src/icalendar.egg-info/requires.txt src/icalendar.egg-info/top_level.txt src/icalendar/tests/__init__.py src/icalendar/tests/america_new_york.ics src/icalendar/tests/encoding.ics src/icalendar/tests/issue_112_missing_tzinfo_on_exdate.ics src/icalendar/tests/issue_53_parsing_failure.ics src/icalendar/tests/multiple.ics src/icalendar/tests/pacific_fiji.ics src/icalendar/tests/recurrence.ics src/icalendar/tests/test_encoding.py src/icalendar/tests/test_fixed_issues.py src/icalendar/tests/test_icalendar.py src/icalendar/tests/test_multiple.py src/icalendar/tests/test_property_params.py src/icalendar/tests/test_recurrence.py src/icalendar/tests/test_time.py src/icalendar/tests/test_timezoned.py src/icalendar/tests/test_unit_cal.py src/icalendar/tests/test_unit_caselessdict.py src/icalendar/tests/test_unit_parser_tools.py src/icalendar/tests/test_unit_prop.py src/icalendar/tests/test_unit_tools.py src/icalendar/tests/time.ics src/icalendar/tests/timezone_rdate.ics src/icalendar/tests/timezone_same_start.ics src/icalendar/tests/timezone_same_start_and_offset.ics src/icalendar/tests/timezoned.ics src/icalendar/tests/x_location.ics src/icalendar/tests/hypothesis/test_fuzzing.pyicalendar-4.0.3/src/icalendar.egg-info/entry_points.txt0000644000076600000240000000006213357326437023764 0ustar mauritsstaff00000000000000[console_scripts] icalendar = icalendar.cli:main icalendar-4.0.3/src/icalendar.egg-info/requires.txt0000644000076600000240000000003513357326437023066 0ustar mauritsstaff00000000000000python-dateutil pytz [test] icalendar-4.0.3/src/icalendar.egg-info/top_level.txt0000644000076600000240000000001213357326437023213 0ustar mauritsstaff00000000000000icalendar icalendar-4.0.3/src/icalendar.egg-info/dependency_links.txt0000644000076600000240000000000113357326437024536 0ustar mauritsstaff00000000000000 icalendar-4.0.3/src/icalendar/0000755000076600000240000000000013357326437016776 5ustar mauritsstaff00000000000000icalendar-4.0.3/src/icalendar/windows_to_olson.py0000644000076600000240000001274713357326437022771 0ustar mauritsstaff00000000000000"""This module contains mappings from Windows timezone identifiers to Olson timezone identifiers. The data is taken from the unicode consortium [0], the proposal and rationale for this mapping is also available at the unicode consortium [1]. [0] http://www.unicode.org/cldr/charts/29/supplemental/zone_tzid.html [1] http://cldr.unicode.org/development/development-process/design-proposals/extended-windows-olson-zid-mapping # noqa """ WINDOWS_TO_OLSON = { 'AUS Central Standard Time': 'Australia/Darwin', 'AUS Eastern Standard Time': 'Australia/Sydney', 'Afghanistan Standard Time': 'Asia/Kabul', 'Alaskan Standard Time': 'America/Anchorage', 'Arab Standard Time': 'Asia/Riyadh', 'Arabian Standard Time': 'Asia/Dubai', 'Arabic Standard Time': 'Asia/Baghdad', 'Argentina Standard Time': 'America/Buenos_Aires', 'Atlantic Standard Time': 'America/Halifax', 'Azerbaijan Standard Time': 'Asia/Baku', 'Azores Standard Time': 'Atlantic/Azores', 'Bahia Standard Time': 'America/Bahia', 'Bangladesh Standard Time': 'Asia/Dhaka', 'Belarus Standard Time': 'Europe/Minsk', 'Canada Central Standard Time': 'America/Regina', 'Cape Verde Standard Time': 'Atlantic/Cape_Verde', 'Caucasus Standard Time': 'Asia/Yerevan', 'Cen. Australia Standard Time': 'Australia/Adelaide', 'Central America Standard Time': 'America/Guatemala', 'Central Asia Standard Time': 'Asia/Almaty', 'Central Brazilian Standard Time': 'America/Cuiaba', 'Central Europe Standard Time': 'Europe/Budapest', 'Central European Standard Time': 'Europe/Warsaw', 'Central Pacific Standard Time': 'Pacific/Guadalcanal', 'Central Standard Time': 'America/Chicago', 'Central Standard Time (Mexico)': 'America/Mexico_City', 'China Standard Time': 'Asia/Shanghai', 'Dateline Standard Time': 'Etc/GMT+12', 'E. Africa Standard Time': 'Africa/Nairobi', 'E. Australia Standard Time': 'Australia/Brisbane', 'E. Europe Standard Time': 'Europe/Chisinau', 'E. South America Standard Time': 'America/Sao_Paulo', 'Eastern Standard Time': 'America/New_York', 'Eastern Standard Time (Mexico)': 'America/Cancun', 'Egypt Standard Time': 'Africa/Cairo', 'Ekaterinburg Standard Time': 'Asia/Yekaterinburg', 'FLE Standard Time': 'Europe/Kiev', 'Fiji Standard Time': 'Pacific/Fiji', 'GMT Standard Time': 'Europe/London', 'GTB Standard Time': 'Europe/Bucharest', 'Georgian Standard Time': 'Asia/Tbilisi', 'Greenland Standard Time': 'America/Godthab', 'Greenwich Standard Time': 'Atlantic/Reykjavik', 'Hawaiian Standard Time': 'Pacific/Honolulu', 'India Standard Time': 'Asia/Calcutta', 'Iran Standard Time': 'Asia/Tehran', 'Israel Standard Time': 'Asia/Jerusalem', 'Jordan Standard Time': 'Asia/Amman', 'Kaliningrad Standard Time': 'Europe/Kaliningrad', 'Korea Standard Time': 'Asia/Seoul', 'Libya Standard Time': 'Africa/Tripoli', 'Line Islands Standard Time': 'Pacific/Kiritimati', 'Magadan Standard Time': 'Asia/Magadan', 'Mauritius Standard Time': 'Indian/Mauritius', 'Middle East Standard Time': 'Asia/Beirut', 'Montevideo Standard Time': 'America/Montevideo', 'Morocco Standard Time': 'Africa/Casablanca', 'Mountain Standard Time': 'America/Denver', 'Mountain Standard Time (Mexico)': 'America/Chihuahua', 'Myanmar Standard Time': 'Asia/Rangoon', 'N. Central Asia Standard Time': 'Asia/Novosibirsk', 'Namibia Standard Time': 'Africa/Windhoek', 'Nepal Standard Time': 'Asia/Katmandu', 'New Zealand Standard Time': 'Pacific/Auckland', 'Newfoundland Standard Time': 'America/St_Johns', 'North Asia East Standard Time': 'Asia/Irkutsk', 'North Asia Standard Time': 'Asia/Krasnoyarsk', 'North Korea Standard Time': 'Asia/Pyongyang', 'Pacific SA Standard Time': 'America/Santiago', 'Pacific Standard Time': 'America/Los_Angeles', 'Pakistan Standard Time': 'Asia/Karachi', 'Paraguay Standard Time': 'America/Asuncion', 'Romance Standard Time': 'Europe/Paris', 'Russia Time Zone 10': 'Asia/Srednekolymsk', 'Russia Time Zone 11': 'Asia/Kamchatka', 'Russia Time Zone 3': 'Europe/Samara', 'Russian Standard Time': 'Europe/Moscow', 'SA Eastern Standard Time': 'America/Cayenne', 'SA Pacific Standard Time': 'America/Bogota', 'SA Western Standard Time': 'America/La_Paz', 'SE Asia Standard Time': 'Asia/Bangkok', 'Samoa Standard Time': 'Pacific/Apia', 'Singapore Standard Time': 'Asia/Singapore', 'South Africa Standard Time': 'Africa/Johannesburg', 'Sri Lanka Standard Time': 'Asia/Colombo', 'Syria Standard Time': 'Asia/Damascus', 'Taipei Standard Time': 'Asia/Taipei', 'Tasmania Standard Time': 'Australia/Hobart', 'Tokyo Standard Time': 'Asia/Tokyo', 'Tonga Standard Time': 'Pacific/Tongatapu', 'Turkey Standard Time': 'Europe/Istanbul', 'US Eastern Standard Time': 'America/Indianapolis', 'US Mountain Standard Time': 'America/Phoenix', 'UTC': 'Etc/GMT', 'UTC+12': 'Etc/GMT-12', 'UTC-02': 'Etc/GMT+2', 'UTC-11': 'Etc/GMT+11', 'Ulaanbaatar Standard Time': 'Asia/Ulaanbaatar', 'Venezuela Standard Time': 'America/Caracas', 'Vladivostok Standard Time': 'Asia/Vladivostok', 'W. Australia Standard Time': 'Australia/Perth', 'W. Central Africa Standard Time': 'Africa/Lagos', 'W. Europe Standard Time': 'Europe/Berlin', 'West Asia Standard Time': 'Asia/Tashkent', 'West Pacific Standard Time': 'Pacific/Port_Moresby', 'Yakutsk Standard Time': 'Asia/Yakutsk', } icalendar-4.0.3/src/icalendar/compat.py0000644000076600000240000000054613357326437020640 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- import sys if sys.version_info[0] == 2: # pragma: no cover unicode_type = unicode bytes_type = str iteritems = lambda d, *args, **kwargs: iter(d.iteritems(*args, **kwargs)) else: # pragma: no cover unicode_type = str bytes_type = bytes iteritems = lambda d, *args, **kwargs: iter(d.items(*args, **kwargs)) icalendar-4.0.3/src/icalendar/prop.py0000644000076600000240000007725513357326437020350 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- """This module contains the parser/generators (or coders/encoders if you prefer) for the classes/datatypes that are used in iCalendar: ########################################################################### # This module defines these property value data types and property parameters 4.2 Defined property parameters are: ALTREP, CN, CUTYPE, DELEGATED-FROM, DELEGATED-TO, DIR, ENCODING, FMTTYPE, FBTYPE, LANGUAGE, MEMBER, PARTSTAT, RANGE, RELATED, RELTYPE, ROLE, RSVP, SENT-BY, TZID, VALUE 4.3 Defined value data types are: BINARY, BOOLEAN, CAL-ADDRESS, DATE, DATE-TIME, DURATION, FLOAT, INTEGER, PERIOD, RECUR, TEXT, TIME, URI, UTC-OFFSET ########################################################################### iCalendar properties have values. The values are strongly typed. This module defines these types, calling val.to_ical() on them will render them as defined in rfc2445. If you pass any of these classes a Python primitive, you will have an object that can render itself as iCalendar formatted date. Property Value Data Types start with a 'v'. they all have an to_ical() and from_ical() method. The to_ical() method generates a text string in the iCalendar format. The from_ical() method can parse this format and return a primitive Python datatype. So it should always be true that: x == vDataType.from_ical(VDataType(x).to_ical()) These types are mainly used for parsing and file generation. But you can set them directly. """ from datetime import date from datetime import datetime from datetime import time from datetime import timedelta from datetime import tzinfo try: from dateutil.tz import tzutc except ImportError: tzutc = None from icalendar import compat from icalendar.caselessdict import CaselessDict from icalendar.parser import Parameters from icalendar.parser import escape_char from icalendar.parser import tzid_from_dt from icalendar.parser import unescape_char from icalendar.parser_tools import DEFAULT_ENCODING from icalendar.parser_tools import SEQUENCE_TYPES from icalendar.parser_tools import to_unicode from icalendar.timezone_cache import _timezone_cache from icalendar.windows_to_olson import WINDOWS_TO_OLSON import base64 import binascii import pytz import re import time as _time DATE_PART = r'(\d+)D' TIME_PART = r'T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?' DATETIME_PART = '(?:%s)?(?:%s)?' % (DATE_PART, TIME_PART) WEEKS_PART = r'(\d+)W' DURATION_REGEX = re.compile(r'([-+]?)P(?:%s|%s)$' % (WEEKS_PART, DATETIME_PART)) WEEKDAY_RULE = re.compile(r'(?P[+-]?)(?P[\d]?)' r'(?P[\w]{2})$') #################################################### # handy tzinfo classes you can use. # ZERO = timedelta(0) HOUR = timedelta(hours=1) STDOFFSET = timedelta(seconds=-_time.timezone) if _time.daylight: DSTOFFSET = timedelta(seconds=-_time.altzone) else: DSTOFFSET = STDOFFSET DSTDIFF = DSTOFFSET - STDOFFSET class FixedOffset(tzinfo): """Fixed offset in minutes east from UTC. """ def __init__(self, offset, name): self.__offset = timedelta(minutes=offset) self.__name = name def utcoffset(self, dt): return self.__offset def tzname(self, dt): return self.__name def dst(self, dt): return ZERO class LocalTimezone(tzinfo): """Timezone of the machine where the code is running. """ def utcoffset(self, dt): if self._isdst(dt): return DSTOFFSET else: return STDOFFSET def dst(self, dt): if self._isdst(dt): return DSTDIFF else: return ZERO def tzname(self, dt): return _time.tzname[self._isdst(dt)] def _isdst(self, dt): tt = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.weekday(), 0, -1) stamp = _time.mktime(tt) tt = _time.localtime(stamp) return tt.tm_isdst > 0 class vBinary(object): """Binary property values are base 64 encoded. """ def __init__(self, obj): self.obj = to_unicode(obj) self.params = Parameters(encoding='BASE64', value="BINARY") def __repr__(self): return "vBinary('%s')" % self.to_ical() def to_ical(self): return binascii.b2a_base64(self.obj.encode('utf-8'))[:-1] @staticmethod def from_ical(ical): try: return base64.b64decode(ical) except UnicodeError: raise ValueError('Not valid base 64 encoding.') class vBoolean(int): """Returns specific string according to state. """ BOOL_MAP = CaselessDict({'true': True, 'false': False}) def __new__(cls, *args, **kwargs): self = super(vBoolean, cls).__new__(cls, *args, **kwargs) self.params = Parameters() return self def to_ical(self): if self: return b'TRUE' return b'FALSE' @classmethod def from_ical(cls, ical): try: return cls.BOOL_MAP[ical] except: raise ValueError("Expected 'TRUE' or 'FALSE'. Got %s" % ical) class vCalAddress(compat.unicode_type): """This just returns an unquoted string. """ def __new__(cls, value, encoding=DEFAULT_ENCODING): value = to_unicode(value, encoding=encoding) self = super(vCalAddress, cls).__new__(cls, value) self.params = Parameters() return self def __repr__(self): return "vCalAddress('%s')" % self.to_ical() def to_ical(self): return self.encode(DEFAULT_ENCODING) @classmethod def from_ical(cls, ical): return cls(ical) class vFloat(float): """Just a float. """ def __new__(cls, *args, **kwargs): self = super(vFloat, cls).__new__(cls, *args, **kwargs) self.params = Parameters() return self def to_ical(self): return compat.unicode_type(self).encode('utf-8') @classmethod def from_ical(cls, ical): try: return cls(ical) except: raise ValueError('Expected float value, got: %s' % ical) class vInt(int): """Just an int. """ def __new__(cls, *args, **kwargs): self = super(vInt, cls).__new__(cls, *args, **kwargs) self.params = Parameters() return self def to_ical(self): return compat.unicode_type(self).encode('utf-8') @classmethod def from_ical(cls, ical): try: return cls(ical) except: raise ValueError('Expected int, got: %s' % ical) class vDDDLists(object): """A list of vDDDTypes values. """ def __init__(self, dt_list): if not hasattr(dt_list, '__iter__'): dt_list = [dt_list] vDDD = [] tzid = None for dt in dt_list: dt = vDDDTypes(dt) vDDD.append(dt) if 'TZID' in dt.params: tzid = dt.params['TZID'] if tzid: # NOTE: no support for multiple timezones here! self.params = Parameters({'TZID': tzid}) self.dts = vDDD def to_ical(self): dts_ical = (dt.to_ical() for dt in self.dts) return b",".join(dts_ical) @staticmethod def from_ical(ical, timezone=None): out = [] ical_dates = ical.split(",") for ical_dt in ical_dates: out.append(vDDDTypes.from_ical(ical_dt, timezone=timezone)) return out class vCategory(object): def __init__(self, c_list): if not hasattr(c_list, '__iter__'): d_list = [c_list] self.cats = [vText(c) for c in c_list] def to_ical(self): return b",".join([c.to_ical() for c in self.cats]) @staticmethod def from_ical(ical, timezone=None): out = unescape_char(ical).split(",") return out class vDDDTypes(object): """A combined Datetime, Date or Duration parser/generator. Their format cannot be confused, and often values can be of either types. So this is practical. """ def __init__(self, dt): if not isinstance(dt, (datetime, date, timedelta, time, tuple)): raise ValueError('You must use datetime, date, timedelta, ' 'time or tuple (for periods)') if isinstance(dt, datetime): self.params = Parameters({'value': 'DATE-TIME'}) elif isinstance(dt, date): self.params = Parameters({'value': 'DATE'}) elif isinstance(dt, time): self.params = Parameters({'value': 'TIME'}) elif isinstance(dt, tuple): self.params = Parameters({'value': 'PERIOD'}) if (isinstance(dt, datetime) or isinstance(dt, time))\ and getattr(dt, 'tzinfo', False): tzinfo = dt.tzinfo if tzinfo is not pytz.utc and\ (tzutc is None or not isinstance(tzinfo, tzutc)): # set the timezone as a parameter to the property tzid = tzid_from_dt(dt) if tzid: self.params.update({'TZID': tzid}) self.dt = dt def to_ical(self): dt = self.dt if isinstance(dt, datetime): return vDatetime(dt).to_ical() elif isinstance(dt, date): return vDate(dt).to_ical() elif isinstance(dt, timedelta): return vDuration(dt).to_ical() elif isinstance(dt, time): return vTime(dt).to_ical() elif isinstance(dt, tuple) and len(dt) == 2: return vPeriod(dt).to_ical() else: raise ValueError('Unknown date type: {}'.format(type(dt))) @classmethod def from_ical(cls, ical, timezone=None): if isinstance(ical, cls): return ical.dt u = ical.upper() if u.startswith(('P', '-P', '+P')): return vDuration.from_ical(ical) if '/' in u: return vPeriod.from_ical(ical) if len(ical) in (15, 16): return vDatetime.from_ical(ical, timezone=timezone) elif len(ical) == 8: return vDate.from_ical(ical) elif len(ical) in (6, 7): return vTime.from_ical(ical) else: raise ValueError( "Expected datetime, date, or time, got: '%s'" % ical ) class vDate(object): """Render and generates iCalendar date format. """ def __init__(self, dt): if not isinstance(dt, date): raise ValueError('Value MUST be a date instance') self.dt = dt self.params = Parameters({'value': 'DATE'}) def to_ical(self): s = "%04d%02d%02d" % (self.dt.year, self.dt.month, self.dt.day) return s.encode('utf-8') @staticmethod def from_ical(ical): try: timetuple = ( int(ical[:4]), # year int(ical[4:6]), # month int(ical[6:8]), # day ) return date(*timetuple) except: raise ValueError('Wrong date format %s' % ical) class vDatetime(object): """Render and generates icalendar datetime format. vDatetime is timezone aware and uses the pytz library, an implementation of the Olson database in Python. When a vDatetime object is created from an ical string, you can pass a valid pytz timezone identifier. When a vDatetime object is created from a python datetime object, it uses the tzinfo component, if present. Otherwise an timezone-naive object is created. Be aware that there are certain limitations with timezone naive DATE-TIME components in the icalendar standard. """ def __init__(self, dt): self.dt = dt self.params = Parameters() def to_ical(self): dt = self.dt tzid = tzid_from_dt(dt) s = "%04d%02d%02dT%02d%02d%02d" % ( dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second ) if tzid == 'UTC': s += "Z" elif tzid: self.params.update({'TZID': tzid}) return s.encode('utf-8') @staticmethod def from_ical(ical, timezone=None): tzinfo = None if timezone: try: tzinfo = pytz.timezone(timezone) except pytz.UnknownTimeZoneError: if timezone in WINDOWS_TO_OLSON: tzinfo = pytz.timezone(WINDOWS_TO_OLSON.get(timezone)) else: tzinfo = _timezone_cache.get(timezone, None) try: timetuple = ( int(ical[:4]), # year int(ical[4:6]), # month int(ical[6:8]), # day int(ical[9:11]), # hour int(ical[11:13]), # minute int(ical[13:15]), # second ) if tzinfo: return tzinfo.localize(datetime(*timetuple)) elif not ical[15:]: return datetime(*timetuple) elif ical[15:16] == 'Z': return pytz.utc.localize(datetime(*timetuple)) else: raise ValueError(ical) except: raise ValueError('Wrong datetime format: %s' % ical) class vDuration(object): """Subclass of timedelta that renders itself in the iCalendar DURATION format. """ def __init__(self, td): if not isinstance(td, timedelta): raise ValueError('Value MUST be a timedelta instance') self.td = td self.params = Parameters() def to_ical(self): sign = "" if self.td.days < 0: sign = "-" self.td = -self.td timepart = "" if self.td.seconds: timepart = "T" hours = self.td.seconds // 3600 minutes = self.td.seconds % 3600 // 60 seconds = self.td.seconds % 60 if hours: timepart += "%dH" % hours if minutes or (hours and seconds): timepart += "%dM" % minutes if seconds: timepart += "%dS" % seconds if self.td.days == 0 and timepart: return (compat.unicode_type(sign).encode('utf-8') + b'P' + compat.unicode_type(timepart).encode('utf-8')) else: return (compat.unicode_type(sign).encode('utf-8') + b'P' + compat.unicode_type(abs(self.td.days)).encode('utf-8') + b'D' + compat.unicode_type(timepart).encode('utf-8')) @staticmethod def from_ical(ical): try: match = DURATION_REGEX.match(ical) sign, weeks, days, hours, minutes, seconds = match.groups() if weeks: value = timedelta(weeks=int(weeks)) else: value = timedelta(days=int(days or 0), hours=int(hours or 0), minutes=int(minutes or 0), seconds=int(seconds or 0)) if sign == '-': value = -value return value except: raise ValueError('Invalid iCalendar duration: %s' % ical) class vPeriod(object): """A precise period of time. """ def __init__(self, per): start, end_or_duration = per if not (isinstance(start, datetime) or isinstance(start, date)): raise ValueError('Start value MUST be a datetime or date instance') if not (isinstance(end_or_duration, datetime) or isinstance(end_or_duration, date) or isinstance(end_or_duration, timedelta)): raise ValueError('end_or_duration MUST be a datetime, ' 'date or timedelta instance') by_duration = 0 if isinstance(end_or_duration, timedelta): by_duration = 1 duration = end_or_duration end = start + duration else: end = end_or_duration duration = end - start if start > end: raise ValueError("Start time is greater than end time") self.params = Parameters() # set the timezone identifier # does not support different timezones for start and end tzid = tzid_from_dt(start) if tzid: self.params['TZID'] = tzid self.start = start self.end = end self.by_duration = by_duration self.duration = duration def __cmp__(self, other): if not isinstance(other, vPeriod): raise NotImplementedError('Cannot compare vPeriod with %r' % other) return cmp((self.start, self.end), (other.start, other.end)) def overlaps(self, other): if self.start > other.start: return other.overlaps(self) if self.start <= other.start < self.end: return True return False def to_ical(self): if self.by_duration: return (vDatetime(self.start).to_ical() + b'/' + vDuration(self.duration).to_ical()) return (vDatetime(self.start).to_ical() + b'/' + vDatetime(self.end).to_ical()) @staticmethod def from_ical(ical): try: start, end_or_duration = ical.split('/') start = vDDDTypes.from_ical(start) end_or_duration = vDDDTypes.from_ical(end_or_duration) return (start, end_or_duration) except: raise ValueError('Expected period format, got: %s' % ical) def __repr__(self): if self.by_duration: p = (self.start, self.duration) else: p = (self.start, self.end) return 'vPeriod(%r)' % (p, ) class vWeekday(compat.unicode_type): """This returns an unquoted weekday abbrevation. """ week_days = CaselessDict({ "SU": 0, "MO": 1, "TU": 2, "WE": 3, "TH": 4, "FR": 5, "SA": 6, }) def __new__(cls, value, encoding=DEFAULT_ENCODING): value = to_unicode(value, encoding=encoding) self = super(vWeekday, cls).__new__(cls, value) match = WEEKDAY_RULE.match(self) if match is None: raise ValueError('Expected weekday abbrevation, got: %s' % self) match = match.groupdict() sign = match['signal'] weekday = match['weekday'] relative = match['relative'] if weekday not in vWeekday.week_days or sign not in '+-': raise ValueError('Expected weekday abbrevation, got: %s' % self) self.relative = relative and int(relative) or None self.params = Parameters() return self def to_ical(self): return self.encode(DEFAULT_ENCODING).upper() @classmethod def from_ical(cls, ical): try: return cls(ical.upper()) except: raise ValueError('Expected weekday abbrevation, got: %s' % ical) class vFrequency(compat.unicode_type): """A simple class that catches illegal values. """ frequencies = CaselessDict({ "SECONDLY": "SECONDLY", "MINUTELY": "MINUTELY", "HOURLY": "HOURLY", "DAILY": "DAILY", "WEEKLY": "WEEKLY", "MONTHLY": "MONTHLY", "YEARLY": "YEARLY", }) def __new__(cls, value, encoding=DEFAULT_ENCODING): value = to_unicode(value, encoding=encoding) self = super(vFrequency, cls).__new__(cls, value) if self not in vFrequency.frequencies: raise ValueError('Expected frequency, got: %s' % self) self.params = Parameters() return self def to_ical(self): return self.encode(DEFAULT_ENCODING).upper() @classmethod def from_ical(cls, ical): try: return cls(ical.upper()) except: raise ValueError('Expected frequency, got: %s' % ical) class vRecur(CaselessDict): """Recurrence definition. """ frequencies = ["SECONDLY", "MINUTELY", "HOURLY", "DAILY", "WEEKLY", "MONTHLY", "YEARLY"] # Mac iCal ignores RRULEs where FREQ is not the first rule part. # Sorts parts according to the order listed in RFC 5545, section 3.3.10. canonical_order = ("FREQ", "UNTIL", "COUNT", "INTERVAL", "BYSECOND", "BYMINUTE", "BYHOUR", "BYDAY", "BYMONTHDAY", "BYYEARDAY", "BYWEEKNO", "BYMONTH", "BYSETPOS", "WKST") types = CaselessDict({ 'COUNT': vInt, 'INTERVAL': vInt, 'BYSECOND': vInt, 'BYMINUTE': vInt, 'BYHOUR': vInt, 'BYWEEKNO': vInt, 'BYMONTHDAY': vInt, 'BYYEARDAY': vInt, 'BYMONTH': vInt, 'UNTIL': vDDDTypes, 'BYSETPOS': vInt, 'WKST': vWeekday, 'BYDAY': vWeekday, 'FREQ': vFrequency, }) def __init__(self, *args, **kwargs): super(vRecur, self).__init__(*args, **kwargs) self.params = Parameters() def to_ical(self): result = [] for key, vals in self.sorted_items(): typ = self.types[key] if not isinstance(vals, SEQUENCE_TYPES): vals = [vals] vals = b','.join(typ(val).to_ical() for val in vals) # CaselessDict keys are always unicode key = key.encode(DEFAULT_ENCODING) result.append(key + b'=' + vals) return b';'.join(result) @classmethod def parse_type(cls, key, values): # integers parser = cls.types.get(key, vText) return [parser.from_ical(v) for v in values.split(',')] @classmethod def from_ical(cls, ical): if isinstance(ical, cls): return ical try: recur = cls() for pairs in ical.split(';'): try: key, vals = pairs.split('=') except ValueError: # E.g. incorrect trailing semicolon, like (issue #157): # FREQ=YEARLY;BYMONTH=11;BYDAY=1SU; continue recur[key] = cls.parse_type(key, vals) return dict(recur) except: raise ValueError('Error in recurrence rule: %s' % ical) class vText(compat.unicode_type): """Simple text. """ def __new__(cls, value, encoding=DEFAULT_ENCODING): value = to_unicode(value, encoding=encoding) self = super(vText, cls).__new__(cls, value) self.encoding = encoding self.params = Parameters() return self def __repr__(self): return "vText('%s')" % self.to_ical() def to_ical(self): return escape_char(self).encode(self.encoding) @classmethod def from_ical(cls, ical): ical_unesc = unescape_char(ical) return cls(ical_unesc) class vTime(object): """Render and generates iCalendar time format. """ def __init__(self, *args): if len(args) == 1: if not isinstance(args[0], (time, datetime)): raise ValueError('Expected a datetime.time, got: %s' % args[0]) self.dt = args[0] else: self.dt = time(*args) self.params = Parameters({'value': 'TIME'}) def to_ical(self): return self.dt.strftime("%H%M%S") @staticmethod def from_ical(ical): # TODO: timezone support try: timetuple = (int(ical[:2]), int(ical[2:4]), int(ical[4:6])) return time(*timetuple) except: raise ValueError('Expected time, got: %s' % ical) class vUri(compat.unicode_type): """Uniform resource identifier is basically just an unquoted string. """ def __new__(cls, value, encoding=DEFAULT_ENCODING): value = to_unicode(value, encoding=encoding) self = super(vUri, cls).__new__(cls, value) self.params = Parameters() return self def to_ical(self): return self.encode(DEFAULT_ENCODING) @classmethod def from_ical(cls, ical): try: return cls(ical) except: raise ValueError('Expected , got: %s' % ical) class vGeo(object): """A special type that is only indirectly defined in the rfc. """ def __init__(self, geo): try: latitude, longitude = (geo[0], geo[1]) latitude = float(latitude) longitude = float(longitude) except: raise ValueError('Input must be (float, float) for ' 'latitude and longitude') self.latitude = latitude self.longitude = longitude self.params = Parameters() def to_ical(self): return '%s;%s' % (self.latitude, self.longitude) @staticmethod def from_ical(ical): try: latitude, longitude = ical.split(';') return (float(latitude), float(longitude)) except: raise ValueError("Expected 'float;float' , got: %s" % ical) class vUTCOffset(object): """Renders itself as a utc offset. """ ignore_exceptions = False # if True, and we cannot parse this # component, we will silently ignore # it, rather than let the exception # propagate upwards def __init__(self, td): if not isinstance(td, timedelta): raise ValueError('Offset value MUST be a timedelta instance') self.td = td self.params = Parameters() def to_ical(self): if self.td < timedelta(0): sign = '-%s' td = timedelta(0) - self.td # get timedelta relative to 0 else: # Google Calendar rejects '0000' but accepts '+0000' sign = '+%s' td = self.td days, seconds = td.days, td.seconds hours = abs(days * 24 + seconds // 3600) minutes = abs((seconds % 3600) // 60) seconds = abs(seconds % 60) if seconds: duration = '%02i%02i%02i' % (hours, minutes, seconds) else: duration = '%02i%02i' % (hours, minutes) return sign % duration @classmethod def from_ical(cls, ical): if isinstance(ical, cls): return ical.td try: sign, hours, minutes, seconds = (ical[0:1], int(ical[1:3]), int(ical[3:5]), int(ical[5:7] or 0)) offset = timedelta(hours=hours, minutes=minutes, seconds=seconds) except: raise ValueError('Expected utc offset, got: %s' % ical) if not cls.ignore_exceptions and offset >= timedelta(hours=24): raise ValueError( 'Offset must be less than 24 hours, was %s' % ical) if sign == '-': return -offset return offset class vInline(compat.unicode_type): """This is an especially dumb class that just holds raw unparsed text and has parameters. Conversion of inline values are handled by the Component class, so no further processing is needed. """ def __new__(cls, value, encoding=DEFAULT_ENCODING): value = to_unicode(value, encoding=encoding) self = super(vInline, cls).__new__(cls, value) self.params = Parameters() return self def to_ical(self): return self.encode(DEFAULT_ENCODING) @classmethod def from_ical(cls, ical): return cls(ical) class TypesFactory(CaselessDict): """All Value types defined in rfc 2445 are registered in this factory class. The value and parameter names don't overlap. So one factory is enough for both kinds. """ def __init__(self, *args, **kwargs): "Set keys to upper for initial dict" super(TypesFactory, self).__init__(*args, **kwargs) self.all_types = ( vBinary, vBoolean, vCalAddress, vDDDLists, vDDDTypes, vDate, vDatetime, vDuration, vFloat, vFrequency, vGeo, vInline, vInt, vPeriod, vRecur, vText, vTime, vUTCOffset, vUri, vWeekday, vCategory, ) self['binary'] = vBinary self['boolean'] = vBoolean self['cal-address'] = vCalAddress self['date'] = vDDDTypes self['date-time'] = vDDDTypes self['duration'] = vDDDTypes self['float'] = vFloat self['integer'] = vInt self['period'] = vPeriod self['recur'] = vRecur self['text'] = vText self['time'] = vTime self['uri'] = vUri self['utc-offset'] = vUTCOffset self['geo'] = vGeo self['inline'] = vInline self['date-time-list'] = vDDDLists self['categories'] = vCategory ################################################# # Property types # These are the default types types_map = CaselessDict({ #################################### # Property value types # Calendar Properties 'calscale': 'text', 'method': 'text', 'prodid': 'text', 'version': 'text', # Descriptive Component Properties 'attach': 'uri', 'categories': 'categories', 'class': 'text', 'comment': 'text', 'description': 'text', 'geo': 'geo', 'location': 'text', 'percent-complete': 'integer', 'priority': 'integer', 'resources': 'text', 'status': 'text', 'summary': 'text', # Date and Time Component Properties 'completed': 'date-time', 'dtend': 'date-time', 'due': 'date-time', 'dtstart': 'date-time', 'duration': 'duration', 'freebusy': 'period', 'transp': 'text', # Time Zone Component Properties 'tzid': 'text', 'tzname': 'text', 'tzoffsetfrom': 'utc-offset', 'tzoffsetto': 'utc-offset', 'tzurl': 'uri', # Relationship Component Properties 'attendee': 'cal-address', 'contact': 'text', 'organizer': 'cal-address', 'recurrence-id': 'date-time', 'related-to': 'text', 'url': 'uri', 'uid': 'text', # Recurrence Component Properties 'exdate': 'date-time-list', 'exrule': 'recur', 'rdate': 'date-time-list', 'rrule': 'recur', # Alarm Component Properties 'action': 'text', 'repeat': 'integer', 'trigger': 'duration', # Change Management Component Properties 'created': 'date-time', 'dtstamp': 'date-time', 'last-modified': 'date-time', 'sequence': 'integer', # Miscellaneous Component Properties 'request-status': 'text', #################################### # parameter types (luckily there is no name overlap) 'altrep': 'uri', 'cn': 'text', 'cutype': 'text', 'delegated-from': 'cal-address', 'delegated-to': 'cal-address', 'dir': 'uri', 'encoding': 'text', 'fmttype': 'text', 'fbtype': 'text', 'language': 'text', 'member': 'cal-address', 'partstat': 'text', 'range': 'text', 'related': 'text', 'reltype': 'text', 'role': 'text', 'rsvp': 'boolean', 'sent-by': 'cal-address', 'tzid': 'text', 'value': 'text', }) def for_property(self, name): """Returns a the default type for a property or parameter """ return self[self.types_map.get(name, 'text')] def to_ical(self, name, value): """Encodes a named value from a primitive python type to an icalendar encoded string. """ type_class = self.for_property(name) return type_class(value).to_ical() def from_ical(self, name, value): """Decodes a named property or parameter value from an icalendar encoded string to a primitive python type. """ type_class = self.for_property(name) decoded = type_class.from_ical(value) return decoded icalendar-4.0.3/src/icalendar/cal.py0000644000076600000240000006310713357326437020116 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- """Calendar is a dictionary like Python object that can render itself as VCAL files according to rfc2445. These are the defined components. """ from datetime import datetime, timedelta from icalendar.caselessdict import CaselessDict from icalendar.parser import Contentline from icalendar.parser import Contentlines from icalendar.parser import Parameters from icalendar.parser import q_join from icalendar.parser import q_split from icalendar.parser_tools import DEFAULT_ENCODING from icalendar.prop import TypesFactory from icalendar.prop import vText, vDDDLists from icalendar.timezone_cache import _timezone_cache import pytz import dateutil.rrule from pytz.tzinfo import DstTzInfo from icalendar.compat import unicode_type ###################################### # The component factory class ComponentFactory(CaselessDict): """All components defined in rfc 2445 are registered in this factory class. To get a component you can use it like this. """ def __init__(self, *args, **kwargs): """Set keys to upper for initial dict. """ super(ComponentFactory, self).__init__(*args, **kwargs) self['VEVENT'] = Event self['VTODO'] = Todo self['VJOURNAL'] = Journal self['VFREEBUSY'] = FreeBusy self['VTIMEZONE'] = Timezone self['STANDARD'] = TimezoneStandard self['DAYLIGHT'] = TimezoneDaylight self['VALARM'] = Alarm self['VCALENDAR'] = Calendar # These Properties have multiple property values inlined in one propertyline # seperated by comma. Use CaselessDict as simple caseless set. INLINE = CaselessDict({ 'CATEGORIES': 1, 'RESOURCES': 1, 'FREEBUSY': 1, }) _marker = [] class Component(CaselessDict): """Component is the base object for calendar, Event and the other components defined in RFC 2445. normally you will not use this class directy, but rather one of the subclasses. """ name = None # should be defined in each component required = () # These properties are required singletons = () # These properties must only appear once multiple = () # may occur more than once exclusive = () # These properties are mutually exclusive inclusive = () # if any occurs the other(s) MUST occur # ('duration', 'repeat') ignore_exceptions = False # if True, and we cannot parse this # component, we will silently ignore # it, rather than let the exception # propagate upwards # not_compliant = [''] # List of non-compliant properties. def __init__(self, *args, **kwargs): """Set keys to upper for initial dict. """ super(Component, self).__init__(*args, **kwargs) # set parameters here for properties that use non-default values self.subcomponents = [] # Components can be nested. self.errors = [] # If we ignored exception(s) while # parsing a property, contains error strings # def is_compliant(self, name): # """Returns True is the given property name is compliant with the # icalendar implementation. # # If the parser is too strict it might prevent parsing erroneous but # otherwise compliant properties. So the parser is pretty lax, but it is # possible to test for non-complience by calling this method. # """ # return name in not_compliant def __bool__(self): """Returns True, CaselessDict would return False if it had no items. """ return True # python 2 compatibility __nonzero__ = __bool__ def is_empty(self): """Returns True if Component has no items or subcomponents, else False. """ return True if not (list(self.values()) + self.subcomponents) else False # noqa @property def is_broken(self): return bool(self.errors) ############################# # handling of property values def _encode(self, name, value, parameters=None, encode=1): """Encode values to icalendar property values. :param name: Name of the property. :type name: string :param value: Value of the property. Either of a basic Python type of any of the icalendar's own property types. :type value: Python native type or icalendar property type. :param parameters: Property parameter dictionary for the value. Only available, if encode is set to True. :type parameters: Dictionary :param encode: True, if the value should be encoded to one of icalendar's own property types (Fallback is "vText") or False, if not. :type encode: Boolean :returns: icalendar property value """ if not encode: return value if isinstance(value, types_factory.all_types): # Don't encode already encoded values. return value klass = types_factory.for_property(name) obj = klass(value) if parameters: if isinstance(parameters, dict): params = Parameters() for key, item in parameters.items(): params[key] = item parameters = params assert isinstance(parameters, Parameters) obj.params = parameters return obj def add(self, name, value, parameters=None, encode=1): """Add a property. :param name: Name of the property. :type name: string :param value: Value of the property. Either of a basic Python type of any of the icalendar's own property types. :type value: Python native type or icalendar property type. :param parameters: Property parameter dictionary for the value. Only available, if encode is set to True. :type parameters: Dictionary :param encode: True, if the value should be encoded to one of icalendar's own property types (Fallback is "vText") or False, if not. :type encode: Boolean :returns: None """ if isinstance(value, datetime) and\ name.lower() in ('dtstamp', 'created', 'last-modified'): # RFC expects UTC for those... force value conversion. if getattr(value, 'tzinfo', False) and value.tzinfo is not None: value = value.astimezone(pytz.utc) else: # assume UTC for naive datetime instances value = pytz.utc.localize(value) # encode value if encode and isinstance(value, list) \ and name.lower() not in ['rdate', 'exdate', 'categories']: # Individually convert each value to an ical type except rdate and # exdate, where lists of dates might be passed to vDDDLists. value = [self._encode(name, v, parameters, encode) for v in value] else: value = self._encode(name, value, parameters, encode) # set value if name in self: # If property already exists, append it. oldval = self[name] if isinstance(oldval, list): if isinstance(value, list): value = oldval + value else: oldval.append(value) value = oldval else: value = [oldval, value] self[name] = value def _decode(self, name, value): """Internal for decoding property values. """ # TODO: Currently the decoded method calls the icalendar.prop instances # from_ical. We probably want to decode properties into Python native # types here. But when parsing from an ical string with from_ical, we # want to encode the string into a real icalendar.prop property. if isinstance(value, vDDDLists): # TODO: Workaround unfinished decoding return value decoded = types_factory.from_ical(name, value) # TODO: remove when proper decoded is implemented in every prop.* class # Workaround to decode vText properly if isinstance(decoded, vText): decoded = decoded.encode(DEFAULT_ENCODING) return decoded def decoded(self, name, default=_marker): """Returns decoded value of property. """ # XXX: fail. what's this function supposed to do in the end? # -rnix if name in self: value = self[name] if isinstance(value, list): return [self._decode(name, v) for v in value] return self._decode(name, value) else: if default is _marker: raise KeyError(name) else: return default ######################################################################## # Inline values. A few properties have multiple values inlined in in one # property line. These methods are used for splitting and joining these. def get_inline(self, name, decode=1): """Returns a list of values (split on comma). """ vals = [v.strip('" ') for v in q_split(self[name])] if decode: return [self._decode(name, val) for val in vals] return vals def set_inline(self, name, values, encode=1): """Converts a list of values into comma seperated string and sets value to that. """ if encode: values = [self._encode(name, value, encode=1) for value in values] self[name] = types_factory['inline'](q_join(values)) ######################### # Handling of components def add_component(self, component): """Add a subcomponent to this component. """ self.subcomponents.append(component) def _walk(self, name): """Walk to given component. """ result = [] if name is None or self.name == name: result.append(self) for subcomponent in self.subcomponents: result += subcomponent._walk(name) return result def walk(self, name=None): """Recursively traverses component and subcomponents. Returns sequence of same. If name is passed, only components with name will be returned. """ if name is not None: name = name.upper() return self._walk(name) ##################### # Generation def property_items(self, recursive=True, sorted=True): """Returns properties in this component and subcomponents as: [(name, value), ...] """ vText = types_factory['text'] properties = [('BEGIN', vText(self.name).to_ical())] if sorted: property_names = self.sorted_keys() else: property_names = self.keys() for name in property_names: values = self[name] if isinstance(values, list): # normally one property is one line for value in values: properties.append((name, value)) else: properties.append((name, values)) if recursive: # recursion is fun! for subcomponent in self.subcomponents: properties += subcomponent.property_items(sorted=sorted) properties.append(('END', vText(self.name).to_ical())) return properties @classmethod def from_ical(cls, st, multiple=False): """Populates the component recursively from a string. """ stack = [] # a stack of components comps = [] for line in Contentlines.from_ical(st): # raw parsing if not line: continue try: name, params, vals = line.parts() except ValueError as e: # if unable to parse a line within a component # that ignores exceptions, mark the component # as broken and skip the line. otherwise raise. component = stack[-1] if stack else None if not component or not component.ignore_exceptions: raise component.errors.append((None, unicode_type(e))) continue uname = name.upper() # check for start of component if uname == 'BEGIN': # try and create one of the components defined in the spec, # otherwise get a general Components for robustness. c_name = vals.upper() c_class = component_factory.get(c_name, Component) # If component factory cannot resolve ``c_name``, the generic # ``Component`` class is used which does not have the name set. # That's opposed to the usage of ``cls``, which represents a # more concrete subclass with a name set (e.g. VCALENDAR). component = c_class() if not getattr(component, 'name', ''): # undefined components component.name = c_name stack.append(component) # check for end of event elif uname == 'END': # we are done adding properties to this component # so pop it from the stack and add it to the new top. component = stack.pop() if not stack: # we are at the end comps.append(component) else: stack[-1].add_component(component) if vals == 'VTIMEZONE' and \ 'TZID' in component and \ component['TZID'] not in pytz.all_timezones and \ component['TZID'] not in _timezone_cache: _timezone_cache[component['TZID']] = component.to_tz() # we are adding properties to the current top of the stack else: factory = types_factory.for_property(name) component = stack[-1] if stack else None if not component: raise ValueError('Property "{prop}" does not have ' 'a parent component.'.format(prop=name)) datetime_names = ('DTSTART', 'DTEND', 'RECURRENCE-ID', 'DUE', 'FREEBUSY', 'RDATE', 'EXDATE') try: if name in datetime_names and 'TZID' in params: vals = factory(factory.from_ical(vals, params['TZID'])) else: vals = factory(factory.from_ical(vals)) except ValueError as e: if not component.ignore_exceptions: raise component.errors.append((uname, unicode_type(e))) component.add(name, None, encode=0) else: vals.params = params component.add(name, vals, encode=0) if multiple: return comps if len(comps) > 1: raise ValueError('Found multiple components where ' 'only one is allowed: {st!r}'.format(**locals())) if len(comps) < 1: raise ValueError('Found no components where ' 'exactly one is required: ' '{st!r}'.format(**locals())) return comps[0] def content_line(self, name, value, sorted=True): """Returns property as content line. """ params = getattr(value, 'params', Parameters()) return Contentline.from_parts(name, params, value, sorted=sorted) def content_lines(self, sorted=True): """Converts the Component and subcomponents into content lines. """ contentlines = Contentlines() for name, value in self.property_items(sorted=sorted): cl = self.content_line(name, value, sorted=sorted) contentlines.append(cl) contentlines.append('') # remember the empty string in the end return contentlines def to_ical(self, sorted=True): ''' :param sorted: Whether parameters and properties should be lexicographically sorted. ''' content_lines = self.content_lines(sorted=sorted) return content_lines.to_ical() def __repr__(self): """String representation of class with all of it's subcomponents. """ subs = ', '.join([str(it) for it in self.subcomponents]) return '%s(%s%s)' % ( self.name or type(self).__name__, dict(self), ', %s' % subs if subs else '' ) ####################################### # components defined in RFC 5545 class Event(Component): name = 'VEVENT' canonical_order = ( 'SUMMARY', 'DTSTART', 'DTEND', 'DURATION', 'DTSTAMP', 'UID', 'RECURRENCE-ID', 'SEQUENCE', 'RRULE', 'RDATE', 'EXDATE', ) required = ('UID', 'DTSTAMP',) singletons = ( 'CLASS', 'CREATED', 'DESCRIPTION', 'DTSTART', 'GEO', 'LAST-MODIFIED', 'LOCATION', 'ORGANIZER', 'PRIORITY', 'DTSTAMP', 'SEQUENCE', 'STATUS', 'SUMMARY', 'TRANSP', 'URL', 'RECURRENCE-ID', 'DTEND', 'DURATION', 'UID', 'CATEGORIES', ) exclusive = ('DTEND', 'DURATION',) multiple = ( 'ATTACH', 'ATTENDEE', 'COMMENT', 'CONTACT', 'EXDATE', 'RSTATUS', 'RELATED', 'RESOURCES', 'RDATE', 'RRULE' ) ignore_exceptions = True class Todo(Component): name = 'VTODO' required = ('UID', 'DTSTAMP',) singletons = ( 'CLASS', 'COMPLETED', 'CREATED', 'DESCRIPTION', 'DTSTAMP', 'DTSTART', 'GEO', 'LAST-MODIFIED', 'LOCATION', 'ORGANIZER', 'PERCENT-COMPLETE', 'PRIORITY', 'RECURRENCE-ID', 'SEQUENCE', 'STATUS', 'SUMMARY', 'UID', 'URL', 'DUE', 'DURATION', ) exclusive = ('DUE', 'DURATION',) multiple = ( 'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE', 'RSTATUS', 'RELATED', 'RESOURCES', 'RDATE', 'RRULE' ) class Journal(Component): name = 'VJOURNAL' required = ('UID', 'DTSTAMP',) singletons = ( 'CLASS', 'CREATED', 'DTSTART', 'DTSTAMP', 'LAST-MODIFIED', 'ORGANIZER', 'RECURRENCE-ID', 'SEQUENCE', 'STATUS', 'SUMMARY', 'UID', 'URL', ) multiple = ( 'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE', 'RELATED', 'RDATE', 'RRULE', 'RSTATUS', 'DESCRIPTION', ) class FreeBusy(Component): name = 'VFREEBUSY' required = ('UID', 'DTSTAMP',) singletons = ( 'CONTACT', 'DTSTART', 'DTEND', 'DTSTAMP', 'ORGANIZER', 'UID', 'URL', ) multiple = ('ATTENDEE', 'COMMENT', 'FREEBUSY', 'RSTATUS',) class Timezone(Component): name = 'VTIMEZONE' canonical_order = ('TZID',) required = ('TZID',) # it also requires one of components DAYLIGHT and STANDARD singletons = ('TZID', 'LAST-MODIFIED', 'TZURL',) @staticmethod def _extract_offsets(component, tzname): """extract offsets and transition times from a VTIMEZONE component :param component: a STANDARD or DAYLIGHT component :param tzname: the name of the zone """ offsetfrom = component['TZOFFSETFROM'].td offsetto = component['TZOFFSETTO'].td dtstart = component['DTSTART'].dt # offsets need to be rounded to the next minute, we might loose up # to 30 seconds accuracy, but it can't be helped (datetime # supposedly cannot handle smaller offsets) offsetto_s = int((offsetto.seconds + 30) / 60) * 60 offsetto = timedelta(days=offsetto.days, seconds=offsetto_s) offsetfrom_s = int((offsetfrom.seconds + 30) / 60) * 60 offsetfrom = timedelta(days=offsetfrom.days, seconds=offsetfrom_s) # expand recurrences if 'RRULE' in component: rrulestr = component['RRULE'].to_ical().decode('utf-8') rrule = dateutil.rrule.rrulestr(rrulestr, dtstart=dtstart) if not {'UNTIL', 'COUNT'}.intersection(component['RRULE'].keys()): # pytz.timezones don't know any transition dates after 2038 # either rrule._until = datetime(2038, 12, 31) elif 'UNTIL' in component['RRULE'] and rrule._until.tzinfo: rrule._until = rrule._until.replace(tzinfo=None) transtimes = rrule # or rdates elif 'RDATE' in component: if not isinstance(component['RDATE'], list): rdates = [component['RDATE']] else: rdates = component['RDATE'] transtimes = [dtstart] + [leaf.dt for tree in rdates for leaf in tree.dts] else: transtimes = [dtstart] transitions = [(transtime, offsetfrom, offsetto, tzname) for transtime in set(transtimes)] if component.name == 'STANDARD': is_dst = 0 elif component.name == 'DAYLIGHT': is_dst = 1 return is_dst, transitions @staticmethod def _make_unique_tzname(tzname, tznames): """ :param tzname: Candidate tzname :param tznames: Other tznames """ # TODO better way of making sure tznames are unique while tzname in tznames: tzname += '_1' tznames.add(tzname) return tzname def to_tz(self): """convert this VTIMEZONE component to a pytz.timezone object """ try: zone = str(self['TZID']) except UnicodeEncodeError: zone = self['TZID'].encode('ascii', 'replace') transitions = [] dst = {} tznames = set() for component in self.walk(): if type(component) == Timezone: continue assert isinstance(component['DTSTART'].dt, datetime), ( "VTIMEZONEs sub-components' DTSTART must be of type datetime, not date" ) try: tzname = str(component['TZNAME']) except UnicodeEncodeError: tzname = component['TZNAME'].encode('ascii', 'replace') tzname = self._make_unique_tzname(tzname, tznames) except KeyError: tzname = '{0}_{1}_{2}_{3}'.format( zone, component['DTSTART'].to_ical().decode('utf-8'), component['TZOFFSETFROM'].to_ical(), # for whatever reason this is str/unicode component['TZOFFSETTO'].to_ical(), # for whatever reason this is str/unicode ) tzname = self._make_unique_tzname(tzname, tznames) dst[tzname], component_transitions = self._extract_offsets( component, tzname ) transitions.extend(component_transitions) transitions.sort() transition_times = [ transtime - osfrom for transtime, osfrom, _, _ in transitions ] # transition_info is a list with tuples in the format # (utcoffset, dstoffset, name) # dstoffset = 0, if current transition is to standard time # = this_utcoffset - prev_standard_utcoffset, otherwise transition_info = [] for num, (transtime, osfrom, osto, name) in enumerate(transitions): dst_offset = False if not dst[name]: dst_offset = timedelta(seconds=0) else: # go back in time until we find a transition to dst for index in range(num - 1, -1, -1): if not dst[transitions[index][3]]: # [3] is the name dst_offset = osto - transitions[index][2] # [2] is osto # noqa break # when the first transition is to dst, we didn't find anything # in the past, so we have to look into the future if not dst_offset: for index in range(num, len(transitions)): if not dst[transitions[index][3]]: # [3] is the name dst_offset = osto - transitions[index][2] # [2] is osto # noqa break assert dst_offset is not False transition_info.append((osto, dst_offset, name)) cls = type(zone, (DstTzInfo,), { 'zone': zone, '_utc_transition_times': transition_times, '_transition_info': transition_info }) return cls() class TimezoneStandard(Component): name = 'STANDARD' required = ('DTSTART', 'TZOFFSETTO', 'TZOFFSETFROM') singletons = ('DTSTART', 'TZOFFSETTO', 'TZOFFSETFROM',) multiple = ('COMMENT', 'RDATE', 'TZNAME', 'RRULE', 'EXDATE') class TimezoneDaylight(Component): name = 'DAYLIGHT' required = TimezoneStandard.required singletons = TimezoneStandard.singletons multiple = TimezoneStandard.multiple class Alarm(Component): name = 'VALARM' # some properties MAY/MUST/MUST NOT appear depending on ACTION value required = ('ACTION', 'TRIGGER',) singletons = ( 'ATTACH', 'ACTION', 'DESCRIPTION', 'SUMMARY', 'TRIGGER', 'DURATION', 'REPEAT', ) inclusive = (('DURATION', 'REPEAT',), ('SUMMARY', 'ATTENDEE',)) multiple = ('ATTENDEE', 'ATTACH') class Calendar(Component): """This is the base object for an iCalendar file. """ name = 'VCALENDAR' canonical_order = ('VERSION', 'PRODID', 'CALSCALE', 'METHOD',) required = ('PRODID', 'VERSION', ) singletons = ('PRODID', 'VERSION', 'CALSCALE', 'METHOD') # These are read only singleton, so one instance is enough for the module types_factory = TypesFactory() component_factory = ComponentFactory() icalendar-4.0.3/src/icalendar/tools.py0000644000076600000240000000206213357326437020510 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- from datetime import datetime from icalendar.parser_tools import to_unicode from icalendar.prop import vDatetime from icalendar.prop import vText from string import ascii_letters from string import digits import random class UIDGenerator(object): """If you are too lazy to create real uid's. """ chars = list(ascii_letters + digits) def rnd_string(self, length=16): """Generates a string with random characters of length. """ return ''.join([random.choice(self.chars) for _ in range(length)]) def uid(self, host_name='example.com', unique=''): """Generates a unique id consisting of: datetime-uniquevalue@host. Like: 20050105T225746Z-HKtJMqUgdO0jDUwm@example.com """ host_name = to_unicode(host_name) unique = unique or self.rnd_string() today = to_unicode(vDatetime(datetime.today()).to_ical()) return vText('%s-%s@%s' % (today, unique, host_name)) icalendar-4.0.3/src/icalendar/tests/0000755000076600000240000000000013357326437020140 5ustar mauritsstaff00000000000000icalendar-4.0.3/src/icalendar/tests/multiple.ics0000644000076600000240000000166413357326437022502 0ustar mauritsstaff00000000000000BEGIN:VCALENDAR VERSION :2.0 PRODID :-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN METHOD :PUBLISH BEGIN:VEVENT UID :956630271 SUMMARY :Christmas Day CLASS :PUBLIC X-MOZILLA-ALARM-DEFAULT-UNITS :minutes X-MOZILLA-ALARM-DEFAULT-LENGTH :15 X-MOZILLA-RECUR-DEFAULT-UNITS :weeks X-MOZILLA-RECUR-DEFAULT-INTERVAL :1 DTSTART ;VALUE=DATE :20031225 DTEND ;VALUE=DATE :20031226 DTSTAMP :20020430T114937Z END:VEVENT END:VCALENDAR BEGIN:VCALENDAR VERSION :2.0 PRODID :-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN METHOD :PUBLISH BEGIN:VEVENT UID :911737808 SUMMARY :Boxing Day CLASS :PUBLIC X-MOZILLA-ALARM-DEFAULT-UNITS :minutes X-MOZILLA-ALARM-DEFAULT-LENGTH :15 X-MOZILLA-RECUR-DEFAULT-UNITS :weeks X-MOZILLA-RECUR-DEFAULT-INTERVAL :1 DTSTART ;VALUE=DATE :20030501 DTSTAMP :20020430T114937Z END:VEVENT BEGIN:VEVENT UID :wh4t3v3r DTSTART;VALUE=DATE:20031225 SUMMARY:Christmas again! END:VEVENT END:VCALENDAR icalendar-4.0.3/src/icalendar/tests/timezoned.ics0000644000076600000240000000145413357326437022642 0ustar mauritsstaff00000000000000BEGIN:VCALENDAR PRODID:-//Plone.org//NONSGML plone.app.event//EN VERSION:2.0 X-WR-CALNAME:test create calendar X-WR-CALDESC:icalendar test X-WR-RELCALID:12345 X-WR-TIMEZONE:Europe/Vienna BEGIN:VTIMEZONE TZID:Europe/Vienna X-LIC-LOCATION:Europe/Vienna BEGIN:DAYLIGHT TZOFFSETFROM:+0100 TZOFFSETTO:+0200 TZNAME:CEST DTSTART:19700329T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:CET DTSTART:19701025T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT DTSTART;TZID=Europe/Vienna:20120213T100000 DTEND;TZID=Europe/Vienna:20120217T180000 DTSTAMP:20101010T091010Z CREATED:20101010T091010Z UID:123456 SUMMARY:artsprint 2012 DESCRIPTION:sprinting at the artsprint LOCATION:aka bild, wien END:VEVENT END:VCALENDAR icalendar-4.0.3/src/icalendar/tests/test_multiple.py0000644000076600000240000000157613357326437023415 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- from icalendar import Calendar from icalendar.prop import vText import unittest import os class TestMultiple(unittest.TestCase): """A example with multiple VCALENDAR components""" def test_multiple(self): directory = os.path.dirname(__file__) with open(os.path.join(directory, 'multiple.ics'), 'rb') as fp: data = fp.read() cals = Calendar.from_ical(data, multiple=True) self.assertEqual(len(cals), 2) self.assertSequenceEqual([comp.name for comp in cals[0].walk()], ['VCALENDAR', 'VEVENT']) self.assertSequenceEqual([comp.name for comp in cals[1].walk()], ['VCALENDAR', 'VEVENT', 'VEVENT']) self.assertEqual( cals[0]['prodid'], vText('-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN') ) icalendar-4.0.3/src/icalendar/tests/test_timezoned.py0000644000076600000240000004044413357326437023555 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import unittest import datetime import dateutil.parser import icalendar import os import pytz class TestTimezoned(unittest.TestCase): def test_create_from_ical(self): directory = os.path.dirname(__file__) with open(os.path.join(directory, 'timezoned.ics'), 'rb') as fp: data = fp.read() cal = icalendar.Calendar.from_ical(data) self.assertEqual( cal['prodid'].to_ical(), b"-//Plone.org//NONSGML plone.app.event//EN" ) timezones = cal.walk('VTIMEZONE') self.assertEqual(len(timezones), 1) tz = timezones[0] self.assertEqual(tz['tzid'].to_ical(), b"Europe/Vienna") std = tz.walk('STANDARD')[0] self.assertEqual( std.decoded('TZOFFSETFROM'), datetime.timedelta(0, 7200) ) ev1 = cal.walk('VEVENT')[0] self.assertEqual( ev1.decoded('DTSTART'), pytz.timezone('Europe/Vienna').localize( datetime.datetime(2012, 2, 13, 10, 0, 0) ) ) self.assertEqual( ev1.decoded('DTSTAMP'), pytz.utc.localize( datetime.datetime(2010, 10, 10, 9, 10, 10) ) ) def test_create_to_ical(self): cal = icalendar.Calendar() cal.add('prodid', "-//Plone.org//NONSGML plone.app.event//EN") cal.add('version', "2.0") cal.add('x-wr-calname', "test create calendar") cal.add('x-wr-caldesc', "icalendar tests") cal.add('x-wr-relcalid', "12345") cal.add('x-wr-timezone', "Europe/Vienna") tzc = icalendar.Timezone() tzc.add('tzid', 'Europe/Vienna') tzc.add('x-lic-location', 'Europe/Vienna') tzs = icalendar.TimezoneStandard() tzs.add('tzname', 'CET') tzs.add('dtstart', datetime.datetime(1970, 10, 25, 3, 0, 0)) tzs.add('rrule', {'freq': 'yearly', 'bymonth': 10, 'byday': '-1su'}) tzs.add('TZOFFSETFROM', datetime.timedelta(hours=2)) tzs.add('TZOFFSETTO', datetime.timedelta(hours=1)) tzd = icalendar.TimezoneDaylight() tzd.add('tzname', 'CEST') tzd.add('dtstart', datetime.datetime(1970, 3, 29, 2, 0, 0)) tzs.add('rrule', {'freq': 'yearly', 'bymonth': 3, 'byday': '-1su'}) tzd.add('TZOFFSETFROM', datetime.timedelta(hours=1)) tzd.add('TZOFFSETTO', datetime.timedelta(hours=2)) tzc.add_component(tzs) tzc.add_component(tzd) cal.add_component(tzc) event = icalendar.Event() tz = pytz.timezone("Europe/Vienna") event.add( 'dtstart', tz.localize(datetime.datetime(2012, 2, 13, 10, 00, 00))) event.add( 'dtend', tz.localize(datetime.datetime(2012, 2, 17, 18, 00, 00))) event.add( 'dtstamp', tz.localize(datetime.datetime(2010, 10, 10, 10, 10, 10))) event.add( 'created', tz.localize(datetime.datetime(2010, 10, 10, 10, 10, 10))) event.add('uid', '123456') event.add( 'last-modified', tz.localize(datetime.datetime(2010, 10, 10, 10, 10, 10))) event.add('summary', 'artsprint 2012') # event.add('rrule', 'FREQ=YEARLY;INTERVAL=1;COUNT=10') event.add('description', 'sprinting at the artsprint') event.add('location', 'aka bild, wien') event.add('categories', 'first subject') event.add('categories', 'second subject') event.add('attendee', 'häns') event.add('attendee', 'franz') event.add('attendee', 'sepp') event.add('contact', 'Max Mustermann, 1010 Wien') event.add('url', 'http://plone.org') cal.add_component(event) test_out = b'|'.join(cal.to_ical().splitlines()) test_out = test_out.decode('utf-8') vtimezone_lines = "BEGIN:VTIMEZONE|TZID:Europe/Vienna|X-LIC-LOCATION:" "Europe/Vienna|BEGIN:STANDARD|DTSTART;VALUE=DATE-TIME:19701025T03" "0000|RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10|RRULE:FREQ=YEARLY;B" "YDAY=-1SU;BYMONTH=3|TZNAME:CET|TZOFFSETFROM:+0200|TZOFFSETTO:+01" "00|END:STANDARD|BEGIN:DAYLIGHT|DTSTART;VALUE=DATE-TIME:19700329T" "020000|TZNAME:CEST|TZOFFSETFROM:+0100|TZOFFSETTO:+0200|END:DAYLI" "GHT|END:VTIMEZONE" self.assertTrue(vtimezone_lines in test_out) test_str = "DTSTART;TZID=Europe/Vienna;VALUE=DATE-TIME:20120213T100000" self.assertTrue(test_str in test_out) self.assertTrue("ATTENDEE:sepp" in test_out) # ical standard expects DTSTAMP and CREATED in UTC self.assertTrue("DTSTAMP;VALUE=DATE-TIME:20101010T081010Z" in test_out) self.assertTrue("CREATED;VALUE=DATE-TIME:20101010T081010Z" in test_out) def test_tzinfo_dateutil(self): # Test for issues #77, #63 # references: #73,7430b66862346fe3a6a100ab25e35a8711446717 date = dateutil.parser.parse('2012-08-30T22:41:00Z') date2 = dateutil.parser.parse('2012-08-30T22:41:00 +02:00') self.assertTrue(date.tzinfo.__module__.startswith('dateutil.tz')) self.assertTrue(date2.tzinfo.__module__.startswith('dateutil.tz')) # make sure, it's parsed properly and doesn't throw an error self.assertTrue(icalendar.vDDDTypes(date).to_ical() == b'20120830T224100Z') self.assertTrue(icalendar.vDDDTypes(date2).to_ical() == b'20120830T224100') class TestTimezoneCreation(unittest.TestCase): @unittest.expectedFailure def test_create_america_new_york(self): """testing America/New_York, the most complex example from the RFC""" # FIXME # This currently fails because of mixed naive and timezone # aware datetimes in dtstart and until which breaks # dateutil recurrence. directory = os.path.dirname(__file__) with open(os.path.join(directory, 'america_new_york.ics'), 'rb') as fp: data = fp.read() cal = icalendar.Calendar.from_ical(data) tz = cal.walk('VEVENT')[0]['DTSTART'][0].dt.tzinfo self.assertEqual(str(tz), 'custom_America/New_York') pytz_new_york = pytz.timezone('America/New_York') # for reasons (tm) the locally installed version of the time zone # database isn't always complete, therefore we only compare some # transition times ny_transition_times = [] ny_transition_info = [] for num, date in enumerate(pytz_new_york._utc_transition_times): if datetime.datetime(1967, 4, 30, 7, 0)\ <= date <= datetime.datetime(2037, 11, 1, 6, 0): ny_transition_times.append(date) ny_transition_info.append(pytz_new_york._transition_info[num]) self.assertEqual(tz._utc_transition_times[:142], ny_transition_times) self.assertEqual(tz._transition_info[0:142], ny_transition_info) self.assertIn( ( datetime.timedelta(-1, 72000), datetime.timedelta(0, 3600), 'EDT' ), tz._tzinfos.keys() ) self.assertIn( (datetime.timedelta(-1, 68400), datetime.timedelta(0), 'EST'), tz._tzinfos.keys() ) def test_create_pacific_fiji(self): """testing Pacific/Fiji, another pretty complex example with more than one RDATE property per subcomponent""" self.maxDiff = None directory = os.path.dirname(__file__) with open(os.path.join(directory, 'pacific_fiji.ics'), 'rb') as fp: data = fp.read() cal = icalendar.Calendar.from_ical(data) tz = cal.walk('VEVENT')[0]['DTSTART'][0].dt.tzinfo self.assertEqual(str(tz), 'custom_Pacific/Fiji') self.assertEqual(tz._utc_transition_times, [datetime.datetime(1915, 10, 25, 12, 4), datetime.datetime(1998, 10, 31, 14, 0), datetime.datetime(1999, 2, 27, 14, 0), datetime.datetime(1999, 11, 6, 14, 0), datetime.datetime(2000, 2, 26, 14, 0), datetime.datetime(2009, 11, 28, 14, 0), datetime.datetime(2010, 3, 27, 14, 0), datetime.datetime(2010, 10, 23, 14, 0), datetime.datetime(2011, 3, 5, 14, 0), datetime.datetime(2011, 10, 22, 14, 0), datetime.datetime(2012, 1, 21, 14, 0), datetime.datetime(2012, 10, 20, 14, 0), datetime.datetime(2013, 1, 19, 14, 0), datetime.datetime(2013, 10, 26, 14, 0), datetime.datetime(2014, 1, 18, 13, 0), datetime.datetime(2014, 10, 25, 14, 0), datetime.datetime(2015, 1, 17, 13, 0), datetime.datetime(2015, 10, 24, 14, 0), datetime.datetime(2016, 1, 23, 13, 0), datetime.datetime(2016, 10, 22, 14, 0), datetime.datetime(2017, 1, 21, 13, 0), datetime.datetime(2017, 10, 21, 14, 0), datetime.datetime(2018, 1, 20, 13, 0), datetime.datetime(2018, 10, 20, 14, 0), datetime.datetime(2019, 1, 19, 13, 0), datetime.datetime(2019, 10, 26, 14, 0), datetime.datetime(2020, 1, 18, 13, 0), datetime.datetime(2020, 10, 24, 14, 0), datetime.datetime(2021, 1, 23, 13, 0), datetime.datetime(2021, 10, 23, 14, 0), datetime.datetime(2022, 1, 22, 13, 0), datetime.datetime(2022, 10, 22, 14, 0), datetime.datetime(2023, 1, 21, 13, 0), datetime.datetime(2023, 10, 21, 14, 0), datetime.datetime(2024, 1, 20, 13, 0), datetime.datetime(2024, 10, 26, 14, 0), datetime.datetime(2025, 1, 18, 13, 0), datetime.datetime(2025, 10, 25, 14, 0), datetime.datetime(2026, 1, 17, 13, 0), datetime.datetime(2026, 10, 24, 14, 0), datetime.datetime(2027, 1, 23, 13, 0), datetime.datetime(2027, 10, 23, 14, 0), datetime.datetime(2028, 1, 22, 13, 0), datetime.datetime(2028, 10, 21, 14, 0), datetime.datetime(2029, 1, 20, 13, 0), datetime.datetime(2029, 10, 20, 14, 0), datetime.datetime(2030, 1, 19, 13, 0), datetime.datetime(2030, 10, 26, 14, 0), datetime.datetime(2031, 1, 18, 13, 0), datetime.datetime(2031, 10, 25, 14, 0), datetime.datetime(2032, 1, 17, 13, 0), datetime.datetime(2032, 10, 23, 14, 0), datetime.datetime(2033, 1, 22, 13, 0), datetime.datetime(2033, 10, 22, 14, 0), datetime.datetime(2034, 1, 21, 13, 0), datetime.datetime(2034, 10, 21, 14, 0), datetime.datetime(2035, 1, 20, 13, 0), datetime.datetime(2035, 10, 20, 14, 0), datetime.datetime(2036, 1, 19, 13, 0), datetime.datetime(2036, 10, 25, 14, 0), datetime.datetime(2037, 1, 17, 13, 0), datetime.datetime(2037, 10, 24, 14, 0), datetime.datetime(2038, 1, 23, 13, 0), datetime.datetime(2038, 10, 23, 14, 0)] ) self.assertEqual( tz._transition_info, [( datetime.timedelta(0, 43200), datetime.timedelta(0), 'custom_Pacific/Fiji_19151026T000000_+115544_+1200' )] + 3 * [( datetime.timedelta(0, 46800), datetime.timedelta(0, 3600), 'custom_Pacific/Fiji_19981101T020000_+1200_+1300' ), ( datetime.timedelta(0, 43200), datetime.timedelta(0), 'custom_Pacific/Fiji_19990228T030000_+1300_+1200') ] + 3 * [( datetime.timedelta(0, 46800), datetime.timedelta(0, 3600), 'custom_Pacific/Fiji_20101024T020000_+1200_+1300' ), ( datetime.timedelta(0, 43200), datetime.timedelta(0), 'custom_Pacific/Fiji_19990228T030000_+1300_+1200' )] + 25 * [( datetime.timedelta(0, 46800), datetime.timedelta(0, 3600), 'custom_Pacific/Fiji_20101024T020000_+1200_+1300' ), ( datetime.timedelta(0, 43200), datetime.timedelta(0), 'custom_Pacific/Fiji_20140119T020000_+1300_+1200' )] + [( datetime.timedelta(0, 46800), datetime.timedelta(0, 3600), 'custom_Pacific/Fiji_20101024T020000_+1200_+1300' )] ) self.assertIn( ( datetime.timedelta(0, 46800), datetime.timedelta(0, 3600), 'custom_Pacific/Fiji_19981101T020000_+1200_+1300' ), tz._tzinfos.keys() ) self.assertIn( ( datetime.timedelta(0, 43200), datetime.timedelta(0), 'custom_Pacific/Fiji_19990228T030000_+1300_+1200' ), tz._tzinfos.keys() ) def test_same_start_date(self): """testing if we can handle VTIMEZONEs whose different components have the same start DTIMEs.""" directory = os.path.dirname(__file__) with open(os.path.join(directory, 'timezone_same_start.ics'), 'rb') as fp: data = fp.read() cal = icalendar.Calendar.from_ical(data) d = cal.subcomponents[1]['DTSTART'].dt self.assertEqual(d.strftime('%c'), 'Fri Feb 24 12:00:00 2017') def test_same_start_date_and_offset(self): """testing if we can handle VTIMEZONEs whose different components have the same DTSTARTs, TZOFFSETFROM, and TZOFFSETTO.""" directory = os.path.dirname(__file__) with open(os.path.join(directory, 'timezone_same_start_and_offset.ics'), 'rb') as fp: data = fp.read() cal = icalendar.Calendar.from_ical(data) d = cal.subcomponents[1]['DTSTART'].dt self.assertEqual(d.strftime('%c'), 'Fri Feb 24 12:00:00 2017') def test_rdate(self): """testing if we can handle VTIMEZONEs who only have an RDATE, not RRULE """ directory = os.path.dirname(__file__) with open(os.path.join(directory, 'timezone_rdate.ics'), 'rb') as fp: data = fp.read() cal = icalendar.Calendar.from_ical(data) vevent = cal.walk('VEVENT')[0] tz = vevent['DTSTART'].dt.tzinfo self.assertEqual(str(tz), 'posix/Europe/Vaduz') self.assertEqual( tz._utc_transition_times[:6], [ datetime.datetime(1901, 12, 13, 20, 45, 38), datetime.datetime(1941, 5, 5, 0, 0, 0), datetime.datetime(1941, 10, 6, 0, 0, 0), datetime.datetime(1942, 5, 4, 0, 0, 0), datetime.datetime(1942, 10, 5, 0, 0, 0), datetime.datetime(1981, 3, 29, 1, 0), ]) self.assertEqual( tz._transition_info[:6], [ (datetime.timedelta(0, 3600), datetime.timedelta(0), 'CET'), (datetime.timedelta(0, 7200), datetime.timedelta(0, 3600), 'CEST'), (datetime.timedelta(0, 3600), datetime.timedelta(0), 'CET'), (datetime.timedelta(0, 7200), datetime.timedelta(0, 3600), 'CEST'), (datetime.timedelta(0, 3600), datetime.timedelta(0), 'CET'), (datetime.timedelta(0, 7200), datetime.timedelta(0, 3600), 'CEST'), ] ) icalendar-4.0.3/src/icalendar/tests/test_time.py0000644000076600000240000000160413357326437022510 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- import unittest import datetime import icalendar import os class TestTime(unittest.TestCase): def setUp(self): icalendar.cal.types_factory.types_map['X-SOMETIME'] = 'time' def tearDown(self): icalendar.cal.types_factory.types_map.pop('X-SOMETIME') def test_create_from_ical(self): directory = os.path.dirname(__file__) ics = open(os.path.join(directory, 'time.ics'), 'rb') cal = icalendar.Calendar.from_ical(ics.read()) ics.close() self.assertEqual(cal['X-SOMETIME'].dt, datetime.time(17, 20, 10)) self.assertEqual(cal['X-SOMETIME'].to_ical(), '172010') def test_create_to_ical(self): cal = icalendar.Calendar() cal.add('X-SOMETIME', datetime.time(17, 20, 10)) self.assertTrue(b'X-SOMETIME;VALUE=TIME:172010' in cal.to_ical().splitlines()) icalendar-4.0.3/src/icalendar/tests/pacific_fiji.ics0000644000076600000240000000230613357326437023240 0ustar mauritsstaff00000000000000BEGIN:VCALENDAR PRODID:-//tzurl.org//NONSGML Olson 2014g//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:custom_Pacific/Fiji TZURL:http://tzurl.org/zoneinfo/Pacific/Fiji X-LIC-LOCATION:Pacific/Fiji BEGIN:DAYLIGHT TZOFFSETFROM:+1200 TZOFFSETTO:+1300 DTSTART:20101024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYMONTHDAY=21,22,23,24,25,26,27;BYDAY=SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+1300 TZOFFSETTO:+1200 DTSTART:20140119T020000 RRULE:FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=18,19,20,21,22,23,24;BYDAY=SU END:STANDARD BEGIN:STANDARD TZOFFSETFROM:+115544 TZOFFSETTO:+1200 DTSTART:19151026T000000 RDATE:19151026T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETFROM:+1200 TZOFFSETTO:+1300 DTSTART:19981101T020000 RDATE:19981101T020000 RDATE:19991107T020000 RDATE:20091129T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+1300 TZOFFSETTO:+1200 DTSTART:19990228T030000 RDATE:19990228T030000 RDATE:20000227T030000 RDATE:20100328T030000 RDATE:20110306T030000 RDATE:20120122T030000 RDATE:20130120T030000 END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:noend123 DTSTART;TZID=custom_Pacific/Fiji;VALUE=DATE-TIME:20140829T080000 DTSTART;TZID=custom_Pacific/Fiji;VALUE=DATE-TIME:20140829T100000 SUMMARY:an event with a custom tz name END:VEVENT END:VCALENDAR icalendar-4.0.3/src/icalendar/tests/america_new_york.ics0000644000076600000240000000253613357326437024164 0ustar mauritsstaff00000000000000BEGIN:VCALENDAR BEGIN:VTIMEZONE TZID:custom_America/New_York LAST-MODIFIED:20050809T050000Z BEGIN:DAYLIGHT DTSTART:19670430T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19730429T070000Z TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:STANDARD DTSTART:19671029T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=20061029T060000Z TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:EST END:STANDARD BEGIN:DAYLIGHT DTSTART:19740106T020000 RDATE:19750223T020000 TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19760425T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19860427T070000Z TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19870405T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU;UNTIL=20060402T070000Z TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:20070311T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:STANDARD DTSTART:20071104T020000 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:EST END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:noend123 DTSTART;TZID=custom_America/New_York;VALUE=DATE-TIME:20140829T080000 DTSTART;TZID=custom_America/New_York;VALUE=DATE-TIME:20140829T100000 SUMMARY:an event with a custom tz name END:VEVENT END:VCALENDAR icalendar-4.0.3/src/icalendar/tests/test_encoding.py0000644000076600000240000000623313357326437023343 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import unittest import datetime import icalendar import os import pytz class TestEncoding(unittest.TestCase): def test_create_from_ical(self): directory = os.path.dirname(__file__) with open(os.path.join(directory, 'encoding.ics'), 'rb') as fp: data = fp.read() cal = icalendar.Calendar.from_ical(data) self.assertEqual(cal['prodid'].to_ical().decode('utf-8'), "-//Plönë.org//NONSGML plone.app.event//EN") self.assertEqual(cal['X-WR-CALDESC'].to_ical().decode('utf-8'), "test non ascii: äöü ÄÖÜ €") event = cal.walk('VEVENT')[0] self.assertEqual(event['SUMMARY'].to_ical().decode('utf-8'), 'Non-ASCII Test: ÄÖÜ äöü €') self.assertEqual( event['DESCRIPTION'].to_ical().decode('utf-8'), 'icalendar should be able to handle non-ascii: €äüöÄÜÖ.' ) self.assertEqual(event['LOCATION'].to_ical().decode('utf-8'), 'Tribstrül') def test_create_to_ical(self): cal = icalendar.Calendar() cal.add('prodid', "-//Plönë.org//NONSGML plone.app.event//EN") cal.add('version', "2.0") cal.add('x-wr-calname', "äöü ÄÖÜ €") cal.add('x-wr-caldesc', "test non ascii: äöü ÄÖÜ €") cal.add('x-wr-relcalid', "12345") event = icalendar.Event() event.add( 'dtstart', pytz.utc.localize(datetime.datetime(2010, 10, 10, 10, 0, 0)) ) event.add( 'dtend', pytz.utc.localize(datetime.datetime(2010, 10, 10, 12, 0, 0)) ) event.add( 'created', pytz.utc.localize(datetime.datetime(2010, 10, 10, 0, 0, 0)) ) event.add('uid', '123456') event.add('summary', 'Non-ASCII Test: ÄÖÜ äöü €') event.add( 'description', 'icalendar should be able to de/serialize non-ascii.' ) event.add('location', 'Tribstrül') cal.add_component(event) ical_lines = cal.to_ical().splitlines() cmp = b'PRODID:-//Pl\xc3\xb6n\xc3\xab.org//NONSGML plone.app.event//EN' self.assertTrue(cmp in ical_lines) def test_create_event_simple(self): event = icalendar.Event() event.add( "dtstart", pytz.utc.localize(datetime.datetime(2010, 10, 10, 0, 0, 0)) ) event.add("summary", "åäö") out = event.to_ical() summary = b'SUMMARY:\xc3\xa5\xc3\xa4\xc3\xb6' self.assertTrue(summary in out.splitlines()) def test_unicode_parameter_name(self): # Test for issue #80 cal = icalendar.Calendar() event = icalendar.Event() event.add('DESCRIPTION', 'äöüßÄÖÜ') cal.add_component(event) c = cal.to_ical() self.assertEqual( c, b'BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDESCRIPTION:' + b'\xc3\xa4\xc3\xb6\xc3\xbc\xc3\x9f\xc3\x84\xc3\x96\xc3\x9c\r\n' + b'END:VEVENT\r\nEND:VCALENDAR\r\n' ) icalendar-4.0.3/src/icalendar/tests/__init__.py0000644000076600000240000000000013357326437022237 0ustar mauritsstaff00000000000000icalendar-4.0.3/src/icalendar/tests/test_icalendar.py0000644000076600000240000002756413357326437023511 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import icalendar import os import textwrap import unittest class IcalendarTestCase (unittest.TestCase): def test_long_lines(self): from ..parser import Contentlines, Contentline c = Contentlines([Contentline('BEGIN:VEVENT')]) c.append(Contentline(''.join('123456789 ' * 10))) self.assertEqual( c.to_ical(), b'BEGIN:VEVENT\r\n123456789 123456789 123456789 123456789 ' b'123456789 123456789 123456789 1234\r\n 56789 123456789 ' b'123456789 \r\n' ) # from doctests # Notice that there is an extra empty string in the end of the content # lines. That is so they can be easily joined with: # '\r\n'.join(contentlines)) self.assertEqual(Contentlines.from_ical('A short line\r\n'), ['A short line', '']) self.assertEqual(Contentlines.from_ical('A faked\r\n long line\r\n'), ['A faked long line', '']) self.assertEqual( Contentlines.from_ical('A faked\r\n long line\r\nAnd another ' 'lin\r\n\te that is folded\r\n'), ['A faked long line', 'And another line that is folded', ''] ) def test_contentline_class(self): from ..parser import Contentline, Parameters from ..prop import vText self.assertEqual( Contentline('Si meliora dies, ut vina, poemata reddit').to_ical(), b'Si meliora dies, ut vina, poemata reddit' ) # A long line gets folded c = Contentline(''.join(['123456789 '] * 10)).to_ical() self.assertEqual( c, (b'123456789 123456789 123456789 123456789 123456789 123456789 ' b'123456789 1234\r\n 56789 123456789 123456789 ') ) # A folded line gets unfolded self.assertEqual( Contentline.from_ical(c), ('123456789 123456789 123456789 123456789 123456789 123456789 ' '123456789 123456789 123456789 123456789 ') ) # http://tools.ietf.org/html/rfc5545#section-3.3.11 # An intentional formatted text line break MUST only be included in # a "TEXT" property value by representing the line break with the # character sequence of BACKSLASH, followed by a LATIN SMALL LETTER # N or a LATIN CAPITAL LETTER N, that is "\n" or "\N". # Newlines are not allwoed in content lines self.assertRaises(AssertionError, Contentline, b'1234\r\n\r\n1234') self.assertEqual( Contentline('1234\\n\\n1234').to_ical(), b'1234\\n\\n1234' ) # We do not fold within a UTF-8 character c = Contentline(b'This line has a UTF-8 character where it should be ' b'folded. Make sure it g\xc3\xabts folded before that ' b'character.') self.assertIn(b'\xc3\xab', c.to_ical()) # Another test of the above c = Contentline(b'x' * 73 + b'\xc3\xab' + b'\\n ' + b'y' * 10) self.assertEqual(c.to_ical().count(b'\xc3'), 1) # Don't fail if we fold a line that is exactly X times 74 characters # long c = Contentline(''.join(['x'] * 148)).to_ical() # It can parse itself into parts, # which is a tuple of (name, params, vals) self.assertEqual( Contentline('dtstart:20050101T120000').parts(), ('dtstart', Parameters({}), '20050101T120000') ) self.assertEqual( Contentline('dtstart;value=datetime:20050101T120000').parts(), ('dtstart', Parameters({'VALUE': 'datetime'}), '20050101T120000') ) c = Contentline('ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:' 'MAILTO:maxm@example.com') self.assertEqual( c.parts(), ('ATTENDEE', Parameters({'ROLE': 'REQ-PARTICIPANT', 'CN': 'Max Rasmussen'}), 'MAILTO:maxm@example.com') ) self.assertEqual( c.to_ical().decode('utf-8'), 'ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:' 'MAILTO:maxm@example.com' ) # and back again # NOTE: we are quoting property values with spaces in it. parts = ('ATTENDEE', Parameters({'ROLE': 'REQ-PARTICIPANT', 'CN': 'Max Rasmussen'}), 'MAILTO:maxm@example.com') self.assertEqual( Contentline.from_parts(*parts), 'ATTENDEE;CN="Max Rasmussen";ROLE=REQ-PARTICIPANT:' 'MAILTO:maxm@example.com' ) # and again parts = ('ATTENDEE', Parameters(), 'MAILTO:maxm@example.com') self.assertEqual( Contentline.from_parts(*parts), 'ATTENDEE:MAILTO:maxm@example.com' ) # A value can also be any of the types defined in PropertyValues parts = ('ATTENDEE', Parameters(), vText('MAILTO:test@example.com')) self.assertEqual( Contentline.from_parts(*parts), 'ATTENDEE:MAILTO:test@example.com' ) # A value in UTF-8 parts = ('SUMMARY', Parameters(), vText('INternational char æ ø å')) self.assertEqual( Contentline.from_parts(*parts), 'SUMMARY:INternational char æ ø å' ) # A value can also be unicode parts = ('SUMMARY', Parameters(), vText('INternational char æ ø å')) self.assertEqual( Contentline.from_parts(*parts), 'SUMMARY:INternational char æ ø å' ) # Traversing could look like this. name, params, vals = c.parts() self.assertEqual(name, 'ATTENDEE') self.assertEqual(vals, 'MAILTO:maxm@example.com') self.assertEqual( sorted(params.items()), sorted([('ROLE', 'REQ-PARTICIPANT'), ('CN', 'Max Rasmussen')]) ) # And the traditional failure with self.assertRaisesRegexp( ValueError, 'Content line could not be parsed into parts' ): Contentline('ATTENDEE;maxm@example.com').parts() # Another failure: with self.assertRaisesRegexp( ValueError, 'Content line could not be parsed into parts' ): Contentline(':maxm@example.com').parts() self.assertEqual( Contentline('key;param=:value').parts(), ('key', Parameters({'PARAM': ''}), 'value') ) self.assertEqual( Contentline('key;param="pvalue":value').parts(), ('key', Parameters({'PARAM': 'pvalue'}), 'value') ) # Should bomb on missing param: with self.assertRaisesRegexp( ValueError, 'Content line could not be parsed into parts' ): Contentline.from_ical("k;:no param").parts() self.assertEqual( Contentline('key;param=pvalue:value', strict=False).parts(), ('key', Parameters({'PARAM': 'pvalue'}), 'value') ) # If strict is set to True, uppercase param values that are not # double-quoted, this is because the spec says non-quoted params are # case-insensitive. self.assertEqual( Contentline('key;param=pvalue:value', strict=True).parts(), ('key', Parameters({'PARAM': 'PVALUE'}), 'value') ) self.assertEqual( Contentline('key;param="pValue":value', strict=True).parts(), ('key', Parameters({'PARAM': 'pValue'}), 'value') ) contains_base64 = ( 'X-APPLE-STRUCTURED-LOCATION;' 'VALUE=URI;X-ADDRESS="Kaiserliche Hofburg, 1010 Wien";' 'X-APPLE-MAPKIT-HANDLE=CAESxQEZgr3QZXJyZWljaA==;' 'X-APPLE-RADIUS=328.7978217977285;X-APPLE-REFERENCEFRAME=1;' 'X-TITLE=Heldenplatz:geo:48.206686,16.363235' ).encode('utf-8') self.assertEqual( Contentline(contains_base64, strict=True).parts(), ('X-APPLE-STRUCTURED-LOCATION', Parameters({ 'X-APPLE-RADIUS': '328.7978217977285', 'X-ADDRESS': 'Kaiserliche Hofburg, 1010 Wien', 'X-APPLE-REFERENCEFRAME': '1', 'X-TITLE': 'HELDENPLATZ', 'X-APPLE-MAPKIT-HANDLE': 'CAESXQEZGR3QZXJYZWLJAA==', 'VALUE': 'URI', }), 'geo:48.206686,16.363235' ) ) def test_fold_line(self): from ..parser import foldline self.assertEqual(foldline('foo'), 'foo') self.assertEqual( foldline("Lorem ipsum dolor sit amet, consectetur adipiscing " "elit. Vestibulum convallis imperdiet dui posuere."), ('Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' 'Vestibulum conval\r\n lis imperdiet dui posuere.') ) # I don't really get this test # at least just but bytes in there # porting it to "run" under python 2 & 3 makes it not much better with self.assertRaises(AssertionError): foldline('привет'.encode('utf-8'), limit=3) self.assertEqual(foldline('foobar', limit=4), 'foo\r\n bar') self.assertEqual( foldline('Lorem ipsum dolor sit amet, consectetur adipiscing elit' '. Vestibulum convallis imperdiet dui posuere.'), ('Lorem ipsum dolor sit amet, consectetur adipiscing elit.' ' Vestibulum conval\r\n lis imperdiet dui posuere.') ) self.assertEqual( foldline('DESCRIPTION:АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭЮЯ'), 'DESCRIPTION:АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭ\r\n ЮЯ' ) def test_value_double_quoting(self): from ..parser import dquote self.assertEqual(dquote('Max'), 'Max') self.assertEqual(dquote('Rasmussen, Max'), '"Rasmussen, Max"') self.assertEqual(dquote('name:value'), '"name:value"') def test_q_split(self): from ..parser import q_split self.assertEqual(q_split('Max,Moller,"Rasmussen, Max"'), ['Max', 'Moller', '"Rasmussen, Max"']) def test_q_split_bin(self): from ..parser import q_split for s in ('X-SOMETHING=ABCDE==', ',,,'): for maxsplit in range(-1, 3): self.assertEqual(q_split(s, '=', maxsplit=maxsplit), s.split('=', maxsplit)) def test_q_join(self): from ..parser import q_join self.assertEqual(q_join(['Max', 'Moller', 'Rasmussen, Max']), 'Max,Moller,"Rasmussen, Max"') class TestEncoding(unittest.TestCase): def test_broken_property(self): """ Test if error messages are encode properly. """ broken_ical = textwrap.dedent(""" BEGIN:VCALENDAR BEGIN:VEVENT SUMMARY:An Event with too many semicolons DTSTART;;VALUE=DATE-TIME:20140409T093000 UID:abc END:VEVENT END:VCALENDAR """) cal = icalendar.Calendar.from_ical(broken_ical) for event in cal.walk('vevent'): self.assertEqual(len(event.errors), 1, 'Not the right amount of errors.') error = event.errors[0][1] self.assertTrue(error.startswith('Content line could not be parsed into parts')) def test_apple_xlocation(self): """ Test if we support base64 encoded binary data in parameter values. """ directory = os.path.dirname(__file__) with open(os.path.join(directory, 'x_location.ics'), 'rb') as fp: data = fp.read() cal = icalendar.Calendar.from_ical(data) for event in cal.walk('vevent'): self.assertEqual(len(event.errors), 0, 'Got too many errors') icalendar-4.0.3/src/icalendar/tests/issue_53_parsing_failure.ics0000644000076600000240000000457313357326437025542 0ustar mauritsstaff00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Meetup//RemoteApi//EN CALSCALE:GREGORIAN METHOD:PUBLISH X-ORIGINAL-URL:http://www.meetup.com/DevOpsDC/events/ical/DevOpsDC/ X-WR-CALNAME:Events - DevOpsDC BEGIN:VTIMEZONE TZID:America/New_York TZURL:http://tzurl.org/zoneinfo-outlook/America/New_York X-LIC-LOCATION:America/New_York BEGIN:DAYLIGHT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT DTSTART:19700308T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:EST DTSTART:19701101T020000 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT DTSTAMP:20120605T003759Z DTSTART;TZID=America/New_York:20120712T183000 DTEND;TZID=America/New_York:20120712T213000 STATUS:CONFIRMED SUMMARY:DevOps DC Meetup DESCRIPTION:DevOpsDC\nThursday\, July 12 at 6:30 PM\n\nThis will be a joi nt meetup / hack night with the DC jQuery Users Group. The idea behind the hack night: Small teams consisting of at least 1 member...\n\nDeta ils: http://www.meetup.com/DevOpsDC/events/47635522/ CLASS:PUBLIC CREATED:20120111T120339Z GEO:38.90;-77.01 LOCATION:Fathom Creative\, Inc. (1333 14th Street Northwest\, Washington D.C.\, DC 20005) URL:http://www.meetup.com/DevOpsDC/events/47635522/ LAST-MODIFIED:20120522T174406Z UID:event_qtkfrcyqkbnb@meetup.com END:VEVENT BEGIN:VEVENT DTSTAMP:20120605T003759Z DTSTART;TZID=America/New_York:20120911T183000 DTEND;TZID=America/New_York:20120911T213000 STATUS:CONFIRMED SUMMARY:DevOps DC Meetup DESCRIPTION:DevOpsDC\nTuesday\, September 11 at 6:30 PM\n\n \n\nDetails: http://www.meetup.com/DevOpsDC/events/47635532/ CLASS:PUBLIC CREATED:20120111T120352Z GEO:38.90;-77.01 LOCATION:CustomInk\, LLC (7902 Westpark Drive\, McLean\, VA 22102) URL:http://www.meetup.com/DevOpsDC/events/47635532/ LAST-MODIFIED:20120316T202210Z UID:event_qtkfrcyqmbpb@meetup.com END:VEVENT BEGIN:VEVENT DTSTAMP:20120605T003759Z DTSTART;TZID=America/New_York:20121113T183000 DTEND;TZID=America/New_York:20121113T213000 STATUS:CONFIRMED SUMMARY:DevOps DC Meetup DESCRIPTION:DevOpsDC\nTuesday\, November 13 at 6:30 PM\n\n \n\nDetails: h ttp://www.meetup.com/DevOpsDC/events/47635552/ CLASS:PUBLIC CREATED:20120111T120402Z GEO:38.90;-77.01 LOCATION:CustomInk\, LLC (7902 Westpark Drive\, McLean\, VA 22102) URL:http://www.meetup.com/DevOpsDC/events/47635552/ LAST-MODIFIED:20120316T202210Z UID:event_qtkfrcyqpbrb@meetup.com END:VEVENT END:VCALENDAR icalendar-4.0.3/src/icalendar/tests/recurrence.ics0000644000076600000240000000134113357326437022774 0ustar mauritsstaff00000000000000BEGIN:VCALENDAR METHOD:Request PRODID:-//My product//mxm.dk/ VERSION:2.0 BEGIN:VEVENT DTSTART:19960401T010000 DTEND:19960401T020000 RRULE:FREQ=DAILY;COUNT=100 EXDATE:19960402T010000Z,19960403T010000Z,19960404T010000Z SUMMARY:A recurring event with exdates END:VEVENT BEGIN:VEVENT DTSTART;TZID=Europe/Vienna:20120327T100000 DTEND;TZID=Europe/Vienna:20120327T180000 RRULE:FREQ=WEEKLY;UNTIL=20120703T080000Z;BYDAY=TU EXDATE;TZID=Europe/Vienna:20120529T100000 EXDATE;TZID=Europe/Vienna:20120403T100000 EXDATE;TZID=Europe/Vienna:20120410T100000 EXDATE;TZID=Europe/Vienna:20120501T100000 EXDATE;TZID=Europe/Vienna:20120417T100000 DTSTAMP:20130716T120638Z SUMMARY:A Recurring event with multiple exdates, one per line. END:VEVENT END:VCALENDAR icalendar-4.0.3/src/icalendar/tests/test_recurrence.py0000644000076600000240000000402213357326437023704 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- from icalendar.caselessdict import CaselessDict import unittest import datetime import icalendar import os import pytz class TestRecurrence(unittest.TestCase): def setUp(self): directory = os.path.dirname(__file__) with open(os.path.join(directory, 'recurrence.ics'), 'rb') as fp: data = fp.read() self.cal = icalendar.Calendar.from_ical(data) def test_recurrence_exdates_one_line(self): first_event = self.cal.walk('vevent')[0] self.assertIsInstance(first_event, CaselessDict) self.assertEqual( first_event['rrule'], {'COUNT': [100], 'FREQ': ['DAILY']} ) self.assertEqual( first_event['exdate'].to_ical(), b'19960402T010000Z,19960403T010000Z,19960404T010000Z' ) self.assertEqual( first_event['exdate'].dts[0].dt, pytz.utc.localize(datetime.datetime(1996, 4, 2, 1, 0)) ) self.assertEqual( first_event['exdate'].dts[1].dt, pytz.utc.localize(datetime.datetime(1996, 4, 3, 1, 0)) ) self.assertEqual( first_event['exdate'].dts[2].dt, pytz.utc.localize(datetime.datetime(1996, 4, 4, 1, 0)) ) def test_recurrence_exdates_multiple_lines(self): event = self.cal.walk('vevent')[1] exdate = event['exdate'] # TODO: DOCUMENT BETTER! # In this case we have multiple EXDATE definitions, one per line. # Icalendar makes a list out of this instead of zipping it into one # vDDDLists object. Actually, this feels correct for me, as it also # allows to define different timezones per exdate line - but client # code has to handle this as list and not blindly expecting to be able # to call event['EXDATE'].to_ical() on it: self.assertEqual(isinstance(exdate, list), True) # multiple EXDATE self.assertEqual(exdate[0].to_ical(), b'20120529T100000') # TODO: test for embedded timezone information! icalendar-4.0.3/src/icalendar/tests/test_unit_caselessdict.py0000644000076600000240000000615513357326437025265 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- import unittest import icalendar class TestCaselessdict(unittest.TestCase): def test_caselessdict_canonsort_keys(self): canonsort_keys = icalendar.caselessdict.canonsort_keys keys = ['DTEND', 'DTSTAMP', 'DTSTART', 'UID', 'SUMMARY', 'LOCATION'] out = canonsort_keys(keys) self.assertEqual( out, ['DTEND', 'DTSTAMP', 'DTSTART', 'LOCATION', 'SUMMARY', 'UID'] ) out = canonsort_keys(keys, ('SUMMARY', 'DTSTART', 'DTEND', )) self.assertEqual( out, ['SUMMARY', 'DTSTART', 'DTEND', 'DTSTAMP', 'LOCATION', 'UID'] ) out = canonsort_keys(keys, ('UID', 'DTSTART', 'DTEND', )) self.assertEqual( out, ['UID', 'DTSTART', 'DTEND', 'DTSTAMP', 'LOCATION', 'SUMMARY'] ) out = canonsort_keys( keys, ('UID', 'DTSTART', 'DTEND', 'RRULE', 'EXDATE') ) self.assertEqual( out, ['UID', 'DTSTART', 'DTEND', 'DTSTAMP', 'LOCATION', 'SUMMARY'] ) def test_caselessdict_canonsort_items(self): canonsort_items = icalendar.caselessdict.canonsort_items d = { 'i': 7, 'c': 'at', 'a': 3.5, 'l': (2, 3), 'e': [4, 5], 'n': 13, 'd': {'x': 'y'}, 'r': 1.0, } out = canonsort_items(d) self.assertEqual( out, [('a', 3.5), ('c', 'at'), ('d', {'x': 'y'}), ('e', [4, 5]), ('i', 7), ('l', (2, 3)), ('n', 13), ('r', 1.0)] ) out = canonsort_items(d, ('i', 'c', 'a')) self.assertTrue( out, [('i', 7), ('c', 'at'), ('a', 3.5), ('d', {'x': 'y'}), ('e', [4, 5]), ('l', (2, 3)), ('n', 13), ('r', 1.0)] ) def test_caselessdict_copy(self): CaselessDict = icalendar.caselessdict.CaselessDict original_dict = CaselessDict(key1='val1', key2='val2') copied_dict = original_dict.copy() self.assertEqual(original_dict, copied_dict) def test_CaselessDict(self): CaselessDict = icalendar.caselessdict.CaselessDict ncd = CaselessDict(key1='val1', key2='val2') self.assertEqual( ncd, CaselessDict({'KEY2': 'val2', 'KEY1': 'val1'}) ) self.assertEqual(ncd['key1'], 'val1') self.assertEqual(ncd['KEY1'], 'val1') ncd['KEY3'] = 'val3' self.assertEqual(ncd['key3'], 'val3') self.assertEqual(ncd.setdefault('key3', 'FOUND'), 'val3') self.assertEqual(ncd.setdefault('key4', 'NOT FOUND'), 'NOT FOUND') self.assertEqual(ncd['key4'], 'NOT FOUND') self.assertEqual(ncd.get('key1'), 'val1') self.assertEqual(ncd.get('key3', 'NOT FOUND'), 'val3') self.assertEqual(ncd.get('key4', 'NOT FOUND'), 'NOT FOUND') self.assertTrue('key4' in ncd) del ncd['key4'] self.assertFalse('key4' in ncd) ncd.update({'key5': 'val5', 'KEY6': 'val6', 'KEY5': 'val7'}) self.assertEqual(ncd['key6'], 'val6') keys = sorted(ncd.keys()) self.assertEqual(keys, ['KEY1', 'KEY2', 'KEY3', 'KEY5', 'KEY6']) icalendar-4.0.3/src/icalendar/tests/timezone_same_start.ics0000644000076600000240000000125513357326437024717 0ustar mauritsstaff00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:Microsoft Exchange Server 2010 METHOD:REQUEST BEGIN:VTIMEZONE TZID:Pacific Standard Time BEGIN:STANDARD DTSTART:16010101T020000 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11 TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:16010101T020000 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3 TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT SUMMARY;LANGUAGE=en-US:Test 4 DTSTART;TZID="Pacific Standard Time":20170224T120000 DTEND;TZID="Pacific Standard Time":20170224T123000 DTSTAMP:20170224T180431Z UID:040000008200E00074C5B7101A82E0080000000090E19664858ED20100000000000000 END:VEVENT END:VCALENDAR icalendar-4.0.3/src/icalendar/tests/test_unit_cal.py0000644000076600000240000004131513357326437023353 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from datetime import datetime from datetime import timedelta import unittest import icalendar import pytz import re class TestCalComponent(unittest.TestCase): def test_cal_Component(self): from icalendar.cal import Component, Calendar, Event from icalendar import prop # A component is like a dictionary with extra methods and attributes. c = Component() c.name = 'VCALENDAR' self.assertTrue(c) self.assertTrue(c.is_empty()) # Every key defines a property.A property can consist of either a # single item. This can be set with a single value... c['prodid'] = '-//max m//icalendar.mxm.dk/' self.assertFalse(c.is_empty()) self.assertEqual( c, Calendar({'PRODID': '-//max m//icalendar.mxm.dk/'}) ) # or with a list c['ATTENDEE'] = ['Max M', 'Rasmussen'] self.assertEqual( c, Calendar({'ATTENDEE': ['Max M', 'Rasmussen'], 'PRODID': '-//max m//icalendar.mxm.dk/'}) ) # ## ADD MULTIPLE VALUES TO A PROPERTY # if you use the add method you don't have to considder if a value is # a list or not. c = Component() c.name = 'VEVENT' # add multiple values at once c.add('attendee', ['test@test.com', 'test2@test.com']) # or add one per line c.add('attendee', 'maxm@mxm.dk') c.add('attendee', 'test@example.dk') # add again multiple values at once to very concatenaton of lists c.add('attendee', ['test3@test.com', 'test4@test.com']) self.assertEqual( c, Event({'ATTENDEE': [ prop.vCalAddress('test@test.com'), prop.vCalAddress('test2@test.com'), prop.vCalAddress('maxm@mxm.dk'), prop.vCalAddress('test@example.dk'), prop.vCalAddress('test3@test.com'), prop.vCalAddress('test4@test.com') ]}) ) ### # You can get the values back directly ... c.add('prodid', '-//my product//') self.assertEqual(c['prodid'], prop.vText('-//my product//')) # ... or decoded to a python type self.assertEqual(c.decoded('prodid'), b'-//my product//') # With default values for non existing properties self.assertEqual(c.decoded('version', 'No Version'), 'No Version') c.add('rdate', [datetime(2013, 3, 28), datetime(2013, 3, 27)]) self.assertTrue(isinstance(c.decoded('rdate'), prop.vDDDLists)) # The component can render itself in the RFC 2445 format. c = Component() c.name = 'VCALENDAR' c.add('attendee', 'Max M') self.assertEqual( c.to_ical(), b'BEGIN:VCALENDAR\r\nATTENDEE:Max M\r\nEND:VCALENDAR\r\n' ) # Components can be nested, so You can add a subcompont. Eg a calendar # holds events. e = Component(summary='A brief history of time') e.name = 'VEVENT' e.add('dtend', '20000102T000000', encode=0) e.add('dtstart', '20000101T000000', encode=0) self.assertEqual( e.to_ical(), b'BEGIN:VEVENT\r\nDTEND:20000102T000000\r\n' + b'DTSTART:20000101T000000\r\nSUMMARY:A brief history of time\r' + b'\nEND:VEVENT\r\n' ) c.add_component(e) self.assertEqual( c.subcomponents, [Event({'DTEND': '20000102T000000', 'DTSTART': '20000101T000000', 'SUMMARY': 'A brief history of time'})] ) # We can walk over nested componentes with the walk method. self.assertEqual([i.name for i in c.walk()], ['VCALENDAR', 'VEVENT']) # We can also just walk over specific component types, by filtering # them on their name. self.assertEqual([i.name for i in c.walk('VEVENT')], ['VEVENT']) self.assertEqual( [i['dtstart'] for i in c.walk('VEVENT')], ['20000101T000000'] ) # We can enumerate property items recursively with the property_items # method. self.assertEqual( c.property_items(), [('BEGIN', b'VCALENDAR'), ('ATTENDEE', prop.vCalAddress('Max M')), ('BEGIN', b'VEVENT'), ('DTEND', '20000102T000000'), ('DTSTART', '20000101T000000'), ('SUMMARY', 'A brief history of time'), ('END', b'VEVENT'), ('END', b'VCALENDAR')] ) # We can also enumerate property items just under the component. self.assertEqual( c.property_items(recursive=False), [('BEGIN', b'VCALENDAR'), ('ATTENDEE', prop.vCalAddress('Max M')), ('END', b'VCALENDAR')] ) sc = c.subcomponents[0] self.assertEqual( sc.property_items(recursive=False), [('BEGIN', b'VEVENT'), ('DTEND', '20000102T000000'), ('DTSTART', '20000101T000000'), ('SUMMARY', 'A brief history of time'), ('END', b'VEVENT')] ) # Text fields which span multiple mulitple lines require proper # indenting c = Calendar() c['description'] = 'Paragraph one\n\nParagraph two' self.assertEqual( c.to_ical(), b'BEGIN:VCALENDAR\r\nDESCRIPTION:Paragraph one\\n\\nParagraph two' + b'\r\nEND:VCALENDAR\r\n' ) # INLINE properties have their values on one property line. Note the # double quoting of the value with a colon in it. c = Calendar() c['resources'] = 'Chair, Table, "Room: 42"' self.assertEqual( c, Calendar({'RESOURCES': 'Chair, Table, "Room: 42"'}) ) self.assertEqual( c.to_ical(), b'BEGIN:VCALENDAR\r\nRESOURCES:Chair\\, Table\\, "Room: 42"\r\n' + b'END:VCALENDAR\r\n' ) # The inline values must be handled by the get_inline() and # set_inline() methods. self.assertEqual( c.get_inline('resources', decode=0), ['Chair', 'Table', 'Room: 42'] ) # These can also be decoded self.assertEqual( c.get_inline('resources', decode=1), [b'Chair', b'Table', b'Room: 42'] ) # You can set them directly ... c.set_inline('resources', ['A', 'List', 'of', 'some, recources'], encode=1) self.assertEqual(c['resources'], 'A,List,of,"some, recources"') # ... and back again self.assertEqual( c.get_inline('resources', decode=0), ['A', 'List', 'of', 'some, recources'] ) c['freebusy'] = '19970308T160000Z/PT3H,19970308T200000Z/PT1H,'\ + '19970308T230000Z/19970309T000000Z' self.assertEqual( c.get_inline('freebusy', decode=0), ['19970308T160000Z/PT3H', '19970308T200000Z/PT1H', '19970308T230000Z/19970309T000000Z'] ) freebusy = c.get_inline('freebusy', decode=1) self.assertTrue(isinstance(freebusy[0][0], datetime)) self.assertTrue(isinstance(freebusy[0][1], timedelta)) def test_cal_Component_add(self): # Test the for timezone correctness: dtstart should preserve it's # timezone, created, dtstamp and last-modified must be in UTC. Component = icalendar.cal.Component comp = Component() vienna = pytz.timezone("Europe/Vienna") comp.add('dtstart', vienna.localize(datetime(2010, 10, 10, 10, 0, 0))) comp.add('created', datetime(2010, 10, 10, 12, 0, 0)) comp.add('dtstamp', vienna.localize(datetime(2010, 10, 10, 14, 0, 0))) comp.add('last-modified', pytz.utc.localize( datetime(2010, 10, 10, 16, 0, 0))) lines = comp.to_ical().splitlines() self.assertTrue( b"DTSTART;TZID=Europe/Vienna;VALUE=DATE-TIME:20101010T100000" in lines) self.assertTrue(b"CREATED;VALUE=DATE-TIME:20101010T120000Z" in lines) self.assertTrue(b"DTSTAMP;VALUE=DATE-TIME:20101010T120000Z" in lines) self.assertTrue( b"LAST-MODIFIED;VALUE=DATE-TIME:20101010T160000Z" in lines ) def test_cal_Component_add_no_reencode(self): """Already encoded values should not be re-encoded. """ from icalendar import cal, prop comp = cal.Component() comp.add('ATTACH', 'me') comp.add('ATTACH', 'you', encode=False) binary = prop.vBinary('us') comp.add('ATTACH', binary) self.assertEqual(comp['ATTACH'], ['me', 'you', binary]) def test_cal_Component_add_property_parameter(self): # Test the for timezone correctness: dtstart should preserve it's # timezone, crated, dtstamp and last-modified must be in UTC. Component = icalendar.cal.Component comp = Component() comp.add('X-TEST-PROP', 'tryout.', parameters={'prop1': 'val1', 'prop2': 'val2'}) lines = comp.to_ical().splitlines() self.assertTrue(b"X-TEST-PROP;PROP1=val1;PROP2=val2:tryout." in lines) def test_cal_Component_from_ical(self): # Check for proper handling of TZID parameter of datetime properties Component = icalendar.cal.Component for component_name, property_name in ( ('VEVENT', 'DTSTART'), ('VEVENT', 'DTEND'), ('VEVENT', 'RECURRENCE-ID'), ('VTODO', 'DUE') ): component_str = 'BEGIN:' + component_name + '\n' component_str += property_name + ';TZID=America/Denver:' component_str += '20120404T073000\nEND:' + component_name component = Component.from_ical(component_str) self.assertEqual(str(component[property_name].dt.tzinfo.zone), "America/Denver") component_str = 'BEGIN:' + component_name + '\n' component_str += property_name + ':' component_str += '20120404T073000\nEND:' + component_name component = Component.from_ical(component_str) self.assertEqual(component[property_name].dt.tzinfo, None) def test_cal_Component_to_ical_property_order(self): Component = icalendar.cal.Component component_str = [b'BEGIN:VEVENT', b'DTSTART:19970714T170000Z', b'DTEND:19970715T035959Z', b'SUMMARY:Bastille Day Party', b'END:VEVENT'] component = Component.from_ical(b'\r\n'.join(component_str)) sorted_str = component.to_ical().splitlines() assert sorted_str != component_str assert set(sorted_str) == set(component_str) preserved_str = component.to_ical(sorted=False).splitlines() assert preserved_str == component_str def test_cal_Component_to_ical_parameter_order(self): Component = icalendar.cal.Component component_str = [b'BEGIN:VEVENT', b'X-FOOBAR;C=one;A=two;B=three:helloworld.', b'END:VEVENT'] component = Component.from_ical(b'\r\n'.join(component_str)) sorted_str = component.to_ical().splitlines() assert sorted_str[0] == component_str[0] assert sorted_str[1] == b'X-FOOBAR;A=two;B=three;C=one:helloworld.' assert sorted_str[2] == component_str[2] preserved_str = component.to_ical(sorted=False).splitlines() assert preserved_str == component_str def test_repr(self): """Test correct class representation. """ from icalendar.cal import Component, Calendar, Event component = Component() component['key1'] = 'value1' self.assertTrue( re.match(r"Component\({u?'KEY1': u?'value1'}\)", str(component)) ) calendar = Calendar() calendar['key1'] = 'value1' self.assertTrue( re.match(r"VCALENDAR\({u?'KEY1': u?'value1'}\)", str(calendar)) ) event = Event() event['key1'] = 'value1' self.assertTrue( re.match(r"VEVENT\({u?'KEY1': u?'value1'}\)", str(event)) ) # Representation of nested Components nested = Component(key1='VALUE1') nested.add_component(component) calendar.add_component(event) nested.add_component(calendar) self.assertTrue( re.match( r"Component\({u?'KEY1': u?'VALUE1'}, " r"Component\({u?'KEY1': u?'value1'}\), " r"VCALENDAR\({u?'KEY1': u?'value1'}, " r"VEVENT\({u?'KEY1': u?'value1'}\)\)\)", str(nested) ) ) class TestCal(unittest.TestCase): def test_cal_ComponentFactory(self): ComponentFactory = icalendar.cal.ComponentFactory factory = ComponentFactory() component = factory['VEVENT'] event = component(dtstart='19700101') self.assertEqual( event.to_ical(), b'BEGIN:VEVENT\r\nDTSTART:19700101\r\nEND:VEVENT\r\n' ) self.assertEqual( factory.get('VCALENDAR', icalendar.cal.Component), icalendar.cal.Calendar) def test_cal_Calendar(self): # Setting up a minimal calendar component looks like this cal = icalendar.cal.Calendar() # Some properties are required to be compliant cal['prodid'] = '-//My calendar product//mxm.dk//' cal['version'] = '2.0' # We also need at least one subcomponent for a calendar to be compliant event = icalendar.cal.Event() event['summary'] = 'Python meeting about calendaring' event['uid'] = '42' event.add('dtstart', datetime(2005, 4, 4, 8, 0, 0)) cal.add_component(event) self.assertEqual( cal.subcomponents[0].to_ical(), b'BEGIN:VEVENT\r\nSUMMARY:Python meeting about calendaring\r\n' + b'DTSTART;VALUE=DATE-TIME:20050404T080000\r\nUID:42\r\n' + b'END:VEVENT\r\n') # Write to disc import tempfile import os directory = tempfile.mkdtemp() with open(os.path.join(directory, 'test.ics'), 'wb') as fp: fp.write(cal.to_ical()) # Parsing a complete calendar from a string will silently ignore wrong # events but adding the error information to the component's 'errors' # attribute. The error in the following is the third EXDATE: it has an # empty DATE. s = '\r\n'.join(('BEGIN:VCALENDAR', 'PRODID:-//Google Inc//Google Calendar 70.9054//EN', 'VERSION:2.0', 'CALSCALE:GREGORIAN', 'METHOD:PUBLISH', 'BEGIN:VEVENT', 'DESCRIPTION:Perfectly OK event', 'DTSTART;VALUE=DATE:20080303', 'DTEND;VALUE=DATE:20080304', 'RRULE:FREQ=DAILY;UNTIL=20080323T235959Z', 'EXDATE;VALUE=DATE:20080311', 'END:VEVENT', 'BEGIN:VEVENT', 'DESCRIPTION:Wrong event', 'DTSTART;VALUE=DATE:20080303', 'DTEND;VALUE=DATE:20080304', 'RRULE:FREQ=DAILY;UNTIL=20080323T235959Z', 'EXDATE;VALUE=DATE:20080311', 'EXDATE;VALUE=DATE:', 'END:VEVENT', 'END:VCALENDAR')) self.assertEqual( [e['DESCRIPTION'].to_ical() for e in icalendar.cal.Calendar.from_ical(s).walk('VEVENT')], [b'Perfectly OK event', b'Wrong event']) self.assertEqual( [e.errors for e in icalendar.cal.Calendar.from_ical(s).walk('VEVENT')], [[], [('EXDATE', "Expected datetime, date, or time, got: ''")]] ) def test_cal_strict_parsing(self): cal_str = b'\r\n'.join( [ b'BEGIN:VCALENDAR', b'BEGIN:VTIMEZONE', b'TZID:Europe/Prague', b'BEGIN:STANDARD', b'DTSTART:18500101T000000', b'TZNAME:PMT', b'TZOFFSETFROM:+5744', b'TZOFFSETTO:+5744', b'END:STANDARD', b'END:VTIMEZONE', b'END:VCALENDAR', b'', ] ) self.assertRaises(ValueError, icalendar.Calendar.from_ical, cal_str) icalendar.vUTCOffset.ignore_exceptions = True self.assertEqual(icalendar.Calendar.from_ical(cal_str).to_ical(), cal_str) icalendar.vUTCOffset.ignore_exceptions = False icalendar-4.0.3/src/icalendar/tests/test_unit_prop.py0000644000076600000240000004601013357326437023571 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from datetime import date from datetime import datetime from datetime import time from datetime import timedelta from icalendar.parser import Parameters import unittest from icalendar.prop import vDatetime from icalendar.windows_to_olson import WINDOWS_TO_OLSON import pytz class TestProp(unittest.TestCase): def test_prop_vBinary(self): from ..prop import vBinary txt = b'This is gibberish' txt_ical = b'VGhpcyBpcyBnaWJiZXJpc2g=' self.assertEqual(vBinary(txt).to_ical(), txt_ical) self.assertEqual(vBinary.from_ical(txt_ical), txt) # The roundtrip test txt = b'Binary data \x13 \x56' txt_ical = b'QmluYXJ5IGRhdGEgEyBW' self.assertEqual(vBinary(txt).to_ical(), txt_ical) self.assertEqual(vBinary.from_ical(txt_ical), txt) self.assertIsInstance(vBinary('txt').params, Parameters) self.assertEqual( vBinary('txt').params, {'VALUE': 'BINARY', 'ENCODING': 'BASE64'} ) # Long data should not have line breaks, as that would interfere txt = b'a' * 99 txt_ical = b'YWFh' * 33 self.assertEqual(vBinary(txt).to_ical(), txt_ical) self.assertEqual(vBinary.from_ical(txt_ical), txt) def test_prop_vBoolean(self): from ..prop import vBoolean self.assertEqual(vBoolean(True).to_ical(), b'TRUE') self.assertEqual(vBoolean(0).to_ical(), b'FALSE') # The roundtrip test self.assertEqual(vBoolean.from_ical(vBoolean(True).to_ical()), True) self.assertEqual(vBoolean.from_ical('true'), True) def test_prop_vCalAddress(self): from ..prop import vCalAddress txt = b'MAILTO:maxm@mxm.dk' a = vCalAddress(txt) a.params['cn'] = 'Max M' self.assertEqual(a.to_ical(), txt) self.assertIsInstance(a.params, Parameters) self.assertEqual(a.params, {'CN': 'Max M'}) self.assertEqual(vCalAddress.from_ical(txt), 'MAILTO:maxm@mxm.dk') def test_prop_vFloat(self): from ..prop import vFloat self.assertEqual(vFloat(1.0).to_ical(), b'1.0') self.assertEqual(vFloat.from_ical('42'), 42.0) self.assertEqual(vFloat(42).to_ical(), b'42.0') def test_prop_vInt(self): from ..prop import vInt self.assertEqual(vInt(42).to_ical(), b'42') self.assertEqual(vInt.from_ical('13'), 13) self.assertRaises(ValueError, vInt.from_ical, '1s3') def test_prop_vDDDLists(self): from ..prop import vDDDLists dt_list = vDDDLists.from_ical('19960402T010000Z') self.assertTrue(isinstance(dt_list, list)) self.assertEqual(len(dt_list), 1) self.assertTrue(isinstance(dt_list[0], datetime)) self.assertEqual(str(dt_list[0]), '1996-04-02 01:00:00+00:00') p = '19960402T010000Z,19960403T010000Z,19960404T010000Z' dt_list = vDDDLists.from_ical(p) self.assertEqual(len(dt_list), 3) self.assertEqual(str(dt_list[0]), '1996-04-02 01:00:00+00:00') self.assertEqual(str(dt_list[2]), '1996-04-04 01:00:00+00:00') dt_list = vDDDLists([]) self.assertEqual(dt_list.to_ical(), b'') dt_list = vDDDLists([datetime(2000, 1, 1)]) self.assertEqual(dt_list.to_ical(), b'20000101T000000') dt_list = vDDDLists([datetime(2000, 1, 1), datetime(2000, 11, 11)]) self.assertEqual(dt_list.to_ical(), b'20000101T000000,20001111T000000') def test_prop_vDDDTypes(self): from ..prop import vDDDTypes self.assertTrue(isinstance(vDDDTypes.from_ical('20010101T123000'), datetime)) self.assertEqual(vDDDTypes.from_ical('20010101T123000Z'), pytz.utc.localize(datetime(2001, 1, 1, 12, 30))) self.assertTrue(isinstance(vDDDTypes.from_ical('20010101'), date)) self.assertEqual(vDDDTypes.from_ical('P31D'), timedelta(31)) self.assertEqual(vDDDTypes.from_ical('-P31D'), timedelta(-31)) # Bad input self.assertRaises(ValueError, vDDDTypes, 42) def test_prop_vDate(self): from ..prop import vDate self.assertEqual(vDate(date(2001, 1, 1)).to_ical(), b'20010101') self.assertEqual(vDate(date(1899, 1, 1)).to_ical(), b'18990101') self.assertEqual(vDate.from_ical('20010102'), date(2001, 1, 2)) self.assertRaises(ValueError, vDate, 'd') def test_prop_vDatetime(self): from ..prop import vDatetime dt = datetime(2001, 1, 1, 12, 30, 0) self.assertEqual(vDatetime(dt).to_ical(), b'20010101T123000') self.assertEqual(vDatetime.from_ical('20000101T120000'), datetime(2000, 1, 1, 12, 0)) dutc = pytz.utc.localize(datetime(2001, 1, 1, 12, 30, 0)) self.assertEqual(vDatetime(dutc).to_ical(), b'20010101T123000Z') dutc = pytz.utc.localize(datetime(1899, 1, 1, 12, 30, 0)) self.assertEqual(vDatetime(dutc).to_ical(), b'18990101T123000Z') self.assertEqual(vDatetime.from_ical('20010101T000000'), datetime(2001, 1, 1, 0, 0)) self.assertRaises(ValueError, vDatetime.from_ical, '20010101T000000A') utc = vDatetime.from_ical('20010101T000000Z') self.assertEqual(vDatetime(utc).to_ical(), b'20010101T000000Z') # 1 minute before transition to DST dat = vDatetime.from_ical('20120311T015959', 'America/Denver') self.assertEqual(dat.strftime('%Y%m%d%H%M%S %z'), '20120311015959 -0700') # After transition to DST dat = vDatetime.from_ical('20120311T030000', 'America/Denver') self.assertEqual(dat.strftime('%Y%m%d%H%M%S %z'), '20120311030000 -0600') dat = vDatetime.from_ical('20101010T000000', 'Europe/Vienna') self.assertEqual(vDatetime(dat).to_ical(), b'20101010T000000') def test_prop_vDuration(self): from ..prop import vDuration self.assertEqual(vDuration(timedelta(11)).to_ical(), b'P11D') self.assertEqual(vDuration(timedelta(-14)).to_ical(), b'-P14D') self.assertEqual( vDuration(timedelta(1, 7384)).to_ical(), b'P1DT2H3M4S' ) self.assertEqual(vDuration(timedelta(1, 7380)).to_ical(), b'P1DT2H3M') self.assertEqual(vDuration(timedelta(1, 7200)).to_ical(), b'P1DT2H') self.assertEqual(vDuration(timedelta(0, 7200)).to_ical(), b'PT2H') self.assertEqual(vDuration(timedelta(0, 7384)).to_ical(), b'PT2H3M4S') self.assertEqual(vDuration(timedelta(0, 184)).to_ical(), b'PT3M4S') self.assertEqual(vDuration(timedelta(0, 22)).to_ical(), b'PT22S') self.assertEqual(vDuration(timedelta(0, 3622)).to_ical(), b'PT1H0M22S') self.assertEqual(vDuration(timedelta(days=1, hours=5)).to_ical(), b'P1DT5H') self.assertEqual(vDuration(timedelta(hours=-5)).to_ical(), b'-PT5H') self.assertEqual(vDuration(timedelta(days=-1, hours=-5)).to_ical(), b'-P1DT5H') # How does the parsing work? self.assertEqual(vDuration.from_ical('PT1H0M22S'), timedelta(0, 3622)) self.assertRaises(ValueError, vDuration.from_ical, 'kox') self.assertEqual(vDuration.from_ical('-P14D'), timedelta(-14)) self.assertRaises(ValueError, vDuration, 11) def test_prop_vPeriod(self): from ..prop import vPeriod # One day in exact datetimes per = (datetime(2000, 1, 1), datetime(2000, 1, 2)) self.assertEqual(vPeriod(per).to_ical(), b'20000101T000000/20000102T000000') per = (datetime(2000, 1, 1), timedelta(days=31)) self.assertEqual(vPeriod(per).to_ical(), b'20000101T000000/P31D') # Roundtrip p = vPeriod.from_ical('20000101T000000/20000102T000000') self.assertEqual( p, (datetime(2000, 1, 1, 0, 0), datetime(2000, 1, 2, 0, 0)) ) self.assertEqual(vPeriod(p).to_ical(), b'20000101T000000/20000102T000000') self.assertEqual(vPeriod.from_ical('20000101T000000/P31D'), (datetime(2000, 1, 1, 0, 0), timedelta(31))) # Roundtrip with absolute time p = vPeriod.from_ical('20000101T000000Z/20000102T000000Z') self.assertEqual(vPeriod(p).to_ical(), b'20000101T000000Z/20000102T000000Z') # And an error self.assertRaises(ValueError, vPeriod.from_ical, '20000101T000000/Psd31D') # Timezoned dk = pytz.timezone('Europe/Copenhagen') start = dk.localize(datetime(2000, 1, 1)) end = dk.localize(datetime(2000, 1, 2)) per = (start, end) self.assertEqual(vPeriod(per).to_ical(), b'20000101T000000/20000102T000000') self.assertEqual(vPeriod(per).params['TZID'], 'Europe/Copenhagen') p = vPeriod((dk.localize(datetime(2000, 1, 1)), timedelta(days=31))) self.assertEqual(p.to_ical(), b'20000101T000000/P31D') def test_prop_vWeekday(self): from ..prop import vWeekday self.assertEqual(vWeekday('mo').to_ical(), b'MO') self.assertRaises(ValueError, vWeekday, 'erwer') self.assertEqual(vWeekday.from_ical('mo'), 'MO') self.assertEqual(vWeekday.from_ical('+3mo'), '+3MO') self.assertRaises(ValueError, vWeekday.from_ical, 'Saturday') self.assertEqual(vWeekday('+mo').to_ical(), b'+MO') self.assertEqual(vWeekday('+3mo').to_ical(), b'+3MO') self.assertEqual(vWeekday('-tu').to_ical(), b'-TU') def test_prop_vFrequency(self): from ..prop import vFrequency self.assertRaises(ValueError, vFrequency, 'bad test') self.assertEqual(vFrequency('daily').to_ical(), b'DAILY') self.assertEqual(vFrequency('daily').from_ical('MONTHLY'), 'MONTHLY') def test_prop_vRecur(self): from ..prop import vRecur # Let's see how close we can get to one from the rfc: # FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30 r = dict({'freq': 'yearly', 'interval': 2}) r.update({ 'bymonth': 1, 'byday': 'su', 'byhour': [8, 9], 'byminute': 30 }) self.assertEqual( vRecur(r).to_ical(), b'FREQ=YEARLY;INTERVAL=2;BYMINUTE=30;BYHOUR=8,9;BYDAY=SU;BYMONTH=1' ) r = vRecur(FREQ='yearly', INTERVAL=2) r.update({ 'BYMONTH': 1, 'BYDAY': 'su', 'BYHOUR': [8, 9], 'BYMINUTE': 30, }) self.assertEqual( r.to_ical(), b'FREQ=YEARLY;INTERVAL=2;BYMINUTE=30;BYHOUR=8,9;BYDAY=SU;BYMONTH=1' ) r = vRecur(freq='DAILY', count=10) r['bysecond'] = [0, 15, 30, 45] self.assertEqual(r.to_ical(), b'FREQ=DAILY;COUNT=10;BYSECOND=0,15,30,45') r = vRecur(freq='DAILY', until=datetime(2005, 1, 1, 12, 0, 0)) self.assertEqual(r.to_ical(), b'FREQ=DAILY;UNTIL=20050101T120000') # How do we fare with regards to parsing? r = vRecur.from_ical('FREQ=DAILY;INTERVAL=2;COUNT=10') self.assertEqual(r, {'COUNT': [10], 'FREQ': ['DAILY'], 'INTERVAL': [2]}) self.assertEqual( vRecur(r).to_ical(), b'FREQ=DAILY;COUNT=10;INTERVAL=2' ) r = vRecur.from_ical('FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=-SU;' 'BYHOUR=8,9;BYMINUTE=30') self.assertEqual( r, {'BYHOUR': [8, 9], 'BYDAY': ['-SU'], 'BYMINUTE': [30], 'BYMONTH': [1], 'FREQ': ['YEARLY'], 'INTERVAL': [2]} ) self.assertEqual( vRecur(r).to_ical(), b'FREQ=YEARLY;INTERVAL=2;BYMINUTE=30;BYHOUR=8,9;BYDAY=-SU;' b'BYMONTH=1' ) # Some examples from the spec r = vRecur.from_ical('FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1') self.assertEqual(vRecur(r).to_ical(), b'FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1') p = 'FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30' r = vRecur.from_ical(p) self.assertEqual( vRecur(r).to_ical(), b'FREQ=YEARLY;INTERVAL=2;BYMINUTE=30;BYHOUR=8,9;BYDAY=SU;BYMONTH=1' ) # and some errors self.assertRaises(ValueError, vRecur.from_ical, 'BYDAY=12') def test_prop_vText(self): from ..prop import vText self.assertEqual(vText('Simple text').to_ical(), b'Simple text') # Escaped text t = vText('Text ; with escaped, chars') self.assertEqual(t.to_ical(), b'Text \\; with escaped\\, chars') # Escaped newlines self.assertEqual(vText('Text with escaped\\N chars').to_ical(), b'Text with escaped\\n chars') # If you pass a unicode object, it will be utf-8 encoded. As this is # the (only) standard that RFC 2445 support. t = vText('international chars \xe4\xf6\xfc') self.assertEqual(t.to_ical(), b'international chars \xc3\xa4\xc3\xb6\xc3\xbc') # and parsing? self.assertEqual(vText.from_ical('Text \\; with escaped\\, chars'), 'Text ; with escaped, chars') t = vText.from_ical('A string with\\; some\\\\ characters in\\it') self.assertEqual(t, "A string with; some\\ characters in\\it") # We are forgiving to utf-8 encoding errors: # We intentionally use a string with unexpected encoding # self.assertEqual(vText.from_ical(b'Ol\xe9'), 'Ol\ufffd') # Notice how accented E character, encoded with latin-1, got replaced # with the official U+FFFD REPLACEMENT CHARACTER. def test_prop_vTime(self): from ..prop import vTime self.assertEqual(vTime(12, 30, 0).to_ical(), '123000') self.assertEqual(vTime.from_ical('123000'), time(12, 30)) # We should also fail, right? self.assertRaises(ValueError, vTime.from_ical, '263000') def test_prop_vUri(self): from ..prop import vUri self.assertEqual(vUri('http://www.example.com/').to_ical(), b'http://www.example.com/') self.assertEqual(vUri.from_ical('http://www.example.com/'), 'http://www.example.com/') def test_prop_vGeo(self): from ..prop import vGeo # Pass a list self.assertEqual(vGeo([1.2, 3.0]).to_ical(), '1.2;3.0') # Pass a tuple self.assertEqual(vGeo((1.2, 3.0)).to_ical(), '1.2;3.0') g = vGeo.from_ical('37.386013;-122.082932') self.assertEqual(g, (float('37.386013'), float('-122.082932'))) self.assertEqual(vGeo(g).to_ical(), '37.386013;-122.082932') self.assertRaises(ValueError, vGeo, 'g') def test_prop_vUTCOffset(self): from ..prop import vUTCOffset self.assertEqual(vUTCOffset(timedelta(hours=2)).to_ical(), '+0200') self.assertEqual(vUTCOffset(timedelta(hours=-5)).to_ical(), '-0500') self.assertEqual(vUTCOffset(timedelta()).to_ical(), '+0000') self.assertEqual(vUTCOffset(timedelta(minutes=-30)).to_ical(), '-0030') self.assertEqual( vUTCOffset(timedelta(hours=2, minutes=-30)).to_ical(), '+0130' ) self.assertEqual(vUTCOffset(timedelta(hours=1, minutes=30)).to_ical(), '+0130') # Support seconds self.assertEqual(vUTCOffset(timedelta(hours=1, minutes=30, seconds=7)).to_ical(), '+013007') # Parsing self.assertEqual(vUTCOffset.from_ical('0000'), timedelta(0)) self.assertEqual(vUTCOffset.from_ical('-0030'), timedelta(-1, 84600)) self.assertEqual(vUTCOffset.from_ical('+0200'), timedelta(0, 7200)) self.assertEqual(vUTCOffset.from_ical('+023040'), timedelta(0, 9040)) self.assertEqual(vUTCOffset(vUTCOffset.from_ical('+0230')).to_ical(), '+0230') # And a few failures self.assertRaises(ValueError, vUTCOffset.from_ical, '+323k') self.assertRaises(ValueError, vUTCOffset.from_ical, '+2400') def test_prop_vInline(self): from ..prop import vInline self.assertEqual(vInline('Some text'), 'Some text') self.assertEqual(vInline.from_ical('Some text'), 'Some text') t2 = vInline('other text') t2.params['cn'] = 'Test Osterone' self.assertIsInstance(t2.params, Parameters) self.assertEqual(t2.params, {'CN': 'Test Osterone'}) def test_prop_TypesFactory(self): from ..prop import TypesFactory # To get a type you can use it like this. factory = TypesFactory() datetime_parser = factory['date-time'] self.assertEqual(datetime_parser(datetime(2001, 1, 1)).to_ical(), b'20010101T000000') # A typical use is when the parser tries to find a content type and use # text as the default value = '20050101T123000' value_type = 'date-time' self.assertEqual(factory.get(value_type, 'text').from_ical(value), datetime(2005, 1, 1, 12, 30)) # It can also be used to directly encode property and parameter values self.assertEqual( factory.to_ical('comment', 'by Rasmussen, Max M\xfcller'), b'by Rasmussen\\, Max M\xc3\xbcller' ) self.assertEqual(factory.to_ical('priority', 1), b'1') self.assertEqual(factory.to_ical('cn', 'Rasmussen, Max M\xfcller'), b'Rasmussen\\, Max M\xc3\xbcller') self.assertEqual( factory.from_ical('cn', b'Rasmussen\\, Max M\xc3\xb8ller'), 'Rasmussen, Max M\xf8ller' ) class TestPropertyValues(unittest.TestCase): def test_vDDDLists_timezone(self): """Test vDDDLists with timezone information. """ from .. import Event vevent = Event() at = pytz.timezone('Europe/Vienna') dt1 = at.localize(datetime(2013, 1, 1)) dt2 = at.localize(datetime(2013, 1, 2)) dt3 = at.localize(datetime(2013, 1, 3)) vevent.add('rdate', [dt1, dt2]) vevent.add('exdate', dt3) ical = vevent.to_ical() self.assertTrue( b'RDATE;TZID=Europe/Vienna:20130101T000000,20130102T000000' in ical ) self.assertTrue(b'EXDATE;TZID=Europe/Vienna:20130103T000000' in ical) class TestWindowsOlsonMapping(unittest.TestCase): """Test the mappings from windows to olson tzids""" def test_windows_timezone(self): """test that an example""" self.assertEqual( vDatetime.from_ical('20170507T181920', 'Eastern Standard Time'), pytz.timezone('America/New_York').localize(datetime(2017, 5, 7, 18, 19, 20)) ) def test_all(self): """test if all mappings actually map to valid pytz tzids""" for olson in WINDOWS_TO_OLSON.values(): pytz.timezone(olson) icalendar-4.0.3/src/icalendar/tests/x_location.ics0000644000076600000240000000257213357326437023005 0ustar mauritsstaff00000000000000BEGIN:VCALENDAR PRODID:-//Google Inc//Google Calendar 70.9054//EN VERSION:2.0 CALSCALE:GREGORIAN METHOD:PUBLISH X-WR-CALNAME:ITC X-WR-TIMEZONE:Europe/Zurich X-WR-CALDESC:ITC Bookings BEGIN:VTIMEZONE TZID:Europe/Zurich X-LIC-LOCATION:Europe/Zurich BEGIN:DAYLIGHT TZOFFSETFROM:+0100 TZOFFSETTO:+0200 TZNAME:CEST DTSTART:19700329T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:CET DTSTART:19701025T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT DTSTART;TZID=Europe/Zurich:20161028T140000 DTEND;TZID=Europe/Zurich:20161028T143000 RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR DTSTAMP:20161031T192828Z UID:BFE33ADD-5553-48B5-B5A5-F9DA5CA4C393 CREATED:20161029T121229Z DESCRIPTION:Some Description LAST-MODIFIED:20161029T121229Z LOCATION:Roadstar 16\n12764 Happyville\nDenmark SEQUENCE:0 STATUS:CONFIRMED SUMMARY:Daily Sync TRANSP:OPAQUE X-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-ADDRESS="Röadstar 16\n12764 Happyvi lle\nDenmark";X-APPLE-MAPKIT-HANDLE=CAESARoSCWYTYFhHQBEGfw4hQCIBDQoHRGVubW FyaxJES0hhcHB5dmlsbGUqSGFwcHl2aWxsZTIHSGFwcHl2aWxsZToEMTI3NjRCDQpSb2Fkc3Rh cloCMTZiUm9hZHN0YXIgMTYBEU1vcmRvcgENCk1vcmRvcioSUm9hZHN0YXIgMTYyUm9hZHN0YX IgMTYxMjc2NCBIYXBweXZpbGxlMgdEZW5tYXJrOThA=;X-APPLE-RADIUS=49.913058665846 98;X-APPLE-REFERENCEFRAME=1;X-TITLE=:geo:52.382762,7.528319 END:VEVENT END:VCALENDAR icalendar-4.0.3/src/icalendar/tests/time.ics0000644000076600000240000000007313357326437021576 0ustar mauritsstaff00000000000000BEGIN:VCALENDAR X-SOMETIME;VALUE=TIME:172010 END:VCALENDAR icalendar-4.0.3/src/icalendar/tests/hypothesis/0000755000076600000240000000000013357326437022337 5ustar mauritsstaff00000000000000icalendar-4.0.3/src/icalendar/tests/hypothesis/test_fuzzing.py0000644000076600000240000000163113357326437025445 0ustar mauritsstaff00000000000000import string from hypothesis import given, settings import hypothesis.strategies as st from icalendar.parser import Contentline, Contentlines, Parameters import unittest def printable_characters(**kw): return st.text( st.characters(blacklist_categories=( 'Cc', 'Cs' ), **kw) ) key = st.text(string.ascii_letters + string.digits, min_size=1) value = printable_characters(blacklist_characters='\\;:\"') class TestFuzzing(unittest.TestCase): @given(lines=st.lists( st.tuples(key, st.dictionaries(key, value), value), min_size=1 )) @settings(max_examples=10**9) def test_main(self, lines): cl = Contentlines() for key, params, value in lines: params = Parameters(**params) cl.append(Contentline.from_parts(key, params, value)) cl.append('') assert Contentlines.from_ical(cl.to_ical()) == cl icalendar-4.0.3/src/icalendar/tests/timezone_rdate.ics0000644000076600000240000000212513357326437023651 0ustar mauritsstaff00000000000000BEGIN:VCALENDAR VERSION:2.0 BEGIN:VTIMEZONE TZID:posix/Europe/Vaduz BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+002946 TZOFFSETTO:+0100 DTSTART:19011213T211538 RDATE;VALUE=DATE-TIME:19011213T211538 END:STANDARD BEGIN:DAYLIGHT TZNAME:CEST TZOFFSETFROM:+0100 TZOFFSETTO:+0200 DTSTART:19810329T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 END:DAYLIGHT BEGIN:DAYLIGHT TZNAME:CEST TZOFFSETFROM:+0100 TZOFFSETTO:+0200 DTSTART:19410505T010000 RDATE;VALUE=DATE-TIME:19410505T010000 RDATE;VALUE=DATE-TIME:19420504T010000 END:DAYLIGHT BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+0200 TZOFFSETTO:+0100 DTSTART:19810927T030000 RRULE:FREQ=YEARLY;COUNT=15;BYDAY=-1SU;BYMONTH=9 END:STANDARD BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+0200 TZOFFSETTO:+0100 DTSTART:19961027T030000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 END:STANDARD BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+0200 TZOFFSETTO:+0100 DTSTART:19411006T020000 RDATE;VALUE=DATE-TIME:19411006T020000 RDATE;VALUE=DATE-TIME:19421005T020000 END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:123 DTSTART;TZID=posix/Europe/Vaduz:20120213T100000 SUMMARY=testevent END:VEVENT END:VCALENDAR icalendar-4.0.3/src/icalendar/tests/test_unit_tools.py0000644000076600000240000000155513357326437023756 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- import unittest from icalendar.tools import UIDGenerator class TestTools(unittest.TestCase): def test_tools_UIDGenerator(self): # Automatic semi-random uid g = UIDGenerator() uid = g.uid() txt = uid.to_ical() length = 15 + 1 + 16 + 1 + 11 self.assertTrue(len(txt) == length) self.assertTrue(b'@example.com' in txt) # You should at least insert your own hostname to be more compliant uid = g.uid('Example.ORG') txt = uid.to_ical() self.assertTrue(len(txt) == length) self.assertTrue(b'@Example.ORG' in txt) # You can also insert a path or similar uid = g.uid('Example.ORG', '/path/to/content') txt = uid.to_ical() self.assertTrue(len(txt) == length) self.assertTrue(b'-/path/to/content@Example.ORG' in txt) icalendar-4.0.3/src/icalendar/tests/timezone_same_start_and_offset.ics0000644000076600000240000000073313357326437027107 0ustar mauritsstaff00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:Microsoft Exchange Server 2010 BEGIN:VTIMEZONE TZID:Tokyo Standard Time BEGIN:STANDARD DTSTART:16010101T000000 TZOFFSETFROM:+0900 TZOFFSETTO:+0900 END:STANDARD BEGIN:DAYLIGHT DTSTART:16010101T000000 TZOFFSETFROM:+0900 TZOFFSETTO:+0900 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT DTSTART;TZID="Tokyo Standard Time":20170224T120000 DTEND;TZID="Tokyo Standard Time":20170224T123000 UID:blafoobar SUMMARY:this is an event END:VEVENT END:VCALENDARD icalendar-4.0.3/src/icalendar/tests/test_property_params.py0000644000076600000240000002004513357326437025001 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from icalendar import Calendar from icalendar import Event from icalendar import Parameters from icalendar import vCalAddress import unittest import icalendar import re class TestPropertyParams(unittest.TestCase): def test_property_params(self): # Property parameters with values containing a COLON character, a # SEMICOLON character or a COMMA character MUST be placed in quoted # text. cal_address = vCalAddress('mailto:john.doe@example.org') cal_address.params["CN"] = "Doe, John" ical = Calendar() ical.add('organizer', cal_address) ical_str = Calendar.to_ical(ical) exp_str = b"""BEGIN:VCALENDAR\r\nORGANIZER;CN="Doe, John":"""\ b"""mailto:john.doe@example.org\r\nEND:VCALENDAR\r\n""" self.assertEqual(ical_str, exp_str) # other way around: ensure the property parameters can be restored from # an icalendar string. ical2 = Calendar.from_ical(ical_str) self.assertEqual(ical2.get('ORGANIZER').params.get('CN'), 'Doe, John') def test_unicode_param(self): cal_address = vCalAddress('mailto:john.doe@example.org') cal_address.params["CN"] = "Джон Доу" vevent = Event() vevent['ORGANIZER'] = cal_address self.assertEqual( vevent.to_ical().decode('utf-8'), 'BEGIN:VEVENT\r\n' 'ORGANIZER;CN="Джон Доу":mailto:john.doe@example.org\r\n' 'END:VEVENT\r\n' ) self.assertEqual(vevent['ORGANIZER'].params['CN'], 'Джон Доу') def test_quoting(self): # not double-quoted self._test_quoting("Aramis", 'Aramis') # if a space is present - enclose in double quotes self._test_quoting("Aramis Alameda", '"Aramis Alameda"') # a single quote in parameter value - double quote the value self._test_quoting("Aramis d'Alameda", '"Aramis d\'Alameda"') # double quote is replaced with single quote self._test_quoting("Aramis d\"Alameda", '"Aramis d\'Alameda"') self._test_quoting("Арамис д'Аламеда", '"Арамис д\'Аламеда"') def _test_quoting(self, cn_param, cn_quoted): """ @param cn_param: CN parameter value to test for quoting @param cn_quoted: expected quoted parameter in icalendar format """ vevent = Event() attendee = vCalAddress('test@mail.com') attendee.params['CN'] = cn_param vevent.add('ATTENDEE', attendee) self.assertEqual( vevent.to_ical(), b'BEGIN:VEVENT\r\nATTENDEE;CN=' + cn_quoted.encode('utf-8') + b':test@mail.com\r\nEND:VEVENT\r\n' ) def test_escaping(self): # verify that escaped non safe chars are decoded correctly NON_SAFE_CHARS = ',\\;:' for char in NON_SAFE_CHARS: cn_escaped = "Society\\%s 2014" % char cn_decoded = "Society%s 2014" % char vevent = Event.from_ical( 'BEGIN:VEVENT\r\n' 'ORGANIZER;CN=%s:that\r\n' 'END:VEVENT\r\n' % cn_escaped ) self.assertEqual(vevent['ORGANIZER'].params['CN'], cn_decoded) vevent = Event.from_ical( 'BEGIN:VEVENT\r\n' 'ORGANIZER;CN=that\\, that\\; %th%%at%\\\\ that\\:' ':это\\, то\\; that\\\\ %th%%at%\\:\r\n' 'END:VEVENT\r\n' ) self.assertEqual( vevent['ORGANIZER'].params['CN'], r'that, that; %th%%at%\ that:' ) self.assertEqual( vevent['ORGANIZER'].to_ical().decode('utf-8'), 'это, то; that\\ %th%%at%:' ) def test_parameters_class(self): # Simple parameter:value pair p = Parameters(parameter1='Value1') self.assertEqual(p.to_ical(), b'PARAMETER1=Value1') # keys are converted to upper self.assertEqual(list(p.keys()), ['PARAMETER1']) # Parameters are case insensitive self.assertEqual(p['parameter1'], 'Value1') self.assertEqual(p['PARAMETER1'], 'Value1') # Parameter with list of values must be seperated by comma p = Parameters({'parameter1': ['Value1', 'Value2']}) self.assertEqual(p.to_ical(), b'PARAMETER1=Value1,Value2') # Multiple parameters must be seperated by a semicolon p = Parameters({'RSVP': 'TRUE', 'ROLE': 'REQ-PARTICIPANT'}) self.assertEqual(p.to_ical(), b'ROLE=REQ-PARTICIPANT;RSVP=TRUE') # Parameter values containing ',;:' must be double quoted p = Parameters({'ALTREP': 'http://www.wiz.org'}) self.assertEqual(p.to_ical(), b'ALTREP="http://www.wiz.org"') # list items must be quoted seperately p = Parameters({'MEMBER': ['MAILTO:projectA@host.com', 'MAILTO:projectB@host.com']}) self.assertEqual( p.to_ical(), b'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"' ) # Now the whole sheebang p = Parameters({'parameter1': 'Value1', 'parameter2': ['Value2', 'Value3'], 'ALTREP': ['http://www.wiz.org', 'value4']}) self.assertEqual( p.to_ical(), (b'ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;' b'PARAMETER2=Value2,Value3') ) # We can also parse parameter strings self.assertEqual( Parameters.from_ical('PARAMETER1=Value 1;param2=Value 2'), Parameters({'PARAMETER1': 'Value 1', 'PARAM2': 'Value 2'}) ) # Including empty strings self.assertEqual(Parameters.from_ical('param='), Parameters({'PARAM': ''})) # We can also parse parameter strings self.assertEqual( Parameters.from_ical( 'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"' ), Parameters({'MEMBER': ['MAILTO:projectA@host.com', 'MAILTO:projectB@host.com']}) ) # We can also parse parameter strings self.assertEqual( Parameters.from_ical('ALTREP="http://www.wiz.org",value4;' 'PARAMETER1=Value1;PARAMETER2=Value2,Value3'), Parameters({'PARAMETER1': 'Value1', 'ALTREP': ['http://www.wiz.org', 'value4'], 'PARAMETER2': ['Value2', 'Value3']}) ) def test_parse_and_access_property_params(self): """Parse an ics string and access some property parameters then. This is a follow-up of a question recieved per email. """ ics = """BEGIN:VCALENDAR VERSION:2.0 PRODID://RESEARCH IN MOTION//BIS 3.0 METHOD:REQUEST BEGIN:VEVENT SEQUENCE:2 X-RIM-REVISION:0 SUMMARY:Test meeting from BB X-MICROSOFT-CDO-ALLDAYEVENT:TRUE CLASS:PUBLIC ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN="RembrandXS":MAILTO:rembrand@xs4all.nl ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN="RembrandDX":MAILTO:rembrand@daxlab.com ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN="RembrandSB":MAILTO:rembspam@xs4all.nl UID:XRIMCAL-628059586-522954492-9750559 DTSTART;VALUE=DATE:20120814 DTEND;VALUE=DATE:20120815 DESCRIPTION:Test meeting from BB DTSTAMP:20120813T151458Z ORGANIZER:mailto:rembrand@daxlab.com END:VEVENT END:VCALENDAR""" cal = icalendar.Calendar.from_ical(ics) event = cal.walk("VEVENT")[0] event['attendee'][0] self.assertEqual(event['attendee'][0].to_ical(), b'MAILTO:rembrand@xs4all.nl') self.assertEqual(event['attendee'][0].params.to_ical(), b'CN=RembrandXS;PARTSTAT=NEEDS-ACTION;RSVP=TRUE') self.assertEqual(event['attendee'][0].params['cn'], 'RembrandXS') def test_repr(self): """Test correct class representation. """ it = Parameters(parameter1='Value1') self.assertTrue( re.match(r"Parameters\({u?'PARAMETER1': u?'Value1'}\)", str(it)) ) icalendar-4.0.3/src/icalendar/tests/test_fixed_issues.py0000644000076600000240000004156213357326437024253 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from icalendar.parser_tools import to_unicode import unittest import datetime import icalendar import os import pytz class TestIssues(unittest.TestCase): def test_issue_53(self): """Issue #53 - Parsing failure on some descriptions? https://github.com/collective/icalendar/issues/53 """ directory = os.path.dirname(__file__) ics = open(os.path.join(directory, 'issue_53_parsing_failure.ics'), 'rb') cal = icalendar.Calendar.from_ical(ics.read()) ics.close() event = cal.walk('VEVENT')[0] desc = event.get('DESCRIPTION') self.assertTrue(b'July 12 at 6:30 PM' in desc.to_ical()) timezones = cal.walk('VTIMEZONE') self.assertEqual(len(timezones), 1) tz = timezones[0] self.assertEqual(tz['tzid'].to_ical(), b"America/New_York") def test_issue_55(self): """Issue #55 - Parse error on utc-offset with seconds value https://github.com/collective/icalendar/issues/55 """ ical_str = """BEGIN:VTIMEZONE TZID:America/Los Angeles BEGIN:STANDARD DTSTART:18831118T120702 RDATE:18831118T120702 TZNAME:PST TZOFFSETFROM:-075258 TZOFFSETTO:-0800 END:STANDARD END:VTIMEZONE""" tz = icalendar.Timezone.from_ical(ical_str) self.assertEqual( tz.to_ical(), b'BEGIN:VTIMEZONE\r\nTZID:America/Los Angeles\r\n' b'BEGIN:STANDARD\r\n' b'DTSTART:18831118T120702\r\nRDATE:18831118T120702\r\nTZNAME:PST' b'\r\nTZOFFSETFROM:-075258\r\nTZOFFSETTO:-0800\r\n' b'END:STANDARD\r\n' b'END:VTIMEZONE\r\n') def test_issue_58(self): """Issue #58 - TZID on UTC DATE-TIMEs https://github.com/collective/icalendar/issues/58 """ # According to RFC 2445: "The TZID property parameter MUST NOT be # applied to DATE-TIME or TIME properties whose time values are # specified in UTC." event = icalendar.Event() dt = pytz.utc.localize(datetime.datetime(2012, 7, 16, 0, 0, 0)) event.add('dtstart', dt) self.assertEqual( event.to_ical(), b"BEGIN:VEVENT\r\n" b"DTSTART;VALUE=DATE-TIME:20120716T000000Z\r\n" b"END:VEVENT\r\n" ) def test_issue_64(self): """Issue #64 - Event.to_ical() fails for unicode strings https://github.com/collective/icalendar/issues/64 """ # Non-unicode characters event = icalendar.Event() event.add("dtstart", datetime.datetime(2012, 9, 3, 0, 0, 0)) event.add("summary", "abcdef") self.assertEqual( event.to_ical(), b"BEGIN:VEVENT\r\nSUMMARY:abcdef\r\nDTSTART;VALUE=DATE-TIME:" b"20120903T000000\r\nEND:VEVENT\r\n" ) # Unicode characters event = icalendar.Event() event.add("dtstart", datetime.datetime(2012, 9, 3, 0, 0, 0)) event.add("summary", "åäö") self.assertEqual( event.to_ical(), b"BEGIN:VEVENT\r\nSUMMARY:\xc3\xa5\xc3\xa4\xc3\xb6\r\n" b"DTSTART;VALUE=DATE-TIME:20120903T000000\r\nEND:VEVENT\r\n" ) def test_issue_70(self): """Issue #70 - e.decode("RRULE") causes Attribute Error https://github.com/collective/icalendar/issues/70 """ ical_str = """BEGIN:VEVENT CREATED:20081114T072804Z UID:D449CA84-00A3-4E55-83E1-34B58268853B DTEND:20070220T180000 RRULE:FREQ=WEEKLY;INTERVAL=1;UNTIL=20070619T225959 TRANSP:OPAQUE SUMMARY:Esb mellon phone conf DTSTART:20070220T170000 DTSTAMP:20070221T095412Z SEQUENCE:0 END:VEVENT""" cal = icalendar.Calendar.from_ical(ical_str) recur = cal.decoded("RRULE") self.assertIsInstance(recur, icalendar.vRecur) self.assertEqual( recur.to_ical(), b'FREQ=WEEKLY;UNTIL=20070619T225959;INTERVAL=1' ) def test_issue_82(self): """Issue #82 - vBinary __repr__ called rather than to_ical from container types https://github.com/collective/icalendar/issues/82 """ b = icalendar.vBinary('text') b.params['FMTTYPE'] = 'text/plain' self.assertEqual(b.to_ical(), b'dGV4dA==') e = icalendar.Event() e.add('ATTACH', b) self.assertEqual( e.to_ical(), b"BEGIN:VEVENT\r\nATTACH;ENCODING=BASE64;FMTTYPE=text/plain;" b"VALUE=BINARY:dGV4dA==\r\nEND:VEVENT\r\n" ) def test_issue_100(self): """Issue #100 - Transformed doctests into unittests, Test fixes and cleanup. https://github.com/collective/icalendar/pull/100 """ ical_content = "BEGIN:VEVENT\r\nSUMMARY;LANGUAGE=ru:te\r\nEND:VEVENT" icalendar.Event.from_ical(ical_content).to_ical() def test_issue_101(self): """Issue #101 - icalender is choking on umlauts in ORGANIZER https://github.com/collective/icalendar/issues/101 """ ical_str = r"""BEGIN:VCALENDAR VERSION:2.0 X-WR-CALNAME:Kalender von acme\, admin PRODID:-//The Horde Project//Horde_iCalendar Library\, Horde 3.3.5//EN METHOD:PUBLISH BEGIN:VEVENT DTSTART:20130416T100000Z DTEND:20130416T110000Z DTSTAMP:20130416T092616Z UID:20130416112341.10064jz0k4j7uem8@acmenet.de CREATED:20130416T092341Z LAST-MODIFIED:20130416T092341Z SUMMARY:wichtiger termin 1 ORGANIZER;CN="acme, ädmin":mailto:adm-acme@mydomain.de LOCATION:im büro CLASS:PUBLIC STATUS:CONFIRMED TRANSP:OPAQUE END:VEVENT END:VCALENDAR""" cal = icalendar.Calendar.from_ical(ical_str) org_cn = cal.walk('VEVENT')[0]['ORGANIZER'].params['CN'] self.assertEqual(org_cn, 'acme, ädmin') def test_issue_104__ignore_exceptions(self): """ Issue #104 - line parsing error in a VEVENT (which has ignore_exceptions). Should mark the event broken but not raise an exception. https://github.com/collective/icalendar/issues/104 """ ical_str = """ BEGIN:VEVENT DTSTART:20140401T000000Z DTEND:20140401T010000Z DTSTAMP:20140401T000000Z SUMMARY:Broken Eevnt CLASS:PUBLIC STATUS:CONFIRMED TRANSP:OPAQUE X END:VEVENT""" event = icalendar.Calendar.from_ical(ical_str) self.assertTrue(isinstance(event, icalendar.Event)) self.assertTrue(event.is_broken) # REMOVE FOR NEXT MAJOR RELEASE self.assertEqual( event.errors, [(None, "Content line could not be parsed into parts: 'X': Invalid content line")] # noqa ) def test_issue_104__no_ignore_exceptions(self): """ Issue #104 - line parsing error in a VCALENDAR (which doesn't have ignore_exceptions). Should raise an exception. """ ical_str = """BEGIN:VCALENDAR VERSION:2.0 METHOD:PUBLISH BEGIN:VEVENT DTSTART:20140401T000000Z DTEND:20140401T010000Z DTSTAMP:20140401T000000Z SUMMARY:Broken Eevnt CLASS:PUBLIC STATUS:CONFIRMED TRANSP:OPAQUE END:VEVENT X END:VCALENDAR""" with self.assertRaises(ValueError): icalendar.Calendar.from_ical(ical_str) def test_issue_112(self): """Issue #112 - No timezone info on EXDATE https://github.com/collective/icalendar/issues/112 """ directory = os.path.dirname(__file__) path = os.path.join(directory, 'issue_112_missing_tzinfo_on_exdate.ics') with open(path, 'rb') as ics: cal = icalendar.Calendar.from_ical(ics.read()) event = cal.walk('VEVENT')[0] event_ical = to_unicode(event.to_ical()) # Py3 str type doesn't # support buffer API # General timezone aware dates in ical string self.assertTrue('DTSTART;TZID=America/New_York:20130907T120000' in event_ical) self.assertTrue('DTEND;TZID=America/New_York:20130907T170000' in event_ical) # Specific timezone aware exdates in ical string self.assertTrue('EXDATE;TZID=America/New_York:20131012T120000' in event_ical) self.assertTrue('EXDATE;TZID=America/New_York:20131011T120000' in event_ical) self.assertEqual(event['exdate'][0].dts[0].dt.tzname(), 'EDT') def test_issue_116(self): """Issue #116/#117 - How to add 'X-APPLE-STRUCTURED-LOCATION' https://github.com/collective/icalendar/issues/116 https://github.com/collective/icalendar/issues/117 """ event = icalendar.Event() event.add( "X-APPLE-STRUCTURED-LOCATION", "geo:-33.868900,151.207000", parameters={ "VALUE": "URI", "X-ADDRESS": "367 George Street Sydney CBD NSW 2000", "X-APPLE-RADIUS": "72", "X-TITLE": "367 George Street" } ) self.assertEqual( event.to_ical(), b'BEGIN:VEVENT\r\nX-APPLE-STRUCTURED-LOCATION;VALUE=URI;' b'X-ADDRESS="367 George Street Sydney \r\n CBD NSW 2000";' b'X-APPLE-RADIUS=72;X-TITLE="367 George Street":' b'geo:-33.868900\r\n \\,151.207000\r\nEND:VEVENT\r\n' ) # roundtrip self.assertEqual( event.to_ical(), icalendar.Event.from_ical(event.to_ical()).to_ical() ) def test_issue_142(self): """Issue #142 - Multivalued parameters This is needed for VCard 3.0. https://github.com/collective/icalendar/pull/142 """ from icalendar.parser import Contentline, Parameters ctl = Contentline.from_ical("TEL;TYPE=HOME,VOICE:000000000") self.assertEqual( ctl.parts(), ('TEL', Parameters({'TYPE': ['HOME', 'VOICE']}), '000000000'), ) def test_issue_143(self): """Issue #143 - Allow dots in property names. Another vCard related issue. https://github.com/collective/icalendar/pull/143 """ from icalendar.parser import Contentline, Parameters ctl = Contentline.from_ical("ITEMADRNULLTHISISTHEADRESS08158SOMECITY12345.ADR:;;This is the Adress 08; Some City;;12345;Germany") # nopep8 self.assertEqual( ctl.parts(), ('ITEMADRNULLTHISISTHEADRESS08158SOMECITY12345.ADR', Parameters(), ';;This is the Adress 08; Some City;;12345;Germany'), ) ctl2 = Contentline.from_ical("ITEMADRNULLTHISISTHEADRESS08158SOMECITY12345.X-ABLABEL:") # nopep8 self.assertEqual( ctl2.parts(), ('ITEMADRNULLTHISISTHEADRESS08158SOMECITY12345.X-ABLABEL', Parameters(), ''), ) def test_issue_157(self): """Issue #157 - Recurring rules and trailing semicolons https://github.com/collective/icalendar/pull/157 """ # The trailing semicolon caused a problem ical_str = """BEGIN:VEVENT DTSTART:20150325T101010 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU; END:VEVENT""" cal = icalendar.Calendar.from_ical(ical_str) recur = cal.decoded("RRULE") self.assertIsInstance(recur, icalendar.vRecur) self.assertEqual( recur.to_ical(), b'FREQ=YEARLY;BYDAY=1SU;BYMONTH=11' ) def test_issue_168(self): """Issue #168 - Parsing invalid icalendars fails without any warning https://github.com/collective/icalendar/issues/168 """ event_str = """ BEGIN:VCALENDAR BEGIN:VEVENT DTEND:20150905T100000Z DTSTART:20150905T090000Z X-APPLE-RADIUS=49.91307046514149 UID:123 END:VEVENT END:VCALENDAR""" calendar = icalendar.Calendar.from_ical(event_str) self.assertEqual( calendar.to_ical(), b'BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20150905T090000Z\r\n' b'DTEND:20150905T100000Z\r\nUID:123\r\n' b'END:VEVENT\r\nEND:VCALENDAR\r\n' ) def test_index_error_issue(self): """Found an issue where from_ical() would raise IndexError for properties without parent components. https://github.com/collective/icalendar/pull/179 """ with self.assertRaises(ValueError): icalendar.Calendar.from_ical('VERSION:2.0') def test_issue_178(self): """Issue #178 - A component with an unknown/invalid name is represented as one of the known components, the information about the original component name is lost. https://github.com/collective/icalendar/issues/178 https://github.com/collective/icalendar/pull/180 """ # Parsing of a nonstandard component ical_str = '\r\n'.join(['BEGIN:MYCOMP', 'END:MYCOMP']) cal = icalendar.Calendar.from_ical(ical_str) self.assertEqual(cal.to_ical(), b'BEGIN:MYCOMP\r\nEND:MYCOMP\r\n') # Nonstandard component inside other components, also has properties ical_str = '\r\n'.join(['BEGIN:VCALENDAR', 'BEGIN:UNKNOWN', 'UID:1234', 'END:UNKNOWN', 'END:VCALENDAR']) cal = icalendar.Calendar.from_ical(ical_str) self.assertEqual(cal.errors, []) self.assertEqual(cal.to_ical(), b'BEGIN:VCALENDAR\r\nBEGIN:UNKNOWN\r\nUID:1234\r\n' b'END:UNKNOWN\r\nEND:VCALENDAR\r\n') # Nonstandard component is able to contain other components ical_str = '\r\n'.join(['BEGIN:MYCOMPTOO', 'DTSTAMP:20150121T080000', 'BEGIN:VEVENT', 'UID:12345', 'DTSTART:20150122', 'END:VEVENT', 'END:MYCOMPTOO']) cal = icalendar.Calendar.from_ical(ical_str) self.assertEqual(cal.errors, []) self.assertEqual(cal.to_ical(), b'BEGIN:MYCOMPTOO\r\nDTSTAMP:20150121T080000\r\n' b'BEGIN:VEVENT\r\nDTSTART:20150122\r\nUID:12345\r\n' b'END:VEVENT\r\nEND:MYCOMPTOO\r\n') def test_issue_184(self): """Issue #184 - Previous changes in code broke already broken representation of PERIOD values - in a new way""" ical_str = ['BEGIN:VEVENT', 'DTSTAMP:20150219T133000', 'DTSTART:20150219T133000', 'UID:1234567', 'RDATE;VALUE=PERIOD:20150219T133000/PT10H', 'END:VEVENT'] event = icalendar.Event.from_ical('\r\n'.join(ical_str)) self.assertEqual(event.errors, []) self.assertEqual(event.to_ical(), b'BEGIN:VEVENT\r\nDTSTART:20150219T133000\r\n' b'DTSTAMP:20150219T133000\r\nUID:1234567\r\n' b'RDATE;VALUE=PERIOD:20150219T133000/PT10H\r\n' b'END:VEVENT\r\n' ) def test_issue_237(self): """Issue #237 - Fail to parse timezone with non-ascii TZID""" ical_str = ['BEGIN:VCALENDAR', 'BEGIN:VTIMEZONE', 'TZID:(UTC-03:00) Brasília', 'BEGIN:STANDARD', 'TZNAME:Brasília standard', 'DTSTART:16010101T235959', 'TZOFFSETFROM:-0200', 'TZOFFSETTO:-0300', 'RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=3SA;BYMONTH=2', 'END:STANDARD', 'BEGIN:DAYLIGHT', 'TZNAME:Brasília daylight', 'DTSTART:16010101T235959', 'TZOFFSETFROM:-0300', 'TZOFFSETTO:-0200', 'RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SA;BYMONTH=10', 'END:DAYLIGHT', 'END:VTIMEZONE', 'BEGIN:VEVENT', 'DTSTART;TZID=\"(UTC-03:00) Brasília\":20170511T133000', 'DTEND;TZID=\"(UTC-03:00) Brasília\":20170511T140000', 'END:VEVENT', 'END:VCALENDAR', ] cal = icalendar.Calendar.from_ical('\r\n'.join(ical_str)) self.assertEqual(cal.errors, []) dtstart = cal.walk(name='VEVENT')[0].decoded("DTSTART") expected = pytz.timezone('America/Sao_Paulo').localize(datetime.datetime(2017, 5, 11, 13, 30)) self.assertEqual(dtstart, expected) try: expected_zone = str('(UTC-03:00) Brasília') expected_tzname = str('Brasília standard') except UnicodeEncodeError: expected_zone = '(UTC-03:00) Brasília'.encode('ascii', 'replace') expected_tzname = 'Brasília standard'.encode('ascii', 'replace') self.assertEqual(dtstart.tzinfo.zone, expected_zone) self.assertEqual(dtstart.tzname(), expected_tzname) icalendar-4.0.3/src/icalendar/tests/issue_112_missing_tzinfo_on_exdate.ics0000644000076600000240000000231213357326437027521 0ustar mauritsstaff00000000000000BEGIN:VCALENDAR PRODID:-//Google Inc//Google Calendar 70.9054//EN VERSION:2.0 CALSCALE:GREGORIAN METHOD:PUBLISH X-WR-CALNAME:Market East X-WR-TIMEZONE:America/New_York X-WR-CALDESC: BEGIN:VTIMEZONE TZID:America/New_York X-LIC-LOCATION:America/New_York BEGIN:DAYLIGHT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT DTSTART:19700308T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:EST DTSTART:19701101T020000 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT DTSTART;TZID=America/New_York:20130907T120000 DTEND;TZID=America/New_York:20130907T170000 RRULE:FREQ=WEEKLY;BYDAY=FR,SA;UNTIL=20131025T035959Z EXDATE;TZID=America/New_York:20131012T120000 EXDATE;TZID=America/New_York:20131011T120000 DTSTAMP:20131021T025552Z UID:ak30b02u7858q1oo6ji9dm4mgg@google.com CREATED:20130903T181453Z DESCRIPTION:The Fieldhouse and Hard Rock Cafe are working with PhillyRising to provide live entertainment on Friday and Saturday afternoons throughout the Summer. LAST-MODIFIED:20131015T210927Z LOCATION:12th and Market Streets (weather permitting) SEQUENCE:0 STATUS:CONFIRMED SUMMARY:Market East Live! TRANSP:OPAQUE END:VEVENT END:VCALENDAR icalendar-4.0.3/src/icalendar/tests/test_unit_parser_tools.py0000644000076600000240000000217213357326437025326 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from icalendar.parser_tools import data_encode from icalendar.parser_tools import to_unicode import unittest class TestParserTools(unittest.TestCase): def test_parser_tools_to_unicode(self): self.assertEqual(to_unicode(b'spam'), 'spam') self.assertEqual(to_unicode('spam'), 'spam') self.assertEqual(to_unicode('spam'.encode('utf-8')), 'spam') self.assertEqual(to_unicode(b'\xc6\xb5'), '\u01b5') self.assertEqual(to_unicode('\xc6\xb5'.encode('iso-8859-1')), '\u01b5') self.assertEqual(to_unicode(b'\xc6\xb5', encoding='ascii'), '\u01b5') self.assertEqual(to_unicode(1), 1) self.assertEqual(to_unicode(None), None) def test_parser_tools_data_encode(self): data1 = { 'k1': 'v1', 'k2': 'v2', 'k3': 'v3', 'li1': ['it1', 'it2', {'k4': 'v4', 'k5': 'v5'}, 123] } res = {b'k3': b'v3', b'k2': b'v2', b'k1': b'v1', b'li1': [b'it1', b'it2', {b'k5': b'v5', b'k4': b'v4'}, 123]} self.assertEqual(data_encode(data1), res) icalendar-4.0.3/src/icalendar/tests/encoding.ics0000644000076600000240000000066413357326437022434 0ustar mauritsstaff00000000000000BEGIN:VCALENDAR PRODID:-//Plönë.org//NONSGML plone.app.event//EN VERSION:2.0 X-WR-CALNAME:äöü ÄÖÜ € X-WR-CALDESC:test non ascii: äöü ÄÖÜ € X-WR-RELCALID:12345 BEGIN:VEVENT DTSTART:20101010T100000Z DTEND:20101010T120000Z CREATED:20101010T100000Z UID:123456 SUMMARY:Non-ASCII Test: ÄÖÜ äöü € DESCRIPTION:icalendar should be able to handle non-ascii: €äüöÄÜÖ. LOCATION:Tribstrül END:VEVENT END:VCALENDAR icalendar-4.0.3/src/icalendar/__init__.py0000644000076600000240000000143513357326437021112 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- __version__ = '4.0.3' from icalendar.cal import ( Calendar, Event, Todo, Journal, Timezone, TimezoneStandard, TimezoneDaylight, FreeBusy, Alarm, ComponentFactory, ) # Property Data Value Types from icalendar.prop import ( vBinary, vBoolean, vCalAddress, vDatetime, vDate, vDDDTypes, vDuration, vFloat, vInt, vPeriod, vWeekday, vFrequency, vRecur, vText, vTime, vUri, vGeo, vUTCOffset, TypesFactory, ) # useful tzinfo subclasses from icalendar.prop import ( FixedOffset, LocalTimezone, ) # Parameters and helper methods for splitting and joining string with escaped # chars. from icalendar.parser import ( Parameters, q_split, q_join, ) icalendar-4.0.3/src/icalendar/parser.py0000644000076600000240000003143713357326437020654 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- """This module parses and generates contentlines as defined in RFC 2445 (iCalendar), but will probably work for other MIME types with similar syntax. Eg. RFC 2426 (vCard) It is stupid in the sense that it treats the content purely as strings. No type conversion is attempted. """ from __future__ import unicode_literals from icalendar import compat from icalendar.caselessdict import CaselessDict from icalendar.parser_tools import DEFAULT_ENCODING from icalendar.parser_tools import SEQUENCE_TYPES from icalendar.parser_tools import to_unicode import re def escape_char(text): """Format value according to iCalendar TEXT escaping rules. """ assert isinstance(text, (compat.unicode_type, compat.bytes_type)) # NOTE: ORDER MATTERS! return text.replace(r'\N', '\n')\ .replace('\\', '\\\\')\ .replace(';', r'\;')\ .replace(',', r'\,')\ .replace('\r\n', r'\n')\ .replace('\n', r'\n') def unescape_char(text): assert isinstance(text, (compat.unicode_type, compat.bytes_type)) # NOTE: ORDER MATTERS! if isinstance(text, compat.unicode_type): return text.replace('\\N', '\\n')\ .replace('\r\n', '\n')\ .replace('\\n', '\n')\ .replace('\\,', ',')\ .replace('\\;', ';')\ .replace('\\\\', '\\') elif isinstance(text, compat.bytes_type): return text.replace(b'\\N', b'\\n')\ .replace(b'\r\n', b'\n')\ .replace(b'\n', b'\n')\ .replace(b'\\,', b',')\ .replace(b'\\;', b';')\ .replace(b'\\\\', b'\\') def tzid_from_dt(dt): tzid = None if hasattr(dt.tzinfo, 'zone'): tzid = dt.tzinfo.zone # pytz implementation elif hasattr(dt.tzinfo, 'tzname'): try: tzid = dt.tzinfo.tzname(dt) # dateutil implementation except AttributeError: # No tzid available pass return tzid def foldline(line, limit=75, fold_sep='\r\n '): """Make a string folded as defined in RFC5545 Lines of text SHOULD NOT be longer than 75 octets, excluding the line break. Long content lines SHOULD be split into a multiple line representations using a line "folding" technique. That is, a long line can be split between any two characters by inserting a CRLF immediately followed by a single linear white-space character (i.e., SPACE or HTAB). """ assert isinstance(line, compat.unicode_type) assert '\n' not in line # Use a fast and simple variant for the common case that line is all ASCII. try: line.encode('ascii') except (UnicodeEncodeError, UnicodeDecodeError): pass else: return fold_sep.join( line[i:i + limit - 1] for i in range(0, len(line), limit - 1) ) ret_chars = [] byte_count = 0 for char in line: char_byte_len = len(char.encode(DEFAULT_ENCODING)) byte_count += char_byte_len if byte_count >= limit: ret_chars.append(fold_sep) byte_count = char_byte_len ret_chars.append(char) return ''.join(ret_chars) ################################################################# # Property parameter stuff def param_value(value): """Returns a parameter value. """ if isinstance(value, SEQUENCE_TYPES): return q_join(value) return dquote(value) # Could be improved # [\w-] because of the iCalendar RFC # . because of the vCard RFC NAME = re.compile(r'[\w.-]+') UNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7F",:;]') QUNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7F"]') FOLD = re.compile(b'(\r?\n)+[ \t]') uFOLD = re.compile('(\r?\n)+[ \t]') NEWLINE = re.compile(r'\r?\n') def validate_token(name): match = NAME.findall(name) if len(match) == 1 and name == match[0]: return raise ValueError(name) def validate_param_value(value, quoted=True): validator = QUNSAFE_CHAR if quoted else UNSAFE_CHAR if validator.findall(value): raise ValueError(value) # chars presence of which in parameter value will be cause the value # to be enclosed in double-quotes QUOTABLE = re.compile("[,;: ’']") def dquote(val): """Enclose parameter values containing [,;:] in double quotes. """ # a double-quote character is forbidden to appear in a parameter value # so replace it with a single-quote character val = val.replace('"', "'") if QUOTABLE.search(val): return '"%s"' % val return val # parsing helper def q_split(st, sep=',', maxsplit=-1): """Splits a string on char, taking double (q)uotes into considderation. """ if maxsplit == 0: return [st] result = [] cursor = 0 length = len(st) inquote = 0 splits = 0 for i in range(length): ch = st[i] if ch == '"': inquote = not inquote if not inquote and ch == sep: result.append(st[cursor:i]) cursor = i + 1 splits += 1 if i + 1 == length or splits == maxsplit: result.append(st[cursor:]) break return result def q_join(lst, sep=','): """Joins a list on sep, quoting strings with QUOTABLE chars. """ return sep.join(dquote(itm) for itm in lst) class Parameters(CaselessDict): """Parser and generator of Property parameter strings. It knows nothing of datatypes. Its main concern is textual structure. """ def params(self): """In rfc2445 keys are called parameters, so this is to be consitent with the naming conventions. """ return self.keys() # TODO? # Later, when I get more time... need to finish this off now. The last major # thing missing. # def _encode(self, name, value, cond=1): # # internal, for conditional convertion of values. # if cond: # klass = types_factory.for_property(name) # return klass(value) # return value # # def add(self, name, value, encode=0): # "Add a parameter value and optionally encode it." # if encode: # value = self._encode(name, value, encode) # self[name] = value # # def decoded(self, name): # "returns a decoded value, or list of same" def to_ical(self, sorted=True): result = [] items = list(self.items()) if sorted: items.sort() for key, value in items: value = param_value(value) if isinstance(value, compat.unicode_type): value = value.encode(DEFAULT_ENCODING) # CaselessDict keys are always unicode key = key.upper().encode(DEFAULT_ENCODING) result.append(key + b'=' + value) return b';'.join(result) @classmethod def from_ical(cls, st, strict=False): """Parses the parameter format from ical text format.""" # parse into strings result = cls() for param in q_split(st, ';'): try: key, val = q_split(param, '=', maxsplit=1) validate_token(key) # Property parameter values that are not in quoted # strings are case insensitive. vals = [] for v in q_split(val, ','): if v.startswith('"') and v.endswith('"'): v = v.strip('"') validate_param_value(v, quoted=True) vals.append(v) else: validate_param_value(v, quoted=False) if strict: vals.append(v.upper()) else: vals.append(v) if not vals: result[key] = val else: if len(vals) == 1: result[key] = vals[0] else: result[key] = vals except ValueError as exc: raise ValueError('%r is not a valid parameter string: %s' % (param, exc)) return result def escape_string(val): # '%{:02X}'.format(i) return val.replace(r'\,', '%2C').replace(r'\:', '%3A')\ .replace(r'\;', '%3B').replace(r'\\', '%5C') def unescape_string(val): return val.replace('%2C', ',').replace('%3A', ':')\ .replace('%3B', ';').replace('%5C', '\\') def unescape_list_or_string(val): if isinstance(val, list): return [unescape_string(s) for s in val] else: return unescape_string(val) ######################################### # parsing and generation of content lines class Contentline(compat.unicode_type): """A content line is basically a string that can be folded and parsed into parts. """ def __new__(cls, value, strict=False, encoding=DEFAULT_ENCODING): value = to_unicode(value, encoding=encoding) assert '\n' not in value, ('Content line can not contain unescaped ' 'new line characters.') self = super(Contentline, cls).__new__(cls, value) self.strict = strict return self @classmethod def from_parts(cls, name, params, values, sorted=True): """Turn a parts into a content line. """ assert isinstance(params, Parameters) if hasattr(values, 'to_ical'): values = values.to_ical() else: values = vText(values).to_ical() # elif isinstance(values, basestring): # values = escape_char(values) # TODO: after unicode only, remove this # Convert back to unicode, after to_ical encoded it. name = to_unicode(name) values = to_unicode(values) if params: params = to_unicode(params.to_ical(sorted=sorted)) return cls('%s;%s:%s' % (name, params, values)) return cls('%s:%s' % (name, values)) def parts(self): """Split the content line up into (name, parameters, values) parts. """ try: st = escape_string(self) name_split = None value_split = None in_quotes = False for i, ch in enumerate(st): if not in_quotes: if ch in ':;' and not name_split: name_split = i if ch == ':' and not value_split: value_split = i if ch == '"': in_quotes = not in_quotes name = unescape_string(st[:name_split]) if not name: raise ValueError('Key name is required') validate_token(name) if not name_split or name_split + 1 == value_split: raise ValueError('Invalid content line') params = Parameters.from_ical(st[name_split + 1: value_split], strict=self.strict) params = Parameters( (unescape_string(key), unescape_list_or_string(value)) for key, value in compat.iteritems(params) ) values = unescape_string(st[value_split + 1:]) return (name, params, values) except ValueError as exc: raise ValueError( "Content line could not be parsed into parts: '%s': %s" % (self, exc) ) @classmethod def from_ical(cls, ical, strict=False): """Unfold the content lines in an iCalendar into long content lines. """ ical = to_unicode(ical) # a fold is carriage return followed by either a space or a tab return cls(uFOLD.sub('', ical), strict=strict) def to_ical(self): """Long content lines are folded so they are less than 75 characters wide. """ return foldline(self).encode(DEFAULT_ENCODING) class Contentlines(list): """I assume that iCalendar files generally are a few kilobytes in size. Then this should be efficient. for Huge files, an iterator should probably be used instead. """ def to_ical(self): """Simply join self. """ return b'\r\n'.join(line.to_ical() for line in self if line) + b'\r\n' @classmethod def from_ical(cls, st): """Parses a string into content lines. """ st = to_unicode(st) try: # a fold is carriage return followed by either a space or a tab unfolded = uFOLD.sub('', st) lines = cls(Contentline(line) for line in NEWLINE.split(unfolded) if line) lines.append('') # '\r\n' at the end of every content line return lines except: raise ValueError('Expected StringType with content lines') # XXX: what kind of hack is this? import depends to be at end from icalendar.prop import vText icalendar-4.0.3/src/icalendar/parser_tools.py0000644000076600000240000000213613357326437022066 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- from icalendar import compat SEQUENCE_TYPES = (list, tuple) DEFAULT_ENCODING = 'utf-8' def to_unicode(value, encoding='utf-8'): """Converts a value to unicode, even if it is already a unicode string. """ if isinstance(value, compat.unicode_type): return value elif isinstance(value, compat.bytes_type): try: value = value.decode(encoding) except UnicodeDecodeError: value = value.decode('utf-8', 'replace') return value def data_encode(data, encoding=DEFAULT_ENCODING): """Encode all datastructures to the given encoding. Currently unicode strings, dicts and lists are supported. """ # http://stackoverflow.com/questions/1254454/fastest-way-to-convert-a-dicts-keys-values-from-unicode-to-str if isinstance(data, compat.unicode_type): return data.encode(encoding) elif isinstance(data, dict): return dict(map(data_encode, compat.iteritems(data))) elif isinstance(data, list) or isinstance(data, tuple): return list(map(data_encode, data)) else: return data icalendar-4.0.3/src/icalendar/cli.py0000644000076600000240000000616013357326437020122 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- """iCalendar utility""" from __future__ import unicode_literals import argparse import sys from datetime import datetime from . import Calendar, __version__ _template = """Organiser: {organiser} Attendees: {attendees} Summary: {summary} When: {time_from}-{time_to} Location: {location} Comment: {comment} Description: {description} """ def _format_name(address): """Retrieve the e-mail and optionally the name from an address. :arg vCalAddress address: An address object. :returns str: The name and optionally the e-mail address. """ if not address: return '' email = address.title().split(':')[1] if 'cn' in address.params: return '{} <{}>'.format(address.params['cn'], email) return email def _format_attendees(attendees): """Format the list of attendees. :arg any attendees: Either a list, a string or a vCalAddress object. :returns str: Formatted list of attendees. """ if type(attendees) == list: return '\n '.join(map(_format_name, attendees)) return _format_name(attendees) def view(input_handle, output_handle): """Make a human readable summary of an iCalendar file. :arg stream handle: Open readable handle to an iCalendar file. :returns str: Human readable summary. """ cal = Calendar.from_ical(input_handle.read()) for event in cal.walk('vevent'): output_handle.write(_template.format( organiser=_format_name(event.get('organizer', '')), attendees=_format_attendees(event.get('attendee')), summary=event.get('summary', ''), time_from=datetime.strftime( event.get('dtstart').dt, '%a %d %b %Y %H:%M'), time_to=datetime.strftime(event.get('dtend').dt, '%H:%M'), location=event.get('location', ''), comment=event.get('comment', ''), description=event.get('description', '')).encode('utf-8')) def main(): """Main entry point.""" parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description=__doc__) parser.add_argument( '-v', '--version', action='version', version='{} version {}'.format(parser.prog, __version__)) # This seems a bit of an overkill now, but we will probably add more # functionality later, e.g., iCalendar to JSON / YAML and vice versa. subparsers = parser.add_subparsers(dest='subcommand') subparser = subparsers.add_parser( 'view', description=view.__doc__.split('\n\n')[0]) subparser.add_argument( 'input_handle', metavar='INPUT', type=argparse.FileType('r'), help='iCalendar file') subparser.add_argument( '-o', dest='output_handle', metavar='OUTPUT', type=argparse.FileType('w'), default=sys.stdout, help='output file (default=)') subparser.set_defaults(func=view) args = parser.parse_args() try: args.func(**{k: v for k, v in vars(args).items() if k not in ('func', 'subcommand')}) except ValueError as error: parser.error(error) if __name__ == '__main__': main() icalendar-4.0.3/src/icalendar/caselessdict.py0000644000076600000240000000722613357326437022025 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- from icalendar.compat import iteritems from icalendar.parser_tools import to_unicode from collections import OrderedDict def canonsort_keys(keys, canonical_order=None): """Sorts leading keys according to canonical_order. Keys not specified in canonical_order will appear alphabetically at the end. """ canonical_map = {k: i for i, k in enumerate(canonical_order or [])} head = [k for k in keys if k in canonical_map] tail = [k for k in keys if k not in canonical_map] return sorted(head, key=lambda k: canonical_map[k]) + sorted(tail) def canonsort_items(dict1, canonical_order=None): """Returns a list of items from dict1, sorted by canonical_order. """ return [(k, dict1[k]) for k in canonsort_keys(dict1.keys(), canonical_order)] class CaselessDict(OrderedDict): """A dictionary that isn't case sensitive, and only uses strings as keys. Values retain their case. """ def __init__(self, *args, **kwargs): """Set keys to upper for initial dict. """ super(CaselessDict, self).__init__(*args, **kwargs) for key, value in self.items(): key_upper = to_unicode(key).upper() if key != key_upper: super(CaselessDict, self).__delitem__(key) self[key_upper] = value def __getitem__(self, key): key = to_unicode(key) return super(CaselessDict, self).__getitem__(key.upper()) def __setitem__(self, key, value): key = to_unicode(key) super(CaselessDict, self).__setitem__(key.upper(), value) def __delitem__(self, key): key = to_unicode(key) super(CaselessDict, self).__delitem__(key.upper()) def __contains__(self, key): key = to_unicode(key) return super(CaselessDict, self).__contains__(key.upper()) def get(self, key, default=None): key = to_unicode(key) return super(CaselessDict, self).get(key.upper(), default) def setdefault(self, key, value=None): key = to_unicode(key) return super(CaselessDict, self).setdefault(key.upper(), value) def pop(self, key, default=None): key = to_unicode(key) return super(CaselessDict, self).pop(key.upper(), default) def popitem(self): return super(CaselessDict, self).popitem() def has_key(self, key): key = to_unicode(key) return super(CaselessDict, self).__contains__(key.upper()) def update(self, *args, **kwargs): # Multiple keys where key1.upper() == key2.upper() will be lost. mappings = list(args) + [kwargs] for mapping in mappings: if hasattr(mapping, 'items'): mapping = iteritems(mapping) for key, value in mapping: self[key] = value def copy(self): return type(self)(super(CaselessDict, self).copy()) def __repr__(self): return '%s(%s)' % (type(self).__name__, dict(self)) def __eq__(self, other): return self is other or dict(self.items()) == dict(other.items()) # A list of keys that must appear first in sorted_keys and sorted_items; # must be uppercase. canonical_order = None def sorted_keys(self): """Sorts keys according to the canonical_order for the derived class. Keys not specified in canonical_order will appear at the end. """ return canonsort_keys(self.keys(), self.canonical_order) def sorted_items(self): """Sorts items according to the canonical_order for the derived class. Items not specified in canonical_order will appear at the end. """ return canonsort_items(self, self.canonical_order) icalendar-4.0.3/src/icalendar/timezone_cache.py0000644000076600000240000000015213357326437022323 0ustar mauritsstaff00000000000000# -*- coding: utf-8 -*- # we save all timezone with TZIDs unknow to the TZDB in here _timezone_cache = {}