From e714a58564daa655ed5616078a2115314f864c01 Mon Sep 17 00:00:00 2001 From: Eduard Itrich Date: Tue, 28 Jun 2022 10:15:32 +0200 Subject: [PATCH 1/5] Loop through all except_on dates and remove them from recurrent event Signed-off-by: Eduard Itrich --- example/test_calendar.yaml | 1 + tests/test_events.py | 4 +++- yaml2ics.py | 9 +++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/example/test_calendar.yaml b/example/test_calendar.yaml index ea8d23d..8da51d3 100644 --- a/example/test_calendar.yaml +++ b/example/test_calendar.yaml @@ -32,6 +32,7 @@ events: # seconds, minutes, hours, days, weeks, months, years weeks: 1 until: 2022-12-31 # required + except_on: [2022-12-19, 2022-12-26] # All-day event - summary: Earth Day diff --git a/tests/test_events.py b/tests/test_events.py index 26ac026..7eeeadd 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -56,6 +56,7 @@ def test_rrule(): interval: years: 1 until: 2030-04-22 + except_on: [2022-04-22, 2026-04-22] """ ) ) @@ -63,7 +64,8 @@ def test_rrule(): # DTEND exists and is the next day, calendar tools import this # correctly as being a one-day event assert "RRULE:FREQ=YEARLY;UNTIL=20300422T000000" in event_str - + assert "EXDATE:20220422" in event_str + assert "EXDATE:20260422" in event_str def test_event_with_time_range(): event = event_from_yaml( diff --git a/yaml2ics.py b/yaml2ics.py index 272f420..301b166 100644 --- a/yaml2ics.py +++ b/yaml2ics.py @@ -90,6 +90,15 @@ def event_from_yaml(event_yaml: dict, tz: tzinfo = None) -> ics.Event: ) ) + if "except_on" in repeat: + for exdate in repeat.get("except_on"): + event.extra.append( + ics.ContentLine( + name="EXDATE", + value=datetime.strftime(exdate, "%Y%m%d"), + ) + ) + event.dtstamp = datetime.utcnow().replace(tzinfo=dateutil.tz.UTC) if tz and event.floating and not event.all_day: event.replace_timezone(tz) From b8e85ddd7e60f4126d0a610af860e250514901f8 Mon Sep 17 00:00:00 2001 From: Eduard Itrich Date: Wed, 29 Jun 2022 09:04:18 +0200 Subject: [PATCH 2/5] Fix CI errors --- tests/test_events.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_events.py b/tests/test_events.py index 7eeeadd..3dbe84d 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -67,6 +67,7 @@ def test_rrule(): assert "EXDATE:20220422" in event_str assert "EXDATE:20260422" in event_str + def test_event_with_time_range(): event = event_from_yaml( parse_yaml( From 97d1e0a49966c8d37c05d3d10958fb4e6ccb282e Mon Sep 17 00:00:00 2001 From: Eduard Itrich Date: Thu, 30 Jun 2022 22:44:53 +0200 Subject: [PATCH 3/5] Combine EXDATE into a comma seperated list Signed-off-by: Eduard Itrich --- tests/test_events.py | 3 +-- yaml2ics.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/test_events.py b/tests/test_events.py index 3dbe84d..521cf3d 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -64,8 +64,7 @@ def test_rrule(): # DTEND exists and is the next day, calendar tools import this # correctly as being a one-day event assert "RRULE:FREQ=YEARLY;UNTIL=20300422T000000" in event_str - assert "EXDATE:20220422" in event_str - assert "EXDATE:20260422" in event_str + assert "EXDATE:20220422,20260422" in event_str def test_event_with_time_range(): diff --git a/yaml2ics.py b/yaml2ics.py index 301b166..e98d92e 100644 --- a/yaml2ics.py +++ b/yaml2ics.py @@ -91,13 +91,15 @@ def event_from_yaml(event_yaml: dict, tz: tzinfo = None) -> ics.Event: ) if "except_on" in repeat: - for exdate in repeat.get("except_on"): - event.extra.append( - ics.ContentLine( - name="EXDATE", - value=datetime.strftime(exdate, "%Y%m%d"), - ) + exdate = map( + lambda exdate: datetime.strftime(exdate, "%Y%m%d"), repeat["except_on"] + ) + event.extra.append( + ics.ContentLine( + name="EXDATE", + value=",".join(exdate), ) + ) event.dtstamp = datetime.utcnow().replace(tzinfo=dateutil.tz.UTC) if tz and event.floating and not event.all_day: From 3d36efa6adfd76efa69bcecfa564b416f87bf0e2 Mon Sep 17 00:00:00 2001 From: Eduard Itrich Date: Thu, 30 Jun 2022 23:01:45 +0200 Subject: [PATCH 4/5] Add time to EXDATE --- yaml2ics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/yaml2ics.py b/yaml2ics.py index e98d92e..1959307 100644 --- a/yaml2ics.py +++ b/yaml2ics.py @@ -92,7 +92,8 @@ def event_from_yaml(event_yaml: dict, tz: tzinfo = None) -> ics.Event: if "except_on" in repeat: exdate = map( - lambda exdate: datetime.strftime(exdate, "%Y%m%d"), repeat["except_on"] + lambda exdate: datetime.strftime(exdate, "%Y%m%dT%H%M%S"), + repeat["except_on"], ) event.extra.append( ics.ContentLine( From f166cf19786bec49954a3b8616655f461883e32b Mon Sep 17 00:00:00 2001 From: Eduard Itrich Date: Fri, 1 Jul 2022 07:58:49 +0200 Subject: [PATCH 5/5] Add TZID to EXDATE if timezone is given --- example/test_calendar.yaml | 13 ++++++++++++- tests/test_events.py | 29 +++++++++++++++++++++++++++-- yaml2ics.py | 36 ++++++++++++++++++++++++++---------- 3 files changed, 65 insertions(+), 13 deletions(-) diff --git a/example/test_calendar.yaml b/example/test_calendar.yaml index 8da51d3..e34aea7 100644 --- a/example/test_calendar.yaml +++ b/example/test_calendar.yaml @@ -32,7 +32,18 @@ events: # seconds, minutes, hours, days, weeks, months, years weeks: 1 until: 2022-12-31 # required - except_on: [2022-12-19, 2022-12-26] + + - summary: Recurring event with exception + begin: 2022-07-01 10:00:00 + duration: {minutes: 60} + repeat: + interval: + # seconds, minutes, hours, days, weeks, months, years + hours: 4 + until: 2022-07-31 # required + except_on: + - 2022-07-13 + - 2022-07-14 06:00:00 # All-day event - summary: Earth Day diff --git a/tests/test_events.py b/tests/test_events.py index 521cf3d..9be236b 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -56,7 +56,6 @@ def test_rrule(): interval: years: 1 until: 2030-04-22 - except_on: [2022-04-22, 2026-04-22] """ ) ) @@ -64,7 +63,33 @@ def test_rrule(): # DTEND exists and is the next day, calendar tools import this # correctly as being a one-day event assert "RRULE:FREQ=YEARLY;UNTIL=20300422T000000" in event_str - assert "EXDATE:20220422,20260422" in event_str + + +def test_exception(): + event = event_from_yaml( + parse_yaml( + """ + summary: Recurring event with exception + timezone: America/Los_Angeles + begin: 2022-07-01 10:00:00 + duration: {minutes: 60} + repeat: + interval: + hours: 4 + until: 2022-07-31 + except_on: + - 2022-07-13 + - 2022-07-14 06:00:00 + """ + ) + ) + event_str = event.serialize() + # DTEND exists and is the next day, calendar tools import this + # correctly as being a one-day event + assert ( + "EXDATE;TZID=/ics.py/2020.1/America/Los_Angeles:20220713,20220714T060000" + in event_str + ) def test_event_with_time_range(): diff --git a/yaml2ics.py b/yaml2ics.py index 1959307..50fd4b5 100644 --- a/yaml2ics.py +++ b/yaml2ics.py @@ -4,9 +4,9 @@ CLI to convert yaml into ics. """ +import datetime import os import sys -from datetime import datetime, tzinfo import dateutil import dateutil.rrule @@ -25,7 +25,14 @@ } -def event_from_yaml(event_yaml: dict, tz: tzinfo = None) -> ics.Event: +def strfexception(exdate): + if isinstance(exdate, datetime.datetime): + return datetime.datetime.strftime(exdate, "%Y%m%dT%H%M%S") + elif isinstance(exdate, datetime.date): + return datetime.datetime.strftime(exdate, "%Y%m%d") + + +def event_from_yaml(event_yaml: dict, tz: datetime.tzinfo = None) -> ics.Event: d = event_yaml repeat = d.pop("repeat", None) if "timezone" in d: @@ -91,18 +98,27 @@ def event_from_yaml(event_yaml: dict, tz: tzinfo = None) -> ics.Event: ) if "except_on" in repeat: - exdate = map( - lambda exdate: datetime.strftime(exdate, "%Y%m%dT%H%M%S"), + exdates = map( + lambda exdate: strfexception(exdate), repeat["except_on"], ) - event.extra.append( - ics.ContentLine( - name="EXDATE", - value=",".join(exdate), + if tz: + event.extra.append( + ics.ContentLine( + name="EXDATE", + params={"TZID": [str(ics.Timezone.from_tzinfo(tz))]}, + value=",".join(exdates), + ) + ) + else: + event.extra.append( + ics.ContentLine( + name="EXDATE", + value=",".join(exdates), + ) ) - ) - event.dtstamp = datetime.utcnow().replace(tzinfo=dateutil.tz.UTC) + event.dtstamp = datetime.datetime.utcnow().replace(tzinfo=dateutil.tz.UTC) if tz and event.floating and not event.all_day: event.replace_timezone(tz) return event