├── icalevents ├── py.typed ├── __init__.py ├── icaldownload.py ├── icalevents.py └── icalparser.py ├── test ├── test_data │ ├── empty.ics │ ├── basic_latin1.ics │ ├── no_description.ics │ ├── google_tz.ics │ ├── duration.ics │ ├── regression_offset_native.ics │ ├── rrule_until.ics │ ├── recurring_small_window.ics │ ├── categories_test.ics │ ├── recurring.ics │ ├── recurrence_tzinfo.ics │ ├── rrule_until_all_day_ms.ics │ ├── no_uid.ics │ ├── status_and_url.ics │ ├── non_ascii_uid.ics │ ├── rrule_until_all_day_google.ics │ ├── rrule_until_only_date.ics │ ├── ms_tz.ics │ ├── cest_every_second_day_for_one_year.ics │ ├── response.ics │ ├── cest_every_day_for_one_year.ics │ ├── per_event_timezone.ics │ ├── multi_attendee_response.ics │ ├── cest_with_deleted.ics │ ├── recurrenceid_google.ics │ ├── recurrence_tz.ics │ ├── multi_exdate_same_line_ms.ics │ ├── non_floating.ics │ ├── google_2024.ics │ ├── created_last_modified.ics │ ├── transparent.ics │ ├── recurrenceid_ms.ics │ ├── small_time_frame.ics │ ├── floating.ics │ ├── recurr_id_dtstart_missmatch.ics │ ├── icloud.ics │ ├── icloud_content.txt │ └── basic.ics ├── __init__.py ├── test_icaldownload.py ├── test_icalparser.py └── test_icalevents.py ├── setup.cfg ├── MANIFEST.in ├── .flake8 ├── .github ├── codecov.yml └── workflows │ ├── release.yml │ └── tests.yml ├── test.py ├── .gitignore ├── .devcontainer └── devcontainer.json ├── .coveragerc ├── CONTRIBUTING.md ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── docs ├── Makefile ├── make.bat ├── conf.py └── index.rst ├── pyproject.toml ├── LICENSE ├── main.py ├── README.md ├── setup.py └── CODE_OF_CONDUCT.md /icalevents/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/test_data/empty.ics: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for icalevents. 3 | """ 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include test *.ics *.txt py.typed 2 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-complexity = 10 3 | select = E9,F63,F7,F82 4 | -------------------------------------------------------------------------------- /test/test_data/basic_latin1.ics: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/icalevents/master/test/test_data/basic_latin1.ics -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | # To validate this file on changes before committing, see https://api.codecov.io/validate 2 | 3 | codecov: 4 | notify: 5 | after_n_builds: 2 6 | -------------------------------------------------------------------------------- /icalevents/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | iCalEvents search all events occurring in a given time frame in an iCal file. 3 | """ 4 | 5 | __all__ = ["icaldownload", "icalparser", "icalevents"] 6 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import unittest 3 | 4 | from test.test_icaldownload import * 5 | from test.test_icalparser import * 6 | from test.test_icalevents import * 7 | 8 | unittest.main() 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | calendars.txt 2 | .cache 3 | */*.pyc 4 | *__pycache__* 5 | .idea/ 6 | .coverage 7 | dist/* 8 | MANIFEST 9 | icalevents.egg-info/* 10 | /build 11 | 12 | docs/_build/** 13 | coverage.xml 14 | -------------------------------------------------------------------------------- /test/test_data/no_description.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | BEGIN:VTIMEZONE 3 | TZID:Europe/Berlin 4 | END:VTIMEZONE 5 | BEGIN:VEVENT 6 | DTSTART;VALUE=DATE:20181030 7 | DTEND;VALUE=DATE:20181031 8 | RRULE:FREQ=WEEKLY;BYDAY=TU 9 | END:VEVENT 10 | END:VCALENDAR 11 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/base:ubuntu", 3 | "features": { 4 | "ghcr.io/devcontainers/features/python:1": { 5 | "version": "3.12", 6 | "toolsToInstall": "poetry,pre-commit" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = icalevents 4 | 5 | [report] 6 | exclude_lines = 7 | if self.debug: 8 | pragma: no cover 9 | raise NotImplementedError 10 | if __name__ == .__main__.: 11 | ignore_errors = True 12 | omit = 13 | tests/* 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) 2 | 3 | This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines). 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v6.0.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/psf/black-pre-commit-mirror 9 | rev: 25.11.0 10 | hooks: 11 | - id: black 12 | - repo: https://github.com/PyCQA/flake8 13 | rev: 7.3.0 14 | hooks: 15 | - id: flake8 16 | -------------------------------------------------------------------------------- /test/test_data/google_tz.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | METHOD:PUBLISH 6 | X-WR-CALNAME:test 7 | X-WR-TIMEZONE:Europe/Zurich 8 | BEGIN:VEVENT 9 | DTSTART;VALUE=DATE:20210324 10 | DTEND;VALUE=DATE:20210327 11 | DTSTAMP:20210328T094322Z 12 | UID:1gn1mvk5325euprb9fcf7ndpdn@google.com 13 | ATTENDEE;X-NUM-GUESTS=0:mailto:hst9ma4s2h5hf2iafmrqffki9o@group.calendar.go 14 | ogle.com 15 | SUMMARY:Busy 16 | END:VEVENT 17 | END:VCALENDAR 18 | -------------------------------------------------------------------------------- /test/test_data/duration.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | BEGIN:VEVENT 3 | DTSTART:20180110 4 | DURATION:P3D 5 | DESCRIPTION:Event with duration (3 days), instead of explicit end. 6 | SUMMARY:Duration Event 7 | END:VEVENT 8 | BEGIN:VEVENT 9 | DTSTART:20180115T100000 10 | DURATION:PT3H 11 | DESCRIPTION:Event with duration (3 hours), instead of explicit end. 12 | SUMMARY:Duration Event 13 | END:VEVENT 14 | BEGIN:VEVENT 15 | DTSTART:20180120T120000 16 | DESCRIPTION:Event without explicit dtend, nor duration property. 17 | SUMMARY:Short event 18 | END:VEVENT 19 | END:VCALENDAR 20 | -------------------------------------------------------------------------------- /test/test_data/regression_offset_native.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//6735d90b475eebfc9f111fc66f5e68fa//NONSGML kigkonsult.se iCalcreat 4 | or 2.29.14// 5 | CALSCALE:GREGORIAN 6 | UID:84c4ae71-1eea-49fc-8e32-b551101c0b2d 7 | X-WR-CALNAME:Company Holidays 8 | BEGIN:VEVENT 9 | UID:6200a801-f5f2-4c9d-a0a0-496f2691b19a 10 | DTSTAMP:20200814T182712Z 11 | CATEGORIES:Company Holidays 12 | DESCRIPTION:Holiday (Jul 31) 13 | DTSTART;VALUE=DATE:20200731 14 | DTEND;VALUE=DATE:20200801 15 | SUMMARY:Company Holiday - Studio Closure 16 | TRANSP:TRANSPARENT 17 | END:VEVENT 18 | END:VCALENDAR 19 | -------------------------------------------------------------------------------- /test/test_data/rrule_until.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | BEGIN:VTIMEZONE 3 | TZID:Europe/Berlin 4 | END:VTIMEZONE 5 | BEGIN:VEVENT 6 | DTSTART;VALUE=DATE:20151030 7 | DTEND;VALUE=DATE:20151031 8 | DESCRIPTION:All-day event recurring on tuesday each week 9 | SUMMARY:Recurring All-day Event 10 | RRULE:FREQ=WEEKLY;BYDAY=TU;UNTIL=20341031 11 | END:VEVENT 12 | BEGIN:VEVENT 13 | DTSTART;TZID=Europe/London:20180522T120000 14 | DTEND;TZID=Europe/London:20180522T130000 15 | DESCRIPTION:Daily lunchtime event with specified hours 16 | SUMMARY:Daily lunch event 17 | RRULE:FREQ=DAILY;UNTIL=20330523T060000Z 18 | END:VEVENT 19 | END:VCALENDAR 20 | -------------------------------------------------------------------------------- /test/test_data/recurring_small_window.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VTIMEZONE 4 | TZID:Europe/Berlin 5 | BEGIN:STANDARD 6 | DTSTART:19701025T030000 7 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 8 | TZNAME:CET 9 | TZOFFSETFROM:+0200 10 | TZOFFSETTO:+0100 11 | END:STANDARD 12 | BEGIN:DAYLIGHT 13 | DTSTART:19700329T020000 14 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 15 | TZNAME:CEST 16 | TZOFFSETFROM:+0100 17 | TZOFFSETTO:+0200 18 | END:DAYLIGHT 19 | END:VTIMEZONE 20 | BEGIN:VEVENT 21 | DTSTART;TZID=Europe/Berlin:20211129T000000 22 | DTEND;TZID=Europe/Berlin:20211129T080000 23 | RRULE:FREQ=WEEKLY;UNTIL=20221230T230000Z;BYDAY=MO,TU,WE 24 | END:VEVENT 25 | END:VCALENDAR 26 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.12" 7 | jobs: 8 | post_create_environment: 9 | # Install poetry 10 | # https://python-poetry.org/docs/#installing-manually 11 | - pip install poetry 12 | post_install: 13 | # Install dependencies with 'docs' dependency group 14 | # https://python-poetry.org/docs/managing-dependencies/#dependency-groups 15 | # VIRTUAL_ENV needs to be set manually for now. 16 | # See https://github.com/readthedocs/readthedocs.org/pull/11152/ 17 | - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install 18 | sphinx: 19 | configuration: docs/conf.py 20 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /test/test_data/categories_test.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | METHOD:PUBLISH 3 | PRODID:-//Moodle Pty Ltd//NONSGML Moodle Version 2019111802//EN 4 | VERSION:2.0 5 | BEGIN:VEVENT 6 | UID:3014@lms.itum.mrt.ac.lk 7 | SUMMARY:Lecture 2 8 | DESCRIPTION: 9 | CLASS:PUBLIC 10 | LAST-MODIFIED:20201110T094221Z 11 | DTSTAMP:20201110T095626Z 12 | DTSTART:20201117T073000Z 13 | DTEND:20201117T093000Z 14 | CATEGORIES:In19-S04-IT2403 15 | END:VEVENT 16 | BEGIN:VEVENT 17 | UID:3003@lms.itum.mrt.ac.lk 18 | SUMMARY:Week 2 (18/11/2020) 19 | DESCRIPTION:If Password ask you can give the following:Password: it2406-MC 20 | CLASS:PUBLIC 21 | LAST-MODIFIED:20201110T062311Z 22 | DTSTAMP:20201110T095626Z 23 | DTSTART:20201118T021500Z 24 | DTEND:20201118T041500Z 25 | CATEGORIES:In19-S04-IT2406,In19-S04-IT2405 26 | END:VEVENT 27 | END:VCALENDAR 28 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "icalevents" 3 | version = "0.3.1" 4 | description = "Simple Python 3 library to download, parse and query iCal sources." 5 | authors = [ 6 | { name = "Martin Eigenmann", email = "github@eigenmannmartin.ch" }, 7 | { name = "Thomas Irgang", email = "thomas@irgang.eu" }, 8 | { name = "Alexander Hultnér", email = "ahultner+github@gmail.com" }, 9 | ] 10 | readme = "README.md" 11 | license = "MIT" 12 | requires-python = ">=3.9" 13 | 14 | dependencies = [ 15 | "icalendar (>=5.0.0)", 16 | "python-dateutil (~=2.9)", 17 | "pytz (>=2024.2)", 18 | "urllib3 (>=1.26.5)", 19 | ] 20 | 21 | [tool.poetry] 22 | requires-poetry = ">=2.0" 23 | 24 | [tool.poetry.group.dev.dependencies] 25 | coverage = "^7.6.10" 26 | pytest = "^8.3.4" 27 | black = "^24.10.0" 28 | flake8 = "^7.1.1" 29 | pook = "^2.1.3" 30 | 31 | [build-system] 32 | requires = ["poetry-core>=2.0"] 33 | build-backend = "poetry.core.masonry.api" 34 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /test/test_data/recurring.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | BEGIN:VTIMEZONE 3 | TZID:Europe/Berlin 4 | END:VTIMEZONE 5 | BEGIN:VEVENT 6 | DTSTART;TZID=Europe/Berlin:20181003T100000 7 | DTEND;TZID=Europe/Berlin:20181003T120000 8 | DESCRIPTION:Event recurring on wednesday each week, except on 2018-10-29 9 | SUMMARY:Recurring Event 10 | RRULE:FREQ=WEEKLY;BYDAY=MO 11 | EXDATE;TZID=Europe/Berlin:20181029T100000 12 | END:VEVENT 13 | BEGIN:VEVENT 14 | DTSTART;TZID=Europe/Berlin:20180601T100000 15 | DTEND;TZID=Europe/Berlin:20180601T120000 16 | DESCRIPTION:Event recurring on friday each week, except on 2018-06-08/22 17 | SUMMARY:Recurring Event 18 | RRULE:FREQ=WEEKLY;BYDAY=FR 19 | EXDATE;TZID=Europe/Berlin:20180608T100000 20 | EXDATE;TZID=Europe/Berlin:20180622T100000 21 | END:VEVENT 22 | BEGIN:VEVENT 23 | DTSTART;VALUE=DATE:20181030 24 | DTEND;VALUE=DATE:20181031 25 | DESCRIPTION:All-day event recurring on tuesday each week 26 | SUMMARY:Recurring All-day Event 27 | RRULE:FREQ=WEEKLY;BYDAY=TU 28 | END:VEVENT 29 | END:VCALENDAR 30 | -------------------------------------------------------------------------------- /test/test_data/recurrence_tzinfo.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VEVENT 2 | DTSTAMP:20231221T180428Z 3 | DTSTART;VALUE=DATE:20231127 4 | DTEND;VALUE=DATE:20231128 5 | SUMMARY:DSOC 6 | CATEGORIES:other 7 | SUBCALENDAR-ID:3dfdd9f8-dc5a-49b6-9b54-644138545076 8 | PARENT-CALENDAR-ID:f681d504-b5ed-4258-a68b-7deb867cf1bf 9 | PARENT-CALENDAR-NAME: 10 | SUBSCRIPTION-ID: 11 | SUBCALENDAR-TZ-ID:America/Los_Angeles 12 | SUBCALENDAR-NAME:My calendar 13 | EVENT-ID:163463 14 | EVENT-ALLDAY:true 15 | UID:20231012T192858Z--1042663660@my_org.com 16 | DESCRIPTION: 17 | ORGANIZER;X-CONFLUENCE-USER-KEY=09ceee004595a94a014595d847942220;CN=My Name;CUTYPE=INDIVIDUAL:mailto:my_email@email.com 18 | RRULE:FREQ=WEEKLY;UNTIL=20240115;INTERVAL=1;BYDAY=MO 19 | CREATED:20231012T192858Z 20 | LAST-MODIFIED:20231130T174704Z 21 | SEQUENCE:3 22 | X-CONFLUENCE-SUBCALENDAR-TYPE:other 23 | TRANSP:TRANSPARENT 24 | STATUS:CONFIRMED 25 | EXDATE;VALUE=DATE:20231225 26 | EXDATE;VALUE=DATE:20231218 27 | EXDATE;VALUE=DATE:20231225 28 | EXDATE;VALUE=DATE:20231218 29 | END:VEVENT 30 | -------------------------------------------------------------------------------- /test/test_data/rrule_until_all_day_ms.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | METHOD:PUBLISH 3 | PRODID:Microsoft Exchange Server 2010 4 | VERSION:2.0 5 | X-WR-CALNAME:Kalender 6 | BEGIN:VTIMEZONE 7 | TZID:W. Europe Standard Time 8 | BEGIN:STANDARD 9 | DTSTART:16010101T030000 10 | TZOFFSETFROM:+0200 11 | TZOFFSETTO:+0100 12 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 13 | END:STANDARD 14 | BEGIN:DAYLIGHT 15 | DTSTART:16010101T020000 16 | TZOFFSETFROM:+0100 17 | TZOFFSETTO:+0200 18 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 19 | END:DAYLIGHT 20 | END:VTIMEZONE 21 | BEGIN:VEVENT 22 | RRULE:FREQ=WEEKLY;UNTIL=20210502T220000Z;INTERVAL=1;BYDAY=FR;WKST=MO 23 | EXDATE;TZID=W. Europe Standard Time:20210430T000000 24 | UID:040000008200E00074C5B7101A82E00800000000D0A3F2C7C4E8D601000000000000000 25 | 0100000006C7ACDDE7E4A5E47ACD97CBB31A1438C 26 | SUMMARY:Away 27 | DTSTART;VALUE=DATE:20210319 28 | DTEND;VALUE=DATE:20210321 29 | CLASS:PUBLIC 30 | PRIORITY:5 31 | DTSTAMP:20210326T082505Z 32 | TRANSP:OPAQUE 33 | STATUS:CONFIRMED 34 | SEQUENCE:0 35 | END:VEVENT 36 | END:VCALENDAR 37 | -------------------------------------------------------------------------------- /test/test_data/no_uid.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | METHOD:PUBLISH 3 | PRODID:Microsoft Exchange Server 2010 4 | VERSION:2.0 5 | X-WR-CALNAME:Kalender 6 | BEGIN:VTIMEZONE 7 | TZID:Central European Standard Time 8 | BEGIN:STANDARD 9 | DTSTART:16010101T030000 10 | TZOFFSETFROM:+0200 11 | TZOFFSETTO:+0100 12 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 13 | END:STANDARD 14 | BEGIN:DAYLIGHT 15 | DTSTART:16010101T020000 16 | TZOFFSETFROM:+0100 17 | TZOFFSETTO:+0200 18 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 19 | END:DAYLIGHT 20 | END:VTIMEZONE 21 | BEGIN:VEVENT 22 | SUMMARY:Free 23 | DTSTART;VALUE=DATE:20210520 24 | DTEND;VALUE=DATE:20210521 25 | CLASS:PUBLIC 26 | PRIORITY:5 27 | DTSTAMP:20210516T144840Z 28 | TRANSP:TRANSPARENT 29 | STATUS:CONFIRMED 30 | SEQUENCE:0 31 | X-MICROSOFT-CDO-APPT-SEQUENCE:0 32 | X-MICROSOFT-CDO-BUSYSTATUS:FREE 33 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 34 | X-MICROSOFT-CDO-ALLDAYEVENT:TRUE 35 | X-MICROSOFT-CDO-IMPORTANCE:1 36 | X-MICROSOFT-CDO-INSTTYPE:1 37 | X-MICROSOFT-DONOTFORWARDMEETING:FALSE 38 | X-MICROSOFT-DISALLOW-COUNTER:FALSE 39 | END:VEVENT 40 | END:VCALENDAR 41 | -------------------------------------------------------------------------------- /test/test_data/status_and_url.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | BEGIN:VTIMEZONE 3 | TZID:Europe/Berlin 4 | END:VTIMEZONE 5 | BEGIN:VEVENT 6 | DESCRIPTION:Event with Status and URL 7 | SUMMARY:Tentative Event w/ Recurrance to test copy 8 | STATUS:TENTATIVE 9 | DTSTART;VALUE=DATE:20181030 10 | DTEND;VALUE=DATE:20181031 11 | RRULE:FREQ=WEEKLY;BYDAY=TU 12 | END:VEVENT 13 | BEGIN:VEVENT 14 | DESCRIPTION:Event with Status and URL 15 | SUMMARY:Confirmed Event 16 | STATUS:CONFIRMED 17 | URL:https://example.com/ 18 | DTSTART;VALUE=DATE:20181030 19 | DTEND;VALUE=DATE:20181031 20 | END:VEVENT 21 | BEGIN:VEVENT 22 | DESCRIPTION:Event with Status 23 | SUMMARY:Cancelled Event 24 | STATUS:CANCELLED 25 | DTSTART;VALUE=DATE:20181030 26 | DTEND;VALUE=DATE:20181031 27 | END:VEVENT 28 | BEGIN:VEVENT 29 | DESCRIPTION:Event with Status 30 | SUMMARY:XPARAM Event 31 | STATUS;X-SOMETHING=IGNOREME:CANCELLED 32 | DTSTART;VALUE=DATE:20181030 33 | DTEND;VALUE=DATE:20181031 34 | END:VEVENT 35 | BEGIN:VEVENT 36 | DESCRIPTION:Event without Status 37 | SUMMARY:Event 38 | DTSTART;VALUE=DATE:20181030 39 | DTEND;VALUE=DATE:20181031 40 | END:VEVENT 41 | END:VCALENDAR 42 | -------------------------------------------------------------------------------- /test/test_data/non_ascii_uid.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | METHOD:PUBLISH 3 | PRODID:Microsoft Exchange Server 2010 4 | VERSION:2.0 5 | X-WR-CALNAME:Kalender 6 | BEGIN:VTIMEZONE 7 | TZID:Central European Standard Time 8 | BEGIN:STANDARD 9 | DTSTART:16010101T030000 10 | TZOFFSETFROM:+0200 11 | TZOFFSETTO:+0100 12 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 13 | END:STANDARD 14 | BEGIN:DAYLIGHT 15 | DTSTART:16010101T020000 16 | TZOFFSETFROM:+0100 17 | TZOFFSETTO:+0200 18 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 19 | END:DAYLIGHT 20 | END:VTIMEZONE 21 | BEGIN:VEVENT 22 | UID:🙉🙈👀 23 | SUMMARY:Free 24 | DTSTART;VALUE=DATE:20210520 25 | DTEND;VALUE=DATE:20210521 26 | CLASS:PUBLIC 27 | PRIORITY:5 28 | DTSTAMP:20210516T144840Z 29 | TRANSP:TRANSPARENT 30 | STATUS:CONFIRMED 31 | SEQUENCE:0 32 | X-MICROSOFT-CDO-APPT-SEQUENCE:0 33 | X-MICROSOFT-CDO-BUSYSTATUS:FREE 34 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 35 | X-MICROSOFT-CDO-ALLDAYEVENT:TRUE 36 | X-MICROSOFT-CDO-IMPORTANCE:1 37 | X-MICROSOFT-CDO-INSTTYPE:1 38 | X-MICROSOFT-DONOTFORWARDMEETING:FALSE 39 | X-MICROSOFT-DISALLOW-COUNTER:FALSE 40 | END:VEVENT 41 | END:VCALENDAR 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Thomas Irgang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from icalevents.icalevents import events_async, latest_events, all_done 2 | from time import sleep 3 | 4 | if __name__ == "__main__": 5 | keys = [] 6 | 7 | with open("calendars.txt", mode="r", encoding="utf-8") as f: 8 | counter = 1 9 | 10 | while True: 11 | line = f.readline() 12 | if not line: 13 | break 14 | 15 | name, url = line.split(maxsplit=1) 16 | name = name.strip() 17 | url = url.strip() 18 | 19 | fix_apple = False 20 | if name == "icloud": 21 | fix_apple = True 22 | 23 | key = "req_%d" % counter 24 | counter += 1 25 | keys.append(key) 26 | events_async(key, url, fix_apple=fix_apple) 27 | 28 | while keys: 29 | print("%d request running." % len(keys)) 30 | 31 | for k in keys[:]: 32 | if all_done(k): 33 | print("Request %s finished." % k) 34 | keys.remove(k) 35 | 36 | es = latest_events(k) 37 | 38 | for e in es: 39 | print(e) 40 | 41 | sleep(2) 42 | -------------------------------------------------------------------------------- /test/test_data/rrule_until_all_day_google.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | METHOD:PUBLISH 6 | X-WR-CALNAME:test 7 | X-WR-TIMEZONE:Europe/Zurich 8 | BEGIN:VEVENT 9 | DTSTART;VALUE=DATE:20210407 10 | DTEND;VALUE=DATE:20210410 11 | DTSTAMP:20210328T184235Z 12 | UID:1gn1mvk5325euprb9fcf7ndpdn@google.com 13 | ATTENDEE;X-NUM-GUESTS=0:mailto:hst9ma4s2h5hf2iafmrqffki9o@group.calendar.go 14 | ogle.com 15 | RECURRENCE-ID;VALUE=DATE:20210407 16 | SUMMARY:Busy 17 | END:VEVENT 18 | BEGIN:VEVENT 19 | DTSTART;VALUE=DATE:20210331 20 | DTEND;VALUE=DATE:20210403 21 | DTSTAMP:20210328T184235Z 22 | UID:1gn1mvk5325euprb9fcf7ndpdn@google.com 23 | ATTENDEE;X-NUM-GUESTS=0:mailto:hst9ma4s2h5hf2iafmrqffki9o@group.calendar.go 24 | ogle.com 25 | RECURRENCE-ID;VALUE=DATE:20210331 26 | SUMMARY:Busy 27 | END:VEVENT 28 | BEGIN:VEVENT 29 | DTSTART;VALUE=DATE:20210324 30 | DTEND;VALUE=DATE:20210327 31 | DTSTAMP:20210328T184235Z 32 | UID:1gn1mvk5325euprb9fcf7ndpdn@google.com 33 | ATTENDEE;X-NUM-GUESTS=0:mailto:hst9ma4s2h5hf2iafmrqffki9o@group.calendar.go 34 | ogle.com 35 | RECURRENCE-ID;VALUE=DATE:20210324 36 | SUMMARY:Busy 37 | END:VEVENT 38 | END:VCALENDAR 39 | -------------------------------------------------------------------------------- /test/test_data/rrule_until_only_date.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | METHOD:PUBLISH 6 | X-WR-CALNAME:NotYourCalendar 7 | X-WR-TIMEZONE:America/Boise 8 | BEGIN:VTIMEZONE 9 | TZID:America/Boise 10 | X-LIC-LOCATION:America/Boise 11 | BEGIN:DAYLIGHT 12 | TZOFFSETFROM:-0700 13 | TZOFFSETTO:-0600 14 | TZNAME:MDT 15 | DTSTART:19700308T020000 16 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 17 | END:DAYLIGHT 18 | BEGIN:STANDARD 19 | TZOFFSETFROM:-0600 20 | TZOFFSETTO:-0700 21 | TZNAME:MST 22 | DTSTART:19701101T020000 23 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 24 | END:STANDARD 25 | END:VTIMEZONE 26 | BEGIN:VEVENT 27 | DTSTART;TZID=America/Boise:20210929T130000 28 | DTEND;TZID=America/Boise:20210929T135000 29 | RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20211020;BYDAY=MO,TH,WE 30 | EXDATE;TZID=America/Boise:20211013T130000 31 | DTSTAMP:20211029T011330Z 32 | UID:9cntado4fkl7simv3loh94ua1m@google.com 33 | CREATED:20210811T153934Z 34 | DESCRIPTION: 35 | LAST-MODIFIED:20210927T184338Z 36 | LOCATION: 37 | SEQUENCE:3 38 | STATUS:CONFIRMED 39 | SUMMARY:LUNCH 40 | TRANSP:OPAQUE 41 | X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC 42 | END:VEVENT 43 | END:VCALENDAR 44 | -------------------------------------------------------------------------------- /test/test_data/ms_tz.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | METHOD:PUBLISH 3 | PRODID:Microsoft Exchange Server 2010 4 | VERSION:2.0 5 | X-WR-CALNAME:test 6 | BEGIN:VTIMEZONE 7 | TZID:W. Europe Standard Time 8 | BEGIN:STANDARD 9 | DTSTART:16010101T030000 10 | TZOFFSETFROM:+0200 11 | TZOFFSETTO:+0100 12 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 13 | END:STANDARD 14 | BEGIN:DAYLIGHT 15 | DTSTART:16010101T020000 16 | TZOFFSETFROM:+0100 17 | TZOFFSETTO:+0200 18 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 19 | END:DAYLIGHT 20 | END:VTIMEZONE 21 | BEGIN:VEVENT 22 | UID:040000008200E00074C5B7101A82E0080000000007E56CFC7E1DD701000000000000000 23 | 01000000068B41E0ADF04374687B1104CB913D622 24 | SUMMARY:Busy 25 | DTSTART;VALUE=DATE:20210324 26 | DTEND;VALUE=DATE:20210327 27 | CLASS:PUBLIC 28 | PRIORITY:5 29 | DTSTAMP:20210328T104140Z 30 | TRANSP:OPAQUE 31 | STATUS:CONFIRMED 32 | SEQUENCE:0 33 | X-MICROSOFT-CDO-APPT-SEQUENCE:0 34 | X-MICROSOFT-CDO-BUSYSTATUS:BUSY 35 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 36 | X-MICROSOFT-CDO-ALLDAYEVENT:TRUE 37 | X-MICROSOFT-CDO-IMPORTANCE:1 38 | X-MICROSOFT-CDO-INSTTYPE:0 39 | X-MICROSOFT-DONOTFORWARDMEETING:FALSE 40 | X-MICROSOFT-DISALLOW-COUNTER:FALSE 41 | END:VEVENT 42 | END:VCALENDAR 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | deploy: 13 | if: github.repository == 'jazzband/icalevents' 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: "Checkout repository with all history for all branches and tags" 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: "Set up latest Python 3 version" 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: "3.x" 26 | 27 | - name: "Install poetry" 28 | uses: abatilo/actions-poetry@v3 29 | with: 30 | poetry-version: "2.0.1" 31 | 32 | - name: "Build package" 33 | run: poetry build 34 | 35 | - name: "Upload packages to Jazzband" 36 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 37 | uses: pypa/gh-action-pypi-publish@release/v1 38 | with: 39 | user: jazzband 40 | password: ${{ secrets.JAZZBAND_RELEASE_KEY }} 41 | repository-url: https://jazzband.co/projects/icalevents/upload 42 | attestations: false 43 | -------------------------------------------------------------------------------- /test/test_data/cest_every_second_day_for_one_year.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | METHOD:PUBLISH 3 | PRODID:Microsoft Exchange Server 2010 4 | VERSION:2.0 5 | X-WR-CALNAME:test2 6 | BEGIN:VTIMEZONE 7 | TZID:W. Europe Standard Time 8 | BEGIN:STANDARD 9 | DTSTART:16010101T030000 10 | TZOFFSETFROM:+0200 11 | TZOFFSETTO:+0100 12 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 13 | END:STANDARD 14 | BEGIN:DAYLIGHT 15 | DTSTART:16010101T020000 16 | TZOFFSETFROM:+0100 17 | TZOFFSETTO:+0200 18 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 19 | END:DAYLIGHT 20 | END:VTIMEZONE 21 | BEGIN:VEVENT 22 | RRULE:FREQ=DAILY;UNTIL=20221110T230000Z;INTERVAL=2 23 | UID:040000008200E00074C5B7101A82E0080000000072B908E2FFD6D701000000000000000 24 | 010000000F1F9FF2029BC6F438B081675570BC889 25 | SUMMARY:Free 26 | DTSTART;VALUE=DATE:20211111 27 | DTEND;VALUE=DATE:20211112 28 | CLASS:PUBLIC 29 | PRIORITY:5 30 | DTSTAMP:20211111T132808Z 31 | TRANSP:TRANSPARENT 32 | STATUS:CONFIRMED 33 | SEQUENCE:0 34 | X-MICROSOFT-CDO-APPT-SEQUENCE:0 35 | X-MICROSOFT-CDO-BUSYSTATUS:FREE 36 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 37 | X-MICROSOFT-CDO-ALLDAYEVENT:TRUE 38 | X-MICROSOFT-CDO-IMPORTANCE:1 39 | X-MICROSOFT-CDO-INSTTYPE:1 40 | X-MICROSOFT-DONOTFORWARDMEETING:FALSE 41 | X-MICROSOFT-DISALLOW-COUNTER:FALSE 42 | END:VEVENT 43 | END:VCALENDAR 44 | -------------------------------------------------------------------------------- /test/test_data/response.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | METHOD:REPLY 3 | PRODID:Microsoft Exchange Server 2010 4 | VERSION:2.0 5 | BEGIN:VTIMEZONE 6 | TZID:Europe/Zurich 7 | BEGIN:STANDARD 8 | DTSTART:20211031T030000 9 | TZOFFSETFROM:+0200 10 | TZOFFSETTO:+0100 11 | END:STANDARD 12 | BEGIN:DAYLIGHT 13 | DTSTART:20210328T020000 14 | TZOFFSETFROM:+0100 15 | TZOFFSETTO:+0200 16 | END:DAYLIGHT 17 | END:VTIMEZONE 18 | BEGIN:VEVENT 19 | ATTENDEE;PARTSTAT=DECLINED;CN="Eigenmann, Martin":mailto:calendar@gmail.com 20 | COMMENT;LANGUAGE=en-US:test-message-from-the-calendar\n 21 | UID:ecc5e718-08eb-4247-a871-d7e09a3ffbbc@localhost 22 | SUMMARY;LANGUAGE=en-US:Declined: *[T] Beratung - Martin Eigenmann 23 | DTSTART;TZID=Europe/Zurich:20210607T133000 24 | DTEND;TZID=Europe/Zurich:20210607T143000 25 | CLASS:PUBLIC 26 | PRIORITY:5 27 | DTSTAMP:20210606T191709Z 28 | TRANSP:OPAQUE 29 | STATUS:CONFIRMED 30 | SEQUENCE:1 31 | LOCATION;LANGUAGE=en-US:Talstrasse 4\, 9000 St.Gallen\, CH 32 | X-MICROSOFT-CDO-APPT-SEQUENCE:1 33 | X-MICROSOFT-CDO-OWNERAPPTID:0 34 | X-MICROSOFT-CDO-BUSYSTATUS:BUSY 35 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 36 | X-MICROSOFT-CDO-ALLDAYEVENT:FALSE 37 | X-MICROSOFT-CDO-IMPORTANCE:1 38 | X-MICROSOFT-CDO-INSTTYPE:0 39 | X-MICROSOFT-DONOTFORWARDMEETING:FALSE 40 | X-MICROSOFT-DISALLOW-COUNTER:FALSE 41 | END:VEVENT 42 | END:VCALENDAR 43 | -------------------------------------------------------------------------------- /test/test_data/cest_every_day_for_one_year.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | METHOD:PUBLISH 3 | PRODID:Microsoft Exchange Server 2010 4 | VERSION:2.0 5 | X-WR-CALNAME:test 6 | BEGIN:VTIMEZONE 7 | TZID:W. Europe Standard Time 8 | BEGIN:STANDARD 9 | DTSTART:16010101T030000 10 | TZOFFSETFROM:+0200 11 | TZOFFSETTO:+0100 12 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 13 | END:STANDARD 14 | BEGIN:DAYLIGHT 15 | DTSTART:16010101T020000 16 | TZOFFSETFROM:+0100 17 | TZOFFSETTO:+0200 18 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 19 | END:DAYLIGHT 20 | END:VTIMEZONE 21 | BEGIN:VEVENT 22 | RRULE:FREQ=DAILY;UNTIL=20221111T093000Z;INTERVAL=1 23 | UID:040000008200E00074C5B7101A82E00800000000B47AD3E5FED6D701000000000000000 24 | 01000000025C7E472027AAB44AD1F2F048390CB09 25 | SUMMARY:Busy 26 | DTSTART;TZID=W. Europe Standard Time:20211111T103000 27 | DTEND;TZID=W. Europe Standard Time:20211111T110000 28 | CLASS:PUBLIC 29 | PRIORITY:5 30 | DTSTAMP:20211111T132438Z 31 | TRANSP:OPAQUE 32 | STATUS:CONFIRMED 33 | SEQUENCE:0 34 | X-MICROSOFT-CDO-APPT-SEQUENCE:0 35 | X-MICROSOFT-CDO-BUSYSTATUS:BUSY 36 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 37 | X-MICROSOFT-CDO-ALLDAYEVENT:FALSE 38 | X-MICROSOFT-CDO-IMPORTANCE:1 39 | X-MICROSOFT-CDO-INSTTYPE:1 40 | X-MICROSOFT-DONOTFORWARDMEETING:FALSE 41 | X-MICROSOFT-DISALLOW-COUNTER:FALSE 42 | END:VEVENT 43 | END:VCALENDAR 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iCalEvents 2 | 3 | Simple Python 3 library to download, parse and query iCal sources. 4 | 5 | [![PyPI version](https://badge.fury.io/py/icalevents.svg)](https://badge.fury.io/py/icalevents)[![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/) 6 | 7 | ## Build info 8 | 9 | last push: [![Run pytest](https://github.com/jazzband/icalevents/actions/workflows/tests.yml/badge.svg)](https://github.com/jazzband/icalevents/actions/workflows/tests.yml) 10 | 11 | master: [![Run pytest](https://github.com/jazzband/icalevents/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/jazzband/icalevents/actions/workflows/tests.yml?query=branch%3Amaster++) 12 | 13 | ## Documentation 14 | 15 | https://icalevents.readthedocs.io/en/latest/ 16 | 17 | ## Usage 18 | 19 | ### iCloud: 20 | 21 | ```python 22 | 23 | from icalevents.icalevents import events 24 | 25 | es = events(, fix_apple=True) 26 | ``` 27 | 28 | ### Google: 29 | 30 | ```python 31 | 32 | from icalevents.icalevents import events 33 | 34 | es = events() 35 | ``` 36 | 37 | # Contributing 38 | 39 | You will need [poetry](https://github.com/python-poetry/poetry) and [pre-commit](https://pre-commit.com/index.html) installed and than run. 40 | 41 | ```bash 42 | pre-commit install 43 | ``` 44 | 45 | Happy contributing! 46 | -------------------------------------------------------------------------------- /test/test_data/per_event_timezone.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | METHOD:PUBLISH 6 | X-WR-CALNAME:test 7 | X-WR-TIMEZONE:Europe/Zurich 8 | BEGIN:VTIMEZONE 9 | TZID:Europe/Zurich 10 | X-LIC-LOCATION:Europe/Zurich 11 | BEGIN:DAYLIGHT 12 | TZOFFSETFROM:+0100 13 | TZOFFSETTO:+0200 14 | TZNAME:GMT+2 15 | DTSTART:19700329T020000 16 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 17 | END:DAYLIGHT 18 | BEGIN:STANDARD 19 | TZOFFSETFROM:+0200 20 | TZOFFSETTO:+0100 21 | TZNAME:GMT+1 22 | DTSTART:19701025T030000 23 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 24 | END:STANDARD 25 | END:VTIMEZONE 26 | BEGIN:VEVENT 27 | DTSTART;TZID=Europe/Zurich:20240325T070000 28 | DTEND;TZID=Europe/Zurich:20240325T080000 29 | DTSTAMP:20240908T134310Z 30 | UID:21u17g5sbq791pn23imf7ue0lc@google.com 31 | CREATED:20240908T134239Z 32 | LAST-MODIFIED:20240908T134239Z 33 | SEQUENCE:0 34 | STATUS:CONFIRMED 35 | SUMMARY:Zürich 36 | TRANSP:OPAQUE 37 | END:VEVENT 38 | BEGIN:VEVENT 39 | DTSTART;TZID=Australia/Perth:20240325T070000 40 | DTEND;TZID=Australia/Perth:20240325T080000 41 | DTSTAMP:20240908T134310Z 42 | UID:21u17g5sbq791pn23imf7ue0ld@google.com 43 | CREATED:20240908T134239Z 44 | LAST-MODIFIED:20240908T134239Z 45 | SEQUENCE:0 46 | STATUS:CONFIRMED 47 | SUMMARY:Perth 48 | TRANSP:OPAQUE 49 | END:VEVENT 50 | END:VCALENDAR 51 | -------------------------------------------------------------------------------- /test/test_data/multi_attendee_response.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | METHOD:REPLY 3 | PRODID:Microsoft Exchange Server 2010 4 | VERSION:2.0 5 | BEGIN:VTIMEZONE 6 | TZID:Europe/Zurich 7 | BEGIN:STANDARD 8 | DTSTART:20211031T030000 9 | TZOFFSETFROM:+0200 10 | TZOFFSETTO:+0100 11 | END:STANDARD 12 | BEGIN:DAYLIGHT 13 | DTSTART:20210328T020000 14 | TZOFFSETFROM:+0100 15 | TZOFFSETTO:+0200 16 | END:DAYLIGHT 17 | END:VTIMEZONE 18 | BEGIN:VEVENT 19 | ATTENDEE;PARTSTAT=DECLINED;CN="Eigenmann, Martin":mailto:calendar@gmail.com 20 | ATTENDEE;CN="Eigenmann, Isabel":mailto:calendar@microsoft.com 21 | COMMENT;LANGUAGE=en-US:test-message-from-the-calendar\n 22 | UID:ecc5e718-08eb-4247-a871-d7e09a3ffbbc@localhost 23 | SUMMARY;LANGUAGE=en-US:Declined: *[T] Beratung - Martin Eigenmann 24 | DTSTART;TZID=Europe/Zurich:20210607T133000 25 | DTEND;TZID=Europe/Zurich:20210607T143000 26 | CLASS:PUBLIC 27 | PRIORITY:5 28 | DTSTAMP:20210606T191709Z 29 | TRANSP:OPAQUE 30 | STATUS:CONFIRMED 31 | SEQUENCE:1 32 | LOCATION;LANGUAGE=en-US:Talstrasse 4\, 9000 St.Gallen\, CH 33 | X-MICROSOFT-CDO-APPT-SEQUENCE:1 34 | X-MICROSOFT-CDO-OWNERAPPTID:0 35 | X-MICROSOFT-CDO-BUSYSTATUS:BUSY 36 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 37 | X-MICROSOFT-CDO-ALLDAYEVENT:FALSE 38 | X-MICROSOFT-CDO-IMPORTANCE:1 39 | X-MICROSOFT-CDO-INSTTYPE:0 40 | X-MICROSOFT-DONOTFORWARDMEETING:FALSE 41 | X-MICROSOFT-DISALLOW-COUNTER:FALSE 42 | END:VEVENT 43 | END:VCALENDAR 44 | -------------------------------------------------------------------------------- /test/test_data/cest_with_deleted.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | METHOD:PUBLISH 3 | PRODID:Microsoft Exchange Server 2010 4 | VERSION:2.0 5 | X-WR-CALNAME:test3 6 | BEGIN:VTIMEZONE 7 | TZID:W. Europe Standard Time 8 | BEGIN:STANDARD 9 | DTSTART:16010101T030000 10 | TZOFFSETFROM:+0200 11 | TZOFFSETTO:+0100 12 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 13 | END:STANDARD 14 | BEGIN:DAYLIGHT 15 | DTSTART:16010101T020000 16 | TZOFFSETFROM:+0100 17 | TZOFFSETTO:+0200 18 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 19 | END:DAYLIGHT 20 | END:VTIMEZONE 21 | BEGIN:VEVENT 22 | RRULE:FREQ=DAILY;UNTIL=20211115T070000Z;INTERVAL=1 23 | EXDATE;TZID=W. Europe Standard Time:20211112T080000,20211114T080000 24 | UID:040000008200E00074C5B7101A82E0080000000008EA7A7A00D7D701000000000000000 25 | 010000000943ACF1BD16B5145AAD2954EF56FF236 26 | SUMMARY:Busy 27 | DTSTART;TZID=W. Europe Standard Time:20211111T080000 28 | DTEND;TZID=W. Europe Standard Time:20211111T083000 29 | CLASS:PUBLIC 30 | PRIORITY:5 31 | DTSTAMP:20211111T133243Z 32 | TRANSP:OPAQUE 33 | STATUS:CONFIRMED 34 | SEQUENCE:0 35 | X-MICROSOFT-CDO-APPT-SEQUENCE:0 36 | X-MICROSOFT-CDO-BUSYSTATUS:BUSY 37 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 38 | X-MICROSOFT-CDO-ALLDAYEVENT:FALSE 39 | X-MICROSOFT-CDO-IMPORTANCE:1 40 | X-MICROSOFT-CDO-INSTTYPE:1 41 | X-MICROSOFT-DONOTFORWARDMEETING:FALSE 42 | X-MICROSOFT-DISALLOW-COUNTER:FALSE 43 | END:VEVENT 44 | END:VCALENDAR 45 | -------------------------------------------------------------------------------- /test/test_data/recurrenceid_google.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | METHOD:PUBLISH 6 | X-WR-CALNAME:test 7 | X-WR-TIMEZONE:Europe/Zurich 8 | BEGIN:VEVENT 9 | DTSTART:20210327T110000Z 10 | DTEND:20210327T120000Z 11 | DTSTAMP:20210320T113243Z 12 | UID:5kml9evtgr3lffqh2lc2lu86kp@google.com 13 | ATTENDEE;X-NUM-GUESTS=0:mailto:hst9ma4s2h5hf2iafmrqffki9o@group.calendar.go 14 | ogle.com 15 | RECURRENCE-ID:20210327T110000Z 16 | SUMMARY:Busy 17 | END:VEVENT 18 | BEGIN:VEVENT 19 | DTSTART:20210326T110000Z 20 | DTEND:20210326T120000Z 21 | DTSTAMP:20210320T113243Z 22 | UID:5kml9evtgr3lffqh2lc2lu86kp@google.com 23 | ATTENDEE;X-NUM-GUESTS=0:mailto:hst9ma4s2h5hf2iafmrqffki9o@group.calendar.go 24 | ogle.com 25 | RECURRENCE-ID:20210326T110000Z 26 | SUMMARY:Busy 27 | END:VEVENT 28 | BEGIN:VEVENT 29 | DTSTART:20210325T110000Z 30 | DTEND:20210325T120000Z 31 | DTSTAMP:20210320T113243Z 32 | UID:5kml9evtgr3lffqh2lc2lu86kp@google.com 33 | ATTENDEE;X-NUM-GUESTS=0:mailto:hst9ma4s2h5hf2iafmrqffki9o@group.calendar.go 34 | ogle.com 35 | RECURRENCE-ID:20210325T110000Z 36 | SUMMARY:Busy 37 | END:VEVENT 38 | BEGIN:VEVENT 39 | DTSTART:20210324T110000Z 40 | DTEND:20210324T120000Z 41 | DTSTAMP:20210320T113243Z 42 | UID:5kml9evtgr3lffqh2lc2lu86kp@google.com 43 | ATTENDEE;X-NUM-GUESTS=0:mailto:hst9ma4s2h5hf2iafmrqffki9o@group.calendar.go 44 | ogle.com 45 | RECURRENCE-ID:20210324T110000Z 46 | SUMMARY:Busy 47 | END:VEVENT 48 | END:VCALENDAR 49 | -------------------------------------------------------------------------------- /test/test_data/recurrence_tz.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | METHOD:PUBLISH 6 | X-WR-CALNAME:email@example.com 7 | X-WR-TIMEZONE:Australia/Sydney 8 | BEGIN:VTIMEZONE 9 | TZID:Australia/Sydney 10 | X-LIC-LOCATION:Australia/Sydney 11 | BEGIN:STANDARD 12 | TZOFFSETFROM:+1100 13 | TZOFFSETTO:+1000 14 | TZNAME:AEST 15 | DTSTART:19700405T030000 16 | RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU 17 | END:STANDARD 18 | BEGIN:DAYLIGHT 19 | TZOFFSETFROM:+1000 20 | TZOFFSETTO:+1100 21 | TZNAME:AEDT 22 | DTSTART:19701004T020000 23 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU 24 | END:DAYLIGHT 25 | END:VTIMEZONE 26 | BEGIN:VTIMEZONE 27 | TZID:Australia/Melbourne 28 | X-LIC-LOCATION:Australia/Melbourne 29 | BEGIN:STANDARD 30 | TZOFFSETFROM:+1100 31 | TZOFFSETTO:+1000 32 | TZNAME:AEST 33 | DTSTART:19700405T030000 34 | RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU 35 | END:STANDARD 36 | BEGIN:DAYLIGHT 37 | TZOFFSETFROM:+1000 38 | TZOFFSETTO:+1100 39 | TZNAME:AEDT 40 | DTSTART:19701004T020000 41 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU 42 | END:DAYLIGHT 43 | END:VTIMEZONE 44 | BEGIN:VEVENT 45 | DTSTART;TZID=Australia/Sydney:20211017T090000 46 | DTEND;TZID=Australia/Sydney:20211017T095000 47 | RRULE:FREQ=WEEKLY;BYDAY=SU 48 | DTSTAMP:20211025T060625Z 49 | UID:random_string@google.com 50 | CLASS:PRIVATE 51 | CREATED:20211020T015841Z 52 | DESCRIPTION: 53 | LAST-MODIFIED:20211020T015856Z 54 | LOCATION: 55 | SEQUENCE:1 56 | STATUS:CONFIRMED 57 | SUMMARY:Event Name 58 | TRANSP:TRANSPARENT 59 | END:VEVENT 60 | END:VCALENDAR 61 | -------------------------------------------------------------------------------- /test/test_data/multi_exdate_same_line_ms.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | METHOD:PUBLISH 3 | PRODID:Microsoft Exchange Server 2010 4 | VERSION:2.0 5 | X-WR-CALNAME:Calendar 6 | BEGIN:VTIMEZONE 7 | TZID:Eastern Standard Time 8 | BEGIN:STANDARD 9 | DTSTART:16010101T020000 10 | TZOFFSETFROM:-0400 11 | TZOFFSETTO:-0500 12 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11 13 | END:STANDARD 14 | BEGIN:DAYLIGHT 15 | DTSTART:16010101T020000 16 | TZOFFSETFROM:-0500 17 | TZOFFSETTO:-0400 18 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3 19 | END:DAYLIGHT 20 | END:VTIMEZONE 21 | BEGIN:VEVENT 22 | DESCRIPTION:Test event that has an RRULE with multiple EXDATE 23 | RRULE:FREQ=WEEKLY;UNTIL=20220429T150000Z;INTERVAL=1;BYDAY=FR;WKST=MO 24 | EXDATE;TZID=Eastern Standard Time:20220318T110000,20220401T110000,20220408T 25 | 110000 26 | UID:040000008200R00074P5O7101N82R00800000000R0Q793689428Q801000000000000000 27 | 010000000QOPN4SS024R0264S9P0Q7OOQQ16PN399 28 | SUMMARY:Recurring With Exclusions 29 | DTSTART;TZID=Eastern Standard Time:20220311T110000 30 | DTEND;TZID=Eastern Standard Time:20220311T113000 31 | CLASS:PUBLIC 32 | PRIORITY:5 33 | DTSTAMP:20220330T125447Z 34 | TRANSP:OPAQUE 35 | STATUS:CONFIRMED 36 | SEQUENCE:0 37 | LOCATION:Microsoft Teams Meeting 38 | X-MICROSOFT-CDO-APPT-SEQUENCE:0 39 | X-MICROSOFT-CDO-BUSYSTATUS:BUSY 40 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 41 | X-MICROSOFT-CDO-ALLDAYEVENT:FALSE 42 | X-MICROSOFT-CDO-IMPORTANCE:1 43 | X-MICROSOFT-CDO-INSTTYPE:1 44 | X-MICROSOFT-DONOTFORWARDMEETING:FALSE 45 | X-MICROSOFT-DISALLOW-COUNTER:FALSE 46 | END:VEVENT 47 | END:VCALENDAR 48 | -------------------------------------------------------------------------------- /test/test_data/non_floating.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | CALSCALE:GREGORIAN 4 | PRODID:-//SabreDAV//SabreDAV//EN 5 | X-WR-CALNAME:Personal (Admin) 6 | REFRESH-INTERVAL;VALUE=DURATION:PT4H 7 | X-PUBLISHED-TTL:PT4H 8 | BEGIN:VTIMEZONE 9 | TZID:Europe/Zurich 10 | BEGIN:DAYLIGHT 11 | TZOFFSETFROM:+0100 12 | TZOFFSETTO:+0200 13 | TZNAME:CEST 14 | DTSTART:19700329T020000 15 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 16 | END:DAYLIGHT 17 | BEGIN:STANDARD 18 | TZOFFSETFROM:+0200 19 | TZOFFSETTO:+0100 20 | TZNAME:CET 21 | DTSTART:19701025T030000 22 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 23 | END:STANDARD 24 | END:VTIMEZONE 25 | BEGIN:VEVENT 26 | LAST-MODIFIED:20210408T060912Z 27 | DTSTAMP:20210408T060912Z 28 | UID:0370d094-b59a-4bb5-b333-84e2dab1329a 29 | SUMMARY:Mobility reservation (Economy 11307) 30 | X-MOZ-LASTACK:20210408T060912Z 31 | DTSTART;TZID=Europe/Zurich:20210408T083000 32 | DTEND;TZID=Europe/Zurich:20210408T160000 33 | X-MOZ-GENERATION:3 34 | BEGIN:VALARM 35 | ACTION:DISPLAY 36 | TRIGGER;VALUE=DURATION:-PT1H 37 | DESCRIPTION:Mobility reservation (Economy 11307) 38 | X-LIC-ERROR;X-LIC-ERRORTYPE=PARAMETER-VALUE-PARSE-ERROR:Got a VALUE paramet 39 | er with an illegal type for property: VALUE=DURATION 40 | END:VALARM 41 | END:VEVENT 42 | BEGIN:VEVENT 43 | DTSTAMP:20211006T083610Z 44 | UID:1baf29fb-7a33-4957-9b4f-cd58944b46a2 45 | SUMMARY:Bern 46 | DTSTART;VALUE=DATE:20211013 47 | DTEND;VALUE=DATE:20211014 48 | STATUS:CONFIRMED 49 | TRANSP:TRANSPARENT 50 | BEGIN:VALARM 51 | TRIGGER:-PT1H 52 | ACTION:DISPLAY 53 | DESCRIPTION:Bern 54 | END:VALARM 55 | END:VEVENT 56 | END:VCALENDAR 57 | -------------------------------------------------------------------------------- /test/test_data/google_2024.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | METHOD:PUBLISH 6 | X-WR-CALNAME:test 7 | X-WR-TIMEZONE:Europe/Zurich 8 | BEGIN:VTIMEZONE 9 | TZID:Europe/Zurich 10 | X-LIC-LOCATION:Europe/Zurich 11 | BEGIN:DAYLIGHT 12 | TZOFFSETFROM:+0100 13 | TZOFFSETTO:+0200 14 | TZNAME:GMT+2 15 | DTSTART:19700329T020000 16 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 17 | END:DAYLIGHT 18 | BEGIN:STANDARD 19 | TZOFFSETFROM:+0200 20 | TZOFFSETTO:+0100 21 | TZNAME:GMT+1 22 | DTSTART:19701025T030000 23 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 24 | END:STANDARD 25 | END:VTIMEZONE 26 | BEGIN:VEVENT 27 | DTSTART;TZID=Europe/Zurich:20240325T070000 28 | DTEND;TZID=Europe/Zurich:20240325T080000 29 | RRULE:FREQ=DAILY;COUNT=30 30 | DTSTAMP:20240908T134310Z 31 | UID:21u17g5sbq791pn23imf7ue0lc@google.com 32 | CREATED:20240908T134239Z 33 | LAST-MODIFIED:20240908T134239Z 34 | SEQUENCE:0 35 | STATUS:CONFIRMED 36 | SUMMARY:07:00-08:00-daily-30x 37 | TRANSP:OPAQUE 38 | END:VEVENT 39 | BEGIN:VEVENT 40 | DTSTART;VALUE=DATE:20240325 41 | DTEND;VALUE=DATE:20240326 42 | DTSTAMP:20240908T134310Z 43 | UID:5nubm2ap0oqrblfsqmhco6l8ml@google.com 44 | CREATED:20240908T134252Z 45 | LAST-MODIFIED:20240908T134252Z 46 | SEQUENCE:0 47 | STATUS:CONFIRMED 48 | SUMMARY:all day 49 | TRANSP:TRANSPARENT 50 | END:VEVENT 51 | BEGIN:VEVENT 52 | DTSTART;VALUE=DATE:20240327 53 | DTEND;VALUE=DATE:20240329 54 | DTSTAMP:20240908T134310Z 55 | UID:2muuhekl2jpfeloqbgq3pm9pf9@google.com 56 | CREATED:20240908T134300Z 57 | LAST-MODIFIED:20240908T134300Z 58 | SEQUENCE:0 59 | STATUS:CONFIRMED 60 | SUMMARY:all 2 days 61 | TRANSP:TRANSPARENT 62 | END:VEVENT 63 | END:VCALENDAR 64 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run pytest 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 18 | # When changing this matrix, you have to change the "after_n_builds" parameter 19 | # in the .github/codecov.yml file. It must match the number of builds being 20 | # started considering the matrix. See the following links for more information: 21 | # https://docs.codecov.com/docs/notifications#preventing-notifications-until-after-n-builds 22 | # https://docs.codecov.com/docs/pull-request-comments#after_n_builds 23 | icalendar-version: 24 | - "5" # means (>=5.0.0,<6.0.0) 25 | - "6" # means (>=6.0.0,<7.0.0) 26 | steps: 27 | - name: Set up Python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | 32 | - name: "Checkout repository" 33 | uses: actions/checkout@v4 34 | 35 | - name: "Install poetry" 36 | uses: abatilo/actions-poetry@v3 37 | with: 38 | poetry-version: "2.1.2" 39 | 40 | - name: "Install icalendar ${{ matrix.icalendar-version }} and other dependencies" 41 | run: | 42 | poetry add icalendar~=${{ matrix.icalendar-version }}.0 --no-interaction 43 | 44 | - name: "Test with pytest" 45 | run: | 46 | poetry run coverage run test.py 47 | poetry run coverage xml 48 | 49 | - name: "Upload coverage to Codecov" 50 | uses: codecov/codecov-action@v5 51 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from setuptools import setup 3 | 4 | version = "0.3.1" 5 | 6 | setup( 7 | name="icalevents", 8 | packages=["icalevents"], 9 | install_requires=[ 10 | "urllib3", 11 | "icalendar", 12 | "pytz", 13 | "datetime", 14 | ], 15 | version=version, 16 | description="iCal downloader and parser", 17 | author="Martin Eigenmann", 18 | author_email="", 19 | url="https://github.com/jazzband/icalevents", 20 | download_url="https://github.com/jazzband/icalevents/archive/v" 21 | + version 22 | + ".tar.gz", 23 | keywords=["iCal"], 24 | classifiers=[ 25 | "Programming Language :: Python", 26 | "Programming Language :: Python :: 3", 27 | "Development Status :: 4 - Beta", 28 | "Environment :: Other Environment", 29 | "Intended Audience :: Developers", 30 | "License :: OSI Approved :: MIT License", 31 | "Programming Language :: Python :: 3", 32 | "Programming Language :: Python :: 3.9", 33 | "Programming Language :: Python :: 3.10", 34 | "Programming Language :: Python :: 3.11", 35 | "Programming Language :: Python :: 3.12", 36 | "Programming Language :: Python :: 3.13", 37 | "Operating System :: OS Independent", 38 | "Topic :: Software Development :: Libraries :: Python Modules", 39 | ], 40 | long_description="""\ 41 | iCal download, parse and query tool 42 | ------------------------------------- 43 | 44 | Supports downloading of iCal URL or loading of iCal files, parsing the content and finding events occurring in a given 45 | time range. 46 | 47 | See: icalevents.icalevents.events(url=None, file=None, start=None, end=None, fix_apple=False) 48 | 49 | This version requires Python 3 or later. 50 | 51 | """, 52 | ) 53 | -------------------------------------------------------------------------------- /test/test_data/created_last_modified.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | METHOD:PUBLISH 6 | BEGIN:VTIMEZONE 7 | TZID:Europe/Berlin 8 | BEGIN:DAYLIGHT 9 | TZOFFSETFROM:+0100 10 | TZOFFSETTO:+0200 11 | TZNAME:CEST 12 | DTSTART:19700329T020000 13 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 14 | END:DAYLIGHT 15 | BEGIN:STANDARD 16 | TZOFFSETFROM:+0200 17 | TZOFFSETTO:+0100 18 | TZNAME:CET 19 | DTSTART:19701025T030000 20 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 21 | END:STANDARD 22 | END:VTIMEZONE 23 | X-WR-CALNAME:Müll 24 | X-WR-TIMEZONE:Europe/Berlin 25 | X-WR-CALDESC:Müllabholung Treuchtlingen Luitpoldstraße 26 | BEGIN:VEVENT 27 | DTSTART;VALUE=DATE:20170712 28 | DTEND;VALUE=DATE:20170713 29 | DTSTAMP:20170711T171222Z 30 | UID:0eedefedba891fcbb49dcfa4279d9d93 31 | CREATED:20170103T080401 32 | DESCRIPTION:graue Restmülltonne nicht vergessen! 33 | LAST-MODIFIED:20170711T160050 34 | LOCATION:Luitpoldstraße\, Treuchtlingen 35 | SEQUENCE:0 36 | STATUS:CONFIRMED 37 | SUMMARY:graue Restmülltonne 38 | TRANSP:TRANSPARENT 39 | END:VEVENT 40 | BEGIN:VEVENT 41 | DTSTART;VALUE=DATE:20170713 42 | DTEND;VALUE=DATE:20170714 43 | DTSTAMP:20170711T171222Z 44 | UID:0eedefedba891fcbb49dcfa4279d9d94 45 | CREATED:20170104T080401Z 46 | DESCRIPTION:graue Restmülltonne nicht vergessen! 47 | LOCATION:Luitpoldstraße\, Treuchtlingen 48 | SEQUENCE:0 49 | STATUS:CONFIRMED 50 | SUMMARY:graue Restmülltonne 51 | TRANSP:TRANSPARENT 52 | END:VEVENT 53 | BEGIN:VEVENT 54 | DTSTART;VALUE=DATE:20170714 55 | DTEND;VALUE=DATE:20170715 56 | DTSTAMP:20170711T171222Z 57 | UID:0eedefedba891fcbb49dcfa4279d9d95 58 | DESCRIPTION:graue Restmülltonne nicht vergessen! 59 | LOCATION:Luitpoldstraße\, Treuchtlingen 60 | SEQUENCE:0 61 | STATUS:CONFIRMED 62 | SUMMARY:graue Restmülltonne 63 | TRANSP:TRANSPARENT 64 | END:VEVENT 65 | END:VCALENDAR 66 | -------------------------------------------------------------------------------- /test/test_data/transparent.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | METHOD:PUBLISH 3 | PRODID:Microsoft Exchange Server 2010 4 | VERSION:2.0 5 | X-WR-CALNAME:Kalender 6 | BEGIN:VTIMEZONE 7 | TZID:Central European Standard Time 8 | BEGIN:STANDARD 9 | DTSTART:16010101T030000 10 | TZOFFSETFROM:+0200 11 | TZOFFSETTO:+0100 12 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 13 | END:STANDARD 14 | BEGIN:DAYLIGHT 15 | DTSTART:16010101T020000 16 | TZOFFSETFROM:+0100 17 | TZOFFSETTO:+0200 18 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 19 | END:DAYLIGHT 20 | END:VTIMEZONE 21 | BEGIN:VEVENT 22 | UID:040000008200E00074C5B7101A82E008000000007BFBACB1614AD701000000000000000 23 | 0100000004A699A72684F7646A814017A9F145FE8 24 | SUMMARY:Free 25 | DTSTART;VALUE=DATE:20210520 26 | DTEND;VALUE=DATE:20210521 27 | CLASS:PUBLIC 28 | PRIORITY:5 29 | DTSTAMP:20210516T144840Z 30 | TRANSP:TRANSPARENT 31 | STATUS:CONFIRMED 32 | SEQUENCE:0 33 | X-MICROSOFT-CDO-APPT-SEQUENCE:0 34 | X-MICROSOFT-CDO-BUSYSTATUS:FREE 35 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 36 | X-MICROSOFT-CDO-ALLDAYEVENT:TRUE 37 | X-MICROSOFT-CDO-IMPORTANCE:1 38 | X-MICROSOFT-CDO-INSTTYPE:1 39 | X-MICROSOFT-DONOTFORWARDMEETING:FALSE 40 | X-MICROSOFT-DISALLOW-COUNTER:FALSE 41 | END:VEVENT 42 | BEGIN:VEVENT 43 | UID:040000008200E00074C5B7101A82E00800000000F7B85AB6614AD701000000000000000 44 | 01000000005576067F6247C45A572F9B9E56ABB32 45 | SUMMARY:Busy 46 | DTSTART;TZID=Central European Standard Time:20210521T070000 47 | DTEND;TZID=Central European Standard Time:20210521T223000 48 | CLASS:PUBLIC 49 | PRIORITY:5 50 | DTSTAMP:20210516T144840Z 51 | TRANSP:OPAQUE 52 | STATUS:CONFIRMED 53 | SEQUENCE:0 54 | X-MICROSOFT-CDO-APPT-SEQUENCE:0 55 | X-MICROSOFT-CDO-BUSYSTATUS:BUSY 56 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 57 | X-MICROSOFT-CDO-ALLDAYEVENT:FALSE 58 | X-MICROSOFT-CDO-IMPORTANCE:1 59 | X-MICROSOFT-CDO-INSTTYPE:0 60 | X-MICROSOFT-DONOTFORWARDMEETING:FALSE 61 | X-MICROSOFT-DISALLOW-COUNTER:FALSE 62 | END:VEVENT 63 | END:VCALENDAR 64 | -------------------------------------------------------------------------------- /test/test_data/recurrenceid_ms.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | METHOD:PUBLISH 3 | PRODID:Microsoft Exchange Server 2010 4 | VERSION:2.0 5 | X-WR-CALNAME:Calendar 6 | BEGIN:VTIMEZONE 7 | TZID:W. Europe Standard Time 8 | BEGIN:STANDARD 9 | DTSTART:16010101T030000 10 | TZOFFSETFROM:+0200 11 | TZOFFSETTO:+0100 12 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 13 | END:STANDARD 14 | BEGIN:DAYLIGHT 15 | DTSTART:16010101T020000 16 | TZOFFSETFROM:+0100 17 | TZOFFSETTO:+0200 18 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 19 | END:DAYLIGHT 20 | END:VTIMEZONE 21 | BEGIN:VEVENT 22 | RRULE:FREQ=WEEKLY;UNTIL=20210701T220000Z;INTERVAL=1;BYDAY=WE,FR;WKST=SU 23 | UID:040000008200E00074C5B7101A82E0080000000062FD14411601D701000000000000000 24 | 01000000052EAF66D5C3BC6458198F47BAE50996F 25 | SUMMARY:Away 26 | DTSTART;VALUE=DATE:20210210 27 | DTEND;VALUE=DATE:20210211 28 | CLASS:PUBLIC 29 | PRIORITY:5 30 | DTSTAMP:20210329T195704Z 31 | TRANSP:OPAQUE 32 | STATUS:CONFIRMED 33 | SEQUENCE:0 34 | X-MICROSOFT-CDO-APPT-SEQUENCE:0 35 | X-MICROSOFT-CDO-BUSYSTATUS:OOF 36 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 37 | X-MICROSOFT-CDO-ALLDAYEVENT:TRUE 38 | X-MICROSOFT-CDO-IMPORTANCE:1 39 | X-MICROSOFT-CDO-INSTTYPE:1 40 | X-MICROSOFT-DONOTFORWARDMEETING:FALSE 41 | X-MICROSOFT-DISALLOW-COUNTER:FALSE 42 | END:VEVENT 43 | BEGIN:VEVENT 44 | UID:040000008200E00074C5B7101A82E0080000000062FD14411601D701000000000000000 45 | 01000000052EAF66D5C3BC6458198F47BAE50996F 46 | RECURRENCE-ID;TZID=W. Europe Standard Time:20210409T000000 47 | SUMMARY:Away 48 | DTSTART;VALUE=DATE:20210410 49 | DTEND;VALUE=DATE:20210411 50 | CLASS:PUBLIC 51 | PRIORITY:5 52 | DTSTAMP:20210329T195704Z 53 | TRANSP:OPAQUE 54 | STATUS:CONFIRMED 55 | SEQUENCE:0 56 | X-MICROSOFT-CDO-APPT-SEQUENCE:0 57 | X-MICROSOFT-CDO-BUSYSTATUS:OOF 58 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 59 | X-MICROSOFT-CDO-ALLDAYEVENT:TRUE 60 | X-MICROSOFT-CDO-IMPORTANCE:1 61 | X-MICROSOFT-CDO-INSTTYPE:3 62 | X-MICROSOFT-DONOTFORWARDMEETING:FALSE 63 | X-MICROSOFT-DISALLOW-COUNTER:FALSE 64 | END:VEVENT 65 | END:VCALENDAR 66 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath("..")) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = "iCalEvents" 22 | copyright = "2024, jazzband" 23 | author = "Martin Eigenmann" 24 | 25 | # The full version, including alpha/beta/rc tags 26 | release = "0.3.1" 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = ["sphinx.ext.autodoc"] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ["_templates"] 38 | 39 | # List of patterns, relative to source directory, that match files and 40 | # directories to ignore when looking for source files. 41 | # This pattern also affects html_static_path and html_extra_path. 42 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 43 | 44 | 45 | # -- Options for HTML output ------------------------------------------------- 46 | 47 | # The theme to use for HTML and HTML Help pages. See the documentation for 48 | # a list of builtin themes. 49 | # 50 | html_theme = "alabaster" 51 | 52 | html_theme_options = { 53 | # Disable showing the sidebar. Defaults to 'false' 54 | "nosidebar": True, 55 | } 56 | 57 | # Add any paths that contain custom static files (such as style sheets) here, 58 | # relative to this directory. They are copied after the builtin static files, 59 | # so a file named "default.css" will overwrite the builtin "default.css". 60 | html_static_path = ["_static"] 61 | -------------------------------------------------------------------------------- /test/test_data/small_time_frame.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | METHOD:PUBLISH 3 | PRODID:Microsoft Exchange Server 2010 4 | VERSION:2.0 5 | X-WR-CALNAME:Calendar 6 | BEGIN:VTIMEZONE 7 | TZID:Pacific Standard Time 8 | BEGIN:STANDARD 9 | DTSTART:16010101T020000 10 | TZOFFSETFROM:-0700 11 | TZOFFSETTO:-0800 12 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11 13 | END:STANDARD 14 | BEGIN:DAYLIGHT 15 | DTSTART:16010101T020000 16 | TZOFFSETFROM:-0800 17 | TZOFFSETTO:-0700 18 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3 19 | END:DAYLIGHT 20 | END:VTIMEZONE 21 | BEGIN:VTIMEZONE 22 | TZID:UTC 23 | BEGIN:STANDARD 24 | DTSTART:16010101T000000 25 | TZOFFSETFROM:+0000 26 | TZOFFSETTO:+0000 27 | END:STANDARD 28 | BEGIN:DAYLIGHT 29 | DTSTART:16010101T000000 30 | TZOFFSETFROM:+0000 31 | TZOFFSETTO:+0000 32 | END:DAYLIGHT 33 | END:VTIMEZONE 34 | BEGIN:VTIMEZONE 35 | TZID:Central Standard Time 36 | BEGIN:STANDARD 37 | DTSTART:16010101T020000 38 | TZOFFSETFROM:-0500 39 | TZOFFSETTO:-0600 40 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11 41 | END:STANDARD 42 | BEGIN:DAYLIGHT 43 | DTSTART:16010101T020000 44 | TZOFFSETFROM:-0600 45 | TZOFFSETTO:-0500 46 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3 47 | END:DAYLIGHT 48 | END:VTIMEZONE 49 | BEGIN:VTIMEZONE 50 | TZID:Eastern Standard Time 51 | BEGIN:STANDARD 52 | DTSTART:16010101T020000 53 | TZOFFSETFROM:-0400 54 | TZOFFSETTO:-0500 55 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11 56 | END:STANDARD 57 | BEGIN:DAYLIGHT 58 | DTSTART:16010101T020000 59 | TZOFFSETFROM:-0500 60 | TZOFFSETTO:-0400 61 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3 62 | END:DAYLIGHT 63 | END:VTIMEZONE 64 | BEGIN:VTIMEZONE 65 | TZID:India Standard Time 66 | BEGIN:STANDARD 67 | DTSTART:16010101T000000 68 | TZOFFSETFROM:+0530 69 | TZOFFSETTO:+0530 70 | END:STANDARD 71 | BEGIN:DAYLIGHT 72 | DTSTART:16010101T000000 73 | TZOFFSETFROM:+0530 74 | TZOFFSETTO:+0530 75 | END:DAYLIGHT 76 | END:VTIMEZONE 77 | BEGIN:VEVENT 78 | UID:0400AAAA8200E00074C5B7101A82E0080000000085E5DC3409AAAA01000000000000000 79 | 010000000AAAADE08C53CAA4283D0AAA1A2AAA97 80 | SUMMARY:TEST Summary 81 | DTSTART;TZID=Pacific Standard Time:20230509T090000 82 | DTEND;TZID=Pacific Standard Time:20230509T100000 83 | CLASS:PUBLIC 84 | PRIORITY:5 85 | DTSTAMP:20230509T014745Z 86 | TRANSP:OPAQUE 87 | STATUS:CONFIRMED 88 | SEQUENCE:0 89 | LOCATION: 90 | END:VEVENT 91 | END:VCALENDAR 92 | -------------------------------------------------------------------------------- /test/test_data/floating.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | CALSCALE:GREGORIAN 4 | PRODID:-//SabreDAV//SabreDAV//EN 5 | X-WR-CALNAME:Personal (Admin) 6 | REFRESH-INTERVAL;VALUE=DURATION:PT4H 7 | X-PUBLISHED-TTL:PT4H 8 | BEGIN:VTIMEZONE 9 | TZID:Europe/Zurich 10 | BEGIN:DAYLIGHT 11 | TZOFFSETFROM:+0100 12 | TZOFFSETTO:+0200 13 | TZNAME:CEST 14 | DTSTART:19700329T020000 15 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 16 | END:DAYLIGHT 17 | BEGIN:STANDARD 18 | TZOFFSETFROM:+0200 19 | TZOFFSETTO:+0100 20 | TZNAME:CET 21 | DTSTART:19701025T030000 22 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 23 | END:STANDARD 24 | END:VTIMEZONE 25 | BEGIN:VTIMEZONE 26 | TZID:Europe/Brussels 27 | BEGIN:DAYLIGHT 28 | TZOFFSETFROM:+0100 29 | TZOFFSETTO:+0200 30 | TZNAME:CEST 31 | DTSTART:19700329T020000 32 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 33 | END:DAYLIGHT 34 | BEGIN:STANDARD 35 | TZOFFSETFROM:+0200 36 | TZOFFSETTO:+0100 37 | TZNAME:CET 38 | DTSTART:19701025T030000 39 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 40 | END:STANDARD 41 | END:VTIMEZONE 42 | BEGIN:VTIMEZONE 43 | TZID:Europe/Kiev 44 | BEGIN:DAYLIGHT 45 | TZOFFSETFROM:+0200 46 | TZOFFSETTO:+0300 47 | TZNAME:EEST 48 | DTSTART:19700329T030000 49 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 50 | END:DAYLIGHT 51 | BEGIN:STANDARD 52 | TZOFFSETFROM:+0300 53 | TZOFFSETTO:+0200 54 | TZNAME:EET 55 | DTSTART:19701025T040000 56 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 57 | END:STANDARD 58 | END:VTIMEZONE 59 | BEGIN:VEVENT 60 | LAST-MODIFIED:20210408T060912Z 61 | DTSTAMP:20210408T060912Z 62 | UID:0370d094-b59a-4bb5-b333-84e2dab1329a 63 | SUMMARY:Mobility reservation (Economy 11307) 64 | X-MOZ-LASTACK:20210408T060912Z 65 | DTSTART;TZID=Europe/Brussels:20210408T083000 66 | DTEND;TZID=Europe/Brussels:20210408T160000 67 | X-MOZ-GENERATION:3 68 | BEGIN:VALARM 69 | ACTION:DISPLAY 70 | TRIGGER;VALUE=DURATION:-PT1H 71 | DESCRIPTION:Mobility reservation (Economy 11307) 72 | X-LIC-ERROR;X-LIC-ERRORTYPE=PARAMETER-VALUE-PARSE-ERROR:Got a VALUE paramet 73 | er with an illegal type for property: VALUE=DURATION 74 | END:VALARM 75 | END:VEVENT 76 | BEGIN:VEVENT 77 | DTSTAMP:20211006T083610Z 78 | UID:1baf29fb-7a33-4957-9b4f-cd58944b46a2 79 | SUMMARY:Bern 80 | DTSTART;VALUE=DATE:20211013 81 | DTEND;VALUE=DATE:20211014 82 | STATUS:CONFIRMED 83 | TRANSP:TRANSPARENT 84 | BEGIN:VALARM 85 | TRIGGER:-PT1H 86 | ACTION:DISPLAY 87 | DESCRIPTION:Bern 88 | END:VALARM 89 | END:VEVENT 90 | END:VCALENDAR 91 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | As contributors and maintainers of the Jazzband projects, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating documentation, 6 | submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in the Jazzband a harassment-free experience 9 | for everyone, regardless of the level of experience, gender, gender identity and 10 | expression, sexual orientation, disability, personal appearance, body size, race, 11 | ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | - The use of sexualized language or imagery 16 | - Personal attacks 17 | - Trolling or insulting/derogatory comments 18 | - Public or private harassment 19 | - Publishing other's private information, such as physical or electronic addresses, 20 | without explicit permission 21 | - Other unethical or unprofessional conduct 22 | 23 | The Jazzband roadies have the right and responsibility to remove, edit, or reject 24 | comments, commits, code, wiki edits, issues, and other contributions that are not 25 | aligned to this Code of Conduct, or to ban temporarily or permanently any contributor 26 | for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 27 | 28 | By adopting this Code of Conduct, the roadies commit themselves to fairly and 29 | consistently applying these principles to every aspect of managing the jazzband 30 | projects. Roadies who do not follow or enforce the Code of Conduct may be permanently 31 | removed from the Jazzband roadies. 32 | 33 | This code of conduct applies both within project spaces and in public spaces when an 34 | individual is representing the project or its community. 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 37 | contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and 38 | investigated and will result in a response that is deemed necessary and appropriate to 39 | the circumstances. Roadies are obligated to maintain confidentiality with regard to the 40 | reporter of an incident. 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 43 | 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/3/0/ 47 | -------------------------------------------------------------------------------- /test/test_icaldownload.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pathlib import Path 3 | 4 | import icalevents.icaldownload 5 | 6 | 7 | class ICalDownloadTests(unittest.TestCase): 8 | def test_apple_data_fix(self): 9 | data = """ 10 | DTSTART:18831118T120702 11 | RDATE;VALUE=DATE-TIME:18831118T120702 12 | TZNAME:PST 13 | TZOFFSETFROM:+5328 14 | TZOFFSETTO:-0800 15 | END:STANDARD 16 | BEGIN:DAYLIGHT 17 | DTSTART:19180331T020000 18 | """ 19 | expected = """ 20 | DTSTART:18831118T120702 21 | RDATE;VALUE=DATE-TIME:18831118T120702 22 | TZNAME:PST 23 | TZOFFSETFROM:+0053 24 | TZOFFSETTO:-0800 25 | END:STANDARD 26 | BEGIN:DAYLIGHT 27 | DTSTART:19180331T020000 28 | """ 29 | res = icalevents.icaldownload.apple_data_fix(data) 30 | self.assertEqual(res, expected, "fix invalid TZOFFSETFROM") 31 | 32 | def test_apple_url_fix(self): 33 | data = "webcal://blah.blub/webcal/" 34 | expected = "http://blah.blub/webcal/" 35 | 36 | res = icalevents.icaldownload.apple_url_fix(data) 37 | self.assertEqual(res, expected, "fix url protocol") 38 | 39 | def test_apple_url_fix_right(self): 40 | data = "https://blah.blub/webcal/" 41 | 42 | res = icalevents.icaldownload.apple_url_fix(data) 43 | self.assertEqual(res, data, "no change") 44 | 45 | def test_data_from_file_google(self): 46 | file = "test/test_data/basic.ics" 47 | result = "test/test_data/basic_content.txt" 48 | 49 | expected = None 50 | 51 | with open(result, mode="r", encoding="utf-8") as f: 52 | expected = f.read() 53 | 54 | for kind, input_file in [("str", file), ("Path", Path(file))]: 55 | with self.subTest(kind): 56 | content = icalevents.icaldownload.ICalDownload().data_from_file( 57 | input_file 58 | ) 59 | 60 | self.assertEqual( 61 | expected, content, "content form iCal file, Google format" 62 | ) 63 | 64 | def test_data_from_file_apple(self): 65 | file = "test/test_data/icloud.ics" 66 | result = "test/test_data/icloud_content.txt" 67 | 68 | expected = None 69 | 70 | with open(result, mode="r", encoding="utf-8") as f: 71 | expected = f.read() 72 | 73 | content = icalevents.icaldownload.ICalDownload().data_from_file( 74 | file, apple_fix=True 75 | ) 76 | 77 | self.assertEqual(expected, content, "content form iCal file, Apple format") 78 | 79 | def test_empty_file(self): 80 | empty_ical = "test/test_data/empty.ics" 81 | 82 | with self.assertRaises(OSError) as cm: 83 | icalevents.icaldownload.ICalDownload().data_from_file(empty_ical) 84 | 85 | self.assertEqual( 86 | str(cm.exception), 87 | "File test/test_data/empty.ics is not readable or is empty!", 88 | ) 89 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. iCalEvents documentation master file, created by 2 | sphinx-quickstart on Wed Sep 8 21:48:51 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to iCalEvents's documentation! 7 | ====================================== 8 | 9 | Simple Python 3 library to download, parse and query iCal sources. 10 | 11 | 12 | Usage 13 | ===== 14 | 15 | iCloud: 16 | ------- 17 | 18 | .. code:: python 19 | 20 | from icalevents.icalevents import events 21 | 22 | es = events(, fix_apple=True) 23 | 24 | Google: 25 | ------- 26 | 27 | .. code:: python 28 | 29 | from icalevents.icalevents import events 30 | 31 | es = events() 32 | 33 | 34 | Example 35 | ======= 36 | 37 | see main.py 38 | 39 | .. code:: python 40 | 41 | from icalevents.icalevents import events_async, latest_events, all_done 42 | from time import sleep 43 | 44 | if __name__ == '__main__': 45 | keys = [] 46 | 47 | with open('calendars.txt', mode='r', encoding='utf-8') as f: 48 | counter = 1 49 | 50 | while True: 51 | line = f.readline() 52 | if not line: 53 | break 54 | 55 | name, url = line.split(maxsplit=1) 56 | name = name.strip() 57 | url = url.strip() 58 | 59 | fix_apple = False 60 | if name == 'icloud': 61 | fix_apple = True 62 | 63 | key = "req_%d" % counter 64 | counter += 1 65 | keys.append(key) 66 | events_async(key, url, fix_apple=fix_apple) 67 | 68 | while keys: 69 | print("%d request running." % len(keys)) 70 | 71 | for k in keys[:]: 72 | if all_done(k): 73 | print("Request %s finished." % k) 74 | keys.remove(k) 75 | 76 | es = latest_events(k) 77 | 78 | for e in es: 79 | print(e) 80 | 81 | sleep(2) 82 | 83 | 84 | API 85 | === 86 | 87 | Module contents 88 | --------------- 89 | 90 | .. automodule:: icalevents 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | 95 | Submodules 96 | ---------- 97 | 98 | icalevents.icaldownload module 99 | ------------------------------ 100 | 101 | .. automodule:: icalevents.icaldownload 102 | :members: 103 | :undoc-members: 104 | :show-inheritance: 105 | 106 | icalevents.icalevents module 107 | ---------------------------- 108 | 109 | .. automodule:: icalevents.icalevents 110 | :members: 111 | :undoc-members: 112 | :show-inheritance: 113 | 114 | icalevents.icalparser module 115 | ---------------------------- 116 | 117 | .. automodule:: icalevents.icalparser 118 | :members: 119 | :undoc-members: 120 | :show-inheritance: 121 | -------------------------------------------------------------------------------- /test/test_data/recurr_id_dtstart_missmatch.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | METHOD:PUBLISH 3 | PRODID:Microsoft Exchange Server 2010 4 | VERSION:2.0 5 | X-WR-CALNAME:Calendar 6 | BEGIN:VTIMEZONE 7 | TZID:Eastern Standard Time 8 | BEGIN:STANDARD 9 | DTSTART:16010101T020000 10 | TZOFFSETFROM:-0400 11 | TZOFFSETTO:-0500 12 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11 13 | END:STANDARD 14 | BEGIN:DAYLIGHT 15 | DTSTART:16010101T020000 16 | TZOFFSETFROM:-0500 17 | TZOFFSETTO:-0400 18 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3 19 | END:DAYLIGHT 20 | END:VTIMEZONE 21 | BEGIN:VEVENT 22 | DESCRIPTION:Recurring Event - Exception 23 | UID:040000008200E00074C5B7101A82E00800000000D09EB37A5746D701000000000000000 24 | 0100000006FB97C329EB9D24C92583A1089CDEFE7 25 | RECURRENCE-ID;TZID=Eastern Standard Time:20220309T130000 26 | SUMMARY:Recurring Event - Exception 1 27 | DTSTART;TZID=Eastern Standard Time:20220309T130000 28 | DTEND;TZID=Eastern Standard Time:20220309T140000 29 | CLASS:PUBLIC 30 | PRIORITY:5 31 | DTSTAMP:20220330T141405Z 32 | TRANSP:OPAQUE 33 | STATUS:CONFIRMED 34 | SEQUENCE:8 35 | LOCATION:Microsoft Teams Meeting 36 | X-MICROSOFT-CDO-APPT-SEQUENCE:8 37 | X-MICROSOFT-CDO-BUSYSTATUS:BUSY 38 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 39 | X-MICROSOFT-CDO-ALLDAYEVENT:FALSE 40 | X-MICROSOFT-CDO-IMPORTANCE:1 41 | X-MICROSOFT-CDO-INSTTYPE:3 42 | X-MICROSOFT-DONOTFORWARDMEETING:FALSE 43 | X-MICROSOFT-DISALLOW-COUNTER:FALSE 44 | END:VEVENT 45 | BEGIN:VEVENT 46 | DESCRIPTION:Recurring Event 47 | RRULE:FREQ=MONTHLY;UNTIL=20220914T170000Z;INTERVAL=1;BYDAY=2WE 48 | UID:040000008200E00074C5B7101A82E00800000000D09EB37A5746D701000000000000000 49 | 0100000006FB97C329EB9D24C92583A1089CDEFE7 50 | SUMMARY:Recurring Event 51 | DTSTART;TZID=Eastern Standard Time:20220309T130000 52 | DTEND;TZID=Eastern Standard Time:20220309T140000 53 | CLASS:PUBLIC 54 | PRIORITY:5 55 | DTSTAMP:20220330T141405Z 56 | TRANSP:OPAQUE 57 | STATUS:CONFIRMED 58 | SEQUENCE:0 59 | LOCATION:Microsoft Teams Meeting 60 | X-MICROSOFT-CDO-APPT-SEQUENCE:0 61 | X-MICROSOFT-CDO-BUSYSTATUS:BUSY 62 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 63 | X-MICROSOFT-CDO-ALLDAYEVENT:FALSE 64 | X-MICROSOFT-CDO-IMPORTANCE:1 65 | X-MICROSOFT-CDO-INSTTYPE:1 66 | X-MICROSOFT-DONOTFORWARDMEETING:FALSE 67 | X-MICROSOFT-DISALLOW-COUNTER:FALSE 68 | END:VEVENT 69 | BEGIN:VEVENT 70 | DESCRIPTION:Recurring Event - Exception 71 | UID:040000008200E00074C5B7101A82E00800000000D09EB37A5746D701000000000000000 72 | 0100000006FB97C329EB9D24C92583A1089CDEFE7 73 | RECURRENCE-ID;TZID=Eastern Standard Time:20220413T130000 74 | SUMMARY:Recurring Event - Exception 2 75 | DTSTART;TZID=Eastern Standard Time:20220413T103000 76 | DTEND;TZID=Eastern Standard Time:20220413T113000 77 | CLASS:PUBLIC 78 | PRIORITY:5 79 | DTSTAMP:20220330T141405Z 80 | TRANSP:OPAQUE 81 | STATUS:CONFIRMED 82 | SEQUENCE:12 83 | LOCATION:Microsoft Teams Meeting 84 | X-MICROSOFT-CDO-APPT-SEQUENCE:12 85 | X-MICROSOFT-CDO-BUSYSTATUS:BUSY 86 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 87 | X-MICROSOFT-CDO-ALLDAYEVENT:FALSE 88 | X-MICROSOFT-CDO-IMPORTANCE:1 89 | X-MICROSOFT-CDO-INSTTYPE:3 90 | X-MICROSOFT-DONOTFORWARDMEETING:FALSE 91 | X-MICROSOFT-DISALLOW-COUNTER:FALSE 92 | END:VEVENT 93 | END:VCALENDAR 94 | -------------------------------------------------------------------------------- /icalevents/icaldownload.py: -------------------------------------------------------------------------------- 1 | """ 2 | Downloads an iCal url or reads an iCal file. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | from contextlib import suppress 8 | from pathlib import Path 9 | 10 | import urllib3 11 | 12 | 13 | def apple_data_fix(content: str) -> str: 14 | """ 15 | Fix Apple tzdata bug. 16 | 17 | :param content: content to fix 18 | :return: fixed content 19 | """ 20 | return content.replace("TZOFFSETFROM:+5328", "TZOFFSETFROM:+0053") 21 | 22 | 23 | def apple_url_fix(url: str) -> str: 24 | """ 25 | Fix Apple URL. 26 | 27 | :param url: URL to fix 28 | :return: fixed URL 29 | """ 30 | if url.startswith("webcal://"): 31 | url = url.replace("webcal://", "http://", 1) 32 | return url 33 | 34 | 35 | class ICalDownload: 36 | """ 37 | Downloads or reads and decodes iCal sources. 38 | """ 39 | 40 | def __init__(self, http: urllib3.PoolManager | None = None) -> None: 41 | # default http connection to use 42 | if http is None: 43 | http = urllib3.PoolManager() 44 | 45 | self.http = http 46 | 47 | def data_from_url(self, url: str, apple_fix: bool = False) -> str: 48 | """ 49 | Download iCal data from URL. 50 | 51 | :param url: URL to download 52 | :param apple_fix: fix Apple bugs (protocol type and tzdata in iCal) 53 | :return: decoded (and fixed) iCal data 54 | """ 55 | if apple_fix: 56 | url = apple_url_fix(url) 57 | 58 | response = self.http.request("GET", url) 59 | 60 | if not response.data: 61 | raise ConnectionError("Could not get data from %s!" % url) 62 | 63 | encoding = "utf-8" 64 | if content_type := response.headers.get("content-type"): 65 | with suppress(AttributeError, IndexError): 66 | encoding = content_type.split("charset=")[1] 67 | 68 | return self.decode(response.data, encoding, apple_fix=apple_fix) 69 | 70 | def data_from_file(self, file: str | Path, apple_fix: bool = False) -> str: 71 | """ 72 | Read iCal data from file. 73 | 74 | :param file: file to read 75 | :param apple_fix: fix wrong Apple tzdata in iCal 76 | :return: decoded (and fixed) iCal data 77 | """ 78 | with open(file, mode="rb") as f: 79 | content = f.read() 80 | 81 | if not content: 82 | raise OSError("File %s is not readable or is empty!" % file) 83 | 84 | return self.decode(content, apple_fix=apple_fix) 85 | 86 | def data_from_string( 87 | self, string_content: bytes | str, apple_fix: bool = False 88 | ) -> str: 89 | if not string_content: 90 | raise OSError("String content is not readable or is empty!") 91 | 92 | return self.decode(string_content, apple_fix=apple_fix) 93 | 94 | @staticmethod 95 | def decode( 96 | content: bytes | str, encoding: str = "utf-8", apple_fix: bool = False 97 | ) -> str: 98 | """ 99 | Decode content using the set charset. 100 | 101 | :param content: content do decode 102 | :param encoding: the used charset for decoding the content 103 | :param apple_fix: fix Apple txdata bug 104 | :return: decoded (and fixed) content 105 | """ 106 | if isinstance(content, bytes): 107 | content = content.decode(encoding) 108 | content = content.replace("\r", "") 109 | 110 | if apple_fix: 111 | content = apple_data_fix(content) 112 | 113 | return content 114 | -------------------------------------------------------------------------------- /test/test_icalparser.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from datetime import datetime 3 | 4 | from dateutil.tz import UTC, gettz 5 | 6 | import icalevents.icalparser 7 | 8 | 9 | class ICalParserTests(unittest.TestCase): 10 | def setUp(self): 11 | self.eventA = icalevents.icalparser.Event() 12 | self.eventA.uid = 1234 13 | self.eventA.start = datetime( 14 | year=2017, month=2, day=3, hour=12, minute=5, tzinfo=UTC 15 | ) 16 | self.eventA.end = datetime( 17 | year=2017, month=2, day=3, hour=15, minute=5, tzinfo=UTC 18 | ) 19 | self.eventA.all_day = False 20 | self.eventA.summary = "Event A" 21 | self.eventA.attendee = "name@example.com" 22 | self.eventA.organizer = "name@example.com" 23 | 24 | self.eventB = icalevents.icalparser.Event() 25 | self.eventB.uid = 1234 26 | self.eventB.start = datetime( 27 | year=2017, month=2, day=1, hour=15, minute=5, tzinfo=UTC 28 | ) 29 | self.eventB.end = datetime( 30 | year=2017, month=2, day=1, hour=16, minute=5, tzinfo=UTC 31 | ) 32 | self.eventB.all_day = False 33 | self.eventB.summary = "Event B" 34 | self.eventB.attendee = ["name@example.com", "another@example.com"] 35 | self.eventB.organizer = "name@example.com" 36 | 37 | self.dtA = datetime(2018, 6, 21, 12) 38 | self.dtB = datetime(2018, 6, 21, 12, tzinfo=gettz("Europe/Berlin")) 39 | 40 | def test_now(self): 41 | n = icalevents.icalparser.now() 42 | 43 | self.assertEqual(type(n), datetime, "result of now has type datetime") 44 | self.assertTrue(n.tzinfo, "result of now has a timezone info") 45 | 46 | def test_time_left(self): 47 | dt = datetime(year=2017, month=2, day=2, hour=11, minute=2, tzinfo=UTC) 48 | time_left = self.eventA.time_left(time=dt) 49 | self.assertEqual(time_left.days, 1) 50 | self.assertEqual(time_left.seconds, 3780) 51 | 52 | def test_event_copy_to(self): 53 | new_start = datetime(year=2017, month=2, day=5, hour=12, minute=5, tzinfo=UTC) 54 | eventC = self.eventA.copy_to(new_start) 55 | new_uid = 1234567890 56 | 57 | self.assertNotEqual(eventC.uid, self.eventA.uid, "new event has new UID") 58 | self.assertEqual(eventC.start, new_start, "new event has new start") 59 | self.assertEqual( 60 | eventC.end - eventC.start, 61 | self.eventA.end - self.eventA.start, 62 | "new event has same duration", 63 | ) 64 | self.assertEqual(eventC.all_day, False, "new event is no all day event") 65 | self.assertEqual(eventC.summary, self.eventA.summary, "copy to: summary") 66 | self.assertEqual( 67 | eventC.description, self.eventA.description, "copy to: description" 68 | ) 69 | 70 | eventD = eventC.copy_to(uid=new_uid) 71 | self.assertEqual(eventD.uid, new_uid, "new event has specified UID") 72 | self.assertEqual(eventD.start, eventC.start, "new event has same start") 73 | self.assertEqual(eventD.end, eventC.end, "new event has same end") 74 | self.assertEqual( 75 | eventD.all_day, eventC.all_day, "new event is no all day event" 76 | ) 77 | self.assertEqual(eventD.summary, eventC.summary, "copy to: summary") 78 | self.assertEqual(eventD.description, eventC.description, "copy to: description") 79 | 80 | def test_event_order(self): 81 | self.assertGreater(self.eventA, self.eventB, "order of events") 82 | 83 | def test_attendee(self): 84 | self.assertIsInstance(self.eventA.attendee, str) 85 | self.assertIsInstance(self.eventB.attendee, list) 86 | 87 | def test_organizer(self): 88 | self.assertIsInstance(self.eventA.organizer, str) 89 | self.assertIsInstance(self.eventB.organizer, str) 90 | 91 | def test_str(self): 92 | self.eventA.start = datetime(year=2017, month=2, day=3, hour=12, minute=5) 93 | self.eventA.end = datetime(year=2017, month=2, day=3, hour=15, minute=5) 94 | self.assertEqual("2017-02-03 12:05:00: Event A (3:00:00)", str(self.eventA)) 95 | -------------------------------------------------------------------------------- /icalevents/icalevents.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime, tzinfo as _tzinfo 4 | from pathlib import Path 5 | from threading import Lock, Thread 6 | 7 | import urllib3 8 | 9 | from .icalparser import Event, parse_events 10 | from .icaldownload import ICalDownload 11 | 12 | 13 | # Lock for event data 14 | event_lock = Lock() 15 | # Event data 16 | event_store: dict[str, list[Event]] = {} 17 | # Threads 18 | threads: dict[str, list[Thread]] = {} 19 | 20 | 21 | def events( 22 | url: str | None = None, 23 | file: str | Path | None = None, 24 | string_content: bytes | str | None = None, 25 | start: datetime | None = None, 26 | end: datetime | None = None, 27 | fix_apple: bool = False, 28 | http: urllib3.PoolManager | None = None, 29 | tzinfo: _tzinfo | None = None, 30 | sort: bool = False, 31 | strict: bool = False, 32 | ) -> list[Event]: 33 | """ 34 | Get all events form the given iCal URL occurring in the given time range. 35 | 36 | :param url: iCal URL 37 | :param file: iCal file path 38 | :param string_content: iCal content as string 39 | :param start: start date (see datetime.date) 40 | :param end: end date (see datetime.date) 41 | :param fix_apple: fix known Apple iCal issues 42 | :param tzinfo: return values in specified tz 43 | :param sort: sort return values 44 | :param strict: return dates, datetimes and datetime with timezones as specified in ical 45 | :sort sorts events by start time 46 | 47 | :return events 48 | """ 49 | found_events = [] 50 | 51 | content = "" 52 | ical_download = ICalDownload(http=http) 53 | 54 | if url: 55 | content = ical_download.data_from_url(url, apple_fix=fix_apple) 56 | 57 | if not content and file: 58 | content = ical_download.data_from_file(file, apple_fix=fix_apple) 59 | 60 | if not content and string_content: 61 | content = ical_download.data_from_string(string_content, apple_fix=fix_apple) 62 | 63 | found_events += parse_events( 64 | content, start=start, end=end, tzinfo=tzinfo, sort=sort, strict=strict 65 | ) 66 | 67 | if sort: 68 | found_events.sort() 69 | 70 | return found_events 71 | 72 | 73 | def request_data( 74 | key: str, 75 | url: str | None, 76 | file: str | Path | None, 77 | string_content: bytes | str | None, 78 | start: datetime | None, 79 | end: datetime | None, 80 | fix_apple: bool, 81 | ) -> None: 82 | """ 83 | Request data, update local data cache and remove this Thread from queue. 84 | 85 | :param key: key for data source to get result later 86 | :param url: iCal URL 87 | :param file: iCal file path 88 | :param string_content: iCal content as string 89 | :param start: start date 90 | :param end: end date 91 | :param fix_apple: fix known Apple iCal issues 92 | """ 93 | data = [] 94 | 95 | try: 96 | data += events( 97 | url=url, 98 | file=file, 99 | string_content=string_content, 100 | start=start, 101 | end=end, 102 | fix_apple=fix_apple, 103 | ) 104 | finally: 105 | update_events(key, data) 106 | request_finished(key) 107 | 108 | 109 | def events_async( 110 | key: str, 111 | url: str | None = None, 112 | file: str | Path | None = None, 113 | start: datetime | None = None, 114 | string_content: bytes | str | None = None, 115 | end: datetime | None = None, 116 | fix_apple: bool = False, 117 | ) -> None: 118 | """ 119 | Trigger an asynchronous data request. 120 | 121 | :param key: key for data source to get result later 122 | :param url: iCal URL 123 | :param file: iCal file path 124 | :param string_content: iCal content as string 125 | :param start: start date 126 | :param end: end date 127 | :param fix_apple: fix known Apple iCal issues 128 | """ 129 | t = Thread( 130 | target=request_data, 131 | args=(key, url, file, string_content, start, end, fix_apple), 132 | ) 133 | 134 | with event_lock: 135 | if key not in threads: 136 | threads[key] = [] 137 | 138 | threads[key].append(t) 139 | 140 | if not threads[key][0].is_alive(): 141 | threads[key][0].start() 142 | 143 | 144 | def request_finished(key: str) -> None: 145 | """ 146 | Remove finished Thread from queue. 147 | 148 | :param key: data source key 149 | """ 150 | with event_lock: 151 | threads[key] = threads[key][1:] 152 | 153 | if threads[key]: 154 | threads[key][0].run() 155 | 156 | 157 | def update_events(key: str, data: list[Event]) -> None: 158 | """ 159 | Set the latest events for a key. 160 | 161 | :param key: key to set 162 | :param data: events for key 163 | """ 164 | with event_lock: 165 | event_store[key] = data 166 | 167 | 168 | def latest_events(key: str) -> list[Event]: 169 | """ 170 | Get the latest downloaded events for the given key. 171 | 172 | :return: events for key 173 | """ 174 | with event_lock: 175 | # copy data 176 | res = event_store[key][:] 177 | 178 | return res 179 | 180 | 181 | def all_done(key: str) -> bool: 182 | """ 183 | Check if requests for the given key are active. 184 | 185 | :param key: key for requests 186 | :return: True if requests are pending or active 187 | """ 188 | with event_lock: 189 | if threads[key]: 190 | return False 191 | return True 192 | -------------------------------------------------------------------------------- /test/test_data/icloud.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | X-WR-CALNAME:Der Wir Kalender 4 | X-WR-CALDESC: 5 | X-APPLE-CALENDAR-COLOR:#ff2d55FF 6 | BEGIN:VTIMEZONE 7 | TZID:US/Pacific 8 | X-LIC-LOCATION:US/Pacific 9 | BEGIN:STANDARD 10 | DTSTART:18831118T120702 11 | RDATE;VALUE=DATE-TIME:18831118T120702 12 | TZNAME:PST 13 | TZOFFSETFROM:-0752 14 | TZOFFSETTO:-0800 15 | END:STANDARD 16 | BEGIN:DAYLIGHT 17 | DTSTART:19180331T020000 18 | RRULE:FREQ=YEARLY;UNTIL=19190330T100000Z;BYDAY=-1SU;BYMONTH=3 19 | TZNAME:PDT 20 | TZOFFSETFROM:-0800 21 | TZOFFSETTO:-0700 22 | END:DAYLIGHT 23 | BEGIN:STANDARD 24 | DTSTART:19181027T020000 25 | RRULE:FREQ=YEARLY;UNTIL=19191026T090000Z;BYDAY=-1SU;BYMONTH=10 26 | TZNAME:PST 27 | TZOFFSETFROM:-0700 28 | TZOFFSETTO:-0800 29 | END:STANDARD 30 | BEGIN:DAYLIGHT 31 | DTSTART:19420209T020000 32 | RDATE;VALUE=DATE-TIME:19420209T020000 33 | TZNAME:PWT 34 | TZOFFSETFROM:-0800 35 | TZOFFSETTO:-0700 36 | END:DAYLIGHT 37 | BEGIN:DAYLIGHT 38 | DTSTART:19450814T160000 39 | RDATE;VALUE=DATE-TIME:19450814T160000 40 | TZNAME:PPT 41 | TZOFFSETFROM:-0700 42 | TZOFFSETTO:-0700 43 | END:DAYLIGHT 44 | BEGIN:STANDARD 45 | DTSTART:19450930T020000 46 | RDATE;VALUE=DATE-TIME:19450930T020000 47 | RDATE;VALUE=DATE-TIME:19490101T020000 48 | TZNAME:PST 49 | TZOFFSETFROM:-0700 50 | TZOFFSETTO:-0800 51 | END:STANDARD 52 | BEGIN:STANDARD 53 | DTSTART:19460101T000000 54 | RDATE;VALUE=DATE-TIME:19460101T000000 55 | RDATE;VALUE=DATE-TIME:19670101T000000 56 | TZNAME:PST 57 | TZOFFSETFROM:-0800 58 | TZOFFSETTO:-0800 59 | END:STANDARD 60 | BEGIN:DAYLIGHT 61 | DTSTART:19480314T020100 62 | RDATE;VALUE=DATE-TIME:19480314T020100 63 | RDATE;VALUE=DATE-TIME:19740106T020000 64 | RDATE;VALUE=DATE-TIME:19750223T020000 65 | TZNAME:PDT 66 | TZOFFSETFROM:-0800 67 | TZOFFSETTO:-0700 68 | END:DAYLIGHT 69 | BEGIN:DAYLIGHT 70 | DTSTART:19500430T010000 71 | RRULE:FREQ=YEARLY;UNTIL=19660424T090000Z;BYDAY=-1SU;BYMONTH=4 72 | TZNAME:PDT 73 | TZOFFSETFROM:-0800 74 | TZOFFSETTO:-0700 75 | END:DAYLIGHT 76 | BEGIN:STANDARD 77 | DTSTART:19500924T020000 78 | RRULE:FREQ=YEARLY;UNTIL=19610924T090000Z;BYDAY=-1SU;BYMONTH=9 79 | TZNAME:PST 80 | TZOFFSETFROM:-0700 81 | TZOFFSETTO:-0800 82 | END:STANDARD 83 | BEGIN:STANDARD 84 | DTSTART:19621028T020000 85 | RRULE:FREQ=YEARLY;UNTIL=19661030T090000Z;BYDAY=-1SU;BYMONTH=10 86 | TZNAME:PST 87 | TZOFFSETFROM:-0700 88 | TZOFFSETTO:-0800 89 | END:STANDARD 90 | BEGIN:DAYLIGHT 91 | DTSTART:19670430T020000 92 | RRULE:FREQ=YEARLY;UNTIL=19730429T100000Z;BYDAY=-1SU;BYMONTH=4 93 | TZNAME:PDT 94 | TZOFFSETFROM:-0800 95 | TZOFFSETTO:-0700 96 | END:DAYLIGHT 97 | BEGIN:STANDARD 98 | DTSTART:19671029T020000 99 | RRULE:FREQ=YEARLY;UNTIL=20061029T090000Z;BYDAY=-1SU;BYMONTH=10 100 | TZNAME:PST 101 | TZOFFSETFROM:-0700 102 | TZOFFSETTO:-0800 103 | END:STANDARD 104 | BEGIN:DAYLIGHT 105 | DTSTART:19760425T020000 106 | RRULE:FREQ=YEARLY;UNTIL=19860427T100000Z;BYDAY=-1SU;BYMONTH=4 107 | TZNAME:PDT 108 | TZOFFSETFROM:-0800 109 | TZOFFSETTO:-0700 110 | END:DAYLIGHT 111 | BEGIN:DAYLIGHT 112 | DTSTART:19870405T020000 113 | RRULE:FREQ=YEARLY;UNTIL=20060402T100000Z;BYDAY=1SU;BYMONTH=4 114 | TZNAME:PDT 115 | TZOFFSETFROM:-0800 116 | TZOFFSETTO:-0700 117 | END:DAYLIGHT 118 | BEGIN:DAYLIGHT 119 | DTSTART:20070311T020000 120 | RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 121 | TZNAME:PDT 122 | TZOFFSETFROM:-0800 123 | TZOFFSETTO:-0700 124 | END:DAYLIGHT 125 | BEGIN:STANDARD 126 | DTSTART:20071104T020000 127 | RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 128 | TZNAME:PST 129 | TZOFFSETFROM:-0700 130 | TZOFFSETTO:-0800 131 | END:STANDARD 132 | END:VTIMEZONE 133 | BEGIN:VTIMEZONE 134 | TZID:Europe/Berlin 135 | X-LIC-LOCATION:Europe/Berlin 136 | BEGIN:STANDARD 137 | DTSTART:18930401T000000 138 | RDATE;VALUE=DATE-TIME:18930401T000000 139 | TZNAME:CEST 140 | TZOFFSETFROM:+5328 141 | TZOFFSETTO:+0100 142 | END:STANDARD 143 | BEGIN:DAYLIGHT 144 | DTSTART:19160430T230000 145 | RDATE;VALUE=DATE-TIME:19160430T230000 146 | RDATE;VALUE=DATE-TIME:19400401T020000 147 | RDATE;VALUE=DATE-TIME:19430329T020000 148 | RDATE;VALUE=DATE-TIME:19460414T020000 149 | RDATE;VALUE=DATE-TIME:19470406T030000 150 | RDATE;VALUE=DATE-TIME:19480418T020000 151 | RDATE;VALUE=DATE-TIME:19490410T020000 152 | RDATE;VALUE=DATE-TIME:19800406T020000 153 | TZNAME:CEST 154 | TZOFFSETFROM:+0100 155 | TZOFFSETTO:+0200 156 | END:DAYLIGHT 157 | BEGIN:STANDARD 158 | DTSTART:19161001T010000 159 | RDATE;VALUE=DATE-TIME:19161001T010000 160 | RDATE;VALUE=DATE-TIME:19421102T030000 161 | RDATE;VALUE=DATE-TIME:19431004T030000 162 | RDATE;VALUE=DATE-TIME:19441002T030000 163 | RDATE;VALUE=DATE-TIME:19451118T030000 164 | RDATE;VALUE=DATE-TIME:19461007T030000 165 | TZNAME:CET 166 | TZOFFSETFROM:+0200 167 | TZOFFSETTO:+0100 168 | END:STANDARD 169 | BEGIN:DAYLIGHT 170 | DTSTART:19170416T020000 171 | RRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYDAY=3MO;BYMONTH=4 172 | TZNAME:CEST 173 | TZOFFSETFROM:+0100 174 | TZOFFSETTO:+0200 175 | END:DAYLIGHT 176 | BEGIN:STANDARD 177 | DTSTART:19170917T030000 178 | RRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYDAY=3MO;BYMONTH=9 179 | TZNAME:CET 180 | TZOFFSETFROM:+0200 181 | TZOFFSETTO:+0100 182 | END:STANDARD 183 | BEGIN:DAYLIGHT 184 | DTSTART:19440403T020000 185 | RRULE:FREQ=YEARLY;UNTIL=19450402T010000Z;BYDAY=1MO;BYMONTH=4 186 | TZNAME:CEST 187 | TZOFFSETFROM:+0100 188 | TZOFFSETTO:+0200 189 | END:DAYLIGHT 190 | BEGIN:DAYLIGHT 191 | DTSTART:19450524T020000 192 | RDATE;VALUE=DATE-TIME:19450524T020000 193 | RDATE;VALUE=DATE-TIME:19470511T030000 194 | TZNAME:CEMT 195 | TZOFFSETFROM:+0200 196 | TZOFFSETTO:+0300 197 | END:DAYLIGHT 198 | BEGIN:DAYLIGHT 199 | DTSTART:19450924T030000 200 | RDATE;VALUE=DATE-TIME:19450924T030000 201 | RDATE;VALUE=DATE-TIME:19470629T030000 202 | TZNAME:CEST 203 | TZOFFSETFROM:+0300 204 | TZOFFSETTO:+0200 205 | END:DAYLIGHT 206 | BEGIN:STANDARD 207 | DTSTART:19460101T000000 208 | RDATE;VALUE=DATE-TIME:19460101T000000 209 | RDATE;VALUE=DATE-TIME:19800101T000000 210 | TZNAME:CEST 211 | TZOFFSETFROM:+0100 212 | TZOFFSETTO:+0100 213 | END:STANDARD 214 | BEGIN:STANDARD 215 | DTSTART:19471005T030000 216 | RRULE:FREQ=YEARLY;UNTIL=19491002T010000Z;BYDAY=1SU;BYMONTH=10 217 | TZNAME:CET 218 | TZOFFSETFROM:+0200 219 | TZOFFSETTO:+0100 220 | END:STANDARD 221 | BEGIN:STANDARD 222 | DTSTART:19800928T030000 223 | RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYDAY=-1SU;BYMONTH=9 224 | TZNAME:CET 225 | TZOFFSETFROM:+0200 226 | TZOFFSETTO:+0100 227 | END:STANDARD 228 | BEGIN:DAYLIGHT 229 | DTSTART:19810329T020000 230 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 231 | TZNAME:CEST 232 | TZOFFSETFROM:+0100 233 | TZOFFSETTO:+0200 234 | END:DAYLIGHT 235 | BEGIN:STANDARD 236 | DTSTART:19961027T030000 237 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 238 | TZNAME:CET 239 | TZOFFSETFROM:+0200 240 | TZOFFSETTO:+0100 241 | END:STANDARD 242 | END:VTIMEZONE 243 | BEGIN:VEVENT 244 | CREATED:20161026T154648Z 245 | DTEND;TZID=Europe/Berlin:20161103T183000 246 | DTSTAMP:20161026T154649Z 247 | DTSTART;TZID=Europe/Berlin:20161103T180000 248 | LAST-MODIFIED:20161026T154648Z 249 | LOCATION:Naturpark Altmühltal\nKanalstraße 5\n91757 250 | Treuchtlingen\nDeutschland 251 | SEQUENCE:0 252 | SUMMARY:Friseur 253 | UID:003AFB7E-BA60-481A-A087-23024D956074 254 | X-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-ADDRESS=Kanalstraße 5\\n91757 255 | Treuchtlingen\\nDeutschland;X-APPLE-RADIUS=100;X-APPLE-REFERENCEFRAME=1; 256 | X-TITLE=Naturpark Altmühltal:geo:48.954682,10.909644 257 | END:VEVENT 258 | BEGIN:VEVENT 259 | DTEND;TZID=Europe/Berlin:20160616T213000 260 | UID:015A230B-1627-4C27-939B-DB0B54F8CF26 261 | DTSTAMP:20160616T173403Z 262 | LOCATION:Römerstraße 49 263 | URL;VALUE=URI:message: 264 | %3C1836419472.103507.1464935907579.JavaMail.open-xchange@omgreatgod.store% 265 | 3E 266 | SEQUENCE:0 267 | SUMMARY:Weinabend Franken – Castell 268 | DTSTART;TZID=Europe/Berlin:20160616T193000 269 | CREATED:20160603T100150Z 270 | LAST-MODIFIED:20160603T100150Z 271 | END:VEVENT 272 | BEGIN:VEVENT 273 | CREATED:20151209T212646Z 274 | DTEND;TZID=Europe/Berlin:20151209T110000 275 | DTSTAMP:20160401T132640Z 276 | DTSTART;TZID=Europe/Berlin:20151209T100000 277 | LAST-MODIFIED:20160401T132551Z 278 | RRULE:FREQ=YEARLY 279 | SEQUENCE:1 280 | SUMMARY:Geburtstag 281 | UID:09094143-005B-478F-BF37-10316FC9490B 282 | END:VEVENT 283 | BEGIN:VEVENT 284 | CREATED:20160222T073027Z 285 | DTEND;TZID=Europe/Berlin:20160222T173000 286 | DTSTAMP:20160926T150116Z 287 | DTSTART;TZID=Europe/Berlin:20160222T161500 288 | EXDATE;TZID=Europe/Berlin:20160321T161500 289 | EXDATE;TZID=Europe/Berlin:20160328T161500 290 | EXDATE;TZID=Europe/Berlin:20160516T161500 291 | EXDATE;TZID=Europe/Berlin:20160523T161500 292 | EXDATE;TZID=Europe/Berlin:20160801T161500 293 | EXDATE;TZID=Europe/Berlin:20160808T161500 294 | EXDATE;TZID=Europe/Berlin:20160815T161500 295 | EXDATE;TZID=Europe/Berlin:20160822T161500 296 | EXDATE;TZID=Europe/Berlin:20160829T161500 297 | EXDATE;TZID=Europe/Berlin:20160905T161500 298 | EXDATE;TZID=Europe/Berlin:20160912T161500 299 | LAST-MODIFIED:20160926T150115Z 300 | RRULE:FREQ=WEEKLY;UNTIL=20161001T215959Z 301 | SEQUENCE:0 302 | SUMMARY:Kinderturnen 303 | UID:0ED5515F-D6C2-4678-9EB1-8C483A12C410 304 | END:VEVENT 305 | END:VCALENDAR 306 | -------------------------------------------------------------------------------- /test/test_data/icloud_content.txt: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | X-WR-CALNAME:Der Wir Kalender 4 | X-WR-CALDESC: 5 | X-APPLE-CALENDAR-COLOR:#ff2d55FF 6 | BEGIN:VTIMEZONE 7 | TZID:US/Pacific 8 | X-LIC-LOCATION:US/Pacific 9 | BEGIN:STANDARD 10 | DTSTART:18831118T120702 11 | RDATE;VALUE=DATE-TIME:18831118T120702 12 | TZNAME:PST 13 | TZOFFSETFROM:-0752 14 | TZOFFSETTO:-0800 15 | END:STANDARD 16 | BEGIN:DAYLIGHT 17 | DTSTART:19180331T020000 18 | RRULE:FREQ=YEARLY;UNTIL=19190330T100000Z;BYDAY=-1SU;BYMONTH=3 19 | TZNAME:PDT 20 | TZOFFSETFROM:-0800 21 | TZOFFSETTO:-0700 22 | END:DAYLIGHT 23 | BEGIN:STANDARD 24 | DTSTART:19181027T020000 25 | RRULE:FREQ=YEARLY;UNTIL=19191026T090000Z;BYDAY=-1SU;BYMONTH=10 26 | TZNAME:PST 27 | TZOFFSETFROM:-0700 28 | TZOFFSETTO:-0800 29 | END:STANDARD 30 | BEGIN:DAYLIGHT 31 | DTSTART:19420209T020000 32 | RDATE;VALUE=DATE-TIME:19420209T020000 33 | TZNAME:PWT 34 | TZOFFSETFROM:-0800 35 | TZOFFSETTO:-0700 36 | END:DAYLIGHT 37 | BEGIN:DAYLIGHT 38 | DTSTART:19450814T160000 39 | RDATE;VALUE=DATE-TIME:19450814T160000 40 | TZNAME:PPT 41 | TZOFFSETFROM:-0700 42 | TZOFFSETTO:-0700 43 | END:DAYLIGHT 44 | BEGIN:STANDARD 45 | DTSTART:19450930T020000 46 | RDATE;VALUE=DATE-TIME:19450930T020000 47 | RDATE;VALUE=DATE-TIME:19490101T020000 48 | TZNAME:PST 49 | TZOFFSETFROM:-0700 50 | TZOFFSETTO:-0800 51 | END:STANDARD 52 | BEGIN:STANDARD 53 | DTSTART:19460101T000000 54 | RDATE;VALUE=DATE-TIME:19460101T000000 55 | RDATE;VALUE=DATE-TIME:19670101T000000 56 | TZNAME:PST 57 | TZOFFSETFROM:-0800 58 | TZOFFSETTO:-0800 59 | END:STANDARD 60 | BEGIN:DAYLIGHT 61 | DTSTART:19480314T020100 62 | RDATE;VALUE=DATE-TIME:19480314T020100 63 | RDATE;VALUE=DATE-TIME:19740106T020000 64 | RDATE;VALUE=DATE-TIME:19750223T020000 65 | TZNAME:PDT 66 | TZOFFSETFROM:-0800 67 | TZOFFSETTO:-0700 68 | END:DAYLIGHT 69 | BEGIN:DAYLIGHT 70 | DTSTART:19500430T010000 71 | RRULE:FREQ=YEARLY;UNTIL=19660424T090000Z;BYDAY=-1SU;BYMONTH=4 72 | TZNAME:PDT 73 | TZOFFSETFROM:-0800 74 | TZOFFSETTO:-0700 75 | END:DAYLIGHT 76 | BEGIN:STANDARD 77 | DTSTART:19500924T020000 78 | RRULE:FREQ=YEARLY;UNTIL=19610924T090000Z;BYDAY=-1SU;BYMONTH=9 79 | TZNAME:PST 80 | TZOFFSETFROM:-0700 81 | TZOFFSETTO:-0800 82 | END:STANDARD 83 | BEGIN:STANDARD 84 | DTSTART:19621028T020000 85 | RRULE:FREQ=YEARLY;UNTIL=19661030T090000Z;BYDAY=-1SU;BYMONTH=10 86 | TZNAME:PST 87 | TZOFFSETFROM:-0700 88 | TZOFFSETTO:-0800 89 | END:STANDARD 90 | BEGIN:DAYLIGHT 91 | DTSTART:19670430T020000 92 | RRULE:FREQ=YEARLY;UNTIL=19730429T100000Z;BYDAY=-1SU;BYMONTH=4 93 | TZNAME:PDT 94 | TZOFFSETFROM:-0800 95 | TZOFFSETTO:-0700 96 | END:DAYLIGHT 97 | BEGIN:STANDARD 98 | DTSTART:19671029T020000 99 | RRULE:FREQ=YEARLY;UNTIL=20061029T090000Z;BYDAY=-1SU;BYMONTH=10 100 | TZNAME:PST 101 | TZOFFSETFROM:-0700 102 | TZOFFSETTO:-0800 103 | END:STANDARD 104 | BEGIN:DAYLIGHT 105 | DTSTART:19760425T020000 106 | RRULE:FREQ=YEARLY;UNTIL=19860427T100000Z;BYDAY=-1SU;BYMONTH=4 107 | TZNAME:PDT 108 | TZOFFSETFROM:-0800 109 | TZOFFSETTO:-0700 110 | END:DAYLIGHT 111 | BEGIN:DAYLIGHT 112 | DTSTART:19870405T020000 113 | RRULE:FREQ=YEARLY;UNTIL=20060402T100000Z;BYDAY=1SU;BYMONTH=4 114 | TZNAME:PDT 115 | TZOFFSETFROM:-0800 116 | TZOFFSETTO:-0700 117 | END:DAYLIGHT 118 | BEGIN:DAYLIGHT 119 | DTSTART:20070311T020000 120 | RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 121 | TZNAME:PDT 122 | TZOFFSETFROM:-0800 123 | TZOFFSETTO:-0700 124 | END:DAYLIGHT 125 | BEGIN:STANDARD 126 | DTSTART:20071104T020000 127 | RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 128 | TZNAME:PST 129 | TZOFFSETFROM:-0700 130 | TZOFFSETTO:-0800 131 | END:STANDARD 132 | END:VTIMEZONE 133 | BEGIN:VTIMEZONE 134 | TZID:Europe/Berlin 135 | X-LIC-LOCATION:Europe/Berlin 136 | BEGIN:STANDARD 137 | DTSTART:18930401T000000 138 | RDATE;VALUE=DATE-TIME:18930401T000000 139 | TZNAME:CEST 140 | TZOFFSETFROM:+0053 141 | TZOFFSETTO:+0100 142 | END:STANDARD 143 | BEGIN:DAYLIGHT 144 | DTSTART:19160430T230000 145 | RDATE;VALUE=DATE-TIME:19160430T230000 146 | RDATE;VALUE=DATE-TIME:19400401T020000 147 | RDATE;VALUE=DATE-TIME:19430329T020000 148 | RDATE;VALUE=DATE-TIME:19460414T020000 149 | RDATE;VALUE=DATE-TIME:19470406T030000 150 | RDATE;VALUE=DATE-TIME:19480418T020000 151 | RDATE;VALUE=DATE-TIME:19490410T020000 152 | RDATE;VALUE=DATE-TIME:19800406T020000 153 | TZNAME:CEST 154 | TZOFFSETFROM:+0100 155 | TZOFFSETTO:+0200 156 | END:DAYLIGHT 157 | BEGIN:STANDARD 158 | DTSTART:19161001T010000 159 | RDATE;VALUE=DATE-TIME:19161001T010000 160 | RDATE;VALUE=DATE-TIME:19421102T030000 161 | RDATE;VALUE=DATE-TIME:19431004T030000 162 | RDATE;VALUE=DATE-TIME:19441002T030000 163 | RDATE;VALUE=DATE-TIME:19451118T030000 164 | RDATE;VALUE=DATE-TIME:19461007T030000 165 | TZNAME:CET 166 | TZOFFSETFROM:+0200 167 | TZOFFSETTO:+0100 168 | END:STANDARD 169 | BEGIN:DAYLIGHT 170 | DTSTART:19170416T020000 171 | RRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYDAY=3MO;BYMONTH=4 172 | TZNAME:CEST 173 | TZOFFSETFROM:+0100 174 | TZOFFSETTO:+0200 175 | END:DAYLIGHT 176 | BEGIN:STANDARD 177 | DTSTART:19170917T030000 178 | RRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYDAY=3MO;BYMONTH=9 179 | TZNAME:CET 180 | TZOFFSETFROM:+0200 181 | TZOFFSETTO:+0100 182 | END:STANDARD 183 | BEGIN:DAYLIGHT 184 | DTSTART:19440403T020000 185 | RRULE:FREQ=YEARLY;UNTIL=19450402T010000Z;BYDAY=1MO;BYMONTH=4 186 | TZNAME:CEST 187 | TZOFFSETFROM:+0100 188 | TZOFFSETTO:+0200 189 | END:DAYLIGHT 190 | BEGIN:DAYLIGHT 191 | DTSTART:19450524T020000 192 | RDATE;VALUE=DATE-TIME:19450524T020000 193 | RDATE;VALUE=DATE-TIME:19470511T030000 194 | TZNAME:CEMT 195 | TZOFFSETFROM:+0200 196 | TZOFFSETTO:+0300 197 | END:DAYLIGHT 198 | BEGIN:DAYLIGHT 199 | DTSTART:19450924T030000 200 | RDATE;VALUE=DATE-TIME:19450924T030000 201 | RDATE;VALUE=DATE-TIME:19470629T030000 202 | TZNAME:CEST 203 | TZOFFSETFROM:+0300 204 | TZOFFSETTO:+0200 205 | END:DAYLIGHT 206 | BEGIN:STANDARD 207 | DTSTART:19460101T000000 208 | RDATE;VALUE=DATE-TIME:19460101T000000 209 | RDATE;VALUE=DATE-TIME:19800101T000000 210 | TZNAME:CEST 211 | TZOFFSETFROM:+0100 212 | TZOFFSETTO:+0100 213 | END:STANDARD 214 | BEGIN:STANDARD 215 | DTSTART:19471005T030000 216 | RRULE:FREQ=YEARLY;UNTIL=19491002T010000Z;BYDAY=1SU;BYMONTH=10 217 | TZNAME:CET 218 | TZOFFSETFROM:+0200 219 | TZOFFSETTO:+0100 220 | END:STANDARD 221 | BEGIN:STANDARD 222 | DTSTART:19800928T030000 223 | RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYDAY=-1SU;BYMONTH=9 224 | TZNAME:CET 225 | TZOFFSETFROM:+0200 226 | TZOFFSETTO:+0100 227 | END:STANDARD 228 | BEGIN:DAYLIGHT 229 | DTSTART:19810329T020000 230 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 231 | TZNAME:CEST 232 | TZOFFSETFROM:+0100 233 | TZOFFSETTO:+0200 234 | END:DAYLIGHT 235 | BEGIN:STANDARD 236 | DTSTART:19961027T030000 237 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 238 | TZNAME:CET 239 | TZOFFSETFROM:+0200 240 | TZOFFSETTO:+0100 241 | END:STANDARD 242 | END:VTIMEZONE 243 | BEGIN:VEVENT 244 | CREATED:20161026T154648Z 245 | DTEND;TZID=Europe/Berlin:20161103T183000 246 | DTSTAMP:20161026T154649Z 247 | DTSTART;TZID=Europe/Berlin:20161103T180000 248 | LAST-MODIFIED:20161026T154648Z 249 | LOCATION:Naturpark Altmühltal\nKanalstraße 5\n91757 250 | Treuchtlingen\nDeutschland 251 | SEQUENCE:0 252 | SUMMARY:Friseur 253 | UID:003AFB7E-BA60-481A-A087-23024D956074 254 | X-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-ADDRESS=Kanalstraße 5\\n91757 255 | Treuchtlingen\\nDeutschland;X-APPLE-RADIUS=100;X-APPLE-REFERENCEFRAME=1; 256 | X-TITLE=Naturpark Altmühltal:geo:48.954682,10.909644 257 | END:VEVENT 258 | BEGIN:VEVENT 259 | DTEND;TZID=Europe/Berlin:20160616T213000 260 | UID:015A230B-1627-4C27-939B-DB0B54F8CF26 261 | DTSTAMP:20160616T173403Z 262 | LOCATION:Römerstraße 49 263 | URL;VALUE=URI:message: 264 | %3C1836419472.103507.1464935907579.JavaMail.open-xchange@omgreatgod.store% 265 | 3E 266 | SEQUENCE:0 267 | SUMMARY:Weinabend Franken – Castell 268 | DTSTART;TZID=Europe/Berlin:20160616T193000 269 | CREATED:20160603T100150Z 270 | LAST-MODIFIED:20160603T100150Z 271 | END:VEVENT 272 | BEGIN:VEVENT 273 | CREATED:20151209T212646Z 274 | DTEND;TZID=Europe/Berlin:20151209T110000 275 | DTSTAMP:20160401T132640Z 276 | DTSTART;TZID=Europe/Berlin:20151209T100000 277 | LAST-MODIFIED:20160401T132551Z 278 | RRULE:FREQ=YEARLY 279 | SEQUENCE:1 280 | SUMMARY:Geburtstag 281 | UID:09094143-005B-478F-BF37-10316FC9490B 282 | END:VEVENT 283 | BEGIN:VEVENT 284 | CREATED:20160222T073027Z 285 | DTEND;TZID=Europe/Berlin:20160222T173000 286 | DTSTAMP:20160926T150116Z 287 | DTSTART;TZID=Europe/Berlin:20160222T161500 288 | EXDATE;TZID=Europe/Berlin:20160321T161500 289 | EXDATE;TZID=Europe/Berlin:20160328T161500 290 | EXDATE;TZID=Europe/Berlin:20160516T161500 291 | EXDATE;TZID=Europe/Berlin:20160523T161500 292 | EXDATE;TZID=Europe/Berlin:20160801T161500 293 | EXDATE;TZID=Europe/Berlin:20160808T161500 294 | EXDATE;TZID=Europe/Berlin:20160815T161500 295 | EXDATE;TZID=Europe/Berlin:20160822T161500 296 | EXDATE;TZID=Europe/Berlin:20160829T161500 297 | EXDATE;TZID=Europe/Berlin:20160905T161500 298 | EXDATE;TZID=Europe/Berlin:20160912T161500 299 | LAST-MODIFIED:20160926T150115Z 300 | RRULE:FREQ=WEEKLY;UNTIL=20161001T215959Z 301 | SEQUENCE:0 302 | SUMMARY:Kinderturnen 303 | UID:0ED5515F-D6C2-4678-9EB1-8C483A12C410 304 | END:VEVENT 305 | END:VCALENDAR 306 | -------------------------------------------------------------------------------- /icalevents/icalparser.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parse iCal data to Events. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | from datetime import date, datetime, timedelta, tzinfo as _tzinfo 8 | from importlib.metadata import version 9 | from random import randint 10 | from typing import cast 11 | from uuid import uuid4 12 | 13 | from dateutil.rrule import rruleset, rrulestr 14 | from dateutil.tz import UTC, gettz 15 | from icalendar import Calendar, Timezone 16 | from icalendar.prop import vDDDLists, vText 17 | from pytz import timezone 18 | 19 | if version("icalendar") >= "6.0": 20 | from icalendar import Component, use_pytz 21 | from icalendar.timezone.windows_to_olson import WINDOWS_TO_OLSON 22 | 23 | use_pytz() 24 | else: 25 | from icalendar.cal import Component 26 | from icalendar.windows_to_olson import WINDOWS_TO_OLSON 27 | 28 | 29 | def now() -> datetime: 30 | """ 31 | Get current time. 32 | 33 | :return: now as datetime with timezone 34 | """ 35 | return datetime.now(UTC) 36 | 37 | 38 | class Attendee(str): 39 | def __init__(self, address: str) -> None: 40 | self.address = address 41 | 42 | def __repr__(self) -> str: 43 | return self.address.encode("utf-8").decode("ascii") 44 | 45 | @property 46 | def params(self): 47 | return self.address.params 48 | 49 | 50 | class Event: 51 | """ 52 | Represents one event (occurrence in case of reoccurring events). 53 | """ 54 | 55 | def __init__(self) -> None: 56 | """ 57 | Create a new event occurrence. 58 | """ 59 | self.uid: str = "-1" 60 | self.summary: str | None = None 61 | self.description: str | None = None 62 | self.start: datetime | None = None 63 | self.end: datetime | None = None 64 | self.all_day: bool = True 65 | self.transparent: bool = False 66 | self.recurring: bool = False 67 | self.location: str | None = None 68 | self.private: bool = False 69 | self.created: datetime | None = None 70 | self.last_modified: datetime | None = None 71 | self.sequence: str | None = None 72 | self.recurrence_id: datetime | date | None = None 73 | self.attendee: Attendee | list[Attendee] | None = None 74 | self.organizer: str | None = None 75 | self.categories: list[str | None] = [] 76 | self.floating: bool = False 77 | self.status: str | None = None 78 | self.url: str | None = None 79 | self.component: Component | None = None 80 | 81 | def time_left(self, time: datetime | None = None) -> timedelta: 82 | """ 83 | timedelta form now to event. 84 | 85 | :return: timedelta from now 86 | """ 87 | time = time or now() 88 | return self.start - time 89 | 90 | def __lt__(self, other: object) -> bool: 91 | """ 92 | Events are sorted by start time by default. 93 | 94 | :param other: other event 95 | :return: True if start of this event is smaller than other 96 | """ 97 | if not other or not isinstance(other, Event): 98 | raise ValueError( 99 | "Only events can be compared with each other! Other is %s" % type(other) 100 | ) 101 | else: 102 | # start and end can be dates, datetimes and datetimes with timezoneinfo 103 | if type(self.start) is date and type(other.start) is date: 104 | return self.start < other.start 105 | elif type(self.start) is datetime and type(other.start) is datetime: 106 | if self.start.tzinfo == other.start.tzinfo: 107 | return self.start < other.start 108 | else: 109 | return self.start.astimezone(UTC) < other.start.astimezone(UTC) 110 | elif type(self.start) is date and type(other.start) is datetime: 111 | return self.start < other.start.date() 112 | elif type(self.start) is datetime and type(other.start) is date: 113 | return self.start.date() < other.start 114 | 115 | def __str__(self) -> str: 116 | return "%s: %s (%s)" % (self.start, self.summary, self.end - self.start) 117 | 118 | def astimezone(self, tzinfo: _tzinfo) -> Event: 119 | if type(self.start) is datetime: 120 | self.start = self.start.astimezone(tzinfo) 121 | 122 | if type(self.end) is datetime: 123 | self.end = self.end.astimezone(tzinfo) 124 | 125 | return self 126 | 127 | def copy_to( 128 | self, new_start: datetime | None = None, uid: str | None = None 129 | ) -> Event: 130 | """ 131 | Create a new event equal to this with new start date. 132 | 133 | :param new_start: new start date 134 | :param uid: UID of new event 135 | :return: new event 136 | """ 137 | if not new_start: 138 | new_start = self.start 139 | 140 | if not uid: 141 | uid = "%s_%d" % (self.uid, randint(0, 1000000)) 142 | 143 | ne = Event() 144 | ne.component = self.component 145 | ne.summary = self.summary 146 | ne.description = self.description 147 | ne.start = new_start 148 | 149 | if self.end: 150 | duration = self.end - self.start 151 | ne.end = new_start + duration 152 | 153 | ne.all_day = self.all_day 154 | ne.recurring = self.recurring 155 | ne.location = self.location 156 | ne.attendee = self.attendee 157 | ne.organizer = self.organizer 158 | ne.private = self.private 159 | ne.transparent = self.transparent 160 | ne.uid = uid 161 | ne.created = self.created 162 | ne.last_modified = self.last_modified 163 | ne.categories = self.categories 164 | ne.floating = self.floating 165 | ne.status = self.status 166 | ne.url = self.url 167 | 168 | return ne 169 | 170 | 171 | def encode(value: vText | None) -> str | None: 172 | if value is None: 173 | return None 174 | try: 175 | return str(value) 176 | except UnicodeEncodeError: 177 | return str(value.encode("utf-8")) 178 | 179 | 180 | def create_event(component: Component, strict: bool) -> Event: 181 | """ 182 | Create an event from its iCal representation. 183 | 184 | :param component: iCal component 185 | :param strict: 186 | :return: event 187 | """ 188 | 189 | event = Event() 190 | 191 | event.component = component 192 | 193 | event.start = component.get("dtstart").dt 194 | # The RFC specifies that the TZID parameter must be specified for datetime or time 195 | # Otherwise we set a default timezone (if only one is set with VTIMEZONE) or utc 196 | if not strict: 197 | event.floating = ( 198 | type(component.get("dtstart").dt) == date 199 | or component.get("dtstart").dt.tzinfo is None 200 | ) 201 | else: 202 | event.floating = ( 203 | type(component.get("dtstart").dt) == datetime 204 | and component.get("dtstart").dt.tzinfo is None 205 | ) 206 | 207 | if component.get("dtend"): 208 | event.end = component.get("dtend").dt 209 | elif component.get("duration"): # compute implicit end as start + duration 210 | event.end = event.start + component.get("duration").dt 211 | else: # compute implicit end as start + 0 212 | event.end = event.start 213 | 214 | event.summary = encode(component.get("summary")) 215 | event.description = encode(component.get("description")) 216 | event.all_day = type(component.get("dtstart").dt) is date 217 | if component.get("rrule"): 218 | event.recurring = True 219 | event.location = encode(component.get("location")) 220 | 221 | if component.get("attendee"): 222 | attendees = component.get("attendee") 223 | if type(attendees) is list: 224 | event.attendee = [Attendee(attendee) for attendee in attendees] 225 | elif attendees: 226 | event.attendee = Attendee(attendees) 227 | 228 | try: 229 | event.uid = component.get("uid").encode("utf-8").decode("ascii") 230 | except (AttributeError, UnicodeDecodeError): 231 | event.uid = str(uuid4()) # Be nice - treat every event as unique 232 | 233 | if component.get("organizer"): 234 | event.organizer = component.get("organizer").encode("utf-8").decode("ascii") 235 | else: 236 | event.organizer = str(None) 237 | 238 | if component.get("class"): 239 | event_class = component.get("class") 240 | event.private = event_class == "PRIVATE" or event_class == "CONFIDENTIAL" 241 | 242 | if component.get("transp"): 243 | event.transparent = component.get("transp") == "TRANSPARENT" 244 | 245 | if component.get("created"): 246 | event.created = component.get("created").dt 247 | 248 | if component.get("RECURRENCE-ID"): 249 | rid = component.get("RECURRENCE-ID").dt 250 | 251 | # Spec defines that if DTSTART is a date RECURRENCE-ID also is to be interpreted as a date 252 | if type(component.get("dtstart").dt) is date: 253 | event.recurrence_id = date(year=rid.year, month=rid.month, day=rid.day) 254 | else: 255 | event.recurrence_id = rid 256 | 257 | if component.get("last-modified"): 258 | event.last_modified = component.get("last-modified").dt 259 | elif event.created: 260 | event.last_modified = event.created 261 | 262 | # sequence can be 0 - test for None instead 263 | if component.get("sequence") is not None: 264 | event.sequence = component.get("sequence") 265 | 266 | if component.get("categories"): 267 | categories = component.get("categories").cats 268 | encoded_categories = list() 269 | for category in categories: 270 | encoded_categories.append(encode(category)) 271 | event.categories = encoded_categories 272 | 273 | if component.get("status"): 274 | event.status = encode(component.get("status")) 275 | 276 | if component.get("url"): 277 | event.url = encode(component.get("url")) 278 | 279 | return event 280 | 281 | 282 | def parse_events( 283 | content: str, 284 | start: datetime | None = None, 285 | end: datetime | None = None, 286 | default_span: timedelta = timedelta(days=7), 287 | tzinfo: _tzinfo | None = None, 288 | sort: bool = False, 289 | strict: bool = False, 290 | ) -> list[Event]: 291 | """ 292 | Query the events occurring in a given time range. 293 | 294 | :param content: iCal URL/file content as String 295 | :param start: start date for search, default today (in UTC format) 296 | :param end: end date for search (in UTC format) 297 | :param default_span: default query length (one week) 298 | :return: events as list 299 | """ 300 | if not start: 301 | start = now() 302 | 303 | if not end: 304 | end = start + default_span 305 | 306 | if not content: 307 | raise ValueError("Content is invalid!") 308 | 309 | calendar = Calendar.from_ical(content) 310 | 311 | # > Will be deprecated ======================== 312 | # Calendar.from_ical already parses timezones as specified in the ical. 313 | # We can specify a 'default' tz but this is not according to spec. 314 | # Kept this here to verify tests and backward compatibility 315 | 316 | # Keep track of the timezones defined in the calendar 317 | timezones = {} 318 | 319 | # Parse non standard timezone name 320 | if "X-WR-TIMEZONE" in calendar: 321 | x_wr_timezone = str(calendar["X-WR-TIMEZONE"]) 322 | timezones[x_wr_timezone] = get_timezone(x_wr_timezone) 323 | 324 | for c in calendar.walk("VTIMEZONE"): 325 | # we search for VTIMEZONE so we only get Timezone back 326 | c = cast(Timezone, c) 327 | name = str(c["TZID"]) 328 | try: 329 | timezones[name] = c.to_tz() 330 | except IndexError: 331 | # This happens if the VTIMEZONE doesn't 332 | # contain start/end times for daylight 333 | # saving time. Get the system pytz 334 | # value from the name as a fallback. 335 | timezones[name] = timezone(name) 336 | 337 | # If there's exactly one timezone in the file, 338 | # assume it applies globally, otherwise UTC 339 | if len(timezones) == 1: 340 | cal_tz = get_timezone(list(timezones)[0]) 341 | else: 342 | cal_tz = UTC 343 | # < ========================================== 344 | 345 | found: list[Event] = [] 346 | 347 | def is_not_exception(date: datetime) -> bool: 348 | exdate = "%04d%02d%02d" % ( 349 | date.year, 350 | date.month, 351 | date.day, 352 | ) 353 | 354 | return exdate not in exceptions 355 | 356 | for component in calendar.walk(): 357 | exceptions = {} 358 | 359 | if "EXDATE" in component: 360 | # Deal with the fact that sometimes it's a list and 361 | # sometimes it's a singleton 362 | exlist = [] 363 | if isinstance(component["EXDATE"], vDDDLists): 364 | exlist = component["EXDATE"].dts 365 | else: 366 | exlist = component["EXDATE"] 367 | for ex in exlist: 368 | exdate = ex.to_ical().decode("UTF-8") 369 | exceptions[exdate[0:8]] = exdate 370 | 371 | if component.name == "VEVENT": 372 | e = create_event(component, strict) 373 | 374 | # make rule.between happy and provide from, to points in time that have the same format as dtstart 375 | if type(e.start) is date and not e.recurring: 376 | f, t = date(start.year, start.month, start.day), date( 377 | end.year, end.month, end.day 378 | ) 379 | elif type(e.start) is datetime and e.start.tzinfo: 380 | f = ( 381 | datetime( 382 | start.year, 383 | start.month, 384 | start.day, 385 | start.hour, 386 | start.minute, 387 | tzinfo=e.start.tzinfo, 388 | ) 389 | if type(start) == datetime 390 | else datetime( 391 | start.year, start.month, start.day, tzinfo=e.start.tzinfo 392 | ) 393 | ) 394 | t = ( 395 | datetime( 396 | end.year, 397 | end.month, 398 | end.day, 399 | end.hour, 400 | end.minute, 401 | tzinfo=e.start.tzinfo, 402 | ) 403 | if type(end) == datetime 404 | else datetime(end.year, end.month, end.day, tzinfo=e.start.tzinfo) 405 | ) 406 | else: 407 | f = ( 408 | datetime( 409 | start.year, start.month, start.day, start.hour, start.minute 410 | ) 411 | if type(start) == datetime 412 | else datetime(start.year, start.month, start.day) 413 | ) 414 | t = ( 415 | datetime(end.year, end.month, end.day, end.hour, end.minute) 416 | if type(end) == datetime 417 | else datetime(end.year, end.month, end.day) 418 | ) 419 | 420 | if e.recurring: 421 | rule = parse_rrule(component) 422 | # We can not use rule.between because the event has to fit in between https://github.com/jazzband/icalevents/issues/101 423 | for dt in [ 424 | dt 425 | for dt in list(rule.between(f - (end - start), t + (end - start))) 426 | if dt >= f and dt <= t 427 | ]: 428 | # Recompute the start time in the current timezone *on* the 429 | # date of *this* occurrence. This handles the case where the 430 | # recurrence has crossed over the daylight savings time boundary. 431 | if is_not_exception(dt): 432 | if type(dt) is datetime and dt.tzinfo: 433 | ecopy = e.copy_to( 434 | dt.replace(tzinfo=get_timezone(str(dt.tzinfo))), 435 | e.uid, 436 | ) 437 | else: 438 | ecopy = e.copy_to( 439 | dt.date() if type(e.start) is date else dt, e.uid 440 | ) 441 | found.append(ecopy) 442 | 443 | elif e.end >= f and e.start <= t and is_not_exception(e.start): 444 | found.append(e) 445 | 446 | result = found.copy() 447 | 448 | # Remove events that are replaced in ical 449 | for event in found: 450 | if not event.recurrence_id and ( 451 | event.uid, 452 | event.start, 453 | ) in [(f.uid, f.recurrence_id) for f in found]: 454 | result.remove(event) 455 | 456 | # > Will be deprecated ======================== 457 | # We will apply default cal_tz as required by some tests. 458 | # This is just here for backward-compatibility 459 | if not strict: 460 | for event in result: 461 | if type(event.start) is date: 462 | event.start = datetime( 463 | year=event.start.year, 464 | month=event.start.month, 465 | day=event.start.day, 466 | hour=0, 467 | minute=0, 468 | tzinfo=cal_tz, 469 | ) 470 | event.end = datetime( 471 | year=event.end.year, 472 | month=event.end.month, 473 | day=event.end.day, 474 | hour=0, 475 | minute=0, 476 | tzinfo=cal_tz, 477 | ) 478 | elif type(event.start) is datetime: 479 | if event.start.tzinfo: 480 | event.start = event.start.astimezone(cal_tz) 481 | event.end = event.end.astimezone(cal_tz) 482 | else: 483 | event.start = event.start.replace(tzinfo=cal_tz) 484 | event.end = event.end.replace(tzinfo=cal_tz) 485 | 486 | if event.created: 487 | if type(event.created) is date: 488 | event.created = datetime( 489 | year=event.created.year, 490 | month=event.created.month, 491 | day=event.created.day, 492 | hour=0, 493 | minute=0, 494 | tzinfo=cal_tz, 495 | ) 496 | if type(event.created) is datetime: 497 | if event.created.tzinfo: 498 | event.created = event.created.astimezone(cal_tz) 499 | else: 500 | event.created = event.created.replace(tzinfo=cal_tz) 501 | 502 | if event.last_modified: 503 | if type(event.last_modified) is date: 504 | event.last_modified = datetime( 505 | year=event.last_modified.year, 506 | month=event.last_modified.month, 507 | day=event.last_modified.day, 508 | hour=0, 509 | minute=0, 510 | tzinfo=cal_tz, 511 | ) 512 | if type(event.last_modified) is datetime: 513 | if event.last_modified.tzinfo: 514 | event.last_modified = event.last_modified.astimezone(cal_tz) 515 | else: 516 | event.last_modified = event.last_modified.replace(tzinfo=cal_tz) 517 | # < ========================================== 518 | 519 | if sort: 520 | result.sort() 521 | 522 | if tzinfo: 523 | result = [event.astimezone(tzinfo) for event in result] 524 | 525 | return result 526 | 527 | 528 | def parse_rrule(component: Component): 529 | """ 530 | Extract a dateutil.rrule object from an icalendar component. Also includes 531 | the component's dtstart and exdate properties. The rdate and exrule 532 | properties are not yet supported. 533 | 534 | :param component: icalendar component 535 | :return: extracted rrule or rruleset or None 536 | """ 537 | 538 | dtstart = component.get("dtstart").dt 539 | 540 | # component['rrule'] can be both a scalar and a list 541 | rrules = component.get("rrule") 542 | if not isinstance(rrules, list): 543 | rrules = [rrules] 544 | 545 | def conform_until(until, dtstart): 546 | if type(dtstart) is datetime: 547 | # If we have timezone defined adjust for daylight saving time 548 | if type(until) is datetime: 549 | return until + abs( 550 | ( 551 | until.astimezone(dtstart.tzinfo).utcoffset() 552 | if until.tzinfo is not None and dtstart.tzinfo is not None 553 | else None 554 | ) 555 | or timedelta() 556 | ) 557 | 558 | return ( 559 | until.astimezone(UTC) 560 | if type(until) is datetime 561 | else datetime( 562 | year=until.year, month=until.month, day=until.day, tzinfo=UTC 563 | ) 564 | ) + ( 565 | dtstart.tzinfo.utcoffset(dtstart) if dtstart.tzinfo else None 566 | ) or timedelta() 567 | 568 | return until.date() + timedelta(days=1) if type(until) is datetime else until 569 | 570 | for index, rru in enumerate(rrules): 571 | if "UNTIL" in rru: 572 | rrules[index]["UNTIL"] = [ 573 | conform_until(until, dtstart) for until in rrules[index]["UNTIL"] 574 | ] 575 | 576 | rule: rruleset = rrulestr( # type: ignore[assignment] 577 | "\n".join(x.to_ical().decode() for x in rrules), 578 | dtstart=dtstart, 579 | forceset=True, 580 | unfold=True, 581 | ) 582 | 583 | if component.get("exdate"): 584 | # Add exdates to the rruleset 585 | for exd in extract_exdates(component): 586 | if type(dtstart) is date: 587 | if type(exd) is date: 588 | # Always convert exdates to datetimes because rrule.between does not like dates 589 | # https://github.com/dateutil/dateutil/issues/938 590 | rule.exdate(datetime.combine(exd, datetime.min.time())) 591 | else: 592 | rule.exdate(exd.replace(tzinfo=None)) 593 | else: 594 | rule.exdate(exd) 595 | 596 | # TODO: What about rdates and exrules? 597 | if component.get("exrule"): 598 | pass 599 | 600 | if component.get("rdate"): 601 | pass 602 | 603 | return rule 604 | 605 | 606 | def extract_exdates(component: Component) -> list[datetime]: 607 | """ 608 | Compile a list of all exception dates stored with a component. 609 | 610 | :param component: icalendar iCal component 611 | :return: list of exception dates 612 | """ 613 | dates: list[datetime] = [] 614 | exd_prop = component.get("exdate") 615 | if isinstance(exd_prop, list): 616 | for exd_list in exd_prop: 617 | dates.extend(exd.dt for exd in exd_list.dts) 618 | else: # it must be a vDDDLists 619 | dates.extend(exd.dt for exd in exd_prop.dts) 620 | 621 | return dates 622 | 623 | 624 | def get_timezone(tz_name: str) -> _tzinfo | None: 625 | if tz_name in WINDOWS_TO_OLSON: 626 | return gettz(WINDOWS_TO_OLSON[tz_name]) 627 | else: 628 | return gettz(tz_name) 629 | -------------------------------------------------------------------------------- /test/test_icalevents.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from datetime import date, datetime, timedelta 3 | from time import sleep 4 | 5 | import pook 6 | import pytz 7 | from dateutil.tz import UTC, gettz 8 | 9 | from icalevents import icalevents 10 | from icalendar.prop import vText 11 | 12 | 13 | class ICalEventsTests(unittest.TestCase): 14 | @pook.on 15 | def test_events_url_without_content_type(self): 16 | url = "https://raw.githubusercontent.com/jazzband/icalevents/master/test/test_data/basic.ics" 17 | 18 | with open("test/test_data/basic.ics", "rb") as file: 19 | body = file.read() 20 | 21 | pook.get( 22 | url, 23 | reply=200, 24 | response_body=body, 25 | ) 26 | 27 | start = date(2017, 5, 18) 28 | end = date(2017, 5, 19) 29 | 30 | events = icalevents.events(url=url, file=None, start=start, end=end) 31 | 32 | self.assertEqual(len(events), 2, "two events are found") 33 | 34 | @pook.on 35 | def test_events_url_without_charset(self): 36 | url = "https://raw.githubusercontent.com/jazzband/icalevents/master/test/test_data/basic.ics" 37 | 38 | with open("test/test_data/basic.ics", "rb") as file: 39 | body = file.read() 40 | 41 | pook.get( 42 | url, 43 | reply=200, 44 | response_headers={"Content-Type": "text/calendar"}, 45 | response_body=body, 46 | ) 47 | 48 | start = date(2017, 5, 18) 49 | end = date(2017, 5, 19) 50 | 51 | events = icalevents.events(url=url, file=None, start=start, end=end) 52 | 53 | self.assertEqual(len(events), 2, "two events are found") 54 | 55 | @pook.on 56 | def test_events_url_with_utf8(self): 57 | url = "https://raw.githubusercontent.com/jazzband/icalevents/master/test/test_data/basic.ics" 58 | 59 | with open("test/test_data/basic.ics", "rb") as file: 60 | body = file.read() 61 | 62 | pook.get( 63 | url, 64 | reply=200, 65 | response_headers={"Content-Type": "text/calendar; charset=UTF-8"}, 66 | response_body=body, 67 | ) 68 | 69 | start = date(2017, 5, 18) 70 | end = date(2017, 5, 19) 71 | 72 | events = icalevents.events(url=url, file=None, start=start, end=end) 73 | 74 | self.assertEqual(len(events), 2, "two events are found") 75 | 76 | @pook.on 77 | def test_events_url_with_latin1(self): 78 | url = "https://raw.githubusercontent.com/jazzband/icalevents/master/test/test_data/basic_latin1.ics" 79 | 80 | with open("test/test_data/basic_latin1.ics", "rb") as file: 81 | body = file.read() 82 | 83 | pook.get( 84 | url, 85 | reply=200, 86 | response_headers={"Content-Type": "text/calendar; charset=ISO-8859-1"}, 87 | response_body=body, 88 | ) 89 | 90 | start = date(2017, 5, 18) 91 | end = date(2017, 5, 19) 92 | 93 | events = icalevents.events(url=url, file=None, start=start, end=end) 94 | 95 | self.assertEqual(len(events), 2, "two events are found") 96 | 97 | @pook.on 98 | def test_exception_on_empty_events_url(self): 99 | url = "https://raw.githubusercontent.com/jazzband/icalevents/master/test/test_data/basic.ics" 100 | 101 | pook.get( 102 | url, 103 | reply=500, 104 | ) 105 | 106 | self.assertRaises(ConnectionError, icalevents.events, url=url) 107 | 108 | def test_events_start(self): 109 | ical = "test/test_data/basic.ics" 110 | start = date(2017, 5, 16) 111 | 112 | evs = icalevents.events(url=None, file=ical, start=start) 113 | 114 | self.assertEqual(len(evs), 3, "three events are found") 115 | 116 | def test_events(self): 117 | ical = "test/test_data/basic.ics" 118 | start = date(2017, 5, 18) 119 | end = date(2017, 5, 19) 120 | 121 | evs = icalevents.events(url=None, file=ical, start=start, end=end) 122 | 123 | self.assertEqual(len(evs), 2, "two events are found") 124 | 125 | def test_events_duration(self): 126 | ical = "test/test_data/duration.ics" 127 | start = date(2018, 1, 1) 128 | end = date(2018, 2, 1) 129 | 130 | evs = icalevents.events(file=ical, start=start, end=end) 131 | 132 | e1 = evs[0] 133 | self.assertEqual(e1.start.day, 10, "explicit event start") 134 | self.assertEqual(e1.end.day, 13, "implicit event end") 135 | 136 | e2 = evs[1] 137 | self.assertEqual(e2.start.hour, 10, "explicit event start") 138 | self.assertEqual(e2.end.hour, 13, "implicit event end") 139 | 140 | e3 = evs[2] 141 | self.assertEqual(e3.start.hour, 12, "explicit event start") 142 | self.assertEqual(e3.end.hour, 12, "implicit event end") 143 | 144 | def test_events_recurring(self): 145 | ical = "test/test_data/recurring.ics" 146 | start = date(2018, 10, 15) 147 | end = date(2018, 11, 15) 148 | 149 | evs = icalevents.events(file=ical, start=start, end=end, sort=False) 150 | 151 | e1 = evs[1] 152 | self.assertEqual(e1.start.hour, 10, "check time with DST") 153 | self.assertEqual( 154 | timedelta(seconds=7200), 155 | e1.start.tzinfo.utcoffset(e1.start), 156 | "check UTC offset with DST", 157 | ) 158 | 159 | e2 = evs[2] 160 | self.assertEqual(e2.start.hour, 10, "check time without DST") 161 | self.assertEqual( 162 | timedelta(seconds=3600), 163 | e2.start.tzinfo.utcoffset(e2.start), 164 | "check UTC offset without DST", 165 | ) 166 | 167 | self.assertEqual(e2.start.day, 5, "Check observance of exdate.") 168 | 169 | def test_events_exdates(self): 170 | ical = "test/test_data/recurring.ics" 171 | start = date(2018, 6, 1) 172 | end = date(2018, 6, 30) 173 | 174 | evs = icalevents.events(file=ical, start=start, end=end) 175 | 176 | self.assertEqual(evs[0].start.day, 1, "check first recurrence.") 177 | self.assertEqual(evs[1].start.day, 15, "check first exdate.") 178 | self.assertEqual(evs[2].start.day, 29, "check second exdate.") 179 | 180 | def test_events_all_day_recurring(self): 181 | ical = "test/test_data/recurring.ics" 182 | start = date(2018, 10, 30) 183 | end = date(2018, 10, 31) 184 | 185 | evs = icalevents.events(file=ical, start=start, end=end) 186 | 187 | event_set = icalevents.events(url=None, file=ical, start=start, end=end) 188 | ev = event_set[0] 189 | 190 | self.assertEqual(len(event_set), 1) 191 | self.assertEqual(ev.summary, "Recurring All-day Event") 192 | self.assertEqual(ev.description, "All-day event recurring on tuesday each week") 193 | self.assertTrue( 194 | ev.all_day, "Recurring All-day Event's first instance is an all-day event" 195 | ) 196 | 197 | start_2nd_instance = date(2018, 11, 6) 198 | end_2nd_instance = date(2018, 11, 7) 199 | 200 | event_set2 = icalevents.events( 201 | url=None, file=ical, start=start_2nd_instance, end=end_2nd_instance 202 | ) 203 | ev_2 = event_set2[0] 204 | 205 | self.assertEqual(len(event_set2), 1) 206 | self.assertEqual(ev_2.summary, "Recurring All-day Event") 207 | self.assertEqual( 208 | ev_2.description, "All-day event recurring on tuesday each week" 209 | ) 210 | self.assertTrue( 211 | ev_2.all_day, 212 | "Recurring All-day Event's second instance is an all-day event", 213 | ) 214 | 215 | def test_events_rrule_until_all_day_ms(self): 216 | ical = "test/test_data/rrule_until_all_day_ms.ics" 217 | start = date(2021, 1, 1) 218 | end = date(2022, 1, 1) 219 | 220 | evs = icalevents.events(file=ical, start=start, end=end) 221 | ev_0 = evs[0] 222 | 223 | self.assertEqual( 224 | len(evs), 6, "Seven events and one is excluded" 225 | ) # rrule_until_all_day_ms has one exdate (EXDATE;TZID=W. Europe Standard Time:20210430T000000) 226 | self.assertEqual( 227 | ev_0.start, datetime(2021, 3, 19, 00, 0, 0, tzinfo=gettz("Europe/Berlin")) 228 | ) 229 | self.assertTrue(ev_0.recurring, "Recurring all day event") 230 | self.assertEqual(ev_0.summary, "Away") 231 | 232 | def test_events_rrule_until_all_day_google(self): 233 | ical = "test/test_data/rrule_until_all_day_google.ics" 234 | start = date(2021, 1, 1) 235 | end = date(2022, 1, 1) 236 | 237 | evs = icalevents.events(file=ical, start=start, end=end, sort=True) 238 | ev = evs[0] 239 | 240 | self.assertEqual(len(evs), 3) 241 | self.assertEqual( 242 | ev.start, datetime(2021, 3, 24, 00, 0, 0, tzinfo=gettz("Europe/Zurich")) 243 | ) 244 | self.assertTrue(ev.all_day, "All day event") 245 | self.assertEqual(ev.summary, "Busy") 246 | 247 | def test_events_rrule_until_only_date(self): 248 | ical = "test/test_data/rrule_until_only_date.ics" 249 | start = date(2021, 9, 29) 250 | end = date(2021, 10, 19) 251 | evs = icalevents.events(file=ical, start=start, end=end) 252 | self.assertEqual(len(evs), 8) 253 | self.assertEqual( 254 | evs[0].start, 255 | datetime(2021, 9, 29, 13, 0, 0, 0, tzinfo=gettz("America/Boise")), 256 | ) 257 | self.assertEqual( 258 | evs[-1].start, 259 | datetime(2021, 10, 18, 13, 0, 0, 0, tzinfo=gettz("America/Boise")), 260 | ) 261 | 262 | def test_events_rrule_until(self): 263 | ical = "test/test_data/rrule_until.ics" 264 | start = date(2019, 4, 2) 265 | end = date(2019, 4, 3) 266 | 267 | evs = icalevents.events(file=ical, start=start, end=end) 268 | 269 | self.assertEqual(len(evs), 2) 270 | self.assertTrue(evs[0].recurring) 271 | self.assertEqual(evs[0].summary, "Recurring All-day Event") 272 | self.assertTrue(evs[1].recurring) 273 | self.assertEqual(evs[1].summary, "Daily lunch event") 274 | 275 | def test_event_attributes(self): 276 | ical = "test/test_data/basic.ics" 277 | start = date(2017, 7, 12) 278 | end = date(2017, 7, 13) 279 | 280 | ev = icalevents.events(url=None, file=ical, start=start, end=end)[0] 281 | 282 | self.assertEqual(ev.summary, "graue Restmülltonne") 283 | self.assertEqual(ev.description, "graue Restmülltonne nicht vergessen!") 284 | self.assertTrue(ev.all_day) 285 | 286 | def test_event_recurring_attribute(self): 287 | ical = "test/test_data/basic.ics" 288 | start = date(2017, 7, 12) 289 | end = date(2017, 7, 13) 290 | 291 | ev = icalevents.events(url=None, file=ical, start=start, end=end)[0] 292 | self.assertFalse(ev.recurring, "check recurring=False for non recurring event") 293 | 294 | ical = "test/test_data/recurring.ics" 295 | start = date(2018, 10, 15) 296 | end = date(2018, 11, 15) 297 | 298 | evs = icalevents.events(file=ical, start=start, end=end) 299 | 300 | e1 = evs[1] 301 | e2 = evs[2] 302 | self.assertTrue(e1.recurring, "check recurring=True for recurring event (1)") 303 | self.assertTrue(e2.recurring, "check recurring=True for recurring event (2)") 304 | 305 | def test_events_async_url(self): 306 | url = "https://raw.githubusercontent.com/jazzband/icalevents/master/test/test_data/basic.ics" 307 | start = date(2017, 5, 18) 308 | end = date(2017, 5, 19) 309 | key = "basic" 310 | 311 | icalevents.events_async(key, url=url, file=None, start=start, end=end) 312 | 313 | sleep(4) 314 | 315 | self.assertTrue(icalevents.all_done(key), "request is finished") 316 | self.assertEqual(len(icalevents.latest_events(key)), 2, "two events are found") 317 | 318 | def test_events_async(self): 319 | ical = "test/test_data/basic.ics" 320 | start = date(2017, 5, 18) 321 | end = date(2017, 5, 19) 322 | key = "basic" 323 | 324 | icalevents.events_async(key, url=None, file=ical, start=start, end=end) 325 | 326 | sleep(4) 327 | 328 | self.assertTrue(icalevents.all_done(key), "request is finished") 329 | self.assertEqual(len(icalevents.latest_events(key)), 2, "two events are found") 330 | 331 | def test_request_data(self): 332 | ical = "test/test_data/basic.ics" 333 | start = date(2017, 5, 18) 334 | end = date(2017, 5, 19) 335 | key = "basic" 336 | 337 | icalevents.request_data( 338 | key, 339 | url=None, 340 | file=ical, 341 | string_content=None, 342 | start=start, 343 | end=end, 344 | fix_apple=False, 345 | ) 346 | 347 | self.assertTrue(icalevents.all_done(key), "request is finished") 348 | self.assertEqual(len(icalevents.latest_events(key)), 2, "two events are found") 349 | 350 | def test_string_data(self): 351 | ical = "test/test_data/basic.ics" 352 | with open(ical, mode="rb") as f: 353 | raw_data = f.read() 354 | 355 | for stest, string_content in [ 356 | ("as bytes", raw_data), 357 | ("as str", raw_data.decode()), 358 | ]: 359 | with self.subTest(stest): 360 | start = date(2017, 5, 18) 361 | end = date(2017, 5, 19) 362 | key = "basic" 363 | 364 | icalevents.request_data( 365 | key, 366 | url=None, 367 | file=None, 368 | string_content=string_content, 369 | start=start, 370 | end=end, 371 | fix_apple=False, 372 | ) 373 | 374 | self.assertTrue(icalevents.all_done(key), "request is finished") 375 | self.assertEqual( 376 | len(icalevents.latest_events(key)), 2, "two events are found" 377 | ) 378 | 379 | def test_events_no_description(self): 380 | ical = "test/test_data/no_description.ics" 381 | start = date(2018, 10, 15) 382 | end = date(2018, 11, 15) 383 | 384 | e1 = icalevents.events(file=ical, start=start, end=end)[0] 385 | 386 | self.assertIsNone(e1.description) 387 | self.assertIsNone(e1.summary) 388 | self.assertIsNone(e1.location) 389 | 390 | def test_event_created_last_modified(self): 391 | ical = "test/test_data/created_last_modified.ics" 392 | start = date(2017, 7, 12) 393 | end = date(2017, 7, 15) 394 | 395 | events = icalevents.events(url=None, file=ical, start=start, end=end) 396 | 397 | self.assertEqual(events[0].created, datetime(2017, 1, 3, 7, 4, 1, tzinfo=UTC)) 398 | self.assertEqual( 399 | events[0].last_modified, datetime(2017, 7, 11, 14, 0, 50, tzinfo=UTC) 400 | ) 401 | 402 | self.assertEqual(events[1].created, datetime(2017, 1, 4, 8, 4, 1, tzinfo=UTC)) 403 | self.assertEqual( 404 | events[1].last_modified, datetime(2017, 1, 4, 8, 4, 1, tzinfo=UTC) 405 | ) 406 | 407 | self.assertIsNone(events[2].created) 408 | self.assertIsNone(events[2].last_modified) 409 | 410 | def test_event_categories(self): 411 | ical = "test/test_data/categories_test.ics" 412 | start = date(2020, 11, 10) 413 | end = date(2020, 11, 19) 414 | events = icalevents.events(url=None, file=ical, start=start, end=end) 415 | self.assertEqual( 416 | events[0].categories, ["In19-S04-IT2403"], "event 1 is not equal" 417 | ) 418 | self.assertEqual( 419 | events[1].categories, 420 | ["In19-S04-IT2406", "In19-S04-IT2405"], 421 | "event 2 is not equal", 422 | ) 423 | 424 | def test_google_timezone(self): 425 | ical = "test/test_data/google_tz.ics" 426 | start = date(2021, 1, 1) 427 | end = date(2021, 12, 31) 428 | 429 | evs = icalevents.events(file=ical, start=start, end=end) 430 | 431 | e1 = evs[0] 432 | self.assertEqual(e1.start.hour, 0, "check start of the day") 433 | self.assertEqual( 434 | e1.start.tzinfo, gettz("Europe/Zurich"), "check tz as specified in calendar" 435 | ) 436 | 437 | def test_ms_timezone(self): 438 | ical = "test/test_data/ms_tz.ics" 439 | start = date(2021, 1, 1) 440 | end = date(2021, 12, 31) 441 | 442 | evs = icalevents.events(file=ical, start=start, end=end) 443 | 444 | e1 = evs[0] 445 | self.assertEqual(e1.start.hour, 0, "check start of the day") 446 | self.assertEqual( 447 | e1.start.tzinfo, gettz("Europe/Berlin"), "check tz as specified in calendar" 448 | ) 449 | 450 | def test_recurence_id_ms(self): 451 | ical = "test/test_data/recurrenceid_ms.ics" 452 | start = date(2021, 1, 1) 453 | end = date(2021, 12, 31) 454 | 455 | evs = icalevents.events(file=ical, start=start, end=end, sort=True) 456 | 457 | self.assertEqual(len(evs), 42, "42 events in total - one was moved") 458 | 459 | def test_recurence_id_ms_moved(self): 460 | ical = "test/test_data/recurrenceid_ms.ics" 461 | start = date(2021, 4, 8) 462 | end = date(2021, 4, 10) 463 | 464 | evs = icalevents.events(file=ical, start=start, end=end, sort=True) 465 | self.assertEqual(evs[0].start.day, 10) 466 | 467 | self.assertEqual(len(evs), 1, "only one event - it was moved") 468 | 469 | def test_recurence_id_google(self): 470 | ical = "test/test_data/recurrenceid_google.ics" 471 | start = date(2021, 1, 1) 472 | end = date(2021, 12, 31) 473 | 474 | evs = icalevents.events(file=ical, start=start, end=end) 475 | 476 | self.assertEqual(len(evs), 4, "4 events in total") 477 | 478 | def test_cest(self): 479 | ical = "test/test_data/cest.ics" 480 | start = date(2010, 1, 1) 481 | end = date(2023, 12, 31) 482 | 483 | evs = icalevents.events(file=ical, start=start, end=end) 484 | 485 | self.assertEqual(len(evs), 239, "239 events in total") 486 | 487 | def test_cest_2021_02(self): 488 | ical = "test/test_data/cest.ics" 489 | start = date(2021, 2, 1) 490 | end = date(2021, 2, 28) 491 | 492 | evs = icalevents.events(file=ical, start=start, end=end) 493 | self.assertEqual(len(evs), 17, "17 in february") 494 | 495 | def test_cest_2021_03(self): 496 | ical = "test/test_data/cest.ics" 497 | start = date(2021, 3, 1) 498 | end = date(2021, 3, 31) 499 | 500 | evs = icalevents.events(file=ical, start=start, end=end) 501 | self.assertEqual(len(evs), 30, "30 in march") 502 | 503 | def test_cest_2021_04(self): 504 | ical = "test/test_data/cest.ics" 505 | start = date(2021, 4, 1) 506 | end = date(2021, 5, 1) 507 | 508 | tz = gettz("Europe/Zurich") 509 | events = icalevents.events( 510 | file=ical, start=start, end=end, tzinfo=tz, sort=True, strict=True 511 | ) 512 | # self.assertEqual(events[2].start, 2) 513 | 514 | times = [ 515 | ((2021, 4, 1, 14, 0), (2021, 4, 1, 14, 30)), 516 | ((2021, 4, 1, 15, 30), (2021, 4, 1, 17, 0)), 517 | ((2021, 4, 2), (2021, 4, 3)), 518 | ((2021, 4, 5, 16, 00), (2021, 4, 5, 17, 0)), 519 | ((2021, 4, 7), (2021, 4, 8)), 520 | ((2021, 4, 8, 11, 0), (2021, 4, 8, 12, 0)), 521 | ((2021, 4, 8, 14, 30), (2021, 4, 8, 15, 0)), 522 | ((2021, 4, 8, 15, 0), (2021, 4, 8, 15, 30)), 523 | ((2021, 4, 9), (2021, 4, 10)), 524 | ((2021, 4, 12, 11, 0), (2021, 4, 12, 11, 30)), 525 | ((2021, 4, 12, 16, 0), (2021, 4, 12, 17, 0)), 526 | ((2021, 4, 14), (2021, 4, 15)), 527 | ((2021, 4, 15, 12, 0), (2021, 4, 15, 13, 0)), 528 | ((2021, 4, 15, 15, 0), (2021, 4, 15, 15, 30)), 529 | ((2021, 4, 16), (2021, 4, 17)), 530 | ((2021, 4, 19, 16, 0), (2021, 4, 19, 17, 0)), 531 | ((2021, 4, 21), (2021, 4, 22)), 532 | ((2021, 4, 22, 11, 0), (2021, 4, 22, 12, 0)), 533 | ((2021, 4, 22, 14, 45), (2021, 4, 22, 15, 15)), 534 | ((2021, 4, 23), (2021, 4, 24)), 535 | ((2021, 4, 26, 16, 0), (2021, 4, 26, 17, 0)), 536 | ((2021, 4, 28), (2021, 4, 29)), 537 | ((2021, 4, 29, 9, 0), (2021, 4, 29, 11, 0)), 538 | ((2021, 4, 29, 11, 0), (2021, 4, 29, 11, 30)), 539 | ((2021, 4, 29, 14, 15), (2021, 4, 29, 15, 00)), 540 | ((2021, 4, 29, 15, 0), (2021, 4, 29, 15, 30)), 541 | ((2021, 4, 30), (2021, 5, 1)), 542 | ] 543 | 544 | for index, time in enumerate(times): 545 | self.assertEqual( 546 | events[index].start, 547 | date(*time[0]) if len(time[0]) == 3 else datetime(*time[0], tzinfo=tz), 548 | ) 549 | self.assertEqual( 550 | events[index].end, 551 | date(*time[1]) if len(time[1]) == 3 else datetime(*time[1], tzinfo=tz), 552 | ) 553 | 554 | self.assertEqual(len(events), len(times)) 555 | 556 | def test_cest_2021_05(self): 557 | ical = "test/test_data/cest.ics" 558 | start = date(2021, 5, 1) 559 | end = date(2021, 6, 1) 560 | 561 | tz = gettz("Europe/Zurich") 562 | events = icalevents.events( 563 | file=ical, start=start, end=end, tzinfo=tz, sort=True, strict=True 564 | ) 565 | 566 | times = [ 567 | ((2021, 5, 3, 16, 0), (2021, 5, 3, 17, 0)), 568 | ((2021, 5, 5), (2021, 5, 6)), 569 | ((2021, 5, 6, 11, 0), (2021, 5, 6, 12, 0)), 570 | ((2021, 5, 6, 15, 0), (2021, 5, 6, 15, 30)), 571 | ((2021, 5, 7), (2021, 5, 8)), 572 | ((2021, 5, 10, 16, 0), (2021, 5, 10, 17, 0)), 573 | ((2021, 5, 12), (2021, 5, 13)), 574 | ((2021, 5, 13, 15, 0), (2021, 5, 13, 15, 30)), 575 | ((2021, 5, 14), (2021, 5, 15)), 576 | ((2021, 5, 17, 16, 0), (2021, 5, 17, 17, 0)), 577 | ((2021, 5, 19), (2021, 5, 20)), 578 | ((2021, 5, 20, 11, 0), (2021, 5, 20, 12, 0)), 579 | ((2021, 5, 20, 12, 0), (2021, 5, 20, 13, 0)), 580 | ((2021, 5, 20, 15, 0), (2021, 5, 20, 15, 30)), 581 | ((2021, 5, 21), (2021, 5, 22)), 582 | ((2021, 5, 24, 16, 0), (2021, 5, 24, 17, 0)), 583 | ((2021, 5, 26), (2021, 5, 27)), 584 | ((2021, 5, 27, 15, 0), (2021, 5, 27, 15, 30)), 585 | ((2021, 5, 28), (2021, 5, 29)), 586 | ((2021, 5, 31, 16, 0), (2021, 5, 31, 17, 0)), 587 | ] 588 | 589 | for index, time in enumerate(times): 590 | self.assertEqual( 591 | events[index].start, 592 | date(*time[0]) if len(time[0]) == 3 else datetime(*time[0], tzinfo=tz), 593 | ) 594 | self.assertEqual( 595 | events[index].end, 596 | date(*time[1]) if len(time[1]) == 3 else datetime(*time[1], tzinfo=tz), 597 | ) 598 | 599 | self.assertEqual(len(events), len(times)) 600 | 601 | def test_cest_2021_06(self): 602 | ical = "test/test_data/cest.ics" 603 | start = date(2021, 6, 1) 604 | end = date(2021, 6, 30) 605 | 606 | evs = icalevents.events(file=ical, start=start, end=end) 607 | self.assertEqual(len(evs), 11, "11 in june") 608 | 609 | def test_cest_2021_07(self): 610 | ical = "test/test_data/cest.ics" 611 | start = date(2021, 7, 1) 612 | end = date(2021, 7, 31) 613 | 614 | evs = icalevents.events(file=ical, start=start, end=end) 615 | self.assertEqual(len(evs), 1, "1 in july") 616 | 617 | def test_cest_1(self): 618 | ical = "test/test_data/cest_every_day_for_one_year.ics" 619 | start = date(2020, 1, 1) 620 | end = date(2024, 12, 31) 621 | 622 | evs = icalevents.events(file=ical, start=start, end=end) 623 | 624 | self.assertEqual( 625 | len(evs), 626 | 366, 627 | "366 events in total - one year + 1 (2021-11-11 to 2022-11-11)", 628 | ) 629 | 630 | def test_cest_2(self): 631 | ical = "test/test_data/cest_every_second_day_for_one_year.ics" 632 | start = date(2020, 1, 1) 633 | end = date(2024, 12, 31) 634 | 635 | evs = icalevents.events(file=ical, start=start, end=end) 636 | self.assertEqual( 637 | len(evs), 183, "183 events in total - one year (2021-11-11 to 2022-11-11)" 638 | ) 639 | 640 | def test_cest_3(self): 641 | ical = "test/test_data/cest_with_deleted.ics" 642 | start = date(2020, 1, 1) 643 | end = date(2024, 12, 31) 644 | 645 | evs = icalevents.events(file=ical, start=start, end=end) 646 | self.assertEqual( 647 | len(evs), 3, "3 events in total - 5 events in rrule but 2 deleted" 648 | ) 649 | 650 | def test_transparent(self): 651 | ical = "test/test_data/transparent.ics" 652 | start = date(2021, 1, 1) 653 | end = date(2021, 12, 31) 654 | 655 | [e1, e2] = icalevents.events(file=ical, start=start, end=end) 656 | 657 | self.assertTrue(e1.transparent, "respect transparency") 658 | self.assertFalse(e2.transparent, "respect opaqueness") 659 | 660 | def test_status_and_url(self): 661 | ical = "test/test_data/status_and_url.ics" 662 | start = date(2018, 10, 30) 663 | end = date(2018, 10, 31) 664 | 665 | [ev1, ev2, ev3, ev4, ev5] = icalevents.events(file=ical, start=start, end=end) 666 | self.assertEqual(ev1.status, "TENTATIVE") 667 | self.assertIsNone(ev1.url) 668 | self.assertEqual(ev2.status, "CONFIRMED") 669 | self.assertEqual(ev2.url, "https://example.com/") 670 | self.assertEqual(ev3.status, "CANCELLED") 671 | self.assertEqual(ev4.status, "CANCELLED") 672 | self.assertIsNone(ev5.status) 673 | 674 | def test_recurrence_tz(self): 675 | ical = "test/test_data/recurrence_tz.ics" 676 | start = datetime(2021, 10, 24, 00, 0, 0, tzinfo=gettz("Australia/Sydney")) 677 | end = datetime(2021, 10, 26, 00, 0, 0, tzinfo=gettz("Australia/Sydney")) 678 | 679 | [e1] = icalevents.events(file=ical, start=start, end=end) 680 | expect = datetime(2021, 10, 24, 9, 0, 0, tzinfo=gettz("Australia/Sydney")) 681 | self.assertEqual( 682 | e1.start, expect, "Recurring event matches event in ical (Issue #89)" 683 | ) 684 | 685 | def test_attenddees_have_params(self): 686 | ical = "test/test_data/response.ics" 687 | start = date(2021, 1, 1) 688 | end = date(2021, 12, 31) 689 | 690 | [e1] = icalevents.events(file=ical, start=start, end=end) 691 | 692 | self.assertEqual(e1.attendee.params["PARTSTAT"], "DECLINED", "add paarams") 693 | self.assertEqual( 694 | e1.attendee, "mailto:calendar@gmail.com", "still is like a string" 695 | ) 696 | 697 | def test_attenddees_can_be_multiple(self): 698 | ical = "test/test_data/multi_attendee_response.ics" 699 | start = date(2021, 1, 1) 700 | end = date(2021, 12, 31) 701 | 702 | [e1] = icalevents.events(file=ical, start=start, end=end) 703 | 704 | self.assertEqual(e1.attendee[0].params["PARTSTAT"], "DECLINED", "add paarams") 705 | self.assertEqual( 706 | e1.attendee[0], "mailto:calendar@gmail.com", "we have a list of attendees" 707 | ) 708 | self.assertEqual( 709 | e1.attendee[1], 710 | "mailto:calendar@microsoft.com", 711 | "we have more than one attendee", 712 | ) 713 | 714 | def test_floating(self): 715 | ical = "test/test_data/floating.ics" 716 | start = date(2021, 1, 1) 717 | end = date(2021, 12, 31) 718 | 719 | [e1, e2] = icalevents.events(file=ical, start=start, end=end) 720 | 721 | self.assertFalse(e1.transparent, "respect transparency") 722 | self.assertEqual(e1.start.hour, 6, "check start of the day") 723 | self.assertEqual(e1.end.hour, 14, "check end of the day") 724 | self.assertFalse(e1.floating, "respect floating time") 725 | self.assertEqual(e1.start.tzinfo, UTC, "check tz as default utc") 726 | 727 | self.assertTrue(e2.transparent, "respect transparency") 728 | self.assertEqual(e2.start.hour, 0, "check start of the day") 729 | self.assertEqual(e2.end.hour, 0, "check end of the day") 730 | self.assertTrue(e2.floating, "respect floating time") 731 | self.assertEqual(e2.start.tzinfo, UTC, "check tz as default utc") 732 | 733 | def test_floating_strict(self): 734 | ical = "test/test_data/floating.ics" 735 | start = date(2021, 1, 1) 736 | end = date(2021, 12, 31) 737 | 738 | [e1, e2] = icalevents.events(file=ical, start=start, end=end, strict=True) 739 | 740 | self.assertFalse(e1.transparent, "respect transparency") 741 | self.assertEqual( 742 | e1.start.astimezone(pytz.utc).hour, 6, "check start of the day" 743 | ) 744 | self.assertEqual(e1.end.astimezone(pytz.utc).hour, 14, "check end of the day") 745 | self.assertFalse(e1.floating, "respect floating time") 746 | self.assertEqual(e1.start.tzname(), "CEST", "check tz as specified in calendar") 747 | 748 | self.assertTrue(e2.transparent, "respect transparency") 749 | self.assertEqual(e2.start, date(2021, 10, 13), "check start of the day") 750 | self.assertEqual(e2.end, date(2021, 10, 14), "check end of the day") 751 | self.assertFalse(e2.floating, "dates are not floating floating time") 752 | 753 | def test_non_floating(self): 754 | ical = "test/test_data/non_floating.ics" 755 | start = date(2021, 1, 1) 756 | end = date(2021, 12, 31) 757 | 758 | [e1, e2] = icalevents.events(file=ical, start=start, end=end) 759 | 760 | self.assertFalse(e1.transparent, "respect transparency") 761 | self.assertEqual(e1.start.hour, 8, "check start of the day") 762 | self.assertEqual(e1.end.hour, 16, "check end of the day") 763 | self.assertFalse(e1.floating, "respect floating time") 764 | self.assertEqual( 765 | e1.start.tzinfo, gettz("Europe/Zurich"), "check tz as specified in calendar" 766 | ) 767 | 768 | self.assertTrue(e2.transparent, "respect transparency") 769 | self.assertEqual(e2.start.hour, 0, "check start of the day") 770 | self.assertEqual(e2.end.hour, 0, "check end of the day") 771 | self.assertTrue(e2.floating, "respect floating time") 772 | self.assertEqual( 773 | e2.start.tzinfo, gettz("Europe/Zurich"), "check tz as specified in calendar" 774 | ) 775 | 776 | def test_non_floating_strict(self): 777 | ical = "test/test_data/non_floating.ics" 778 | start = date(2021, 1, 1) 779 | end = date(2021, 12, 31) 780 | 781 | [e1, e2] = icalevents.events(file=ical, start=start, end=end, strict=True) 782 | 783 | self.assertFalse(e1.transparent, "respect transparency") 784 | self.assertEqual(e1.start.hour, 8, "check start of the day") 785 | self.assertEqual(e1.end.hour, 16, "check end of the day") 786 | self.assertFalse(e1.floating, "respect floating time") 787 | self.assertEqual(e1.start.tzname(), "CEST", "check tz as specified in calendar") 788 | 789 | self.assertTrue(e2.transparent, "respect transparency") 790 | self.assertFalse(e2.floating, "respect floating time") 791 | self.assertTrue(e2.all_day, "it is an all day event") 792 | self.assertEqual(e2.start, date(2021, 10, 13), "it is an all day event") 793 | self.assertEqual(e2.end, date(2021, 10, 14), "it is an all day event") 794 | 795 | def test_recurring_override(self): 796 | ical = "test/test_data/recurring_override.ics" 797 | start = date(2021, 11, 23) 798 | end = date(2021, 11, 24) 799 | 800 | [e0, e1, e2] = icalevents.events(file=ical, start=start, end=end) 801 | 802 | # Here all dates are in utc because the .ics has two timezones and this causes a transformation 803 | self.assertEqual(e0.start, datetime(2021, 11, 23, 9, 0, tzinfo=UTC)) 804 | self.assertEqual(e1.start, datetime(2021, 11, 23, 10, 45, tzinfo=UTC)) 805 | self.assertEqual( 806 | e2.start, 807 | datetime(2021, 11, 23, 13, 0, tzinfo=UTC), 808 | "moved 1 hour from 12:00 to 13:00", 809 | ) 810 | 811 | def test_recurring_tz_passover_fall(self): 812 | ical = "test/test_data/recurring_override.ics" 813 | start = date(2021, 8, 30) 814 | end = date(2021, 9, 18) 815 | 816 | tz = gettz("Europe/Zurich") 817 | events = icalevents.events( 818 | file=ical, start=start, end=end, tzinfo=tz, sort=True, strict=True 819 | ) 820 | 821 | times = [ 822 | ((2021, 8, 30, 8, 0), (2021, 8, 30, 17, 0)), 823 | ((2021, 8, 30, 9, 30), (2021, 8, 30, 10, 0)), 824 | ((2021, 8, 31, 10, 0), (2021, 8, 31, 10, 30)), 825 | ((2021, 8, 31, 10, 15), (2021, 8, 31, 10, 45)), 826 | ((2021, 8, 31, 13, 15), (2021, 8, 31, 14, 0)), 827 | ((2021, 9, 1, 9, 0), (2021, 9, 1, 10, 0)), 828 | ((2021, 9, 1, 9, 30), (2021, 9, 1, 10, 0)), 829 | ((2021, 9, 1, 12, 0), (2021, 9, 1, 13, 0)), 830 | ((2021, 9, 2, 10, 0), (2021, 9, 2, 10, 30)), 831 | ((2021, 9, 3, 8, 0), (2021, 9, 3, 8, 30)), 832 | ((2021, 9, 3, 9, 0), (2021, 9, 3, 9, 30)), 833 | ((2021, 9, 3, 9, 30), (2021, 9, 3, 10, 0)), 834 | ((2021, 9, 3, 15, 30), (2021, 9, 3, 16, 0)), 835 | ((2021, 9, 3, 17, 30), (2021, 9, 3, 19, 0)), 836 | ((2021, 9, 6, 8, 0), (2021, 9, 6, 17, 0)), 837 | ((2021, 9, 6, 9, 30), (2021, 9, 6, 10, 0)), 838 | ((2021, 9, 7, 9, 0), (2021, 9, 7, 12, 0)), 839 | ((2021, 9, 7, 9, 0), (2021, 9, 7, 12, 0)), 840 | ((2021, 9, 7, 10, 0), (2021, 9, 7, 10, 30)), 841 | ((2021, 9, 8, 9, 30), (2021, 9, 8, 10, 0)), 842 | ((2021, 9, 8, 12, 0), (2021, 9, 8, 13, 0)), 843 | ((2021, 9, 9), (2021, 9, 10)), 844 | ((2021, 9, 9, 10, 0), (2021, 9, 9, 10, 30)), 845 | ((2021, 9, 9, 11, 0), (2021, 9, 9, 12, 0)), 846 | ((2021, 9, 10, 8, 0), (2021, 9, 10, 8, 30)), 847 | ((2021, 9, 10, 9, 30), (2021, 9, 10, 10, 0)), 848 | ((2021, 9, 10, 17, 30), (2021, 9, 10, 19, 0)), 849 | ((2021, 9, 13, 9, 30), (2021, 9, 13, 10, 0)), 850 | ((2021, 9, 14, 9, 0), (2021, 9, 14, 10, 0)), 851 | ((2021, 9, 14, 10, 0), (2021, 9, 14, 10, 30)), 852 | ((2021, 9, 14, 15, 0), (2021, 9, 14, 15, 30)), 853 | ((2021, 9, 15, 9, 30), (2021, 9, 15, 10, 0)), 854 | ((2021, 9, 16, 10, 0), (2021, 9, 16, 10, 30)), 855 | ((2021, 9, 16), (2021, 9, 17)), 856 | ((2021, 9, 17, 9, 30), (2021, 9, 17, 10, 0)), 857 | ((2021, 9, 17, 17, 30), (2021, 9, 17, 19, 0)), 858 | ] 859 | 860 | for index, time in enumerate(times): 861 | self.assertEqual( 862 | events[index].start, 863 | date(*time[0]) if len(time[0]) == 3 else datetime(*time[0], tzinfo=tz), 864 | ) 865 | self.assertEqual( 866 | events[index].end, 867 | date(*time[1]) if len(time[1]) == 3 else datetime(*time[1], tzinfo=tz), 868 | ) 869 | 870 | self.assertEqual(len(events), len(times)) 871 | 872 | def test_recurring_tz_passover_spring(self): 873 | ical = "test/test_data/recurring_override.ics" 874 | start = date(2022, 3, 6) 875 | end = date(2022, 4, 10) 876 | 877 | tz = gettz("Europe/Zurich") 878 | events = icalevents.events( 879 | file=ical, start=start, end=end, tzinfo=tz, sort=True, strict=True 880 | ) 881 | 882 | times = [ 883 | ((2022, 3, 8, 11, 45), (2022, 3, 8, 12, 0)), 884 | ((2022, 3, 10), (2022, 3, 11)), 885 | ((2022, 3, 10, 11, 0), (2022, 3, 10, 12, 0)), 886 | ((2022, 3, 15, 11, 45), (2022, 3, 15, 12, 0)), 887 | ((2022, 3, 22, 11, 45), (2022, 3, 22, 12, 0)), 888 | ((2022, 3, 22, 14, 00), (2022, 3, 22, 15, 0)), 889 | ((2022, 3, 24), (2022, 3, 25)), 890 | ((2022, 3, 29, 11, 45), (2022, 3, 29, 12, 0)), 891 | ((2022, 4, 3, 8, 0), (2022, 4, 3, 8, 30)), 892 | ((2022, 4, 7), (2022, 4, 8)), 893 | ] 894 | 895 | for index, time in enumerate(times): 896 | self.assertEqual( 897 | events[index].start, 898 | date(*time[0]) if len(time[0]) == 3 else datetime(*time[0], tzinfo=tz), 899 | ) 900 | self.assertEqual( 901 | events[index].end, 902 | date(*time[1]) if len(time[1]) == 3 else datetime(*time[1], tzinfo=tz), 903 | ) 904 | 905 | self.assertEqual(len(events), len(times)) 906 | 907 | def test_multi_exdate_same_line(self): 908 | ical = "test/test_data/multi_exdate_same_line_ms.ics" 909 | tz = gettz("America/New_York") 910 | start = date(2022, 3, 1) 911 | end = date(2022, 5, 1) 912 | 913 | evs = icalevents.events(file=ical, start=start, end=end) 914 | 915 | # parsing starts at 2022-03-01 916 | self.assertEqual(evs[0].start, datetime(2022, 3, 11, 11, 0, 0, tzinfo=tz)) 917 | # 2022-03-18 is excluded by EXDATE rule 918 | self.assertEqual(evs[1].start, datetime(2022, 3, 25, 11, 0, 0, tzinfo=tz)) 919 | # 2022-04-01 is excluded by EXDATE rule 920 | # 2022-04-08 is excluded by EXDATE rule 921 | self.assertEqual(evs[2].start, datetime(2022, 4, 15, 11, 0, 0, tzinfo=tz)) 922 | self.assertEqual(evs[3].start, datetime(2022, 4, 22, 11, 0, 0, tzinfo=tz)) 923 | self.assertEqual(evs[4].start, datetime(2022, 4, 29, 11, 0, 0, tzinfo=tz)) 924 | # parsing stops at 2022-05-01 925 | 926 | def test_google_2024(self): 927 | ical = "test/test_data/google_2024.ics" 928 | start = date(2024, 1, 1) 929 | end = date(2024, 12, 31) 930 | 931 | [e1, *events] = icalevents.events(file=ical, start=start, end=end, strict=True) 932 | 933 | self.assertEqual(e1.start.astimezone(pytz.utc).hour, 6, "starts at 6 utc") 934 | self.assertEqual(e1.end.astimezone(pytz.utc).hour, 7, "ends at 7 utc") 935 | self.assertFalse(e1.floating, "respect floating time") 936 | self.assertEqual(e1.start.tzname(), "CET", "check tz as specified in calendar") 937 | 938 | self.assertEqual( 939 | events[4].start.astimezone(pytz.utc).hour, 6, "starts at 6 utc" 940 | ) 941 | self.assertEqual( 942 | events[5].start.astimezone(pytz.utc).hour, 943 | 5, 944 | "starts at 5 utc summer time (+2:00)", 945 | ) 946 | self.assertEqual( 947 | events[6].start.astimezone(pytz.utc).hour, 948 | 5, 949 | "starts at 5 utc summer time (+2:00)", 950 | ) 951 | 952 | def test_small_time_frame(self): 953 | ical = "test/test_data/small_time_frame.ics" 954 | 955 | PT = gettz("America/Los_Angeles") 956 | start = datetime(month=5, day=9, year=2023, tzinfo=PT) 957 | end = datetime(month=5, day=9, year=2023, hour=23, tzinfo=PT) 958 | 959 | events = icalevents.events(file=ical, start=start, end=end, strict=True) 960 | 961 | self.assertEqual(len(events), 1, "1 events") 962 | 963 | def test_recurr_id_dtstart_missmatch(self): 964 | ical = "test/test_data/recurr_id_dtstart_missmatch.ics" 965 | tz = gettz("America/New_York") 966 | start = date(2022, 3, 1) 967 | end = date(2022, 6, 30) 968 | 969 | evs = icalevents.events(file=ical, start=start, end=end) 970 | 971 | # input file isn't in sorted order, so dates are out of order 972 | evs.sort(key=lambda ev: (ev.start, ev.sequence)) 973 | 974 | self.assertEqual(len(evs), 4) 975 | 976 | # time didn't change, but description/summary did 977 | self.assertEqual(evs[0].start, datetime(2022, 3, 9, 13, 00, 0, tzinfo=tz)) 978 | self.assertEqual(evs[0].summary, "Recurring Event - Exception 1") 979 | 980 | # time/description/summary changed 981 | self.assertEqual(evs[1].start, datetime(2022, 4, 13, 10, 30, 0, tzinfo=tz)) 982 | self.assertEqual(evs[1].summary, "Recurring Event - Exception 2") 983 | 984 | # normally scheduled event 985 | self.assertEqual(evs[2].start, datetime(2022, 5, 11, 13, 00, 0, tzinfo=tz)) 986 | self.assertEqual(evs[2].summary, "Recurring Event") 987 | 988 | # normally scheduled event 989 | self.assertEqual(evs[3].start, datetime(2022, 6, 8, 13, 00, 0, tzinfo=tz)) 990 | self.assertEqual(evs[3].summary, "Recurring Event") 991 | 992 | def test_per_event_timezone(self): 993 | ical = "test/test_data/per_event_timezone.ics" 994 | start = date(2024, 1, 1) 995 | end = date(2024, 12, 30) 996 | 997 | events = icalevents.events(file=ical, start=start, end=end, strict=True) 998 | self.assertEqual( 999 | events[0].start.tzname(), "CET", "check tz as specified in calendar" 1000 | ) 1001 | self.assertEqual( 1002 | events[1].start.tzname(), "AWST", "check tz as specified in calendar" 1003 | ) 1004 | 1005 | def test_regression_repeating_events_raise_an_error(self): 1006 | ical = "test/test_data/recurrence_tzinfo.ics" 1007 | start = date(2023, 1, 1) 1008 | end = date(2024, 12, 31) 1009 | 1010 | events = icalevents.events(file=ical, start=start, end=end, strict=True) 1011 | 1012 | self.assertEqual(len(events), 6, "6 events") 1013 | self.assertEqual(events[0].start, date(2023, 11, 27), "first on 27. nov") 1014 | self.assertEqual(events[1].start, date(2023, 12, 4), "second event on 4. dec") 1015 | self.assertEqual(events[2].start, date(2023, 12, 11), "third event on 11. dec") 1016 | self.assertEqual( 1017 | events[3].start, 1018 | date(2024, 1, 1), 1019 | "fourth event on 1. jan - 18. and 25. dec are excluded", 1020 | ) 1021 | self.assertEqual(events[4].start, date(2024, 1, 8), "fifth event on 8. jan") 1022 | 1023 | def test_regression_recurring_events_with_timezones(self): 1024 | # we need to test if all active events are returned, even if they do not fit fully in the defined window 1025 | tz = gettz("Europe/Berlin") 1026 | ical = "test/test_data/recurring_small_window.ics" 1027 | start = datetime(2022, 1, 11, 0, 0, 1, tzinfo=tz) 1028 | end = datetime(2022, 1, 11, 8, 0, 1, tzinfo=tz) 1029 | 1030 | events = icalevents.events(file=ical, start=start, end=end, strict=True) 1031 | 1032 | self.assertEqual(len(events), 1) 1033 | self.assertEqual(events[0].end.hour, 8) 1034 | 1035 | def test_regression_offset_aware_comparison(self): 1036 | ical = "test/test_data/regression_offset_native.ics" 1037 | start = datetime(2020, 7, 1) 1038 | end = datetime(2020, 7, 31) 1039 | 1040 | events = icalevents.events(file=ical, start=start, end=end, strict=True) 1041 | 1042 | self.assertEqual(len(events), 1) 1043 | 1044 | def test_no_uid(self): 1045 | ical = "test/test_data/no_uid.ics" 1046 | 1047 | # noinspection DuplicatedCode 1048 | start = date(2021, 1, 1) 1049 | end = date(2021, 12, 31) 1050 | 1051 | [event] = icalevents.events(file=ical, start=start, end=end) 1052 | 1053 | self.assertIsNot(event.uid, "-1") 1054 | self.assertIsInstance(event.uid, str) 1055 | 1056 | def test_non_ascii_uid(self): 1057 | ical = "test/test_data/non_ascii_uid.ics" 1058 | 1059 | # noinspection DuplicatedCode 1060 | start = date(2021, 1, 1) 1061 | end = date(2021, 12, 31) 1062 | 1063 | [event] = icalevents.events(file=ical, start=start, end=end) 1064 | 1065 | self.assertIsNot(event.uid, "-1") 1066 | self.assertIsInstance(event.uid, str) 1067 | 1068 | def test_component(self): 1069 | ical = "test/test_data/cest_every_day_for_one_year.ics" 1070 | start = date(2020, 1, 1) 1071 | end = date(2024, 12, 31) 1072 | 1073 | evs = icalevents.events(file=ical, start=start, end=end) 1074 | event = evs[0] 1075 | 1076 | self.assertEqual(str(event.component["X-MICROSOFT-CDO-BUSYSTATUS"]), "BUSY") 1077 | self.assertIsInstance(event.component["X-MICROSOFT-CDO-BUSYSTATUS"], vText) 1078 | -------------------------------------------------------------------------------- /test/test_data/basic.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | METHOD:PUBLISH 6 | X-WR-CALNAME:Müll 7 | X-WR-TIMEZONE:Europe/Berlin 8 | X-WR-CALDESC:Müllabholung Treuchtlingen Luitpoldstraße 9 | BEGIN:VEVENT 10 | DTSTART;VALUE=DATE:20170712 11 | DTEND;VALUE=DATE:20170713 12 | DTSTAMP:20170711T171222Z 13 | UID:0eedefedba891fcbb49dcfa4279d9d93 14 | CREATED:20170103T080401Z 15 | DESCRIPTION:graue Restmülltonne nicht vergessen! 16 | LAST-MODIFIED:20170711T160050Z 17 | LOCATION:Luitpoldstraße\, Treuchtlingen 18 | SEQUENCE:0 19 | STATUS:CONFIRMED 20 | SUMMARY:graue Restmülltonne 21 | TRANSP:TRANSPARENT 22 | BEGIN:VALARM 23 | ACTION:NONE 24 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 25 | END:VALARM 26 | END:VEVENT 27 | BEGIN:VEVENT 28 | DTSTART;VALUE=DATE:20170712 29 | DTEND;VALUE=DATE:20170713 30 | DTSTAMP:20170711T171222Z 31 | UID:e122da9cfb107b38516b45571d212f4f 32 | CREATED:20170103T080401Z 33 | DESCRIPTION:braune Biotonne nicht vergessen! 34 | LAST-MODIFIED:20170711T160048Z 35 | LOCATION:Luitpoldstraße\, Treuchtlingen 36 | SEQUENCE:0 37 | STATUS:CONFIRMED 38 | SUMMARY:braune Biotonne 39 | TRANSP:TRANSPARENT 40 | BEGIN:VALARM 41 | ACTION:NONE 42 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 43 | END:VALARM 44 | END:VEVENT 45 | BEGIN:VEVENT 46 | DTSTART;VALUE=DATE:20170626 47 | DTEND;VALUE=DATE:20170627 48 | DTSTAMP:20170711T171222Z 49 | UID:efe9e2c0be259e0f3aabd383ee6c4d15 50 | CREATED:20170103T080401Z 51 | DESCRIPTION:Gelber Sack nicht vergessen! 52 | LAST-MODIFIED:20170625T160105Z 53 | LOCATION:Luitpoldstraße\, Treuchtlingen 54 | SEQUENCE:0 55 | STATUS:CONFIRMED 56 | SUMMARY:Gelber Sack 57 | TRANSP:TRANSPARENT 58 | BEGIN:VALARM 59 | ACTION:NONE 60 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 61 | END:VALARM 62 | END:VEVENT 63 | BEGIN:VEVENT 64 | DTSTART;VALUE=DATE:20170622 65 | DTEND;VALUE=DATE:20170623 66 | DTSTAMP:20170711T171222Z 67 | UID:07c802ef4f8fe50f95faf0ac95ac881c 68 | CREATED:20170103T080401Z 69 | DESCRIPTION:grüne Papiertonne und grüner 1\,1m³ Papiercontainer nicht verge 70 | ssen! 71 | LAST-MODIFIED:20170621T144339Z 72 | LOCATION:Luitpoldstraße\, Treuchtlingen 73 | SEQUENCE:0 74 | STATUS:CONFIRMED 75 | SUMMARY:grüne Papiertonne und grüner 1\,1m³ Papiercontainer 76 | TRANSP:TRANSPARENT 77 | BEGIN:VALARM 78 | ACTION:NONE 79 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 80 | END:VALARM 81 | END:VEVENT 82 | BEGIN:VEVENT 83 | DTSTART;VALUE=DATE:20170614 84 | DTEND;VALUE=DATE:20170615 85 | DTSTAMP:20170711T171222Z 86 | UID:7c994ff7de1de11183053c180adfc867 87 | CREATED:20170103T080401Z 88 | DESCRIPTION:graue Restmülltonne nicht vergessen! 89 | LAST-MODIFIED:20170613T173416Z 90 | LOCATION:Luitpoldstraße\, Treuchtlingen 91 | SEQUENCE:0 92 | STATUS:CONFIRMED 93 | SUMMARY:graue Restmülltonne 94 | TRANSP:TRANSPARENT 95 | X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC 96 | BEGIN:VALARM 97 | ACTION:AUDIO 98 | TRIGGER:-PT15H 99 | X-WR-ALARMUID:BC3EDCEF-AAB1-4A24-AB4D-69B6B4A9F597 100 | UID:BC3EDCEF-AAB1-4A24-AB4D-69B6B4A9F597 101 | ATTACH;VALUE=URI:Basso 102 | X-APPLE-DEFAULT-ALARM:TRUE 103 | ACKNOWLEDGED:20170613T173415Z 104 | END:VALARM 105 | END:VEVENT 106 | BEGIN:VEVENT 107 | DTSTART;VALUE=DATE:20170614 108 | DTEND;VALUE=DATE:20170615 109 | DTSTAMP:20170711T171222Z 110 | UID:36b222845f3311adc661fe262b1154b8 111 | CREATED:20170103T080401Z 112 | DESCRIPTION:braune Biotonne nicht vergessen! 113 | LAST-MODIFIED:20170613T173340Z 114 | LOCATION:Luitpoldstraße\, Treuchtlingen 115 | SEQUENCE:0 116 | STATUS:CONFIRMED 117 | SUMMARY:braune Biotonne 118 | TRANSP:TRANSPARENT 119 | X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC 120 | BEGIN:VALARM 121 | ACTION:AUDIO 122 | TRIGGER:-PT15H 123 | X-WR-ALARMUID:E005835A-4FBA-4FC8-A8EA-3DDAFF0D195F 124 | UID:E005835A-4FBA-4FC8-A8EA-3DDAFF0D195F 125 | ATTACH;VALUE=URI:Basso 126 | X-APPLE-DEFAULT-ALARM:TRUE 127 | ACKNOWLEDGED:20170613T173339Z 128 | END:VALARM 129 | END:VEVENT 130 | BEGIN:VEVENT 131 | DTSTART;VALUE=DATE:20170608 132 | DTEND;VALUE=DATE:20170609 133 | DTSTAMP:20170711T171222Z 134 | UID:c396c07c3daa1c21849738723d9a595e 135 | CREATED:20170103T080401Z 136 | DESCRIPTION:braune Biotonne nicht vergessen! 137 | LAST-MODIFIED:20170608T152251Z 138 | LOCATION:Luitpoldstraße\, Treuchtlingen 139 | SEQUENCE:0 140 | STATUS:CONFIRMED 141 | SUMMARY:braune Biotonne 142 | TRANSP:TRANSPARENT 143 | BEGIN:VALARM 144 | ACTION:NONE 145 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 146 | END:VALARM 147 | END:VEVENT 148 | BEGIN:VEVENT 149 | DTSTART;VALUE=DATE:20170531 150 | DTEND;VALUE=DATE:20170601 151 | DTSTAMP:20170711T171222Z 152 | UID:4dbfa315cac19965a33882b0aa4c9e90 153 | CREATED:20170103T080401Z 154 | DESCRIPTION:graue Restmülltonne nicht vergessen! 155 | LAST-MODIFIED:20170530T082532Z 156 | LOCATION:Luitpoldstraße\, Treuchtlingen 157 | SEQUENCE:0 158 | STATUS:CONFIRMED 159 | SUMMARY:graue Restmülltonne 160 | TRANSP:TRANSPARENT 161 | BEGIN:VALARM 162 | ACTION:NONE 163 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 164 | END:VALARM 165 | END:VEVENT 166 | BEGIN:VEVENT 167 | DTSTART;VALUE=DATE:20170531 168 | DTEND;VALUE=DATE:20170601 169 | DTSTAMP:20170711T171222Z 170 | UID:5ca0da577f9c2b932c2ac7b430b38ab4 171 | CREATED:20170103T080401Z 172 | DESCRIPTION:braune Biotonne nicht vergessen! 173 | LAST-MODIFIED:20170530T082531Z 174 | LOCATION:Luitpoldstraße\, Treuchtlingen 175 | SEQUENCE:0 176 | STATUS:CONFIRMED 177 | SUMMARY:braune Biotonne 178 | TRANSP:TRANSPARENT 179 | BEGIN:VALARM 180 | ACTION:NONE 181 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 182 | END:VALARM 183 | END:VEVENT 184 | BEGIN:VEVENT 185 | DTSTART;VALUE=DATE:20170524 186 | DTEND;VALUE=DATE:20170525 187 | DTSTAMP:20170711T171222Z 188 | UID:617bdd5b0817191ed3c8b95ec8aa910b 189 | CREATED:20170103T080401Z 190 | DESCRIPTION:braune Biotonne nicht vergessen! 191 | LAST-MODIFIED:20170523T090111Z 192 | LOCATION:Luitpoldstraße\, Treuchtlingen 193 | SEQUENCE:0 194 | STATUS:CONFIRMED 195 | SUMMARY:braune Biotonne 196 | TRANSP:TRANSPARENT 197 | BEGIN:VALARM 198 | ACTION:NONE 199 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 200 | END:VALARM 201 | END:VEVENT 202 | BEGIN:VEVENT 203 | DTSTART;VALUE=DATE:20170524 204 | DTEND;VALUE=DATE:20170525 205 | DTSTAMP:20170711T171222Z 206 | UID:92dddc277b05b3b25a60f3896beb85f9 207 | CREATED:20170103T080401Z 208 | DESCRIPTION:Gelber Sack nicht vergessen! 209 | LAST-MODIFIED:20170523T090111Z 210 | LOCATION:Luitpoldstraße\, Treuchtlingen 211 | SEQUENCE:0 212 | STATUS:CONFIRMED 213 | SUMMARY:Gelber Sack 214 | TRANSP:TRANSPARENT 215 | BEGIN:VALARM 216 | ACTION:NONE 217 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 218 | END:VALARM 219 | END:VEVENT 220 | BEGIN:VEVENT 221 | DTSTART;VALUE=DATE:20170522 222 | DTEND;VALUE=DATE:20170523 223 | DTSTAMP:20170711T171222Z 224 | UID:12a65763abc1354e36aa04e7b57d3ae0 225 | CREATED:20170103T080401Z 226 | DESCRIPTION:grüne Papiertonne und grüner 1\,1m³ Papiercontainer nicht verge 227 | ssen! 228 | LAST-MODIFIED:20170521T125619Z 229 | LOCATION:Luitpoldstraße\, Treuchtlingen 230 | SEQUENCE:0 231 | STATUS:CONFIRMED 232 | SUMMARY:grüne Papiertonne und grüner 1\,1m³ Papiercontainer 233 | TRANSP:TRANSPARENT 234 | BEGIN:VALARM 235 | ACTION:NONE 236 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 237 | END:VALARM 238 | END:VEVENT 239 | BEGIN:VEVENT 240 | DTSTART;VALUE=DATE:20170517 241 | DTEND;VALUE=DATE:20170518 242 | DTSTAMP:20170711T171222Z 243 | UID:f6e6b6d3eb68b176f8a88fbdaba6910e 244 | CREATED:20170103T080401Z 245 | DESCRIPTION:braune Biotonne nicht vergessen! 246 | LAST-MODIFIED:20170516T092545Z 247 | LOCATION:Luitpoldstraße\, Treuchtlingen 248 | SEQUENCE:0 249 | STATUS:CONFIRMED 250 | SUMMARY:braune Biotonne 251 | TRANSP:TRANSPARENT 252 | BEGIN:VALARM 253 | ACTION:NONE 254 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 255 | END:VALARM 256 | END:VEVENT 257 | BEGIN:VEVENT 258 | DTSTART;VALUE=DATE:20170517 259 | DTEND;VALUE=DATE:20170518 260 | DTSTAMP:20170711T171222Z 261 | UID:c49bacfe7ceb769cf61d842448dd6102 262 | CREATED:20170103T080401Z 263 | DESCRIPTION:graue Restmülltonne nicht vergessen! 264 | LAST-MODIFIED:20170516T092544Z 265 | LOCATION:Luitpoldstraße\, Treuchtlingen 266 | SEQUENCE:0 267 | STATUS:CONFIRMED 268 | SUMMARY:graue Restmülltonne 269 | TRANSP:TRANSPARENT 270 | BEGIN:VALARM 271 | ACTION:NONE 272 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 273 | END:VALARM 274 | END:VEVENT 275 | BEGIN:VEVENT 276 | DTSTART;VALUE=DATE:20170510 277 | DTEND;VALUE=DATE:20170511 278 | DTSTAMP:20170711T171222Z 279 | UID:0432f7eea3d2ae4032ea41bbe76392d1 280 | CREATED:20170103T080401Z 281 | DESCRIPTION:braune Biotonne nicht vergessen! 282 | LAST-MODIFIED:20170509T160147Z 283 | LOCATION:Luitpoldstraße\, Treuchtlingen 284 | SEQUENCE:0 285 | STATUS:CONFIRMED 286 | SUMMARY:braune Biotonne 287 | TRANSP:TRANSPARENT 288 | BEGIN:VALARM 289 | ACTION:NONE 290 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 291 | END:VALARM 292 | END:VEVENT 293 | BEGIN:VEVENT 294 | DTSTART;VALUE=DATE:20170426 295 | DTEND;VALUE=DATE:20170427 296 | DTSTAMP:20170711T171222Z 297 | UID:012af86d58d90698ea922fb6b4661b06 298 | CREATED:20170103T080401Z 299 | DESCRIPTION:braune Biotonne nicht vergessen! 300 | LAST-MODIFIED:20170425T174313Z 301 | LOCATION:Luitpoldstraße\, Treuchtlingen 302 | SEQUENCE:0 303 | STATUS:CONFIRMED 304 | SUMMARY:braune Biotonne 305 | TRANSP:TRANSPARENT 306 | BEGIN:VALARM 307 | ACTION:NONE 308 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 309 | END:VALARM 310 | END:VEVENT 311 | BEGIN:VEVENT 312 | DTSTART;VALUE=DATE:20170424 313 | DTEND;VALUE=DATE:20170425 314 | DTSTAMP:20170711T171222Z 315 | UID:a918e86e2a2f4c6098a7c619ae2f4a40 316 | CREATED:20170103T080401Z 317 | DESCRIPTION:Gelber Sack nicht vergessen! 318 | LAST-MODIFIED:20170423T162255Z 319 | LOCATION:Luitpoldstraße\, Treuchtlingen 320 | SEQUENCE:0 321 | STATUS:CONFIRMED 322 | SUMMARY:Gelber Sack 323 | TRANSP:TRANSPARENT 324 | BEGIN:VALARM 325 | ACTION:NONE 326 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 327 | END:VALARM 328 | END:VEVENT 329 | BEGIN:VEVENT 330 | DTSTART;VALUE=DATE:20170412 331 | DTEND;VALUE=DATE:20170413 332 | DTSTAMP:20170711T171222Z 333 | UID:935847042462cd08b76015c97cd90d14 334 | CREATED:20170103T080401Z 335 | DESCRIPTION:braune Biotonne nicht vergessen! 336 | LAST-MODIFIED:20170412T155528Z 337 | LOCATION:Luitpoldstraße\, Treuchtlingen 338 | SEQUENCE:0 339 | STATUS:CONFIRMED 340 | SUMMARY:braune Biotonne 341 | TRANSP:TRANSPARENT 342 | BEGIN:VALARM 343 | ACTION:NONE 344 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 345 | END:VALARM 346 | END:VEVENT 347 | BEGIN:VEVENT 348 | DTSTART;VALUE=DATE:20170405 349 | DTEND;VALUE=DATE:20170406 350 | DTSTAMP:20170711T171222Z 351 | UID:a6414e7f22bf7b30f45c427aaa947b4a 352 | CREATED:20170103T080401Z 353 | DESCRIPTION:graue Restmülltonne nicht vergessen! 354 | LAST-MODIFIED:20170404T070148Z 355 | LOCATION:Luitpoldstraße\, Treuchtlingen 356 | SEQUENCE:0 357 | STATUS:CONFIRMED 358 | SUMMARY:graue Restmülltonne 359 | TRANSP:TRANSPARENT 360 | BEGIN:VALARM 361 | ACTION:NONE 362 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 363 | END:VALARM 364 | END:VEVENT 365 | BEGIN:VEVENT 366 | DTSTART;VALUE=DATE:20170329 367 | DTEND;VALUE=DATE:20170330 368 | DTSTAMP:20170711T171222Z 369 | UID:1a567b4e0d1efc31dc7893f087697a4f 370 | CREATED:20170103T080401Z 371 | DESCRIPTION:braune Biotonne nicht vergessen! 372 | LAST-MODIFIED:20170328T165756Z 373 | LOCATION:Luitpoldstraße\, Treuchtlingen 374 | SEQUENCE:0 375 | STATUS:CONFIRMED 376 | SUMMARY:braune Biotonne 377 | TRANSP:TRANSPARENT 378 | BEGIN:VALARM 379 | ACTION:NONE 380 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 381 | END:VALARM 382 | END:VEVENT 383 | BEGIN:VEVENT 384 | DTSTART;VALUE=DATE:20170320 385 | DTEND;VALUE=DATE:20170321 386 | DTSTAMP:20170711T171222Z 387 | UID:7a5416a2ca1ac6d4bb77f8b94ed0a8be 388 | CREATED:20170103T080401Z 389 | DESCRIPTION:grüne Papiertonne und grüner 1\,1m³ Papiercontainer nicht verge 390 | ssen! 391 | LAST-MODIFIED:20170319T131719Z 392 | LOCATION:Luitpoldstraße\, Treuchtlingen 393 | SEQUENCE:0 394 | STATUS:CONFIRMED 395 | SUMMARY:grüne Papiertonne und grüner 1\,1m³ Papiercontainer 396 | TRANSP:TRANSPARENT 397 | X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC 398 | BEGIN:VALARM 399 | ACTION:AUDIO 400 | TRIGGER:-PT15H 401 | X-WR-ALARMUID:4BB6A40E-6845-4541-BD87-0962514D03DC 402 | UID:4BB6A40E-6845-4541-BD87-0962514D03DC 403 | ATTACH;VALUE=URI:Basso 404 | X-APPLE-DEFAULT-ALARM:TRUE 405 | ACKNOWLEDGED:20170319T131719Z 406 | END:VALARM 407 | END:VEVENT 408 | BEGIN:VEVENT 409 | DTSTART;VALUE=DATE:20170308 410 | DTEND;VALUE=DATE:20170309 411 | DTSTAMP:20170711T171222Z 412 | UID:3c72a54356c95bfbfe7383d1d3d19379 413 | CREATED:20170103T080401Z 414 | DESCRIPTION:graue Restmülltonne nicht vergessen! 415 | LAST-MODIFIED:20170307T100509Z 416 | LOCATION:Luitpoldstraße\, Treuchtlingen 417 | SEQUENCE:0 418 | STATUS:CONFIRMED 419 | SUMMARY:graue Restmülltonne 420 | TRANSP:TRANSPARENT 421 | X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC 422 | BEGIN:VALARM 423 | ACTION:AUDIO 424 | TRIGGER:-PT15H 425 | X-WR-ALARMUID:2DB10823-1AD7-4793-BC69-BC4FC1383E5E 426 | UID:2DB10823-1AD7-4793-BC69-BC4FC1383E5E 427 | ATTACH;VALUE=URI:Basso 428 | X-APPLE-DEFAULT-ALARM:TRUE 429 | ACKNOWLEDGED:20170307T100501Z 430 | END:VALARM 431 | BEGIN:VALARM 432 | ACTION:AUDIO 433 | TRIGGER;VALUE=DATE-TIME:20170307T100500Z 434 | X-WR-ALARMUID:EE4099B4-69A5-4199-8601-F131819E01E0 435 | UID:EE4099B4-69A5-4199-8601-F131819E01E0 436 | ATTACH;VALUE=URI:Basso 437 | RELATED-TO:2DB10823-1AD7-4793-BC69-BC4FC1383E5E 438 | ACKNOWLEDGED:20170307T100501Z 439 | END:VALARM 440 | END:VEVENT 441 | BEGIN:VEVENT 442 | DTSTART;VALUE=DATE:20170301 443 | DTEND;VALUE=DATE:20170302 444 | DTSTAMP:20170711T171222Z 445 | UID:0e17dbf248106fc4e0750b82204b1eb3 446 | CREATED:20170103T080401Z 447 | DESCRIPTION:braune Biotonne nicht vergessen! 448 | LAST-MODIFIED:20170228T174301Z 449 | LOCATION:Luitpoldstraße\, Treuchtlingen 450 | SEQUENCE:0 451 | STATUS:CONFIRMED 452 | SUMMARY:braune Biotonne 453 | TRANSP:TRANSPARENT 454 | X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC 455 | BEGIN:VALARM 456 | ACTION:AUDIO 457 | TRIGGER:-PT15H 458 | X-WR-ALARMUID:BE658068-8213-4228-BE68-AF2A53D8508F 459 | UID:BE658068-8213-4228-BE68-AF2A53D8508F 460 | ATTACH;VALUE=URI:Basso 461 | X-APPLE-DEFAULT-ALARM:TRUE 462 | ACKNOWLEDGED:20170228T174318Z 463 | END:VALARM 464 | END:VEVENT 465 | BEGIN:VEVENT 466 | DTSTART;VALUE=DATE:20170215 467 | DTEND;VALUE=DATE:20170216 468 | DTSTAMP:20170711T171222Z 469 | UID:1e540f2f49688b87332de4f74c77b9c9 470 | CREATED:20170103T080401Z 471 | DESCRIPTION:braune Biotonne nicht vergessen! 472 | LAST-MODIFIED:20170214T182848Z 473 | LOCATION:Luitpoldstraße\, Treuchtlingen 474 | SEQUENCE:0 475 | STATUS:CONFIRMED 476 | SUMMARY:braune Biotonne 477 | TRANSP:TRANSPARENT 478 | BEGIN:VALARM 479 | ACTION:NONE 480 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 481 | END:VALARM 482 | END:VEVENT 483 | BEGIN:VEVENT 484 | DTSTART;VALUE=DATE:20170203 485 | DTEND;VALUE=DATE:20170204 486 | DTSTAMP:20170711T171222Z 487 | UID:bd22e2dec844adf3bddca2f4c0fd2f88 488 | CREATED:20170103T080401Z 489 | DESCRIPTION:Sondermüll 13.00-16.00 Volksfestplatz nicht vergessen! 490 | LAST-MODIFIED:20170202T125220Z 491 | LOCATION:Luitpoldstraße\, Treuchtlingen 492 | SEQUENCE:0 493 | STATUS:CONFIRMED 494 | SUMMARY:Sondermüll 495 | TRANSP:TRANSPARENT 496 | BEGIN:VALARM 497 | ACTION:NONE 498 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 499 | END:VALARM 500 | END:VEVENT 501 | BEGIN:VEVENT 502 | DTSTART;VALUE=DATE:20170201 503 | DTEND;VALUE=DATE:20170202 504 | DTSTAMP:20170711T171222Z 505 | UID:8d6b72cc11bc18b07b6662e4230ebf6b 506 | CREATED:20170103T080401Z 507 | DESCRIPTION:braune Biotonne nicht vergessen! 508 | LAST-MODIFIED:20170201T184715Z 509 | LOCATION:Luitpoldstraße\, Treuchtlingen 510 | SEQUENCE:0 511 | STATUS:CONFIRMED 512 | SUMMARY:braune Biotonne 513 | TRANSP:TRANSPARENT 514 | BEGIN:VALARM 515 | ACTION:NONE 516 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 517 | END:VALARM 518 | END:VEVENT 519 | BEGIN:VEVENT 520 | DTSTART;VALUE=DATE:20171027 521 | DTEND;VALUE=DATE:20171028 522 | DTSTAMP:20170711T171222Z 523 | UID:b9f96af011f03644a186fd1633dd26c6 524 | CREATED:20170103T080401Z 525 | DESCRIPTION:Sondermüll 13.00-16.00 Volksfestplatz nicht vergessen! 526 | LAST-MODIFIED:20170126T100735Z 527 | LOCATION:Luitpoldstraße\, Treuchtlingen 528 | SEQUENCE:0 529 | STATUS:CONFIRMED 530 | SUMMARY:Sondermüll 531 | TRANSP:TRANSPARENT 532 | END:VEVENT 533 | BEGIN:VEVENT 534 | DTSTART;VALUE=DATE:20170804 535 | DTEND;VALUE=DATE:20170805 536 | DTSTAMP:20170711T171222Z 537 | UID:4cb27817622e7b3274ad087e1894572a 538 | CREATED:20170103T080401Z 539 | DESCRIPTION:Sondermüll 13.00-16.00 Volksfestplatz nicht vergessen! 540 | LAST-MODIFIED:20170126T100735Z 541 | LOCATION:Luitpoldstraße\, Treuchtlingen 542 | SEQUENCE:0 543 | STATUS:CONFIRMED 544 | SUMMARY:Sondermüll 545 | TRANSP:TRANSPARENT 546 | END:VEVENT 547 | BEGIN:VEVENT 548 | DTSTART;VALUE=DATE:20170422 549 | DTEND;VALUE=DATE:20170423 550 | DTSTAMP:20170711T171222Z 551 | UID:3ab8a5bdbf22fa9b330e203611f5bcc7 552 | CREATED:20170103T080401Z 553 | DESCRIPTION:Sondermüll 11.00-14.00 Volksfestplatz nicht vergessen! 554 | LAST-MODIFIED:20170126T100735Z 555 | LOCATION:Luitpoldstraße\, Treuchtlingen 556 | SEQUENCE:0 557 | STATUS:CONFIRMED 558 | SUMMARY:Sondermüll 559 | TRANSP:TRANSPARENT 560 | END:VEVENT 561 | BEGIN:VEVENT 562 | DTSTART;VALUE=DATE:20170123 563 | DTEND;VALUE=DATE:20170124 564 | DTSTAMP:20170711T171222Z 565 | UID:e3ac69ae8d6f05c15e5c1a8e2688c9b7 566 | CREATED:20170103T080401Z 567 | DESCRIPTION:grüne Papiertonne und grüner 1\,1m³ Papiercontainer nicht verge 568 | ssen! 569 | LAST-MODIFIED:20170126T100735Z 570 | LOCATION:Luitpoldstraße\, Treuchtlingen 571 | SEQUENCE:0 572 | STATUS:CONFIRMED 573 | SUMMARY:grüne Papiertonne und grüner 1\,1m³ Papiercontainer 574 | TRANSP:TRANSPARENT 575 | END:VEVENT 576 | BEGIN:VEVENT 577 | DTSTART;VALUE=DATE:20170720 578 | DTEND;VALUE=DATE:20170721 579 | DTSTAMP:20170711T171222Z 580 | UID:db02708b90bcac408e84a37f1f09010e 581 | CREATED:20170103T080401Z 582 | DESCRIPTION:grüne Papiertonne und grüner 1\,1m³ Papiercontainer nicht verge 583 | ssen! 584 | LAST-MODIFIED:20170126T100735Z 585 | LOCATION:Luitpoldstraße\, Treuchtlingen 586 | SEQUENCE:0 587 | STATUS:CONFIRMED 588 | SUMMARY:grüne Papiertonne und grüner 1\,1m³ Papiercontainer 589 | TRANSP:TRANSPARENT 590 | END:VEVENT 591 | BEGIN:VEVENT 592 | DTSTART;VALUE=DATE:20170914 593 | DTEND;VALUE=DATE:20170915 594 | DTSTAMP:20170711T171222Z 595 | UID:b987aeb6ceedae19a92ed8e6d47feede 596 | CREATED:20170103T080401Z 597 | DESCRIPTION:grüne Papiertonne und grüner 1\,1m³ Papiercontainer nicht verge 598 | ssen! 599 | LAST-MODIFIED:20170126T100735Z 600 | LOCATION:Luitpoldstraße\, Treuchtlingen 601 | SEQUENCE:0 602 | STATUS:CONFIRMED 603 | SUMMARY:grüne Papiertonne und grüner 1\,1m³ Papiercontainer 604 | TRANSP:TRANSPARENT 605 | END:VEVENT 606 | BEGIN:VEVENT 607 | DTSTART;VALUE=DATE:20170220 608 | DTEND;VALUE=DATE:20170221 609 | DTSTAMP:20170711T171222Z 610 | UID:ab0aaeb9d8487558c283be1422915899 611 | CREATED:20170103T080401Z 612 | DESCRIPTION:grüne Papiertonne und grüner 1\,1m³ Papiercontainer nicht verge 613 | ssen! 614 | LAST-MODIFIED:20170126T100735Z 615 | LOCATION:Luitpoldstraße\, Treuchtlingen 616 | SEQUENCE:0 617 | STATUS:CONFIRMED 618 | SUMMARY:grüne Papiertonne und grüner 1\,1m³ Papiercontainer 619 | TRANSP:TRANSPARENT 620 | END:VEVENT 621 | BEGIN:VEVENT 622 | DTSTART;VALUE=DATE:20170817 623 | DTEND;VALUE=DATE:20170818 624 | DTSTAMP:20170711T171222Z 625 | UID:8b8b1253498443ea1dcf4996b85384dc 626 | CREATED:20170103T080401Z 627 | DESCRIPTION:grüne Papiertonne und grüner 1\,1m³ Papiercontainer nicht verge 628 | ssen! 629 | LAST-MODIFIED:20170126T100735Z 630 | LOCATION:Luitpoldstraße\, Treuchtlingen 631 | SEQUENCE:0 632 | STATUS:CONFIRMED 633 | SUMMARY:grüne Papiertonne und grüner 1\,1m³ Papiercontainer 634 | TRANSP:TRANSPARENT 635 | END:VEVENT 636 | BEGIN:VEVENT 637 | DTSTART;VALUE=DATE:20171219 638 | DTEND;VALUE=DATE:20171220 639 | DTSTAMP:20170711T171222Z 640 | UID:71cba68020f1918038e158901906c38d 641 | CREATED:20170103T080401Z 642 | DESCRIPTION:grüne Papiertonne und grüner 1\,1m³ Papiercontainer nicht verge 643 | ssen! 644 | LAST-MODIFIED:20170126T100735Z 645 | LOCATION:Luitpoldstraße\, Treuchtlingen 646 | SEQUENCE:0 647 | STATUS:CONFIRMED 648 | SUMMARY:grüne Papiertonne und grüner 1\,1m³ Papiercontainer 649 | TRANSP:TRANSPARENT 650 | END:VEVENT 651 | BEGIN:VEVENT 652 | DTSTART;VALUE=DATE:20171121 653 | DTEND;VALUE=DATE:20171122 654 | DTSTAMP:20170711T171222Z 655 | UID:3e1b9285d22c9e3fc6aea39f30dcbf32 656 | CREATED:20170103T080401Z 657 | DESCRIPTION:grüne Papiertonne und grüner 1\,1m³ Papiercontainer nicht verge 658 | ssen! 659 | LAST-MODIFIED:20170126T100735Z 660 | LOCATION:Luitpoldstraße\, Treuchtlingen 661 | SEQUENCE:0 662 | STATUS:CONFIRMED 663 | SUMMARY:grüne Papiertonne und grüner 1\,1m³ Papiercontainer 664 | TRANSP:TRANSPARENT 665 | END:VEVENT 666 | BEGIN:VEVENT 667 | DTSTART;VALUE=DATE:20170418 668 | DTEND;VALUE=DATE:20170419 669 | DTSTAMP:20170711T171222Z 670 | UID:2dd84b7600eec3f674e2704b8b1ae442 671 | CREATED:20170103T080401Z 672 | DESCRIPTION:grüne Papiertonne und grüner 1\,1m³ Papiercontainer nicht verge 673 | ssen! 674 | LAST-MODIFIED:20170126T100735Z 675 | LOCATION:Luitpoldstraße\, Treuchtlingen 676 | SEQUENCE:0 677 | STATUS:CONFIRMED 678 | SUMMARY:grüne Papiertonne und grüner 1\,1m³ Papiercontainer 679 | TRANSP:TRANSPARENT 680 | END:VEVENT 681 | BEGIN:VEVENT 682 | DTSTART;VALUE=DATE:20171019 683 | DTEND;VALUE=DATE:20171020 684 | DTSTAMP:20170711T171222Z 685 | UID:295a8a624928c949ece1f3991f2ab200 686 | CREATED:20170103T080401Z 687 | DESCRIPTION:grüne Papiertonne und grüner 1\,1m³ Papiercontainer nicht verge 688 | ssen! 689 | LAST-MODIFIED:20170126T100735Z 690 | LOCATION:Luitpoldstraße\, Treuchtlingen 691 | SEQUENCE:0 692 | STATUS:CONFIRMED 693 | SUMMARY:grüne Papiertonne und grüner 1\,1m³ Papiercontainer 694 | TRANSP:TRANSPARENT 695 | END:VEVENT 696 | BEGIN:VEVENT 697 | DTSTART;VALUE=DATE:20171115 698 | DTEND;VALUE=DATE:20171116 699 | DTSTAMP:20170711T171222Z 700 | UID:f84edfee25e57fec7b2a0036235ae3ef 701 | CREATED:20170103T080401Z 702 | DESCRIPTION:graue Restmülltonne nicht vergessen! 703 | LAST-MODIFIED:20170126T100735Z 704 | LOCATION:Luitpoldstraße\, Treuchtlingen 705 | SEQUENCE:0 706 | STATUS:CONFIRMED 707 | SUMMARY:graue Restmülltonne 708 | TRANSP:TRANSPARENT 709 | END:VEVENT 710 | BEGIN:VEVENT 711 | DTSTART;VALUE=DATE:20171129 712 | DTEND;VALUE=DATE:20171130 713 | DTSTAMP:20170711T171222Z 714 | UID:e00a31d744277d44633bb8ff219853fb 715 | CREATED:20170103T080401Z 716 | DESCRIPTION:graue Restmülltonne nicht vergessen! 717 | LAST-MODIFIED:20170126T100735Z 718 | LOCATION:Luitpoldstraße\, Treuchtlingen 719 | SEQUENCE:0 720 | STATUS:CONFIRMED 721 | SUMMARY:graue Restmülltonne 722 | TRANSP:TRANSPARENT 723 | END:VEVENT 724 | BEGIN:VEVENT 725 | DTSTART;VALUE=DATE:20170823 726 | DTEND;VALUE=DATE:20170824 727 | DTSTAMP:20170711T171222Z 728 | UID:ddf5c06a0f0ac02bebc0e8485fadd43f 729 | CREATED:20170103T080401Z 730 | DESCRIPTION:graue Restmülltonne nicht vergessen! 731 | LAST-MODIFIED:20170126T100735Z 732 | LOCATION:Luitpoldstraße\, Treuchtlingen 733 | SEQUENCE:0 734 | STATUS:CONFIRMED 735 | SUMMARY:graue Restmülltonne 736 | TRANSP:TRANSPARENT 737 | END:VEVENT 738 | BEGIN:VEVENT 739 | DTSTART;VALUE=DATE:20170322 740 | DTEND;VALUE=DATE:20170323 741 | DTSTAMP:20170711T171222Z 742 | UID:dc7c42b1704baecd17811f6ccf5fb1ba 743 | CREATED:20170103T080401Z 744 | DESCRIPTION:graue Restmülltonne nicht vergessen! 745 | LAST-MODIFIED:20170126T100735Z 746 | LOCATION:Luitpoldstraße\, Treuchtlingen 747 | SEQUENCE:0 748 | STATUS:CONFIRMED 749 | SUMMARY:graue Restmülltonne 750 | TRANSP:TRANSPARENT 751 | END:VEVENT 752 | BEGIN:VEVENT 753 | DTSTART;VALUE=DATE:20170726 754 | DTEND;VALUE=DATE:20170727 755 | DTSTAMP:20170711T171222Z 756 | UID:d758da5abb4acd5f3f84d25dec69ec1b 757 | CREATED:20170103T080401Z 758 | DESCRIPTION:graue Restmülltonne nicht vergessen! 759 | LAST-MODIFIED:20170126T100735Z 760 | LOCATION:Luitpoldstraße\, Treuchtlingen 761 | SEQUENCE:0 762 | STATUS:CONFIRMED 763 | SUMMARY:graue Restmülltonne 764 | TRANSP:TRANSPARENT 765 | END:VEVENT 766 | BEGIN:VEVENT 767 | DTSTART;VALUE=DATE:20170222 768 | DTEND;VALUE=DATE:20170223 769 | DTSTAMP:20170711T171222Z 770 | UID:ceb59dbac23995412bfb62a59019f25c 771 | CREATED:20170103T080401Z 772 | DESCRIPTION:graue Restmülltonne nicht vergessen! 773 | LAST-MODIFIED:20170126T100735Z 774 | LOCATION:Luitpoldstraße\, Treuchtlingen 775 | SEQUENCE:0 776 | STATUS:CONFIRMED 777 | SUMMARY:graue Restmülltonne 778 | TRANSP:TRANSPARENT 779 | END:VEVENT 780 | BEGIN:VEVENT 781 | DTSTART;VALUE=DATE:20170920 782 | DTEND;VALUE=DATE:20170921 783 | DTSTAMP:20170711T171222Z 784 | UID:c5f71249b1943da7bdbdad13922421cf 785 | CREATED:20170103T080401Z 786 | DESCRIPTION:graue Restmülltonne nicht vergessen! 787 | LAST-MODIFIED:20170126T100735Z 788 | LOCATION:Luitpoldstraße\, Treuchtlingen 789 | SEQUENCE:0 790 | STATUS:CONFIRMED 791 | SUMMARY:graue Restmülltonne 792 | TRANSP:TRANSPARENT 793 | END:VEVENT 794 | BEGIN:VEVENT 795 | DTSTART;VALUE=DATE:20170420 796 | DTEND;VALUE=DATE:20170421 797 | DTSTAMP:20170711T171222Z 798 | UID:c14c86836347f9922f0c5d68a952dfd6 799 | CREATED:20170103T080401Z 800 | DESCRIPTION:graue Restmülltonne nicht vergessen! 801 | LAST-MODIFIED:20170126T100735Z 802 | LOCATION:Luitpoldstraße\, Treuchtlingen 803 | SEQUENCE:0 804 | STATUS:CONFIRMED 805 | SUMMARY:graue Restmülltonne 806 | TRANSP:TRANSPARENT 807 | END:VEVENT 808 | BEGIN:VEVENT 809 | DTSTART;VALUE=DATE:20170809 810 | DTEND;VALUE=DATE:20170810 811 | DTSTAMP:20170711T171222Z 812 | UID:93239592ff0f8d16bf34b2aa5c7288e2 813 | CREATED:20170103T080401Z 814 | DESCRIPTION:graue Restmülltonne nicht vergessen! 815 | LAST-MODIFIED:20170126T100735Z 816 | LOCATION:Luitpoldstraße\, Treuchtlingen 817 | SEQUENCE:0 818 | STATUS:CONFIRMED 819 | SUMMARY:graue Restmülltonne 820 | TRANSP:TRANSPARENT 821 | END:VEVENT 822 | BEGIN:VEVENT 823 | DTSTART;VALUE=DATE:20170628 824 | DTEND;VALUE=DATE:20170629 825 | DTSTAMP:20170711T171222Z 826 | UID:8e5e62128fe3ed2a22577725e6a605af 827 | CREATED:20170103T080401Z 828 | DESCRIPTION:graue Restmülltonne nicht vergessen! 829 | LAST-MODIFIED:20170126T100735Z 830 | LOCATION:Luitpoldstraße\, Treuchtlingen 831 | SEQUENCE:0 832 | STATUS:CONFIRMED 833 | SUMMARY:graue Restmülltonne 834 | TRANSP:TRANSPARENT 835 | END:VEVENT 836 | BEGIN:VEVENT 837 | DTSTART;VALUE=DATE:20170111 838 | DTEND;VALUE=DATE:20170112 839 | DTSTAMP:20170711T171222Z 840 | UID:8c1c6aff0a57960ea573409be9c14c32 841 | CREATED:20170103T080401Z 842 | DESCRIPTION:graue Restmülltonne nicht vergessen! 843 | LAST-MODIFIED:20170126T100735Z 844 | LOCATION:Luitpoldstraße\, Treuchtlingen 845 | SEQUENCE:0 846 | STATUS:CONFIRMED 847 | SUMMARY:graue Restmülltonne 848 | TRANSP:TRANSPARENT 849 | END:VEVENT 850 | BEGIN:VEVENT 851 | DTSTART;VALUE=DATE:20170208 852 | DTEND;VALUE=DATE:20170209 853 | DTSTAMP:20170711T171222Z 854 | UID:7f7f6d57ee0058e042d9da043c111cb8 855 | CREATED:20170103T080401Z 856 | DESCRIPTION:graue Restmülltonne nicht vergessen! 857 | LAST-MODIFIED:20170126T100735Z 858 | LOCATION:Luitpoldstraße\, Treuchtlingen 859 | SEQUENCE:0 860 | STATUS:CONFIRMED 861 | SUMMARY:graue Restmülltonne 862 | TRANSP:TRANSPARENT 863 | END:VEVENT 864 | BEGIN:VEVENT 865 | DTSTART;VALUE=DATE:20170125 866 | DTEND;VALUE=DATE:20170126 867 | DTSTAMP:20170711T171222Z 868 | UID:68d38cb88d40779fa9aec09b84c004d8 869 | CREATED:20170103T080401Z 870 | DESCRIPTION:graue Restmülltonne nicht vergessen! 871 | LAST-MODIFIED:20170126T100735Z 872 | LOCATION:Luitpoldstraße\, Treuchtlingen 873 | SEQUENCE:0 874 | STATUS:CONFIRMED 875 | SUMMARY:graue Restmülltonne 876 | TRANSP:TRANSPARENT 877 | END:VEVENT 878 | BEGIN:VEVENT 879 | DTSTART;VALUE=DATE:20170906 880 | DTEND;VALUE=DATE:20170907 881 | DTSTAMP:20170711T171222Z 882 | UID:63dc783377f10a4c141a663fcdcff2a9 883 | CREATED:20170103T080401Z 884 | DESCRIPTION:graue Restmülltonne nicht vergessen! 885 | LAST-MODIFIED:20170126T100735Z 886 | LOCATION:Luitpoldstraße\, Treuchtlingen 887 | SEQUENCE:0 888 | STATUS:CONFIRMED 889 | SUMMARY:graue Restmülltonne 890 | TRANSP:TRANSPARENT 891 | END:VEVENT 892 | BEGIN:VEVENT 893 | DTSTART;VALUE=DATE:20171102 894 | DTEND;VALUE=DATE:20171103 895 | DTSTAMP:20170711T171222Z 896 | UID:5e6fc787cb425c39cb3998639b4966a7 897 | CREATED:20170103T080401Z 898 | DESCRIPTION:graue Restmülltonne nicht vergessen! 899 | LAST-MODIFIED:20170126T100735Z 900 | LOCATION:Luitpoldstraße\, Treuchtlingen 901 | SEQUENCE:0 902 | STATUS:CONFIRMED 903 | SUMMARY:graue Restmülltonne 904 | TRANSP:TRANSPARENT 905 | END:VEVENT 906 | BEGIN:VEVENT 907 | DTSTART;VALUE=DATE:20170504 908 | DTEND;VALUE=DATE:20170505 909 | DTSTAMP:20170711T171222Z 910 | UID:5bf75396a5d201d18efebd220b1c1177 911 | CREATED:20170103T080401Z 912 | DESCRIPTION:graue Restmülltonne nicht vergessen! 913 | LAST-MODIFIED:20170126T100735Z 914 | LOCATION:Luitpoldstraße\, Treuchtlingen 915 | SEQUENCE:0 916 | STATUS:CONFIRMED 917 | SUMMARY:graue Restmülltonne 918 | TRANSP:TRANSPARENT 919 | END:VEVENT 920 | BEGIN:VEVENT 921 | DTSTART;VALUE=DATE:20171228 922 | DTEND;VALUE=DATE:20171229 923 | DTSTAMP:20170711T171222Z 924 | UID:5a30134566e84224f30128808d89a31e 925 | CREATED:20170103T080401Z 926 | DESCRIPTION:graue Restmülltonne nicht vergessen! 927 | LAST-MODIFIED:20170126T100735Z 928 | LOCATION:Luitpoldstraße\, Treuchtlingen 929 | SEQUENCE:0 930 | STATUS:CONFIRMED 931 | SUMMARY:graue Restmülltonne 932 | TRANSP:TRANSPARENT 933 | END:VEVENT 934 | BEGIN:VEVENT 935 | DTSTART;VALUE=DATE:20171005 936 | DTEND;VALUE=DATE:20171006 937 | DTSTAMP:20170711T171222Z 938 | UID:4c58836e8beaee68920f11cfbb19b820 939 | CREATED:20170103T080401Z 940 | DESCRIPTION:graue Restmülltonne nicht vergessen! 941 | LAST-MODIFIED:20170126T100735Z 942 | LOCATION:Luitpoldstraße\, Treuchtlingen 943 | SEQUENCE:0 944 | STATUS:CONFIRMED 945 | SUMMARY:graue Restmülltonne 946 | TRANSP:TRANSPARENT 947 | END:VEVENT 948 | BEGIN:VEVENT 949 | DTSTART;VALUE=DATE:20171018 950 | DTEND;VALUE=DATE:20171019 951 | DTSTAMP:20170711T171222Z 952 | UID:2415471b7ab7a6aa92a84eb7fddcee92 953 | CREATED:20170103T080401Z 954 | DESCRIPTION:graue Restmülltonne nicht vergessen! 955 | LAST-MODIFIED:20170126T100735Z 956 | LOCATION:Luitpoldstraße\, Treuchtlingen 957 | SEQUENCE:0 958 | STATUS:CONFIRMED 959 | SUMMARY:graue Restmülltonne 960 | TRANSP:TRANSPARENT 961 | END:VEVENT 962 | BEGIN:VEVENT 963 | DTSTART;VALUE=DATE:20171213 964 | DTEND;VALUE=DATE:20171214 965 | DTSTAMP:20170711T171222Z 966 | UID:11be18f68972fbb15e8dff78616ea3a8 967 | CREATED:20170103T080401Z 968 | DESCRIPTION:graue Restmülltonne nicht vergessen! 969 | LAST-MODIFIED:20170126T100735Z 970 | LOCATION:Luitpoldstraße\, Treuchtlingen 971 | SEQUENCE:0 972 | STATUS:CONFIRMED 973 | SUMMARY:graue Restmülltonne 974 | TRANSP:TRANSPARENT 975 | END:VEVENT 976 | BEGIN:VEVENT 977 | DTSTART;VALUE=DATE:20170322 978 | DTEND;VALUE=DATE:20170323 979 | DTSTAMP:20170711T171222Z 980 | UID:fe7bdd0f1ae26125306052a4a03d2841 981 | CREATED:20170103T080401Z 982 | DESCRIPTION:Gelber Sack nicht vergessen! 983 | LAST-MODIFIED:20170126T100735Z 984 | LOCATION:Luitpoldstraße\, Treuchtlingen 985 | SEQUENCE:0 986 | STATUS:CONFIRMED 987 | SUMMARY:Gelber Sack 988 | TRANSP:TRANSPARENT 989 | END:VEVENT 990 | BEGIN:VEVENT 991 | DTSTART;VALUE=DATE:20170222 992 | DTEND;VALUE=DATE:20170223 993 | DTSTAMP:20170711T171222Z 994 | UID:fbe837e05f4d57a238fd21f1fd990525 995 | CREATED:20170103T080401Z 996 | DESCRIPTION:Gelber Sack nicht vergessen! 997 | LAST-MODIFIED:20170126T100735Z 998 | LOCATION:Luitpoldstraße\, Treuchtlingen 999 | SEQUENCE:0 1000 | STATUS:CONFIRMED 1001 | SUMMARY:Gelber Sack 1002 | TRANSP:TRANSPARENT 1003 | END:VEVENT 1004 | BEGIN:VEVENT 1005 | DTSTART;VALUE=DATE:20170823 1006 | DTEND;VALUE=DATE:20170824 1007 | DTSTAMP:20170711T171222Z 1008 | UID:c5b7d52b109fbcbd55039c4d8e3fa69f 1009 | CREATED:20170103T080401Z 1010 | DESCRIPTION:Gelber Sack nicht vergessen! 1011 | LAST-MODIFIED:20170126T100735Z 1012 | LOCATION:Luitpoldstraße\, Treuchtlingen 1013 | SEQUENCE:0 1014 | STATUS:CONFIRMED 1015 | SUMMARY:Gelber Sack 1016 | TRANSP:TRANSPARENT 1017 | END:VEVENT 1018 | BEGIN:VEVENT 1019 | DTSTART;VALUE=DATE:20170125 1020 | DTEND;VALUE=DATE:20170126 1021 | DTSTAMP:20170711T171222Z 1022 | UID:becee7d5a184726bb0096bee84297e63 1023 | CREATED:20170103T080401Z 1024 | DESCRIPTION:Gelber Sack nicht vergessen! 1025 | LAST-MODIFIED:20170126T100735Z 1026 | LOCATION:Luitpoldstraße\, Treuchtlingen 1027 | SEQUENCE:0 1028 | STATUS:CONFIRMED 1029 | SUMMARY:Gelber Sack 1030 | TRANSP:TRANSPARENT 1031 | END:VEVENT 1032 | BEGIN:VEVENT 1033 | DTSTART;VALUE=DATE:20171023 1034 | DTEND;VALUE=DATE:20171024 1035 | DTSTAMP:20170711T171222Z 1036 | UID:aee6a98c7260300e8b857fb6c94855d2 1037 | CREATED:20170103T080401Z 1038 | DESCRIPTION:Gelber Sack nicht vergessen! 1039 | LAST-MODIFIED:20170126T100735Z 1040 | LOCATION:Luitpoldstraße\, Treuchtlingen 1041 | SEQUENCE:0 1042 | STATUS:CONFIRMED 1043 | SUMMARY:Gelber Sack 1044 | TRANSP:TRANSPARENT 1045 | END:VEVENT 1046 | BEGIN:VEVENT 1047 | DTSTART;VALUE=DATE:20170726 1048 | DTEND;VALUE=DATE:20170727 1049 | DTSTAMP:20170711T171222Z 1050 | UID:a223fb2ab3c9c6948c2bfe17f5201fb2 1051 | CREATED:20170103T080401Z 1052 | DESCRIPTION:Gelber Sack nicht vergessen! 1053 | LAST-MODIFIED:20170126T100735Z 1054 | LOCATION:Luitpoldstraße\, Treuchtlingen 1055 | SEQUENCE:0 1056 | STATUS:CONFIRMED 1057 | SUMMARY:Gelber Sack 1058 | TRANSP:TRANSPARENT 1059 | END:VEVENT 1060 | BEGIN:VEVENT 1061 | DTSTART;VALUE=DATE:20171120 1062 | DTEND;VALUE=DATE:20171121 1063 | DTSTAMP:20170711T171222Z 1064 | UID:7e0895d1e0462593836c6ffab1051a44 1065 | CREATED:20170103T080401Z 1066 | DESCRIPTION:Gelber Sack nicht vergessen! 1067 | LAST-MODIFIED:20170126T100735Z 1068 | LOCATION:Luitpoldstraße\, Treuchtlingen 1069 | SEQUENCE:0 1070 | STATUS:CONFIRMED 1071 | SUMMARY:Gelber Sack 1072 | TRANSP:TRANSPARENT 1073 | END:VEVENT 1074 | BEGIN:VEVENT 1075 | DTSTART;VALUE=DATE:20170921 1076 | DTEND;VALUE=DATE:20170922 1077 | DTSTAMP:20170711T171222Z 1078 | UID:4c5c9c893de7b4f7b3900cecb8198d56 1079 | CREATED:20170103T080401Z 1080 | DESCRIPTION:Gelber Sack nicht vergessen! 1081 | LAST-MODIFIED:20170126T100735Z 1082 | LOCATION:Luitpoldstraße\, Treuchtlingen 1083 | SEQUENCE:0 1084 | STATUS:CONFIRMED 1085 | SUMMARY:Gelber Sack 1086 | TRANSP:TRANSPARENT 1087 | END:VEVENT 1088 | BEGIN:VEVENT 1089 | DTSTART;VALUE=DATE:20171220 1090 | DTEND;VALUE=DATE:20171221 1091 | DTSTAMP:20170711T171222Z 1092 | UID:0d3a14a2db43e83871d6e890d1bf106d 1093 | CREATED:20170103T080401Z 1094 | DESCRIPTION:Gelber Sack nicht vergessen! 1095 | LAST-MODIFIED:20170126T100735Z 1096 | LOCATION:Luitpoldstraße\, Treuchtlingen 1097 | SEQUENCE:0 1098 | STATUS:CONFIRMED 1099 | SUMMARY:Gelber Sack 1100 | TRANSP:TRANSPARENT 1101 | END:VEVENT 1102 | BEGIN:VEVENT 1103 | DTSTART;VALUE=DATE:20170913 1104 | DTEND;VALUE=DATE:20170914 1105 | DTSTAMP:20170711T171222Z 1106 | UID:ffdb3556ae7d8e69cdf1abdb3b8724e5 1107 | CREATED:20170103T080401Z 1108 | DESCRIPTION:braune Biotonne nicht vergessen! 1109 | LAST-MODIFIED:20170126T100735Z 1110 | LOCATION:Luitpoldstraße\, Treuchtlingen 1111 | SEQUENCE:0 1112 | STATUS:CONFIRMED 1113 | SUMMARY:braune Biotonne 1114 | TRANSP:TRANSPARENT 1115 | END:VEVENT 1116 | BEGIN:VEVENT 1117 | DTSTART;VALUE=DATE:20170726 1118 | DTEND;VALUE=DATE:20170727 1119 | DTSTAMP:20170711T171222Z 1120 | UID:eb12b8e39471d01a0731370caee3a8d9 1121 | CREATED:20170103T080401Z 1122 | DESCRIPTION:braune Biotonne nicht vergessen! 1123 | LAST-MODIFIED:20170126T100735Z 1124 | LOCATION:Luitpoldstraße\, Treuchtlingen 1125 | SEQUENCE:0 1126 | STATUS:CONFIRMED 1127 | SUMMARY:braune Biotonne 1128 | TRANSP:TRANSPARENT 1129 | END:VEVENT 1130 | BEGIN:VEVENT 1131 | DTSTART;VALUE=DATE:20170920 1132 | DTEND;VALUE=DATE:20170921 1133 | DTSTAMP:20170711T171222Z 1134 | UID:e6b7080131d472ce18d7287d69f28f95 1135 | CREATED:20170103T080401Z 1136 | DESCRIPTION:braune Biotonne nicht vergessen! 1137 | LAST-MODIFIED:20170126T100735Z 1138 | LOCATION:Luitpoldstraße\, Treuchtlingen 1139 | SEQUENCE:0 1140 | STATUS:CONFIRMED 1141 | SUMMARY:braune Biotonne 1142 | TRANSP:TRANSPARENT 1143 | END:VEVENT 1144 | BEGIN:VEVENT 1145 | DTSTART;VALUE=DATE:20170705 1146 | DTEND;VALUE=DATE:20170706 1147 | DTSTAMP:20170711T171222Z 1148 | UID:e3a004ecd454d47b251d9a54ca9eb1f4 1149 | CREATED:20170103T080401Z 1150 | DESCRIPTION:braune Biotonne nicht vergessen! 1151 | LAST-MODIFIED:20170126T100735Z 1152 | LOCATION:Luitpoldstraße\, Treuchtlingen 1153 | SEQUENCE:0 1154 | STATUS:CONFIRMED 1155 | SUMMARY:braune Biotonne 1156 | TRANSP:TRANSPARENT 1157 | END:VEVENT 1158 | BEGIN:VEVENT 1159 | DTSTART;VALUE=DATE:20171025 1160 | DTEND;VALUE=DATE:20171026 1161 | DTSTAMP:20170711T171222Z 1162 | UID:e19e1c3c2a9a53087d7965bcc9ced05f 1163 | CREATED:20170103T080401Z 1164 | DESCRIPTION:braune Biotonne nicht vergessen! 1165 | LAST-MODIFIED:20170126T100735Z 1166 | LOCATION:Luitpoldstraße\, Treuchtlingen 1167 | SEQUENCE:0 1168 | STATUS:CONFIRMED 1169 | SUMMARY:braune Biotonne 1170 | TRANSP:TRANSPARENT 1171 | END:VEVENT 1172 | BEGIN:VEVENT 1173 | DTSTART;VALUE=DATE:20170927 1174 | DTEND;VALUE=DATE:20170928 1175 | DTSTAMP:20170711T171222Z 1176 | UID:dab760f60854de69501f6f12f561640c 1177 | CREATED:20170103T080401Z 1178 | DESCRIPTION:braune Biotonne nicht vergessen! 1179 | LAST-MODIFIED:20170126T100735Z 1180 | LOCATION:Luitpoldstraße\, Treuchtlingen 1181 | SEQUENCE:0 1182 | STATUS:CONFIRMED 1183 | SUMMARY:braune Biotonne 1184 | TRANSP:TRANSPARENT 1185 | END:VEVENT 1186 | BEGIN:VEVENT 1187 | DTSTART;VALUE=DATE:20170830 1188 | DTEND;VALUE=DATE:20170831 1189 | DTSTAMP:20170711T171222Z 1190 | UID:c8d70536a1eba5b4a75f154c2b262747 1191 | CREATED:20170103T080401Z 1192 | DESCRIPTION:braune Biotonne nicht vergessen! 1193 | LAST-MODIFIED:20170126T100735Z 1194 | LOCATION:Luitpoldstraße\, Treuchtlingen 1195 | SEQUENCE:0 1196 | STATUS:CONFIRMED 1197 | SUMMARY:braune Biotonne 1198 | TRANSP:TRANSPARENT 1199 | END:VEVENT 1200 | BEGIN:VEVENT 1201 | DTSTART;VALUE=DATE:20171102 1202 | DTEND;VALUE=DATE:20171103 1203 | DTSTAMP:20170711T171222Z 1204 | UID:b61452b731eda4e297ddc5077371b23b 1205 | CREATED:20170103T080401Z 1206 | DESCRIPTION:braune Biotonne nicht vergessen! 1207 | LAST-MODIFIED:20170126T100735Z 1208 | LOCATION:Luitpoldstraße\, Treuchtlingen 1209 | SEQUENCE:0 1210 | STATUS:CONFIRMED 1211 | SUMMARY:braune Biotonne 1212 | TRANSP:TRANSPARENT 1213 | END:VEVENT 1214 | BEGIN:VEVENT 1215 | DTSTART;VALUE=DATE:20170118 1216 | DTEND;VALUE=DATE:20170119 1217 | DTSTAMP:20170711T171222Z 1218 | UID:acb89bbb7132daf5d1dfad9d37465566 1219 | CREATED:20170103T080401Z 1220 | DESCRIPTION:braune Biotonne nicht vergessen! 1221 | LAST-MODIFIED:20170126T100735Z 1222 | LOCATION:Luitpoldstraße\, Treuchtlingen 1223 | SEQUENCE:0 1224 | STATUS:CONFIRMED 1225 | SUMMARY:braune Biotonne 1226 | TRANSP:TRANSPARENT 1227 | END:VEVENT 1228 | BEGIN:VEVENT 1229 | DTSTART;VALUE=DATE:20171108 1230 | DTEND;VALUE=DATE:20171109 1231 | DTSTAMP:20170711T171222Z 1232 | UID:a93e6810ba360eea0abbc25d9625c6b5 1233 | CREATED:20170103T080401Z 1234 | DESCRIPTION:braune Biotonne nicht vergessen! 1235 | LAST-MODIFIED:20170126T100735Z 1236 | LOCATION:Luitpoldstraße\, Treuchtlingen 1237 | SEQUENCE:0 1238 | STATUS:CONFIRMED 1239 | SUMMARY:braune Biotonne 1240 | TRANSP:TRANSPARENT 1241 | END:VEVENT 1242 | BEGIN:VEVENT 1243 | DTSTART;VALUE=DATE:20170621 1244 | DTEND;VALUE=DATE:20170622 1245 | DTSTAMP:20170711T171222Z 1246 | UID:a6718774e02d1fc560e885b555c80635 1247 | CREATED:20170103T080401Z 1248 | DESCRIPTION:braune Biotonne nicht vergessen! 1249 | LAST-MODIFIED:20170126T100735Z 1250 | LOCATION:Luitpoldstraße\, Treuchtlingen 1251 | SEQUENCE:0 1252 | STATUS:CONFIRMED 1253 | SUMMARY:braune Biotonne 1254 | TRANSP:TRANSPARENT 1255 | END:VEVENT 1256 | BEGIN:VEVENT 1257 | DTSTART;VALUE=DATE:20170504 1258 | DTEND;VALUE=DATE:20170505 1259 | DTSTAMP:20170711T171222Z 1260 | UID:a649b385d5b63bf49c1708ee4c48e5fb 1261 | CREATED:20170103T080401Z 1262 | DESCRIPTION:braune Biotonne nicht vergessen! 1263 | LAST-MODIFIED:20170126T100735Z 1264 | LOCATION:Luitpoldstraße\, Treuchtlingen 1265 | SEQUENCE:0 1266 | STATUS:CONFIRMED 1267 | SUMMARY:braune Biotonne 1268 | TRANSP:TRANSPARENT 1269 | END:VEVENT 1270 | BEGIN:VEVENT 1271 | DTSTART;VALUE=DATE:20170719 1272 | DTEND;VALUE=DATE:20170720 1273 | DTSTAMP:20170711T171222Z 1274 | UID:9bc9a9a7a20118f55e8f8a3874f7caee 1275 | CREATED:20170103T080401Z 1276 | DESCRIPTION:braune Biotonne nicht vergessen! 1277 | LAST-MODIFIED:20170126T100735Z 1278 | LOCATION:Luitpoldstraße\, Treuchtlingen 1279 | SEQUENCE:0 1280 | STATUS:CONFIRMED 1281 | SUMMARY:braune Biotonne 1282 | TRANSP:TRANSPARENT 1283 | END:VEVENT 1284 | BEGIN:VEVENT 1285 | DTSTART;VALUE=DATE:20170906 1286 | DTEND;VALUE=DATE:20170907 1287 | DTSTAMP:20170711T171222Z 1288 | UID:863ef88cd36254a31ef0e940e89f7650 1289 | CREATED:20170103T080401Z 1290 | DESCRIPTION:braune Biotonne nicht vergessen! 1291 | LAST-MODIFIED:20170126T100735Z 1292 | LOCATION:Luitpoldstraße\, Treuchtlingen 1293 | SEQUENCE:0 1294 | STATUS:CONFIRMED 1295 | SUMMARY:braune Biotonne 1296 | TRANSP:TRANSPARENT 1297 | END:VEVENT 1298 | BEGIN:VEVENT 1299 | DTSTART;VALUE=DATE:20170817 1300 | DTEND;VALUE=DATE:20170818 1301 | DTSTAMP:20170711T171222Z 1302 | UID:63d836662f6ee8fafe3dab60077da9ad 1303 | CREATED:20170103T080401Z 1304 | DESCRIPTION:braune Biotonne nicht vergessen! 1305 | LAST-MODIFIED:20170126T100735Z 1306 | LOCATION:Luitpoldstraße\, Treuchtlingen 1307 | SEQUENCE:0 1308 | STATUS:CONFIRMED 1309 | SUMMARY:braune Biotonne 1310 | TRANSP:TRANSPARENT 1311 | END:VEVENT 1312 | BEGIN:VEVENT 1313 | DTSTART;VALUE=DATE:20171005 1314 | DTEND;VALUE=DATE:20171006 1315 | DTSTAMP:20170711T171222Z 1316 | UID:6026bb0d055caf74eac88b8b31dd7f05 1317 | CREATED:20170103T080401Z 1318 | DESCRIPTION:braune Biotonne nicht vergessen! 1319 | LAST-MODIFIED:20170126T100735Z 1320 | LOCATION:Luitpoldstraße\, Treuchtlingen 1321 | SEQUENCE:0 1322 | STATUS:CONFIRMED 1323 | SUMMARY:braune Biotonne 1324 | TRANSP:TRANSPARENT 1325 | END:VEVENT 1326 | BEGIN:VEVENT 1327 | DTSTART;VALUE=DATE:20171206 1328 | DTEND;VALUE=DATE:20171207 1329 | DTSTAMP:20170711T171222Z 1330 | UID:58b9d72d6641e50499b527f7e2afc8de 1331 | CREATED:20170103T080401Z 1332 | DESCRIPTION:braune Biotonne nicht vergessen! 1333 | LAST-MODIFIED:20170126T100735Z 1334 | LOCATION:Luitpoldstraße\, Treuchtlingen 1335 | SEQUENCE:0 1336 | STATUS:CONFIRMED 1337 | SUMMARY:braune Biotonne 1338 | TRANSP:TRANSPARENT 1339 | END:VEVENT 1340 | BEGIN:VEVENT 1341 | DTSTART;VALUE=DATE:20170809 1342 | DTEND;VALUE=DATE:20170810 1343 | DTSTAMP:20170711T171222Z 1344 | UID:55bb6c11f2a3556431e943f2dd10cd2c 1345 | CREATED:20170103T080401Z 1346 | DESCRIPTION:braune Biotonne nicht vergessen! 1347 | LAST-MODIFIED:20170126T100735Z 1348 | LOCATION:Luitpoldstraße\, Treuchtlingen 1349 | SEQUENCE:0 1350 | STATUS:CONFIRMED 1351 | SUMMARY:braune Biotonne 1352 | TRANSP:TRANSPARENT 1353 | END:VEVENT 1354 | BEGIN:VEVENT 1355 | DTSTART;VALUE=DATE:20171122 1356 | DTEND;VALUE=DATE:20171123 1357 | DTSTAMP:20170711T171222Z 1358 | UID:48dc695b62a967fed5a3f4c35c082b9e 1359 | CREATED:20170103T080401Z 1360 | DESCRIPTION:braune Biotonne nicht vergessen! 1361 | LAST-MODIFIED:20170126T100735Z 1362 | LOCATION:Luitpoldstraße\, Treuchtlingen 1363 | SEQUENCE:0 1364 | STATUS:CONFIRMED 1365 | SUMMARY:braune Biotonne 1366 | TRANSP:TRANSPARENT 1367 | END:VEVENT 1368 | BEGIN:VEVENT 1369 | DTSTART;VALUE=DATE:20170823 1370 | DTEND;VALUE=DATE:20170824 1371 | DTSTAMP:20170711T171222Z 1372 | UID:44d5882112b286940fd474c1256a330c 1373 | CREATED:20170103T080401Z 1374 | DESCRIPTION:braune Biotonne nicht vergessen! 1375 | LAST-MODIFIED:20170126T100735Z 1376 | LOCATION:Luitpoldstraße\, Treuchtlingen 1377 | SEQUENCE:0 1378 | STATUS:CONFIRMED 1379 | SUMMARY:braune Biotonne 1380 | TRANSP:TRANSPARENT 1381 | END:VEVENT 1382 | BEGIN:VEVENT 1383 | DTSTART;VALUE=DATE:20170104 1384 | DTEND;VALUE=DATE:20170105 1385 | DTSTAMP:20170711T171222Z 1386 | UID:35b4b6d13b585bccc3d9e76f209de769 1387 | CREATED:20170103T080401Z 1388 | DESCRIPTION:braune Biotonne nicht vergessen! 1389 | LAST-MODIFIED:20170126T100735Z 1390 | LOCATION:Luitpoldstraße\, Treuchtlingen 1391 | SEQUENCE:0 1392 | STATUS:CONFIRMED 1393 | SUMMARY:braune Biotonne 1394 | TRANSP:TRANSPARENT 1395 | BEGIN:VALARM 1396 | ACTION:NONE 1397 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 1398 | END:VALARM 1399 | END:VEVENT 1400 | BEGIN:VEVENT 1401 | DTSTART;VALUE=DATE:20170628 1402 | DTEND;VALUE=DATE:20170629 1403 | DTSTAMP:20170711T171222Z 1404 | UID:347383367fdf3f0f47e7c7720f626e1c 1405 | CREATED:20170103T080401Z 1406 | DESCRIPTION:braune Biotonne nicht vergessen! 1407 | LAST-MODIFIED:20170126T100735Z 1408 | LOCATION:Luitpoldstraße\, Treuchtlingen 1409 | SEQUENCE:0 1410 | STATUS:CONFIRMED 1411 | SUMMARY:braune Biotonne 1412 | TRANSP:TRANSPARENT 1413 | END:VEVENT 1414 | BEGIN:VEVENT 1415 | DTSTART;VALUE=DATE:20171220 1416 | DTEND;VALUE=DATE:20171221 1417 | DTSTAMP:20170711T171222Z 1418 | UID:2afeb7fb18f41bcbed600c88109fb34d 1419 | CREATED:20170103T080401Z 1420 | DESCRIPTION:braune Biotonne nicht vergessen! 1421 | LAST-MODIFIED:20170126T100735Z 1422 | LOCATION:Luitpoldstraße\, Treuchtlingen 1423 | SEQUENCE:0 1424 | STATUS:CONFIRMED 1425 | SUMMARY:braune Biotonne 1426 | TRANSP:TRANSPARENT 1427 | END:VEVENT 1428 | BEGIN:VEVENT 1429 | DTSTART;VALUE=DATE:20170802 1430 | DTEND;VALUE=DATE:20170803 1431 | DTSTAMP:20170711T171222Z 1432 | UID:22b2a3c2db5d4f5d156478197ce2f099 1433 | CREATED:20170103T080401Z 1434 | DESCRIPTION:braune Biotonne nicht vergessen! 1435 | LAST-MODIFIED:20170126T100735Z 1436 | LOCATION:Luitpoldstraße\, Treuchtlingen 1437 | SEQUENCE:0 1438 | STATUS:CONFIRMED 1439 | SUMMARY:braune Biotonne 1440 | TRANSP:TRANSPARENT 1441 | END:VEVENT 1442 | BEGIN:VEVENT 1443 | DTSTART;VALUE=DATE:20170315 1444 | DTEND;VALUE=DATE:20170316 1445 | DTSTAMP:20170711T171222Z 1446 | UID:1c9b834c5770278fb11c534741eddd22 1447 | CREATED:20170103T080401Z 1448 | DESCRIPTION:braune Biotonne nicht vergessen! 1449 | LAST-MODIFIED:20170126T100735Z 1450 | LOCATION:Luitpoldstraße\, Treuchtlingen 1451 | SEQUENCE:0 1452 | STATUS:CONFIRMED 1453 | SUMMARY:braune Biotonne 1454 | TRANSP:TRANSPARENT 1455 | END:VEVENT 1456 | BEGIN:VEVENT 1457 | DTSTART;VALUE=DATE:20171011 1458 | DTEND;VALUE=DATE:20171012 1459 | DTSTAMP:20170711T171222Z 1460 | UID:16ac78600ddabe1f3687ddbfe117759a 1461 | CREATED:20170103T080401Z 1462 | DESCRIPTION:braune Biotonne nicht vergessen! 1463 | LAST-MODIFIED:20170126T100735Z 1464 | LOCATION:Luitpoldstraße\, Treuchtlingen 1465 | SEQUENCE:0 1466 | STATUS:CONFIRMED 1467 | SUMMARY:braune Biotonne 1468 | TRANSP:TRANSPARENT 1469 | END:VEVENT 1470 | BEGIN:VEVENT 1471 | DTSTART;VALUE=DATE:20171018 1472 | DTEND;VALUE=DATE:20171019 1473 | DTSTAMP:20170711T171222Z 1474 | UID:02ffbdd421078a217b1a8055482e4012 1475 | CREATED:20170103T080401Z 1476 | DESCRIPTION:braune Biotonne nicht vergessen! 1477 | LAST-MODIFIED:20170126T100735Z 1478 | LOCATION:Luitpoldstraße\, Treuchtlingen 1479 | SEQUENCE:0 1480 | STATUS:CONFIRMED 1481 | SUMMARY:braune Biotonne 1482 | TRANSP:TRANSPARENT 1483 | END:VEVENT 1484 | BEGIN:VEVENT 1485 | DTSTART;VALUE=DATE:20161213 1486 | DTEND;VALUE=DATE:20161214 1487 | DTSTAMP:20170711T171222Z 1488 | UID:d2fqot83imae46g8ov14p93jv4@google.com 1489 | CREATED:20161208T124911Z 1490 | DESCRIPTION: 1491 | LAST-MODIFIED:20161212T122336Z 1492 | LOCATION:Treuchtlingen\, 91757 Treuchtlingen\, Deutschland 1493 | SEQUENCE:1 1494 | STATUS:CONFIRMED 1495 | SUMMARY:Bio 1496 | TRANSP:TRANSPARENT 1497 | X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC 1498 | BEGIN:VALARM 1499 | ACTION:AUDIO 1500 | TRIGGER:-PT15H 1501 | X-WR-ALARMUID:4B966FA0-EAB2-43C6-A054-0782318F886A 1502 | UID:4B966FA0-EAB2-43C6-A054-0782318F886A 1503 | ATTACH;VALUE=URI:Basso 1504 | X-APPLE-DEFAULT-ALARM:TRUE 1505 | ACKNOWLEDGED:20161212T122336Z 1506 | END:VALARM 1507 | END:VEVENT 1508 | END:VCALENDAR 1509 | --------------------------------------------------------------------------------