pax_global_header00006660000000000000000000000064145764131620014523gustar00rootroot0000000000000052 comment=72966e7610416cf718f6ad9575db4dab4c2d7d96 icalendar-5.0.12/000077500000000000000000000000001457641316200135325ustar00rootroot00000000000000icalendar-5.0.12/.github/000077500000000000000000000000001457641316200150725ustar00rootroot00000000000000icalendar-5.0.12/.github/ISSUE_TEMPLATE/000077500000000000000000000000001457641316200172555ustar00rootroot00000000000000icalendar-5.0.12/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000023031457641316200217450ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: "[BUG] " labels: '' assignees: '' --- ## Describe the bug ## To Reproduce ``` import icalendar ... ``` Output: ``` ``` ## Expected behavior ## Environment - [ ] OS: - [ ] Python version: - [ ] `icalendar` version: ## Additional context - [ ] I tested it with the latest version `pip3 install https://github.com/collective/icalendar.git` - [ ] I attached the ICS source file or there is no ICS source file icalendar-5.0.12/.github/ISSUE_TEMPLATE/empty-issue.md000066400000000000000000000007521457641316200220670ustar00rootroot00000000000000--- name: empty issue about: This is an unstructured issue template to use with issues that are not described, yet. title: '' labels: '' assignees: '' --- icalendar-5.0.12/.github/dependabot.yml000066400000000000000000000010061457641316200177170ustar00rootroot00000000000000# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem version: 2 updates: - package-ecosystem: github-actions directory: / groups: github-actions: patterns: - "*" # Group all Actions updates into a single larger pull request schedule: interval: weekly icalendar-5.0.12/.github/workflows/000077500000000000000000000000001457641316200171275ustar00rootroot00000000000000icalendar-5.0.12/.github/workflows/cifuzz.yml000066400000000000000000000023561457641316200211720ustar00rootroot00000000000000name: CIFuzz on: [pull_request] permissions: {} jobs: Fuzzing: runs-on: ubuntu-latest permissions: security-events: write steps: - name: Build Fuzzers id: build uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master with: oss-fuzz-project-name: 'icalendar' language: python - name: Run Fuzzers uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master with: oss-fuzz-project-name: 'icalendar' language: python fuzz-seconds: 600 output-sarif: true - name: Upload Crash uses: actions/upload-artifact@v4 if: failure() && steps.build.outcome == 'success' with: name: artifacts path: ./out/artifacts - name: Upload Sarif if: always() && steps.build.outcome == 'success' uses: github/codeql-action/upload-sarif@v3 with: # Path to SARIF file relative to the root of the repository sarif_file: cifuzz-sarif/results.sarif checkout_path: cifuzz-sarif check-distribution: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Do not include Apache files in the distribution. run: | src/icalendar/tests/test_create_release.sh icalendar-5.0.12/.github/workflows/tests.yml000066400000000000000000000014061457641316200210150ustar00rootroot00000000000000name: tests on: push: branches: - main tags: - v* pull_request: schedule: - cron: '14 7 * * 0' # run once a week on Sunday workflow_dispatch: jobs: deploy-github-release: # only deploy on tags, see https://stackoverflow.com/a/58478262/1320237 if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest environment: name: github-release steps: - uses: actions/checkout@v4 - name: create release uses: ncipollo/release-action@v1 with: body: To view the changes, please see the [Changelog](https://icalendar.readthedocs.io/en/latest/changelog.html). This release can be installed from [PyPI](https://pypi.org/project/icalendar/#history). icalendar-5.0.12/.gitignore000066400000000000000000000003441457641316200155230ustar00rootroot00000000000000*.mo *.py? *.sw? *~ .* bin/ build/ coverage-* develop-eggs/ dist/ docs/_build/ eggs/ htmlcov/ include/ lib/ lib64 parts/ pip-selfcheck.json src/icalendar.egg-info/ !.gitattributes !.github !.gitignore venv /ical_fuzzer.pkg.spec icalendar-5.0.12/.readthedocs.yml000066400000000000000000000007441457641316200166250ustar00rootroot00000000000000# .readthedocs.yml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py # Optionally build your docs in additional formats such as PDF and ePub formats: all build: os: ubuntu-22.04 tools: python: "3.11" python: install: - requirements: requirements_docs.txt - method: pip path: . icalendar-5.0.12/CHANGES.rst000066400000000000000000000571211457641316200153420ustar00rootroot00000000000000Changelog ========= 5.0.12 (2024-03-19) ------------------- Minor changes: - Added corpus to fuzzing directory - Added exclusion of fuzzing corpus in MANIFEST.in - Augmented fuzzer to optionally convert multiple calendars from a source string - Add script to convert OSS FUZZ test cases to Python/pytest test cases - Added additional exception handling of defined errors to fuzzer, to allow fuzzer to explore deeper - Added more instrumentation to fuzz-harness - Rename "contributor" to "collaborator" in documentation - Correct the outdated "icalendar view myfile.ics" command in documentation. #588 - Update GitHub Actions steps versions - Keep GitHub Actions up to date with GitHub's Dependabot Breaking changes: - ... New features: - ... Bug fixes: - ... - Fixed index error in cal.py when attempting to pop from an empty stack - Fixed type error in prop.py when attempting to join strings into a byte-string - Caught Wrong Date Format in ical_fuzzer to resolve fuzzing coverage blocker 5.0.11 (2023-11-03) ------------------- Minor changes: - The cli utility now displays start and end datetimes in the user's local timezone. Ref: #561 [vimpostor] New features: - Added fuzzing harnesses, for integration to OSSFuzz. - icalendar releases are deployed to Github releases Fixes: #563 [jacadzaca] Bug fixes: - CATEGORIES field now accepts a string as argument Ref: #322 [jacadzaca] - Multivalue FREEBUSY property is now parsed properly Ref: #27 [jacadzaca] - Compare equality and inequality of calendars more completely Ref: #570 - Use non legacy timezone name. Ref: #567 - Add some compare functions. Ref: #568 - Change OSS Fuzz build script to point to harnesses in fuzzing directory Ref: #574 5.0.10 (2023-09-26) ------------------- Bug fixes: - Component._encode stops ignoring parameters argument on native values, now merges them Fixes: #557 [zocker1999net] 5.0.9 (2023-09-24) ------------------ Bug fixes: - PERIOD values now set the timezone of their start and end. #556 5.0.8 (2023-09-18) ------------------ Minor changes: - Update build configuration to build readthedocs. #538 - No longer run the ``plone.app.event`` tests. - Add documentation on how to parse ``.ics`` files. #152 - Move pip caching into Python setup action. - Check that issue #165 can be closed. - Updated about.rst for issue #527 - Avoid ``vText.__repr__`` BytesWarning. Bug fixes: - Calendar components are now properly compared Ref: #550 Fixes: #526 [jacadzaca] 5.0.7 (2023-05-29) ------------------ Bug fixes: - to_ical() now accepts RRULE BYDAY values>=10 #518 5.0.6 (2023-05-26) ------------------ Minor changes: - Adjusted duration regex 5.0.5 (2023-04-13) ------------------ Minor changes: - Added support for BYWEEKDAY in vRecur ref: #268 Bug fixes: - Fix problem with ORGANIZER in FREE/BUSY #348 5.0.4 (2022-12-29) ------------------ Minor changes: - Improved documentation Ref: #503, #504 Bug fixes: - vBoolean can now be used as an parameter Ref: #501 Fixes: #500 [jacadzaca] 5.0.3 (2022-11-23) ------------------ New features: - vDDDTypes is hashable #487 #492 [niccokunzmann] Bug fixes: - vDDDTypes' equality also checks the dt attribute #497 #492 [niccokunzmann] 5.0.2 (2022-11-03) ------------------ Minor changes: - Refactored cal.py, tools.py and completed remaining minimal refactoring in parser.py. Ref: #481 [pronoym99] - Calendar.from_ical no longer throws long errors Ref: #473 Fixes: #472 [jacadzaca] - Make datetime value shorter by removing the value parameter where possible. Fixes: #318 [jacadzaca], [niccokunzmann] New features: - source code in documentation is tested using doctest #445 [niccokunzmann] Bug fixes: - broken properties are not added to the parent component Ref: #471 Fixes: #464 [jacadzaca] 5.0.1 (2022-10-22) ------------------ Minor changes: - fixed setuptools deprecation warnings [mgorny] Bug fixes: - a well-known timezone timezone prefixed with a `/` is treated as if the slash wasn't present Ref: #467 Fixes: #466 [jacadzaca] 5.0.0 (2022-10-17) ------------------ Minor changes: - removed deprecated test checks [tuergeist] - Fix: cli does not support DURATION #354 [mamico] - Add changelog and contributing to readthedocs documentation #428 [peleccom] - fixed small typos #323 [rohnsha0] - unittest to parametrized pytest refactoring [jacadzaca] Breaking changes: - Require Python 3.7 as minimum Python version. [maurits] [niccokunzmann] - icalendar now takes a ics file directly as an input - icalendar's CLI utility program's output is different - Drop Support for Python 3.6. Versions 3.7 - 3.11 are supported and tested. New features: - icalendar utility outputs a 'Duration' row - icalendar can take multiple ics files as an input Bug fixes: - Changed tools.UIDGenerator instance methods to static methods Ref: #345 [spralja] - proper handling of datetime objects with `tzinfo` generated through zoneinfo.ZoneInfo. Ref: #334 Fixes: #333 [tobixen] - Timestamps in UTC does not need tzid Ref: #338 Fixes: #335 [tobixen] - add ``__eq__`` to ``icalendar.prop.vDDDTypes`` #391 [jacadzaca] - Refactor deprecated unittest aliases for Python 3.11 compatibility #330 [tirkarthi] 5.0.0a1 (2022-07-11) -------------------- Breaking changes: - Drop support for Python 3.4, 3.5 and PyPy2. [maurits] New features: - Document development setup Ref: #358 [niccokunzmann] Bug fixes: - Test with GitHub Actions. [maurits] 4.1.0 (2022-07-11) ------------------ New features: - No longer test on Python 3.4, 3.5 and PyPy2, because we cannot get it to work. Technically it should still work, it is just no longer tested. Do not expect much development on branch 4.x anymore. The master branch will be for the remaining Python versions that we support. [maurits] Bug fixes: - Test with GitHub Actions. [maurits] 4.0.9 (2021-10-16) ------------------ Bug fixes: - Fix vCategories for correct en/de coding. [thet] - vDuration property value: Fix changing duration sign after multiple ``to_ical`` calls. Ref: #320 Fixes: #319 [barlik] 4.0.8 (2021-10-07) ------------------ Bug fixes: - Support added for Python 3.9 and 3.10 (no code changes needed). - Replace bare 'except:' with 'except Exception:' (#281) 4.0.7 (2020-09-07) ------------------ Bug fixes: - fixed rrule handling, re-enabled test_create_america_new_york() 4.0.6 (2020-05-06) ------------------ Bug fixes: - Use ``vText`` as default type, when convert recurrence definition to ical string. [kam193] 4.0.5 (2020-03-21) ------------------ Bug fixes: - Fixed a docs issue related to building on Read the Docs [davidfischer] 4.0.4 (2019-11-25) ------------------ Bug fixes: - Reduce Hypothesis iterations to speed up testing, allowing PRs to pass [UniversalSuperBox] 4.0.3 (2018-10-10) ------------------ Bug fixes: - Categories are comma separated not 1 per line #265. [cleder] - mark test with mixed timezoneaware and naive datetimes as an expected failure. [cleder] 4.0.2 (2018-06-20) ------------------ Bug fixes: - Update all pypi.python.org URLs to pypi.org [jon.dufresne] 4.0.1 (2018-02-11) ------------------ - Added rudimentary command line interface. [jfjlaros] - Readme, setup and travis updates. [jdufresne, PabloCastellano] 4.0.0 (2017-11-08) ------------------ Breaking changes: - Drop support for Python 2.6 and 3.3. 3.12 (2017-11-07) ----------------- New features: - Accept Windows timezone identifiers as valid. #242 [geier] Bug fixes: - Fix ResourceWarnings in setup.py when Python warnings are enabled. #244 [jdufresne] - Fix invalid escape sequences in string and bytes literals. #245 [jdufresne] - Include license file in the generated wheel package. #243 [jdufresne] - Fix non-ASCII TZID and TZNAME parameter handling. #238 [clivest] - Docs: update install instructions. #240 [Ekran] 3.11.7 (2017-08-27) ------------------- New features: - added vUTCOffset.ignore_exceptions to allow surpressing of failed TZOFFSET parsing (for now this ignores the check for offsets > 24h) [geier] 3.11.6 (2017-08-04) ------------------- Bug fixes: - Fix VTIMEZONEs including RDATEs #234. [geier] 3.11.5 (2017-07-03) ------------------- Bug fixes: - added an assertion that VTIMEZONE sub-components' DTSTART must be of type DATETIME [geier] - Fix handling of VTIMEZONEs with subcomponents with the same DTSTARTs and OFFSETs but which are of different types [geier] 3.11.4 (2017-05-10) ------------------- Bug fixes: - Don't break on parameter values which contain equal signs, e.g. base64 encoded binary data [geier] - Fix handling of VTIMEZONEs with subcomponents with the same DTSTARTs. [geier] 3.11.3 (2017-02-15) ------------------- Bug fixes: - Removed ``setuptools`` as a dependency as it was only required by setup.py and not by the package. - Don't split content lines on the unicode ``LINE SEPARATOR`` character ``\u2028`` but only on ``CRLF`` or ``LF``. 3.11.2 (2017-01-12) ------------------- Bug fixes: - Run tests with python 3.5 and 3.6. [geier] - Allow tests failing with pypy3 on travis.ci. [geier] 3.11.1 (2016-12-19) ------------------- Bug fixes: - Encode error message before adding it to the stack of collected error messages. 3.11 (2016-11-18) ----------------- Fixes: - Successfully test with pypy and pypy3. [gforcada] - Minor documentation update. [tpltnt] 3.10 (2016-05-26) ----------------- New: - Updated components description to better comply with RFC 5545. Refs #183. [stlaz] - Added PERIOD value type to date types. Also fixes incompatibilities described in #184. Refs #189. [stlaz] Fixes: - Fix testsuite for use with ``dateutil>=2.5``. Refs #195. [untitaker] - Reintroduce cal.Component.is_broken that was removed with 3.9.2. Refs #185. [geier] 3.9.2 (2016-02-05) ------------------ New: - Defined ``test_suite`` in setup.py. Now tests can be run via ``python setup.py test``. [geier] Fixes: - Fixed cal.Component.from_ical() representing an unknown component as one of the known. [stlaz] - Fixed possible IndexError exception during parsing of an ical string. [stlaz] - When doing a boolean test on ``icalendar.cal.Component``, always return ``True``. Before it was returning ``False`` due to CaselessDict, if it didn't contain any items. [stlaz] - Fixed date-time being recognized as date or time during parsing. Added better error handling to parsing from ical strings. [stlaz] - Added __version__ attribute to init.py. [TomTry] - Documentation fixes. [TomTry] - Pep 8, UTF 8 headers, dict/list calls to literals. [thet] 3.9.1 (2015-09-08) ------------------ - Fix ``vPeriod.__repr__``. [spacekpe] - Improve foldline() performance. This improves the foldline performance, especially for large strings like base64-encoded inline attachements. In some cases (1MB string) from 7 Minutes to less than 20ms for ASCII data and 500ms for non-ASCII data. Ref: #163. [emfree] 3.9.0 (2015-03-24) ------------------ - Creating timezone objects from VTIMEZONE components. [geier] - Make ``python-dateutil`` a dependency. [geier] - Made RRULE tolerant of trailing semicolons. [sleeper] - Documentation fixes. [t-8ch, thet] 3.8.4 (2014-11-01) ------------------ - Add missing BYWEEKNO to recurrence rules. [russkel] 3.8.3 (2014-08-26) ------------------ - PERCENT property in VTODO renamed to PERCENT-COMPLETE, according to RFC5545. [thomascube] 3.8.2 (2014-07-22) ------------------ - Exclude editor backup files from egg distributions. Fixes #144. [thet] 3.8.1 (2014-07-17) ------------------ - The representation of CaselessDicts in 3.8 changed the name attribute of Components and therefore broke the external API. This has been fixed. [untitaker] 3.8 (2014-07-17) ---------------- - Allow dots in property names (Needed for vCard compatibility). Refs #143. [untitaker] - Change class representation for CaselessDict objects to always include the class name or the class' name attribute, if available. Also show subcomponents for Component objects. [thet] - Don't use data_encode for CaselessDict class representation but use dict's __repr__ method. [t-8ch] - Handle parameters with multiple values, which is needed for vCard 3.0. Refs #142. [t-8ch] 3.7 (2014-06-02) ---------------- - For components with ``ignore_exceptions`` set to ``True``, mark unparseable lines as broken instead rising a ``ValueError``. ``VEVENT`` components have ``ignore_exceptions`` set to ``True`` by default. Ref #131. Fixes #104. [jkiang13] - Make ``python-dateutil`` a soft-dependency. [boltnev] - Add optional ``sorted`` parameter to ``Component.to_ical``. Setting it to false allows the user to preserve the original property and parameter order. Ref #136. Fixes #133. [untitaker] - Fix tests for latest ``pytz``. Don't set ``tzinfo`` directly on datetime objects, but use pytz's ``localize`` function. Ref #138. [untitaker, thet] - Remove incorrect use of __all__. We don't encourage using ``from package import *`` imports. Fixes #129. [eric-wieser] 3.6.2 (2014-04-05) ------------------ - Pep8 and cleanup. [lasudry] 3.6.1 (2014-01-13) ------------------ - Open text files referenced by setup.py as utf-8, no matter what the locale settings are set to. Fixes #122. [sochotnicky] - Add tox.ini to source tarball, which simplifies testing for in distributions. [sochotnicky] 3.6 (2014-01-06) ---------------- - Python3 (3.3+) + Python 2 (2.6+) support [geier] - Made sure to_ical() always returns bytes [geier] - Support adding lists to a component property, which value already was a list and remove the Component.set method, which was only used by the add method. [thet] - Remove ability to add property parameters via a value's params attribute when adding via cal.add (that was only possible for custom value objects and makes up a strange API), but support a parameter attribute on cal.add's method signature to pass a dictionary with property parameter key/value pairs. Fixes #116. [thet] - Backport some of Regebro's changes from his regebro-refactor branch. [thet] - Raise explicit error on another malformed content line case. [hajdbo] - Correctly parse datetime component property values with timezone information when parsed from ical strings. [untitaker] 3.5 (2013-07-03) ---------------- - Let to_unicode be more graceful for non-unicode strings, as like CMFPlone's safe_unicode does it. [thet] 3.4 (2013-04-24) ---------------- - Switch to unicode internally. This should fix all en/decoding errors. [thet] - Support for non-ascii parameter values. Fixes #88. [warvariuc] - Added functions to transform chars in string with '\\' + any of r'\,;:' chars into '%{:02X}' form to avoid splitting on chars escaped with '\\'. [warvariuc] - Allow seconds in vUTCOffset properties. Fixes #55. [thet] - Let ``Component.decode`` better handle vRecur and vDDDLists properties. Fixes #70. [thet] - Don't let ``Component.add`` re-encode already encoded values. This simplifies the API, since there is no need explicitly pass ``encode=False``. Fixes #82. [thet] - Rename tzinfo_from_dt to tzid_from_dt, which is what it does. [thet] - More support for dateutil parsed tzinfo objects. Fixes #89. [leo-naeka] - Remove python-dateutil version fix at all. Current python-dateutil has Py3 and Py2 compatibility. [thet] - Declare the required python-dateutil dependency in setup.py. Fixes #90. [kleink] - Raise test coverage. [thet] - Remove interfaces module, as it is unused. [thet] - Remove ``test_doctests.py``, test suite already created properly in ``test_icalendar.py``. [rnix] - Transformed doctests into unittests, Test fixes and cleanup. [warvariuc] 3.3 (2013-02-08) ---------------- - Drop support for Python < 2.6. [thet] - Allow vGeo to be instantiated with list and not only tuples of geo coordinates. Fixes #83. [thet] - Don't force to pass a list to vDDDLists and allow setting individual RDATE and EXDATE values without having to wrap them in a list. [thet] - Fix encoding function to allow setting RDATE and EXDATE values and not to have bypass encoding with an icalendar property. [thet] - Allow setting of timezone for vDDDLists and support timezone properties for RDATE and EXDATE component properties. [thet] - Move setting of TZID properties to vDDDTypes, where it belongs to. [thet] - Use @staticmethod decorator instead of wrapper function. [warvariuc, thet] - Extend quoting of parameter values to all of those characters: ",;: ’'". This fixes an outlook incompatibility with some characters. Fixes: #79, Fixes: #81. [warvariuc] - Define VTIMETZONE subcomponents STANDARD and DAYLIGHT for RFC5545 compliance. [thet] 3.2 (2012-11-27) ---------------- - Documentation file layout restructuring. [thet] - Fix time support. vTime events can be instantiated with a datetime.time object, and do not inherit from datetime.time itself. [rdunklau] - Correctly handle tzinfo objects parsed with dateutil. Fixes #77. [warvariuc, thet] - Text values are escaped correclty. Fixes #74. [warvariuc] - Returned old folding algorithm, as the current implementation fails in some cases. Fixes #72, Fixes #73. [warvariuc] - Supports to_ical() on date/time properties for dates prior to 1900. [cdevienne] 3.1 (2012-09-05) ---------------- - Make sure parameters to certain properties propagate to the ical output. [kanarip] - Re-include doctests. [rnix] - Ensure correct datatype at instance creation time in ``prop.vCalAddress`` and ``prop.vText``. [rnix] - Apply TZID parameter to datetimes parsed from RECURRENCE-ID [dbstovall] - Localize datetimes for timezones to avoid DST transition errors. [dbstovall] - Allow UTC-OFFSET property value data types in seconds, which follows RFC5545 specification. [nikolaeff] - Remove utctz and normalized_timezone methods to simplify the codebase. The methods were too tiny to be useful and just used at one place. [thet] - When using Component.add() to add icalendar properties, force a value conversion to UTC for CREATED, DTSTART and LAST-MODIFIED. The RFC expects UTC for those properties. [thet] - Removed last occurrences of old API (from_string). [Rembane] - Add 'recursive' argument to property_items() to switch recursive listing. For example when parsing a text/calendar text including multiple components (e.g. a VCALENDAR with 5 VEVENTs), the previous situation required us to look over all properties in VEVENTs even if we just want the properties under the VCALENDAR component (VERSION, PRODID, CALSCALE, METHOD). [dmikurube] - All unit tests fixed. [mikaelfrykholm] 3.0.1b2 (2012-03-01) -------------------- - For all TZID parameters in DATE-TIME properties, use timezone identifiers (e.g. Europe/Vienna) instead of timezone names (e.g. CET), as required by RFC5545. Timezone names are used together with timezone identifiers in the Timezone components. [thet] - Timezone parsing, issues and test fixes. [mikaelfrykholm, garbas, tgecho] - Since we use pytz for timezones, also use UTC tzinfo object from the pytz library instead of own implementation. [thet] 3.0.1b1 (2012-02-24) -------------------- - Update Release information. [thet] 3.0 --- - Add API for proper Timezone support. Allow creating ical DATE-TIME strings with timezone information from Python datetimes with pytz based timezone information and vice versa. [thet] - Unify API to only use to_ical and from_ical and remove string casting as a requirement for Python 3 compatibility: New: to_ical. Old: ical, string, as_string and string casting via __str__ and str. New: from_ical. Old: from_string. [thet] 2.2 (2011-08-24) ---------------- - migration to https://github.com/collective/icalendar using svn2git preserving tags, branches and authors. [garbas] - using tox for testing on python 2.4, 2.5, 2.6, 2.6. [garbas] - fixed tests so they pass also under python 2.7. [garbas] - running tests on https://jenkins.plone.org/job/icalendar (only 2.6 for now) with some other metrics (pylint, clonedigger, coverage). [garbas] - review and merge changes from https://github.com/cozi/icalendar fork. [garbas] - created sphinx documentation and started documenting development and goals. [garbas] - hook out github repository to https://readthedocs.org service so sphinx documentation is generated on each commit (for master). Documentation can be visible on: https://icalendar.readthedocs.io/en/latest/ [garbas] 2.1 (2009-12-14) ---------------- - Fix deprecation warnings about ``object.__init__`` taking no parameters. - Set the VALUE parameter correctly for date values. - Long binary data would be base64 encoded with newlines, which made the iCalendar files incorrect. (This still needs testing). - Correctly handle content lines which include newlines. 2.0.1 (2008-07-11) ------------------ - Made the tests run under Python 2.5+ - Renamed the UTC class to Utc, so it would not clash with the UTC object, since that rendered the UTC object unpicklable. 2.0 (2008-07-11) ---------------- - EXDATE and RDATE now returns a vDDDLists object, which contains a list of vDDDTypes objects. This is do that EXDATE and RDATE can contain lists of dates, as per RFC. ***Note!***: This change is incompatible with earlier behavior, so if you handle EXDATE and RDATE you will need to update your code. - When createing a vDuration of -5 hours (which in itself is nonsensical), the ical output of that was -P1DT19H, which is correct, but ugly. Now it's '-PT5H', which is prettier. 1.2 (2006-11-25) ---------------- - Fixed a string index out of range error in the new folding code. 1.1 (2006-11-23) ---------------- - Fixed a bug in caselessdicts popitem. (thanks to Michael Smith ) - The RFC 2445 was a bit unclear on how to handle line folding when it happened to be in the middle of a UTF-8 character. This has been clarified in the following discussion: http://lists.osafoundation.org/pipermail/ietf-calsify/2006-August/001126.html And this is now implemented in iCalendar. It will not fold in the middle of a UTF-8 character, but may fold in the middle of a UTF-8 composing character sequence. 1.0 (2006-08-03) ---------------- - make get_inline and set_inline support non ascii codes. - Added support for creating a python egg distribution. 0.11 (2005-11-08) ----------------- - Changed component .from_string to use types_factory instead of hardcoding entries to 'inline' - Changed UTC tzinfo to a singleton so the same one is used everywhere - Made the parser more strict by using regular expressions for key name, param name and quoted/unquoted safe char as per the RFC - Added some tests from the schooltool icalendar parser for better coverage - Be more forgiving on the regex for folding lines - Allow for multiple top-level components on .from_string - Fix vWeekdays, wasn't accepting relative param (eg: -3SA vs -SA) - vDDDTypes didn't accept negative period (eg: -P30M) - 'N' is also acceptable as newline on content lines, per RFC 0.10 (2005-04-28) ----------------- - moved code to codespeak.net subversion. - reorganized package structure so that source code is under 'src' directory. Non-package files remain in distribution root. - redid doc/.py files as doc/.txt, using more modern doctest. Before they were .py files with big docstrings. - added test.py testrunner, and tests/test_icalendar.py that picks up all doctests in source code and doc directory, and runs them, when typing:: python2.3 test.py - renamed iCalendar to lower case package name, lowercased, de-pluralized and shorted module names, which are mostly implementation detail. - changed tests so they generate .ics files in a temp directory, not in the structure itself. icalendar-5.0.12/CONTRIBUTING.rst000066400000000000000000000014011457641316200161670ustar00rootroot00000000000000You 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 (https://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 Development Setup ----------------- If you would like to setup icalendar to contribute changes, the `Installation Section `_ should help you further. icalendar-5.0.12/LICENSE.rst000066400000000000000000000024561457641316200153550ustar00rootroot00000000000000License ======= Copyright (c) 2012-2013, Plone Foundation All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. icalendar-5.0.12/MANIFEST.in000066400000000000000000000002441457641316200152700ustar00rootroot00000000000000include *.rst tox.ini graft docs recursive-include src/icalendar * recursive-exclude src/icalendar *.pyc *~ recursive-exclude src/icalendar/fuzzing *.py *.sh *.ics icalendar-5.0.12/README.rst000066400000000000000000000077141457641316200152320ustar00rootroot00000000000000========================================================== Internet Calendaring and Scheduling (iCalendar) for Python ========================================================== The `icalendar`_ package is a `RFC 5545`_ compatible parser/generator for iCalendar files. ---- :Homepage: https://icalendar.readthedocs.io :Code: https://github.com/collective/icalendar :Mailing list: https://github.com/collective/icalendar/issues :Dependencies: `python-dateutil`_ and `pytz`_. :Compatible with: Python 2.7 and 3.4+ :License: `BSD`_ ---- .. image:: https://badge.fury.io/py/icalendar.svg :target: https://pypi.org/project/icalendar/ :alt: Python Package Version on PyPI .. image:: https://img.shields.io/pypi/dm/icalendar.svg :target: https://pypi.org/project/icalendar/#files :alt: Downloads from PyPI .. image:: https://img.shields.io/github/actions/workflow/status/collective/icalendar/tests.yml?branch=master&label=master&logo=github :target: https://github.com/collective/icalendar/actions/workflows/tests.yml?query=branch%3Amaster :alt: GitHub Actions build status for master .. image:: https://img.shields.io/github/actions/workflow/status/collective/icalendar/tests.yml?branch=4.x&label=4.x&logo=github :target: https://github.com/collective/icalendar/actions/workflows/tests.yml?query=branch%3A4.x++ :alt: GitHub Actions build status for 4.x .. image:: https://readthedocs.org/projects/icalendar/badge/?version=latest :target: https://icalendar.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. _`icalendar`: https://pypi.org/project/icalendar/ .. _`RFC 5545`: https://www.ietf.org/rfc/rfc5545.txt .. _`python-dateutil`: https://github.com/dateutil/dateutil/ .. _`pytz`: https://pypi.org/project/pytz/ .. _`BSD`: https://github.com/collective/icalendar/issues/2 Quick Guide ----------- To **install** the package, run:: pip install icalendar You can open an ``.ics`` file and see all the events:: >>> import icalendar >>> path_to_ics_file = "src/icalendar/tests/calendars/example.ics" >>> with open(path_to_ics_file) as f: ... calendar = icalendar.Calendar.from_ical(f.read()) >>> for event in calendar.walk('VEVENT'): ... print(event.get("SUMMARY")) New Year's Day Orthodox Christmas International Women's Day Using this package, you can also create calendars from scratch or edit existing ones. Versions and Compatibility -------------------------- ``icalendar`` is a critical project used by many. It has been there for a long time and maintaining long-term compatibility with projects conflicts partially with providing and using the features that the latest Python versions bring. Since we pour more `effort into maintaining and developing icalendar `__, we split the project into two: - `Branch 4.x `__ with maximum compatibility to Python versions ``2.7`` and ``3.4+``, ``PyPy2`` and ``PyPy3``. - `Branch master `__ with the compatibility to Python versions ``3.7+`` and ``PyPy3``. We expect the ``master`` branch with versions ``5+`` receive the latest updates and features, and the ``4.x`` branch the subset of security and bug fixes only. We recommend migrating to later Python versions and also providing feedback if you depend on the ``4.x`` features. Related projects ================ * `icalevents `_. It is built on top of icalendar and allows you to query iCal files and get the events happening on specific dates. It manages recurrent events as well. * `recurring-ical-events `_. Library to query an ``ICalendar`` object for events happening at a certain date or within a certain time. * `x-wr-timezone `_. Library to make ``ICalendar`` objects and files using the non-standard ``X-WR-TIMEZONE`` compliant with the standard (RFC 5545). icalendar-5.0.12/bootstrap.py000066400000000000000000000140161457641316200161230ustar00rootroot00000000000000############################################################################## # # Copyright (c) 2006 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Bootstrap a buildout-based project Simply run this script in a directory containing a buildout.cfg. The script accepts buildout command-line options, so you can use the -c option to specify an alternate configuration file. """ import os import shutil import sys import tempfile from optparse import OptionParser tmpeggs = tempfile.mkdtemp() usage = '''\ [DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] Bootstraps a buildout-based project. Simply run this script in a directory containing a buildout.cfg, using the Python that you want bin/buildout to use. Note that by using --find-links to point to local resources, you can keep this script from going over the network. ''' parser = OptionParser(usage=usage) parser.add_option("-v", "--version", help="use a specific zc.buildout version") parser.add_option("-t", "--accept-buildout-test-releases", dest='accept_buildout_test_releases', action="store_true", default=False, help=("Normally, if you do not specify a --version, the " "bootstrap script and buildout gets the newest " "*final* versions of zc.buildout and its recipes and " "extensions for you. If you use this flag, " "bootstrap and buildout will get the newest releases " "even if they are alphas or betas.")) parser.add_option("-c", "--config-file", help=("Specify the path to the buildout configuration " "file to be used.")) parser.add_option("-f", "--find-links", help=("Specify a URL to search for buildout releases")) parser.add_option("--allow-site-packages", action="store_true", default=False, help=("Let bootstrap.py use existing site packages")) options, args = parser.parse_args() ###################################################################### # load/install setuptools try: if options.allow_site_packages: import setuptools import pkg_resources from urllib.request import urlopen except ImportError: from urllib2 import urlopen ez = {} exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez) if not options.allow_site_packages: # ez_setup imports site, which adds site packages # this will remove them from the path to ensure that incompatible versions # of setuptools are not in the path import site # inside a virtualenv, there is no 'getsitepackages'. # We can't remove these reliably if hasattr(site, 'getsitepackages'): for sitepackage_path in site.getsitepackages(): sys.path[:] = [x for x in sys.path if sitepackage_path not in x] setup_args = {'to_dir': tmpeggs, 'download_delay': 0} ez['use_setuptools'](**setup_args) import setuptools import pkg_resources # This does not (always?) update the default working set. We will # do it. for path in sys.path: if path not in pkg_resources.working_set.entries: pkg_resources.working_set.add_entry(path) ###################################################################### # Install buildout ws = pkg_resources.working_set cmd = [sys.executable, '-c', 'from setuptools.command.easy_install import main; main()', '-mZqNxd', tmpeggs] find_links = os.environ.get( 'bootstrap-testing-find-links', options.find_links or ('https://downloads.buildout.org/' if options.accept_buildout_test_releases else None) ) if find_links: cmd.extend(['-f', find_links]) setuptools_path = ws.find( pkg_resources.Requirement.parse('setuptools')).location requirement = 'zc.buildout' version = options.version if version is None and not options.accept_buildout_test_releases: # Figure out the most recent final version of zc.buildout. import setuptools.package_index _final_parts = '*final-', '*final' def _final_version(parsed_version): for part in parsed_version: if (part[:1] == '*') and (part not in _final_parts): return False return True index = setuptools.package_index.PackageIndex( search_path=[setuptools_path]) if find_links: index.add_find_links((find_links,)) req = pkg_resources.Requirement.parse(requirement) if index.obtain(req) is not None: best = [] bestv = None for dist in index[req.project_name]: distv = dist.parsed_version if _final_version(distv): if bestv is None or distv > bestv: best = [dist] bestv = distv elif distv == bestv: best.append(dist) if best: best.sort() version = best[-1].version if version: requirement = '=='.join((requirement, version)) cmd.append(requirement) import subprocess if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0: raise Exception( "Failed to execute command:\n%s" % repr(cmd)[1:-1]) ###################################################################### # Import and run buildout ws.add_entry(tmpeggs) ws.require(requirement) import zc.buildout.buildout if not [a for a in args if '=' not in a]: args.append('bootstrap') # if -c was provided, we push it back into args for buildout' main function if options.config_file is not None: args[0:0] = ['-c', options.config_file] zc.buildout.buildout.main(args) shutil.rmtree(tmpeggs) icalendar-5.0.12/buildout.cfg000066400000000000000000000006641457641316200160500ustar00rootroot00000000000000[buildout] develop = . parts = test py coverage [test] recipe = zc.recipe.testrunner eggs = icalendar[test] defaults = ['--auto-color', '--auto-progress'] [py] recipe = zc.recipe.egg interpreter = py eggs = ${test:eggs} [coverage] recipe = collective.recipe.template input = inline: #!/bin/sh ./bin/test --coverage ../../coverage -v --auto-progress "$@" output = ${buildout:directory}/bin/coverage mode = 755 icalendar-5.0.12/docs/000077500000000000000000000000001457641316200144625ustar00rootroot00000000000000icalendar-5.0.12/docs/Makefile000066400000000000000000000131601457641316200161230ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build LOCALESDIR = _locales # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in docs/$(BUILDDIR)/html." rm -rf build manual: *.rst $(SPHINXBUILD) -b html -t manual . manual presentation: *.rst $(SPHINXBUILD) -b html -t presentation . presentation dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/icalendar.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/icalendar.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/icalendar" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/icalendar" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(LOCALESDIR) @echo @echo "Build finished. The message catalogs are in $(LOCALESDIR)." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." icalendar-5.0.12/docs/about.rst000066400000000000000000000010461457641316200163270ustar00rootroot00000000000000About ===== `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. The icalendar package is an RFC 5545-compatible parser/generator for iCalendar files. .. _`Max M`: http://www.mxm.dk .. _`RFC 2445`: https://tools.ietf.org/html/rfc2445 .. _`RFC 5545`: https://tools.ietf.org/html/rfc5545 icalendar-5.0.12/docs/api.rst000066400000000000000000000002611457641316200157640ustar00rootroot00000000000000API Reference ------------- icalendar.cal +++++++++++++ .. automodule:: icalendar.cal :members: icalendar.prop ++++++++++++++ .. automodule:: icalendar.prop :members: icalendar-5.0.12/docs/changelog.rst000066400000000000000000000000341457641316200171400ustar00rootroot00000000000000.. include:: ../CHANGES.rst icalendar-5.0.12/docs/cli.rst000066400000000000000000000015551457641316200157710ustar00rootroot00000000000000iCalendar utility ================= To get more information about the command line interface, use the ``-h`` option:: $ icalendar -h view ---- To output a human readable summary of an event:: $ icalendar myfile.ics The output will look something like this:: Organiser: Secretary Attendees: John Doe Randy Summary: Yearly evaluation. When: Tue 14 Mar 2017 11:00-12:00 Location: Randy's office Comment: Reminder. Description: Your yearly evaluation is scheduled for next Tuesday. Please be on time. To use this in terminal based e-mail clients like mutt, add a new mime type (as root):: # cat << EOF > /usr/lib/mime/packages/icalendar text/calendar; icalendar '%s'; copiousoutput; description=iCalendar text; priority=2 EOF # update-mime icalendar-5.0.12/docs/conf.py000066400000000000000000000020451457641316200157620ustar00rootroot00000000000000# icalendar documentation build configuration file import pkg_resources import datetime import os on_rtd = os.environ.get('READTHEDOCS', None) == 'True' try: import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] except ImportError: html_theme = 'default' if not on_rtd: print('-' * 74) print('Warning: sphinx-rtd-theme not installed, building with default ' 'theme.') print('-' * 74) extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.viewcode' ] source_suffix = '.rst' master_doc = 'index' project = 'icalendar' this_year = datetime.date.today().year copyright = f'{this_year}, Plone Foundation' version = pkg_resources.get_distribution('icalendar').version release = version exclude_patterns = ['_build', 'lib', 'bin', 'include', 'local'] pygments_style = 'sphinx' htmlhelp_basename = 'icalendardoc' man_pages = [ ('index', 'icalendar', 'icalendar Documentation', ['Plone Foundation'], 1) ] icalendar-5.0.12/docs/contributing.rst000066400000000000000000000001501457641316200177170ustar00rootroot00000000000000.. _contributing: ------------------ Contributing ------------------ .. include:: ../CONTRIBUTING.rst icalendar-5.0.12/docs/credits.rst000066400000000000000000000054321457641316200166550ustar00rootroot00000000000000icalendar contributors ====================== - Johannes Raggam (Maintainer) - Max M (Original author) - Andreas Zeidler - Andrey Nikolaev - Barak Michener - Christian Geier - Christophe de Vienne - Dai MIKURUBE - Dan Stovall - Eric Hanchrow - Eric Wieser - Erik Simmler - George V. Reilly - Jannis Leidel - Jeroen F.J. Laros - Jeroen van Meeuwen (Kolab Systems) - Jochen Sprickerhof - Jordan Kiang - Klaus Klein - Laurent Lasudry - Lennart Regebro - Léo S - Marc Egli - Markus Unterwaditzer - Martijn Faassen - Martin Melin - Michael Smith - Mikael Frykholm - Olivier Grisel - Pavel Repin - Pedro Ferreira - Rembane - Robert Niederreiter - Rok Garbas - Ronan Dunklau - Russ - Sidnei da Silva - Stanislav Láznička - Stanislav Ochotnicky - Stefan Schwarzer - Thomas Bruederli - Thomas Weißschuh - Victor Varvaryuk - Ville Skyttä - Wichert Akkerman - cillianderoiste - fitnr - hajdbo - ilya - spanktar - tgecho - tisto - TomTry - Andreas Ruppen - Clive Stevens - Dalton Durst - Kamil Mańkowski - Tobias Brox - `Nicco Kunzmann `_ - Robert Spralja - Maurits van Rees - jacadzaca - Mauro Amico - Alexander Pitkin - Michał Górny - Pronoy - Abe Hanoka - `Natasha Mattson `_ - Matt Lewis - Felix Stupp Find out who contributed:: $ git shortlog -s -e icalendar-5.0.12/docs/index.rst000066400000000000000000000003461457641316200163260ustar00rootroot00000000000000.. include:: ../README.rst Contents ======== .. toctree:: :maxdepth: 2 about install usage api cli credits maintenance license changelog .. toctree:: :titlesonly: contributing icalendar-5.0.12/docs/install.rst000066400000000000000000000065101457641316200166640ustar00rootroot00000000000000Installing iCalendar ==================== To install the icalendar package, use:: pip install icalendar If installation is successful, you will be able to import the iCalendar package, like this:: >>> import icalendar Development Setup ----------------- To start contributing changes to icalendar, you can clone the project to your file system using Git. You can `fork `_ the project first and clone your fork, too. .. code-block:: bash git clone https://github.com/collective/icalendar.git cd icalendar Installing Python ----------------- You will need a version of Python installed to run the tests and execute the code. The latest version of Python 3 should work and will be enough to get you started. If you like to run the tests with different Python versions, the following setup process should work the same. Install Tox ----------- First, install `tox `_.. .. code-block:: bash pip install tox From now on, tox will manage Python versions and test commands for you. Running Tests ------------- ``tox`` manages all test environments in all Python versions. To run all tests in all environments, simply run ``tox`` .. code-block:: bash tox You may not have all Python versions installed or you may want to run a specific one. Have a look at the `documentation `__. This is how you can run ``tox`` with Python 3.9: .. code-block:: bash tox -e py39 Accessing a ``tox`` environment ------------------------------- If you like to enter a specific tox environment, you can do this: .. code-block:: bash source .tox/py39/bin/activate Install ``icalendar`` Manually ------------------------------- The best way to test the package is to use ``tox`` as described above. If for some reason you cannot install ``tox``, you can go ahead with the following section using your installed version of Python and ``pip``. If for example, you would like to use your local copy of icalendar in another Python environment, this section explains how to do it. You can install the local copy of ``icalendar`` with ``pip`` like this: .. code-block:: bash cd icalendar python -m pip install -e . This installs the module and dependencies in your Python environment so that you can access local changes. If tox fails to install ``icalendar`` during its first run, you can activate the environment in the ``.tox`` folder and manually setup ``icalendar`` like this. Try it out: .. code-block:: python Python 3.9.5 (default, Nov 23 2021, 15:27:38) Type "help", "copyright", "credits" or "license" for more information. >>> import icalendar >>> icalendar.__version__ '5.0.12' Building the documentation locally ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To build the documentation follow these steps: .. code-block:: bash $ source .tox/py39/bin/activate $ pip install -r requirements_docs.txt $ cd docs $ make html You can now open the output from ``_build/html/index.html``. To build the presentation-version use ``make presentation`` instead of ``make html``. You can open the presentation at ``presentation/index.html``. You can also use ``tox`` to build the documentation: .. code-block:: bash cd icalendar tox -e docs icalendar-5.0.12/docs/license.rst000066400000000000000000000000341457641316200166330ustar00rootroot00000000000000.. include:: ../LICENSE.rst icalendar-5.0.12/docs/maintenance.rst000066400000000000000000000124661457641316200175070ustar00rootroot00000000000000Maintenance =========== The goal of this section is to make sure that the ``icalendar`` library receives a clear maintenance structure with it that is transparent. Maintainers ----------- Currently, the maintainers are - `@geier `_ - `@jacadzaca `_ - `@niccokunzmann `_ Maintainers need this: - ``Admin`` access to the `repository `_. These can be enabled by a current maintainer or an GitHub organisation administrator in the `settings `_. - ``Maintainer`` or ``Owner`` access to the `PyPI project `_. The new maintainer needs a PyPI account for this with Two Factor Authentication (2FA) enabled because ``icalendar`` is a critical project on PyPI. The access can be given in the `collaboration Section `_ on PyPI. - ``Maintainer`` access to the `Read The Docs project `_. This can be given by existing maintainers listed on the project's page. TODO: link to the settings - ``PyPI environment access for GitHub Actions`` grant new releases from tags. This access can be granted in `Settings → Environments → PyPI `__ by adding the GitHub username to the list of "Required Reviewers". Collaborators ------------- Collaborators are people with write access to the repository. As a collaborator, you can - merge Pull Requests, - initiate a new release, see below. We want to have as many knowledgeable people with write access taking responsibility as possible for these reasons: - a constant flow of fixes and new features - better code review - lower bus factor and backup - future maintainers Nobody should merge their own pull requests. If you like to review or merge pull requests of other people and you have shown that you know how to contribute, you can ask for becoming a collaborator or a maintainer asks you if you would like to become one. New Releases ------------ This explains how to create a new release on `PyPI `_. Since collaborators and maintainers have write access the the repository, they can start the release process. However, only people with ``PyPI environment access for GitHub Actions`` can approve an automated release to PyPI. 1. Check that the ``CHANGES.rst`` is up to date with the `latest merged pull requests `__ and the version you want to release is correctly named. 2. Change the ``__version__`` variable in - the ``src/icalendar/__init__.py`` file and - in the ``docs/install.rst`` file (look for ``icalendar.__version__``) 3. Create a commit on the ``release`` branch (or equivalent) to release this version. .. code-block:: bash git checkout master git pull git checkout -b release master git add CHANGES.rst src/icalendar/__init__.py docs/install.rst git commit -m"version 5.0.0" 4. Push the commit and `create a pull request `__ Here is an `example pull request #457 `__. .. code-block:: bash git push -u origin release 5. See if the `CI-tests `_ are running on the pull request. If they are not running, no new release can be issued. If the tests are running, merge the pull request. 6. Clean up behind you! .. code-block:: bash git checkout master git pull git branch -d release git push -d origin release 7. Create a tag for the release and see if the `CI-tests`_ are running. .. code-block:: bash git checkout master git pull git tag v5.0.0 git push upstream v5.0.0 # could be origin or whatever reference 8. Once the tag is pushed and its `CI-tests`_ are passing, maintainers will get an e-mail:: Subject: Deployment review in collective/icalendar tests: PyPI is waiting for your review 9. If the release is approved by a maintainer. It will be pushed to `PyPI`_. If that happens, notify the issues that were fixed about this release. 10. Copy this to the start of ``CHANGES.rst``:: 5.0.2 (unreleased) ------------------ Minor changes: - ... Breaking changes: - ... New features: - ... Bug fixes: - ... 11. Push the new CHANGELOG so it is used for future changes. .. code-block:: bash git checkout master git pull git add CHANGES.rst git commit -m"Add new CHANGELOG section for future release See https://icalendar.readthedocs.io/en/latest/maintenance.html#new-releases" git push upstream master # could be origin or whatever reference Links ----- This section contains useful links for maintainers and collaborators: - `Future of icalendar, looking for maintainer #360 `__ - `Comment on the Plone tests running with icalendar `__ icalendar-5.0.12/docs/usage.rst000066400000000000000000000240771457641316200163320ustar00rootroot00000000000000iCalendar 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 ('DTSTART', '20050404T080000') ('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() b'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().decode("utf-8").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({'UID': '42', 'DTSTART': '20050404T080000'})] Value types ----------- Property values are utf-8 encoded strings. This is impractical if you want to use the data for further computation. The datetime format for example looks like this: '20050404T080000'. But the package makes it simple to parse and generate iCalendar formatted strings. Basically you can make the add() method do the thinking, or you can do it yourself. To add a datetime value, you can use Pythons built in datetime types, and the set the encode parameter to true, and it will convert to the type defined in the spec:: >>> from datetime import datetime >>> cal.add('dtstart', datetime(2005,4,4,8,0,0)) >>> cal['dtstart'].to_ical() b'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() b'20050404T080000' So the drill is to initialise the object with a python built in type, and then call the "to_ical()" method on the object. That will return an ical encoded string. You can do it the other way around too. To parse an encoded string, just call the "from_ical()" method, and it will return an instance of the corresponding Python type:: >>> vDatetime.from_ical('20050404T080000') datetime.datetime(2005, 4, 4, 8, 0) >>> dt = vDatetime.from_ical('20050404T080000Z') >>> repr(dt)[:62] 'datetime.datetime(2005, 4, 4, 8, 0, tzinfo=)' You can also choose to use the decoded() method, which will return a decoded value directly:: >>> cal = Calendar() >>> cal.add('dtstart', datetime(2005,4,4,8,0,0)) >>> cal['dtstart'].to_ical() b'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:: >>> import pytz >>> event = Event() >>> event.add('dtstart', datetime(2010, 10, 10, 10, 0, 0, ... tzinfo=pytz.timezone("Europe/Vienna"))) >>> lines = event.to_ical().splitlines() >>> assert ( ... b"DTSTART;TZID=Europe/Vienna: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() >>> assert 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 >>> import pytz 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:: >>> 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()) 522 >>> f.close() Print out the calendar:: >>> print(cal.to_ical().decode("utf-8")) # doctest: +NORMALIZE_WHITESPACE BEGIN:VCALENDAR VERSION:2.0 PRODID:-//My calendar product//mxm.dk// BEGIN:VEVENT SUMMARY:Python meeting about calendaring DTSTART:20050404T080000Z DTEND:20050404T100000Z DTSTAMP:20050404T001000Z UID:20050115T101010/27346262376@mxm.dk ATTENDEE;CN="Max Rasmussen";ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com ATTENDEE;CN="The Dude";ROLE=REQ-PARTICIPANT:MAILTO:the-dude@example.com LOCATION:Odense\, Denmark ORGANIZER;CN="Max Rasmussen";ROLE=CHAIR:MAILTO:noone@example.com PRIORITY:5 END:VEVENT END:VCALENDAR More documentation ================== Have a look at the `tests `__ of this package to get more examples. All modules and classes docstrings, which document how they work. icalendar-5.0.12/requirements_docs.txt000066400000000000000000000000411457641316200200210ustar00rootroot00000000000000Sphinx>=1.2.3 sphinx_rtd_theme . icalendar-5.0.12/setup.cfg000066400000000000000000000005001457641316200153460ustar00rootroot00000000000000[check-manifest] ignore = *.cfg .readthedocs.yml bootstrap.py requirements_docs.txt [zest.releaser] python-file-with-version = src/icalendar/__init__.py [bdist_wheel] universal = 0 [metadata] license_files = LICENSE.rst [tool:pytest] norecursedirs = .* env* docs *.egg src/icalendar/tests/hypothesis icalendar-5.0.12/setup.py000066400000000000000000000042551457641316200152520ustar00rootroot00000000000000import codecs import setuptools import re import ast _version_re = re.compile(r'__version__\s+=\s+(.*)') with open('src/icalendar/__init__.py', 'rb') as f: version = str(ast.literal_eval(_version_re.search( f.read().decode('utf-8')).group(1))) shortdesc = 'iCalendar parser/generator' longdesc = '' for fname in ['README.rst', 'CONTRIBUTING.rst', 'CHANGES.rst', 'LICENSE.rst']: with codecs.open(fname, encoding='utf-8') as f: longdesc += f.read() longdesc += '\n' tests_require = [] install_requires = [ 'python-dateutil', 'pytz', # install requirements depending on python version # see https://www.python.org/dev/peps/pep-0508/#environment-markers 'backports.zoneinfo; python_version == "3.7" or python_version == "3.8"', ] setuptools.setup( name='icalendar', version=version, description=shortdesc, long_description=longdesc, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ], keywords='calendar calendaring ical icalendar event todo journal ' 'recurring', author='Plone Foundation', author_email='plone-developers@lists.sourceforge.net', url='https://github.com/collective/icalendar', license='BSD-2-Clause', packages=setuptools.find_namespace_packages('src', exclude=["icalendar.fuzzing"]), package_dir={'': 'src'}, include_package_data=True, zip_safe=False, python_requires=">=3.7", install_requires=install_requires, entry_points = {'console_scripts': ['icalendar = icalendar.cli:main']}, extras_require={ 'test': tests_require }, test_suite='icalendar.tests' ) icalendar-5.0.12/src/000077500000000000000000000000001457641316200143215ustar00rootroot00000000000000icalendar-5.0.12/src/icalendar/000077500000000000000000000000001457641316200162435ustar00rootroot00000000000000icalendar-5.0.12/src/icalendar/__init__.py000066400000000000000000000014061457641316200203550ustar00rootroot00000000000000__version__ = '5.0.12' from icalendar.cal import ( Calendar, Event, Todo, Journal, Timezone, TimezoneStandard, TimezoneDaylight, FreeBusy, Alarm, ComponentFactory, ) # Property Data Value Types from icalendar.prop import ( vBinary, vBoolean, vCalAddress, vDatetime, vDate, vDDDTypes, vDuration, vFloat, vInt, vPeriod, vWeekday, vFrequency, vRecur, vText, vTime, vUri, vGeo, vUTCOffset, TypesFactory, ) # useful tzinfo subclasses from icalendar.prop import ( FixedOffset, LocalTimezone, ) # Parameters and helper methods for splitting and joining string with escaped # chars. from icalendar.parser import ( Parameters, q_split, q_join, ) icalendar-5.0.12/src/icalendar/cal.py000066400000000000000000000666351457641316200173740ustar00rootroot00000000000000"""Calendar is a dictionary like Python object that can render itself as VCAL files according to rfc2445. These are the defined components. """ from datetime import datetime, timedelta from icalendar.caselessdict import CaselessDict from icalendar.parser import Contentline from icalendar.parser import Contentlines from icalendar.parser import Parameters from icalendar.parser import q_join from icalendar.parser import q_split from icalendar.parser_tools import DEFAULT_ENCODING from icalendar.prop import TypesFactory from icalendar.prop import vText, vDDDLists from icalendar.timezone_cache import _timezone_cache import pytz import dateutil.rrule, dateutil.tz from pytz.tzinfo import DstTzInfo ###################################### # The component factory class ComponentFactory(CaselessDict): """All components defined in rfc 2445 are registered in this factory class. To get a component you can use it like this. """ def __init__(self, *args, **kwargs): """Set keys to upper for initial dict. """ super().__init__(*args, **kwargs) self['VEVENT'] = Event self['VTODO'] = Todo self['VJOURNAL'] = Journal self['VFREEBUSY'] = FreeBusy self['VTIMEZONE'] = Timezone self['STANDARD'] = TimezoneStandard self['DAYLIGHT'] = TimezoneDaylight self['VALARM'] = Alarm self['VCALENDAR'] = Calendar # These Properties have multiple property values inlined in one propertyline # seperated by comma. Use CaselessDict as simple caseless set. INLINE = CaselessDict({ 'CATEGORIES': 1, 'RESOURCES': 1, 'FREEBUSY': 1, }) _marker = [] class Component(CaselessDict): """Component is the base object for calendar, Event and the other components defined in RFC 2445. Normally you will not use this class directly, but rather one of the subclasses. """ name = None # should be defined in each component required = () # These properties are required singletons = () # These properties must only appear once multiple = () # may occur more than once exclusive = () # These properties are mutually exclusive inclusive = () # if any occurs the other(s) MUST occur # ('duration', 'repeat') ignore_exceptions = False # if True, and we cannot parse this # component, we will silently ignore # it, rather than let the exception # propagate upwards # not_compliant = [''] # List of non-compliant properties. def __init__(self, *args, **kwargs): """Set keys to upper for initial dict. """ super().__init__(*args, **kwargs) # set parameters here for properties that use non-default values self.subcomponents = [] # Components can be nested. self.errors = [] # If we ignored exception(s) while # parsing a property, contains error strings # def is_compliant(self, name): # """Returns True is the given property name is compliant with the # icalendar implementation. # # If the parser is too strict it might prevent parsing erroneous but # otherwise compliant properties. So the parser is pretty lax, but it is # possible to test for non-compliance by calling this method. # """ # return name in not_compliant def __bool__(self): """Returns True, CaselessDict would return False if it had no items. """ return True # python 2 compatibility __nonzero__ = __bool__ def is_empty(self): """Returns True if Component has no items or subcomponents, else False. """ return True if not (list(self.values()) + self.subcomponents) else False # noqa @property def is_broken(self): return bool(self.errors) ############################# # handling of property values @staticmethod def _encode(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. obj = value else: klass = types_factory.for_property(name) obj = klass(value) if parameters: if not hasattr(obj, "params"): obj.params = Parameters() for key, item in parameters.items(): if item is None: if key in obj.params: del obj.params[key] else: obj.params[key] = item return obj def add(self, name, value, parameters=None, encode=1): """Add a property. :param name: Name of the property. :type name: string :param value: Value of the property. Either of a basic Python type of any of the icalendar's own property types. :type value: Python native type or icalendar property type. :param parameters: Property parameter dictionary for the value. Only available, if encode is set to True. :type parameters: Dictionary :param encode: True, if the value should be encoded to one of icalendar's own property types (Fallback is "vText") or False, if not. :type encode: Boolean :returns: None """ if isinstance(value, datetime) and\ name.lower() in ('dtstamp', 'created', 'last-modified'): # RFC expects UTC for those... force value conversion. if getattr(value, 'tzinfo', False) and value.tzinfo is not None: value = value.astimezone(pytz.utc) else: # assume UTC for naive datetime instances value = pytz.utc.localize(value) # encode value if encode and isinstance(value, list) \ and name.lower() not in ['rdate', 'exdate', 'categories']: # Individually convert each value to an ical type except rdate and # exdate, where lists of dates might be passed to vDDDLists. value = [self._encode(name, v, parameters, encode) for v in value] else: value = self._encode(name, value, parameters, encode) # set value if name in self: # If property already exists, append it. oldval = self[name] if isinstance(oldval, list): if isinstance(value, list): value = oldval + value else: oldval.append(value) value = oldval else: value = [oldval, value] self[name] = value def _decode(self, name, value): """Internal for decoding property values. """ # TODO: Currently the decoded method calls the icalendar.prop instances # from_ical. We probably want to decode properties into Python native # types here. But when parsing from an ical string with from_ical, we # want to encode the string into a real icalendar.prop property. if isinstance(value, vDDDLists): # TODO: Workaround unfinished decoding return value decoded = types_factory.from_ical(name, value) # TODO: remove when proper decoded is implemented in every prop.* class # Workaround to decode vText properly if isinstance(decoded, vText): decoded = decoded.encode(DEFAULT_ENCODING) return decoded def decoded(self, name, default=_marker): """Returns decoded value of property. """ # XXX: fail. what's this function supposed to do in the end? # -rnix if name in self: value = self[name] if isinstance(value, list): return [self._decode(name, v) for v in value] return self._decode(name, value) else: if default is _marker: raise KeyError(name) else: return default ######################################################################## # Inline values. A few properties have multiple values inlined in in one # property line. These methods are used for splitting and joining these. def get_inline(self, name, decode=1): """Returns a list of values (split on comma). """ vals = [v.strip('" ') for v in q_split(self[name])] if decode: return [self._decode(name, val) for val in vals] return vals def set_inline(self, name, values, encode=1): """Converts a list of values into comma separated string and sets value to that. """ if encode: values = [self._encode(name, value, encode=1) for value in values] self[name] = types_factory['inline'](q_join(values)) ######################### # Handling of components def add_component(self, component): """Add a subcomponent to this component. """ self.subcomponents.append(component) def _walk(self, name): """Walk to given component. """ result = [] if name is None or self.name == name: result.append(self) for subcomponent in self.subcomponents: result += subcomponent._walk(name) return result def walk(self, name=None): """Recursively traverses component and subcomponents. Returns sequence of same. If name is passed, only components with name will be returned. """ if name is not None: name = name.upper() return self._walk(name) ##################### # Generation def property_items(self, recursive=True, sorted=True): """Returns properties in this component and subcomponents as: [(name, value), ...] """ vText = types_factory['text'] properties = [('BEGIN', vText(self.name).to_ical())] if sorted: property_names = self.sorted_keys() else: property_names = self.keys() for name in property_names: values = self[name] if isinstance(values, list): # normally one property is one line for value in values: properties.append((name, value)) else: properties.append((name, values)) if recursive: # recursion is fun! for subcomponent in self.subcomponents: properties += subcomponent.property_items(sorted=sorted) properties.append(('END', vText(self.name).to_ical())) return properties @classmethod def from_ical(cls, st, multiple=False): """Populates the component recursively from a string. """ stack = [] # a stack of components comps = [] for line in Contentlines.from_ical(st): # raw parsing if not line: continue try: name, params, vals = line.parts() except ValueError as e: # if unable to parse a line within a component # that ignores exceptions, mark the component # as broken and skip the line. otherwise raise. component = stack[-1] if stack else None if not component or not component.ignore_exceptions: raise component.errors.append((None, str(e))) continue uname = name.upper() # check for start of component if uname == 'BEGIN': # try and create one of the components defined in the spec, # otherwise get a general Components for robustness. c_name = vals.upper() c_class = component_factory.get(c_name, Component) # If component factory cannot resolve ``c_name``, the generic # ``Component`` class is used which does not have the name set. # That's opposed to the usage of ``cls``, which represents a # more concrete subclass with a name set (e.g. VCALENDAR). component = c_class() if not getattr(component, 'name', ''): # undefined components component.name = c_name stack.append(component) # check for end of event elif uname == 'END': # we are done adding properties to this component # so pop it from the stack and add it to the new top. if not stack: # The stack is currently empty, the input must be invalid raise ValueError('END encountered without an accompanying BEGIN!') component = stack.pop() if not stack: # we are at the end comps.append(component) else: stack[-1].add_component(component) if vals == 'VTIMEZONE' and \ 'TZID' in component and \ component['TZID'] not in pytz.all_timezones and \ component['TZID'] not in _timezone_cache: _timezone_cache[component['TZID']] = component.to_tz() # we are adding properties to the current top of the stack else: factory = types_factory.for_property(name) component = stack[-1] if stack else None if not component: raise ValueError(f'Property "{name}" does not have a parent component.') datetime_names = ('DTSTART', 'DTEND', 'RECURRENCE-ID', 'DUE', 'RDATE', 'EXDATE') try: if name == 'FREEBUSY': vals = vals.split(',') if 'TZID' in params: parsed_components = [factory(factory.from_ical(val, params['TZID'])) for val in vals] else: parsed_components = [factory(factory.from_ical(val)) for val in vals] elif name in datetime_names and 'TZID' in params: parsed_components = [factory(factory.from_ical(vals, params['TZID']))] else: parsed_components = [factory(factory.from_ical(vals))] except ValueError as e: if not component.ignore_exceptions: raise component.errors.append((uname, str(e))) else: for parsed_component in parsed_components: parsed_component.params = params component.add(name, parsed_component, encode=0) if multiple: return comps if len(comps) > 1: raise ValueError(cls._format_error( 'Found multiple components where only one is allowed', st)) if len(comps) < 1: raise ValueError(cls._format_error( 'Found no components where exactly one is required', st)) return comps[0] def _format_error(error_description, bad_input, elipsis='[...]'): # there's three character more in the error, ie. ' ' x2 and a ':' max_error_length = 100 - 3 if len(error_description) + len(bad_input) + len(elipsis) > max_error_length: truncate_to = max_error_length - len(error_description) - len(elipsis) return f'{error_description}: {bad_input[:truncate_to]} {elipsis}' else: return f'{error_description}: {bad_input}' def content_line(self, name, value, sorted=True): """Returns property as content line. """ params = getattr(value, 'params', Parameters()) return Contentline.from_parts(name, params, value, sorted=sorted) def content_lines(self, sorted=True): """Converts the Component and subcomponents into content lines. """ contentlines = Contentlines() for name, value in self.property_items(sorted=sorted): cl = self.content_line(name, value, sorted=sorted) contentlines.append(cl) contentlines.append('') # remember the empty string in the end return contentlines def to_ical(self, sorted=True): ''' :param sorted: Whether parameters and properties should be lexicographically sorted. ''' content_lines = self.content_lines(sorted=sorted) return content_lines.to_ical() def __repr__(self): """String representation of class with all of it's subcomponents. """ subs = ', '.join(str(it) for it in self.subcomponents) return f"{self.name or type(self).__name__}({dict(self)}{', ' + subs if subs else ''})" def __eq__(self, other): if len(self.subcomponents) != len(other.subcomponents): return False properties_equal = super().__eq__(other) if not properties_equal: return False # The subcomponents might not be in the same order, # neither there's a natural key we can sort the subcomponents by nor # are the subcomponent types hashable, so we cant put them in a set to # check for set equivalence. We have to iterate over the subcomponents # and look for each of them in the list. for subcomponent in self.subcomponents: if subcomponent not in other.subcomponents: return False return True ####################################### # components defined in RFC 5545 class Event(Component): name = 'VEVENT' canonical_order = ( 'SUMMARY', 'DTSTART', 'DTEND', 'DURATION', 'DTSTAMP', 'UID', 'RECURRENCE-ID', 'SEQUENCE', 'RRULE', 'RDATE', 'EXDATE', ) required = ('UID', 'DTSTAMP',) singletons = ( 'CLASS', 'CREATED', 'DESCRIPTION', 'DTSTART', 'GEO', 'LAST-MODIFIED', 'LOCATION', 'ORGANIZER', 'PRIORITY', 'DTSTAMP', 'SEQUENCE', 'STATUS', 'SUMMARY', 'TRANSP', 'URL', 'RECURRENCE-ID', 'DTEND', 'DURATION', 'UID', 'CATEGORIES', ) exclusive = ('DTEND', 'DURATION',) multiple = ( 'ATTACH', 'ATTENDEE', 'COMMENT', 'CONTACT', 'EXDATE', 'RSTATUS', 'RELATED', 'RESOURCES', 'RDATE', 'RRULE' ) ignore_exceptions = True class Todo(Component): name = 'VTODO' required = ('UID', 'DTSTAMP',) singletons = ( 'CLASS', 'COMPLETED', 'CREATED', 'DESCRIPTION', 'DTSTAMP', 'DTSTART', 'GEO', 'LAST-MODIFIED', 'LOCATION', 'ORGANIZER', 'PERCENT-COMPLETE', 'PRIORITY', 'RECURRENCE-ID', 'SEQUENCE', 'STATUS', 'SUMMARY', 'UID', 'URL', 'DUE', 'DURATION', ) exclusive = ('DUE', 'DURATION',) multiple = ( 'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE', 'RSTATUS', 'RELATED', 'RESOURCES', 'RDATE', 'RRULE' ) class Journal(Component): name = 'VJOURNAL' required = ('UID', 'DTSTAMP',) singletons = ( 'CLASS', 'CREATED', 'DTSTART', 'DTSTAMP', 'LAST-MODIFIED', 'ORGANIZER', 'RECURRENCE-ID', 'SEQUENCE', 'STATUS', 'SUMMARY', 'UID', 'URL', ) multiple = ( 'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE', 'RELATED', 'RDATE', 'RRULE', 'RSTATUS', 'DESCRIPTION', ) class FreeBusy(Component): name = 'VFREEBUSY' required = ('UID', 'DTSTAMP',) singletons = ( 'CONTACT', 'DTSTART', 'DTEND', 'DTSTAMP', 'ORGANIZER', 'UID', 'URL', ) multiple = ('ATTENDEE', 'COMMENT', 'FREEBUSY', 'RSTATUS',) class Timezone(Component): name = 'VTIMEZONE' canonical_order = ('TZID',) required = ('TZID',) # it also requires one of components DAYLIGHT and STANDARD singletons = ('TZID', 'LAST-MODIFIED', 'TZURL',) @staticmethod def _extract_offsets(component, tzname): """extract offsets and transition times from a VTIMEZONE component :param component: a STANDARD or DAYLIGHT component :param tzname: the name of the zone """ offsetfrom = component['TZOFFSETFROM'].td offsetto = component['TZOFFSETTO'].td dtstart = component['DTSTART'].dt # offsets need to be rounded to the next minute, we might loose up # to 30 seconds accuracy, but it can't be helped (datetime # supposedly cannot handle smaller offsets) offsetto_s = int((offsetto.seconds + 30) / 60) * 60 offsetto = timedelta(days=offsetto.days, seconds=offsetto_s) offsetfrom_s = int((offsetfrom.seconds + 30) / 60) * 60 offsetfrom = timedelta(days=offsetfrom.days, seconds=offsetfrom_s) # expand recurrences if 'RRULE' in component: # to be paranoid about correct weekdays # evaluate the rrule with the current offset tzi = dateutil.tz.tzoffset ("(offsetfrom)", offsetfrom) rrstart = dtstart.replace (tzinfo=tzi) rrulestr = component['RRULE'].to_ical().decode('utf-8') rrule = dateutil.rrule.rrulestr(rrulestr, dtstart=rrstart) if not {'UNTIL', 'COUNT'}.intersection(component['RRULE'].keys()): # pytz.timezones don't know any transition dates after 2038 # either rrule._until = datetime(2038, 12, 31, tzinfo=pytz.UTC) # constructing the pytz-timezone requires UTC transition times. # here we construct local times without tzinfo, the offset to UTC # gets subtracted in to_tz(). transtimes = [dt.replace (tzinfo=None) for dt in rrule] # or rdates elif 'RDATE' in component: if not isinstance(component['RDATE'], list): rdates = [component['RDATE']] else: rdates = component['RDATE'] transtimes = [dtstart] + [leaf.dt for tree in rdates for leaf in tree.dts] else: transtimes = [dtstart] transitions = [(transtime, offsetfrom, offsetto, tzname) for transtime in set(transtimes)] if component.name == 'STANDARD': is_dst = 0 elif component.name == 'DAYLIGHT': is_dst = 1 return is_dst, transitions @staticmethod def _make_unique_tzname(tzname, tznames): """ :param tzname: Candidate tzname :param tznames: Other tznames """ # TODO better way of making sure tznames are unique while tzname in tznames: tzname += '_1' tznames.add(tzname) return tzname def to_tz(self): """convert this VTIMEZONE component to a pytz.timezone object """ try: zone = str(self['TZID']) except UnicodeEncodeError: zone = self['TZID'].encode('ascii', 'replace') transitions = [] dst = {} tznames = set() for component in self.walk(): if type(component) == Timezone: continue assert isinstance(component['DTSTART'].dt, datetime), ( "VTIMEZONEs sub-components' DTSTART must be of type datetime, not date" ) try: tzname = str(component['TZNAME']) except UnicodeEncodeError: tzname = component['TZNAME'].encode('ascii', 'replace') tzname = self._make_unique_tzname(tzname, tznames) except KeyError: # for whatever reason this is str/unicode tzname = f"{zone}_{component['DTSTART'].to_ical().decode('utf-8')}_" + \ f"{component['TZOFFSETFROM'].to_ical()}_" + \ f"{component['TZOFFSETTO'].to_ical()}" tzname = self._make_unique_tzname(tzname, tznames) dst[tzname], component_transitions = self._extract_offsets( component, tzname ) transitions.extend(component_transitions) transitions.sort() transition_times = [ transtime - osfrom for transtime, osfrom, _, _ in transitions ] # transition_info is a list with tuples in the format # (utcoffset, dstoffset, name) # dstoffset = 0, if current transition is to standard time # = this_utcoffset - prev_standard_utcoffset, otherwise transition_info = [] for num, (transtime, osfrom, osto, name) in enumerate(transitions): dst_offset = False if not dst[name]: dst_offset = timedelta(seconds=0) else: # go back in time until we find a transition to dst for index in range(num - 1, -1, -1): if not dst[transitions[index][3]]: # [3] is the name dst_offset = osto - transitions[index][2] # [2] is osto # noqa break # when the first transition is to dst, we didn't find anything # in the past, so we have to look into the future if not dst_offset: for index in range(num, len(transitions)): if not dst[transitions[index][3]]: # [3] is the name dst_offset = osto - transitions[index][2] # [2] is osto # noqa break assert dst_offset is not False transition_info.append((osto, dst_offset, name)) cls = type(zone, (DstTzInfo,), { 'zone': zone, '_utc_transition_times': transition_times, '_transition_info': transition_info }) return cls() class TimezoneStandard(Component): name = 'STANDARD' required = ('DTSTART', 'TZOFFSETTO', 'TZOFFSETFROM') singletons = ('DTSTART', 'TZOFFSETTO', 'TZOFFSETFROM',) multiple = ('COMMENT', 'RDATE', 'TZNAME', 'RRULE', 'EXDATE') class TimezoneDaylight(Component): name = 'DAYLIGHT' required = TimezoneStandard.required singletons = TimezoneStandard.singletons multiple = TimezoneStandard.multiple class Alarm(Component): name = 'VALARM' # some properties MAY/MUST/MUST NOT appear depending on ACTION value required = ('ACTION', 'TRIGGER',) singletons = ( 'ATTACH', 'ACTION', 'DESCRIPTION', 'SUMMARY', 'TRIGGER', 'DURATION', 'REPEAT', ) inclusive = (('DURATION', 'REPEAT',), ('SUMMARY', 'ATTENDEE',)) multiple = ('ATTENDEE', 'ATTACH') class Calendar(Component): """This is the base object for an iCalendar file. """ name = 'VCALENDAR' canonical_order = ('VERSION', 'PRODID', 'CALSCALE', 'METHOD',) required = ('PRODID', 'VERSION', ) singletons = ('PRODID', 'VERSION', 'CALSCALE', 'METHOD') # These are read only singleton, so one instance is enough for the module types_factory = TypesFactory() component_factory = ComponentFactory() icalendar-5.0.12/src/icalendar/caselessdict.py000066400000000000000000000066731457641316200212770ustar00rootroot00000000000000from icalendar.parser_tools import to_unicode from collections import OrderedDict def canonsort_keys(keys, canonical_order=None): """Sorts leading keys according to canonical_order. Keys not specified in canonical_order will appear alphabetically at the end. """ canonical_map = {k: i for i, k in enumerate(canonical_order or [])} head = [k for k in keys if k in canonical_map] tail = [k for k in keys if k not in canonical_map] return sorted(head, key=lambda k: canonical_map[k]) + sorted(tail) def canonsort_items(dict1, canonical_order=None): """Returns a list of items from dict1, sorted by canonical_order. """ return [(k, dict1[k]) for k in canonsort_keys(dict1.keys(), canonical_order)] class CaselessDict(OrderedDict): """A dictionary that isn't case sensitive, and only uses strings as keys. Values retain their case. """ def __init__(self, *args, **kwargs): """Set keys to upper for initial dict. """ super().__init__(*args, **kwargs) for key, value in self.items(): key_upper = to_unicode(key).upper() if key != key_upper: super().__delitem__(key) self[key_upper] = value def __getitem__(self, key): key = to_unicode(key) return super().__getitem__(key.upper()) def __setitem__(self, key, value): key = to_unicode(key) super().__setitem__(key.upper(), value) def __delitem__(self, key): key = to_unicode(key) super().__delitem__(key.upper()) def __contains__(self, key): key = to_unicode(key) return super().__contains__(key.upper()) def get(self, key, default=None): key = to_unicode(key) return super().get(key.upper(), default) def setdefault(self, key, value=None): key = to_unicode(key) return super().setdefault(key.upper(), value) def pop(self, key, default=None): key = to_unicode(key) return super().pop(key.upper(), default) def popitem(self): return super().popitem() def has_key(self, key): key = to_unicode(key) return super().__contains__(key.upper()) def update(self, *args, **kwargs): # Multiple keys where key1.upper() == key2.upper() will be lost. mappings = list(args) + [kwargs] for mapping in mappings: if hasattr(mapping, 'items'): mapping = iter(mapping.items()) for key, value in mapping: self[key] = value def copy(self): return type(self)(super().copy()) def __repr__(self): return f'{type(self).__name__}({dict(self)})' def __eq__(self, other): return self is other or dict(self.items()) == dict(other.items()) def __ne__(self, other): return not self == other # A list of keys that must appear first in sorted_keys and sorted_items; # must be uppercase. canonical_order = None def sorted_keys(self): """Sorts keys according to the canonical_order for the derived class. Keys not specified in canonical_order will appear at the end. """ return canonsort_keys(self.keys(), self.canonical_order) def sorted_items(self): """Sorts items according to the canonical_order for the derived class. Items not specified in canonical_order will appear at the end. """ return canonsort_items(self, self.canonical_order) icalendar-5.0.12/src/icalendar/cli.py000077500000000000000000000054371457641316200174000ustar00rootroot00000000000000#!/usr/bin/env python3 """utility program that allows user to preview calendar's events""" import sys import pathlib import argparse from datetime import datetime from icalendar import Calendar, __version__ def _format_name(address): """Retrieve the e-mail and the name from an address. :arg an address object, e.g. mailto:test@test.test :returns str: The name and the e-mail address. """ email = address.split(':')[-1] name = email.split('@')[0] if not email: return '' return f"{name} <{email}>" def _format_attendees(attendees): """Format the list of attendees. :arg any attendees: Either a list, a string or a vCalAddress object. :returns str: Formatted list of attendees. """ if isinstance(attendees, list): return '\n'.join(map(lambda s: s.rjust(len(s) + 5), map(_format_name, attendees))) return _format_name(attendees) def view(event): """Make a human readable summary of an iCalendar file. :returns str: Human readable summary. """ summary = event.get('summary', default='') organizer = _format_name(event.get('organizer', default='')) attendees = _format_attendees(event.get('attendee', default=[])) location = event.get('location', default='') comment = event.get('comment', '') description = event.get('description', '').split('\n') description = '\n'.join(map(lambda s: s.rjust(len(s) + 5), description)) start = event.decoded('dtstart') if 'duration' in event: end = event.decoded('dtend', default=start + event.decoded('duration')) else: end = event.decoded('dtend', default=start) duration = event.decoded('duration', default=end - start) if isinstance(start, datetime): start = start.astimezone() start = start.strftime('%c') if isinstance(end, datetime): end = end.astimezone() end = end.strftime('%c') return f""" Organizer: {organizer} Attendees: {attendees} Summary : {summary} Starts : {start} End : {end} Duration : {duration} Location : {location} Comment : {comment} Description: {description}""" def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('calendar_files', nargs='+', type=pathlib.Path) parser.add_argument('--output', '-o', type=argparse.FileType('w'), default=sys.stdout, help='output file') parser.add_argument('-v', '--version', action='version', version=f'{parser.prog} version {__version__}') argv = parser.parse_args() for calendar_file in argv.calendar_files: with open(calendar_file) as f: calendar = Calendar.from_ical(f.read()) for event in calendar.walk('vevent'): argv.output.write(view(event) + '\n\n') if __name__ == '__main__': main() icalendar-5.0.12/src/icalendar/fuzzing/000077500000000000000000000000001457641316200177375ustar00rootroot00000000000000icalendar-5.0.12/src/icalendar/fuzzing/build.sh000077500000000000000000000015771457641316200214070ustar00rootroot00000000000000#!/bin/bash -eu # Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ################################################################################ cd "$SRC"/icalendar pip3 install . # Build fuzzers in $OUT for fuzzer in $(find src/icalendar/fuzzing -name '*_fuzzer.py');do compile_python_fuzzer "$fuzzer" done zip -q $OUT/ical_fuzzer_seed_corpus.zip $SRC/corpus/* icalendar-5.0.12/src/icalendar/fuzzing/corpus/000077500000000000000000000000001457641316200212525ustar00rootroot00000000000000icalendar-5.0.12/src/icalendar/fuzzing/corpus/Index_Error.ics000066400000000000000000000000551457641316200241720ustar00rootroot00000000000000!bEGN: END:‪!bEGN: END:ใVT󠁒IOEEicalendar-5.0.12/src/icalendar/fuzzing/corpus/Type_Error.ics000066400000000000000000000000571457641316200240460ustar00rootroot00000000000000(EBEGIN: U: TJDATEBEGIN:VEV: RDATE:0 ENd:icalendar-5.0.12/src/icalendar/fuzzing/corpus/america_new_york.ics000066400000000000000000000025361457641316200252760ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VTIMEZONE TZID:custom_America/New_York LAST-MODIFIED:20050809T050000Z BEGIN:DAYLIGHT DTSTART:19670430T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19730429T070000Z TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:STANDARD DTSTART:19671029T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=20061029T060000Z TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:EST END:STANDARD BEGIN:DAYLIGHT DTSTART:19740106T020000 RDATE:19750223T020000 TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19760425T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19860427T070000Z TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19870405T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU;UNTIL=20060402T070000Z TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:20070311T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:STANDARD DTSTART:20071104T020000 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:EST END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:noend123 DTSTART;TZID=custom_America/New_York;VALUE=DATE-TIME:20140829T080000 DTSTART;TZID=custom_America/New_York;VALUE=DATE-TIME:20140829T100000 SUMMARY:an event with a custom tz name END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/fuzzing/corpus/calendar_with_unicode.ics000066400000000000000000000003061457641316200262630ustar00rootroot00000000000000BEGIN: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 END:VCALENDAR icalendar-5.0.12/src/icalendar/fuzzing/corpus/event_with_escaped_character1.ics000066400000000000000000000000741457641316200277100ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=Society\, 2014:that END:VEVENT icalendar-5.0.12/src/icalendar/fuzzing/corpus/event_with_escaped_character2.ics000066400000000000000000000000741457641316200277110ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=Society\\ 2014:that END:VEVENT icalendar-5.0.12/src/icalendar/fuzzing/corpus/event_with_escaped_character3.ics000066400000000000000000000000741457641316200277120ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=Society\; 2014:that END:VEVENT icalendar-5.0.12/src/icalendar/fuzzing/corpus/event_with_escaped_character4.ics000066400000000000000000000000741457641316200277130ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=Society\: 2014:that END:VEVENT icalendar-5.0.12/src/icalendar/fuzzing/corpus/event_with_escaped_characters.ics000066400000000000000000000001501457641316200300050ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=that\, that\; %th%%at%\ that\::это\, то\; that\ %th%%at%\: END:VEVENT icalendar-5.0.12/src/icalendar/fuzzing/corpus/event_with_recurrence.ics000066400000000000000000000003101457641316200263350ustar00rootroot00000000000000BEGIN:VEVENT DTSTART:19960401T010000 DTEND:19960401T020000 RRULE:FREQ=DAILY;COUNT=100 EXDATE:19960402T010000Z,19960403T010000Z,19960404T010000Z SUMMARY:A recurring event with exdates END:VEVENT icalendar-5.0.12/src/icalendar/fuzzing/corpus/event_with_recurrence_exdates_on_different_lines.ics000066400000000000000000000007101457641316200337720ustar00rootroot00000000000000BEGIN: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 icalendar-5.0.12/src/icalendar/fuzzing/corpus/event_with_rsvp.ics000066400000000000000000000001111457641316200251710ustar00rootroot00000000000000BEGIN:VEVENT ATTENDEE;RSVP=TRUE:mailto:someone@example.com END:VEVENT icalendar-5.0.12/src/icalendar/fuzzing/corpus/event_with_unicode_fields.ics000066400000000000000000000003761457641316200271700ustar00rootroot00000000000000BEGIN: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 icalendar-5.0.12/src/icalendar/fuzzing/corpus/event_with_unicode_organizer.ics000066400000000000000000000001261457641316200277130ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN="Джон Доу":mailto:john.doe@example.org END:VEVENT icalendar-5.0.12/src/icalendar/fuzzing/corpus/issue_100_transformed_doctests_into_unittests.ics000066400000000000000000000000651457641316200331520ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY;LANGUAGE=ru:te END:VEVENT icalendar-5.0.12/src/icalendar/fuzzing/corpus/issue_101_icalendar_chokes_on_umlauts_in_organizer.ics000066400000000000000000000005471457641316200340430ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:wichtiger termin 1 DTSTART:20130416T100000Z DTEND:20130416T110000Z DTSTAMP:20130416T092616Z UID:20130416112341.10064jz0k4j7uem8@acmenet.de CLASS:PUBLIC CREATED:20130416T092341Z LAST-MODIFIED:20130416T092341Z LOCATION:im büro ORGANIZER;CN="acme, ädmin":mailto:adm-acme@mydomain.de STATUS:CONFIRMED TRANSP:OPAQUE END:VEVENT icalendar-5.0.12/src/icalendar/fuzzing/corpus/issue_104_mark_events_broken.ics000066400000000000000000000002441457641316200274240ustar00rootroot00000000000000BEGIN:VEVENT DTSTART:20140401T000000Z DTEND:20140401T010000Z DTSTAMP:20140401T000000Z SUMMARY:Broken Eevnt CLASS:PUBLIC STATUS:CONFIRMED TRANSP:OPAQUE X END:VEVENT icalendar-5.0.12/src/icalendar/fuzzing/corpus/issue_112_missing_tzinfo_on_exdate.ics000066400000000000000000000012361457641316200306370ustar00rootroot00000000000000BEGIN: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 icalendar-5.0.12/src/icalendar/fuzzing/corpus/issue_156_RDATE_with_PERIOD.ics000066400000000000000000000002521457641316200265100ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:RDATE period DTSTART:19961230T020000Z DTEND:19961230T060000Z UID:rdate_period RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z END:VEVENT icalendar-5.0.12/src/icalendar/fuzzing/corpus/issue_156_RDATE_with_PERIOD_list.ics000066400000000000000000000003061457641316200275430ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:RDATE period DTSTART:19961230T020000Z DTEND:19961230T060000Z UID:rdate_period RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z,19970109T180000Z/PT5H 30M END:VEVENT icalendar-5.0.12/src/icalendar/fuzzing/corpus/issue_157_removes_trailing_semicolon.ics000066400000000000000000000001341457641316200311750ustar00rootroot00000000000000BEGIN:VEVENT DTSTART:20150325T101010 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU; END:VEVENT icalendar-5.0.12/src/icalendar/fuzzing/corpus/issue_184_broken_representation_of_period.ics000066400000000000000000000002031457641316200322010ustar00rootroot00000000000000BEGIN:VEVENT DTSTART:20150219T133000 DTSTAMP:20150219T133000 UID:1234567 RDATE;VALUE=PERIOD:20150219T133000/PT10H END:VEVENT icalendar-5.0.12/src/icalendar/fuzzing/corpus/issue_464_invalid_rdate.ics000066400000000000000000000003011457641316200263560ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:RDATE period DTSTART:19961230T020000Z DTEND:19961230T060000Z UID:rdate_period RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z,199709T180000Z/PT5H30M END:VEVENT icalendar-5.0.12/src/icalendar/fuzzing/corpus/issue_53_description_parsed_properly.ics000066400000000000000000000013431457641316200313070ustar00rootroot00000000000000BEGIN: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 icalendar-5.0.12/src/icalendar/fuzzing/corpus/issue_64_event_with_ascii_summary.ics000066400000000000000000000000521457641316200305710ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:abcdef END:VEVENT icalendar-5.0.12/src/icalendar/fuzzing/corpus/issue_64_event_with_non_ascii_summary.ics000066400000000000000000000000521457641316200314430ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:åäö END:VEVENT icalendar-5.0.12/src/icalendar/fuzzing/corpus/issue_70_rrule_causes_attribute_error.ics000066400000000000000000000004271457641316200314630ustar00rootroot00000000000000BEGIN: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 icalendar-5.0.12/src/icalendar/fuzzing/corpus/issue_82_expected_output.ics000066400000000000000000000001331457641316200267110ustar00rootroot00000000000000BEGIN:VEVENT ATTACH;ENCODING=BASE64;FMTTYPE=text/plain;VALUE=BINARY:dGV4dA== END:VEVENT icalendar-5.0.12/src/icalendar/fuzzing/corpus/pacific_fiji.ics000066400000000000000000000023061457641316200243520ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//tzurl.org//NONSGML Olson 2014g//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:custom_Pacific/Fiji TZURL:http://tzurl.org/zoneinfo/Pacific/Fiji X-LIC-LOCATION:Pacific/Fiji BEGIN:DAYLIGHT TZOFFSETFROM:+1200 TZOFFSETTO:+1300 DTSTART:20101024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYMONTHDAY=21,22,23,24,25,26,27;BYDAY=SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+1300 TZOFFSETTO:+1200 DTSTART:20140119T020000 RRULE:FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=18,19,20,21,22,23,24;BYDAY=SU END:STANDARD BEGIN:STANDARD TZOFFSETFROM:+115544 TZOFFSETTO:+1200 DTSTART:19151026T000000 RDATE:19151026T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETFROM:+1200 TZOFFSETTO:+1300 DTSTART:19981101T020000 RDATE:19981101T020000 RDATE:19991107T020000 RDATE:20091129T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+1300 TZOFFSETTO:+1200 DTSTART:19990228T030000 RDATE:19990228T030000 RDATE:20000227T030000 RDATE:20100328T030000 RDATE:20110306T030000 RDATE:20120122T030000 RDATE:20130120T030000 END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:noend123 DTSTART;TZID=custom_Pacific/Fiji;VALUE=DATE-TIME:20140829T080000 DTSTART;TZID=custom_Pacific/Fiji;VALUE=DATE-TIME:20140829T100000 SUMMARY:an event with a custom tz name END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/fuzzing/corpus/time.ics000066400000000000000000000000731457641316200227100ustar00rootroot00000000000000BEGIN:VCALENDAR X-SOMETIME;VALUE=TIME:172010 END:VCALENDAR icalendar-5.0.12/src/icalendar/fuzzing/corpus/timezone_rdate.ics000066400000000000000000000021251457641316200247630ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 BEGIN:VTIMEZONE TZID:posix/Europe/Vaduz BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+002946 TZOFFSETTO:+0100 DTSTART:19011213T211538 RDATE;VALUE=DATE-TIME:19011213T211538 END:STANDARD BEGIN:DAYLIGHT TZNAME:CEST TZOFFSETFROM:+0100 TZOFFSETTO:+0200 DTSTART:19810329T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 END:DAYLIGHT BEGIN:DAYLIGHT TZNAME:CEST TZOFFSETFROM:+0100 TZOFFSETTO:+0200 DTSTART:19410505T010000 RDATE;VALUE=DATE-TIME:19410505T010000 RDATE;VALUE=DATE-TIME:19420504T010000 END:DAYLIGHT BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+0200 TZOFFSETTO:+0100 DTSTART:19810927T030000 RRULE:FREQ=YEARLY;COUNT=15;BYDAY=-1SU;BYMONTH=9 END:STANDARD BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+0200 TZOFFSETTO:+0100 DTSTART:19961027T030000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 END:STANDARD BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+0200 TZOFFSETTO:+0100 DTSTART:19411006T020000 RDATE;VALUE=DATE-TIME:19411006T020000 RDATE;VALUE=DATE-TIME:19421005T020000 END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:123 DTSTART;TZID=posix/Europe/Vaduz:20120213T100000 SUMMARY=testevent END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/fuzzing/corpus/timezone_same_start.ics000066400000000000000000000012551457641316200260310ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:Microsoft Exchange Server 2010 METHOD:REQUEST BEGIN:VTIMEZONE TZID:Pacific Standard Time BEGIN:STANDARD DTSTART:16010101T020000 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11 TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:16010101T020000 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3 TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT SUMMARY;LANGUAGE=en-US:Test 4 DTSTART;TZID="Pacific Standard Time":20170224T120000 DTEND;TZID="Pacific Standard Time":20170224T123000 DTSTAMP:20170224T180431Z UID:040000008200E00074C5B7101A82E0080000000090E19664858ED20100000000000000 END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/fuzzing/corpus/timezone_same_start_and_offset.ics000066400000000000000000000007331457641316200302210ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:Microsoft Exchange Server 2010 BEGIN:VTIMEZONE TZID:Tokyo Standard Time BEGIN:STANDARD DTSTART:16010101T000000 TZOFFSETFROM:+0900 TZOFFSETTO:+0900 END:STANDARD BEGIN:DAYLIGHT DTSTART:16010101T000000 TZOFFSETFROM:+0900 TZOFFSETTO:+0900 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT DTSTART;TZID="Tokyo Standard Time":20170224T120000 DTEND;TZID="Tokyo Standard Time":20170224T123000 UID:blafoobar SUMMARY:this is an event END:VEVENT END:VCALENDARD icalendar-5.0.12/src/icalendar/fuzzing/corpus/timezoned.ics000066400000000000000000000014541457641316200237540ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//Plone.org//NONSGML plone.app.event//EN VERSION:2.0 X-WR-CALNAME:test create calendar X-WR-CALDESC:icalendar test X-WR-RELCALID:12345 X-WR-TIMEZONE:Europe/Vienna BEGIN:VTIMEZONE TZID:Europe/Vienna X-LIC-LOCATION:Europe/Vienna BEGIN:DAYLIGHT TZOFFSETFROM:+0100 TZOFFSETTO:+0200 TZNAME:CEST DTSTART:19700329T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:CET DTSTART:19701025T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT DTSTART;TZID=Europe/Vienna:20120213T100000 DTEND;TZID=Europe/Vienna:20120217T180000 DTSTAMP:20101010T091010Z CREATED:20101010T091010Z UID:123456 SUMMARY:artsprint 2012 DESCRIPTION:sprinting at the artsprint LOCATION:aka bild, wien END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/fuzzing/ical_fuzzer.py000066400000000000000000000042141457641316200226270ustar00rootroot00000000000000#!/usr/bin/python3 # Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ################################################################################ import atheris import sys import base64 with atheris.instrument_imports(): import icalendar from icalendar.tests.fuzzed import fuzz_calendar_v1 _value_error_matches = [ "component", "parse", "Expected", "Wrong date format", "END encountered", "vDDD", 'recurrence', 'Offset must', 'Invalid iCalendar', 'alue MUST', 'Key name', 'Invalid content line', 'does not exist', 'base 64', 'must use datetime', 'Unknown date type', 'Wrong', 'Start time', 'iCalendar', 'recurrence', 'float, float', 'utc offset', 'parent', 'MUST be a datetime' ] @atheris.instrument_func def TestOneInput(data): fdp = atheris.FuzzedDataProvider(data) try: multiple = fdp.ConsumeBool() should_walk = fdp.ConsumeBool() calendar_string = fdp.ConsumeString(fdp.remaining_bytes()) print("--- start calendar ---") try: # print the ICS file for the test case extraction # see https://stackoverflow.com/a/27367173/1320237 print(base64.b64encode(calendar_string.encode("UTF-8", "surrogateescape")).decode("ASCII")) except UnicodeEncodeError: pass print("--- end calendar ---") fuzz_calendar_v1(icalendar.Calendar.from_ical, calendar_string, multiple, should_walk) except ValueError as e: if any(m in str(e) for m in _value_error_matches): return -1 raise e def main(): atheris.Setup(sys.argv, TestOneInput) atheris.Fuzz() if __name__ == "__main__": main() icalendar-5.0.12/src/icalendar/parser.py000066400000000000000000000313571457641316200201220ustar00rootroot00000000000000"""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.caselessdict import CaselessDict from icalendar.parser_tools import DEFAULT_ENCODING from icalendar.parser_tools import SEQUENCE_TYPES from icalendar.parser_tools import to_unicode import re def escape_char(text): """Format value according to iCalendar TEXT escaping rules. """ assert isinstance(text, (str, bytes)) # 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, (str, bytes)) # NOTE: ORDER MATTERS! if isinstance(text, str): return text.replace('\\N', '\\n')\ .replace('\r\n', '\n')\ .replace('\\n', '\n')\ .replace('\\,', ',')\ .replace('\\;', ';')\ .replace('\\\\', '\\') elif isinstance(text, bytes): 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, 'key'): tzid = dt.tzinfo.key # ZoneInfo implementation elif hasattr(dt.tzinfo, 'tzname'): # dateutil implementation, but this is broken # See https://github.com/collective/icalendar/issues/333 for details tzid = dt.tzinfo.tzname(dt) return tzid def foldline(line, limit=75, fold_sep='\r\n '): """Make a string folded as defined in RFC5545 Lines of text SHOULD NOT be longer than 75 octets, excluding the line break. Long content lines SHOULD be split into a multiple line representations using a line "folding" technique. That is, a long line can be split between any two characters by inserting a CRLF immediately followed by a single linear white-space character (i.e., SPACE or HTAB). """ assert isinstance(line, str) assert '\n' not in line # Use a fast and simple variant for the common case that line is all ASCII. try: line.encode('ascii') except (UnicodeEncodeError, UnicodeDecodeError): pass else: return fold_sep.join( line[i:i + limit - 1] for i in range(0, len(line), limit - 1) ) ret_chars = [] byte_count = 0 for char in line: char_byte_len = len(char.encode(DEFAULT_ENCODING)) byte_count += char_byte_len if byte_count >= limit: ret_chars.append(fold_sep) byte_count = char_byte_len ret_chars.append(char) return ''.join(ret_chars) ################################################################# # Property parameter stuff def param_value(value): """Returns a parameter value. """ if isinstance(value, SEQUENCE_TYPES): return q_join(value) elif isinstance(value, str): return dquote(value) else: return dquote(value.to_ical().decode(DEFAULT_ENCODING)) # Could be improved # [\w-] because of the iCalendar RFC # . because of the vCard RFC NAME = re.compile(r'[\w.-]+') UNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7F",:;]') QUNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7F"]') FOLD = re.compile(b'(\r?\n)+[ \t]') uFOLD = re.compile('(\r?\n)+[ \t]') NEWLINE = re.compile(r'\r?\n') def validate_token(name): match = NAME.findall(name) if len(match) == 1 and name == match[0]: return raise ValueError(name) def validate_param_value(value, quoted=True): validator = QUNSAFE_CHAR if quoted else UNSAFE_CHAR if validator.findall(value): raise ValueError(value) # chars presence of which in parameter value will be cause the value # to be enclosed in double-quotes QUOTABLE = re.compile("[,;: ’']") def dquote(val): """Enclose parameter values containing [,;:] in double quotes. """ # a double-quote character is forbidden to appear in a parameter value # so replace it with a single-quote character val = val.replace('"', "'") if QUOTABLE.search(val): return f'"{val}"' return val # parsing helper def q_split(st, sep=',', maxsplit=-1): """Splits a string on char, taking double (q)uotes into considderation. """ if maxsplit == 0: return [st] result = [] cursor = 0 length = len(st) inquote = 0 splits = 0 for i, ch in enumerate(st): if ch == '"': inquote = not inquote if not inquote and ch == sep: result.append(st[cursor:i]) cursor = i + 1 splits += 1 if i + 1 == length or splits == maxsplit: result.append(st[cursor:]) break return result def q_join(lst, sep=','): """Joins a list on sep, quoting strings with QUOTABLE chars. """ return sep.join(dquote(itm) for itm in lst) class Parameters(CaselessDict): """Parser and generator of Property parameter strings. It knows nothing of datatypes. Its main concern is textual structure. """ def params(self): """In rfc2445 keys are called parameters, so this is to be consitent with the naming conventions. """ return self.keys() # TODO? # Later, when I get more time... need to finish this off now. The last major # thing missing. # def _encode(self, name, value, cond=1): # # internal, for conditional convertion of values. # if cond: # klass = types_factory.for_property(name) # return klass(value) # return value # # def add(self, name, value, encode=0): # "Add a parameter value and optionally encode it." # if encode: # value = self._encode(name, value, encode) # self[name] = value # # def decoded(self, name): # "returns a decoded value, or list of same" def to_ical(self, sorted=True): result = [] items = list(self.items()) if sorted: items.sort() for key, value in items: value = param_value(value) if isinstance(value, str): value = value.encode(DEFAULT_ENCODING) # CaselessDict keys are always unicode key = key.upper().encode(DEFAULT_ENCODING) result.append(key + b'=' + value) return b';'.join(result) @classmethod def from_ical(cls, st, strict=False): """Parses the parameter format from ical text format.""" # parse into strings result = cls() for param in q_split(st, ';'): try: key, val = q_split(param, '=', maxsplit=1) validate_token(key) # Property parameter values that are not in quoted # strings are case insensitive. vals = [] for v in q_split(val, ','): if v.startswith('"') and v.endswith('"'): v = v.strip('"') validate_param_value(v, quoted=True) vals.append(v) else: validate_param_value(v, quoted=False) if strict: vals.append(v.upper()) else: vals.append(v) if not vals: result[key] = val else: if len(vals) == 1: result[key] = vals[0] else: result[key] = vals except ValueError as exc: raise ValueError( f'{param!r} is not a valid parameter string: {exc}') return result def escape_string(val): # f'{i:02X}' return val.replace(r'\,', '%2C').replace(r'\:', '%3A')\ .replace(r'\;', '%3B').replace(r'\\', '%5C') def unescape_string(val): return val.replace('%2C', ',').replace('%3A', ':')\ .replace('%3B', ';').replace('%5C', '\\') def unescape_list_or_string(val): if isinstance(val, list): return [unescape_string(s) for s in val] else: return unescape_string(val) ######################################### # parsing and generation of content lines class Contentline(str): """A content line is basically a string that can be folded and parsed into parts. """ def __new__(cls, value, strict=False, encoding=DEFAULT_ENCODING): value = to_unicode(value, encoding=encoding) assert '\n' not in value, ('Content line can not contain unescaped ' 'new line characters.') self = super().__new__(cls, value) self.strict = strict return self @classmethod def from_parts(cls, name, params, values, sorted=True): """Turn a parts into a content line. """ assert isinstance(params, Parameters) if hasattr(values, 'to_ical'): values = values.to_ical() else: values = vText(values).to_ical() # elif isinstance(values, basestring): # values = escape_char(values) # TODO: after unicode only, remove this # Convert back to unicode, after to_ical encoded it. name = to_unicode(name) values = to_unicode(values) if params: params = to_unicode(params.to_ical(sorted=sorted)) return cls(f'{name};{params}:{values}') return cls(f'{name}:{values}') def parts(self): """Split the content line up into (name, parameters, values) parts. """ try: st = escape_string(self) name_split = None value_split = None in_quotes = False for i, ch in enumerate(st): if not in_quotes: if ch in ':;' and not name_split: name_split = i if ch == ':' and not value_split: value_split = i if ch == '"': in_quotes = not in_quotes name = unescape_string(st[:name_split]) if not name: raise ValueError('Key name is required') validate_token(name) if not value_split: value_split = i + 1 if not name_split or name_split + 1 == value_split: raise ValueError('Invalid content line') params = Parameters.from_ical(st[name_split + 1: value_split], strict=self.strict) params = Parameters( (unescape_string(key), unescape_list_or_string(value)) for key, value in iter(params.items()) ) values = unescape_string(st[value_split + 1:]) return (name, params, values) except ValueError as exc: raise ValueError( f"Content line could not be parsed into parts: '{self}': {exc}") @classmethod def from_ical(cls, ical, strict=False): """Unfold the content lines in an iCalendar into long content lines. """ ical = to_unicode(ical) # a fold is carriage return followed by either a space or a tab return cls(uFOLD.sub('', ical), strict=strict) def to_ical(self): """Long content lines are folded so they are less than 75 characters wide. """ return foldline(self).encode(DEFAULT_ENCODING) class Contentlines(list): """I assume that iCalendar files generally are a few kilobytes in size. Then this should be efficient. for Huge files, an iterator should probably be used instead. """ def to_ical(self): """Simply join self. """ return b'\r\n'.join(line.to_ical() for line in self if line) + b'\r\n' @classmethod def from_ical(cls, st): """Parses a string into content lines. """ st = to_unicode(st) try: # a fold is carriage return followed by either a space or a tab unfolded = uFOLD.sub('', st) lines = cls(Contentline(line) for line in NEWLINE.split(unfolded) if line) lines.append('') # '\r\n' at the end of every content line return lines except Exception: raise ValueError('Expected StringType with content lines') # XXX: what kind of hack is this? import depends to be at end from icalendar.prop import vText icalendar-5.0.12/src/icalendar/parser_tools.py000066400000000000000000000030441457641316200213320ustar00rootroot00000000000000from typing import Any SEQUENCE_TYPES = (list, tuple) DEFAULT_ENCODING = 'utf-8' def from_unicode(value: Any, encoding='utf-8') -> bytes: """ Converts a value to bytes, even if it already is bytes :param value: The value to convert :param encoding: The encoding to use in the conversion :return: The bytes representation of the value """ if isinstance(value, bytes): value = value elif isinstance(value, str): try: value = value.encode(encoding) except UnicodeEncodeError: value = value.encode('utf-8', 'replace') return value def to_unicode(value, encoding='utf-8'): """Converts a value to unicode, even if it is already a unicode string. """ if isinstance(value, str): return value elif isinstance(value, bytes): 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. """ # https://stackoverflow.com/questions/1254454/fastest-way-to-convert-a-dicts-keys-values-from-unicode-to-str if isinstance(data, str): return data.encode(encoding) elif isinstance(data, dict): return dict(map(data_encode, iter(data.items()))) elif isinstance(data, list) or isinstance(data, tuple): return list(map(data_encode, data)) else: return data icalendar-5.0.12/src/icalendar/prop.py000066400000000000000000000776371457641316200176210ustar00rootroot00000000000000"""This module contains the parser/generators (or coders/encoders if you prefer) for the classes/datatypes that are used in iCalendar: ########################################################################### # This module defines these property value data types and property parameters 4.2 Defined property parameters are: ALTREP, CN, CUTYPE, DELEGATED-FROM, DELEGATED-TO, DIR, ENCODING, FMTTYPE, FBTYPE, LANGUAGE, MEMBER, PARTSTAT, RANGE, RELATED, RELTYPE, ROLE, RSVP, SENT-BY, TZID, VALUE 4.3 Defined value data types are: BINARY, BOOLEAN, CAL-ADDRESS, DATE, DATE-TIME, DURATION, FLOAT, INTEGER, PERIOD, RECUR, TEXT, TIME, URI, UTC-OFFSET ########################################################################### iCalendar properties have values. The values are strongly typed. This module defines these types, calling val.to_ical() on them will render them as defined in rfc2445. If you pass any of these classes a Python primitive, you will have an object that can render itself as iCalendar formatted date. Property Value Data Types start with a 'v'. they all have an to_ical() and from_ical() method. The to_ical() method generates a text string in the iCalendar format. The from_ical() method can parse this format and return a primitive Python datatype. So it should always be true that: x == vDataType.from_ical(VDataType(x).to_ical()) These types are mainly used for parsing and file generation. But you can set them directly. """ from datetime import date from datetime import datetime from datetime import time from datetime import timedelta from datetime import tzinfo try: from dateutil.tz import tzutc except ImportError: tzutc = None from icalendar.caselessdict import CaselessDict from icalendar.parser import Parameters from icalendar.parser import escape_char from icalendar.parser import tzid_from_dt from icalendar.parser import unescape_char from icalendar.parser_tools import DEFAULT_ENCODING from icalendar.parser_tools import SEQUENCE_TYPES from icalendar.parser_tools import to_unicode from icalendar.parser_tools import from_unicode from icalendar.timezone_cache import _timezone_cache from icalendar.windows_to_olson import WINDOWS_TO_OLSON import base64 import binascii import pytz import re import time as _time DURATION_REGEX = re.compile(r'([-+]?)P(?:(\d+)W)?(?:(\d+)D)?' r'(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$') WEEKDAY_RULE = re.compile(r'(?P[+-]?)(?P[\d]{0,2})' r'(?P[\w]{2})$') #################################################### # handy tzinfo classes you can use. # ZERO = timedelta(0) HOUR = timedelta(hours=1) STDOFFSET = timedelta(seconds=-_time.timezone) if _time.daylight: DSTOFFSET = timedelta(seconds=-_time.altzone) else: DSTOFFSET = STDOFFSET DSTDIFF = DSTOFFSET - STDOFFSET class FixedOffset(tzinfo): """Fixed offset in minutes east from UTC. """ def __init__(self, offset, name): self.__offset = timedelta(minutes=offset) self.__name = name def utcoffset(self, dt): return self.__offset def tzname(self, dt): return self.__name def dst(self, dt): return ZERO class LocalTimezone(tzinfo): """Timezone of the machine where the code is running. """ def utcoffset(self, dt): return DSTOFFSET if self._isdst(dt) else STDOFFSET def dst(self, dt): return DSTDIFF if self._isdst(dt) else 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: """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 f"vBinary('{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.') def __eq__(self, other): """self == other""" return isinstance(other, vBinary) and self.obj == other.obj class vBoolean(int): """Returns specific string according to state. """ BOOL_MAP = CaselessDict({'true': True, 'false': False}) def __new__(cls, *args, **kwargs): self = super().__new__(cls, *args, **kwargs) self.params = Parameters() return self def to_ical(self): return b'TRUE' if self else b'FALSE' @classmethod def from_ical(cls, ical): try: return cls.BOOL_MAP[ical] except Exception: raise ValueError(f"Expected 'TRUE' or 'FALSE'. Got {ical}") class vCalAddress(str): """This just returns an unquoted string. """ def __new__(cls, value, encoding=DEFAULT_ENCODING): value = to_unicode(value, encoding=encoding) self = super().__new__(cls, value) self.params = Parameters() return self def __repr__(self): return f"vCalAddress('{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().__new__(cls, *args, **kwargs) self.params = Parameters() return self def to_ical(self): return str(self).encode('utf-8') @classmethod def from_ical(cls, ical): try: return cls(ical) except Exception: raise ValueError(f'Expected float value, got: {ical}') class vInt(int): """Just an int. """ def __new__(cls, *args, **kwargs): self = super().__new__(cls, *args, **kwargs) self.params = Parameters() return self def to_ical(self): return str(self).encode('utf-8') @classmethod def from_ical(cls, ical): try: return cls(ical) except Exception: raise ValueError(f'Expected int, got: {ical}') class vDDDLists: """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 = (from_unicode(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 def __eq__(self, other): if not isinstance(other, vDDDLists): return False return self.dts == other.dts class vCategory: def __init__(self, c_list): if not hasattr(c_list, '__iter__') or isinstance(c_list, str): c_list = [c_list] self.cats = [vText(c) for c in c_list] self.params = Parameters() def to_ical(self): return b",".join([c.to_ical() for c in self.cats]) @staticmethod def from_ical(ical): ical = to_unicode(ical) out = unescape_char(ical).split(',') return out def __eq__(self, other): """self == other""" return isinstance(other, vCategory) and self.cats == other.cats class TimeBase: """Make classes with a datetime/date comparable.""" def __eq__(self, other): """self == other""" if isinstance(other, TimeBase): return self.params == other.params and self.dt == other.dt return False def __hash__(self): return hash(self.dt) class vDDDTypes(TimeBase): """A combined Datetime, Date or Duration parser/generator. Their format cannot be confused, and often values can be of either types. So this is practical. """ def __init__(self, dt): if not isinstance(dt, (datetime, date, timedelta, time, tuple)): raise ValueError('You must use datetime, date, timedelta, ' 'time or tuple (for periods)') if isinstance(dt, (datetime, timedelta)): self.params = Parameters() elif isinstance(dt, date): self.params = Parameters({'value': 'DATE'}) elif isinstance(dt, time): self.params = Parameters({'value': 'TIME'}) elif isinstance(dt, tuple): self.params = Parameters({'value': 'PERIOD'}) tzid = tzid_from_dt(dt) if isinstance(dt, (datetime, time)) else None if not tzid is None and tzid != 'UTC': self.params.update({'TZID': tzid}) self.dt = dt def to_ical(self): dt = self.dt if isinstance(dt, datetime): return vDatetime(dt).to_ical() elif isinstance(dt, date): return vDate(dt).to_ical() elif isinstance(dt, timedelta): return vDuration(dt).to_ical() elif isinstance(dt, time): return vTime(dt).to_ical() elif isinstance(dt, tuple) and len(dt) == 2: return vPeriod(dt).to_ical() else: raise ValueError(f'Unknown date type: {type(dt)}') @classmethod def from_ical(cls, ical, timezone=None): if isinstance(ical, cls): return ical.dt u = ical.upper() if u.startswith(('P', '-P', '+P')): return vDuration.from_ical(ical) if '/' in u: return vPeriod.from_ical(ical, timezone=timezone) if len(ical) in (15, 16): return vDatetime.from_ical(ical, timezone=timezone) elif len(ical) == 8: return vDate.from_ical(ical) elif len(ical) in (6, 7): return vTime.from_ical(ical) else: raise ValueError( f"Expected datetime, date, or time, got: '{ical}'" ) def __repr__(self): """repr(self)""" return f"{self.__class__.__name__}({self.dt}, {self.params})" class vDate(TimeBase): """Render and generates iCalendar date format. """ def __init__(self, dt): if not isinstance(dt, date): raise ValueError('Value MUST be a date instance') self.dt = dt self.params = Parameters({'value': 'DATE'}) def to_ical(self): s = f"{self.dt.year:04}{self.dt.month:02}{self.dt.day:02}" 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 Exception: raise ValueError(f'Wrong date format {ical}') class vDatetime(TimeBase): """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 = f"{dt.year:04}{dt.month:02}{dt.day:02}T{dt.hour:02}{dt.minute:02}{dt.second:02}" 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.strip('/')) except pytz.UnknownTimeZoneError: if timezone in WINDOWS_TO_OLSON: tzinfo = pytz.timezone( WINDOWS_TO_OLSON.get(timezone.strip('/'))) else: tzinfo = _timezone_cache.get(timezone, None) try: timetuple = ( int(ical[:4]), # year int(ical[4:6]), # month int(ical[6:8]), # day int(ical[9:11]), # hour int(ical[11:13]), # minute int(ical[13:15]), # second ) if tzinfo: return tzinfo.localize(datetime(*timetuple)) elif not ical[15:]: return datetime(*timetuple) elif ical[15:16] == 'Z': return pytz.utc.localize(datetime(*timetuple)) else: raise ValueError(ical) except Exception: raise ValueError(f'Wrong datetime format: {ical}') class vDuration(TimeBase): """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 = "" td = self.td if td.days < 0: sign = "-" td = -td timepart = "" if td.seconds: timepart = "T" hours = td.seconds // 3600 minutes = td.seconds % 3600 // 60 seconds = td.seconds % 60 if hours: timepart += f"{hours}H" if minutes or (hours and seconds): timepart += f"{minutes}M" if seconds: timepart += f"{seconds}S" if td.days == 0 and timepart: return (str(sign).encode('utf-8') + b'P' + str(timepart).encode('utf-8')) else: return (str(sign).encode('utf-8') + b'P' + str(abs(td.days)).encode('utf-8') + b'D' + str(timepart).encode('utf-8')) @staticmethod def from_ical(ical): match = DURATION_REGEX.match(ical) if not match: raise ValueError(f'Invalid iCalendar duration: {ical}') sign, weeks, days, hours, minutes, seconds = match.groups() value = timedelta( weeks=int(weeks or 0), 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 @property def dt(self): """The time delta for compatibility.""" return self.td class vPeriod(TimeBase): """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({'value': 'PERIOD'}) # 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 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, timezone=None): try: start, end_or_duration = ical.split('/') start = vDDDTypes.from_ical(start, timezone=timezone) end_or_duration = vDDDTypes.from_ical(end_or_duration, timezone=timezone) return (start, end_or_duration) except Exception: raise ValueError(f'Expected period format, got: {ical}') def __repr__(self): if self.by_duration: p = (self.start, self.duration) else: p = (self.start, self.end) return f'vPeriod({p!r})' @property def dt(self): """Make this cooperate with the other vDDDTypes.""" return (self.start, (self.duration if self.by_duration else self.end)) class vWeekday(str): """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().__new__(cls, value) match = WEEKDAY_RULE.match(self) if match is None: raise ValueError(f'Expected weekday abbrevation, got: {self}') match = match.groupdict() sign = match['signal'] weekday = match['weekday'] relative = match['relative'] if weekday not in vWeekday.week_days or sign not in '+-': raise ValueError(f'Expected weekday abbrevation, got: {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 Exception: raise ValueError(f'Expected weekday abbrevation, got: {ical}') class vFrequency(str): """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().__new__(cls, value) if self not in vFrequency.frequencies: raise ValueError(f'Expected frequency, got: {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 Exception: raise ValueError(f'Expected frequency, got: {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", "BYWEEKDAY", "BYMONTHDAY", "BYYEARDAY", "BYWEEKNO", "BYMONTH", "BYSETPOS", "WKST") types = CaselessDict({ 'COUNT': vInt, 'INTERVAL': vInt, 'BYSECOND': vInt, 'BYMINUTE': vInt, 'BYHOUR': vInt, 'BYWEEKNO': vInt, 'BYMONTHDAY': vInt, 'BYYEARDAY': vInt, 'BYMONTH': vInt, 'UNTIL': vDDDTypes, 'BYSETPOS': vInt, 'WKST': vWeekday, 'BYDAY': vWeekday, 'FREQ': vFrequency, 'BYWEEKDAY': vWeekday, }) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.params = Parameters() def to_ical(self): result = [] for key, vals in self.sorted_items(): typ = self.types.get(key, vText) if not isinstance(vals, SEQUENCE_TYPES): vals = [vals] vals = b','.join(typ(val).to_ical() for val in vals) # CaselessDict keys are always unicode key = key.encode(DEFAULT_ENCODING) result.append(key + b'=' + vals) return b';'.join(result) @classmethod def parse_type(cls, key, values): # integers parser = cls.types.get(key, vText) return [parser.from_ical(v) for v in values.split(',')] @classmethod def from_ical(cls, ical): if isinstance(ical, cls): return ical try: recur = cls() for pairs in ical.split(';'): try: key, vals = pairs.split('=') except ValueError: # E.g. incorrect trailing semicolon, like (issue #157): # FREQ=YEARLY;BYMONTH=11;BYDAY=1SU; continue recur[key] = cls.parse_type(key, vals) return dict(recur) except Exception: raise ValueError(f'Error in recurrence rule: {ical}') class vText(str): """Simple text. """ def __new__(cls, value, encoding=DEFAULT_ENCODING): value = to_unicode(value, encoding=encoding) self = super().__new__(cls, value) self.encoding = encoding self.params = Parameters() return self def __repr__(self): return f"vText('{self.to_ical()!r}')" 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(TimeBase): """Render and generates iCalendar time format. """ def __init__(self, *args): if len(args) == 1: if not isinstance(args[0], (time, datetime)): raise ValueError(f'Expected a datetime.time, got: {args[0]}') self.dt = args[0] else: self.dt = time(*args) self.params = Parameters({'value': 'TIME'}) def to_ical(self): return self.dt.strftime("%H%M%S") @staticmethod def from_ical(ical): # TODO: timezone support try: timetuple = (int(ical[:2]), int(ical[2:4]), int(ical[4:6])) return time(*timetuple) except Exception: raise ValueError(f'Expected time, got: {ical}') class vUri(str): """Uniform resource identifier is basically just an unquoted string. """ def __new__(cls, value, encoding=DEFAULT_ENCODING): value = to_unicode(value, encoding=encoding) self = super().__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 Exception: raise ValueError(f'Expected , got: {ical}') class vGeo: """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 Exception: 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 f'{self.latitude};{self.longitude}' @staticmethod def from_ical(ical): try: latitude, longitude = ical.split(';') return (float(latitude), float(longitude)) except Exception: raise ValueError(f"Expected 'float;float' , got: {ical}") def __eq__(self, other): return self.to_ical() == other.to_ical() class vUTCOffset: """Renders itself as a utc offset. """ ignore_exceptions = False # if True, and we cannot parse this # component, we will silently ignore # it, rather than let the exception # propagate upwards def __init__(self, td): if not isinstance(td, timedelta): raise ValueError('Offset value MUST be a timedelta instance') self.td = td self.params = Parameters() def to_ical(self): if self.td < timedelta(0): sign = '-%s' td = timedelta(0) - self.td # get timedelta relative to 0 else: # Google Calendar rejects '0000' but accepts '+0000' sign = '+%s' td = self.td days, seconds = td.days, td.seconds hours = abs(days * 24 + seconds // 3600) minutes = abs((seconds % 3600) // 60) seconds = abs(seconds % 60) if seconds: duration = f'{hours:02}{minutes:02}{seconds:02}' else: duration = f'{hours:02}{minutes:02}' 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 Exception: raise ValueError(f'Expected utc offset, got: {ical}') if not cls.ignore_exceptions and offset >= timedelta(hours=24): raise ValueError( f'Offset must be less than 24 hours, was {ical}') if sign == '-': return -offset return offset def __eq__(self, other): if not isinstance(other, vUTCOffset): return False return self.td == other.td class vInline(str): """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().__new__(cls, value) self.params = Parameters() return self def to_ical(self): return self.encode(DEFAULT_ENCODING) @classmethod def from_ical(cls, ical): return cls(ical) class TypesFactory(CaselessDict): """All Value types defined in rfc 2445 are registered in this factory class. The value and parameter names don't overlap. So one factory is enough for both kinds. """ def __init__(self, *args, **kwargs): "Set keys to upper for initial dict" super().__init__(*args, **kwargs) self.all_types = ( vBinary, vBoolean, vCalAddress, vDDDLists, vDDDTypes, vDate, vDatetime, vDuration, vFloat, vFrequency, vGeo, vInline, vInt, vPeriod, vRecur, vText, vTime, vUTCOffset, vUri, vWeekday, vCategory, ) self['binary'] = vBinary self['boolean'] = vBoolean self['cal-address'] = vCalAddress self['date'] = vDDDTypes self['date-time'] = vDDDTypes self['duration'] = vDDDTypes self['float'] = vFloat self['integer'] = vInt self['period'] = vPeriod self['recur'] = vRecur self['text'] = vText self['time'] = vTime self['uri'] = vUri self['utc-offset'] = vUTCOffset self['geo'] = vGeo self['inline'] = vInline self['date-time-list'] = vDDDLists self['categories'] = vCategory ################################################# # Property types # These are the default types types_map = CaselessDict({ #################################### # Property value types # Calendar Properties 'calscale': 'text', 'method': 'text', 'prodid': 'text', 'version': 'text', # Descriptive Component Properties 'attach': 'uri', 'categories': 'categories', 'class': 'text', 'comment': 'text', 'description': 'text', 'geo': 'geo', 'location': 'text', 'percent-complete': 'integer', 'priority': 'integer', 'resources': 'text', 'status': 'text', 'summary': 'text', # Date and Time Component Properties 'completed': 'date-time', 'dtend': 'date-time', 'due': 'date-time', 'dtstart': 'date-time', 'duration': 'duration', 'freebusy': 'period', 'transp': 'text', # Time Zone Component Properties 'tzid': 'text', 'tzname': 'text', 'tzoffsetfrom': 'utc-offset', 'tzoffsetto': 'utc-offset', 'tzurl': 'uri', # Relationship Component Properties 'attendee': 'cal-address', 'contact': 'text', 'organizer': 'cal-address', 'recurrence-id': 'date-time', 'related-to': 'text', 'url': 'uri', 'uid': 'text', # Recurrence Component Properties 'exdate': 'date-time-list', 'exrule': 'recur', 'rdate': 'date-time-list', 'rrule': 'recur', # Alarm Component Properties 'action': 'text', 'repeat': 'integer', 'trigger': 'duration', # Change Management Component Properties 'created': 'date-time', 'dtstamp': 'date-time', 'last-modified': 'date-time', 'sequence': 'integer', # Miscellaneous Component Properties 'request-status': 'text', #################################### # parameter types (luckily there is no name overlap) 'altrep': 'uri', 'cn': 'text', 'cutype': 'text', 'delegated-from': 'cal-address', 'delegated-to': 'cal-address', 'dir': 'uri', 'encoding': 'text', 'fmttype': 'text', 'fbtype': 'text', 'language': 'text', 'member': 'cal-address', 'partstat': 'text', 'range': 'text', 'related': 'text', 'reltype': 'text', 'role': 'text', 'rsvp': 'boolean', 'sent-by': 'cal-address', 'tzid': 'text', 'value': 'text', }) def for_property(self, name): """Returns a the default type for a property or parameter """ return self[self.types_map.get(name, 'text')] def to_ical(self, name, value): """Encodes a named value from a primitive python type to an icalendar encoded string. """ type_class = self.for_property(name) return type_class(value).to_ical() def from_ical(self, name, value): """Decodes a named property or parameter value from an icalendar encoded string to a primitive python type. """ type_class = self.for_property(name) decoded = type_class.from_ical(value) return decoded icalendar-5.0.12/src/icalendar/tests/000077500000000000000000000000001457641316200174055ustar00rootroot00000000000000icalendar-5.0.12/src/icalendar/tests/__init__.py000066400000000000000000000000001457641316200215040ustar00rootroot00000000000000icalendar-5.0.12/src/icalendar/tests/calendars/000077500000000000000000000000001457641316200213415ustar00rootroot00000000000000icalendar-5.0.12/src/icalendar/tests/calendars/america_new_york.ics000066400000000000000000000025361457641316200253650ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VTIMEZONE TZID:custom_America/New_York LAST-MODIFIED:20050809T050000Z BEGIN:DAYLIGHT DTSTART:19670430T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19730429T070000Z TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:STANDARD DTSTART:19671029T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=20061029T060000Z TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:EST END:STANDARD BEGIN:DAYLIGHT DTSTART:19740106T020000 RDATE:19750223T020000 TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19760425T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19860427T070000Z TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19870405T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU;UNTIL=20060402T070000Z TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:20070311T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:STANDARD DTSTART:20071104T020000 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:EST END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:noend123 DTSTART;TZID=custom_America/New_York;VALUE=DATE-TIME:20140829T080000 DTSTART;TZID=custom_America/New_York;VALUE=DATE-TIME:20140829T100000 SUMMARY:an event with a custom tz name END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/big_bad_calendar.ics000066400000000000000000000007601457641316200252440ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT icalendar-5.0.12/src/icalendar/tests/calendars/broken_ical.ics000066400000000000000000000002201457641316200243030ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT SUMMARY:An Event with too many semicolons DTSTART;;VALUE=DATE-TIME:20140409T093000 UID:abc END:VEVENT END:VCALENDARicalendar-5.0.12/src/icalendar/tests/calendars/calendar_with_unicode.ics000066400000000000000000000003061457641316200263520ustar00rootroot00000000000000BEGIN: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 END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/created_calendar_with_unicode_fields.ics000066400000000000000000000010631457641316200313700ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Plönë.org//NONSGML plone.app.event//EN X-WR-CALDESC:test non ascii: äöü ÄÖÜ € X-WR-CALNAME:äöü ÄÖÜ € X-WR-RELCALID:12345 BEGIN:VEVENT SUMMARY:Non-ASCII Test: ÄÖÜ äöü € DTSTART:20101010T100000Z DTEND:20101010T120000Z UID:123456 CREATED:20101010T000000Z DESCRIPTION:icalendar should be able to de/serialize non-ascii. LOCATION:Tribstrül END:VEVENT BEGIN:VEVENT SUMMARY:åäö DTSTART:20101010T000000Z END:VEVENT BEGIN:VEVENT DESCRIPTION:äöüßÄÖÜ END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/example.ics000066400000000000000000000015071457641316200234770ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:collective/icalendar CALSCALE:GREGORIAN METHOD:PUBLISH X-WR-CALNAME:Holidays X-WR-TIMEZONE:Etc/GMT BEGIN:VEVENT SUMMARY:New Year's Day DTSTART:20220101 DTEND:20220101 DESCRIPTION:Happy New Year! UID:636a0cc1dbd5a1667894465@icalendar DTSTAMP:20221108T080105Z STATUS:CONFIRMED TRANSP:TRANSPARENT SEQUENCE:0 END:VEVENT BEGIN:VEVENT SUMMARY:Orthodox Christmas DTSTART:20220107 DTEND:20220107 LOCATION:Russia DESCRIPTION:It is Christmas again! UID:636a0cc1dbfd91667894465@icalendar STATUS:CONFIRMED TRANSP:TRANSPARENT SEQUENCE:0 END:VEVENT BEGIN:VEVENT SUMMARY:International Women's Day DTSTART:20220308 DTEND:20220308 DESCRIPTION:May the feminine be honoured! UID:636a0cc1dc0f11667894465@icalendar STATUS:CONFIRMED TRANSP:TRANSPARENT SEQUENCE:0 END:VEVENT END:VCALENDARicalendar-5.0.12/src/icalendar/tests/calendars/issue_104_broken_calendar.ics000066400000000000000000000003531457641316200267470ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 METHOD:PUBLISH BEGIN:VEVENT DTSTART:20140401T000000Z DTEND:20140401T010000Z DTSTAMP:20140401T000000Z SUMMARY:Broken Eevnt CLASS:PUBLIC STATUS:CONFIRMED TRANSP:OPAQUE END:VEVENT X END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/issue_156_RDATE_with_PERIOD_TZID_khal.ics000066400000000000000000000022731457641316200304350ustar00rootroot00000000000000BEGIN:VCALENDAR X-SOURCE-URL:https://github.com/pimutils/khal/issues/152#issuecomment-387410353 VERSION:2.0 PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN BEGIN:VEVENT SUMMARY:Event DTSTART;TZID=America/Chicago;VALUE=DATE-TIME:20180327T080000 DTEND;TZID=America/Chicago;VALUE=DATE-TIME:20180327T090000 DTSTAMP:20180323T200333Z RECURRENCE-ID;RANGE=THISANDFUTURE:20180327T130000Z SEQUENCE:10 RDATE;TZID="Central Standard Time";VALUE=PERIOD:20180327T080000/20180327T0 90000,20180403T080000/20180403T090000,20180410T080000/20180410T090000,2018 0417T080000/20180417T090000,20180424T080000/20180424T090000,20180501T08000 0/20180501T090000,20180508T080000/20180508T090000,20180515T080000/20180515 T090000,20180522T080000/20180522T090000,20180529T080000/20180529T090000,20 180605T080000/20180605T090000,20180612T080000/20180612T090000,20180619T080 000/20180619T090000,20180626T080000/20180626T090000,20180703T080000/201807 03T090000,20180710T080000/20180710T090000,20180717T080000/20180717T090000, 20180724T080000/20180724T090000,20180731T080000/20180731T090000 ATTENDEE;CN="XYZ";PARTSTAT=ACCEPTED;ROLE=CHAIR;RSVP= FALSE:mailto:xyz@xyz.com CLASS:PUBLIC END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/issue_156_RDATE_with_PERIOD_TZID_khal_2.ics000066400000000000000000000041161457641316200306540ustar00rootroot00000000000000BEGIN:VCALENDAR X-SOURCE-URL:https://github.com/pimutils/khal/issues/152#issuecomment-933635248 VERSION:2.0 PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN BEGIN:VTIMEZONE TZID:Western/Central Europe BEGIN:STANDARD DTSTART:19501029T020000 RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=10 TZOFFSETFROM:+0200 TZOFFSETTO:+0100 END:STANDARD BEGIN:DAYLIGHT DTSTART:19500326T020000 RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=3 TZOFFSETFROM:+0100 TZOFFSETTO:+0200 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT SUMMARY:(omitted) DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000 DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000 DTSTAMP:20211004T150245Z UID:BF5109494E67AAE20025875100566D31-Lotus_Notes_Generated RECURRENCE-ID;RANGE=THISANDFUTURE:20211101T150000Z SEQUENCE:0 RDATE;TZID="Western/Central Europe";VALUE=PERIOD:20211101T160000/20211101T 163000,20211206T160000/20211206T163000,20220103T160000/20220103T163000,202 20207T160000/20220207T163000 ATTENDEE;CN="(omitted)";PARTSTAT=ACCEPTED;ROLE=CHAIR;RSVP=FAL SE:mailto:omitted@example.com CLASS:PUBLIC TRANSP:OPAQUE X-LOTUS-APPTTYPE:3 X-LOTUS-AUDIOVIDEOFLAGS:0 X-LOTUS-BROADCAST:FALSE X-LOTUS-CHANGE-INST-DATES:20211101T150000Z\,20211206T150000Z\,20220103T150 000Z\,20220207T150000Z X-LOTUS-CHILD-UID:567EFBAF6CBD07FC0025875100566D3B X-LOTUS-INITIAL-RDATES:20211101T150000Z\,20211206T150000Z\,20220103T150000 Z\,20220207T150000Z X-LOTUS-LASTALL-RDATES;TZID="Western/Central Europe":20211101T160000\,2021 1206T160000\,20220103T160000\,20220207T160000 X-LOTUS-NOTESVERSION:2 X-LOTUS-NOTICETYPE:I X-LOTUS-RECURID;RANGE=THISANDFUTURE:20211101T150000Z X-LOTUS-UPDATE-SEQ:2 X-LOTUS-UPDATE-WISL:$W:1\;$O:1\;$M:1\;RequiredAttendees:1\;INetRequiredNam es:1\;AltRequiredNames:1\;StorageRequiredNames:1\;OptionalAttendees:1\;INe tOptionalNames:1\;AltOptionalNames:1\;StorageOptionalNames:1\;ApptUNIDURL: 1\;STUnyteConferenceURL:1\;STUnyteConferenceID:1\;SametimeType:1\;WhiteBoa rdContent:1\;STRoomName:1\;$S:2\;$B:2\;$L:2\;$E:2\;$R:2 END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/issue_165_missing_event.ics000066400000000000000000000014241457641316200265170ustar00rootroot00000000000000BEGIN:VCALENDAR METHOD:REQUEST PRODID:Microsoft CDO for Microsoft Exchange VERSION:2.0 BEGIN:VTIMEZONE TZID:GMT +0100 (Standard) / GMT +0200 (Daylight) BEGIN:STANDARD DTSTART:16010101T030000 TZOFFSETFROM:+0200 TZOFFSETTO:+0100 RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=10;BYDAY=-1SU END:STANDARD BEGIN:DAYLIGHT DTSTART:16010101T020000 TZOFFSETFROM:+0100 TZOFFSETTO:+0200 RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT DTSTAMP:20150703T071009Z DTSTART;TZID="GMT +0100 (Standard) / GMT +0200 (Daylight)":20150703T100000 SUMMARY:Sprint 25 Daily Standup DTEND;TZID="GMT +0100 (Standard) / GMT +0200 (Daylight)":20150703T103000 RRULE:FREQ=DAILY;UNTIL=20150722T080000Z;INTERVAL=1;BYDAY=MO, TU, WE, TH, FR ;WKST=SU END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/issue_168_expected_output.ics000066400000000000000000000001651457641316200270720ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT DTSTART:20150905T090000Z DTEND:20150905T100000Z UID:123 END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/issue_168_input.ics000066400000000000000000000002271457641316200250070ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT DTSTART:20150905T090000Z DTEND:20150905T100000Z UID:123 X-APPLE-RADIUS=49.91307046514149 END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/issue_178_component_with_invalid_name_represented.ics000066400000000000000000000000321457641316200340060ustar00rootroot00000000000000BEGIN:MYCOMP END:MYCOMP icalendar-5.0.12/src/icalendar/tests/calendars/issue_178_custom_component_contains_other.ics000066400000000000000000000001601457641316200323400ustar00rootroot00000000000000BEGIN:MYCOMPTOO DTSTAMP:20150121T080000 BEGIN:VEVENT DTSTART:20150122 UID:12345 END:VEVENT END:MYCOMPTOO icalendar-5.0.12/src/icalendar/tests/calendars/issue_178_custom_component_inside_other.ics000066400000000000000000000001061457641316200317750ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:UNKNOWN UID:1234 END:UNKNOWN END:VCALENDAR issue_237_fail_to_parse_timezone_with_non_ascii_tzid.ics000066400000000000000000000010651457641316200344170ustar00rootroot00000000000000icalendar-5.0.12/src/icalendar/tests/calendarsBEGIN:VCALENDAR BEGIN:VTIMEZONE TZID:(UTC-03:00) Brasília BEGIN:STANDARD TZNAME:Brasília standard DTSTART:16010101T235959 TZOFFSETFROM:-0200 TZOFFSETTO:-0300 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=3SA;BYMONTH=2 END:STANDARD BEGIN:DAYLIGHT TZNAME:Brasília daylight DTSTART:16010101T235959 TZOFFSETFROM:-0300 TZOFFSETTO:-0200 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SA;BYMONTH=10 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT DTSTART;TZID="(UTC-03:00) Brasília":20170511T133000 DTEND;TZID="(UTC-03:00) Brasília":20170511T140000 END:VEVENT END:VCALENDAR issue_27_multiple_periods_in_freebusy_multiple_freebusies.ics000066400000000000000000000013441457641316200356050ustar00rootroot00000000000000icalendar-5.0.12/src/icalendar/tests/calendarsBEGIN:VCALENDAR VERSION:2.0 PRODID:-//davmail.sf.net/NONSGML DavMail Calendar V1.1//EN METHOD:REPLY BEGIN:VFREEBUSY DTSTAMP:20120131T123000Z ORGANIZER:MAILTO:organizer@domain.tld DTSTART:20120101T000000Z DTEND:20120201T000000Z UID:null ATTENDEE:MAILTO:attendee@domain.tld FREEBUSY;FBTYPE=BUSY:20120103T091500Z/20120103T101500Z FREEBUSY;FBTYPE=BUSY:20120113T130000Z/20120113T150000Z FREEBUSY;FBTYPE=BUSY:20120116T130000Z/20120116T150000Z FREEBUSY;FBTYPE=BUSY:20120117T091500Z/20120117T101500Z FREEBUSY;FBTYPE=BUSY:20120118T160000Z/20120118T163000Z FREEBUSY;FBTYPE=BUSY:20120124T083000Z/20120124T093000Z FREEBUSY;FBTYPE=BUSY:20120124T123000Z/20120124T143000Z FREEBUSY;FBTYPE=BUSY:20120131T091500Z/20120131T101500Z END:VFREEBUSY END:VCALENDAR issue_27_multiple_periods_in_freebusy_one_freebusy.ics000066400000000000000000000011211457641316200342140ustar00rootroot00000000000000icalendar-5.0.12/src/icalendar/tests/calendarsBEGIN:VCALENDAR VERSION:2.0 PRODID:-//davmail.sf.net/NONSGML DavMail Calendar V1.1//EN METHOD:REPLY BEGIN:VFREEBUSY DTSTAMP:20120131T123000Z ORGANIZER:MAILTO:organizer@domain.tld DTSTART:20120101T000000Z DTEND:20120201T000000Z UID:null ATTENDEE:MAILTO:attendee@domain.tld FREEBUSY;FBTYPE=BUSY:20120103T091500Z/20120103T101500Z,20120113T130000Z/20120113T150000Z,20120116T130000Z/20120116T150000Z,20120117T091500Z/20120117T101500Z,20120118T160000Z/20120118T163000Z,20120124T083000Z/20120124T093000Z,20120124T123000Z/20120124T143000Z,20120131T091500Z/20120131T101500Z END:VFREEBUSY END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/issue_322_expected_calendar.ics000066400000000000000000000002111457641316200272630ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT SUMMARY:Event with bare string as argument for categories CATEGORIES:Lecture END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/issue_348_exception_parsing_value.ics000066400000000000000000000026421457641316200305700ustar00rootroot00000000000000BEGIN:VCALENDAR CALSCALE:GREGORIAN METHOD:PUBLISH VERSION:2.0 PRODID:-//Sixt//RAC//EN BEGIN:VFREEBUSY FREEBUSY;FBTYPE=FREE:20190624T063000Z/20190624T163000Z ORGANIZER;CN=Sixt SE X-ORGANIZER2;CN=Sixt SE;CN2=Test! UID:SIXT_9879691160 DTSTAMP:20190612T104813Z END:VFREEBUSY BEGIN:VEVENT CLASS:PUBLIC DESCRIPTION:\nVotre véhicule avec le numéro de réservation xxxxxxxxxxx sera à votre disposition le 24.06.2019 à 08:30 heures (heure locale). Retour prévu : Ferney-Voltaire AP de Genève Cointrin Sect(F), le 24.06.2019 à 18:30 heures.\nVous trouverez des informations relatives à la modification ou à l'annulation de votre réservation sous http://www.sixt.fr\n DTSTART:20190624T063000Z DTSTAMP:20190612T104813Z DTEND:20190624T163000Z LOCATION:Sixt Genève Aéroport secteur France, AP de Genève Cointrin Sect(F), 01210 Ferney-Voltaire, FR PRIORITY:5 TRANSP:TRANSPARENT SEQUENCE:0 STATUS:CONFIRMED SUMMARY:Sixt : détails de votre réservation UID:SIXT_9879691160 BEGIN:VALARM TRIGGER:-PT30M ACTION:DISPLAY DESCRIPTION:\nLe véhicule avec la n° de réservation xxxxxxxxxxx sera à votre disposition entre 24.06.2019 et 08:30 heures (heure locale) à l'agence Genève Aéroport secteur France. Retour prévu : Ferney-Voltaire AP de Genève Cointrin Sect(F), le 24.06.2019 à 18:30 heures.\nTrouver des informations de localisation et modifier / annuler votre réservation ici: http://www.sixt.fr\n END:VALARM END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/issue_466_convert_tzid_with_slash.ics000066400000000000000000000005051457641316200306070ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT UID:0cab49a0-1167-40f0-bfed-ecb4d117047d DTSTAMP:20221019T102950Z DTSTART;TZID=/Europe/Stockholm:20221021T200000 DTEND;TZID=/Europe/Stockholm:20221021T210000 SUMMARY:Just chatting DESCRIPTION:Just Chatting. CATEGORIES:Just Chatting RRULE:FREQ=WEEKLY;BYDAY=FR END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/issue_466_respect_unique_timezone.ics000066400000000000000000000011731457641316200306170ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VTIMEZONE TZID:/Europe/CUSTOM 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 UID:0cab49a0-1167-40f0-bfed-ecb4d117047d DTSTAMP:20221019T102950Z DTSTART;TZID=/Europe/CUSTOM:20221021T200000 DTEND;TZID=/Europe/CUSTOM:20221021T210000 SUMMARY:Just chatting DESCRIPTION:Just Chatting. CATEGORIES:Just Chatting RRULE:FREQ=WEEKLY;BYDAY=FR END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/issue_526_calendar_with_different_events.ics000066400000000000000000000010601457641316200320600ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:icalendar-2023 BEGIN:VEVENT UID:ical-jacadzaca-3 SUMMARY: Some very different event ':' DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000 DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000 DTSTAMP:20211004T150245Z END:VEVENT BEGIN:VEVENT UID:ical-jacadzaca-4 SUMMARY: Some very different other event DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T164000 DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T165000 DTSTAMP:20211004T150245Z END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/issue_526_calendar_with_event_subset.ics000066400000000000000000000004311457641316200312350ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:icalendar-2023 BEGIN:VEVENT UID:1 SUMMARY: Some event ':' DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000 DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000 DTSTAMP:20211004T150245Z END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/issue_526_calendar_with_events.ics000066400000000000000000000007641457641316200300440ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:icalendar-2023 BEGIN:VEVENT UID:1 SUMMARY: Some event ':' DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000 DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000 DTSTAMP:20211004T150245Z END:VEVENT BEGIN:VEVENT UID:2 SUMMARY: Some other event DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T164000 DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T165000 DTSTAMP:20211004T150245Z END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/issue_526_calendar_with_shuffeled_events.ics000066400000000000000000000007641457641316200320710ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:icalendar-2023 BEGIN:VEVENT UID:2 SUMMARY: Some other event DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T164000 DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T165000 DTSTAMP:20211004T150245Z END:VEVENT BEGIN:VEVENT UID:1 SUMMARY: Some event ':' DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000 DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000 DTSTAMP:20211004T150245Z END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/multiple_calendar_components.ics000066400000000000000000000016641457641316200300010ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION :2.0 PRODID :-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN METHOD :PUBLISH BEGIN:VEVENT UID :956630271 SUMMARY :Christmas Day CLASS :PUBLIC X-MOZILLA-ALARM-DEFAULT-UNITS :minutes X-MOZILLA-ALARM-DEFAULT-LENGTH :15 X-MOZILLA-RECUR-DEFAULT-UNITS :weeks X-MOZILLA-RECUR-DEFAULT-INTERVAL :1 DTSTART ;VALUE=DATE :20031225 DTEND ;VALUE=DATE :20031226 DTSTAMP :20020430T114937Z END:VEVENT END:VCALENDAR BEGIN:VCALENDAR VERSION :2.0 PRODID :-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN METHOD :PUBLISH BEGIN:VEVENT UID :911737808 SUMMARY :Boxing Day CLASS :PUBLIC X-MOZILLA-ALARM-DEFAULT-UNITS :minutes X-MOZILLA-ALARM-DEFAULT-LENGTH :15 X-MOZILLA-RECUR-DEFAULT-UNITS :weeks X-MOZILLA-RECUR-DEFAULT-INTERVAL :1 DTSTART ;VALUE=DATE :20030501 DTSTAMP :20020430T114937Z END:VEVENT BEGIN:VEVENT UID :wh4t3v3r DTSTART;VALUE=DATE:20031225 SUMMARY:Christmas again! END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/pacific_fiji.ics000066400000000000000000000023061457641316200244410ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//tzurl.org//NONSGML Olson 2014g//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:custom_Pacific/Fiji TZURL:http://tzurl.org/zoneinfo/Pacific/Fiji X-LIC-LOCATION:Pacific/Fiji BEGIN:DAYLIGHT TZOFFSETFROM:+1200 TZOFFSETTO:+1300 DTSTART:20101024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYMONTHDAY=21,22,23,24,25,26,27;BYDAY=SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+1300 TZOFFSETTO:+1200 DTSTART:20140119T020000 RRULE:FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=18,19,20,21,22,23,24;BYDAY=SU END:STANDARD BEGIN:STANDARD TZOFFSETFROM:+115544 TZOFFSETTO:+1200 DTSTART:19151026T000000 RDATE:19151026T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETFROM:+1200 TZOFFSETTO:+1300 DTSTART:19981101T020000 RDATE:19981101T020000 RDATE:19991107T020000 RDATE:20091129T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+1300 TZOFFSETTO:+1200 DTSTART:19990228T030000 RDATE:19990228T030000 RDATE:20000227T030000 RDATE:20100328T030000 RDATE:20110306T030000 RDATE:20120122T030000 RDATE:20130120T030000 END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:noend123 DTSTART;TZID=custom_Pacific/Fiji;VALUE=DATE-TIME:20140829T080000 DTSTART;TZID=custom_Pacific/Fiji;VALUE=DATE-TIME:20140829T100000 SUMMARY:an event with a custom tz name END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/parsing_error.ics000066400000000000000000000007521457641316200247210ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//Google Inc//Google Calendar 70.9054//EN VERSION:2.0 CALSCALE:GREGORIAN METHOD:PUBLISH BEGIN:VEVENT DESCRIPTION:Perfectly OK event DTSTART;VALUE=DATE:20080303 DTEND;VALUE=DATE:20080304 RRULE:FREQ=DAILY;UNTIL=20080323T235959Z EXDATE;VALUE=DATE:20080311 END:VEVENT BEGIN:VEVENT DESCRIPTION:Wrong event DTSTART;VALUE=DATE:20080303 DTEND;VALUE=DATE:20080304 RRULE:FREQ=DAILY;UNTIL=20080323T235959Z EXDATE;VALUE=DATE:20080311 EXDATE;VALUE=DATE: END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/parsing_error_in_UTC_offset.ics000066400000000000000000000002751457641316200274700ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VTIMEZONE TZID:Europe/Prague BEGIN:STANDARD DTSTART:18500101T000000 TZNAME:PMT TZOFFSETFROM:+5744 TZOFFSETTO:+5744 END:STANDARD END:VTIMEZONE END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/period_with_timezone.ics000066400000000000000000000013501457641316200262670ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 X-WR-CALNAME;VALUE=TEXT:Test RDATE BEGIN:VTIMEZONE TZID:America/Vancouver BEGIN:STANDARD DTSTART:20221106T020000 TZOFFSETFROM:-0700 TZOFFSETTO:-0800 RDATE:20231105T020000 TZNAME:PST END:STANDARD BEGIN:DAYLIGHT DTSTART:20230312T020000 TZOFFSETFROM:-0800 TZOFFSETTO:-0700 RDATE:20240310T020000 TZNAME:PDT END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT UID:1 DESCRIPTION:Test RDATE DTSTART;TZID=America/Vancouver:20230920T120000 DTEND;TZID=America/Vancouver:20230920T140000 EXDATE;TZID=America/Vancouver:20231220T120000 RDATE;VALUE=PERIOD;TZID=America/Vancouver:20231213T120000/20231213T150000 RRULE:FREQ=MONTHLY;COUNT=9;INTERVAL=1;BYDAY=+3WE;BYMONTH=1,2,3,4,5,9,10,11, 12;WKST=MO SUMMARY:Test RDATE END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/pr_480_summary_with_colon.ics000066400000000000000000000002451457641316200270600ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT SUMMARY:Example calendar with a ': ' in the summary END:VEVENT BEGIN:VEVENT SUMMARY:Another event with a ': ' in the summary END:VEVENT icalendar-5.0.12/src/icalendar/tests/calendars/small_bad_calendar.ics000066400000000000000000000000501457641316200256030ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT END:VEVENT icalendar-5.0.12/src/icalendar/tests/calendars/time.ics000066400000000000000000000000731457641316200227770ustar00rootroot00000000000000BEGIN:VCALENDAR X-SOMETIME;VALUE=TIME:172010 END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/timezone_rdate.ics000066400000000000000000000021251457641316200250520ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 BEGIN:VTIMEZONE TZID:posix/Europe/Vaduz BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+002946 TZOFFSETTO:+0100 DTSTART:19011213T211538 RDATE;VALUE=DATE-TIME:19011213T211538 END:STANDARD BEGIN:DAYLIGHT TZNAME:CEST TZOFFSETFROM:+0100 TZOFFSETTO:+0200 DTSTART:19810329T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 END:DAYLIGHT BEGIN:DAYLIGHT TZNAME:CEST TZOFFSETFROM:+0100 TZOFFSETTO:+0200 DTSTART:19410505T010000 RDATE;VALUE=DATE-TIME:19410505T010000 RDATE;VALUE=DATE-TIME:19420504T010000 END:DAYLIGHT BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+0200 TZOFFSETTO:+0100 DTSTART:19810927T030000 RRULE:FREQ=YEARLY;COUNT=15;BYDAY=-1SU;BYMONTH=9 END:STANDARD BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+0200 TZOFFSETTO:+0100 DTSTART:19961027T030000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 END:STANDARD BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+0200 TZOFFSETTO:+0100 DTSTART:19411006T020000 RDATE;VALUE=DATE-TIME:19411006T020000 RDATE;VALUE=DATE-TIME:19421005T020000 END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:123 DTSTART;TZID=posix/Europe/Vaduz:20120213T100000 SUMMARY=testevent END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/timezone_same_start.ics000066400000000000000000000012551457641316200261200ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:Microsoft Exchange Server 2010 METHOD:REQUEST BEGIN:VTIMEZONE TZID:Pacific Standard Time BEGIN:STANDARD DTSTART:16010101T020000 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11 TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:16010101T020000 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3 TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT SUMMARY;LANGUAGE=en-US:Test 4 DTSTART;TZID="Pacific Standard Time":20170224T120000 DTEND;TZID="Pacific Standard Time":20170224T123000 DTSTAMP:20170224T180431Z UID:040000008200E00074C5B7101A82E0080000000090E19664858ED20100000000000000 END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/timezone_same_start_and_offset.ics000066400000000000000000000007331457641316200303100ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:Microsoft Exchange Server 2010 BEGIN:VTIMEZONE TZID:Tokyo Standard Time BEGIN:STANDARD DTSTART:16010101T000000 TZOFFSETFROM:+0900 TZOFFSETTO:+0900 END:STANDARD BEGIN:DAYLIGHT DTSTART:16010101T000000 TZOFFSETFROM:+0900 TZOFFSETTO:+0900 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT DTSTART;TZID="Tokyo Standard Time":20170224T120000 DTEND;TZID="Tokyo Standard Time":20170224T123000 UID:blafoobar SUMMARY:this is an event END:VEVENT END:VCALENDARD icalendar-5.0.12/src/icalendar/tests/calendars/timezoned.ics000066400000000000000000000014541457641316200240430ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//Plone.org//NONSGML plone.app.event//EN VERSION:2.0 X-WR-CALNAME:test create calendar X-WR-CALDESC:icalendar test X-WR-RELCALID:12345 X-WR-TIMEZONE:Europe/Vienna BEGIN:VTIMEZONE TZID:Europe/Vienna X-LIC-LOCATION:Europe/Vienna BEGIN:DAYLIGHT TZOFFSETFROM:+0100 TZOFFSETTO:+0200 TZNAME:CEST DTSTART:19700329T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:CET DTSTART:19701025T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT DTSTART;TZID=Europe/Vienna:20120213T100000 DTEND;TZID=Europe/Vienna:20120217T180000 DTSTAMP:20101010T091010Z CREATED:20101010T091010Z UID:123456 SUMMARY:artsprint 2012 DESCRIPTION:sprinting at the artsprint LOCATION:aka bild, wien END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/calendars/x_location.ics000066400000000000000000000025721457641316200242060ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//Google Inc//Google Calendar 70.9054//EN VERSION:2.0 CALSCALE:GREGORIAN METHOD:PUBLISH X-WR-CALNAME:ITC X-WR-TIMEZONE:Europe/Zurich X-WR-CALDESC:ITC Bookings BEGIN:VTIMEZONE TZID:Europe/Zurich X-LIC-LOCATION:Europe/Zurich BEGIN:DAYLIGHT TZOFFSETFROM:+0100 TZOFFSETTO:+0200 TZNAME:CEST DTSTART:19700329T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:CET DTSTART:19701025T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT DTSTART;TZID=Europe/Zurich:20161028T140000 DTEND;TZID=Europe/Zurich:20161028T143000 RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR DTSTAMP:20161031T192828Z UID:BFE33ADD-5553-48B5-B5A5-F9DA5CA4C393 CREATED:20161029T121229Z DESCRIPTION:Some Description LAST-MODIFIED:20161029T121229Z LOCATION:Roadstar 16\n12764 Happyville\nDenmark SEQUENCE:0 STATUS:CONFIRMED SUMMARY:Daily Sync TRANSP:OPAQUE X-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-ADDRESS="Röadstar 16\n12764 Happyvi lle\nDenmark";X-APPLE-MAPKIT-HANDLE=CAESARoSCWYTYFhHQBEGfw4hQCIBDQoHRGVubW FyaxJES0hhcHB5dmlsbGUqSGFwcHl2aWxsZTIHSGFwcHl2aWxsZToEMTI3NjRCDQpSb2Fkc3Rh cloCMTZiUm9hZHN0YXIgMTYBEU1vcmRvcgENCk1vcmRvcioSUm9hZHN0YXIgMTYyUm9hZHN0YX IgMTYxMjc2NCBIYXBweXZpbGxlMgdEZW5tYXJrOThA=;X-APPLE-RADIUS=49.913058665846 98;X-APPLE-REFERENCEFRAME=1;X-TITLE=:geo:52.382762,7.528319 END:VEVENT END:VCALENDAR icalendar-5.0.12/src/icalendar/tests/conftest.py000066400000000000000000000106501457641316200216060ustar00rootroot00000000000000import os import pytest import icalendar import pytz from datetime import datetime from dateutil import tz try: import zoneinfo except ModuleNotFoundError: from backports import zoneinfo from icalendar.cal import Component, Calendar, Event, ComponentFactory class DataSource: '''A collection of parsed ICS elements (e.g calendars, timezones, events)''' def __init__(self, data_source_folder, parser): self._parser = parser self._data_source_folder = data_source_folder def keys(self): """Return all the files that could be used.""" return [file[:-4] for file in os.listdir(self._data_source_folder) if file.lower().endswith(".ics")] def __getitem__(self, attribute): """Parse a file and return the result stored in the attribute.""" source_file = attribute + '.ics' source_path = os.path.join(self._data_source_folder, source_file) if not os.path.isfile(source_path): raise AttributeError(f"{source_path} does not exist.") with open(source_path, 'rb') as f: raw_ics = f.read() source = self._parser(raw_ics) if not isinstance(source, list): source.raw_ics = raw_ics self.__dict__[attribute] = source return source def __getattr__(self, key): return self[key] def __repr__(self): return repr(self.__dict__) @property def multiple(self): """Return a list of all components parsed.""" return self.__class__(self._data_source_folder, lambda data: self._parser(data, multiple=True)) HERE = os.path.dirname(__file__) CALENDARS_FOLDER = os.path.join(HERE, 'calendars') CALENDARS = DataSource(CALENDARS_FOLDER, icalendar.Calendar.from_ical) TIMEZONES_FOLDER = os.path.join(HERE, 'timezones') TIMEZONES = DataSource(TIMEZONES_FOLDER, icalendar.Timezone.from_ical) EVENTS_FOLDER = os.path.join(HERE, 'events') EVENTS = DataSource(EVENTS_FOLDER, icalendar.Event.from_ical) @pytest.fixture() def calendars(): return CALENDARS @pytest.fixture() def timezones(): return TIMEZONES @pytest.fixture() def events(): return EVENTS @pytest.fixture(params=[ pytz.utc, zoneinfo.ZoneInfo('UTC'), pytz.timezone('UTC'), tz.UTC, tz.gettz('UTC')]) def utc(request): return request.param @pytest.fixture(params=[ lambda dt, tzname: pytz.timezone(tzname).localize(dt), lambda dt, tzname: dt.replace(tzinfo=tz.gettz(tzname)), lambda dt, tzname: dt.replace(tzinfo=zoneinfo.ZoneInfo(tzname)) ]) def in_timezone(request): return request.param ICS_FILES = [ (data, key) for data in [CALENDARS, TIMEZONES, EVENTS] for key in data.keys() if key not in ( # exclude broken calendars here "big_bad_calendar", "issue_104_broken_calendar", "small_bad_calendar", "multiple_calendar_components", "pr_480_summary_with_colon", "parsing_error_in_UTC_offset", "parsing_error", ) ] @pytest.fixture(params=ICS_FILES) def ics_file(request): """An example ICS file.""" data, key = request.param print(key) return data[key] FUZZ_V1 = [os.path.join(CALENDARS_FOLDER, key) for key in os.listdir(CALENDARS_FOLDER) if "fuzz-testcase" in key] @pytest.fixture(params=FUZZ_V1) def fuzz_v1_calendar(request): """Clusterfuzz calendars.""" return request.param @pytest.fixture() def factory(): """Return a new component factory.""" return icalendar.ComponentFactory() @pytest.fixture() def vUTCOffset_ignore_exceptions(): icalendar.vUTCOffset.ignore_exceptions = True yield icalendar.vUTCOffset.ignore_exceptions = False @pytest.fixture() def event_component(): """Return an event component.""" c = Component() c.name = 'VEVENT' return c @pytest.fixture() def c(): """Return an empty component.""" c = Component() return c comp = c @pytest.fixture() def calendar_component(): """Return an empty component.""" c = Component() c.name = 'VCALENDAR' return c @pytest.fixture() def filled_event_component(c, calendar_component): """Return an event with some values and add it to calendar_component.""" e = Component(summary='A brief history of time') e.name = 'VEVENT' e.add('dtend', '20000102T000000', encode=0) e.add('dtstart', '20000101T000000', encode=0) calendar_component.add_component(e) return e @pytest.fixture() def calendar_with_resources(): c = Calendar() c['resources'] = 'Chair, Table, "Room: 42"' return c icalendar-5.0.12/src/icalendar/tests/events/000077500000000000000000000000001457641316200207115ustar00rootroot00000000000000icalendar-5.0.12/src/icalendar/tests/events/event_with_escaped_character1.ics000066400000000000000000000000741457641316200273470ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=Society\, 2014:that END:VEVENT icalendar-5.0.12/src/icalendar/tests/events/event_with_escaped_character2.ics000066400000000000000000000000741457641316200273500ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=Society\\ 2014:that END:VEVENT icalendar-5.0.12/src/icalendar/tests/events/event_with_escaped_character3.ics000066400000000000000000000000741457641316200273510ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=Society\; 2014:that END:VEVENT icalendar-5.0.12/src/icalendar/tests/events/event_with_escaped_character4.ics000066400000000000000000000000741457641316200273520ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=Society\: 2014:that END:VEVENT icalendar-5.0.12/src/icalendar/tests/events/event_with_escaped_characters.ics000066400000000000000000000001501457641316200274440ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=that\, that\; %th%%at%\ that\::это\, то\; that\ %th%%at%\: END:VEVENT icalendar-5.0.12/src/icalendar/tests/events/event_with_recurrence.ics000066400000000000000000000003101457641316200257740ustar00rootroot00000000000000BEGIN:VEVENT DTSTART:19960401T010000 DTEND:19960401T020000 RRULE:FREQ=DAILY;COUNT=100 EXDATE:19960402T010000Z,19960403T010000Z,19960404T010000Z SUMMARY:A recurring event with exdates END:VEVENT icalendar-5.0.12/src/icalendar/tests/events/event_with_recurrence_exdates_on_different_lines.ics000066400000000000000000000007101457641316200334310ustar00rootroot00000000000000BEGIN: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 icalendar-5.0.12/src/icalendar/tests/events/event_with_rsvp.ics000066400000000000000000000001111457641316200246300ustar00rootroot00000000000000BEGIN:VEVENT ATTENDEE;RSVP=TRUE:mailto:someone@example.com END:VEVENT icalendar-5.0.12/src/icalendar/tests/events/event_with_unicode_fields.ics000066400000000000000000000003761457641316200266270ustar00rootroot00000000000000BEGIN: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 icalendar-5.0.12/src/icalendar/tests/events/event_with_unicode_organizer.ics000066400000000000000000000001261457641316200273520ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN="Джон Доу":mailto:john.doe@example.org END:VEVENT icalendar-5.0.12/src/icalendar/tests/events/issue_100_transformed_doctests_into_unittests.ics000066400000000000000000000000651457641316200326110ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY;LANGUAGE=ru:te END:VEVENT icalendar-5.0.12/src/icalendar/tests/events/issue_101_icalendar_chokes_on_umlauts_in_organizer.ics000066400000000000000000000005471457641316200335020ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:wichtiger termin 1 DTSTART:20130416T100000Z DTEND:20130416T110000Z DTSTAMP:20130416T092616Z UID:20130416112341.10064jz0k4j7uem8@acmenet.de CLASS:PUBLIC CREATED:20130416T092341Z LAST-MODIFIED:20130416T092341Z LOCATION:im büro ORGANIZER;CN="acme, ädmin":mailto:adm-acme@mydomain.de STATUS:CONFIRMED TRANSP:OPAQUE END:VEVENT icalendar-5.0.12/src/icalendar/tests/events/issue_104_mark_events_broken.ics000066400000000000000000000002441457641316200270630ustar00rootroot00000000000000BEGIN:VEVENT DTSTART:20140401T000000Z DTEND:20140401T010000Z DTSTAMP:20140401T000000Z SUMMARY:Broken Eevnt CLASS:PUBLIC STATUS:CONFIRMED TRANSP:OPAQUE X END:VEVENT icalendar-5.0.12/src/icalendar/tests/events/issue_112_missing_tzinfo_on_exdate.ics000066400000000000000000000012361457641316200302760ustar00rootroot00000000000000BEGIN: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 icalendar-5.0.12/src/icalendar/tests/events/issue_156_RDATE_with_PERIOD.ics000066400000000000000000000002521457641316200261470ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:RDATE period DTSTART:19961230T020000Z DTEND:19961230T060000Z UID:rdate_period RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z END:VEVENT icalendar-5.0.12/src/icalendar/tests/events/issue_156_RDATE_with_PERIOD_list.ics000066400000000000000000000003061457641316200272020ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:RDATE period DTSTART:19961230T020000Z DTEND:19961230T060000Z UID:rdate_period RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z,19970109T180000Z/PT5H 30M END:VEVENT icalendar-5.0.12/src/icalendar/tests/events/issue_157_removes_trailing_semicolon.ics000066400000000000000000000001341457641316200306340ustar00rootroot00000000000000BEGIN:VEVENT DTSTART:20150325T101010 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU; END:VEVENT icalendar-5.0.12/src/icalendar/tests/events/issue_184_broken_representation_of_period.ics000066400000000000000000000002031457641316200316400ustar00rootroot00000000000000BEGIN:VEVENT DTSTART:20150219T133000 DTSTAMP:20150219T133000 UID:1234567 RDATE;VALUE=PERIOD:20150219T133000/PT10H END:VEVENT icalendar-5.0.12/src/icalendar/tests/events/issue_464_invalid_rdate.ics000066400000000000000000000003011457641316200260150ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:RDATE period DTSTART:19961230T020000Z DTEND:19961230T060000Z UID:rdate_period RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z,199709T180000Z/PT5H30M END:VEVENT icalendar-5.0.12/src/icalendar/tests/events/issue_53_description_parsed_properly.ics000066400000000000000000000013431457641316200307460ustar00rootroot00000000000000BEGIN: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 icalendar-5.0.12/src/icalendar/tests/events/issue_64_event_with_ascii_summary.ics000066400000000000000000000000521457641316200302300ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:abcdef END:VEVENT icalendar-5.0.12/src/icalendar/tests/events/issue_64_event_with_non_ascii_summary.ics000066400000000000000000000000521457641316200311020ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:åäö END:VEVENT icalendar-5.0.12/src/icalendar/tests/events/issue_70_rrule_causes_attribute_error.ics000066400000000000000000000004271457641316200311220ustar00rootroot00000000000000BEGIN: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 icalendar-5.0.12/src/icalendar/tests/events/issue_82_expected_output.ics000066400000000000000000000001331457641316200263500ustar00rootroot00000000000000BEGIN:VEVENT ATTACH;ENCODING=BASE64;FMTTYPE=text/plain;VALUE=BINARY:dGV4dA== END:VEVENT icalendar-5.0.12/src/icalendar/tests/fuzzed/000077500000000000000000000000001457641316200207145ustar00rootroot00000000000000icalendar-5.0.12/src/icalendar/tests/fuzzed/__init__.py000066400000000000000000000013161457641316200230260ustar00rootroot00000000000000"""This is a collection of test files that are generated from the fuzzer. The fuzzer finds the cases in which the icalendar module breaks. These test cases reproduce the failure. Some more tests can be added to make sure that the behavior works properly. """ def fuzz_calendar_v1(from_ical, calendar_string: str, multiple: bool, should_walk: bool): """Take a from_ical function and reproduce the error. The calendar_string is a fuzzed input. """ cal = from_ical(calendar_string, multiple=multiple) if not multiple: cal = [cal] for c in cal: if should_walk: for event in cal.walk('VEVENT'): event.to_ical() else: cal.to_ical() generate_python_test_cases_from_downloaded_clusterfuzz_test_cases.sh000077500000000000000000000031541457641316200367270ustar00rootroot00000000000000icalendar-5.0.12/src/icalendar/tests/fuzzed#!/usr/bin/env bash # # This script generates a test case from a test case file that was downloaded. # # You will need to follow the setup instructions here: # https://google.github.io/oss-fuzz/advanced-topics/reproducing/#reproduce-using-local-source-checkout # set -e HERE="`dirname \"$0\"`" OSS_FUZZ_DIRECTORY="$HOME/oss-fuzz" DOWNLOADS_DIRECTORY="$HOME/Downloads" LOCAL_ICALENDAR_DIRECTORY="$HERE/../../../../" PYTHON_TEST_CASE_DIRECTORY="$HERE/../calendars/" PROJECT_NAME="icalendar" echo "### Building Project $PROJECT_NAME" python "$OSS_FUZZ_DIRECTORY/infra/helper.py" build_fuzzers --sanitizer undefined "$PROJECT_NAME" "$LOCAL_ICALENDAR_DIRECTORY" # we capture the output OUTPUT="`mktemp`" # test case files look like this: # clusterfuzz-testcase-minimized-ical_fuzzer-4878676239712256 for testcase in "$DOWNLOADS_DIRECTORY/clusterfuzz-testcase-"* do echo "### Reproducing $testcase" python "$OSS_FUZZ_DIRECTORY/infra/helper.py" reproduce "$PROJECT_NAME" ical_fuzzer "$testcase" | tee "$OUTPUT" if [ $PIPESTATUS -eq 0 ] then echo "### Testcase fixed! $testcase" continue fi echo "### Testcase reproduced! $testcase" TEST_FILE_CONTENT="`cat \"$OUTPUT\" | sed -n '/--- start calendar ---/,/--- end calendar ---/{/--- start calendar ---/b;/--- end calendar ---/b;p}'`" if [ -z "$TEST_FILE_CONTENT" ] then echo "### No test file content for $testcase" exit 1 fi ICS_FILE="$PYTHON_TEST_CASE_DIRECTORY/`basename \"$testcase\"`.ics" # decode and ignore garbage, see https://stackoverflow.com/a/15490765/1320237 echo $TEST_FILE_CONTENT | base64 -di > /dev/null echo "Created $ICS_FILE" done icalendar-5.0.12/src/icalendar/tests/fuzzed/test_fuzzed_calendars.py000066400000000000000000000005711457641316200256530ustar00rootroot00000000000000"""This test tests all fuzzed calendars.""" from icalendar.tests.fuzzed import fuzz_calendar_v1 import icalendar def test_fuzz_v1(fuzz_v1_calendar): """Test a calendar.""" with open(fuzz_v1_calendar, "rb") as f: fuzz_calendar_v1( icalendar.Calendar.from_ical, f.read(), multiple=True, should_walk=True ) icalendar-5.0.12/src/icalendar/tests/hypothesis/000077500000000000000000000000001457641316200216045ustar00rootroot00000000000000icalendar-5.0.12/src/icalendar/tests/hypothesis/test_fuzzing.py000066400000000000000000000020511457641316200247070ustar00rootroot00000000000000import string from hypothesis import given, settings import hypothesis.strategies as st from icalendar.parser import Contentline, Contentlines, Parameters import unittest def printable_characters(**kw): return st.text( st.characters(blacklist_categories=( 'Cc', 'Cs' ), **kw) ) key = st.text(string.ascii_letters + string.digits, min_size=1) value = printable_characters(blacklist_characters='\\;:\"') class TestFuzzing(unittest.TestCase): @given(lines=st.lists( st.tuples(key, st.dictionaries(key, value), value), min_size=1 )) @settings(max_examples=10**3) def test_main(self, lines): cl = Contentlines() for key, params, value in lines: try: params = Parameters(**params) except TypeError: # Happens when there is a random parameter 'self'... continue cl.append(Contentline.from_parts(key, params, value)) cl.append('') assert Contentlines.from_ical(cl.to_ical()) == cl icalendar-5.0.12/src/icalendar/tests/test_cli_tool.py000066400000000000000000000053031457641316200226230ustar00rootroot00000000000000import unittest from datetime import tzinfo, datetime from icalendar import Calendar, cli try: import zoneinfo except ModuleNotFoundError: from backports import zoneinfo INPUT = ''' BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN BEGIN:VEVENT SUMMARY:Test Summary ORGANIZER:organizer@test.test ATTENDEE:attendee1@example.com ATTENDEE:attendee2@test.test COMMENT:Comment DTSTART;TZID=Europe/Warsaw:20220820T103400 DTEND;TZID=Europe/Warsaw:20220820T113400 LOCATION:New Amsterdam, 1000 Sunrise Test Street DESCRIPTION: Test Description END:VEVENT BEGIN:VEVENT ORGANIZER:organizer@test.test ATTENDEE:attendee1@example.com ATTENDEE:attendee2@test.test SUMMARY:Test summary DTSTART;TZID=Europe/Warsaw:20220820T200000 DTEND;TZID=Europe/Warsaw:20220820T203000 LOCATION:New Amsterdam, 1010 Test Street DESCRIPTION:Test Description\\nThis one is multiline END:VEVENT BEGIN:VEVENT UID:1 SUMMARY:TEST DTSTART:20220511 DURATION:P5D END:VEVENT END:VCALENDAR ''' def local_datetime(dt): return datetime.strptime(dt, "%Y%m%dT%H%M%S").replace(tzinfo=zoneinfo.ZoneInfo("Europe/Warsaw")).astimezone().strftime('%c') # datetimes are displayed in the local timezone, so we cannot just hardcode them firststart = local_datetime('20220820T103400') firstend = local_datetime('20220820T113400') secondstart = local_datetime('20220820T200000') secondend = local_datetime('20220820T203000') PROPER_OUTPUT = f""" Organizer: organizer Attendees: attendee1 attendee2 Summary : Test Summary Starts : {firststart} End : {firstend} Duration : 1:00:00 Location : New Amsterdam, 1000 Sunrise Test Street Comment : Comment Description: Test Description Organizer: organizer Attendees: attendee1 attendee2 Summary : Test summary Starts : {secondstart} End : {secondend} Duration : 0:30:00 Location : New Amsterdam, 1010 Test Street Comment : Description: Test Description This one is multiline Organizer: Attendees: Summary : TEST Starts : Wed May 11 00:00:00 2022 End : Mon May 16 00:00:00 2022 Duration : 5 days, 0:00:00 Location : Comment : Description: """ class CLIToolTest(unittest.TestCase): def test_output_is_proper(self): self.maxDiff = None calendar = Calendar.from_ical(INPUT) output = '' for event in calendar.walk('vevent'): output += cli.view(event) + '\n\n' self.assertEqual(PROPER_OUTPUT, output) if __name__ == '__main__': unittest.main() icalendar-5.0.12/src/icalendar/tests/test_components_break_on_bad_ics.py000066400000000000000000000032661457641316200265160ustar00rootroot00000000000000import pytest from icalendar import Event, Calendar def test_ignore_exceptions_on_broken_events_issue_104(events): ''' Issue #104 - line parsing error in a VEVENT (which has ignore_exceptions). Should mark the event broken but not raise an exception. https://github.com/collective/icalendar/issues/104 ''' assert events.issue_104_mark_events_broken.is_broken # TODO: REMOVE FOR NEXT MAJOR RELEASE assert events.issue_104_mark_events_broken.errors == [(None, "Content line could not be parsed into parts: 'X': Invalid content line")] def test_dont_ignore_exceptions_on_broken_calendars_issue_104(calendars): '''Issue #104 - line parsing error in a VCALENDAR (which doesn't have ignore_exceptions). Should raise an exception. ''' with pytest.raises(ValueError): calendars.issue_104_broken_calendar def test_rdate_dosent_become_none_on_invalid_input_issue_464(events): '''Issue #464 - [BUG] RDATE can become None if value is invalid https://github.com/collective/icalendar/issues/464 ''' assert events.issue_464_invalid_rdate.is_broken assert ('RDATE', 'Expected period format, got: 199709T180000Z/PT5H30M') in events.issue_464_invalid_rdate.errors assert not b'RDATE:None' in events.issue_464_invalid_rdate.to_ical() @pytest.mark.parametrize('calendar_name', [ 'big_bad_calendar', 'small_bad_calendar', 'multiple_calendar_components', 'pr_480_summary_with_colon', ]) def test_error_message_doesnt_get_too_big(calendars, calendar_name): with pytest.raises(ValueError) as exception: calendars[calendar_name] # Ignore part before first : for the test. assert len(str(exception).split(': ', 1)[1]) <= 100 icalendar-5.0.12/src/icalendar/tests/test_create_release.sh000077500000000000000000000007271457641316200237540ustar00rootroot00000000000000#!/bin/sh # # Create a release file and test it. # set -e cd "`dirname \"$0\"`" cd "../../.." python3 setup.py sdist archive=`echo dist/icalendar-*.tar.gz` if ! [ -f "$archive" ]; then echo "ERROR: Cannot find distribution archive '$archive'." exit 1 fi if tar -tf "$archive" | grep 'fuzzing/'; then echo "ERROR: Fuzzing files are included in the release." echo " See https://github.com/collective/icalendar/pull/569" exit 1 fi echo "Checks passed." icalendar-5.0.12/src/icalendar/tests/test_encoding.py000066400000000000000000000065041457641316200226110ustar00rootroot00000000000000import pytest import datetime @pytest.mark.parametrize('field, expected_value', [ ('PRODID', '-//Plönë.org//NONSGML plone.app.event//EN'), ('X-WR-CALDESC', 'test non ascii: äöü ÄÖÜ €'), ]) def test_calendar_from_ical_respects_unicode(field, expected_value, calendars): cal = calendars.calendar_with_unicode assert cal[field].to_ical().decode('utf-8') == expected_value @pytest.mark.parametrize('test_input, field, expected_value', [ ('event_with_unicode_fields', 'SUMMARY', 'Non-ASCII Test: ÄÖÜ äöü €'), ('event_with_unicode_fields', 'DESCRIPTION', 'icalendar should be able to handle non-ascii: €äüöÄÜÖ.'), ('event_with_unicode_fields', 'LOCATION', 'Tribstrül'), # Non-unicode characters in summary # https://github.com/collective/icalendar/issues/64 ('issue_64_event_with_non_ascii_summary', 'SUMMARY', 'åäö'), # Unicode characters in summary ('issue_64_event_with_ascii_summary', 'SUMMARY', 'abcdef'), ]) def test_event_from_ical_respects_unicode(test_input, field, expected_value, events): event = events[test_input] assert event[field].to_ical().decode('utf-8') == expected_value @pytest.mark.parametrize('test_input, expected_output', [ # chokes on umlauts in ORGANIZER # https://github.com/collective/icalendar/issues/101 ('issue_101_icalendar_chokes_on_umlauts_in_organizer', 'acme, ädmin'), ('event_with_unicode_organizer', 'Джон Доу'), ]) def test_events_parameter_unicoded(events, test_input, expected_output): assert events[test_input]['ORGANIZER'].params['CN'] == expected_output def test_parses_event_with_non_ascii_tzid_issue_237(calendars, in_timezone): """Issue #237 - Fail to parse timezone with non-ascii TZID see https://github.com/collective/icalendar/issues/237 """ start = calendars.issue_237_fail_to_parse_timezone_with_non_ascii_tzid.walk('VEVENT')[0].decoded('DTSTART') expected = in_timezone(datetime.datetime(2017, 5, 11, 13, 30), 'America/Sao_Paulo') assert not calendars.issue_237_fail_to_parse_timezone_with_non_ascii_tzid.errors assert start == expected def test_parses_timezone_with_non_ascii_tzid_issue_237(timezones): """Issue #237 - Fail to parse timezone with non-ascii TZID see https://github.com/collective/icalendar/issues/237 """ assert timezones.issue_237_brazilia_standard['tzid'] == '(UTC-03:00) Brasília' @pytest.mark.parametrize('timezone_name', ['standard', 'daylight']) def test_parses_timezone_with_non_ascii_tzname_issue_273(timezones, timezone_name): """Issue #237 - Fail to parse timezone with non-ascii TZID see https://github.com/collective/icalendar/issues/237 """ assert timezones.issue_237_brazilia_standard.walk(timezone_name)[0]['TZNAME'] == f'Brasília {timezone_name}' def test_broken_property(calendars): """ Test if error messages are encoded properly. """ for event in calendars.broken_ical.walk('vevent'): assert len(event.errors) == 1, 'Not the right amount of errors.' error = event.errors[0][1] assert error.startswith('Content line could not be parsed into parts') def test_apple_xlocation(calendars): """ Test if we support base64 encoded binary data in parameter values. """ for event in calendars.x_location.walk('vevent'): assert len(event.errors) == 0, 'Got too many errors' icalendar-5.0.12/src/icalendar/tests/test_equality.py000066400000000000000000000116221457641316200226550ustar00rootroot00000000000000"""Test the equality and inequality of components.""" import copy import pytz from icalendar.prop import * from datetime import datetime, date, timedelta import pytest def assert_equal(actual_value, expected_value): """Make sure both values are equal""" assert actual_value == expected_value assert not actual_value != expected_value def assert_not_equal(actual_value, expected_value): """Make sure both values are not equal""" assert not actual_value == expected_value assert actual_value != expected_value def test_parsed_calendars_are_equal_if_parsed_again(ics_file): """Ensure that a calendar equals the same calendar.""" copy_of_calendar = ics_file.__class__.from_ical(ics_file.to_ical()) assert_equal(copy_of_calendar, ics_file) def test_parsed_calendars_are_equal_if_from_same_source(ics_file): """Ensure that a calendar equals the same calendar.""" cal1 = ics_file.__class__.from_ical(ics_file.raw_ics) cal2 = ics_file.__class__.from_ical(ics_file.raw_ics) assert_equal(cal1, cal2) def test_copies_are_equal(ics_file): """Ensure that copies are equal.""" copy1 = ics_file.copy(); copy1.subcomponents = ics_file.subcomponents copy2 = ics_file.copy(); copy2.subcomponents = ics_file.subcomponents[:] assert_equal(copy1, copy2) assert_equal(copy1, ics_file) assert_equal(copy2, ics_file) def test_copy_does_not_copy_subcomponents(calendars): """If we copy the subcomponents, assumptions around copies will be broken.""" assert calendars.timezoned.subcomponents assert not calendars.timezoned.copy().subcomponents def test_deep_copies_are_equal(ics_file): """Ensure that deep copies are equal.""" try: assert_equal(copy.deepcopy(ics_file), copy.deepcopy(ics_file)) except pytz.UnknownTimeZoneError: # Ignore errors when a custom time zone is used. # This is still covered by the parsing test. pass try: assert_equal(copy.deepcopy(ics_file), ics_file) except pytz.UnknownTimeZoneError: # Ignore errors when a custom time zone is used. # This is still covered by the parsing test. pass def test_vGeo(): """Check the equality of vGeo.""" assert_equal(vGeo(("100", "12.33")), vGeo(("100.00", "12.330"))) assert_not_equal(vGeo(("100", "12.331")), vGeo(("100.00", "12.330"))) assert_not_equal(vGeo(("10", "12.33")), vGeo(("100.00", "12.330"))) def test_vBinary(): assert_equal(vBinary('asd'), vBinary('asd')) assert_not_equal(vBinary('asdf'), vBinary('asd')) def test_vBoolean(): assert_equal(vBoolean.from_ical('TRUE'), vBoolean.from_ical('TRUE')) assert_equal(vBoolean.from_ical('FALSE'), vBoolean.from_ical('FALSE')) assert_not_equal(vBoolean.from_ical('TRUE'), vBoolean.from_ical('FALSE')) def test_vCategory(): assert_equal(vCategory("HELLO"), vCategory("HELLO")) assert_equal(vCategory(["a","b"]), vCategory(["a","b"])) assert_not_equal(vCategory(["a","b"]), vCategory(["a","b", "c"])) def test_vText(): assert_equal(vText("HELLO"), vText("HELLO")) assert_not_equal(vText("HELLO1"), vText("HELLO")) @pytest.mark.parametrize( "vType,v1,v2", [ (vDatetime, datetime(2023, 11, 1, 10, 11), datetime(2023, 11, 1, 10, 10)), (vDate, date(2023, 11, 1), date(2023, 10, 31)), (vDuration, timedelta(3, 11, 1), timedelta(23, 10, 31)), (vPeriod, (datetime(2023, 11, 1, 10, 11), timedelta(3, 11, 1)), (datetime(2023, 11, 1, 10, 11), timedelta(23, 10, 31))), (vPeriod, (datetime(2023, 11, 1, 10, 1), timedelta(3, 11, 1)), (datetime(2023, 11, 1, 10, 11), timedelta(3, 11, 1))), (vPeriod, (datetime(2023, 11, 1, 10, 1), datetime(2023, 11, 1, 10, 3)), (datetime(2023, 11, 1, 10, 1), datetime(2023, 11, 1, 10, 2))), (vTime, time(10, 10, 10), time(10, 10, 11)), ] ) @pytest.mark.parametrize("eq", ["==", "!="]) @pytest.mark.parametrize("cls1", [0, 1]) @pytest.mark.parametrize("cls2", [0, 1]) @pytest.mark.parametrize("hash", [lambda x:x, hash]) def test_vDDDTypes_and_others(vType, v1, v2, cls1, cls2, eq, hash): """Check equality and inequality.""" t1 = (vType, vDDDTypes)[cls1] t2 = (vType, vDDDTypes)[cls2] if eq == "==": assert hash(v1) == hash(v1) assert hash(t1(v1)) == hash(t2(v1)) assert not hash(t1(v1)) != hash(t2(v1)) else: assert hash(v1) != hash(v2) assert hash(t1(v1)) != hash(t2(v2)) def test_repr_vDDDTypes(): assert "vDDDTypes" in repr(vDDDTypes(timedelta(3, 11, 1))) vDDDLists_examples = [ vDDDLists([]), vDDDLists([datetime(2023, 11, 1, 10, 1)]), vDDDLists([datetime(2023, 11, 1, 10, 1), date(2023, 11, 1)]), ] @pytest.mark.parametrize("l1", vDDDLists_examples) @pytest.mark.parametrize("l2", vDDDLists_examples) def test_vDDDLists(l1, l2): """Check the equality functions of vDDDLists.""" equal = l1 is l2 l2 = copy.deepcopy(l2) assert equal == (l1 == l2) assert equal != (l1 != l2) icalendar-5.0.12/src/icalendar/tests/test_examples.py000066400000000000000000000030171457641316200226350ustar00rootroot00000000000000'''tests ensuring that *the* way of doing things works''' import datetime from icalendar import Calendar, Event import pytest def test_creating_calendar_with_unicode_fields(calendars, utc): ''' create a calendar with events that contain unicode characters in their fields ''' cal = Calendar() cal.add('PRODID', '-//Plönë.org//NONSGML plone.app.event//EN') cal.add('VERSION', '2.0') cal.add('X-WR-CALNAME', 'äöü ÄÖÜ €') cal.add('X-WR-CALDESC', 'test non ascii: äöü ÄÖÜ €') cal.add('X-WR-RELCALID', '12345') event = Event() event.add('DTSTART', datetime.datetime(2010, 10, 10, 10, 0, 0, tzinfo=utc)) event.add('DTEND', datetime.datetime(2010, 10, 10, 12, 0, 0, tzinfo=utc)) event.add('CREATED', datetime.datetime(2010, 10, 10, 0, 0, 0, tzinfo=utc)) event.add('UID', '123456') event.add('SUMMARY', 'Non-ASCII Test: ÄÖÜ äöü €') event.add('DESCRIPTION', 'icalendar should be able to de/serialize non-ascii.') event.add('LOCATION', 'Tribstrül') cal.add_component(event) # test_create_event_simple event1 = Event() event1.add('DTSTART', datetime.datetime(2010, 10, 10, 0, 0, 0, tzinfo=utc)) event1.add('SUMMARY', 'åäö') cal.add_component(event1) # test_unicode_parameter_name # test for issue #80 https://github.com/collective/icalendar/issues/80 event2 = Event() event2.add('DESCRIPTION', 'äöüßÄÖÜ') cal.add_component(event2) assert cal.to_ical() == calendars.created_calendar_with_unicode_fields.raw_ics icalendar-5.0.12/src/icalendar/tests/test_icalendar.py000066400000000000000000000250631457641316200227460ustar00rootroot00000000000000import icalendar import os import textwrap from ..parser import Contentlines, Contentline, Parameters, foldline from ..parser import q_join, q_split, dquote from ..prop import vText import unittest class IcalendarTestCase (unittest.TestCase): def setUp(self): if not hasattr(self, 'assertRaisesRegex'): self.assertRaisesRegex = self.assertRaisesRegexp def test_long_lines(self): 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): 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 ') ) # https://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 allowed in content lines self.assertRaises(AssertionError, Contentline, b'1234\r\n\r\n1234') self.assertEqual( Contentline('1234\\n\\n1234').to_ical(), b'1234\\n\\n1234' ) # We do not fold within a UTF-8 character c = Contentline(b'This line has a UTF-8 character where it should be ' b'folded. Make sure it g\xc3\xabts folded before that ' b'character.') self.assertIn(b'\xc3\xab', c.to_ical()) # Another test of the above c = Contentline(b'x' * 73 + b'\xc3\xab' + b'\\n ' + b'y' * 10) self.assertEqual(c.to_ical().count(b'\xc3'), 1) # Don't fail if we fold a line that is exactly X times 74 characters # long c = Contentline(''.join(['x'] * 148)).to_ical() # It can parse itself into parts, # which is a tuple of (name, params, vals) self.assertEqual( Contentline('dtstart:20050101T120000').parts(), ('dtstart', Parameters({}), '20050101T120000') ) self.assertEqual( Contentline('dtstart;value=datetime:20050101T120000').parts(), ('dtstart', Parameters({'VALUE': 'datetime'}), '20050101T120000') ) c = Contentline('ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:' 'MAILTO:maxm@example.com') self.assertEqual( c.parts(), ('ATTENDEE', Parameters({'ROLE': 'REQ-PARTICIPANT', 'CN': 'Max Rasmussen'}), 'MAILTO:maxm@example.com') ) self.assertEqual( c.to_ical().decode('utf-8'), 'ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:' 'MAILTO:maxm@example.com' ) # and back again # NOTE: we are quoting property values with spaces in it. parts = ('ATTENDEE', Parameters({'ROLE': 'REQ-PARTICIPANT', 'CN': 'Max Rasmussen'}), 'MAILTO:maxm@example.com') self.assertEqual( Contentline.from_parts(*parts), 'ATTENDEE;CN="Max Rasmussen";ROLE=REQ-PARTICIPANT:' 'MAILTO:maxm@example.com' ) # and again parts = ('ATTENDEE', Parameters(), 'MAILTO:maxm@example.com') self.assertEqual( Contentline.from_parts(*parts), 'ATTENDEE:MAILTO:maxm@example.com' ) # A value can also be any of the types defined in PropertyValues parts = ('ATTENDEE', Parameters(), vText('MAILTO:test@example.com')) self.assertEqual( Contentline.from_parts(*parts), 'ATTENDEE:MAILTO:test@example.com' ) # A value in UTF-8 parts = ('SUMMARY', Parameters(), vText('INternational char æ ø å')) self.assertEqual( Contentline.from_parts(*parts), 'SUMMARY:INternational char æ ø å' ) # A value can also be unicode parts = ('SUMMARY', Parameters(), vText('INternational char æ ø å')) self.assertEqual( Contentline.from_parts(*parts), 'SUMMARY:INternational char æ ø å' ) # Traversing could look like this. name, params, vals = c.parts() self.assertEqual(name, 'ATTENDEE') self.assertEqual(vals, 'MAILTO:maxm@example.com') self.assertEqual( sorted(params.items()), sorted([('ROLE', 'REQ-PARTICIPANT'), ('CN', 'Max Rasmussen')]) ) # And the traditional failure with self.assertRaisesRegex( ValueError, 'Content line could not be parsed into parts' ): Contentline('ATTENDEE;maxm@example.com').parts() # Another failure: with self.assertRaisesRegex( 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.assertRaisesRegex( ValueError, 'Content line could not be parsed into parts' ): Contentline.from_ical("k;:no param").parts() self.assertEqual( Contentline('key;param=pvalue:value', strict=False).parts(), ('key', Parameters({'PARAM': 'pvalue'}), 'value') ) # If strict is set to True, uppercase param values that are not # double-quoted, this is because the spec says non-quoted params are # case-insensitive. self.assertEqual( Contentline('key;param=pvalue:value', strict=True).parts(), ('key', Parameters({'PARAM': 'PVALUE'}), 'value') ) self.assertEqual( Contentline('key;param="pValue":value', strict=True).parts(), ('key', Parameters({'PARAM': 'pValue'}), 'value') ) contains_base64 = ( b'X-APPLE-STRUCTURED-LOCATION;' b'VALUE=URI;X-ADDRESS="Kaiserliche Hofburg, 1010 Wien";' b'X-APPLE-MAPKIT-HANDLE=CAESxQEZgr3QZXJyZWljaA==;' b'X-APPLE-RADIUS=328.7978217977285;X-APPLE-REFERENCEFRAME=1;' b'X-TITLE=Heldenplatz:geo:48.206686,16.363235' ) self.assertEqual( Contentline(contains_base64, strict=True).parts(), ('X-APPLE-STRUCTURED-LOCATION', Parameters({ 'X-APPLE-RADIUS': '328.7978217977285', 'X-ADDRESS': 'Kaiserliche Hofburg, 1010 Wien', 'X-APPLE-REFERENCEFRAME': '1', 'X-TITLE': 'HELDENPLATZ', 'X-APPLE-MAPKIT-HANDLE': 'CAESXQEZGR3QZXJYZWLJAA==', 'VALUE': 'URI', }), 'geo:48.206686,16.363235' ) ) def test_fold_line(self): self.assertEqual(foldline('foo'), 'foo') self.assertEqual( foldline("Lorem ipsum dolor sit amet, consectetur adipiscing " "elit. Vestibulum convallis imperdiet dui posuere."), ('Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' 'Vestibulum conval\r\n lis imperdiet dui posuere.') ) # I don't really get this test # at least just but bytes in there # porting it to "run" under python 2 & 3 makes it not much better with self.assertRaises(AssertionError): foldline('привет'.encode(), limit=3) self.assertEqual(foldline('foobar', limit=4), 'foo\r\n bar') self.assertEqual( foldline('Lorem ipsum dolor sit amet, consectetur adipiscing elit' '. Vestibulum convallis imperdiet dui posuere.'), ('Lorem ipsum dolor sit amet, consectetur adipiscing elit.' ' Vestibulum conval\r\n lis imperdiet dui posuere.') ) self.assertEqual( foldline('DESCRIPTION:АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭЮЯ'), 'DESCRIPTION:АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭ\r\n ЮЯ' ) def test_value_double_quoting(self): self.assertEqual(dquote('Max'), 'Max') self.assertEqual(dquote('Rasmussen, Max'), '"Rasmussen, Max"') self.assertEqual(dquote('name:value'), '"name:value"') def test_q_split(self): self.assertEqual(q_split('Max,Moller,"Rasmussen, Max"'), ['Max', 'Moller', '"Rasmussen, Max"']) def test_q_split_bin(self): for s in ('X-SOMETHING=ABCDE==', ',,,'): for maxsplit in range(-1, 3): self.assertEqual(q_split(s, '=', maxsplit=maxsplit), s.split('=', maxsplit)) def test_q_join(self): self.assertEqual(q_join(['Max', 'Moller', 'Rasmussen, Max']), 'Max,Moller,"Rasmussen, Max"') icalendar-5.0.12/src/icalendar/tests/test_issue_116.py000066400000000000000000000020201457641316200225270ustar00rootroot00000000000000import unittest import datetime import icalendar import os import pytz import pytest from dateutil import tz def test_issue_116(): """Issue #116/#117 - How to add 'X-APPLE-STRUCTURED-LOCATION' https://github.com/collective/icalendar/issues/116 https://github.com/collective/icalendar/issues/117 """ event = icalendar.Event() event.add( "X-APPLE-STRUCTURED-LOCATION", "geo:-33.868900,151.207000", parameters={ "VALUE": "URI", "X-ADDRESS": "367 George Street Sydney CBD NSW 2000", "X-APPLE-RADIUS": "72", "X-TITLE": "367 George Street" } ) assert 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 assert event.to_ical() == icalendar.Event.from_ical(event.to_ical()).to_ical() icalendar-5.0.12/src/icalendar/tests/test_issue_165_missing_event.py000066400000000000000000000005541457641316200254770ustar00rootroot00000000000000'''Issue #165 - Problem parsing a file with event recurring on weekdays https://github.com/collective/icalendar/issues/165 ''' from icalendar import Calendar def test_issue_165_missing_event(calendars): events = list(calendars.issue_165_missing_event.walk('VEVENT')) assert len(events) == 1, "There was an event missing from the parsed events' list." icalendar-5.0.12/src/icalendar/tests/test_issue_168_parsing_invalid_calendars_no_warning.py000066400000000000000000000010431457641316200322300ustar00rootroot00000000000000'''Issue #168 - Parsing invalid icalendars fails without any warning https://github.com/collective/icalendar/issues/168 ''' from icalendar import Calendar def test_issue_168_parsing_inavlid_calendars_no_warning(calendars): expected_error = (None, "Content line could not be parsed into parts: 'X-APPLE-RADIUS=49.91307046514149': X-APPLE-RADIUS=49.91307046514149") assert expected_error in calendars.issue_168_input.walk('VEVENT')[0].errors assert calendars.issue_168_input.to_ical() == calendars.issue_168_expected_output.raw_ics icalendar-5.0.12/src/icalendar/tests/test_issue_27_period.py000066400000000000000000000011701457641316200240170ustar00rootroot00000000000000'''Issue #27 - multiple periods https://github.com/collective/icalendar/issues/27 ''' from icalendar import Calendar def test_issue_27_multiple_periods(calendars): free_busy = list(calendars.issue_27_multiple_periods_in_freebusy_multiple_freebusies.walk('VFREEBUSY'))[0] free_busy_period = free_busy['freebusy'] print(free_busy['freebusy']) equivalent_way_of_defining_free_busy = list(calendars.issue_27_multiple_periods_in_freebusy_one_freebusy.walk('VFREEBUSY'))[0] free_busy_period_equivalent = equivalent_way_of_defining_free_busy['freebusy'] assert free_busy_period == free_busy_period_equivalent icalendar-5.0.12/src/icalendar/tests/test_issue_318_skip_default_parameters.py000066400000000000000000000012051457641316200275140ustar00rootroot00000000000000"""Some parameters are specified as default by the RFC 5545. These tests make sure that these parameter values are not added to the properties. Example: DTSTART;VALUE=DATE-TIME:20190616T050000Z equals DTSTART:20190616T050000Z """ import pytest from icalendar import Event from datetime import datetime @pytest.mark.parametrize("attr", [ "DTSTART", "DTEND", "DTSTAMP", ]) def test_datetime_in_event(attr): """Check that the "VALUE=DATE-TIME" is absent because not needed.""" event = Event() event.add(attr, datetime(2022, 10, 13, 9, 16, 42)) ics = event.to_ical() assert b"VALUE=DATE-TIME" not in ics test_issue_322_single_strings_characters_split_into_multiple_categories.py000066400000000000000000000006101457641316200363470ustar00rootroot00000000000000icalendar-5.0.12/src/icalendar/testsfrom icalendar import Calendar, Event def test_issue_322_single_string_split_into_multiple_categories(calendars): calendar = Calendar() event = Event() event.add('summary', 'Event with bare string as argument for categories') event.add('categories', "Lecture") calendar.add_component(event) assert calendar.to_ical() == calendars.issue_322_expected_calendar.raw_ics icalendar-5.0.12/src/icalendar/tests/test_issue_348_exception_parsing_value.py000066400000000000000000000014761457641316200275510ustar00rootroot00000000000000"""These are tests for Issue #348 see https://github.com/collective/icalendar/issues/348 """ def test_calendar_can_be_parsed_correctly(calendars): """Exception when there's no ':' when parsing value #348 see https://github.com/collective/icalendar/issues/348 """ freebusy = calendars.issue_348_exception_parsing_value.walk("VFREEBUSY")[0] assert freebusy["ORGANIZER"].params["CN"] == "Sixt SE" def test_parameters_are_not_truncated(calendars): """We skip to the end and we do not want to loose parameters. see https://github.com/collective/icalendar/pull/514#issuecomment-1505878801 """ freebusy = calendars.issue_348_exception_parsing_value.walk("VFREEBUSY")[0] assert freebusy["X-ORGANIZER2"].params["CN"] == "Sixt SE" assert freebusy["X-ORGANIZER2"].params["CN2"] == "Test!" icalendar-5.0.12/src/icalendar/tests/test_issue_500_vboolean_for_parameter.py000066400000000000000000000006131457641316200273250ustar00rootroot00000000000000from icalendar import Event, vBoolean, vCalAddress def test_vBoolean_can_be_used_as_parameter_issue_500(events): '''https://github.com/collective/icalendar/issues/500''' attendee = vCalAddress(f'mailto:someone@example.com') attendee.params['rsvp'] = vBoolean(True) event = Event() event.add('attendee', attendee) assert event.to_ical() == events.event_with_rsvp.raw_ics icalendar-5.0.12/src/icalendar/tests/test_issue_557_encode_native_parameters.py000066400000000000000000000117061457641316200276610ustar00rootroot00000000000000"""These are tests for Issue #557 TL;DR: Component._encode lost given parameters if the object to encode was already of native type, making its behavior unexpected. see https://github.com/collective/icalendar/issues/557""" import unittest from icalendar.cal import Component class TestComponentEncode(unittest.TestCase): def test_encode_non_native_parameters(self): """Test _encode to add parameters to non-natives""" self.__assert_native_content(self.summary) self.__assert_native_kept_parameters(self.summary) def test_encode_native_keep_params_None(self): """_encode should keep parameters on natives if parameters=None """ new_sum = self.__add_params( self.summary, parameters=None, ) self.__assert_native_content(new_sum) self.__assert_native_kept_parameters(new_sum) def test_encode_native_keep_params_empty(self): """_encode should keep paramters on natives if parameters={} """ new_sum = self.__add_params( self.summary, parameters={}, ) self.__assert_native_content(new_sum) self.__assert_native_kept_parameters(new_sum) def test_encode_native_append_params(self): """_encode should append paramters on natives keeping old parameters """ new_sum = self.__add_params( self.summary, parameters={"X-PARAM": "Test123"}, ) self.__assert_native_content(new_sum) self.__assert_native_kept_parameters(new_sum) self.assertParameter(new_sum, "X-PARAM", "Test123") def test_encode_native_overwrite_params(self): """_encode should overwrite single parameters if they have the same name as old ones""" new_sum = self.__add_params( self.summary, parameters={"LANGUAGE": "de"}, ) self.__assert_native_content(new_sum) self.assertParameter(new_sum, "LANGUAGE", "de") def test_encode_native_remove_params(self): """_encode should remove single parameters if they are explicitly set to None""" new_sum = self.__add_params( self.summary, parameters={"LANGUAGE": None}, ) self.__assert_native_content(new_sum) self.assertParameterMissing(new_sum, "LANGUAGE") def test_encode_native_remove_already_missing(self): """_encode should ignore removing a parameter that was already missing""" self.assertParameterMissing(self.summary, "X-MISSING") new_sum = self.__add_params( self.summary, parameters={"X-MISSING": None}, ) self.__assert_native_content(new_sum) self.__assert_native_kept_parameters(new_sum) self.assertParameterMissing(self.summary, "X-MISSING") def test_encode_native_full_test(self): """full test case with keeping, overwriting & removing properties""" # preperation orig_sum = self.__add_params( self.summary, parameters={ "X-OVERWRITE": "overwrite me!", "X-REMOVE": "remove me!", "X-MISSING": None, }, ) # preperation check self.__assert_native_content(orig_sum) self.__assert_native_kept_parameters(orig_sum) self.assertParameter(orig_sum, "X-OVERWRITE", "overwrite me!") self.assertParameter(orig_sum, "X-REMOVE", "remove me!") self.assertParameterMissing(orig_sum, "X-MISSING") # modification new_sum = self.__add_params( orig_sum, parameters={ "X-OVERWRITE": "overwritten", "X-REMOVE": None, "X-MISSING": None, }, ) # final asserts self.__assert_native_content(new_sum) self.__assert_native_kept_parameters(new_sum) self.assertParameter(new_sum, "X-OVERWRITE", "overwritten") self.assertParameterMissing(new_sum, "X-REMOVE") self.assertParameterMissing(new_sum, "X-MISSING") def setUp(self): self.summary = self.__gen_native() def __assert_native_kept_parameters(self, obj): self.assertParameter(obj, "LANGUAGE", "en") def __assert_native_content(self, obj): self.assertEqual(obj, "English Summary") def __add_params(self, obj, parameters): return Component._encode( "SUMMARY", obj, parameters=parameters, encode=True, ) def __gen_native(self): return Component._encode( "SUMMARY", "English Summary", parameters={ "LANGUAGE": "en", }, encode=True, ) def assertParameterMissing(self, obj, name): self.assertNotIn(name, obj.params) def assertParameter(self, obj, name, val): self.assertIn(name, obj.params) self.assertEqual(obj.params[name], val) icalendar-5.0.12/src/icalendar/tests/test_multiple.py000066400000000000000000000010261457641316200226500ustar00rootroot00000000000000"""An example with multiple VCALENDAR components""" from icalendar import Calendar from icalendar.prop import vText def test_multiple(calendars): """Check opening multiple calendars.""" cals = calendars.multiple.multiple_calendar_components assert len(cals) == 2 assert [comp.name for comp in cals[0].walk()] == ['VCALENDAR', 'VEVENT'] assert [comp.name for comp in cals[1].walk()] == ['VCALENDAR', 'VEVENT', 'VEVENT'] assert cals[0]['prodid'] == vText('-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN') icalendar-5.0.12/src/icalendar/tests/test_oss_fuzz_errors.py000066400000000000000000000011051457641316200242710ustar00rootroot00000000000000"""This file collects errors that the OSS FUZZ build has found.""" from datetime import time from icalendar import Calendar from icalendar.prop import vDDDLists import pytest def test_stack_is_empty(): """If we get passed an invalid string, we expect to get a ValueError.""" with pytest.raises(ValueError): Calendar.from_ical("END:CALENDAR") def test_vdd_list_type_mismatch(): """If we pass in a string type, we expect it to be converted to bytes""" vddd_list = vDDDLists([time(hour=6, minute=6, second=6)]) assert vddd_list.to_ical() == b'060606' icalendar-5.0.12/src/icalendar/tests/test_parsing.py000066400000000000000000000204011457641316200224560ustar00rootroot00000000000000'''Tests checking that parsing works''' import pytest import base64 from icalendar import Calendar, vRecur, vBinary, Event from datetime import datetime from icalendar.parser import Contentline, Parameters @pytest.mark.parametrize('calendar_name', [ # Issue #178 - A component with an unknown/invalid name is represented # as one of the known components, the information about the original # component name is lost. # https://github.com/collective/icalendar/issues/178 https://github.com/collective/icalendar/pull/180 # Parsing of a nonstandard component 'issue_178_component_with_invalid_name_represented', # Nonstandard component inside other components, also has properties 'issue_178_custom_component_inside_other', # Nonstandard component is able to contain other components 'issue_178_custom_component_contains_other', ]) def test_calendar_to_ical_is_inverse_of_from_ical(calendars, calendar_name): calendar = getattr(calendars, calendar_name) assert calendar.to_ical().splitlines() == calendar.raw_ics.splitlines() assert calendar.to_ical() == calendar.raw_ics @pytest.mark.parametrize('raw_content_line, expected_output', [ # Issue #142 - Multivalued parameters. This is needed for VCard 3.0. # see https://github.com/collective/icalendar/pull/142 ('TEL;TYPE=HOME,VOICE:000000000', ('TEL', Parameters({'TYPE': ['HOME', 'VOICE']}), '000000000')), # Issue #143 - Allow dots in property names. Another vCard related issue. # see https://github.com/collective/icalendar/pull/143 ('ITEMADRNULLTHISISTHEADRESS08158SOMECITY12345.ADR:;;This is the Adress 08; Some City;;12345;Germany', \ ('ITEMADRNULLTHISISTHEADRESS08158SOMECITY12345.ADR', \ Parameters(),\ ';;This is the Adress 08; Some City;;12345;Germany')), ('ITEMADRNULLTHISISTHEADRESS08158SOMECITY12345.X-ABLABEL:', \ ('ITEMADRNULLTHISISTHEADRESS08158SOMECITY12345.X-ABLABEL', \ Parameters(), \ '')) ]) def test_content_lines_parsed_properly(raw_content_line, expected_output): assert Contentline.from_ical(raw_content_line).parts() == expected_output @pytest.mark.parametrize('timezone_info', [ # General timezone aware dates in ical string (b'DTSTART;TZID=America/New_York:20130907T120000'), (b'DTEND;TZID=America/New_York:20130907T170000'), # Specific timezone aware exdates in ical string (b'EXDATE;TZID=America/New_York:20131012T120000'), (b'EXDATE;TZID=America/New_York:20131011T120000') ]) def test_timezone_info_present_in_ical_issue_112(events, timezone_info): '''Issue #112 - No timezone info on EXDATE https://github.com/collective/icalendar/issues/112 ''' timezone_info in events.issue_112_missing_tzinfo_on_exdate.to_ical() def test_timezone_name_parsed_issue_112(events): '''Issue #112 - No timezone info on EXDATE https://github.com/collective/icalendar/issues/112 ''' assert events.issue_112_missing_tzinfo_on_exdate['exdate'][0].dts[0].dt.tzname() == 'EDT' def test_issue_157_removes_trailing_semicolon(events): '''Issue #157 - Recurring rules and trailing semicolons https://github.com/collective/icalendar/pull/157 ''' recur = events.issue_157_removes_trailing_semicolon.decoded("RRULE") assert isinstance(recur, vRecur) assert recur.to_ical() == b'FREQ=YEARLY;BYDAY=1SU;BYMONTH=11' @pytest.mark.parametrize('event_name', [ # https://github.com/collective/icalendar/pull/100 ('issue_100_transformed_doctests_into_unittests'), ('issue_184_broken_representation_of_period'), # PERIOD should be put back into shape 'issue_156_RDATE_with_PERIOD', 'issue_156_RDATE_with_PERIOD_list', 'event_with_unicode_organizer', ]) def test_event_to_ical_is_inverse_of_from_ical(events, event_name): """Make sure that an event's ICS is equal to the ICS it was made from.""" event = events[event_name] assert event.to_ical().splitlines() == event.raw_ics.splitlines() assert event.to_ical() == event.raw_ics def test_decode_rrule_attribute_error_issue_70(events): # Issue #70 - e.decode("RRULE") causes Attribute Error # see https://github.com/collective/icalendar/issues/70 recur = events.issue_70_rrule_causes_attribute_error.decoded('RRULE') assert isinstance(recur, vRecur) assert recur.to_ical() == b'FREQ=WEEKLY;UNTIL=20070619T225959;INTERVAL=1' def test_description_parsed_properly_issue_53(events): '''Issue #53 - Parsing failure on some descriptions? https://github.com/collective/icalendar/issues/53 ''' assert b'July 12 at 6:30 PM' in events.issue_53_description_parsed_properly['DESCRIPTION'].to_ical() def test_raises_value_error_for_properties_without_parent_pull_179(): '''Found an issue where from_ical() would raise IndexError for properties without parent components. https://github.com/collective/icalendar/pull/179 ''' with pytest.raises(ValueError): Calendar.from_ical('VERSION:2.0') def test_tzid_parsed_properly_issue_53(timezones): '''Issue #53 - Parsing failure on some descriptions? https://github.com/collective/icalendar/issues/53 ''' assert timezones.issue_53_tzid_parsed_properly['tzid'].to_ical() == b'America/New_York' def test_timezones_to_ical_is_inverse_of_from_ical(timezones): '''Issue #55 - Parse error on utc-offset with seconds value see https://github.com/collective/icalendar/issues/55''' timezone = timezones['issue_55_parse_error_on_utc_offset_with_seconds'] assert timezone.to_ical() == timezone.raw_ics @pytest.mark.parametrize('date, expected_output', [ (datetime(2012, 7, 16, 0, 0, 0), b'DTSTART:20120716T000000Z'), (datetime(2021, 11, 17, 15, 9, 15), b'DTSTART:20211117T150915Z') ]) def test_no_tzid_when_utc(utc, date, expected_output): '''Issue #58 - TZID on UTC DATE-TIMEs Issue #335 - UTC timezone identification is broken https://github.com/collective/icalendar/issues/58 https://github.com/collective/icalendar/issues/335 ''' # 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. date = date.replace(tzinfo=utc) event = Event() event.add('dtstart', date) assert expected_output in event.to_ical() def test_vBinary_base64_encoded_issue_82(): '''Issue #82 - vBinary __repr__ called rather than to_ical from container types https://github.com/collective/icalendar/issues/82 ''' b = vBinary('text') b.params['FMTTYPE'] = 'text/plain' assert b.to_ical() == base64.b64encode(b'text') def test_creates_event_with_base64_encoded_attachment_issue_82(events): '''Issue #82 - vBinary __repr__ called rather than to_ical from container types https://github.com/collective/icalendar/issues/82 ''' b = vBinary('text') b.params['FMTTYPE'] = 'text/plain' event = Event() event.add('ATTACH', b) assert event.to_ical() == events.issue_82_expected_output.raw_ics @pytest.mark.parametrize('calendar_name', [ # Issue #466 - [BUG] TZID timezone is ignored when forward-slash is used # https://github.com/collective/icalendar/issues/466 'issue_466_respect_unique_timezone', 'issue_466_convert_tzid_with_slash' ]) def test_handles_unique_tzid(calendars, in_timezone, calendar_name): calendar = calendars[calendar_name] start_dt = calendar.walk('VEVENT')[0]['dtstart'].dt end_dt = calendar.walk('VEVENT')[0]['dtend'].dt assert start_dt == in_timezone(datetime(2022, 10, 21, 20, 0, 0), 'Europe/Stockholm') assert end_dt == in_timezone(datetime(2022, 10, 21, 21, 0, 0), 'Europe/Stockholm') @pytest.mark.parametrize('event_name, expected_cn, expected_ics', [ ('event_with_escaped_characters', r'that, that; %th%%at%\ that:', 'это, то; that\\ %th%%at%:'), ('event_with_escaped_character1', r'Society, 2014', 'that'), ('event_with_escaped_character2', r'Society\ 2014', 'that'), ('event_with_escaped_character3', r'Society; 2014', 'that'), ('event_with_escaped_character4', r'Society: 2014', 'that'), ]) def test_escaped_characters_read(event_name, expected_cn, expected_ics, events): event = events[event_name] assert event['ORGANIZER'].params['CN'] == expected_cn assert event['ORGANIZER'].to_ical() == expected_ics.encode('utf-8') icalendar-5.0.12/src/icalendar/tests/test_period.py000066400000000000000000000057701457641316200223110ustar00rootroot00000000000000"""These tests make sure that we have some coverage on the usage of the PERIOD value type. See - https://github.com/collective/icalendar/issues/156 - https://github.com/pimutils/khal/issues/152#issuecomment-933635248 """ import pytest import pytz from icalendar.prop import vDDDTypes import datetime @pytest.mark.parametrize("calname,tzname,index,period_string", [ ("issue_156_RDATE_with_PERIOD_TZID_khal_2", "Europe/Berlin", 0, "20211101T160000/20211101T163000"), ("issue_156_RDATE_with_PERIOD_TZID_khal_2", "Europe/Berlin", 1, "20211206T160000/20211206T163000"), ("issue_156_RDATE_with_PERIOD_TZID_khal_2", "Europe/Berlin", 2, "20220103T160000/20220103T163000"), ("issue_156_RDATE_with_PERIOD_TZID_khal_2", "Europe/Berlin", 3, "20220207T160000/20220207T163000"), ] + [ ("issue_156_RDATE_with_PERIOD_TZID_khal", "America/Chicago", i, period) for i, period in enumerate(("20180327T080000/20180327T0" "90000,20180403T080000/20180403T090000,20180410T080000/20180410T090000,2018" "0417T080000/20180417T090000,20180424T080000/20180424T090000,20180501T08000" "0/20180501T090000,20180508T080000/20180508T090000,20180515T080000/20180515" "T090000,20180522T080000/20180522T090000,20180529T080000/20180529T090000,20" "180605T080000/20180605T090000,20180612T080000/20180612T090000,20180619T080" "000/20180619T090000,20180626T080000/20180626T090000,20180703T080000/201807" "03T090000,20180710T080000/20180710T090000,20180717T080000/20180717T090000," "20180724T080000/20180724T090000,20180731T080000/20180731T090000").split(",")) ]) def test_issue_156_period_list_in_rdate(calendars, calname, tzname, index, period_string): """Check items in a list of period values.""" calendar = calendars[calname] rdate = calendar.walk("vevent")[0]["rdate"] period = rdate.dts[index] assert period.dt == vDDDTypes.from_ical(period_string, timezone=tzname) def test_duration_properly_parsed(events): """This checks the duration PT5H30M.""" start = vDDDTypes.from_ical("19970109T180000Z") duration = vDDDTypes.from_ical("PT5H30M") rdate = events.issue_156_RDATE_with_PERIOD_list["RDATE"] print(rdate) period = rdate.dts[1].dt print(dir(duration)) assert period[0] == start assert period[1].days == 0 assert period[1].seconds == (5 * 60 + 30) * 60 assert period[1] == duration def test_tzid_is_part_of_the_parameters(calendars): """The TZID should be mentioned in the parameters.""" event = list(calendars.period_with_timezone.walk("VEVENT"))[0] assert event["RDATE"].params["TZID"] == "America/Vancouver" def test_tzid_is_part_of_the_period_values(calendars): """The TZID should be set in the datetime.""" event = list(calendars.period_with_timezone.walk("VEVENT"))[0] start, end = event["RDATE"].dts[0].dt assert start == pytz.timezone("America/Vancouver").localize(datetime.datetime(2023, 12, 13, 12)) assert end == pytz.timezone("America/Vancouver").localize(datetime.datetime(2023, 12, 13, 15)) icalendar-5.0.12/src/icalendar/tests/test_property_params.py000066400000000000000000000131271457641316200242510ustar00rootroot00000000000000import pytest from icalendar import Calendar, Event, Parameters, vCalAddress import unittest import icalendar import re @pytest.mark.parametrize('parameter, expected', [ # Simple parameter:value pair (Parameters(parameter1='Value1'), b'PARAMETER1=Value1'), # Parameter with list of values must be separated by comma (Parameters({'parameter1': ['Value1', 'Value2']}), b'PARAMETER1=Value1,Value2'), # Multiple parameters must be separated by a semicolon (Parameters({'RSVP': 'TRUE', 'ROLE': 'REQ-PARTICIPANT'}), b'ROLE=REQ-PARTICIPANT;RSVP=TRUE'), # Parameter values containing ',;:' must be double quoted (Parameters({'ALTREP': 'http://www.wiz.org'}), b'ALTREP="http://www.wiz.org"'), # list items must be quoted separately (Parameters({'MEMBER': ['MAILTO:projectA@host.com', 'MAILTO:projectB@host.com']}), b'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"'), (Parameters({'parameter1': 'Value1', 'parameter2': ['Value2', 'Value3'], 'ALTREP': ['http://www.wiz.org', 'value4']}), b'ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;PARAMETER2=Value2,Value3'), # Including empty strings (Parameters({'PARAM': ''}), b'PARAM='), # We can also parse parameter strings (Parameters({'MEMBER': ['MAILTO:projectA@host.com', 'MAILTO:projectB@host.com']}), b'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"'), # We can also parse parameter strings (Parameters({'PARAMETER1': 'Value1', 'ALTREP': ['http://www.wiz.org', 'value4'], 'PARAMETER2': ['Value2', 'Value3']}), b'ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;PARAMETER2=Value2,Value3'), ]) def test_parameter_to_ical_is_inverse_of_from_ical(parameter, expected): assert parameter.to_ical() == expected assert Parameters.from_ical(expected.decode('utf-8')) == parameter def test_parse_parameter_string_without_quotes(): assert Parameters.from_ical('PARAM1=Value 1;PARA2=Value 2') == Parameters({'PARAM1': 'Value 1', 'PARA2': 'Value 2'}) def test_parametr_is_case_insensitive(): parameter = Parameters(parameter1='Value1') assert parameter['parameter1'] == parameter['PARAMETER1'] == parameter['PaRaMeTer1'] def test_parameter_keys_are_uppercase(): parameter = Parameters(parameter1='Value1') assert list(parameter.keys()) == ['PARAMETER1'] @pytest.mark.parametrize('cn_param, cn_quoted', [ # not double-quoted ('Aramis', 'Aramis'), # if a space is present - enclose in double quotes ('Aramis Alameda', '"Aramis Alameda"'), # a single quote in parameter value - double quote the value ('Aramis d\'Alameda', '"Aramis d\'Alameda"'), ('Арамис д\'Аламеда', '"Арамис д\'Аламеда"'), # double quote is replaced with single quote ('Aramis d\"Alameda', '"Aramis d\'Alameda"'), ]) def test_quoting(cn_param, cn_quoted): event = Event() attendee = vCalAddress('test@example.com') attendee.params['CN'] = cn_param event.add('ATTENDEE', attendee) assert f'ATTENDEE;CN={cn_quoted}:test@example.com' in event.to_ical().decode('utf-8') 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_parse_and_access_property_params(self): """Parse an ics string and access some property parameters then. This is a follow-up of a question received per email. """ ics = """BEGIN:VCALENDAR VERSION:2.0 PRODID://RESEARCH IN MOTION//BIS 3.0 METHOD:REQUEST BEGIN:VEVENT SEQUENCE:2 X-RIM-REVISION:0 SUMMARY:Test meeting from BB X-MICROSOFT-CDO-ALLDAYEVENT:TRUE CLASS:PUBLIC ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN="RembrandXS":MAILTO:rembrand@xs4all.nl ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN="RembrandDX":MAILTO:rembrand@daxlab.com ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN="RembrandSB":MAILTO:rembspam@xs4all.nl UID:XRIMCAL-628059586-522954492-9750559 DTSTART;VALUE=DATE:20120814 DTEND;VALUE=DATE:20120815 DESCRIPTION:Test meeting from BB DTSTAMP:20120813T151458Z ORGANIZER:mailto:rembrand@daxlab.com END:VEVENT END:VCALENDAR""" cal = icalendar.Calendar.from_ical(ics) event = cal.walk("VEVENT")[0] event['attendee'][0] self.assertEqual(event['attendee'][0].to_ical(), b'MAILTO:rembrand@xs4all.nl') self.assertEqual(event['attendee'][0].params.to_ical(), b'CN=RembrandXS;PARTSTAT=NEEDS-ACTION;RSVP=TRUE') self.assertEqual(event['attendee'][0].params['cn'], 'RembrandXS') def test_repr(self): """Test correct class representation. """ it = Parameters(parameter1='Value1') self.assertTrue( re.match(r"Parameters\({u?'PARAMETER1': u?'Value1'}\)", str(it)) ) icalendar-5.0.12/src/icalendar/tests/test_recurrence.py000066400000000000000000000105111457641316200231510ustar00rootroot00000000000000from icalendar import Event from datetime import date, datetime import pytest def test_recurrence_properly_parsed(events): assert events.event_with_recurrence['rrule'] == {'COUNT': [100], 'FREQ': ['DAILY']} @pytest.mark.parametrize('i, exception_date', [ (0, datetime(1996, 4, 2, 1, 0)), (1, datetime(1996, 4, 3, 1, 0)), (2, datetime(1996, 4, 4, 1, 0)) ]) def test_exdate_properly_parsed(events, i, exception_date, in_timezone): assert events.event_with_recurrence['exdate'].dts[i].dt == in_timezone(exception_date, 'UTC') def test_exdate_properly_marshalled(events): actual = events.event_with_recurrence['exdate'].to_ical() assert actual == b'19960402T010000Z,19960403T010000Z,19960404T010000Z' # 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: def test_exdate_formed_from_exdates_on_multiple_lines_is_a_list(events): exdate = events.event_with_recurrence_exdates_on_different_lines['exdate'] assert isinstance(exdate, list) @pytest.mark.parametrize('i, exception_date, exception_date_ics', [ (0, datetime(2012, 5, 29, 10, 0), b'20120529T100000'), (1, datetime(2012, 4, 3, 10, 0), b'20120403T100000'), (2, datetime(2012, 4, 10, 10, 0), b'20120410T100000'), (3, datetime(2012, 5, 1, 10, 0), b'20120501T100000'), (4, datetime(2012, 4, 17, 10, 0), b'20120417T100000') ]) def test_list_exdate_to_ical_is_inverse_of_from_ical(events, i, exception_date, exception_date_ics, in_timezone): exdate = events.event_with_recurrence_exdates_on_different_lines['exdate'] assert exdate[i].dts[0].dt == in_timezone(exception_date, 'Europe/Vienna') assert exdate[i].to_ical() == exception_date_ics @pytest.mark.parametrize('freq, byday, dtstart, expected', [ # Test some YEARLY BYDAY repeats ('YEARLY', '1SU', date(2016,1,3), # 1st Sunday in year b'BEGIN:VEVENT\r\nSUMMARY:Event YEARLY 1SU\r\nDTSTART;VALUE=DATE:20160103\r\nRRULE:FREQ=YEARLY;BYDAY=1SU\r\nEND:VEVENT\r\n'), ('YEARLY', '53MO', date(1984,12,31), # 53rd Monday in (leap) year b'BEGIN:VEVENT\r\nSUMMARY:Event YEARLY 53MO\r\nDTSTART;VALUE=DATE:19841231\r\nRRULE:FREQ=YEARLY;BYDAY=53MO\r\nEND:VEVENT\r\n'), ('YEARLY', '-1TU', date(1999,12,28), # Last Tuesday in year b'BEGIN:VEVENT\r\nSUMMARY:Event YEARLY -1TU\r\nDTSTART;VALUE=DATE:19991228\r\nRRULE:FREQ=YEARLY;BYDAY=-1TU\r\nEND:VEVENT\r\n'), ('YEARLY', '-17WE', date(2000,9,6), # 17th-to-last Wednesday in year b'BEGIN:VEVENT\r\nSUMMARY:Event YEARLY -17WE\r\nDTSTART;VALUE=DATE:20000906\r\nRRULE:FREQ=YEARLY;BYDAY=-17WE\r\nEND:VEVENT\r\n'), # Test some MONTHLY BYDAY repeats ('MONTHLY', '2TH', date(2003,4,10), # 2nd Thursday in month b'BEGIN:VEVENT\r\nSUMMARY:Event MONTHLY 2TH\r\nDTSTART;VALUE=DATE:20030410\r\nRRULE:FREQ=MONTHLY;BYDAY=2TH\r\nEND:VEVENT\r\n'), ('MONTHLY', '-3FR', date(2017,5,12), # 3rd-to-last Friday in month b'BEGIN:VEVENT\r\nSUMMARY:Event MONTHLY -3FR\r\nDTSTART;VALUE=DATE:20170512\r\nRRULE:FREQ=MONTHLY;BYDAY=-3FR\r\nEND:VEVENT\r\n'), ('MONTHLY', '-5SA', date(2053,11,1), # 5th-to-last Saturday in month b'BEGIN:VEVENT\r\nSUMMARY:Event MONTHLY -5SA\r\nDTSTART;VALUE=DATE:20531101\r\nRRULE:FREQ=MONTHLY;BYDAY=-5SA\r\nEND:VEVENT\r\n'), # Specifically test examples from the report of Issue #518 # https://github.com/collective/icalendar/issues/518 ('YEARLY', '9MO', date(2023,2,27), # 9th Monday in year b'BEGIN:VEVENT\r\nSUMMARY:Event YEARLY 9MO\r\nDTSTART;VALUE=DATE:20230227\r\nRRULE:FREQ=YEARLY;BYDAY=9MO\r\nEND:VEVENT\r\n'), ('YEARLY', '10MO', date(2023,3,6), # 10th Monday in year b'BEGIN:VEVENT\r\nSUMMARY:Event YEARLY 10MO\r\nDTSTART;VALUE=DATE:20230306\r\nRRULE:FREQ=YEARLY;BYDAY=10MO\r\nEND:VEVENT\r\n'), ]) def test_byday_to_ical(freq, byday, dtstart, expected): 'Test the BYDAY rule is correctly processed by to_ical().' event = Event() event.add('SUMMARY', ' '.join(['Event', freq, byday])) event.add('DTSTART', dtstart) event.add('RRULE', {'FREQ':[freq], 'BYDAY':byday}) assert event.to_ical() == expected icalendar-5.0.12/src/icalendar/tests/test_time.py000066400000000000000000000015711457641316200217600ustar00rootroot00000000000000import 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, 'calendars', 'time.ics'), 'rb') cal = icalendar.Calendar.from_ical(ics.read()) ics.close() self.assertEqual(cal['X-SOMETIME'].dt, datetime.time(17, 20, 10)) self.assertEqual(cal['X-SOMETIME'].to_ical(), '172010') def test_create_to_ical(self): cal = icalendar.Calendar() cal.add('X-SOMETIME', datetime.time(17, 20, 10)) self.assertTrue(b'X-SOMETIME;VALUE=TIME:172010' in cal.to_ical().splitlines()) icalendar-5.0.12/src/icalendar/tests/test_timezoned.py000066400000000000000000000505101457641316200230150ustar00rootroot00000000000000import unittest import datetime import dateutil.parser import icalendar import os import pytz try: import zoneinfo except: from backports import zoneinfo HERE = os.path.dirname(__file__) CALENDARS_DIRECTORY = os.path.join(HERE, 'calendars') class TestTimezoned(unittest.TestCase): def test_create_from_ical_zoneinfo(self): with open(os.path.join(CALENDARS_DIRECTORY, 'timezoned.ics'), 'rb') as fp: data = fp.read() cal = icalendar.Calendar.from_ical(data) self.assertEqual( cal['prodid'].to_ical(), b"-//Plone.org//NONSGML plone.app.event//EN" ) timezones = cal.walk('VTIMEZONE') self.assertEqual(len(timezones), 1) tz = timezones[0] self.assertEqual(tz['tzid'].to_ical(), b"Europe/Vienna") std = tz.walk('STANDARD')[0] self.assertEqual( std.decoded('TZOFFSETFROM'), datetime.timedelta(0, 7200) ) ev1 = cal.walk('VEVENT')[0] self.assertEqual( ev1.decoded('DTSTART'), datetime.datetime(2012, 2, 13, 10, 0, 0, tzinfo=zoneinfo.ZoneInfo('Europe/Vienna')) ) self.assertEqual( ev1.decoded('DTSTAMP'), datetime.datetime(2010, 10, 10, 9, 10, 10, tzinfo=zoneinfo.ZoneInfo('UTC')) ) def test_create_from_ical_pytz(self): with open(os.path.join(CALENDARS_DIRECTORY, 'timezoned.ics'), 'rb') as fp: data = fp.read() cal = icalendar.Calendar.from_ical(data) self.assertEqual( cal['prodid'].to_ical(), b"-//Plone.org//NONSGML plone.app.event//EN" ) timezones = cal.walk('VTIMEZONE') self.assertEqual(len(timezones), 1) tz = timezones[0] self.assertEqual(tz['tzid'].to_ical(), b"Europe/Vienna") std = tz.walk('STANDARD')[0] self.assertEqual( std.decoded('TZOFFSETFROM'), datetime.timedelta(0, 7200) ) ev1 = cal.walk('VEVENT')[0] self.assertEqual( ev1.decoded('DTSTART'), pytz.timezone('Europe/Vienna').localize( datetime.datetime(2012, 2, 13, 10, 0, 0) ) ) self.assertEqual( ev1.decoded('DTSTAMP'), pytz.utc.localize( datetime.datetime(2010, 10, 10, 9, 10, 10) ) ) def test_create_to_ical_pytz(self): cal = icalendar.Calendar() cal.add('prodid', "-//Plone.org//NONSGML plone.app.event//EN") cal.add('version', "2.0") cal.add('x-wr-calname', "test create calendar") cal.add('x-wr-caldesc', "icalendar tests") cal.add('x-wr-relcalid', "12345") cal.add('x-wr-timezone', "Europe/Vienna") tzc = icalendar.Timezone() tzc.add('tzid', 'Europe/Vienna') tzc.add('x-lic-location', 'Europe/Vienna') tzs = icalendar.TimezoneStandard() tzs.add('tzname', 'CET') tzs.add('dtstart', datetime.datetime(1970, 10, 25, 3, 0, 0)) tzs.add('rrule', {'freq': 'yearly', 'bymonth': 10, 'byday': '-1su'}) tzs.add('TZOFFSETFROM', datetime.timedelta(hours=2)) tzs.add('TZOFFSETTO', datetime.timedelta(hours=1)) tzd = icalendar.TimezoneDaylight() tzd.add('tzname', 'CEST') tzd.add('dtstart', datetime.datetime(1970, 3, 29, 2, 0, 0)) tzs.add('rrule', {'freq': 'yearly', 'bymonth': 3, 'byday': '-1su'}) tzd.add('TZOFFSETFROM', datetime.timedelta(hours=1)) tzd.add('TZOFFSETTO', datetime.timedelta(hours=2)) tzc.add_component(tzs) tzc.add_component(tzd) cal.add_component(tzc) event = icalendar.Event() tz = pytz.timezone("Europe/Vienna") event.add( 'dtstart', tz.localize(datetime.datetime(2012, 2, 13, 10, 00, 00))) event.add( 'dtend', tz.localize(datetime.datetime(2012, 2, 17, 18, 00, 00))) event.add( 'dtstamp', tz.localize(datetime.datetime(2010, 10, 10, 10, 10, 10))) event.add( 'created', tz.localize(datetime.datetime(2010, 10, 10, 10, 10, 10))) event.add('uid', '123456') event.add( 'last-modified', tz.localize(datetime.datetime(2010, 10, 10, 10, 10, 10))) event.add('summary', 'artsprint 2012') # event.add('rrule', 'FREQ=YEARLY;INTERVAL=1;COUNT=10') event.add('description', 'sprinting at the artsprint') event.add('location', 'aka bild, wien') event.add('categories', 'first subject') event.add('categories', 'second subject') event.add('attendee', 'häns') event.add('attendee', 'franz') event.add('attendee', 'sepp') event.add('contact', 'Max Mustermann, 1010 Wien') event.add('url', 'https://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: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: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: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:20101010T081010Z" in test_out) self.assertTrue("CREATED:20101010T081010Z" in test_out) def test_create_to_ical_zoneinfo(self): cal = icalendar.Calendar() cal.add('prodid', "-//Plone.org//NONSGML plone.app.event//EN") cal.add('version', "2.0") cal.add('x-wr-calname', "test create calendar") cal.add('x-wr-caldesc', "icalendar tests") cal.add('x-wr-relcalid', "12345") cal.add('x-wr-timezone', "Europe/Vienna") tzc = icalendar.Timezone() tzc.add('tzid', 'Europe/Vienna') tzc.add('x-lic-location', 'Europe/Vienna') tzs = icalendar.TimezoneStandard() tzs.add('tzname', 'CET') tzs.add('dtstart', datetime.datetime(1970, 10, 25, 3, 0, 0)) tzs.add('rrule', {'freq': 'yearly', 'bymonth': 10, 'byday': '-1su'}) tzs.add('TZOFFSETFROM', datetime.timedelta(hours=2)) tzs.add('TZOFFSETTO', datetime.timedelta(hours=1)) tzd = icalendar.TimezoneDaylight() tzd.add('tzname', 'CEST') tzd.add('dtstart', datetime.datetime(1970, 3, 29, 2, 0, 0)) tzs.add('rrule', {'freq': 'yearly', 'bymonth': 3, 'byday': '-1su'}) tzd.add('TZOFFSETFROM', datetime.timedelta(hours=1)) tzd.add('TZOFFSETTO', datetime.timedelta(hours=2)) tzc.add_component(tzs) tzc.add_component(tzd) cal.add_component(tzc) event = icalendar.Event() tz = zoneinfo.ZoneInfo("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', '123456') event.add( 'last-modified', datetime.datetime(2010, 10, 10, 10, 10, 10, tzinfo=tz)) event.add('summary', 'artsprint 2012') # event.add('rrule', 'FREQ=YEARLY;INTERVAL=1;COUNT=10') event.add('description', 'sprinting at the artsprint') event.add('location', 'aka bild, wien') event.add('categories', 'first subject') event.add('categories', 'second subject') event.add('attendee', 'häns') event.add('attendee', 'franz') event.add('attendee', 'sepp') event.add('contact', 'Max Mustermann, 1010 Wien') event.add('url', 'http://plone.org') cal.add_component(event) test_out = b'|'.join(cal.to_ical().splitlines()) test_out = test_out.decode('utf-8') vtimezone_lines = "BEGIN:VTIMEZONE|TZID:Europe/Vienna|X-LIC-LOCATION:" "Europe/Vienna|BEGIN:STANDARD|DTSTART: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: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: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:20101010T081010Z" in test_out) self.assertTrue("CREATED:20101010T081010Z" in test_out) def test_tzinfo_dateutil(self): # Test for issues #77, #63 # references: #73,7430b66862346fe3a6a100ab25e35a8711446717 date = dateutil.parser.parse('2012-08-30T22:41:00Z') date2 = dateutil.parser.parse('2012-08-30T22:41:00 +02:00') self.assertTrue(date.tzinfo.__module__.startswith('dateutil.tz')) self.assertTrue(date2.tzinfo.__module__.startswith('dateutil.tz')) # make sure, it's parsed properly and doesn't throw an error self.assertTrue(icalendar.vDDDTypes(date).to_ical() == b'20120830T224100Z') self.assertTrue(icalendar.vDDDTypes(date2).to_ical() == b'20120830T224100') class TestTimezoneCreation(unittest.TestCase): def test_create_america_new_york(self): """testing America/New_York, the most complex example from the RFC""" with open(os.path.join(CALENDARS_DIRECTORY, 'america_new_york.ics'), 'rb') as fp: data = fp.read() cal = icalendar.Calendar.from_ical(data) tz = cal.walk('VEVENT')[0]['DTSTART'][0].dt.tzinfo self.assertEqual(str(tz), 'custom_America/New_York') pytz_new_york = pytz.timezone('America/New_York') # for reasons (tm) the locally installed version of the time zone # database isn't always complete, therefore we only compare some # transition times ny_transition_times = [] ny_transition_info = [] for num, date in enumerate(pytz_new_york._utc_transition_times): if datetime.datetime(1967, 4, 30, 7, 0)\ <= date <= datetime.datetime(2037, 11, 1, 6, 0): ny_transition_times.append(date) ny_transition_info.append(pytz_new_york._transition_info[num]) self.assertEqual(tz._utc_transition_times[:142], ny_transition_times) self.assertEqual(tz._transition_info[0:142], ny_transition_info) self.assertIn( ( datetime.timedelta(-1, 72000), datetime.timedelta(0, 3600), 'EDT' ), tz._tzinfos.keys() ) self.assertIn( (datetime.timedelta(-1, 68400), datetime.timedelta(0), 'EST'), tz._tzinfos.keys() ) def test_create_pacific_fiji(self): """testing Pacific/Fiji, another pretty complex example with more than one RDATE property per subcomponent""" self.maxDiff = None with open(os.path.join(CALENDARS_DIRECTORY, 'pacific_fiji.ics'), 'rb') as fp: data = fp.read() cal = icalendar.Calendar.from_ical(data) tz = cal.walk('VEVENT')[0]['DTSTART'][0].dt.tzinfo self.assertEqual(str(tz), 'custom_Pacific/Fiji') self.assertEqual(tz._utc_transition_times, [datetime.datetime(1915, 10, 25, 12, 4), datetime.datetime(1998, 10, 31, 14, 0), datetime.datetime(1999, 2, 27, 14, 0), datetime.datetime(1999, 11, 6, 14, 0), datetime.datetime(2000, 2, 26, 14, 0), datetime.datetime(2009, 11, 28, 14, 0), datetime.datetime(2010, 3, 27, 14, 0), datetime.datetime(2010, 10, 23, 14, 0), datetime.datetime(2011, 3, 5, 14, 0), datetime.datetime(2011, 10, 22, 14, 0), datetime.datetime(2012, 1, 21, 14, 0), datetime.datetime(2012, 10, 20, 14, 0), datetime.datetime(2013, 1, 19, 14, 0), datetime.datetime(2013, 10, 26, 14, 0), datetime.datetime(2014, 1, 18, 13, 0), datetime.datetime(2014, 10, 25, 14, 0), datetime.datetime(2015, 1, 17, 13, 0), datetime.datetime(2015, 10, 24, 14, 0), datetime.datetime(2016, 1, 23, 13, 0), datetime.datetime(2016, 10, 22, 14, 0), datetime.datetime(2017, 1, 21, 13, 0), datetime.datetime(2017, 10, 21, 14, 0), datetime.datetime(2018, 1, 20, 13, 0), datetime.datetime(2018, 10, 20, 14, 0), datetime.datetime(2019, 1, 19, 13, 0), datetime.datetime(2019, 10, 26, 14, 0), datetime.datetime(2020, 1, 18, 13, 0), datetime.datetime(2020, 10, 24, 14, 0), datetime.datetime(2021, 1, 23, 13, 0), datetime.datetime(2021, 10, 23, 14, 0), datetime.datetime(2022, 1, 22, 13, 0), datetime.datetime(2022, 10, 22, 14, 0), datetime.datetime(2023, 1, 21, 13, 0), datetime.datetime(2023, 10, 21, 14, 0), datetime.datetime(2024, 1, 20, 13, 0), datetime.datetime(2024, 10, 26, 14, 0), datetime.datetime(2025, 1, 18, 13, 0), datetime.datetime(2025, 10, 25, 14, 0), datetime.datetime(2026, 1, 17, 13, 0), datetime.datetime(2026, 10, 24, 14, 0), datetime.datetime(2027, 1, 23, 13, 0), datetime.datetime(2027, 10, 23, 14, 0), datetime.datetime(2028, 1, 22, 13, 0), datetime.datetime(2028, 10, 21, 14, 0), datetime.datetime(2029, 1, 20, 13, 0), datetime.datetime(2029, 10, 20, 14, 0), datetime.datetime(2030, 1, 19, 13, 0), datetime.datetime(2030, 10, 26, 14, 0), datetime.datetime(2031, 1, 18, 13, 0), datetime.datetime(2031, 10, 25, 14, 0), datetime.datetime(2032, 1, 17, 13, 0), datetime.datetime(2032, 10, 23, 14, 0), datetime.datetime(2033, 1, 22, 13, 0), datetime.datetime(2033, 10, 22, 14, 0), datetime.datetime(2034, 1, 21, 13, 0), datetime.datetime(2034, 10, 21, 14, 0), datetime.datetime(2035, 1, 20, 13, 0), datetime.datetime(2035, 10, 20, 14, 0), datetime.datetime(2036, 1, 19, 13, 0), datetime.datetime(2036, 10, 25, 14, 0), datetime.datetime(2037, 1, 17, 13, 0), datetime.datetime(2037, 10, 24, 14, 0), datetime.datetime(2038, 1, 23, 13, 0), datetime.datetime(2038, 10, 23, 14, 0)] ) self.assertEqual( tz._transition_info, [( datetime.timedelta(0, 43200), datetime.timedelta(0), 'custom_Pacific/Fiji_19151026T000000_+115544_+1200' )] + 3 * [( datetime.timedelta(0, 46800), datetime.timedelta(0, 3600), 'custom_Pacific/Fiji_19981101T020000_+1200_+1300' ), ( datetime.timedelta(0, 43200), datetime.timedelta(0), 'custom_Pacific/Fiji_19990228T030000_+1300_+1200') ] + 3 * [( datetime.timedelta(0, 46800), datetime.timedelta(0, 3600), 'custom_Pacific/Fiji_20101024T020000_+1200_+1300' ), ( datetime.timedelta(0, 43200), datetime.timedelta(0), 'custom_Pacific/Fiji_19990228T030000_+1300_+1200' )] + 25 * [( datetime.timedelta(0, 46800), datetime.timedelta(0, 3600), 'custom_Pacific/Fiji_20101024T020000_+1200_+1300' ), ( datetime.timedelta(0, 43200), datetime.timedelta(0), 'custom_Pacific/Fiji_20140119T020000_+1300_+1200' )] + [( datetime.timedelta(0, 46800), datetime.timedelta(0, 3600), 'custom_Pacific/Fiji_20101024T020000_+1200_+1300' )] ) self.assertIn( ( datetime.timedelta(0, 46800), datetime.timedelta(0, 3600), 'custom_Pacific/Fiji_19981101T020000_+1200_+1300' ), tz._tzinfos.keys() ) self.assertIn( ( datetime.timedelta(0, 43200), datetime.timedelta(0), 'custom_Pacific/Fiji_19990228T030000_+1300_+1200' ), tz._tzinfos.keys() ) def test_same_start_date(self): """testing if we can handle VTIMEZONEs whose different components have the same start DTIMEs.""" with open(os.path.join(CALENDARS_DIRECTORY, 'timezone_same_start.ics'), 'rb') as fp: data = fp.read() cal = icalendar.Calendar.from_ical(data) d = cal.subcomponents[1]['DTSTART'].dt self.assertEqual(d.strftime('%c'), 'Fri Feb 24 12:00:00 2017') def test_same_start_date_and_offset(self): """testing if we can handle VTIMEZONEs whose different components have the same DTSTARTs, TZOFFSETFROM, and TZOFFSETTO.""" with open(os.path.join(CALENDARS_DIRECTORY, 'timezone_same_start_and_offset.ics'), 'rb') as fp: data = fp.read() cal = icalendar.Calendar.from_ical(data) d = cal.subcomponents[1]['DTSTART'].dt self.assertEqual(d.strftime('%c'), 'Fri Feb 24 12:00:00 2017') def test_rdate(self): """testing if we can handle VTIMEZONEs who only have an RDATE, not RRULE """ with open(os.path.join(CALENDARS_DIRECTORY, 'timezone_rdate.ics'), 'rb') as fp: data = fp.read() cal = icalendar.Calendar.from_ical(data) vevent = cal.walk('VEVENT')[0] tz = vevent['DTSTART'].dt.tzinfo self.assertEqual(str(tz), 'posix/Europe/Vaduz') self.assertEqual( tz._utc_transition_times[:6], [ datetime.datetime(1901, 12, 13, 20, 45, 38), datetime.datetime(1941, 5, 5, 0, 0, 0), datetime.datetime(1941, 10, 6, 0, 0, 0), datetime.datetime(1942, 5, 4, 0, 0, 0), datetime.datetime(1942, 10, 5, 0, 0, 0), datetime.datetime(1981, 3, 29, 1, 0), ]) self.assertEqual( tz._transition_info[:6], [ (datetime.timedelta(0, 3600), datetime.timedelta(0), 'CET'), (datetime.timedelta(0, 7200), datetime.timedelta(0, 3600), 'CEST'), (datetime.timedelta(0, 3600), datetime.timedelta(0), 'CET'), (datetime.timedelta(0, 7200), datetime.timedelta(0, 3600), 'CEST'), (datetime.timedelta(0, 3600), datetime.timedelta(0), 'CET'), (datetime.timedelta(0, 7200), datetime.timedelta(0, 3600), 'CEST'), ] ) icalendar-5.0.12/src/icalendar/tests/test_unit_cal.py000066400000000000000000000375171457641316200226310ustar00rootroot00000000000000import itertools from datetime import datetime from datetime import timedelta import unittest import pytest import icalendar import pytz import re from icalendar.cal import Component, Calendar, Event, ComponentFactory from icalendar import prop, cal def test_cal_Component(calendar_component): """A component is like a dictionary with extra methods and attributes.""" assert calendar_component assert calendar_component.is_empty() def test_nonempty_calendar_component(calendar_component): """Every key defines a property.A property can consist of either a single item. This can be set with a single value... """ calendar_component['prodid'] = '-//max m//icalendar.mxm.dk/' assert not calendar_component.is_empty() assert calendar_component == Calendar({'PRODID': '-//max m//icalendar.mxm.dk/'}) # or with a list calendar_component['ATTENDEE'] = ['Max M', 'Rasmussen'] assert calendar_component == Calendar( {'ATTENDEE': ['Max M', 'Rasmussen'], 'PRODID': '-//max m//icalendar.mxm.dk/'}) def test_add_multiple_values(event_component): """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. """ # add multiple values at once event_component.add('attendee', ['test@test.com', 'test2@test.com']) # or add one per line event_component.add('attendee', 'maxm@mxm.dk') event_component.add('attendee', 'test@example.dk') # add again multiple values at once to very concatenaton of lists event_component.add('attendee', ['test3@test.com', 'test4@test.com']) assert event_component == 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') ]}) def test_get_content_directly(c): """You can get the values back directly ...""" c.add('prodid', '-//my product//') assert c['prodid'] == prop.vText('-//my product//') # ... or decoded to a python type assert c.decoded('prodid') == b'-//my product//' def test_get_default_value(c): """With default values for non existing properties""" assert c.decoded('version', 'No Version') == 'No Version' def test_default_list_example(c): c.add('rdate', [datetime(2013, 3, 28), datetime(2013, 3, 27)]) assert isinstance(c.decoded('rdate'), prop.vDDDLists) def test_render_component(calendar_component): """The component can render itself in the RFC 2445 format.""" calendar_component.add('attendee', 'Max M') assert calendar_component.to_ical() == b'BEGIN:VCALENDAR\r\nATTENDEE:Max M\r\nEND:VCALENDAR\r\n' def test_nested_component_event_ics(filled_event_component): """Check the ical string of the event component.""" assert filled_event_component.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' ) def test_nested_components(calendar_component, filled_event_component): """Components can be nested, so You can add a subcomponent. Eg a calendar holds events.""" self.assertEqual( calendar_component.subcomponents, [Event({'DTEND': '20000102T000000', 'DTSTART': '20000101T000000', 'SUMMARY': 'A brief history of time'})] ) def test_walk_filled_calendar_component(calendar_component, filled_event_component): """We can walk over nested componentes with the walk method.""" assert [i.name for i in calendar_component.walk()] == ['VCALENDAR', 'VEVENT'] def test_filter_walk(calendar_component, filled_event_component): """We can also just walk over specific component types, by filtering them on their name.""" assert [i.name for i in calendar_component.walk('VEVENT')] == ['VEVENT'] assert [i['dtstart'] for i in calendar_component.walk('VEVENT')] == ['20000101T000000'] def test_recursive_property_items(calendar_component, filled_event_component): """We can enumerate property items recursively with the property_items method.""" calendar_component.add('attendee', 'Max M') assert calendar_component.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')] def test_flat_property_items(calendar_component, filled_event_component): """We can also enumerate property items just under the component.""" assert calendar_component.property_items(recursive=False) == [ ('BEGIN', b'VCALENDAR'), ('ATTENDEE', prop.vCalAddress('Max M')), ('END', b'VCALENDAR')] def test_flat_property_items(filled_event_component): """Flat enumeration on the event.""" assert filled_event_component.property_items(recursive=False) == [ ('BEGIN', b'VEVENT'), ('DTEND', '20000102T000000'), ('DTSTART', '20000101T000000'), ('SUMMARY', 'A brief history of time'), ('END', b'VEVENT')] def test_indent(): """Text fields which span multiple mulitple lines require proper indenting""" c = Calendar() c['description'] = 'Paragraph one\n\nParagraph two' assert c.to_ical() == ( b'BEGIN:VCALENDAR\r\nDESCRIPTION:Paragraph one\\n\\nParagraph two' + b'\r\nEND:VCALENDAR\r\n' ) def test_INLINE_properties(calendar_with_resources): """INLINE properties have their values on one property line. Note the double quoting of the value with a colon in it. """ assert calendar_with_resources == Calendar({'RESOURCES': 'Chair, Table, "Room: 42"'}) assert calendar_with_resources.to_ical() == ( b'BEGIN:VCALENDAR\r\nRESOURCES:Chair\\, Table\\, "Room: 42"\r\n' + b'END:VCALENDAR\r\n' ) def test_get_inline(calendar_with_resources): """The inline values must be handled by the get_inline() and set_inline() methods. """ assert calendar_with_resources.get_inline('resources', decode=0) == \ ['Chair', 'Table', 'Room: 42'] def test_get_inline_decoded(calendar_with_resources): """These can also be decoded""" assert calendar_with_resources.get_inline('resources', decode=1) == \ [b'Chair', b'Table', b'Room: 42'] def test_set_inline(calendar_with_resources): """You can set them directly ...""" calendar_with_resources.set_inline('resources', ['A', 'List', 'of', 'some, recources'], encode=1) assert calendar_with_resources['resources'] == 'A,List,of,"some, recources"' assert calendar_with_resources.get_inline('resources', decode=0) == ['A', 'List', 'of', 'some, recources'] def test_inline_free_busy_inline(c): c['freebusy'] = '19970308T160000Z/PT3H,19970308T200000Z/PT1H,'\ + '19970308T230000Z/19970309T000000Z' assert c.get_inline('freebusy', decode=0) == \ ['19970308T160000Z/PT3H', '19970308T200000Z/PT1H', '19970308T230000Z/19970309T000000Z'] freebusy = c.get_inline('freebusy', decode=1) assert isinstance(freebusy[0][0], datetime) assert isinstance(freebusy[0][1], timedelta) def test_cal_Component_add(comp): """Test the for timezone correctness: dtstart should preserve it's timezone, created, dtstamp and last-modified must be in UTC. """ vienna = pytz.timezone("Europe/Vienna") comp.add('dtstart', vienna.localize(datetime(2010, 10, 10, 10, 0, 0))) comp.add('created', datetime(2010, 10, 10, 12, 0, 0)) comp.add('dtstamp', vienna.localize(datetime(2010, 10, 10, 14, 0, 0))) comp.add('last-modified', pytz.utc.localize( datetime(2010, 10, 10, 16, 0, 0))) lines = comp.to_ical().splitlines() assert b"DTSTART;TZID=Europe/Vienna:20101010T100000" in lines assert b"CREATED:20101010T120000Z" in lines assert b"DTSTAMP:20101010T120000Z" in lines assert b"LAST-MODIFIED:20101010T160000Z" in lines def test_cal_Component_add_no_reencode(comp): """Already encoded values should not be re-encoded. """ comp.add('ATTACH', 'me') comp.add('ATTACH', 'you', encode=False) binary = prop.vBinary('us') comp.add('ATTACH', binary) assert comp['ATTACH'] == ['me', 'you', binary] def test_cal_Component_add_property_parameter(comp): """Test the for timezone correctness: dtstart should preserve it's timezone, crated, dtstamp and last-modified must be in UTC. """ comp.add('X-TEST-PROP', 'tryout.', parameters={'prop1': 'val1', 'prop2': 'val2'}) lines = comp.to_ical().splitlines() assert b"X-TEST-PROP;PROP1=val1;PROP2=val2:tryout." in lines comp_prop = pytest.mark.parametrize( "component_name, property_name", [ ('VEVENT', 'DTSTART'), ('VEVENT', 'DTEND'), ('VEVENT', 'RECURRENCE-ID'), ('VTODO', 'DUE') ] ) @comp_prop def test_cal_Component_from_ical(component_name, property_name): """Check for proper handling of TZID parameter of datetime properties""" 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) assert str(component[property_name].dt.tzinfo.zone) == "America/Denver" @comp_prop def test_cal_Component_from_ical_2(component_name, property_name): """Check for proper handling of TZID parameter of datetime properties""" component_str = 'BEGIN:' + component_name + '\n' component_str += property_name + ':' component_str += '20120404T073000\nEND:' + component_name component = Component.from_ical(component_str) assert component[property_name].dt.tzinfo == None def test_cal_Component_to_ical_property_order(): component_str = [b'BEGIN:VEVENT', b'DTSTART:19970714T170000Z', b'DTEND:19970715T035959Z', b'SUMMARY:Bastille Day Party', b'END:VEVENT'] component = Component.from_ical(b'\r\n'.join(component_str)) sorted_str = component.to_ical().splitlines() assert sorted_str != component_str assert set(sorted_str) == set(component_str) preserved_str = component.to_ical(sorted=False).splitlines() assert preserved_str == component_str def test_cal_Component_to_ical_parameter_order(): component_str = [b'BEGIN:VEVENT', b'X-FOOBAR;C=one;A=two;B=three:helloworld.', b'END:VEVENT'] component = Component.from_ical(b'\r\n'.join(component_str)) sorted_str = component.to_ical().splitlines() assert sorted_str[0] == component_str[0] assert sorted_str[1] == b'X-FOOBAR;A=two;B=three;C=one:helloworld.' assert sorted_str[2] == component_str[2] preserved_str = component.to_ical(sorted=False).splitlines() assert preserved_str == component_str @pytest.fixture() def repr_example(c): class ReprExample: component = c component['key1'] = 'value1' calendar = Calendar() calendar['key1'] = 'value1' event = Event() event['key1'] = 'value1' nested = Component(key1='VALUE1') nested.add_component(component) nested.add_component(calendar) return ReprExample def test_repr_component(repr_example): """Test correct class representation. """ assert re.match(r"Component\({u?'KEY1': u?'value1'}\)", str(repr_example.component)) def test_repr_calendar(repr_example): assert re.match(r"VCALENDAR\({u?'KEY1': u?'value1'}\)", str(repr_example.calendar)) def test_repr_event(repr_example): assert re.match(r"VEVENT\({u?'KEY1': u?'value1'}\)", str(repr_example.event)) def test_nested_components(repr_example): """Representation of nested Components""" repr_example.calendar.add_component(repr_example.event) print(repr_example.nested) assert re.match( r"Component\({u?'KEY1': u?'VALUE1'}, " r"Component\({u?'KEY1': u?'value1'}\), " r"VCALENDAR\({u?'KEY1': u?'value1'}, " r"VEVENT\({u?'KEY1': u?'value1'}\)\)\)", str(repr_example.nested) ) def test_component_factory_VEVENT(factory): """Check the events in the component factory""" component = factory['VEVENT'] event = component(dtstart='19700101') assert event.to_ical() == b'BEGIN:VEVENT\r\nDTSTART:19700101\r\nEND:VEVENT\r\n' def test_component_factory_VCALENDAR(factory): """Check the VCALENDAR in the factory.""" assert factory.get('VCALENDAR') == icalendar.cal.Calendar def test_minimal_calendar_component_with_one_event(): """Setting up a minimal calendar component looks like this""" 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 = Event() event['summary'] = 'Python meeting about calendaring' event['uid'] = '42' event.add('dtstart', datetime(2005, 4, 4, 8, 0, 0)) cal.add_component(event) assert cal.subcomponents[0].to_ical() == \ b'BEGIN:VEVENT\r\nSUMMARY:Python meeting about calendaring\r\n' \ + b'DTSTART:20050404T080000\r\nUID:42\r\n' \ + b'END:VEVENT\r\n' def test_calendar_with_parsing_errors_includes_all_events(calendars): """Parsing a complete calendar from a string will silently ignore wrong events but adding the error information to the component's 'errors' attribute. The error in the following is the third EXDATE: it has an empty DATE. """ event_descriptions = [e['DESCRIPTION'].to_ical() for e in calendars.parsing_error.walk('VEVENT')] assert event_descriptions == [b'Perfectly OK event', b'Wrong event'] def test_calendar_with_parsing_errors_has_an_error_in_one_event(calendars): """Parsing a complete calendar from a string will silently ignore wrong events but adding the error information to the component's 'errors' attribute. The error in the following is the third EXDATE: it has an empty DATE. """ errors = [e.errors for e in calendars.parsing_error.walk('VEVENT')] assert errors == [[], [('EXDATE', "Expected datetime, date, or time, got: ''")]] def test_cal_strict_parsing(calendars): """If components are damaged, we raise an exception.""" with pytest.raises(ValueError): calendars.parsing_error_in_UTC_offset def test_cal_ignore_errors_parsing(calendars, vUTCOffset_ignore_exceptions): """If we diable the errors, we should be able to put the calendar back together.""" assert calendars.parsing_error_in_UTC_offset.to_ical() == calendars.parsing_error_in_UTC_offset.raw_ics @pytest.mark.parametrize( 'calendar, other_calendar', itertools.product([ 'issue_156_RDATE_with_PERIOD_TZID_khal', 'issue_156_RDATE_with_PERIOD_TZID_khal_2', 'issue_178_custom_component_contains_other', 'issue_178_custom_component_inside_other', 'issue_526_calendar_with_events', 'issue_526_calendar_with_different_events', 'issue_526_calendar_with_event_subset', ], repeat=2) ) def test_comparing_calendars(calendars, calendar, other_calendar): are_calendars_equal = calendars[calendar] == calendars[other_calendar] are_calendars_actually_equal = calendar == other_calendar assert are_calendars_equal == are_calendars_actually_equal @pytest.mark.parametrize('calendar, shuffeled_calendar', [ ( 'issue_526_calendar_with_events', 'issue_526_calendar_with_shuffeled_events', ), ]) def test_calendars_with_same_subcomponents_in_different_order_are_equal(calendars, calendar, shuffeled_calendar): assert not calendars[calendar].subcomponents == calendars[shuffeled_calendar].subcomponents assert calendars[calendar] == calendars[shuffeled_calendar] icalendar-5.0.12/src/icalendar/tests/test_unit_caselessdict.py000066400000000000000000000061251457641316200245270ustar00rootroot00000000000000import unittest import icalendar class TestCaselessdict(unittest.TestCase): def test_caselessdict_canonsort_keys(self): canonsort_keys = icalendar.caselessdict.canonsort_keys keys = ['DTEND', 'DTSTAMP', 'DTSTART', 'UID', 'SUMMARY', 'LOCATION'] out = canonsort_keys(keys) self.assertEqual( out, ['DTEND', 'DTSTAMP', 'DTSTART', 'LOCATION', 'SUMMARY', 'UID'] ) out = canonsort_keys(keys, ('SUMMARY', 'DTSTART', 'DTEND', )) self.assertEqual( out, ['SUMMARY', 'DTSTART', 'DTEND', 'DTSTAMP', 'LOCATION', 'UID'] ) out = canonsort_keys(keys, ('UID', 'DTSTART', 'DTEND', )) self.assertEqual( out, ['UID', 'DTSTART', 'DTEND', 'DTSTAMP', 'LOCATION', 'SUMMARY'] ) out = canonsort_keys( keys, ('UID', 'DTSTART', 'DTEND', 'RRULE', 'EXDATE') ) self.assertEqual( out, ['UID', 'DTSTART', 'DTEND', 'DTSTAMP', 'LOCATION', 'SUMMARY'] ) def test_caselessdict_canonsort_items(self): canonsort_items = icalendar.caselessdict.canonsort_items d = { 'i': 7, 'c': 'at', 'a': 3.5, 'l': (2, 3), 'e': [4, 5], 'n': 13, 'd': {'x': 'y'}, 'r': 1.0, } out = canonsort_items(d) self.assertEqual( out, [('a', 3.5), ('c', 'at'), ('d', {'x': 'y'}), ('e', [4, 5]), ('i', 7), ('l', (2, 3)), ('n', 13), ('r', 1.0)] ) out = canonsort_items(d, ('i', 'c', 'a')) self.assertTrue( out, [('i', 7), ('c', 'at'), ('a', 3.5), ('d', {'x': 'y'}), ('e', [4, 5]), ('l', (2, 3)), ('n', 13), ('r', 1.0)] ) def test_caselessdict_copy(self): CaselessDict = icalendar.caselessdict.CaselessDict original_dict = CaselessDict(key1='val1', key2='val2') copied_dict = original_dict.copy() self.assertEqual(original_dict, copied_dict) def test_CaselessDict(self): CaselessDict = icalendar.caselessdict.CaselessDict ncd = CaselessDict(key1='val1', key2='val2') self.assertEqual( ncd, CaselessDict({'KEY2': 'val2', 'KEY1': 'val1'}) ) self.assertEqual(ncd['key1'], 'val1') self.assertEqual(ncd['KEY1'], 'val1') ncd['KEY3'] = 'val3' self.assertEqual(ncd['key3'], 'val3') self.assertEqual(ncd.setdefault('key3', 'FOUND'), 'val3') self.assertEqual(ncd.setdefault('key4', 'NOT FOUND'), 'NOT FOUND') self.assertEqual(ncd['key4'], 'NOT FOUND') self.assertEqual(ncd.get('key1'), 'val1') self.assertEqual(ncd.get('key3', 'NOT FOUND'), 'val3') self.assertEqual(ncd.get('key4', 'NOT FOUND'), 'NOT FOUND') self.assertTrue('key4' in ncd) del ncd['key4'] self.assertFalse('key4' in ncd) ncd.update({'key5': 'val5', 'KEY6': 'val6', 'KEY5': 'val7'}) self.assertEqual(ncd['key6'], 'val6') keys = sorted(ncd.keys()) self.assertEqual(keys, ['KEY1', 'KEY2', 'KEY3', 'KEY5', 'KEY6']) icalendar-5.0.12/src/icalendar/tests/test_unit_parser_tools.py000066400000000000000000000020261457641316200245710ustar00rootroot00000000000000from icalendar.parser_tools import data_encode from icalendar.parser_tools import to_unicode import unittest class TestParserTools(unittest.TestCase): def test_parser_tools_to_unicode(self): self.assertEqual(to_unicode(b'spam'), 'spam') self.assertEqual(to_unicode('spam'), 'spam') self.assertEqual(to_unicode(b'spam'), 'spam') self.assertEqual(to_unicode(b'\xc6\xb5'), '\u01b5') self.assertEqual(to_unicode(b'\xc6\xb5'), '\u01b5') self.assertEqual(to_unicode(b'\xc6\xb5', encoding='ascii'), '\u01b5') self.assertEqual(to_unicode(1), 1) self.assertEqual(to_unicode(None), None) def test_parser_tools_data_encode(self): data1 = { 'k1': 'v1', 'k2': 'v2', 'k3': 'v3', 'li1': ['it1', 'it2', {'k4': 'v4', 'k5': 'v5'}, 123] } res = {b'k3': b'v3', b'k2': b'v2', b'k1': b'v1', b'li1': [b'it1', b'it2', {b'k5': b'v5', b'k4': b'v4'}, 123]} self.assertEqual(data_encode(data1), res) icalendar-5.0.12/src/icalendar/tests/test_unit_prop.py000066400000000000000000000524401457641316200230420ustar00rootroot00000000000000from datetime import date from datetime import datetime from datetime import time from datetime import timedelta from icalendar.parser import Parameters import unittest from icalendar.prop import vDatetime, vDDDTypes from icalendar.windows_to_olson import WINDOWS_TO_OLSON import pytest import pytz from copy import deepcopy from dateutil import tz class TestProp(unittest.TestCase): def test_prop_vBinary(self): from ..prop import vBinary txt = b'This is gibberish' txt_ical = b'VGhpcyBpcyBnaWJiZXJpc2g=' self.assertEqual(vBinary(txt).to_ical(), txt_ical) self.assertEqual(vBinary.from_ical(txt_ical), txt) # The roundtrip test txt = b'Binary data \x13 \x56' txt_ical = b'QmluYXJ5IGRhdGEgEyBW' self.assertEqual(vBinary(txt).to_ical(), txt_ical) self.assertEqual(vBinary.from_ical(txt_ical), txt) self.assertIsInstance(vBinary('txt').params, Parameters) self.assertEqual( vBinary('txt').params, {'VALUE': 'BINARY', 'ENCODING': 'BASE64'} ) # Long data should not have line breaks, as that would interfere txt = b'a' * 99 txt_ical = b'YWFh' * 33 self.assertEqual(vBinary(txt).to_ical(), txt_ical) self.assertEqual(vBinary.from_ical(txt_ical), txt) def test_prop_vBoolean(self): from ..prop import vBoolean self.assertEqual(vBoolean(True).to_ical(), b'TRUE') self.assertEqual(vBoolean(0).to_ical(), b'FALSE') # The roundtrip test self.assertEqual(vBoolean.from_ical(vBoolean(True).to_ical()), True) self.assertEqual(vBoolean.from_ical('true'), True) def test_prop_vCalAddress(self): from ..prop import vCalAddress txt = b'MAILTO:maxm@mxm.dk' a = vCalAddress(txt) a.params['cn'] = 'Max M' self.assertEqual(a.to_ical(), txt) self.assertIsInstance(a.params, Parameters) self.assertEqual(a.params, {'CN': 'Max M'}) self.assertEqual(vCalAddress.from_ical(txt), 'MAILTO:maxm@mxm.dk') def test_prop_vFloat(self): from ..prop import vFloat self.assertEqual(vFloat(1.0).to_ical(), b'1.0') self.assertEqual(vFloat.from_ical('42'), 42.0) self.assertEqual(vFloat(42).to_ical(), b'42.0') def test_prop_vInt(self): from ..prop import vInt self.assertEqual(vInt(42).to_ical(), b'42') self.assertEqual(vInt.from_ical('13'), 13) self.assertRaises(ValueError, vInt.from_ical, '1s3') def test_prop_vDDDLists(self): from ..prop import vDDDLists dt_list = vDDDLists.from_ical('19960402T010000Z') self.assertTrue(isinstance(dt_list, list)) self.assertEqual(len(dt_list), 1) self.assertTrue(isinstance(dt_list[0], datetime)) self.assertEqual(str(dt_list[0]), '1996-04-02 01:00:00+00:00') p = '19960402T010000Z,19960403T010000Z,19960404T010000Z' dt_list = vDDDLists.from_ical(p) self.assertEqual(len(dt_list), 3) self.assertEqual(str(dt_list[0]), '1996-04-02 01:00:00+00:00') self.assertEqual(str(dt_list[2]), '1996-04-04 01:00:00+00:00') dt_list = vDDDLists([]) self.assertEqual(dt_list.to_ical(), b'') dt_list = vDDDLists([datetime(2000, 1, 1)]) self.assertEqual(dt_list.to_ical(), b'20000101T000000') dt_list = vDDDLists([datetime(2000, 1, 1), datetime(2000, 11, 11)]) self.assertEqual(dt_list.to_ical(), b'20000101T000000,20001111T000000') def test_prop_vDDDTypes(self): from ..prop import vDDDTypes self.assertTrue(isinstance(vDDDTypes.from_ical('20010101T123000'), datetime)) self.assertEqual(vDDDTypes.from_ical('20010101T123000Z'), pytz.utc.localize(datetime(2001, 1, 1, 12, 30))) self.assertTrue(isinstance(vDDDTypes.from_ical('20010101'), date)) self.assertEqual(vDDDTypes.from_ical('P31D'), timedelta(31)) self.assertEqual(vDDDTypes.from_ical('-P31D'), timedelta(-31)) # Bad input self.assertRaises(ValueError, vDDDTypes, 42) def test_prop_vDate(self): from ..prop import vDate self.assertEqual(vDate(date(2001, 1, 1)).to_ical(), b'20010101') self.assertEqual(vDate(date(1899, 1, 1)).to_ical(), b'18990101') self.assertEqual(vDate.from_ical('20010102'), date(2001, 1, 2)) self.assertRaises(ValueError, vDate, 'd') def test_prop_vDatetime(self): from ..prop import vDatetime dt = datetime(2001, 1, 1, 12, 30, 0) self.assertEqual(vDatetime(dt).to_ical(), b'20010101T123000') self.assertEqual(vDatetime.from_ical('20000101T120000'), datetime(2000, 1, 1, 12, 0)) dutc = pytz.utc.localize(datetime(2001, 1, 1, 12, 30, 0)) self.assertEqual(vDatetime(dutc).to_ical(), b'20010101T123000Z') dutc = pytz.utc.localize(datetime(1899, 1, 1, 12, 30, 0)) self.assertEqual(vDatetime(dutc).to_ical(), b'18990101T123000Z') self.assertEqual(vDatetime.from_ical('20010101T000000'), datetime(2001, 1, 1, 0, 0)) self.assertRaises(ValueError, vDatetime.from_ical, '20010101T000000A') utc = vDatetime.from_ical('20010101T000000Z') self.assertEqual(vDatetime(utc).to_ical(), b'20010101T000000Z') # 1 minute before transition to DST dat = vDatetime.from_ical('20120311T015959', 'America/Denver') self.assertEqual(dat.strftime('%Y%m%d%H%M%S %z'), '20120311015959 -0700') # After transition to DST dat = vDatetime.from_ical('20120311T030000', 'America/Denver') self.assertEqual(dat.strftime('%Y%m%d%H%M%S %z'), '20120311030000 -0600') dat = vDatetime.from_ical('20101010T000000', 'Europe/Vienna') self.assertEqual(vDatetime(dat).to_ical(), b'20101010T000000') def test_prop_vDuration(self): from ..prop import vDuration self.assertEqual(vDuration(timedelta(11)).to_ical(), b'P11D') self.assertEqual(vDuration(timedelta(-14)).to_ical(), b'-P14D') self.assertEqual( vDuration(timedelta(1, 7384)).to_ical(), b'P1DT2H3M4S' ) self.assertEqual(vDuration(timedelta(1, 7380)).to_ical(), b'P1DT2H3M') self.assertEqual(vDuration(timedelta(1, 7200)).to_ical(), b'P1DT2H') self.assertEqual(vDuration(timedelta(0, 7200)).to_ical(), b'PT2H') self.assertEqual(vDuration(timedelta(0, 7384)).to_ical(), b'PT2H3M4S') self.assertEqual(vDuration(timedelta(0, 184)).to_ical(), b'PT3M4S') self.assertEqual(vDuration(timedelta(0, 22)).to_ical(), b'PT22S') self.assertEqual(vDuration(timedelta(0, 3622)).to_ical(), b'PT1H0M22S') self.assertEqual(vDuration(timedelta(days=1, hours=5)).to_ical(), b'P1DT5H') self.assertEqual(vDuration(timedelta(hours=-5)).to_ical(), b'-PT5H') self.assertEqual(vDuration(timedelta(days=-1, hours=-5)).to_ical(), b'-P1DT5H') # How does the parsing work? self.assertEqual(vDuration.from_ical('PT1H0M22S'), timedelta(0, 3622)) self.assertRaises(ValueError, vDuration.from_ical, 'kox') self.assertEqual(vDuration.from_ical('-P14D'), timedelta(-14)) self.assertRaises(ValueError, vDuration, 11) # calling to_ical twice should result in same output duration = vDuration(timedelta(days=-1, hours=-5)) self.assertEqual(duration.to_ical(), b'-P1DT5H') self.assertEqual(duration.to_ical(), b'-P1DT5H') def test_prop_vPeriod(self): from ..prop import vPeriod # One day in exact datetimes per = (datetime(2000, 1, 1), datetime(2000, 1, 2)) self.assertEqual(vPeriod(per).to_ical(), b'20000101T000000/20000102T000000') per = (datetime(2000, 1, 1), timedelta(days=31)) self.assertEqual(vPeriod(per).to_ical(), b'20000101T000000/P31D') # Roundtrip p = vPeriod.from_ical('20000101T000000/20000102T000000') self.assertEqual( p, (datetime(2000, 1, 1, 0, 0), datetime(2000, 1, 2, 0, 0)) ) self.assertEqual(vPeriod(p).to_ical(), b'20000101T000000/20000102T000000') self.assertEqual(vPeriod.from_ical('20000101T000000/P31D'), (datetime(2000, 1, 1, 0, 0), timedelta(31))) # Roundtrip with absolute time p = vPeriod.from_ical('20000101T000000Z/20000102T000000Z') self.assertEqual(vPeriod(p).to_ical(), b'20000101T000000Z/20000102T000000Z') # And an error self.assertRaises(ValueError, vPeriod.from_ical, '20000101T000000/Psd31D') # Timezoned dk = pytz.timezone('Europe/Copenhagen') start = dk.localize(datetime(2000, 1, 1)) end = dk.localize(datetime(2000, 1, 2)) per = (start, end) self.assertEqual(vPeriod(per).to_ical(), b'20000101T000000/20000102T000000') self.assertEqual(vPeriod(per).params['TZID'], 'Europe/Copenhagen') p = vPeriod((dk.localize(datetime(2000, 1, 1)), timedelta(days=31))) self.assertEqual(p.to_ical(), b'20000101T000000/P31D') def test_prop_vWeekday(self): from ..prop import vWeekday self.assertEqual(vWeekday('mo').to_ical(), b'MO') self.assertRaises(ValueError, vWeekday, 'erwer') self.assertEqual(vWeekday.from_ical('mo'), 'MO') self.assertEqual(vWeekday.from_ical('+3mo'), '+3MO') self.assertRaises(ValueError, vWeekday.from_ical, 'Saturday') self.assertEqual(vWeekday('+mo').to_ical(), b'+MO') self.assertEqual(vWeekday('+3mo').to_ical(), b'+3MO') self.assertEqual(vWeekday('-tu').to_ical(), b'-TU') def test_prop_vFrequency(self): from ..prop import vFrequency self.assertRaises(ValueError, vFrequency, 'bad test') self.assertEqual(vFrequency('daily').to_ical(), b'DAILY') self.assertEqual(vFrequency('daily').from_ical('MONTHLY'), 'MONTHLY') def test_prop_vRecur(self): from ..prop import vRecur # Let's see how close we can get to one from the rfc: # FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30 r = dict({'freq': 'yearly', 'interval': 2}) r.update({ 'bymonth': 1, 'byday': 'su', 'byhour': [8, 9], 'byminute': 30 }) self.assertEqual( vRecur(r).to_ical(), b'FREQ=YEARLY;INTERVAL=2;BYMINUTE=30;BYHOUR=8,9;BYDAY=SU;BYMONTH=1' ) r = vRecur(FREQ='yearly', INTERVAL=2) r.update({ 'BYMONTH': 1, 'BYDAY': 'su', 'BYHOUR': [8, 9], 'BYMINUTE': 30, }) self.assertEqual( r.to_ical(), b'FREQ=YEARLY;INTERVAL=2;BYMINUTE=30;BYHOUR=8,9;BYDAY=SU;BYMONTH=1' ) r = vRecur(freq='DAILY', count=10) r['bysecond'] = [0, 15, 30, 45] self.assertEqual(r.to_ical(), b'FREQ=DAILY;COUNT=10;BYSECOND=0,15,30,45') r = vRecur(freq='DAILY', until=datetime(2005, 1, 1, 12, 0, 0)) self.assertEqual(r.to_ical(), b'FREQ=DAILY;UNTIL=20050101T120000') # How do we fare with regards to parsing? r = vRecur.from_ical('FREQ=DAILY;INTERVAL=2;COUNT=10') self.assertEqual(r, {'COUNT': [10], 'FREQ': ['DAILY'], 'INTERVAL': [2]}) self.assertEqual( vRecur(r).to_ical(), b'FREQ=DAILY;COUNT=10;INTERVAL=2' ) r = vRecur.from_ical('FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=-SU;' 'BYHOUR=8,9;BYMINUTE=30') self.assertEqual( r, {'BYHOUR': [8, 9], 'BYDAY': ['-SU'], 'BYMINUTE': [30], 'BYMONTH': [1], 'FREQ': ['YEARLY'], 'INTERVAL': [2]} ) self.assertEqual( vRecur(r).to_ical(), b'FREQ=YEARLY;INTERVAL=2;BYMINUTE=30;BYHOUR=8,9;BYDAY=-SU;' b'BYMONTH=1' ) r = vRecur.from_ical('FREQ=WEEKLY;INTERVAL=1;BYWEEKDAY=TH') self.assertEqual( r, {'FREQ': ['WEEKLY'], 'INTERVAL': [1], 'BYWEEKDAY': ['TH']} ) self.assertEqual( vRecur(r).to_ical(), b'FREQ=WEEKLY;INTERVAL=1;BYWEEKDAY=TH' ) # 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') # when key is not RFC-compliant, parse it as vText r = vRecur.from_ical('FREQ=MONTHLY;BYOTHER=TEXT;BYEASTER=-3') self.assertEqual(vRecur(r).to_ical(), b'FREQ=MONTHLY;BYEASTER=-3;BYOTHER=TEXT') def test_prop_vText(self): from ..prop import vText self.assertEqual(vText('Simple text').to_ical(), b'Simple text') # Escaped text t = vText('Text ; with escaped, chars') self.assertEqual(t.to_ical(), b'Text \\; with escaped\\, chars') # Escaped newlines self.assertEqual(vText('Text with escaped\\N chars').to_ical(), b'Text with escaped\\n chars') # If you pass a unicode object, it will be utf-8 encoded. As this is # the (only) standard that RFC 2445 support. t = vText('international chars \xe4\xf6\xfc') self.assertEqual(t.to_ical(), b'international chars \xc3\xa4\xc3\xb6\xc3\xbc') # and parsing? self.assertEqual(vText.from_ical('Text \\; with escaped\\, chars'), 'Text ; with escaped, chars') t = vText.from_ical('A string with\\; some\\\\ characters in\\it') self.assertEqual(t, "A string with; some\\ characters in\\it") # We are forgiving to utf-8 encoding errors: # We intentionally use a string with unexpected encoding # self.assertEqual(vText.from_ical(b'Ol\xe9'), 'Ol\ufffd') # Notice how accented E character, encoded with latin-1, got replaced # with the official U+FFFD REPLACEMENT CHARACTER. def test_prop_vTime(self): from ..prop import vTime self.assertEqual(vTime(12, 30, 0).to_ical(), '123000') self.assertEqual(vTime.from_ical('123000'), time(12, 30)) # We should also fail, right? self.assertRaises(ValueError, vTime.from_ical, '263000') def test_prop_vUri(self): from ..prop import vUri self.assertEqual(vUri('http://www.example.com/').to_ical(), b'http://www.example.com/') self.assertEqual(vUri.from_ical('http://www.example.com/'), 'http://www.example.com/') def test_prop_vGeo(self): from ..prop import vGeo # Pass a list self.assertEqual(vGeo([1.2, 3.0]).to_ical(), '1.2;3.0') # Pass a tuple self.assertEqual(vGeo((1.2, 3.0)).to_ical(), '1.2;3.0') g = vGeo.from_ical('37.386013;-122.082932') self.assertEqual(g, (float('37.386013'), float('-122.082932'))) self.assertEqual(vGeo(g).to_ical(), '37.386013;-122.082932') self.assertRaises(ValueError, vGeo, 'g') def test_prop_vUTCOffset(self): from ..prop import vUTCOffset self.assertEqual(vUTCOffset(timedelta(hours=2)).to_ical(), '+0200') self.assertEqual(vUTCOffset(timedelta(hours=-5)).to_ical(), '-0500') self.assertEqual(vUTCOffset(timedelta()).to_ical(), '+0000') self.assertEqual(vUTCOffset(timedelta(minutes=-30)).to_ical(), '-0030') self.assertEqual( vUTCOffset(timedelta(hours=2, minutes=-30)).to_ical(), '+0130' ) self.assertEqual(vUTCOffset(timedelta(hours=1, minutes=30)).to_ical(), '+0130') # Support seconds self.assertEqual(vUTCOffset(timedelta(hours=1, minutes=30, seconds=7)).to_ical(), '+013007') # Parsing self.assertEqual(vUTCOffset.from_ical('0000'), timedelta(0)) self.assertEqual(vUTCOffset.from_ical('-0030'), timedelta(-1, 84600)) self.assertEqual(vUTCOffset.from_ical('+0200'), timedelta(0, 7200)) self.assertEqual(vUTCOffset.from_ical('+023040'), timedelta(0, 9040)) self.assertEqual(vUTCOffset(vUTCOffset.from_ical('+0230')).to_ical(), '+0230') # And a few failures self.assertRaises(ValueError, vUTCOffset.from_ical, '+323k') self.assertRaises(ValueError, vUTCOffset.from_ical, '+2400') def test_prop_vInline(self): from ..prop import vInline self.assertEqual(vInline('Some text'), 'Some text') self.assertEqual(vInline.from_ical('Some text'), 'Some text') t2 = vInline('other text') t2.params['cn'] = 'Test Osterone' self.assertIsInstance(t2.params, Parameters) self.assertEqual(t2.params, {'CN': 'Test Osterone'}) def test_prop_vCategory(self): from ..prop import vCategory catz = ['cat 1', 'cat 2', 'cat 3'] v_cat = vCategory(catz) self.assertEqual(v_cat.to_ical(), b'cat 1,cat 2,cat 3') self.assertEqual(vCategory.from_ical(v_cat.to_ical()), catz) def test_prop_TypesFactory(self): from ..prop import TypesFactory # To get a type you can use it like this. factory = TypesFactory() datetime_parser = factory['date-time'] self.assertEqual(datetime_parser(datetime(2001, 1, 1)).to_ical(), b'20010101T000000') # A typical use is when the parser tries to find a content type and use # text as the default value = '20050101T123000' value_type = 'date-time' self.assertEqual(factory.get(value_type, 'text').from_ical(value), datetime(2005, 1, 1, 12, 30)) # It can also be used to directly encode property and parameter values self.assertEqual( factory.to_ical('comment', 'by Rasmussen, Max M\xfcller'), b'by Rasmussen\\, Max M\xc3\xbcller' ) self.assertEqual(factory.to_ical('priority', 1), b'1') self.assertEqual(factory.to_ical('cn', 'Rasmussen, Max M\xfcller'), b'Rasmussen\\, Max M\xc3\xbcller') self.assertEqual( factory.from_ical('cn', b'Rasmussen\\, Max M\xc3\xb8ller'), 'Rasmussen, Max M\xf8ller' ) vDDDTypes_list = [ vDDDTypes(pytz.timezone('EST').localize(datetime(year=2022, month=7, day=22, hour=12, minute=7))), vDDDTypes(datetime(year=2022, month=7, day=22, hour=12, minute=7)), vDDDTypes(datetime(year=2022, month=7, day=22, hour=12, minute=7, tzinfo=tz.UTC)), vDDDTypes(date(year=2022, month=7, day=22)), vDDDTypes(date(year=2022, month=7, day=23)), vDDDTypes(time(hour=22, minute=7, second=2)) ] def identity(x): return x @pytest.mark.parametrize("map", [ deepcopy, identity, hash, ]) @pytest.mark.parametrize("v_type", vDDDTypes_list) @pytest.mark.parametrize("other", vDDDTypes_list) def test_vDDDTypes_equivalance(map, v_type, other): if v_type is other: assert map(v_type) == map(other), f"identity implies equality: {map.__name__}()" assert not (map(v_type) != map(other)), f"identity implies equality: {map.__name__}()" else: assert map(v_type) != map(other), f"expected inequality: {map.__name__}()" assert not (map(v_type) == map(other)), f"expected inequality: {map.__name__}()" @pytest.mark.parametrize("v_type", vDDDTypes_list) def test_inequality_with_different_types(v_type): assert v_type != 42 assert v_type != 'test' class TestPropertyValues(unittest.TestCase): def test_vDDDLists_timezone(self): """Test vDDDLists with timezone information. """ from .. import Event vevent = Event() at = pytz.timezone('Europe/Vienna') dt1 = at.localize(datetime(2013, 1, 1)) dt2 = at.localize(datetime(2013, 1, 2)) dt3 = at.localize(datetime(2013, 1, 3)) vevent.add('rdate', [dt1, dt2]) vevent.add('exdate', dt3) ical = vevent.to_ical() self.assertTrue( b'RDATE;TZID=Europe/Vienna:20130101T000000,20130102T000000' in ical ) self.assertTrue(b'EXDATE;TZID=Europe/Vienna:20130103T000000' in ical) class TestWindowsOlsonMapping(unittest.TestCase): """Test the mappings from windows to olson tzids""" def test_windows_timezone(self): """test that an example""" self.assertEqual( vDatetime.from_ical('20170507T181920', 'Eastern Standard Time'), pytz.timezone('America/New_York').localize(datetime(2017, 5, 7, 18, 19, 20)) ) def test_all(self): """test if all mappings actually map to valid pytz tzids""" for olson in WINDOWS_TO_OLSON.values(): pytz.timezone(olson) icalendar-5.0.12/src/icalendar/tests/test_unit_tools.py000066400000000000000000000035061457641316200232210ustar00rootroot00000000000000import pytest 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) @pytest.mark.parametrize('split,expected,args,kw', [ # default argument host_name ("@", "example.com", (), {},), ("@", "example.com", ("example.com",), {}), ("@", "example.com", (), {"host_name":"example.com"}), # replaced host_name ("@", "test.test", ("test.test",), {}), ("@", "test.test", (), {"host_name":"test.test"}), # replace unique ("-", "123@example.com", (), {"unique": "123"},), ("-", "abc@example.com", (), {"unique": "abc"},), # replace host_name and unique ("-", "1234@test.icalendar", (), {"unique": "1234", "host_name":"test.icalendar"},), ("-", "abc@test.example.com", ("test.example.com", "abc"), {},), ]) def test_uid_generator_issue_345(args, kw, split, expected): '''Issue #345 - Why is tools.UIDGenerator a class (that must be instantiated) instead of a module? see https://github.com/collective/icalendar/issues/345 ''' uid = UIDGenerator.uid(*args, **kw) assert uid.split(split)[1] == expected icalendar-5.0.12/src/icalendar/tests/test_with_doctest.py000066400000000000000000000037771457641316200235340ustar00rootroot00000000000000"""This file tests the source code provided by the documentation. See - doctest documentation: https://docs.python.org/3/library/doctest.html - Issue 443: https://github.com/collective/icalendar/issues/443 This file should be tests, too: >>> print("Hello World!") Hello World! """ import doctest import os import pytest import importlib HERE = os.path.dirname(__file__) or "." ICALENDAR_PATH = os.path.dirname(HERE) PYTHON_FILES = [ os.path.join(dirpath, filename) for dirpath, dirnames, filenames in os.walk(ICALENDAR_PATH) for filename in filenames if filename.lower().endswith(".py") and 'fuzzing' not in dirpath ] MODULE_NAMES = [ "icalendar" + python_file[len(ICALENDAR_PATH):-3].replace("/", ".") for python_file in PYTHON_FILES ] def test_this_module_is_among_them(): assert __name__ in MODULE_NAMES @pytest.mark.parametrize("module_name", MODULE_NAMES) def test_docstring_of_python_file(module_name): """This test runs doctest on the Python module.""" module = importlib.import_module(module_name) test_result = doctest.testmod(module, name=module_name) assert test_result.failed == 0, f"{test_result.failed} errors in {module_name}" # This collection needs to exclude .tox and other subdirectories DOCUMENTATION_PATH = os.path.join(HERE, "../../../") DOCUMENT_PATHS = [ os.path.join(DOCUMENTATION_PATH, subdir, filename) for subdir in ["docs", "."] for filename in os.listdir(os.path.join(DOCUMENTATION_PATH, subdir)) if filename.lower().endswith(".rst") ] @pytest.mark.parametrize("filename", [ "README.rst", "index.rst", ]) def test_files_is_included(filename): assert any(path.endswith(filename) for path in DOCUMENT_PATHS) @pytest.mark.parametrize("document", DOCUMENT_PATHS) def test_documentation_file(document): """This test runs doctest on a documentation file.""" test_result = doctest.testfile(document, module_relative=False) assert test_result.failed == 0, f"{test_result.failed} errors in {os.path.basename(document)}" icalendar-5.0.12/src/icalendar/tests/timezones/000077500000000000000000000000001457641316200214225ustar00rootroot00000000000000icalendar-5.0.12/src/icalendar/tests/timezones/issue_237_brazilia_standard.ics000066400000000000000000000006201457641316200274000ustar00rootroot00000000000000BEGIN:VTIMEZONE TZID:(UTC-03:00) Brasília BEGIN:STANDARD TZNAME:Brasília standard DTSTART:16010101T235959 TZOFFSETFROM:-0200 TZOFFSETTO:-0300 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=3SA;BYMONTH=2 END:STANDARD BEGIN:DAYLIGHT TZNAME:Brasília daylight DTSTART:16010101T235959 TZOFFSETFROM:-0300 TZOFFSETTO:-0200 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SA;BYMONTH=10 END:DAYLIGHT END:VTIMEZONE icalendar-5.0.12/src/icalendar/tests/timezones/issue_53_tzid_parsed_properly.ics000066400000000000000000000006621457641316200301110ustar00rootroot00000000000000BEGIN: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 icalendar-5.0.12/src/icalendar/tests/timezones/issue_55_parse_error_on_utc_offset_with_seconds.ics000066400000000000000000000002741457641316200336570ustar00rootroot00000000000000BEGIN:VTIMEZONE TZID:America/Los Angeles BEGIN:STANDARD DTSTART:18831118T120702 RDATE:18831118T120702 TZNAME:PST TZOFFSETFROM:-075258 TZOFFSETTO:-0800 END:STANDARD END:VTIMEZONE icalendar-5.0.12/src/icalendar/timezone_cache.py000066400000000000000000000001231457641316200215660ustar00rootroot00000000000000# we save all timezone with TZIDs unknown to the TZDB in here _timezone_cache = {} icalendar-5.0.12/src/icalendar/tools.py000066400000000000000000000017541457641316200177640ustar00rootroot00000000000000from 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: """If you are too lazy to create real uid's. """ chars = list(ascii_letters + digits) @staticmethod def rnd_string(length=16): """Generates a string with random characters of length. """ return ''.join([random.choice(UIDGenerator.chars) for _ in range(length)]) @staticmethod def uid(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 UIDGenerator.rnd_string() today = to_unicode(vDatetime(datetime.today()).to_ical()) return vText(f'{today}-{unique}@{host_name}') icalendar-5.0.12/src/icalendar/windows_to_olson.py000066400000000000000000000130031457641316200222200ustar00rootroot00000000000000"""This module contains mappings from Windows timezone identifiers to Olson timezone identifiers. The data is taken from the unicode consortium [0], the proposal and rationale for this mapping is also available at the unicode consortium [1]. [0] https://www.unicode.org/cldr/cldr-aux/charts/29/supplemental/zone_tzid.html [1] https://cldr.unicode.org/development/development-process/design-proposals/extended-windows-olson-zid-mapping # noqa """ WINDOWS_TO_OLSON = { 'AUS Central Standard Time': 'Australia/Darwin', 'AUS Eastern Standard Time': 'Australia/Sydney', 'Afghanistan Standard Time': 'Asia/Kabul', 'Alaskan Standard Time': 'America/Anchorage', 'Arab Standard Time': 'Asia/Riyadh', 'Arabian Standard Time': 'Asia/Dubai', 'Arabic Standard Time': 'Asia/Baghdad', 'Argentina Standard Time': 'America/Argentina/Buenos_Aires', 'Atlantic Standard Time': 'America/Halifax', 'Azerbaijan Standard Time': 'Asia/Baku', 'Azores Standard Time': 'Atlantic/Azores', 'Bahia Standard Time': 'America/Bahia', 'Bangladesh Standard Time': 'Asia/Dhaka', 'Belarus Standard Time': 'Europe/Minsk', 'Canada Central Standard Time': 'America/Regina', 'Cape Verde Standard Time': 'Atlantic/Cape_Verde', 'Caucasus Standard Time': 'Asia/Yerevan', 'Cen. Australia Standard Time': 'Australia/Adelaide', 'Central America Standard Time': 'America/Guatemala', 'Central Asia Standard Time': 'Asia/Almaty', 'Central Brazilian Standard Time': 'America/Cuiaba', 'Central Europe Standard Time': 'Europe/Budapest', 'Central European Standard Time': 'Europe/Warsaw', 'Central Pacific Standard Time': 'Pacific/Guadalcanal', 'Central Standard Time': 'America/Chicago', 'Central Standard Time (Mexico)': 'America/Mexico_City', 'China Standard Time': 'Asia/Shanghai', 'Dateline Standard Time': 'Etc/GMT+12', 'E. Africa Standard Time': 'Africa/Nairobi', 'E. Australia Standard Time': 'Australia/Brisbane', 'E. Europe Standard Time': 'Europe/Chisinau', 'E. South America Standard Time': 'America/Sao_Paulo', 'Eastern Standard Time': 'America/New_York', 'Eastern Standard Time (Mexico)': 'America/Cancun', 'Egypt Standard Time': 'Africa/Cairo', 'Ekaterinburg Standard Time': 'Asia/Yekaterinburg', 'FLE Standard Time': 'Europe/Kyiv', 'Fiji Standard Time': 'Pacific/Fiji', 'GMT Standard Time': 'Europe/London', 'GTB Standard Time': 'Europe/Bucharest', 'Georgian Standard Time': 'Asia/Tbilisi', 'Greenland Standard Time': 'America/Godthab', 'Greenwich Standard Time': 'Atlantic/Reykjavik', 'Hawaiian Standard Time': 'Pacific/Honolulu', 'India Standard Time': 'Asia/Kolkata', 'Iran Standard Time': 'Asia/Tehran', 'Israel Standard Time': 'Asia/Jerusalem', 'Jordan Standard Time': 'Asia/Amman', 'Kaliningrad Standard Time': 'Europe/Kaliningrad', 'Korea Standard Time': 'Asia/Seoul', 'Libya Standard Time': 'Africa/Tripoli', 'Line Islands Standard Time': 'Pacific/Kiritimati', 'Magadan Standard Time': 'Asia/Magadan', 'Mauritius Standard Time': 'Indian/Mauritius', 'Middle East Standard Time': 'Asia/Beirut', 'Montevideo Standard Time': 'America/Montevideo', 'Morocco Standard Time': 'Africa/Casablanca', 'Mountain Standard Time': 'America/Denver', 'Mountain Standard Time (Mexico)': 'America/Chihuahua', 'Myanmar Standard Time': 'Asia/Yangon', 'N. Central Asia Standard Time': 'Asia/Novosibirsk', 'Namibia Standard Time': 'Africa/Windhoek', 'Nepal Standard Time': 'Asia/Kathmandu', 'New Zealand Standard Time': 'Pacific/Auckland', 'Newfoundland Standard Time': 'America/St_Johns', 'North Asia East Standard Time': 'Asia/Irkutsk', 'North Asia Standard Time': 'Asia/Krasnoyarsk', 'North Korea Standard Time': 'Asia/Pyongyang', 'Pacific SA Standard Time': 'America/Santiago', 'Pacific Standard Time': 'America/Los_Angeles', 'Pakistan Standard Time': 'Asia/Karachi', 'Paraguay Standard Time': 'America/Asuncion', 'Romance Standard Time': 'Europe/Paris', 'Russia Time Zone 10': 'Asia/Srednekolymsk', 'Russia Time Zone 11': 'Asia/Kamchatka', 'Russia Time Zone 3': 'Europe/Samara', 'Russian Standard Time': 'Europe/Moscow', 'SA Eastern Standard Time': 'America/Cayenne', 'SA Pacific Standard Time': 'America/Bogota', 'SA Western Standard Time': 'America/La_Paz', 'SE Asia Standard Time': 'Asia/Bangkok', 'Samoa Standard Time': 'Pacific/Apia', 'Singapore Standard Time': 'Asia/Singapore', 'South Africa Standard Time': 'Africa/Johannesburg', 'Sri Lanka Standard Time': 'Asia/Colombo', 'Syria Standard Time': 'Asia/Damascus', 'Taipei Standard Time': 'Asia/Taipei', 'Tasmania Standard Time': 'Australia/Hobart', 'Tokyo Standard Time': 'Asia/Tokyo', 'Tonga Standard Time': 'Pacific/Tongatapu', 'Turkey Standard Time': 'Europe/Istanbul', 'US Eastern Standard Time': 'America/Indiana/Indianapolis', 'US Mountain Standard Time': 'America/Phoenix', 'UTC': 'Etc/GMT', 'UTC+12': 'Etc/GMT-12', 'UTC-02': 'Etc/GMT+2', 'UTC-11': 'Etc/GMT+11', 'Ulaanbaatar Standard Time': 'Asia/Ulaanbaatar', 'Venezuela Standard Time': 'America/Caracas', 'Vladivostok Standard Time': 'Asia/Vladivostok', 'W. Australia Standard Time': 'Australia/Perth', 'W. Central Africa Standard Time': 'Africa/Lagos', 'W. Europe Standard Time': 'Europe/Berlin', 'West Asia Standard Time': 'Asia/Tashkent', 'West Pacific Standard Time': 'Pacific/Port_Moresby', 'Yakutsk Standard Time': 'Asia/Yakutsk', } icalendar-5.0.12/tox.ini000066400000000000000000000012721457641316200150470ustar00rootroot00000000000000# to run for a specific environment, use ``tox -e ENVNAME`` [tox] envlist = py37,py38,py39,py310,py311,pypy3,docs # Note: the 'docs' env creates a 'build' directory which may interfere in strange ways # with the other environments. You might see this when you run the tests in parallel. # See https://github.com/collective/icalendar/pull/359#issuecomment-1214150269 [testenv] usedevelop=True deps = pytest coverage hypothesis commands = coverage run --source=src/icalendar --omit=*/tests/* --module pytest [] coverage report coverage html [testenv:docs] deps = -r {toxinidir}/requirements_docs.txt changedir = docs allowlist_externals = make commands = make html