예제 #1
0
 def is_transition(self, dt1: datetime, dt2: datetime) -> bool:
     """Determine if dt1 -> dt2 is a UTC offset transition. If
     detect_dst_transition is True, then also detect DST offset transition.
     """
     if dt1.utcoffset() != dt2.utcoffset():
         return True
     if self.detect_dst_transition:
         return dt1.dst() != dt2.dst()
     return False
예제 #2
0
    def _create_test_item(dt: datetime, tag: str) -> TestItem:
        """Create a TestItem from a datetime."""
        unix_seconds = int(dt.timestamp())
        epoch_seconds = unix_seconds - SECONDS_SINCE_UNIX_EPOCH
        total_offset = int(dt.utcoffset().total_seconds())  # type: ignore
        dst_offset = int(dt.dst().total_seconds())  # type: ignore

        # See https://stackoverflow.com/questions/5946499 for more info on how
        # to extract the abbreviation. dt.tzinfo will never be None because the
        # timezone will always be defined.
        assert dt.tzinfo is not None
        abbrev = dt.tzinfo.tzname(dt)

        return {
            'epoch': epoch_seconds,
            'total_offset': total_offset,
            'dst_offset': dst_offset,
            'y': dt.year,
            'M': dt.month,
            'd': dt.day,
            'h': dt.hour,
            'm': dt.minute,
            's': dt.second,
            'abbrev': abbrev,
            'type': tag,
        }
예제 #3
0
    def _write_timestamp(self, value: datetime.datetime) -> typing.NoReturn:
        """Write a datetime.datetime value

        :param datetime.datetime value: The value to write

        """
        self._write_int(value.second)
        self._write_int(value.minute)
        self._write_int(value.hour)
        self._write_int(value.day)
        self._write_int(value.month - 1)
        self._write_int(value.year - 1900)
        self._write_int(1 if value.dst() else 0)
예제 #4
0
def recode_timezone_info(dt: datetime.datetime):
    name = dt.tzname()
    dst = dt.dst()
    dst = (' ' + dst) if dst != datetime.timedelta() else ''

    if name == 'UTC':
        return f'{name}{dst}'

    offset = dt.utcoffset()
    offset = ('+' if offset >= datetime.timedelta() else '') + str(offset.total_seconds() / 3600)

    if name is None or not name:
        return f'UTC{offset}{dst}'

    return f'{name} (UTC{offset}{dst})'
예제 #5
0
def find_next_time_expression_time(now: dt.datetime, seconds: List[int],
                                   minutes: List[int],
                                   hours: List[int]) -> dt.datetime:
    """Find the next datetime from now for which the time expression matches.

    The algorithm looks at each time unit separately and tries to find the
    next one that matches for each. If any of them would roll over, all
    time units below that are reset to the first matching value.

    Timezones are also handled (the tzinfo of the now object is used),
    including daylight saving time.
    """
    if not seconds or not minutes or not hours:
        raise ValueError("Cannot find a next time: Time expression never "
                         "matches!")

    def _lower_bound(arr: List[int], cmp: int) -> Optional[int]:
        """Return the first value in arr greater or equal to cmp.

        Return None if no such value exists.
        """
        left = 0
        right = len(arr)
        while left < right:
            mid = (left + right) // 2
            if arr[mid] < cmp:
                left = mid + 1
            else:
                right = mid

        if left == len(arr):
            return None
        return arr[left]

    result = now.replace(microsecond=0)

    # Match next second
    next_second = _lower_bound(seconds, result.second)
    if next_second is None:
        # No second to match in this minute. Roll-over to next minute.
        next_second = seconds[0]
        result += dt.timedelta(minutes=1)

    result = result.replace(second=next_second)

    # Match next minute
    next_minute = _lower_bound(minutes, result.minute)
    if next_minute != result.minute:
        # We're in the next minute. Seconds needs to be reset.
        result = result.replace(second=seconds[0])

    if next_minute is None:
        # No minute to match in this hour. Roll-over to next hour.
        next_minute = minutes[0]
        result += dt.timedelta(hours=1)

    result = result.replace(minute=next_minute)

    # Match next hour
    next_hour = _lower_bound(hours, result.hour)
    if next_hour != result.hour:
        # We're in the next hour. Seconds+minutes needs to be reset.
        result = result.replace(second=seconds[0], minute=minutes[0])

    if next_hour is None:
        # No minute to match in this day. Roll-over to next day.
        next_hour = hours[0]
        result += dt.timedelta(days=1)

    result = result.replace(hour=next_hour)

    if result.tzinfo is None:
        return result

    # Now we need to handle timezones. We will make this datetime object
    # "naive" first and then re-convert it to the target timezone.
    # This is so that we can call pytz's localize and handle DST changes.
    tzinfo = result.tzinfo  # type: pytzinfo.DstTzInfo
    result = result.replace(tzinfo=None)

    try:
        result = tzinfo.localize(result, is_dst=None)
    except pytzexceptions.AmbiguousTimeError:
        # This happens when we're leaving daylight saving time and local
        # clocks are rolled back. In this case, we want to trigger
        # on both the DST and non-DST time. So when "now" is in the DST
        # use the DST-on time, and if not, use the DST-off time.
        use_dst = bool(now.dst())
        result = tzinfo.localize(result, is_dst=use_dst)
    except pytzexceptions.NonExistentTimeError:
        # This happens when we're entering daylight saving time and local
        # clocks are rolled forward, thus there are local times that do
        # not exist. In this case, we want to trigger on the next time
        # that *does* exist.
        # In the worst case, this will run through all the seconds in the
        # time shift, but that's max 3600 operations for once per year
        result = result.replace(tzinfo=tzinfo) + dt.timedelta(seconds=1)
        return find_next_time_expression_time(result, seconds, minutes, hours)

    result_dst = cast(dt.timedelta, result.dst())
    now_dst = cast(dt.timedelta, now.dst())
    if result_dst >= now_dst:
        return result

    # Another edge-case when leaving DST:
    # When now is in DST and ambiguous *and* the next trigger time we *should*
    # trigger is ambiguous and outside DST, the excepts above won't catch it.
    # For example: if triggering on 2:30 and now is 28.10.2018 2:30 (in DST)
    # we should trigger next on 28.10.2018 2:30 (out of DST), but our
    # algorithm above would produce 29.10.2018 2:30 (out of DST)

    # Step 1: Check if now is ambiguous
    try:
        tzinfo.localize(now.replace(tzinfo=None), is_dst=None)
        return result
    except pytzexceptions.AmbiguousTimeError:
        pass

    # Step 2: Check if result of (now - DST) is ambiguous.
    check = now - now_dst
    check_result = find_next_time_expression_time(check, seconds, minutes,
                                                  hours)
    try:
        tzinfo.localize(check_result.replace(tzinfo=None), is_dst=None)
        return result
    except pytzexceptions.AmbiguousTimeError:
        pass

    # OK, edge case does apply. We must override the DST to DST-off
    check_result = tzinfo.localize(check_result.replace(tzinfo=None),
                                   is_dst=False)
    return check_result
