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.
 
 
 

711 lines
20 KiB

  1. #!/usr/bin/python
  2. #
  3. # Copyright 2009 Google Inc.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. #
  17. #
  18. # Disable the invalid name warning as we are inheriting from a standard library
  19. # object.
  20. # pylint: disable-msg=C6409,W0212
  21. """A version of the datetime module which *cares* about timezones.
  22. This module will never return a naive datetime object. This requires the module
  23. know your local timezone, which it tries really hard to figure out.
  24. You can override the detection by using the datetime.tzaware.defaulttz_set
  25. method. It the module is unable to figure out the timezone itself this method
  26. *must* be called before the normal module is imported. If done before importing
  27. it can also speed up the time taken to import as the defaulttz will no longer
  28. try and do the detection.
  29. """
  30. __author__ = "tansell@google.com (Tim Ansell)"
  31. import calendar
  32. import datetime
  33. import os
  34. import os.path
  35. import re
  36. import time
  37. import warnings
  38. import dateutil.parser
  39. import dateutil.relativedelta
  40. import dateutil.tz
  41. import pytz
  42. from . import pytz_abbr
  43. try:
  44. # pylint: disable-msg=C6204
  45. import functools
  46. except ImportError as e:
  47. class functools(object):
  48. """Fake replacement for a full functools."""
  49. # pylint: disable-msg=W0613
  50. @staticmethod
  51. def wraps(f, *args, **kw):
  52. return f
  53. # Need to patch pytz.utc to have a _utcoffset so you can normalize/localize
  54. # using it.
  55. pytz.utc._utcoffset = datetime.timedelta()
  56. timedelta = datetime.timedelta
  57. def _tzinfome(tzinfo):
  58. """Gets a tzinfo object from a string.
  59. Args:
  60. tzinfo: A string (or string like) object, or a datetime.tzinfo object.
  61. Returns:
  62. An datetime.tzinfo object.
  63. Raises:
  64. UnknownTimeZoneError: If the timezone given can't be decoded.
  65. """
  66. if not isinstance(tzinfo, datetime.tzinfo):
  67. try:
  68. tzinfo = pytz.timezone(tzinfo)
  69. except AttributeError:
  70. raise pytz.UnknownTimeZoneError("Unknown timezone! %s" % tzinfo)
  71. return tzinfo
  72. # Our "local" timezone
  73. _localtz = None
  74. def localtz():
  75. """Get the local timezone.
  76. Returns:
  77. The localtime timezone as a tzinfo object.
  78. """
  79. # pylint: disable-msg=W0603
  80. global _localtz
  81. if _localtz is None:
  82. _localtz = detect_timezone()
  83. return _localtz
  84. def localtz_set(timezone):
  85. """Set the local timezone."""
  86. # pylint: disable-msg=W0603
  87. global _localtz
  88. _localtz = _tzinfome(timezone)
  89. def detect_timezone():
  90. """Try and detect the timezone that Python is currently running in.
  91. We have a bunch of different methods for trying to figure this out (listed in
  92. order they are attempted).
  93. * Try TZ environment variable.
  94. * Try and find /etc/timezone file (with timezone name).
  95. * Try and find /etc/localtime file (with timezone data).
  96. * Try and match a TZ to the current dst/offset/shortname.
  97. Returns:
  98. The detected local timezone as a tzinfo object
  99. Raises:
  100. pytz.UnknownTimeZoneError: If it was unable to detect a timezone.
  101. """
  102. # First we try the TZ variable
  103. tz = _detect_timezone_environ()
  104. if tz is not None:
  105. return tz
  106. # Second we try /etc/timezone and use the value in that
  107. tz = _detect_timezone_etc_timezone()
  108. if tz is not None:
  109. return tz
  110. # Next we try and see if something matches the tzinfo in /etc/localtime
  111. tz = _detect_timezone_etc_localtime()
  112. if tz is not None:
  113. return tz
  114. # Next we try and use a similiar method to what PHP does.
  115. # We first try to search on time.tzname, time.timezone, time.daylight to
  116. # match a pytz zone.
  117. warnings.warn("Had to fall back to worst detection method (the 'PHP' "
  118. "method).")
  119. tz = _detect_timezone_php()
  120. if tz is not None:
  121. return tz
  122. raise pytz.UnknownTimeZoneError("Unable to detect your timezone!")
  123. def _detect_timezone_environ():
  124. if "TZ" in os.environ:
  125. try:
  126. return pytz.timezone(os.environ["TZ"])
  127. except (IOError, pytz.UnknownTimeZoneError):
  128. warnings.warn("You provided a TZ environment value (%r) we did not "
  129. "understand!" % os.environ["TZ"])
  130. def _detect_timezone_etc_timezone():
  131. if os.path.exists("/etc/timezone"):
  132. try:
  133. tz = file("/etc/timezone").read().strip()
  134. try:
  135. return pytz.timezone(tz)
  136. except (IOError, pytz.UnknownTimeZoneError) as ei:
  137. warnings.warn("Your /etc/timezone file references a timezone (%r) that"
  138. " is not valid (%r)." % (tz, ei))
  139. # Problem reading the /etc/timezone file
  140. except IOError as eo:
  141. warnings.warn("Could not access your /etc/timezone file: %s" % eo)
  142. def _detect_timezone_etc_localtime():
  143. matches = []
  144. if os.path.exists("/etc/localtime"):
  145. localtime = pytz.tzfile.build_tzinfo("/etc/localtime",
  146. file("/etc/localtime"))
  147. # See if we can find a "Human Name" for this..
  148. for tzname in pytz.all_timezones:
  149. tz = _tzinfome(tzname)
  150. if dir(tz) != dir(localtime):
  151. continue
  152. for attrib in dir(tz):
  153. # Ignore functions and specials
  154. if callable(getattr(tz, attrib)) or attrib.startswith("__"):
  155. continue
  156. # This will always be different
  157. if attrib == "zone" or attrib == "_tzinfos":
  158. continue
  159. if getattr(tz, attrib) != getattr(localtime, attrib):
  160. break
  161. # We get here iff break didn't happen, i.e. no meaningful attributes
  162. # differ between tz and localtime
  163. else:
  164. matches.append(tzname)
  165. if len(matches) == 1:
  166. return _tzinfome(matches[0])
  167. else:
  168. # Warn the person about this!
  169. warning = "Could not get a human name for your timezone: "
  170. if len(matches) > 1:
  171. warning += ("We detected multiple matches for your /etc/localtime. "
  172. "(Matches where %s)" % matches)
  173. return _tzinfome(matches[0])
  174. else:
  175. warning += "We detected no matches for your /etc/localtime."
  176. warnings.warn(warning)
  177. # Register /etc/localtime as the timezone loaded.
  178. pytz._tzinfo_cache['/etc/localtime'] = localtime
  179. return localtime
  180. def _detect_timezone_php():
  181. tomatch = (time.tzname[0], time.timezone, time.daylight)
  182. now = datetime.datetime.now()
  183. matches = []
  184. for tzname in pytz.all_timezones:
  185. try:
  186. tz = pytz.timezone(tzname)
  187. except IOError:
  188. continue
  189. try:
  190. indst = tz.localize(now).timetuple()[-1]
  191. if tomatch == (tz._tzname, -tz._utcoffset.seconds, indst):
  192. matches.append(tzname)
  193. # pylint: disable-msg=W0704
  194. except AttributeError:
  195. pass
  196. if len(matches) > 1:
  197. warnings.warn("We detected multiple matches for the timezone, choosing "
  198. "the first %s. (Matches where %s)" % (matches[0], matches))
  199. return pytz.timezone(matches[0])
  200. class datetime_tz(datetime.datetime):
  201. """An extension of the inbuilt datetime adding more functionality.
  202. The extra functionality includes:
  203. * Partial parsing support (IE 2006/02/30 matches %Y/%M/%D %H:%M)
  204. * Full integration with pytz (just give it the string of the timezone!)
  205. * Proper support for going to/from Unix timestamps (which are in UTC!).
  206. """
  207. __slots__ = ["is_dst"]
  208. def __new__(cls, *args, **kw):
  209. args = list(args)
  210. if not args:
  211. raise TypeError("Not enough arguments given.")
  212. # See if we are given a tzinfo object...
  213. tzinfo = None
  214. if isinstance(args[-1], (datetime.tzinfo, str)):
  215. tzinfo = _tzinfome(args.pop(-1))
  216. elif kw.get("tzinfo", None) is not None:
  217. tzinfo = _tzinfome(kw.pop("tzinfo"))
  218. # Create a datetime object if we don't have one
  219. if isinstance(args[0], datetime.datetime):
  220. # Convert the datetime instance to a datetime object.
  221. newargs = (list(args[0].timetuple()[0:6]) +
  222. [args[0].microsecond, args[0].tzinfo])
  223. dt = datetime.datetime(*newargs)
  224. if tzinfo is None and dt.tzinfo is None:
  225. raise TypeError("Must specify a timezone!")
  226. if tzinfo is not None and dt.tzinfo is not None:
  227. raise TypeError("Can not give a timezone with timezone aware"
  228. " datetime object! (Use localize.)")
  229. else:
  230. dt = datetime.datetime(*args, **kw)
  231. if dt.tzinfo is not None:
  232. # Re-normalize the dt object
  233. dt = dt.tzinfo.normalize(dt)
  234. else:
  235. if tzinfo is None:
  236. tzinfo = localtz()
  237. try:
  238. dt = tzinfo.localize(dt, is_dst=None)
  239. except pytz.AmbiguousTimeError:
  240. is_dst = None
  241. if "is_dst" in kw:
  242. is_dst = kw.pop("is_dst")
  243. try:
  244. dt = tzinfo.localize(dt, is_dst)
  245. except IndexError:
  246. raise pytz.AmbiguousTimeError("No such time exists!")
  247. newargs = list(dt.timetuple()[0:6])+[dt.microsecond, dt.tzinfo]
  248. obj = datetime.datetime.__new__(cls, *newargs)
  249. obj.is_dst = obj.dst() != datetime.timedelta(0)
  250. return obj
  251. def asdatetime(self, naive=True):
  252. """Return this datetime_tz as a datetime object.
  253. Args:
  254. naive: Return *without* any tz info.
  255. Returns:
  256. This datetime_tz as a datetime object.
  257. """
  258. args = list(self.timetuple()[0:6])+[self.microsecond]
  259. if not naive:
  260. args.append(self.tzinfo)
  261. return datetime.datetime(*args)
  262. def asdate(self):
  263. """Return this datetime_tz as a date object.
  264. Returns:
  265. This datetime_tz as a date object.
  266. """
  267. return datetime.date(self.year, self.month, self.day)
  268. def totimestamp(self):
  269. """Convert this datetime object back to a unix timestamp.
  270. The Unix epoch is the time 00:00:00 UTC on January 1, 1970.
  271. Returns:
  272. Unix timestamp.
  273. """
  274. return calendar.timegm(self.utctimetuple())+1e-6*self.microsecond
  275. def astimezone(self, tzinfo):
  276. """Returns a version of this timestamp converted to the given timezone.
  277. Args:
  278. tzinfo: Either a datetime.tzinfo object or a string (which will be looked
  279. up in pytz.
  280. Returns:
  281. A datetime_tz object in the given timezone.
  282. """
  283. # Assert we are not a naive datetime object
  284. assert self.tzinfo is not None
  285. tzinfo = _tzinfome(tzinfo)
  286. d = self.asdatetime(naive=False).astimezone(tzinfo)
  287. return datetime_tz(d)
  288. # pylint: disable-msg=C6113
  289. def replace(self, **kw):
  290. """Return datetime with new specified fields given as arguments.
  291. For example, dt.replace(days=4) would return a new datetime_tz object with
  292. exactly the same as dt but with the days attribute equal to 4.
  293. Any attribute can be replaced, but tzinfo can not be set to None.
  294. Args:
  295. Any datetime_tz attribute.
  296. Returns:
  297. A datetime_tz object with the attributes replaced.
  298. Raises:
  299. TypeError: If the given replacement is invalid.
  300. """
  301. if "tzinfo" in kw:
  302. if kw["tzinfo"] is None:
  303. raise TypeError("Can not remove the timezone use asdatetime()")
  304. is_dst = None
  305. if "is_dst" in kw:
  306. is_dst = kw["is_dst"]
  307. del kw["is_dst"]
  308. else:
  309. # Use our own DST setting..
  310. is_dst = self.is_dst
  311. replaced = self.asdatetime().replace(**kw)
  312. return datetime_tz(replaced, tzinfo=self.tzinfo.zone, is_dst=is_dst)
  313. # pylint: disable-msg=C6310
  314. @classmethod
  315. def smartparse(cls, toparse, tzinfo=None):
  316. """Method which uses dateutil.parse and extras to try and parse the string.
  317. Valid dates are found at:
  318. http://labix.org/python-dateutil#head-1443e0f14ad5dff07efd465e080d1110920673d8-2
  319. Other valid formats include:
  320. "now" or "today"
  321. "yesterday"
  322. "tommorrow"
  323. "5 minutes ago"
  324. "10 hours ago"
  325. "10h5m ago"
  326. "start of yesterday"
  327. "end of tommorrow"
  328. "end of 3rd of March"
  329. Args:
  330. toparse: The string to parse.
  331. tzinfo: Timezone for the resultant datetime_tz object should be in.
  332. (Defaults to your local timezone.)
  333. Returns:
  334. New datetime_tz object.
  335. Raises:
  336. ValueError: If unable to make sense of the input.
  337. """
  338. # Default for empty fields are:
  339. # year/month/day == now
  340. # hour/minute/second/microsecond == 0
  341. toparse = toparse.strip()
  342. if tzinfo is None:
  343. dt = cls.now()
  344. else:
  345. dt = cls.now(tzinfo)
  346. default = dt.replace(hour=0, minute=0, second=0, microsecond=0)
  347. # Remove "start of " and "end of " prefix in the string
  348. if toparse.lower().startswith("end of "):
  349. toparse = toparse[7:].strip()
  350. dt += datetime.timedelta(days=1)
  351. dt = dt.replace(hour=0, minute=0, second=0, microsecond=0)
  352. dt -= datetime.timedelta(microseconds=1)
  353. default = dt
  354. elif toparse.lower().startswith("start of "):
  355. toparse = toparse[9:].strip()
  356. dt = dt.replace(hour=0, minute=0, second=0, microsecond=0)
  357. default = dt
  358. # Handle strings with "now", "today", "yesterday", "tomorrow" and "ago".
  359. # Need to use lowercase
  360. toparselower = toparse.lower()
  361. if toparselower in ["now", "today"]:
  362. pass
  363. elif toparselower == "yesterday":
  364. dt -= datetime.timedelta(days=1)
  365. elif toparselower == "tommorrow":
  366. dt += datetime.timedelta(days=1)
  367. elif "ago" in toparselower:
  368. # Remove the "ago" bit
  369. toparselower = toparselower[:-3]
  370. # Replace all "a day and an hour" with "1 day 1 hour"
  371. toparselower = toparselower.replace("a ", "1 ")
  372. toparselower = toparselower.replace("an ", "1 ")
  373. toparselower = toparselower.replace(" and ", " ")
  374. # Match the following
  375. # 1 hour ago
  376. # 1h ago
  377. # 1 h ago
  378. # 1 hour ago
  379. # 2 hours ago
  380. # Same with minutes, seconds, etc.
  381. tocheck = ("seconds", "minutes", "hours", "days", "weeks", "months",
  382. "years")
  383. result = {}
  384. for match in re.finditer("([0-9]+)([^0-9]*)", toparselower):
  385. amount = int(match.group(1))
  386. unit = match.group(2).strip()
  387. for bit in tocheck:
  388. regex = "^([%s]|((%s)s?))$" % (
  389. bit[0], bit[:-1])
  390. bitmatch = re.search(regex, unit)
  391. if bitmatch:
  392. result[bit] = amount
  393. break
  394. else:
  395. raise ValueError("Was not able to parse date unit %r!" % unit)
  396. delta = dateutil.relativedelta.relativedelta(**result)
  397. dt -= delta
  398. else:
  399. # Handle strings with normal datetime format, use original case.
  400. dt = dateutil.parser.parse(toparse, default=default.asdatetime(),
  401. tzinfos=pytz_abbr.tzinfos)
  402. if dt is None:
  403. raise ValueError("Was not able to parse date!")
  404. if dt.tzinfo is pytz_abbr.unknown:
  405. dt = dt.replace(tzinfo=None)
  406. if dt.tzinfo is None:
  407. if tzinfo is None:
  408. tzinfo = localtz()
  409. dt = cls(dt, tzinfo)
  410. else:
  411. if isinstance(dt.tzinfo, pytz_abbr.tzabbr):
  412. abbr = dt.tzinfo
  413. dt = dt.replace(tzinfo=None)
  414. dt = cls(dt, abbr.zone, is_dst=abbr.dst)
  415. dt = cls(dt)
  416. return dt
  417. @classmethod
  418. def utcfromtimestamp(cls, timestamp):
  419. """Returns a datetime object of a given timestamp (in UTC)."""
  420. obj = datetime.datetime.utcfromtimestamp(timestamp)
  421. obj = pytz.utc.localize(obj)
  422. return cls(obj)
  423. @classmethod
  424. def fromtimestamp(cls, timestamp):
  425. """Returns a datetime object of a given timestamp (in local tz)."""
  426. d = cls.utcfromtimestamp(timestamp)
  427. return d.astimezone(localtz())
  428. @classmethod
  429. def utcnow(cls):
  430. """Return a new datetime representing UTC day and time."""
  431. obj = datetime.datetime.utcnow()
  432. obj = cls(obj, tzinfo=pytz.utc)
  433. return obj
  434. @classmethod
  435. def now(cls, tzinfo=None):
  436. """[tz] -> new datetime with tz's local day and time."""
  437. obj = cls.utcnow()
  438. if tzinfo is None:
  439. tzinfo = localtz()
  440. return obj.astimezone(tzinfo)
  441. today = now
  442. @staticmethod
  443. def fromordinal(ordinal):
  444. raise SyntaxError("Not enough information to create a datetime_tz object "
  445. "from an ordinal. Please use datetime.date.fromordinal")
  446. class iterate(object):
  447. """Helpful iterators for working with datetime_tz objects."""
  448. @staticmethod
  449. def between(start, delta, end=None):
  450. """Return an iterator between this date till given end point.
  451. Example usage:
  452. >>> d = datetime_tz.smartparse("5 days ago")
  453. 2008/05/12 11:45
  454. >>> for i in d.between(timedelta(days=1), datetime_tz.now()):
  455. >>> print i
  456. 2008/05/12 11:45
  457. 2008/05/13 11:45
  458. 2008/05/14 11:45
  459. 2008/05/15 11:45
  460. 2008/05/16 11:45
  461. Args:
  462. start: The date to start at.
  463. delta: The interval to iterate with.
  464. end: (Optional) Date to end at. If not given the iterator will never
  465. terminate.
  466. Yields:
  467. datetime_tz objects.
  468. """
  469. toyield = start
  470. while end is None or toyield < end:
  471. yield toyield
  472. toyield += delta
  473. @staticmethod
  474. def weeks(start, end=None):
  475. """Iterate over the weeks between the given datetime_tzs.
  476. Args:
  477. start: datetime_tz to start from.
  478. end: (Optional) Date to end at, if not given the iterator will never
  479. terminate.
  480. Returns:
  481. An iterator which generates datetime_tz objects a week apart.
  482. """
  483. return iterate.between(start, datetime.timedelta(days=7), end)
  484. @staticmethod
  485. def days(start, end=None):
  486. """Iterate over the days between the given datetime_tzs.
  487. Args:
  488. start: datetime_tz to start from.
  489. end: (Optional) Date to end at, if not given the iterator will never
  490. terminate.
  491. Returns:
  492. An iterator which generates datetime_tz objects a day apart.
  493. """
  494. return iterate.between(start, datetime.timedelta(days=1), end)
  495. @staticmethod
  496. def hours(start, end=None):
  497. """Iterate over the hours between the given datetime_tzs.
  498. Args:
  499. start: datetime_tz to start from.
  500. end: (Optional) Date to end at, if not given the iterator will never
  501. terminate.
  502. Returns:
  503. An iterator which generates datetime_tz objects a hour apart.
  504. """
  505. return iterate.between(start, datetime.timedelta(hours=1), end)
  506. @staticmethod
  507. def minutes(start, end=None):
  508. """Iterate over the minutes between the given datetime_tzs.
  509. Args:
  510. start: datetime_tz to start from.
  511. end: (Optional) Date to end at, if not given the iterator will never
  512. terminate.
  513. Returns:
  514. An iterator which generates datetime_tz objects a minute apart.
  515. """
  516. return iterate.between(start, datetime.timedelta(minutes=1), end)
  517. @staticmethod
  518. def seconds(start, end=None):
  519. """Iterate over the seconds between the given datetime_tzs.
  520. Args:
  521. start: datetime_tz to start from.
  522. end: (Optional) Date to end at, if not given the iterator will never
  523. terminate.
  524. Returns:
  525. An iterator which generates datetime_tz objects a second apart.
  526. """
  527. return iterate.between(start, datetime.timedelta(minutes=1), end)
  528. def _wrap_method(name):
  529. """Wrap a method.
  530. Patch a method which might return a datetime.datetime to return a
  531. datetime_tz.datetime_tz instead.
  532. Args:
  533. name: The name of the method to patch
  534. """
  535. method = getattr(datetime.datetime, name)
  536. # Have to give the second argument as method has no __module__ option.
  537. @functools.wraps(method, ("__name__", "__doc__"), ())
  538. def wrapper(*args, **kw):
  539. r = method(*args, **kw)
  540. if isinstance(r, datetime.datetime) and not isinstance(r, datetime_tz):
  541. r = datetime_tz(r)
  542. return r
  543. setattr(datetime_tz, name, wrapper)
  544. for methodname in ["__add__", "__radd__", "__rsub__", "__sub__", "combine"]:
  545. # Make sure we have not already got an override for this method
  546. assert methodname not in datetime_tz.__dict__
  547. _wrap_method(methodname)
  548. __all__ = ['datetime_tz', 'detect_timezone', 'iterate', 'localtz',
  549. 'localtz_set', 'timedelta', '_detect_timezone_environ',
  550. '_detect_timezone_etc_localtime', '_detect_timezone_etc_timezone',
  551. '_detect_timezone_php']