File: occurrence.py

package info (click to toggle)
python-recurring-ical-events 3.8.0%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,584 kB
  • sloc: python: 4,476; makefile: 84
file content (212 lines) | stat: -rw-r--r-- 6,523 bytes parent folder | download
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
"""Occurrences of events and other components."""

from __future__ import annotations

from datetime import date, datetime, timedelta
from typing import TYPE_CHECKING, NamedTuple, Optional

from icalendar import Alarm

from recurring_ical_events.adapters.component import ComponentAdapter
from recurring_ical_events.util import (
    cached_property,
    make_comparable,
    time_span_contains_event,
)

if TYPE_CHECKING:
    from icalendar import Alarm
    from icalendar.cal import Component

    from recurring_ical_events.adapters.component import ComponentAdapter
    from recurring_ical_events.types import UID, RecurrenceIDs, Time


class OccurrenceID(NamedTuple):
    """The ID of a component's occurrence to identify it clearly.

    Attributes:
        name: The name of the component, e.g. "VEVENT"
        uid: The UID of the component.
        recurrence_id: The Recurrence-ID of the component in UTC but without tzinfo.
        start: The start of the component

    """

    name: str
    uid: UID
    recurrence_id: Optional[Time]
    start: Time

    def to_string(self) -> str:
        """Return a string representation of this id."""
        return "#".join(
            [
                self.name,
                self.recurrence_id.isoformat() if self.recurrence_id else "",
                self.start.isoformat(),
                self.uid,
            ]
        )

    @staticmethod
    def _dt_from_string(iso_string: str) -> Time:
        """Create a datetime from the string representation."""
        if len(iso_string) == 10:
            return date.fromisoformat(iso_string)
        return datetime.fromisoformat(iso_string)

    @classmethod
    def from_string(cls, string_id: str) -> OccurrenceID:
        """Parse a string and return the component id."""
        name, recurrence_id, start, uid = string_id.split("#", 3)
        return cls(
            name,
            uid,
            cls._dt_from_string(recurrence_id) if recurrence_id else None,
            cls._dt_from_string(start),
        )

    @classmethod
    def from_occurrence(
        cls, name: str, uid: str, recurrence_ids: RecurrenceIDs, start: Time
    ):
        """Create a new OccurrenceID from the given values.

        Args:
            name: The component name.
            uid: The UID string.
            recurrence_ids: The recurrence ID tuple.
                This is expected as UTC with tzinfo being None.
            start: start time of the component either with or without timezone
        """
        return cls(name, uid, recurrence_ids[0] if recurrence_ids else None, start)


class Occurrence:
    """A repetition of an event."""

    def __init__(
        self,
        adapter: ComponentAdapter,
        start: Time | None = None,
        end: Time | None | timedelta = None,
        sequence: int = -1,
    ):
        """Create an event repetition.

        - source - the icalendar Event
        - start - the start date/datetime to replace
        - stop - the end date/datetime to replace
        - sequence - if positive or 0, this sets the SEQUENCE property
        """
        self._adapter = adapter
        self.start = adapter.start if start is None else start
        self.end = adapter.end if end is None else end
        self.sequence = sequence

    def as_component(self, keep_recurrence_attributes: bool) -> Component:  # noqa: FBT001
        """Create a shallow copy of the source component and modify some attributes."""
        component = self._adapter.as_component(
            self.start, self.end, keep_recurrence_attributes
        )
        if self.sequence >= 0:
            component["SEQUENCE"] = self.sequence
        return component

    def is_in_span(self, span_start: Time, span_stop: Time) -> bool:
        """Return whether the component is in the span."""
        return time_span_contains_event(span_start, span_stop, self.start, self.end)

    def __lt__(self, other: Occurrence) -> bool:
        """Compare two occurrences for sorting.

        See https://stackoverflow.com/a/4010558/1320237
        """
        self_start, other_start = make_comparable((self.start, other.start))
        return self_start < other_start

    @cached_property
    def id(self) -> OccurrenceID:
        """The id of the component."""
        return OccurrenceID.from_occurrence(
            self._adapter.component_name(),
            self._adapter.uid,
            self._adapter.recurrence_ids,
            self.start,
        )

    def __hash__(self) -> int:
        """Hash this for an occurrence."""
        return hash(self.id)

    def __eq__(self, other: Occurrence) -> bool:
        """self == other"""
        return self.id == other.id

    def component_name(self) -> str:
        """The name of this component."""
        return self._adapter.component_name()

    @property
    def uid(self) -> str:
        """The UID of this occurrence."""
        return self._adapter.uid

    def has_alarm(self, alarm: Alarm) -> bool:
        """Wether this alarm is in this occurrence."""
        return alarm in self._adapter.alarms

    @property
    def recurrence_ids(self) -> RecurrenceIDs:
        """The recurrence ids."""
        return self._adapter.recurrence_ids


class AlarmOccurrence(Occurrence):
    """Adapter for absolute alarms."""

    def __init__(
        self,
        trigger: datetime,
        alarm: Alarm,
        parent: ComponentAdapter | Occurrence,
    ) -> None:
        super().__init__(alarm, trigger, trigger)
        self.parent = parent
        self.alarm = alarm

    def as_component(self, keep_recurrence_attributes):
        """Return the alarm's parent as a modified component."""
        parent = self.parent.as_component(
            keep_recurrence_attributes=keep_recurrence_attributes
        )
        alarm_once = self.alarm.copy()
        alarm_once.TRIGGER = self.start
        alarm_once.REPEAT = 0
        parent.subcomponents = [alarm_once]
        return parent

    @cached_property
    def id(self) -> OccurrenceID:
        """The id of the component."""
        return OccurrenceID.from_occurrence(
            self.parent.component_name(),
            self.parent.uid,
            self.parent.recurrence_ids,
            self.start,
        )

    def __repr__(self) -> str:
        """repr(self)"""
        return (
            f"<{self.__class__.__name__} at {self.start} of"
            f" {self.alarm} in {self.parent}"
        )


__all__ = [
    "AlarmOccurrence",
    "Occurrence",
    "OccurrenceID",
]