File: period.py

package info (click to toggle)
python-ical 12.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,776 kB
  • sloc: python: 15,157; sh: 9; makefile: 5
file content (144 lines) | stat: -rw-r--r-- 4,914 bytes parent folder | download
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]