Exemple #1
0
class TodoAttrs(CalendarEntryAttrs):
    percent: Optional[int] = attr.ib(default=None,
                                     validator=v_optional(
                                         in_(range(0, MAX_PERCENT + 1))))
    priority: Optional[int] = attr.ib(default=None,
                                      validator=v_optional(
                                          in_(range(0, MAX_PRIORITY + 1))))
    completed: Optional[datetime] = attr.ib(
        default=None, converter=ensure_datetime)  # type: ignore
Exemple #2
0
class EventAttrs(CalendarEntryAttrs):
    classification: Optional[str] = attr.ib(default=None, validator=v_optional(instance_of(str)))

    transparent: Optional[bool] = attr.ib(default=None)
    organizer: Optional[Organizer] = attr.ib(default=None, validator=v_optional(instance_of(Organizer)))
    geo: Optional[Geo] = attr.ib(default=None, converter=make_geo)

    attendees: List[Attendee] = attr.ib(factory=list, converter=list)
    categories: List[str] = attr.ib(factory=list, converter=list)

    def add_attendee(self, attendee: Attendee):
        """ Add an attendee to the attendees set """
        check_is_instance("attendee", attendee, Attendee)
        self.attendees.append(attendee)
Exemple #3
0
class BaseAlarm(Component, metaclass=ABCMeta):
    """
    A calendar event VALARM base class
    """

    class Meta:
        name = "VALARM"
        parser = BaseAlarmParser
        serializer = BaseAlarmSerializer

    trigger: Union[timedelta, datetime, None] = attr.ib(
        default=None,
        validator=v_optional(instance_of((timedelta, datetime)))  # type: ignore
    )
    repeat: int = attr.ib(default=None, validator=call_validate_on_inst)
    duration: timedelta = attr.ib(default=None, converter=c_optional(ensure_timedelta), validator=call_validate_on_inst)  # type: ignore

    def validate(self, attr=None, value=None):
        if self.repeat is not None:
            if self.repeat < 0:
                raise ValueError("Repeat must be great than or equal to 0.")
            if self.duration is None:
                raise ValueError(
                    "A definition of an alarm with a repeating trigger MUST include both the DURATION and REPEAT properties."
                )

        if self.duration is not None and self.duration.total_seconds() < 0:
            raise ValueError("Alarm duration timespan must be positive.")

    @classmethod
    def _from_container(cls: Type[ComponentType], container: Container, *args: Any, **kwargs: Any) -> ComponentType:
        ret = super(BaseAlarm, cls)._from_container(container, *args, **kwargs)  # type: ignore
        get_lines(ret.extra, "ACTION", keep=False)  # Just drop the ACTION line
        return ret

    @property
    @abstractmethod
    def action(self):
        """ VALARM action to be implemented by concrete classes
        """
        raise NotImplementedError("Base class cannot be instantiated directly")

    def __repr__(self):
        value = "{0} trigger:{1}".format(type(self).__name__, self.trigger)
        if self.repeat:
            value += " repeat:{0} duration:{1}".format(self.repeat, self.duration)

        return "<{0}>".format(value)
Exemple #4
0
class AudioAlarm(BaseAlarm):
    """
    A calendar event VALARM with AUDIO option.
    """
    class Meta:
        name = "VALARM"
        parser = AudioAlarmParser
        serializer = AudioAlarmSerializer

    sound: Optional[ContentLine] = attr.ib(default=None,
                                           validator=v_optional(
                                               instance_of(ContentLine)))

    @property
    def action(self):
        return "AUDIO"
Exemple #5
0
class BaseAlarm(Component, metaclass=ABCMeta):
    """
    A calendar event VALARM base class
    """
    Meta = ComponentMeta("VALARM", converter_class=AlarmConverter)

    trigger: Union[timedelta, datetime, None] = attr.ib(
        default=None, validator=v_optional(instance_of(
            (timedelta, datetime))))  # TODO is this relative to begin or end?
    repeat: int = attr.ib(default=None, validator=call_validate_on_inst)
    duration: timedelta = attr.ib(
        default=None,
        converter=c_optional(ensure_timedelta),
        validator=call_validate_on_inst)  # type: ignore

    # FIXME: `attach` can be specified multiple times in a "VEVENT", "VTODO", "VJOURNAL", or "VALARM" calendar component
    #  with the exception of AUDIO alarm that only allows this property to occur once.
    #  (This property is used in "VALARM" calendar components to specify an audio sound resource or an email message attachment.)

    def validate(self, attr=None, value=None):
        if self.repeat is not None:
            if self.repeat < 0:
                raise ValueError("Repeat must be great than or equal to 0.")
            if self.duration is None:
                raise ValueError(
                    "A definition of an alarm with a repeating trigger MUST include both the DURATION and REPEAT properties."
                )

        if self.duration is not None and self.duration.total_seconds() < 0:
            raise ValueError("Alarm duration timespan must be positive.")

    @property
    @abstractmethod
    def action(self):
        """ VALARM action to be implemented by concrete classes """
        ...
