420 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			420 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from six import PY2
 | |
| 
 | |
| from functools import wraps
 | |
| 
 | |
| from datetime import datetime, timedelta, tzinfo
 | |
| 
 | |
| 
 | |
| ZERO = timedelta(0)
 | |
| 
 | |
| __all__ = ['tzname_in_python2', 'enfold']
 | |
| 
 | |
| 
 | |
| def tzname_in_python2(namefunc):
 | |
|     """Change unicode output into bytestrings in Python 2
 | |
| 
 | |
|     tzname() API changed in Python 3. It used to return bytes, but was changed
 | |
|     to unicode strings
 | |
|     """
 | |
|     if PY2:
 | |
|         @wraps(namefunc)
 | |
|         def adjust_encoding(*args, **kwargs):
 | |
|             name = namefunc(*args, **kwargs)
 | |
|             if name is not None:
 | |
|                 name = name.encode()
 | |
| 
 | |
|             return name
 | |
| 
 | |
|         return adjust_encoding
 | |
|     else:
 | |
|         return namefunc
 | |
| 
 | |
| 
 | |
| # The following is adapted from Alexander Belopolsky's tz library
 | |
| # https://github.com/abalkin/tz
 | |
| if hasattr(datetime, 'fold'):
 | |
|     # This is the pre-python 3.6 fold situation
 | |
|     def enfold(dt, fold=1):
 | |
|         """
 | |
|         Provides a unified interface for assigning the ``fold`` attribute to
 | |
|         datetimes both before and after the implementation of PEP-495.
 | |
| 
 | |
|         :param fold:
 | |
|             The value for the ``fold`` attribute in the returned datetime. This
 | |
|             should be either 0 or 1.
 | |
| 
 | |
|         :return:
 | |
|             Returns an object for which ``getattr(dt, 'fold', 0)`` returns
 | |
|             ``fold`` for all versions of Python. In versions prior to
 | |
|             Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
 | |
|             subclass of :py:class:`datetime.datetime` with the ``fold``
 | |
|             attribute added, if ``fold`` is 1.
 | |
| 
 | |
|         .. versionadded:: 2.6.0
 | |
|         """
 | |
|         return dt.replace(fold=fold)
 | |
| 
 | |
| else:
 | |
|     class _DatetimeWithFold(datetime):
 | |
|         """
 | |
|         This is a class designed to provide a PEP 495-compliant interface for
 | |
|         Python versions before 3.6. It is used only for dates in a fold, so
 | |
|         the ``fold`` attribute is fixed at ``1``.
 | |
| 
 | |
|         .. versionadded:: 2.6.0
 | |
|         """
 | |
|         __slots__ = ()
 | |
| 
 | |
|         def replace(self, *args, **kwargs):
 | |
|             """
 | |
|             Return a datetime with the same attributes, except for those
 | |
|             attributes given new values by whichever keyword arguments are
 | |
|             specified. Note that tzinfo=None can be specified to create a naive
 | |
|             datetime from an aware datetime with no conversion of date and time
 | |
|             data.
 | |
| 
 | |
|             This is reimplemented in ``_DatetimeWithFold`` because pypy3 will
 | |
|             return a ``datetime.datetime`` even if ``fold`` is unchanged.
 | |
|             """
 | |
|             argnames = (
 | |
|                 'year', 'month', 'day', 'hour', 'minute', 'second',
 | |
|                 'microsecond', 'tzinfo'
 | |
|             )
 | |
| 
 | |
|             for arg, argname in zip(args, argnames):
 | |
|                 if argname in kwargs:
 | |
|                     raise TypeError('Duplicate argument: {}'.format(argname))
 | |
| 
 | |
|                 kwargs[argname] = arg
 | |
| 
 | |
|             for argname in argnames:
 | |
|                 if argname not in kwargs:
 | |
|                     kwargs[argname] = getattr(self, argname)
 | |
| 
 | |
|             dt_class = self.__class__ if kwargs.get('fold', 1) else datetime
 | |
| 
 | |
|             return dt_class(**kwargs)
 | |
| 
 | |
|         @property
 | |
