1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
|
"""This tests the range parameter for ics file.
see https://github.com/niccokunzmann/python-recurring-ical-events/issues/75
Description: This parameter can be specified on a property that
specifies a recurrence identifier. The parameter specifies the
effective range of recurrence instances that is specified by the
property. The effective range is from the recurrence identifier
specified by the property. If this parameter is not specified on
an allowed property, then the default range is the single instance
specified by the recurrence identifier value of the property. The
parameter value can only be "THISANDFUTURE" to indicate a range
defined by the recurrence identifier and all subsequent instances.
The value "THISANDPRIOR" is deprecated by this revision of
iCalendar and MUST NOT be generated by applications.
- https://www.rfc-editor.org/rfc/rfc5545.html#section-3.2.13
"""
from datetime import time
from datetime import timedelta as td
from typing import TYPE_CHECKING
import pytest
from recurring_ical_events import EventAdapter
if TYPE_CHECKING:
from calendar import Calendar
@pytest.mark.parametrize(
("date", "summary"),
[
("20240901", "ORIGINAL EVENT"),
("20240911", "ORIGINAL EVENT"),
("20240913", "MODIFIED EVENT"),
("20240914", "MODIFIED EVENT"), # RDATE
("20240915", "MODIFIED EVENT"), # Normal recurrence-id
("20240917", "MODIFIED EVENT"),
("20240919", "MODIFIED EVENT"),
("20240922", "EDITED EVENT"),
("20240924", "EDITED EVENT"),
("20240926", "EDITED EVENT"),
],
)
def test_issue_75_RANGE_AT_parameter(calendars, date, summary):
events = calendars.issue_75_range_parameter.at(date)
assert len(events) == 1, f"Expecting one event at {date}"
event = events[0]
assert str(event["SUMMARY"]) == summary
@pytest.mark.parametrize(
("start", "end", "summary", "total"),
[
("20240901T000000Z", "20240911T235959Z", "ORIGINAL EVENT", 6),
("20240901T000000Z", "20240913T000000Z", "ORIGINAL EVENT", 6),
("20240901T000000Z", "20240913T235959Z", "MODIFIED EVENT", 7),
("20240901T000000Z", "20240914T235959Z", "MODIFIED EVENT", 8), # RDATE
(
"20240901T000000Z",
"20240915T235959Z",
"MODIFIED EVENT",
9,
), # Normal recurrence-id
("20240901T000000Z", "20240917T235959Z", "MODIFIED EVENT", 10),
("20240901T000000Z", "20240919T235959Z", "MODIFIED EVENT", 11),
("20240901T000000Z", "20240921T235959Z", "MODIFIED EVENT", 11),
("20240901T000000Z", "20240922T000000Z", "MODIFIED EVENT", 11),
("20240901T000000Z", "20240922T235959Z", "EDITED EVENT", 12),
("20240901T000000Z", "20240923T000000Z", "EDITED EVENT", 12),
("20240901T000000Z", "20240923T235959Z", "EDITED EVENT", 12),
("20240901T000000Z", "20240924T235959Z", "EDITED EVENT", 13),
("20240901T000000Z", "20240925T235959Z", "EDITED EVENT", 13),
(
"20240913T000000Z",
"20240922T000000Z",
"MODIFIED EVENT",
5,
), # out of query bounds
(
"20240913T000000Z",
"20240922T235959Z",
"EDITED EVENT",
6,
), # out of query bounds
(
"20240924T000000Z",
"20240925T235959Z",
"EDITED EVENT",
1,
), # out of query bounds
],
)
def test_issue_75_RANGE_BETWEEN_parameter(calendars, start, end, summary, total):
events = calendars.issue_75_range_parameter.between(start, end)
assert (
len(events) == total
), f"Expecting {total} events at range {start}, {end}, get {len(events)}"
event = events[-1]
assert str(event["SUMMARY"]) == summary
@pytest.mark.parametrize(
("date", "start", "end"),
[
# moved by 3 hours forward
((2024, 9, 13, 9), (9, 0), (16, 0)), # The modification itself
((2024, 9, 17, 9), (9, 0), (16, 0)), # The recurrence after this moved
# moved by 2h22m backward
((2024, 9, 22, 14, 22), (14, 22), (16, 13)), # The modification itself
((2024, 9, 24, 14, 22), (14, 22), (16, 13)), # The recurrence after this moved
],
)
def test_the_length_of_modified_events(calendars, date, start, end):
"""There should be one event exactly starting and ending at these times."""
events = calendars.issue_75_range_parameter.at(date)
assert len(events) != 0, "The calculation could not find an event!"
assert len(events) == 1, "Modify the test to yield one event only!"
event = events[0]
assert event["DTSTART"].dt.time() == time(*start)
assert event["DTEND"].dt.time() == time(*end)
@pytest.mark.parametrize(
("calendar", "event_index", "expected_start_delta", "expected_end_delta"),
[
# no recurrence id means 0
("issue_62_moved_event", 1, td(0), td(0)),
# we moved 31 -> 17; 31-17
("issue_62_moved_event", 0, td(0), td(14)),
# we have a duration added on top
("one_event", 0, td(minutes=30), td(0)),
# we move to a later date, +1 day
("same_event_recurring_at_same_time", 1, td(days=1, hours=1), td(0)),
# we move to the front, so we should still add the duration
("same_event_recurring_at_same_time", 2, td(0), td(hours=1)),
# we moved with the THISANDFUTURE
(
"issue_75_range_parameter",
3,
td(days=1, hours=2, minutes=22) + td(hours=1, minutes=51),
td(0),
),
],
)
def test_span_extension(
calendars, calendar, event_index, expected_start_delta, expected_end_delta
):
"""If we have an event that is moved with THISANDFUTURE,
other events move, too.
This requires us to extend the range which we query:
- If an event moves forward, we need to extend the span to the back ...
- If an event moves backward, we need to extend the span to the front ...
... in order to capture the recurrences from the rrule that would yield
the occurrence.
If the length is extended, we can shorten the span
If the length is reduced, we have to extend the span
This tests the adapter to yield the correct values for the given types
of moves.
We only have to extend the range for THISANDFUTURE events because
we iterate over all modifications either way.
TODO: However, for optimization, one could approach to create ranges that
specify how to extend and contract the spans.
This test has to test of types of recurrence id, start and end.
- date
- datetime without tzinfo
- datetime with UTC
- datetime with tzinfo other than UTC
>.The default value type is DATE-TIME. The value type can
be set to a DATE value type. This property MUST have the same
value type as the "DTSTART" property contained within the
recurring component. Furthermore, this property MUST be specified
as a date with local time if and only if the "DTSTART" property
contained within the recurring component is specified as a date
with local time.
- https://www.rfc-editor.org/rfc/rfc5545.html#section-3.8.4.4
moves must include:
- time forward
- time backward
- several days forward
- several days backward
Assumptions
-----------
This test is for a rought estimate. We can extend the range by +1 day into each direction.
This will allow us to capture everything.
Future examples and tests may help us improve the situation by narrowing it further down.
Safe:
- move 1 h forward -> END: add 1 day for timezone + 1 day for timedelta without timezone involvement (round up)
"""
cal = calendars.raw[calendar]
event = list(cal.walk("VEVENT"))[event_index]
adapter = EventAdapter(event)
assert adapter.duration >= td(0)
start_delta, end_delta = adapter.extend_query_span_by
assert start_delta >= expected_start_delta
assert end_delta >= expected_end_delta
def test_can_calculate_query_span_extension_on_all_events(calendars, calendar_name):
"""Check that the calclulation succeeds."""
for i, event in enumerate(calendars.raw[calendar_name].walk("VEVENT")):
adapter = EventAdapter(event)
start_delta, end_delta = adapter.extend_query_span_by
message = f"{calendar_name}.VEVENT[{i}]"
assert isinstance(start_delta, td), message
assert isinstance(end_delta, td), message
assert start_delta >= td(0), message
assert end_delta >= td(0), message
def test_deletion_of_THISANDFUTURE_by_SEQUENCE():
"""We need to make sure that the components we have only work on what is actual."""
pytest.skip("TODO")
def test_RDATE_with_PERIOD():
"""When an RDATE has a PERIOD, we can assume that that defines the new length."""
pytest.skip("TODO")
@pytest.mark.parametrize(
("calendar_name", "event_index", "delta"),
[
("one_event", 0, td(0)),
("same_event_recurring_at_same_time", 0, td(0)),
("issue_75_range_parameter", 1, td(hours=-3)),
("issue_75_range_parameter", 3, td(days=1, hours=2, minutes=22)),
],
)
def test_move_by_time(calendars, calendar_name, event_index, delta):
"""Check the moving of events."""
cal: Calendar = calendars.raw[calendar_name]
event = list(cal.walk("VEVENT"))[event_index]
adapter = EventAdapter(event)
assert adapter.move_recurrences_by == delta
# TODO: Test event with DTSTART = DATE - does it occur properly as it is
# one day long, I believe. Loot at the RFC 5545.
|