fix: 포트 충돌 회피 — note_bridge 8098, intent_service 8099
Jellyfin(8096), OrbStack(8097) 포트 충돌으로 변경. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
"""Base class for all adapters."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Optional, Sequence
|
||||
|
||||
from icalendar.prop import vDDDTypes
|
||||
|
||||
from recurring_ical_events.util import (
|
||||
cached_property,
|
||||
make_comparable,
|
||||
time_span_contains_event,
|
||||
to_recurrence_ids,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from icalendar import Alarm
|
||||
from icalendar.cal import Component
|
||||
|
||||
from recurring_ical_events.series import Series
|
||||
from recurring_ical_events.types import UID, RecurrenceIDs, Time
|
||||
|
||||
|
||||
class ComponentAdapter(ABC):
|
||||
"""A unified interface to work with icalendar components."""
|
||||
|
||||
ATTRIBUTES_TO_DELETE_ON_COPY = ["RRULE", "RDATE", "EXDATE"]
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def component_name() -> str:
|
||||
"""The icalendar component name."""
|
||||
|
||||
def __init__(self, component: Component):
|
||||
"""Create a new adapter."""
|
||||
self._component = component
|
||||
|
||||
@property
|
||||
def alarms(self) -> list[Alarm]:
|
||||
"""The alarms in this component."""
|
||||
return self._component.walk("VALARM")
|
||||
|
||||
@property
|
||||
def end_property(self) -> str | None:
|
||||
"""The name of the end property."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def start(self) -> Time:
|
||||
"""The start time."""
|
||||
return self.span[0]
|
||||
|
||||
@property
|
||||
def end(self) -> Time:
|
||||
"""The end time."""
|
||||
return self.span[1]
|
||||
|
||||
@cached_property
|
||||
def span(self):
|
||||
"""Return (start, end)."""
|
||||
start, end = make_comparable((self.raw_start, self.raw_end))
|
||||
if start > end:
|
||||
return end, start
|
||||
return start, end
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def raw_start(self):
|
||||
"""Return the start property of the component."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def raw_end(self):
|
||||
"""Return the start property of the component."""
|
||||
|
||||
@property
|
||||
def uid(self) -> UID:
|
||||
"""The UID of a component.
|
||||
|
||||
UID is required by RFC5545.
|
||||
If the UID is absent, we use the Python ID.
|
||||
"""
|
||||
return self._component.get("UID", str(id(self._component)))
|
||||
|
||||
@classmethod
|
||||
def collect_series_from(
|
||||
cls, source: Component, suppress_errors: tuple[Exception]
|
||||
) -> Sequence[Series]:
|
||||
"""Collect all components for this adapter.
|
||||
|
||||
This is a shortcut.
|
||||
"""
|
||||
from recurring_ical_events.selection.name import ComponentsWithName
|
||||
|
||||
return ComponentsWithName(cls.component_name(), cls).collect_series_from(
|
||||
source, suppress_errors
|
||||
)
|
||||
|
||||
def as_component(
|
||||
self,
|
||||
start: Optional[Time] = None,
|
||||
stop: Optional[Time] = None,
|
||||
keep_recurrence_attributes: bool = True, # noqa: FBT001
|
||||
):
|
||||
"""Create a shallow copy of the source event and modify some attributes."""
|
||||
copied_component = self._component.copy()
|
||||
copied_component["DTSTART"] = vDDDTypes(self.start if start is None else start)
|
||||
copied_component.pop("DURATION", None) # remove duplication in event length
|
||||
if self.end_property is not None:
|
||||
copied_component[self.end_property] = vDDDTypes(
|
||||
self.end if stop is None else stop
|
||||
)
|
||||
if not keep_recurrence_attributes:
|
||||
for attribute in self.ATTRIBUTES_TO_DELETE_ON_COPY:
|
||||
if attribute in copied_component:
|
||||
del copied_component[attribute]
|
||||
for subcomponent in self._component.subcomponents:
|
||||
copied_component.add_component(subcomponent)
|
||||
if "RECURRENCE-ID" not in copied_component:
|
||||
copied_component["RECURRENCE-ID"] = vDDDTypes(
|
||||
copied_component["DTSTART"].dt
|
||||
)
|
||||
return copied_component
|
||||
|
||||
@cached_property
|
||||
def recurrence_ids(self) -> RecurrenceIDs:
|
||||
"""The recurrence ids of the component that might be used to identify it."""
|
||||
recurrence_id = self._component.get("RECURRENCE-ID")
|
||||
if recurrence_id is None:
|
||||
return ()
|
||||
return to_recurrence_ids(recurrence_id.dt)
|
||||
|
||||
@cached_property
|
||||
def this_and_future(self) -> bool:
|
||||
"""The recurrence ids has a thisand future range property"""
|
||||
recurrence_id = self._component.get("RECURRENCE-ID")
|
||||
if recurrence_id is None:
|
||||
return False
|
||||
if "RANGE" in recurrence_id.params:
|
||||
return recurrence_id.params["RANGE"] == "THISANDFUTURE"
|
||||
return False
|
||||
|
||||
def is_modification(self) -> bool:
|
||||
"""Whether the adapter is a modification."""
|
||||
return bool(self.recurrence_ids)
|
||||
|
||||
@cached_property
|
||||
def sequence(self) -> int:
|
||||
"""The sequence in the history of modification.
|
||||
|
||||
The sequence is negative if none was found.
|
||||
"""
|
||||
return self._component.get("SEQUENCE", -1)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Debug representation with more info."""
|
||||
return (
|
||||
f"<{self.__class__.__name__} UID={self.uid} start={self.start} "
|
||||
f"recurrence_ids={self.recurrence_ids} sequence={self.sequence} "
|
||||
f"end={self.end}>"
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def exdates(self) -> list[Time]:
|
||||
"""A list of exdates."""
|
||||
result: list[Time] = []
|
||||
exdates = self._component.get("EXDATE", [])
|
||||
for exdates in (exdates,) if not isinstance(exdates, list) else exdates:
|
||||
result.extend(exdate.dt for exdate in exdates.dts)
|
||||
return result
|
||||
|
||||
@cached_property
|
||||
def rrules(self) -> set[str]:
|
||||
"""A list of rrules of this component."""
|
||||
rules = self._component.get("RRULE", None)
|
||||
if not rules:
|
||||
return set()
|
||||
return {
|
||||
rrule.to_ical().decode()
|
||||
for rrule in (rules if isinstance(rules, list) else [rules])
|
||||
}
|
||||
|
||||
@cached_property
|
||||
def rdates(self) -> list[Time, tuple[Time, Time]]:
|
||||
"""A list of rdates, possibly a period."""
|
||||
rdates = self._component.get("RDATE", [])
|
||||
result = []
|
||||
for rdates in (rdates,) if not isinstance(rdates, list) else rdates:
|
||||
result.extend(rdate.dt for rdate in rdates.dts)
|
||||
return result
|
||||
|
||||
@cached_property
|
||||
def duration(self) -> datetime.timedelta:
|
||||
"""The duration of the component."""
|
||||
return self.end - self.start
|
||||
|
||||
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)
|
||||
|
||||
@cached_property
|
||||
def extend_query_span_by(self) -> tuple[datetime.timedelta, datetime.timedelta]:
|
||||
"""Calculate how much we extend the query span.
|
||||
|
||||
If an event is long, we need to extend the query span by the event's duration.
|
||||
If an event has moved, we need to make sure that that is included, too.
|
||||
|
||||
This is so that the RECURRENCE-ID falls within the modified span.
|
||||
Imagine if the span is exactly a second. How much would we need to query
|
||||
forward and backward to capture the recurrence id?
|
||||
|
||||
Returns two positive spans: (subtract_from_start, add_to_stop)
|
||||
"""
|
||||
subtract_from_start = self.duration
|
||||
add_to_stop = datetime.timedelta(0)
|
||||
recurrence_id_prop = self._component.get("RECURRENCE-ID")
|
||||
if recurrence_id_prop:
|
||||
start, end, recurrence_id = make_comparable(
|
||||
(self.start, self.end, recurrence_id_prop.dt)
|
||||
)
|
||||
if start < recurrence_id:
|
||||
add_to_stop = recurrence_id - start
|
||||
if start > recurrence_id:
|
||||
subtract_from_start = end - recurrence_id
|
||||
return subtract_from_start, add_to_stop
|
||||
|
||||
@cached_property
|
||||
def move_recurrences_by(self) -> datetime.timedelta:
|
||||
"""Occurrences of this component should be moved by this amount.
|
||||
|
||||
Usually, the occurrence starts at the new start time.
|
||||
However, if we have a RANGE=THISANDFUTURE, we need to move the occurrence.
|
||||
|
||||
RFC 5545:
|
||||
|
||||
When the given recurrence instance is
|
||||
rescheduled, all subsequent instances are also rescheduled by the
|
||||
same time difference. For instance, if the given recurrence
|
||||
instance is rescheduled to start 2 hours later, then all
|
||||
subsequent instances are also rescheduled 2 hours later.
|
||||
Similarly, if the duration of the given recurrence instance is
|
||||
modified, then all subsequence instances are also modified to have
|
||||
this same duration.
|
||||
"""
|
||||
if self.this_and_future:
|
||||
recurrence_id_prop = self._component.get("RECURRENCE-ID")
|
||||
assert recurrence_id_prop, "RANGE=THISANDFUTURE implies RECURRENCE-ID."
|
||||
start, recurrence_id = make_comparable((self.start, recurrence_id_prop.dt))
|
||||
return start - recurrence_id
|
||||
return datetime.timedelta(0)
|
||||
|
||||
|
||||
__all__ = ["ComponentAdapter"]
|
||||
Reference in New Issue
Block a user