|         def fold(self):
 | |
|             return 1
 | |
| 
 | |
|     def enfold(dt, fold=1):
 | |
|         """
 | |
|         Provides a unified interface for assigning the ``fold`` attribute to
 | |
|         datetimes both before and after the implementation of PEP-495.
 | |
| 
 | |
|         :param fold:
 | |
|             The value for the ``fold`` attribute in the returned datetime. This
 | |
|             should be either 0 or 1.
 | |
| 
 | |
|         :return:
 | |
|             Returns an object for which ``getattr(dt, 'fold', 0)`` returns
 | |
|             ``fold`` for all versions of Python. In versions prior to
 | |
|             Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
 | |
|             subclass of :py:class:`datetime.datetime` with the ``fold``
 | |
|             attribute added, if ``fold`` is 1.
 | |
| 
 | |
|         .. versionadded:: 2.6.0
 | |
|         """
 | |
|         if getattr(dt, 'fold', 0) == fold:
 | |
|             return dt
 | |
| 
 | |
|         args = dt.timetuple()[:6]
 | |
|         args += (dt.microsecond, dt.tzinfo)
 | |
| 
 | |
|         if fold:
 | |
|             return _DatetimeWithFold(*args)
 | |
|         else:
 | |
|             return datetime(*args)
 | |
| 
 | |
| 
 | |
| def _validate_fromutc_inputs(f):
 | |
|     """
 | |
|     The CPython version of ``fromutc`` checks that the input is a ``datetime``
 | |
|     object and that ``self`` is attached as its ``tzinfo``.
 | |
|     """
 | |
|     @wraps(f)
 | |
|     def fromutc(self, dt):
 | |
|         if not isinstance(dt, datetime):
 | |
|             raise TypeError("fromutc() requires a datetime argument")
 | |
|         if dt.tzinfo is not self:
 | |
|             raise ValueError("dt.tzinfo is not self")
 | |
| 
 | |
|         return f(self, dt)
 | |
| 
 | |
|     return fromutc
 | |
| 
 | |
| 
 | |
| class _tzinfo(tzinfo):
 | |
|     """
 | |
|     Base class for all ``dateutil`` ``tzinfo`` objects.
 | |
|     """
 | |
| 
 | |
|     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
 | |
|         """
 | |
| 
 | |
|         dt = dt.replace(tzinfo=self)
 | |
| 
 | |
|         wall_0 = enfold(dt, fold=0)
 | |
|         wall_1 = enfold(dt, fold=1)
 | |
| 
 | |
|         same_offset = wall_0.utcoffset() == wall_1.utcoffset()
 | |
|         same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None)
 | |
| 
 | |
|         return same_dt and not same_offset
 | |
| 
 | |
|     def _fold_status(self, dt_utc, dt_wall):
 | |
|         """
 | |
|         Determine the fold status of a "wall" datetime, given a representation
 | |
|         of the same datetime as a (naive) UTC datetime. This is calculated based
 | |
|         on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all
 | |
|         datetimes, and that this offset is the actual number of hours separating
 | |
|         ``dt_utc`` and ``dt_wall``.
 | |
| 
 | |
|         :param dt_utc:
 | |
|             Representation of the datetime as UTC
 | |
| 
 | |
|         :param dt_wall:
 | |
|             Representation of the datetime as "wall time". This parameter must
 | |
|             either have a `fold` attribute or have a fold-naive
 | |
|             :class:`datetime.tzinfo` attached, otherwise the calculation may
 | |
|             fail.
 | |
|         """
 | |
|         if self.is_ambiguous(dt_wall):
 | |
|             delta_wall = dt_wall - dt_utc
 | |
|             _fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst()))
 | |
|         else:
 | |
|             _fold = 0
 | |
| 
 | |
|         return _fold
 | |
| 
 | |
|     def _fold(self, dt):
 | |
|         return getattr(dt, 'fold', 0)
 | |
| 
 | |
|     def _fromutc(self, dt):
 | |
|         """
 | |
|         Given a timezone-aware datetime in a given timezone, calculates a
 | |
