summaryrefslogtreecommitdiffstats
path: root/src/pendulum/tz
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2023-12-17 14:32:20 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2023-12-17 14:32:20 +0000
commitdb51f7f103bbbd6c91c8f47d75b3482ef8939691 (patch)
treeab59b1147bd0cd39f31a48073cff236ede4ec1df /src/pendulum/tz
parentAdding upstream version 3.0.0~a1. (diff)
downloadpendulum-db51f7f103bbbd6c91c8f47d75b3482ef8939691.tar.xz
pendulum-db51f7f103bbbd6c91c8f47d75b3482ef8939691.zip
Adding upstream version 3.0.0.upstream/3.0.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/pendulum/tz')
-rw-r--r--src/pendulum/tz/__init__.py64
-rw-r--r--src/pendulum/tz/data/__init__.py0
-rw-r--r--src/pendulum/tz/data/windows.py140
-rw-r--r--src/pendulum/tz/exceptions.py33
-rw-r--r--src/pendulum/tz/local_timezone.py264
-rw-r--r--src/pendulum/tz/timezone.py239
6 files changed, 740 insertions, 0 deletions
diff --git a/src/pendulum/tz/__init__.py b/src/pendulum/tz/__init__.py
new file mode 100644
index 0000000..36c2c69
--- /dev/null
+++ b/src/pendulum/tz/__init__.py
@@ -0,0 +1,64 @@
+from __future__ import annotations
+
+from pathlib import Path
+from typing import cast
+
+from pendulum.tz.local_timezone import get_local_timezone
+from pendulum.tz.local_timezone import set_local_timezone
+from pendulum.tz.local_timezone import test_local_timezone
+from pendulum.tz.timezone import UTC
+from pendulum.tz.timezone import FixedTimezone
+from pendulum.tz.timezone import Timezone
+from pendulum.utils._compat import resources
+
+
+PRE_TRANSITION = "pre"
+POST_TRANSITION = "post"
+TRANSITION_ERROR = "error"
+
+_timezones = None
+
+_tz_cache: dict[int, FixedTimezone] = {}
+
+
+def timezones() -> tuple[str, ...]:
+ global _timezones
+
+ if _timezones is None:
+ with cast(Path, resources.files("tzdata").joinpath("zones")).open() as f:
+ _timezones = tuple(tz.strip() for tz in f.readlines())
+
+ return _timezones
+
+
+def fixed_timezone(offset: int) -> FixedTimezone:
+ """
+ Return a Timezone instance given its offset in seconds.
+ """
+ if offset in _tz_cache:
+ return _tz_cache[offset]
+
+ tz = FixedTimezone(offset)
+ _tz_cache[offset] = tz
+
+ return tz
+
+
+def local_timezone() -> Timezone | FixedTimezone:
+ """
+ Return the local timezone.
+ """
+ return get_local_timezone()
+
+
+__all__ = [
+ "UTC",
+ "Timezone",
+ "FixedTimezone",
+ "set_local_timezone",
+ "get_local_timezone",
+ "test_local_timezone",
+ "fixed_timezone",
+ "local_timezone",
+ "timezones",
+]
diff --git a/src/pendulum/tz/data/__init__.py b/src/pendulum/tz/data/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/pendulum/tz/data/__init__.py
diff --git a/src/pendulum/tz/data/windows.py b/src/pendulum/tz/data/windows.py
new file mode 100644
index 0000000..e76e6bb
--- /dev/null
+++ b/src/pendulum/tz/data/windows.py
@@ -0,0 +1,140 @@
+from __future__ import annotations
+
+
+windows_timezones = {
+ "AUS Central Standard Time": "Australia/Darwin",
+ "AUS Eastern Standard Time": "Australia/Sydney",
+ "Afghanistan Standard Time": "Asia/Kabul",
+ "Alaskan Standard Time": "America/Anchorage",
+ "Aleutian Standard Time": "America/Adak",
+ "Altai Standard Time": "Asia/Barnaul",
+ "Arab Standard Time": "Asia/Riyadh",
+ "Arabian Standard Time": "Asia/Dubai",
+ "Arabic Standard Time": "Asia/Baghdad",
+ "Argentina Standard Time": "America/Buenos_Aires",
+ "Astrakhan Standard Time": "Europe/Astrakhan",
+ "Atlantic Standard Time": "America/Halifax",
+ "Aus Central W. Standard Time": "Australia/Eucla",
+ "Azerbaijan Standard Time": "Asia/Baku",
+ "Azores Standard Time": "Atlantic/Azores",
+ "Bahia Standard Time": "America/Bahia",
+ "Bangladesh Standard Time": "Asia/Dhaka",
+ "Belarus Standard Time": "Europe/Minsk",
+ "Bougainville Standard Time": "Pacific/Bougainville",
+ "Canada Central Standard Time": "America/Regina",
+ "Cape Verde Standard Time": "Atlantic/Cape_Verde",
+ "Caucasus Standard Time": "Asia/Yerevan",
+ "Cen. Australia Standard Time": "Australia/Adelaide",
+ "Central America Standard Time": "America/Guatemala",
+ "Central Asia Standard Time": "Asia/Almaty",
+ "Central Brazilian Standard Time": "America/Cuiaba",
+ "Central Europe Standard Time": "Europe/Budapest",
+ "Central European Standard Time": "Europe/Warsaw",
+ "Central Pacific Standard Time": "Pacific/Guadalcanal",
+ "Central Standard Time": "America/Chicago",
+ "Central Standard Time (Mexico)": "America/Mexico_City",
+ "Chatham Islands Standard Time": "Pacific/Chatham",
+ "China Standard Time": "Asia/Shanghai",
+ "Cuba Standard Time": "America/Havana",
+ "Dateline Standard Time": "Etc/GMT+12",
+ "E. Africa Standard Time": "Africa/Nairobi",
+ "E. Australia Standard Time": "Australia/Brisbane",
+ "E. Europe Standard Time": "Europe/Chisinau",
+ "E. South America Standard Time": "America/Sao_Paulo",
+ "Easter Island Standard Time": "Pacific/Easter",
+ "Eastern Standard Time": "America/New_York",
+ "Eastern Standard Time (Mexico)": "America/Cancun",
+ "Egypt Standard Time": "Africa/Cairo",
+ "Ekaterinburg Standard Time": "Asia/Yekaterinburg",
+ "FLE Standard Time": "Europe/Kiev",
+ "Fiji Standard Time": "Pacific/Fiji",
+ "GMT Standard Time": "Europe/London",
+ "GTB Standard Time": "Europe/Bucharest",
+ "Georgian Standard Time": "Asia/Tbilisi",
+ "Greenland Standard Time": "America/Godthab",
+ "Greenwich Standard Time": "Atlantic/Reykjavik",
+ "Haiti Standard Time": "America/Port-au-Prince",
+ "Hawaiian Standard Time": "Pacific/Honolulu",
+ "India Standard Time": "Asia/Calcutta",
+ "Iran Standard Time": "Asia/Tehran",
+ "Israel Standard Time": "Asia/Jerusalem",
+ "Jordan Standard Time": "Asia/Amman",
+ "Kaliningrad Standard Time": "Europe/Kaliningrad",
+ "Korea Standard Time": "Asia/Seoul",
+ "Libya Standard Time": "Africa/Tripoli",
+ "Line Islands Standard Time": "Pacific/Kiritimati",
+ "Lord Howe Standard Time": "Australia/Lord_Howe",
+ "Magadan Standard Time": "Asia/Magadan",
+ "Magallanes Standard Time": "America/Punta_Arenas",
+ "Marquesas Standard Time": "Pacific/Marquesas",
+ "Mauritius Standard Time": "Indian/Mauritius",
+ "Middle East Standard Time": "Asia/Beirut",
+ "Montevideo Standard Time": "America/Montevideo",
+ "Morocco Standard Time": "Africa/Casablanca",
+ "Mountain Standard Time": "America/Denver",
+ "Mountain Standard Time (Mexico)": "America/Chihuahua",
+ "Myanmar Standard Time": "Asia/Rangoon",
+ "N. Central Asia Standard Time": "Asia/Novosibirsk",
+ "Namibia Standard Time": "Africa/Windhoek",
+ "Nepal Standard Time": "Asia/Katmandu",
+ "New Zealand Standard Time": "Pacific/Auckland",
+ "Newfoundland Standard Time": "America/St_Johns",
+ "Norfolk Standard Time": "Pacific/Norfolk",
+ "North Asia East Standard Time": "Asia/Irkutsk",
+ "North Asia Standard Time": "Asia/Krasnoyarsk",
+ "North Korea Standard Time": "Asia/Pyongyang",
+ "Omsk Standard Time": "Asia/Omsk",
+ "Pacific SA Standard Time": "America/Santiago",
+ "Pacific Standard Time": "America/Los_Angeles",
+ "Pacific Standard Time (Mexico)": "America/Tijuana",
+ "Pakistan Standard Time": "Asia/Karachi",
+ "Paraguay Standard Time": "America/Asuncion",
+ "Romance Standard Time": "Europe/Paris",
+ "Russia Time Zone 10": "Asia/Srednekolymsk",
+ "Russia Time Zone 11": "Asia/Kamchatka",
+ "Russia Time Zone 3": "Europe/Samara",
+ "Russian Standard Time": "Europe/Moscow",
+ "SA Eastern Standard Time": "America/Cayenne",
+ "SA Pacific Standard Time": "America/Bogota",
+ "SA Western Standard Time": "America/La_Paz",
+ "SE Asia Standard Time": "Asia/Bangkok",
+ "Saint Pierre Standard Time": "America/Miquelon",
+ "Sakhalin Standard Time": "Asia/Sakhalin",
+ "Samoa Standard Time": "Pacific/Apia",
+ "Sao Tome Standard Time": "Africa/Sao_Tome",
+ "Saratov Standard Time": "Europe/Saratov",
+ "Singapore Standard Time": "Asia/Singapore",
+ "South Africa Standard Time": "Africa/Johannesburg",
+ "Sri Lanka Standard Time": "Asia/Colombo",
+ "Sudan Standard Time": "Africa/Khartoum",
+ "Syria Standard Time": "Asia/Damascus",
+ "Taipei Standard Time": "Asia/Taipei",
+ "Tasmania Standard Time": "Australia/Hobart",
+ "Tocantins Standard Time": "America/Araguaina",
+ "Tokyo Standard Time": "Asia/Tokyo",
+ "Tomsk Standard Time": "Asia/Tomsk",
+ "Tonga Standard Time": "Pacific/Tongatapu",
+ "Transbaikal Standard Time": "Asia/Chita",
+ "Turkey Standard Time": "Europe/Istanbul",
+ "Turks And Caicos Standard Time": "America/Grand_Turk",
+ "US Eastern Standard Time": "America/Indianapolis",
+ "US Mountain Standard Time": "America/Phoenix",
+ "UTC": "Etc/GMT",
+ "UTC+12": "Etc/GMT-12",
+ "UTC+13": "Etc/GMT-13",
+ "UTC-02": "Etc/GMT+2",
+ "UTC-08": "Etc/GMT+8",
+ "UTC-09": "Etc/GMT+9",
+ "UTC-11": "Etc/GMT+11",
+ "Ulaanbaatar Standard Time": "Asia/Ulaanbaatar",
+ "Venezuela Standard Time": "America/Caracas",
+ "Vladivostok Standard Time": "Asia/Vladivostok",
+ "W. Australia Standard Time": "Australia/Perth",
+ "W. Central Africa Standard Time": "Africa/Lagos",
+ "W. Europe Standard Time": "Europe/Berlin",
+ "W. Mongolia Standard Time": "Asia/Hovd",
+ "West Asia Standard Time": "Asia/Tashkent",
+ "West Bank Standard Time": "Asia/Hebron",
+ "West Pacific Standard Time": "Pacific/Port_Moresby",
+ "Yakutsk Standard Time": "Asia/Yakutsk",
+}
diff --git a/src/pendulum/tz/exceptions.py b/src/pendulum/tz/exceptions.py
new file mode 100644
index 0000000..d06c133
--- /dev/null
+++ b/src/pendulum/tz/exceptions.py
@@ -0,0 +1,33 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+
+if TYPE_CHECKING:
+ from datetime import datetime
+
+
+class TimezoneError(ValueError):
+ pass
+
+
+class InvalidTimezone(TimezoneError):
+ pass
+
+
+class NonExistingTime(TimezoneError):
+ message = "The datetime {} does not exist."
+
+ def __init__(self, dt: datetime) -> None:
+ message = self.message.format(dt)
+
+ super().__init__(message)
+
+
+class AmbiguousTime(TimezoneError):
+ message = "The datetime {} is ambiguous."
+
+ def __init__(self, dt: datetime) -> None:
+ message = self.message.format(dt)
+
+ super().__init__(message)
diff --git a/src/pendulum/tz/local_timezone.py b/src/pendulum/tz/local_timezone.py
new file mode 100644
index 0000000..3cb488c
--- /dev/null
+++ b/src/pendulum/tz/local_timezone.py
@@ -0,0 +1,264 @@
+from __future__ import annotations
+
+import contextlib
+import os
+import re
+import sys
+import warnings
+
+from contextlib import contextmanager
+from typing import Iterator
+from typing import cast
+
+from pendulum.tz.exceptions import InvalidTimezone
+from pendulum.tz.timezone import UTC
+from pendulum.tz.timezone import FixedTimezone
+from pendulum.tz.timezone import Timezone
+
+
+if sys.platform == "win32":
+ import winreg
+
+_mock_local_timezone = None
+_local_timezone = None
+
+
+def get_local_timezone() -> Timezone | FixedTimezone:
+ global _local_timezone
+
+ if _mock_local_timezone is not None:
+ return _mock_local_timezone
+
+ if _local_timezone is None:
+ tz = _get_system_timezone()
+
+ _local_timezone = tz
+
+ return _local_timezone
+
+
+def set_local_timezone(mock: str | Timezone | None = None) -> None:
+ global _mock_local_timezone
+
+ _mock_local_timezone = mock
+
+
+@contextmanager
+def test_local_timezone(mock: Timezone) -> Iterator[None]:
+ set_local_timezone(mock)
+
+ yield
+
+ set_local_timezone()
+
+
+def _get_system_timezone() -> Timezone:
+ if sys.platform == "win32":
+ return _get_windows_timezone()
+ elif "darwin" in sys.platform:
+ return _get_darwin_timezone()
+
+ return _get_unix_timezone()
+
+
+if sys.platform == "win32":
+
+ def _get_windows_timezone() -> Timezone:
+ from pendulum.tz.data.windows import windows_timezones
+
+ # Windows is special. It has unique time zone names (in several
+ # meanings of the word) available, but unfortunately, they can be
+ # translated to the language of the operating system, so we need to
+ # do a backwards lookup, by going through all time zones and see which
+ # one matches.
+ handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
+
+ tz_local_key_name = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
+ localtz = winreg.OpenKey(handle, tz_local_key_name)
+
+ timezone_info = {}
+ size = winreg.QueryInfoKey(localtz)[1]
+ for i in range(size):
+ data = winreg.EnumValue(localtz, i)
+ timezone_info[data[0]] = data[1]
+
+ localtz.Close()
+
+ if "TimeZoneKeyName" in timezone_info:
+ # Windows 7 (and Vista?)
+
+ # For some reason this returns a string with loads of NUL bytes at
+ # least on some systems. I don't know if this is a bug somewhere, I
+ # just work around it.
+ tzkeyname = timezone_info["TimeZoneKeyName"].split("\x00", 1)[0]
+ else:
+ # Windows 2000 or XP
+
+ # This is the localized name:
+ tzwin = timezone_info["StandardName"]
+
+ # Open the list of timezones to look up the real name:
+ tz_key_name = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
+ tzkey = winreg.OpenKey(handle, tz_key_name)
+
+ # Now, match this value to Time Zone information
+ tzkeyname = None
+ for i in range(winreg.QueryInfoKey(tzkey)[0]):
+ subkey = winreg.EnumKey(tzkey, i)
+ sub = winreg.OpenKey(tzkey, subkey)
+
+ info = {}
+ size = winreg.QueryInfoKey(sub)[1]
+ for i in range(size):
+ data = winreg.EnumValue(sub, i)
+ info[data[0]] = data[1]
+
+ sub.Close()
+ with contextlib.suppress(KeyError):
+ # This timezone didn't have proper configuration.
+ # Ignore it.
+ if info["Std"] == tzwin:
+ tzkeyname = subkey
+ break
+
+ tzkey.Close()
+ handle.Close()
+
+ if tzkeyname is None:
+ raise LookupError("Can not find Windows timezone configuration")
+
+ timezone = windows_timezones.get(tzkeyname)
+ if timezone is None:
+ # Nope, that didn't work. Try adding "Standard Time",
+ # it seems to work a lot of times:
+ timezone = windows_timezones.get(tzkeyname + " Standard Time")
+
+ # Return what we have.
+ if timezone is None:
+ raise LookupError("Unable to find timezone " + tzkeyname)
+
+ return Timezone(timezone)
+
+else:
+
+ def _get_windows_timezone() -> Timezone:
+ raise NotImplementedError
+
+
+def _get_darwin_timezone() -> Timezone:
+ # link will be something like /usr/share/zoneinfo/America/Los_Angeles.
+ link = os.readlink("/etc/localtime")
+ tzname = link[link.rfind("zoneinfo/") + 9 :]
+
+ return Timezone(tzname)
+
+
+def _get_unix_timezone(_root: str = "/") -> Timezone:
+ tzenv = os.environ.get("TZ")
+ if tzenv:
+ with contextlib.suppress(ValueError):
+ return _tz_from_env(tzenv)
+
+ # Now look for distribution specific configuration files
+ # that contain the timezone name.
+ tzpath = os.path.join(_root, "etc/timezone")
+ if os.path.isfile(tzpath):
+ with open(tzpath, "rb") as tzfile:
+ tzfile_data = tzfile.read()
+
+ # Issue #3 was that /etc/timezone was a zoneinfo file.
+ # That's a misconfiguration, but we need to handle it gracefully:
+ if tzfile_data[:5] != b"TZif2":
+ etctz = tzfile_data.strip().decode()
+ # Get rid of host definitions and comments:
+ if " " in etctz:
+ etctz, dummy = etctz.split(" ", 1)
+ if "#" in etctz:
+ etctz, dummy = etctz.split("#", 1)
+
+ return Timezone(etctz.replace(" ", "_"))
+
+ # CentOS has a ZONE setting in /etc/sysconfig/clock,
+ # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and
+ # Gentoo has a TIMEZONE setting in /etc/conf.d/clock
+ # We look through these files for a timezone:
+ zone_re = re.compile(r'\s*ZONE\s*=\s*"')
+ timezone_re = re.compile(r'\s*TIMEZONE\s*=\s*"')
+ end_re = re.compile('"')
+
+ for filename in ("etc/sysconfig/clock", "etc/conf.d/clock"):
+ tzpath = os.path.join(_root, filename)
+ if not os.path.isfile(tzpath):
+ continue
+
+ with open(tzpath) as tzfile:
+ data = tzfile.readlines()
+
+ for line in data:
+ # Look for the ZONE= setting.
+ match = zone_re.match(line)
+ if match is None:
+ # No ZONE= setting. Look for the TIMEZONE= setting.
+ match = timezone_re.match(line)
+
+ if match is not None:
+ # Some setting existed
+ line = line[match.end() :]
+ etctz = line[
+ : cast(
+ re.Match, end_re.search(line) # type: ignore[type-arg]
+ ).start()
+ ]
+
+ parts = list(reversed(etctz.replace(" ", "_").split(os.path.sep)))
+ tzpath_parts: list[str] = []
+ while parts:
+ tzpath_parts.insert(0, parts.pop(0))
+
+ with contextlib.suppress(InvalidTimezone):
+ return Timezone(os.path.join(*tzpath_parts))
+
+ # systemd distributions use symlinks that include the zone name,
+ # see manpage of localtime(5) and timedatectl(1)
+ tzpath = os.path.join(_root, "etc", "localtime")
+ if os.path.isfile(tzpath) and os.path.islink(tzpath):
+ parts = list(
+ reversed(os.path.realpath(tzpath).replace(" ", "_").split(os.path.sep))
+ )
+ tzpath_parts: list[str] = [] # type: ignore[no-redef]
+ while parts:
+ tzpath_parts.insert(0, parts.pop(0))
+ with contextlib.suppress(InvalidTimezone):
+ return Timezone(os.path.join(*tzpath_parts))
+
+ # No explicit setting existed. Use localtime
+ for filename in ("etc/localtime", "usr/local/etc/localtime"):
+ tzpath = os.path.join(_root, filename)
+
+ if not os.path.isfile(tzpath):
+ continue
+
+ with open(tzpath, "rb") as f:
+ return Timezone.from_file(f)
+
+ warnings.warn(
+ "Unable not find any timezone configuration, defaulting to UTC.", stacklevel=1
+ )
+
+ return UTC
+
+
+def _tz_from_env(tzenv: str) -> Timezone:
+ if tzenv[0] == ":":
+ tzenv = tzenv[1:]
+
+ # TZ specifies a file
+ if os.path.isfile(tzenv):
+ with open(tzenv, "rb") as f:
+ return Timezone.from_file(f)
+
+ # TZ specifies a zoneinfo zone.
+ try:
+ return Timezone(tzenv)
+ except ValueError:
+ raise
diff --git a/src/pendulum/tz/timezone.py b/src/pendulum/tz/timezone.py
new file mode 100644
index 0000000..0c6eb8d
--- /dev/null
+++ b/src/pendulum/tz/timezone.py
@@ -0,0 +1,239 @@
+# 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")