kopia lustrzana https://github.com/jazzband/icalevents
Merge pull request #97 from nalpch/recurring-and-floating-events
Recurring and floating eventspull/119/head
commit
c0c9c46475
|
@ -20,7 +20,9 @@ def events(
|
|||
end=None,
|
||||
fix_apple=False,
|
||||
http=None,
|
||||
sort=True,
|
||||
tzinfo=None,
|
||||
sort=None,
|
||||
strict=False,
|
||||
) -> list[Event]:
|
||||
"""
|
||||
Get all events form the given iCal URL occurring in the given time range.
|
||||
|
@ -31,9 +33,12 @@ def events(
|
|||
:param start: start date (see dateutils.date)
|
||||
:param end: end date (see dateutils.date)
|
||||
:param fix_apple: fix known Apple iCal issues
|
||||
:sort: sorts events by start time
|
||||
:param tzinfo: return values in specified tz
|
||||
:param sort: sort return values
|
||||
:param strict: return dates, datetimes and datetime with timezones as specified in ical
|
||||
:sort sorts events by start time
|
||||
|
||||
:return: events
|
||||
:return events
|
||||
"""
|
||||
found_events = []
|
||||
|
||||
|
@ -49,7 +54,9 @@ def events(
|
|||
if not content and string_content:
|
||||
content = ical_download.data_from_string(string_content, apple_fix=fix_apple)
|
||||
|
||||
found_events += parse_events(content, start=start, end=end)
|
||||
found_events += parse_events(
|
||||
content, start=start, end=end, tzinfo=tzinfo, sort=sort, strict=strict
|
||||
)
|
||||
|
||||
if found_events is not None and sort is True:
|
||||
found_events.sort()
|
||||
|
|
|
@ -2,20 +2,21 @@
|
|||
Parse iCal data to Events.
|
||||
"""
|
||||
# for UID generation
|
||||
from faulthandler import is_enabled
|
||||
from random import randint
|
||||
from datetime import datetime, timedelta, date
|
||||
from datetime import datetime, timedelta, date, tzinfo
|
||||
from typing import Optional
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from dateutil.rrule import rrule, rruleset, rrulestr
|
||||
from dateutil.rrule import rrulestr
|
||||
from dateutil.tz import UTC, gettz
|
||||
|
||||
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
|
||||
|
||||
from icalendar.windows_to_olson import WINDOWS_TO_OLSON
|
||||
from pytz import timezone
|
||||
|
||||
|
||||
def now():
|
||||
"""
|
||||
|
@ -89,40 +90,31 @@ class Event:
|
|||
"Only events can be compared with each other! Other is %s" % type(other)
|
||||
)
|
||||
else:
|
||||
return self.start < other.start
|
||||
# start and end can be dates, datetimes and datetimes with timezoneinfo
|
||||
if type(self.start) is date and type(other.start) is date:
|
||||
return self.start < other.start
|
||||
elif type(self.start) is datetime and type(other.start) is datetime:
|
||||
if self.start.tzinfo == other.start.tzinfo:
|
||||
return self.start < other.start
|
||||
else:
|
||||
return self.start.astimezone(UTC) < other.start.astimezone(UTC)
|
||||
elif type(self.start) is date and type(other.start) is datetime:
|
||||
return self.start < other.start.date()
|
||||
elif type(self.start) is datetime and type(other.start) is date:
|
||||
return self.start.date() < other.start
|
||||
|
||||
def __str__(self):
|
||||
n = now()
|
||||
return "%s: %s (%s)" % (self.start, self.summary, self.end - self.start)
|
||||
|
||||
if not self.start.tzinfo:
|
||||
self.start = normalize(self.start)
|
||||
if not self.end.tzinfo:
|
||||
self.end = normalize(self.end)
|
||||
def astimezone(self, tzinfo):
|
||||
|
||||
# compute time delta description
|
||||
if not self.all_day:
|
||||
if self.end > n > self.start:
|
||||
# event is now
|
||||
delta = "now"
|
||||
elif self.start > n:
|
||||
# event is a future event
|
||||
if self.time_left().days > 0:
|
||||
delta = "%s days left" % self.time_left().days
|
||||
else:
|
||||
hours = self.time_left().seconds / (60 * 60)
|
||||
delta = "%.1f hours left" % hours
|
||||
else:
|
||||
# event is over
|
||||
delta = "ended"
|
||||
else:
|
||||
if self.end > n > self.start:
|
||||
delta = "today"
|
||||
elif self.start > n:
|
||||
delta = "%s days left" % self.time_left().days
|
||||
else:
|
||||
delta = "ended"
|
||||
if type(self.start) is datetime:
|
||||
self.start = self.start.astimezone(tzinfo)
|
||||
|
||||
return "%s: %s (%s)" % (self.start, self.summary, delta)
|
||||
if type(self.end) is datetime:
|
||||
self.end = self.end.astimezone(tzinfo)
|
||||
|
||||
return self
|
||||
|
||||
def copy_to(self, new_start=None, uid=None):
|
||||
"""
|
||||
|
@ -174,24 +166,23 @@ def encode(value: Optional[vText]) -> Optional[str]:
|
|||
return str(value.encode("utf-8"))
|
||||
|
||||
|
||||
def create_event(component, utc_default, tz=UTC):
|
||||
def create_event(component, utc_default):
|
||||
"""
|
||||
Create an event from its iCal representation.
|
||||
|
||||
:param component: iCal component
|
||||
:param tz: timezone for start and end times
|
||||
:return: event
|
||||
"""
|
||||
|
||||
event = Event()
|
||||
|
||||
event.start = normalize(component.get("dtstart").dt, tz=tz)
|
||||
event.start = component.get("dtstart").dt
|
||||
# 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)
|
||||
event.end = component.get("dtend").dt
|
||||
elif component.get("duration"): # compute implicit end as start + duration
|
||||
event.end = event.start + component.get("duration").dt
|
||||
else: # compute implicit end as start + 0
|
||||
|
@ -231,13 +222,19 @@ def create_event(component, utc_default, tz=UTC):
|
|||
event.transparent = component.get("transp") == "TRANSPARENT"
|
||||
|
||||
if component.get("created"):
|
||||
event.created = normalize(component.get("created").dt, tz)
|
||||
event.created = component.get("created").dt
|
||||
|
||||
if component.get("RECURRENCE-ID"):
|
||||
event.recurrence_id = normalize(component.get("RECURRENCE-ID").dt, tz)
|
||||
rid = component.get("RECURRENCE-ID").dt
|
||||
|
||||
# Spec defines that if DTSTART is a date RECURRENCE-ID also is to be interpreted as a date
|
||||
if type(component.get("dtstart").dt) is date:
|
||||
event.recurrence_id = date(year=rid.year, month=rid.month, day=rid.day)
|
||||
else:
|
||||
event.recurrence_id = rid
|
||||
|
||||
if component.get("last-modified"):
|
||||
event.last_modified = normalize(component.get("last-modified").dt, tz)
|
||||
event.last_modified = component.get("last-modified").dt
|
||||
elif event.created:
|
||||
event.last_modified = event.created
|
||||
|
||||
|
@ -261,58 +258,15 @@ def create_event(component, utc_default, tz=UTC):
|
|||
return event
|
||||
|
||||
|
||||
def normalize(dt, tz=UTC):
|
||||
"""
|
||||
Convert date or datetime to datetime with timezone.
|
||||
|
||||
:param dt: date to normalize
|
||||
:param tz: the normalized date's timezone
|
||||
:return: date as datetime with timezone
|
||||
"""
|
||||
if type(dt) is date:
|
||||
dt = dt + relativedelta(hour=0)
|
||||
elif type(dt) is datetime:
|
||||
pass
|
||||
else:
|
||||
raise ValueError("unknown type %s" % type(dt))
|
||||
|
||||
if dt.tzinfo:
|
||||
dt = dt.astimezone(tz)
|
||||
else:
|
||||
dt = dt.replace(tzinfo=tz)
|
||||
|
||||
return dt
|
||||
|
||||
|
||||
def get_timezone(tz_name):
|
||||
if tz_name in WINDOWS_TO_OLSON:
|
||||
return gettz(WINDOWS_TO_OLSON[tz_name])
|
||||
else:
|
||||
return gettz(tz_name)
|
||||
|
||||
|
||||
def adjust_timezone(component, dates, tz=None):
|
||||
# Remove timezone if none is present in component
|
||||
if (
|
||||
isinstance(component["dtstart"].dt, date)
|
||||
or component["dtstart"].dt.tzinfo is None
|
||||
):
|
||||
dates = [
|
||||
date.replace(tzinfo=None) if type(date) is datetime else date
|
||||
for date in dates
|
||||
]
|
||||
|
||||
# Add timezone if one is present in component
|
||||
if (
|
||||
isinstance(component["dtstart"].dt, datetime)
|
||||
and not component["dtstart"].dt.tzinfo is None
|
||||
):
|
||||
dates = [normalize(date) for date in dates]
|
||||
|
||||
return dates
|
||||
|
||||
|
||||
def parse_events(content, start=None, end=None, default_span=timedelta(days=7)):
|
||||
def parse_events(
|
||||
content,
|
||||
start=None,
|
||||
end=None,
|
||||
default_span=timedelta(days=7),
|
||||
tzinfo=None,
|
||||
sort=False,
|
||||
strict=False,
|
||||
):
|
||||
"""
|
||||
Query the events occurring in a given time range.
|
||||
|
||||
|
@ -333,6 +287,11 @@ def parse_events(content, start=None, end=None, default_span=timedelta(days=7)):
|
|||
|
||||
calendar = Calendar.from_ical(content)
|
||||
|
||||
# > Will be deprecated ========================
|
||||
# Calendar.from_ical already parses timezones as specified in the ical.
|
||||
# We can specify a 'default' tz but this is not according to spec.
|
||||
# Kept this here to verify tests and backward compatibility
|
||||
|
||||
# Keep track of the timezones defined in the calendar
|
||||
timezones = {}
|
||||
|
||||
|
@ -360,183 +319,222 @@ def parse_events(content, start=None, end=None, default_span=timedelta(days=7)):
|
|||
else:
|
||||
utc_default = True
|
||||
cal_tz = UTC
|
||||
|
||||
start = normalize(start, cal_tz)
|
||||
end = normalize(end, cal_tz)
|
||||
# < ==========================================
|
||||
|
||||
found = []
|
||||
|
||||
def add_if_not_exception(event):
|
||||
exdate = "%04d%02d%02d" % (
|
||||
event.start.year,
|
||||
event.start.month,
|
||||
event.start.day,
|
||||
)
|
||||
|
||||
if exdate not in exceptions:
|
||||
found.append(event)
|
||||
|
||||
for component in calendar.walk():
|
||||
# Skip dates that are stored as exceptions.
|
||||
exceptions = {}
|
||||
|
||||
if "EXDATE" in component:
|
||||
# Deal with the fact that sometimes it's a list and
|
||||
# sometimes it's a singleton
|
||||
exlist = []
|
||||
if isinstance(component["EXDATE"], vDDDLists):
|
||||
exlist = component["EXDATE"].dts
|
||||
else:
|
||||
exlist = component["EXDATE"]
|
||||
for ex in exlist:
|
||||
exdate = ex.to_ical().decode("UTF-8")
|
||||
exceptions[exdate[0:8]] = exdate
|
||||
|
||||
if component.name == "VEVENT":
|
||||
e = create_event(component, utc_default, cal_tz)
|
||||
e = create_event(component, utc_default)
|
||||
|
||||
if "EXDATE" in component:
|
||||
# Deal with the fact that sometimes it's a list and
|
||||
# sometimes it's a singleton
|
||||
exlist = []
|
||||
if isinstance(component["EXDATE"], list):
|
||||
exlist = component["EXDATE"]
|
||||
else:
|
||||
exlist.append(component["EXDATE"])
|
||||
for ex in exlist:
|
||||
exdate = ex.to_ical().decode("UTF-8")
|
||||
exceptions[exdate[0:8]] = exdate
|
||||
# make rule.between happy and provide from, to points in time that have the same format as dtstart
|
||||
s = component["dtstart"].dt
|
||||
if type(s) is date and not e.recurring:
|
||||
f, t = date(start.year, start.month, start.day), date(
|
||||
end.year, end.month, end.day
|
||||
)
|
||||
elif type(s) is datetime and s.tzinfo:
|
||||
f, t = datetime(
|
||||
start.year, start.month, start.day, tzinfo=s.tzinfo
|
||||
), datetime(end.year, end.month, end.day, tzinfo=s.tzinfo)
|
||||
else:
|
||||
f, t = datetime(start.year, start.month, start.day), datetime(
|
||||
end.year, end.month, end.day
|
||||
)
|
||||
|
||||
# Attempt to work out what timezone is used for the start
|
||||
# and end times. If the timezone is defined in the calendar,
|
||||
# use it; otherwise, attempt to load the rules from pytz.
|
||||
start_tz = None
|
||||
|
||||
if e.start.tzinfo != UTC:
|
||||
if str(e.start.tzinfo) in timezones:
|
||||
start_tz = timezones[str(e.start.tzinfo)]
|
||||
else:
|
||||
start_tz = e.start.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
|
||||
# and end to have a timezone. Otherwise, python will
|
||||
# raise an exception for comparing timezone naive
|
||||
# and offset-aware values.
|
||||
if e.start.tzinfo and not start.tzinfo:
|
||||
start = normalize(start, e.start.tzinfo)
|
||||
if e.start.tzinfo and not end.tzinfo:
|
||||
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, 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.
|
||||
ecopy = e.copy_to(dt, e.uid)
|
||||
rule = parse_rrule(component)
|
||||
for dt in rule.between(f, t, inc=True):
|
||||
# Recompute the start time in the current timezone *on* the
|
||||
# date of *this* occurrence. This handles the case where the
|
||||
# recurrence has crossed over the daylight savings time boundary.
|
||||
if type(dt) is datetime and dt.tzinfo:
|
||||
dtstart = dt.replace(tzinfo=get_timezone(str(dt.tzinfo)))
|
||||
ecopy = e.copy_to(
|
||||
dtstart.date() if type(s) is date else dtstart, e.uid
|
||||
)
|
||||
else:
|
||||
# Recompute the start time in the current timezone *on* the
|
||||
# date of *this* occurrence. This handles the case where the
|
||||
# recurrence has crossed over the daylight savings time boundary.
|
||||
naive = datetime(
|
||||
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)
|
||||
ecopy = e.copy_to(dt.date() if type(s) is date else dt, e.uid)
|
||||
add_if_not_exception(ecopy)
|
||||
|
||||
# We're effectively looping over the start time, we might need
|
||||
# to adjust the end time too, but don't have it's recurred value.
|
||||
# Make sure it's adjusted by constructing it from the meeting
|
||||
# duration. Pro: it'll be right. Con: if it was in a different
|
||||
# timezone from the start time, we'll have lost that.
|
||||
ecopy.end = dtstart + duration
|
||||
|
||||
exdate = "%04d%02d%02d" % (
|
||||
ecopy.start.year,
|
||||
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)
|
||||
elif e.end >= f and e.start <= t:
|
||||
add_if_not_exception(e)
|
||||
|
||||
result = found.copy()
|
||||
|
||||
# Remove events that are replaced in ical
|
||||
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)
|
||||
|
||||
# > Will be deprecated ========================
|
||||
# We will apply default cal_tz as required by some tests.
|
||||
# This is just here for backward-compatibility
|
||||
if not strict:
|
||||
for event in result:
|
||||
if type(event.start) is date:
|
||||
event.start = datetime(
|
||||
year=event.start.year,
|
||||
month=event.start.month,
|
||||
day=event.start.day,
|
||||
hour=0,
|
||||
minute=0,
|
||||
tzinfo=cal_tz,
|
||||
)
|
||||
event.end = datetime(
|
||||
year=event.end.year,
|
||||
month=event.end.month,
|
||||
day=event.end.day,
|
||||
hour=0,
|
||||
minute=0,
|
||||
tzinfo=cal_tz,
|
||||
)
|
||||
elif type(event.start) is datetime:
|
||||
if event.start.tzinfo:
|
||||
event.start = event.start.astimezone(cal_tz)
|
||||
event.end = event.end.astimezone(cal_tz)
|
||||
else:
|
||||
event.start = event.start.replace(tzinfo=cal_tz)
|
||||
event.end = event.end.replace(tzinfo=cal_tz)
|
||||
|
||||
if event.created:
|
||||
if type(event.created) is date:
|
||||
event.created = datetime(
|
||||
year=event.created.year,
|
||||
month=event.created.month,
|
||||
day=event.created.day,
|
||||
hour=0,
|
||||
minute=0,
|
||||
tzinfo=cal_tz,
|
||||
)
|
||||
if type(event.created) is datetime:
|
||||
if event.created.tzinfo:
|
||||
event.created = event.created.astimezone(cal_tz)
|
||||
else:
|
||||
event.created = event.created.replace(tzinfo=cal_tz)
|
||||
|
||||
if event.last_modified:
|
||||
if type(event.last_modified) is date:
|
||||
event.last_modified = datetime(
|
||||
year=event.last_modified.year,
|
||||
month=event.last_modified.month,
|
||||
day=event.last_modified.day,
|
||||
hour=0,
|
||||
minute=0,
|
||||
tzinfo=cal_tz,
|
||||
)
|
||||
if type(event.last_modified) is datetime:
|
||||
if event.last_modified.tzinfo:
|
||||
event.last_modified = event.last_modified.astimezone(cal_tz)
|
||||
else:
|
||||
event.last_modified = event.last_modified.replace(tzinfo=cal_tz)
|
||||
# < ==========================================
|
||||
|
||||
if sort:
|
||||
result.sort()
|
||||
|
||||
if tzinfo:
|
||||
result = [event.astimezone(tzinfo) for event in result]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def parse_rrule(component, tz=UTC):
|
||||
def parse_rrule(component):
|
||||
"""
|
||||
Extract a dateutil.rrule object from an icalendar component. Also includes
|
||||
the component's dtstart and exdate properties. The rdate and exrule
|
||||
properties are not yet supported.
|
||||
|
||||
:param component: icalendar component
|
||||
:param tz: timezone for DST handling
|
||||
:return: extracted rrule or rruleset
|
||||
:return: extracted rrule or rruleset or None
|
||||
"""
|
||||
if component.get("rrule"):
|
||||
# component['rrule'] can be both a scalar and a list
|
||||
rrules = component["rrule"]
|
||||
if not isinstance(rrules, list):
|
||||
rrules = [rrules]
|
||||
|
||||
# If dtstart is a datetime, make sure it's in a timezone.
|
||||
rdtstart = component["dtstart"].dt
|
||||
if type(rdtstart) is datetime:
|
||||
rdtstart = normalize(rdtstart, tz=tz)
|
||||
dtstart = component.get("dtstart").dt
|
||||
|
||||
# Remove/add timezone to rrule until dates depending on component
|
||||
for index, rru in enumerate(rrules):
|
||||
if "UNTIL" in rru:
|
||||
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"]
|
||||
]
|
||||
# component['rrule'] can be both a scalar and a list
|
||||
rrules = component.get("rrule")
|
||||
if not isinstance(rrules, list):
|
||||
rrules = [rrules]
|
||||
|
||||
# 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,
|
||||
forceset=True,
|
||||
unfold=True,
|
||||
)
|
||||
def conform_until(until, dtstart):
|
||||
if type(dtstart) is datetime:
|
||||
# If we have timezone defined adjust for daylight saving time
|
||||
if type(until) is datetime:
|
||||
return until + abs(
|
||||
(
|
||||
until.astimezone(dtstart.tzinfo).utcoffset()
|
||||
if until.tzinfo is not None and dtstart.tzinfo is not None
|
||||
else None
|
||||
)
|
||||
or timedelta()
|
||||
)
|
||||
|
||||
if component.get("exdate"):
|
||||
# Add exdates to the rruleset
|
||||
for exd in extract_exdates(component):
|
||||
return (
|
||||
until.astimezone(UTC)
|
||||
if type(until) is datetime
|
||||
else datetime(
|
||||
year=until.year, month=until.month, day=until.day, tzinfo=UTC
|
||||
)
|
||||
) + (
|
||||
(dtstart.tzinfo.utcoffset(dtstart) if dtstart.tzinfo else None)
|
||||
or timedelta()
|
||||
)
|
||||
|
||||
return until.date() + timedelta(days=1) if type(until) is datetime else until
|
||||
|
||||
for index, rru in enumerate(rrules):
|
||||
if "UNTIL" in rru:
|
||||
rrules[index]["UNTIL"] = [
|
||||
conform_until(until, dtstart) for until in rrules[index]["UNTIL"]
|
||||
]
|
||||
|
||||
rule = rrulestr(
|
||||
"\n".join(x.to_ical().decode() for x in rrules),
|
||||
dtstart=dtstart,
|
||||
forceset=True,
|
||||
unfold=True,
|
||||
)
|
||||
|
||||
if component.get("exdate"):
|
||||
# Add exdates to the rruleset
|
||||
for exd in extract_exdates(component):
|
||||
if type(dtstart) is date:
|
||||
rule.exdate(exd.replace(tzinfo=None))
|
||||
else:
|
||||
rule.exdate(exd)
|
||||
|
||||
# TODO: What about rdates and exrules?
|
||||
if component.get("exrule"):
|
||||
pass
|
||||
# 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:
|
||||
rule = rruleset()
|
||||
rule.rdate(normalize(component["dtstart"].dt, tz=tz))
|
||||
if component.get("rdate"):
|
||||
pass
|
||||
|
||||
return rule
|
||||
|
||||
|
@ -549,13 +547,18 @@ def extract_exdates(component):
|
|||
:return: list of exception dates
|
||||
"""
|
||||
dates = []
|
||||
|
||||
exd_prop = component.get("exdate")
|
||||
if exd_prop:
|
||||
if isinstance(exd_prop, list): # In case there is more than one exdate property
|
||||
for exd_list in exd_prop:
|
||||
dates.extend(normalize(exd.dt) for exd in exd_list.dts)
|
||||
elif isinstance(exd_prop, vDDDLists):
|
||||
dates.extend(normalize(exd.dt) for exd in exd_prop.dts)
|
||||
if isinstance(exd_prop, list):
|
||||
for exd_list in exd_prop:
|
||||
dates.extend(exd.dt for exd in exd_list.dts)
|
||||
else: # it must be a vDDDLists
|
||||
dates.extend(exd.dt for exd in exd_prop.dts)
|
||||
|
||||
return adjust_timezone(component, dates)
|
||||
return dates
|
||||
|
||||
|
||||
def get_timezone(tz_name):
|
||||
if tz_name in WINDOWS_TO_OLSON:
|
||||
return gettz(WINDOWS_TO_OLSON[tz_name])
|
||||
else:
|
||||
return gettz(tz_name)
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -0,0 +1,47 @@
|
|||
BEGIN:VCALENDAR
|
||||
METHOD:PUBLISH
|
||||
PRODID:Microsoft Exchange Server 2010
|
||||
VERSION:2.0
|
||||
X-WR-CALNAME:Calendar
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Eastern Standard Time
|
||||
BEGIN:STANDARD
|
||||
DTSTART:16010101T020000
|
||||
TZOFFSETFROM:-0400
|
||||
TZOFFSETTO:-0500
|
||||
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:16010101T020000
|
||||
TZOFFSETFROM:-0500
|
||||
TZOFFSETTO:-0400
|
||||
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DESCRIPTION:Test event that has an RRULE with multiple EXDATE
|
||||
RRULE:FREQ=WEEKLY;UNTIL=20220429T150000Z;INTERVAL=1;BYDAY=FR;WKST=MO
|
||||
EXDATE;TZID=Eastern Standard Time:20220318T110000,20220401T110000,20220408T
|
||||
110000
|
||||
UID:040000008200R00074P5O7101N82R00800000000R0Q793689428Q801000000000000000
|
||||
010000000QOPN4SS024R0264S9P0Q7OOQQ16PN399
|
||||
SUMMARY:Recurring With Exclusions
|
||||
DTSTART;TZID=Eastern Standard Time:20220311T110000
|
||||
DTEND;TZID=Eastern Standard Time:20220311T113000
|
||||
CLASS:PUBLIC
|
||||
PRIORITY:5
|
||||
DTSTAMP:20220330T125447Z
|
||||
TRANSP:OPAQUE
|
||||
STATUS:CONFIRMED
|
||||
SEQUENCE:0
|
||||
LOCATION:Microsoft Teams Meeting
|
||||
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
|
|
@ -146,7 +146,7 @@ class ICalEventsTests(unittest.TestCase):
|
|||
start = date(2021, 1, 1)
|
||||
end = date(2022, 1, 1)
|
||||
|
||||
evs = icalevents.events(file=ical, start=start, end=end)
|
||||
evs = icalevents.events(file=ical, start=start, end=end, sort=True)
|
||||
ev = evs[0]
|
||||
|
||||
self.assertEqual(len(evs), 3)
|
||||
|
@ -288,47 +288,6 @@ class ICalEventsTests(unittest.TestCase):
|
|||
self.assertTrue(icalevents.all_done(key), "request is finished")
|
||||
self.assertEqual(len(icalevents.latest_events(key)), 2, "two events are found")
|
||||
|
||||
def test_event_str(self):
|
||||
ical = "test/test_data/duration.ics"
|
||||
start = date(2018, 1, 1)
|
||||
end = date(2018, 2, 1)
|
||||
n = datetime.now(UTC)
|
||||
m = relativedelta(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
evs = icalevents.events(file=ical, start=start, end=end)
|
||||
|
||||
e1 = evs[0]
|
||||
self.assertIsNotNone(
|
||||
search(r"ended", str(e1.copy_to(n - relativedelta(days=5) + m))),
|
||||
"stringify past event",
|
||||
)
|
||||
self.assertIsNotNone(
|
||||
search(r"today", str(e1.copy_to(n - relativedelta(days=1) + m))),
|
||||
"stringify ongoing event",
|
||||
)
|
||||
self.assertIsNotNone(
|
||||
search(r"days left", str(e1.copy_to(n + relativedelta(days=3) + m))),
|
||||
"stringify future event",
|
||||
)
|
||||
|
||||
e2 = evs[1]
|
||||
self.assertIsNotNone(
|
||||
search(r"ended", str(e2.copy_to(n - relativedelta(hours=5)))),
|
||||
"stringify past event",
|
||||
)
|
||||
self.assertIsNotNone(
|
||||
search(r"now", str(e2.copy_to(n - relativedelta(hours=1)))),
|
||||
"stringify ongoing event",
|
||||
)
|
||||
self.assertIsNotNone(
|
||||
search(r"hours left", str(e2.copy_to(n + relativedelta(hours=3)))),
|
||||
"stringify future event",
|
||||
)
|
||||
self.assertIsNotNone(
|
||||
search(r"days left", str(e2.copy_to(n + relativedelta(days=3)))),
|
||||
"stringify future event",
|
||||
)
|
||||
|
||||
def test_events_no_description(self):
|
||||
ical = "test/test_data/no_description.ics"
|
||||
start = date(2018, 10, 15)
|
||||
|
@ -405,10 +364,20 @@ class ICalEventsTests(unittest.TestCase):
|
|||
start = date(2021, 1, 1)
|
||||
end = date(2021, 12, 31)
|
||||
|
||||
evs = icalevents.events(file=ical, start=start, end=end)
|
||||
evs = icalevents.events(file=ical, start=start, end=end, sort=True)
|
||||
|
||||
self.assertEqual(len(evs), 42, "42 events in total - one was moved")
|
||||
|
||||
def test_recurence_id_ms_moved(self):
|
||||
ical = "test/test_data/recurrenceid_ms.ics"
|
||||
start = date(2021, 4, 8)
|
||||
end = date(2021, 4, 10)
|
||||
|
||||
evs = icalevents.events(file=ical, start=start, end=end, sort=True)
|
||||
self.assertEqual(evs[0].start.day, 10)
|
||||
|
||||
self.assertEqual(len(evs), 1, "only one event - it was moved")
|
||||
|
||||
def test_recurence_id_google(self):
|
||||
ical = "test/test_data/recurrenceid_google.ics"
|
||||
start = date(2021, 1, 1)
|
||||
|
@ -425,9 +394,15 @@ class ICalEventsTests(unittest.TestCase):
|
|||
|
||||
evs = icalevents.events(file=ical, start=start, end=end)
|
||||
|
||||
self.assertEqual(
|
||||
len(evs), 239, "239 events in total"
|
||||
) # 102 events / 91 + 11 recurring
|
||||
self.assertEqual(len(evs), 239, "239 events in total")
|
||||
|
||||
def test_cest_2021_02(self):
|
||||
ical = "test/test_data/cest.ics"
|
||||
start = date(2021, 2, 1)
|
||||
end = date(2021, 2, 28)
|
||||
|
||||
evs = icalevents.events(file=ical, start=start, end=end)
|
||||
self.assertEqual(len(evs), 17, "17 in february")
|
||||
|
||||
def test_cest_2021_03(self):
|
||||
ical = "test/test_data/cest.ics"
|
||||
|
@ -442,16 +417,114 @@ class ICalEventsTests(unittest.TestCase):
|
|||
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")
|
||||
tz = gettz("Europe/Zurich")
|
||||
events = icalevents.events(
|
||||
file=ical, start=start, end=end, tzinfo=tz, sort=True, strict=True
|
||||
)
|
||||
|
||||
times = [
|
||||
((2021, 4, 1, 14, 0), (2021, 4, 1, 14, 30)),
|
||||
((2021, 4, 1, 15, 30), (2021, 4, 1, 17, 0)),
|
||||
((2021, 4, 2), (2021, 4, 3)),
|
||||
((2021, 4, 5, 16, 00), (2021, 4, 5, 17, 0)),
|
||||
((2021, 4, 7), (2021, 4, 8)),
|
||||
((2021, 4, 8, 11, 0), (2021, 4, 8, 12, 0)),
|
||||
((2021, 4, 8, 14, 30), (2021, 4, 8, 15, 0)),
|
||||
((2021, 4, 8, 15, 0), (2021, 4, 8, 15, 30)),
|
||||
((2021, 4, 9), (2021, 4, 10)),
|
||||
# ((2021, 4, 9), (2021, 4, 10)), # Google said this should exist, microsoft disagrees
|
||||
((2021, 4, 12, 11, 0), (2021, 4, 12, 11, 30)),
|
||||
((2021, 4, 12, 16, 0), (2021, 4, 12, 17, 0)),
|
||||
((2021, 4, 14), (2021, 4, 15)),
|
||||
((2021, 4, 15, 12, 0), (2021, 4, 15, 13, 0)),
|
||||
((2021, 4, 15, 15, 0), (2021, 4, 15, 15, 30)),
|
||||
((2021, 4, 16), (2021, 4, 17)),
|
||||
((2021, 4, 19, 16, 0), (2021, 4, 19, 17, 0)),
|
||||
((2021, 4, 21), (2021, 4, 22)),
|
||||
((2021, 4, 22, 11, 0), (2021, 4, 22, 12, 0)),
|
||||
((2021, 4, 22, 14, 45), (2021, 4, 22, 15, 15)),
|
||||
((2021, 4, 23), (2021, 4, 24)),
|
||||
((2021, 4, 26, 16, 0), (2021, 4, 26, 17, 0)),
|
||||
((2021, 4, 28), (2021, 4, 29)),
|
||||
((2021, 4, 29, 9, 0), (2021, 4, 29, 11, 0)),
|
||||
((2021, 4, 29, 11, 0), (2021, 4, 29, 11, 30)),
|
||||
((2021, 4, 29, 14, 15), (2021, 4, 29, 15, 00)),
|
||||
((2021, 4, 29, 15, 0), (2021, 4, 29, 15, 30)),
|
||||
((2021, 4, 30), (2021, 5, 1)),
|
||||
]
|
||||
|
||||
for index, time in enumerate(times):
|
||||
self.assertEqual(
|
||||
events[index].start,
|
||||
date(*time[0]) if len(time[0]) == 3 else datetime(*time[0], tzinfo=tz),
|
||||
)
|
||||
self.assertEqual(
|
||||
events[index].end,
|
||||
date(*time[1]) if len(time[1]) == 3 else datetime(*time[1], tzinfo=tz),
|
||||
)
|
||||
|
||||
self.assertEqual(len(events), len(times))
|
||||
|
||||
def test_cest_2021_05(self):
|
||||
ical = "test/test_data/cest.ics"
|
||||
start = date(2021, 5, 1)
|
||||
end = date(2021, 6, 1)
|
||||
|
||||
tz = gettz("Europe/Zurich")
|
||||
events = icalevents.events(
|
||||
file=ical, start=start, end=end, tzinfo=tz, sort=True, strict=True
|
||||
)
|
||||
|
||||
times = [
|
||||
((2021, 5, 3, 16, 0), (2021, 5, 3, 17, 0)),
|
||||
((2021, 5, 5), (2021, 5, 6)),
|
||||
((2021, 5, 6, 11, 0), (2021, 5, 6, 12, 0)),
|
||||
((2021, 5, 6, 15, 0), (2021, 5, 6, 15, 30)),
|
||||
((2021, 5, 7), (2021, 5, 8)),
|
||||
((2021, 5, 10, 16, 0), (2021, 5, 10, 17, 0)),
|
||||
((2021, 5, 12), (2021, 5, 13)),
|
||||
((2021, 5, 13, 15, 0), (2021, 5, 13, 15, 30)),
|
||||
((2021, 5, 14), (2021, 5, 15)),
|
||||
((2021, 5, 17, 16, 0), (2021, 5, 17, 17, 0)),
|
||||
((2021, 5, 19), (2021, 5, 20)),
|
||||
((2021, 5, 20, 11, 0), (2021, 5, 20, 12, 0)),
|
||||
((2021, 5, 20, 12, 0), (2021, 5, 20, 13, 0)),
|
||||
((2021, 5, 20, 15, 0), (2021, 5, 20, 15, 30)),
|
||||
((2021, 5, 21), (2021, 5, 22)),
|
||||
((2021, 5, 24, 16, 0), (2021, 5, 24, 17, 0)),
|
||||
((2021, 5, 26), (2021, 5, 27)),
|
||||
((2021, 5, 27, 15, 0), (2021, 5, 27, 15, 30)),
|
||||
((2021, 5, 28), (2021, 5, 29)),
|
||||
((2021, 5, 31, 16, 0), (2021, 5, 31, 17, 0)),
|
||||
]
|
||||
|
||||
for index, time in enumerate(times):
|
||||
self.assertEqual(
|
||||
events[index].start,
|
||||
date(*time[0]) if len(time[0]) == 3 else datetime(*time[0], tzinfo=tz),
|
||||
)
|
||||
self.assertEqual(
|
||||
events[index].end,
|
||||
date(*time[1]) if len(time[1]) == 3 else datetime(*time[1], tzinfo=tz),
|
||||
)
|
||||
|
||||
self.assertEqual(len(events), len(times))
|
||||
|
||||
def test_cest_2021_06(self):
|
||||
ical = "test/test_data/cest.ics"
|
||||
start = date(2021, 6, 1)
|
||||
end = date(2021, 6, 30)
|
||||
|
||||
evs = icalevents.events(file=ical, start=start, end=end)
|
||||
self.assertEqual(len(evs), 20, "20 in mai")
|
||||
self.assertEqual(len(evs), 11, "11 in june")
|
||||
|
||||
def test_cest_2021_07(self):
|
||||
ical = "test/test_data/cest.ics"
|
||||
start = date(2021, 7, 1)
|
||||
end = date(2021, 7, 31)
|
||||
|
||||
evs = icalevents.events(file=ical, start=start, end=end)
|
||||
self.assertEqual(len(evs), 1, "1 in july")
|
||||
|
||||
def test_cest_1(self):
|
||||
ical = "test/test_data/cest_every_day_for_one_year.ics"
|
||||
|
@ -607,3 +680,118 @@ class ICalEventsTests(unittest.TestCase):
|
|||
datetime(2021, 11, 23, 13, 0, tzinfo=UTC),
|
||||
"moved 1 hour from 12:00 to 13:00",
|
||||
)
|
||||
|
||||
def test_recurring_tz_passover_fall(self):
|
||||
ical = "test/test_data/recurring_override.ics"
|
||||
start = date(2021, 8, 30)
|
||||
end = date(2021, 9, 18)
|
||||
|
||||
tz = gettz("Europe/Zurich")
|
||||
events = icalevents.events(
|
||||
file=ical, start=start, end=end, tzinfo=tz, sort=True, strict=True
|
||||
)
|
||||
|
||||
times = [
|
||||
((2021, 8, 30, 8, 0), (2021, 8, 30, 17, 0)),
|
||||
((2021, 8, 30, 9, 30), (2021, 8, 30, 10, 0)),
|
||||
((2021, 8, 31, 10, 0), (2021, 8, 31, 10, 30)),
|
||||
((2021, 8, 31, 10, 15), (2021, 8, 31, 10, 45)),
|
||||
((2021, 8, 31, 13, 15), (2021, 8, 31, 14, 0)),
|
||||
((2021, 9, 1, 9, 0), (2021, 9, 1, 10, 0)),
|
||||
((2021, 9, 1, 9, 30), (2021, 9, 1, 10, 0)),
|
||||
((2021, 9, 1, 12, 0), (2021, 9, 1, 13, 0)),
|
||||
((2021, 9, 2, 10, 0), (2021, 9, 2, 10, 30)),
|
||||
((2021, 9, 3, 8, 0), (2021, 9, 3, 8, 30)),
|
||||
((2021, 9, 3, 9, 0), (2021, 9, 3, 9, 30)),
|
||||
((2021, 9, 3, 9, 30), (2021, 9, 3, 10, 0)),
|
||||
((2021, 9, 3, 15, 30), (2021, 9, 3, 16, 0)),
|
||||
((2021, 9, 3, 17, 30), (2021, 9, 3, 19, 0)),
|
||||
((2021, 9, 6, 8, 0), (2021, 9, 6, 17, 0)),
|
||||
((2021, 9, 6, 9, 30), (2021, 9, 6, 10, 0)),
|
||||
((2021, 9, 7, 9, 0), (2021, 9, 7, 12, 0)),
|
||||
((2021, 9, 7, 9, 0), (2021, 9, 7, 12, 0)),
|
||||
((2021, 9, 7, 10, 0), (2021, 9, 7, 10, 30)),
|
||||
((2021, 9, 8, 9, 30), (2021, 9, 8, 10, 0)),
|
||||
((2021, 9, 8, 12, 0), (2021, 9, 8, 13, 0)),
|
||||
((2021, 9, 9), (2021, 9, 10)),
|
||||
((2021, 9, 9, 10, 0), (2021, 9, 9, 10, 30)),
|
||||
((2021, 9, 9, 11, 0), (2021, 9, 9, 12, 0)),
|
||||
((2021, 9, 10, 8, 0), (2021, 9, 10, 8, 30)),
|
||||
((2021, 9, 10, 9, 30), (2021, 9, 10, 10, 0)),
|
||||
((2021, 9, 10, 17, 30), (2021, 9, 10, 19, 0)),
|
||||
((2021, 9, 13, 9, 30), (2021, 9, 13, 10, 0)),
|
||||
((2021, 9, 14, 9, 0), (2021, 9, 14, 10, 0)),
|
||||
((2021, 9, 14, 10, 0), (2021, 9, 14, 10, 30)),
|
||||
((2021, 9, 14, 15, 0), (2021, 9, 14, 15, 30)),
|
||||
((2021, 9, 15, 9, 30), (2021, 9, 15, 10, 0)),
|
||||
((2021, 9, 16, 10, 0), (2021, 9, 16, 10, 30)),
|
||||
((2021, 9, 16), (2021, 9, 17)),
|
||||
((2021, 9, 17, 9, 30), (2021, 9, 17, 10, 0)),
|
||||
((2021, 9, 17, 17, 30), (2021, 9, 17, 19, 0)),
|
||||
]
|
||||
|
||||
for index, time in enumerate(times):
|
||||
self.assertEqual(
|
||||
events[index].start,
|
||||
date(*time[0]) if len(time[0]) == 3 else datetime(*time[0], tzinfo=tz),
|
||||
)
|
||||
self.assertEqual(
|
||||
events[index].end,
|
||||
date(*time[1]) if len(time[1]) == 3 else datetime(*time[1], tzinfo=tz),
|
||||
)
|
||||
|
||||
self.assertEqual(len(events), len(times))
|
||||
|
||||
def test_recurring_tz_passover_spring(self):
|
||||
ical = "test/test_data/recurring_override.ics"
|
||||
start = date(2022, 3, 6)
|
||||
end = date(2022, 4, 10)
|
||||
|
||||
tz = gettz("Europe/Zurich")
|
||||
events = icalevents.events(
|
||||
file=ical, start=start, end=end, tzinfo=tz, sort=True, strict=True
|
||||
)
|
||||
|
||||
times = [
|
||||
((2022, 3, 8, 11, 45), (2022, 3, 8, 12, 0)),
|
||||
((2022, 3, 10), (2022, 3, 11)),
|
||||
((2022, 3, 10, 11, 0), (2022, 3, 10, 12, 0)),
|
||||
((2022, 3, 15, 11, 45), (2022, 3, 15, 12, 0)),
|
||||
((2022, 3, 22, 11, 45), (2022, 3, 22, 12, 0)),
|
||||
((2022, 3, 22, 14, 00), (2022, 3, 22, 15, 0)),
|
||||
((2022, 3, 24), (2022, 3, 25)),
|
||||
((2022, 3, 29, 11, 45), (2022, 3, 29, 12, 0)),
|
||||
((2022, 4, 3, 8, 0), (2022, 4, 3, 8, 30)),
|
||||
((2022, 4, 7), (2022, 4, 8)),
|
||||
]
|
||||
|
||||
for index, time in enumerate(times):
|
||||
self.assertEqual(
|
||||
events[index].start,
|
||||
date(*time[0]) if len(time[0]) == 3 else datetime(*time[0], tzinfo=tz),
|
||||
)
|
||||
self.assertEqual(
|
||||
events[index].end,
|
||||
date(*time[1]) if len(time[1]) == 3 else datetime(*time[1], tzinfo=tz),
|
||||
)
|
||||
|
||||
self.assertEqual(len(events), len(times))
|
||||
|
||||
def test_multi_exdate_same_line(self):
|
||||
ical = "test/test_data/multi_exdate_same_line_ms.ics"
|
||||
tz = gettz("America/New_York")
|
||||
start = date(2022, 3, 1)
|
||||
end = date(2022, 5, 1)
|
||||
|
||||
evs = icalevents.events(file=ical, start=start, end=end)
|
||||
|
||||
# parsing starts at 2022-03-01
|
||||
self.assertEqual(evs[0].start, datetime(2022, 3, 11, 11, 0, 0, tzinfo=tz))
|
||||
# 2022-03-18 is excluded by EXDATE rule
|
||||
self.assertEqual(evs[1].start, datetime(2022, 3, 25, 11, 0, 0, tzinfo=tz))
|
||||
# 2022-04-01 is excluded by EXDATE rule
|
||||
# 2022-04-08 is excluded by EXDATE rule
|
||||
self.assertEqual(evs[2].start, datetime(2022, 4, 15, 11, 0, 0, tzinfo=tz))
|
||||
self.assertEqual(evs[3].start, datetime(2022, 4, 22, 11, 0, 0, tzinfo=tz))
|
||||
self.assertEqual(evs[4].start, datetime(2022, 4, 29, 11, 0, 0, tzinfo=tz))
|
||||
# parsing stops at 2022-05-01
|
||||
|
|
|
@ -78,36 +78,6 @@ class ICalParserTests(unittest.TestCase):
|
|||
def test_event_order(self):
|
||||
self.assertTrue(self.eventA > self.eventB, "order of events")
|
||||
|
||||
def test_normalize(self):
|
||||
dt = date(year=2016, month=11, day=13)
|
||||
norm = icalevents.icalparser.normalize(dt)
|
||||
|
||||
self.assertTrue(type(norm) is datetime, "type is datetime")
|
||||
self.assertEqual(2016, norm.year, "year")
|
||||
self.assertEqual(11, norm.month, "month")
|
||||
self.assertEqual(13, norm.day, "day")
|
||||
self.assertEqual(0, norm.hour, "hour")
|
||||
self.assertEqual(0, norm.minute, "minute")
|
||||
self.assertEqual(0, norm.second, "second")
|
||||
self.assertEqual(0, norm.microsecond, "microsecond")
|
||||
self.assertEqual(UTC, norm.tzinfo, "timezone")
|
||||
|
||||
dt = datetime(year=2016, month=11, day=13, hour=1, minute=2, second=3)
|
||||
norm = icalevents.icalparser.normalize(dt)
|
||||
|
||||
self.assertTrue(type(norm) is datetime, "type is datetime")
|
||||
self.assertEqual(2016, norm.year, "year")
|
||||
self.assertEqual(11, norm.month, "month")
|
||||
self.assertEqual(13, norm.day, "day")
|
||||
self.assertEqual(1, norm.hour, "hour")
|
||||
self.assertEqual(2, norm.minute, "minute")
|
||||
self.assertEqual(3, norm.second, "second")
|
||||
self.assertEqual(0, norm.microsecond, "microsecond")
|
||||
self.assertEqual(UTC, norm.tzinfo, "timezone")
|
||||
|
||||
with self.assertRaises(ValueError, msg="type check effective"):
|
||||
icalevents.icalparser.normalize(None)
|
||||
|
||||
def test_attendee(self):
|
||||
self.assertIsInstance(self.eventA.attendee, str)
|
||||
self.assertIsInstance(self.eventB.attendee, list)
|
||||
|
@ -119,4 +89,4 @@ class ICalParserTests(unittest.TestCase):
|
|||
def test_str(self):
|
||||
self.eventA.start = datetime(year=2017, month=2, day=3, hour=12, minute=5)
|
||||
self.eventA.end = datetime(year=2017, month=2, day=3, hour=15, minute=5)
|
||||
self.assertEqual("2017-02-03 12:05:00+00:00: Event A (ended)", str(self.eventA))
|
||||
self.assertEqual("2017-02-03 12:05:00: Event A (3:00:00)", str(self.eventA))
|
||||
|
|
Ładowanie…
Reference in New Issue