1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
|
"""Library for parsing and encoding PERIOD values."""
import dataclasses
import datetime
import enum
import logging
from typing import Any, Optional, Self
from pydantic import BaseModel, ConfigDict, Field, field_serializer, model_validator
from ical.parsing.property import ParsedProperty, ParsedPropertyParameter
from .data_types import DATA_TYPE, encode_model_property_params, serialize_field
from .date_time import DateTimeEncoder
from .duration import DurationEncoder
from .parsing import parse_parameter_values
_LOGGER = logging.getLogger(__name__)
@DATA_TYPE.register("FBTYPE")
class FreeBusyType(str, enum.Enum):
"""Specifies the free/busy time type."""
FREE = "FREE"
"""The time interval is free for scheduling."""
BUSY = "BUSY"
"""One or more events have been scheduled for the interval."""
BUSY_UNAVAILABLE = "BUSY-UNAVAILABLE"
"""The interval can not be scheduled."""
BUSY_TENTATIVE = "BUSY-TENTATIVE"
"""One or more events have been tentatively scheduled for the interval."""
@classmethod
def __parse_property_value__(cls, prop: ParsedProperty) -> Self | None:
"""Parse value into enum."""
try:
return cls(prop.value)
except ValueError:
return None
@DATA_TYPE.register("PERIOD")
class Period(BaseModel):
"""A value with a precise period of time."""
start: datetime.datetime
"""Start of the period of time."""
end: Optional[datetime.datetime] = None
"""End of the period of the time (duration is implicit)."""
duration: Optional[datetime.timedelta] = None
"""Duration of the period of time (end time is implicit)."""
# Context specific property parameters
free_busy_type: Optional[FreeBusyType] = Field(alias="FBTYPE", default=None)
"""Specifies the free or busy time type."""
_parse_parameter_values = model_validator(mode="before")(parse_parameter_values)
@property
def end_value(self) -> datetime.datetime:
"""A computed end value based on either end or duration."""
if self.end:
return self.end
if not self.duration:
raise ValueError("Invalid period missing both end and duration")
return self.start + self.duration
@model_validator(mode="before")
@classmethod
def parse_period_fields(cls, values: dict[str, Any]) -> dict[str, Any]:
"""Parse a rfc5545 priority value."""
if not (value := values.pop("value", None)):
return values
parts = value.split("/")
if len(parts) != 2:
raise ValueError(f"Period did not have two time values: {value}")
try:
start = DateTimeEncoder.__parse_property_value__(
ParsedProperty(name="ignored", value=parts[0])
)
except ValueError as err:
_LOGGER.debug("Failed to parse start date as date time: %s", parts[0])
raise err
values["start"] = start
try:
end = DateTimeEncoder.__parse_property_value__(
ParsedProperty(name="ignored", value=parts[1])
)
except ValueError:
pass
else:
values["end"] = end
return values
try:
duration = DurationEncoder.__parse_property_value__(
ParsedProperty(name="ignored", value=parts[1])
)
except ValueError as err:
raise err
values["duration"] = duration
return values
@classmethod
def __parse_property_value__(cls, prop: ParsedProperty) -> dict[str, str]:
"""Convert the property into a dictionary for pydantic model."""
return dataclasses.asdict(prop)
@classmethod
def __encode_property_value__(cls, model_data: dict[str, Any]) -> str:
"""Encode property value."""
if not (start := model_data.pop("start", None)):
raise ValueError(f"Invalid period object missing start: {model_data}")
end = model_data.pop("end", None)
duration = model_data.pop("duration", None)
if not end and not duration:
raise ValueError(
f"Invalid period missing both end and duration: {model_data}"
)
# End and duration are already encoded values
if end:
return "/".join([start, end])
return "/".join([start, duration])
@classmethod
def __encode_property_params__(
cls, model_data: dict[str, Any]
) -> list[ParsedPropertyParameter]:
return encode_model_property_params(
cls.model_fields,
{
k: v
for k, v in model_data.items()
if k not in ("end", "duration", "start")
},
)
model_config = ConfigDict(populate_by_name=True)
serialize_fields = field_serializer("*")(serialize_field) # type: ignore[pydantic-field]
|