diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2023-12-17 14:32:20 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2023-12-17 14:32:20 +0000 |
commit | db51f7f103bbbd6c91c8f47d75b3482ef8939691 (patch) | |
tree | ab59b1147bd0cd39f31a48073cff236ede4ec1df /src/pendulum/tz | |
parent | Adding upstream version 3.0.0~a1. (diff) | |
download | pendulum-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__.py | 64 | ||||
-rw-r--r-- | src/pendulum/tz/data/__init__.py | 0 | ||||
-rw-r--r-- | src/pendulum/tz/data/windows.py | 140 | ||||
-rw-r--r-- | src/pendulum/tz/exceptions.py | 33 | ||||
-rw-r--r-- | src/pendulum/tz/local_timezone.py | 264 | ||||
-rw-r--r-- | src/pendulum/tz/timezone.py | 239 |
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") |