2019-04-11 00:02:14 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2022-01-24 04:07:52 +00:00
|
|
|
import os
|
2020-04-19 00:42:27 +00:00
|
|
|
import time
|
2019-04-11 00:02:14 +00:00
|
|
|
import datetime
|
2020-04-19 00:42:27 +00:00
|
|
|
import calendar
|
2022-01-24 04:07:52 +00:00
|
|
|
import pytz_deprecation_shim as pds
|
|
|
|
|
|
|
|
try:
|
|
|
|
import zoneinfo # pragma: no cover
|
|
|
|
except ImportError:
|
|
|
|
from backports import zoneinfo # pragma: no cover
|
|
|
|
|
|
|
|
from tzlocal import windows_tz
|
|
|
|
|
|
|
|
|
|
|
|
class ZoneInfoNotFoundError(pds.UnknownTimeZoneError, zoneinfo.ZoneInfoNotFoundError):
|
|
|
|
"""An exception derived from both pytz and zoneinfo
|
|
|
|
|
|
|
|
This exception will be trappable both by pytz expecting clients and
|
|
|
|
zoneinfo expecting clients.
|
|
|
|
"""
|
2019-04-11 00:02:14 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_system_offset():
|
|
|
|
"""Get system's timezone offset using built-in library time.
|
|
|
|
|
|
|
|
For the Timezone constants (altzone, daylight, timezone, and tzname), the
|
|
|
|
value is determined by the timezone rules in effect at module load time or
|
|
|
|
the last time tzset() is called and may be incorrect for times in the past.
|
|
|
|
|
|
|
|
To keep compatibility with Windows, we're always importing time module here.
|
|
|
|
"""
|
2020-04-19 00:42:27 +00:00
|
|
|
|
|
|
|
localtime = calendar.timegm(time.localtime())
|
|
|
|
gmtime = calendar.timegm(time.gmtime())
|
|
|
|
offset = gmtime - localtime
|
|
|
|
# We could get the localtime and gmtime on either side of a second switch
|
|
|
|
# so we check that the difference is less than one minute, because nobody
|
|
|
|
# has that small DST differences.
|
|
|
|
if abs(offset - time.altzone) < 60:
|
2022-01-24 04:07:52 +00:00
|
|
|
return -time.altzone # pragma: no cover
|
2019-04-11 00:02:14 +00:00
|
|
|
else:
|
2022-01-24 04:07:52 +00:00
|
|
|
return -time.timezone # pragma: no cover
|
2019-04-11 00:02:14 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_tz_offset(tz):
|
|
|
|
"""Get timezone's offset using built-in function datetime.utcoffset()."""
|
|
|
|
return int(datetime.datetime.now(tz).utcoffset().total_seconds())
|
|
|
|
|
|
|
|
|
|
|
|
def assert_tz_offset(tz):
|
|
|
|
"""Assert that system's timezone offset equals to the timezone offset found.
|
|
|
|
|
|
|
|
If they don't match, we probably have a misconfiguration, for example, an
|
|
|
|
incorrect timezone set in /etc/timezone file in systemd distributions."""
|
|
|
|
tz_offset = get_tz_offset(tz)
|
|
|
|
system_offset = get_system_offset()
|
|
|
|
if tz_offset != system_offset:
|
2022-01-24 04:07:52 +00:00
|
|
|
msg = (
|
|
|
|
"Timezone offset does not match system offset: {} != {}. "
|
|
|
|
"Please, check your config files."
|
|
|
|
).format(tz_offset, system_offset)
|
2019-04-11 00:02:14 +00:00
|
|
|
raise ValueError(msg)
|
2022-01-24 04:07:52 +00:00
|
|
|
|
|
|
|
|
|
|
|
def _tz_name_from_env(tzenv=None):
|
|
|
|
if tzenv is None:
|
|
|
|
tzenv = os.environ.get("TZ")
|
|
|
|
|
|
|
|
if not tzenv:
|
|
|
|
return None
|
|
|
|
|
2022-11-07 18:06:49 +00:00
|
|
|
if tzenv[0] == ":":
|
|
|
|
tzenv = tzenv[1:]
|
|
|
|
|
2022-01-24 04:07:52 +00:00
|
|
|
if tzenv in windows_tz.tz_win:
|
|
|
|
# Yup, it's a timezone
|
|
|
|
return tzenv
|
|
|
|
|
|
|
|
if os.path.isabs(tzenv) and os.path.exists(tzenv):
|
2022-11-07 18:06:49 +00:00
|
|
|
# It's a file specification, expand it, if possible
|
|
|
|
parts = os.path.realpath(tzenv).split(os.sep)
|
2022-01-24 04:07:52 +00:00
|
|
|
|
|
|
|
# Is it a zone info zone?
|
|
|
|
possible_tz = "/".join(parts[-2:])
|
|
|
|
if possible_tz in windows_tz.tz_win:
|
|
|
|
# Yup, it is
|
|
|
|
return possible_tz
|
|
|
|
|
|
|
|
# Maybe it's a short one, like UTC?
|
|
|
|
if parts[-1] in windows_tz.tz_win:
|
|
|
|
# Indeed
|
|
|
|
return parts[-1]
|
|
|
|
|
|
|
|
|
|
|
|
def _tz_from_env(tzenv=None):
|
|
|
|
if tzenv is None:
|
|
|
|
tzenv = os.environ.get("TZ")
|
|
|
|
|
|
|
|
if not tzenv:
|
|
|
|
return None
|
|
|
|
|
|
|
|
# Some weird format that exists:
|
|
|
|
if tzenv[0] == ":":
|
|
|
|
tzenv = tzenv[1:]
|
|
|
|
|
|
|
|
# TZ specifies a file
|
|
|
|
if os.path.isabs(tzenv) and os.path.exists(tzenv):
|
|
|
|
# Try to see if we can figure out the name
|
|
|
|
tzname = _tz_name_from_env(tzenv)
|
|
|
|
if not tzname:
|
|
|
|
# Nope, not a standard timezone name, just take the filename
|
|
|
|
tzname = tzenv.split(os.sep)[-1]
|
|
|
|
with open(tzenv, "rb") as tzfile:
|
|
|
|
zone = zoneinfo.ZoneInfo.from_file(tzfile, key=tzname)
|
|
|
|
return pds.wrap_zone(zone)
|
|
|
|
|
|
|
|
# TZ must specify a zoneinfo zone.
|
|
|
|
try:
|
|
|
|
tz = pds.timezone(tzenv)
|
|
|
|
# That worked, so we return this:
|
|
|
|
return tz
|
|
|
|
except pds.UnknownTimeZoneError:
|
|
|
|
# Nope, it's something like "PST4DST" etc, we can't handle that.
|
|
|
|
raise ZoneInfoNotFoundError(
|
|
|
|
"tzlocal() does not support non-zoneinfo timezones like %s. \n"
|
|
|
|
"Please use a timezone in the form of Continent/City"
|
|
|
|
) from None
|