mirror of
https://github.com/rembo10/headphones.git
synced 2026-07-22 00:44:00 +01:00
tzlocal: 1.1.2 -> 5.2
This commit is contained in:
@@ -1,121 +0,0 @@
|
|||||||
Creative Commons Legal Code
|
|
||||||
|
|
||||||
CC0 1.0 Universal
|
|
||||||
|
|
||||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
|
||||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
|
||||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
|
||||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
|
||||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
|
||||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
|
||||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
|
||||||
HEREUNDER.
|
|
||||||
|
|
||||||
Statement of Purpose
|
|
||||||
|
|
||||||
The laws of most jurisdictions throughout the world automatically confer
|
|
||||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
|
||||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
|
||||||
authorship and/or a database (each, a "Work").
|
|
||||||
|
|
||||||
Certain owners wish to permanently relinquish those rights to a Work for
|
|
||||||
the purpose of contributing to a commons of creative, cultural and
|
|
||||||
scientific works ("Commons") that the public can reliably and without fear
|
|
||||||
of later claims of infringement build upon, modify, incorporate in other
|
|
||||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
|
||||||
and for any purposes, including without limitation commercial purposes.
|
|
||||||
These owners may contribute to the Commons to promote the ideal of a free
|
|
||||||
culture and the further production of creative, cultural and scientific
|
|
||||||
works, or to gain reputation or greater distribution for their Work in
|
|
||||||
part through the use and efforts of others.
|
|
||||||
|
|
||||||
For these and/or other purposes and motivations, and without any
|
|
||||||
expectation of additional consideration or compensation, the person
|
|
||||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
|
||||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
|
||||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
|
||||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
|
||||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
|
||||||
|
|
||||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
|
||||||
protected by copyright and related or neighboring rights ("Copyright and
|
|
||||||
Related Rights"). Copyright and Related Rights include, but are not
|
|
||||||
limited to, the following:
|
|
||||||
|
|
||||||
i. the right to reproduce, adapt, distribute, perform, display,
|
|
||||||
communicate, and translate a Work;
|
|
||||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
|
||||||
iii. publicity and privacy rights pertaining to a person's image or
|
|
||||||
likeness depicted in a Work;
|
|
||||||
iv. rights protecting against unfair competition in regards to a Work,
|
|
||||||
subject to the limitations in paragraph 4(a), below;
|
|
||||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
|
||||||
in a Work;
|
|
||||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
|
||||||
European Parliament and of the Council of 11 March 1996 on the legal
|
|
||||||
protection of databases, and under any national implementation
|
|
||||||
thereof, including any amended or successor version of such
|
|
||||||
directive); and
|
|
||||||
vii. other similar, equivalent or corresponding rights throughout the
|
|
||||||
world based on applicable law or treaty, and any national
|
|
||||||
implementations thereof.
|
|
||||||
|
|
||||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
|
||||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
|
||||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
|
||||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
|
||||||
of action, whether now known or unknown (including existing as well as
|
|
||||||
future claims and causes of action), in the Work (i) in all territories
|
|
||||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
|
||||||
treaty (including future time extensions), (iii) in any current or future
|
|
||||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
|
||||||
including without limitation commercial, advertising or promotional
|
|
||||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
|
||||||
member of the public at large and to the detriment of Affirmer's heirs and
|
|
||||||
successors, fully intending that such Waiver shall not be subject to
|
|
||||||
revocation, rescission, cancellation, termination, or any other legal or
|
|
||||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
|
||||||
as contemplated by Affirmer's express Statement of Purpose.
|
|
||||||
|
|
||||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
|
||||||
be judged legally invalid or ineffective under applicable law, then the
|
|
||||||
Waiver shall be preserved to the maximum extent permitted taking into
|
|
||||||
account Affirmer's express Statement of Purpose. In addition, to the
|
|
||||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
|
||||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
|
||||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
|
||||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
|
||||||
maximum duration provided by applicable law or treaty (including future
|
|
||||||
time extensions), (iii) in any current or future medium and for any number
|
|
||||||
of copies, and (iv) for any purpose whatsoever, including without
|
|
||||||
limitation commercial, advertising or promotional purposes (the
|
|
||||||
"License"). The License shall be deemed effective as of the date CC0 was
|
|
||||||
applied by Affirmer to the Work. Should any part of the License for any
|
|
||||||
reason be judged legally invalid or ineffective under applicable law, such
|
|
||||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
|
||||||
of the License, and in such case Affirmer hereby affirms that he or she
|
|
||||||
will not (i) exercise any of his or her remaining Copyright and Related
|
|
||||||
Rights in the Work or (ii) assert any associated claims and causes of
|
|
||||||
action with respect to the Work, in either case contrary to Affirmer's
|
|
||||||
express Statement of Purpose.
|
|
||||||
|
|
||||||
4. Limitations and Disclaimers.
|
|
||||||
|
|
||||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
|
||||||
surrendered, licensed or otherwise affected by this document.
|
|
||||||
b. Affirmer offers the Work as-is and makes no representations or
|
|
||||||
warranties of any kind concerning the Work, express, implied,
|
|
||||||
statutory or otherwise, including without limitation warranties of
|
|
||||||
title, merchantability, fitness for a particular purpose, non
|
|
||||||
infringement, or the absence of latent or other defects, accuracy, or
|
|
||||||
the present or absence of errors, whether or not discoverable, all to
|
|
||||||
the greatest extent permissible under applicable law.
|
|
||||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
|
||||||
that may apply to the Work or any use thereof, including without
|
|
||||||
limitation any person's Copyright and Related Rights in the Work.
|
|
||||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
|
||||||
consents, permissions or other rights required for any use of the
|
|
||||||
Work.
|
|
||||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
|
||||||
party to this document and has no duty or obligation with respect to
|
|
||||||
this CC0 or use of the Work.
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
tzlocal
|
|
||||||
=======
|
|
||||||
|
|
||||||
This Python module returns a `tzinfo` object with the local timezone information under Unix and Win-32.
|
|
||||||
It requires `pytz`, and returns `pytz` `tzinfo` objects.
|
|
||||||
|
|
||||||
This module attempts to fix a glaring hole in `pytz`, that there is no way to
|
|
||||||
get the local timezone information, unless you know the zoneinfo name, and
|
|
||||||
under several Linux distros that's hard or impossible to figure out.
|
|
||||||
|
|
||||||
Also, with Windows different timezone system using pytz isn't of much use
|
|
||||||
unless you separately configure the zoneinfo timezone name.
|
|
||||||
|
|
||||||
With `tzlocal` you only need to call `get_localzone()` and you will get a
|
|
||||||
`tzinfo` object with the local time zone info. On some Unices you will still
|
|
||||||
not get to know what the timezone name is, but you don't need that when you
|
|
||||||
have the tzinfo file. However, if the timezone name is readily available it
|
|
||||||
will be used.
|
|
||||||
|
|
||||||
|
|
||||||
Supported systems
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
These are the systems that are in theory supported:
|
|
||||||
|
|
||||||
* Windows 2000 and later
|
|
||||||
|
|
||||||
* Any unix-like system with a /etc/localtime or /usr/local/etc/localtime
|
|
||||||
|
|
||||||
If you have one of the above systems and it does not work, it's a bug.
|
|
||||||
Please report it.
|
|
||||||
|
|
||||||
|
|
||||||
Usage
|
|
||||||
-----
|
|
||||||
|
|
||||||
Load the local timezone:
|
|
||||||
|
|
||||||
>>> from tzlocal import get_localzone
|
|
||||||
>>> tz = get_localzone()
|
|
||||||
>>> tz
|
|
||||||
<DstTzInfo 'Europe/Warsaw' WMT+1:24:00 STD>
|
|
||||||
|
|
||||||
Create a local datetime:
|
|
||||||
|
|
||||||
>>> from datetime import datetime
|
|
||||||
>>> dt = tz.localize(datetime.now())
|
|
||||||
>>> dt
|
|
||||||
datetime.datetime(2012, 9, 11, 14, 43, 42, 518871, tzinfo=<DstTzInfo 'Europe/Warsaw' CEST+2:00:00 DST>)
|
|
||||||
|
|
||||||
Lookup another timezone with `pytz`:
|
|
||||||
|
|
||||||
>>> import pytz
|
|
||||||
>>> eastern = pytz.timezone('US/Eastern')
|
|
||||||
|
|
||||||
Convert the datetime:
|
|
||||||
|
|
||||||
>>> dt.astimezone(eastern)
|
|
||||||
datetime.datetime(2012, 9, 11, 8, 43, 42, 518871, tzinfo=<DstTzInfo 'US/Eastern' EDT-1 day, 20:00:00 DST>)
|
|
||||||
|
|
||||||
|
|
||||||
Maintainer
|
|
||||||
----------
|
|
||||||
|
|
||||||
* Lennart Regebro, regebro@gmail.com
|
|
||||||
|
|
||||||
Contributors
|
|
||||||
------------
|
|
||||||
|
|
||||||
* Marc Van Olmen
|
|
||||||
* Benjamen Meyer
|
|
||||||
* Manuel Ebert
|
|
||||||
* Xiaokun Zhu
|
|
||||||
|
|
||||||
(Sorry if I forgot someone)
|
|
||||||
|
|
||||||
License
|
|
||||||
-------
|
|
||||||
|
|
||||||
* CC0 1.0 Universal http://creativecommons.org/publicdomain/zero/1.0/
|
|
||||||
+17
-5
@@ -1,7 +1,19 @@
|
|||||||
import sys
|
import sys
|
||||||
if sys.platform == 'win32':
|
|
||||||
from tzlocal.win32 import get_localzone, reload_localzone
|
if sys.platform == "win32":
|
||||||
elif 'darwin' in sys.platform:
|
from tzlocal.win32 import (
|
||||||
from tzlocal.darwin import get_localzone, reload_localzone
|
get_localzone,
|
||||||
|
get_localzone_name,
|
||||||
|
reload_localzone,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
from tzlocal.unix import get_localzone, reload_localzone
|
from tzlocal.unix import get_localzone, get_localzone_name, reload_localzone
|
||||||
|
|
||||||
|
from tzlocal.utils import assert_tz_offset
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"get_localzone",
|
||||||
|
"get_localzone_name",
|
||||||
|
"reload_localzone",
|
||||||
|
"assert_tz_offset",
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
|
|
||||||
import os
|
|
||||||
import pytz
|
|
||||||
|
|
||||||
_cache_tz = None
|
|
||||||
|
|
||||||
def _get_localzone():
|
|
||||||
tzname = os.popen("systemsetup -gettimezone").read().replace("Time Zone: ", "").strip()
|
|
||||||
if not tzname or tzname not in pytz.all_timezones_set:
|
|
||||||
# link will be something like /usr/share/zoneinfo/America/Los_Angeles.
|
|
||||||
link = os.readlink("/etc/localtime")
|
|
||||||
tzname = link[link.rfind("zoneinfo/") + 9:]
|
|
||||||
return pytz.timezone(tzname)
|
|
||||||
|
|
||||||
def get_localzone():
|
|
||||||
"""Get the computers configured local timezone, if any."""
|
|
||||||
global _cache_tz
|
|
||||||
if _cache_tz is None:
|
|
||||||
_cache_tz = _get_localzone()
|
|
||||||
return _cache_tz
|
|
||||||
|
|
||||||
def reload_localzone():
|
|
||||||
"""Reload the cached localzone. You need to call this if the timezone has changed."""
|
|
||||||
global _cache_tz
|
|
||||||
_cache_tz = _get_localzone()
|
|
||||||
return _cache_tz
|
|
||||||
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import sys
|
|
||||||
import os
|
|
||||||
from datetime import datetime
|
|
||||||
import unittest
|
|
||||||
import pytz
|
|
||||||
import tzlocal.unix
|
|
||||||
|
|
||||||
class TzLocalTests(unittest.TestCase):
|
|
||||||
|
|
||||||
def test_env(self):
|
|
||||||
tz_harare = tzlocal.unix._tz_from_env(':Africa/Harare')
|
|
||||||
self.assertEqual(tz_harare.zone, 'Africa/Harare')
|
|
||||||
|
|
||||||
# Some Unices allow this as well, so we must allow it:
|
|
||||||
tz_harare = tzlocal.unix._tz_from_env('Africa/Harare')
|
|
||||||
self.assertEqual(tz_harare.zone, 'Africa/Harare')
|
|
||||||
|
|
||||||
local_path = os.path.split(__file__)[0]
|
|
||||||
tz_local = tzlocal.unix._tz_from_env(':' + os.path.join(local_path, 'test_data', 'Harare'))
|
|
||||||
self.assertEqual(tz_local.zone, 'local')
|
|
||||||
# Make sure the local timezone is the same as the Harare one above.
|
|
||||||
# We test this with a past date, so that we don't run into future changes
|
|
||||||
# of the Harare timezone.
|
|
||||||
dt = datetime(2012, 1, 1, 5)
|
|
||||||
self.assertEqual(tz_harare.localize(dt), tz_local.localize(dt))
|
|
||||||
|
|
||||||
# Non-zoneinfo timezones are not supported in the TZ environment.
|
|
||||||
self.assertRaises(pytz.UnknownTimeZoneError, tzlocal.unix._tz_from_env, 'GMT+03:00')
|
|
||||||
|
|
||||||
def test_timezone(self):
|
|
||||||
# Most versions of Ubuntu
|
|
||||||
local_path = os.path.split(__file__)[0]
|
|
||||||
tz = tzlocal.unix._get_localzone(_root=os.path.join(local_path, 'test_data', 'timezone'))
|
|
||||||
self.assertEqual(tz.zone, 'Africa/Harare')
|
|
||||||
|
|
||||||
def test_zone_setting(self):
|
|
||||||
# A ZONE setting in /etc/sysconfig/clock, f ex CentOS
|
|
||||||
local_path = os.path.split(__file__)[0]
|
|
||||||
tz = tzlocal.unix._get_localzone(_root=os.path.join(local_path, 'test_data', 'zone_setting'))
|
|
||||||
self.assertEqual(tz.zone, 'Africa/Harare')
|
|
||||||
|
|
||||||
def test_timezone_setting(self):
|
|
||||||
# A ZONE setting in /etc/conf.d/clock, f ex Gentoo
|
|
||||||
local_path = os.path.split(__file__)[0]
|
|
||||||
tz = tzlocal.unix._get_localzone(_root=os.path.join(local_path, 'test_data', 'timezone_setting'))
|
|
||||||
self.assertEqual(tz.zone, 'Africa/Harare')
|
|
||||||
|
|
||||||
def test_only_localtime(self):
|
|
||||||
local_path = os.path.split(__file__)[0]
|
|
||||||
tz = tzlocal.unix._get_localzone(_root=os.path.join(local_path, 'test_data', 'localtime'))
|
|
||||||
self.assertEqual(tz.zone, 'local')
|
|
||||||
dt = datetime(2012, 1, 1, 5)
|
|
||||||
self.assertEqual(pytz.timezone('Africa/Harare').localize(dt), tz.localize(dt))
|
|
||||||
|
|
||||||
if sys.platform == 'win32':
|
|
||||||
|
|
||||||
import tzlocal.win32
|
|
||||||
class TzWin32Tests(unittest.TestCase):
|
|
||||||
|
|
||||||
def test_win32(self):
|
|
||||||
tzlocal.win32.get_localzone()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
+185
-69
@@ -1,115 +1,231 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import pytz
|
import sys
|
||||||
|
import warnings
|
||||||
|
from datetime import timezone
|
||||||
|
|
||||||
|
from tzlocal import utils
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 9):
|
||||||
|
import zoneinfo # pragma: no cover
|
||||||
|
else:
|
||||||
|
from backports import zoneinfo # pragma: no cover
|
||||||
|
|
||||||
_cache_tz = None
|
_cache_tz = None
|
||||||
|
_cache_tz_name = None
|
||||||
|
|
||||||
def _tz_from_env(tzenv):
|
log = logging.getLogger("tzlocal")
|
||||||
if tzenv[0] == ':':
|
|
||||||
tzenv = tzenv[1:]
|
|
||||||
|
|
||||||
# TZ specifies a file
|
|
||||||
if os.path.exists(tzenv):
|
|
||||||
with open(tzenv, 'rb') as tzfile:
|
|
||||||
return pytz.tzfile.build_tzinfo('local', tzfile)
|
|
||||||
|
|
||||||
# TZ specifies a zoneinfo zone.
|
def _get_localzone_name(_root="/"):
|
||||||
try:
|
|
||||||
tz = pytz.timezone(tzenv)
|
|
||||||
# That worked, so we return this:
|
|
||||||
return tz
|
|
||||||
except pytz.UnknownTimeZoneError:
|
|
||||||
raise pytz.UnknownTimeZoneError(
|
|
||||||
"tzlocal() does not support non-zoneinfo timezones like %s. \n"
|
|
||||||
"Please use a timezone in the form of Continent/City")
|
|
||||||
|
|
||||||
def _get_localzone(_root='/'):
|
|
||||||
"""Tries to find the local timezone configuration.
|
"""Tries to find the local timezone configuration.
|
||||||
|
|
||||||
This method prefers finding the timezone name and passing that to pytz,
|
This method finds the timezone name, if it can, or it returns None.
|
||||||
over passing in the localtime file, as in the later case the zoneinfo
|
|
||||||
name is unknown.
|
|
||||||
|
|
||||||
The parameter _root makes the function look for files like /etc/localtime
|
The parameter _root makes the function look for files like /etc/localtime
|
||||||
beneath the _root directory. This is primarily used by the tests.
|
beneath the _root directory. This is primarily used by the tests.
|
||||||
In normal usage you call the function without parameters."""
|
In normal usage you call the function without parameters."""
|
||||||
|
|
||||||
tzenv = os.environ.get('TZ')
|
# First try the ENV setting.
|
||||||
|
tzenv = utils._tz_name_from_env()
|
||||||
if tzenv:
|
if tzenv:
|
||||||
|
return tzenv
|
||||||
|
|
||||||
|
# Are we under Termux on Android?
|
||||||
|
if os.path.exists(os.path.join(_root, "system/bin/getprop")):
|
||||||
|
log.debug("This looks like Termux")
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return _tz_from_env(tzenv)
|
androidtz = (
|
||||||
except pytz.UnknownTimeZoneError:
|
subprocess.check_output(["getprop", "persist.sys.timezone"])
|
||||||
|
.strip()
|
||||||
|
.decode()
|
||||||
|
)
|
||||||
|
return androidtz
|
||||||
|
except (OSError, subprocess.CalledProcessError):
|
||||||
|
# proot environment or failed to getprop
|
||||||
|
log.debug("It's not termux?")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Now look for distribution specific configuration files
|
# Now look for distribution specific configuration files
|
||||||
# that contain the timezone name.
|
# that contain the timezone name.
|
||||||
tzpath = os.path.join(_root, 'etc/timezone')
|
|
||||||
if os.path.exists(tzpath):
|
|
||||||
with open(tzpath, 'rb') as tzfile:
|
|
||||||
data = tzfile.read()
|
|
||||||
|
|
||||||
# Issue #3 was that /etc/timezone was a zoneinfo file.
|
# Stick all of them in a dict, to compare later.
|
||||||
# That's a misconfiguration, but we need to handle it gracefully:
|
found_configs = {}
|
||||||
if data[:5] != 'TZif2':
|
|
||||||
etctz = data.strip().decode()
|
for configfile in ("etc/timezone", "var/db/zoneinfo"):
|
||||||
# Get rid of host definitions and comments:
|
tzpath = os.path.join(_root, configfile)
|
||||||
if ' ' in etctz:
|
try:
|
||||||
etctz, dummy = etctz.split(' ', 1)
|
with open(tzpath) as tzfile:
|
||||||
if '#' in etctz:
|
data = tzfile.read()
|
||||||
etctz, dummy = etctz.split('#', 1)
|
log.debug(f"{tzpath} found, contents:\n {data}")
|
||||||
return pytz.timezone(etctz.replace(' ', '_'))
|
|
||||||
|
etctz = data.strip("/ \t\r\n")
|
||||||
|
if not etctz:
|
||||||
|
# Empty file, skip
|
||||||
|
continue
|
||||||
|
for etctz in etctz.splitlines():
|
||||||
|
# Get rid of host definitions and comments:
|
||||||
|
if " " in etctz:
|
||||||
|
etctz, dummy = etctz.split(" ", 1)
|
||||||
|
if "#" in etctz:
|
||||||
|
etctz, dummy = etctz.split("#", 1)
|
||||||
|
if not etctz:
|
||||||
|
continue
|
||||||
|
|
||||||
|
found_configs[tzpath] = etctz.replace(" ", "_")
|
||||||
|
|
||||||
|
except (OSError, UnicodeDecodeError):
|
||||||
|
# File doesn't exist or is a directory, or it's a binary file.
|
||||||
|
continue
|
||||||
|
|
||||||
# CentOS has a ZONE setting in /etc/sysconfig/clock,
|
# CentOS has a ZONE setting in /etc/sysconfig/clock,
|
||||||
# OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and
|
# OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and
|
||||||
# Gentoo has a TIMEZONE setting in /etc/conf.d/clock
|
# Gentoo has a TIMEZONE setting in /etc/conf.d/clock
|
||||||
# We look through these files for a timezone:
|
# We look through these files for a timezone:
|
||||||
|
|
||||||
zone_re = re.compile('\s*ZONE\s*=\s*\"')
|
zone_re = re.compile(r"\s*ZONE\s*=\s*\"")
|
||||||
timezone_re = re.compile('\s*TIMEZONE\s*=\s*\"')
|
timezone_re = re.compile(r"\s*TIMEZONE\s*=\s*\"")
|
||||||
end_re = re.compile('\"')
|
end_re = re.compile('"')
|
||||||
|
|
||||||
for filename in ('etc/sysconfig/clock', 'etc/conf.d/clock'):
|
for filename in ("etc/sysconfig/clock", "etc/conf.d/clock"):
|
||||||
tzpath = os.path.join(_root, filename)
|
tzpath = os.path.join(_root, filename)
|
||||||
if not os.path.exists(tzpath):
|
try:
|
||||||
|
with open(tzpath, "rt") as tzfile:
|
||||||
|
data = tzfile.readlines()
|
||||||
|
log.debug(f"{tzpath} found, contents:\n {data}")
|
||||||
|
|
||||||
|
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[: end_re.search(line).start()]
|
||||||
|
|
||||||
|
# We found a timezone
|
||||||
|
found_configs[tzpath] = etctz.replace(" ", "_")
|
||||||
|
|
||||||
|
except (OSError, UnicodeDecodeError):
|
||||||
|
# UnicodeDecode handles when clock is symlink to /etc/localtime
|
||||||
continue
|
continue
|
||||||
with open(tzpath, 'rt') as tzfile:
|
|
||||||
data = tzfile.readlines()
|
|
||||||
|
|
||||||
for line in data:
|
# systemd distributions use symlinks that include the zone name,
|
||||||
# Look for the ZONE= setting.
|
# see manpage of localtime(5) and timedatectl(1)
|
||||||
match = zone_re.match(line)
|
tzpath = os.path.join(_root, "etc/localtime")
|
||||||
if match is None:
|
if os.path.exists(tzpath) and os.path.islink(tzpath):
|
||||||
# No ZONE= setting. Look for the TIMEZONE= setting.
|
log.debug(f"{tzpath} found")
|
||||||
match = timezone_re.match(line)
|
etctz = os.path.realpath(tzpath)
|
||||||
if match is not None:
|
start = etctz.find("/") + 1
|
||||||
# Some setting existed
|
while start != 0:
|
||||||
line = line[match.end():]
|
etctz = etctz[start:]
|
||||||
etctz = line[:end_re.search(line).start()]
|
try:
|
||||||
|
zoneinfo.ZoneInfo(etctz)
|
||||||
|
tzinfo = f"{tzpath} is a symlink to"
|
||||||
|
found_configs[tzinfo] = etctz.replace(" ", "_")
|
||||||
|
# Only need first valid relative path in simlink.
|
||||||
|
break
|
||||||
|
except zoneinfo.ZoneInfoNotFoundError:
|
||||||
|
pass
|
||||||
|
start = etctz.find("/") + 1
|
||||||
|
|
||||||
# We found a timezone
|
if len(found_configs) > 0:
|
||||||
return pytz.timezone(etctz.replace(' ', '_'))
|
log.debug(f"{len(found_configs)} found:\n {found_configs}")
|
||||||
|
# We found some explicit config of some sort!
|
||||||
|
if len(found_configs) > 1:
|
||||||
|
# Uh-oh, multiple configs. See if they match:
|
||||||
|
unique_tzs = set()
|
||||||
|
zoneinfopath = os.path.join(_root, "usr", "share", "zoneinfo")
|
||||||
|
directory_depth = len(zoneinfopath.split(os.path.sep))
|
||||||
|
|
||||||
# No explicit setting existed. Use localtime
|
for tzname in found_configs.values():
|
||||||
for filename in ('etc/localtime', 'usr/local/etc/localtime'):
|
# Look them up in /usr/share/zoneinfo, and find what they
|
||||||
tzpath = os.path.join(_root, filename)
|
# really point to:
|
||||||
|
path = os.path.realpath(os.path.join(zoneinfopath, *tzname.split("/")))
|
||||||
|
real_zone_name = "/".join(path.split(os.path.sep)[directory_depth:])
|
||||||
|
unique_tzs.add(real_zone_name)
|
||||||
|
|
||||||
if not os.path.exists(tzpath):
|
if len(unique_tzs) != 1:
|
||||||
continue
|
message = "Multiple conflicting time zone configurations found:\n"
|
||||||
with open(tzpath, 'rb') as tzfile:
|
for key, value in found_configs.items():
|
||||||
return pytz.tzfile.build_tzinfo('local', tzfile)
|
message += f"{key}: {value}\n"
|
||||||
|
message += "Fix the configuration, or set the time zone in a TZ environment variable.\n"
|
||||||
|
raise zoneinfo.ZoneInfoNotFoundError(message)
|
||||||
|
|
||||||
raise pytz.UnknownTimeZoneError('Can not find any timezone configuration')
|
# We found exactly one config! Use it.
|
||||||
|
return list(found_configs.values())[0]
|
||||||
|
|
||||||
def get_localzone():
|
|
||||||
|
def _get_localzone(_root="/"):
|
||||||
|
"""Creates a timezone object from the timezone name.
|
||||||
|
|
||||||
|
If there is no timezone config, it will try to create a file from the
|
||||||
|
localtime timezone, and if there isn't one, it will default to UTC.
|
||||||
|
|
||||||
|
The parameter _root makes the function look for files like /etc/localtime
|
||||||
|
beneath the _root directory. This is primarily used by the tests.
|
||||||
|
In normal usage you call the function without parameters."""
|
||||||
|
|
||||||
|
# First try the ENV setting.
|
||||||
|
tzenv = utils._tz_from_env()
|
||||||
|
if tzenv:
|
||||||
|
return tzenv
|
||||||
|
|
||||||
|
tzname = _get_localzone_name(_root)
|
||||||
|
if tzname is None:
|
||||||
|
# No explicit setting existed. Use localtime
|
||||||
|
log.debug("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.exists(tzpath):
|
||||||
|
continue
|
||||||
|
with open(tzpath, "rb") as tzfile:
|
||||||
|
tz = zoneinfo.ZoneInfo.from_file(tzfile, key="local")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
warnings.warn("Can not find any timezone configuration, defaulting to UTC.")
|
||||||
|
tz = timezone.utc
|
||||||
|
else:
|
||||||
|
tz = zoneinfo.ZoneInfo(tzname)
|
||||||
|
|
||||||
|
if _root == "/":
|
||||||
|
# We are using a file in etc to name the timezone.
|
||||||
|
# Verify that the timezone specified there is actually used:
|
||||||
|
utils.assert_tz_offset(tz, error=False)
|
||||||
|
return tz
|
||||||
|
|
||||||
|
|
||||||
|
def get_localzone_name() -> str:
|
||||||
|
"""Get the computers configured local timezone name, if any."""
|
||||||
|
global _cache_tz_name
|
||||||
|
if _cache_tz_name is None:
|
||||||
|
_cache_tz_name = _get_localzone_name()
|
||||||
|
|
||||||
|
return _cache_tz_name
|
||||||
|
|
||||||
|
|
||||||
|
def get_localzone() -> zoneinfo.ZoneInfo:
|
||||||
"""Get the computers configured local timezone, if any."""
|
"""Get the computers configured local timezone, if any."""
|
||||||
|
|
||||||
global _cache_tz
|
global _cache_tz
|
||||||
if _cache_tz is None:
|
if _cache_tz is None:
|
||||||
_cache_tz = _get_localzone()
|
_cache_tz = _get_localzone()
|
||||||
|
|
||||||
return _cache_tz
|
return _cache_tz
|
||||||
|
|
||||||
def reload_localzone():
|
|
||||||
|
def reload_localzone() -> zoneinfo.ZoneInfo:
|
||||||
"""Reload the cached localzone. You need to call this if the timezone has changed."""
|
"""Reload the cached localzone. You need to call this if the timezone has changed."""
|
||||||
|
global _cache_tz_name
|
||||||
global _cache_tz
|
global _cache_tz
|
||||||
|
_cache_tz_name = _get_localzone_name()
|
||||||
_cache_tz = _get_localzone()
|
_cache_tz = _get_localzone()
|
||||||
|
|
||||||
return _cache_tz
|
return _cache_tz
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import calendar
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
try:
|
||||||
|
import zoneinfo # pragma: no cover
|
||||||
|
except ImportError:
|
||||||
|
from backports import zoneinfo # pragma: no cover
|
||||||
|
|
||||||
|
from tzlocal import windows_tz
|
||||||
|
|
||||||
|
log = logging.getLogger("tzlocal")
|
||||||
|
|
||||||
|
|
||||||
|
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, error=True):
|
||||||
|
"""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.
|
||||||
|
|
||||||
|
If error is True, this method will raise a ValueError, otherwise it will
|
||||||
|
emit a warning.
|
||||||
|
"""
|
||||||
|
|
||||||
|
tz_offset = get_tz_offset(tz)
|
||||||
|
system_offset = calendar.timegm(time.localtime()) - calendar.timegm(time.gmtime())
|
||||||
|
# No one has timezone offsets less than a minute, so this should be close enough:
|
||||||
|
if abs(tz_offset - system_offset) > 60:
|
||||||
|
msg = (
|
||||||
|
f"Timezone offset does not match system offset: {tz_offset} != {system_offset}. "
|
||||||
|
"Please, check your config files."
|
||||||
|
)
|
||||||
|
if error:
|
||||||
|
raise ValueError(msg)
|
||||||
|
warnings.warn(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def _tz_name_from_env(tzenv=None):
|
||||||
|
if tzenv is None:
|
||||||
|
tzenv = os.environ.get("TZ")
|
||||||
|
|
||||||
|
if not tzenv:
|
||||||
|
return None
|
||||||
|
|
||||||
|
log.debug(f"Found a TZ environment: {tzenv}")
|
||||||
|
|
||||||
|
if tzenv[0] == ":":
|
||||||
|
tzenv = tzenv[1:]
|
||||||
|
|
||||||
|
if tzenv in windows_tz.tz_win:
|
||||||
|
# Yup, it's a timezone
|
||||||
|
return tzenv
|
||||||
|
|
||||||
|
if os.path.isabs(tzenv) and os.path.exists(tzenv):
|
||||||
|
# It's a file specification, expand it, if possible
|
||||||
|
parts = os.path.realpath(tzenv).split(os.sep)
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
log.debug("TZ does not contain a time zone name")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
return zoneinfo.ZoneInfo.from_file(tzfile, key=tzname)
|
||||||
|
|
||||||
|
# TZ must specify a zoneinfo zone.
|
||||||
|
try:
|
||||||
|
tz = zoneinfo.ZoneInfo(tzenv)
|
||||||
|
# That worked, so we return this:
|
||||||
|
return tz
|
||||||
|
except zoneinfo.ZoneInfoNotFoundError:
|
||||||
|
# Nope, it's something like "PST4DST" etc, we can't handle that.
|
||||||
|
raise zoneinfo.ZoneInfoNotFoundError(
|
||||||
|
f"tzlocal() does not support non-zoneinfo timezones like {tzenv}. \n"
|
||||||
|
"Please use a timezone in the form of Continent/City"
|
||||||
|
) from None
|
||||||
+98
-44
@@ -1,72 +1,78 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import winreg as winreg
|
import _winreg as winreg
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import winreg
|
import winreg
|
||||||
|
|
||||||
|
try:
|
||||||
|
import zoneinfo # pragma: no cover
|
||||||
|
except ImportError:
|
||||||
|
from backports import zoneinfo # pragma: no cover
|
||||||
|
|
||||||
|
from tzlocal import utils
|
||||||
from tzlocal.windows_tz import win_tz
|
from tzlocal.windows_tz import win_tz
|
||||||
import pytz
|
|
||||||
|
|
||||||
_cache_tz = None
|
_cache_tz = None
|
||||||
|
_cache_tz_name = None
|
||||||
|
|
||||||
|
log = logging.getLogger("tzlocal")
|
||||||
|
|
||||||
|
|
||||||
def valuestodict(key):
|
def valuestodict(key):
|
||||||
"""Convert a registry key's values to a dictionary."""
|
"""Convert a registry key's values to a dictionary."""
|
||||||
dict = {}
|
result = {}
|
||||||
size = winreg.QueryInfoKey(key)[1]
|
size = winreg.QueryInfoKey(key)[1]
|
||||||
for i in range(size):
|
for i in range(size):
|
||||||
data = winreg.EnumValue(key, i)
|
data = winreg.EnumValue(key, i)
|
||||||
dict[data[0]] = data[1]
|
result[data[0]] = data[1]
|
||||||
return dict
|
return result
|
||||||
|
|
||||||
def get_localzone_name():
|
|
||||||
|
def _get_dst_info(tz):
|
||||||
|
# Find the offset for when it doesn't have DST:
|
||||||
|
dst_offset = std_offset = None
|
||||||
|
has_dst = False
|
||||||
|
year = datetime.now().year
|
||||||
|
for dt in (datetime(year, 1, 1), datetime(year, 6, 1)):
|
||||||
|
if tz.dst(dt).total_seconds() == 0.0:
|
||||||
|
# OK, no DST during winter, get this offset
|
||||||
|
std_offset = tz.utcoffset(dt).total_seconds()
|
||||||
|
else:
|
||||||
|
has_dst = True
|
||||||
|
|
||||||
|
return has_dst, std_offset, dst_offset
|
||||||
|
|
||||||
|
|
||||||
|
def _get_localzone_name():
|
||||||
# Windows is special. It has unique time zone names (in several
|
# Windows is special. It has unique time zone names (in several
|
||||||
# meanings of the word) available, but unfortunately, they can be
|
# meanings of the word) available, but unfortunately, they can be
|
||||||
# translated to the language of the operating system, so we need to
|
# 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
|
# do a backwards lookup, by going through all time zones and see which
|
||||||
# one matches.
|
# one matches.
|
||||||
|
tzenv = utils._tz_name_from_env()
|
||||||
|
if tzenv:
|
||||||
|
return tzenv
|
||||||
|
|
||||||
|
log.debug("Looking up time zone info from registry")
|
||||||
handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
|
handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
|
||||||
|
|
||||||
TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
|
TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
|
||||||
localtz = winreg.OpenKey(handle, TZLOCALKEYNAME)
|
localtz = winreg.OpenKey(handle, TZLOCALKEYNAME)
|
||||||
keyvalues = valuestodict(localtz)
|
keyvalues = valuestodict(localtz)
|
||||||
localtz.Close()
|
localtz.Close()
|
||||||
if 'TimeZoneKeyName' in keyvalues:
|
|
||||||
# Windows 7 (and Vista?)
|
if "TimeZoneKeyName" in keyvalues:
|
||||||
|
# Windows 7 and later
|
||||||
|
|
||||||
# For some reason this returns a string with loads of NUL bytes at
|
# 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
|
# least on some systems. I don't know if this is a bug somewhere, I
|
||||||
# just work around it.
|
# just work around it.
|
||||||
tzkeyname = keyvalues['TimeZoneKeyName'].split('\x00', 1)[0]
|
tzkeyname = keyvalues["TimeZoneKeyName"].split("\x00", 1)[0]
|
||||||
else:
|
else:
|
||||||
# Windows 2000 or XP
|
# Don't support XP any longer
|
||||||
|
raise LookupError("Can not find Windows timezone configuration")
|
||||||
# This is the localized name:
|
|
||||||
tzwin = keyvalues['StandardName']
|
|
||||||
|
|
||||||
# Open the list of timezones to look up the real name:
|
|
||||||
TZKEYNAME = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
|
|
||||||
tzkey = winreg.OpenKey(handle, TZKEYNAME)
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
data = valuestodict(sub)
|
|
||||||
sub.Close()
|
|
||||||
try:
|
|
||||||
if data['Std'] == tzwin:
|
|
||||||
tzkeyname = subkey
|
|
||||||
break
|
|
||||||
except KeyError:
|
|
||||||
# This timezone didn't have proper configuration.
|
|
||||||
# Ignore it.
|
|
||||||
pass
|
|
||||||
|
|
||||||
tzkey.Close()
|
|
||||||
handle.Close()
|
|
||||||
|
|
||||||
if tzkeyname is None:
|
|
||||||
raise LookupError('Can not find Windows timezone configuration')
|
|
||||||
|
|
||||||
timezone = win_tz.get(tzkeyname)
|
timezone = win_tz.get(tzkeyname)
|
||||||
if timezone is None:
|
if timezone is None:
|
||||||
@@ -76,18 +82,66 @@ def get_localzone_name():
|
|||||||
|
|
||||||
# Return what we have.
|
# Return what we have.
|
||||||
if timezone is None:
|
if timezone is None:
|
||||||
raise pytz.UnknownTimeZoneError('Can not find timezone ' + tzkeyname)
|
raise zoneinfo.ZoneInfoNotFoundError(tzkeyname)
|
||||||
|
|
||||||
|
if keyvalues.get("DynamicDaylightTimeDisabled", 0) == 1:
|
||||||
|
# DST is disabled, so don't return the timezone name,
|
||||||
|
# instead return Etc/GMT+offset
|
||||||
|
|
||||||
|
tz = zoneinfo.ZoneInfo(timezone)
|
||||||
|
has_dst, std_offset, dst_offset = _get_dst_info(tz)
|
||||||
|
if not has_dst:
|
||||||
|
# The DST is turned off in the windows configuration,
|
||||||
|
# but this timezone doesn't have DST so it doesn't matter
|
||||||
|
return timezone
|
||||||
|
|
||||||
|
if std_offset is None:
|
||||||
|
raise zoneinfo.ZoneInfoNotFoundError(
|
||||||
|
f"{tzkeyname} claims to not have a non-DST time!?"
|
||||||
|
)
|
||||||
|
|
||||||
|
if std_offset % 3600:
|
||||||
|
# I can't convert this to an hourly offset
|
||||||
|
raise zoneinfo.ZoneInfoNotFoundError(
|
||||||
|
f"tzlocal can't support disabling DST in the {timezone} zone."
|
||||||
|
)
|
||||||
|
|
||||||
|
# This has whole hours as offset, return it as Etc/GMT
|
||||||
|
return f"Etc/GMT{-std_offset//3600:+.0f}"
|
||||||
|
|
||||||
return timezone
|
return timezone
|
||||||
|
|
||||||
def get_localzone():
|
|
||||||
|
def get_localzone_name() -> str:
|
||||||
|
"""Get the zoneinfo timezone name that matches the Windows-configured timezone."""
|
||||||
|
global _cache_tz_name
|
||||||
|
if _cache_tz_name is None:
|
||||||
|
_cache_tz_name = _get_localzone_name()
|
||||||
|
|
||||||
|
return _cache_tz_name
|
||||||
|
|
||||||
|
|
||||||
|
def get_localzone() -> zoneinfo.ZoneInfo:
|
||||||
"""Returns the zoneinfo-based tzinfo object that matches the Windows-configured timezone."""
|
"""Returns the zoneinfo-based tzinfo object that matches the Windows-configured timezone."""
|
||||||
|
|
||||||
global _cache_tz
|
global _cache_tz
|
||||||
if _cache_tz is None:
|
if _cache_tz is None:
|
||||||
_cache_tz = pytz.timezone(get_localzone_name())
|
_cache_tz = zoneinfo.ZoneInfo(get_localzone_name())
|
||||||
|
|
||||||
|
if not utils._tz_name_from_env():
|
||||||
|
# If the timezone does NOT come from a TZ environment variable,
|
||||||
|
# verify that it's correct. If it's from the environment,
|
||||||
|
# we accept it, this is so you can run tests with different timezones.
|
||||||
|
utils.assert_tz_offset(_cache_tz, error=False)
|
||||||
|
|
||||||
return _cache_tz
|
return _cache_tz
|
||||||
|
|
||||||
def reload_localzone():
|
|
||||||
|
def reload_localzone() -> zoneinfo.ZoneInfo:
|
||||||
"""Reload the cached localzone. You need to call this if the timezone has changed."""
|
"""Reload the cached localzone. You need to call this if the timezone has changed."""
|
||||||
global _cache_tz
|
global _cache_tz
|
||||||
_cache_tz = pytz.timezone(get_localzone_name())
|
global _cache_tz_name
|
||||||
|
_cache_tz_name = _get_localzone_name()
|
||||||
|
_cache_tz = zoneinfo.ZoneInfo(_cache_tz_name)
|
||||||
|
utils.assert_tz_offset(_cache_tz, error=False)
|
||||||
|
return _cache_tz
|
||||||
|
|||||||
+731
-537
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user