123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239 |
- # mypy: no-warn-redundant-casts
- from __future__ import annotations
- import datetime as _datetime
- from abc import ABC
- from abc import abstractmethod
- from typing import TYPE_CHECKING
- from typing import TypeVar
- from typing import cast
- from pendulum.tz.exceptions import AmbiguousTime
- from pendulum.tz.exceptions import InvalidTimezone
- from pendulum.tz.exceptions import NonExistingTime
- from pendulum.utils._compat import zoneinfo
- if TYPE_CHECKING:
- from typing_extensions import Self
- POST_TRANSITION = "post"
- PRE_TRANSITION = "pre"
- TRANSITION_ERROR = "error"
- _DT = TypeVar("_DT", bound=_datetime.datetime)
- class PendulumTimezone(ABC):
- @property
- @abstractmethod
- def name(self) -> str:
- raise NotImplementedError
- @abstractmethod
- def convert(self, dt: _DT, raise_on_unknown_times: bool = False) -> _DT:
- raise NotImplementedError
- @abstractmethod
- def datetime(
- self,
- year: int,
- month: int,
- day: int,
- hour: int = 0,
- minute: int = 0,
- second: int = 0,
- microsecond: int = 0,
- ) -> _datetime.datetime:
- raise NotImplementedError
- class Timezone(zoneinfo.ZoneInfo, PendulumTimezone):
- """
- Represents a named timezone.
- The accepted names are those provided by the IANA time zone database.
- >>> from pendulum.tz.timezone import Timezone
- >>> tz = Timezone('Europe/Paris')
- """
- def __new__(cls, key: str) -> Self:
- try:
- return super().__new__(cls, key) # type: ignore[call-arg]
- except zoneinfo.ZoneInfoNotFoundError:
- raise InvalidTimezone(key)
- @property
- def name(self) -> str:
- return self.key
- def convert(self, dt: _DT, raise_on_unknown_times: bool = False) -> _DT:
- """
- Converts a datetime in the current timezone.
- If the datetime is naive, it will be normalized.
- >>> from datetime import datetime
- >>> from pendulum import timezone
- >>> paris = timezone('Europe/Paris')
- >>> dt = datetime(2013, 3, 31, 2, 30, fold=1)
- >>> in_paris = paris.convert(dt)
- >>> in_paris.isoformat()
- '2013-03-31T03:30:00+02:00'
- If the datetime is aware, it will be properly converted.
- >>> new_york = timezone('America/New_York')
- >>> in_new_york = new_york.convert(in_paris)
- >>> in_new_york.isoformat()
- '2013-03-30T21:30:00-04:00'
- """
- if dt.tzinfo is None:
- # Technically, utcoffset() can return None, but none of the zone information
- # in tzdata sets _tti_before to None. This can be checked with the following
- # code:
- #
- # >>> import zoneinfo
- # >>> from zoneinfo._zoneinfo import ZoneInfo
- #
- # >>> for tzname in zoneinfo.available_timezones():
- # >>> if ZoneInfo(tzname)._tti_before is None:
- # >>> print(tzname)
- offset_before = cast(
- _datetime.timedelta,
- (self.utcoffset(dt.replace(fold=0)) if dt.fold else self.utcoffset(dt)),
- )
- offset_after = cast(
- _datetime.timedelta,
- (self.utcoffset(dt) if dt.fold else self.utcoffset(dt.replace(fold=1))),
- )
- if offset_after > offset_before:
- # Skipped time
- if raise_on_unknown_times:
- raise NonExistingTime(dt)
- dt = cast(
- _DT,
- dt
- + (
- (offset_after - offset_before)
- if dt.fold
- else (offset_before - offset_after)
- ),
- )
- elif offset_before > offset_after and raise_on_unknown_times:
- # Repeated time
- raise AmbiguousTime(dt)
- return dt.replace(tzinfo=self)
- return cast(_DT, dt.astimezone(self))
- def datetime(
- self,
- year: int,
- month: int,
- day: int,
- hour: int = 0,
- minute: int = 0,
- second: int = 0,
- microsecond: int = 0,
- ) -> _datetime.datetime:
- """
- Return a normalized datetime for the current timezone.
- """
- return self.convert(
- _datetime.datetime(
- year, month, day, hour, minute, second, microsecond, fold=1
- )
- )
- def __repr__(self) -> str:
- return f"{self.__class__.__name__}('{self.name}')"
- class FixedTimezone(_datetime.tzinfo, PendulumTimezone):
- def __init__(self, offset: int, name: str | None = None) -> None:
- sign = "-" if offset < 0 else "+"
- minutes = offset / 60
- hour, minute = divmod(abs(int(minutes)), 60)
- if not name:
- name = f"{sign}{hour:02d}:{minute:02d}"
- self._name = name
- self._offset = offset
- self._utcoffset = _datetime.timedelta(seconds=offset)
- @property
- def name(self) -> str:
- return self._name
- def convert(self, dt: _DT, raise_on_unknown_times: bool = False) -> _DT:
- if dt.tzinfo is None:
- return dt.__class__(
- dt.year,
- dt.month,
- dt.day,
- dt.hour,
- dt.minute,
- dt.second,
- dt.microsecond,
- tzinfo=self,
- fold=0,
- )
- return cast(_DT, dt.astimezone(self))
- def datetime(
- self,
- year: int,
- month: int,
- day: int,
- hour: int = 0,
- minute: int = 0,
- second: int = 0,
- microsecond: int = 0,
- ) -> _datetime.datetime:
- return self.convert(
- _datetime.datetime(
- year, month, day, hour, minute, second, microsecond, fold=1
- )
- )
- @property
- def offset(self) -> int:
- return self._offset
- def utcoffset(self, dt: _datetime.datetime | None) -> _datetime.timedelta:
- return self._utcoffset
- def dst(self, dt: _datetime.datetime | None) -> _datetime.timedelta:
- return _datetime.timedelta()
- def fromutc(self, dt: _datetime.datetime) -> _datetime.datetime:
- # Use the stdlib datetime's add method to avoid infinite recursion
- return (_datetime.datetime.__add__(dt, self._utcoffset)).replace(tzinfo=self)
- def tzname(self, dt: _datetime.datetime | None) -> str | None:
- return self._name
- def __getinitargs__(self) -> tuple[int, str]:
- return self._offset, self._name
- def __repr__(self) -> str:
- name = ""
- if self._name:
- name = f', name="{self._name}"'
- return f"{self.__class__.__name__}({self._offset}{name})"
- UTC = Timezone("UTC")
|