added yaml files for the first five meetings on the wiki page, along with yaml, ical, and setuptools python libraries

This commit is contained in:
gspindles 2014-02-24 22:10:56 -06:00
parent 4ec020f743
commit cf48d522b8
89 changed files with 11921 additions and 0 deletions

View File

@ -0,0 +1,391 @@
Changelog
=========
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
<msmith@fluendo.com>)
- 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.

View File

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

View File

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

View File

@ -0,0 +1,4 @@
include *.rst tox.ini
graft docs
recursive-include src/icalendar *
recursive-exclude src/icalendar *.pyc

View File

@ -0,0 +1,534 @@
Metadata-Version: 1.1
Name: icalendar
Version: 3.6.1
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 parser/generator of iCalendar files for use
with Python.
----
:Homepage: http://icalendar.readthedocs.org
:Code: http://github.com/collective/icalendar
:Mailing list: http://github.com/collective/icalendar/issues
:Dependencies: `setuptools`_ and since version 3.0 we depend on `pytz`_.
:Compatible with: Python 2.6, 2.7 and 3.3+
:License: `BSD`_
----
Roadmap
=======
- 3.6: Python 3 support (current version)
- 4.0: API refactoring
Changes in version 3.0
======================
API Change
----------
Since version we unified to icalendar de/serialization API to use only to_ical
(for writing an ical string from the internal representation) and from_ical
(for parsing an ical string into the internal representation).
to_ical is now used instead of the methods ical, string, as_string and instead
of string casting via __str__ and str.
from_ical is now used instead of from_string.
This change is a requirement for future Python 3 compatibility. Please update
your code to reflect to the new API.
Timezone support
----------------
Timezones are now fully supported in icalendar for serialization and
deserialization. We use the pytz library for timezone components of datetime
instances. The timezone identifiers must be valid pytz respectively Olson
database timezone identifiers. This can be a problem for 'GMT' identifiers,
which are not defined in the Olson database.
Instead of the own UTC tzinfo implementation we use pytz UTC tzinfo object now.
About this fork which is not a fork anymore
===========================================
Aim of this fork (not fork anymore, read further) was to bring this package up
to date with latest icalendar `RFC`_ specification as part of
`plone.app.event`_ project which goal is to bring recurrent evens to `Plone`_.
After some thoughts we (Plone developers involved with `plone.app.event`_) send
a suggestion to icalendar-dev@codespeak.net to take over mainaining of
`icalendar`_. Nobody object and since version 2.2 we are back to development.
.. _`icalendar`: http://pypi.python.org/pypi/icalendar
.. _`plone.app.event`: http://github.com/plone/plone.app.event
.. _`Plone`: http://plone.org
.. _`pytz`: http://pypi.python.org/pypi/pytz
.. _`setuptools`: http://pypi.python.org/pypi/setuptools
.. _`RFC`: http://www.ietf.org/rfc/rfc5545.txt
.. _`BSD`: https://github.com/collective/icalendar/issues/2
Test Coverage Report
====================
Output from coverage test::
Name Stmts Miss Cover
----------------------------------------------------------------------------------
.tox/py27/lib/python2.7/site-packages/icalendar/__init__ 5 0 100%
.tox/py27/lib/python2.7/site-packages/icalendar/cal 234 7 97%
.tox/py27/lib/python2.7/site-packages/icalendar/caselessdict 55 5 91%
.tox/py27/lib/python2.7/site-packages/icalendar/compat 1 0 100%
.tox/py27/lib/python2.7/site-packages/icalendar/parser 189 6 97%
.tox/py27/lib/python2.7/site-packages/icalendar/parser_tools 20 0 100%
.tox/py27/lib/python2.7/site-packages/icalendar/prop 533 62 88%
.tox/py27/lib/python2.7/site-packages/icalendar/tools 16 0 100%
----------------------------------------------------------------------------------
TOTAL 1053 80 92%
Changelog
=========
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
<msmith@fluendo.com>)
- 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: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.3
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent

View File

@ -0,0 +1,96 @@
==========================================================
Internet Calendaring and Scheduling (iCalendar) for Python
==========================================================
The `icalendar`_ package is a parser/generator of iCalendar files for use
with Python.
----
:Homepage: http://icalendar.readthedocs.org
:Code: http://github.com/collective/icalendar
:Mailing list: http://github.com/collective/icalendar/issues
:Dependencies: `setuptools`_ and since version 3.0 we depend on `pytz`_.
:Compatible with: Python 2.6, 2.7 and 3.3+
:License: `BSD`_
----
Roadmap
=======
- 3.6: Python 3 support (current version)
- 4.0: API refactoring
Changes in version 3.0
======================
API Change
----------
Since version we unified to icalendar de/serialization API to use only to_ical
(for writing an ical string from the internal representation) and from_ical
(for parsing an ical string into the internal representation).
to_ical is now used instead of the methods ical, string, as_string and instead
of string casting via __str__ and str.
from_ical is now used instead of from_string.
This change is a requirement for future Python 3 compatibility. Please update
your code to reflect to the new API.
Timezone support
----------------
Timezones are now fully supported in icalendar for serialization and
deserialization. We use the pytz library for timezone components of datetime
instances. The timezone identifiers must be valid pytz respectively Olson
database timezone identifiers. This can be a problem for 'GMT' identifiers,
which are not defined in the Olson database.
Instead of the own UTC tzinfo implementation we use pytz UTC tzinfo object now.
About this fork which is not a fork anymore
===========================================
Aim of this fork (not fork anymore, read further) was to bring this package up
to date with latest icalendar `RFC`_ specification as part of
`plone.app.event`_ project which goal is to bring recurrent evens to `Plone`_.
After some thoughts we (Plone developers involved with `plone.app.event`_) send
a suggestion to icalendar-dev@codespeak.net to take over mainaining of
`icalendar`_. Nobody object and since version 2.2 we are back to development.
.. _`icalendar`: http://pypi.python.org/pypi/icalendar
.. _`plone.app.event`: http://github.com/plone/plone.app.event
.. _`Plone`: http://plone.org
.. _`pytz`: http://pypi.python.org/pypi/pytz
.. _`setuptools`: http://pypi.python.org/pypi/setuptools
.. _`RFC`: http://www.ietf.org/rfc/rfc5545.txt
.. _`BSD`: https://github.com/collective/icalendar/issues/2
Test Coverage Report
====================
Output from coverage test::
Name Stmts Miss Cover
----------------------------------------------------------------------------------
.tox/py27/lib/python2.7/site-packages/icalendar/__init__ 5 0 100%
.tox/py27/lib/python2.7/site-packages/icalendar/cal 234 7 97%
.tox/py27/lib/python2.7/site-packages/icalendar/caselessdict 55 5 91%
.tox/py27/lib/python2.7/site-packages/icalendar/compat 1 0 100%
.tox/py27/lib/python2.7/site-packages/icalendar/parser 189 6 97%
.tox/py27/lib/python2.7/site-packages/icalendar/parser_tools 20 0 100%
.tox/py27/lib/python2.7/site-packages/icalendar/prop 533 62 88%
.tox/py27/lib/python2.7/site-packages/icalendar/tools 16 0 100%
----------------------------------------------------------------------------------
TOTAL 1053 80 92%

View File

@ -0,0 +1,26 @@
TODO
====
- Update docs.
- Add a __add__ method to cal.Component, so that ``cal[key] = val`` works as
expected. Currently, the value is added as is, but not converted to the
correct subcomponent, as specified in prop.TypesFactory. See also the NOTE
in: icalendar.tests.example.rst, Components, line 82.
- Eventually implement a ``decoded`` method for all icalendar.prop properties,
so that cal.decoded doesn't call the from_ical methods but decode it into
realy python natives. We want from_ical encode a ical string into a
icalendar.prop instance, so decoding into a python native seems not to be
appropriate there. (but the vDDD-types are encoded into python natives, so
there is an inconsistence...)
OLD TODO's
==========
- Check and Fix VTIMEZONE component functionality and creating VTIMEZONE
components from tzinfo instances.
- Automatic encoding and decoding of parameter values. Most of the
work is done already. Just need to get it finished. Look at line 153
in 'src/icalendar/parser.py'

View File

@ -0,0 +1,59 @@
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,
)
__all__ = [
Calendar, Event, Todo, Journal,
FreeBusy, Alarm, ComponentFactory,
Timezone, TimezoneStandard, TimezoneDaylight,
vBinary, vBoolean, vCalAddress, vDatetime, vDate,
vDDDTypes, vDuration, vFloat, vInt, vPeriod,
vWeekday, vFrequency, vRecur, vText, vTime, vUri,
vGeo, vUTCOffset, TypesFactory,
FixedOffset, LocalTimezone,
Parameters, q_split, q_join,
]

View File

