kopia lustrzana https://github.com/jazzband/icalevents
Merge pull request #95 from jazzband/recurring-events-and-timezones
handle timezone and recurring eventspull/114/head
commit
01ee1e31ac
|
@ -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:
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -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
|
@ -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,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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
@ -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
|
|
@ -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",
|
||||
)
|
||||
|
|
Ładowanie…
Reference in New Issue