diff env/lib/python3.9/site-packages/boltons/timeutils.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/env/lib/python3.9/site-packages/boltons/timeutils.py	Mon Mar 22 18:12:50 2021 +0000
@@ -0,0 +1,548 @@
+# -*- coding: utf-8 -*-
+"""Python's :mod:`datetime` module provides some of the most complex
+and powerful primitives in the Python standard library. Time is
+nontrivial, but thankfully its support is first-class in
+Python. ``dateutils`` provides some additional tools for working with
+time.
+
+Additionally, timeutils provides a few basic utilities for working
+with timezones in Python. The Python :mod:`datetime` module's
+documentation describes how to create a
+:class:`~datetime.datetime`-compatible :class:`~datetime.tzinfo`
+subtype. It even provides a few examples.
+
+The following module defines usable forms of the timezones in those
+docs, as well as a couple other useful ones, :data:`UTC` (aka GMT) and
+:data:`LocalTZ` (representing the local timezone as configured in the
+operating system). For timezones beyond these, as well as a higher
+degree of accuracy in corner cases, check out `pytz`_ and `dateutil`_.
+
+.. _pytz: https://pypi.python.org/pypi/pytz
+.. _dateutil: https://dateutil.readthedocs.io/en/stable/index.html
+"""
+
+import re
+import time
+import bisect
+import operator
+from datetime import tzinfo, timedelta, date, datetime
+
+
+def total_seconds(td):
+    """For those with older versions of Python, a pure-Python
+    implementation of Python 2.7's :meth:`~datetime.timedelta.total_seconds`.
+
+    Args:
+        td (datetime.timedelta): The timedelta to convert to seconds.
+    Returns:
+        float: total number of seconds
+
+    >>> td = timedelta(days=4, seconds=33)
+    >>> total_seconds(td)
+    345633.0
+    """
+    a_milli = 1000000.0
+    td_ds = td.seconds + (td.days * 86400)  # 24 * 60 * 60
+    td_micro = td.microseconds + (td_ds * a_milli)
+    return td_micro / a_milli
+
+
+def dt_to_timestamp(dt):
+    """Converts from a :class:`~datetime.datetime` object to an integer
+    timestamp, suitable interoperation with :func:`time.time` and
+    other `Epoch-based timestamps`.
+
+    .. _Epoch-based timestamps: https://en.wikipedia.org/wiki/Unix_time
+
+    >>> abs(round(time.time() - dt_to_timestamp(datetime.utcnow()), 2))
+    0.0
+
+    ``dt_to_timestamp`` supports both timezone-aware and naïve
+    :class:`~datetime.datetime` objects. Note that it assumes naïve
+    datetime objects are implied UTC, such as those generated with
+    :meth:`datetime.datetime.utcnow`. If your datetime objects are
+    local time, such as those generated with
+    :meth:`datetime.datetime.now`, first convert it using the
+    :meth:`datetime.datetime.replace` method with ``tzinfo=``
+    :class:`LocalTZ` object in this module, then pass the result of
+    that to ``dt_to_timestamp``.
+    """
+    if dt.tzinfo:
+        td = dt - EPOCH_AWARE
+    else:
+        td = dt - EPOCH_NAIVE
+    return total_seconds(td)
+
+
+_NONDIGIT_RE = re.compile(r'\D')
+
+
+def isoparse(iso_str):
+    """Parses the limited subset of `ISO8601-formatted time`_ strings as
+    returned by :meth:`datetime.datetime.isoformat`.
+
+    >>> epoch_dt = datetime.utcfromtimestamp(0)
+    >>> iso_str = epoch_dt.isoformat()
+    >>> print(iso_str)
+    1970-01-01T00:00:00
+    >>> isoparse(iso_str)
+    datetime.datetime(1970, 1, 1, 0, 0)
+
+    >>> utcnow = datetime.utcnow()
+    >>> utcnow == isoparse(utcnow.isoformat())
+    True
+
+    For further datetime parsing, see the `iso8601`_ package for strict
+    ISO parsing and `dateutil`_ package for loose parsing and more.
+
+    .. _ISO8601-formatted time: https://en.wikipedia.org/wiki/ISO_8601
+    .. _iso8601: https://pypi.python.org/pypi/iso8601
+    .. _dateutil: https://pypi.python.org/pypi/python-dateutil
+
+    """
+    dt_args = [int(p) for p in _NONDIGIT_RE.split(iso_str)]
+    return datetime(*dt_args)
+
+
+_BOUNDS = [(0, timedelta(seconds=1), 'second'),
+           (1, timedelta(seconds=60), 'minute'),
+           (1, timedelta(seconds=3600), 'hour'),
+           (1, timedelta(days=1), 'day'),
+           (1, timedelta(days=7), 'week'),
+           (2, timedelta(days=30), 'month'),
+           (1, timedelta(days=365), 'year')]
+_BOUNDS = [(b[0] * b[1], b[1], b[2]) for b in _BOUNDS]
+_BOUND_DELTAS = [b[0] for b in _BOUNDS]
+
+_FLOAT_PATTERN = r'[+-]?\ *(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?'
+_PARSE_TD_RE = re.compile(r"((?P<value>%s)\s*(?P<unit>\w)\w*)" % _FLOAT_PATTERN)
+_PARSE_TD_KW_MAP = dict([(unit[0], unit + 's')
+                         for _, _, unit in reversed(_BOUNDS[:-2])])
+
+
+def parse_timedelta(text):
+    """Robustly parses a short text description of a time period into a
+    :class:`datetime.timedelta`. Supports weeks, days, hours, minutes,
+    and seconds, with or without decimal points:
+
+    Args:
+        text (str): Text to parse.
+    Returns:
+        datetime.timedelta
+    Raises:
+        ValueError: on parse failure.
+
+    >>> parse_td('1d 2h 3.5m 0s') == timedelta(days=1, seconds=7410)
+    True
+
+    Also supports full words and whitespace.
+
+    >>> parse_td('2 weeks 1 day') == timedelta(days=15)
+    True
+
+    Negative times are supported, too:
+
+    >>> parse_td('-1.5 weeks 3m 20s') == timedelta(days=-11, seconds=43400)
+    True
+    """
+    td_kwargs = {}
+    for match in _PARSE_TD_RE.finditer(text):
+        value, unit = match.group('value'), match.group('unit')
+        try:
+            unit_key = _PARSE_TD_KW_MAP[unit]
+        except KeyError:
+            raise ValueError('invalid time unit %r, expected one of %r'
+                             % (unit, _PARSE_TD_KW_MAP.keys()))
+        try:
+            value = float(value)
+        except ValueError:
+            raise ValueError('invalid time value for unit %r: %r'
+                             % (unit, value))
+        td_kwargs[unit_key] = value
+    return timedelta(**td_kwargs)
+
+
+parse_td = parse_timedelta  # legacy alias
+
+
+def _cardinalize_time_unit(unit, value):
+    # removes dependency on strutils; nice and simple because
+    # all time units cardinalize normally
+    if value == 1:
+        return unit
+    return unit + 's'
+
+
+def decimal_relative_time(d, other=None, ndigits=0, cardinalize=True):
+    """Get a tuple representing the relative time difference between two
+    :class:`~datetime.datetime` objects or one
+    :class:`~datetime.datetime` and now.
+
+    Args:
+        d (datetime): The first datetime object.
+        other (datetime): An optional second datetime object. If
+            unset, defaults to the current time as determined
+            :meth:`datetime.utcnow`.
+        ndigits (int): The number of decimal digits to round to,
+            defaults to ``0``.
+        cardinalize (bool): Whether to pluralize the time unit if
+            appropriate, defaults to ``True``.
+    Returns:
+        (float, str): A tuple of the :class:`float` difference and
+           respective unit of time, pluralized if appropriate and
+           *cardinalize* is set to ``True``.
+
+    Unlike :func:`relative_time`, this method's return is amenable to
+    localization into other languages and custom phrasing and
+    formatting.
+
+    >>> now = datetime.utcnow()
+    >>> decimal_relative_time(now - timedelta(days=1, seconds=3600), now)
+    (1.0, 'day')
+    >>> decimal_relative_time(now - timedelta(seconds=0.002), now, ndigits=5)
+    (0.002, 'seconds')
+    >>> decimal_relative_time(now, now - timedelta(days=900), ndigits=1)
+    (-2.5, 'years')
+
+    """
+    if other is None:
+        other = datetime.utcnow()
+    diff = other - d
+    diff_seconds = total_seconds(diff)
+    abs_diff = abs(diff)
+    b_idx = bisect.bisect(_BOUND_DELTAS, abs_diff) - 1
+    bbound, bunit, bname = _BOUNDS[b_idx]
+    f_diff = diff_seconds / total_seconds(bunit)
+    rounded_diff = round(f_diff, ndigits)
+    if cardinalize:
+        return rounded_diff, _cardinalize_time_unit(bname, abs(rounded_diff))
+    return rounded_diff, bname
+
+
+def relative_time(d, other=None, ndigits=0):
+    """Get a string representation of the difference between two
+    :class:`~datetime.datetime` objects or one
+    :class:`~datetime.datetime` and the current time. Handles past and
+    future times.
+
+    Args:
+        d (datetime): The first datetime object.
+        other (datetime): An optional second datetime object. If
+            unset, defaults to the current time as determined
+            :meth:`datetime.utcnow`.
+        ndigits (int): The number of decimal digits to round to,
+            defaults to ``0``.
+    Returns:
+        A short English-language string.
+
+    >>> now = datetime.utcnow()
+    >>> relative_time(now, ndigits=1)
+    '0 seconds ago'
+    >>> relative_time(now - timedelta(days=1, seconds=36000), ndigits=1)
+    '1.4 days ago'
+    >>> relative_time(now + timedelta(days=7), now, ndigits=1)
+    '1 week from now'
+
+    """
+    drt, unit = decimal_relative_time(d, other, ndigits, cardinalize=True)
+    phrase = 'ago'
+    if drt < 0:
+        phrase = 'from now'
+    return '%g %s %s' % (abs(drt), unit, phrase)
+
+
+def strpdate(string, format):
+    """Parse the date string according to the format in `format`.  Returns a
+    :class:`date` object.  Internally, :meth:`datetime.strptime` is used to
+    parse the string and thus conversion specifiers for time fields (e.g. `%H`)
+    may be provided;  these will be parsed but ignored.
+
+    Args:
+        string (str): The date string to be parsed.
+        format (str): The `strptime`_-style date format string.
+    Returns:
+        datetime.date
+
+    .. _`strptime`: https://docs.python.org/2/library/datetime.html#strftime-strptime-behavior
+
+    >>> strpdate('2016-02-14', '%Y-%m-%d')
+    datetime.date(2016, 2, 14)
+    >>> strpdate('26/12 (2015)', '%d/%m (%Y)')
+    datetime.date(2015, 12, 26)
+    >>> strpdate('20151231 23:59:59', '%Y%m%d %H:%M:%S')
+    datetime.date(2015, 12, 31)
+    >>> strpdate('20160101 00:00:00.001', '%Y%m%d %H:%M:%S.%f')
+    datetime.date(2016, 1, 1)
+    """
+    whence = datetime.strptime(string, format)
+    return whence.date()
+
+
+def daterange(start, stop, step=1, inclusive=False):
+    """In the spirit of :func:`range` and :func:`xrange`, the `daterange`
+    generator that yields a sequence of :class:`~datetime.date`
+    objects, starting at *start*, incrementing by *step*, until *stop*
+    is reached.
+
+    When *inclusive* is True, the final date may be *stop*, **if**
+    *step* falls evenly on it. By default, *step* is one day. See
+    details below for many more details.
+
+    Args:
+        start (datetime.date): The starting date The first value in
+            the sequence.
+        stop (datetime.date): The stopping date. By default not
+            included in return. Can be `None` to yield an infinite
+            sequence.
+        step (int): The value to increment *start* by to reach
+            *stop*. Can be an :class:`int` number of days, a
+            :class:`datetime.timedelta`, or a :class:`tuple` of integers,
+            `(year, month, day)`. Positive and negative *step* values
+            are supported.
+        inclusive (bool): Whether or not the *stop* date can be
+            returned. *stop* is only returned when a *step* falls evenly
+            on it.
+
+    >>> christmas = date(year=2015, month=12, day=25)
+    >>> boxing_day = date(year=2015, month=12, day=26)
+    >>> new_year = date(year=2016, month=1,  day=1)
+    >>> for day in daterange(christmas, new_year):
+    ...     print(repr(day))
+    datetime.date(2015, 12, 25)
+    datetime.date(2015, 12, 26)
+    datetime.date(2015, 12, 27)
+    datetime.date(2015, 12, 28)
+    datetime.date(2015, 12, 29)
+    datetime.date(2015, 12, 30)
+    datetime.date(2015, 12, 31)
+    >>> for day in daterange(christmas, boxing_day):
+    ...     print(repr(day))
+    datetime.date(2015, 12, 25)
+    >>> for day in daterange(date(2017, 5, 1), date(2017, 8, 1),
+    ...                      step=(0, 1, 0), inclusive=True):
+    ...     print(repr(day))
+    datetime.date(2017, 5, 1)
+    datetime.date(2017, 6, 1)
+    datetime.date(2017, 7, 1)
+    datetime.date(2017, 8, 1)
+
+    *Be careful when using stop=None, as this will yield an infinite
+    sequence of dates.*
+    """
+    if not isinstance(start, date):
+        raise TypeError("start expected datetime.date instance")
+    if stop and not isinstance(stop, date):
+        raise TypeError("stop expected datetime.date instance or None")
+    try:
+        y_step, m_step, d_step = step
+    except TypeError:
+        y_step, m_step, d_step = 0, 0, step
+    else:
+        y_step, m_step = int(y_step), int(m_step)
+    if isinstance(d_step, int):
+        d_step = timedelta(days=int(d_step))
+    elif isinstance(d_step, timedelta):
+        pass
+    else:
+        raise ValueError('step expected int, timedelta, or tuple'
+                         ' (year, month, day), not: %r' % step)
+
+    if stop is None:
+        finished = lambda now, stop: False
+    elif start < stop:
+        finished = operator.gt if inclusive else operator.ge
+    else:
+        finished = operator.lt if inclusive else operator.le
+    now = start
+
+    while not finished(now, stop):
+        yield now
+        if y_step or m_step:
+            m_y_step, cur_month = divmod(now.month + m_step, 12)
+            now = now.replace(year=now.year + y_step + m_y_step,
+                              month=cur_month or 12)
+        now = now + d_step
+    return
+
+
+# Timezone support (brought in from tzutils)
+
+
+ZERO = timedelta(0)
+HOUR = timedelta(hours=1)
+
+
+class ConstantTZInfo(tzinfo):
+    """
+    A :class:`~datetime.tzinfo` subtype whose *offset* remains constant
+    (no daylight savings).
+
+    Args:
+        name (str): Name of the timezone.
+        offset (datetime.timedelta): Offset of the timezone.
+    """
+    def __init__(self, name="ConstantTZ", offset=ZERO):
+        self.name = name
+        self.offset = offset
+
+    @property
+    def utcoffset_hours(self):
+        return total_seconds(self.offset) / (60 * 60)
+
+    def utcoffset(self, dt):
+        return self.offset
+
+    def tzname(self, dt):
+        return self.name
+
+    def dst(self, dt):
+        return ZERO
+
+    def __repr__(self):
+        cn = self.__class__.__name__
+        return '%s(name=%r, offset=%r)' % (cn, self.name, self.offset)
+
+
+UTC = ConstantTZInfo('UTC')
+EPOCH_AWARE = datetime.fromtimestamp(0, UTC)
+EPOCH_NAIVE = datetime.utcfromtimestamp(0)
+
+
+class LocalTZInfo(tzinfo):
+    """The ``LocalTZInfo`` type takes data available in the time module
+    about the local timezone and makes a practical
+    :class:`datetime.tzinfo` to represent the timezone settings of the
+    operating system.
+
+    For a more in-depth integration with the operating system, check
+    out `tzlocal`_. It builds on `pytz`_ and implements heuristics for
+    many versions of major operating systems to provide the official
+    ``pytz`` tzinfo, instead of the LocalTZ generalization.
+
+    .. _tzlocal: https://pypi.python.org/pypi/tzlocal
+    .. _pytz: https://pypi.python.org/pypi/pytz
+
+    """
+    _std_offset = timedelta(seconds=-time.timezone)
+    _dst_offset = _std_offset
+    if time.daylight:
+        _dst_offset = timedelta(seconds=-time.altzone)
+
+    def is_dst(self, dt):
+        dt_t = (dt.year, dt.month, dt.day, dt.hour, dt.minute,
+                dt.second, dt.weekday(), 0, -1)
+        local_t = time.localtime(time.mktime(dt_t))
+        return local_t.tm_isdst > 0
+
+    def utcoffset(self, dt):
+        if self.is_dst(dt):
+            return self._dst_offset
+        return self._std_offset
+
+    def dst(self, dt):
+        if self.is_dst(dt):
+            return self._dst_offset - self._std_offset
+        return ZERO
+
+    def tzname(self, dt):
+        return time.tzname[self.is_dst(dt)]
+
+    def __repr__(self):
+        return '%s()' % self.__class__.__name__
+
+
+LocalTZ = LocalTZInfo()
+
+
+def _first_sunday_on_or_after(dt):
+    days_to_go = 6 - dt.weekday()
+    if days_to_go:
+        dt += timedelta(days_to_go)
+    return dt
+
+
+# US DST Rules
+#
+# This is a simplified (i.e., wrong for a few cases) set of rules for US
+# DST start and end times. For a complete and up-to-date set of DST rules
+# and timezone definitions, visit the Olson Database (or try pytz):
+# http://www.twinsun.com/tz/tz-link.htm
+# http://sourceforge.net/projects/pytz/ (might not be up-to-date)
+#
+# In the US, since 2007, DST starts at 2am (standard time) on the second
+# Sunday in March, which is the first Sunday on or after Mar 8.
+DSTSTART_2007 = datetime(1, 3, 8, 2)
+# and ends at 2am (DST time; 1am standard time) on the first Sunday of Nov.
+DSTEND_2007 = datetime(1, 11, 1, 1)
+# From 1987 to 2006, DST used to start at 2am (standard time) on the first
+# Sunday in April and to end at 2am (DST time; 1am standard time) on the last
+# Sunday of October, which is the first Sunday on or after Oct 25.
+DSTSTART_1987_2006 = datetime(1, 4, 1, 2)
+DSTEND_1987_2006 = datetime(1, 10, 25, 1)
+# From 1967 to 1986, DST used to start at 2am (standard time) on the last
+# Sunday in April (the one on or after April 24) and to end at 2am (DST time;
+# 1am standard time) on the last Sunday of October, which is the first Sunday
+# on or after Oct 25.
+DSTSTART_1967_1986 = datetime(1, 4, 24, 2)
+DSTEND_1967_1986 = DSTEND_1987_2006
+
+
+class USTimeZone(tzinfo):
+    """Copied directly from the Python docs, the ``USTimeZone`` is a
+    :class:`datetime.tzinfo` subtype used to create the
+    :data:`Eastern`, :data:`Central`, :data:`Mountain`, and
+    :data:`Pacific` tzinfo types.
+    """
+    def __init__(self, hours, reprname, stdname, dstname):
+        self.stdoffset = timedelta(hours=hours)
+        self.reprname = reprname
+        self.stdname = stdname
+        self.dstname = dstname
+
+    def __repr__(self):
+        return self.reprname
+
+    def tzname(self, dt):
+        if self.dst(dt):
+            return self.dstname
+        else:
+            return self.stdname
+
+    def utcoffset(self, dt):
+        return self.stdoffset + self.dst(dt)
+
+    def dst(self, dt):
+        if dt is None or dt.tzinfo is None:
+            # An exception may be sensible here, in one or both cases.
+            # It depends on how you want to treat them.  The default
+            # fromutc() implementation (called by the default astimezone()
+            # implementation) passes a datetime with dt.tzinfo is self.
+            return ZERO
+        assert dt.tzinfo is self
+
+        # Find start and end times for US DST. For years before 1967, return
+        # ZERO for no DST.
+        if 2006 < dt.year:
+            dststart, dstend = DSTSTART_2007, DSTEND_2007
+        elif 1986 < dt.year < 2007:
+            dststart, dstend = DSTSTART_1987_2006, DSTEND_1987_2006
+        elif 1966 < dt.year < 1987:
+            dststart, dstend = DSTSTART_1967_1986, DSTEND_1967_1986
+        else:
+            return ZERO
+
+        start = _first_sunday_on_or_after(dststart.replace(year=dt.year))
+        end = _first_sunday_on_or_after(dstend.replace(year=dt.year))
+
+        # Can't compare naive to aware objects, so strip the timezone
+        # from dt first.
+        if start <= dt.replace(tzinfo=None) < end:
+            return HOUR
+        else:
+            return ZERO
+
+
+Eastern = USTimeZone(-5, "Eastern",  "EST", "EDT")
+Central = USTimeZone(-6, "Central",  "CST", "CDT")
+Mountain = USTimeZone(-7, "Mountain", "MST", "MDT")
+Pacific = USTimeZone(-8, "Pacific",  "PST", "PDT")