@ -0,0 +1,493 @@
# -*- 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
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.parser_tools import data_encode
from icalendar.prop import TypesFactory
from icalendar.prop import vText, vDDDLists
import pytz
######################################
# 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.
"""
CaselessDict.__init__(self, *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(
[(cat, 1) for cat in ('CATEGORIES', 'RESOURCES', 'FREEBUSY')]
)
_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 = '' # must 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.
"""
CaselessDict.__init__(self, *args, **kwargs)
# set parameters here for properties that use non-default values
self.subcomponents = [] # Components can be nested.
self.is_broken = False # True if we ignored an exception while
# parsing a property
#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
#############################
# 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']:
# 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.
#if name == 'attendee': import pdb; pdb.set_trace()
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 not name is None:
name = name.upper()
return self._walk(name)
#####################
# Generation
def property_items(self, recursive=True):
"""Returns properties in this component and subcomponents as:
[(name, value), ...]
"""
vText = types_factory['text']
properties = [('BEGIN', vText(self.name).to_ical())]
property_names = self.sorted_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()
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
name, params, vals = line.parts()
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, cls)
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:
if not component.is_broken:
stack[-1].add_component(component)
# we are adding properties to the current top of the stack
else:
factory = types_factory.for_property(name)
component = stack[-1]
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:
if not component.ignore_exceptions:
raise
component.is_broken = True
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 __repr__(self):
return '%s(%s)' % (self.name, data_encode(self))
def content_line(self, name, value):
"""Returns property as content line.
"""
params = getattr(value, 'params', Parameters())
return Contentline.from_parts(name, params, value)
def content_lines(self):
"""Converts the Component and subcomponents into content lines.
"""
contentlines = Contentlines()
for name, value in self.property_items():
cl = self.content_line(name, value)
contentlines.append(cl)
contentlines.append('') # remember the empty string in the end
return contentlines
def to_ical(self):
content_lines = self.content_lines()
return content_lines.to_ical()
#######################################
# components defined in RFC 2445
class Event(Component):
name = 'VEVENT'
canonical_order = (
'SUMMARY', 'DTSTART', 'DTEND', 'DURATION', 'DTSTAMP',
'UID', 'RECURRENCE-ID', 'SEQUENCE',
'RRULE' 'EXRULE', 'RDATE', 'EXDATE',
)
required = ('UID',)
singletons = (
'CLASS', 'CREATED', 'DESCRIPTION', 'DTSTART', 'GEO', 'LAST-MODIFIED',
'LOCATION', 'ORGANIZER', 'PRIORITY', 'DTSTAMP', 'SEQUENCE', 'STATUS',
'SUMMARY', 'TRANSP', 'URL', 'RECURRENCE-ID', 'DTEND', 'DURATION',
'DTSTART',
)
exclusive = ('DTEND', 'DURATION', )
multiple = (
'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE',
'EXRULE', 'RSTATUS', 'RELATED', 'RESOURCES', 'RDATE', 'RRULE'
)
ignore_exceptions = True
class Todo(Component):
name = 'VTODO'
required = ('UID',)
singletons = (
'CLASS', 'COMPLETED', 'CREATED', 'DESCRIPTION', 'DTSTAMP', 'DTSTART',
'GEO', 'LAST-MODIFIED', 'LOCATION', 'ORGANIZER', 'PERCENT', 'PRIORITY',
'RECURRENCE-ID', 'SEQUENCE', 'STATUS', 'SUMMARY', 'UID', 'URL', 'DUE',
'DURATION',
)
exclusive = ('DUE', 'DURATION',)
multiple = (
'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE',
'EXRULE', 'RSTATUS', 'RELATED', 'RESOURCES', 'RDATE', 'RRULE'
)
class Journal(Component):
name = 'VJOURNAL'
required = ('UID',)
singletons = (
'CLASS', 'CREATED', 'DESCRIPTION', 'DTSTART', 'DTSTAMP',
'LAST-MODIFIED', 'ORGANIZER', 'RECURRENCE-ID', 'SEQUENCE', 'STATUS',
'SUMMARY', 'UID', 'URL',
)
multiple = (
'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE',
'EXRULE', 'RELATED', 'RDATE', 'RRULE', 'RSTATUS',
)
class FreeBusy(Component):
name = 'VFREEBUSY'
required = ('UID',)
singletons = (
'CONTACT', 'DTSTART', 'DTEND', 'DURATION', 'DTSTAMP', 'ORGANIZER',
'UID', 'URL',
)
multiple = ('ATTENDEE', 'COMMENT', 'FREEBUSY', 'RSTATUS',)
class Timezone(Component):
name = 'VTIMEZONE'
canonical_order = ('TZID', 'STANDARD', 'DAYLIGHT',)
required = ('TZID', 'STANDARD', 'DAYLIGHT',)
singletons = ('TZID', 'LAST-MODIFIED', 'TZURL',)
class TimezoneStandard(Component):
name = 'STANDARD'
required = ('DTSTART', 'TZOFFSETTO', 'TZOFFSETFROM')
singletons = ('DTSTART', 'TZOFFSETTO', 'TZOFFSETFROM', 'RRULE')
multiple = ('COMMENT', 'RDATE', 'TZNAME')
class TimezoneDaylight(Component):
name = 'DAYLIGHT'
required = ('DTSTART', 'TZOFFSETTO', 'TZOFFSETFROM')
singletons = ('DTSTART', 'TZOFFSETTO', 'TZOFFSETFROM', 'RRULE')
multiple = ('COMMENT', 'RDATE', 'TZNAME')
class Alarm(Component):
name = 'VALARM'
# not quite sure about these ...
required = ('ACTION', 'TRIGGER',)
singletons = ('ATTACH', 'ACTION', 'TRIGGER', 'DURATION', 'REPEAT',)
inclusive = (('DURATION', 'REPEAT',),)
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', )
multiple = ('calscale', 'method', )
# These are read only singleton, so one instance is enough for the module
types_factory = TypesFactory()
component_factory = ComponentFactory()

View File

