Jellyfin(8096), OrbStack(8097) 포트 충돌으로 변경. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
252 lines
7.5 KiB
Python
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",
|
|
]
|