diff --git a/example/test_calendar.yaml b/example/test_calendar.yaml index ea8d23d..e34aea7 100644 --- a/example/test_calendar.yaml +++ b/example/test_calendar.yaml @@ -33,6 +33,18 @@ events: weeks: 1 until: 2022-12-31 # required + - 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 begin: 2021-04-22 diff --git a/tests/test_events.py b/tests/test_events.py index 26ac026..9be236b 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -65,6 +65,33 @@ def test_rrule(): assert "RRULE:FREQ=YEARLY;UNTIL=20300422T000000" 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(): event = event_from_yaml( parse_yaml( diff --git a/yaml2ics.py b/yaml2ics.py index 272f420..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: @@ -90,7 +97,28 @@ def event_from_yaml(event_yaml: dict, tz: tzinfo = None) -> ics.Event: ) ) - event.dtstamp = datetime.utcnow().replace(tzinfo=dateutil.tz.UTC) + if "except_on" in repeat: + exdates = map( + lambda exdate: strfexception(exdate), + repeat["except_on"], + ) + 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.datetime.utcnow().replace(tzinfo=dateutil.tz.UTC) if tz and event.floating and not event.all_day: event.replace_timezone(tz) return event