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",
]
|