|         timezone-aware datetime in a new timezone.
 | |
| 
 | |
|         Since this is the one time that we *know* we have an unambiguous
 | |
|         datetime object, we take this opportunity to determine whether the
 | |
|         datetime is ambiguous and in a "fold" state (e.g. if it's the first
 | |
|         occurrence, chronologically, of the ambiguous datetime).
 | |
| 
 | |
|         :param dt:
 | |
|             A timezone-aware :class:`datetime.datetime` object.
 | |
|         """
 | |
| 
 | |
|         # Re-implement the algorithm from Python's datetime.py
 | |
|         dtoff = dt.utcoffset()
 | |
|         if dtoff is None:
 | |
|             raise ValueError("fromutc() requires a non-None utcoffset() "
 | |
|                              "result")
 | |
| 
 | |
|         # The original datetime.py code assumes that `dst()` defaults to
 | |
|         # zero during ambiguous times. PEP 495 inverts this presumption, so
 | |
|         # for pre-PEP 495 versions of python, we need to tweak the algorithm.
 | |
|         dtdst = dt.dst()
 | |
|         if dtdst is None:
 | |
|             raise ValueError("fromutc() requires a non-None dst() result")
 | |
|         delta = dtoff - dtdst
 | |
| 
 | |
|         dt += delta
 | |
|         # Set fold=1 so we can default to being in the fold for
 | |
|         # ambiguous dates.
 | |
|         dtdst = enfold(dt, fold=1).dst()
 | |
|         if dtdst is None:
 | |
|             raise ValueError("fromutc(): dt.dst gave inconsistent "
 | |
|                              "results; cannot convert")
 | |
|         return dt + dtdst
 | |
| 
 | |
|     @_validate_fromutc_inputs
 | |
|     def fromutc(self, dt):
 | |
|         """
 | |
|         Given a timezone-aware datetime in a given timezone, calculates a
 | |
|         timezone-aware datetime in a new timezone.
 | |
| 
 | |
|         Since this is the one time that we *know* we have an unambiguous
 | |
|         datetime object, we take this opportunity to determine whether the
 | |
|         datetime is ambiguous and in a "fold" state (e.g. if it's the first
 | |
|         occurrence, chronologically, of the ambiguous datetime).
 | |
| 
 | |
|         :param dt:
 | |
|             A timezone-aware :class:`datetime.datetime` object.
 | |
|         """
 | |
|         dt_wall = self._fromutc(dt)
 | |
| 
 | |
|         # Calculate the fold status given the two datetimes.
 | |
|         _fold = self._fold_status(dt, dt_wall)
 | |
| 
 | |
|         # Set the default fold value for ambiguous dates
 | |
|         return enfold(dt_wall, fold=_fold)
 | |
| 
 | |
| 
 | |
| class tzrangebase(_tzinfo):
 | |
|     """
 | |
|     This is an abstract base class for time zones represented by an annual
 | |
|     transition into and out of DST. Child classes should implement the following
 | |
|     methods:
 | |
| 
 | |
|         * ``__init__(self, *args, **kwargs)``
 | |
|         * ``transitions(self, year)`` - this is expected to return a tuple of
 | |
|           datetimes representing the DST on and off transitions in standard
 | |
|           time.
 | |
| 
 | |
|     A fully initialized ``tzrangebase`` subclass should also provide the
 | |
|     following attributes:
 | |
|         * ``hasdst``: Boolean whether or not the zone uses DST.
 | |
|         * ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects
 | |
|           representing the respective UTC offsets.
 | |
|         * ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short
 | |
|           abbreviations in DST and STD, respectively.
 | |
|         * ``_hasdst``: Whether or not the zone has DST.
 | |
| 
 | |
|     .. versionadded:: 2.6.0
 | |
