You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
297 lines
9.2 KiB
297 lines
9.2 KiB
3 years ago
|
# -*- coding: utf-8 -*-
|
||
|
import warnings
|
||
|
from datetime import tzinfo
|
||
|
|
||
|
from . import _compat
|
||
|
from ._exceptions import (
|
||
|
AmbiguousTimeError,
|
||
|
NonExistentTimeError,
|
||
|
PytzUsageWarning,
|
||
|
UnknownTimeZoneError,
|
||
|
get_exception,
|
||
|
)
|
||
|
|
||
|
IS_DST_SENTINEL = object()
|
||
|
KEY_SENTINEL = object()
|
||
|
|
||
|
|
||
|
def timezone(key, _cache={}):
|
||
|
"""Builds an IANA database time zone shim.
|
||
|
|
||
|
This is the equivalent of ``pytz.timezone``.
|
||
|
|
||
|
:param key:
|
||
|
A valid key from the IANA time zone database.
|
||
|
|
||
|
:raises UnknownTimeZoneError:
|
||
|
If an unknown value is passed, this will raise an exception that can be
|
||
|
caught by :exc:`pytz_deprecation_shim.UnknownTimeZoneError` or
|
||
|
``pytz.UnknownTimeZoneError``. Like
|
||
|
:exc:`zoneinfo.ZoneInfoNotFoundError`, both of those are subclasses of
|
||
|
:exc:`KeyError`.
|
||
|
"""
|
||
|
instance = _cache.get(key, None)
|
||
|
if instance is None:
|
||
|
if len(key) == 3 and key.lower() == "utc":
|
||
|
instance = _cache.setdefault(key, UTC)
|
||
|
else:
|
||
|
try:
|
||
|
zone = _compat.get_timezone(key)
|
||
|
except KeyError:
|
||
|
raise get_exception(UnknownTimeZoneError, key)
|
||
|
instance = _cache.setdefault(key, wrap_zone(zone, key=key))
|
||
|
|
||
|
return instance
|
||
|
|
||
|
|
||
|
def fixed_offset_timezone(offset, _cache={}):
|
||
|
"""Builds a fixed offset time zone shim.
|
||
|
|
||
|
This is the equivalent of ``pytz.FixedOffset``. An alias is available as
|
||
|
``pytz_deprecation_shim.FixedOffset`` as well.
|
||
|
|
||
|
:param offset:
|
||
|
A fixed offset from UTC, in minutes. This must be in the range ``-1439
|
||
|
<= offset <= 1439``.
|
||
|
|
||
|
:raises ValueError:
|
||
|
For offsets whose absolute value is greater than or equal to 24 hours.
|
||
|
|
||
|
:return:
|
||
|
A shim time zone.
|
||
|
"""
|
||
|
if not (-1440 < offset < 1440):
|
||
|
raise ValueError("absolute offset is too large", offset)
|
||
|
|
||
|
instance = _cache.get(offset, None)
|
||
|
if instance is None:
|
||
|
if offset == 0:
|
||
|
instance = _cache.setdefault(offset, UTC)
|
||
|
else:
|
||
|
zone = _compat.get_fixed_offset_zone(offset)
|
||
|
instance = _cache.setdefault(offset, wrap_zone(zone, key=None))
|
||
|
|
||
|
return instance
|
||
|
|
||
|
|
||
|
def build_tzinfo(zone, fp):
|
||
|
"""Builds a shim object from a TZif file.
|
||
|
|
||
|
This is a shim for ``pytz.build_tzinfo``. Given a value to use as the zone
|
||
|
IANA key and a file-like object containing a valid TZif file (i.e.
|
||
|
conforming to :rfc:`8536`), this builds a time zone object and wraps it in
|
||
|
a shim class.
|
||
|
|
||
|
The argument names are chosen to match those in ``pytz.build_tzinfo``.
|
||
|
|
||
|
:param zone:
|
||
|
A string to be used as the time zone object's IANA key.
|
||
|
|
||
|
:param fp:
|
||
|
A readable file-like object emitting bytes, pointing to a valid TZif
|
||
|
file.
|
||
|
|
||
|
:return:
|
||
|
A shim time zone.
|
||
|
"""
|
||
|
zone_file = _compat.get_timezone_file(fp)
|
||
|
|
||
|
return wrap_zone(zone_file, key=zone)
|
||
|
|
||
|
|
||
|
def wrap_zone(tz, key=KEY_SENTINEL, _cache={}):
|
||
|
"""Wrap an existing time zone object in a shim class.
|
||
|
|
||
|
This is likely to be useful if you would like to work internally with
|
||
|
non-``pytz`` zones, but you expose an interface to callers relying on
|
||
|
``pytz``'s interface. It may also be useful for passing non-``pytz`` zones
|
||
|
to libraries expecting to use ``pytz``'s interface.
|
||
|
|
||
|
:param tz:
|
||
|
A :pep:`495`-compatible time zone, such as those provided by
|
||
|
:mod:`dateutil.tz` or :mod:`zoneinfo`.
|
||
|
|
||
|
:param key:
|
||
|
The value for the IANA time zone key. This is optional for ``zoneinfo``
|
||
|
zones, but required for ``dateutil.tz`` zones.
|
||
|
|
||
|
:return:
|
||
|
A shim time zone.
|
||
|
"""
|
||
|
if key is KEY_SENTINEL:
|
||
|
key = getattr(tz, "key", KEY_SENTINEL)
|
||
|
|
||
|
if key is KEY_SENTINEL:
|
||
|
raise TypeError(
|
||
|
"The `key` argument is required when wrapping zones that do not "
|
||
|
+ "have a `key` attribute."
|
||
|
)
|
||
|
|
||
|
instance = _cache.get((id(tz), key), None)
|
||
|
if instance is None:
|
||
|
instance = _cache.setdefault((id(tz), key), _PytzShimTimezone(tz, key))
|
||
|
|
||
|
return instance
|
||
|
|
||
|
|
||
|
class _PytzShimTimezone(tzinfo):
|
||
|
# Add instance variables for _zone and _key because this will make error
|
||
|
# reporting with partially-initialized _BasePytzShimTimezone objects
|
||
|
# work better.
|
||
|
_zone = None
|
||
|
_key = None
|
||
|
|
||
|
def __init__(self, zone, key):
|
||
|
self._key = key
|
||
|
self._zone = zone
|
||
|
|
||
|
def utcoffset(self, dt):
|
||
|
return self._zone.utcoffset(dt)
|
||
|
|
||
|
def dst(self, dt):
|
||
|
return self._zone.dst(dt)
|
||
|
|
||
|
def tzname(self, dt):
|
||
|
return self._zone.tzname(dt)
|
||
|
|
||
|
def fromutc(self, dt):
|
||
|
# The default fromutc implementation only works if tzinfo is "self"
|
||
|
dt_base = dt.replace(tzinfo=self._zone)
|
||
|
dt_out = self._zone.fromutc(dt_base)
|
||
|
|
||
|
return dt_out.replace(tzinfo=self)
|
||
|
|
||
|
def __str__(self):
|
||
|
if self._key is not None:
|
||
|
return str(self._key)
|
||
|
else:
|
||
|
return repr(self)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "%s(%s, %s)" % (
|
||
|
self.__class__.__name__,
|
||
|
repr(self._zone),
|
||
|
repr(self._key),
|
||
|
)
|
||
|
|
||
|
def unwrap_shim(self):
|
||
|
"""Returns the underlying class that the shim is a wrapper for.
|
||
|
|
||
|
This is a shim-specific method equivalent to
|
||
|
:func:`pytz_deprecation_shim.helpers.upgrade_tzinfo`. It is provided as
|
||
|
a method to allow end-users to upgrade shim timezones without requiring
|
||
|
an explicit dependency on ``pytz_deprecation_shim``, e.g.:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
if getattr(tz, "unwrap_shim", None) is None:
|
||
|
tz = tz.unwrap_shim()
|
||
|
"""
|
||
|
return self._zone
|
||
|
|
||
|
@property
|
||
|
def zone(self):
|
||
|
warnings.warn(
|
||
|
"The zone attribute is specific to pytz's interface; "
|
||
|
+ "please migrate to a new time zone provider. "
|
||
|
+ "For more details on how to do so, see %s"
|
||
|
% PYTZ_MIGRATION_GUIDE_URL,
|
||
|
PytzUsageWarning,
|
||
|
stacklevel=2,
|
||
|
)
|
||
|
|
||
|
return self._key
|
||
|
|
||
|
def localize(self, dt, is_dst=IS_DST_SENTINEL):
|
||
|
warnings.warn(
|
||
|
"The localize method is no longer necessary, as this "
|
||
|
+ "time zone supports the fold attribute (PEP 495). "
|
||
|
+ "For more details on migrating to a PEP 495-compliant "
|
||
|
+ "implementation, see %s" % PYTZ_MIGRATION_GUIDE_URL,
|
||
|
PytzUsageWarning,
|
||
|
stacklevel=2,
|
||
|
)
|
||
|
|
||
|
if dt.tzinfo is not None:
|
||
|
raise ValueError("Not naive datetime (tzinfo is already set)")
|
||
|
|
||
|
dt_out = dt.replace(tzinfo=self)
|
||
|
|
||
|
if is_dst is IS_DST_SENTINEL:
|
||
|
return dt_out
|
||
|
|
||
|
dt_ambiguous = _compat.is_ambiguous(dt_out)
|
||
|
dt_imaginary = (
|
||
|
_compat.is_imaginary(dt_out) if not dt_ambiguous else False
|
||
|
)
|
||
|
|
||
|
if is_dst is None:
|
||
|
if dt_imaginary:
|
||
|
raise get_exception(
|
||
|
NonExistentTimeError, dt.replace(tzinfo=None)
|
||
|
)
|
||
|
|
||
|
if dt_ambiguous:
|
||
|
raise get_exception(AmbiguousTimeError, dt.replace(tzinfo=None))
|
||
|
|
||
|
elif dt_ambiguous or dt_imaginary:
|
||
|
# Start by normalizing the folds; dt_out may have fold=0 or fold=1,
|
||
|
# but we need to know the DST offset on both sides anyway, so we
|
||
|
# will get one datetime representing each side of the fold, then
|
||
|
# decide which one we're going to return.
|
||
|
if _compat.get_fold(dt_out):
|
||
|
dt_enfolded = dt_out
|
||
|
dt_out = _compat.enfold(dt_out, fold=0)
|
||
|
else:
|
||
|
dt_enfolded = _compat.enfold(dt_out, fold=1)
|
||
|
|
||
|
# Now we want to decide whether the fold=0 or fold=1 represents
|
||
|
# what pytz would return for `is_dst=True`
|
||
|
enfolded_dst = bool(dt_enfolded.dst())
|
||
|
if bool(dt_out.dst()) == enfolded_dst:
|
||
|
# If this is not a transition between standard time and
|
||
|
# daylight saving time, pytz will consider the larger offset
|
||
|
# the DST offset.
|
||
|
enfolded_dst = dt_enfolded.utcoffset() > dt_out.utcoffset()
|
||
|
|
||
|
# The default we've established is that dt_out is fold=0; swap it
|
||
|
# for the fold=1 datetime if is_dst == True and the enfolded side
|
||
|
# is DST or if is_dst == False and the enfolded side is *not* DST.
|
||
|
if is_dst == enfolded_dst:
|
||
|
dt_out = dt_enfolded
|
||
|
|
||
|
return dt_out
|
||
|
|
||
|
def normalize(self, dt):
|
||
|
warnings.warn(
|
||
|
"The normalize method is no longer necessary, as this "
|
||
|
+ "time zone supports the fold attribute (PEP 495). "
|
||
|
+ "For more details on migrating to a PEP 495-compliant "
|
||
|
+ "implementation, see %s" % PYTZ_MIGRATION_GUIDE_URL,
|
||
|
PytzUsageWarning,
|
||
|
stacklevel=2,
|
||
|
)
|
||
|
|
||
|
if dt.tzinfo is None:
|
||
|
raise ValueError("Naive time - no tzinfo set")
|
||
|
|
||
|
if dt.tzinfo is self:
|
||
|
return dt
|
||
|
|
||
|
return dt.astimezone(self)
|
||
|
|
||
|
def __copy__(self):
|
||
|
return self
|
||
|
|
||
|
def __deepcopy__(self, memo=None):
|
||
|
return self
|
||
|
|
||
|
def __reduce__(self):
|
||
|
return wrap_zone, (self._zone, self._key)
|
||
|
|
||
|
|
||
|
UTC = wrap_zone(_compat.UTC, "UTC")
|
||
|
PYTZ_MIGRATION_GUIDE_URL = (
|
||
|
"https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html"
|
||
|
)
|