예제 #6
0
def find_next_time_expression_time(
    now: dt.datetime,  # pylint: disable=redefined-outer-name
    seconds: list[int],
    minutes: list[int],
    hours: list[int],
) -> dt.datetime:
    """Find the next datetime from now for which the time expression matches.

    The algorithm looks at each time unit separately and tries to find the
    next one that matches for each. If any of them would roll over, all
    time units below that are reset to the first matching value.

    Timezones are also handled (the tzinfo of the now object is used),
    including daylight saving time.
    """
    if not seconds or not minutes or not hours:
        raise ValueError("Cannot find a next time: Time expression never matches!")

    def _lower_bound(arr: list[int], cmp: int) -> int | None:
        """Return the first value in arr greater or equal to cmp.

        Return None if no such value exists.
        """
        left = bisect.bisect_left(arr, cmp)
        if left == len(arr):
            return None
        return arr[left]

    result = now.replace(microsecond=0)

    # Match next second
    next_second = _lower_bound(seconds, result.second)
    if next_second is None:
        # No second to match in this minute. Roll-over to next minute.
        next_second = seconds[0]
        result += dt.timedelta(minutes=1)

    result = result.replace(second=next_second)

    # Match next minute
    next_minute = _lower_bound(minutes, result.minute)
    if next_minute != result.minute:
        # We're in the next minute. Seconds needs to be reset.
        result = result.replace(second=seconds[0])

    if next_minute is None:
        # No minute to match in this hour. Roll-over to next hour.
        next_minute = minutes[0]
        result += dt.timedelta(hours=1)

    result = result.replace(minute=next_minute)

    # Match next hour
    next_hour = _lower_bound(hours, result.hour)
    if next_hour != result.hour:
        # We're in the next hour. Seconds+minutes needs to be reset.
        result = result.replace(second=seconds[0], minute=minutes[0])

    if next_hour is None:
        # No minute to match in this day. Roll-over to next day.
        next_hour = hours[0]
        result += dt.timedelta(days=1)

    result = result.replace(hour=next_hour)

    if result.tzinfo in (None, UTC):
        return result

    if _datetime_ambiguous(result):
        # This happens when we're leaving daylight saving time and local
        # clocks are rolled back. In this case, we want to trigger
        # on both the DST and non-DST time. So when "now" is in the DST
        # use the DST-on time, and if not, use the DST-off time.
        fold = 1 if now.dst() else 0
        if result.fold != fold:
            result = result.replace(fold=fold)

    if not _datetime_exists(result):
        # This happens when we're entering daylight saving time and local
        # clocks are rolled forward, thus there are local times that do
        # not exist. In this case, we want to trigger on the next time
        # that *does* exist.
        # In the worst case, this will run through all the seconds in the
        # time shift, but that's max 3600 operations for once per year
        return find_next_time_expression_time(
            result + dt.timedelta(seconds=1), seconds, minutes, hours
        )

    # Another edge-case when leaving DST:
    # When now is in DST and ambiguous *and* the next trigger time we *should*
    # trigger is ambiguous and outside DST, the excepts above won't catch it.
    # For example: if triggering on 2:30 and now is 28.10.2018 2:30 (in DST)
    # we should trigger next on 28.10.2018 2:30 (out of DST), but our
    # algorithm above would produce 29.10.2018 2:30 (out of DST)
    if _datetime_ambiguous(now):
        check_result = find_next_time_expression_time(
            now + _dst_offset_diff(now), seconds, minutes, hours
        )
        if _datetime_ambiguous(check_result):
            return check_result

    return result
예제 #7
0
def export_tzinfo(dt: datetime):
    return dt.replace(tzinfo=None), dt.replace(tzinfo=None).timestamp(
    ), dt.fold, dt.tzname(), dt.utcoffset(), dt.dst()
예제 #8
0
 def from_transition_dt(cls, trans_tz: datetime):
     before_tz = trans_tz - timedelta(minutes=1)
     return cls(tzoffsetfrom=before_tz.utcoffset(),
                tzoffsetto=trans_tz.utcoffset(),
                dst=trans_tz.dst(),
                tzname=trans_tz.tzname())
예제 #9
0
 def only_dst(self, dt1: datetime, dt2: datetime) -> bool:
     """Determine if dt1 -> dt2 is only a DST transition."""
     if not self.detect_dst_transition:
         return False
     return dt1.utcoffset() == dt2.utcoffset() and dt1.dst() != dt2.dst()