time.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. # -*- coding: utf-8 -*-
  2. """Utilities related to dates, times, intervals, and timezones."""
  3. from __future__ import absolute_import, print_function, unicode_literals
  4. import numbers
  5. import os
  6. import sys
  7. import time as _time
  8. from calendar import monthrange
  9. from datetime import date, datetime, timedelta, tzinfo
  10. from kombu.utils.functional import reprcall
  11. from kombu.utils.objects import cached_property
  12. from pytz import timezone as _timezone, AmbiguousTimeError, FixedOffset
  13. from celery.five import python_2_unicode_compatible, string_t
  14. from .functional import dictfilter
  15. from .iso8601 import parse_iso8601
  16. from .text import pluralize
  17. __all__ = [
  18. 'LocalTimezone', 'timezone', 'maybe_timedelta',
  19. 'delta_resolution', 'remaining', 'rate', 'weekday',
  20. 'humanize_seconds', 'maybe_iso8601', 'is_naive',
  21. 'make_aware', 'localize', 'to_utc', 'maybe_make_aware',
  22. 'ffwd', 'utcoffset', 'adjust_timestamp',
  23. ]
  24. PY3 = sys.version_info[0] == 3
  25. PY33 = sys.version_info >= (3, 3)
  26. C_REMDEBUG = os.environ.get('C_REMDEBUG', False)
  27. DAYNAMES = 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
  28. WEEKDAYS = dict(zip(DAYNAMES, range(7)))
  29. RATE_MODIFIER_MAP = {
  30. 's': lambda n: n,
  31. 'm': lambda n: n / 60.0,
  32. 'h': lambda n: n / 60.0 / 60.0,
  33. }
  34. TIME_UNITS = (
  35. ('day', 60 * 60 * 24.0, lambda n: format(n, '.2f')),
  36. ('hour', 60 * 60.0, lambda n: format(n, '.2f')),
  37. ('minute', 60.0, lambda n: format(n, '.2f')),
  38. ('second', 1.0, lambda n: format(n, '.2f')),
  39. )
  40. ZERO = timedelta(0)
  41. _local_timezone = None
  42. @python_2_unicode_compatible
  43. class LocalTimezone(tzinfo):
  44. """Local time implementation.
  45. Note:
  46. Used only when the :setting:`enable_utc` setting is disabled.
  47. """
  48. _offset_cache = {}
  49. def __init__(self):
  50. # This code is moved in __init__ to execute it as late as possible
  51. # See get_default_timezone().
  52. self.STDOFFSET = timedelta(seconds=-_time.timezone)
  53. if _time.daylight:
  54. self.DSTOFFSET = timedelta(seconds=-_time.altzone)
  55. else:
  56. self.DSTOFFSET = self.STDOFFSET
  57. self.DSTDIFF = self.DSTOFFSET - self.STDOFFSET
  58. tzinfo.__init__(self)
  59. def __repr__(self):
  60. return '<LocalTimezone: UTC{0:+03d}>'.format(
  61. int(self.DSTOFFSET.total_seconds() / 3600),
  62. )
  63. def utcoffset(self, dt):
  64. return self.DSTOFFSET if self._isdst(dt) else self.STDOFFSET
  65. def dst(self, dt):
  66. return self.DSTDIFF if self._isdst(dt) else ZERO
  67. def tzname(self, dt):
  68. return _time.tzname[self._isdst(dt)]
  69. if PY3: # pragma: no cover
  70. def fromutc(self, dt):
  71. # The base tzinfo class no longer implements a DST
  72. # offset aware .fromutc() in Python 3 (Issue #2306).
  73. # I'd rather rely on pytz to do this, than port
  74. # the C code from cpython's fromutc [asksol]
  75. offset = int(self.utcoffset(dt).seconds / 60.0)
  76. try:
  77. tz = self._offset_cache[offset]
  78. except KeyError:
  79. tz = self._offset_cache[offset] = FixedOffset(offset)
  80. return tz.fromutc(dt.replace(tzinfo=tz))
  81. def _isdst(self, dt):
  82. tt = (dt.year, dt.month, dt.day,
  83. dt.hour, dt.minute, dt.second,
  84. dt.weekday(), 0, 0)
  85. stamp = _time.mktime(tt)
  86. tt = _time.localtime(stamp)
  87. return tt.tm_isdst > 0
  88. class _Zone(object):
  89. def tz_or_local(self, tzinfo=None):
  90. # pylint: disable=redefined-outer-name
  91. if tzinfo is None:
  92. return self.local
  93. return self.get_timezone(tzinfo)
  94. def to_local(self, dt, local=None, orig=None):
  95. if is_naive(dt):
  96. dt = make_aware(dt, orig or self.utc)
  97. return localize(dt, self.tz_or_local(local))
  98. if PY33: # pragma: no cover
  99. def to_system(self, dt):
  100. # tz=None is a special case since Python 3.3, and will
  101. # convert to the current local timezone (Issue #2306).
  102. return dt.astimezone(tz=None)
  103. else:
  104. def to_system(self, dt): # noqa
  105. return localize(dt, self.local)
  106. def to_local_fallback(self, dt):
  107. if is_naive(dt):
  108. return make_aware(dt, self.local)
  109. return localize(dt, self.local)
  110. def get_timezone(self, zone):
  111. if isinstance(zone, string_t):
  112. return _timezone(zone)
  113. return zone
  114. @cached_property
  115. def local(self):
  116. return LocalTimezone()
  117. @cached_property
  118. def utc(self):
  119. return self.get_timezone('UTC')
  120. timezone = _Zone()
  121. def maybe_timedelta(delta):
  122. """Convert integer to timedelta, if argument is an integer."""
  123. if isinstance(delta, numbers.Real):
  124. return timedelta(seconds=delta)
  125. return delta
  126. def delta_resolution(dt, delta):
  127. """Round a :class:`~datetime.datetime` to the resolution of timedelta.
  128. If the :class:`~datetime.timedelta` is in days, the
  129. :class:`~datetime.datetime` will be rounded to the nearest days,
  130. if the :class:`~datetime.timedelta` is in hours the
  131. :class:`~datetime.datetime` will be rounded to the nearest hour,
  132. and so on until seconds, which will just return the original
  133. :class:`~datetime.datetime`.
  134. """
  135. delta = max(delta.total_seconds(), 0)
  136. resolutions = ((3, lambda x: x / 86400),
  137. (4, lambda x: x / 3600),
  138. (5, lambda x: x / 60))
  139. args = dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second
  140. for res, predicate in resolutions:
  141. if predicate(delta) >= 1.0:
  142. return datetime(*args[:res], tzinfo=dt.tzinfo)
  143. return dt
  144. def remaining(start, ends_in, now=None, relative=False):
  145. """Calculate the remaining time for a start date and a timedelta.
  146. For example, "how many seconds left for 30 seconds after start?"
  147. Arguments:
  148. start (~datetime.datetime): Starting date.
  149. ends_in (~datetime.timedelta): The end delta.
  150. relative (bool): If enabled the end time will be calculated
  151. using :func:`delta_resolution` (i.e., rounded to the
  152. resolution of `ends_in`).
  153. now (Callable): Function returning the current time and date.
  154. Defaults to :func:`datetime.utcnow`.
  155. Returns:
  156. ~datetime.timedelta: Remaining time.
  157. """
  158. now = now or datetime.utcnow()
  159. end_date = start + ends_in
  160. if relative:
  161. end_date = delta_resolution(end_date, ends_in)
  162. ret = end_date - now
  163. if C_REMDEBUG: # pragma: no cover
  164. print('rem: NOW:%r START:%r ENDS_IN:%r END_DATE:%s REM:%s' % (
  165. now, start, ends_in, end_date, ret))
  166. return ret
  167. def rate(r):
  168. """Convert rate string (`"100/m"`, `"2/h"` or `"0.5/s"`) to seconds."""
  169. if r:
  170. if isinstance(r, string_t):
  171. ops, _, modifier = r.partition('/')
  172. return RATE_MODIFIER_MAP[modifier or 's'](float(ops)) or 0
  173. return r or 0
  174. return 0
  175. def weekday(name):
  176. """Return the position of a weekday: 0 - 7, where 0 is Sunday.
  177. Example:
  178. >>> weekday('sunday'), weekday('sun'), weekday('mon')
  179. (0, 0, 1)
  180. """
  181. abbreviation = name[0:3].lower()
  182. try:
  183. return WEEKDAYS[abbreviation]
  184. except KeyError:
  185. # Show original day name in exception, instead of abbr.
  186. raise KeyError(name)
  187. def humanize_seconds(secs, prefix='', sep='', now='now', microseconds=False):
  188. """Show seconds in human form.
  189. For example, 60 becomes "1 minute", and 7200 becomes "2 hours".
  190. Arguments:
  191. prefix (str): can be used to add a preposition to the output
  192. (e.g., 'in' will give 'in 1 second', but add nothing to 'now').
  193. now (str): Literal 'now'.
  194. microseconds (bool): Include microseconds.
  195. """
  196. secs = float(format(float(secs), '.2f'))
  197. for unit, divider, formatter in TIME_UNITS:
  198. if secs >= divider:
  199. w = secs / float(divider)
  200. return '{0}{1}{2} {3}'.format(prefix, sep, formatter(w),
  201. pluralize(w, unit))
  202. if microseconds and secs > 0.0:
  203. return '{prefix}{sep}{0:.2f} seconds'.format(
  204. secs, sep=sep, prefix=prefix)
  205. return now
  206. def maybe_iso8601(dt):
  207. """Either ``datetime | str -> datetime`` or ``None -> None``."""
  208. if not dt:
  209. return
  210. if isinstance(dt, datetime):
  211. return dt
  212. return parse_iso8601(dt)
  213. def is_naive(dt):
  214. """Return :const:`True` if :class:`~datetime.datetime` is naive."""
  215. return dt.tzinfo is None or dt.tzinfo.utcoffset(dt) is None
  216. def make_aware(dt, tz):
  217. """Set timezone for a :class:`~datetime.datetime` object."""
  218. try:
  219. _localize = tz.localize
  220. except AttributeError:
  221. return dt.replace(tzinfo=tz)
  222. else:
  223. # works on pytz timezones
  224. try:
  225. return _localize(dt, is_dst=None)
  226. except AmbiguousTimeError:
  227. return min(_localize(dt, is_dst=True),
  228. _localize(dt, is_dst=False))
  229. def localize(dt, tz):
  230. """Convert aware :class:`~datetime.datetime` to another timezone."""
  231. dt = dt.astimezone(tz)
  232. try:
  233. _normalize = tz.normalize
  234. except AttributeError: # non-pytz tz
  235. return dt
  236. else:
  237. try:
  238. return _normalize(dt, is_dst=None)
  239. except TypeError:
  240. return _normalize(dt)
  241. except AmbiguousTimeError:
  242. return min(_normalize(dt, is_dst=True),
  243. _normalize(dt, is_dst=False))
  244. def to_utc(dt):
  245. """Convert naive :class:`~datetime.datetime` to UTC."""
  246. return make_aware(dt, timezone.utc)
  247. def maybe_make_aware(dt, tz=None):
  248. """Convert dt to aware datetime, do nothing if dt is already aware."""
  249. if is_naive(dt):
  250. dt = to_utc(dt)
  251. return localize(
  252. dt, timezone.utc if tz is None else timezone.tz_or_local(tz),
  253. )
  254. @python_2_unicode_compatible
  255. class ffwd(object):
  256. """Version of ``dateutil.relativedelta`` that only supports addition."""
  257. def __init__(self, year=None, month=None, weeks=0, weekday=None, day=None,
  258. hour=None, minute=None, second=None, microsecond=None,
  259. **kwargs):
  260. # pylint: disable=redefined-outer-name
  261. # weekday is also a function in outer scope.
  262. self.year = year
  263. self.month = month
  264. self.weeks = weeks
  265. self.weekday = weekday
  266. self.day = day
  267. self.hour = hour
  268. self.minute = minute
  269. self.second = second
  270. self.microsecond = microsecond
  271. self.days = weeks * 7
  272. self._has_time = self.hour is not None or self.minute is not None
  273. def __repr__(self):
  274. return reprcall('ffwd', (), self._fields(weeks=self.weeks,
  275. weekday=self.weekday))
  276. def __radd__(self, other):
  277. if not isinstance(other, date):
  278. return NotImplemented
  279. year = self.year or other.year
  280. month = self.month or other.month
  281. day = min(monthrange(year, month)[1], self.day or other.day)
  282. ret = other.replace(**dict(dictfilter(self._fields()),
  283. year=year, month=month, day=day))
  284. if self.weekday is not None:
  285. ret += timedelta(days=(7 - ret.weekday() + self.weekday) % 7)
  286. return ret + timedelta(days=self.days)
  287. def _fields(self, **extra):
  288. return dictfilter({
  289. 'year': self.year, 'month': self.month, 'day': self.day,
  290. 'hour': self.hour, 'minute': self.minute,
  291. 'second': self.second, 'microsecond': self.microsecond,
  292. }, **extra)
  293. def utcoffset(time=_time, localtime=_time.localtime):
  294. """Return the current offset to UTC in hours."""
  295. if localtime().tm_isdst:
  296. return time.altzone // 3600
  297. return time.timezone // 3600
  298. def adjust_timestamp(ts, offset, here=utcoffset):
  299. """Adjust timestamp based on provided utcoffset."""
  300. return ts - (offset - here()) * 3600