Spaces:
Running
Running
# -*- coding: utf-8 -*- | |
""" | |
This module offers timezone implementations subclassing the abstract | |
:py:class:`datetime.tzinfo` type. There are classes to handle tzfile format | |
files (usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`, | |
etc), TZ environment string (in all known formats), given ranges (with help | |
from relative deltas), local machine timezone, fixed offset timezone, and UTC | |
timezone. | |
""" | |
import datetime | |
import struct | |
import time | |
import sys | |
import os | |
import bisect | |
import weakref | |
from collections import OrderedDict | |
import six | |
from six import string_types | |
from six.moves import _thread | |
from ._common import tzname_in_python2, _tzinfo | |
from ._common import tzrangebase, enfold | |
from ._common import _validate_fromutc_inputs | |
from ._factories import _TzSingleton, _TzOffsetFactory | |
from ._factories import _TzStrFactory | |
try: | |
from .win import tzwin, tzwinlocal | |
except ImportError: | |
tzwin = tzwinlocal = None | |
# For warning about rounding tzinfo | |
from warnings import warn | |
ZERO = datetime.timedelta(0) | |
EPOCH = datetime.datetime(1970, 1, 1, 0, 0) | |
EPOCHORDINAL = EPOCH.toordinal() | |
class tzutc(datetime.tzinfo): | |
""" | |
This is a tzinfo object that represents the UTC time zone. | |
**Examples:** | |
.. doctest:: | |
>>> from datetime import * | |
>>> from dateutil.tz import * | |
>>> datetime.now() | |
datetime.datetime(2003, 9, 27, 9, 40, 1, 521290) | |
>>> datetime.now(tzutc()) | |
datetime.datetime(2003, 9, 27, 12, 40, 12, 156379, tzinfo=tzutc()) | |
>>> datetime.now(tzutc()).tzname() | |
'UTC' | |
.. versionchanged:: 2.7.0 | |
``tzutc()`` is now a singleton, so the result of ``tzutc()`` will | |
always return the same object. | |
.. doctest:: | |
>>> from dateutil.tz import tzutc, UTC | |
>>> tzutc() is tzutc() | |
True | |
>>> tzutc() is UTC | |
True | |
""" | |
def utcoffset(self, dt): | |
return ZERO | |
def dst(self, dt): | |
return ZERO | |
def tzname(self, dt): | |
return "UTC" | |
def is_ambiguous(self, dt): | |
""" | |
Whether or not the "wall time" of a given datetime is ambiguous in this | |
zone. | |
:param dt: | |
A :py:class:`datetime.datetime`, naive or time zone aware. | |
:return: | |
Returns ``True`` if ambiguous, ``False`` otherwise. | |
.. versionadded:: 2.6.0 | |
""" | |
return False | |
def fromutc(self, dt): | |
""" | |
Fast track version of fromutc() returns the original ``dt`` object for | |
any valid :py:class:`datetime.datetime` object. | |
""" | |
return dt | |
def __eq__(self, other): | |
if not isinstance(other, (tzutc, tzoffset)): | |
return NotImplemented | |
return (isinstance(other, tzutc) or | |
(isinstance(other, tzoffset) and other._offset == ZERO)) | |
__hash__ = None | |
def __ne__(self, other): | |
return not (self == other) | |
def __repr__(self): | |
return "%s()" % self.__class__.__name__ | |
__reduce__ = object.__reduce__ | |
#: Convenience constant providing a :class:`tzutc()` instance | |
#: | |
#: .. versionadded:: 2.7.0 | |
UTC = tzutc() | |
class tzoffset(datetime.tzinfo): | |
""" | |
A simple class for representing a fixed offset from UTC. | |
:param name: | |
The timezone name, to be returned when ``tzname()`` is called. | |
:param offset: | |
The time zone offset in seconds, or (since version 2.6.0, represented | |
as a :py:class:`datetime.timedelta` object). | |
""" | |
def __init__(self, name, offset): | |
self._name = name | |
try: | |
# Allow a timedelta | |
offset = offset.total_seconds() | |
except (TypeError, AttributeError): | |
pass | |
self._offset = datetime.timedelta(seconds=_get_supported_offset(offset)) | |
def utcoffset(self, dt): | |
return self._offset | |
def dst(self, dt): | |
return ZERO | |
def tzname(self, dt): | |
return self._name | |
def fromutc(self, dt): | |
return dt + self._offset | |
def is_ambiguous(self, dt): | |
""" | |
Whether or not the "wall time" of a given datetime is ambiguous in this | |
zone. | |
:param dt: | |
A :py:class:`datetime.datetime`, naive or time zone aware. | |
:return: | |
Returns ``True`` if ambiguous, ``False`` otherwise. | |
.. versionadded:: 2.6.0 | |
""" | |
return False | |
def __eq__(self, other): | |
if not isinstance(other, tzoffset): | |
return NotImplemented | |
return self._offset == other._offset | |
__hash__ = None | |
def __ne__(self, other): | |
return not (self == other) | |
def __repr__(self): | |
return "%s(%s, %s)" % (self.__class__.__name__, | |
repr(self._name), | |
int(self._offset.total_seconds())) | |
__reduce__ = object.__reduce__ | |
class tzlocal(_tzinfo): | |
""" | |
A :class:`tzinfo` subclass built around the ``time`` timezone functions. | |
""" | |
def __init__(self): | |
super(tzlocal, self).__init__() | |
self._std_offset = datetime.timedelta(seconds=-time.timezone) | |
if time.daylight: | |
self._dst_offset = datetime.timedelta(seconds=-time.altzone) | |
else: | |
self._dst_offset = self._std_offset | |
self._dst_saved = self._dst_offset - self._std_offset | |
self._hasdst = bool(self._dst_saved) | |
self._tznames = tuple(time.tzname) | |
def utcoffset(self, dt): | |
if dt is None and self._hasdst: | |
return None | |
if self._isdst(dt): | |
return self._dst_offset | |
else: | |
return self._std_offset | |
def dst(self, dt): | |
if dt is None and self._hasdst: | |
return None | |
if self._isdst(dt): | |
return self._dst_offset - self._std_offset | |
else: | |
return ZERO | |
def tzname(self, dt): | |
return self._tznames[self._isdst(dt)] | |
def is_ambiguous(self, dt): | |
""" | |
Whether or not the "wall time" of a given datetime is ambiguous in this | |
zone. | |
:param dt: | |
A :py:class:`datetime.datetime`, naive or time zone aware. | |
:return: | |
Returns ``True`` if ambiguous, ``False`` otherwise. | |
.. versionadded:: 2.6.0 | |
""" | |
naive_dst = self._naive_is_dst(dt) | |
return (not naive_dst and | |
(naive_dst != self._naive_is_dst(dt - self._dst_saved))) | |
def _naive_is_dst(self, dt): | |
timestamp = _datetime_to_timestamp(dt) | |
return time.localtime(timestamp + time.timezone).tm_isdst | |
def _isdst(self, dt, fold_naive=True): | |
# We can't use mktime here. It is unstable when deciding if | |
# the hour near to a change is DST or not. | |
# | |
# timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour, | |
# dt.minute, dt.second, dt.weekday(), 0, -1)) | |
# return time.localtime(timestamp).tm_isdst | |
# | |
# The code above yields the following result: | |
# | |
# >>> import tz, datetime | |
# >>> t = tz.tzlocal() | |
# >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() | |
# 'BRDT' | |
# >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname() | |
# 'BRST' | |
# >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() | |
# 'BRST' | |
# >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname() | |
# 'BRDT' | |
# >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() | |
# 'BRDT' | |
# | |
# Here is a more stable implementation: | |
# | |
if not self._hasdst: | |
return False | |
# Check for ambiguous times: | |
dstval = self._naive_is_dst(dt) | |
fold = getattr(dt, 'fold', None) | |
if self.is_ambiguous(dt): | |
if fold is not None: | |
return not self._fold(dt) | |
else: | |
return True | |
return dstval | |
def __eq__(self, other): | |
if isinstance(other, tzlocal): | |
return (self._std_offset == other._std_offset and | |
self._dst_offset == other._dst_offset) | |
elif isinstance(other, tzutc): | |
return (not self._hasdst and | |
self._tznames[0] in {'UTC', 'GMT'} and | |
self._std_offset == ZERO) | |
elif isinstance(other, tzoffset): | |
return (not self._hasdst and | |
self._tznames[0] == other._name and | |
self._std_offset == other._offset) | |
else: | |
return NotImplemented | |
__hash__ = None | |
def __ne__(self, other): | |
return not (self == other) | |
def __repr__(self): | |
return "%s()" % self.__class__.__name__ | |
__reduce__ = object.__reduce__ | |
class _ttinfo(object): | |
__slots__ = ["offset", "delta", "isdst", "abbr", | |
"isstd", "isgmt", "dstoffset"] | |
def __init__(self): | |
for attr in self.__slots__: | |
setattr(self, attr, None) | |
def __repr__(self): | |
l = [] | |
for attr in self.__slots__: | |
value = getattr(self, attr) | |
if value is not None: | |
l.append("%s=%s" % (attr, repr(value))) | |
return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) | |
def __eq__(self, other): | |
if not isinstance(other, _ttinfo): | |
return NotImplemented | |
return (self.offset == other.offset and | |
self.delta == other.delta and | |
self.isdst == other.isdst and | |
self.abbr == other.abbr and | |
self.isstd == other.isstd and | |
self.isgmt == other.isgmt and | |
self.dstoffset == other.dstoffset) | |
__hash__ = None | |
def __ne__(self, other): | |
return not (self == other) | |
def __getstate__(self): | |
state = {} | |
for name in self.__slots__: | |
state[name] = getattr(self, name, None) | |
return state | |
def __setstate__(self, state): | |
for name in self.__slots__: | |
if name in state: | |
setattr(self, name, state[name]) | |
class _tzfile(object): | |
""" | |
Lightweight class for holding the relevant transition and time zone | |
information read from binary tzfiles. | |
""" | |
attrs = ['trans_list', 'trans_list_utc', 'trans_idx', 'ttinfo_list', | |
'ttinfo_std', 'ttinfo_dst', 'ttinfo_before', 'ttinfo_first'] | |
def __init__(self, **kwargs): | |
for attr in self.attrs: | |
setattr(self, attr, kwargs.get(attr, None)) | |
class tzfile(_tzinfo): | |
""" | |
This is a ``tzinfo`` subclass that allows one to use the ``tzfile(5)`` | |
format timezone files to extract current and historical zone information. | |
:param fileobj: | |
This can be an opened file stream or a file name that the time zone | |
information can be read from. | |
:param filename: | |
This is an optional parameter specifying the source of the time zone | |
information in the event that ``fileobj`` is a file object. If omitted | |
and ``fileobj`` is a file stream, this parameter will be set either to | |
``fileobj``'s ``name`` attribute or to ``repr(fileobj)``. | |
See `Sources for Time Zone and Daylight Saving Time Data | |
<https://data.iana.org/time-zones/tz-link.html>`_ for more information. | |
Time zone files can be compiled from the `IANA Time Zone database files | |
<https://www.iana.org/time-zones>`_ with the `zic time zone compiler | |
<https://www.freebsd.org/cgi/man.cgi?query=zic&sektion=8>`_ | |
.. note:: | |
Only construct a ``tzfile`` directly if you have a specific timezone | |
file on disk that you want to read into a Python ``tzinfo`` object. | |
If you want to get a ``tzfile`` representing a specific IANA zone, | |
(e.g. ``'America/New_York'``), you should call | |
:func:`dateutil.tz.gettz` with the zone identifier. | |
**Examples:** | |
Using the US Eastern time zone as an example, we can see that a ``tzfile`` | |
provides time zone information for the standard Daylight Saving offsets: | |
.. testsetup:: tzfile | |
from dateutil.tz import gettz | |
from datetime import datetime | |
.. doctest:: tzfile | |
>>> NYC = gettz('America/New_York') | |
>>> NYC | |
tzfile('/usr/share/zoneinfo/America/New_York') | |
>>> print(datetime(2016, 1, 3, tzinfo=NYC)) # EST | |
2016-01-03 00:00:00-05:00 | |
>>> print(datetime(2016, 7, 7, tzinfo=NYC)) # EDT | |
2016-07-07 00:00:00-04:00 | |
The ``tzfile`` structure contains a fully history of the time zone, | |
so historical dates will also have the right offsets. For example, before | |
the adoption of the UTC standards, New York used local solar mean time: | |
.. doctest:: tzfile | |
>>> print(datetime(1901, 4, 12, tzinfo=NYC)) # LMT | |
1901-04-12 00:00:00-04:56 | |
And during World War II, New York was on "Eastern War Time", which was a | |
state of permanent daylight saving time: | |
.. doctest:: tzfile | |
>>> print(datetime(1944, 2, 7, tzinfo=NYC)) # EWT | |
1944-02-07 00:00:00-04:00 | |
""" | |
def __init__(self, fileobj, filename=None): | |
super(tzfile, self).__init__() | |
file_opened_here = False | |
if isinstance(fileobj, string_types): | |
self._filename = fileobj | |
fileobj = open(fileobj, 'rb') | |
file_opened_here = True | |
elif filename is not None: | |
self._filename = filename | |
elif hasattr(fileobj, "name"): | |
self._filename = fileobj.name | |
else: | |
self._filename = repr(fileobj) | |
if fileobj is not None: | |
if not file_opened_here: | |
fileobj = _nullcontext(fileobj) | |
with fileobj as file_stream: | |
tzobj = self._read_tzfile(file_stream) | |
self._set_tzdata(tzobj) | |
def _set_tzdata(self, tzobj): | |
""" Set the time zone data of this object from a _tzfile object """ | |
# Copy the relevant attributes over as private attributes | |
for attr in _tzfile.attrs: | |
setattr(self, '_' + attr, getattr(tzobj, attr)) | |
def _read_tzfile(self, fileobj): | |
out = _tzfile() | |
# From tzfile(5): | |
# | |
# The time zone information files used by tzset(3) | |
# begin with the magic characters "TZif" to identify | |
# them as time zone information files, followed by | |
# sixteen bytes reserved for future use, followed by | |
# six four-byte values of type long, written in a | |
# ``standard'' byte order (the high-order byte | |
# of the value is written first). | |
if fileobj.read(4).decode() != "TZif": | |
raise ValueError("magic not found") | |
fileobj.read(16) | |
( | |
# The number of UTC/local indicators stored in the file. | |
ttisgmtcnt, | |
# The number of standard/wall indicators stored in the file. | |
ttisstdcnt, | |
# The number of leap seconds for which data is | |
# stored in the file. | |
leapcnt, | |
# The number of "transition times" for which data | |
# is stored in the file. | |
timecnt, | |
# The number of "local time types" for which data | |
# is stored in the file (must not be zero). | |
typecnt, | |
# The number of characters of "time zone | |
# abbreviation strings" stored in the file. | |
charcnt, | |
) = struct.unpack(">6l", fileobj.read(24)) | |
# The above header is followed by tzh_timecnt four-byte | |
# values of type long, sorted in ascending order. | |
# These values are written in ``standard'' byte order. | |
# Each is used as a transition time (as returned by | |
# time(2)) at which the rules for computing local time | |
# change. | |
if timecnt: | |
out.trans_list_utc = list(struct.unpack(">%dl" % timecnt, | |
fileobj.read(timecnt*4))) | |
else: | |
out.trans_list_utc = [] | |
# Next come tzh_timecnt one-byte values of type unsigned | |
# char; each one tells which of the different types of | |
# ``local time'' types described in the file is associated | |
# with the same-indexed transition time. These values | |
# serve as indices into an array of ttinfo structures that | |
# appears next in the file. | |
if timecnt: | |
out.trans_idx = struct.unpack(">%dB" % timecnt, | |
fileobj.read(timecnt)) | |
else: | |
out.trans_idx = [] | |
# Each ttinfo structure is written as a four-byte value | |
# for tt_gmtoff of type long, in a standard byte | |
# order, followed by a one-byte value for tt_isdst | |
# and a one-byte value for tt_abbrind. In each | |
# structure, tt_gmtoff gives the number of | |
# seconds to be added to UTC, tt_isdst tells whether | |
# tm_isdst should be set by localtime(3), and | |
# tt_abbrind serves as an index into the array of | |
# time zone abbreviation characters that follow the | |
# ttinfo structure(s) in the file. | |
ttinfo = [] | |
for i in range(typecnt): | |
ttinfo.append(struct.unpack(">lbb", fileobj.read(6))) | |
abbr = fileobj.read(charcnt).decode() | |
# Then there are tzh_leapcnt pairs of four-byte | |
# values, written in standard byte order; the | |
# first value of each pair gives the time (as | |
# returned by time(2)) at which a leap second | |
# occurs; the second gives the total number of | |
# leap seconds to be applied after the given time. | |
# The pairs of values are sorted in ascending order | |
# by time. | |
# Not used, for now (but seek for correct file position) | |
if leapcnt: | |
fileobj.seek(leapcnt * 8, os.SEEK_CUR) | |
# Then there are tzh_ttisstdcnt standard/wall | |
# indicators, each stored as a one-byte value; | |
# they tell whether the transition times associated | |
# with local time types were specified as standard | |
# time or wall clock time, and are used when | |
# a time zone file is used in handling POSIX-style | |
# time zone environment variables. | |
if ttisstdcnt: | |
isstd = struct.unpack(">%db" % ttisstdcnt, | |
fileobj.read(ttisstdcnt)) | |
# Finally, there are tzh_ttisgmtcnt UTC/local | |
# indicators, each stored as a one-byte value; | |
# they tell whether the transition times associated | |
# with local time types were specified as UTC or | |
# local time, and are used when a time zone file | |
# is used in handling POSIX-style time zone envi- | |
# ronment variables. | |
if ttisgmtcnt: | |
isgmt = struct.unpack(">%db" % ttisgmtcnt, | |
fileobj.read(ttisgmtcnt)) | |
# Build ttinfo list | |
out.ttinfo_list = [] | |
for i in range(typecnt): | |
gmtoff, isdst, abbrind = ttinfo[i] | |
gmtoff = _get_supported_offset(gmtoff) | |
tti = _ttinfo() | |
tti.offset = gmtoff | |
tti.dstoffset = datetime.timedelta(0) | |
tti.delta = datetime.timedelta(seconds=gmtoff) | |
tti.isdst = isdst | |
tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)] | |
tti.isstd = (ttisstdcnt > i and isstd[i] != 0) | |
tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0) | |
out.ttinfo_list.append(tti) | |
# Replace ttinfo indexes for ttinfo objects. | |
out.trans_idx = [out.ttinfo_list[idx] for idx in out.trans_idx] | |
# Set standard, dst, and before ttinfos. before will be | |
# used when a given time is before any transitions, | |
# and will be set to the first non-dst ttinfo, or to | |
# the first dst, if all of them are dst. | |
out.ttinfo_std = None | |
out.ttinfo_dst = None | |
out.ttinfo_before = None | |
if out.ttinfo_list: | |
if not out.trans_list_utc: | |
out.ttinfo_std = out.ttinfo_first = out.ttinfo_list[0] | |
else: | |
for i in range(timecnt-1, -1, -1): | |
tti = out.trans_idx[i] | |
if not out.ttinfo_std and not tti.isdst: | |
out.ttinfo_std = tti | |
elif not out.ttinfo_dst and tti.isdst: | |
out.ttinfo_dst = tti | |
if out.ttinfo_std and out.ttinfo_dst: | |
break | |
else: | |
if out.ttinfo_dst and not out.ttinfo_std: | |
out.ttinfo_std = out.ttinfo_dst | |
for tti in out.ttinfo_list: | |
if not tti.isdst: | |
out.ttinfo_before = tti | |
break | |
else: | |
out.ttinfo_before = out.ttinfo_list[0] | |
# Now fix transition times to become relative to wall time. | |
# | |
# I'm not sure about this. In my tests, the tz source file | |
# is setup to wall time, and in the binary file isstd and | |
# isgmt are off, so it should be in wall time. OTOH, it's | |
# always in gmt time. Let me know if you have comments | |
# about this. | |
lastdst = None | |
lastoffset = None | |
lastdstoffset = None | |
lastbaseoffset = None | |
out.trans_list = [] | |
for i, tti in enumerate(out.trans_idx): | |
offset = tti.offset | |
dstoffset = 0 | |
if lastdst is not None: | |
if tti.isdst: | |
if not lastdst: | |
dstoffset = offset - lastoffset | |
if not dstoffset and lastdstoffset: | |
dstoffset = lastdstoffset | |
tti.dstoffset = datetime.timedelta(seconds=dstoffset) | |
lastdstoffset = dstoffset | |
# If a time zone changes its base offset during a DST transition, | |
# then you need to adjust by the previous base offset to get the | |
# transition time in local time. Otherwise you use the current | |
# base offset. Ideally, I would have some mathematical proof of | |
# why this is true, but I haven't really thought about it enough. | |
baseoffset = offset - dstoffset | |
adjustment = baseoffset | |
if (lastbaseoffset is not None and baseoffset != lastbaseoffset | |
and tti.isdst != lastdst): | |
# The base DST has changed | |
adjustment = lastbaseoffset | |
lastdst = tti.isdst | |
lastoffset = offset | |
lastbaseoffset = baseoffset | |
out.trans_list.append(out.trans_list_utc[i] + adjustment) | |
out.trans_idx = tuple(out.trans_idx) | |
out.trans_list = tuple(out.trans_list) | |
out.trans_list_utc = tuple(out.trans_list_utc) | |
return out | |
def _find_last_transition(self, dt, in_utc=False): | |
# If there's no list, there are no transitions to find | |
if not self._trans_list: | |
return None | |
timestamp = _datetime_to_timestamp(dt) | |
# Find where the timestamp fits in the transition list - if the | |
# timestamp is a transition time, it's part of the "after" period. | |
trans_list = self._trans_list_utc if in_utc else self._trans_list | |
idx = bisect.bisect_right(trans_list, timestamp) | |
# We want to know when the previous transition was, so subtract off 1 | |
return idx - 1 | |
def _get_ttinfo(self, idx): | |
# For no list or after the last transition, default to _ttinfo_std | |
if idx is None or (idx + 1) >= len(self._trans_list): | |
return self._ttinfo_std | |
# If there is a list and the time is before it, return _ttinfo_before | |
if idx < 0: | |
return self._ttinfo_before | |
return self._trans_idx[idx] | |
def _find_ttinfo(self, dt): | |
idx = self._resolve_ambiguous_time(dt) | |
return self._get_ttinfo(idx) | |
def fromutc(self, dt): | |
""" | |
The ``tzfile`` implementation of :py:func:`datetime.tzinfo.fromutc`. | |
:param dt: | |
A :py:class:`datetime.datetime` object. | |
:raises TypeError: | |
Raised if ``dt`` is not a :py:class:`datetime.datetime` object. | |
:raises ValueError: | |
Raised if this is called with a ``dt`` which does not have this | |
``tzinfo`` attached. | |
:return: | |
Returns a :py:class:`datetime.datetime` object representing the | |
wall time in ``self``'s time zone. | |
""" | |
# These isinstance checks are in datetime.tzinfo, so we'll preserve | |
# them, even if we don't care about duck typing. | |
if not isinstance(dt, datetime.datetime): | |
raise TypeError("fromutc() requires a datetime argument") | |
if dt.tzinfo is not self: | |
raise ValueError("dt.tzinfo is not self") | |
# First treat UTC as wall time and get the transition we're in. | |
idx = self._find_last_transition(dt, in_utc=True) | |
tti = self._get_ttinfo(idx) | |
dt_out = dt + datetime.timedelta(seconds=tti.offset) | |
fold = self.is_ambiguous(dt_out, idx=idx) | |
return enfold(dt_out, fold=int(fold)) | |
def is_ambiguous(self, dt, idx=None): | |
""" | |
Whether or not the "wall time" of a given datetime is ambiguous in this | |
zone. | |
:param dt: | |
A :py:class:`datetime.datetime`, naive or time zone aware. | |
:return: | |
Returns ``True`` if ambiguous, ``False`` otherwise. | |
.. versionadded:: 2.6.0 | |
""" | |
if idx is None: | |
idx = self._find_last_transition(dt) | |
# Calculate the difference in offsets from current to previous | |
timestamp = _datetime_to_timestamp(dt) | |
tti = self._get_ttinfo(idx) | |
if idx is None or idx <= 0: | |
return False | |
od = self._get_ttinfo(idx - 1).offset - tti.offset | |
tt = self._trans_list[idx] # Transition time | |
return timestamp < tt + od | |
def _resolve_ambiguous_time(self, dt): | |
idx = self._find_last_transition(dt) | |
# If we have no transitions, return the index | |
_fold = self._fold(dt) | |
if idx is None or idx == 0: | |
return idx | |
# If it's ambiguous and we're in a fold, shift to a different index. | |
idx_offset = int(not _fold and self.is_ambiguous(dt, idx)) | |
return idx - idx_offset | |
def utcoffset(self, dt): | |
if dt is None: | |
return None | |
if not self._ttinfo_std: | |
return ZERO | |
return self._find_ttinfo(dt).delta | |
def dst(self, dt): | |
if dt is None: | |
return None | |
if not self._ttinfo_dst: | |
return ZERO | |
tti = self._find_ttinfo(dt) | |
if not tti.isdst: | |
return ZERO | |
# The documentation says that utcoffset()-dst() must | |
# be constant for every dt. | |
return tti.dstoffset | |
def tzname(self, dt): | |
if not self._ttinfo_std or dt is None: | |
return None | |
return self._find_ttinfo(dt).abbr | |
def __eq__(self, other): | |
if not isinstance(other, tzfile): | |
return NotImplemented | |
return (self._trans_list == other._trans_list and | |
self._trans_idx == other._trans_idx and | |
self._ttinfo_list == other._ttinfo_list) | |
__hash__ = None | |
def __ne__(self, other): | |
return not (self == other) | |
def __repr__(self): | |
return "%s(%s)" % (self.__class__.__name__, repr(self._filename)) | |
def __reduce__(self): | |
return self.__reduce_ex__(None) | |
def __reduce_ex__(self, protocol): | |
return (self.__class__, (None, self._filename), self.__dict__) | |
class tzrange(tzrangebase): | |
""" | |
The ``tzrange`` object is a time zone specified by a set of offsets and | |
abbreviations, equivalent to the way the ``TZ`` variable can be specified | |
in POSIX-like systems, but using Python delta objects to specify DST | |
start, end and offsets. | |
:param stdabbr: | |
The abbreviation for standard time (e.g. ``'EST'``). | |
:param stdoffset: | |
An integer or :class:`datetime.timedelta` object or equivalent | |
specifying the base offset from UTC. | |
If unspecified, +00:00 is used. | |
:param dstabbr: | |
The abbreviation for DST / "Summer" time (e.g. ``'EDT'``). | |
If specified, with no other DST information, DST is assumed to occur | |
and the default behavior or ``dstoffset``, ``start`` and ``end`` is | |
used. If unspecified and no other DST information is specified, it | |
is assumed that this zone has no DST. | |
If this is unspecified and other DST information is *is* specified, | |
DST occurs in the zone but the time zone abbreviation is left | |
unchanged. | |
:param dstoffset: | |
A an integer or :class:`datetime.timedelta` object or equivalent | |
specifying the UTC offset during DST. If unspecified and any other DST | |
information is specified, it is assumed to be the STD offset +1 hour. | |
:param start: | |
A :class:`relativedelta.relativedelta` object or equivalent specifying | |
the time and time of year that daylight savings time starts. To | |
specify, for example, that DST starts at 2AM on the 2nd Sunday in | |
March, pass: | |
``relativedelta(hours=2, month=3, day=1, weekday=SU(+2))`` | |
If unspecified and any other DST information is specified, the default | |
value is 2 AM on the first Sunday in April. | |
:param end: | |
A :class:`relativedelta.relativedelta` object or equivalent | |
representing the time and time of year that daylight savings time | |
ends, with the same specification method as in ``start``. One note is | |
that this should point to the first time in the *standard* zone, so if | |
a transition occurs at 2AM in the DST zone and the clocks are set back | |
1 hour to 1AM, set the ``hours`` parameter to +1. | |
**Examples:** | |
.. testsetup:: tzrange | |
from dateutil.tz import tzrange, tzstr | |
.. doctest:: tzrange | |
>>> tzstr('EST5EDT') == tzrange("EST", -18000, "EDT") | |
True | |
>>> from dateutil.relativedelta import * | |
>>> range1 = tzrange("EST", -18000, "EDT") | |
>>> range2 = tzrange("EST", -18000, "EDT", -14400, | |
... relativedelta(hours=+2, month=4, day=1, | |
... weekday=SU(+1)), | |
... relativedelta(hours=+1, month=10, day=31, | |
... weekday=SU(-1))) | |
>>> tzstr('EST5EDT') == range1 == range2 | |
True | |
""" | |
def __init__(self, stdabbr, stdoffset=None, | |
dstabbr=None, dstoffset=None, | |
start=None, end=None): | |
global relativedelta | |
from dateutil import relativedelta | |
self._std_abbr = stdabbr | |
self._dst_abbr = dstabbr | |
try: | |
stdoffset = stdoffset.total_seconds() | |
except (TypeError, AttributeError): | |
pass | |
try: | |
dstoffset = dstoffset.total_seconds() | |
except (TypeError, AttributeError): | |
pass | |
if stdoffset is not None: | |
self._std_offset = datetime.timedelta(seconds=stdoffset) | |
else: | |
self._std_offset = ZERO | |
if dstoffset is not None: | |
self._dst_offset = datetime.timedelta(seconds=dstoffset) | |
elif dstabbr and stdoffset is not None: | |
self._dst_offset = self._std_offset + datetime.timedelta(hours=+1) | |
else: | |
self._dst_offset = ZERO | |
if dstabbr and start is None: | |
self._start_delta = relativedelta.relativedelta( | |
hours=+2, month=4, day=1, weekday=relativedelta.SU(+1)) | |
else: | |
self._start_delta = start | |
if dstabbr and end is None: | |
self._end_delta = relativedelta.relativedelta( | |
hours=+1, month=10, day=31, weekday=relativedelta.SU(-1)) | |
else: | |
self._end_delta = end | |
self._dst_base_offset_ = self._dst_offset - self._std_offset | |
self.hasdst = bool(self._start_delta) | |
def transitions(self, year): | |
""" | |
For a given year, get the DST on and off transition times, expressed | |
always on the standard time side. For zones with no transitions, this | |
function returns ``None``. | |
:param year: | |
The year whose transitions you would like to query. | |
:return: | |
Returns a :class:`tuple` of :class:`datetime.datetime` objects, | |
``(dston, dstoff)`` for zones with an annual DST transition, or | |
``None`` for fixed offset zones. | |
""" | |
if not self.hasdst: | |
return None | |
base_year = datetime.datetime(year, 1, 1) | |
start = base_year + self._start_delta | |
end = base_year + self._end_delta | |
return (start, end) | |
def __eq__(self, other): | |
if not isinstance(other, tzrange): | |
return NotImplemented | |
return (self._std_abbr == other._std_abbr and | |
self._dst_abbr == other._dst_abbr and | |
self._std_offset == other._std_offset and | |
self._dst_offset == other._dst_offset and | |
self._start_delta == other._start_delta and | |
self._end_delta == other._end_delta) | |
def _dst_base_offset(self): | |
return self._dst_base_offset_ | |
class tzstr(tzrange): | |
""" | |
``tzstr`` objects are time zone objects specified by a time-zone string as | |
it would be passed to a ``TZ`` variable on POSIX-style systems (see | |
the `GNU C Library: TZ Variable`_ for more details). | |
There is one notable exception, which is that POSIX-style time zones use an | |
inverted offset format, so normally ``GMT+3`` would be parsed as an offset | |
3 hours *behind* GMT. The ``tzstr`` time zone object will parse this as an | |
offset 3 hours *ahead* of GMT. If you would like to maintain the POSIX | |
behavior, pass a ``True`` value to ``posix_offset``. | |
The :class:`tzrange` object provides the same functionality, but is | |
specified using :class:`relativedelta.relativedelta` objects. rather than | |
strings. | |
:param s: | |
A time zone string in ``TZ`` variable format. This can be a | |
:class:`bytes` (2.x: :class:`str`), :class:`str` (2.x: | |
:class:`unicode`) or a stream emitting unicode characters | |
(e.g. :class:`StringIO`). | |
:param posix_offset: | |
Optional. If set to ``True``, interpret strings such as ``GMT+3`` or | |
``UTC+3`` as being 3 hours *behind* UTC rather than ahead, per the | |
POSIX standard. | |
.. caution:: | |
Prior to version 2.7.0, this function also supported time zones | |
in the format: | |
* ``EST5EDT,4,0,6,7200,10,0,26,7200,3600`` | |
* ``EST5EDT,4,1,0,7200,10,-1,0,7200,3600`` | |
This format is non-standard and has been deprecated; this function | |
will raise a :class:`DeprecatedTZFormatWarning` until | |
support is removed in a future version. | |
.. _`GNU C Library: TZ Variable`: | |
https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html | |
""" | |
def __init__(self, s, posix_offset=False): | |
global parser | |
from dateutil.parser import _parser as parser | |
self._s = s | |
res = parser._parsetz(s) | |
if res is None or res.any_unused_tokens: | |
raise ValueError("unknown string format") | |
# Here we break the compatibility with the TZ variable handling. | |
# GMT-3 actually *means* the timezone -3. | |
if res.stdabbr in ("GMT", "UTC") and not posix_offset: | |
res.stdoffset *= -1 | |
# We must initialize it first, since _delta() needs | |
# _std_offset and _dst_offset set. Use False in start/end | |
# to avoid building it two times. | |
tzrange.__init__(self, res.stdabbr, res.stdoffset, | |
res.dstabbr, res.dstoffset, | |
start=False, end=False) | |
if not res.dstabbr: | |
self._start_delta = None | |
self._end_delta = None | |
else: | |
self._start_delta = self._delta(res.start) | |
if self._start_delta: | |
self._end_delta = self._delta(res.end, isend=1) | |
self.hasdst = bool(self._start_delta) | |
def _delta(self, x, isend=0): | |
from dateutil import relativedelta | |
kwargs = {} | |
if x.month is not None: | |
kwargs["month"] = x.month | |
if x.weekday is not None: | |
kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week) | |
if x.week > 0: | |
kwargs["day"] = 1 | |
else: | |
kwargs["day"] = 31 | |
elif x.day: | |
kwargs["day"] = x.day | |
elif x.yday is not None: | |
kwargs["yearday"] = x.yday | |
elif x.jyday is not None: | |
kwargs["nlyearday"] = x.jyday | |
if not kwargs: | |
# Default is to start on first sunday of april, and end | |
# on last sunday of october. | |
if not isend: | |
kwargs["month"] = 4 | |
kwargs["day"] = 1 | |
kwargs["weekday"] = relativedelta.SU(+1) | |
else: | |
kwargs["month"] = 10 | |
kwargs["day"] = 31 | |
kwargs["weekday"] = relativedelta.SU(-1) | |
if x.time is not None: | |
kwargs["seconds"] = x.time | |
else: | |
# Default is 2AM. | |
kwargs["seconds"] = 7200 | |
if isend: | |
# Convert to standard time, to follow the documented way | |
# of working with the extra hour. See the documentation | |
# of the tzinfo class. | |
delta = self._dst_offset - self._std_offset | |
kwargs["seconds"] -= delta.seconds + delta.days * 86400 | |
return relativedelta.relativedelta(**kwargs) | |
def __repr__(self): | |
return "%s(%s)" % (self.__class__.__name__, repr(self._s)) | |
class _tzicalvtzcomp(object): | |
def __init__(self, tzoffsetfrom, tzoffsetto, isdst, | |
tzname=None, rrule=None): | |
self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom) | |
self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto) | |
self.tzoffsetdiff = self.tzoffsetto - self.tzoffsetfrom | |
self.isdst = isdst | |
self.tzname = tzname | |
self.rrule = rrule | |
class _tzicalvtz(_tzinfo): | |
def __init__(self, tzid, comps=[]): | |
super(_tzicalvtz, self).__init__() | |
self._tzid = tzid | |
self._comps = comps | |
self._cachedate = [] | |
self._cachecomp = [] | |
self._cache_lock = _thread.allocate_lock() | |
def _find_comp(self, dt): | |
if len(self._comps) == 1: | |
return self._comps[0] | |
dt = dt.replace(tzinfo=None) | |
try: | |
with self._cache_lock: | |
return self._cachecomp[self._cachedate.index( | |
(dt, self._fold(dt)))] | |
except ValueError: | |
pass | |
lastcompdt = None | |
lastcomp = None | |
for comp in self._comps: | |
compdt = self._find_compdt(comp, dt) | |
if compdt and (not lastcompdt or lastcompdt < compdt): | |
lastcompdt = compdt | |
lastcomp = comp | |
if not lastcomp: | |
# RFC says nothing about what to do when a given | |
# time is before the first onset date. We'll look for the | |
# first standard component, or the first component, if | |
# none is found. | |
for comp in self._comps: | |
if not comp.isdst: | |
lastcomp = comp | |
break | |
else: | |
lastcomp = comp[0] | |
with self._cache_lock: | |
self._cachedate.insert(0, (dt, self._fold(dt))) | |
self._cachecomp.insert(0, lastcomp) | |
if len(self._cachedate) > 10: | |
self._cachedate.pop() | |
self._cachecomp.pop() | |
return lastcomp | |
def _find_compdt(self, comp, dt): | |
if comp.tzoffsetdiff < ZERO and self._fold(dt): | |
dt -= comp.tzoffsetdiff | |
compdt = comp.rrule.before(dt, inc=True) | |
return compdt | |
def utcoffset(self, dt): | |
if dt is None: | |
return None | |
return self._find_comp(dt).tzoffsetto | |
def dst(self, dt): | |
comp = self._find_comp(dt) | |
if comp.isdst: | |
return comp.tzoffsetdiff | |
else: | |
return ZERO | |
def tzname(self, dt): | |
return self._find_comp(dt).tzname | |
def __repr__(self): | |
return "<tzicalvtz %s>" % repr(self._tzid) | |
__reduce__ = object.__reduce__ | |
class tzical(object): | |
""" | |
This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure | |
as set out in `RFC 5545`_ Section 4.6.5 into one or more `tzinfo` objects. | |
:param `fileobj`: | |
A file or stream in iCalendar format, which should be UTF-8 encoded | |
with CRLF endings. | |
.. _`RFC 5545`: https://tools.ietf.org/html/rfc5545 | |
""" | |
def __init__(self, fileobj): | |
global rrule | |
from dateutil import rrule | |
if isinstance(fileobj, string_types): | |
self._s = fileobj | |
# ical should be encoded in UTF-8 with CRLF | |
fileobj = open(fileobj, 'r') | |
else: | |
self._s = getattr(fileobj, 'name', repr(fileobj)) | |
fileobj = _nullcontext(fileobj) | |
self._vtz = {} | |
with fileobj as fobj: | |
self._parse_rfc(fobj.read()) | |
def keys(self): | |
""" | |
Retrieves the available time zones as a list. | |
""" | |
return list(self._vtz.keys()) | |
def get(self, tzid=None): | |
""" | |
Retrieve a :py:class:`datetime.tzinfo` object by its ``tzid``. | |
:param tzid: | |
If there is exactly one time zone available, omitting ``tzid`` | |
or passing :py:const:`None` value returns it. Otherwise a valid | |
key (which can be retrieved from :func:`keys`) is required. | |
:raises ValueError: | |
Raised if ``tzid`` is not specified but there are either more | |
or fewer than 1 zone defined. | |
:returns: | |
Returns either a :py:class:`datetime.tzinfo` object representing | |
the relevant time zone or :py:const:`None` if the ``tzid`` was | |
not found. | |
""" | |
if tzid is None: | |
if len(self._vtz) == 0: | |
raise ValueError("no timezones defined") | |
elif len(self._vtz) > 1: | |
raise ValueError("more than one timezone available") | |
tzid = next(iter(self._vtz)) | |
return self._vtz.get(tzid) | |
def _parse_offset(self, s): | |
s = s.strip() | |
if not s: | |
raise ValueError("empty offset") | |
if s[0] in ('+', '-'): | |
signal = (-1, +1)[s[0] == '+'] | |
s = s[1:] | |
else: | |
signal = +1 | |
if len(s) == 4: | |
return (int(s[:2]) * 3600 + int(s[2:]) * 60) * signal | |
elif len(s) == 6: | |
return (int(s[:2]) * 3600 + int(s[2:4]) * 60 + int(s[4:])) * signal | |
else: | |
raise ValueError("invalid offset: " + s) | |
def _parse_rfc(self, s): | |
lines = s.splitlines() | |
if not lines: | |
raise ValueError("empty string") | |
# Unfold | |
i = 0 | |
while i < len(lines): | |
line = lines[i].rstrip() | |
if not line: | |
del lines[i] | |
elif i > 0 and line[0] == " ": | |
lines[i-1] += line[1:] | |
del lines[i] | |
else: | |
i += 1 | |
tzid = None | |
comps = [] | |
invtz = False | |
comptype = None | |
for line in lines: | |
if not line: | |
continue | |
name, value = line.split(':', 1) | |
parms = name.split(';') | |
if not parms: | |
raise ValueError("empty property name") | |
name = parms[0].upper() | |
parms = parms[1:] | |
if invtz: | |
if name == "BEGIN": | |
if value in ("STANDARD", "DAYLIGHT"): | |
# Process component | |
pass | |
else: | |
raise ValueError("unknown component: "+value) | |
comptype = value | |
founddtstart = False | |
tzoffsetfrom = None | |
tzoffsetto = None | |
rrulelines = [] | |
tzname = None | |
elif name == "END": | |
if value == "VTIMEZONE": | |
if comptype: | |
raise ValueError("component not closed: "+comptype) | |
if not tzid: | |
raise ValueError("mandatory TZID not found") | |
if not comps: | |
raise ValueError( | |
"at least one component is needed") | |
# Process vtimezone | |
self._vtz[tzid] = _tzicalvtz(tzid, comps) | |
invtz = False | |
elif value == comptype: | |
if not founddtstart: | |
raise ValueError("mandatory DTSTART not found") | |
if tzoffsetfrom is None: | |
raise ValueError( | |
"mandatory TZOFFSETFROM not found") | |
if tzoffsetto is None: | |
raise ValueError( | |
"mandatory TZOFFSETFROM not found") | |
# Process component | |
rr = None | |
if rrulelines: | |
rr = rrule.rrulestr("\n".join(rrulelines), | |
compatible=True, | |
ignoretz=True, | |
cache=True) | |
comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto, | |
(comptype == "DAYLIGHT"), | |
tzname, rr) | |
comps.append(comp) | |
comptype = None | |
else: | |
raise ValueError("invalid component end: "+value) | |
elif comptype: | |
if name == "DTSTART": | |
# DTSTART in VTIMEZONE takes a subset of valid RRULE | |
# values under RFC 5545. | |
for parm in parms: | |
if parm != 'VALUE=DATE-TIME': | |
msg = ('Unsupported DTSTART param in ' + | |
'VTIMEZONE: ' + parm) | |
raise ValueError(msg) | |
rrulelines.append(line) | |
founddtstart = True | |
elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"): | |
rrulelines.append(line) | |
elif name == "TZOFFSETFROM": | |
if parms: | |
raise ValueError( | |
"unsupported %s parm: %s " % (name, parms[0])) | |
tzoffsetfrom = self._parse_offset(value) | |
elif name == "TZOFFSETTO": | |
if parms: | |
raise ValueError( | |
"unsupported TZOFFSETTO parm: "+parms[0]) | |
tzoffsetto = self._parse_offset(value) | |
elif name == "TZNAME": | |
if parms: | |
raise ValueError( | |
"unsupported TZNAME parm: "+parms[0]) | |
tzname = value | |
elif name == "COMMENT": | |
pass | |
else: | |
raise ValueError("unsupported property: "+name) | |
else: | |
if name == "TZID": | |
if parms: | |
raise ValueError( | |
"unsupported TZID parm: "+parms[0]) | |
tzid = value | |
elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"): | |
pass | |
else: | |
raise ValueError("unsupported property: "+name) | |
elif name == "BEGIN" and value == "VTIMEZONE": | |
tzid = None | |
comps = [] | |
invtz = True | |
def __repr__(self): | |
return "%s(%s)" % (self.__class__.__name__, repr(self._s)) | |
if sys.platform != "win32": | |
TZFILES = ["/etc/localtime", "localtime"] | |
TZPATHS = ["/usr/share/zoneinfo", | |
"/usr/lib/zoneinfo", | |
"/usr/share/lib/zoneinfo", | |
"/etc/zoneinfo"] | |
else: | |
TZFILES = [] | |
TZPATHS = [] | |
def __get_gettz(): | |
tzlocal_classes = (tzlocal,) | |
if tzwinlocal is not None: | |
tzlocal_classes += (tzwinlocal,) | |
class GettzFunc(object): | |
""" | |
Retrieve a time zone object from a string representation | |
This function is intended to retrieve the :py:class:`tzinfo` subclass | |
that best represents the time zone that would be used if a POSIX | |
`TZ variable`_ were set to the same value. | |
If no argument or an empty string is passed to ``gettz``, local time | |
is returned: | |
.. code-block:: python3 | |
>>> gettz() | |
tzfile('/etc/localtime') | |
This function is also the preferred way to map IANA tz database keys | |
to :class:`tzfile` objects: | |
.. code-block:: python3 | |
>>> gettz('Pacific/Kiritimati') | |
tzfile('/usr/share/zoneinfo/Pacific/Kiritimati') | |
On Windows, the standard is extended to include the Windows-specific | |
zone names provided by the operating system: | |
.. code-block:: python3 | |
>>> gettz('Egypt Standard Time') | |
tzwin('Egypt Standard Time') | |
Passing a GNU ``TZ`` style string time zone specification returns a | |
:class:`tzstr` object: | |
.. code-block:: python3 | |
>>> gettz('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') | |
tzstr('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') | |
:param name: | |
A time zone name (IANA, or, on Windows, Windows keys), location of | |
a ``tzfile(5)`` zoneinfo file or ``TZ`` variable style time zone | |
specifier. An empty string, no argument or ``None`` is interpreted | |
as local time. | |
:return: | |
Returns an instance of one of ``dateutil``'s :py:class:`tzinfo` | |
subclasses. | |
.. versionchanged:: 2.7.0 | |
After version 2.7.0, any two calls to ``gettz`` using the same | |
input strings will return the same object: | |
.. code-block:: python3 | |
>>> tz.gettz('America/Chicago') is tz.gettz('America/Chicago') | |
True | |
In addition to improving performance, this ensures that | |
`"same zone" semantics`_ are used for datetimes in the same zone. | |
.. _`TZ variable`: | |
https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html | |
.. _`"same zone" semantics`: | |
https://blog.ganssle.io/articles/2018/02/aware-datetime-arithmetic.html | |
""" | |
def __init__(self): | |
self.__instances = weakref.WeakValueDictionary() | |
self.__strong_cache_size = 8 | |
self.__strong_cache = OrderedDict() | |
self._cache_lock = _thread.allocate_lock() | |
def __call__(self, name=None): | |
with self._cache_lock: | |
rv = self.__instances.get(name, None) | |
if rv is None: | |
rv = self.nocache(name=name) | |
if not (name is None | |
or isinstance(rv, tzlocal_classes) | |
or rv is None): | |
# tzlocal is slightly more complicated than the other | |
# time zone providers because it depends on environment | |
# at construction time, so don't cache that. | |
# | |
# We also cannot store weak references to None, so we | |
# will also not store that. | |
self.__instances[name] = rv | |
else: | |
# No need for strong caching, return immediately | |
return rv | |
self.__strong_cache[name] = self.__strong_cache.pop(name, rv) | |
if len(self.__strong_cache) > self.__strong_cache_size: | |
self.__strong_cache.popitem(last=False) | |
return rv | |
def set_cache_size(self, size): | |
with self._cache_lock: | |
self.__strong_cache_size = size | |
while len(self.__strong_cache) > size: | |
self.__strong_cache.popitem(last=False) | |
def cache_clear(self): | |
with self._cache_lock: | |
self.__instances = weakref.WeakValueDictionary() | |
self.__strong_cache.clear() | |
def nocache(name=None): | |
"""A non-cached version of gettz""" | |
tz = None | |
if not name: | |
try: | |
name = os.environ["TZ"] | |
except KeyError: | |
pass | |
if name is None or name in ("", ":"): | |
for filepath in TZFILES: | |
if not os.path.isabs(filepath): | |
filename = filepath | |
for path in TZPATHS: | |
filepath = os.path.join(path, filename) | |
if os.path.isfile(filepath): | |
break | |
else: | |
continue | |
if os.path.isfile(filepath): | |
try: | |
tz = tzfile(filepath) | |
break | |
except (IOError, OSError, ValueError): | |
pass | |
else: | |
tz = tzlocal() | |
else: | |
try: | |
if name.startswith(":"): | |
name = name[1:] | |
except TypeError as e: | |
if isinstance(name, bytes): | |
new_msg = "gettz argument should be str, not bytes" | |
six.raise_from(TypeError(new_msg), e) | |
else: | |
raise | |
if os.path.isabs(name): | |
if os.path.isfile(name): | |
tz = tzfile(name) | |
else: | |
tz = None | |
else: | |
for path in TZPATHS: | |
filepath = os.path.join(path, name) | |
if not os.path.isfile(filepath): | |
filepath = filepath.replace(' ', '_') | |
if not os.path.isfile(filepath): | |
continue | |
try: | |
tz = tzfile(filepath) | |
break | |
except (IOError, OSError, ValueError): | |
pass | |
else: | |
tz = None | |
if tzwin is not None: | |
try: | |
tz = tzwin(name) | |
except (WindowsError, UnicodeEncodeError): | |
# UnicodeEncodeError is for Python 2.7 compat | |
tz = None | |
if not tz: | |
from dateutil.zoneinfo import get_zonefile_instance | |
tz = get_zonefile_instance().get(name) | |
if not tz: | |
for c in name: | |
# name is not a tzstr unless it has at least | |
# one offset. For short values of "name", an | |
# explicit for loop seems to be the fastest way | |
# To determine if a string contains a digit | |
if c in "0123456789": | |
try: | |
tz = tzstr(name) | |
except ValueError: | |
pass | |
break | |
else: | |
if name in ("GMT", "UTC"): | |
tz = UTC | |
elif name in time.tzname: | |
tz = tzlocal() | |
return tz | |
return GettzFunc() | |
gettz = __get_gettz() | |
del __get_gettz | |
def datetime_exists(dt, tz=None): | |
""" | |
Given a datetime and a time zone, determine whether or not a given datetime | |
would fall in a gap. | |
:param dt: | |
A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` | |
is provided.) | |
:param tz: | |
A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If | |
``None`` or not provided, the datetime's own time zone will be used. | |
:return: | |
Returns a boolean value whether or not the "wall time" exists in | |
``tz``. | |
.. versionadded:: 2.7.0 | |
""" | |
if tz is None: | |
if dt.tzinfo is None: | |
raise ValueError('Datetime is naive and no time zone provided.') | |
tz = dt.tzinfo | |
dt = dt.replace(tzinfo=None) | |
# This is essentially a test of whether or not the datetime can survive | |
# a round trip to UTC. | |
dt_rt = dt.replace(tzinfo=tz).astimezone(UTC).astimezone(tz) | |
dt_rt = dt_rt.replace(tzinfo=None) | |
return dt == dt_rt | |
def datetime_ambiguous(dt, tz=None): | |
""" | |
Given a datetime and a time zone, determine whether or not a given datetime | |
is ambiguous (i.e if there are two times differentiated only by their DST | |
status). | |
:param dt: | |
A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` | |
is provided.) | |
:param tz: | |
A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If | |
``None`` or not provided, the datetime's own time zone will be used. | |
:return: | |
Returns a boolean value whether or not the "wall time" is ambiguous in | |
``tz``. | |
.. versionadded:: 2.6.0 | |
""" | |
if tz is None: | |
if dt.tzinfo is None: | |
raise ValueError('Datetime is naive and no time zone provided.') | |
tz = dt.tzinfo | |
# If a time zone defines its own "is_ambiguous" function, we'll use that. | |
is_ambiguous_fn = getattr(tz, 'is_ambiguous', None) | |
if is_ambiguous_fn is not None: | |
try: | |
return tz.is_ambiguous(dt) | |
except Exception: | |
pass | |
# If it doesn't come out and tell us it's ambiguous, we'll just check if | |
# the fold attribute has any effect on this particular date and time. | |
dt = dt.replace(tzinfo=tz) | |
wall_0 = enfold(dt, fold=0) | |
wall_1 = enfold(dt, fold=1) | |
same_offset = wall_0.utcoffset() == wall_1.utcoffset() | |
same_dst = wall_0.dst() == wall_1.dst() | |
return not (same_offset and same_dst) | |
def resolve_imaginary(dt): | |
""" | |
Given a datetime that may be imaginary, return an existing datetime. | |
This function assumes that an imaginary datetime represents what the | |
wall time would be in a zone had the offset transition not occurred, so | |
it will always fall forward by the transition's change in offset. | |
.. doctest:: | |
>>> from dateutil import tz | |
>>> from datetime import datetime | |
>>> NYC = tz.gettz('America/New_York') | |
>>> print(tz.resolve_imaginary(datetime(2017, 3, 12, 2, 30, tzinfo=NYC))) | |
2017-03-12 03:30:00-04:00 | |
>>> KIR = tz.gettz('Pacific/Kiritimati') | |
>>> print(tz.resolve_imaginary(datetime(1995, 1, 1, 12, 30, tzinfo=KIR))) | |
1995-01-02 12:30:00+14:00 | |
As a note, :func:`datetime.astimezone` is guaranteed to produce a valid, | |
existing datetime, so a round-trip to and from UTC is sufficient to get | |
an extant datetime, however, this generally "falls back" to an earlier time | |
rather than falling forward to the STD side (though no guarantees are made | |
about this behavior). | |
:param dt: | |
A :class:`datetime.datetime` which may or may not exist. | |
:return: | |
Returns an existing :class:`datetime.datetime`. If ``dt`` was not | |
imaginary, the datetime returned is guaranteed to be the same object | |
passed to the function. | |
.. versionadded:: 2.7.0 | |
""" | |
if dt.tzinfo is not None and not datetime_exists(dt): | |
curr_offset = (dt + datetime.timedelta(hours=24)).utcoffset() | |
old_offset = (dt - datetime.timedelta(hours=24)).utcoffset() | |
dt += curr_offset - old_offset | |
return dt | |
def _datetime_to_timestamp(dt): | |
""" | |
Convert a :class:`datetime.datetime` object to an epoch timestamp in | |
seconds since January 1, 1970, ignoring the time zone. | |
""" | |
return (dt.replace(tzinfo=None) - EPOCH).total_seconds() | |
if sys.version_info >= (3, 6): | |
def _get_supported_offset(second_offset): | |
return second_offset | |
else: | |
def _get_supported_offset(second_offset): | |
# For python pre-3.6, round to full-minutes if that's not the case. | |
# Python's datetime doesn't accept sub-minute timezones. Check | |
# http://python.org/sf/1447945 or https://bugs.python.org/issue5288 | |
# for some information. | |
old_offset = second_offset | |
calculated_offset = 60 * ((second_offset + 30) // 60) | |
return calculated_offset | |
try: | |
# Python 3.7 feature | |
from contextlib import nullcontext as _nullcontext | |
except ImportError: | |
class _nullcontext(object): | |
""" | |
Class for wrapping contexts so that they are passed through in a | |
with statement. | |
""" | |
def __init__(self, context): | |
self.context = context | |
def __enter__(self): | |
return self.context | |
def __exit__(*args, **kwargs): | |
pass | |
# vim:ts=4:sw=4:et | |