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:
Hyungi Ahn
2026-03-19 13:53:55 +09:00
parent dc08d29509
commit c2257d3a86
2709 changed files with 619549 additions and 10 deletions

View File

@@ -0,0 +1,15 @@
"""All adapters for different kinds of components."""
from .alarm import AbsoluteAlarmAdapter
from .component import ComponentAdapter
from .event import EventAdapter
from .journal import JournalAdapter
from .todo import TodoAdapter
__all__ = [
"AbsoluteAlarmAdapter",
"ComponentAdapter",
"EventAdapter",
"JournalAdapter",
"TodoAdapter",
]

View File

@@ -0,0 +1,22 @@
"""Adapter for VALARM components."""
from __future__ import annotations
from typing import TYPE_CHECKING
from recurring_ical_events.adapters.component import ComponentAdapter
if TYPE_CHECKING:
from icalendar import Alarm
class AbsoluteAlarmAdapter(ComponentAdapter): # TODO: remove
"""Adapter for absolute alarms."""
def __init__(self, alarm: Alarm, parent: ComponentAdapter):
"""Create a new adapter."""
super().__init__(alarm)
self.parent = parent
__all__ = ["AbsoluteAlarmAdapter"]

View File

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

View File

@@ -0,0 +1,60 @@
"""Adapter for VEVENT."""
from __future__ import annotations
import datetime
from typing import TYPE_CHECKING
from recurring_ical_events.adapters.component import ComponentAdapter
from recurring_ical_events.util import (
convert_to_datetime,
is_date,
normalize_pytz,
)
if TYPE_CHECKING:
from recurring_ical_events.types import Time
class EventAdapter(ComponentAdapter):
"""An icalendar event adapter."""
@staticmethod
def component_name() -> str:
"""The icalendar component name."""
return "VEVENT"
@property
def end_property(self) -> str:
"""DTEND"""
return "DTEND"
@property
def raw_start(self) -> Time:
"""Return DTSTART"""
# Arguably, it may be considered a feature that this breaks
# if no DTSTART is set
return self._component["DTSTART"].dt
@property
def raw_end(self) -> Time:
"""Yield DTEND or calculate the end of the event based on
DTSTART and DURATION.
"""
## an even may have DTEND or DURATION, but not both
end = self._component.get("DTEND")
if end is not None:
return end.dt
duration = self._component.get("DURATION")
if duration is not None:
start = self._component["DTSTART"].dt
if duration.dt.seconds != 0 and is_date(start):
start = convert_to_datetime(start, None)
return normalize_pytz(start + duration.dt)
start = self._component["DTSTART"].dt
if is_date(start):
return start + datetime.timedelta(days=1)
return start
__all__ = ["EventAdapter"]

View File

@@ -0,0 +1,39 @@
"""Adapter for VJOURNAL."""
from recurring_ical_events.adapters.component import ComponentAdapter
from recurring_ical_events.constants import DATE_MIN_DT
from recurring_ical_events.types import Time
from recurring_ical_events.util import cached_property
class JournalAdapter(ComponentAdapter):
"""Apdater for journal entries."""
@staticmethod
def component_name() -> str:
"""The icalendar component name."""
return "VJOURNAL"
@property
def end_property(self) -> None:
"""There is no end property"""
@property
def raw_start(self) -> Time:
"""Return DTSTART if it set, do not panic if it's not set."""
## according to the specification, DTSTART in a VJOURNAL is optional
dtstart = self._component.get("DTSTART")
if dtstart is not None:
return dtstart.dt
return DATE_MIN_DT
@cached_property
def raw_end(self) -> Time:
"""The end time is the same as the start."""
## VJOURNAL cannot have a DTEND. We should consider a VJOURNAL to
## describe one day if DTSTART is a date, and we can probably
## consider it to have zero duration if a timestamp is given.
return self.raw_start
__all__ = ["JournalAdapter"]

View File

@@ -0,0 +1,80 @@
"""Adapter for VTODO."""
from recurring_ical_events.adapters.component import ComponentAdapter
from recurring_ical_events.constants import DATE_MAX_DT, DATE_MIN_DT
from recurring_ical_events.types import Time
from recurring_ical_events.util import (
convert_to_datetime,
is_date,
normalize_pytz,
)
class TodoAdapter(ComponentAdapter):
"""Unified access to TODOs."""
@staticmethod
def component_name() -> str:
"""The icalendar component name."""
return "VTODO"
@property
def end_property(self) -> str:
"""DUE"""
return "DUE"
@property
def raw_start(self) -> Time:
"""Return DTSTART if it set, do not panic if it's not set."""
## easy case - DTSTART set
start = self._component.get("DTSTART")
if start is not None:
return start.dt
## Tasks may have DUE set, but no DTSTART.
## Let's assume 0 duration and return the DUE
due = self._component.get("DUE")
if due is not None:
return due.dt
## Assume infinite time span if neither is given
## (see the comments under _get_event_end)
return DATE_MIN_DT
@property
def raw_end(self) -> Time:
"""Return DUE or DTSTART+DURATION or something"""
## Easy case - DUE is set
end = self._component.get("DUE")
if end is not None:
return end.dt
dtstart = self._component.get("DTSTART")
## DURATION can be specified instead of DUE.
duration = self._component.get("DURATION")
## It is no requirement that DTSTART is set.
## Perhaps duration is a time estimate rather than an indirect
## way to set DUE.
if duration is not None and dtstart is not None:
start = dtstart.dt
if duration.dt.seconds != 0 and is_date(start):
start = convert_to_datetime(start, None)
return normalize_pytz(start + duration.dt)
## According to the RFC, a VEVENT without an end/duration
## is to be considered to have zero duration. Assuming the
## same applies to VTODO.
if dtstart:
return dtstart.dt
## The RFC says this about VTODO:
## > A "VTODO" calendar component without the "DTSTART" and "DUE" (or
## > "DURATION") properties specifies a to-do that will be associated
## > with each successive calendar date, until it is completed.
## It can be interpreted in different ways, though probably it may
## be considered equivalent with a DTSTART in the infinite past and DUE
## in the infinite future?
return DATE_MAX_DT
__all__ = ["TodoAdapter"]