Merge pull request #95 from jazzband/recurring-events-and-timezones

handle timezone and recurring events
pull/114/head
elizaaverywilson 2022-09-16 15:25:07 -05:00 zatwierdzone przez GitHub
commit 01ee1e31ac
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
15 zmienionych plików z 7285 dodań i 54 usunięć

Wyświetl plik

@ -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():
@ -25,6 +26,18 @@ def now():
return datetime.now(UTC)
class Attendee(str):
def __init__(self, address):
self.address = address
def __repr__(self):
return self.address.encode("utf-8").decode("ascii")
@property
def params(self):
return self.address.params
class Event:
"""
Represents one event (occurrence in case of reoccurring events).
@ -47,9 +60,11 @@ 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
self.floating = None
self.status = None
self.url = None
@ -143,6 +158,7 @@ class Event:
ne.created = self.created
ne.last_modified = self.last_modified
ne.categories = self.categories
ne.floating = self.floating
ne.status = self.status
ne.url = self.url
@ -158,7 +174,7 @@ def encode(value: Optional[vText]) -> Optional[str]:
return str(value.encode("utf-8"))
def create_event(component, tz=UTC):
def create_event(component, utc_default, tz=UTC):
"""
Create an event from its iCal representation.
@ -170,6 +186,9 @@ def create_event(component, tz=UTC):
event = Event()
event.start = normalize(component.get("dtstart").dt, tz=tz)
# The RFC specifies that the TZID parameter must be specified for datetime or time
# Otherwise we set a default timezone (if only one is set with VTIMEZONE) or utc
event.floating = type(component.get("dtstart").dt) == date and utc_default
if component.get("dtend"):
event.end = normalize(component.get("dtend").dt, tz=tz)
@ -188,17 +207,16 @@ def create_event(component, tz=UTC):
if component.get("attendee"):
event.attendee = component.get("attendee")
if type(event.attendee) is list:
temp = []
for a in event.attendee:
temp.append(a.encode("utf-8").decode("ascii"))
event.attendee = temp
event.attendee = [Attendee(attendee) for attendee in event.attendee]
else:
event.attendee = event.attendee.encode("utf-8").decode("ascii")
event.attendee = Attendee(event.attendee)
else:
event.attendee = str(None)
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")
@ -209,12 +227,15 @@ def create_event(component, tz=UTC):
event_class = component.get("class")
event.private = event_class == "PRIVATE" or event_class == "CONFIDENTIAL"
if component.get("class"):
if component.get("transp"):
event.transparent = component.get("transp") == "TRANSPARENT"
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:
@ -333,27 +354,23 @@ def parse_events(content, start=None, end=None, default_span=timedelta(days=7)):
# If there's exactly one timezone in the file,
# assume it applies globally, otherwise UTC
utc_default = False
if len(timezones) == 1:
cal_tz = get_timezone(list(timezones)[0])
else:
utc_default = True
cal_tz = UTC
start = normalize(start, cal_tz)
end = normalize(end, cal_tz)
found = []
recurrence_ids = []
# Skip dates that are stored as exceptions.
exceptions = {}
for component in calendar.walk():
if component.name == "VEVENT":
e = create_event(component, cal_tz)
# Skip dates that are stored as exceptions.
exceptions = {}
if "RECURRENCE-ID" in component:
recurrence_ids.append(
(e.uid, component["RECURRENCE-ID"].dt, e.sequence)
)
if component.name == "VEVENT":
e = create_event(component, utc_default, cal_tz)
if "EXDATE" in component:
# Deal with the fact that sometimes it's a list and
@ -371,7 +388,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 +395,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 +407,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 +429,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 +443,38 @@ 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:
if (
type(e.recurrence_id) == datetime
and type(component.get("dtstart").dt) == datetime
):
naive = datetime(
e.recurrence_id.year,
e.recurrence_id.month,
e.recurrence_id.day,
e.recurrence_id.hour,
e.recurrence_id.minute,
e.recurrence_id.second,
)
e.recurrence_id = normalize(
naive, tz=component.get("dtstart").dt.tzinfo
)
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 +501,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:

