Files
syn-chat-bot/.venv/lib/python3.9/site-packages/recurring_ical_events/util.py
Hyungi Ahn c2257d3a86 fix: 포트 충돌 회피 — note_bridge 8098, intent_service 8099
Jellyfin(8096), OrbStack(8097) 포트 충돌으로 변경.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:53:55 +09:00

252 lines
7.5 KiB
Python

"""Utility functions."""
from __future__ import annotations
import datetime
from functools import wraps
from typing import TYPE_CHECKING, Callable, Optional, Sequence
from recurring_ical_events.errors import PeriodEndBeforeStart
if TYPE_CHECKING:
from recurring_ical_events.adapters.component import ComponentAdapter
from recurring_ical_events.types import RecurrenceIDs, Time, Timestamp
def timestamp(dt: datetime.datetime) -> Timestamp:
"""Return the time stamp of a datetime"""
return dt.timestamp()
def convert_to_date(date: Time) -> datetime.date:
"""Converts a date or datetime to a date"""
return datetime.date(date.year, date.month, date.day)
def is_pytz(tzinfo: datetime.tzinfo | Time):
"""Whether the time zone requires localize() and normalize().
pytz requires these funtions to be used in order to correctly use the
time zones after operations.
"""
return hasattr(tzinfo, "localize")
def normalize_pytz(time: Time) -> Time:
"""We have to normalize the time after a calculation if we use pytz."""
if is_pytz_dt(time):
return time.tzinfo.normalize(time)
return time
def is_date(time: Time) -> bool:
"""Whether this is a date and not a datetime."""
return isinstance(time, datetime.date) and not isinstance(time, datetime.datetime)
def is_datetime(time: Time) -> bool:
"""Whether this is a datetime and not a date."""
return isinstance(time, datetime.datetime)
def has_timezone(time: Time) -> bool:
"""Whether this date/datetime has a timezone."""
return is_datetime(time) and time.tzinfo is not None
def convert_to_datetime(
date: Time, tzinfo: Optional[datetime.tzinfo]
) -> datetime.datetime:
"""Converts a date to a datetime.
Dates are converted to datetimes with tzinfo.
Datetimes loose their timezone if tzinfo is None.
Datetimes receive tzinfo as a timezone if they do not have a timezone.
Datetimes retain their timezone if they have one already (tzinfo is not None).
"""
if is_date(date):
date = datetime.datetime(date.year, date.month, date.day) # noqa: DTZ001
if isinstance(date, datetime.datetime):
if date.tzinfo is None:
if tzinfo is not None:
if is_pytz(tzinfo):
return tzinfo.localize(date)
return date.replace(tzinfo=tzinfo)
elif tzinfo is None:
return normalize_pytz(date).replace(tzinfo=None)
return date
return date
def make_comparable(dates: Sequence[Time]) -> list[Time]:
"""Make an list or tuple of dates comparable.
Returns an list.
"""
tzinfo = None
all_dates = True
for date in dates:
if not is_date(date):
all_dates = False
if has_timezone(date):
tzinfo = date.tzinfo
break
if all_dates:
return dates
return [convert_to_datetime(date, tzinfo) for date in dates]
def time_span_contains_event(
span_start: Time,
span_stop: Time,
event_start: Time,
event_stop: Time,
comparable: bool = False, # noqa: FBT001
) -> bool:
"""Return whether an event should is included within a time span.
- span_start and span_stop define the time span
- event_start and event_stop define the event time
- comparable indicates whether the dates can be compared.
You can set it to True if you are sure you have timezones and
date/datetime correctly or used make_comparable() before.
Note that the stops are exlusive but the starts are inclusive.
This is an essential function of the module. It should be tested in
test/test_time_span_contains_event.py.
This raises a PeriodEndBeforeStart exception if a start is after an end.
"""
if not comparable:
span_start, span_stop, event_start, event_stop = make_comparable(
(span_start, span_stop, event_start, event_stop)
)
if event_start > event_stop:
raise PeriodEndBeforeStart(
(
"the event must start before it ends"
f"(start: {event_start} end: {event_stop})"
),
event_start,
event_stop,
)
if span_start > span_stop:
raise PeriodEndBeforeStart(
(
"the time span must start before it ends"
f"(start: {span_start} end: {span_stop})"
),
span_start,
span_stop,
)
if event_start == event_stop:
if span_start == span_stop:
return event_start == span_start
return span_start <= event_start < span_stop
if span_start == span_stop:
return event_start <= span_start < event_stop
return event_start < span_stop and span_start < event_stop
def compare_greater(date1: Time, date2: Time) -> bool:
"""Compare two dates if date1 > date2 and make them comparable before."""
date1, date2 = make_comparable((date1, date2))
return date1 > date2
def cmp(date1: Time, date2: Time) -> int:
"""Compare two dates, like cmp().
Returns
-------
-1 if date1 < date2
0 if date1 = date2
1 if date1 > date2
"""
# credits: https://www.geeksforgeeks.org/python-cmp-function/
# see https://stackoverflow.com/a/22490617/1320237
date1, date2 = make_comparable((date1, date2))
return (date1 > date2) - (date1 < date2)
def is_pytz_dt(time: Time) -> bool:
"""Whether the time requires localize() and normalize().
pytz requires these funtions to be used in order to correctly use the
time zones after operations.
"""
return isinstance(time, datetime.datetime) and is_pytz(time.tzinfo)
def cached_property(func: Callable) -> property:
"""Cache the property value for speed up."""
name = f"_cached_{func.__name__}"
not_found = object()
@property
@wraps(func)
def cached_property(self: object):
value = self.__dict__.get(name, not_found)
if value is not_found:
self.__dict__[name] = value = func(self)
return value
return cached_property
def to_recurrence_ids(time: Time) -> RecurrenceIDs:
"""Convert the time to a recurrence id so it can be hashed and recognized.
The first value should be used to identify a component as it is a datetime in UTC.
The other values can be used to look the component up.
"""
# We are inside the Series calculation with this and want to identify
# a date. It is fair to assume that the timezones are the same now.
if not isinstance(time, datetime.datetime):
return (convert_to_datetime(time, None),)
if time.tzinfo is None:
return (time,)
return (
time.astimezone(datetime.timezone.utc).replace(tzinfo=None),
time.replace(tzinfo=None),
)
def with_highest_sequence(
adapter1: ComponentAdapter | None, adapter2: ComponentAdapter | None
):
"""Return the one with the highest sequence."""
return max(
adapter1,
adapter2,
key=lambda adapter: -1e10 if adapter is None else adapter.sequence,
)
def get_any(dictionary: dict, keys: Sequence[object], default: object = None):
"""Get any item from the keys and return it."""
result = default
for key in keys:
result = dictionary.get(key, result)
return result
__all__ = [
"PeriodEndBeforeStart",
"cmp",
"convert_to_datetime",
"get_any",
"has_timezone",
"is_date",
"is_datetime",
"is_pytz",
"is_pytz_dt",
"make_comparable",
"normalize_pytz",
"time_span_contains_event",
"to_recurrence_ids",
"with_highest_sequence",
]