comparison 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
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
1 # -*- coding: utf-8 -*-
2 """Python's :mod:`datetime` module provides some of the most complex
3 and powerful primitives in the Python standard library. Time is
4 nontrivial, but thankfully its support is first-class in
5 Python. ``dateutils`` provides some additional tools for working with
6 time.
7
8 Additionally, timeutils provides a few basic utilities for working
9 with timezones in Python. The Python :mod:`datetime` module's
10 documentation describes how to create a
11 :class:`~datetime.datetime`-compatible :class:`~datetime.tzinfo`
12 subtype. It even provides a few examples.
13
14 The following module defines usable forms of the timezones in those
15 docs, as well as a couple other useful ones, :data:`UTC` (aka GMT) and
16 :data:`LocalTZ` (representing the local timezone as configured in the
17 operating system). For timezones beyond these, as well as a higher
18 degree of accuracy in corner cases, check out `pytz`_ and `dateutil`_.
19
20 .. _pytz: https://pypi.python.org/pypi/pytz
21 .. _dateutil: https://dateutil.readthedocs.io/en/stable/index.html
22 """
23
24 import re
25 import time
26 import bisect
27 import operator
28 from datetime import tzinfo, timedelta, date, datetime
29
30
31 def total_seconds(td):
32 """For those with older versions of Python, a pure-Python
33 implementation of Python 2.7's :meth:`~datetime.timedelta.total_seconds`.
34
35 Args:
36 td (datetime.timedelta): The timedelta to convert to seconds.
37 Returns:
38 float: total number of seconds
39
40 >>> td = timedelta(days=4, seconds=33)
41 >>> total_seconds(td)
42 345633.0
43 """
44 a_milli = 1000000.0
45 td_ds = td.seconds + (td.days * 86400) # 24 * 60 * 60
46 td_micro = td.microseconds + (td_ds * a_milli)
47 return td_micro / a_milli
48
49
50 def dt_to_timestamp(dt):
51 """Converts from a :class:`~datetime.datetime` object to an integer
52 timestamp, suitable interoperation with :func:`time.time` and
53 other `Epoch-based timestamps`.
54
55 .. _Epoch-based timestamps: https://en.wikipedia.org/wiki/Unix_time
56
57 >>> abs(round(time.time() - dt_to_timestamp(datetime.utcnow()), 2))
58 0.0
59
60 ``dt_to_timestamp`` supports both timezone-aware and naïve
61 :class:`~datetime.datetime` objects. Note that it assumes naïve
62 datetime objects are implied UTC, such as those generated with
63 :meth:`datetime.datetime.utcnow`. If your datetime objects are
64 local time, such as those generated with
65 :meth:`datetime.datetime.now`, first convert it using the
66 :meth:`datetime.datetime.replace` method with ``tzinfo=``
67 :class:`LocalTZ` object in this module, then pass the result of
68 that to ``dt_to_timestamp``.
69 """
70 if dt.tzinfo:
71 td = dt - EPOCH_AWARE
72 else:
73 td = dt - EPOCH_NAIVE
74 return total_seconds(td)
75
76
77 _NONDIGIT_RE = re.compile(r'\D')
78
79
80 def isoparse(iso_str):
81 """Parses the limited subset of `ISO8601-formatted time`_ strings as
82 returned by :meth:`datetime.datetime.isoformat`.
83
84 >>> epoch_dt = datetime.utcfromtimestamp(0)
85 >>> iso_str = epoch_dt.isoformat()
86 >>> print(iso_str)
87 1970-01-01T00:00:00
88 >>> isoparse(iso_str)
89 datetime.datetime(1970, 1, 1, 0, 0)
90
91 >>> utcnow = datetime.utcnow()
92 >>> utcnow == isoparse(utcnow.isoformat())
93 True
94
95 For further datetime parsing, see the `iso8601`_ package for strict
96 ISO parsing and `dateutil`_ package for loose parsing and more.
97
98 .. _ISO8601-formatted time: https://en.wikipedia.org/wiki/ISO_8601
99 .. _iso8601: https://pypi.python.org/pypi/iso8601
100 .. _dateutil: https://pypi.python.org/pypi/python-dateutil
101
102 """
103 dt_args = [int(p) for p in _NONDIGIT_RE.split(iso_str)]
104 return datetime(*dt_args)
105
106
107 _BOUNDS = [(0, timedelta(seconds=1), 'second'),
108 (1, timedelta(seconds=60), 'minute'),
109 (1, timedelta(seconds=3600), 'hour'),
110 (1, timedelta(days=1), 'day'),
111 (1, timedelta(days=7), 'week'),
112 (2, timedelta(days=30), 'month'),
113 (1, timedelta(days=365), 'year')]
114 _BOUNDS = [(b[0] * b[1], b[1], b[2]) for b in _BOUNDS]
115 _BOUND_DELTAS = [b[0] for b in _BOUNDS]
116
117 _FLOAT_PATTERN = r'[+-]?\ *(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?'
118 _PARSE_TD_RE = re.compile(r"((?P<value>%s)\s*(?P<unit>\w)\w*)" % _FLOAT_PATTERN)
119 _PARSE_TD_KW_MAP = dict([(unit[0], unit + 's')
120 for _, _, unit in reversed(_BOUNDS[:-2])])
121
122
123 def parse_timedelta(text):
124 """Robustly parses a short text description of a time period into a
125 :class:`datetime.timedelta`. Supports weeks, days, hours, minutes,
126 and seconds, with or without decimal points:
127
128 Args:
129 text (str): Text to parse.
130 Returns:
131 datetime.timedelta
132 Raises:
133 ValueError: on parse failure.
134
135 >>> parse_td('1d 2h 3.5m 0s') == timedelta(days=1, seconds=7410)
136 True
137
138 Also supports full words and whitespace.
139
140 >>> parse_td('2 weeks 1 day') == timedelta(days=15)
141 True
142
143 Negative times are supported, too:
144
145 >>> parse_td('-1.5 weeks 3m 20s') == timedelta(days=-11, seconds=43400)
146 True
147 """
148 td_kwargs = {}
149 for match in _PARSE_TD_RE.finditer(text):
150 value, unit = match.group('value'), match.group('unit')
151 try:
152 unit_key = _PARSE_TD_KW_MAP[unit]
153 except KeyError:
154 raise ValueError('invalid time unit %r, expected one of %r'
155 % (unit, _PARSE_TD_KW_MAP.keys()))
156 try:
157 value = float(value)
158 except ValueError:
159 raise ValueError('invalid time value for unit %r: %r'
160 % (unit, value))
161 td_kwargs[unit_key] = value
162 return timedelta(**td_kwargs)
163
164
165 parse_td = parse_timedelta # legacy alias
166
167
168 def _cardinalize_time_unit(unit, value):
169 # removes dependency on strutils; nice and simple because
170 # all time units cardinalize normally
171 if value == 1:
172 return unit
173 return unit + 's'
174
175
176 def decimal_relative_time(d, other=None, ndigits=0, cardinalize=True):
177 """Get a tuple representing the relative time difference between two
178 :class:`~datetime.datetime` objects or one
179 :class:`~datetime.datetime` and now.
180
181 Args:
182 d (datetime): The first datetime object.
183 other (datetime): An optional second datetime object. If
184 unset, defaults to the current time as determined
185 :meth:`datetime.utcnow`.
186 ndigits (int): The number of decimal digits to round to,
187 defaults to ``0``.
188 cardinalize (bool): Whether to pluralize the time unit if
189 appropriate, defaults to ``True``.
190 Returns:
191 (float, str): A tuple of the :class:`float` difference and
192 respective unit of time, pluralized if appropriate and
193 *cardinalize* is set to ``True``.
194
195 Unlike :func:`relative_time`, this method's return is amenable to
196 localization into other languages and custom phrasing and
197 formatting.
198
199 >>> now = datetime.utcnow()
200 >>> decimal_relative_time(now - timedelta(days=1, seconds=3600), now)
201 (1.0, 'day')
202 >>> decimal_relative_time(now - timedelta(seconds=0.002), now, ndigits=5)
203 (0.002, 'seconds')
204 >>> decimal_relative_time(now, now - timedelta(days=900), ndigits=1)
205 (-2.5, 'years')
206
207 """
208 if other is None:
209 other = datetime.utcnow()
210 diff = other - d
211 diff_seconds = total_seconds(diff)
212 abs_diff = abs(diff)
213 b_idx = bisect.bisect(_BOUND_DELTAS, abs_diff) - 1
214 bbound, bunit, bname = _BOUNDS[b_idx]
215 f_diff = diff_seconds / total_seconds(bunit)
216 rounded_diff = round(f_diff, ndigits)
217 if cardinalize:
218 return rounded_diff, _cardinalize_time_unit(bname, abs(rounded_diff))
219 return rounded_diff, bname
220
221
222 def relative_time(d, other=None, ndigits=0):
223 """Get a string representation of the difference between two
224 :class:`~datetime.datetime` objects or one
225 :class:`~datetime.datetime` and the current time. Handles past and
226 future times.
227
228 Args:
229 d (datetime): The first datetime object.
230 other (datetime): An optional second datetime object. If
231 unset, defaults to the current time as determined
232 :meth:`datetime.utcnow`.
233 ndigits (int): The number of decimal digits to round to,
234 defaults to ``0``.
235 Returns:
236 A short English-language string.
237
238 >>> now = datetime.utcnow()
239 >>> relative_time(now, ndigits=1)
240 '0 seconds ago'
241 >>> relative_time(now - timedelta(days=1, seconds=36000), ndigits=1)
242 '1.4 days ago'
243 >>> relative_time(now + timedelta(days=7), now, ndigits=1)
244 '1 week from now'
245
246 """
247 drt, unit = decimal_relative_time(d, other, ndigits, cardinalize=True)
248 phrase = 'ago'
249 if drt < 0:
250 phrase = 'from now'
251 return '%g %s %s' % (abs(drt), unit, phrase)
252
253
254 def strpdate(string, format):
255 """Parse the date string according to the format in `format`. Returns a
256 :class:`date` object. Internally, :meth:`datetime.strptime` is used to
257 parse the string and thus conversion specifiers for time fields (e.g. `%H`)
258 may be provided; these will be parsed but ignored.
259
260 Args:
261 string (str): The date string to be parsed.
262 format (str): The `strptime`_-style date format string.
263 Returns:
264 datetime.date
265
266 .. _`strptime`: https://docs.python.org/2/library/datetime.html#strftime-strptime-behavior
267
268 >>> strpdate('2016-02-14', '%Y-%m-%d')
269 datetime.date(2016, 2, 14)
270 >>> strpdate('26/12 (2015)', '%d/%m (%Y)')
271 datetime.date(2015, 12, 26)
272 >>> strpdate('20151231 23:59:59', '%Y%m%d %H:%M:%S')
273 datetime.date(2015, 12, 31)
274 >>> strpdate('20160101 00:00:00.001', '%Y%m%d %H:%M:%S.%f')
275 datetime.date(2016, 1, 1)
276 """
277 whence = datetime.strptime(string, format)
278 return whence.date()
279
280
281 def daterange(start, stop, step=1, inclusive=False):
282 """In the spirit of :func:`range` and :func:`xrange`, the `daterange`
283 generator that yields a sequence of :class:`~datetime.date`
284 objects, starting at *start*, incrementing by *step*, until *stop*
285 is reached.
286
287 When *inclusive* is True, the final date may be *stop*, **if**
288 *step* falls evenly on it. By default, *step* is one day. See
289 details below for many more details.
290
291 Args:
292 start (datetime.date): The starting date The first value in
293 the sequence.
294 stop (datetime.date): The stopping date. By default not
295 included in return. Can be `None` to yield an infinite
296 sequence.
297 step (int): The value to increment *start* by to reach
298 *stop*. Can be an :class:`int` number of days, a
299 :class:`datetime.timedelta`, or a :class:`tuple` of integers,
300 `(year, month, day)`. Positive and negative *step* values
301 are supported.
302 inclusive (bool): Whether or not the *stop* date can be
303 returned. *stop* is only returned when a *step* falls evenly
304 on it.
305
306 >>> christmas = date(year=2015, month=12, day=25)
307 >>> boxing_day = date(year=2015, month=12, day=26)
308 >>> new_year = date(year=2016, month=1, day=1)
309 >>> for day in daterange(christmas, new_year):
310 ... print(repr(day))
311 datetime.date(2015, 12, 25)
312 datetime.date(2015, 12, 26)
313 datetime.date(2015, 12, 27)
314 datetime.date(2015, 12, 28)
315 datetime.date(2015, 12, 29)
316 datetime.date(2015, 12, 30)
317 datetime.date(2015, 12, 31)
318 >>> for day in daterange(christmas, boxing_day):
319 ... print(repr(day))
320 datetime.date(2015, 12, 25)
321 >>> for day in daterange(date(2017, 5, 1), date(2017, 8, 1),
322 ... step=(0, 1, 0), inclusive=True):
323 ... print(repr(day))
324 datetime.date(2017, 5, 1)
325 datetime.date(2017, 6, 1)
326 datetime.date(2017, 7, 1)
327 datetime.date(2017, 8, 1)
328
329 *Be careful when using stop=None, as this will yield an infinite
330 sequence of dates.*
331 """
332 if not isinstance(start, date):
333 raise TypeError("start expected datetime.date instance")
334 if stop and not isinstance(stop, date):
335 raise TypeError("stop expected datetime.date instance or None")
336 try:
337 y_step, m_step, d_step = step
338 except TypeError:
339 y_step, m_step, d_step = 0, 0, step
340 else:
341 y_step, m_step = int(y_step), int(m_step)
342 if isinstance(d_step, int):
343 d_step = timedelta(days=int(d_step))
344 elif isinstance(d_step, timedelta):
345 pass
346 else:
347 raise ValueError('step expected int, timedelta, or tuple'
348 ' (year, month, day), not: %r' % step)
349
350 if stop is None:
351 finished = lambda now, stop: False
352 elif start < stop:
353 finished = operator.gt if inclusive else operator.ge
354 else:
355 finished = operator.lt if inclusive else operator.le
356 now = start
357
358 while not finished(now, stop):
359 yield now
360 if y_step or m_step:
361 m_y_step, cur_month = divmod(now.month + m_step, 12)
362 now = now.replace(year=now.year + y_step + m_y_step,
363 month=cur_month or 12)
364 now = now + d_step
365 return
366
367
368 # Timezone support (brought in from tzutils)
369
370
371 ZERO = timedelta(0)
372 HOUR = timedelta(hours=1)
373
374
375 class ConstantTZInfo(tzinfo):
376 """
377 A :class:`~datetime.tzinfo` subtype whose *offset* remains constant
378 (no daylight savings).
379
380 Args:
381 name (str): Name of the timezone.
382 offset (datetime.timedelta): Offset of the timezone.
383 """
384 def __init__(self, name="ConstantTZ", offset=ZERO):
385 self.name = name
386 self.offset = offset
387
388 @property
389 def utcoffset_hours(self):
390 return total_seconds(self.offset) / (60 * 60)
391
392 def utcoffset(self, dt):
393 return self.offset
394
395 def tzname(self, dt):
396 return self.name
397
398 def dst(self, dt):
399 return ZERO
400
401 def __repr__(self):
402 cn = self.__class__.__name__
403 return '%s(name=%r, offset=%r)' % (cn, self.name, self.offset)
404
405
406 UTC = ConstantTZInfo('UTC')
407 EPOCH_AWARE = datetime.fromtimestamp(0, UTC)
408 EPOCH_NAIVE = datetime.utcfromtimestamp(0)
409
410
411 class LocalTZInfo(tzinfo):
412 """The ``LocalTZInfo`` type takes data available in the time module
413 about the local timezone and makes a practical
414 :class:`datetime.tzinfo` to represent the timezone settings of the
415 operating system.
416
417 For a more in-depth integration with the operating system, check
418 out `tzlocal`_. It builds on `pytz`_ and implements heuristics for
419 many versions of major operating systems to provide the official
420 ``pytz`` tzinfo, instead of the LocalTZ generalization.
421
422 .. _tzlocal: https://pypi.python.org/pypi/tzlocal
423 .. _pytz: https://pypi.python.org/pypi/pytz
424
425 """
426 _std_offset = timedelta(seconds=-time.timezone)
427 _dst_offset = _std_offset
428 if time.daylight:
429 _dst_offset = timedelta(seconds=-time.altzone)
430
431 def is_dst(self, dt):
432 dt_t = (dt.year, dt.month, dt.day, dt.hour, dt.minute,
433 dt.second, dt.weekday(), 0, -1)
434 local_t = time.localtime(time.mktime(dt_t))
435 return local_t.tm_isdst > 0
436
437 def utcoffset(self, dt):
438 if self.is_dst(dt):
439 return self._dst_offset
440 return self._std_offset
441
442 def dst(self, dt):
443 if self.is_dst(dt):
444 return self._dst_offset - self._std_offset
445 return ZERO
446
447 def tzname(self, dt):
448 return time.tzname[self.is_dst(dt)]
449
450 def __repr__(self):
451 return '%s()' % self.__class__.__name__
452
453
454 LocalTZ = LocalTZInfo()
455
456
457 def _first_sunday_on_or_after(dt):
458 days_to_go = 6 - dt.weekday()
459 if days_to_go:
460 dt += timedelta(days_to_go)
461 return dt
462
463
464 # US DST Rules
465 #
466 # This is a simplified (i.e., wrong for a few cases) set of rules for US
467 # DST start and end times. For a complete and up-to-date set of DST rules
468 # and timezone definitions, visit the Olson Database (or try pytz):
469 # http://www.twinsun.com/tz/tz-link.htm
470 # http://sourceforge.net/projects/pytz/ (might not be up-to-date)
471 #
472 # In the US, since 2007, DST starts at 2am (standard time) on the second
473 # Sunday in March, which is the first Sunday on or after Mar 8.
474 DSTSTART_2007 = datetime(1, 3, 8, 2)
475 # and ends at 2am (DST time; 1am standard time) on the first Sunday of Nov.
476 DSTEND_2007 = datetime(1, 11, 1, 1)
477 # From 1987 to 2006, DST used to start at 2am (standard time) on the first
478 # Sunday in April and to end at 2am (DST time; 1am standard time) on the last
479 # Sunday of October, which is the first Sunday on or after Oct 25.
480 DSTSTART_1987_2006 = datetime(1, 4, 1, 2)
481 DSTEND_1987_2006 = datetime(1, 10, 25, 1)
482 # From 1967 to 1986, DST used to start at 2am (standard time) on the last
483 # Sunday in April (the one on or after April 24) and to end at 2am (DST time;
484 # 1am standard time) on the last Sunday of October, which is the first Sunday
485 # on or after Oct 25.
486 DSTSTART_1967_1986 = datetime(1, 4, 24, 2)
487 DSTEND_1967_1986 = DSTEND_1987_2006
488
489
490 class USTimeZone(tzinfo):
491 """Copied directly from the Python docs, the ``USTimeZone`` is a
492 :class:`datetime.tzinfo` subtype used to create the
493 :data:`Eastern`, :data:`Central`, :data:`Mountain`, and
494 :data:`Pacific` tzinfo types.
495 """
496 def __init__(self, hours, reprname, stdname, dstname):
497 self.stdoffset = timedelta(hours=hours)
498 self.reprname = reprname
499 self.stdname = stdname
500 self.dstname = dstname
501
502 def __repr__(self):
503 return self.reprname
504
505 def tzname(self, dt):
506 if self.dst(dt):
507 return self.dstname
508 else:
509 return self.stdname
510
511 def utcoffset(self, dt):
512 return self.stdoffset + self.dst(dt)
513
514 def dst(self, dt):
515 if dt is None or dt.tzinfo is None:
516 # An exception may be sensible here, in one or both cases.
517 # It depends on how you want to treat them. The default
518 # fromutc() implementation (called by the default astimezone()
519 # implementation) passes a datetime with dt.tzinfo is self.
520 return ZERO
521 assert dt.tzinfo is self
522
523 # Find start and end times for US DST. For years before 1967, return
524 # ZERO for no DST.
525 if 2006 < dt.year:
526 dststart, dstend = DSTSTART_2007, DSTEND_2007
527 elif 1986 < dt.year < 2007:
528 dststart, dstend = DSTSTART_1987_2006, DSTEND_1987_2006
529 elif 1966 < dt.year < 1987:
530 dststart, dstend = DSTSTART_1967_1986, DSTEND_1967_1986
531 else:
532 return ZERO
533
534 start = _first_sunday_on_or_after(dststart.replace(year=dt.year))
535 end = _first_sunday_on_or_after(dstend.replace(year=dt.year))
536
537 # Can't compare naive to aware objects, so strip the timezone
538 # from dt first.
539 if start <= dt.replace(tzinfo=None) < end:
540 return HOUR
541 else:
542 return ZERO
543
544
545 Eastern = USTimeZone(-5, "Eastern", "EST", "EDT")
546 Central = USTimeZone(-6, "Central", "CST", "CDT")
547 Mountain = USTimeZone(-7, "Mountain", "MST", "MDT")
548 Pacific = USTimeZone(-8, "Pacific", "PST", "PDT")