Jellyfin(8096), OrbStack(8097) 포트 충돌으로 변경. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
133 lines
4.8 KiB
Python
133 lines
4.8 KiB
Python
"""Series calculation for alarms."""
|
|
|
|
import datetime
|
|
from collections import defaultdict
|
|
from typing import Generator
|
|
|
|
from dateutil.rrule import rruleset
|
|
from icalendar import Alarm
|
|
|
|
from recurring_ical_events.adapters.component import ComponentAdapter
|
|
from recurring_ical_events.occurrence import AlarmOccurrence, Occurrence
|
|
from recurring_ical_events.series.rrule import Series
|
|
from recurring_ical_events.types import Time
|
|
from recurring_ical_events.util import convert_to_datetime
|
|
|
|
|
|
class AbsoluteAlarmSeries:
|
|
"""A series of absolute alarms."""
|
|
|
|
tzinfo = datetime.timezone.utc
|
|
|
|
def __init__(self):
|
|
"""Create a new series of absolute alarms."""
|
|
self.times = rruleset(cache=True)
|
|
self.times2occurence: dict[datetime.datetime, list[Occurrence]] = defaultdict(
|
|
list
|
|
)
|
|
|
|
def add(self, alarm: Alarm, parent: ComponentAdapter):
|
|
"""Add an absolute alarm with a parent component."""
|
|
trigger = alarm.TRIGGER
|
|
self._add(trigger, alarm, parent)
|
|
for _ in range(alarm.REPEAT):
|
|
trigger += alarm.DURATION
|
|
self._add(trigger, alarm, parent)
|
|
|
|
def _add(self, dt: datetime.datetime, alarm: Alarm, parent: ComponentAdapter):
|
|
"""Add an alarm at a specific time."""
|
|
self.times.rdate(dt)
|
|
self.times2occurence[dt].append(self.occurrence(dt, alarm, parent))
|
|
|
|
def between(
|
|
self, span_start: Time, span_stop: Time
|
|
) -> Generator[Occurrence, None, None]:
|
|
"""Components between the start (inclusive) and end (exclusive).
|
|
|
|
The result does not need to be ordered.
|
|
"""
|
|
span_start_dt = convert_to_datetime(span_start, self.tzinfo)
|
|
span_stop_dt = convert_to_datetime(span_stop, self.tzinfo)
|
|
for dt in self.times.between(span_start_dt, span_stop_dt, inc=True):
|
|
for occurrence in self.times2occurence[dt]:
|
|
if occurrence.is_in_span(span_start_dt, span_stop_dt):
|
|
yield occurrence
|
|
|
|
def occurrence(
|
|
self, dt: datetime.datetime, alarm: Alarm, parent: ComponentAdapter
|
|
) -> Occurrence:
|
|
"""Create a new occurrence."""
|
|
return AlarmOccurrence(dt, alarm, parent)
|
|
|
|
def is_empty(self) -> bool:
|
|
"""Whether this series is empty."""
|
|
return not self.times2occurence
|
|
|
|
|
|
class AlarmSeriesRelativeToStart:
|
|
"""A series of alarms relative to the start of a component."""
|
|
|
|
def __init__(self, alarm: Alarm, series: Series) -> None:
|
|
"""Create a series of alarms that are relative to the start of a series."""
|
|
self._alarm = alarm
|
|
self._series = series
|
|
self._offsets: list[datetime.timedelta] = [alarm.TRIGGER]
|
|
for _ in range(alarm.REPEAT):
|
|
self._offsets.append(self._offsets[-1] + alarm.DURATION)
|
|
|
|
def between(
|
|
self, span_start: Time, span_stop: Time
|
|
) -> Generator[Occurrence, None, None]:
|
|
"""Components between the start (inclusive) and end (exclusive).
|
|
|
|
The result does not need to be ordered.
|
|
"""
|
|
# TODO: Reduce time span to reduce occurrences
|
|
for offset in self._offsets:
|
|
# If we are before the event start (negative offset),
|
|
# we have to add the time span to request the event later.
|
|
for parent in self._series.between(span_start - offset, span_stop - offset):
|
|
if parent.has_alarm(self._alarm):
|
|
occurrence = self.occurrence(offset, self._alarm, parent)
|
|
if occurrence.is_in_span(span_start, span_stop):
|
|
yield occurrence
|
|
|
|
def occurrence(
|
|
self, offset: datetime.timedelta, alarm: Alarm, parent: Occurrence
|
|
) -> Occurrence:
|
|
"""Create a new occurrence."""
|
|
return AlarmOccurrence(offset + parent.start, alarm, parent)
|
|
|
|
def __repr__(self) -> str:
|
|
"""repr()"""
|
|
return (
|
|
f"<{self.__class__.__name__} "
|
|
f"of {self._alarm} in {self._series} "
|
|
f"with offsets {', '.join(map(str, self._offsets))}>"
|
|
)
|
|
|
|
|
|
class AlarmSeriesRelativeToEnd(AlarmSeriesRelativeToStart):
|
|
"""A series of alarms relative to the start of a component."""
|
|
|
|
def between(self, span_start, span_stop):
|
|
"""Components between the start (inclusive) and end (exclusive).
|
|
|
|
The result does not need to be ordered.
|
|
"""
|
|
# The end is exclusive. We must adjust the timespan to include it.
|
|
return super().between(span_start - datetime.timedelta(seconds=1), span_stop)
|
|
|
|
def occurrence(
|
|
self, offset: datetime.timedelta, alarm: Alarm, parent: Occurrence
|
|
) -> Occurrence:
|
|
"""Create a new occurrence."""
|
|
return AlarmOccurrence(offset + parent.end, alarm, parent)
|
|
|
|
|
|
__all__ = [
|
|
"AbsoluteAlarmSeries",
|
|
"AlarmSeriesRelativeToEnd",
|
|
"AlarmSeriesRelativeToStart",
|
|
]
|