Source code for whenever._tz.ambiguity

from __future__ import annotations

from datetime import (
    datetime as _datetime,
    timedelta as _timedelta,
)

from .._common import UTC, mk_fixed_tzinfo
from .._typing import DisambiguateStr
from .common import Fold, Gap, Unambiguous
from .tzif import TimeZone


[docs] class RepeatedTime(ValueError): """A datetime is repeated in a timezone, e.g. because of DST""" @classmethod def _for_tz(cls, d: _datetime, tzid: str | None) -> RepeatedTime: return cls(f"{d} is repeated in {_tzid_display(tzid)}")
[docs] class SkippedTime(ValueError): """A datetime is skipped in a timezone, e.g. because of DST""" @classmethod def _for_tz(cls, d: _datetime, tzid: str | None) -> SkippedTime: return cls(f"{d} is skipped in {_tzid_display(tzid)}")
def _tzid_display(tzid: str | None) -> str: if tzid is None: return "system timezone (with unknown ID)" else: return f"timezone '{tzid}'" def resolve_ambiguity( dt: _datetime, tz: TimeZone, disambiguate: DisambiguateStr | _timedelta ) -> _datetime: assert dt.tzinfo is None, "dt must be naive" if isinstance(disambiguate, _timedelta): return resolve_ambiguity_using_prev_offset(dt, disambiguate, tz) elif disambiguate not in ("compatible", "earlier", "later", "raise"): raise ValueError( "disambiguate must be 'compatible', 'earlier', 'later', or 'raise'" ) ambiguity = tz.ambiguity_for_local(int(dt.replace(tzinfo=UTC).timestamp())) match ambiguity: case Unambiguous(offset): pass case Fold(before, after): if disambiguate in ("compatible", "earlier"): offset = before elif disambiguate == "later": offset = after else: # disambiguate == "raise" raise RepeatedTime._for_tz(dt, tz.key) case Gap(before, after): # pragma: no branch if disambiguate in ("compatible", "later"): offset = before shift = before - after elif disambiguate == "earlier": offset = after shift = after - before else: # disambiguate == "raise" raise SkippedTime._for_tz(dt, tz.key) # shift the datetime out of the gap dt += _timedelta(seconds=shift) resolved = dt.replace(tzinfo=mk_fixed_tzinfo(offset)) # This ensures we raise an exception if the instant is out of range, # even if the local time is valid. resolved.astimezone(UTC) return resolved def resolve_ambiguity_using_prev_offset( dt: _datetime, prev_offset: _timedelta, tz: TimeZone ) -> _datetime: ambiguity = tz.ambiguity_for_local(int(dt.replace(tzinfo=UTC).timestamp())) offset = int(prev_offset.total_seconds()) if isinstance(ambiguity, Unambiguous): offset = ambiguity.offset elif isinstance(ambiguity, Fold): # If the offset is already valid, there's nothing to do # otherwise, always use the earlier offset if ambiguity.after != offset: offset = ambiguity.before else: # isinstance(ambiguity, Gap) # Don't try to reuse the previous offset in case of a gap, # since we can't prevent an unexpected shift anyway. # We just do the default (compatible) behavior. offset = ambiguity.before dt += _timedelta(seconds=offset - ambiguity.after) return dt.replace(tzinfo=mk_fixed_tzinfo(offset))