Exemple #6
0
class Timespan(object):
    begin_time: Optional[datetime] = attr.ib(validator=v_optional(
        instance_of(datetime)),
                                             default=None)
    end_time: Optional[datetime] = attr.ib(validator=v_optional(
        instance_of(datetime)),
                                           default=None)
    duration: Optional[timedelta] = attr.ib(validator=v_optional(
        instance_of(timedelta)),
                                            default=None)
    precision: str = attr.ib(default="second")

    def _end_name(self) -> str:
        return "end"

    def __attrs_post_init__(self):
        self.validate()

    def replace(self: TimespanT,
                begin_time: Union[datetime, None, "Literal[False]"] = False,
                end_time: Union[datetime, None, "Literal[False]"] = False,
                duration: Union[timedelta, None, "Literal[False]"] = False,
                precision: Union[str, "Literal[False]"] = False) -> TimespanT:
        if begin_time is False:
            begin_time = self.begin_time
        if end_time is False:
            end_time = self.end_time
        if duration is False:
            duration = self.duration
        if precision is False:
            precision = self.precision
        return type(self)(begin_time=cast(Optional[datetime], begin_time),
                          end_time=cast(Optional[datetime], end_time),
                          duration=cast(Optional[timedelta], duration),
                          precision=cast(str, precision))

    def replace_timezone(self: TimespanT,
                         tzinfo: Optional[TZInfo]) -> TimespanT:
        if self.is_all_day():
            raise ValueError("can't replace timezone of all-day event")
        begin = self.get_begin()
        if begin is not None:
            begin = begin.replace(tzinfo=tzinfo)
        if self.end_time is not None:
            return self.replace(begin_time=begin,
                                end_time=self.end_time.replace(tzinfo=tzinfo))
        else:
            return self.replace(begin_time=begin)

    def convert_timezone(self: TimespanT,
                         tzinfo: Optional[TZInfo]) -> TimespanT:
        if self.is_all_day():
            raise ValueError("can't convert timezone of all-day timespan")
        if self.is_floating():
            raise ValueError(
                "can't convert timezone of timezone-naive floating timespan, use replace_timezone"
            )
        begin = self.get_begin()
        if begin is not None:
            begin = begin.astimezone(tzinfo)
        if self.end_time is not None:
            return self.replace(begin_time=begin,
                                end_time=self.end_time.astimezone(tzinfo))
        else:
            return self.replace(begin_time=begin)

    def validate(self):
        def validate_timeprecision(value, name):
            if self.precision == "day":
                if floor_datetime_to_midnight(value) != value:
                    raise ValueError(
                        "%s time value %s has higher precision than set precision %s"
                        % (name, value, self.precision))
                if value.tzinfo is not None:
                    raise ValueError(
                        "all-day timespan %s time %s can't have a timezone" %
                        (name, value))

        if self.begin_time is not None:
            validate_timeprecision(self.begin_time, "begin")

            if self.end_time is not None:
                validate_timeprecision(self.end_time, self._end_name())
                if self.begin_time > self.end_time:
                    raise ValueError("begin time must be before " +
                                     self._end_name() + " time")
                if self.precision == "day" and self.end_time < (
                        self.begin_time + TIMEDELTA_DAY):
                    raise ValueError(
                        "all-day timespan duration must be at least one day")
                if self.duration is not None:
                    raise ValueError("can't set duration together with " +
                                     self._end_name() + " time")
                if self.begin_time.tzinfo is None and self.end_time.tzinfo is not None:
                    raise ValueError(
                        self._end_name() +
                        " time may not have a timezone as the begin time doesn't either"
                    )
                if self.begin_time.tzinfo is not None and self.end_time.tzinfo is None:
                    raise ValueError(
                        self._end_name() +
                        " time must have a timezone as the begin time also does"
                    )
                duration = self.get_effective_duration()
                if duration and not timedelta_nearly_zero(
                        duration % TIMEDELTA_CACHE[self.precision]):
                    raise ValueError(
                        "effective duration value %s has higher precision than set precision %s"
                        % (self.get_effective_duration(), self.precision))

            if self.duration is not None:
                if self.duration < TIMEDELTA_ZERO:
                    raise ValueError("timespan duration must be positive")
                if self.precision == "day" and self.duration < TIMEDELTA_DAY:
                    raise ValueError(
                        "all-day timespan duration must be at least one day")
                if not timedelta_nearly_zero(
                        self.duration % TIMEDELTA_CACHE[self.precision]):
                    raise ValueError(
                        "duration value %s has higher precision than set precision %s"
                        % (self.duration, self.precision))

        else:
            if self.end_time is not None:
                # Todos might have end/due time without begin
                validate_timeprecision(self.end_time, self._end_name())

            if self.duration is not None:
                raise ValueError(
                    "timespan without begin time can't have duration")

    def get_str_segments(self):
        if self.is_all_day():
            prefix = ["all-day"]
        elif self.is_floating():
            prefix = ["floating"]
        else:
            prefix = []

        suffix = []

        begin = self.begin_time
        if begin is not None:
            suffix.append("begin:")
            if self.is_all_day():
                suffix.append(begin.strftime('%Y-%m-%d'))
            else:
                suffix.append(str(begin))

        end = self.get_effective_end()
        end_repr = self.get_end_representation()
        if end is not None:
            if end_repr == "end":
                suffix.append("fixed")
            suffix.append(self._end_name() + ":")
            if self.is_all_day():
                suffix.append(end.strftime('%Y-%m-%d'))
            else:
                suffix.append(str(end))

        duration = self.get_effective_duration()
        if duration is not None and end_repr is not None:
            if end_repr == "duration":
                suffix.append("fixed")
            suffix.append("duration:")
            suffix.append(str(duration))

        return prefix, [self.__class__.__name__], suffix

    def __str__(self) -> str:
        prefix, name, suffix = self.get_str_segments()
        return "<%s>" % (" ".join(prefix + name + suffix))

    def __bool__(self):
        return self.begin_time is not None or self.end_time is not None

    ####################################################################################################################

    def make_all_day(self) -> "Timespan":
        if self.is_all_day():
            return self  # Do nothing if we already are a all day timespan

        begin = self.begin_time
        if begin is not None:
            begin = floor_datetime_to_midnight(begin).replace(tzinfo=None)

        end = self.get_effective_end()
        if end is not None:
            end = ceil_datetime_to_midnight(end).replace(tzinfo=None)
            if end == begin:  # we also add another day if the duration would be 0 otherwise
                end = end + TIMEDELTA_DAY

        if self.get_end_representation() == "duration":
            assert end is not None
            assert begin is not None
            return self.replace(begin, None, end - begin, "day")
        else:
            return self.replace(begin, end, None, "day")

    def convert_end(self, target: Optional[str]) -> "Timespan":
        current = self.get_end_representation()
        current_is_end = current == "end" or current == self._end_name()
        target_is_end = target == "end" or target == self._end_name()
        if current == target or (current_is_end and target_is_end):
            return self
        elif current_is_end and target == "duration":
            return self.replace(end_time=None,
                                duration=self.get_effective_duration())
        elif current == "duration" and target_is_end:
            return self.replace(end_time=self.get_effective_end(),
                                duration=None)
        elif target is None:
            return self.replace(end_time=None, duration=None)
        else:
            raise ValueError("can't convert from representation %s to %s" %
                             (current, target))

    ####################################################################################################################

    def get_begin(self) -> Optional[datetime]:
        return self.begin_time

    def get_effective_end(self) -> Optional[datetime]:
        if self.end_time is not None:
            return self.end_time
        elif self.begin_time is not None:
            duration = self.get_effective_duration()
            if duration is not None:
                return self.begin_time + duration

        return None

    def get_effective_duration(self) -> Optional[timedelta]:
        if self.duration is not None:
            return self.duration
        elif self.end_time is not None and self.begin_time is not None:
            return self.end_time - self.begin_time
        else:
            return None

    def get_precision(self) -> str:
        return self.precision

    def is_all_day(self) -> bool:
        return self.precision == "day"

    def is_floating(self) -> bool:
        if self.begin_time is None:
            if self.end_time is None:
                return True
            else:
                return self.end_time.tzinfo is None
        else:
            return self.begin_time.tzinfo is None

    def get_end_representation(self) -> Optional[str]:
        if self.duration is not None:
            return "duration"
        elif self.end_time is not None:
            return "end"
        else:
            return None

    def has_explicit_end(self) -> bool:
        return self.get_end_representation() is not None

    ####################################################################################################################

    @overload
    def timespan_tuple(
            self,
            default: None = None,
            normalization: Normalization = None) -> NullableTimespanTuple:
        ...

    @overload
    def timespan_tuple(self,
                       default: datetime,
                       normalization: Normalization = None) -> TimespanTuple:
        ...

    def timespan_tuple(self, default=None, normalization=None):
        if normalization:
            return TimespanTuple(
                normalization.normalize(self.get_begin() or default),
                normalization.normalize(self.get_effective_end() or default))
        else:
            return TimespanTuple(self.get_begin() or default,
                                 self.get_effective_end() or default)

    def cmp_tuple(self) -> TimespanTuple:
        return self.timespan_tuple(default=CMP_DATETIME_NONE_DEFAULT,
                                   normalization=CMP_NORMALIZATION)

    def __require_tuple_components(self, values, *required):
        for nr, (val, req) in enumerate(zip(values, required)):
            if req and val is None:
                event = "this event" if nr < 2 else "other event"
                prop = "begin" if nr % 2 == 0 else "end"
                raise ValueError("%s has no %s time" % (event, prop))

    def starts_within(self, other: "Timespan") -> bool:
        first = cast(TimespanTuple,
                     self.timespan_tuple(normalization=CMP_NORMALIZATION))
        second = cast(TimespanTuple,
                      other.timespan_tuple(normalization=CMP_NORMALIZATION))
        self.__require_tuple_components(first + second, True, False, True,
                                        True)

        # the timespan doesn't include its end instant / day
        return second.begin <= first.begin < second.end

    def ends_within(self, other: "Timespan") -> bool:
        first = cast(TimespanTuple,
                     self.timespan_tuple(normalization=CMP_NORMALIZATION))
        second = cast(TimespanTuple,
                      other.timespan_tuple(normalization=CMP_NORMALIZATION))
        self.__require_tuple_components(first + second, False, True, True,
                                        True)

        # the timespan doesn't include its end instant / day
        return second.begin <= first.end < second.end

    def intersects(self, other: "Timespan") -> bool:
        first = cast(TimespanTuple,
                     self.timespan_tuple(normalization=CMP_NORMALIZATION))
        second = cast(TimespanTuple,
                      other.timespan_tuple(normalization=CMP_NORMALIZATION))
        self.__require_tuple_components(first + second, True, True, True, True)

        # the timespan doesn't include its end instant / day
        return second.begin <= first.begin < second.end or \
               second.begin <= first.end < second.end or \
               first.begin <= second.begin < first.end or \
               first.begin <= second.end < first.end

    def includes(self, other: Union["Timespan", datetime]) -> bool:
        if isinstance(other, datetime):
            first = cast(TimespanTuple,
                         self.timespan_tuple(normalization=CMP_NORMALIZATION))
            other = CMP_NORMALIZATION.normalize(other)
            self.__require_tuple_components(first, True, True)

            # the timespan doesn't include its end instant / day
            return first.begin <= other < first.end

        else:
            first = cast(TimespanTuple,
                         self.timespan_tuple(normalization=CMP_NORMALIZATION))
            second = cast(
                TimespanTuple,
                other.timespan_tuple(normalization=CMP_NORMALIZATION))
            self.__require_tuple_components(first + second, True, True, True,
                                            True)

            # the timespan doesn't include its end instant / day
            return first.begin <= second.begin and second.end < first.end

    __contains__ = includes

    def is_included_in(self, other: "Timespan") -> bool:
        first = cast(TimespanTuple,
                     self.timespan_tuple(normalization=CMP_NORMALIZATION))
        second = cast(TimespanTuple,
                      other.timespan_tuple(normalization=CMP_NORMALIZATION))
        self.__require_tuple_components(first + second, True, True, True, True)

        # the timespan doesn't include its end instant / day
        return second.begin <= first.begin and first.end < second.end

    def __lt__(self, other: Any) -> bool:
        if isinstance(other, Timespan):
            return self.cmp_tuple() < other.cmp_tuple()
        else:
            return NotImplemented

    def __gt__(self, other: Any) -> bool:
        if isinstance(other, Timespan):
            return self.cmp_tuple() > other.cmp_tuple()
        else:
            return NotImplemented

    def __le__(self, other: Any) -> bool:
        if isinstance(other, Timespan):
            return self.cmp_tuple() <= other.cmp_tuple()
        else:
            return NotImplemented

    def __ge__(self, other: Any) -> bool:
        if isinstance(other, Timespan):
            return self.cmp_tuple() >= other.cmp_tuple()
        else:
            return NotImplemented