|     """
 | |
|     def __init__(self):
 | |
|         raise NotImplementedError('tzrangebase is an abstract base class')
 | |
| 
 | |
|     def utcoffset(self, dt):
 | |
|         isdst = self._isdst(dt)
 | |
| 
 | |
|         if isdst is None:
 | |
|             return None
 | |
|         elif isdst:
 | |
|             return self._dst_offset
 | |
|         else:
 | |
|             return self._std_offset
 | |
| 
 | |
|     def dst(self, dt):
 | |
|         isdst = self._isdst(dt)
 | |
| 
 | |
|         if isdst is None:
 | |
|             return None
 | |
|         elif isdst:
 | |
|             return self._dst_base_offset
 | |
|         else:
 | |
|             return ZERO
 | |
| 
 | |
|     @tzname_in_python2
 | |
|     def tzname(self, dt):
 | |
|         if self._isdst(dt):
 | |
|             return self._dst_abbr
 | |
|         else:
 | |
|             return self._std_abbr
 | |
| 
 | |
|     def fromutc(self, dt):
 | |
|         """ Given a datetime in UTC, return local time """
 | |
|         if not isinstance(dt, datetime):
 | |
|             raise TypeError("fromutc() requires a datetime argument")
 | |
| 
 | |
|         if dt.tzinfo is not self:
 | |
|             raise ValueError("dt.tzinfo is not self")
 | |
| 
 | |
|         # Get transitions - if there are none, fixed offset
 | |
|         transitions = self.transitions(dt.year)
 | |
|         if transitions is None:
 | |
|             return dt + self.utcoffset(dt)
 | |
| 
 | |
|         # Get the transition times in UTC
 | |
|         dston, dstoff = transitions
 | |
| 
 | |
|         dston -= self._std_offset
 | |
|         dstoff -= self._std_offset
 | |
| 
 | |
|         utc_transitions = (dston, dstoff)
 | |
|         dt_utc = dt.replace(tzinfo=None)
 | |
| 
 | |
|         isdst = self._naive_isdst(dt_utc, utc_transitions)
 | |
| 
 | |
|         if isdst:
 | |
|             dt_wall = dt + self._dst_offset
 | |
|         else:
 | |
|             dt_wall = dt + self._std_offset
 | |
| 
 | |
|         _fold = int(not isdst and self.is_ambiguous(dt_wall))
 | |
| 
 | |
|         return enfold(dt_wall, fold=_fold)
 | |
| 
 | |
|     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
 | |
|         """
 | |
|         if not self.hasdst:
 | |
|             return False
 | |
| 
 | |
|         start, end = self.transitions(dt.year)
 | |
| 
 | |
|         dt = dt.replace(tzinfo=None)
 | |
|         return (end <= dt < end + self._dst_base_offset)
 | |
| 
 | |
|     def _isdst(self, dt):
 | |
|         if not self.hasdst:
 | |
|             return False
 | |
|         elif dt is None:
 | |
|             return None
 | |
| 
 | |
|         transitions = self.transitions(dt.year)
 | |
| 
 | |
|         if transitions is None:
 | |
|             return False
 | |
| 
 | |
|         dt = dt.replace(tzinfo=None)
 | |
| 
 | |
|         isdst = self._naive_isdst(dt, transitions)
 | |
| 
 | |
|         # Handle ambiguous dates
 | |
|         if not isdst and self.is_ambiguous(dt):
 | |
|             return not self._fold(dt)
 | |
|         else:
 | |
|             return isdst
 | |
| 
 | |
|     def _naive_isdst(self, dt, transitions):
 | |
|         dston, dstoff = transitions
 | |
| 
 | |
|         dt = dt.replace(tzinfo=None)
 | |
| 
 | |
|         if dston < dstoff:
 | |
|             isdst = dston <= dt < dstoff
 | |
|         else:
 | |
|             isdst = not dstoff <= dt < dston
 | |
| 
 | |
|         return isdst
 | |
| 
 | |
|     @property
 | |
|     def _dst_base_offset(self):
 | |
|         return self._dst_offset - self._std_offset
 | |
| 
 | |
|     __hash__ = None
 | |
| 
 | |
|     def __ne__(self, other):
 | |
|         return not (self == other)
 | |
| 
 | |
|     def __repr__(self):
 | |
|         return "%s(...)" % self.__class__.__name__
 | |
| 
 | |
|     __reduce__ = object.__reduce__
 |