@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
from icalendar.parser_tools import to_unicode
from icalendar.parser_tools import data_encode
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 = dict((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(dict):
"""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.
"""
dict.__init__(self, *args, **kwargs)
for key, value in self.items():
key_upper = to_unicode(key).upper()
if key != key_upper:
dict.__delitem__(self, key)
self[key_upper] = value
def __getitem__(self, key):
key = to_unicode(key)
return dict.__getitem__(self, key.upper())
def __setitem__(self, key, value):
key = to_unicode(key)
dict.__setitem__(self, key.upper(), value)
def __delitem__(self, key):
key = to_unicode(key)
dict.__delitem__(self, key.upper())
def __contains__(self, key):
key = to_unicode(key)
return dict.__contains__(self, key.upper())
def get(self, key, default=None):
key = to_unicode(key)
return dict.get(self, key.upper(), default)
def setdefault(self, key, value=None):
key = to_unicode(key)
return dict.setdefault(self, key.upper(), value)
def pop(self, key, default=None):
key = to_unicode(key)
return dict.pop(self, key.upper(), default)
def popitem(self):
return dict.popitem(self)
def has_key(self, key):
key = to_unicode(key)
return dict.__contains__(self, key.upper())
def update(self, indict):
# Multiple keys where key1.upper() == key2.upper() will be lost.
for key, value in indict.items(): # TODO optimize in python 2
self[key] = value
def copy(self):
return CaselessDict(dict.copy(self))
def __repr__(self):
return 'CaselessDict(%s)' % data_encode(self)
# 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)

View File

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

View File

@ -0,0 +1,371 @@
# -*- 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 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 data_encode
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(u'\\N', u'\\n')\
.replace(u'\r\n', u'\n')\
.replace(u'\\n', u'\n')\
.replace(u'\\,', u',')\
.replace(u'\\;', u';')\
.replace(u'\\\\', u'\\')
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=u'\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 u'\n' not in line
ret_line = u''
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_line += fold_sep
byte_count = char_byte_len
ret_line += char
return ret_line
#################################################################
# 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
NAME = re.compile('[\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(u'(\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=','):
"""Splits a string on char, taking double (q)uotes into considderation.
"""
result = []
cursor = 0
length = len(st)
inquote = 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
if i + 1 == length:
result.append(st[cursor:])
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 __repr__(self):
return 'Parameters(%s)' % data_encode(self)
def to_ical(self):
result = []
items = self.items()
for key, value in sorted(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, '=')
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 unsescape_string(val):
return val.replace('%2C', ',').replace('%3A', ':')\
.replace('%3B', ';').replace('%5C', '\\')
#########################################
# 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 u'\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):
"""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())
return cls(u'%s;%s:%s' % (name, params, values))
return cls(u'%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 = unsescape_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(
(unsescape_string(key), unsescape_string(value))
for key, value in compat.iteritems(params)
)
values = unsescape_string(st[value_split + 1:])
return (name, params, values)
except ValueError as exc:
raise ValueError(
u"Content line could not be parsed into parts: %r: %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 unfolded.splitlines() 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

View File

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

View File

@ -0,0 +1,993 @@
# -*- 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 has 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 starts 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 allways 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
from dateutil.tz import tzutc
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
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('(?P<signal>[+-]?)(?P<relative>[\d]?)'
'(?P<weekday>[\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 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)):
raise ValueError('You must use datetime, date, timedelta or time')
if isinstance(dt, datetime):
self.params = Parameters(dict(value='DATE-TIME'))
elif isinstance(dt, date):
self.params = Parameters(dict(value='DATE'))
elif isinstance(dt, time):
self.params = Parameters(dict(value='TIME'))
if (isinstance(dt, datetime) or isinstance(dt, time))\
and getattr(dt, 'tzinfo', False):
tzinfo = dt.tzinfo
if tzinfo is not pytz.utc and 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()
else:
raise ValueError('Unknown date type')
@classmethod
def from_ical(cls, ical, timezone=None):
if isinstance(ical, cls):
return ical.dt
u = ical.upper()
if u.startswith('-P') or u.startswith('P'):
return vDuration.from_ical(ical)
try:
return vDatetime.from_ical(ical, timezone=timezone)
except ValueError:
try:
return vDate.from_ical(ical)
except ValueError:
return vTime.from_ical(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(dict(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:
pass
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 datetime(tzinfo=pytz.utc, *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 not weekday 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 not self 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,
'BYMONTHDAY': vInt,
'BYYEARDAY': vInt,
'BYMONTH': vInt,
'UNTIL': vDDDTypes,
'BYSETPOS': vInt,
'WKST': vWeekday,
'BYDAY': vWeekday,
'FREQ': vFrequency,
})
def __init__(self, *args, **kwargs):
CaselessDict.__init__(self, *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(';'):
key, vals = pairs.split('=')
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(dict(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.
"""
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 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"
CaselessDict.__init__(self, *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
)
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
#################################################
# 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': 'text',
'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

View File

@ -0,0 +1,5 @@
# unittest/unittest2 importer
import unittest
if not hasattr(unittest.TestCase, 'assertIsNotNone'):
import unittest2 as unittest
unittest # pep 8

View File

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

View File

@ -0,0 +1,48 @@
BEGIN: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

View File

@ -0,0 +1,41 @@
BEGIN:VEVENT
DTSTART:20130927T130000Z
DTEND:20130927T140000Z
DTSTAMP:20131107T004757Z
ORGANIZER;CN=gxxxxxxxn@nxx.fr:mailto:gxxxxxn@nxx.fr
UID:040000008200E00074C5B7101A82E00800000000A0F3321606B6CE01000000000000000
010000000F09F33F0E8ED4C44B99F6027ACF588D0
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=St
eve Bxxxxxx;X-NUM-GUESTS=0:mailto:sxxxxxt@nxx.fr
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Boris
Hxxxxx;X-NUM-GUESTS=0:mailto:bxxxxxxk@vxxxxxxxx.com
CREATED:20130920T113409Z
DESCRIPTION:Quand : vendredi 27 septembre 2013 15:00-16:00 (UTC+01:00) Brux
elles\, Copenhague\, Madrid\, Paris.\nEmplacement : Conf-Call - 01 xx xx xx
xx\n\nRemarque : le décalage GMT ci-dessus ne tient pas compte des réglage
s de l'heure d'été.\n\n*~*~*~*~*~*~*~*~*~*\n\nComme convenu à linstant par
e-mail\n
LAST-MODIFIED:20130920T115104Z
LOCATION:Conf-Call - 01 xx xx xx xx
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Nxx - Réunion lancement PxxxxxxT
TRANSP:OPAQUE
X-ALT-DESC;FMTTYPE=text/html:<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//E
N">\n<HTML>\n<HEAD>\n<META NAME="Generator" CONTENT="MS Exchange Server ver
sion 08.00.0681.000">\n<TITLE></TITLE>\n</HEAD>\n<BODY>\n<!-- Converted fro
m text/rtf format -->\n\n<P DIR=LTR><SPAN LANG="fr"><FONT FACE="Calibri">Qu
and : vendredi 27 septembre 2013 15:00-16:00 (UTC+01:00) Bruxelles\, Copenh
ague\, Madrid\, Paris.</FONT></SPAN></P>\n\n<P DIR=LTR><SPAN LANG="fr"><FON
T FACE="Calibri">Emplacement : Conf-Call - 01 xx xx xx xx</FONT></SPAN></P>
\n\n<P DIR=LTR><SPAN LANG="fr"><FONT FACE="Calibri">Remarque : le décalage
GMT ci-dessus ne tient pas compte des réglages de l'heure d'été.</FONT></SP
AN></P>\n\n<P DIR=LTR><SPAN LANG="fr"><FONT FACE="Calibri">*~*~*~*~*~*~*~*~
*~*</FONT></SPAN></P>\n\n<P DIR=LTR><SPAN LANG="fr"><FONT FACE="Calibri">Co
mme convenu à linstant par e-mail</FONT></SPAN><SPAN LANG="fr"></SPAN></P>
\n\n</BODY>\n</HTML>
X-MICROSOFT-CDO-BUSYSTATUS:TENTATIVE
X-MICROSOFT-CDO-IMPORTANCE:1
X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY
X
END:VEVENT

View File

@ -0,0 +1,78 @@
BEGIN: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

View File

@ -0,0 +1,80 @@
BEGIN: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

View File

@ -0,0 +1,24 @@
BEGIN: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

View File

@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
from icalendar.tests 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__)
data = open(os.path.join(directory, 'encoding.ics'), 'rb').read()
cal = icalendar.Calendar.from_ical(data)
self.assertEqual(cal['prodid'].to_ical().decode('utf-8'),
u"-//Plönë.org//NONSGML plone.app.event//EN")
self.assertEqual(cal['X-WR-CALDESC'].to_ical().decode('utf-8'),
u"test non ascii: äöü ÄÖÜ €")
event = cal.walk('VEVENT')[0]
self.assertEqual(event['SUMMARY'].to_ical().decode('utf-8'),
u'Non-ASCII Test: ÄÖÜ äöü €')
self.assertEqual(
event['DESCRIPTION'].to_ical().decode('utf-8'),
u'icalendar should be able to handle non-ascii: €äüöÄÜÖ.'
)
self.assertEqual(event['LOCATION'].to_ical().decode('utf-8'),
u'Tribstrül')
def test_create_to_ical(self):
cal = icalendar.Calendar()
cal.add('prodid', u"-//Plönë.org//NONSGML plone.app.event//EN")
cal.add('version', u"2.0")
cal.add('x-wr-calname', u"äöü ÄÖÜ €")
cal.add('x-wr-caldesc', u"test non ascii: äöü ÄÖÜ €")
cal.add('x-wr-relcalid', u"12345")
event = icalendar.Event()
event.add(
'dtstart',
datetime.datetime(2010, 10, 10, 10, 00, 00, tzinfo=pytz.utc)
)
event.add(
'dtend',
datetime.datetime(2010, 10, 10, 12, 00, 00, tzinfo=pytz.utc)
)
event.add(
'created',
datetime.datetime(2010, 10, 10, 0, 0, 0, tzinfo=pytz.utc)
)
event.add('uid', u'123456')
event.add('summary', u'Non-ASCII Test: ÄÖÜ äöü €')
event.add(
'description',
u'icalendar should be able to de/serialize non-ascii.'
)
event.add('location', u'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",
datetime.datetime(2010, 10, 10, 0, 0, 0, tzinfo=pytz.utc)
)
event.add("summary", u"åäö")
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(u'DESCRIPTION', u'äöüßÄÖÜ')
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'
)

View File

@ -0,0 +1,247 @@
# -*- coding: utf-8 -*-
from icalendar.parser_tools import to_unicode
from icalendar.tests 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", u"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", u"åäö")
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 = """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, u'acme, ädmin')
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_114(self):
"""Issue #114/#115 - invalid line in event breaks the parser
https://github.com/collective/icalendar/issues/114
"""
directory = os.path.dirname(__file__)
ics = open(os.path.join(directory, 'issue_114_invalid_line.ics'), 'rb')
with self.assertRaises(ValueError):
cal = icalendar.Calendar.from_ical(ics.read())
cal # pep 8
ics.close()
def test_issue_116(self):
"""Issue #116/#117 - How to add 'X-APPLE-STRUCTURED-LOCATION'
"""
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()
)

View File

@ -0,0 +1,253 @@
# coding: utf-8
from icalendar.tests 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),
u'SUMMARY:INternational char æ ø å'
)
# A value can also be unicode
parts = ('SUMMARY', Parameters(), vText(u'INternational char æ ø å'))
self.assertEqual(
Contentline.from_parts(*parts),
u'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')
)
def test_fold_line(self):
from ..parser import foldline
self.assertEqual(foldline(u'foo'), u'foo')
self.assertEqual(
foldline(u"Lorem ipsum dolor sit amet, consectetur adipiscing "
u"elit. Vestibulum convallis imperdiet dui posuere."),
(u'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '
u'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(u'привет'.encode('utf-8'), limit=3)
self.assertEqual(foldline(u'foobar', limit=4), u'foo\r\n bar')
self.assertEqual(
foldline(u'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
u'. Vestibulum convallis imperdiet dui posuere.'),
(u'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
u' Vestibulum conval\r\n lis imperdiet dui posuere.')
)
self.assertEqual(
foldline(u'DESCRIPTION:АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭЮЯ'),
u'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_join(self):
from ..parser import q_join
self.assertEqual(q_join(['Max', 'Moller', 'Rasmussen, Max']),
'Max,Moller,"Rasmussen, Max"')

View File

@ -0,0 +1,28 @@
from icalendar import Calendar
from icalendar.prop import vText
from icalendar.tests import unittest
import os
class TestMultiple(unittest.TestCase):
"""A example with multiple VCALENDAR components"""
def test_multiple(self):
directory = os.path.dirname(__file__)
cals = Calendar.from_ical(
open(os.path.join(directory, 'multiple.ics'), 'rb').read(),
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')
)

View File

@ -0,0 +1,207 @@
# coding: utf-8
from icalendar import Calendar
from icalendar import Event
from icalendar import Parameters
from icalendar import vCalAddress
from icalendar.tests import unittest
import icalendar
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'),
u'BEGIN:VEVENT\r\n'
u'ORGANIZER;CN="Джон Доу":mailto:john.doe@example.org\r\n'
u'END:VEVENT\r\n'
)
self.assertEqual(vevent['ORGANIZER'].params['CN'],
'Джон Доу')
def test_quoting(self):
# not double-quoted
self._test_quoting(u"Aramis", u'Aramis')
# if a space is present - enclose in double quotes
self._test_quoting(u"Aramis Alameda", u'"Aramis Alameda"')
# a single quote in parameter value - double quote the value
self._test_quoting(u"Aramis d'Alameda", u'"Aramis d\'Alameda"')
# double quote is replaced with single quote
self._test_quoting(u"Aramis d\"Alameda", u'"Aramis d\'Alameda"')
self._test_quoting(u"Арамис д'Аламеда", u'"Арамис д\'Аламеда"')
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 = u',\\;:'
for char in NON_SAFE_CHARS:
cn_escaped = u"Society\\%s 2014" % char
cn_decoded = u"Society%s 2014" % char
vevent = Event.from_ical(
u'BEGIN:VEVENT\r\n'
u'ORGANIZER;CN=%s:that\r\n'
u'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'),
u'это, то; 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'], u'RembrandXS')

View File

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
from icalendar.caselessdict import CaselessDict
from icalendar.tests import unittest
import datetime
import icalendar
import os
import pytz
class TestRecurrence(unittest.TestCase):
def setUp(self):
directory = os.path.dirname(__file__)
self.cal = icalendar.Calendar.from_ical(
open(os.path.join(directory, 'recurrence.ics'), 'rb').read()
)
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,
datetime.datetime(1996, 4, 2, 1, 0, tzinfo=pytz.utc)
)
self.assertEqual(
first_event['exdate'].dts[1].dt,
datetime.datetime(1996, 4, 3, 1, 0, tzinfo=pytz.utc)
)
self.assertEqual(
first_event['exdate'].dts[2].dt,
datetime.datetime(1996, 4, 4, 1, 0, tzinfo=pytz.utc)
)
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!

View File

@ -0,0 +1,29 @@
from icalendar.tests 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())

View File

@ -0,0 +1,141 @@
# -*- coding: utf-8 -*-
from icalendar.tests 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__)
cal = icalendar.Calendar.from_ical(
open(os.path.join(directory, 'timezoned.ics'), 'rb').read()
)
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'),
datetime.datetime(2012, 2, 13, 10, 0, 0,
tzinfo=pytz.timezone('Europe/Vienna')))
self.assertEqual(
ev1.decoded('DTSTAMP'),
datetime.datetime(2010, 10, 10, 9, 10, 10, tzinfo=pytz.utc))
def test_create_to_ical(self):
cal = icalendar.Calendar()
cal.add('prodid', u"-//Plone.org//NONSGML plone.app.event//EN")
cal.add('version', u"2.0")
cal.add('x-wr-calname', u"test create calendar")
cal.add('x-wr-caldesc', u"icalendar tests")
cal.add('x-wr-relcalid', u"12345")
cal.add('x-wr-timezone', u"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',
datetime.datetime(2012, 2, 13, 10, 00, 00, tzinfo=tz))
event.add(
'dtend',
datetime.datetime(2012, 2, 17, 18, 00, 00, tzinfo=tz))
event.add(
'dtstamp',
datetime.datetime(2010, 10, 10, 10, 10, 10, tzinfo=tz))
event.add(
'created',
datetime.datetime(2010, 10, 10, 10, 10, 10, tzinfo=tz))
event.add('uid', u'123456')
event.add(
'last-modified',
datetime.datetime(2010, 10, 10, 10, 10, 10, tzinfo=tz))
event.add('summary', u'artsprint 2012')
# event.add('rrule', u'FREQ=YEARLY;INTERVAL=1;COUNT=10')
event.add('description', u'sprinting at the artsprint')
event.add('location', u'aka bild, wien')
event.add('categories', u'first subject')
event.add('categories', u'second subject')
event.add('attendee', u'häns')
event.add('attendee', u'franz')
event.add('attendee', u'sepp')
event.add('contact', u'Max Mustermann, 1010 Wien')
event.add('url', u'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:20101010T091010Z" in test_out)
self.assertTrue("CREATED;VALUE=DATE-TIME:20101010T091010Z" 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__ == 'dateutil.tz')
self.assertTrue(date2.tzinfo.__module__ == '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')

View File

@ -0,0 +1,349 @@
from datetime import datetime
from datetime import timedelta
from icalendar.tests import unittest
import icalendar
import pytz
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'
# 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.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(u'-//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'] = u'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),
[u'Chair', u'Table', u'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, crated, dtstamp and last-modified must be in UTC.
Component = icalendar.cal.Component
comp = Component()
comp.add('dtstart', datetime(2010, 10, 10, 10, 0, 0,
tzinfo=pytz.timezone("Europe/Vienna")))
comp.add('created', datetime(2010, 10, 10, 12, 0, 0))
comp.add('dtstamp', datetime(2010, 10, 10, 14, 0, 0,
tzinfo=pytz.timezone("Europe/Vienna")))
comp.add('last-modified', datetime(2010, 10, 10, 16, 0, 0,
tzinfo=pytz.utc))
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:20101010T130000Z" 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'], [u'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)
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()
open(os.path.join(directory, 'test.ics'), 'wb').write(cal.to_ical())
# Parsing a complete calendar from a string will silently ignore bogus
# events. The bogosity 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:Bogus 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'])

View File

@ -0,0 +1,90 @@
from icalendar.tests 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 = dict(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(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'])

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from icalendar.parser_tools import data_encode
from icalendar.parser_tools import to_unicode
from icalendar.tests import unittest
class TestParserTools(unittest.TestCase):
def test_parser_tools_to_unicode(self):
self.assertEqual(to_unicode('spam'), u'spam')
self.assertEqual(to_unicode(u'spam'), u'spam')
self.assertEqual(to_unicode(u'spam'.encode('utf-8')), u'spam')
self.assertEqual(to_unicode(b'\xc6\xb5'), u'\u01b5')
self.assertEqual(to_unicode(u'\xc6\xb5'.encode('iso-8859-1')),
u'\u01b5')
self.assertEqual(to_unicode(b'\xc6\xb5', encoding='ascii'), u'\u01b5')
self.assertEqual(to_unicode(1), 1)
self.assertEqual(to_unicode(None), None)
def test_parser_tools_data_encode(self):
data1 = {
u'k1': u'v1', 'k2': 'v2', u'k3': u'v3',
'li1': ['it1', u'it2', {'k4': u'v4', u'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)

View File

@ -0,0 +1,497 @@
# -*- coding: utf-8 -*-
from datetime import date
from datetime import datetime
from datetime import time
from datetime import timedelta
from icalendar.parser import Parameters
from icalendar.tests import unittest
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'),
datetime(2001, 1, 1, 12, 30, tzinfo=pytz.utc))
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 = datetime(2001, 1, 1, 12, 30, 0, tzinfo=pytz.utc)
self.assertEqual(vDatetime(dutc).to_ical(), b'20010101T123000Z')
dutc = datetime(1899, 1, 1, 12, 30, 0, tzinfo=pytz.utc)
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 = datetime(2000, 1, 1, tzinfo=dk)
end = datetime(2000, 1, 2, tzinfo=dk)
per = (start, end)
self.assertEqual(vPeriod(per).to_ical(),
b'20000101T000000/20000102T000000')
self.assertEqual(vPeriod(per).params['TZID'],
'Europe/Copenhagen')
p = vPeriod((datetime(2000, 1, 1, tzinfo=dk), 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(u'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(u'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'),
u'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'), u'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', u'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', u'Rasmussen, Max M\xfcller'),
b'Rasmussen\\, Max M\xc3\xbcller')
self.assertEqual(
factory.from_ical('cn', b'Rasmussen\\, Max M\xc3\xb8ller'),
u'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)

View File

@ -0,0 +1,28 @@
from icalendar.tests 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)

View File

@ -0,0 +1,3 @@
BEGIN:VCALENDAR
X-SOMETIME;VALUE=TIME:172010
END:VCALENDAR

View File

@ -0,0 +1,36 @@
BEGIN: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

View File

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

Binary file not shown.

View File

@ -0,0 +1,130 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
help:
@echo "Please use \`make <target>' where <target> 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 " 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."
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."
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."

View File

@ -0,0 +1,14 @@
{#
sphinxdoc/layout.html
~~~~~~~~~~~~~~~~~~~~~
Sphinx layout template for the sphinxdoc theme.
:copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
#}
{% extends "basic/layout.html" %}
{# put the sidebar before the body #}
{% block sidebar1 %}{{ sidebar() }}{% endblock %}
{% block sidebar2 %}{% endblock %}

View File

@ -0,0 +1,428 @@
/*
* sphinxdoc.css_t
* ~~~~~~~~~~~~~~~
*
* Sphinx stylesheet -- sphinxdoc theme. Originally created by
* Armin Ronacher for Werkzeug.
*
* :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
@import url("basic.css");
/* -- page layout ----------------------------------------------------------- */
body {
font-family: Arial, sans-serif, serif;
font-size: 15px;
letter-spacing: -0.05em;
line-height: 1.3em;
background-color: #F5F4EF;
padding: 0;
margin: 0 auto;
width: 950px;
-moz-box-shadow: 0px 0px 15px #bbb;
-webkit-box-shadow: 0px 0px 15px #bbb;
box-shadow: 0px 0px 15px #bbb;
}
div.document {
background-color: white;
text-align: left;
}
div.bodywrapper {
margin: 0 240px 0 0;
border-right: 1px solid #ccc;
}
div.body {
margin: 0;
padding: 0.5em 20px 20px 20px;
}
/*Banner*/
#banner {
height: 100px;
background: #fff;
}
#vlinux-logo {
margin-top: 10px;
margin-left: 10px;
}
#nav {
background: #48443d url('menu_bg.png') repeat-x;
height: 30px;
}
#nav ul {
list-style: none;
padding: 0;
margin: 0;
}
#nav li {
display: inline;
float: left;
margin: 0;
}
#nav a:link, #nav a:visited {
color: #fff;
display: inline-block;
padding: 5px 20px;
height: 20px;
text-decoration: none;
font-size: 0.8em;
border-bottom: none;
}
#nav a:hover, #nav a:active, #nav .active a:link, #nav .active a:visited {
background: #89c912;
color: #fff;
text-shadow: #666 2px 2px 2px;
}
div.related {
font-size: 0.8em;
}
div.related ul {
/*background-image: url(navigation.png);*/
background: #fafafa;
height: 2em;
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
}
div.related ul li {
margin: 0;
padding: 0;
height: 2em;
float: left;
}
div.related ul li.right {
float: right;
margin-right: 5px;
}
div.related ul li a {
margin: 0;
padding: 0 5px 0 5px;
line-height: 1.75em;
color: #48443d;
border-bottom: #48443d 1px dotted;
}
div.related ul li a:hover {
color: #89c912;
text-decoration: none;
border-bottom: #89c912 1px dotted;
}
div.sphinxsidebarwrapper {
padding: 0;
}
div.sphinxsidebar {
margin: 0;
padding: 0.5em 15px 15px 0;
width: 210px;
float: right;
font-size: 0.8em;
text-align: left;
}
div.sphinxsidebar h3, div.sphinxsidebar h4 {
margin: 1em 0;
font-size: 1.8em;
line-height: 1.3em;
padding: 0.2em 0.5em;
color: white;
/*border: 1px solid #86989B;*/
background-color: #000000;
}
div.sphinxsidebar h3 a {
color: white;
}
div.sphinxsidebar ul {
padding-left: 1.5em;
margin-top: 7px;
padding: 0;
line-height: 130%;
font-size: 14px;
margin-left: 1em;
}
div.sphinxsidebar ul ul {
margin-left: 20px;
}
div.footer {
display: none;
background-color: #FAFAFA;
border-bottom: 1px solid #DDDDDD;
border-top: 1px solid #DDDDDD;
clear: both;
height: 2em;
line-height: 1.75em;
padding: 0;
text-align: center;
}
div.footer a,
div.footer a:hover {
border-bottom: 1px dotted #48443D;
text-decoration: none;
}
/* -- body styles ----------------------------------------------------------- */
p {
margin: 0.8em 0 0.5em 0;
}
a {
color: #48443d;
text-decoration: none;
border-bottom: #48443d 1px dotted;
font-weight: bold;
}
a:hover {
color: #89c912;
text-decoration: none;
border-bottom: #89c912 1px dotted;
}
/*div.body a {
text-decoration: underline;
}*/
h1 {
margin: 0;
padding: 0.7em 0 0.3em 0;
font-size: 2.5em;
line-height: 1em;
color: #48443d;
}
h2 {
margin: 1.3em 0 0.2em 0;
font-size: 2em;
line-height: 0.8em;
padding: 0;
color: #48443d;
}
h3 {
margin: 1em 0 -0.3em 0;
font-size: 1.5em;
color: #48443d;
}
div.body h1 a, div.body h2 a, div.body h3 a, div.body h4 a, div.body h5 a, div.body h6 a {
color: #48443d;
}
h1 a.anchor, h2 a.anchor, h3 a.anchor, h4 a.anchor, h5 a.anchor, h6 a.anchor {
display: none;
margin: 0 0 0 0.3em;
padding: 0 0.2em 0 0.2em;
color: #aaa!important;
}
h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor,
h5:hover a.anchor, h6:hover a.anchor {
display: inline;
}
h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover,
h5 a.anchor:hover, h6 a.anchor:hover {
color: #777;
background-color: #eee;
}
a.headerlink {
color: #c60f0f!important;
font-size: 1em;
margin-left: 6px;
padding: 0 4px 0 4px;
text-decoration: none!important;
}
a.headerlink:hover {
background-color: #ccc;
color: white!important;
}
cite, code, tt {
font-family: 'Consolas', 'Deja Vu Sans Mono',
'Bitstream Vera Sans Mono', monospace;
font-size: 0.95em;
letter-spacing: 0.01em;
}
tt {
background-color: #f2f2f2;
border-bottom: 1px solid #ddd;
color: #333;
}
tt.descname, tt.descclassname, tt.xref {
border: 0;
}
hr {
display: none;
}
.body hr {
border: 1px solid #abc;
margin: 2em;
}
a tt {
border: 0;
color: #CA7900;
}
a tt:hover {
color: #2491CF;
}
pre {
font-family: 'Consolas', 'Deja Vu Sans Mono',
'Bitstream Vera Sans Mono', monospace;
font-size: 0.95em;
letter-spacing: 0.015em;
line-height: 120%;
padding: 0.5em;
border: 1px solid #ccc;
background-color: #f8f8f8;
}
pre a {
color: inherit;
text-decoration: underline;
}
td.linenos pre {
padding: 0.5em 0;
}
div.quotebar {
background-color: #f8f8f8;
max-width: 250px;
float: right;
padding: 2px 7px;
border: 1px solid #ccc;
}
div.topic {
background-color: #f8f8f8;
}
table {
border-collapse: collapse;
margin: 0 -0.5em 0 -0.5em;
}
table td, table th {
padding: 0.2em 0.5em 0.2em 0.5em;
}
div.admonition, div.warning {
font-size: 0.9em;
margin: 1em 0 1em 0;
border: 1px solid #86989B;
background-color: #f7f7f7;
padding: 0;
}
div.admonition p, div.warning p {
margin: 0.5em 1em 0.5em 1em;
padding: 0;
}
div.admonition pre, div.warning pre {
margin: 0.4em 1em 0.4em 1em;
}
div.admonition p.admonition-title,
div.warning p.admonition-title {
margin: 0;
padding: 0.1em 0 0.1em 0.5em;
color: white;
border-bottom: 1px solid #86989B;
font-weight: bold;
background-color: #AFC1C4;
}
div.warning {
border: 1px solid #940000;
}
div.warning p.admonition-title {
background-color: #CF0000;
border-bottom-color: #940000;
}
div.admonition ul, div.admonition ol,
div.warning ul, div.warning ol {
margin: 0.1em 0.5em 0.5em 3em;
padding: 0;
}
div.versioninfo {
margin: 1em 0 0 0;
border: 1px solid #ccc;
background-color: #DDEAF0;
padding: 8px;
line-height: 1.3em;
font-size: 0.9em;
}
.viewcode-back {
font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva',
'Verdana', sans-serif;
}
div.viewcode-block:target {
background-color: #f4debf;
border-top: 1px solid #ac9;
border-bottom: 1px solid #ac9;
}
/* custom */
#twitter a {
border-bottom: none;
}
#twitter a:hover {
border-bottom: none;
}
.twtr-widget {
font-family: font-family: 'Droid Sans', serif !important;
}
#disqus_thread {
padding: 0.5em 20px 20px 20px;
width: 670px;
float: left;
}
p#comments {
padding: 0.5em 20px 20px 20px;
}

View File

@ -0,0 +1,4 @@
[theme]
inherit = basic
stylesheet = icalendar.css
pygments_style = friendly

View File

@ -0,0 +1,14 @@
About
=====
`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

View File

@ -0,0 +1 @@
.. include:: ../CHANGES.rst

View File

@ -0,0 +1,221 @@
# -*- coding: utf-8 -*-
#
# icalendar documentation build configuration file, created by
# sphinx-quickstart on Wed Aug 17 00:40:41 2011.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.viewcode']
# Add any paths that contain templates here, relative to this directory.
# templates_path = []
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'icalendar'
copyright = u'2011, MaxM'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '3.0'
# The full version, including alpha/beta/rc tags.
release = '3.0dev'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'icalendar'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
html_theme_path = ['_themes']
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
#html_static_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
html_sidebars = {
'**': [
'globaltoc.html',
'searchbox.html',
],
}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'icalendardoc'
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'icalendar.tex', u'icalendar Documentation',
u'MaxM', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'icalendar', u'icalendar Documentation',
[u'MaxM'], 1)
]

View File

@ -0,0 +1,37 @@
icalendar contributors
======================
- Johannes Raggam <johannes@raggam.co.at> (Maintainer)
- Max M <maxm@mxm.dk> (Original author)
- Andreas Zeidler <az@zitc.de>
- Andrey Nikolaev <nikolaeff@gmail.com>
- Barak Michener <me@barakmich.com>
- Christophe de Vienne <cdevienne@gmail.com>
- Christian Geier <contact@lostpackets.de>
- Dai MIKURUBE <dmikurube@acm.org>
- Dan Stovall <dbstovall@gmail.com>
- Eric Hanchrow <erich@cozi.com>
- Erik Simmler <tgecho@gmail.com>
- George V. Reilly <george@reilly.org>
- Jannis Leidel <jannis@leidel.info>
- Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen@kolabsys.com>
- Lennart Regebro <lregebro@nuxeo.com>
- Marc Egli <frog32@me.com>
- Martijn Faassen <faassen@infrae.com>
- Martin Melin <git@martinmelin.com>
- Michael Smith <msmith@fluendo.com>
- Mikael Frykholm <mikael@frykholm.com>
- Olivier Grisel <ogrisel@nuxeo.com>
- Pavel Repin <prepin@gmail.com>
- Pedro Ferreira <jose.pedro.ferreira@cern.ch>
- Rembane <andeke@gmail.com>
- Robert Niederreiter <rnix@squarewave.at>
- Rok Garbas <rok@garbas.si>
- Ronan Dunklau <ronan@dunklau.fr>
- Sidnei da Silva <sidnei@enfoldsystems.com>
- Stanislav Ochotnicky <sochotnicky@redhat.com>
- Stefan Schwarzer <sschwarzer@sschwarzer.net>
- Victor Varvaryuk <victor.varvariuc@gmail.com>
- Wichert Akkerman <wichert@wiggy.net>
- spanktar <spanky@kapanka.com>
- tgecho <tgecho@gmail.com>

View File

@ -0,0 +1,16 @@
.. include:: ../README.rst
Contents
========
.. toctree::
:maxdepth: 2
about
install
usage
RFC 5545 <rfc5545/index>
changelog
credits
license

View File

@ -0,0 +1,11 @@
Installing iCalendar
====================
To install the icalendar package, use::
python setup.py install
If installation is successful, you be able to import the iCalendar
package, like this::
>>> import icalendar

View File

@ -0,0 +1 @@
.. include:: ../LICENSE.rst

View File

@ -0,0 +1,324 @@
iCalendar 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. Eg. the datetime format 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=<UTC>)'
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.

View File

@ -0,0 +1,5 @@
[egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0

View File

@ -0,0 +1,53 @@
import codecs
import setuptools
import sys
version = '3.6.1'
shortdesc = 'iCalendar parser/generator'
longdesc = codecs.open('README.rst', encoding='utf-8').read()
longdesc += codecs.open('CHANGES.rst', encoding='utf-8').read()
longdesc += codecs.open('LICENSE.rst', encoding='utf-8').read()
tests_require = []
if sys.version_info[:2] == (2, 6):
# Python unittest2 only needed for Python 2.6
tests_require = ['unittest2']
setuptools.setup(
name='icalendar',
version=version,
description=shortdesc,
long_description=longdesc,
classifiers=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
"Programming Language :: Python",
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3",
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
],
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,
install_requires=[
'setuptools',
'python-dateutil',
'pytz',
],
extras_require={
'test': tests_require
}
)

View File

@ -0,0 +1,534 @@
Metadata-Version: 1.1
Name: icalendar
Version: 3.6.1
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 parser/generator of iCalendar files for use
with Python.
----
:Homepage: http://icalendar.readthedocs.org
:Code: http://github.com/collective/icalendar
:Mailing list: http://github.com/collective/icalendar/issues
:Dependencies: `setuptools`_ and since version 3.0 we depend on `pytz`_.
:Compatible with: Python 2.6, 2.7 and 3.3+
:License: `BSD`_
----
Roadmap
=======
- 3.6: Python 3 support (current version)
- 4.0: API refactoring
Changes in version 3.0
======================
API Change
----------
Since version we unified to icalendar de/serialization API to use only to_ical
(for writing an ical string from the internal representation) and from_ical
(for parsing an ical string into the internal representation).
to_ical is now used instead of the methods ical, string, as_string and instead
of string casting via __str__ and str.
from_ical is now used instead of from_string.
This change is a requirement for future Python 3 compatibility. Please update
your code to reflect to the new API.
Timezone support
----------------
Timezones are now fully supported in icalendar for serialization and
deserialization. We use the pytz library for timezone components of datetime
instances. The timezone identifiers must be valid pytz respectively Olson
database timezone identifiers. This can be a problem for 'GMT' identifiers,
which are not defined in the Olson database.
Instead of the own UTC tzinfo implementation we use pytz UTC tzinfo object now.
About this fork which is not a fork anymore
===========================================
Aim of this fork (not fork anymore, read further) was to bring this package up
to date with latest icalendar `RFC`_ specification as part of
`plone.app.event`_ project which goal is to bring recurrent evens to `Plone`_.
After some thoughts we (Plone developers involved with `plone.app.event`_) send
a suggestion to icalendar-dev@codespeak.net to take over mainaining of
`icalendar`_. Nobody object and since version 2.2 we are back to development.
.. _`icalendar`: http://pypi.python.org/pypi/icalendar
.. _`plone.app.event`: http://github.com/plone/plone.app.event
.. _`Plone`: http://plone.org
.. _`pytz`: http://pypi.python.org/pypi/pytz
.. _`setuptools`: http://pypi.python.org/pypi/setuptools
.. _`RFC`: http://www.ietf.org/rfc/rfc5545.txt
.. _`BSD`: https://github.com/collective/icalendar/issues/2
Test Coverage Report
====================
Output from coverage test::
Name Stmts Miss Cover
----------------------------------------------------------------------------------
.tox/py27/lib/python2.7/site-packages/icalendar/__init__ 5 0 100%
.tox/py27/lib/python2.7/site-packages/icalendar/cal 234 7 97%
.tox/py27/lib/python2.7/site-packages/icalendar/caselessdict 55 5 91%
.tox/py27/lib/python2.7/site-packages/icalendar/compat 1 0 100%
.tox/py27/lib/python2.7/site-packages/icalendar/parser 189 6 97%
.tox/py27/lib/python2.7/site-packages/icalendar/parser_tools 20 0 100%
.tox/py27/lib/python2.7/site-packages/icalendar/prop 533 62 88%
.tox/py27/lib/python2.7/site-packages/icalendar/tools 16 0 100%
----------------------------------------------------------------------------------
TOTAL 1053 80 92%
Changelog
=========
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
<msmith@fluendo.com>)
- 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: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.3
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent

View File

@ -0,0 +1,57 @@
CHANGES.rst
CONTRIBUTING.rst
LICENSE.rst
MANIFEST.in
README.rst
TODO.rst
setup.cfg
setup.py
tox.ini
docs/Makefile
docs/about.rst
docs/changelog.rst
docs/conf.py
docs/credits.rst
docs/index.rst
docs/install.rst
docs/license.rst
docs/usage.rst
docs/_themes/icalendar/layout.html
docs/_themes/icalendar/theme.conf
docs/_themes/icalendar/static/icalendar.css
src/icalendar/__init__.py
src/icalendar/cal.py
src/icalendar/caselessdict.py
src/icalendar/compat.py
src/icalendar/parser.py
src/icalendar/parser_tools.py
src/icalendar/prop.py
src/icalendar/tools.py
src/icalendar.egg-info/PKG-INFO
src/icalendar.egg-info/SOURCES.txt
src/icalendar.egg-info/dependency_links.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/encoding.ics
src/icalendar/tests/issue_112_missing_tzinfo_on_exdate.ics
src/icalendar/tests/issue_114_invalid_line.ics
src/icalendar/tests/issue_53_parsing_failure.ics
src/icalendar/tests/multiple.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/timezoned.ics

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,5 @@
setuptools
python-dateutil
pytz
[test]

View File

@ -0,0 +1 @@
icalendar

View File

@ -0,0 +1,59 @@
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,
)
__all__ = [
Calendar, Event, Todo, Journal,
FreeBusy, Alarm, ComponentFactory,
Timezone, TimezoneStandard, TimezoneDaylight,
vBinary, vBoolean, vCalAddress, vDatetime, vDate,
vDDDTypes, vDuration, vFloat, vInt, vPeriod,
vWeekday, vFrequency, vRecur, vText, vTime, vUri,
vGeo, vUTCOffset, TypesFactory,
FixedOffset, LocalTimezone,
Parameters, q_split, q_join,
]

View File

@ -0,0 +1,493 @@
# -*- 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
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.parser_tools import data_encode
from icalendar.prop import TypesFactory
from icalendar.prop import vText, vDDDLists
import pytz
######################################
# 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.
"""
CaselessDict.__init__(self, *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(
[(cat, 1) for cat in ('CATEGORIES', 'RESOURCES', 'FREEBUSY')]
)
_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 = '' # must 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.
"""
CaselessDict.__init__(self, *args, **kwargs)
# set parameters here for properties that use non-default values
self.subcomponents = [] # Components can be nested.
self.is_broken = False # True if we ignored an exception while
# parsing a property
#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
#############################
# 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']:
# 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.
#if name == 'attendee': import pdb; pdb.set_trace()
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 not name is None:
name = name.upper()
return self._walk(name)
#####################
# Generation
def property_items(self, recursive=True):
"""Returns properties in this component and subcomponents as:
[(name, value), ...]
"""
vText = types_factory['text']
properties = [('BEGIN', vText(self.name).to_ical())]
property_names = self.sorted_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()
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
name, params, vals = line.parts()
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, cls)
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:
if not component.is_broken:
stack[-1].add_component(component)
# we are adding properties to the current top of the stack
else:
factory = types_factory.for_property(name)
component = stack[-1]
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:
if not component.ignore_exceptions:
raise
component.is_broken = True
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 __repr__(self):
return '%s(%s)' % (self.name, data_encode(self))
def content_line(self, name, value):
"""Returns property as content line.
"""
params = getattr(value, 'params', Parameters())
return Contentline.from_parts(name, params, value)
def content_lines(self):
"""Converts the Component and subcomponents into content lines.
"""
contentlines = Contentlines()
for name, value in self.property_items():
cl = self.content_line(name, value)
contentlines.append(cl)
contentlines.append('') # remember the empty string in the end
return contentlines
def to_ical(self):
content_lines = self.content_lines()
return content_lines.to_ical()
#######################################
# components defined in RFC 2445
class Event(Component):
name = 'VEVENT'
canonical_order = (
'SUMMARY', 'DTSTART', 'DTEND', 'DURATION', 'DTSTAMP',
'UID', 'RECURRENCE-ID', 'SEQUENCE',
'RRULE' 'EXRULE', 'RDATE', 'EXDATE',
)
required = ('UID',)
singletons = (
'CLASS', 'CREATED', 'DESCRIPTION', 'DTSTART', 'GEO', 'LAST-MODIFIED',
'LOCATION', 'ORGANIZER', 'PRIORITY', 'DTSTAMP', 'SEQUENCE', 'STATUS',
'SUMMARY', 'TRANSP', 'URL', 'RECURRENCE-ID', 'DTEND', 'DURATION',
'DTSTART',
)
exclusive = ('DTEND', 'DURATION', )
multiple = (
'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE',
'EXRULE', 'RSTATUS', 'RELATED', 'RESOURCES', 'RDATE', 'RRULE'
)
ignore_exceptions = True
class Todo(Component):
name = 'VTODO'
required = ('UID',)
singletons = (
'CLASS', 'COMPLETED', 'CREATED', 'DESCRIPTION', 'DTSTAMP', 'DTSTART',
'GEO', 'LAST-MODIFIED', 'LOCATION', 'ORGANIZER', 'PERCENT', 'PRIORITY',
'RECURRENCE-ID', 'SEQUENCE', 'STATUS', 'SUMMARY', 'UID', 'URL', 'DUE',
'DURATION',
)
exclusive = ('DUE', 'DURATION',)
multiple = (
'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE',
'EXRULE', 'RSTATUS', 'RELATED', 'RESOURCES', 'RDATE', 'RRULE'
)
class Journal(Component):
name = 'VJOURNAL'
required = ('UID',)
singletons = (
'CLASS', 'CREATED', 'DESCRIPTION', 'DTSTART', 'DTSTAMP',
'LAST-MODIFIED', 'ORGANIZER', 'RECURRENCE-ID', 'SEQUENCE', 'STATUS',
'SUMMARY', 'UID', 'URL',
)
multiple = (
'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE',
'EXRULE', 'RELATED', 'RDATE', 'RRULE', 'RSTATUS',
)
class FreeBusy(Component):
name = 'VFREEBUSY'
required = ('UID',)
singletons = (
'CONTACT', 'DTSTART', 'DTEND', 'DURATION', 'DTSTAMP', 'ORGANIZER',
'UID', 'URL',
)
multiple = ('ATTENDEE', 'COMMENT', 'FREEBUSY', 'RSTATUS',)
class Timezone(Component):
name = 'VTIMEZONE'
canonical_order = ('TZID', 'STANDARD', 'DAYLIGHT',)
required = ('TZID', 'STANDARD', 'DAYLIGHT',)
singletons = ('TZID', 'LAST-MODIFIED', 'TZURL',)
class TimezoneStandard(Component):
name = 'STANDARD'
required = ('DTSTART', 'TZOFFSETTO', 'TZOFFSETFROM')
singletons = ('DTSTART', 'TZOFFSETTO', 'TZOFFSETFROM', 'RRULE')
multiple = ('COMMENT', 'RDATE', 'TZNAME')
class TimezoneDaylight(Component):
name = 'DAYLIGHT'
required = ('DTSTART', 'TZOFFSETTO', 'TZOFFSETFROM')
singletons = ('DTSTART', 'TZOFFSETTO', 'TZOFFSETFROM', 'RRULE')
multiple = ('COMMENT', 'RDATE', 'TZNAME')
class Alarm(Component):
name = 'VALARM'
# not quite sure about these ...
required = ('ACTION', 'TRIGGER',)
singletons = ('ATTACH', 'ACTION', 'TRIGGER', 'DURATION', 'REPEAT',)
inclusive = (('DURATION', 'REPEAT',),)
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', )
multiple = ('calscale', 'method', )
# These are read only singleton, so one instance is enough for the module
types_factory = TypesFactory()
component_factory = ComponentFactory()

View File

@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
from icalendar.parser_tools import to_unicode
from icalendar.parser_tools import data_encode
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 = dict((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(dict):
"""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.
"""
dict.__init__(self, *args, **kwargs)
for key, value in self.items():
key_upper = to_unicode(key).upper()
if key != key_upper:
dict.__delitem__(self, key)
self[key_upper] = value
def __getitem__(self, key):
key = to_unicode(key)
return dict.__getitem__(self, key.upper())
def __setitem__(self, key, value):
key = to_unicode(key)
dict.__setitem__(self, key.upper(), value)
def __delitem__(self, key):
key = to_unicode(key)
dict.__delitem__(self, key.upper())
def __contains__(self, key):
key = to_unicode(key)
return dict.__contains__(self, key.upper())
def get(self, key, default=None):
key = to_unicode(key)
return dict.get(self, key.upper(), default)
def setdefault(self, key, value=None):
key = to_unicode(key)
return dict.setdefault(self, key.upper(), value)
def pop(self, key, default=None):
key = to_unicode(key)
return dict.pop(self, key.upper(), default)
def popitem(self):
return dict.popitem(self)
def has_key(self, key):
key = to_unicode(key)
return dict.__contains__(self, key.upper())
def update(self, indict):
# Multiple keys where key1.upper() == key2.upper() will be lost.
for key, value in indict.items(): # TODO optimize in python 2
self[key] = value
def copy(self):
return CaselessDict(dict.copy(self))
def __repr__(self):
return 'CaselessDict(%s)' % data_encode(self)
# 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)

View File

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

View File

@ -0,0 +1,371 @@
# -*- 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 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 data_encode
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(u'\\N', u'\\n')\
.replace(u'\r\n', u'\n')\
.replace(u'\\n', u'\n')\
.replace(u'\\,', u',')\
.replace(u'\\;', u';')\
.replace(u'\\\\', u'\\')
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=u'\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 u'\n' not in line
ret_line = u''
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_line += fold_sep
byte_count = char_byte_len
ret_line += char
return ret_line
#################################################################
# 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
NAME = re.compile('[\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(u'(\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=','):
"""Splits a string on char, taking double (q)uotes into considderation.
"""
result = []
cursor = 0
length = len(st)
inquote = 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
if i + 1 == length:
result.append(st[cursor:])
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 __repr__(self):
return 'Parameters(%s)' % data_encode(self)
def to_ical(self):
result = []
items = self.items()
for key, value in sorted(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, '=')
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 unsescape_string(val):
return val.replace('%2C', ',').replace('%3A', ':')\
.replace('%3B', ';').replace('%5C', '\\')
#########################################
# 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 u'\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):
"""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())
return cls(u'%s;%s:%s' % (name, params, values))
return cls(u'%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 = unsescape_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(
(unsescape_string(key), unsescape_string(value))
for key, value in compat.iteritems(params)
)
values = unsescape_string(st[value_split + 1:])
return (name, params, values)
except ValueError as exc:
raise ValueError(
u"Content line could not be parsed into parts: %r: %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 unfolded.splitlines() 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

View File

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

View File

@ -0,0 +1,993 @@
# -*- 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 has 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 starts 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 allways 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
from dateutil.tz import tzutc
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
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('(?P<signal>[+-]?)(?P<relative>[\d]?)'
'(?P<weekday>[\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 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)):
raise ValueError('You must use datetime, date, timedelta or time')
if isinstance(dt, datetime):
self.params = Parameters(dict(value='DATE-TIME'))
elif isinstance(dt, date):
self.params = Parameters(dict(value='DATE'))
elif isinstance(dt, time):
self.params = Parameters(dict(value='TIME'))
if (isinstance(dt, datetime) or isinstance(dt, time))\
and getattr(dt, 'tzinfo', False):
tzinfo = dt.tzinfo
if tzinfo is not pytz.utc and 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()
else:
raise ValueError('Unknown date type')
@classmethod
def from_ical(cls, ical, timezone=None):
if isinstance(ical, cls):
return ical.dt
u = ical.upper()
if u.startswith('-P') or u.startswith('P'):
return vDuration.from_ical(ical)
try:
return vDatetime.from_ical(ical, timezone=timezone)
except ValueError:
try:
return vDate.from_ical(ical)
except ValueError:
return vTime.from_ical(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(dict(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:
pass
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 datetime(tzinfo=pytz.utc, *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 not weekday 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 not self 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,
'BYMONTHDAY': vInt,
'BYYEARDAY': vInt,
'BYMONTH': vInt,
'UNTIL': vDDDTypes,
'BYSETPOS': vInt,
'WKST': vWeekday,
'BYDAY': vWeekday,
'FREQ': vFrequency,
})
def __init__(self, *args, **kwargs):
CaselessDict.__init__(self, *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(';'):
key, vals = pairs.split('=')
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(dict(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.
"""
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 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"
CaselessDict.__init__(self, *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
)
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
#################################################
# 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': 'text',
'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

View File

@ -0,0 +1,5 @@
# unittest/unittest2 importer
import unittest
if not hasattr(unittest.TestCase, 'assertIsNotNone'):
import unittest2 as unittest
unittest # pep 8

View File

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

View File

@ -0,0 +1,48 @@
BEGIN: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

View File

@ -0,0 +1,41 @@
BEGIN:VEVENT
DTSTART:20130927T130000Z
DTEND:20130927T140000Z
DTSTAMP:20131107T004757Z
ORGANIZER;CN=gxxxxxxxn@nxx.fr:mailto:gxxxxxn@nxx.fr
UID:040000008200E00074C5B7101A82E00800000000A0F3321606B6CE01000000000000000
010000000F09F33F0E8ED4C44B99F6027ACF588D0
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=St
eve Bxxxxxx;X-NUM-GUESTS=0:mailto:sxxxxxt@nxx.fr
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Boris
Hxxxxx;X-NUM-GUESTS=0:mailto:bxxxxxxk@vxxxxxxxx.com
CREATED:20130920T113409Z
DESCRIPTION:Quand : vendredi 27 septembre 2013 15:00-16:00 (UTC+01:00) Brux
elles\, Copenhague\, Madrid\, Paris.\nEmplacement : Conf-Call - 01 xx xx xx
xx\n\nRemarque : le décalage GMT ci-dessus ne tient pas compte des réglage
s de l'heure d'été.\n\n*~*~*~*~*~*~*~*~*~*\n\nComme convenu à linstant par
e-mail\n
LAST-MODIFIED:20130920T115104Z
LOCATION:Conf-Call - 01 xx xx xx xx
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Nxx - Réunion lancement PxxxxxxT
TRANSP:OPAQUE
X-ALT-DESC;FMTTYPE=text/html:<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//E
N">\n<HTML>\n<HEAD>\n<META NAME="Generator" CONTENT="MS Exchange Server ver
sion 08.00.0681.000">\n<TITLE></TITLE>\n</HEAD>\n<BODY>\n<!-- Converted fro
m text/rtf format -->\n\n<P DIR=LTR><SPAN LANG="fr"><FONT FACE="Calibri">Qu
and : vendredi 27 septembre 2013 15:00-16:00 (UTC+01:00) Bruxelles\, Copenh
ague\, Madrid\, Paris.</FONT></SPAN></P>\n\n<P DIR=LTR><SPAN LANG="fr"><FON
T FACE="Calibri">Emplacement : Conf-Call - 01 xx xx xx xx</FONT></SPAN></P>
\n\n<P DIR=LTR><SPAN LANG="fr"><FONT FACE="Calibri">Remarque : le décalage
GMT ci-dessus ne tient pas compte des réglages de l'heure d'été.</FONT></SP
AN></P>\n\n<P DIR=LTR><SPAN LANG="fr"><FONT FACE="Calibri">*~*~*~*~*~*~*~*~
*~*</FONT></SPAN></P>\n\n<P DIR=LTR><SPAN LANG="fr"><FONT FACE="Calibri">Co
mme convenu à linstant par e-mail</FONT></SPAN><SPAN LANG="fr"></SPAN></P>
\n\n</BODY>\n</HTML>
X-MICROSOFT-CDO-BUSYSTATUS:TENTATIVE
X-MICROSOFT-CDO-IMPORTANCE:1
X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY
X
END:VEVENT

View File

@ -0,0 +1,78 @@
BEGIN: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

View File

@ -0,0 +1,80 @@
BEGIN: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

View File

@ -0,0 +1,24 @@
BEGIN: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

View File

@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
from icalendar.tests 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__)
data = open(os.path.join(directory, 'encoding.ics'), 'rb').read()
cal = icalendar.Calendar.from_ical(data)
self.assertEqual(cal['prodid'].to_ical().decode('utf-8'),
u"-//Plönë.org//NONSGML plone.app.event//EN")
self.assertEqual(cal['X-WR-CALDESC'].to_ical().decode('utf-8'),
u"test non ascii: äöü ÄÖÜ €")
event = cal.walk('VEVENT')[0]
self.assertEqual(event['SUMMARY'].to_ical().decode('utf-8'),
u'Non-ASCII Test: ÄÖÜ äöü €')
self.assertEqual(
event['DESCRIPTION'].to_ical().decode('utf-8'),
u'icalendar should be able to handle non-ascii: €äüöÄÜÖ.'
)
self.assertEqual(event['LOCATION'].to_ical().decode('utf-8'),
u'Tribstrül')
def test_create_to_ical(self):
cal = icalendar.Calendar()
cal.add('prodid', u"-//Plönë.org//NONSGML plone.app.event//EN")
cal.add('version', u"2.0")
cal.add('x-wr-calname', u"äöü ÄÖÜ €")
cal.add('x-wr-caldesc', u"test non ascii: äöü ÄÖÜ €")
cal.add('x-wr-relcalid', u"12345")
event = icalendar.Event()
event.add(
'dtstart',
datetime.datetime(2010, 10, 10, 10, 00, 00, tzinfo=pytz.utc)
)
event.add(
'dtend',
datetime.datetime(2010, 10, 10, 12, 00, 00, tzinfo=pytz.utc)
)
event.add(
'created',
datetime.datetime(2010, 10, 10, 0, 0, 0, tzinfo=pytz.utc)
)
event.add('uid', u'123456')
event.add('summary', u'Non-ASCII Test: ÄÖÜ äöü €')
event.add(
'description',
u'icalendar should be able to de/serialize non-ascii.'
)
event.add('location', u'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",
datetime.datetime(2010, 10, 10, 0, 0, 0, tzinfo=pytz.utc)
)
event.add("summary", u"åäö")
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(u'DESCRIPTION', u'äöüßÄÖÜ')
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'
)

View File

@ -0,0 +1,247 @@
# -*- coding: utf-8 -*-
from icalendar.parser_tools import to_unicode
from icalendar.tests 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", u"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", u"åäö")
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 = """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, u'acme, ädmin')
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_114(self):
"""Issue #114/#115 - invalid line in event breaks the parser
https://github.com/collective/icalendar/issues/114
"""
directory = os.path.dirname(__file__)
ics = open(os.path.join(directory, 'issue_114_invalid_line.ics'), 'rb')
with self.assertRaises(ValueError):
cal = icalendar.Calendar.from_ical(ics.read())
cal # pep 8
ics.close()
def test_issue_116(self):
"""Issue #116/#117 - How to add 'X-APPLE-STRUCTURED-LOCATION'
"""
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()
)

View File

@ -0,0 +1,253 @@
# coding: utf-8
from icalendar.tests 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),
u'SUMMARY:INternational char æ ø å'
)
# A value can also be unicode
parts = ('SUMMARY', Parameters(), vText(u'INternational char æ ø å'))
self.assertEqual(
Contentline.from_parts(*parts),
u'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')
)
def test_fold_line(self):
from ..parser import foldline
self.assertEqual(foldline(u'foo'), u'foo')
self.assertEqual(
foldline(u"Lorem ipsum dolor sit amet, consectetur adipiscing "
u"elit. Vestibulum convallis imperdiet dui posuere."),
(u'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '
u'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(u'привет'.encode('utf-8'), limit=3)
self.assertEqual(foldline(u'foobar', limit=4), u'foo\r\n bar')
self.assertEqual(
foldline(u'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
u'. Vestibulum convallis imperdiet dui posuere.'),
(u'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
u' Vestibulum conval\r\n lis imperdiet dui posuere.')
)
self.assertEqual(
foldline(u'DESCRIPTION:АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭЮЯ'),
u'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_join(self):
from ..parser import q_join
self.assertEqual(q_join(['Max', 'Moller', 'Rasmussen, Max']),
'Max,Moller,"Rasmussen, Max"')

View File

@ -0,0 +1,28 @@
from icalendar import Calendar
from icalendar.prop import vText
from icalendar.tests import unittest
import os
class TestMultiple(unittest.TestCase):
"""A example with multiple VCALENDAR components"""
def test_multiple(self):
directory = os.path.dirname(__file__)
cals = Calendar.from_ical(
open(os.path.join(directory, 'multiple.ics'), 'rb').read(),
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')
)

View File

@ -0,0 +1,207 @@
# coding: utf-8
from icalendar import Calendar
from icalendar import Event
from icalendar import Parameters
from icalendar import vCalAddress
from icalendar.tests import unittest
import icalendar
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'),
u'BEGIN:VEVENT\r\n'
u'ORGANIZER;CN="Джон Доу":mailto:john.doe@example.org\r\n'
u'END:VEVENT\r\n'
)
self.assertEqual(vevent['ORGANIZER'].params['CN'],
'Джон Доу')
def test_quoting(self):
# not double-quoted
self._test_quoting(u"Aramis", u'Aramis')
# if a space is present - enclose in double quotes
self._test_quoting(u"Aramis Alameda", u'"Aramis Alameda"')
# a single quote in parameter value - double quote the value
self._test_quoting(u"Aramis d'Alameda", u'"Aramis d\'Alameda"')
# double quote is replaced with single quote
self._test_quoting(u"Aramis d\"Alameda", u'"Aramis d\'Alameda"')
self._test_quoting(u"Арамис д'Аламеда", u'"Арамис д\'Аламеда"')
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 = u',\\;:'
for char in NON_SAFE_CHARS:
cn_escaped = u"Society\\%s 2014" % char
cn_decoded = u"Society%s 2014" % char
vevent = Event.from_ical(
u'BEGIN:VEVENT\r\n'
u'ORGANIZER;CN=%s:that\r\n'
u'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'),
u'это, то; 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'], u'RembrandXS')

View File

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
from icalendar.caselessdict import CaselessDict
from icalendar.tests import unittest
import datetime
import icalendar
import os
import pytz
class TestRecurrence(unittest.TestCase):
def setUp(self):
directory = os.path.dirname(__file__)
self.cal = icalendar.Calendar.from_ical(
open(os.path.join(directory, 'recurrence.ics'), 'rb').read()
)
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,
datetime.datetime(1996, 4, 2, 1, 0, tzinfo=pytz.utc)
)
self.assertEqual(
first_event['exdate'].dts[1].dt,
datetime.datetime(1996, 4, 3, 1, 0, tzinfo=pytz.utc)
)
self.assertEqual(
first_event['exdate'].dts[2].dt,
datetime.datetime(1996, 4, 4, 1, 0, tzinfo=pytz.utc)
)
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!

View File

@ -0,0 +1,29 @@
from icalendar.tests 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())

View File

@ -0,0 +1,141 @@
# -*- coding: utf-8 -*-
from icalendar.tests 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__)
cal = icalendar.Calendar.from_ical(
open(os.path.join(directory, 'timezoned.ics'), 'rb').read()
)
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'),
datetime.datetime(2012, 2, 13, 10, 0, 0,
tzinfo=pytz.timezone('Europe/Vienna')))
self.assertEqual(
ev1.decoded('DTSTAMP'),
datetime.datetime(2010, 10, 10, 9, 10, 10, tzinfo=pytz.utc))
def test_create_to_ical(self):
cal = icalendar.Calendar()
cal.add('prodid', u"-//Plone.org//NONSGML plone.app.event//EN")
cal.add('version', u"2.0")
cal.add('x-wr-calname', u"test create calendar")
cal.add('x-wr-caldesc', u"icalendar tests")
cal.add('x-wr-relcalid', u"12345")
cal.add('x-wr-timezone', u"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',
datetime.datetime(2012, 2, 13, 10, 00, 00, tzinfo=tz))
event.add(
'dtend',
datetime.datetime(2012, 2, 17, 18, 00, 00, tzinfo=tz))
event.add(
'dtstamp',
datetime.datetime(2010, 10, 10, 10, 10, 10, tzinfo=tz))
event.add(
'created',
datetime.datetime(2010, 10, 10, 10, 10, 10, tzinfo=tz))
event.add('uid', u'123456')
event.add(
'last-modified',
datetime.datetime(2010, 10, 10, 10, 10, 10, tzinfo=tz))
event.add('summary', u'artsprint 2012')
# event.add('rrule', u'FREQ=YEARLY;INTERVAL=1;COUNT=10')
event.add('description', u'sprinting at the artsprint')
event.add('location', u'aka bild, wien')
event.add('categories', u'first subject')
event.add('categories', u'second subject')
event.add('attendee', u'häns')
event.add('attendee', u'franz')
event.add('attendee', u'sepp')
event.add('contact', u'Max Mustermann, 1010 Wien')
event.add('url', u'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:20101010T091010Z" in test_out)
self.assertTrue("CREATED;VALUE=DATE-TIME:20101010T091010Z" 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__ == 'dateutil.tz')
self.assertTrue(date2.tzinfo.__module__ == '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')

View File

@ -0,0 +1,349 @@
from datetime import datetime
from datetime import timedelta
from icalendar.tests import unittest
import icalendar
import pytz
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'
# 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.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(u'-//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'] = u'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),
[u'Chair', u'Table', u'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, crated, dtstamp and last-modified must be in UTC.
Component = icalendar.cal.Component
comp = Component()
comp.add('dtstart', datetime(2010, 10, 10, 10, 0, 0,
tzinfo=pytz.timezone("Europe/Vienna")))
comp.add('created', datetime(2010, 10, 10, 12, 0, 0))
comp.add('dtstamp', datetime(2010, 10, 10, 14, 0, 0,
tzinfo=pytz.timezone("Europe/Vienna")))
comp.add('last-modified', datetime(2010, 10, 10, 16, 0, 0,
tzinfo=pytz.utc))
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:20101010T130000Z" 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'], [u'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)
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()
open(os.path.join(directory, 'test.ics'), 'wb').write(cal.to_ical())
# Parsing a complete calendar from a string will silently ignore bogus
# events. The bogosity 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:Bogus 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'])

View File

@ -0,0 +1,90 @@
from icalendar.tests 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 = dict(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(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'])

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from icalendar.parser_tools import data_encode
from icalendar.parser_tools import to_unicode
from icalendar.tests import unittest
class TestParserTools(unittest.TestCase):
def test_parser_tools_to_unicode(self):
self.assertEqual(to_unicode('spam'), u'spam')
self.assertEqual(to_unicode(u'spam'), u'spam')
self.assertEqual(to_unicode(u'spam'.encode('utf-8')), u'spam')
self.assertEqual(to_unicode(b'\xc6\xb5'), u'\u01b5')
self.assertEqual(to_unicode(u'\xc6\xb5'.encode('iso-8859-1')),
u'\u01b5')
self.assertEqual(to_unicode(b'\xc6\xb5', encoding='ascii'), u'\u01b5')
self.assertEqual(to_unicode(1), 1)
self.assertEqual(to_unicode(None), None)
def test_parser_tools_data_encode(self):
data1 = {
u'k1': u'v1', 'k2': 'v2', u'k3': u'v3',
'li1': ['it1', u'it2', {'k4': u'v4', u'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)

View File

@ -0,0 +1,497 @@
# -*- coding: utf-8 -*-
from datetime import date
from datetime import datetime
from datetime import time
from datetime import timedelta
from icalendar.parser import Parameters
from icalendar.tests import unittest
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'),
datetime(2001, 1, 1, 12, 30, tzinfo=pytz.utc))
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 = datetime(2001, 1, 1, 12, 30, 0, tzinfo=pytz.utc)
self.assertEqual(vDatetime(dutc).to_ical(), b'20010101T123000Z')
dutc = datetime(1899, 1, 1, 12, 30, 0, tzinfo=pytz.utc)
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 = datetime(2000, 1, 1, tzinfo=dk)
end = datetime(2000, 1, 2, tzinfo=dk)
per = (start, end)
self.assertEqual(vPeriod(per).to_ical(),
b'20000101T000000/20000102T000000')
self.assertEqual(vPeriod(per).params['TZID'],
'Europe/Copenhagen')
p = vPeriod((datetime(2000, 1, 1, tzinfo=dk), 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(u'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(u'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'),
u'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'), u'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', u'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', u'Rasmussen, Max M\xfcller'),
b'Rasmussen\\, Max M\xc3\xbcller')
self.assertEqual(
factory.from_ical('cn', b'Rasmussen\\, Max M\xc3\xb8ller'),
u'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)

View File

@ -0,0 +1,28 @@
from icalendar.tests 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)

View File

@ -0,0 +1,3 @@
BEGIN:VCALENDAR
X-SOMETIME;VALUE=TIME:172010
END:VCALENDAR

View File

@ -0,0 +1,36 @@
BEGIN: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

View File

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

View File

@ -0,0 +1,23 @@
[tox]
envlist = py26,py27,py33
[testenv:py26]
deps =
unittest2
discover
coverage
commands =
coverage erase
coverage run --source=icalendar --omit=*tests* {envbindir}/discover icalendar
coverage report --omit=*tests*
coverage html --omit=*tests*
[testenv]
deps =
discover
coverage
commands =
coverage erase
coverage run --source=icalendar --omit=*tests* {envbindir}/discover icalendar
coverage report --omit=*tests*
coverage html --omit=*tests*