kopia lustrzana https://github.com/jazzband/icalevents
feat: handle timezone and recurring events
rodzic
c368ad8ec4
commit
eaaa26320e
|
@ -14,6 +14,7 @@ from icalendar import Calendar
|
|||
from icalendar.windows_to_olson import WINDOWS_TO_OLSON
|
||||
from icalendar.prop import vDDDLists, vText
|
||||
from pytz import timezone
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
def now():
|
||||
|
@ -47,6 +48,7 @@ class Event:
|
|||
self.created = None
|
||||
self.last_modified = None
|
||||
self.sequence = None
|
||||
self.recurrence_id = None
|
||||
self.attendee = None
|
||||
self.organizer = None
|
||||
self.categories = None
|
||||
|
@ -199,6 +201,8 @@ def create_event(component, tz=UTC):
|
|||
|
||||
if component.get("uid"):
|
||||
event.uid = component.get("uid").encode("utf-8").decode("ascii")
|
||||
else:
|
||||
event.uid = str(uuid4()) # Be nice - treat every event as unique
|
||||
|
||||
if component.get("organizer"):
|
||||
event.organizer = component.get("organizer").encode("utf-8").decode("ascii")
|
||||
|
@ -215,6 +219,9 @@ def create_event(component, tz=UTC):
|
|||
if component.get("created"):
|
||||
event.created = normalize(component.get("created").dt, tz)
|
||||
|
||||
if component.get("RECURRENCE-ID"):
|
||||
event.recurrence_id = normalize(component.get("RECURRENCE-ID").dt, tz)
|
||||
|
||||
if component.get("last-modified"):
|
||||
event.last_modified = normalize(component.get("last-modified").dt, tz)
|
||||
elif event.created:
|
||||
|
@ -342,19 +349,13 @@ def parse_events(content, start=None, end=None, default_span=timedelta(days=7)):
|
|||
end = normalize(end, cal_tz)
|
||||
|
||||
found = []
|
||||
recurrence_ids = []
|
||||
|
||||
# Skip dates that are stored as exceptions.
|
||||
exceptions = {}
|
||||
for component in calendar.walk():
|
||||
# Skip dates that are stored as exceptions.
|
||||
exceptions = {}
|
||||
|
||||
if component.name == "VEVENT":
|
||||
e = create_event(component, cal_tz)
|
||||
|
||||
if "RECURRENCE-ID" in component:
|
||||
recurrence_ids.append(
|
||||
(e.uid, component["RECURRENCE-ID"].dt, e.sequence)
|
||||
)
|
||||
|
||||
if "EXDATE" in component:
|
||||
# Deal with the fact that sometimes it's a list and
|
||||
# sometimes it's a singleton
|
||||
|
@ -371,7 +372,6 @@ def parse_events(content, start=None, end=None, default_span=timedelta(days=7)):
|
|||
# and end times. If the timezone is defined in the calendar,
|
||||
# use it; otherwise, attempt to load the rules from pytz.
|
||||
start_tz = None
|
||||
end_tz = None
|
||||
|
||||
if e.start.tzinfo != UTC:
|
||||
if str(e.start.tzinfo) in timezones:
|
||||
|
@ -379,12 +379,6 @@ def parse_events(content, start=None, end=None, default_span=timedelta(days=7)):
|
|||
else:
|
||||
start_tz = e.start.tzinfo
|
||||
|
||||
if e.end.tzinfo != UTC:
|
||||
if str(e.end.tzinfo) in timezones:
|
||||
end_tz = timezones[str(e.end.tzinfo)]
|
||||
else:
|
||||
end_tz = e.end.tzinfo
|
||||
|
||||
# If we've been passed or constructed start/end values
|
||||
# that are timezone naive, but the actual appointment
|
||||
# start and end times are in a timezone, convert start
|
||||
|
@ -397,12 +391,16 @@ def parse_events(content, start=None, end=None, default_span=timedelta(days=7)):
|
|||
end = normalize(end, e.start.tzinfo)
|
||||
|
||||
duration = e.end - e.start
|
||||
|
||||
event_start = component.get("dtstart").dt
|
||||
rule_start_time_zone = cal_tz
|
||||
if type(event_start) is datetime and event_start.tzinfo:
|
||||
rule_start_time_zone = component.get("dtstart").dt.tzinfo
|
||||
if e.recurring:
|
||||
# Unfold recurring events according to their rrule
|
||||
rule = parse_rrule(component, cal_tz)
|
||||
[after] = adjust_timezone(component, [start - duration], start_tz)
|
||||
rule = parse_rrule(component, rule_start_time_zone)
|
||||
[after] = adjust_timezone(component, [start], start_tz)
|
||||
[end] = adjust_timezone(component, [end], start_tz)
|
||||
|
||||
for dt in rule.between(after, end, inc=True):
|
||||
if start_tz is None:
|
||||
# Shrug. If we couldn't work out the timezone, it is what it is.
|
||||
|
@ -415,7 +413,6 @@ def parse_events(content, start=None, end=None, default_span=timedelta(days=7)):
|
|||
dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second
|
||||
)
|
||||
dtstart = normalize(naive, tz=start_tz)
|
||||
|
||||
ecopy = e.copy_to(dtstart, e.uid)
|
||||
|
||||
# We're effectively looping over the start time, we might need
|
||||
|
@ -430,19 +427,23 @@ def parse_events(content, start=None, end=None, default_span=timedelta(days=7)):
|
|||
ecopy.start.month,
|
||||
ecopy.start.day,
|
||||
)
|
||||
|
||||
if exdate not in exceptions:
|
||||
found.append(ecopy)
|
||||
elif e.end >= start and e.start <= end:
|
||||
exdate = "%04d%02d%02d" % (e.start.year, e.start.month, e.start.day)
|
||||
if exdate not in exceptions:
|
||||
found.append(e)
|
||||
# Filter out all events that are moved as indicated by the recurrence-id prop
|
||||
return [
|
||||
event
|
||||
for event in found
|
||||
if e.sequence is None
|
||||
or not (event.uid, event.start, e.sequence) in recurrence_ids
|
||||
]
|
||||
|
||||
result = found.copy()
|
||||
|
||||
for event in found:
|
||||
if not event.recurrence_id and (event.uid, event.start) in [
|
||||
(f.uid, f.recurrence_id) for f in found
|
||||
]:
|
||||
result.remove(event)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def parse_rrule(component, tz=UTC):
|
||||
|
@ -469,25 +470,37 @@ def parse_rrule(component, tz=UTC):
|
|||
# Remove/add timezone to rrule until dates depending on component
|
||||
for index, rru in enumerate(rrules):
|
||||
if "UNTIL" in rru:
|
||||
rrules[index]["UNTIL"] = adjust_timezone(component, rru["UNTIL"], tz)
|
||||
if type(rdtstart) is date:
|
||||
rrules[index]["UNTIL"] = [
|
||||
normalize(until, tz).date() for until in rrules[index]["UNTIL"]
|
||||
]
|
||||
else:
|
||||
# Handle summer/winter time
|
||||
rrules[index]["UNTIL"] = [
|
||||
normalize(until, UTC)
|
||||
+ tz.utcoffset(component["dtstart"].dt, is_dst=True)
|
||||
for until in rrules[index]["UNTIL"]
|
||||
]
|
||||
|
||||
# Parse the rrules, might return a rruleset instance, instead of rrule
|
||||
rule = rrulestr(
|
||||
"\n".join(x.to_ical().decode() for x in rrules), dtstart=rdtstart
|
||||
"\n".join(x.to_ical().decode() for x in rrules),
|
||||
dtstart=rdtstart,
|
||||
forceset=True,
|
||||
unfold=True,
|
||||
)
|
||||
|
||||
if component.get("exdate"):
|
||||
# Make sure, to work with a rruleset
|
||||
if isinstance(rule, rrule):
|
||||
rules = rruleset()
|
||||
rules.rrule(rule)
|
||||
rule = rules
|
||||
|
||||
# Add exdates to the rruleset
|
||||
for exd in extract_exdates(component):
|
||||
rule.exdate(exd)
|
||||
|
||||
# TODO: What about rdates and exrules?
|
||||
if component.get("exrule"):
|
||||
pass
|
||||
|
||||
if component.get("rdate"):
|
||||
pass
|
||||
|
||||
# You really want an rrule for a component without rrule? Here you are.
|
||||
else:
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -0,0 +1,43 @@
|
|||
BEGIN:VCALENDAR
|
||||
METHOD:PUBLISH
|
||||
PRODID:Microsoft Exchange Server 2010
|
||||
VERSION:2.0
|
||||
X-WR-CALNAME:test
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:W. Europe Standard Time
|
||||
BEGIN:STANDARD
|
||||
DTSTART:16010101T030000
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:16010101T020000
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
RRULE:FREQ=DAILY;UNTIL=20221111T093000Z;INTERVAL=1
|
||||
UID:040000008200E00074C5B7101A82E00800000000B47AD3E5FED6D701000000000000000
|
||||
01000000025C7E472027AAB44AD1F2F048390CB09
|
||||
SUMMARY:Busy
|
||||
DTSTART;TZID=W. Europe Standard Time:20211111T103000
|
||||
DTEND;TZID=W. Europe Standard Time:20211111T110000
|
||||
CLASS:PUBLIC
|
||||
PRIORITY:5
|
||||
DTSTAMP:20211111T132438Z
|
||||
TRANSP:OPAQUE
|
||||
STATUS:CONFIRMED
|
||||
SEQUENCE:0
|
||||
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:1
|
||||
X-MICROSOFT-DONOTFORWARDMEETING:FALSE
|
||||
X-MICROSOFT-DISALLOW-COUNTER:FALSE
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,43 @@
|
|||
BEGIN:VCALENDAR
|
||||
METHOD:PUBLISH
|
||||
PRODID:Microsoft Exchange Server 2010
|
||||
VERSION:2.0
|
||||
X-WR-CALNAME:test2
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:W. Europe Standard Time
|
||||
BEGIN:STANDARD
|
||||
DTSTART:16010101T030000
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:16010101T020000
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
RRULE:FREQ=DAILY;UNTIL=20221110T230000Z;INTERVAL=2
|
||||
UID:040000008200E00074C5B7101A82E0080000000072B908E2FFD6D701000000000000000
|
||||
010000000F1F9FF2029BC6F438B081675570BC889
|
||||
SUMMARY:Free
|
||||
DTSTART;VALUE=DATE:20211111
|
||||
DTEND;VALUE=DATE:20211112
|
||||
CLASS:PUBLIC
|
||||
PRIORITY:5
|
||||
DTSTAMP:20211111T132808Z
|
||||
TRANSP:TRANSPARENT
|
||||
STATUS:CONFIRMED
|
||||
SEQUENCE:0
|
||||
X-MICROSOFT-CDO-APPT-SEQUENCE:0
|
||||
X-MICROSOFT-CDO-BUSYSTATUS:FREE
|
||||
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
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,44 @@
|
|||
BEGIN:VCALENDAR
|
||||
METHOD:PUBLISH
|
||||
PRODID:Microsoft Exchange Server 2010
|
||||
VERSION:2.0
|
||||
X-WR-CALNAME:test3
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:W. Europe Standard Time
|
||||
BEGIN:STANDARD
|
||||
DTSTART:16010101T030000
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:16010101T020000
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
RRULE:FREQ=DAILY;UNTIL=20211115T070000Z;INTERVAL=1
|
||||
EXDATE;TZID=W. Europe Standard Time:20211112T080000,20211114T080000
|
||||
UID:040000008200E00074C5B7101A82E0080000000008EA7A7A00D7D701000000000000000
|
||||
010000000943ACF1BD16B5145AAD2954EF56FF236
|
||||
SUMMARY:Busy
|
||||
DTSTART;TZID=W. Europe Standard Time:20211111T080000
|
||||
DTEND;TZID=W. Europe Standard Time:20211111T083000
|
||||
CLASS:PUBLIC
|
||||
PRIORITY:5
|
||||
DTSTAMP:20211111T133243Z
|
||||
TRANSP:OPAQUE
|
||||
STATUS:CONFIRMED
|
||||
SEQUENCE:0
|
||||
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:1
|
||||
X-MICROSOFT-DONOTFORWARDMEETING:FALSE
|
||||
X-MICROSOFT-DISALLOW-COUNTER:FALSE
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -41,7 +41,7 @@ BEGIN:VEVENT
|
|||
DTSTART;VALUE=DATE:20170713
|
||||
DTEND;VALUE=DATE:20170714
|
||||
DTSTAMP:20170711T171222Z
|
||||
UID:0eedefedba891fcbb49dcfa4279d9d93
|
||||
UID:0eedefedba891fcbb49dcfa4279d9d94
|
||||
CREATED:20170104T080401Z
|
||||
DESCRIPTION:graue Restmülltonne nicht vergessen!
|
||||
LOCATION:Luitpoldstraße\, Treuchtlingen
|
||||
|
@ -54,7 +54,7 @@ BEGIN:VEVENT
|
|||
DTSTART;VALUE=DATE:20170714
|
||||
DTEND;VALUE=DATE:20170715
|
||||
DTSTAMP:20170711T171222Z
|
||||
UID:0eedefedba891fcbb49dcfa4279d9d93
|
||||
UID:0eedefedba891fcbb49dcfa4279d9d95
|
||||
DESCRIPTION:graue Restmülltonne nicht vergessen!
|
||||
LOCATION:Luitpoldstraße\, Treuchtlingen
|
||||
SEQUENCE:0
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
X-WR-CALNAME:email@example.com
|
||||
X-WR-TIMEZONE:Australia/Sydney
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Australia/Sydney
|
||||
X-LIC-LOCATION:Australia/Sydney
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:+1100
|
||||
TZOFFSETTO:+1000
|
||||
TZNAME:AEST
|
||||
DTSTART:19700405T030000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:+1000
|
||||
TZOFFSETTO:+1100
|
||||
TZNAME:AEDT
|
||||
DTSTART:19701004T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Australia/Melbourne
|
||||
X-LIC-LOCATION:Australia/Melbourne
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:+1100
|
||||
TZOFFSETTO:+1000
|
||||
TZNAME:AEST
|
||||
DTSTART:19700405T030000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:+1000
|
||||
TZOFFSETTO:+1100
|
||||
TZNAME:AEDT
|
||||
DTSTART:19701004T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=Australia/Sydney:20211017T090000
|
||||
DTEND;TZID=Australia/Sydney:20211017T095000
|
||||
RRULE:FREQ=WEEKLY;BYDAY=SU
|
||||
DTSTAMP:20211025T060625Z
|
||||
UID:random_string@google.com
|
||||
CLASS:PRIVATE
|
||||
CREATED:20211020T015841Z
|
||||
DESCRIPTION:
|
||||
LAST-MODIFIED:20211020T015856Z
|
||||
LOCATION:
|
||||
SEQUENCE:1
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:Event Name
|
||||
TRANSP:TRANSPARENT
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -407,7 +407,7 @@ class ICalEventsTests(unittest.TestCase):
|
|||
|
||||
evs = icalevents.events(file=ical, start=start, end=end)
|
||||
|
||||
self.assertEqual(len(evs), 41, "41 events in total - one was moved")
|
||||
self.assertEqual(len(evs), 42, "42 events in total - one was moved")
|
||||
|
||||
def test_recurence_id_google(self):
|
||||
ical = "test/test_data/recurrenceid_google.ics"
|
||||
|
@ -420,12 +420,71 @@ class ICalEventsTests(unittest.TestCase):
|
|||
|
||||
def test_cest(self):
|
||||
ical = "test/test_data/cest.ics"
|
||||
start = date(2021, 1, 1)
|
||||
end = date(2021, 12, 31)
|
||||
start = date(2010, 1, 1)
|
||||
end = date(2023, 12, 31)
|
||||
|
||||
evs = icalevents.events(file=ical, start=start, end=end)
|
||||
|
||||
self.assertEqual(len(evs), 115, "4 events in total")
|
||||
self.assertEqual(
|
||||
len(evs), 239, "239 events in total"
|
||||
) # 102 events / 91 + 11 recurring
|
||||
|
||||
def test_cest_2021_03(self):
|
||||
ical = "test/test_data/cest.ics"
|
||||
start = date(2021, 3, 1)
|
||||
end = date(2021, 3, 31)
|
||||
|
||||
evs = icalevents.events(file=ical, start=start, end=end)
|
||||
self.assertEqual(len(evs), 30, "30 in march")
|
||||
|
||||
def test_cest_2021_04(self):
|
||||
ical = "test/test_data/cest.ics"
|
||||
start = date(2021, 4, 1)
|
||||
end = date(2021, 5, 1)
|
||||
|
||||
evs = icalevents.events(file=ical, start=start, end=end)
|
||||
self.assertEqual(len(evs), 27, "27 in april")
|
||||
|
||||
def test_cest_2021_05(self):
|
||||
ical = "test/test_data/cest.ics"
|
||||
start = date(2021, 5, 1)
|
||||
end = date(2021, 6, 1)
|
||||
|
||||
evs = icalevents.events(file=ical, start=start, end=end)
|
||||
self.assertEqual(len(evs), 20, "20 in mai")
|
||||
|
||||
def test_cest_1(self):
|
||||
ical = "test/test_data/cest_every_day_for_one_year.ics"
|
||||
start = date(2020, 1, 1)
|
||||
end = date(2024, 12, 31)
|
||||
|
||||
evs = icalevents.events(file=ical, start=start, end=end)
|
||||
|
||||
self.assertEqual(
|
||||
len(evs),
|
||||
366,
|
||||
"366 events in total - one year + 1 (2021-11-11 to 2022-11-11)",
|
||||
)
|
||||
|
||||
def test_cest_2(self):
|
||||
ical = "test/test_data/cest_every_second_day_for_one_year.ics"
|
||||
start = date(2020, 1, 1)
|
||||
end = date(2024, 12, 31)
|
||||
|
||||
evs = icalevents.events(file=ical, start=start, end=end)
|
||||
self.assertEqual(
|
||||
len(evs), 183, "183 events in total - one year (2021-11-11 to 2022-11-11)"
|
||||
)
|
||||
|
||||
def test_cest_3(self):
|
||||
ical = "test/test_data/cest_with_deleted.ics"
|
||||
start = date(2020, 1, 1)
|
||||
end = date(2024, 12, 31)
|
||||
|
||||
evs = icalevents.events(file=ical, start=start, end=end)
|
||||
self.assertEqual(
|
||||
len(evs), 3, "3 events in total - 5 events in rrule but 2 deleted"
|
||||
)
|
||||
|
||||
def test_transparent(self):
|
||||
ical = "test/test_data/transparent.ics"
|
||||
|
@ -450,3 +509,14 @@ class ICalEventsTests(unittest.TestCase):
|
|||
self.assertEqual(ev3.status, "CANCELLED")
|
||||
self.assertEqual(ev4.status, "CANCELLED")
|
||||
self.assertEqual(ev5.status, None)
|
||||
|
||||
def test_recurrence_tz(self):
|
||||
ical = "test/test_data/recurrence_tz.ics"
|
||||
start = datetime(2021, 10, 24, 00, 0, 0, tzinfo=gettz("Australia/Sydney"))
|
||||
end = datetime(2021, 10, 26, 00, 0, 0, tzinfo=gettz("Australia/Sydney"))
|
||||
|
||||
[e1] = icalevents.events(file=ical, start=start, end=end)
|
||||
expect = datetime(2021, 10, 24, 9, 0, 0, tzinfo=gettz("Australia/Sydney"))
|
||||
self.assertEqual(
|
||||
e1.start, expect, "Recurring event matches event in ical (Issue #89)"
|
||||
)
|
||||
|
|
Ładowanie…
Reference in New Issue