From 312b05a27521c54fee82271b27d05acd9c47aea9 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sun, 6 Oct 2024 20:10:37 +0100 Subject: [PATCH 01/50] add example calendars --- .../tests/calendars/alarm_etar_future.ics | 235 +++++++ .../calendars/alarm_etar_notification.ics | 235 +++++++ .../alarm_etar_notification_clicked.ics | 225 +++++++ .../calendars/alarm_google_acknowledged.ics | 60 ++ .../tests/calendars/alarm_google_future.ics | 60 ++ .../tests/calendars/alarm_outlook_closed.ics | 95 +++ .../calendars/alarm_outlook_in_future.ics | 95 +++ .../alarm_outlook_postponed_5_min.ics | 95 +++ ...ird_closed_postponed_and_closed_future.ics | 631 +++++++++++++++++ ...rm_thunderbird_closed_postponed_future.ics | 632 ++++++++++++++++++ 10 files changed, 2363 insertions(+) create mode 100644 src/icalendar/tests/calendars/alarm_etar_future.ics create mode 100644 src/icalendar/tests/calendars/alarm_etar_notification.ics create mode 100644 src/icalendar/tests/calendars/alarm_etar_notification_clicked.ics create mode 100644 src/icalendar/tests/calendars/alarm_google_acknowledged.ics create mode 100644 src/icalendar/tests/calendars/alarm_google_future.ics create mode 100644 src/icalendar/tests/calendars/alarm_outlook_closed.ics create mode 100644 src/icalendar/tests/calendars/alarm_outlook_in_future.ics create mode 100644 src/icalendar/tests/calendars/alarm_outlook_postponed_5_min.ics create mode 100644 src/icalendar/tests/calendars/alarm_thunderbird_closed_postponed_and_closed_future.ics create mode 100644 src/icalendar/tests/calendars/alarm_thunderbird_closed_postponed_future.ics diff --git a/src/icalendar/tests/calendars/alarm_etar_future.ics b/src/icalendar/tests/calendars/alarm_etar_future.ics new file mode 100644 index 00000000..d2735f5d --- /dev/null +++ b/src/icalendar/tests/calendars/alarm_etar_future.ics @@ -0,0 +1,235 @@ +BEGIN:VCALENDAR +PRODID:-//Offline Calendar//iCal Import/Export 2.8.1//EN +VERSION:2.0 +METHOD:PUBLISH +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/London +TZURL:http://tzurl.org/zoneinfo/Europe/London +X-LIC-LOCATION:Europe/London +BEGIN:DAYLIGHT +TZOFFSETFROM:+0000 +TZOFFSETTO:+0100 +TZNAME:BST +DTSTART:19810329T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0100 +TZOFFSETTO:+0000 +TZNAME:GMT +DTSTART:19961027T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +BEGIN:STANDARD +TZOFFSETFROM:-000115 +TZOFFSETTO:+0000 +TZNAME:GMT +DTSTART:18471201T000115 +RDATE:18471201T000115 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETFROM:+0000 +TZOFFSETTO:+0100 +TZNAME:BST +DTSTART:19160521T020000 +RDATE:19160521T020000 +RDATE:19170408T020000 +RDATE:19180324T020000 +RDATE:19190330T020000 +RDATE:19200328T020000 +RDATE:19210403T020000 +RDATE:19220326T020000 +RDATE:19230422T020000 +RDATE:19240413T020000 +RDATE:19250419T020000 +RDATE:19260418T020000 +RDATE:19270410T020000 +RDATE:19280422T020000 +RDATE:19290421T020000 +RDATE:19300413T020000 +RDATE:19310419T020000 +RDATE:19320417T020000 +RDATE:19330409T020000 +RDATE:19340422T020000 +RDATE:19350414T020000 +RDATE:19360419T020000 +RDATE:19370418T020000 +RDATE:19380410T020000 +RDATE:19390416T020000 +RDATE:19400225T020000 +RDATE:19460414T020000 +RDATE:19470316T020000 +RDATE:19480314T020000 +RDATE:19490403T020000 +RDATE:19500416T020000 +RDATE:19510415T020000 +RDATE:19520420T020000 +RDATE:19530419T020000 +RDATE:19540411T020000 +RDATE:19550417T020000 +RDATE:19560422T020000 +RDATE:19570414T020000 +RDATE:19580420T020000 +RDATE:19590419T020000 +RDATE:19600410T020000 +RDATE:19610326T020000 +RDATE:19620325T020000 +RDATE:19630331T020000 +RDATE:19640322T020000 +RDATE:19650321T020000 +RDATE:19660320T020000 +RDATE:19670319T020000 +RDATE:19680218T020000 +RDATE:19720319T020000 +RDATE:19730318T020000 +RDATE:19740317T020000 +RDATE:19750316T020000 +RDATE:19760321T020000 +RDATE:19770320T020000 +RDATE:19780319T020000 +RDATE:19790318T020000 +RDATE:19800316T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0100 +TZOFFSETTO:+0000 +TZNAME:GMT +DTSTART:19161001T030000 +RDATE:19161001T030000 +RDATE:19170917T030000 +RDATE:19180930T030000 +RDATE:19190929T030000 +RDATE:19201025T030000 +RDATE:19211003T030000 +RDATE:19221008T030000 +RDATE:19230916T030000 +RDATE:19240921T030000 +RDATE:19251004T030000 +RDATE:19261003T030000 +RDATE:19271002T030000 +RDATE:19281007T030000 +RDATE:19291006T030000 +RDATE:19301005T030000 +RDATE:19311004T030000 +RDATE:19321002T030000 +RDATE:19331008T030000 +RDATE:19341007T030000 +RDATE:19351006T030000 +RDATE:19361004T030000 +RDATE:19371003T030000 +RDATE:19381002T030000 +RDATE:19391119T030000 +RDATE:19451007T030000 +RDATE:19461006T030000 +RDATE:19471102T030000 +RDATE:19481031T030000 +RDATE:19491030T030000 +RDATE:19501022T030000 +RDATE:19511021T030000 +RDATE:19521026T030000 +RDATE:19531004T030000 +RDATE:19541003T030000 +RDATE:19551002T030000 +RDATE:19561007T030000 +RDATE:19571006T030000 +RDATE:19581005T030000 +RDATE:19591004T030000 +RDATE:19601002T030000 +RDATE:19611029T030000 +RDATE:19621028T030000 +RDATE:19631027T030000 +RDATE:19641025T030000 +RDATE:19651024T030000 +RDATE:19661023T030000 +RDATE:19671029T030000 +RDATE:19711031T030000 +RDATE:19721029T030000 +RDATE:19731028T030000 +RDATE:19741027T030000 +RDATE:19751026T030000 +RDATE:19761024T030000 +RDATE:19771023T030000 +RDATE:19781029T030000 +RDATE:19791028T030000 +RDATE:19801026T030000 +RDATE:19811025T020000 +RDATE:19821024T020000 +RDATE:19831023T020000 +RDATE:19841028T020000 +RDATE:19851027T020000 +RDATE:19861026T020000 +RDATE:19871025T020000 +RDATE:19881023T020000 +RDATE:19891029T020000 +RDATE:19901028T020000 +RDATE:19911027T020000 +RDATE:19921025T020000 +RDATE:19931024T020000 +RDATE:19941023T020000 +RDATE:19951022T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:BDST +DTSTART:19410504T010000 +RDATE:19410504T010000 +RDATE:19420405T010000 +RDATE:19430404T010000 +RDATE:19440402T010000 +RDATE:19450402T010000 +RDATE:19470413T010000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:BST +DTSTART:19410810T030000 +RDATE:19410810T030000 +RDATE:19420809T030000 +RDATE:19430815T030000 +RDATE:19440917T030000 +RDATE:19450715T030000 +RDATE:19470810T030000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0100 +TZOFFSETTO:+0100 +TZNAME:BST +DTSTART:19681026T230000 +RDATE:19681026T230000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETFROM:+0000 +TZOFFSETTO:+0000 +TZNAME:GMT +DTSTART:19960101T000000 +RDATE:19960101T000000 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTAMP:20241005T112701Z +UID:17281276213728ad54d03afa44d1ca60b8c52afaece9e@sufficientlysecure.org +SUMMARY:event with alarms android +STATUS:CONFIRMED +DTSTART;TZID=Europe/London:20241005T130000 +DTEND:20241005T130000Z +LAST-MODIFIED:20241005T112701Z +BEGIN:VALARM +TRIGGER:-PT30M +ACTION:DISPLAY +DESCRIPTION:event with alarms android +END:VALARM +BEGIN:VALARM +TRIGGER:-PT25M +ACTION:DISPLAY +DESCRIPTION:event with alarms android +END:VALARM +BEGIN:VALARM +TRIGGER:-PT5M +ACTION:DISPLAY +DESCRIPTION:event with alarms android +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/alarm_etar_notification.ics b/src/icalendar/tests/calendars/alarm_etar_notification.ics new file mode 100644 index 00000000..bbee664b --- /dev/null +++ b/src/icalendar/tests/calendars/alarm_etar_notification.ics @@ -0,0 +1,235 @@ +BEGIN:VCALENDAR +PRODID:-//Offline Calendar//iCal Import/Export 2.8.1//EN +VERSION:2.0 +METHOD:PUBLISH +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/London +TZURL:http://tzurl.org/zoneinfo/Europe/London +X-LIC-LOCATION:Europe/London +BEGIN:DAYLIGHT +TZOFFSETFROM:+0000 +TZOFFSETTO:+0100 +TZNAME:BST +DTSTART:19810329T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0100 +TZOFFSETTO:+0000 +TZNAME:GMT +DTSTART:19961027T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +BEGIN:STANDARD +TZOFFSETFROM:-000115 +TZOFFSETTO:+0000 +TZNAME:GMT +DTSTART:18471201T000115 +RDATE:18471201T000115 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETFROM:+0000 +TZOFFSETTO:+0100 +TZNAME:BST +DTSTART:19160521T020000 +RDATE:19160521T020000 +RDATE:19170408T020000 +RDATE:19180324T020000 +RDATE:19190330T020000 +RDATE:19200328T020000 +RDATE:19210403T020000 +RDATE:19220326T020000 +RDATE:19230422T020000 +RDATE:19240413T020000 +RDATE:19250419T020000 +RDATE:19260418T020000 +RDATE:19270410T020000 +RDATE:19280422T020000 +RDATE:19290421T020000 +RDATE:19300413T020000 +RDATE:19310419T020000 +RDATE:19320417T020000 +RDATE:19330409T020000 +RDATE:19340422T020000 +RDATE:19350414T020000 +RDATE:19360419T020000 +RDATE:19370418T020000 +RDATE:19380410T020000 +RDATE:19390416T020000 +RDATE:19400225T020000 +RDATE:19460414T020000 +RDATE:19470316T020000 +RDATE:19480314T020000 +RDATE:19490403T020000 +RDATE:19500416T020000 +RDATE:19510415T020000 +RDATE:19520420T020000 +RDATE:19530419T020000 +RDATE:19540411T020000 +RDATE:19550417T020000 +RDATE:19560422T020000 +RDATE:19570414T020000 +RDATE:19580420T020000 +RDATE:19590419T020000 +RDATE:19600410T020000 +RDATE:19610326T020000 +RDATE:19620325T020000 +RDATE:19630331T020000 +RDATE:19640322T020000 +RDATE:19650321T020000 +RDATE:19660320T020000 +RDATE:19670319T020000 +RDATE:19680218T020000 +RDATE:19720319T020000 +RDATE:19730318T020000 +RDATE:19740317T020000 +RDATE:19750316T020000 +RDATE:19760321T020000 +RDATE:19770320T020000 +RDATE:19780319T020000 +RDATE:19790318T020000 +RDATE:19800316T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0100 +TZOFFSETTO:+0000 +TZNAME:GMT +DTSTART:19161001T030000 +RDATE:19161001T030000 +RDATE:19170917T030000 +RDATE:19180930T030000 +RDATE:19190929T030000 +RDATE:19201025T030000 +RDATE:19211003T030000 +RDATE:19221008T030000 +RDATE:19230916T030000 +RDATE:19240921T030000 +RDATE:19251004T030000 +RDATE:19261003T030000 +RDATE:19271002T030000 +RDATE:19281007T030000 +RDATE:19291006T030000 +RDATE:19301005T030000 +RDATE:19311004T030000 +RDATE:19321002T030000 +RDATE:19331008T030000 +RDATE:19341007T030000 +RDATE:19351006T030000 +RDATE:19361004T030000 +RDATE:19371003T030000 +RDATE:19381002T030000 +RDATE:19391119T030000 +RDATE:19451007T030000 +RDATE:19461006T030000 +RDATE:19471102T030000 +RDATE:19481031T030000 +RDATE:19491030T030000 +RDATE:19501022T030000 +RDATE:19511021T030000 +RDATE:19521026T030000 +RDATE:19531004T030000 +RDATE:19541003T030000 +RDATE:19551002T030000 +RDATE:19561007T030000 +RDATE:19571006T030000 +RDATE:19581005T030000 +RDATE:19591004T030000 +RDATE:19601002T030000 +RDATE:19611029T030000 +RDATE:19621028T030000 +RDATE:19631027T030000 +RDATE:19641025T030000 +RDATE:19651024T030000 +RDATE:19661023T030000 +RDATE:19671029T030000 +RDATE:19711031T030000 +RDATE:19721029T030000 +RDATE:19731028T030000 +RDATE:19741027T030000 +RDATE:19751026T030000 +RDATE:19761024T030000 +RDATE:19771023T030000 +RDATE:19781029T030000 +RDATE:19791028T030000 +RDATE:19801026T030000 +RDATE:19811025T020000 +RDATE:19821024T020000 +RDATE:19831023T020000 +RDATE:19841028T020000 +RDATE:19851027T020000 +RDATE:19861026T020000 +RDATE:19871025T020000 +RDATE:19881023T020000 +RDATE:19891029T020000 +RDATE:19901028T020000 +RDATE:19911027T020000 +RDATE:19921025T020000 +RDATE:19931024T020000 +RDATE:19941023T020000 +RDATE:19951022T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:BDST +DTSTART:19410504T010000 +RDATE:19410504T010000 +RDATE:19420405T010000 +RDATE:19430404T010000 +RDATE:19440402T010000 +RDATE:19450402T010000 +RDATE:19470413T010000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:BST +DTSTART:19410810T030000 +RDATE:19410810T030000 +RDATE:19420809T030000 +RDATE:19430815T030000 +RDATE:19440917T030000 +RDATE:19450715T030000 +RDATE:19470810T030000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0100 +TZOFFSETTO:+0100 +TZNAME:BST +DTSTART:19681026T230000 +RDATE:19681026T230000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETFROM:+0000 +TZOFFSETTO:+0000 +TZNAME:GMT +DTSTART:19960101T000000 +RDATE:19960101T000000 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTAMP:20241005T113036Z +UID:17281276213728ad54d03afa44d1ca60b8c52afaece9e@sufficientlysecure.org +SUMMARY:event with alarms android +STATUS:CONFIRMED +DTSTART;TZID=Europe/London:20241005T130000 +DTEND:20241005T130000Z +LAST-MODIFIED:20241005T112701Z +BEGIN:VALARM +TRIGGER:-PT30M +ACTION:DISPLAY +DESCRIPTION:event with alarms android +END:VALARM +BEGIN:VALARM +TRIGGER:-PT25M +ACTION:DISPLAY +DESCRIPTION:event with alarms android +END:VALARM +BEGIN:VALARM +TRIGGER:-PT5M +ACTION:DISPLAY +DESCRIPTION:event with alarms android +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/alarm_etar_notification_clicked.ics b/src/icalendar/tests/calendars/alarm_etar_notification_clicked.ics new file mode 100644 index 00000000..c61e048f --- /dev/null +++ b/src/icalendar/tests/calendars/alarm_etar_notification_clicked.ics @@ -0,0 +1,225 @@ +BEGIN:VCALENDAR +PRODID:-//Offline Calendar//iCal Import/Export 2.8.1//EN +VERSION:2.0 +METHOD:PUBLISH +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/London +TZURL:http://tzurl.org/zoneinfo/Europe/London +X-LIC-LOCATION:Europe/London +BEGIN:DAYLIGHT +TZOFFSETFROM:+0000 +TZOFFSETTO:+0100 +TZNAME:BST +DTSTART:19810329T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0100 +TZOFFSETTO:+0000 +TZNAME:GMT +DTSTART:19961027T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +BEGIN:STANDARD +TZOFFSETFROM:-000115 +TZOFFSETTO:+0000 +TZNAME:GMT +DTSTART:18471201T000115 +RDATE:18471201T000115 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETFROM:+0000 +TZOFFSETTO:+0100 +TZNAME:BST +DTSTART:19160521T020000 +RDATE:19160521T020000 +RDATE:19170408T020000 +RDATE:19180324T020000 +RDATE:19190330T020000 +RDATE:19200328T020000 +RDATE:19210403T020000 +RDATE:19220326T020000 +RDATE:19230422T020000 +RDATE:19240413T020000 +RDATE:19250419T020000 +RDATE:19260418T020000 +RDATE:19270410T020000 +RDATE:19280422T020000 +RDATE:19290421T020000 +RDATE:19300413T020000 +RDATE:19310419T020000 +RDATE:19320417T020000 +RDATE:19330409T020000 +RDATE:19340422T020000 +RDATE:19350414T020000 +RDATE:19360419T020000 +RDATE:19370418T020000 +RDATE:19380410T020000 +RDATE:19390416T020000 +RDATE:19400225T020000 +RDATE:19460414T020000 +RDATE:19470316T020000 +RDATE:19480314T020000 +RDATE:19490403T020000 +RDATE:19500416T020000 +RDATE:19510415T020000 +RDATE:19520420T020000 +RDATE:19530419T020000 +RDATE:19540411T020000 +RDATE:19550417T020000 +RDATE:19560422T020000 +RDATE:19570414T020000 +RDATE:19580420T020000 +RDATE:19590419T020000 +RDATE:19600410T020000 +RDATE:19610326T020000 +RDATE:19620325T020000 +RDATE:19630331T020000 +RDATE:19640322T020000 +RDATE:19650321T020000 +RDATE:19660320T020000 +RDATE:19670319T020000 +RDATE:19680218T020000 +RDATE:19720319T020000 +RDATE:19730318T020000 +RDATE:19740317T020000 +RDATE:19750316T020000 +RDATE:19760321T020000 +RDATE:19770320T020000 +RDATE:19780319T020000 +RDATE:19790318T020000 +RDATE:19800316T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0100 +TZOFFSETTO:+0000 +TZNAME:GMT +DTSTART:19161001T030000 +RDATE:19161001T030000 +RDATE:19170917T030000 +RDATE:19180930T030000 +RDATE:19190929T030000 +RDATE:19201025T030000 +RDATE:19211003T030000 +RDATE:19221008T030000 +RDATE:19230916T030000 +RDATE:19240921T030000 +RDATE:19251004T030000 +RDATE:19261003T030000 +RDATE:19271002T030000 +RDATE:19281007T030000 +RDATE:19291006T030000 +RDATE:19301005T030000 +RDATE:19311004T030000 +RDATE:19321002T030000 +RDATE:19331008T030000 +RDATE:19341007T030000 +RDATE:19351006T030000 +RDATE:19361004T030000 +RDATE:19371003T030000 +RDATE:19381002T030000 +RDATE:19391119T030000 +RDATE:19451007T030000 +RDATE:19461006T030000 +RDATE:19471102T030000 +RDATE:19481031T030000 +RDATE:19491030T030000 +RDATE:19501022T030000 +RDATE:19511021T030000 +RDATE:19521026T030000 +RDATE:19531004T030000 +RDATE:19541003T030000 +RDATE:19551002T030000 +RDATE:19561007T030000 +RDATE:19571006T030000 +RDATE:19581005T030000 +RDATE:19591004T030000 +RDATE:19601002T030000 +RDATE:19611029T030000 +RDATE:19621028T030000 +RDATE:19631027T030000 +RDATE:19641025T030000 +RDATE:19651024T030000 +RDATE:19661023T030000 +RDATE:19671029T030000 +RDATE:19711031T030000 +RDATE:19721029T030000 +RDATE:19731028T030000 +RDATE:19741027T030000 +RDATE:19751026T030000 +RDATE:19761024T030000 +RDATE:19771023T030000 +RDATE:19781029T030000 +RDATE:19791028T030000 +RDATE:19801026T030000 +RDATE:19811025T020000 +RDATE:19821024T020000 +RDATE:19831023T020000 +RDATE:19841028T020000 +RDATE:19851027T020000 +RDATE:19861026T020000 +RDATE:19871025T020000 +RDATE:19881023T020000 +RDATE:19891029T020000 +RDATE:19901028T020000 +RDATE:19911027T020000 +RDATE:19921025T020000 +RDATE:19931024T020000 +RDATE:19941023T020000 +RDATE:19951022T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:BDST +DTSTART:19410504T010000 +RDATE:19410504T010000 +RDATE:19420405T010000 +RDATE:19430404T010000 +RDATE:19440402T010000 +RDATE:19450402T010000 +RDATE:19470413T010000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:BST +DTSTART:19410810T030000 +RDATE:19410810T030000 +RDATE:19420809T030000 +RDATE:19430815T030000 +RDATE:19440917T030000 +RDATE:19450715T030000 +RDATE:19470810T030000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0100 +TZOFFSETTO:+0100 +TZNAME:BST +DTSTART:19681026T230000 +RDATE:19681026T230000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETFROM:+0000 +TZOFFSETTO:+0000 +TZNAME:GMT +DTSTART:19960101T000000 +RDATE:19960101T000000 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTAMP:20241005T130738Z +UID:17281336589228ad54d03afa44d1ca60b8c52afaece9e@sufficientlysecure.org +SUMMARY:event with alarm acknowledged +STATUS:CONFIRMED +DTSTART;TZID=Europe/London:20241005T141700 +DTEND:20241005T141700Z +LAST-MODIFIED:20241005T130738Z +BEGIN:VALARM +TRIGGER:-PT10M +ACTION:DISPLAY +DESCRIPTION:event with alarm acknowledged +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/alarm_google_acknowledged.ics b/src/icalendar/tests/calendars/alarm_google_acknowledged.ics new file mode 100644 index 00000000..013c2405 --- /dev/null +++ b/src/icalendar/tests/calendars/alarm_google_acknowledged.ics @@ -0,0 +1,60 @@ +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:Nicco Kunzmann +X-WR-TIMEZONE:Europe/London +BEGIN:VTIMEZONE +TZID:Europe/Berlin +X-LIC-LOCATION:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:GMT+2 +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:GMT+1 +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART:20241004T181500Z +DTEND:20241004T190000Z +DTSTAMP:20241004T180026Z +UID:79fs7pkqvht9m5igs0vjv1sfra@google.com +CREATED:20241004T175920Z +LAST-MODIFIED:20241004T175928Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:event with alarms +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-P0DT0H10M0S +DESCRIPTION:This is an event reminder +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-P0DT0H14M0S +DESCRIPTION:This is an event reminder +END:VALARM +BEGIN:VALARM +ACTION:EMAIL +ATTENDEE:mailto:niccokunzmann@googlemail.com +TRIGGER:-P0DT0H15M0S +DESCRIPTION:This is an event reminder +SUMMARY:Alarm notification +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-P0DT0H15M0S +DESCRIPTION:This is an event reminder +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/alarm_google_future.ics b/src/icalendar/tests/calendars/alarm_google_future.ics new file mode 100644 index 00000000..f7e4e7ef --- /dev/null +++ b/src/icalendar/tests/calendars/alarm_google_future.ics @@ -0,0 +1,60 @@ +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:Nicco Kunzmann +X-WR-TIMEZONE:Europe/London +BEGIN:VTIMEZONE +TZID:Europe/Berlin +X-LIC-LOCATION:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:GMT+2 +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:GMT+1 +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART:20241004T181500Z +DTEND:20241004T190000Z +DTSTAMP:20241004T175945Z +UID:79fs7pkqvht9m5igs0vjv1sfra@google.com +CREATED:20241004T175920Z +LAST-MODIFIED:20241004T175928Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:event with alarms +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-P0DT0H10M0S +DESCRIPTION:This is an event reminder +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-P0DT0H14M0S +DESCRIPTION:This is an event reminder +END:VALARM +BEGIN:VALARM +ACTION:EMAIL +ATTENDEE:mailto:niccokunzmann@googlemail.com +TRIGGER:-P0DT0H15M0S +DESCRIPTION:This is an event reminder +SUMMARY:Alarm notification +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-P0DT0H15M0S +DESCRIPTION:This is an event reminder +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/alarm_outlook_closed.ics b/src/icalendar/tests/calendars/alarm_outlook_closed.ics new file mode 100644 index 00000000..3e073b82 --- /dev/null +++ b/src/icalendar/tests/calendars/alarm_outlook_closed.ics @@ -0,0 +1,95 @@ +BEGIN:VCALENDAR +METHOD:PUBLISH +PRODID:Microsoft Exchange Server 2010 +VERSION:2.0 +X-WR-CALNAME:Kalender +BEGIN:VTIMEZONE +TZID:GMT Standard Time +BEGIN:STANDARD +DTSTART:16010101T020000 +TZOFFSETFROM:+0100 +TZOFFSETTO:+0000 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T010000 +TZOFFSETFROM:+0000 +TZOFFSETTO:+0100 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +RRULE:FREQ=WEEKLY;UNTIL=20240728T230000Z;INTERVAL=2;BYDAY=MO;WKST=MO +EXDATE;TZID=GMT Standard Time:20240715T000000 +UID:111 +SUMMARY:test123 +DTSTART;VALUE=DATE:20240701 +DTEND;VALUE=DATE:20240708 +CLASS:PUBLIC +PRIORITY:5 +DTSTAMP:20241004T173601Z +TRANSP:OPAQUE +STATUS:CONFIRMED +SEQUENCE:3 +X-MICROSOFT-CDO-APPT-SEQUENCE:3 +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-ALLDAYEVENT:TRUE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:1 +X-MICROSOFT-DONOTFORWARDMEETING:FALSE +X-MICROSOFT-DISALLOW-COUNTER:FALSE +X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT +X-MICROSOFT-ISRESPONSEREQUESTED:FALSE +END:VEVENT +BEGIN:VEVENT +RRULE:FREQ=WEEKLY;UNTIL=20240728T230000Z;INTERVAL=2;BYDAY=MO;WKST=MO +EXDATE;TZID=GMT Standard Time:20240715T000000 +UID:040000008200E00074C5B7101A82E00800000000C41E94C5B4F3DA01000000000000000 + 0100000007EA1D0AA9DB96545A536FABC0B0C1F5A +SUMMARY:test123 - edited event!!!! +DTSTART;VALUE=DATE:20240701 +DTEND;VALUE=DATE:20240708 +CLASS:PUBLIC +PRIORITY:5 +DTSTAMP:20241004T173601Z +TRANSP:OPAQUE +STATUS:CONFIRMED +SEQUENCE:2 +X-MICROSOFT-CDO-APPT-SEQUENCE:2 +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-ALLDAYEVENT:TRUE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:1 +X-MICROSOFT-DONOTFORWARDMEETING:FALSE +X-MICROSOFT-DISALLOW-COUNTER:FALSE +X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT +X-MICROSOFT-ISRESPONSEREQUESTED:FALSE +END:VEVENT +BEGIN:VEVENT +DESCRIPTION:\n +UID:040000008200E00074C5B7101A82E00800000000EF8046288216DB01000000000000000 + 0100000000D748168D60A25439FA3E76297E80CB0 +SUMMARY:outlook event with alarm +DTSTART;TZID=GMT Standard Time:20241004T183000 +DTEND;TZID=GMT Standard Time:20241004T190000 +CLASS:PUBLIC +PRIORITY:5 +DTSTAMP:20241004T173601Z +TRANSP:OPAQUE +STATUS:CONFIRMED +SEQUENCE:0 +LOCATION: +X-MICROSOFT-CDO-APPT-SEQUENCE:0 +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-ALLDAYEVENT:FALSE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:0 +X-MICROSOFT-DONOTFORWARDMEETING:FALSE +X-MICROSOFT-DISALLOW-COUNTER:FALSE +X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT +X-MICROSOFT-ISRESPONSEREQUESTED:FALSE +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/alarm_outlook_in_future.ics b/src/icalendar/tests/calendars/alarm_outlook_in_future.ics new file mode 100644 index 00000000..874a7d3a --- /dev/null +++ b/src/icalendar/tests/calendars/alarm_outlook_in_future.ics @@ -0,0 +1,95 @@ +BEGIN:VCALENDAR +METHOD:PUBLISH +PRODID:Microsoft Exchange Server 2010 +VERSION:2.0 +X-WR-CALNAME:Kalender +BEGIN:VTIMEZONE +TZID:GMT Standard Time +BEGIN:STANDARD +DTSTART:16010101T020000 +TZOFFSETFROM:+0100 +TZOFFSETTO:+0000 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T010000 +TZOFFSETFROM:+0000 +TZOFFSETTO:+0100 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +RRULE:FREQ=WEEKLY;UNTIL=20240728T230000Z;INTERVAL=2;BYDAY=MO;WKST=MO +EXDATE;TZID=GMT Standard Time:20240715T000000 +UID:111 +SUMMARY:test123 +DTSTART;VALUE=DATE:20240701 +DTEND;VALUE=DATE:20240708 +CLASS:PUBLIC +PRIORITY:5 +DTSTAMP:20241004T172524Z +TRANSP:OPAQUE +STATUS:CONFIRMED +SEQUENCE:3 +X-MICROSOFT-CDO-APPT-SEQUENCE:3 +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-ALLDAYEVENT:TRUE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:1 +X-MICROSOFT-DONOTFORWARDMEETING:FALSE +X-MICROSOFT-DISALLOW-COUNTER:FALSE +X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT +X-MICROSOFT-ISRESPONSEREQUESTED:FALSE +END:VEVENT +BEGIN:VEVENT +RRULE:FREQ=WEEKLY;UNTIL=20240728T230000Z;INTERVAL=2;BYDAY=MO;WKST=MO +EXDATE;TZID=GMT Standard Time:20240715T000000 +UID:040000008200E00074C5B7101A82E00800000000C41E94C5B4F3DA01000000000000000 + 0100000007EA1D0AA9DB96545A536FABC0B0C1F5A +SUMMARY:test123 - edited event!!!! +DTSTART;VALUE=DATE:20240701 +DTEND;VALUE=DATE:20240708 +CLASS:PUBLIC +PRIORITY:5 +DTSTAMP:20241004T172524Z +TRANSP:OPAQUE +STATUS:CONFIRMED +SEQUENCE:2 +X-MICROSOFT-CDO-APPT-SEQUENCE:2 +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-ALLDAYEVENT:TRUE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:1 +X-MICROSOFT-DONOTFORWARDMEETING:FALSE +X-MICROSOFT-DISALLOW-COUNTER:FALSE +X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT +X-MICROSOFT-ISRESPONSEREQUESTED:FALSE +END:VEVENT +BEGIN:VEVENT +DESCRIPTION:\n +UID:040000008200E00074C5B7101A82E00800000000EF8046288216DB01000000000000000 + 0100000000D748168D60A25439FA3E76297E80CB0 +SUMMARY:outlook event with alarm +DTSTART;TZID=GMT Standard Time:20241004T183000 +DTEND;TZID=GMT Standard Time:20241004T190000 +CLASS:PUBLIC +PRIORITY:5 +DTSTAMP:20241004T172524Z +TRANSP:OPAQUE +STATUS:CONFIRMED +SEQUENCE:0 +LOCATION: +X-MICROSOFT-CDO-APPT-SEQUENCE:0 +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-ALLDAYEVENT:FALSE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:0 +X-MICROSOFT-DONOTFORWARDMEETING:FALSE +X-MICROSOFT-DISALLOW-COUNTER:FALSE +X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT +X-MICROSOFT-ISRESPONSEREQUESTED:FALSE +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/alarm_outlook_postponed_5_min.ics b/src/icalendar/tests/calendars/alarm_outlook_postponed_5_min.ics new file mode 100644 index 00000000..0a4abf0d --- /dev/null +++ b/src/icalendar/tests/calendars/alarm_outlook_postponed_5_min.ics @@ -0,0 +1,95 @@ +BEGIN:VCALENDAR +METHOD:PUBLISH +PRODID:Microsoft Exchange Server 2010 +VERSION:2.0 +X-WR-CALNAME:Kalender +BEGIN:VTIMEZONE +TZID:GMT Standard Time +BEGIN:STANDARD +DTSTART:16010101T020000 +TZOFFSETFROM:+0100 +TZOFFSETTO:+0000 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T010000 +TZOFFSETFROM:+0000 +TZOFFSETTO:+0100 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +RRULE:FREQ=WEEKLY;UNTIL=20240728T230000Z;INTERVAL=2;BYDAY=MO;WKST=MO +EXDATE;TZID=GMT Standard Time:20240715T000000 +UID:111 +SUMMARY:test123 +DTSTART;VALUE=DATE:20240701 +DTEND;VALUE=DATE:20240708 +CLASS:PUBLIC +PRIORITY:5 +DTSTAMP:20241004T173132Z +TRANSP:OPAQUE +STATUS:CONFIRMED +SEQUENCE:3 +X-MICROSOFT-CDO-APPT-SEQUENCE:3 +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-ALLDAYEVENT:TRUE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:1 +X-MICROSOFT-DONOTFORWARDMEETING:FALSE +X-MICROSOFT-DISALLOW-COUNTER:FALSE +X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT +X-MICROSOFT-ISRESPONSEREQUESTED:FALSE +END:VEVENT +BEGIN:VEVENT +RRULE:FREQ=WEEKLY;UNTIL=20240728T230000Z;INTERVAL=2;BYDAY=MO;WKST=MO +EXDATE;TZID=GMT Standard Time:20240715T000000 +UID:040000008200E00074C5B7101A82E00800000000C41E94C5B4F3DA01000000000000000 + 0100000007EA1D0AA9DB96545A536FABC0B0C1F5A +SUMMARY:test123 - edited event!!!! +DTSTART;VALUE=DATE:20240701 +DTEND;VALUE=DATE:20240708 +CLASS:PUBLIC +PRIORITY:5 +DTSTAMP:20241004T173132Z +TRANSP:OPAQUE +STATUS:CONFIRMED +SEQUENCE:2 +X-MICROSOFT-CDO-APPT-SEQUENCE:2 +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-ALLDAYEVENT:TRUE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:1 +X-MICROSOFT-DONOTFORWARDMEETING:FALSE +X-MICROSOFT-DISALLOW-COUNTER:FALSE +X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT +X-MICROSOFT-ISRESPONSEREQUESTED:FALSE +END:VEVENT +BEGIN:VEVENT +DESCRIPTION:\n +UID:040000008200E00074C5B7101A82E00800000000EF8046288216DB01000000000000000 + 0100000000D748168D60A25439FA3E76297E80CB0 +SUMMARY:outlook event with alarm +DTSTART;TZID=GMT Standard Time:20241004T183000 +DTEND;TZID=GMT Standard Time:20241004T190000 +CLASS:PUBLIC +PRIORITY:5 +DTSTAMP:20241004T173132Z +TRANSP:OPAQUE +STATUS:CONFIRMED +SEQUENCE:0 +LOCATION: +X-MICROSOFT-CDO-APPT-SEQUENCE:0 +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-ALLDAYEVENT:FALSE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:0 +X-MICROSOFT-DONOTFORWARDMEETING:FALSE +X-MICROSOFT-DISALLOW-COUNTER:FALSE +X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT +X-MICROSOFT-ISRESPONSEREQUESTED:FALSE +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/alarm_thunderbird_closed_postponed_and_closed_future.ics b/src/icalendar/tests/calendars/alarm_thunderbird_closed_postponed_and_closed_future.ics new file mode 100644 index 00000000..9e984408 --- /dev/null +++ b/src/icalendar/tests/calendars/alarm_thunderbird_closed_postponed_and_closed_future.ics @@ -0,0 +1,631 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Europe/London +X-TZINFO:Europe/London[2024a] +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:-000115 +TZNAME:Europe/London(STD) +DTSTART:18471201T000000 +RDATE:18471201T000000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19160521T020000 +RDATE:19160521T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19161001T030000 +RDATE:19161001T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19170408T020000 +RDATE:19170408T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19170917T030000 +RDATE:19170917T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19180324T020000 +RDATE:19180324T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19180930T030000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19190330T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19201025T030000 +RDATE:19201025T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19210403T020000 +RDATE:19210403T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19211003T030000 +RDATE:19211003T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19220326T020000 +RDATE:19220326T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19221008T030000 +RDATE:19221008T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19230422T020000 +RDATE:19230422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19240413T020000 +RDATE:19240413T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19230916T030000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19250419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19270410T020000 +RDATE:19270410T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19280422T020000 +RDATE:19280422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19290421T020000 +RDATE:19290421T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19300413T020000 +RDATE:19300413T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19310419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19251004T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19330409T020000 +RDATE:19330409T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19331008T030000 +RDATE:19331008T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19340422T020000 +RDATE:19340422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19350414T020000 +RDATE:19350414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19360419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19380410T020000 +RDATE:19380410T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19341007T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19390416T020000 +RDATE:19390416T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19400225T020000 +RDATE:19400225T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19410504T020000 +RDATE:19410504T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19410810T030000 +RDATE:19410810T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19420405T020000 +RDATE:19420405T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19420809T030000 +RDATE:19420809T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19430404T020000 +RDATE:19430404T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19430815T030000 +RDATE:19430815T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19440402T020000 +RDATE:19440402T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19440917T030000 +RDATE:19440917T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19450402T020000 +RDATE:19450402T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19391119T030000 +RDATE:19391119T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19450715T030000 +RDATE:19450715T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19460414T020000 +RDATE:19460414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19470316T020000 +RDATE:19470316T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19470413T020000 +RDATE:19470413T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19451007T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19470810T030000 +RDATE:19470810T030000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19471102T030000 +RDATE:19471102T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19480314T020000 +RDATE:19480314T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19490403T020000 +RDATE:19490403T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19481031T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19501022T030000 +RDATE:19501022T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19511021T030000 +RDATE:19511021T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19521026T030000 +RDATE:19521026T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19500416T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19540411T020000 +RDATE:19540411T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19550417T020000 +RDATE:19550417T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19560422T020000 +RDATE:19560422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19570414T020000 +RDATE:19570414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19580420T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19600410T020000 +RDATE:19600410T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19531004T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19610326T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19640322T020000 +RDATE:19640322T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19611029T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19651024T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19650321T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19671029T030000 +RDATE:19671029T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+010000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19681027T000000 +RDATE:19681027T000000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19680218T020000 +RDATE:19680218T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19711031T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19761024T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19720319T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19781029T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19811025T020000 +RDATE:19811025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19821024T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19841028T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19881023T020000 +RDATE:19881023T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19891029T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19931024T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19810329T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19961027T020000 +RDATE:19961027T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:(DST) +DTSTART:19970330T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:(STD) +DTSTART:19971026T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20241004T172045Z +LAST-MODIFIED:20241004T173612Z +DTSTAMP:20241004T173612Z +UID:5dbcd1f8-4aec-4e7d-a1db-4df8e330dd71 +SUMMARY:event with alarms +X-MOZ-LASTACK:20241004T173612Z +DTSTART;TZID=Europe/London:20241004T190000 +DTEND;TZID=Europe/London:20241004T200000 +TRANSP:OPAQUE +X-MOZ-GENERATION:6 +SEQUENCE:1 +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT30M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT40M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT18M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/alarm_thunderbird_closed_postponed_future.ics b/src/icalendar/tests/calendars/alarm_thunderbird_closed_postponed_future.ics new file mode 100644 index 00000000..8fea4c81 --- /dev/null +++ b/src/icalendar/tests/calendars/alarm_thunderbird_closed_postponed_future.ics @@ -0,0 +1,632 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Europe/London +X-TZINFO:Europe/London[2024a] +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:-000115 +TZNAME:Europe/London(STD) +DTSTART:18471201T000000 +RDATE:18471201T000000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19160521T020000 +RDATE:19160521T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19161001T030000 +RDATE:19161001T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19170408T020000 +RDATE:19170408T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19170917T030000 +RDATE:19170917T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19180324T020000 +RDATE:19180324T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19180930T030000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19190330T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19201025T030000 +RDATE:19201025T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19210403T020000 +RDATE:19210403T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19211003T030000 +RDATE:19211003T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19220326T020000 +RDATE:19220326T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19221008T030000 +RDATE:19221008T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19230422T020000 +RDATE:19230422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19240413T020000 +RDATE:19240413T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19230916T030000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19250419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19270410T020000 +RDATE:19270410T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19280422T020000 +RDATE:19280422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19290421T020000 +RDATE:19290421T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19300413T020000 +RDATE:19300413T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19310419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19251004T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19330409T020000 +RDATE:19330409T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19331008T030000 +RDATE:19331008T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19340422T020000 +RDATE:19340422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19350414T020000 +RDATE:19350414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19360419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19380410T020000 +RDATE:19380410T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19341007T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19390416T020000 +RDATE:19390416T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19400225T020000 +RDATE:19400225T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19410504T020000 +RDATE:19410504T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19410810T030000 +RDATE:19410810T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19420405T020000 +RDATE:19420405T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19420809T030000 +RDATE:19420809T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19430404T020000 +RDATE:19430404T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19430815T030000 +RDATE:19430815T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19440402T020000 +RDATE:19440402T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19440917T030000 +RDATE:19440917T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19450402T020000 +RDATE:19450402T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19391119T030000 +RDATE:19391119T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19450715T030000 +RDATE:19450715T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19460414T020000 +RDATE:19460414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19470316T020000 +RDATE:19470316T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19470413T020000 +RDATE:19470413T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19451007T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19470810T030000 +RDATE:19470810T030000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19471102T030000 +RDATE:19471102T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19480314T020000 +RDATE:19480314T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19490403T020000 +RDATE:19490403T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19481031T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19501022T030000 +RDATE:19501022T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19511021T030000 +RDATE:19511021T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19521026T030000 +RDATE:19521026T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19500416T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19540411T020000 +RDATE:19540411T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19550417T020000 +RDATE:19550417T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19560422T020000 +RDATE:19560422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19570414T020000 +RDATE:19570414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19580420T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19600410T020000 +RDATE:19600410T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19531004T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19610326T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19640322T020000 +RDATE:19640322T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19611029T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19651024T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19650321T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19671029T030000 +RDATE:19671029T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+010000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19681027T000000 +RDATE:19681027T000000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19680218T020000 +RDATE:19680218T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19711031T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19761024T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19720319T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19781029T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19811025T020000 +RDATE:19811025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19821024T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19841028T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19881023T020000 +RDATE:19881023T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19891029T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19931024T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19810329T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19961027T020000 +RDATE:19961027T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:(DST) +DTSTART:19970330T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:(STD) +DTSTART:19971026T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20241004T172045Z +LAST-MODIFIED:20241004T173006Z +DTSTAMP:20241004T173006Z +UID:5dbcd1f8-4aec-4e7d-a1db-4df8e330dd71 +SUMMARY:event with alarms +X-MOZ-LASTACK:20241004T173006Z +DTSTART;TZID=Europe/London:20241004T190000 +DTEND;TZID=Europe/London:20241004T200000 +TRANSP:OPAQUE +X-MOZ-GENERATION:5 +SEQUENCE:1 +X-MOZ-SNOOZE-TIME:20241004T173506Z +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT30M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT40M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT18M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +END:VEVENT +END:VCALENDAR From 43e07fc379a053ddc35342d0cd8103ae321f0e3b Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Tue, 22 Oct 2024 12:19:53 +0100 Subject: [PATCH 02/50] Create interface that I would like to use. --- src/icalendar/alarms.py | 121 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/icalendar/alarms.py diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py new file mode 100644 index 00000000..92b81a13 --- /dev/null +++ b/src/icalendar/alarms.py @@ -0,0 +1,121 @@ +"""Compute the times and states of alarms. + +This takes different calendar software into account and RFC 9074 (Alarm Extension). +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from datetime import date, datetime +from typing import Optional + +from icalendar.cal import Alarm, Component, Event, Todo + + +class IncompleteAlarmInformation(ValueError): + """The alarms cannot be calculated yet because information is missing.""" + + +class AlarmTime(ABC): + """An alarm time with all the information.""" + + def __init__(self, alarm: Alarm, parent: Optional[Component]): + """Create a new AlarmTime.""" + self._alarm = alarm + self._parent = parent + + @property + def alarm(self) -> Alarm: + """The alarm component.""" + return self._alarm + + @property + def parent(self) -> Optional[Component]: + """This is the component that contains the alarm. + + This is None if you did not use Alarms.set_component(). + """ + return self._parent + + @property + @abstractmethod + def is_active(self) -> bool: + """Whether this alarm is active (True) or acknowledged (False). + + E.g. in some calendar software, this is True until the user had a look + at the alarm message and clicked the dismiss button. + """ + + @property + @abstractmethod + def trigger(self) -> datetime: + """This is the time to trigger the alarm.""" + + +class Alarms: + """Compute the times and states of alarms. + + TODO: example! + + RFC 9074 specifies that alarms can also be triggered by proximity. + This is not implemented yet. + """ + + def __init__(self): + """Start computing alarm times.""" + + def from_component(self, component: Event | Todo) -> None: + """Create an Alarm computation from the component.""" + + def set_component(self, component: Component): + """Optional: Set the component of the computed alarms. + + This does not change the computation in any way. + It makes it easier to identify the components of the alarms in case + you combine several computations. + """ + + def add_alarm(self, alarm: Alarm) -> None: + """Optional: Add an alarm component.""" + + def set_component_start(self, dt: date): + """Set the start of the component. + + If you have only absolute alarms, this is not required. + If you have alarms relative to start or end, you need to + set the start or the end respectively. + """ + + def set_component_end(self, dt: date): + """Set the end of the component. + + If you have only absolute alarms, this is not required. + If you have alarms relative to start or end, you need to + set the start or the end respectively. + """ + + def add_acknowledged_time(self, dt: date) -> None: + """This is the time when all the alarms of this component were acknowledged. + + You can set several times like this. Only the latest one counts. + + Since RFC 9074 (Alarm Extension) was created later, + calendar implementations differ in how they ackknowledge alarms. + E.g. Thunderbird and Google Calendar store the last time + an event has been acknowledged because of an alarm. + All alarms that happen before this time will be ackknowledged at + the same time. + """ + + def compute_alarm_times(self) -> list[AlarmTime]: + """Compute and return the times of the alarms given. + + If the information for calculation is incomplete, this will raise a + IncompleteAlarmInformation exception. + + Please make sure to set all the required parameters before calculating. + If you forget to set the acknowledged times, that is not problem. + """ + + +__all__ = ["Alarms", "AlarmTime", "IncompleteAlarmInformation"] From d4d10748dc88b28e8b21b368233c22132d4a1a8a Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Tue, 22 Oct 2024 12:59:54 +0100 Subject: [PATCH 03/50] Add test for absolute alarm --- src/icalendar/alarms.py | 3 ++- .../rfc_5545_absolute_alarm_example.ics | 8 +++++++ src/icalendar/tests/conftest.py | 6 +++++ .../test_issue_716_alarm_time_computation.py | 22 +++++++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 src/icalendar/tests/alarms/rfc_5545_absolute_alarm_example.ics create mode 100644 src/icalendar/tests/test_issue_716_alarm_time_computation.py diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index 92b81a13..df157b14 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -107,7 +107,8 @@ def add_acknowledged_time(self, dt: date) -> None: the same time. """ - def compute_alarm_times(self) -> list[AlarmTime]: + @property + def times(self) -> list[AlarmTime]: """Compute and return the times of the alarms given. If the information for calculation is incomplete, this will raise a diff --git a/src/icalendar/tests/alarms/rfc_5545_absolute_alarm_example.ics b/src/icalendar/tests/alarms/rfc_5545_absolute_alarm_example.ics new file mode 100644 index 00000000..e87f2b66 --- /dev/null +++ b/src/icalendar/tests/alarms/rfc_5545_absolute_alarm_example.ics @@ -0,0 +1,8 @@ +BEGIN:VALARM +TRIGGER;VALUE=DATE-TIME:19970317T133000Z +REPEAT:4 +DURATION:PT15M +ACTION:AUDIO +ATTACH;FMTTYPE=audio/basic:ftp://example.com/pub/ +sounds/bell-01.aud +END:VALARM diff --git a/src/icalendar/tests/conftest.py b/src/icalendar/tests/conftest.py index 8db1941e..3062b6d2 100644 --- a/src/icalendar/tests/conftest.py +++ b/src/icalendar/tests/conftest.py @@ -93,6 +93,7 @@ def multiple(self): CALENDARS_FOLDER = HERE / "calendars" TIMEZONES_FOLDER = HERE / "timezones" EVENTS_FOLDER = HERE / "events" +ALARMS_FOLDER = HERE / "alarms" @pytest.fixture(scope="module") @@ -110,6 +111,11 @@ def events(tzp): return DataSource(EVENTS_FOLDER, icalendar.Event.from_ical) +@pytest.fixture(scope="module") +def alarms(tzp): + return DataSource(ALARMS_FOLDER, icalendar.Alarm.from_ical) + + @pytest.fixture(params=PYTZ_UTC + [zoneinfo.ZoneInfo("UTC"), tz.UTC, tz.gettz("UTC")]) def utc(request, tzp): return request.param diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py new file mode 100644 index 00000000..d1ccd54e --- /dev/null +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -0,0 +1,22 @@ +"""Test the alarm time computation.""" + +from icalendar.alarms import Alarms +from datetime import datetime, timedelta, timezone + + +EXAMPLE_TRIGGER = datetime(1997, 3, 17, 13, 30, timezone.utc) + + +def test_absolute_alarm_time(alarms): + """Check that the absolute alarm is recognized. + + The following example is for a "VALARM" calendar component + that specifies an audio alarm that will sound at a precise time + and repeat 4 more times at 15-minute intervals: + """ + a = Alarms(alarms.rfc_5545_absolute_alarm_example) + times = a.times + assert len(times) == 5 + for i, t in enumerate(times): + assert t.trigger == EXAMPLE_TRIGGER + timedelta(minutes=15 * i) + From dee670d4d9c390add4600aea38212065a79fabf8 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Tue, 22 Oct 2024 13:00:46 +0100 Subject: [PATCH 04/50] fix syntax errors for alarms test --- src/icalendar/tests/alarms/rfc_5545_absolute_alarm_example.ics | 3 +-- src/icalendar/tests/test_issue_716_alarm_time_computation.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/icalendar/tests/alarms/rfc_5545_absolute_alarm_example.ics b/src/icalendar/tests/alarms/rfc_5545_absolute_alarm_example.ics index e87f2b66..9f7f71f3 100644 --- a/src/icalendar/tests/alarms/rfc_5545_absolute_alarm_example.ics +++ b/src/icalendar/tests/alarms/rfc_5545_absolute_alarm_example.ics @@ -3,6 +3,5 @@ TRIGGER;VALUE=DATE-TIME:19970317T133000Z REPEAT:4 DURATION:PT15M ACTION:AUDIO -ATTACH;FMTTYPE=audio/basic:ftp://example.com/pub/ -sounds/bell-01.aud +ATTACH;FMTTYPE=audio/basic:ftp://example.com/pub/sounds/bell-01.aud END:VALARM diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py index d1ccd54e..1c993850 100644 --- a/src/icalendar/tests/test_issue_716_alarm_time_computation.py +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta, timezone -EXAMPLE_TRIGGER = datetime(1997, 3, 17, 13, 30, timezone.utc) +EXAMPLE_TRIGGER = datetime(1997, 3, 17, 13, 30, tzinfo=timezone.utc) def test_absolute_alarm_time(alarms): From 4c2ac64c3cb9641a2efe0d779e31b47ce91cbb09 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Tue, 22 Oct 2024 13:44:04 +0100 Subject: [PATCH 05/50] Calculate absolute alarm times --- src/icalendar/alarms.py | 32 +++++++- src/icalendar/cal.py | 16 ++++ .../test_issue_716_alarm_time_computation.py | 77 ++++++++++++++++++- 3 files changed, 119 insertions(+), 6 deletions(-) diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index df157b14..5d666b5a 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -10,6 +10,7 @@ from typing import Optional from icalendar.cal import Alarm, Component, Event, Todo +from icalendar.prop import vDatetime class IncompleteAlarmInformation(ValueError): @@ -19,10 +20,12 @@ class IncompleteAlarmInformation(ValueError): class AlarmTime(ABC): """An alarm time with all the information.""" - def __init__(self, alarm: Alarm, parent: Optional[Component]): + def __init__(self, alarm: Alarm, trigger : datetime, acknowledged:Optional[datetime], parent: Optional[Component]): """Create a new AlarmTime.""" self._alarm = alarm self._parent = parent + self._trigger = trigger + self._acknowledged = acknowledged @property def alarm(self) -> Alarm: @@ -38,7 +41,6 @@ def parent(self) -> Optional[Component]: return self._parent @property - @abstractmethod def is_active(self) -> bool: """Whether this alarm is active (True) or acknowledged (False). @@ -47,9 +49,9 @@ def is_active(self) -> bool: """ @property - @abstractmethod def trigger(self) -> datetime: """This is the time to trigger the alarm.""" + return self._trigger class Alarms: @@ -61,8 +63,12 @@ class Alarms: This is not implemented yet. """ - def __init__(self): + def __init__(self, component:Optional[Alarm]=None): """Start computing alarm times.""" + self._absolute_alarms : list[Alarm] = [] + + if isinstance(component, Alarm): + self.add_alarm(component) def from_component(self, component: Event | Todo) -> None: """Create an Alarm computation from the component.""" @@ -77,6 +83,8 @@ def set_component(self, component: Component): def add_alarm(self, alarm: Alarm) -> None: """Optional: Add an alarm component.""" + trigger = alarm.get("TRIGGER") + self._absolute_alarms.append(alarm) def set_component_start(self, dt: date): """Set the start of the component. @@ -117,6 +125,22 @@ def times(self) -> list[AlarmTime]: Please make sure to set all the required parameters before calculating. If you forget to set the acknowledged times, that is not problem. """ + return self._get_absolute_alarm_times() + + + def _get_absolute_alarm_times(self) -> list[AlarmTime]: + """Return a list of absolute alarm times.""" + result : list[AlarmTime] = [] + for absolute_alarm in self._absolute_alarms: + trigger : datetime = absolute_alarm["TRIGGER"].dt + result.append(AlarmTime(absolute_alarm, trigger, None, None)) + repeat = absolute_alarm.REPEAT + if repeat: + duration = absolute_alarm.DURATION + # TODO: test duration should not be None + for i in range(1, repeat + 1): + result.append(AlarmTime(absolute_alarm, trigger + duration * i, None, None)) + return result __all__ = ["Alarms", "AlarmTime", "IncompleteAlarmInformation"] diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 9ace5d38..38494eaf 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -965,6 +965,22 @@ class Alarm(Component): multiple = ('ATTENDEE', 'ATTACH', 'RELATED-TO') + @property + def REPEAT(self) -> int: + """The REPEAT property of an alarm component.""" + try: + return int(self.get("REPEAT", 0)) + except ValueError as e: + raise InvalidCalendar("REPEAT must be an int") from e + + @REPEAT.setter + def REPEAT(self, value: int) -> None: + """The REPEAT property of an alarm component.""" + self["REPEAT"] = int(value) + + DURATION = Event.DURATION # TODO: adjust once https://github.com/collective/icalendar/pull/733 is merged + + class Calendar(Component): """This is the base object for an iCalendar file. """ diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py index 1c993850..901c0be5 100644 --- a/src/icalendar/tests/test_issue_716_alarm_time_computation.py +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -1,13 +1,17 @@ """Test the alarm time computation.""" -from icalendar.alarms import Alarms from datetime import datetime, timedelta, timezone +import pytest + +from icalendar.alarms import Alarms +from icalendar.cal import Alarm, InvalidCalendar +from icalendar.prop import vDatetime EXAMPLE_TRIGGER = datetime(1997, 3, 17, 13, 30, tzinfo=timezone.utc) -def test_absolute_alarm_time(alarms): +def test_absolute_alarm_time_rfc_example(alarms): """Check that the absolute alarm is recognized. The following example is for a "VALARM" calendar component @@ -20,3 +24,72 @@ def test_absolute_alarm_time(alarms): for i, t in enumerate(times): assert t.trigger == EXAMPLE_TRIGGER + timedelta(minutes=15 * i) + +alarm_1 = Alarm() +alarm_1.add("TRIGGER", EXAMPLE_TRIGGER) +alarm_2 = Alarm() +alarm_2["TRIGGER"] = vDatetime(EXAMPLE_TRIGGER) + +@pytest.mark.parametrize( + "alarm", + [ + alarm_1, alarm_2 + ] +) +def test_absolute_alarm_time_with_vDatetime(alarm): + """Check that the absolute alarm is recognized. + + The following example is for a "VALARM" calendar component + that specifies an audio alarm that will sound at a precise time + and repeat 4 more times at 15-minute intervals: + """ + a = Alarms(alarm) + times = a.times + assert len(times) == 1 + assert times[0].trigger == EXAMPLE_TRIGGER + + + +def test_repeat_absent(): + """Test the absence of REPEAT.""" + assert Alarm().REPEAT == 0 + +def test_repeat_number(): + """Test the absence of REPEAT.""" + assert Alarm({"REPEAT": 10}).REPEAT == 10 + + +def test_set_REPEAT(): + """Check setting the value.""" + a = Alarm() + a.REPEAT = 10 + assert a.REPEAT == 10 + +def test_set_REPEAT_twice(): + """Check setting the value.""" + a = Alarm() + a.REPEAT = 10 + a.REPEAT = 20 + assert a.REPEAT == 20 + +def test_add_REPEAT(): + """Check setting the value.""" + a = Alarm() + a.add("REPEAT", 10) + assert a.REPEAT == 10 + + +def test_invalid_repeat_value(): + """Check setting the value.""" + a = Alarm() + with pytest.raises(ValueError): + a.REPEAT = "asd" + a["REPEAT"] = "asd" + with pytest.raises(InvalidCalendar): + a.REPEAT # noqa: B018 + + +def test_alarm_to_string(): + a = Alarm() + a.REPEAT = 11 + assert a.to_ical() == b"BEGIN:VALARM\r\nREPEAT:11\r\nEND:VALARM\r\n" From b5d9b7571d9c49730cbd2ca69d22f1d51039779b Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Tue, 22 Oct 2024 13:46:01 +0100 Subject: [PATCH 06/50] TODO test for edge case --- src/icalendar/alarms.py | 1 - src/icalendar/tests/test_issue_716_alarm_time_computation.py | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index 5d666b5a..c8e12ee6 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -137,7 +137,6 @@ def _get_absolute_alarm_times(self) -> list[AlarmTime]: repeat = absolute_alarm.REPEAT if repeat: duration = absolute_alarm.DURATION - # TODO: test duration should not be None for i in range(1, repeat + 1): result.append(AlarmTime(absolute_alarm, trigger + duration * i, None, None)) return result diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py index 901c0be5..39763b42 100644 --- a/src/icalendar/tests/test_issue_716_alarm_time_computation.py +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -93,3 +93,8 @@ def test_alarm_to_string(): a = Alarm() a.REPEAT = 11 assert a.to_ical() == b"BEGIN:VALARM\r\nREPEAT:11\r\nEND:VALARM\r\n" + + +def test_alarm_has_only_one_of_repeat_or_duration(): + """This is an edge case and we should ignore the repetition.""" + pytest.skip("TODO") From f993bd968cc195ce39bdca58f35087d6393fc99f Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 23 Oct 2024 04:40:40 +0100 Subject: [PATCH 07/50] Raise exception on missing information --- src/icalendar/alarms.py | 44 +++++++++----- .../test_issue_716_alarm_time_computation.py | 60 +++++++++++++++++-- 2 files changed, 83 insertions(+), 21 deletions(-) diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index c8e12ee6..de49c468 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -5,19 +5,20 @@ from __future__ import annotations -from abc import ABC, abstractmethod -from datetime import date, datetime -from typing import Optional +from datetime import date +from typing import TYPE_CHECKING, Optional from icalendar.cal import Alarm, Component, Event, Todo -from icalendar.prop import vDatetime + +if TYPE_CHECKING: + from datetime import datetime class IncompleteAlarmInformation(ValueError): """The alarms cannot be calculated yet because information is missing.""" -class AlarmTime(ABC): +class AlarmTime: """An alarm time with all the information.""" def __init__(self, alarm: Alarm, trigger : datetime, acknowledged:Optional[datetime], parent: Optional[Component]): @@ -66,6 +67,8 @@ class Alarms: def __init__(self, component:Optional[Alarm]=None): """Start computing alarm times.""" self._absolute_alarms : list[Alarm] = [] + self._start_alarms : list[Alarm] = [] + self._start : Optional[date] = None if isinstance(component, Alarm): self.add_alarm(component) @@ -84,22 +87,26 @@ def set_component(self, component: Component): def add_alarm(self, alarm: Alarm) -> None: """Optional: Add an alarm component.""" trigger = alarm.get("TRIGGER") - self._absolute_alarms.append(alarm) - - def set_component_start(self, dt: date): + if trigger is None: + return + if isinstance(trigger.dt, date): + self._absolute_alarms.append(alarm) + else: + self._start_alarms.append(alarm) + + def set_start(self, dt: date): """Set the start of the component. If you have only absolute alarms, this is not required. - If you have alarms relative to start or end, you need to - set the start or the end respectively. + If you have alarms relative to the start of a compoment, set the start here. """ + self._start = dt - def set_component_end(self, dt: date): + def set_end(self, dt: date): """Set the end of the component. If you have only absolute alarms, this is not required. - If you have alarms relative to start or end, you need to - set the start or the end respectively. + If you have alarms relative to the end of a compoment, set the end here. """ def add_acknowledged_time(self, dt: date) -> None: @@ -125,9 +132,8 @@ def times(self) -> list[AlarmTime]: Please make sure to set all the required parameters before calculating. If you forget to set the acknowledged times, that is not problem. """ - return self._get_absolute_alarm_times() - - + return self._get_start_alarm_times() + self._get_absolute_alarm_times() + def _get_absolute_alarm_times(self) -> list[AlarmTime]: """Return a list of absolute alarm times.""" result : list[AlarmTime] = [] @@ -141,5 +147,11 @@ def _get_absolute_alarm_times(self) -> list[AlarmTime]: result.append(AlarmTime(absolute_alarm, trigger + duration * i, None, None)) return result + def _get_start_alarm_times(self) -> list[AlarmTime]: + """Return a list of alarm times relative to the start of the component.""" + if self._start is None and self._start_alarms: + raise IncompleteAlarmInformation("Use Alarms.set_start because at least one alarm is relative to the start of a component.") + result : list[AlarmTime] = [] + return result __all__ = ["Alarms", "AlarmTime", "IncompleteAlarmInformation"] diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py index 39763b42..18d90cc7 100644 --- a/src/icalendar/tests/test_issue_716_alarm_time_computation.py +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -1,14 +1,21 @@ """Test the alarm time computation.""" -from datetime import datetime, timedelta, timezone +from datetime import date, datetime, timedelta, timezone import pytest -from icalendar.alarms import Alarms +from icalendar.alarms import Alarms, IncompleteAlarmInformation from icalendar.cal import Alarm, InvalidCalendar from icalendar.prop import vDatetime +from pytz import timezone as pytz_timezone +try: + from zoneinfo import ZoneInfo +except ImportError: + from backports.zoneinfo import ZoneInfo -EXAMPLE_TRIGGER = datetime(1997, 3, 17, 13, 30, tzinfo=timezone.utc) + +UTC = timezone.utc +EXAMPLE_TRIGGER = datetime(1997, 3, 17, 13, 30, tzinfo=UTC) def test_absolute_alarm_time_rfc_example(alarms): @@ -49,11 +56,11 @@ def test_absolute_alarm_time_with_vDatetime(alarm): assert times[0].trigger == EXAMPLE_TRIGGER - def test_repeat_absent(): """Test the absence of REPEAT.""" assert Alarm().REPEAT == 0 - + + def test_repeat_number(): """Test the absence of REPEAT.""" assert Alarm({"REPEAT": 10}).REPEAT == 10 @@ -65,6 +72,7 @@ def test_set_REPEAT(): a.REPEAT = 10 assert a.REPEAT == 10 + def test_set_REPEAT_twice(): """Check setting the value.""" a = Alarm() @@ -72,6 +80,7 @@ def test_set_REPEAT_twice(): a.REPEAT = 20 assert a.REPEAT == 20 + def test_add_REPEAT(): """Check setting the value.""" a = Alarm() @@ -98,3 +107,44 @@ def test_alarm_to_string(): def test_alarm_has_only_one_of_repeat_or_duration(): """This is an edge case and we should ignore the repetition.""" pytest.skip("TODO") + + +@pytest.fixture(params=[(0, timedelta(minutes=-30)), (1, timedelta(minutes=-25))]) +def alarm_before_start(calendars, request): + """An example alarm relative to the start of a component.""" + index, td = request.param + alarm = calendars.alarm_etar_future.subcomponents[-1].subcomponents[index] + assert isinstance(alarm, Alarm) + assert alarm.get("TRIGGER").dt == td + alarm.test_td = td + return alarm + + +def test_cannot_compute_relative_alarm_without_start(alarm_before_start): + """We have an alarm without a start of a component.""" + with pytest.raises(IncompleteAlarmInformation) as e: + Alarms(alarm_before_start).times # noqa: B018 + assert e.value.args[0] == f"Use {Alarms.__name__}.{Alarms.set_start.__name__} because at least one alarm is relative to the start of a component." + + +@pytest.mark.parametrize( + ("dtstart", "timezone", "trigger"), + [ + (datetime(2024, 10, 29, 13, 10), "UTC", datetime(2024, 10, 29, 13, 10, tzinfo=UTC)), + (date(2024, 11, 16), None, datetime(2024, 10, 29, 0, 0)), + (datetime(2024, 10, 29, 13, 10), "Asia/Singapore", datetime(2024, 10, 29, 5, 10, tzinfo=UTC)), + (datetime(2024, 10, 29, 13, 20), None, datetime(2024, 10, 29, 13, 20)), + ] +) +def test_can_complete_relative_calculation_if_a_start_is_given(alarm_before_start, dtstart, timezone, trigger, tzp): + """The start is given and required.""" + start = (dtstart if timezone is None else tzp.localize(dtstart, timezone)) + alarms = Alarms(alarm_before_start) + alarms.set_start(start) + assert len(alarms.times) == 1 + time = alarms.times[0] + assert time.trigger == trigger + alarm_before_start.test_td + + +def test_add_multiple_alarms(): + pytest.skip("TODO") From e358101350e2c3ba33e34baa8363e9284bbed779 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 23 Oct 2024 05:12:05 +0100 Subject: [PATCH 08/50] Calculate alarms relative to the start --- src/icalendar/alarms.py | 36 ++++++----- src/icalendar/cal.py | 9 +-- .../test_issue_716_alarm_time_computation.py | 12 ++-- src/icalendar/timezone/__init__.py | 4 +- src/icalendar/tools.py | 60 +++++++++++++++---- 5 files changed, 79 insertions(+), 42 deletions(-) diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index de49c468..967ce7ac 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -6,9 +6,10 @@ from __future__ import annotations from datetime import date -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Generator, Optional from icalendar.cal import Alarm, Component, Event, Todo +from icalendar.tools import normalize_pytz, to_datetime if TYPE_CHECKING: from datetime import datetime @@ -100,7 +101,7 @@ def set_start(self, dt: date): If you have only absolute alarms, this is not required. If you have alarms relative to the start of a compoment, set the start here. """ - self._start = dt + self._start = to_datetime(dt) def set_end(self, dt: date): """Set the end of the component. @@ -134,24 +135,31 @@ def times(self) -> list[AlarmTime]: """ return self._get_start_alarm_times() + self._get_absolute_alarm_times() + def _repeat(self, first: datetime, alarm: Alarm) -> Generator[datetime]: + """The times when the alarm is triggered relative to start.""" + yield first # we trigger at the start + repeat = alarm.REPEAT + if repeat: + duration = alarm.DURATION + for i in range(1, repeat + 1): + yield normalize_pytz(first + duration * i) + def _get_absolute_alarm_times(self) -> list[AlarmTime]: """Return a list of absolute alarm times.""" - result : list[AlarmTime] = [] - for absolute_alarm in self._absolute_alarms: - trigger : datetime = absolute_alarm["TRIGGER"].dt - result.append(AlarmTime(absolute_alarm, trigger, None, None)) - repeat = absolute_alarm.REPEAT - if repeat: - duration = absolute_alarm.DURATION - for i in range(1, repeat + 1): - result.append(AlarmTime(absolute_alarm, trigger + duration * i, None, None)) - return result + return [ + AlarmTime(alarm, trigger, None, None) + for alarm in self._absolute_alarms + for trigger in self._repeat(alarm["TRIGGER"].dt, alarm) + ] def _get_start_alarm_times(self) -> list[AlarmTime]: """Return a list of alarm times relative to the start of the component.""" if self._start is None and self._start_alarms: raise IncompleteAlarmInformation("Use Alarms.set_start because at least one alarm is relative to the start of a component.") - result : list[AlarmTime] = [] - return result + return [ + AlarmTime(alarm, trigger, None, None) + for alarm in self._start_alarms + for trigger in self._repeat(normalize_pytz(alarm["TRIGGER"].dt + self._start), alarm) + ] __all__ = ["Alarms", "AlarmTime", "IncompleteAlarmInformation"] diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 38494eaf..4c450c82 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -16,6 +16,7 @@ from icalendar.parser_tools import DEFAULT_ENCODING from icalendar.prop import TypesFactory, vDDDLists, vDDDTypes, vText, vDuration from icalendar.timezone import tzp +from icalendar.tools import is_date, is_datetime def get_example(component_directory: str, example_name: str) -> bytes: @@ -540,14 +541,6 @@ def p_del(self:Component): return property(p_get, p_set, p_del, p_doc) -def is_date(dt: date) -> bool: - """Whether this is a date and not a datetime.""" - return isinstance(dt, date) and not isinstance(dt, datetime) - -def is_datetime(dt: date) -> bool: - """Whether this is a date and not a datetime.""" - return isinstance(dt, datetime) - class Event(Component): name = 'VEVENT' diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py index 18d90cc7..fe70d188 100644 --- a/src/icalendar/tests/test_issue_716_alarm_time_computation.py +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -7,12 +7,7 @@ from icalendar.alarms import Alarms, IncompleteAlarmInformation from icalendar.cal import Alarm, InvalidCalendar from icalendar.prop import vDatetime -from pytz import timezone as pytz_timezone -try: - from zoneinfo import ZoneInfo -except ImportError: - from backports.zoneinfo import ZoneInfo - +from icalendar.tools import normalize_pytz UTC = timezone.utc EXAMPLE_TRIGGER = datetime(1997, 3, 17, 13, 30, tzinfo=UTC) @@ -131,7 +126,7 @@ def test_cannot_compute_relative_alarm_without_start(alarm_before_start): ("dtstart", "timezone", "trigger"), [ (datetime(2024, 10, 29, 13, 10), "UTC", datetime(2024, 10, 29, 13, 10, tzinfo=UTC)), - (date(2024, 11, 16), None, datetime(2024, 10, 29, 0, 0)), + (date(2024, 11, 16), None, datetime(2024, 11, 16, 0, 0)), (datetime(2024, 10, 29, 13, 10), "Asia/Singapore", datetime(2024, 10, 29, 5, 10, tzinfo=UTC)), (datetime(2024, 10, 29, 13, 20), None, datetime(2024, 10, 29, 13, 20)), ] @@ -143,7 +138,8 @@ def test_can_complete_relative_calculation_if_a_start_is_given(alarm_before_star alarms.set_start(start) assert len(alarms.times) == 1 time = alarms.times[0] - assert time.trigger == trigger + alarm_before_start.test_td + expected_trigger = normalize_pytz(trigger + alarm_before_start.test_td) + assert time.trigger == expected_trigger def test_add_multiple_alarms(): diff --git a/src/icalendar/timezone/__init__.py b/src/icalendar/timezone/__init__.py index dc751808..09ae612c 100644 --- a/src/icalendar/timezone/__init__.py +++ b/src/icalendar/timezone/__init__.py @@ -7,8 +7,10 @@ def use_pytz(): """Use pytz as the implementation that looks up and creates timezones.""" tzp.use_pytz() + def use_zoneinfo(): """Use zoneinfo as the implementation that looks up and creates timezones.""" tzp.use_zoneinfo() -__all__ = ["tzp", "use_pytz", "use_zoneinfo"] + +__all__ = ["tzp", "use_pytz", "use_zoneinfo", "normalize_pytz"] diff --git a/src/icalendar/tools.py b/src/icalendar/tools.py index 896aa7a6..191e4233 100644 --- a/src/icalendar/tools.py +++ b/src/icalendar/tools.py @@ -1,11 +1,10 @@ -from datetime import datetime -from icalendar.parser_tools import to_unicode -from icalendar.prop import vDatetime -from icalendar.prop import vText -from string import ascii_letters -from string import digits - +from __future__ import annotations import random +from datetime import date, datetime, tzinfo +from string import ascii_letters, digits + +from icalendar.parser_tools import to_unicode +from icalendar.prop import vDatetime, vText class UIDGenerator: @@ -18,10 +17,10 @@ class UIDGenerator: def rnd_string(length=16): """Generates a string with random characters of length. """ - return ''.join([random.choice(UIDGenerator.chars) for _ in range(length)]) + return "".join([random.choice(UIDGenerator.chars) for _ in range(length)]) @staticmethod - def uid(host_name='example.com', unique=''): + def uid(host_name="example.com", unique=""): """Generates a unique id consisting of: datetime-uniquevalue@host. Like: @@ -30,6 +29,45 @@ def uid(host_name='example.com', unique=''): host_name = to_unicode(host_name) unique = unique or UIDGenerator.rnd_string() today = to_unicode(vDatetime(datetime.today()).to_ical()) - return vText(f'{today}-{unique}@{host_name}') + return vText(f"{today}-{unique}@{host_name}") + + +def is_date(dt: date) -> bool: + """Whether this is a date and not a datetime.""" + return isinstance(dt, date) and not isinstance(dt, datetime) + + +def is_datetime(dt: date) -> bool: + """Whether this is a date and not a datetime.""" + return isinstance(dt, datetime) + + +def to_datetime(dt: date) -> datetime: + """Make sure we have a datetime, not a date.""" + if is_date(dt): + return datetime(dt.year, dt.month, dt.day) # noqa: DTZ001 + return dt + + +def is_pytz(tz: tzinfo): + """Whether the timezone requires localize() and normalize().""" + return hasattr(tz, "localize") + + +def is_pytz_dt(dt: date): + """Whether the time requires localize() and normalize().""" + return is_datetime(dt) and is_pytz(dt.tzinfo) + + +def normalize_pytz(dt: date): + """We have to normalize the time after a calculation if we use pytz. + + pytz requires this funtion to be used in order to correctly calculate the + timezone's offset after calculations. + """ + if is_pytz_dt(dt): + return dt.tzinfo.normalize(dt) + return dt + -__all__ = ["UIDGenerator"] +__all__ = ["UIDGenerator", "is_date", "is_datetime", "to_datetime", "is_pytz", "is_pytz_dt", "normalize_pytz"] From e29c545558343f9af4074a123f38694fb92f2569 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 23 Oct 2024 05:54:42 +0100 Subject: [PATCH 09/50] Compute alarms relative to their end --- src/icalendar/alarms.py | 49 +++++++++++++---- .../test_issue_716_alarm_time_computation.py | 54 +++++++++++++++++++ 2 files changed, 94 insertions(+), 9 deletions(-) diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index 967ce7ac..7adc22e2 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -5,11 +5,11 @@ from __future__ import annotations -from datetime import date +from datetime import date, timedelta from typing import TYPE_CHECKING, Generator, Optional from icalendar.cal import Alarm, Component, Event, Todo -from icalendar.tools import normalize_pytz, to_datetime +from icalendar.tools import is_date, normalize_pytz, to_datetime if TYPE_CHECKING: from datetime import datetime @@ -69,7 +69,9 @@ def __init__(self, component:Optional[Alarm]=None): """Start computing alarm times.""" self._absolute_alarms : list[Alarm] = [] self._start_alarms : list[Alarm] = [] + self._end_alarms : list[Alarm] = [] self._start : Optional[date] = None + self._end : Optional[date] = None if isinstance(component, Alarm): self.add_alarm(component) @@ -92,8 +94,10 @@ def add_alarm(self, alarm: Alarm) -> None: return if isinstance(trigger.dt, date): self._absolute_alarms.append(alarm) - else: + elif trigger.params.get("RELATED", "START").upper() == "START": self._start_alarms.append(alarm) + else: + self._end_alarms.append(alarm) def set_start(self, dt: date): """Set the start of the component. @@ -101,7 +105,7 @@ def set_start(self, dt: date): If you have only absolute alarms, this is not required. If you have alarms relative to the start of a compoment, set the start here. """ - self._start = to_datetime(dt) + self._start = dt def set_end(self, dt: date): """Set the end of the component. @@ -109,6 +113,15 @@ def set_end(self, dt: date): If you have only absolute alarms, this is not required. If you have alarms relative to the end of a compoment, set the end here. """ + self._end = dt + + def _add(self, dt: date, td:timedelta): + """Add a timedelta to a datetime.""" + if is_date(dt): + if td.seconds == 0: + return dt + td + dt = to_datetime(dt) + return normalize_pytz(dt + td) def add_acknowledged_time(self, dt: date) -> None: """This is the time when all the alarms of this component were acknowledged. @@ -133,7 +146,11 @@ def times(self) -> list[AlarmTime]: Please make sure to set all the required parameters before calculating. If you forget to set the acknowledged times, that is not problem. """ - return self._get_start_alarm_times() + self._get_absolute_alarm_times() + return ( + self._get_end_alarm_times() + + self._get_start_alarm_times() + + self._get_absolute_alarm_times() + ) def _repeat(self, first: datetime, alarm: Alarm) -> Generator[datetime]: """The times when the alarm is triggered relative to start.""" @@ -142,12 +159,16 @@ def _repeat(self, first: datetime, alarm: Alarm) -> Generator[datetime]: if repeat: duration = alarm.DURATION for i in range(1, repeat + 1): - yield normalize_pytz(first + duration * i) + yield self._add(first, duration * i) + + def _alarm_time(self, alarm: Alarm, trigger:date): + """Create an alarm time with the additional attributes.""" + return AlarmTime(alarm, trigger, None, None) def _get_absolute_alarm_times(self) -> list[AlarmTime]: """Return a list of absolute alarm times.""" return [ - AlarmTime(alarm, trigger, None, None) + self._alarm_time(alarm , trigger) for alarm in self._absolute_alarms for trigger in self._repeat(alarm["TRIGGER"].dt, alarm) ] @@ -157,9 +178,19 @@ def _get_start_alarm_times(self) -> list[AlarmTime]: if self._start is None and self._start_alarms: raise IncompleteAlarmInformation("Use Alarms.set_start because at least one alarm is relative to the start of a component.") return [ - AlarmTime(alarm, trigger, None, None) + self._alarm_time(alarm , trigger) for alarm in self._start_alarms - for trigger in self._repeat(normalize_pytz(alarm["TRIGGER"].dt + self._start), alarm) + for trigger in self._repeat(self._add(self._start, alarm["TRIGGER"].dt), alarm) + ] + + def _get_end_alarm_times(self) -> list[AlarmTime]: + """Return a list of alarm times relative to the start of the component.""" + if self._end is None and self._end_alarms: + raise IncompleteAlarmInformation("Use Alarms.set_end because at least one alarm is relative to the end of a component.") + return [ + self._alarm_time(alarm , trigger) + for alarm in self._end_alarms + for trigger in self._repeat(self._add(self._end, alarm["TRIGGER"].dt), alarm) ] __all__ = ["Alarms", "AlarmTime", "IncompleteAlarmInformation"] diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py index fe70d188..df8b9fd3 100644 --- a/src/icalendar/tests/test_issue_716_alarm_time_computation.py +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -142,5 +142,59 @@ def test_can_complete_relative_calculation_if_a_start_is_given(alarm_before_star assert time.trigger == expected_trigger +@pytest.mark.parametrize("dtstart", [date(1998, 10, 1), date(2023, 12, 31)]) +def test_start_as_date_with_delta_as_date_stays_date(alarms, dtstart): + """If we have an alarm with a day delta and the event is a day event, we should stay as a date.""" + a = Alarms(alarms.start_date) + a.set_start(dtstart) + assert len(a.times) == 1 + assert a.times[0].trigger == dtstart - timedelta(days=2) + + +def test_cannot_compute_relative_alarm_without_end(alarms): + """We have an alarm without an end of a component.""" + with pytest.raises(IncompleteAlarmInformation) as e: + Alarms(alarms.rfc_5545_end).times # noqa: B018 + assert e.value.args[0] == f"Use {Alarms.__name__}.{Alarms.set_end.__name__} because at least one alarm is relative to the end of a component." + + +@pytest.mark.parametrize( + ("dtend", "timezone", "trigger"), + [ + (datetime(2024, 10, 29, 13, 10), "UTC", datetime(2024, 10, 29, 13, 10, tzinfo=UTC)), + (date(2024, 11, 16), None, date(2024, 11, 16)), + (datetime(2024, 10, 29, 13, 10), "Asia/Singapore", datetime(2024, 10, 29, 5, 10, tzinfo=UTC)), + (datetime(2024, 10, 29, 13, 20), None, datetime(2024, 10, 29, 13, 20)), + ] +) +def test_can_complete_relative_calculation_if_a_start_is_given(alarms, dtend, timezone, trigger, tzp): + """The start is given and required.""" + start = (dtend if timezone is None else tzp.localize(dtend, timezone)) + alarms = Alarms(alarms.rfc_5545_end) + alarms.set_end(start) + assert len(alarms.times) == 1 + time = alarms.times[0] + expected_trigger = normalize_pytz(trigger - timedelta(days=2)) + assert time.trigger == expected_trigger + + +@pytest.mark.parametrize("dtend", [date(1998, 10, 1), date(2023, 12, 31)]) +def test_end_as_date_with_delta_as_date_stays_date(alarms, dtend): + """If we have an alarm with a day delta and the event is a day event, we should stay as a date.""" + a = Alarms(alarms.rfc_5545_end) + a.set_end(dtend) + assert len(a.times) == 1 + assert a.times[0].trigger == dtend - timedelta(days=2) + + + def test_add_multiple_alarms(): pytest.skip("TODO") + + +def test_alarms_from_event(): + pytest.skip("TODO") + + +def test_alarms_from_calendar(): + pytest.skip("TODO") From 560acb9e57763dccbe27d4c41ae26f009864f16e Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 23 Oct 2024 06:25:52 +0100 Subject: [PATCH 10/50] Collect alarms from event --- src/icalendar/alarms.py | 47 ++++++++++++------- src/icalendar/tests/alarms/rfc_5545_end.ics | 8 ++++ src/icalendar/tests/alarms/start_date.ics | 8 ++++ .../test_issue_716_alarm_time_computation.py | 37 ++++++++++++--- 4 files changed, 78 insertions(+), 22 deletions(-) create mode 100644 src/icalendar/tests/alarms/rfc_5545_end.ics create mode 100644 src/icalendar/tests/alarms/start_date.ics diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index 7adc22e2..6d9610f4 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -6,7 +6,7 @@ from __future__ import annotations from datetime import date, timedelta -from typing import TYPE_CHECKING, Generator, Optional +from typing import TYPE_CHECKING, Generator, Optional, Union from icalendar.cal import Alarm, Component, Event, Todo from icalendar.tools import is_date, normalize_pytz, to_datetime @@ -14,6 +14,8 @@ if TYPE_CHECKING: from datetime import datetime +Parent = Union[Event,Todo] + class IncompleteAlarmInformation(ValueError): """The alarms cannot be calculated yet because information is missing.""" @@ -22,7 +24,7 @@ class IncompleteAlarmInformation(ValueError): class AlarmTime: """An alarm time with all the information.""" - def __init__(self, alarm: Alarm, trigger : datetime, acknowledged:Optional[datetime], parent: Optional[Component]): + def __init__(self, alarm: Alarm, trigger : datetime, acknowledged:Optional[datetime], parent: Optional[Parent]): """Create a new AlarmTime.""" self._alarm = alarm self._parent = parent @@ -35,7 +37,7 @@ def alarm(self) -> Alarm: return self._alarm @property - def parent(self) -> Optional[Component]: + def parent(self) -> Optional[Parent]: """This is the component that contains the alarm. This is None if you did not use Alarms.set_component(). @@ -65,27 +67,40 @@ class Alarms: This is not implemented yet. """ - def __init__(self, component:Optional[Alarm]=None): + def __init__(self, component:Optional[Alarm|Event|Todo]=None): """Start computing alarm times.""" self._absolute_alarms : list[Alarm] = [] self._start_alarms : list[Alarm] = [] self._end_alarms : list[Alarm] = [] self._start : Optional[date] = None self._end : Optional[date] = None + self._parent : Optional[Parent] = None - if isinstance(component, Alarm): - self.add_alarm(component) - - def from_component(self, component: Event | Todo) -> None: - """Create an Alarm computation from the component.""" + if component is not None: + self.add_component(component) - def set_component(self, component: Component): - """Optional: Set the component of the computed alarms. + def add_component(self, component:Alarm|Parent): + """Add a component. - This does not change the computation in any way. - It makes it easier to identify the components of the alarms in case - you combine several computations. + If this is an alarm, it is added. + Events and Todos are added as a parent and all + their alarms are added, too. + """ + if isinstance(component, (Event, Todo)): + self.set_parent(component) + self.set_start(component.start) + self.set_end(component.end) + for alarm in component.walk("VALARM"): + self.add_alarm(alarm) + + def set_parent(self, parent: Parent): + """Set the parent of all the alarms. + + If you would like to collect alarms from a component, use add_component """ + if self._parent is not None and self._parent is not parent: + raise ValueError("You can only set one parent for this alarm calculation.") + self._parent = parent def add_alarm(self, alarm: Alarm) -> None: """Optional: Add an alarm component.""" @@ -160,10 +175,10 @@ def _repeat(self, first: datetime, alarm: Alarm) -> Generator[datetime]: duration = alarm.DURATION for i in range(1, repeat + 1): yield self._add(first, duration * i) - + def _alarm_time(self, alarm: Alarm, trigger:date): """Create an alarm time with the additional attributes.""" - return AlarmTime(alarm, trigger, None, None) + return AlarmTime(alarm, trigger, None, self._parent) def _get_absolute_alarm_times(self) -> list[AlarmTime]: """Return a list of absolute alarm times.""" diff --git a/src/icalendar/tests/alarms/rfc_5545_end.ics b/src/icalendar/tests/alarms/rfc_5545_end.ics new file mode 100644 index 00000000..a9f52ddd --- /dev/null +++ b/src/icalendar/tests/alarms/rfc_5545_end.ics @@ -0,0 +1,8 @@ +BEGIN:VALARM +TRIGGER;RELATED=END:-P2D +ACTION:EMAIL +ATTENDEE:mailto:john_doe@example.com +SUMMARY:*** REMINDER: SEND AGENDA FOR WEEKLY STAFF MEETING *** +DESCRIPTION:A draft agenda needs to be sent out to the attendees to the weekly managers meeting (MGR-LIST). Attached is a pointer the document template for the agenda file. +ATTACH;FMTTYPE=application/msword:http://example.com/templates/agenda.doc +END:VALARM diff --git a/src/icalendar/tests/alarms/start_date.ics b/src/icalendar/tests/alarms/start_date.ics new file mode 100644 index 00000000..cc87c948 --- /dev/null +++ b/src/icalendar/tests/alarms/start_date.ics @@ -0,0 +1,8 @@ +BEGIN:VALARM +TRIGGER;RELATED=START:-P2D +ACTION:EMAIL +ATTENDEE:mailto:john_doe@example.com +SUMMARY:*** REMINDER: SEND AGENDA FOR WEEKLY STAFF MEETING *** +DESCRIPTION:A draft agenda needs to be sent out to the attendees to the weekly managers meeting (MGR-LIST). Attached is a pointer the document template for the agenda file. +ATTACH;FMTTYPE=application/msword:http://example.com/templates/agenda.doc +END:VALARM diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py index df8b9fd3..30843b2e 100644 --- a/src/icalendar/tests/test_issue_716_alarm_time_computation.py +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -188,12 +188,37 @@ def test_end_as_date_with_delta_as_date_stays_date(alarms, dtend): -def test_add_multiple_alarms(): - pytest.skip("TODO") - - -def test_alarms_from_event(): - pytest.skip("TODO") +def test_add_multiple_alarms(alarms): + """We can add multiple alarms.""" + a = Alarms() + a.add_alarm(alarms.start_date) + a.add_alarm(alarms.rfc_5545_end) + a.add_alarm(alarms.rfc_5545_absolute_alarm_example) + with pytest.raises(IncompleteAlarmInformation): + a.times # noqa: B018 + a.set_start(datetime(2012, 3, 5)) + with pytest.raises(IncompleteAlarmInformation): + a.times # noqa: B018 + a.set_end(datetime(2012, 3, 5)) + assert len(a.times) == 7 + + +def test_alarms_from_event_have_right_times(calendars): + """We can collect from an event.""" + event = calendars.alarm_etar_future.subcomponents[-1] + a = Alarms(event) + assert len(a.times) == 3 + assert a.times[0].parent == event + + +def test_cannot_set_the_event_twice(calendars): + """We cannot set an event twice. This make the state ambiguous.""" + event = calendars.alarm_etar_future.subcomponents[-1] + a = Alarms() + a.add_component(event) + a.add_component(event) # same component is ok + with pytest.raises(ValueError): + a.add_component(calendars.alarm_google_future.subcomponents[-1]) def test_alarms_from_calendar(): From 30472097f54a99a870ff9e52347e0dfb7114632d Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 23 Oct 2024 13:23:53 +0100 Subject: [PATCH 11/50] Add DTSTAMP property --- src/icalendar/alarms.py | 30 ++++-- src/icalendar/cal.py | 53 ++++++++++- .../tests/calendars/alarm_outlook_closed.ics | 95 ------------------- .../calendars/alarm_outlook_in_future.ics | 95 ------------------- .../alarm_outlook_postponed_5_min.ics | 95 ------------------- src/icalendar/tests/prop/test_alarm.py | 57 +++++++++++ src/icalendar/tests/prop/test_component.py | 74 +++++++++++++++ .../test_issue_716_alarm_time_computation.py | 20 ++++ 8 files changed, 227 insertions(+), 292 deletions(-) delete mode 100644 src/icalendar/tests/calendars/alarm_outlook_closed.ics delete mode 100644 src/icalendar/tests/calendars/alarm_outlook_in_future.ics delete mode 100644 src/icalendar/tests/calendars/alarm_outlook_postponed_5_min.ics create mode 100644 src/icalendar/tests/prop/test_alarm.py create mode 100644 src/icalendar/tests/prop/test_component.py diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index 6d9610f4..94a6e5bb 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -1,6 +1,12 @@ """Compute the times and states of alarms. This takes different calendar software into account and RFC 9074 (Alarm Extension). + +- Outlook does not export VALARM information +- Google uses the DTSTAMP to snooze the alarms +- Thunderbird snoozes the alarms with a X-MOZ-SNOOZE-TIME attribute in the event +- Thunderbird acknowledges the alarms with a X-MOZ-LASTACK attribute in the event +- Etar deletes alarms that are not active any more """ from __future__ import annotations @@ -8,7 +14,7 @@ from datetime import date, timedelta from typing import TYPE_CHECKING, Generator, Optional, Union -from icalendar.cal import Alarm, Component, Event, Todo +from icalendar.cal import Alarm, Event, Todo from icalendar.tools import is_date, normalize_pytz, to_datetime if TYPE_CHECKING: @@ -95,7 +101,7 @@ def add_component(self, component:Alarm|Parent): def set_parent(self, parent: Parent): """Set the parent of all the alarms. - + If you would like to collect alarms from a component, use add_component """ if self._parent is not None and self._parent is not parent: @@ -138,17 +144,24 @@ def _add(self, dt: date, td:timedelta): dt = to_datetime(dt) return normalize_pytz(dt + td) - def add_acknowledged_time(self, dt: date) -> None: + def acknowledge_until(self, dt: date) -> None: """This is the time when all the alarms of this component were acknowledged. You can set several times like this. Only the latest one counts. Since RFC 9074 (Alarm Extension) was created later, - calendar implementations differ in how they ackknowledge alarms. + calendar implementations differ in how they acknowledge alarms. E.g. Thunderbird and Google Calendar store the last time an event has been acknowledged because of an alarm. - All alarms that happen before this time will be ackknowledged at - the same time. + All alarms that happen before this time will be ackknowledged at this dt. + """ + + def snooze_until(self, dt: date) -> None: + """This is the time when all the alarms of this component were snoozed. + + You can set several times like this. Only the latest one counts. + The alarms are supposed to turn up again at dt when they are not acknowledged + but snoozed. """ @property @@ -208,4 +221,9 @@ def _get_end_alarm_times(self) -> list[AlarmTime]: for trigger in self._repeat(self._add(self._end, alarm["TRIGGER"].dt), alarm) ] + @property + def active(self): + """The alarm times that are still active and not acknowledged.""" + return [alarm_time for alarm_time in self.times if alarm_time.is_active] + __all__ = ["Alarms", "AlarmTime", "IncompleteAlarmInformation"] diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 4c450c82..1e8ae2e4 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -16,7 +16,7 @@ from icalendar.parser_tools import DEFAULT_ENCODING from icalendar.prop import TypesFactory, vDDDLists, vDDDTypes, vText, vDuration from icalendar.timezone import tzp -from icalendar.tools import is_date, is_datetime +from icalendar.tools import is_date, is_datetime, to_datetime def get_example(component_directory: str, example_name: str) -> bytes: @@ -77,9 +77,42 @@ class IncompleteComponent(ValueError): The attributes are not required, otherwise this would be an InvalidCalendar. But in order to perform calculations, this attribute is required. + + This error is not raised in the UPPERCASE properties like .DTSTART, + only in the lowercase computations like .start. + """ + +def create_utc_property(name:str, docs:str): + """Create a property to access a value of datetime in UTC timezone. + + name - name of the property + docs - documentation string """ + docs = f"""The {name} property. datetime in UTC + + All values will be converted to a datetime in UTC. + """ + docs + + def p_get(self: Component) -> Optional[datetime]: + """Get the value.""" + if name not in self: + return None + dt = self.get(name) + value = getattr(dt, "dt", None) + print(value) + if value is None or not isinstance(value, date): + raise InvalidCalendar(f"{name} must be a datetime in UTC, not {value}") + return value + + def p_set(self: Component, value: datetime): + """Set the value""" + if not isinstance(value, date): + raise TypeError(f"{name} takes a datetime in UTC, not {value}") + self.pop(name) + self.add(name, tzp.localize_utc(to_datetime(value))) + return property(p_get, p_set, doc=docs) class Component(CaselessDict): """Component is the base object for calendar, Event and the other @@ -495,6 +528,24 @@ def __eq__(self, other): return True + DTSTAMP = create_utc_property("DTSTAMP", """RFC 5545: + + In the case of an iCalendar object that specifies a + "METHOD" property, this property specifies the date and time that + the instance of the iCalendar object was created. In the case of + an iCalendar object that doesn't specify a "METHOD" property, this + property specifies the date and time that the information + associated with the calendar component was last revised in the + calendar store. + + The value MUST be specified in the UTC time format. + + In the case of an iCalendar object that doesn't specify a "METHOD" + property, this property is equivalent to the "LAST-MODIFIED" + property. + """) + + ####################################### # components defined in RFC 5545 diff --git a/src/icalendar/tests/calendars/alarm_outlook_closed.ics b/src/icalendar/tests/calendars/alarm_outlook_closed.ics deleted file mode 100644 index 3e073b82..00000000 --- a/src/icalendar/tests/calendars/alarm_outlook_closed.ics +++ /dev/null @@ -1,95 +0,0 @@ -BEGIN:VCALENDAR -METHOD:PUBLISH -PRODID:Microsoft Exchange Server 2010 -VERSION:2.0 -X-WR-CALNAME:Kalender -BEGIN:VTIMEZONE -TZID:GMT Standard Time -BEGIN:STANDARD -DTSTART:16010101T020000 -TZOFFSETFROM:+0100 -TZOFFSETTO:+0000 -RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 -END:STANDARD -BEGIN:DAYLIGHT -DTSTART:16010101T010000 -TZOFFSETFROM:+0000 -TZOFFSETTO:+0100 -RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 -END:DAYLIGHT -END:VTIMEZONE -BEGIN:VEVENT -RRULE:FREQ=WEEKLY;UNTIL=20240728T230000Z;INTERVAL=2;BYDAY=MO;WKST=MO -EXDATE;TZID=GMT Standard Time:20240715T000000 -UID:111 -SUMMARY:test123 -DTSTART;VALUE=DATE:20240701 -DTEND;VALUE=DATE:20240708 -CLASS:PUBLIC -PRIORITY:5 -DTSTAMP:20241004T173601Z -TRANSP:OPAQUE -STATUS:CONFIRMED -SEQUENCE:3 -X-MICROSOFT-CDO-APPT-SEQUENCE:3 -X-MICROSOFT-CDO-BUSYSTATUS:BUSY -X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY -X-MICROSOFT-CDO-ALLDAYEVENT:TRUE -X-MICROSOFT-CDO-IMPORTANCE:1 -X-MICROSOFT-CDO-INSTTYPE:1 -X-MICROSOFT-DONOTFORWARDMEETING:FALSE -X-MICROSOFT-DISALLOW-COUNTER:FALSE -X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT -X-MICROSOFT-ISRESPONSEREQUESTED:FALSE -END:VEVENT -BEGIN:VEVENT -RRULE:FREQ=WEEKLY;UNTIL=20240728T230000Z;INTERVAL=2;BYDAY=MO;WKST=MO -EXDATE;TZID=GMT Standard Time:20240715T000000 -UID:040000008200E00074C5B7101A82E00800000000C41E94C5B4F3DA01000000000000000 - 0100000007EA1D0AA9DB96545A536FABC0B0C1F5A -SUMMARY:test123 - edited event!!!! -DTSTART;VALUE=DATE:20240701 -DTEND;VALUE=DATE:20240708 -CLASS:PUBLIC -PRIORITY:5 -DTSTAMP:20241004T173601Z -TRANSP:OPAQUE -STATUS:CONFIRMED -SEQUENCE:2 -X-MICROSOFT-CDO-APPT-SEQUENCE:2 -X-MICROSOFT-CDO-BUSYSTATUS:BUSY -X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY -X-MICROSOFT-CDO-ALLDAYEVENT:TRUE -X-MICROSOFT-CDO-IMPORTANCE:1 -X-MICROSOFT-CDO-INSTTYPE:1 -X-MICROSOFT-DONOTFORWARDMEETING:FALSE -X-MICROSOFT-DISALLOW-COUNTER:FALSE -X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT -X-MICROSOFT-ISRESPONSEREQUESTED:FALSE -END:VEVENT -BEGIN:VEVENT -DESCRIPTION:\n -UID:040000008200E00074C5B7101A82E00800000000EF8046288216DB01000000000000000 - 0100000000D748168D60A25439FA3E76297E80CB0 -SUMMARY:outlook event with alarm -DTSTART;TZID=GMT Standard Time:20241004T183000 -DTEND;TZID=GMT Standard Time:20241004T190000 -CLASS:PUBLIC -PRIORITY:5 -DTSTAMP:20241004T173601Z -TRANSP:OPAQUE -STATUS:CONFIRMED -SEQUENCE:0 -LOCATION: -X-MICROSOFT-CDO-APPT-SEQUENCE:0 -X-MICROSOFT-CDO-BUSYSTATUS:BUSY -X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY -X-MICROSOFT-CDO-ALLDAYEVENT:FALSE -X-MICROSOFT-CDO-IMPORTANCE:1 -X-MICROSOFT-CDO-INSTTYPE:0 -X-MICROSOFT-DONOTFORWARDMEETING:FALSE -X-MICROSOFT-DISALLOW-COUNTER:FALSE -X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT -X-MICROSOFT-ISRESPONSEREQUESTED:FALSE -END:VEVENT -END:VCALENDAR diff --git a/src/icalendar/tests/calendars/alarm_outlook_in_future.ics b/src/icalendar/tests/calendars/alarm_outlook_in_future.ics deleted file mode 100644 index 874a7d3a..00000000 --- a/src/icalendar/tests/calendars/alarm_outlook_in_future.ics +++ /dev/null @@ -1,95 +0,0 @@ -BEGIN:VCALENDAR -METHOD:PUBLISH -PRODID:Microsoft Exchange Server 2010 -VERSION:2.0 -X-WR-CALNAME:Kalender -BEGIN:VTIMEZONE -TZID:GMT Standard Time -BEGIN:STANDARD -DTSTART:16010101T020000 -TZOFFSETFROM:+0100 -TZOFFSETTO:+0000 -RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 -END:STANDARD -BEGIN:DAYLIGHT -DTSTART:16010101T010000 -TZOFFSETFROM:+0000 -TZOFFSETTO:+0100 -RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 -END:DAYLIGHT -END:VTIMEZONE -BEGIN:VEVENT -RRULE:FREQ=WEEKLY;UNTIL=20240728T230000Z;INTERVAL=2;BYDAY=MO;WKST=MO -EXDATE;TZID=GMT Standard Time:20240715T000000 -UID:111 -SUMMARY:test123 -DTSTART;VALUE=DATE:20240701 -DTEND;VALUE=DATE:20240708 -CLASS:PUBLIC -PRIORITY:5 -DTSTAMP:20241004T172524Z -TRANSP:OPAQUE -STATUS:CONFIRMED -SEQUENCE:3 -X-MICROSOFT-CDO-APPT-SEQUENCE:3 -X-MICROSOFT-CDO-BUSYSTATUS:BUSY -X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY -X-MICROSOFT-CDO-ALLDAYEVENT:TRUE -X-MICROSOFT-CDO-IMPORTANCE:1 -X-MICROSOFT-CDO-INSTTYPE:1 -X-MICROSOFT-DONOTFORWARDMEETING:FALSE -X-MICROSOFT-DISALLOW-COUNTER:FALSE -X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT -X-MICROSOFT-ISRESPONSEREQUESTED:FALSE -END:VEVENT -BEGIN:VEVENT -RRULE:FREQ=WEEKLY;UNTIL=20240728T230000Z;INTERVAL=2;BYDAY=MO;WKST=MO -EXDATE;TZID=GMT Standard Time:20240715T000000 -UID:040000008200E00074C5B7101A82E00800000000C41E94C5B4F3DA01000000000000000 - 0100000007EA1D0AA9DB96545A536FABC0B0C1F5A -SUMMARY:test123 - edited event!!!! -DTSTART;VALUE=DATE:20240701 -DTEND;VALUE=DATE:20240708 -CLASS:PUBLIC -PRIORITY:5 -DTSTAMP:20241004T172524Z -TRANSP:OPAQUE -STATUS:CONFIRMED -SEQUENCE:2 -X-MICROSOFT-CDO-APPT-SEQUENCE:2 -X-MICROSOFT-CDO-BUSYSTATUS:BUSY -X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY -X-MICROSOFT-CDO-ALLDAYEVENT:TRUE -X-MICROSOFT-CDO-IMPORTANCE:1 -X-MICROSOFT-CDO-INSTTYPE:1 -X-MICROSOFT-DONOTFORWARDMEETING:FALSE -X-MICROSOFT-DISALLOW-COUNTER:FALSE -X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT -X-MICROSOFT-ISRESPONSEREQUESTED:FALSE -END:VEVENT -BEGIN:VEVENT -DESCRIPTION:\n -UID:040000008200E00074C5B7101A82E00800000000EF8046288216DB01000000000000000 - 0100000000D748168D60A25439FA3E76297E80CB0 -SUMMARY:outlook event with alarm -DTSTART;TZID=GMT Standard Time:20241004T183000 -DTEND;TZID=GMT Standard Time:20241004T190000 -CLASS:PUBLIC -PRIORITY:5 -DTSTAMP:20241004T172524Z -TRANSP:OPAQUE -STATUS:CONFIRMED -SEQUENCE:0 -LOCATION: -X-MICROSOFT-CDO-APPT-SEQUENCE:0 -X-MICROSOFT-CDO-BUSYSTATUS:BUSY -X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY -X-MICROSOFT-CDO-ALLDAYEVENT:FALSE -X-MICROSOFT-CDO-IMPORTANCE:1 -X-MICROSOFT-CDO-INSTTYPE:0 -X-MICROSOFT-DONOTFORWARDMEETING:FALSE -X-MICROSOFT-DISALLOW-COUNTER:FALSE -X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT -X-MICROSOFT-ISRESPONSEREQUESTED:FALSE -END:VEVENT -END:VCALENDAR diff --git a/src/icalendar/tests/calendars/alarm_outlook_postponed_5_min.ics b/src/icalendar/tests/calendars/alarm_outlook_postponed_5_min.ics deleted file mode 100644 index 0a4abf0d..00000000 --- a/src/icalendar/tests/calendars/alarm_outlook_postponed_5_min.ics +++ /dev/null @@ -1,95 +0,0 @@ -BEGIN:VCALENDAR -METHOD:PUBLISH -PRODID:Microsoft Exchange Server 2010 -VERSION:2.0 -X-WR-CALNAME:Kalender -BEGIN:VTIMEZONE -TZID:GMT Standard Time -BEGIN:STANDARD -DTSTART:16010101T020000 -TZOFFSETFROM:+0100 -TZOFFSETTO:+0000 -RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 -END:STANDARD -BEGIN:DAYLIGHT -DTSTART:16010101T010000 -TZOFFSETFROM:+0000 -TZOFFSETTO:+0100 -RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 -END:DAYLIGHT -END:VTIMEZONE -BEGIN:VEVENT -RRULE:FREQ=WEEKLY;UNTIL=20240728T230000Z;INTERVAL=2;BYDAY=MO;WKST=MO -EXDATE;TZID=GMT Standard Time:20240715T000000 -UID:111 -SUMMARY:test123 -DTSTART;VALUE=DATE:20240701 -DTEND;VALUE=DATE:20240708 -CLASS:PUBLIC -PRIORITY:5 -DTSTAMP:20241004T173132Z -TRANSP:OPAQUE -STATUS:CONFIRMED -SEQUENCE:3 -X-MICROSOFT-CDO-APPT-SEQUENCE:3 -X-MICROSOFT-CDO-BUSYSTATUS:BUSY -X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY -X-MICROSOFT-CDO-ALLDAYEVENT:TRUE -X-MICROSOFT-CDO-IMPORTANCE:1 -X-MICROSOFT-CDO-INSTTYPE:1 -X-MICROSOFT-DONOTFORWARDMEETING:FALSE -X-MICROSOFT-DISALLOW-COUNTER:FALSE -X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT -X-MICROSOFT-ISRESPONSEREQUESTED:FALSE -END:VEVENT -BEGIN:VEVENT -RRULE:FREQ=WEEKLY;UNTIL=20240728T230000Z;INTERVAL=2;BYDAY=MO;WKST=MO -EXDATE;TZID=GMT Standard Time:20240715T000000 -UID:040000008200E00074C5B7101A82E00800000000C41E94C5B4F3DA01000000000000000 - 0100000007EA1D0AA9DB96545A536FABC0B0C1F5A -SUMMARY:test123 - edited event!!!! -DTSTART;VALUE=DATE:20240701 -DTEND;VALUE=DATE:20240708 -CLASS:PUBLIC -PRIORITY:5 -DTSTAMP:20241004T173132Z -TRANSP:OPAQUE -STATUS:CONFIRMED -SEQUENCE:2 -X-MICROSOFT-CDO-APPT-SEQUENCE:2 -X-MICROSOFT-CDO-BUSYSTATUS:BUSY -X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY -X-MICROSOFT-CDO-ALLDAYEVENT:TRUE -X-MICROSOFT-CDO-IMPORTANCE:1 -X-MICROSOFT-CDO-INSTTYPE:1 -X-MICROSOFT-DONOTFORWARDMEETING:FALSE -X-MICROSOFT-DISALLOW-COUNTER:FALSE -X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT -X-MICROSOFT-ISRESPONSEREQUESTED:FALSE -END:VEVENT -BEGIN:VEVENT -DESCRIPTION:\n -UID:040000008200E00074C5B7101A82E00800000000EF8046288216DB01000000000000000 - 0100000000D748168D60A25439FA3E76297E80CB0 -SUMMARY:outlook event with alarm -DTSTART;TZID=GMT Standard Time:20241004T183000 -DTEND;TZID=GMT Standard Time:20241004T190000 -CLASS:PUBLIC -PRIORITY:5 -DTSTAMP:20241004T173132Z -TRANSP:OPAQUE -STATUS:CONFIRMED -SEQUENCE:0 -LOCATION: -X-MICROSOFT-CDO-APPT-SEQUENCE:0 -X-MICROSOFT-CDO-BUSYSTATUS:BUSY -X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY -X-MICROSOFT-CDO-ALLDAYEVENT:FALSE -X-MICROSOFT-CDO-IMPORTANCE:1 -X-MICROSOFT-CDO-INSTTYPE:0 -X-MICROSOFT-DONOTFORWARDMEETING:FALSE -X-MICROSOFT-DISALLOW-COUNTER:FALSE -X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT -X-MICROSOFT-ISRESPONSEREQUESTED:FALSE -END:VEVENT -END:VCALENDAR diff --git a/src/icalendar/tests/prop/test_alarm.py b/src/icalendar/tests/prop/test_alarm.py new file mode 100644 index 00000000..7decf33a --- /dev/null +++ b/src/icalendar/tests/prop/test_alarm.py @@ -0,0 +1,57 @@ +"""Test the properties of the alarm.""" + + +import pytest + +from icalendar.cal import Alarm, InvalidCalendar + + +def test_repeat_absent(): + """Test the absence of REPEAT.""" + assert Alarm().REPEAT == 0 + + +def test_repeat_number(): + """Test the absence of REPEAT.""" + assert Alarm({"REPEAT": 10}).REPEAT == 10 + + +def test_set_REPEAT(): + """Check setting the value.""" + a = Alarm() + a.REPEAT = 10 + assert a.REPEAT == 10 + + +def test_set_REPEAT_twice(): + """Check setting the value.""" + a = Alarm() + a.REPEAT = 10 + a.REPEAT = 20 + assert a.REPEAT == 20 + + +def test_add_REPEAT(): + """Check setting the value.""" + a = Alarm() + a.add("REPEAT", 10) + assert a.REPEAT == 10 + + +def test_invalid_repeat_value(): + """Check setting the value.""" + a = Alarm() + with pytest.raises(ValueError): + a.REPEAT = "asd" + a["REPEAT"] = "asd" + with pytest.raises(InvalidCalendar): + a.REPEAT # noqa: B018 + + +def test_alarm_to_string(): + a = Alarm() + a.REPEAT = 11 + assert a.to_ical() == b"BEGIN:VALARM\r\nREPEAT:11\r\nEND:VALARM\r\n" + + + diff --git a/src/icalendar/tests/prop/test_component.py b/src/icalendar/tests/prop/test_component.py new file mode 100644 index 00000000..a79755fa --- /dev/null +++ b/src/icalendar/tests/prop/test_component.py @@ -0,0 +1,74 @@ +"""Test common properties of components.""" + +from datetime import date, datetime, timedelta + +import pytest + +from icalendar import Event, FreeBusy, Journal, Todo, vDDDTypes +from icalendar.cal import InvalidCalendar + + +@pytest.fixture(params=[Event, Todo, Journal, FreeBusy]) +def dtstamp_comp(request): + """a component to test""" + return request.param() + + +def test_no_dtstamp(dtstamp_comp): + """We have None as a value.""" + assert dtstamp_comp.DTSTAMP is None + + +@pytest.mark.parametrize( + ("value", "timezone", "expected"), + [ + (datetime(2024, 10, 11, 23, 1), None, datetime(2024, 10, 11, 23, 1)), + (datetime(2024, 10, 11, 23, 1), "Europe/Berlin", datetime(2024, 10, 11, 21, 1)), + (datetime(2024, 10, 11, 22, 1), "UTC", datetime(2024, 10, 11, 22, 1)), + (date(2024, 10, 10), None, datetime(2024, 10, 10)), + ] +) +def test_set_value_and_get_it(dtstamp_comp, value, timezone, expected, tzp): + """Set and get the DTSTAMP value.""" + dtstamp = value if timezone is None else tzp.localize(value, timezone) + dtstamp_comp.DTSTAMP = dtstamp + in_utc = tzp.localize_utc(expected) + get_value = dtstamp_comp.get("DTSTAMP").dt + assert in_utc == get_value + assert in_utc == dtstamp_comp.DTSTAMP + + +@pytest.mark.parametrize( + "invalid_value", + [ + None, + timedelta() + ] +) +def test_set_invalid_value(invalid_value, dtstamp_comp): + """Check handling of invalid values.""" + with pytest.raises(TypeError) as e: + dtstamp_comp.DTSTAMP = invalid_value + assert e.value.args[0] == f"DTSTAMP takes a datetime in UTC, not {invalid_value}" + + +@pytest.mark.parametrize( + "invalid_value", + [ + None, + vDDDTypes(timedelta()) + ] +) +def test_get_invalid_value(invalid_value, dtstamp_comp): + """Check handling of invalid values.""" + dtstamp_comp["DTSTAMP"] = invalid_value + with pytest.raises(InvalidCalendar) as e: + dtstamp_comp.DTSTAMP # noqa: B018 + assert e.value.args[0] == f"DTSTAMP must be a datetime in UTC, not {getattr(invalid_value, 'dt', invalid_value)}" + + +def test_set_twice(dtstamp_comp, tzp): + """Set the value twice.""" + dtstamp_comp.DTSTAMP = date(2014, 1, 1) + dtstamp_comp.DTSTAMP = date(2014, 1, 2) + assert tzp.localize_utc(datetime(2014, 1, 2)) == dtstamp_comp.DTSTAMP diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py index 30843b2e..14134389 100644 --- a/src/icalendar/tests/test_issue_716_alarm_time_computation.py +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -223,3 +223,23 @@ def test_cannot_set_the_event_twice(calendars): def test_alarms_from_calendar(): pytest.skip("TODO") + + + +@pytest.mark.parametrize( + ("calendar", "index", "count", "message"), + [ + ("alarm_etar_future", -1, 3, "Etar: we just created the alarm"), + ("alarm_etar_notification", -1, 3, "Etar: the notification popped up"), + ("alarm_etar_notification_clicked", -1, 1, "Etar: the notification was dismissed"), + ("alarm_google_future", -1, 4, "Google: we just created the event with alarms"), + ("alarm_google_acknowledged", -1, 2, "Google: 2 alarms happened at the same time"), + ("", -1, 1, ""), + ("", -1, 1, ""), + ] +) +def test_active_alarms(calendars, calendar, index, count, message): + """Check that we extract calculate the correct amount of active alarms.""" + event = calendars[calendar].subcomponents[index] + a = Alarms(event) + assert len(a.active) == count, f"{message} - I expect {count} alarms active but got {len(a.active)}." From 914b20453542b21f8e79856f08bdfd5023dfa78f Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 23 Oct 2024 13:41:41 +0100 Subject: [PATCH 12/50] Improve error message if wrong provider is used --- src/icalendar/cal.py | 3 +- src/icalendar/prop.py | 35 +++++++++---------- src/icalendar/tests/prop/test_component.py | 23 ++++++++++-- .../tests/test_pytz_zoneinfo_integration.py | 12 +++++++ src/icalendar/timezone/__init__.py | 2 +- src/icalendar/timezone/tzp.py | 31 +++++++++------- src/icalendar/tools.py | 2 +- 7 files changed, 70 insertions(+), 38 deletions(-) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 1e8ae2e4..a689fd1b 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -99,7 +99,6 @@ def p_get(self: Component) -> Optional[datetime]: return None dt = self.get(name) value = getattr(dt, "dt", None) - print(value) if value is None or not isinstance(value, date): raise InvalidCalendar(f"{name} must be a datetime in UTC, not {value}") return value @@ -109,7 +108,7 @@ def p_set(self: Component, value: datetime): if not isinstance(value, date): raise TypeError(f"{name} takes a datetime in UTC, not {value}") self.pop(name) - self.add(name, tzp.localize_utc(to_datetime(value))) + self.add(name, tzp.localize_utc(value)) return property(p_get, p_set, doc=docs) diff --git a/src/icalendar/prop.py b/src/icalendar/prop.py index c2969ecc..d15630cc 100644 --- a/src/icalendar/prop.py +++ b/src/icalendar/prop.py @@ -35,28 +35,25 @@ These types are mainly used for parsing and file generation. But you can set them directly. """ -from datetime import date -from datetime import datetime -from datetime import time -from datetime import timedelta -from datetime import tzinfo -from icalendar.caselessdict import CaselessDict -from icalendar.parser import Parameters -from icalendar.parser import escape_char -from icalendar.parser import unescape_char -from icalendar.parser_tools import ( - DEFAULT_ENCODING, SEQUENCE_TYPES, to_unicode, from_unicode, ICAL_TYPE -) - import base64 import binascii -from .timezone import tzp import re import time as _time - -from typing import Optional, Union +from datetime import date, datetime, time, timedelta, tzinfo from enum import Enum, auto +from typing import Optional, Union + +from icalendar.caselessdict import CaselessDict +from icalendar.parser import Parameters, escape_char, unescape_char +from icalendar.parser_tools import ( + DEFAULT_ENCODING, + ICAL_TYPE, + SEQUENCE_TYPES, + from_unicode, + to_unicode, +) +from . import timezone as _timezone DURATION_REGEX = re.compile(r'([-+]?)P(?:(\d+)W)?(?:(\d+)D)?' r'(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$') @@ -410,7 +407,7 @@ def to_ical(self): def from_ical(ical, timezone=None): tzinfo = None if timezone: - tzinfo = tzp.timezone(timezone) + tzinfo = _timezone.tzp.timezone(timezone) try: timetuple = ( @@ -422,11 +419,11 @@ def from_ical(ical, timezone=None): int(ical[13:15]), # second ) if tzinfo: - return tzp.localize(datetime(*timetuple), tzinfo) + return _timezone.tzp.localize(datetime(*timetuple), tzinfo) elif not ical[15:]: return datetime(*timetuple) elif ical[15:16] == 'Z': - return tzp.localize_utc(datetime(*timetuple)) + return _timezone.tzp.localize_utc(datetime(*timetuple)) else: raise ValueError(ical) except Exception: diff --git a/src/icalendar/tests/prop/test_component.py b/src/icalendar/tests/prop/test_component.py index a79755fa..dd25bdf1 100644 --- a/src/icalendar/tests/prop/test_component.py +++ b/src/icalendar/tests/prop/test_component.py @@ -5,7 +5,7 @@ import pytest from icalendar import Event, FreeBusy, Journal, Todo, vDDDTypes -from icalendar.cal import InvalidCalendar +from icalendar.cal import Component, InvalidCalendar @pytest.fixture(params=[Event, Todo, Journal, FreeBusy]) @@ -19,6 +19,20 @@ def test_no_dtstamp(dtstamp_comp): assert dtstamp_comp.DTSTAMP is None +def set_dtstamp_attribute(component:Component, value:date): + """Use the setter.""" + component.DTSTAMP = value + +def set_dtstamp_item(component: Component, value:date): + """Use setitem.""" + component["DTSTAMP"] = vDDDTypes(value) + +def set_dtstamp_add(component: Component, value:date): + """Use add.""" + component.add("DTSTAMP", value) + + + @pytest.mark.parametrize( ("value", "timezone", "expected"), [ @@ -28,10 +42,13 @@ def test_no_dtstamp(dtstamp_comp): (date(2024, 10, 10), None, datetime(2024, 10, 10)), ] ) -def test_set_value_and_get_it(dtstamp_comp, value, timezone, expected, tzp): +@pytest.mark.parametrize( + "set_dtstamp", [set_dtstamp_add, set_dtstamp_attribute, set_dtstamp_item] +) +def test_set_value_and_get_it(dtstamp_comp, value, timezone, expected, tzp, set_dtstamp): """Set and get the DTSTAMP value.""" dtstamp = value if timezone is None else tzp.localize(value, timezone) - dtstamp_comp.DTSTAMP = dtstamp + set_dtstamp(dtstamp_comp, dtstamp) in_utc = tzp.localize_utc(expected) get_value = dtstamp_comp.get("DTSTAMP").dt assert in_utc == get_value diff --git a/src/icalendar/tests/test_pytz_zoneinfo_integration.py b/src/icalendar/tests/test_pytz_zoneinfo_integration.py index a02aa10d..aad6d9a6 100644 --- a/src/icalendar/tests/test_pytz_zoneinfo_integration.py +++ b/src/icalendar/tests/test_pytz_zoneinfo_integration.py @@ -90,3 +90,15 @@ def test_cache_is_emptied_when_tzp_is_switched(tzp, timezones, new_tzp_name): tzp.cache_timezone_component(timezones.pacific_fiji) tz2 = tzp.timezone("custom_Pacific/Fiji") assert tz1 is not tz2 + + +def test_invalid_name(tzp): + """Check that the provider name is ok.""" + provider = "invalid_provider" + with pytest.raises(ValueError) as e: + tzp.use(provider) + # f"Unknown provider {provider}. Use 'pytz' or 'zoneinfo'." + message = e.value.args[0] + assert f"Unknown provider {provider}." in message + assert "zoneinfo" in message + assert "pytz" in message diff --git a/src/icalendar/timezone/__init__.py b/src/icalendar/timezone/__init__.py index 09ae612c..e0da7837 100644 --- a/src/icalendar/timezone/__init__.py +++ b/src/icalendar/timezone/__init__.py @@ -13,4 +13,4 @@ def use_zoneinfo(): tzp.use_zoneinfo() -__all__ = ["tzp", "use_pytz", "use_zoneinfo", "normalize_pytz"] +__all__ = ["tzp", "use_pytz", "use_zoneinfo"] diff --git a/src/icalendar/timezone/tzp.py b/src/icalendar/timezone/tzp.py index 48cc1e4b..67939f4e 100644 --- a/src/icalendar/timezone/tzp.py +++ b/src/icalendar/timezone/tzp.py @@ -1,12 +1,19 @@ from __future__ import annotations -import datetime -from .. import cal -from typing import Optional, Union + +from typing import TYPE_CHECKING, Optional, Union + +from icalendar.tools import to_datetime + from .windows_to_olson import WINDOWS_TO_OLSON -from .provider import TZProvider -from icalendar import prop -from dateutil.rrule import rrule +if TYPE_CHECKING: + import datetime + + from dateutil.rrule import rrule + + from icalendar import cal, prop + + from .provider import TZProvider DEFAULT_TIMEZONE_PROVIDER = "zoneinfo" @@ -41,10 +48,10 @@ def _use(self, provider:TZProvider) -> None: def use(self, provider:Union[str, TZProvider]): """Switch to a different timezone provider.""" if isinstance(provider, str): - provider = getattr(self, f"use_{provider}", None) - if provider is None: - raise ValueError(f"Unknown provider {provider_name}. Use 'pytz' or 'zoneinfo'.") - provider() + use_provider = getattr(self, f"use_{provider}", None) + if use_provider is None: + raise ValueError(f"Unknown provider {provider}. Use 'pytz' or 'zoneinfo'.") + use_provider() else: self._use(provider) @@ -52,12 +59,12 @@ def use_default(self): """Use the default timezone provider.""" self.use(DEFAULT_TIMEZONE_PROVIDER) - def localize_utc(self, dt: datetime.datetime) -> datetime.datetime: + def localize_utc(self, dt: datetime.date) -> datetime.datetime: """Return the datetime in UTC. If the datetime has no timezone, set UTC as its timezone. """ - return self.__provider.localize_utc(dt) + return self.__provider.localize_utc(to_datetime(dt)) def localize(self, dt: datetime.datetime, tz: Union[datetime.tzinfo, str]) -> datetime.datetime: """Localize a datetime to a timezone.""" diff --git a/src/icalendar/tools.py b/src/icalendar/tools.py index 191e4233..24c05fab 100644 --- a/src/icalendar/tools.py +++ b/src/icalendar/tools.py @@ -4,7 +4,6 @@ from string import ascii_letters, digits from icalendar.parser_tools import to_unicode -from icalendar.prop import vDatetime, vText class UIDGenerator: @@ -26,6 +25,7 @@ def uid(host_name="example.com", unique=""): Like: 20050105T225746Z-HKtJMqUgdO0jDUwm@example.com """ + from icalendar.prop import vDatetime, vText host_name = to_unicode(host_name) unique = unique or UIDGenerator.rnd_string() today = to_unicode(vDatetime(datetime.today()).to_ical()) From 51099d4e6979cdd01698951026bd7dda9cff4668 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 23 Oct 2024 13:54:45 +0100 Subject: [PATCH 13/50] Make sure the UTC datetime is returned by DTSTAMP --- src/icalendar/cal.py | 2 +- src/icalendar/tests/prop/test_component.py | 2 +- src/icalendar/timezone/tzp.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index a689fd1b..17714f5b 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -101,7 +101,7 @@ def p_get(self: Component) -> Optional[datetime]: value = getattr(dt, "dt", None) if value is None or not isinstance(value, date): raise InvalidCalendar(f"{name} must be a datetime in UTC, not {value}") - return value + return tzp.localize_utc(value) def p_set(self: Component, value: datetime): """Set the value""" diff --git a/src/icalendar/tests/prop/test_component.py b/src/icalendar/tests/prop/test_component.py index dd25bdf1..80442f48 100644 --- a/src/icalendar/tests/prop/test_component.py +++ b/src/icalendar/tests/prop/test_component.py @@ -51,7 +51,7 @@ def test_set_value_and_get_it(dtstamp_comp, value, timezone, expected, tzp, set_ set_dtstamp(dtstamp_comp, dtstamp) in_utc = tzp.localize_utc(expected) get_value = dtstamp_comp.get("DTSTAMP").dt - assert in_utc == get_value + assert in_utc == get_value or set_dtstamp != set_dtstamp_attribute assert in_utc == dtstamp_comp.DTSTAMP diff --git a/src/icalendar/timezone/tzp.py b/src/icalendar/timezone/tzp.py index 67939f4e..6c24b389 100644 --- a/src/icalendar/timezone/tzp.py +++ b/src/icalendar/timezone/tzp.py @@ -66,13 +66,13 @@ def localize_utc(self, dt: datetime.date) -> datetime.datetime: """ return self.__provider.localize_utc(to_datetime(dt)) - def localize(self, dt: datetime.datetime, tz: Union[datetime.tzinfo, str]) -> datetime.datetime: + def localize(self, dt: datetime.date, tz: Union[datetime.tzinfo, str]) -> datetime.datetime: """Localize a datetime to a timezone.""" if isinstance(tz, str): tz = self.timezone(tz) - return self.__provider.localize(dt, tz) + return self.__provider.localize(to_datetime(dt), tz) - def cache_timezone_component(self, timezone_component: cal.VTIMEZONE) -> None: + def cache_timezone_component(self, timezone_component: cal.Timezone) -> None: """Cache the timezone that is created from a timezone component if it is not already known. From 61dc8c6a215b97f4fc0e40e11ef2ffadd63fed0e Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 23 Oct 2024 14:05:10 +0100 Subject: [PATCH 14/50] Improve docs and add LAST_MODIFIED --- src/icalendar/cal.py | 41 +++++++++++++++------- src/icalendar/tests/prop/test_component.py | 6 ++++ 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 17714f5b..4758ae1f 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -529,19 +529,34 @@ def __eq__(self, other): DTSTAMP = create_utc_property("DTSTAMP", """RFC 5545: - In the case of an iCalendar object that specifies a - "METHOD" property, this property specifies the date and time that - the instance of the iCalendar object was created. In the case of - an iCalendar object that doesn't specify a "METHOD" property, this - property specifies the date and time that the information - associated with the calendar component was last revised in the - calendar store. - - The value MUST be specified in the UTC time format. - - In the case of an iCalendar object that doesn't specify a "METHOD" - property, this property is equivalent to the "LAST-MODIFIED" - property. + Conformance: This property MUST be included in the "VEVENT", + "VTODO", "VJOURNAL", or "VFREEBUSY" calendar components. + + Description: In the case of an iCalendar object that specifies a + "METHOD" property, this property specifies the date and time that + the instance of the iCalendar object was created. In the case of + an iCalendar object that doesn't specify a "METHOD" property, this + property specifies the date and time that the information + associated with the calendar component was last revised in the + calendar store. + + The value MUST be specified in the UTC time format. + + In the case of an iCalendar object that doesn't specify a "METHOD" + property, this property is equivalent to the "LAST-MODIFIED" + property. + """) + LAST_MODIFIED = create_utc_property("LAST-MODIFIED", """RFC 5545: + + Purpose: This property specifies the date and time that the + information associated with the calendar component was last + revised in the calendar store. + + Note: This is analogous to the modification date and time for a + file in the file system. + + Conformance: This property can be specified in the "VEVENT", + "VTODO", "VJOURNAL", or "VTIMEZONE" calendar components. """) diff --git a/src/icalendar/tests/prop/test_component.py b/src/icalendar/tests/prop/test_component.py index 80442f48..adc5e21f 100644 --- a/src/icalendar/tests/prop/test_component.py +++ b/src/icalendar/tests/prop/test_component.py @@ -89,3 +89,9 @@ def test_set_twice(dtstamp_comp, tzp): dtstamp_comp.DTSTAMP = date(2014, 1, 1) dtstamp_comp.DTSTAMP = date(2014, 1, 2) assert tzp.localize_utc(datetime(2014, 1, 2)) == dtstamp_comp.DTSTAMP + + +def test_last_modified(dtstamp_comp, tzp): + """Check we can set LAST_MODIFIED in the same way as DTSTAMP""" + dtstamp_comp.LAST_MODIFIED = date(2014, 1, 2) + assert tzp.localize_utc(datetime(2014, 1, 2)) == dtstamp_comp.LAST_MODIFIED From 09cf9b10c56460200b61880bfe258a543f88ba49 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 23 Oct 2024 15:40:01 +0100 Subject: [PATCH 15/50] Allow acknowledgement of alarms --- src/icalendar/alarms.py | 55 ++++++++++--- .../test_issue_716_alarm_time_computation.py | 81 +++++++------------ 2 files changed, 71 insertions(+), 65 deletions(-) diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index 94a6e5bb..ed183770 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -11,11 +11,12 @@ from __future__ import annotations -from datetime import date, timedelta +from datetime import date, timedelta, tzinfo from typing import TYPE_CHECKING, Generator, Optional, Union from icalendar.cal import Alarm, Event, Todo from icalendar.tools import is_date, normalize_pytz, to_datetime +from icalendar.timezone import tzp if TYPE_CHECKING: from datetime import datetime @@ -30,12 +31,19 @@ class IncompleteAlarmInformation(ValueError): class AlarmTime: """An alarm time with all the information.""" - def __init__(self, alarm: Alarm, trigger : datetime, acknowledged:Optional[datetime], parent: Optional[Parent]): - """Create a new AlarmTime.""" + def __init__(self, alarm: Alarm, trigger : datetime, acknowledged_until:Optional[datetime], parent: Optional[Parent]): + """Create a new AlarmTime. + + alarm - the Alarm component + trigger - a date or datetime at which to trigger the alarm + acknowledged_until - an optional datetime in UTC until when all alarms + have been acknowledged + parent - the optional parent component the alarm refers to + """ self._alarm = alarm self._parent = parent self._trigger = trigger - self._acknowledged = acknowledged + self._last_ack = acknowledged_until @property def alarm(self) -> Alarm: @@ -50,17 +58,29 @@ def parent(self) -> Optional[Parent]: """ return self._parent - @property - def is_active(self) -> bool: + def is_active_in(self, timezone:Optional[tzinfo]=None) -> bool: """Whether this alarm is active (True) or acknowledged (False). E.g. in some calendar software, this is True until the user had a look at the alarm message and clicked the dismiss button. + + Alarms can be in local time (without a timezone). + To calculate if the alarm really happened, we need it to be in a timezone. + If a timezone is required but not given, we throw an IncompleteAlarmInformation. """ + if not self._last_ack: + # if nothing is acknowledged, this alarm counts + return True + trigger = self.trigger if timezone is None else tzp.localize(self.trigger, timezone) + return trigger > self._last_ack @property - def trigger(self) -> datetime: - """This is the time to trigger the alarm.""" + def trigger(self) -> date: + """This is the time to trigger the alarm. + + If the alarm has been snoozed, this can differ from the TRIGGER property. + Use is_active_in() to avoid timezone issues. + """ return self._trigger @@ -81,6 +101,7 @@ def __init__(self, component:Optional[Alarm|Event|Todo]=None): self._start : Optional[date] = None self._end : Optional[date] = None self._parent : Optional[Parent] = None + self._last_ack : Optional[datetime] = None if component is not None: self.add_component(component) @@ -155,6 +176,7 @@ def acknowledge_until(self, dt: date) -> None: an event has been acknowledged because of an alarm. All alarms that happen before this time will be ackknowledged at this dt. """ + self._last_ack = tzp.localize_utc(dt) def snooze_until(self, dt: date) -> None: """This is the time when all the alarms of this component were snoozed. @@ -191,7 +213,7 @@ def _repeat(self, first: datetime, alarm: Alarm) -> Generator[datetime]: def _alarm_time(self, alarm: Alarm, trigger:date): """Create an alarm time with the additional attributes.""" - return AlarmTime(alarm, trigger, None, self._parent) + return AlarmTime(alarm, trigger, self._last_ack, self._parent) def _get_absolute_alarm_times(self) -> list[AlarmTime]: """Return a list of absolute alarm times.""" @@ -221,9 +243,16 @@ def _get_end_alarm_times(self) -> list[AlarmTime]: for trigger in self._repeat(self._add(self._end, alarm["TRIGGER"].dt), alarm) ] - @property - def active(self): - """The alarm times that are still active and not acknowledged.""" - return [alarm_time for alarm_time in self.times if alarm_time.is_active] + def active_in(self, timezone:Optional[tzinfo|str]=None) -> list[AlarmTime]: + """The alarm times that are still active and not acknowledged. + + This considers snoozed alarms. + + Alarms can be in local time (without a timezone). + To calculate if the alarm really happened, we need it to be in a timezone. + If a timezone is required but not given, we throw an IncompleteAlarmInformation. + """ + timezone = tzp.timezone(timezone) if isinstance(timezone, str) else timezone + return [alarm_time for alarm_time in self.times if alarm_time.is_active_in(timezone)] __all__ = ["Alarms", "AlarmTime", "IncompleteAlarmInformation"] diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py index 14134389..8b5db22c 100644 --- a/src/icalendar/tests/test_issue_716_alarm_time_computation.py +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -51,54 +51,6 @@ def test_absolute_alarm_time_with_vDatetime(alarm): assert times[0].trigger == EXAMPLE_TRIGGER -def test_repeat_absent(): - """Test the absence of REPEAT.""" - assert Alarm().REPEAT == 0 - - -def test_repeat_number(): - """Test the absence of REPEAT.""" - assert Alarm({"REPEAT": 10}).REPEAT == 10 - - -def test_set_REPEAT(): - """Check setting the value.""" - a = Alarm() - a.REPEAT = 10 - assert a.REPEAT == 10 - - -def test_set_REPEAT_twice(): - """Check setting the value.""" - a = Alarm() - a.REPEAT = 10 - a.REPEAT = 20 - assert a.REPEAT == 20 - - -def test_add_REPEAT(): - """Check setting the value.""" - a = Alarm() - a.add("REPEAT", 10) - assert a.REPEAT == 10 - - -def test_invalid_repeat_value(): - """Check setting the value.""" - a = Alarm() - with pytest.raises(ValueError): - a.REPEAT = "asd" - a["REPEAT"] = "asd" - with pytest.raises(InvalidCalendar): - a.REPEAT # noqa: B018 - - -def test_alarm_to_string(): - a = Alarm() - a.REPEAT = 11 - assert a.to_ical() == b"BEGIN:VALARM\r\nREPEAT:11\r\nEND:VALARM\r\n" - - def test_alarm_has_only_one_of_repeat_or_duration(): """This is an edge case and we should ignore the repetition.""" pytest.skip("TODO") @@ -234,12 +186,37 @@ def test_alarms_from_calendar(): ("alarm_etar_notification_clicked", -1, 1, "Etar: the notification was dismissed"), ("alarm_google_future", -1, 4, "Google: we just created the event with alarms"), ("alarm_google_acknowledged", -1, 2, "Google: 2 alarms happened at the same time"), - ("", -1, 1, ""), - ("", -1, 1, ""), ] ) -def test_active_alarms(calendars, calendar, index, count, message): +def test_number_of_active_alarms_from_calendar_software(calendars, calendar, index, count, message): """Check that we extract calculate the correct amount of active alarms.""" + pytest.skip("TODO") event = calendars[calendar].subcomponents[index] a = Alarms(event) - assert len(a.active) == count, f"{message} - I expect {count} alarms active but got {len(a.active)}." + active_alarms = a.active_in() # We do not need to pass a timezone because the events have a timezone + assert len(active_alarms) == count, f"{message} - I expect {count} alarms active but got {len(active_alarms)}." + + +three_alarms = Alarm() +three_alarms.REPEAT = 2 +three_alarms.add("DURATION", timedelta(hours=1)) # 2 hours & 1 hour before +three_alarms.add("TRIGGER", -timedelta(hours=3)) # 3 hours before + + +@pytest.mark.parametrize( + ("start", "acknowledged", "timezone", "count"), + [ + (datetime(2024, 10, 10), datetime(2024, 10, 9), "UTC", 3), + (datetime(2024, 10, 10, 12), datetime(2024, 10, 10, 9, 1), "UTC", 2), + (datetime(2024, 10, 10, 12), datetime(2024, 10, 10, 10, 1), "UTC", 1), + (datetime(2024, 10, 10, 12), datetime(2024, 10, 10, 11, 1), "UTC", 0), + ] +) +def test_number_of_active_alarms_with_moving_time(start, acknowledged, count, tzp, timezone): + """Check how many alarms are active after a time they are acknowledged.""" + a = Alarms() + a.add_alarm(three_alarms) + a.set_start(start) + a.acknowledge_until(tzp.localize_utc(acknowledged)) + active = a.active_in(timezone) + assert len(active) == count From 681c064dc8966db1a231939145ac7d8483cde3b4 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 23 Oct 2024 15:40:56 +0100 Subject: [PATCH 16/50] replace Thunderbird example --- ...uture.ics => alarm_thunderbird_closed.ics} | 24 +- ...uture.ics => alarm_thunderbird_future.ics} | 26 +- .../alarm_thunderbird_snoozed_until_1457.ics | 626 ++++++++++++++++++ 3 files changed, 644 insertions(+), 32 deletions(-) rename src/icalendar/tests/calendars/{alarm_thunderbird_closed_postponed_and_closed_future.ics => alarm_thunderbird_closed.ics} (93%) rename src/icalendar/tests/calendars/{alarm_thunderbird_closed_postponed_future.ics => alarm_thunderbird_future.ics} (92%) create mode 100644 src/icalendar/tests/calendars/alarm_thunderbird_snoozed_until_1457.ics diff --git a/src/icalendar/tests/calendars/alarm_thunderbird_closed_postponed_and_closed_future.ics b/src/icalendar/tests/calendars/alarm_thunderbird_closed.ics similarity index 93% rename from src/icalendar/tests/calendars/alarm_thunderbird_closed_postponed_and_closed_future.ics rename to src/icalendar/tests/calendars/alarm_thunderbird_closed.ics index 9e984408..524f069a 100644 --- a/src/icalendar/tests/calendars/alarm_thunderbird_closed_postponed_and_closed_future.ics +++ b/src/icalendar/tests/calendars/alarm_thunderbird_closed.ics @@ -601,30 +601,24 @@ RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT -CREATED:20241004T172045Z -LAST-MODIFIED:20241004T173612Z -DTSTAMP:20241004T173612Z -UID:5dbcd1f8-4aec-4e7d-a1db-4df8e330dd71 +CREATED:20241023T131035Z +LAST-MODIFIED:20241023T141941Z +DTSTAMP:20241023T141941Z +UID:b9a23b47-f109-4e7a-908c-75e925b27def SUMMARY:event with alarms -X-MOZ-LASTACK:20241004T173612Z -DTSTART;TZID=Europe/London:20241004T190000 -DTEND;TZID=Europe/London:20241004T200000 +X-MOZ-LASTACK:20241023T141941Z +DTSTART;TZID=Europe/London:20241023T150000 +DTEND;TZID=Europe/London:20241023T160000 TRANSP:OPAQUE X-MOZ-GENERATION:6 -SEQUENCE:1 BEGIN:VALARM ACTION:DISPLAY -TRIGGER:-PT30M +TRIGGER:-PT15M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM BEGIN:VALARM ACTION:DISPLAY -TRIGGER:-PT40M -DESCRIPTION:Mozilla Standardbeschreibung -END:VALARM -BEGIN:VALARM -ACTION:DISPLAY -TRIGGER:-PT18M +TRIGGER:-PT45M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM END:VEVENT diff --git a/src/icalendar/tests/calendars/alarm_thunderbird_closed_postponed_future.ics b/src/icalendar/tests/calendars/alarm_thunderbird_future.ics similarity index 92% rename from src/icalendar/tests/calendars/alarm_thunderbird_closed_postponed_future.ics rename to src/icalendar/tests/calendars/alarm_thunderbird_future.ics index 8fea4c81..5c491a6f 100644 --- a/src/icalendar/tests/calendars/alarm_thunderbird_closed_postponed_future.ics +++ b/src/icalendar/tests/calendars/alarm_thunderbird_future.ics @@ -601,31 +601,23 @@ RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT -CREATED:20241004T172045Z -LAST-MODIFIED:20241004T173006Z -DTSTAMP:20241004T173006Z -UID:5dbcd1f8-4aec-4e7d-a1db-4df8e330dd71 +CREATED:20241023T131035Z +LAST-MODIFIED:20241023T131141Z +DTSTAMP:20241023T131141Z +UID:b9a23b47-f109-4e7a-908c-75e925b27def SUMMARY:event with alarms -X-MOZ-LASTACK:20241004T173006Z -DTSTART;TZID=Europe/London:20241004T190000 -DTEND;TZID=Europe/London:20241004T200000 +DTSTART;TZID=Europe/London:20241023T150000 +DTEND;TZID=Europe/London:20241023T160000 TRANSP:OPAQUE -X-MOZ-GENERATION:5 -SEQUENCE:1 -X-MOZ-SNOOZE-TIME:20241004T173506Z +X-MOZ-GENERATION:2 BEGIN:VALARM ACTION:DISPLAY -TRIGGER:-PT30M +TRIGGER:-PT15M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM BEGIN:VALARM ACTION:DISPLAY -TRIGGER:-PT40M -DESCRIPTION:Mozilla Standardbeschreibung -END:VALARM -BEGIN:VALARM -ACTION:DISPLAY -TRIGGER:-PT18M +TRIGGER:-PT45M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM END:VEVENT diff --git a/src/icalendar/tests/calendars/alarm_thunderbird_snoozed_until_1457.ics b/src/icalendar/tests/calendars/alarm_thunderbird_snoozed_until_1457.ics new file mode 100644 index 00000000..89aa1f32 --- /dev/null +++ b/src/icalendar/tests/calendars/alarm_thunderbird_snoozed_until_1457.ics @@ -0,0 +1,626 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Europe/London +X-TZINFO:Europe/London[2024a] +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:-000115 +TZNAME:Europe/London(STD) +DTSTART:18471201T000000 +RDATE:18471201T000000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19160521T020000 +RDATE:19160521T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19161001T030000 +RDATE:19161001T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19170408T020000 +RDATE:19170408T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19170917T030000 +RDATE:19170917T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19180324T020000 +RDATE:19180324T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19180930T030000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19190330T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19201025T030000 +RDATE:19201025T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19210403T020000 +RDATE:19210403T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19211003T030000 +RDATE:19211003T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19220326T020000 +RDATE:19220326T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19221008T030000 +RDATE:19221008T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19230422T020000 +RDATE:19230422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19240413T020000 +RDATE:19240413T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19230916T030000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19250419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19270410T020000 +RDATE:19270410T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19280422T020000 +RDATE:19280422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19290421T020000 +RDATE:19290421T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19300413T020000 +RDATE:19300413T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19310419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19251004T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19330409T020000 +RDATE:19330409T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19331008T030000 +RDATE:19331008T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19340422T020000 +RDATE:19340422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19350414T020000 +RDATE:19350414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19360419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19380410T020000 +RDATE:19380410T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19341007T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19390416T020000 +RDATE:19390416T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19400225T020000 +RDATE:19400225T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19410504T020000 +RDATE:19410504T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19410810T030000 +RDATE:19410810T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19420405T020000 +RDATE:19420405T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19420809T030000 +RDATE:19420809T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19430404T020000 +RDATE:19430404T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19430815T030000 +RDATE:19430815T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19440402T020000 +RDATE:19440402T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19440917T030000 +RDATE:19440917T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19450402T020000 +RDATE:19450402T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19391119T030000 +RDATE:19391119T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19450715T030000 +RDATE:19450715T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19460414T020000 +RDATE:19460414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19470316T020000 +RDATE:19470316T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19470413T020000 +RDATE:19470413T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19451007T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19470810T030000 +RDATE:19470810T030000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19471102T030000 +RDATE:19471102T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19480314T020000 +RDATE:19480314T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19490403T020000 +RDATE:19490403T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19481031T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19501022T030000 +RDATE:19501022T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19511021T030000 +RDATE:19511021T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19521026T030000 +RDATE:19521026T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19500416T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19540411T020000 +RDATE:19540411T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19550417T020000 +RDATE:19550417T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19560422T020000 +RDATE:19560422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19570414T020000 +RDATE:19570414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19580420T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19600410T020000 +RDATE:19600410T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19531004T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19610326T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19640322T020000 +RDATE:19640322T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19611029T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19651024T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19650321T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19671029T030000 +RDATE:19671029T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+010000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19681027T000000 +RDATE:19681027T000000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19680218T020000 +RDATE:19680218T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19711031T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19761024T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19720319T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19781029T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19811025T020000 +RDATE:19811025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19821024T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19841028T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19881023T020000 +RDATE:19881023T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19891029T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19931024T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19810329T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19961027T020000 +RDATE:19961027T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:(DST) +DTSTART:19970330T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:(STD) +DTSTART:19971026T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20241023T131035Z +LAST-MODIFIED:20241023T135202Z +DTSTAMP:20241023T135202Z +UID:b9a23b47-f109-4e7a-908c-75e925b27def +SUMMARY:event with alarms +X-MOZ-LASTACK:20241023T135202Z +DTSTART;TZID=Europe/London:20241023T150000 +DTEND;TZID=Europe/London:20241023T160000 +TRANSP:OPAQUE +X-MOZ-GENERATION:4 +X-MOZ-SNOOZE-TIME:20241023T135702Z +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT15M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT45M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +END:VEVENT +END:VCALENDAR From 610a96f7cccdb0e38bd6de10dae836427ffcd29d Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 23 Oct 2024 15:48:40 +0100 Subject: [PATCH 17/50] Check optional timezone argument error --- src/icalendar/alarms.py | 2 ++ .../tests/test_issue_716_alarm_time_computation.py | 14 +++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index ed183770..ae069574 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -72,6 +72,8 @@ def is_active_in(self, timezone:Optional[tzinfo]=None) -> bool: # if nothing is acknowledged, this alarm counts return True trigger = self.trigger if timezone is None else tzp.localize(self.trigger, timezone) + if trigger.tzinfo is None: + raise IncompleteAlarmInformation("A timezone is required to check if the alarm is still active.") return trigger > self._last_ack @property diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py index 8b5db22c..349882e5 100644 --- a/src/icalendar/tests/test_issue_716_alarm_time_computation.py +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -1,6 +1,6 @@ """Test the alarm time computation.""" -from datetime import date, datetime, timedelta, timezone +from datetime import date, datetime, timedelta, timezone, tzinfo import pytest @@ -210,6 +210,7 @@ def test_number_of_active_alarms_from_calendar_software(calendars, calendar, ind (datetime(2024, 10, 10, 12), datetime(2024, 10, 10, 9, 1), "UTC", 2), (datetime(2024, 10, 10, 12), datetime(2024, 10, 10, 10, 1), "UTC", 1), (datetime(2024, 10, 10, 12), datetime(2024, 10, 10, 11, 1), "UTC", 0), + (datetime(2024, 10, 10, 12, tzinfo=timezone.utc), datetime(2024, 10, 10, 11, 1), None, 0), ] ) def test_number_of_active_alarms_with_moving_time(start, acknowledged, count, tzp, timezone): @@ -220,3 +221,14 @@ def test_number_of_active_alarms_with_moving_time(start, acknowledged, count, tz a.acknowledge_until(tzp.localize_utc(acknowledged)) active = a.active_in(timezone) assert len(active) == count + + +def test_incomplete_alarm_information_for_active_state(tzp): + """Make sure we throw the right error.""" + a = Alarms() + a.add_alarm(three_alarms) + a.set_start(date(2017, 12, 1)) + a.acknowledge_until(tzp.localize_utc(datetime(2012, 10, 10, 12))) + with pytest.raises(IncompleteAlarmInformation) as e: + a.active_in() + assert e.value.args[0] == "A timezone is required to check if the alarm is still active." From 463a5f09bd3db403504c591272e9599f76356bcf Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 23 Oct 2024 18:53:54 +0100 Subject: [PATCH 18/50] Implement alarm algorithm of Thunderbird --- src/icalendar/alarms.py | 40 +- src/icalendar/cal.py | 28 +- .../calendars/alarm_thunderbird_2_future.ics | 624 +++++++++++++++++ ...derbird_2_notification_5_min_postponed.ics | 626 ++++++++++++++++++ ...otification_5_min_postponed_and_closed.ics | 625 +++++++++++++++++ ...fication_5_min_postponed_and_popped_up.ics | 626 ++++++++++++++++++ ...m_thunderbird_2_notification_popped_up.ics | 624 +++++++++++++++++ .../test_issue_716_alarm_time_computation.py | 38 +- 8 files changed, 3215 insertions(+), 16 deletions(-) create mode 100644 src/icalendar/tests/calendars/alarm_thunderbird_2_future.ics create mode 100644 src/icalendar/tests/calendars/alarm_thunderbird_2_notification_5_min_postponed.ics create mode 100644 src/icalendar/tests/calendars/alarm_thunderbird_2_notification_5_min_postponed_and_closed.ics create mode 100644 src/icalendar/tests/calendars/alarm_thunderbird_2_notification_5_min_postponed_and_popped_up.ics create mode 100644 src/icalendar/tests/calendars/alarm_thunderbird_2_notification_popped_up.ics diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index ae069574..8d61457f 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -3,7 +3,7 @@ This takes different calendar software into account and RFC 9074 (Alarm Extension). - Outlook does not export VALARM information -- Google uses the DTSTAMP to snooze the alarms +- Google Calendar uses the DTSTAMP to acknowledge the alarms - Thunderbird snoozes the alarms with a X-MOZ-SNOOZE-TIME attribute in the event - Thunderbird acknowledges the alarms with a X-MOZ-LASTACK attribute in the event - Etar deletes alarms that are not active any more @@ -15,8 +15,8 @@ from typing import TYPE_CHECKING, Generator, Optional, Union from icalendar.cal import Alarm, Event, Todo -from icalendar.tools import is_date, normalize_pytz, to_datetime from icalendar.timezone import tzp +from icalendar.tools import is_date, normalize_pytz, to_datetime if TYPE_CHECKING: from datetime import datetime @@ -31,19 +31,29 @@ class IncompleteAlarmInformation(ValueError): class AlarmTime: """An alarm time with all the information.""" - def __init__(self, alarm: Alarm, trigger : datetime, acknowledged_until:Optional[datetime], parent: Optional[Parent]): + def __init__( + self, + alarm: Alarm, + trigger : datetime, + acknowledged_until:Optional[datetime]=None, + snoozed_until:Optional[datetime]=None, + parent: Optional[Parent]=None + ): """Create a new AlarmTime. alarm - the Alarm component trigger - a date or datetime at which to trigger the alarm acknowledged_until - an optional datetime in UTC until when all alarms have been acknowledged + snoozed_until - an optional datetime in UTC until which all alarms of + the same parent are snoozed parent - the optional parent component the alarm refers to """ self._alarm = alarm self._parent = parent self._trigger = trigger self._last_ack = acknowledged_until + self._snooze_until = snoozed_until @property def alarm(self) -> Alarm: @@ -74,6 +84,9 @@ def is_active_in(self, timezone:Optional[tzinfo]=None) -> bool: trigger = self.trigger if timezone is None else tzp.localize(self.trigger, timezone) if trigger.tzinfo is None: raise IncompleteAlarmInformation("A timezone is required to check if the alarm is still active.") + if self._snooze_until is not None and self._snooze_until > self._last_ack: + return True + print(f"trigger == {trigger} > {self._last_ack} == last ack") return trigger > self._last_ack @property @@ -104,6 +117,7 @@ def __init__(self, component:Optional[Alarm|Event|Todo]=None): self._end : Optional[date] = None self._parent : Optional[Parent] = None self._last_ack : Optional[datetime] = None + self._snooze_until : Optional[datetime] = None if component is not None: self.add_component(component) @@ -119,6 +133,13 @@ def add_component(self, component:Alarm|Parent): self.set_parent(component) self.set_start(component.start) self.set_end(component.end) + if component.is_thunderbird(): + print("component.DTSTAMP", component.DTSTAMP) + self.acknowledge_until(component.X_MOZ_LASTACK) + self.snooze_until(component.X_MOZ_SNOOZE_TIME) + else: + self.acknowledge_until(component.DTSTAMP) + for alarm in component.walk("VALARM"): self.add_alarm(alarm) @@ -167,7 +188,7 @@ def _add(self, dt: date, td:timedelta): dt = to_datetime(dt) return normalize_pytz(dt + td) - def acknowledge_until(self, dt: date) -> None: + def acknowledge_until(self, dt: Optional[date]) -> None: """This is the time when all the alarms of this component were acknowledged. You can set several times like this. Only the latest one counts. @@ -178,15 +199,20 @@ def acknowledge_until(self, dt: date) -> None: an event has been acknowledged because of an alarm. All alarms that happen before this time will be ackknowledged at this dt. """ - self._last_ack = tzp.localize_utc(dt) + print("acknowledge_until", dt) + if dt is not None: + self._last_ack = tzp.localize_utc(dt) - def snooze_until(self, dt: date) -> None: + def snooze_until(self, dt: Optional[date]) -> None: """This is the time when all the alarms of this component were snoozed. You can set several times like this. Only the latest one counts. The alarms are supposed to turn up again at dt when they are not acknowledged but snoozed. """ + print("snooze_until ", dt) + if dt is not None: + self._snooze_until = tzp.localize_utc(dt) @property def times(self) -> list[AlarmTime]: @@ -215,7 +241,7 @@ def _repeat(self, first: datetime, alarm: Alarm) -> Generator[datetime]: def _alarm_time(self, alarm: Alarm, trigger:date): """Create an alarm time with the additional attributes.""" - return AlarmTime(alarm, trigger, self._last_ack, self._parent) + return AlarmTime(alarm, trigger, self._last_ack, self._snooze_until, self._parent) def _get_absolute_alarm_times(self) -> list[AlarmTime]: """Return a list of absolute alarm times.""" diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 4758ae1f..8240ad11 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -82,7 +82,7 @@ class IncompleteComponent(ValueError): only in the lowercase computations like .start. """ -def create_utc_property(name:str, docs:str): +def create_utc_property(name:str, docs:str) -> property: """Create a property to access a value of datetime in UTC timezone. name - name of the property @@ -98,7 +98,11 @@ def p_get(self: Component) -> Optional[datetime]: if name not in self: return None dt = self.get(name) - value = getattr(dt, "dt", None) + if isinstance(dt, vText): + # we might be in an attribute that is not typed + value = vDDDTypes.from_ical(dt) + else: + value = getattr(dt, "dt", None) if value is None or not isinstance(value, date): raise InvalidCalendar(f"{name} must be a datetime in UTC, not {value}") return tzp.localize_utc(value) @@ -559,6 +563,11 @@ def __eq__(self, other): "VTODO", "VJOURNAL", or "VTIMEZONE" calendar components. """) + def is_thunderbird(self) -> bool: + """Whether this component has attributes that indicate that Mozilla Thunderbird createsd it.""" + return any(attr.startswith("X-MOZ-") for attr in self.keys()) + + ####################################### # components defined in RFC 5545 @@ -606,6 +615,15 @@ def p_del(self:Component): return property(p_get, p_set, p_del, p_doc) +_X_MOZ_SNOOZE_TIME = create_utc_property( + "X-MOZ-SNOOZE-TIME", + "Thunderbird: Alarms before this time are snoozed." +) +_X_MOZ_LASTACK = create_utc_property( + "X-MOZ-LASTACK", + "Thunderbird: Alarms before this time are acknowledged." +) + class Event(Component): name = 'VEVENT' @@ -749,6 +767,9 @@ def end(self, end: date | datetime | None): """Set the end.""" self.DTEND = end + X_MOZ_SNOOZE_TIME = _X_MOZ_SNOOZE_TIME + X_MOZ_LASTACK = _X_MOZ_LASTACK + class Todo(Component): @@ -767,6 +788,8 @@ class Todo(Component): 'RSTATUS', 'RELATED', 'RESOURCES', 'RDATE', 'RRULE' ) + X_MOZ_SNOOZE_TIME = _X_MOZ_SNOOZE_TIME + X_MOZ_LASTACK = _X_MOZ_LASTACK class Journal(Component): """A descriptive text at a certain time or associated with a component. @@ -1038,7 +1061,6 @@ def REPEAT(self, value: int) -> None: DURATION = Event.DURATION # TODO: adjust once https://github.com/collective/icalendar/pull/733 is merged - class Calendar(Component): """This is the base object for an iCalendar file. """ diff --git a/src/icalendar/tests/calendars/alarm_thunderbird_2_future.ics b/src/icalendar/tests/calendars/alarm_thunderbird_2_future.ics new file mode 100644 index 00000000..6d25d536 --- /dev/null +++ b/src/icalendar/tests/calendars/alarm_thunderbird_2_future.ics @@ -0,0 +1,624 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Europe/London +X-TZINFO:Europe/London[2024a] +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:-000115 +TZNAME:Europe/London(STD) +DTSTART:18471201T000000 +RDATE:18471201T000000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19160521T020000 +RDATE:19160521T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19161001T030000 +RDATE:19161001T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19170408T020000 +RDATE:19170408T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19170917T030000 +RDATE:19170917T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19180324T020000 +RDATE:19180324T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19180930T030000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19190330T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19201025T030000 +RDATE:19201025T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19210403T020000 +RDATE:19210403T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19211003T030000 +RDATE:19211003T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19220326T020000 +RDATE:19220326T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19221008T030000 +RDATE:19221008T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19230422T020000 +RDATE:19230422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19240413T020000 +RDATE:19240413T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19230916T030000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19250419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19270410T020000 +RDATE:19270410T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19280422T020000 +RDATE:19280422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19290421T020000 +RDATE:19290421T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19300413T020000 +RDATE:19300413T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19310419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19251004T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19330409T020000 +RDATE:19330409T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19331008T030000 +RDATE:19331008T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19340422T020000 +RDATE:19340422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19350414T020000 +RDATE:19350414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19360419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19380410T020000 +RDATE:19380410T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19341007T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19390416T020000 +RDATE:19390416T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19400225T020000 +RDATE:19400225T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19410504T020000 +RDATE:19410504T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19410810T030000 +RDATE:19410810T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19420405T020000 +RDATE:19420405T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19420809T030000 +RDATE:19420809T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19430404T020000 +RDATE:19430404T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19430815T030000 +RDATE:19430815T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19440402T020000 +RDATE:19440402T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19440917T030000 +RDATE:19440917T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19450402T020000 +RDATE:19450402T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19391119T030000 +RDATE:19391119T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19450715T030000 +RDATE:19450715T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19460414T020000 +RDATE:19460414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19470316T020000 +RDATE:19470316T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19470413T020000 +RDATE:19470413T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19451007T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19470810T030000 +RDATE:19470810T030000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19471102T030000 +RDATE:19471102T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19480314T020000 +RDATE:19480314T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19490403T020000 +RDATE:19490403T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19481031T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19501022T030000 +RDATE:19501022T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19511021T030000 +RDATE:19511021T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19521026T030000 +RDATE:19521026T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19500416T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19540411T020000 +RDATE:19540411T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19550417T020000 +RDATE:19550417T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19560422T020000 +RDATE:19560422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19570414T020000 +RDATE:19570414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19580420T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19600410T020000 +RDATE:19600410T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19531004T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19610326T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19640322T020000 +RDATE:19640322T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19611029T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19651024T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19650321T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19671029T030000 +RDATE:19671029T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+010000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19681027T000000 +RDATE:19681027T000000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19680218T020000 +RDATE:19680218T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19711031T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19761024T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19720319T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19781029T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19811025T020000 +RDATE:19811025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19821024T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19841028T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19881023T020000 +RDATE:19881023T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19891029T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19931024T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19810329T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19961027T020000 +RDATE:19961027T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:(DST) +DTSTART:19970330T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:(STD) +DTSTART:19971026T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20241023T173412Z +LAST-MODIFIED:20241023T173453Z +DTSTAMP:20241023T173453Z +UID:731b9b91-cf72-499b-bbc9-c53c28e21fc7 +SUMMARY:event +DTSTART;TZID=Europe/London:20241023T190000 +DTEND;TZID=Europe/London:20241023T200000 +TRANSP:OPAQUE +X-MOZ-GENERATION:2 +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT1M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT24M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/alarm_thunderbird_2_notification_5_min_postponed.ics b/src/icalendar/tests/calendars/alarm_thunderbird_2_notification_5_min_postponed.ics new file mode 100644 index 00000000..851a8db0 --- /dev/null +++ b/src/icalendar/tests/calendars/alarm_thunderbird_2_notification_5_min_postponed.ics @@ -0,0 +1,626 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Europe/London +X-TZINFO:Europe/London[2024a] +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:-000115 +TZNAME:Europe/London(STD) +DTSTART:18471201T000000 +RDATE:18471201T000000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19160521T020000 +RDATE:19160521T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19161001T030000 +RDATE:19161001T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19170408T020000 +RDATE:19170408T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19170917T030000 +RDATE:19170917T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19180324T020000 +RDATE:19180324T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19180930T030000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19190330T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19201025T030000 +RDATE:19201025T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19210403T020000 +RDATE:19210403T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19211003T030000 +RDATE:19211003T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19220326T020000 +RDATE:19220326T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19221008T030000 +RDATE:19221008T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19230422T020000 +RDATE:19230422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19240413T020000 +RDATE:19240413T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19230916T030000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19250419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19270410T020000 +RDATE:19270410T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19280422T020000 +RDATE:19280422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19290421T020000 +RDATE:19290421T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19300413T020000 +RDATE:19300413T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19310419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19251004T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19330409T020000 +RDATE:19330409T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19331008T030000 +RDATE:19331008T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19340422T020000 +RDATE:19340422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19350414T020000 +RDATE:19350414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19360419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19380410T020000 +RDATE:19380410T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19341007T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19390416T020000 +RDATE:19390416T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19400225T020000 +RDATE:19400225T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19410504T020000 +RDATE:19410504T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19410810T030000 +RDATE:19410810T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19420405T020000 +RDATE:19420405T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19420809T030000 +RDATE:19420809T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19430404T020000 +RDATE:19430404T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19430815T030000 +RDATE:19430815T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19440402T020000 +RDATE:19440402T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19440917T030000 +RDATE:19440917T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19450402T020000 +RDATE:19450402T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19391119T030000 +RDATE:19391119T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19450715T030000 +RDATE:19450715T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19460414T020000 +RDATE:19460414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19470316T020000 +RDATE:19470316T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19470413T020000 +RDATE:19470413T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19451007T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19470810T030000 +RDATE:19470810T030000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19471102T030000 +RDATE:19471102T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19480314T020000 +RDATE:19480314T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19490403T020000 +RDATE:19490403T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19481031T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19501022T030000 +RDATE:19501022T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19511021T030000 +RDATE:19511021T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19521026T030000 +RDATE:19521026T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19500416T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19540411T020000 +RDATE:19540411T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19550417T020000 +RDATE:19550417T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19560422T020000 +RDATE:19560422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19570414T020000 +RDATE:19570414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19580420T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19600410T020000 +RDATE:19600410T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19531004T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19610326T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19640322T020000 +RDATE:19640322T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19611029T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19651024T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19650321T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19671029T030000 +RDATE:19671029T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+010000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19681027T000000 +RDATE:19681027T000000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19680218T020000 +RDATE:19680218T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19711031T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19761024T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19720319T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19781029T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19811025T020000 +RDATE:19811025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19821024T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19841028T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19881023T020000 +RDATE:19881023T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19891029T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19931024T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19810329T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19961027T020000 +RDATE:19961027T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:(DST) +DTSTART:19970330T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:(STD) +DTSTART:19971026T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20241023T173412Z +LAST-MODIFIED:20241023T173630Z +DTSTAMP:20241023T173630Z +UID:731b9b91-cf72-499b-bbc9-c53c28e21fc7 +SUMMARY:event +X-MOZ-LASTACK:20241023T173630Z +DTSTART;TZID=Europe/London:20241023T190000 +DTEND;TZID=Europe/London:20241023T200000 +TRANSP:OPAQUE +X-MOZ-GENERATION:3 +X-MOZ-SNOOZE-TIME:20241023T174130Z +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT1M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT24M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/alarm_thunderbird_2_notification_5_min_postponed_and_closed.ics b/src/icalendar/tests/calendars/alarm_thunderbird_2_notification_5_min_postponed_and_closed.ics new file mode 100644 index 00000000..cfa7035b --- /dev/null +++ b/src/icalendar/tests/calendars/alarm_thunderbird_2_notification_5_min_postponed_and_closed.ics @@ -0,0 +1,625 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Europe/London +X-TZINFO:Europe/London[2024a] +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:-000115 +TZNAME:Europe/London(STD) +DTSTART:18471201T000000 +RDATE:18471201T000000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19160521T020000 +RDATE:19160521T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19161001T030000 +RDATE:19161001T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19170408T020000 +RDATE:19170408T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19170917T030000 +RDATE:19170917T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19180324T020000 +RDATE:19180324T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19180930T030000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19190330T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19201025T030000 +RDATE:19201025T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19210403T020000 +RDATE:19210403T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19211003T030000 +RDATE:19211003T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19220326T020000 +RDATE:19220326T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19221008T030000 +RDATE:19221008T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19230422T020000 +RDATE:19230422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19240413T020000 +RDATE:19240413T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19230916T030000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19250419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19270410T020000 +RDATE:19270410T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19280422T020000 +RDATE:19280422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19290421T020000 +RDATE:19290421T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19300413T020000 +RDATE:19300413T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19310419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19251004T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19330409T020000 +RDATE:19330409T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19331008T030000 +RDATE:19331008T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19340422T020000 +RDATE:19340422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19350414T020000 +RDATE:19350414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19360419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19380410T020000 +RDATE:19380410T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19341007T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19390416T020000 +RDATE:19390416T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19400225T020000 +RDATE:19400225T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19410504T020000 +RDATE:19410504T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19410810T030000 +RDATE:19410810T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19420405T020000 +RDATE:19420405T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19420809T030000 +RDATE:19420809T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19430404T020000 +RDATE:19430404T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19430815T030000 +RDATE:19430815T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19440402T020000 +RDATE:19440402T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19440917T030000 +RDATE:19440917T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19450402T020000 +RDATE:19450402T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19391119T030000 +RDATE:19391119T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19450715T030000 +RDATE:19450715T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19460414T020000 +RDATE:19460414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19470316T020000 +RDATE:19470316T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19470413T020000 +RDATE:19470413T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19451007T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19470810T030000 +RDATE:19470810T030000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19471102T030000 +RDATE:19471102T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19480314T020000 +RDATE:19480314T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19490403T020000 +RDATE:19490403T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19481031T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19501022T030000 +RDATE:19501022T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19511021T030000 +RDATE:19511021T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19521026T030000 +RDATE:19521026T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19500416T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19540411T020000 +RDATE:19540411T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19550417T020000 +RDATE:19550417T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19560422T020000 +RDATE:19560422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19570414T020000 +RDATE:19570414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19580420T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19600410T020000 +RDATE:19600410T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19531004T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19610326T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19640322T020000 +RDATE:19640322T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19611029T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19651024T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19650321T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19671029T030000 +RDATE:19671029T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+010000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19681027T000000 +RDATE:19681027T000000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19680218T020000 +RDATE:19680218T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19711031T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19761024T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19720319T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19781029T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19811025T020000 +RDATE:19811025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19821024T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19841028T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19881023T020000 +RDATE:19881023T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19891029T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19931024T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19810329T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19961027T020000 +RDATE:19961027T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:(DST) +DTSTART:19970330T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:(STD) +DTSTART:19971026T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20241023T173412Z +LAST-MODIFIED:20241023T174207Z +DTSTAMP:20241023T174207Z +UID:731b9b91-cf72-499b-bbc9-c53c28e21fc7 +SUMMARY:event +X-MOZ-LASTACK:20241023T174207Z +DTSTART;TZID=Europe/London:20241023T190000 +DTEND;TZID=Europe/London:20241023T200000 +TRANSP:OPAQUE +X-MOZ-GENERATION:4 +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT1M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT24M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/alarm_thunderbird_2_notification_5_min_postponed_and_popped_up.ics b/src/icalendar/tests/calendars/alarm_thunderbird_2_notification_5_min_postponed_and_popped_up.ics new file mode 100644 index 00000000..851a8db0 --- /dev/null +++ b/src/icalendar/tests/calendars/alarm_thunderbird_2_notification_5_min_postponed_and_popped_up.ics @@ -0,0 +1,626 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Europe/London +X-TZINFO:Europe/London[2024a] +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:-000115 +TZNAME:Europe/London(STD) +DTSTART:18471201T000000 +RDATE:18471201T000000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19160521T020000 +RDATE:19160521T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19161001T030000 +RDATE:19161001T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19170408T020000 +RDATE:19170408T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19170917T030000 +RDATE:19170917T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19180324T020000 +RDATE:19180324T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19180930T030000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19190330T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19201025T030000 +RDATE:19201025T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19210403T020000 +RDATE:19210403T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19211003T030000 +RDATE:19211003T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19220326T020000 +RDATE:19220326T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19221008T030000 +RDATE:19221008T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19230422T020000 +RDATE:19230422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19240413T020000 +RDATE:19240413T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19230916T030000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19250419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19270410T020000 +RDATE:19270410T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19280422T020000 +RDATE:19280422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19290421T020000 +RDATE:19290421T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19300413T020000 +RDATE:19300413T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19310419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19251004T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19330409T020000 +RDATE:19330409T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19331008T030000 +RDATE:19331008T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19340422T020000 +RDATE:19340422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19350414T020000 +RDATE:19350414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19360419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19380410T020000 +RDATE:19380410T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19341007T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19390416T020000 +RDATE:19390416T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19400225T020000 +RDATE:19400225T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19410504T020000 +RDATE:19410504T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19410810T030000 +RDATE:19410810T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19420405T020000 +RDATE:19420405T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19420809T030000 +RDATE:19420809T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19430404T020000 +RDATE:19430404T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19430815T030000 +RDATE:19430815T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19440402T020000 +RDATE:19440402T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19440917T030000 +RDATE:19440917T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19450402T020000 +RDATE:19450402T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19391119T030000 +RDATE:19391119T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19450715T030000 +RDATE:19450715T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19460414T020000 +RDATE:19460414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19470316T020000 +RDATE:19470316T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19470413T020000 +RDATE:19470413T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19451007T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19470810T030000 +RDATE:19470810T030000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19471102T030000 +RDATE:19471102T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19480314T020000 +RDATE:19480314T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19490403T020000 +RDATE:19490403T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19481031T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19501022T030000 +RDATE:19501022T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19511021T030000 +RDATE:19511021T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19521026T030000 +RDATE:19521026T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19500416T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19540411T020000 +RDATE:19540411T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19550417T020000 +RDATE:19550417T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19560422T020000 +RDATE:19560422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19570414T020000 +RDATE:19570414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19580420T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19600410T020000 +RDATE:19600410T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19531004T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19610326T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19640322T020000 +RDATE:19640322T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19611029T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19651024T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19650321T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19671029T030000 +RDATE:19671029T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+010000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19681027T000000 +RDATE:19681027T000000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19680218T020000 +RDATE:19680218T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19711031T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19761024T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19720319T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19781029T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19811025T020000 +RDATE:19811025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19821024T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19841028T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19881023T020000 +RDATE:19881023T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19891029T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19931024T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19810329T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19961027T020000 +RDATE:19961027T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:(DST) +DTSTART:19970330T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:(STD) +DTSTART:19971026T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20241023T173412Z +LAST-MODIFIED:20241023T173630Z +DTSTAMP:20241023T173630Z +UID:731b9b91-cf72-499b-bbc9-c53c28e21fc7 +SUMMARY:event +X-MOZ-LASTACK:20241023T173630Z +DTSTART;TZID=Europe/London:20241023T190000 +DTEND;TZID=Europe/London:20241023T200000 +TRANSP:OPAQUE +X-MOZ-GENERATION:3 +X-MOZ-SNOOZE-TIME:20241023T174130Z +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT1M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT24M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/alarm_thunderbird_2_notification_popped_up.ics b/src/icalendar/tests/calendars/alarm_thunderbird_2_notification_popped_up.ics new file mode 100644 index 00000000..6d25d536 --- /dev/null +++ b/src/icalendar/tests/calendars/alarm_thunderbird_2_notification_popped_up.ics @@ -0,0 +1,624 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Europe/London +X-TZINFO:Europe/London[2024a] +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:-000115 +TZNAME:Europe/London(STD) +DTSTART:18471201T000000 +RDATE:18471201T000000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19160521T020000 +RDATE:19160521T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19161001T030000 +RDATE:19161001T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19170408T020000 +RDATE:19170408T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19170917T030000 +RDATE:19170917T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19180324T020000 +RDATE:19180324T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19180930T030000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19190330T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19201025T030000 +RDATE:19201025T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19210403T020000 +RDATE:19210403T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19211003T030000 +RDATE:19211003T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19220326T020000 +RDATE:19220326T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19221008T030000 +RDATE:19221008T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19230422T020000 +RDATE:19230422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19240413T020000 +RDATE:19240413T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19230916T030000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19250419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19270410T020000 +RDATE:19270410T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19280422T020000 +RDATE:19280422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19290421T020000 +RDATE:19290421T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19300413T020000 +RDATE:19300413T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19310419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19251004T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19330409T020000 +RDATE:19330409T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19331008T030000 +RDATE:19331008T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19340422T020000 +RDATE:19340422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19350414T020000 +RDATE:19350414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19360419T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19380410T020000 +RDATE:19380410T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19341007T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19390416T020000 +RDATE:19390416T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19400225T020000 +RDATE:19400225T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19410504T020000 +RDATE:19410504T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19410810T030000 +RDATE:19410810T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19420405T020000 +RDATE:19420405T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19420809T030000 +RDATE:19420809T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19430404T020000 +RDATE:19430404T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19430815T030000 +RDATE:19430815T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19440402T020000 +RDATE:19440402T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19440917T030000 +RDATE:19440917T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19450402T020000 +RDATE:19450402T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19391119T030000 +RDATE:19391119T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19450715T030000 +RDATE:19450715T030000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19460414T020000 +RDATE:19460414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19470316T020000 +RDATE:19470316T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+020000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(DST) +DTSTART:19470413T020000 +RDATE:19470413T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19451007T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+020000 +TZNAME:Europe/London(DST) +DTSTART:19470810T030000 +RDATE:19470810T030000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19471102T030000 +RDATE:19471102T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19480314T020000 +RDATE:19480314T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19490403T020000 +RDATE:19490403T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19481031T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19501022T030000 +RDATE:19501022T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19511021T030000 +RDATE:19511021T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19521026T030000 +RDATE:19521026T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19500416T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19540411T020000 +RDATE:19540411T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19550417T020000 +RDATE:19550417T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19560422T020000 +RDATE:19560422T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19570414T020000 +RDATE:19570414T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19580420T020000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19600410T020000 +RDATE:19600410T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19531004T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19610326T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19640322T020000 +RDATE:19640322T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19611029T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19651024T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19650321T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19671029T030000 +RDATE:19671029T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+010000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19681027T000000 +RDATE:19681027T000000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19680218T020000 +RDATE:19680218T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19711031T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19761024T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19720319T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19781029T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19811025T020000 +RDATE:19811025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19821024T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19841028T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19881023T020000 +RDATE:19881023T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19891029T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19931024T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:Europe/London(DST) +DTSTART:19810329T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:Europe/London(STD) +DTSTART:19961027T020000 +RDATE:19961027T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETTO:+010000 +TZOFFSETFROM:+000000 +TZNAME:(DST) +DTSTART:19970330T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETTO:+000000 +TZOFFSETFROM:+010000 +TZNAME:(STD) +DTSTART:19971026T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20241023T173412Z +LAST-MODIFIED:20241023T173453Z +DTSTAMP:20241023T173453Z +UID:731b9b91-cf72-499b-bbc9-c53c28e21fc7 +SUMMARY:event +DTSTART;TZID=Europe/London:20241023T190000 +DTEND;TZID=Europe/London:20241023T200000 +TRANSP:OPAQUE +X-MOZ-GENERATION:2 +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT1M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT24M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py index 349882e5..9fce9be0 100644 --- a/src/icalendar/tests/test_issue_716_alarm_time_computation.py +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -2,6 +2,7 @@ from datetime import date, datetime, timedelta, timezone, tzinfo +from icalendar import Event import pytest from icalendar.alarms import Alarms, IncompleteAlarmInformation @@ -181,16 +182,23 @@ def test_alarms_from_calendar(): @pytest.mark.parametrize( ("calendar", "index", "count", "message"), [ - ("alarm_etar_future", -1, 3, "Etar: we just created the alarm"), - ("alarm_etar_notification", -1, 3, "Etar: the notification popped up"), - ("alarm_etar_notification_clicked", -1, 1, "Etar: the notification was dismissed"), - ("alarm_google_future", -1, 4, "Google: we just created the event with alarms"), - ("alarm_google_acknowledged", -1, 2, "Google: 2 alarms happened at the same time"), + ("alarm_etar_future", -1, 3, "Etar (1): we just created the alarm"), + ("alarm_etar_notification", -1, 2, "Etar (2): the notification popped up"), + ("alarm_etar_notification_clicked", -1, 0, "Etar (3): the notification was dismissed"), # TODO: check that that is really true + ("alarm_google_future", -1, 4, "Google (1): we just created the event with alarms"), + ("alarm_google_acknowledged", -1, 2, "Google (2): 2 alarms happened at the same time"), + ("alarm_thunderbird_future", -1, 2, "Thunderbird (1.1): 2 alarms are set"), + ("alarm_thunderbird_snoozed_until_1457", -1, 2, "Thunderbird (1.2): 2 alarms are snoozed to another time"), + ("alarm_thunderbird_closed", -1, 0, "Thunderbird (1.3): all alarms are dismissed (closed)"), + ("alarm_thunderbird_2_future", -1, 2, "Thunderbird (2.1): 2 alarms active"), + ("alarm_thunderbird_2_notification_popped_up", -1, 2, "Thunderbird (2.2): one alarm popped up as a notification"), + ("alarm_thunderbird_2_notification_5_min_postponed", -1, 2, "Thunderbird (2.3): 1 alarm active and one postponed by 5 minutes"), + ("alarm_thunderbird_2_notification_5_min_postponed_and_popped_up", -1, 2, "Thunderbird (2.4): 1 alarm active and one postponed by 5 minutes and now popped up"), + ("alarm_thunderbird_2_notification_5_min_postponed_and_closed", -1, 1, "Thunderbird (2.5): 1 alarm active and one postponed by 5 minutes and is now acknowledged"), ] ) def test_number_of_active_alarms_from_calendar_software(calendars, calendar, index, count, message): """Check that we extract calculate the correct amount of active alarms.""" - pytest.skip("TODO") event = calendars[calendar].subcomponents[index] a = Alarms(event) active_alarms = a.active_in() # We do not need to pass a timezone because the events have a timezone @@ -232,3 +240,21 @@ def test_incomplete_alarm_information_for_active_state(tzp): with pytest.raises(IncompleteAlarmInformation) as e: a.active_in() assert e.value.args[0] == "A timezone is required to check if the alarm is still active." + + +@pytest.mark.parametrize( + "calendar_name", + [ + "alarm_etar_future", + "alarm_google_acknowledged", + "alarm_thunderbird_closed", + "alarm_thunderbird_future", + "alarm_thunderbird_snoozed_until_1457", + ] +) +def test_thunderbird_recognition(calendars, calendar_name): + """Check if we correctly discover Thunderbird's alarm algorithm.""" + calendar = calendars[calendar_name] + event = calendar.subcomponents[-1] + assert isinstance(event, Event) + assert event.is_thunderbird() == ("thunderbird" in calendar_name) From ebbf54b4f1d6b716b43833c213910ce8792e3554 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 23 Oct 2024 18:55:30 +0100 Subject: [PATCH 19/50] Improve docs and remove print --- src/icalendar/alarms.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index 8d61457f..88649b83 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -86,7 +86,7 @@ def is_active_in(self, timezone:Optional[tzinfo]=None) -> bool: raise IncompleteAlarmInformation("A timezone is required to check if the alarm is still active.") if self._snooze_until is not None and self._snooze_until > self._last_ack: return True - print(f"trigger == {trigger} > {self._last_ack} == last ack") + # print(f"trigger == {trigger} > {self._last_ack} == last ack") return trigger > self._last_ack @property @@ -134,7 +134,6 @@ def add_component(self, component:Alarm|Parent): self.set_start(component.start) self.set_end(component.end) if component.is_thunderbird(): - print("component.DTSTAMP", component.DTSTAMP) self.acknowledge_until(component.X_MOZ_LASTACK) self.snooze_until(component.X_MOZ_SNOOZE_TIME) else: @@ -191,7 +190,7 @@ def _add(self, dt: date, td:timedelta): def acknowledge_until(self, dt: Optional[date]) -> None: """This is the time when all the alarms of this component were acknowledged. - You can set several times like this. Only the latest one counts. + You can set several times like this. Only the last one counts. Since RFC 9074 (Alarm Extension) was created later, calendar implementations differ in how they acknowledge alarms. @@ -199,18 +198,16 @@ def acknowledge_until(self, dt: Optional[date]) -> None: an event has been acknowledged because of an alarm. All alarms that happen before this time will be ackknowledged at this dt. """ - print("acknowledge_until", dt) if dt is not None: self._last_ack = tzp.localize_utc(dt) def snooze_until(self, dt: Optional[date]) -> None: """This is the time when all the alarms of this component were snoozed. - You can set several times like this. Only the latest one counts. + You can set several times like this. Only the last one counts. The alarms are supposed to turn up again at dt when they are not acknowledged but snoozed. """ - print("snooze_until ", dt) if dt is not None: self._snooze_until = tzp.localize_utc(dt) From 41244bad5ddc946d7476c510e3bb6124a71bfe24 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Fri, 25 Oct 2024 09:22:33 +0100 Subject: [PATCH 20/50] Document vDatetime.from_ical --- src/icalendar/prop.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/icalendar/prop.py b/src/icalendar/prop.py index d15630cc..2e5ac643 100644 --- a/src/icalendar/prop.py +++ b/src/icalendar/prop.py @@ -405,9 +405,28 @@ def to_ical(self): @staticmethod def from_ical(ical, timezone=None): + """Create a datetime from the RFC string. + + Format: YYYYMMDDTHHMMSS + + >>> from icalendar import vDatetime + >>> vDatetime.from_ical("20210302T101500") + datetime.datetime(2021, 3, 2, 10, 15) + + >>> vDatetime.from_ical("20210302T101500", "America/New_York") + datetime.datetime(2021, 3, 2, 10, 15, tzinfo=ZoneInfo(key='America/New_York')) + + >>> from zoneinfo import ZoneInfo + >>> timezone = ZoneInfo("Europe/Berlin") + >>> vDatetime.from_ical("20210302T101500", timezone) + datetime.datetime(2021, 3, 2, 10, 15, tzinfo=ZoneInfo(key='Europe/Berlin')) + + """ tzinfo = None - if timezone: + if isinstance(timezone, str): tzinfo = _timezone.tzp.timezone(timezone) + elif timezone is not None: + tzinfo = timezone try: timetuple = ( @@ -426,8 +445,8 @@ def from_ical(ical, timezone=None): return _timezone.tzp.localize_utc(datetime(*timetuple)) else: raise ValueError(ical) - except Exception: - raise ValueError(f'Wrong datetime format: {ical}') + except Exception as e: + raise ValueError(f'Wrong datetime format: {ical}') from e class vDuration(TimeBase): From c3bd555068dbcdaaa6a2dca8a5bfc9467cf07ee2 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Fri, 25 Oct 2024 09:41:06 +0100 Subject: [PATCH 21/50] Test the snoozing of alarms --- src/icalendar/alarms.py | 107 +++++++++++++----- .../test_issue_716_alarm_time_computation.py | 58 +++++++++- 2 files changed, 134 insertions(+), 31 deletions(-) diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index 88649b83..60d06b52 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -27,6 +27,24 @@ class IncompleteAlarmInformation(ValueError): """The alarms cannot be calculated yet because information is missing.""" +class ComponentStartMissing(IncompleteAlarmInformation): + """We are missing the start of a component that the alarm is for. + + Use Alarms.set_start(). + """ + +class ComponentEndMissing(IncompleteAlarmInformation): + """We are missing the end of a component that the alarm is for. + + Use Alarms.set_end(). + """ + +class LocalTimezoneMissing(IncompleteAlarmInformation): + """We are missing the local timezone to compute the value. + + Use Alarms.set_local_timezone(). + """ + class AlarmTime: """An alarm time with all the information.""" @@ -37,17 +55,29 @@ def __init__( trigger : datetime, acknowledged_until:Optional[datetime]=None, snoozed_until:Optional[datetime]=None, - parent: Optional[Parent]=None + parent: Optional[Parent]=None, ): """Create a new AlarmTime. - - alarm - the Alarm component - trigger - a date or datetime at which to trigger the alarm - acknowledged_until - an optional datetime in UTC until when all alarms + + alarm + the Alarm component + + trigger + a date or datetime at which to trigger the alarm + + acknowledged_until + an optional datetime in UTC until when all alarms have been acknowledged - snoozed_until - an optional datetime in UTC until which all alarms of + + snoozed_until + an optional datetime in UTC until which all alarms of the same parent are snoozed - parent - the optional parent component the alarm refers to + + parent + the optional parent component the alarm refers to + + local_tzinfo + the local timezone that events without tzinfo should have """ self._alarm = alarm self._parent = parent @@ -68,7 +98,7 @@ def parent(self) -> Optional[Parent]: """ return self._parent - def is_active_in(self, timezone:Optional[tzinfo]=None) -> bool: + def is_active(self) -> bool: """Whether this alarm is active (True) or acknowledged (False). E.g. in some calendar software, this is True until the user had a look @@ -81,11 +111,11 @@ def is_active_in(self, timezone:Optional[tzinfo]=None) -> bool: if not self._last_ack: # if nothing is acknowledged, this alarm counts return True - trigger = self.trigger if timezone is None else tzp.localize(self.trigger, timezone) - if trigger.tzinfo is None: - raise IncompleteAlarmInformation("A timezone is required to check if the alarm is still active.") if self._snooze_until is not None and self._snooze_until > self._last_ack: return True + trigger = self.trigger + if trigger.tzinfo is None: + raise LocalTimezoneMissing("A local timezone is required to check if the alarm is still active. Use Alarms.set_local_timezone().") # print(f"trigger == {trigger} > {self._last_ack} == last ack") return trigger > self._last_ack @@ -94,8 +124,9 @@ def trigger(self) -> date: """This is the time to trigger the alarm. If the alarm has been snoozed, this can differ from the TRIGGER property. - Use is_active_in() to avoid timezone issues. """ + if self._snooze_until is not None and self._snooze_until > self._trigger: + return self._snooze_until return self._trigger @@ -118,6 +149,7 @@ def __init__(self, component:Optional[Alarm|Event|Todo]=None): self._parent : Optional[Parent] = None self._last_ack : Optional[datetime] = None self._snooze_until : Optional[datetime] = None + self._local_tzinfo : Optional[tzinfo] = None if component is not None: self.add_component(component) @@ -163,7 +195,7 @@ def add_alarm(self, alarm: Alarm) -> None: else: self._end_alarms.append(alarm) - def set_start(self, dt: date): + def set_start(self, dt: Optional[date]): """Set the start of the component. If you have only absolute alarms, this is not required. @@ -171,7 +203,7 @@ def set_start(self, dt: date): """ self._start = dt - def set_end(self, dt: date): + def set_end(self, dt: Optional[date]): """Set the end of the component. If you have only absolute alarms, this is not required. @@ -188,29 +220,43 @@ def _add(self, dt: date, td:timedelta): return normalize_pytz(dt + td) def acknowledge_until(self, dt: Optional[date]) -> None: - """This is the time when all the alarms of this component were acknowledged. + """This is the time in UTC when all the alarms of this component were acknowledged. - You can set several times like this. Only the last one counts. + Only the last call counts. Since RFC 9074 (Alarm Extension) was created later, calendar implementations differ in how they acknowledge alarms. E.g. Thunderbird and Google Calendar store the last time an event has been acknowledged because of an alarm. - All alarms that happen before this time will be ackknowledged at this dt. + All alarms that happen before this time count as ackknowledged. """ if dt is not None: self._last_ack = tzp.localize_utc(dt) def snooze_until(self, dt: Optional[date]) -> None: - """This is the time when all the alarms of this component were snoozed. + """This is the time in UTC when all the alarms of this component were snoozed. + + Only the last call counts. - You can set several times like this. Only the last one counts. The alarms are supposed to turn up again at dt when they are not acknowledged but snoozed. """ if dt is not None: self._snooze_until = tzp.localize_utc(dt) + def set_local_timezone(self, tzinfo:Optional[tzinfo|str]): + """Set the local timezone. + + Events are sometimes in local time. + In order to compute the exact time of the alarm, some + alarms without timezone are considered local. + + Some computations work without setting this, others don't. + If they need this information, expect a LocalTimezoneMissing exception + somewhere down the line. + """ + self._local_tzinfo = tzp.timezone(tzinfo) if isinstance(tzinfo, str) else tzinfo + @property def times(self) -> list[AlarmTime]: """Compute and return the times of the alarms given. @@ -238,6 +284,8 @@ def _repeat(self, first: datetime, alarm: Alarm) -> Generator[datetime]: def _alarm_time(self, alarm: Alarm, trigger:date): """Create an alarm time with the additional attributes.""" + if getattr(trigger, "tzinfo", None) is None and self._local_tzinfo is not None: + trigger = normalize_pytz(trigger.replace(tzinfo=self._local_tzinfo)) return AlarmTime(alarm, trigger, self._last_ack, self._snooze_until, self._parent) def _get_absolute_alarm_times(self) -> list[AlarmTime]: @@ -251,7 +299,7 @@ def _get_absolute_alarm_times(self) -> list[AlarmTime]: def _get_start_alarm_times(self) -> list[AlarmTime]: """Return a list of alarm times relative to the start of the component.""" if self._start is None and self._start_alarms: - raise IncompleteAlarmInformation("Use Alarms.set_start because at least one alarm is relative to the start of a component.") + raise ComponentStartMissing("Use Alarms.set_start because at least one alarm is relative to the start of a component.") return [ self._alarm_time(alarm , trigger) for alarm in self._start_alarms @@ -261,14 +309,15 @@ def _get_start_alarm_times(self) -> list[AlarmTime]: def _get_end_alarm_times(self) -> list[AlarmTime]: """Return a list of alarm times relative to the start of the component.""" if self._end is None and self._end_alarms: - raise IncompleteAlarmInformation("Use Alarms.set_end because at least one alarm is relative to the end of a component.") + raise ComponentEndMissing("Use Alarms.set_end because at least one alarm is relative to the end of a component.") return [ self._alarm_time(alarm , trigger) for alarm in self._end_alarms for trigger in self._repeat(self._add(self._end, alarm["TRIGGER"].dt), alarm) ] - def active_in(self, timezone:Optional[tzinfo|str]=None) -> list[AlarmTime]: + @property + def active(self) -> list[AlarmTime]: """The alarm times that are still active and not acknowledged. This considers snoozed alarms. @@ -277,7 +326,13 @@ def active_in(self, timezone:Optional[tzinfo|str]=None) -> list[AlarmTime]: To calculate if the alarm really happened, we need it to be in a timezone. If a timezone is required but not given, we throw an IncompleteAlarmInformation. """ - timezone = tzp.timezone(timezone) if isinstance(timezone, str) else timezone - return [alarm_time for alarm_time in self.times if alarm_time.is_active_in(timezone)] - -__all__ = ["Alarms", "AlarmTime", "IncompleteAlarmInformation"] + return [alarm_time for alarm_time in self.times if alarm_time.is_active()] + +__all__ = [ + "Alarms", + "AlarmTime", + "IncompleteAlarmInformation", + "ComponentEndMissing", + "ComponentStartMissing", + "LocalTimezoneMissing" +] diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py index 9fce9be0..dea0522e 100644 --- a/src/icalendar/tests/test_issue_716_alarm_time_computation.py +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -120,7 +120,7 @@ def test_cannot_compute_relative_alarm_without_end(alarms): (datetime(2024, 10, 29, 13, 20), None, datetime(2024, 10, 29, 13, 20)), ] ) -def test_can_complete_relative_calculation_if_a_start_is_given(alarms, dtend, timezone, trigger, tzp): +def test_can_complete_relative_calculation(alarms, dtend, timezone, trigger, tzp): """The start is given and required.""" start = (dtend if timezone is None else tzp.localize(dtend, timezone)) alarms = Alarms(alarms.rfc_5545_end) @@ -201,7 +201,7 @@ def test_number_of_active_alarms_from_calendar_software(calendars, calendar, ind """Check that we extract calculate the correct amount of active alarms.""" event = calendars[calendar].subcomponents[index] a = Alarms(event) - active_alarms = a.active_in() # We do not need to pass a timezone because the events have a timezone + active_alarms = a.active # We do not need to pass a timezone because the events have a timezone assert len(active_alarms) == count, f"{message} - I expect {count} alarms active but got {len(active_alarms)}." @@ -226,8 +226,9 @@ def test_number_of_active_alarms_with_moving_time(start, acknowledged, count, tz a = Alarms() a.add_alarm(three_alarms) a.set_start(start) + a.set_local_timezone(timezone) a.acknowledge_until(tzp.localize_utc(acknowledged)) - active = a.active_in(timezone) + active = a.active assert len(active) == count @@ -238,8 +239,8 @@ def test_incomplete_alarm_information_for_active_state(tzp): a.set_start(date(2017, 12, 1)) a.acknowledge_until(tzp.localize_utc(datetime(2012, 10, 10, 12))) with pytest.raises(IncompleteAlarmInformation) as e: - a.active_in() - assert e.value.args[0] == "A timezone is required to check if the alarm is still active." + a.active # noqa: B018 + assert e.value.args[0] == f"A local timezone is required to check if the alarm is still active. Use Alarms.{Alarms.set_local_timezone.__name__}()." @pytest.mark.parametrize( @@ -258,3 +259,50 @@ def test_thunderbird_recognition(calendars, calendar_name): event = calendar.subcomponents[-1] assert isinstance(event, Event) assert event.is_thunderbird() == ("thunderbird" in calendar_name) + + +@pytest.mark.parametrize( + "snooze", + [ + datetime(2012, 10, 10, 11, 1), # before everything + datetime(2017, 12, 1, 10, 1), + datetime(2017, 12, 1, 11, 1), + datetime(2017, 12, 1, 12, 1), + datetime(2017, 12, 1, 13, 1), # snooze until after the start of the event + ] +) +def test_snoozed_alarm_has_trigger_at_snooze_time(tzp, snooze): + """When an alarm is snoozed, it pops up after the snooze time.""" + a = Alarms() + a.add_alarm(three_alarms) + a.set_start(datetime(2017, 12, 1, 13)) + a.set_local_timezone("UTC") + snooze_utc = tzp.localize_utc(snooze) + a.snooze_until(snooze_utc) + active = a.active + assert len(active) == 3 + for alarm in active: + assert alarm.trigger >= snooze_utc + + +@pytest.mark.parametrize( + ("event_index", "alarm_times"), + [ + (1, ("20210302T101500",)), + ] +) +def test_rfc_9074_alarm_times(events, event_index, alarm_times): + """Test the examples from the RFC and their timing. + + Add times use America/New_York as timezone. + """ + a = Alarms(events[f"rfc_9074_example_{event_index}"]) + assert len(a.times) == len(alarm_times) + expected_alarm_times = {vDatetime.from_ical(t, "America/New_York") for t in alarm_times} + computed_alarm_times = {alarm.trigger for alarm in a.times} + assert expected_alarm_times == computed_alarm_times + + +def test_set_to_None(): + """acknowledge_until, snooze_until, set_local_timezone.""" + pytest.skip("TODO") From 92dae9c7b8d2b25f5e2bc04efe8a6f300d8f1d41 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Fri, 25 Oct 2024 12:05:35 +0100 Subject: [PATCH 22/50] Test RFC 9074 alarms --- src/icalendar/alarms.py | 42 +++++++++++++------ src/icalendar/cal.py | 29 +++++++++++++ .../test_issue_716_alarm_time_computation.py | 19 ++++++++- 3 files changed, 76 insertions(+), 14 deletions(-) diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index 60d06b52..0b68867d 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -1,12 +1,14 @@ """Compute the times and states of alarms. -This takes different calendar software into account and RFC 9074 (Alarm Extension). - -- Outlook does not export VALARM information -- Google Calendar uses the DTSTAMP to acknowledge the alarms -- Thunderbird snoozes the alarms with a X-MOZ-SNOOZE-TIME attribute in the event -- Thunderbird acknowledges the alarms with a X-MOZ-LASTACK attribute in the event -- Etar deletes alarms that are not active any more +This takes different calendar software into account and the RFC 9074 (Alarm Extension). + +- RFC 9074 defines an ACKNOWLEDGED property in the VALARM. +- Outlook does not export VALARM information. +- Google Calendar uses the DTSTAMP to acknowledge the alarms. +- Thunderbird snoozes the alarms with a X-MOZ-SNOOZE-TIME attribute in the event. +- Thunderbird acknowledges the alarms with a X-MOZ-LASTACK attribute in the event. +- Etar deletes alarms that are acknowledged. +- Nextcloud's Webinterface does not do anything with the alarms when the time passes. """ from __future__ import annotations @@ -85,6 +87,19 @@ def __init__( self._last_ack = acknowledged_until self._snooze_until = snoozed_until + @property + def acknowledged(self) -> Optional[datetime]: + """The time in UTC at which this alarm was last acknowledged. + + If the alarm was not acknowledged (dismissed), then this is None. + """ + ack = self.alarm.ACKNOWLEDGED + if ack is None: + return self._last_ack + if self._last_ack is None: + return ack + return max(ack, self._last_ack) + @property def alarm(self) -> Alarm: """The alarm component.""" @@ -108,16 +123,19 @@ def is_active(self) -> bool: To calculate if the alarm really happened, we need it to be in a timezone. If a timezone is required but not given, we throw an IncompleteAlarmInformation. """ - if not self._last_ack: + acknowledged = self.acknowledged + if not acknowledged: # if nothing is acknowledged, this alarm counts return True - if self._snooze_until is not None and self._snooze_until > self._last_ack: + if self._snooze_until is not None and self._snooze_until > acknowledged: return True trigger = self.trigger if trigger.tzinfo is None: - raise LocalTimezoneMissing("A local timezone is required to check if the alarm is still active. Use Alarms.set_local_timezone().") - # print(f"trigger == {trigger} > {self._last_ack} == last ack") - return trigger > self._last_ack + raise LocalTimezoneMissing( + "A local timezone is required to check if the alarm is still active. " + "Use Alarms.set_local_timezone()." + ) + return trigger > acknowledged @property def trigger(self) -> date: diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 8240ad11..45334f6c 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -1060,6 +1060,35 @@ def REPEAT(self, value: int) -> None: self["REPEAT"] = int(value) DURATION = Event.DURATION # TODO: adjust once https://github.com/collective/icalendar/pull/733 is merged + ACKNOWLEDGED = create_utc_property("ACKNOWLEDGED", + """This is defined in RFC 9074: + + Purpose: This property specifies the UTC date and time at which the + corresponding alarm was last sent or acknowledged. + + This property is used to specify when an alarm was last sent or acknowledged. + This allows clients to determine when a pending alarm has been acknowledged + by a calendar user so that any alerts can be dismissed across multiple devices. + It also allows clients to track repeating alarms or alarms on recurring events or + to-dos to ensure that the right number of missed alarms can be tracked. + + Clients SHOULD set this property to the current date-time value in UTC + when a calendar user acknowledges a pending alarm. Certain kinds of alarms, + such as email-based alerts, might not provide feedback as to when the calendar user + sees them. For those kinds of alarms, the client SHOULD set this property + when the alarm is triggered and the action is successfully carried out. + + When an alarm is triggered on a client, clients can check to see if an"ACKNOWLEDGED" + property is present. If it is, and the value of that property is greater than or + equal to the computed trigger time for the alarm, then the client SHOULD NOT trigger + the alarm. Similarly, if an alarm has been triggered and + an "alert" has been presented to a calendar user, clients can monitor + the iCalendar data to determine whether an "ACKNOWLEDGED" property is added or + changed in the alarm component. If the value of any "ACKNOWLEDGED" property + in the alarm changes and is greater than or equal to the trigger time of the alarm, + then clients SHOULD dismiss or cancel any "alert" presented to the calendar user. + """) + class Calendar(Component): """This is the base object for an iCalendar file. diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py index dea0522e..a3084890 100644 --- a/src/icalendar/tests/test_issue_716_alarm_time_computation.py +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -288,7 +288,22 @@ def test_snoozed_alarm_has_trigger_at_snooze_time(tzp, snooze): @pytest.mark.parametrize( ("event_index", "alarm_times"), [ + # Assume that we have the following event with an alarm set to trigger 15 minutes before the meeting: (1, ("20210302T101500",)), + # When the alarm is triggered, the user decides to "snooze" it for 5 minutes. + # The client acknowledges the original alarm and creates a new "snooze" + # alarm as a sibling of, and relates it to, the original alarm (note that + # both occurrences of "VALARM" reside within the same "parent" VEVENT): + (2, ("20210302T102000",)), + # When the "snooze" alarm is triggered, the user decides to "snooze" it + # again for an additional 5 minutes. The client once again acknowledges + # the original alarm, removes the triggered "snooze" alarm, and creates another + # new "snooze" alarm as a sibling of, and relates it to, the original alarm + # (note the different UID for the new "snooze" alarm): + (3, ("20210302T102500",)), + # When the second "snooze" alarm is triggered, the user decides to dismiss it. + # The client acknowledges both the original alarm and the second "snooze" alarm: + (4, ()), ] ) def test_rfc_9074_alarm_times(events, event_index, alarm_times): @@ -297,9 +312,9 @@ def test_rfc_9074_alarm_times(events, event_index, alarm_times): Add times use America/New_York as timezone. """ a = Alarms(events[f"rfc_9074_example_{event_index}"]) - assert len(a.times) == len(alarm_times) + assert len(a.active) == len(alarm_times) expected_alarm_times = {vDatetime.from_ical(t, "America/New_York") for t in alarm_times} - computed_alarm_times = {alarm.trigger for alarm in a.times} + computed_alarm_times = {alarm.trigger for alarm in a.active} assert expected_alarm_times == computed_alarm_times From d8292cebc6f1a11ee3319e61e81e2339f687820e Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sat, 26 Oct 2024 09:22:48 +0100 Subject: [PATCH 23/50] Document alarms module --- docs/api.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 757f905c..ac211638 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,6 +1,13 @@ API Reference ------------- +icalendar.alarms +++++++++++++++++ + +.. automodule:: icalendar.alarms + :members: + + icalendar.cal +++++++++++++ From 20eb10b29048a1e04c0e64f0c5e62cd3d499f4e0 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sat, 26 Oct 2024 09:41:44 +0100 Subject: [PATCH 24/50] Create usage example documentation --- src/icalendar/__init__.py | 14 ++++++++++++++ src/icalendar/alarms.py | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/icalendar/__init__.py b/src/icalendar/__init__.py index d0e5f10e..e1a09a02 100644 --- a/src/icalendar/__init__.py +++ b/src/icalendar/__init__.py @@ -1,3 +1,11 @@ +from icalendar.alarms import ( + Alarms, + AlarmTime, + ComponentEndMissing, + ComponentStartMissing, + IncompleteAlarmInformation, + LocalTimezoneMissing, +) from icalendar.cal import ( Alarm, Calendar, @@ -94,4 +102,10 @@ "vMonth", "IncompleteComponent", "InvalidCalendar", + "Alarms", + "AlarmTime", + "ComponentEndMissing", + "ComponentStartMissing", + "IncompleteAlarmInformation", + "LocalTimezoneMissing", ] diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index 0b68867d..ba1601b2 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -151,7 +151,41 @@ def trigger(self) -> date: class Alarms: """Compute the times and states of alarms. - TODO: example! + This is an example using RFC 9074. + One alarm is 30 minutes before the event and acknowledged. + Another alarm is 15 minutes before the event and still active. + + >>> from icalendar import Event, Alarms + >>> event = Event.from_ical( + ... '''BEGIN:VEVENT + ... CREATED:20210301T151004Z + ... UID:AC67C078-CED3-4BF5-9726-832C3749F627 + ... DTSTAMP:20210301T151004Z + ... DTSTART;TZID=America/New_York:20210302T103000 + ... DTEND;TZID=America/New_York:20210302T113000 + ... SUMMARY:Meeting + ... BEGIN:VALARM + ... UID:8297C37D-BA2D-4476-91AE-C1EAA364F8E1 + ... TRIGGER:-PT30M + ... ACKNOWLEDGED:20210302T150004Z + ... DESCRIPTION:Event reminder + ... ACTION:DISPLAY + ... END:VALARM + ... BEGIN:VALARM + ... UID:8297C37D-BA2D-4476-91AE-C1EAA364F8E1 + ... TRIGGER:-PT15M + ... DESCRIPTION:Event reminder + ... ACTION:DISPLAY + ... END:VALARM + ... END:VEVENT + ... ''') + >>> alarms = Alarms(event) + >>> len(alarms.times) # all alarms including those acknowledged + 2 + >>> len(alarms.active) # the alarms that are not acknowledged, yet + 1 + >>> alarms.active[0].trigger # this alarm triggers 15 minutes before 10:30 + datetime.datetime(2021, 3, 2, 10, 15, tzinfo=ZoneInfo(key='America/New_York')) RFC 9074 specifies that alarms can also be triggered by proximity. This is not implemented yet. From 3b12b90c10c93adf04cd77b11e9112ab0b054622 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sat, 26 Oct 2024 10:14:36 +0100 Subject: [PATCH 25/50] Create alarms attribute --- src/icalendar/cal.py | 46 +++++++++++++++++-- .../test_issue_716_alarm_time_computation.py | 8 ++-- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 45334f6c..954cc6aa 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -7,16 +7,20 @@ import os from datetime import date, datetime, timedelta -from typing import List, Optional, Tuple +from typing import TYPE_CHECKING, List, Optional, Tuple import dateutil.rrule import dateutil.tz + from icalendar.caselessdict import CaselessDict from icalendar.parser import Contentline, Contentlines, Parameters, q_join, q_split from icalendar.parser_tools import DEFAULT_ENCODING -from icalendar.prop import TypesFactory, vDDDLists, vDDDTypes, vText, vDuration +from icalendar.prop import TypesFactory, vDDDLists, vDDDTypes, vDuration, vText from icalendar.timezone import tzp -from icalendar.tools import is_date, is_datetime, to_datetime +from icalendar.tools import is_date + +if TYPE_CHECKING: + from icalendar.alarms import Alarms def get_example(component_directory: str, example_name: str) -> bytes: @@ -647,6 +651,26 @@ class Event(Component): 'RSTATUS', 'RELATED', 'RESOURCES', 'RDATE', 'RRULE' ) ignore_exceptions = True + + @property + def alarms(self) -> Alarms: + """Compute the alarm times for this component. + + >>> from icalendar import Event + >>> event = Event.example("rfc_9074_example_1") + >>> len(event.alarms.times) + 1 + >>> alarm_time = event.alarms.times[0] + >>> alarm_time.trigger # The time when the alarm pops up + datetime.datetime(2021, 3, 2, 10, 15, tzinfo=ZoneInfo(key='America/New_York')) + >>> alarm_time.is_active() # This alarm has not been acknowledged + True + + Note that this only uses DTSTART and DTEND but ignores + RDATE, EXDATE and RRULE properties. + """ + from icalendar.alarms import Alarms + return Alarms(self) @classmethod def example(cls, name:str) -> Event: @@ -791,6 +815,22 @@ class Todo(Component): X_MOZ_SNOOZE_TIME = _X_MOZ_SNOOZE_TIME X_MOZ_LASTACK = _X_MOZ_LASTACK + @property + def alarms(self) -> Alarms: + """Compute the alarm times for this component. + + >>> from icalendar import Todo + >>> todo = Todo() # empty without alarms + >>> len(todo.alarms.times) + 0 + + Note that this only uses DTSTART and DUE but ignores + RDATE, EXDATE and RRULE properties. + """ + from icalendar.alarms import Alarms + return Alarms(self) + + class Journal(Component): """A descriptive text at a certain time or associated with a component. diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py index a3084890..ece3e644 100644 --- a/src/icalendar/tests/test_issue_716_alarm_time_computation.py +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -1,12 +1,12 @@ """Test the alarm time computation.""" -from datetime import date, datetime, timedelta, timezone, tzinfo +from datetime import date, datetime, timedelta, timezone -from icalendar import Event import pytest +from icalendar import Event from icalendar.alarms import Alarms, IncompleteAlarmInformation -from icalendar.cal import Alarm, InvalidCalendar +from icalendar.cal import Alarm from icalendar.prop import vDatetime from icalendar.tools import normalize_pytz @@ -311,7 +311,7 @@ def test_rfc_9074_alarm_times(events, event_index, alarm_times): Add times use America/New_York as timezone. """ - a = Alarms(events[f"rfc_9074_example_{event_index}"]) + a = events[f"rfc_9074_example_{event_index}"].alarms assert len(a.active) == len(alarm_times) expected_alarm_times = {vDatetime.from_ical(t, "America/New_York") for t in alarm_times} computed_alarm_times = {alarm.trigger for alarm in a.active} From b1d18ecdd57d6a677d2f5902f1bbd62c057f0097 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sat, 26 Oct 2024 10:22:18 +0100 Subject: [PATCH 26/50] Make doctests run --- src/icalendar/cal.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index b26f7fd0..1f994d52 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -915,8 +915,10 @@ def duration(self) -> timedelta: def alarms(self) -> Alarms: """Compute the alarm times for this component. + >>> from datetime import datetime >>> from icalendar import Todo >>> todo = Todo() # empty without alarms + >>> todo.start = datetime(2024, 10, 26, 10, 21) >>> len(todo.alarms.times) 0 From a5170e47f00cdad9a2dcf55f1ed0a3fe589ac11f Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sat, 26 Oct 2024 10:31:46 +0100 Subject: [PATCH 27/50] Documents Alarm.DURATION --- src/icalendar/cal.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 1f994d52..c7a735cf 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -1186,7 +1186,16 @@ class Alarm(Component): @property def REPEAT(self) -> int: - """The REPEAT property of an alarm component.""" + """The REPEAT property of an alarm component. + + The alarm can be defined such that it triggers repeatedly. A + definition of an alarm with a repeating trigger MUST include both + the "DURATION" and "REPEAT" properties. The "DURATION" property + specifies the delay period, after which the alarm will repeat. + The "REPEAT" property specifies the number of additional + repetitions that the alarm will be triggered. This repetition + count is in addition to the initial triggering of the alarm. + """ try: return int(self.get("REPEAT", 0)) except ValueError as e: @@ -1197,7 +1206,15 @@ def REPEAT(self, value: int) -> None: """The REPEAT property of an alarm component.""" self["REPEAT"] = int(value) - DURATION = Event.DURATION # TODO: adjust once https://github.com/collective/icalendar/pull/733 is merged + DURATION = property(_get_duration, _set_duration, _del_duration, + """The DURATION property of an alarm component. + + The alarm can be defined such that it triggers repeatedly. A + definition of an alarm with a repeating trigger MUST include both + the "DURATION" and "REPEAT" properties. The "DURATION" property + specifies the delay period, after which the alarm will repeat. + """) + ACKNOWLEDGED = create_utc_property("ACKNOWLEDGED", """This is defined in RFC 9074: From ff8552b913dd771fb025b98493fb76dbd7d86271 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sat, 26 Oct 2024 10:40:32 +0100 Subject: [PATCH 28/50] log changes --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ea571088..d0eea36e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,6 +18,8 @@ New features: - Add ``VALARM`` properties for :rfc:`9074`. See `Issue 657 `_ - Test compatibility with Python 3.13 +- Add ``icalendar.alarms`` module to calculate alarm times. See `Issue 716 `_. +- Add ``Event.alarms`` and ``Todo.alarms`` to access alarm calculation. Bug fixes: From 42d330f894ff7310a3e1f79af3c887d9a64cca4b Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sat, 26 Oct 2024 10:46:48 +0100 Subject: [PATCH 29/50] log changes --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d0eea36e..84299a5d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,8 @@ Minor changes: - Added ``end``, ``start``, ``duration``, ``DTSTART``, ``DUE``, and ``DURATION`` attributes to ``Todo`` components. See `Issue 662`_. - Format test code with Ruff. See `Issue 672 `_. - Document the Debian package. See `Issue 701 `_. +- Document ``vDatetime.from_ical`` +- Allow passing a ``datetime.date`` to ``TZP.localize_utc`` and ``TZP.localize`` methods. Breaking changes: @@ -20,6 +22,10 @@ New features: - Test compatibility with Python 3.13 - Add ``icalendar.alarms`` module to calculate alarm times. See `Issue 716 `_. - Add ``Event.alarms`` and ``Todo.alarms`` to access alarm calculation. +- Add ``Component.DTSTAMP`` and ``Component.LAST_MODIFIED`` properties for datetime in UTC. +- Add ``Component.is_thunderbird()`` to check if the component uses custom properties by Thunderbird. +- Add ``X_MOZ_SNOOZE_TIME`` and ``X_MOZ_LASTACK`` properties to ``Event`` and ``Todo``. +- Add ``Alarm.ACKNOWLEDGED``, ``Alarm.REPEAT`` and ``Alarm.DURATION`` properties. Bug fixes: From b3fa5b65330cd7e54fda023b4ddee2128a747704 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 30 Oct 2024 08:09:44 +0000 Subject: [PATCH 30/50] remove old file --- .../tests/test_issue_716_alarm_extension.py | 13 ------------- .../test_issue_716_alarm_time_computation.py | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 14 deletions(-) delete mode 100644 src/icalendar/tests/test_issue_716_alarm_extension.py diff --git a/src/icalendar/tests/test_issue_716_alarm_extension.py b/src/icalendar/tests/test_issue_716_alarm_extension.py deleted file mode 100644 index b6beead9..00000000 --- a/src/icalendar/tests/test_issue_716_alarm_extension.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Test the alarm classification. - -Events can have alarms. -Alarms can be in this state: - -- active - the user wants the alarm to pop up -- acknowledged - the user no longer wants the alarm -- snoozed - the user moved that alarm to another time - -The alarms can only work on the properties of the event like -DTSTART, DTEND and DURATION. - -""" diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py index ece3e644..c83fe9e2 100644 --- a/src/icalendar/tests/test_issue_716_alarm_time_computation.py +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -1,4 +1,17 @@ -"""Test the alarm time computation.""" +"""Test the alarm time computation. + +Events can have alarms. +Alarms can be in this state: + +- active - the user wants the alarm to pop up +- acknowledged - the user no longer wants the alarm +- snoozed - the user moved that alarm to another time + +The alarms can only work on the properties of the event like +DTSTART, DTEND and DURATION. + +""" + from datetime import date, datetime, timedelta, timezone From 07d8d9288c0ffd26792277601e70f11e2ceb3917 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 30 Oct 2024 09:53:26 +0000 Subject: [PATCH 31/50] Compute relative and absolute alarm triggers in the Alarm component --- src/icalendar/alarms.py | 6 +- src/icalendar/cal.py | 97 +++++++++++++++- .../test_issue_662_component_properties.py | 108 ++++++++++++++++++ 3 files changed, 206 insertions(+), 5 deletions(-) diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index ba1601b2..0d01a435 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -237,12 +237,12 @@ def set_parent(self, parent: Parent): def add_alarm(self, alarm: Alarm) -> None: """Optional: Add an alarm component.""" - trigger = alarm.get("TRIGGER") + trigger = alarm.TRIGGER if trigger is None: return - if isinstance(trigger.dt, date): + if isinstance(trigger, date): self._absolute_alarms.append(alarm) - elif trigger.params.get("RELATED", "START").upper() == "START": + elif alarm.TRIGGER_RELATED == "START": self._start_alarms.append(alarm) else: self._end_alarms.append(alarm) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index e98edb7e..a91819dc 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -7,7 +7,7 @@ import os from datetime import date, datetime, timedelta -from typing import TYPE_CHECKING, List, Optional, Tuple +from typing import TYPE_CHECKING, List, NamedTuple, Optional, Tuple, Union import dateutil.rrule import dateutil.tz @@ -577,7 +577,9 @@ def is_thunderbird(self) -> bool: # components defined in RFC 5545 def create_single_property(prop:str, value_attr:str, value_type:tuple[type], type_def:type, doc:str): - """Create a single property getter and setter.""" + """Create a single property getter and setter. + + This is a getter and setter for a property that only occurs once or not (None).""" def p_get(self : Component): default = object() @@ -1286,6 +1288,97 @@ def REPEAT(self, value: int) -> None: then clients SHOULD dismiss or cancel any "alert" presented to the calendar user. """) + TRIGGER = create_single_property( + "TRIGGER", "dt", (datetime, timedelta), Optional[Union[timedelta, datetime]], + """Purpose: This property specifies when an alarm will trigger. + + Value Type: The default value type is DURATION. The value type can + be set to a DATE-TIME value type, in which case the value MUST + specify a UTC-formatted DATE-TIME value. + + Either a positive or negative duration may be specified for the + "TRIGGER" property. An alarm with a positive duration is + triggered after the associated start or end of the event or to-do. + An alarm with a negative duration is triggered before the + associated start or end of the event or to-do.""" + ) + + @property + def TRIGGER_RELATED(self) -> str: + """The RELATED parameter of the TRIGGER property. + + Values are either "START" (default) or "END". + + A value of START will set the alarm to trigger off the + start of the associated event or to-do. A value of END will set + the alarm to trigger off the end of the associated event or to-do. + """ + trigger = self.get("TRIGGER") + if trigger is None: + return "START" + return trigger.params.get("RELATED", "START") + + @TRIGGER_RELATED.setter + def TRIGGER_RELATED(self, value: str): + """Set "START" or "END".""" + trigger = self.get("TRIGGER") + if trigger is None: + raise ValueError("You must set a TRIGGER before setting the RELATED parameter.") + trigger.params["RELATED"] = value + + class Triggers(NamedTuple): + """The computed times of alarm triggers. + + start - relative to the start of the Event or Todo (timedelta) + + end - relateive to the end of the Event or Todo (timedelta) + + absolute - datetime in UTC + """ + start: tuple[timedelta] + end: tuple[timedelta] + absolute: tuple[datetime] + + @property + def triggers(self): + """The computed triggers of an Alarm. + + This takes the TRIGGER, DURATION and REPEAT properties into account. + + Here, we create an alarm that triggers 3 times before the start of the + parent component: + + >>> from icalendar import Alarm + >>> from datetime import timedelta + >>> alarm = Alarm() + >>> alarm.TRIGGER = timedelta(hours=-4) # trigger 4 hours after + >>> alarm.DURATION = timedelta(hours=1) # after 1 hour trigger again + >>> alarm.REPEAT = 2 # trigger 2 more times + >>> alarm.triggers.start == (timedelta(hours=-4), timedelta(hours=-3), timedelta(hours=-2)) + True + >>> alarm.triggers.end + () + >>> alarm.triggers.absolute + () + """ + start = [] + end = [] + absolute = [] + trigger = self.TRIGGER + if trigger is not None: + if isinstance(trigger, date): + absolute.append(trigger) + add = absolute + elif self.TRIGGER_RELATED == "START": + start.append(trigger) + add = start + else: + end.append(trigger) + add = end + duration = self.DURATION + for _ in range(self.REPEAT): + add.append(add[-1] + duration) + return self.Triggers(start=tuple(start), end=tuple(end), absolute=tuple(absolute)) class Calendar(Component): """ diff --git a/src/icalendar/tests/test_issue_662_component_properties.py b/src/icalendar/tests/test_issue_662_component_properties.py index 095e94a6..8fa6957a 100644 --- a/src/icalendar/tests/test_issue_662_component_properties.py +++ b/src/icalendar/tests/test_issue_662_component_properties.py @@ -4,6 +4,8 @@ import pytest +from icalendar.cal import Alarm + try: from zoneinfo import ZoneInfo except ImportError: @@ -16,6 +18,7 @@ Journal, Todo, vDDDTypes, + vDatetime, ) from icalendar.prop import vDuration @@ -493,3 +496,108 @@ def setting_twice_does_not_duplicate_the_entry(): assert j.start == date(2024, 1, 3) assert j.end == date(2024, 1, 3) + +@pytest.mark.parametrize( + ("file", "trigger", "related"), + [ + ("rfc_5545_absolute_alarm_example", vDatetime.from_ical("19970317T133000Z"), "START"), + ("rfc_5545_end", timedelta(days=-2), "END"), + ("start_date", timedelta(days=-2), "START"), + ] +) +def test_get_alarm_trigger_property(alarms, file, trigger, related): + """Get the trigger property.""" + alarm = alarms[file] + assert alarm.TRIGGER == trigger + assert alarm.TRIGGER_RELATED == related + + +def test_set_alarm_trigger(): + """Set the alarm trigger.""" + a = Alarm() + a.TRIGGER = timedelta(hours=1) + assert a.TRIGGER == timedelta(hours=1) + assert a.TRIGGER_RELATED == "START" + + +def test_set_alarm_trigger_related(): + """Set the alarm trigger.""" + a = Alarm() + a.TRIGGER = timedelta(hours=1) + a.TRIGGER_RELATED = "END" + assert a.TRIGGER == timedelta(hours=1) + assert a.TRIGGER_RELATED == "END" + + +def test_get_related_without_trigger(): + """The default is start""" + assert Alarm().TRIGGER_RELATED == "START" + +def test_cannot_set_related_without_trigger(): + """TRIGGER must be set to set the parameter.""" + with pytest.raises(ValueError) as e: + a = Alarm() + a.TRIGGER_RELATED = "END" + assert e.value.args[0] == "You must set a TRIGGER before setting the RELATED parameter." + + +@pytest.mark.parametrize( + ("file", "triggers"), + [ + ("rfc_5545_absolute_alarm_example", ((), (), (vDatetime.from_ical("19970317T133000Z"), vDatetime.from_ical("19970317T134500Z"),vDatetime.from_ical("19970317T140000Z"),vDatetime.from_ical("19970317T141500Z"),vDatetime.from_ical("19970317T143000Z"),))), + ("rfc_5545_end", ((), (timedelta(days=-2),), ())), + ("start_date", ((timedelta(days=-2),), (), ())), + ] +) +def test_get_alarm_triggers(alarms, file, triggers): + """Get the trigger property.""" + alarm = alarms[file] + print(tuple(alarm.triggers)) + print(triggers) + assert alarm.triggers == triggers + + +def triggers_emtpy_alarm(): + """An alarm with no trigger has no triggers.""" + assert Alarm().triggers == ((), (), ()) + +h1 = timedelta(hours=1) + +def triggers_emtpy_with_no_repeat(): + """Check incomplete values.""" + a = Alarm() + a.TRIGGER = h1 + a.DURATION = h1 + assert a.triggers == ((h1,), (), ()) + +def triggers_emtpy_with_no_duration(): + """Check incomplete values.""" + a = Alarm() + a.TRIGGER = h1 + a.REPEAT = 10 + assert a.triggers == ((h1,), (), ()) + + +@pytest.mark.parametrize( + ("file", "triggers"), + [ + ("rfc_5545_absolute_alarm_example", ((), (), (vDatetime.from_ical("19970317T133000Z"),))), + ("rfc_5545_end", ((), (timedelta(days=-2),), ())), + ("start_date", ((timedelta(days=-2),), (), ())), + ] +) +@pytest.mark.parametrize("duration", [timedelta(days=-1), h1]) +@pytest.mark.parametrize("repeat", [1, 3]) +def test_get_alarm_triggers_repeated(alarms, file, triggers, duration, repeat): + """Get the trigger property.""" + alarm = alarms[file].copy() + alarm.REPEAT = repeat + alarm.DURATION = duration + for expected, triggers in zip(triggers, alarm.triggers): + if not expected: + assert triggers == () + continue + assert len(triggers) == 1 + repeat + assert triggers[0] == expected[0] + for x, y in zip(triggers[:-1], triggers[1:]): + assert y - x == duration From 78b9e5b4e1aa3d92c06b8c3e05d9b5642a4d13a1 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 30 Oct 2024 09:54:50 +0000 Subject: [PATCH 32/50] log changes --- CHANGES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 84dbb724..56b815a3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -26,7 +26,8 @@ New features: - Add ``Component.DTSTAMP`` and ``Component.LAST_MODIFIED`` properties for datetime in UTC. - Add ``Component.is_thunderbird()`` to check if the component uses custom properties by Thunderbird. - Add ``X_MOZ_SNOOZE_TIME`` and ``X_MOZ_LASTACK`` properties to ``Event`` and ``Todo``. -- Add ``Alarm.ACKNOWLEDGED``, ``Alarm.REPEAT`` and ``Alarm.DURATION`` properties. +- Add ``Alarm.ACKNOWLEDGED``, ``Alarm.TRIGGER``, ``Alarm.REPEAT`` and ``Alarm.DURATION`` properties + as well as ``Alarm.triggers`` to calculate alarm triggers. Bug fixes: From 5ea719cb4e864354dc00be98eb1a70d0715b4404 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 30 Oct 2024 10:05:20 +0000 Subject: [PATCH 33/50] Improve documentation and fix edge cases --- src/icalendar/cal.py | 22 ++++++++++++++----- .../test_issue_662_component_properties.py | 6 ++--- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index a91819dc..95c081ee 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -1312,6 +1312,15 @@ def TRIGGER_RELATED(self) -> str: A value of START will set the alarm to trigger off the start of the associated event or to-do. A value of END will set the alarm to trigger off the end of the associated event or to-do. + + In this example, we create an alarm that triggers two hours after the + end of its parent component: + + >>> from icalendar import Alarm + >>> from datetime import timedelta + >>> alarm = Alarm() + >>> alarm.TRIGGER = timedelta(hours=2) + >>> alarm.TRIGGER_RELATED = "END" """ trigger = self.get("TRIGGER") if trigger is None: @@ -1329,11 +1338,11 @@ def TRIGGER_RELATED(self, value: str): class Triggers(NamedTuple): """The computed times of alarm triggers. - start - relative to the start of the Event or Todo (timedelta) + start - triggers relative to the start of the Event or Todo (timedelta) - end - relateive to the end of the Event or Todo (timedelta) + end - triggers relative to the end of the Event or Todo (timedelta) - absolute - datetime in UTC + absolute - triggers at a datetime in UTC """ start: tuple[timedelta] end: tuple[timedelta] @@ -1351,7 +1360,7 @@ def triggers(self): >>> from icalendar import Alarm >>> from datetime import timedelta >>> alarm = Alarm() - >>> alarm.TRIGGER = timedelta(hours=-4) # trigger 4 hours after + >>> alarm.TRIGGER = timedelta(hours=-4) # trigger 4 hours before START >>> alarm.DURATION = timedelta(hours=1) # after 1 hour trigger again >>> alarm.REPEAT = 2 # trigger 2 more times >>> alarm.triggers.start == (timedelta(hours=-4), timedelta(hours=-3), timedelta(hours=-2)) @@ -1376,8 +1385,9 @@ def triggers(self): end.append(trigger) add = end duration = self.DURATION - for _ in range(self.REPEAT): - add.append(add[-1] + duration) + if duration is not None: + for _ in range(self.REPEAT): + add.append(add[-1] + duration) return self.Triggers(start=tuple(start), end=tuple(end), absolute=tuple(absolute)) class Calendar(Component): diff --git a/src/icalendar/tests/test_issue_662_component_properties.py b/src/icalendar/tests/test_issue_662_component_properties.py index 8fa6957a..b524c95a 100644 --- a/src/icalendar/tests/test_issue_662_component_properties.py +++ b/src/icalendar/tests/test_issue_662_component_properties.py @@ -557,20 +557,20 @@ def test_get_alarm_triggers(alarms, file, triggers): assert alarm.triggers == triggers -def triggers_emtpy_alarm(): +def test_triggers_emtpy_alarm(): """An alarm with no trigger has no triggers.""" assert Alarm().triggers == ((), (), ()) h1 = timedelta(hours=1) -def triggers_emtpy_with_no_repeat(): +def test_triggers_emtpy_with_no_repeat(): """Check incomplete values.""" a = Alarm() a.TRIGGER = h1 a.DURATION = h1 assert a.triggers == ((h1,), (), ()) -def triggers_emtpy_with_no_duration(): +def test_triggers_emtpy_with_no_duration(): """Check incomplete values.""" a = Alarm() a.TRIGGER = h1 From fab00aa5cd6964686e01b7d4872d2da496bf1adf Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 30 Oct 2024 11:33:34 +0000 Subject: [PATCH 34/50] use new alarm attributes --- src/icalendar/alarms.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index 0d01a435..cad94952 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -345,7 +345,7 @@ def _get_absolute_alarm_times(self) -> list[AlarmTime]: return [ self._alarm_time(alarm , trigger) for alarm in self._absolute_alarms - for trigger in self._repeat(alarm["TRIGGER"].dt, alarm) + for trigger in self._repeat(alarm.TRIGGER, alarm) ] def _get_start_alarm_times(self) -> list[AlarmTime]: @@ -355,7 +355,7 @@ def _get_start_alarm_times(self) -> list[AlarmTime]: return [ self._alarm_time(alarm , trigger) for alarm in self._start_alarms - for trigger in self._repeat(self._add(self._start, alarm["TRIGGER"].dt), alarm) + for trigger in self._repeat(self._add(self._start, alarm.TRIGGER), alarm) ] def _get_end_alarm_times(self) -> list[AlarmTime]: @@ -365,7 +365,7 @@ def _get_end_alarm_times(self) -> list[AlarmTime]: return [ self._alarm_time(alarm , trigger) for alarm in self._end_alarms - for trigger in self._repeat(self._add(self._end, alarm["TRIGGER"].dt), alarm) + for trigger in self._repeat(self._add(self._end, alarm.TRIGGER), alarm) ] @property From 72b2489f2c7602164b949657d24e9971c3e72a6f Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sat, 2 Nov 2024 13:15:45 +0000 Subject: [PATCH 35/50] Test incomplete alarms --- src/icalendar/alarms.py | 4 ++-- .../tests/test_issue_716_alarm_time_computation.py | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index cad94952..9b55e1f0 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -329,8 +329,8 @@ def _repeat(self, first: datetime, alarm: Alarm) -> Generator[datetime]: """The times when the alarm is triggered relative to start.""" yield first # we trigger at the start repeat = alarm.REPEAT - if repeat: - duration = alarm.DURATION + duration = alarm.DURATION + if repeat and duration: for i in range(1, repeat + 1): yield self._add(first, duration * i) diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py index c83fe9e2..45aaf8f6 100644 --- a/src/icalendar/tests/test_issue_716_alarm_time_computation.py +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -65,9 +65,19 @@ def test_absolute_alarm_time_with_vDatetime(alarm): assert times[0].trigger == EXAMPLE_TRIGGER -def test_alarm_has_only_one_of_repeat_or_duration(): +alarm_incomplete_1 = Alarm() +alarm_incomplete_1.TRIGGER = timedelta(hours=2) +alarm_incomplete_1.DURATION = timedelta(hours=1) +alarm_incomplete_2 = Alarm() +alarm_incomplete_2.TRIGGER = timedelta(hours=2) +alarm_incomplete_2.REPEAT = 100 + +@pytest.mark.parametrize("alarm", [alarm_incomplete_1, alarm_incomplete_2]) +def test_alarm_has_only_one_of_repeat_or_duration(alarm): """This is an edge case and we should ignore the repetition.""" - pytest.skip("TODO") + a = Alarms(alarm) + a.set_start(datetime(2027, 12, 2)) + assert len(a.times) == 1 @pytest.fixture(params=[(0, timedelta(minutes=-30)), (1, timedelta(minutes=-25))]) From 3a61ba0c407c18b1bb60f59ba298316b52804c7c Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sat, 2 Nov 2024 13:16:04 +0000 Subject: [PATCH 36/50] remove todo --- src/icalendar/tests/test_issue_716_alarm_time_computation.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py index 45aaf8f6..376deebe 100644 --- a/src/icalendar/tests/test_issue_716_alarm_time_computation.py +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -197,11 +197,6 @@ def test_cannot_set_the_event_twice(calendars): a.add_component(calendars.alarm_google_future.subcomponents[-1]) -def test_alarms_from_calendar(): - pytest.skip("TODO") - - - @pytest.mark.parametrize( ("calendar", "index", "count", "message"), [ From 761a1664d9917eed15acdd4776c79e0e50120385 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sat, 2 Nov 2024 13:21:14 +0000 Subject: [PATCH 37/50] Test deleting values --- src/icalendar/alarms.py | 6 ++---- .../tests/test_issue_716_alarm_time_computation.py | 8 +++++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index 9b55e1f0..ff3988fe 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -282,8 +282,7 @@ def acknowledge_until(self, dt: Optional[date]) -> None: an event has been acknowledged because of an alarm. All alarms that happen before this time count as ackknowledged. """ - if dt is not None: - self._last_ack = tzp.localize_utc(dt) + self._last_ack = tzp.localize_utc(dt) if dt is not None else None def snooze_until(self, dt: Optional[date]) -> None: """This is the time in UTC when all the alarms of this component were snoozed. @@ -293,8 +292,7 @@ def snooze_until(self, dt: Optional[date]) -> None: The alarms are supposed to turn up again at dt when they are not acknowledged but snoozed. """ - if dt is not None: - self._snooze_until = tzp.localize_utc(dt) + self._snooze_until = tzp.localize_utc(dt) if dt is not None else None def set_local_timezone(self, tzinfo:Optional[tzinfo|str]): """Set the local timezone. diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py index 376deebe..e24a2fbb 100644 --- a/src/icalendar/tests/test_issue_716_alarm_time_computation.py +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -338,4 +338,10 @@ def test_rfc_9074_alarm_times(events, event_index, alarm_times): def test_set_to_None(): """acknowledge_until, snooze_until, set_local_timezone.""" - pytest.skip("TODO") + a = Alarms() + a.set_start(None) + a.set_end(None) + a.set_local_timezone(None) + a.acknowledge_until(None) + a.snooze_until(None) + assert vars(a) == vars(Alarms()) \ No newline at end of file From e74bebb5c4f8dd9da1f1e080dbeb510c9e06fe0d Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Thu, 14 Nov 2024 11:23:38 +0000 Subject: [PATCH 38/50] Update CHANGES.rst Co-authored-by: Steve Piercy --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 56b815a3..587b1bfd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -26,7 +26,7 @@ New features: - Add ``Component.DTSTAMP`` and ``Component.LAST_MODIFIED`` properties for datetime in UTC. - Add ``Component.is_thunderbird()`` to check if the component uses custom properties by Thunderbird. - Add ``X_MOZ_SNOOZE_TIME`` and ``X_MOZ_LASTACK`` properties to ``Event`` and ``Todo``. -- Add ``Alarm.ACKNOWLEDGED``, ``Alarm.TRIGGER``, ``Alarm.REPEAT`` and ``Alarm.DURATION`` properties +- Add ``Alarm.ACKNOWLEDGED``, ``Alarm.TRIGGER``, ``Alarm.REPEAT``, and ``Alarm.DURATION`` properties as well as ``Alarm.triggers`` to calculate alarm triggers. Bug fixes: From a3598d7865b72bc467d8f305b2b29200fb79de3b Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Thu, 14 Nov 2024 11:23:58 +0000 Subject: [PATCH 39/50] Update src/icalendar/alarms.py Co-authored-by: Steve Piercy --- src/icalendar/alarms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index ff3988fe..48f8f834 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -116,7 +116,7 @@ def parent(self) -> Optional[Parent]: def is_active(self) -> bool: """Whether this alarm is active (True) or acknowledged (False). - E.g. in some calendar software, this is True until the user had a look + For example, in some calendar software, this is True until the user looks at the alarm message and clicked the dismiss button. Alarms can be in local time (without a timezone). From 4e7b0facf9035b0192114291bd1917d02718c6b3 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Thu, 14 Nov 2024 11:24:10 +0000 Subject: [PATCH 40/50] Update src/icalendar/alarms.py Co-authored-by: Steve Piercy --- src/icalendar/alarms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index 48f8f834..c5ec4bc7 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -278,7 +278,7 @@ def acknowledge_until(self, dt: Optional[date]) -> None: Since RFC 9074 (Alarm Extension) was created later, calendar implementations differ in how they acknowledge alarms. - E.g. Thunderbird and Google Calendar store the last time + For example, Thunderbird and Google Calendar store the last time an event has been acknowledged because of an alarm. All alarms that happen before this time count as ackknowledged. """ From df9c811082035ba7a974683ae2afa0b5e93c9e8b Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Thu, 14 Nov 2024 11:24:19 +0000 Subject: [PATCH 41/50] Update src/icalendar/alarms.py Co-authored-by: Steve Piercy --- src/icalendar/alarms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/icalendar/alarms.py b/src/icalendar/alarms.py index c5ec4bc7..e8999d13 100644 --- a/src/icalendar/alarms.py +++ b/src/icalendar/alarms.py @@ -280,7 +280,7 @@ def acknowledge_until(self, dt: Optional[date]) -> None: calendar implementations differ in how they acknowledge alarms. For example, Thunderbird and Google Calendar store the last time an event has been acknowledged because of an alarm. - All alarms that happen before this time count as ackknowledged. + All alarms that happen before this time count as acknowledged. """ self._last_ack = tzp.localize_utc(dt) if dt is not None else None From bff242859d3f327d712311b634b6a546db290c27 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Thu, 14 Nov 2024 11:24:28 +0000 Subject: [PATCH 42/50] Update src/icalendar/tools.py Co-authored-by: Steve Piercy --- src/icalendar/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/icalendar/tools.py b/src/icalendar/tools.py index 24c05fab..b8b2f7f2 100644 --- a/src/icalendar/tools.py +++ b/src/icalendar/tools.py @@ -62,7 +62,7 @@ def is_pytz_dt(dt: date): def normalize_pytz(dt: date): """We have to normalize the time after a calculation if we use pytz. - pytz requires this funtion to be used in order to correctly calculate the + pytz requires this function to be used in order to correctly calculate the timezone's offset after calculations. """ if is_pytz_dt(dt): From ca2995bed4e03e089434ec945bd0b3a0c7b9bb88 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Thu, 14 Nov 2024 11:24:41 +0000 Subject: [PATCH 43/50] Update src/icalendar/tests/test_pytz_zoneinfo_integration.py Co-authored-by: Steve Piercy --- src/icalendar/tests/test_pytz_zoneinfo_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/icalendar/tests/test_pytz_zoneinfo_integration.py b/src/icalendar/tests/test_pytz_zoneinfo_integration.py index aad6d9a6..c93735e7 100644 --- a/src/icalendar/tests/test_pytz_zoneinfo_integration.py +++ b/src/icalendar/tests/test_pytz_zoneinfo_integration.py @@ -93,7 +93,7 @@ def test_cache_is_emptied_when_tzp_is_switched(tzp, timezones, new_tzp_name): def test_invalid_name(tzp): - """Check that the provider name is ok.""" + """Check that the provider name is OK.""" provider = "invalid_provider" with pytest.raises(ValueError) as e: tzp.use(provider) From 286d0ccb68b16b0109a0573ee92bd18e1f42b06b Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Thu, 14 Nov 2024 11:24:52 +0000 Subject: [PATCH 44/50] Update src/icalendar/cal.py Co-authored-by: Steve Piercy --- src/icalendar/cal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 95c081ee..c9c793f4 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -938,7 +938,7 @@ def alarms(self) -> Alarms: 0 Note that this only uses DTSTART and DUE but ignores - RDATE, EXDATE and RRULE properties. + RDATE, EXDATE, and RRULE properties. """ from icalendar.alarms import Alarms return Alarms(self) From a8b1ed86e242b2e96f768481251528fc57935abe Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Thu, 14 Nov 2024 11:25:02 +0000 Subject: [PATCH 45/50] Update src/icalendar/cal.py Co-authored-by: Steve Piercy --- src/icalendar/cal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index c9c793f4..69f9dd2c 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -716,7 +716,7 @@ def alarms(self) -> Alarms: >>> alarm_time.is_active() # This alarm has not been acknowledged True - Note that this only uses DTSTART and DTEND but ignores + Note that this only uses DTSTART and DTEND, but ignores RDATE, EXDATE and RRULE properties. """ from icalendar.alarms import Alarms From b9228e9229a520d03681f0d1c446fd5fb570946c Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Thu, 14 Nov 2024 11:25:10 +0000 Subject: [PATCH 46/50] Update src/icalendar/cal.py Co-authored-by: Steve Piercy --- src/icalendar/cal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 69f9dd2c..980d3c44 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -717,7 +717,7 @@ def alarms(self) -> Alarms: True Note that this only uses DTSTART and DTEND, but ignores - RDATE, EXDATE and RRULE properties. + RDATE, EXDATE, and RRULE properties. """ from icalendar.alarms import Alarms return Alarms(self) From c92b27275f88a994842abe5fb7dc024560c7d51a Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Thu, 14 Nov 2024 11:25:21 +0000 Subject: [PATCH 47/50] Update src/icalendar/cal.py Co-authored-by: Steve Piercy --- src/icalendar/cal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 980d3c44..52f2e898 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -568,7 +568,7 @@ def __eq__(self, other): """) def is_thunderbird(self) -> bool: - """Whether this component has attributes that indicate that Mozilla Thunderbird createsd it.""" + """Whether this component has attributes that indicate that Mozilla Thunderbird created it.""" return any(attr.startswith("X-MOZ-") for attr in self.keys()) From b63e9f116968b79b88b4a17aace00c662c4619b8 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Thu, 14 Nov 2024 11:25:32 +0000 Subject: [PATCH 48/50] Update src/icalendar/cal.py Co-authored-by: Steve Piercy --- src/icalendar/cal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 52f2e898..eeabb0d8 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -937,7 +937,7 @@ def alarms(self) -> Alarms: >>> len(todo.alarms.times) 0 - Note that this only uses DTSTART and DUE but ignores + Note that this only uses DTSTART and DUE, but ignores RDATE, EXDATE, and RRULE properties. """ from icalendar.alarms import Alarms From a44d15179ed974615b090b3d07effba62f3f8f41 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Thu, 14 Nov 2024 11:25:46 +0000 Subject: [PATCH 49/50] Update src/icalendar/tests/test_issue_716_alarm_time_computation.py Co-authored-by: Steve Piercy --- src/icalendar/tests/test_issue_716_alarm_time_computation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/icalendar/tests/test_issue_716_alarm_time_computation.py b/src/icalendar/tests/test_issue_716_alarm_time_computation.py index e24a2fbb..3f31368f 100644 --- a/src/icalendar/tests/test_issue_716_alarm_time_computation.py +++ b/src/icalendar/tests/test_issue_716_alarm_time_computation.py @@ -8,7 +8,7 @@ - snoozed - the user moved that alarm to another time The alarms can only work on the properties of the event like -DTSTART, DTEND and DURATION. +DTSTART, DTEND, and DURATION. """ From bdfd9d2a041c97fe7d259b85bfc4e36e3f34336d Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Thu, 14 Nov 2024 11:26:01 +0000 Subject: [PATCH 50/50] Update src/icalendar/cal.py Co-authored-by: Steve Piercy --- src/icalendar/cal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index eeabb0d8..8522d24f 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -1277,7 +1277,7 @@ def REPEAT(self, value: int) -> None: sees them. For those kinds of alarms, the client SHOULD set this property when the alarm is triggered and the action is successfully carried out. - When an alarm is triggered on a client, clients can check to see if an"ACKNOWLEDGED" + When an alarm is triggered on a client, clients can check to see if an "ACKNOWLEDGED" property is present. If it is, and the value of that property is greater than or equal to the computed trigger time for the alarm, then the client SHOULD NOT trigger the alarm. Similarly, if an alarm has been triggered and