8
poetry.lock wygenerowano
Wyświetl plik

@ -146,7 +146,7 @@ pyparsing = ">=2.4.2,<3"
[[package]]
name = "icalendar"
version = "4.0.8"
version = "4.0.9"
description = "iCalendar parser/generator"
category = "main"
optional = false
@ -390,7 +390,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
content-hash = "df78c6613912c3547a62e5c37d8cfa6c5e4e0866437dd117b6747ed71ea678e2"
content-hash = "4f19d83b34af680e41bde58b1ce2ac03265b32f390831545b81a07eb69cc0937"
[metadata.files]
atomicwrites = [
@ -474,8 +474,8 @@ httplib2 = [
{file = "httplib2-0.20.1.tar.gz", hash = "sha256:0efbcb8bfbfbc11578130d87d8afcc65c2274c6eb446e59fc674e4d7c972d327"},
]
icalendar = [
{file = "icalendar-4.0.8-py2.py3-none-any.whl", hash = "sha256:825209edeb8f3067cf31f0712b0fc9507b1ded270165f98f3176d6b50e8c7b78"},
{file = "icalendar-4.0.8.tar.gz", hash = "sha256:7508a92b4e36049777640b0ae393e7219a16488d852841a0e57b44fe51d9f848"},
{file = "icalendar-4.0.9-py2.py3-none-any.whl", hash = "sha256:cf1446ffdf1b6ad469451a8966cfa7694f5fac796ac6fc7cd93e28c51a637d2c"},
{file = "icalendar-4.0.9.tar.gz", hash = "sha256:cc73fa9c848744843046228cb66ea86cd8c18d73a51b140f7c003f760b84a997"},
]
idna = [
{file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"},

Wyświetl plik

@ -9,7 +9,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = "^3.9"
httplib2 = "==0.20.1"
icalendar = "==4.0.8"
icalendar = "4.0.9"
python-dateutil = "==2.8.2"
pytz = "==2021.3"
DateTime = "==4.3"

Plik diff jest za duży Load Diff

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -0,0 +1,90 @@
BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//SabreDAV//SabreDAV//EN
X-WR-CALNAME:Personal (Admin)
REFRESH-INTERVAL;VALUE=DURATION:PT4H
X-PUBLISHED-TTL:PT4H
BEGIN:VTIMEZONE
TZID:Europe/Zurich
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
END:STANDARD
END:VTIMEZONE
BEGIN:VTIMEZONE
TZID:Europe/Brussels
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
END:STANDARD
END:VTIMEZONE
BEGIN:VTIMEZONE
TZID:Europe/Kiev
BEGIN:DAYLIGHT
TZOFFSETFROM:+0200
TZOFFSETTO:+0300
TZNAME:EEST
DTSTART:19700329T030000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0300
TZOFFSETTO:+0200
TZNAME:EET
DTSTART:19701025T040000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTAMP:20211006T083610Z
UID:1baf29fb-7a33-4957-9b4f-cd58944b46a2
SUMMARY:Bern
DTSTART;VALUE=DATE:20211013
DTEND;VALUE=DATE:20211014
STATUS:CONFIRMED
TRANSP:TRANSPARENT
BEGIN:VALARM
TRIGGER:-PT1H
ACTION:DISPLAY
DESCRIPTION:Bern
END:VALARM
END:VEVENT
BEGIN:VEVENT
LAST-MODIFIED:20210408T060912Z
DTSTAMP:20210408T060912Z
UID:0370d094-b59a-4bb5-b333-84e2dab1329a
SUMMARY:Mobility reservation (Economy 11307)
X-MOZ-LASTACK:20210408T060912Z
DTSTART;TZID=Europe/Brussels:20210408T083000
DTEND;TZID=Europe/Brussels:20210408T160000
X-MOZ-GENERATION:3
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;VALUE=DURATION:-PT1H
DESCRIPTION:Mobility reservation (Economy 11307)
X-LIC-ERROR;X-LIC-ERRORTYPE=PARAMETER-VALUE-PARSE-ERROR:Got a VALUE paramet
er with an illegal type for property: VALUE=DURATION
END:VALARM
END:VEVENT
END:VCALENDAR

Wyświetl plik

@ -0,0 +1,43 @@
BEGIN:VCALENDAR
METHOD:REPLY
PRODID:Microsoft Exchange Server 2010
VERSION:2.0
BEGIN:VTIMEZONE
TZID:Europe/Zurich
BEGIN:STANDARD
DTSTART:20211031T030000
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:20210328T020000
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
ATTENDEE;PARTSTAT=DECLINED;CN="Eigenmann, Martin":mailto:calendar@gmail.com
ATTENDEE;CN="Eigenmann, Isabel":mailto:calendar@microsoft.com
COMMENT;LANGUAGE=en-US:test-message-from-the-calendar\n
UID:ecc5e718-08eb-4247-a871-d7e09a3ffbbc@localhost
SUMMARY;LANGUAGE=en-US:Declined: *[T] Beratung - Martin Eigenmann
DTSTART;TZID=Europe/Zurich:20210607T133000
DTEND;TZID=Europe/Zurich:20210607T143000
CLASS:PUBLIC
PRIORITY:5
DTSTAMP:20210606T191709Z
TRANSP:OPAQUE
STATUS:CONFIRMED
SEQUENCE:1
LOCATION;LANGUAGE=en-US:Talstrasse 4\, 9000 St.Gallen\, CH
X-MICROSOFT-CDO-APPT-SEQUENCE:1
X-MICROSOFT-CDO-OWNERAPPTID: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
END:VEVENT
END:VCALENDAR

Wyświetl plik

@ -0,0 +1,56 @@
BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//SabreDAV//SabreDAV//EN
X-WR-CALNAME:Personal (Admin)
REFRESH-INTERVAL;VALUE=DURATION:PT4H
X-PUBLISHED-TTL:PT4H
BEGIN:VTIMEZONE
TZID:Europe/Zurich
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTAMP:20211006T083610Z
UID:1baf29fb-7a33-4957-9b4f-cd58944b46a2
SUMMARY:Bern
DTSTART;VALUE=DATE:20211013
DTEND;VALUE=DATE:20211014
STATUS:CONFIRMED
TRANSP:TRANSPARENT
BEGIN:VALARM
TRIGGER:-PT1H
ACTION:DISPLAY
DESCRIPTION:Bern
END:VALARM
END:VEVENT
BEGIN:VEVENT
LAST-MODIFIED:20210408T060912Z
DTSTAMP:20210408T060912Z
UID:0370d094-b59a-4bb5-b333-84e2dab1329a
SUMMARY:Mobility reservation (Economy 11307)
X-MOZ-LASTACK:20210408T060912Z
DTSTART;TZID=Europe/Zurich:20210408T083000
DTEND;TZID=Europe/Zurich:20210408T160000
X-MOZ-GENERATION:3
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;VALUE=DURATION:-PT1H
DESCRIPTION:Mobility reservation (Economy 11307)
X-LIC-ERROR;X-LIC-ERRORTYPE=PARAMETER-VALUE-PARSE-ERROR:Got a VALUE paramet
er with an illegal type for property: VALUE=DURATION
END:VALARM
END:VEVENT
END:VCALENDAR

Wyświetl plik

@ -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

Plik diff jest za duży Load Diff

Wyświetl plik

@ -0,0 +1,42 @@
BEGIN:VCALENDAR
METHOD:REPLY
PRODID:Microsoft Exchange Server 2010
VERSION:2.0
BEGIN:VTIMEZONE
TZID:Europe/Zurich
BEGIN:STANDARD
DTSTART:20211031T030000
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:20210328T020000
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
ATTENDEE;PARTSTAT=DECLINED;CN="Eigenmann, Martin":mailto:calendar@gmail.com
COMMENT;LANGUAGE=en-US:test-message-from-the-calendar\n
UID:ecc5e718-08eb-4247-a871-d7e09a3ffbbc@localhost
SUMMARY;LANGUAGE=en-US:Declined: *[T] Beratung - Martin Eigenmann
DTSTART;TZID=Europe/Zurich:20210607T133000
DTEND;TZID=Europe/Zurich:20210607T143000
CLASS:PUBLIC
PRIORITY:5
DTSTAMP:20210606T191709Z
TRANSP:OPAQUE
STATUS:CONFIRMED
SEQUENCE:1
LOCATION;LANGUAGE=en-US:Talstrasse 4\, 9000 St.Gallen\, CH
X-MICROSOFT-CDO-APPT-SEQUENCE:1
X-MICROSOFT-CDO-OWNERAPPTID: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
END:VEVENT
END:VCALENDAR

Wyświetl plik

@ -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,101 @@ 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)"
)
def test_attenddees_have_params(self):
ical = "test/test_data/response.ics"
start = date(2021, 1, 1)
end = date(2021, 12, 31)
[e1] = icalevents.events(file=ical, start=start, end=end)
self.assertEqual(e1.attendee.params["PARTSTAT"], "DECLINED", "add paarams")
self.assertEqual(
e1.attendee, "mailto:calendar@gmail.com", "still is like a string"
)
def test_attenddees_can_be_multiple(self):
ical = "test/test_data/multi_attendee_response.ics"
start = date(2021, 1, 1)
end = date(2021, 12, 31)
[e1] = icalevents.events(file=ical, start=start, end=end)
self.assertEqual(e1.attendee[0].params["PARTSTAT"], "DECLINED", "add paarams")
self.assertEqual(
e1.attendee[0], "mailto:calendar@gmail.com", "we have a list of attendees"
)
self.assertEqual(
e1.attendee[1],
"mailto:calendar@microsoft.com",
"we have more than one attendee",
)
def test_floating(self):
ical = "test/test_data/floating.ics"
start = date(2021, 1, 1)
end = date(2021, 12, 31)
[e1, e2] = icalevents.events(file=ical, start=start, end=end)
self.assertEqual(e1.transparent, True, "respect transparency")
self.assertEqual(e1.start.hour, 0, "check start of the day")
self.assertEqual(e1.end.hour, 0, "check end of the day")
self.assertEqual(e1.floating, True, "respect floating time")
self.assertEqual(e1.start.tzinfo, UTC, "check tz as default utc")
self.assertEqual(e2.transparent, False, "respect transparency")
self.assertEqual(e2.start.hour, 6, "check start of the day")
self.assertEqual(e2.end.hour, 14, "check end of the day")
self.assertEqual(e2.floating, False, "respect floating time")
self.assertEqual(e2.start.tzinfo, UTC, "check tz as default utc")
def test_non_floating(self):
ical = "test/test_data/non_floating.ics"
start = date(2021, 1, 1)
end = date(2021, 12, 31)
[e1, e2] = icalevents.events(file=ical, start=start, end=end)
self.assertEqual(e1.transparent, True, "respect transparency")
self.assertEqual(e1.start.hour, 0, "check start of the day")
self.assertEqual(e1.end.hour, 0, "check end of the day")
self.assertEqual(e1.floating, False, "respect floating time")
self.assertEqual(
e1.start.tzinfo, gettz("Europe/Zurich"), "check tz as specified in calendar"
)
self.assertEqual(e2.transparent, False, "respect transparency")
self.assertEqual(e2.start.hour, 8, "check start of the day")
self.assertEqual(e2.end.hour, 16, "check end of the day")
self.assertEqual(e2.floating, False, "respect floating time")
self.assertEqual(
e1.start.tzinfo, gettz("Europe/Zurich"), "check tz as specified in calendar"
)
def test_recurring_override(self):
ical = "test/test_data/recurring_override.ics"
start = date(2021, 11, 23)
end = date(2021, 11, 24)
[e0, e1, e2] = icalevents.events(file=ical, start=start, end=end)
# Here all dates are in utc because the .ics has two timezones and this causes a transformation
self.assertEqual(e0.start, datetime(2021, 11, 23, 9, 0, tzinfo=UTC))
self.assertEqual(e1.start, datetime(2021, 11, 23, 10, 45, tzinfo=UTC))
self.assertEqual(
e2.start,
datetime(2021, 11, 23, 13, 0, tzinfo=UTC),
"moved 1 hour from 12:00 to 13:00",
)