ical.util

Utility methods used by multiple components.

  1"""Utility methods used by multiple components."""
  2
  3from __future__ import annotations
  4
  5from collections.abc import Sequence
  6import datetime
  7from importlib import metadata
  8from types import NoneType
  9from typing import TYPE_CHECKING, Any, Union, cast, get_args, get_origin, overload
 10import uuid
 11
 12__all__ = [
 13    "dtstamp_factory",
 14    "uid_factory",
 15    "prodid_factory",
 16]
 17
 18
 19MIDNIGHT = datetime.time()
 20PRODID = "github.com/allenporter/ical"
 21VERSION = metadata.version("ical")
 22
 23
 24def dtstamp_factory() -> datetime.datetime:
 25    """Factory method for new event timestamps to facilitate mocking."""
 26    return datetime.datetime.now(tz=datetime.UTC)
 27
 28
 29def uid_factory() -> str:
 30    """Factory method for new uids to facilitate mocking."""
 31    return str(uuid.uuid1())
 32
 33
 34def prodid_factory() -> str:
 35    """Return the ical version to facilitate mocking."""
 36    return f"-//{PRODID}//{VERSION}//EN"
 37
 38
 39def local_timezone() -> datetime.tzinfo:
 40    """Get the local timezone to use when converting date to datetime."""
 41    if local_tz := datetime.datetime.now().astimezone().tzinfo:
 42        return local_tz
 43    return datetime.timezone.utc
 44
 45
 46def normalize_datetime(
 47    value: datetime.date | datetime.datetime, tzinfo: datetime.tzinfo | None = None
 48) -> datetime.datetime:
 49    """Convert date or datetime to a value that can be used for comparison."""
 50    if not isinstance(value, datetime.datetime):
 51        value = datetime.datetime.combine(value, MIDNIGHT)
 52    if value.tzinfo is None:
 53        if tzinfo is None:
 54            tzinfo = local_timezone()
 55        value = value.replace(tzinfo=tzinfo)
 56    return value
 57
 58
 59@overload
 60def parse_date_and_datetime(value: None) -> None: ...
 61
 62
 63@overload
 64def parse_date_and_datetime(value: str | datetime.date) -> datetime.date: ...
 65
 66
 67def parse_date_and_datetime(value: str | datetime.date | None) -> datetime.date | None:
 68    """Coerce str into date and datetime value."""
 69    if not isinstance(value, str):
 70        return value
 71    if "T" in value or " " in value:
 72        return datetime.datetime.fromisoformat(value)
 73    return datetime.date.fromisoformat(value)
 74
 75
 76def parse_date_and_datetime_list(
 77    values: Sequence[str] | Sequence[datetime.date],
 78) -> list[datetime.date | datetime.datetime]:
 79    """Coerce list[str] into list[date | datetime] values."""
 80    if not values:
 81        return []
 82    if not isinstance(values[0], str):
 83        if TYPE_CHECKING:
 84            values = cast(list[datetime.date | datetime.datetime], values)
 85        return values
 86    if TYPE_CHECKING:
 87        values = cast(Sequence[str], values)
 88    return [
 89        datetime.datetime.fromisoformat(val)
 90        if "T" in val or " " in val
 91        else datetime.date.fromisoformat(val)
 92        for val in values
 93    ]
 94
 95
 96def parse_rdate_list(
 97    values: Sequence[Any],
 98) -> list[Any]:
 99    """Coerce list[str] into list[date | datetime | Period] values."""
100    if not values:
101        return []
102    results: list[Any] = []
103    for val in values:
104        if not isinstance(val, str):
105            results.append(val)
106        elif "/" in val:
107            # A solidus character '/' indicates a PERIOD value type (e.g. start/end or start/duration).
108            # We wrap it in a dict so that Pydantic validates it as a Period model.
109            results.append({"value": val})
110        elif "T" in val or " " in val:
111            results.append(datetime.datetime.fromisoformat(val))
112        else:
113            results.append(datetime.date.fromisoformat(val))
114    return results
def dtstamp_factory() -> datetime.datetime:
25def dtstamp_factory() -> datetime.datetime:
26    """Factory method for new event timestamps to facilitate mocking."""
27    return datetime.datetime.now(tz=datetime.UTC)

Factory method for new event timestamps to facilitate mocking.

def uid_factory() -> str:
30def uid_factory() -> str:
31    """Factory method for new uids to facilitate mocking."""
32    return str(uuid.uuid1())

Factory method for new uids to facilitate mocking.

def prodid_factory() -> str:
35def prodid_factory() -> str:
36    """Return the ical version to facilitate mocking."""
37    return f"-//{PRODID}//{VERSION}//EN"

Return the ical version to facilitate mocking.