|
- #!/usr/bin/python
- #
- # Copyright 2009 Google Inc.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- #
- #
- # Disable the invalid name warning as we are inheriting from a standard library
- # object.
- # pylint: disable-msg=C6409,W0212
-
- """A version of the datetime module which *cares* about timezones.
-
- This module will never return a naive datetime object. This requires the module
- know your local timezone, which it tries really hard to figure out.
-
- You can override the detection by using the datetime.tzaware.defaulttz_set
- method. It the module is unable to figure out the timezone itself this method
- *must* be called before the normal module is imported. If done before importing
- it can also speed up the time taken to import as the defaulttz will no longer
- try and do the detection.
- """
-
- __author__ = "tansell@google.com (Tim Ansell)"
-
- import calendar
- import datetime
- import os
- import os.path
- import re
- import time
- import warnings
- import dateutil.parser
- import dateutil.relativedelta
- import dateutil.tz
- import pytz
- from . import pytz_abbr
-
-
- try:
- # pylint: disable-msg=C6204
- import functools
- except ImportError as e:
-
- class functools(object):
- """Fake replacement for a full functools."""
-
- # pylint: disable-msg=W0613
- @staticmethod
- def wraps(f, *args, **kw):
- return f
-
-
- # Need to patch pytz.utc to have a _utcoffset so you can normalize/localize
- # using it.
- pytz.utc._utcoffset = datetime.timedelta()
-
-
- timedelta = datetime.timedelta
-
-
- def _tzinfome(tzinfo):
- """Gets a tzinfo object from a string.
-
- Args:
- tzinfo: A string (or string like) object, or a datetime.tzinfo object.
-
- Returns:
- An datetime.tzinfo object.
-
- Raises:
- UnknownTimeZoneError: If the timezone given can't be decoded.
- """
- if not isinstance(tzinfo, datetime.tzinfo):
- try:
- tzinfo = pytz.timezone(tzinfo)
- except AttributeError:
- raise pytz.UnknownTimeZoneError("Unknown timezone! %s" % tzinfo)
- return tzinfo
-
-
- # Our "local" timezone
- _localtz = None
-
-
- def localtz():
- """Get the local timezone.
-
- Returns:
- The localtime timezone as a tzinfo object.
- """
- # pylint: disable-msg=W0603
- global _localtz
- if _localtz is None:
- _localtz = detect_timezone()
- return _localtz
-
-
- def localtz_set(timezone):
- """Set the local timezone."""
- # pylint: disable-msg=W0603
- global _localtz
- _localtz = _tzinfome(timezone)
-
-
- def detect_timezone():
- """Try and detect the timezone that Python is currently running in.
-
- We have a bunch of different methods for trying to figure this out (listed in
- order they are attempted).
- * Try TZ environment variable.
- * Try and find /etc/timezone file (with timezone name).
- * Try and find /etc/localtime file (with timezone data).
- * Try and match a TZ to the current dst/offset/shortname.
-
- Returns:
- The detected local timezone as a tzinfo object
-
- Raises:
- pytz.UnknownTimeZoneError: If it was unable to detect a timezone.
- """
- # First we try the TZ variable
- tz = _detect_timezone_environ()
- if tz is not None:
- return tz
-
- # Second we try /etc/timezone and use the value in that
- tz = _detect_timezone_etc_timezone()
- if tz is not None:
- return tz
-
- # Next we try and see if something matches the tzinfo in /etc/localtime
- tz = _detect_timezone_etc_localtime()
- if tz is not None:
- return tz
-
- # Next we try and use a similiar method to what PHP does.
- # We first try to search on time.tzname, time.timezone, time.daylight to
- # match a pytz zone.
- warnings.warn("Had to fall back to worst detection method (the 'PHP' "
- "method).")
-
- tz = _detect_timezone_php()
- if tz is not None:
- return tz
-
- raise pytz.UnknownTimeZoneError("Unable to detect your timezone!")
-
-
- def _detect_timezone_environ():
- if "TZ" in os.environ:
- try:
- return pytz.timezone(os.environ["TZ"])
- except (IOError, pytz.UnknownTimeZoneError):
- warnings.warn("You provided a TZ environment value (%r) we did not "
- "understand!" % os.environ["TZ"])
-
-
- def _detect_timezone_etc_timezone():
- if os.path.exists("/etc/timezone"):
- try:
- tz = file("/etc/timezone").read().strip()
- try:
- return pytz.timezone(tz)
- except (IOError, pytz.UnknownTimeZoneError) as ei:
- warnings.warn("Your /etc/timezone file references a timezone (%r) that"
- " is not valid (%r)." % (tz, ei))
-
- # Problem reading the /etc/timezone file
- except IOError as eo:
- warnings.warn("Could not access your /etc/timezone file: %s" % eo)
-
-
- def _detect_timezone_etc_localtime():
- matches = []
- if os.path.exists("/etc/localtime"):
- localtime = pytz.tzfile.build_tzinfo("/etc/localtime",
- file("/etc/localtime"))
-
- # See if we can find a "Human Name" for this..
- for tzname in pytz.all_timezones:
- tz = _tzinfome(tzname)
-
- if dir(tz) != dir(localtime):
- continue
-
- for attrib in dir(tz):
- # Ignore functions and specials
- if callable(getattr(tz, attrib)) or attrib.startswith("__"):
- continue
-
- # This will always be different
- if attrib == "zone" or attrib == "_tzinfos":
- continue
-
- if getattr(tz, attrib) != getattr(localtime, attrib):
- break
-
- # We get here iff break didn't happen, i.e. no meaningful attributes
- # differ between tz and localtime
- else:
- matches.append(tzname)
-
- if len(matches) == 1:
- return _tzinfome(matches[0])
- else:
- # Warn the person about this!
- warning = "Could not get a human name for your timezone: "
- if len(matches) > 1:
- warning += ("We detected multiple matches for your /etc/localtime. "
- "(Matches where %s)" % matches)
- return _tzinfome(matches[0])
- else:
- warning += "We detected no matches for your /etc/localtime."
- warnings.warn(warning)
-
- # Register /etc/localtime as the timezone loaded.
- pytz._tzinfo_cache['/etc/localtime'] = localtime
- return localtime
-
-
- def _detect_timezone_php():
- tomatch = (time.tzname[0], time.timezone, time.daylight)
- now = datetime.datetime.now()
-
- matches = []
- for tzname in pytz.all_timezones:
- try:
- tz = pytz.timezone(tzname)
- except IOError:
- continue
-
- try:
- indst = tz.localize(now).timetuple()[-1]
-
- if tomatch == (tz._tzname, -tz._utcoffset.seconds, indst):
- matches.append(tzname)
-
- # pylint: disable-msg=W0704
- except AttributeError:
- pass
-
- if len(matches) > 1:
- warnings.warn("We detected multiple matches for the timezone, choosing "
- "the first %s. (Matches where %s)" % (matches[0], matches))
- return pytz.timezone(matches[0])
-
-
- class datetime_tz(datetime.datetime):
- """An extension of the inbuilt datetime adding more functionality.
-
- The extra functionality includes:
- * Partial parsing support (IE 2006/02/30 matches %Y/%M/%D %H:%M)
- * Full integration with pytz (just give it the string of the timezone!)
- * Proper support for going to/from Unix timestamps (which are in UTC!).
- """
- __slots__ = ["is_dst"]
-
- def __new__(cls, *args, **kw):
- args = list(args)
- if not args:
- raise TypeError("Not enough arguments given.")
-
- # See if we are given a tzinfo object...
- tzinfo = None
- if isinstance(args[-1], (datetime.tzinfo, str)):
- tzinfo = _tzinfome(args.pop(-1))
- elif kw.get("tzinfo", None) is not None:
- tzinfo = _tzinfome(kw.pop("tzinfo"))
-
- # Create a datetime object if we don't have one
- if isinstance(args[0], datetime.datetime):
- # Convert the datetime instance to a datetime object.
- newargs = (list(args[0].timetuple()[0:6]) +
- [args[0].microsecond, args[0].tzinfo])
- dt = datetime.datetime(*newargs)
-
- if tzinfo is None and dt.tzinfo is None:
- raise TypeError("Must specify a timezone!")
-
- if tzinfo is not None and dt.tzinfo is not None:
- raise TypeError("Can not give a timezone with timezone aware"
- " datetime object! (Use localize.)")
- else:
- dt = datetime.datetime(*args, **kw)
-
- if dt.tzinfo is not None:
- # Re-normalize the dt object
- dt = dt.tzinfo.normalize(dt)
-
- else:
- if tzinfo is None:
- tzinfo = localtz()
-
- try:
- dt = tzinfo.localize(dt, is_dst=None)
- except pytz.AmbiguousTimeError:
- is_dst = None
- if "is_dst" in kw:
- is_dst = kw.pop("is_dst")
-
- try:
- dt = tzinfo.localize(dt, is_dst)
- except IndexError:
- raise pytz.AmbiguousTimeError("No such time exists!")
-
- newargs = list(dt.timetuple()[0:6])+[dt.microsecond, dt.tzinfo]
- obj = datetime.datetime.__new__(cls, *newargs)
- obj.is_dst = obj.dst() != datetime.timedelta(0)
- return obj
-
- def asdatetime(self, naive=True):
- """Return this datetime_tz as a datetime object.
-
- Args:
- naive: Return *without* any tz info.
-
- Returns:
- This datetime_tz as a datetime object.
- """
- args = list(self.timetuple()[0:6])+[self.microsecond]
- if not naive:
- args.append(self.tzinfo)
- return datetime.datetime(*args)
-
- def asdate(self):
- """Return this datetime_tz as a date object.
-
- Returns:
- This datetime_tz as a date object.
- """
- return datetime.date(self.year, self.month, self.day)
-
- def totimestamp(self):
- """Convert this datetime object back to a unix timestamp.
-
- The Unix epoch is the time 00:00:00 UTC on January 1, 1970.
-
- Returns:
- Unix timestamp.
- """
- return calendar.timegm(self.utctimetuple())+1e-6*self.microsecond
-
- def astimezone(self, tzinfo):
- """Returns a version of this timestamp converted to the given timezone.
-
- Args:
- tzinfo: Either a datetime.tzinfo object or a string (which will be looked
- up in pytz.
-
- Returns:
- A datetime_tz object in the given timezone.
- """
- # Assert we are not a naive datetime object
- assert self.tzinfo is not None
-
- tzinfo = _tzinfome(tzinfo)
-
- d = self.asdatetime(naive=False).astimezone(tzinfo)
- return datetime_tz(d)
-
- # pylint: disable-msg=C6113
- def replace(self, **kw):
- """Return datetime with new specified fields given as arguments.
-
- For example, dt.replace(days=4) would return a new datetime_tz object with
- exactly the same as dt but with the days attribute equal to 4.
-
- Any attribute can be replaced, but tzinfo can not be set to None.
-
- Args:
- Any datetime_tz attribute.
-
- Returns:
- A datetime_tz object with the attributes replaced.
-
- Raises:
- TypeError: If the given replacement is invalid.
- """
- if "tzinfo" in kw:
- if kw["tzinfo"] is None:
- raise TypeError("Can not remove the timezone use asdatetime()")
-
- is_dst = None
- if "is_dst" in kw:
- is_dst = kw["is_dst"]
- del kw["is_dst"]
- else:
- # Use our own DST setting..
- is_dst = self.is_dst
-
- replaced = self.asdatetime().replace(**kw)
-
- return datetime_tz(replaced, tzinfo=self.tzinfo.zone, is_dst=is_dst)
-
- # pylint: disable-msg=C6310
- @classmethod
- def smartparse(cls, toparse, tzinfo=None):
- """Method which uses dateutil.parse and extras to try and parse the string.
-
- Valid dates are found at:
- http://labix.org/python-dateutil#head-1443e0f14ad5dff07efd465e080d1110920673d8-2
-
- Other valid formats include:
- "now" or "today"
- "yesterday"
- "tommorrow"
- "5 minutes ago"
- "10 hours ago"
- "10h5m ago"
- "start of yesterday"
- "end of tommorrow"
- "end of 3rd of March"
-
- Args:
- toparse: The string to parse.
- tzinfo: Timezone for the resultant datetime_tz object should be in.
- (Defaults to your local timezone.)
-
- Returns:
- New datetime_tz object.
-
- Raises:
- ValueError: If unable to make sense of the input.
- """
- # Default for empty fields are:
- # year/month/day == now
- # hour/minute/second/microsecond == 0
- toparse = toparse.strip()
-
- if tzinfo is None:
- dt = cls.now()
- else:
- dt = cls.now(tzinfo)
-
- default = dt.replace(hour=0, minute=0, second=0, microsecond=0)
-
- # Remove "start of " and "end of " prefix in the string
- if toparse.lower().startswith("end of "):
- toparse = toparse[7:].strip()
-
- dt += datetime.timedelta(days=1)
- dt = dt.replace(hour=0, minute=0, second=0, microsecond=0)
- dt -= datetime.timedelta(microseconds=1)
-
- default = dt
-
- elif toparse.lower().startswith("start of "):
- toparse = toparse[9:].strip()
-
- dt = dt.replace(hour=0, minute=0, second=0, microsecond=0)
- default = dt
-
- # Handle strings with "now", "today", "yesterday", "tomorrow" and "ago".
- # Need to use lowercase
- toparselower = toparse.lower()
-
- if toparselower in ["now", "today"]:
- pass
-
- elif toparselower == "yesterday":
- dt -= datetime.timedelta(days=1)
-
- elif toparselower == "tommorrow":
- dt += datetime.timedelta(days=1)
-
- elif "ago" in toparselower:
- # Remove the "ago" bit
- toparselower = toparselower[:-3]
- # Replace all "a day and an hour" with "1 day 1 hour"
- toparselower = toparselower.replace("a ", "1 ")
- toparselower = toparselower.replace("an ", "1 ")
- toparselower = toparselower.replace(" and ", " ")
-
- # Match the following
- # 1 hour ago
- # 1h ago
- # 1 h ago
- # 1 hour ago
- # 2 hours ago
- # Same with minutes, seconds, etc.
-
- tocheck = ("seconds", "minutes", "hours", "days", "weeks", "months",
- "years")
- result = {}
- for match in re.finditer("([0-9]+)([^0-9]*)", toparselower):
- amount = int(match.group(1))
- unit = match.group(2).strip()
-
- for bit in tocheck:
- regex = "^([%s]|((%s)s?))$" % (
- bit[0], bit[:-1])
-
- bitmatch = re.search(regex, unit)
- if bitmatch:
- result[bit] = amount
- break
- else:
- raise ValueError("Was not able to parse date unit %r!" % unit)
-
- delta = dateutil.relativedelta.relativedelta(**result)
- dt -= delta
-
- else:
- # Handle strings with normal datetime format, use original case.
- dt = dateutil.parser.parse(toparse, default=default.asdatetime(),
- tzinfos=pytz_abbr.tzinfos)
- if dt is None:
- raise ValueError("Was not able to parse date!")
-
- if dt.tzinfo is pytz_abbr.unknown:
- dt = dt.replace(tzinfo=None)
-
- if dt.tzinfo is None:
- if tzinfo is None:
- tzinfo = localtz()
- dt = cls(dt, tzinfo)
- else:
- if isinstance(dt.tzinfo, pytz_abbr.tzabbr):
- abbr = dt.tzinfo
- dt = dt.replace(tzinfo=None)
- dt = cls(dt, abbr.zone, is_dst=abbr.dst)
-
- dt = cls(dt)
-
- return dt
-
- @classmethod
- def utcfromtimestamp(cls, timestamp):
- """Returns a datetime object of a given timestamp (in UTC)."""
- obj = datetime.datetime.utcfromtimestamp(timestamp)
- obj = pytz.utc.localize(obj)
- return cls(obj)
-
- @classmethod
- def fromtimestamp(cls, timestamp):
- """Returns a datetime object of a given timestamp (in local tz)."""
- d = cls.utcfromtimestamp(timestamp)
- return d.astimezone(localtz())
-
- @classmethod
- def utcnow(cls):
- """Return a new datetime representing UTC day and time."""
- obj = datetime.datetime.utcnow()
- obj = cls(obj, tzinfo=pytz.utc)
- return obj
-
- @classmethod
- def now(cls, tzinfo=None):
- """[tz] -> new datetime with tz's local day and time."""
- obj = cls.utcnow()
- if tzinfo is None:
- tzinfo = localtz()
- return obj.astimezone(tzinfo)
-
- today = now
-
- @staticmethod
- def fromordinal(ordinal):
- raise SyntaxError("Not enough information to create a datetime_tz object "
- "from an ordinal. Please use datetime.date.fromordinal")
-
-
- class iterate(object):
- """Helpful iterators for working with datetime_tz objects."""
-
- @staticmethod
- def between(start, delta, end=None):
- """Return an iterator between this date till given end point.
-
- Example usage:
- >>> d = datetime_tz.smartparse("5 days ago")
- 2008/05/12 11:45
- >>> for i in d.between(timedelta(days=1), datetime_tz.now()):
- >>> print i
- 2008/05/12 11:45
- 2008/05/13 11:45
- 2008/05/14 11:45
- 2008/05/15 11:45
- 2008/05/16 11:45
-
- Args:
- start: The date to start at.
- delta: The interval to iterate with.
- end: (Optional) Date to end at. If not given the iterator will never
- terminate.
-
- Yields:
- datetime_tz objects.
- """
- toyield = start
- while end is None or toyield < end:
- yield toyield
- toyield += delta
-
- @staticmethod
- def weeks(start, end=None):
- """Iterate over the weeks between the given datetime_tzs.
-
- Args:
- start: datetime_tz to start from.
- end: (Optional) Date to end at, if not given the iterator will never
- terminate.
-
- Returns:
- An iterator which generates datetime_tz objects a week apart.
- """
- return iterate.between(start, datetime.timedelta(days=7), end)
-
- @staticmethod
- def days(start, end=None):
- """Iterate over the days between the given datetime_tzs.
-
- Args:
- start: datetime_tz to start from.
- end: (Optional) Date to end at, if not given the iterator will never
- terminate.
-
- Returns:
- An iterator which generates datetime_tz objects a day apart.
- """
- return iterate.between(start, datetime.timedelta(days=1), end)
-
- @staticmethod
- def hours(start, end=None):
- """Iterate over the hours between the given datetime_tzs.
-
- Args:
- start: datetime_tz to start from.
- end: (Optional) Date to end at, if not given the iterator will never
- terminate.
-
- Returns:
- An iterator which generates datetime_tz objects a hour apart.
- """
- return iterate.between(start, datetime.timedelta(hours=1), end)
-
- @staticmethod
- def minutes(start, end=None):
- """Iterate over the minutes between the given datetime_tzs.
-
- Args:
- start: datetime_tz to start from.
- end: (Optional) Date to end at, if not given the iterator will never
- terminate.
-
- Returns:
- An iterator which generates datetime_tz objects a minute apart.
- """
- return iterate.between(start, datetime.timedelta(minutes=1), end)
-
- @staticmethod
- def seconds(start, end=None):
- """Iterate over the seconds between the given datetime_tzs.
-
- Args:
- start: datetime_tz to start from.
- end: (Optional) Date to end at, if not given the iterator will never
- terminate.
-
- Returns:
- An iterator which generates datetime_tz objects a second apart.
- """
- return iterate.between(start, datetime.timedelta(minutes=1), end)
-
-
- def _wrap_method(name):
- """Wrap a method.
-
- Patch a method which might return a datetime.datetime to return a
- datetime_tz.datetime_tz instead.
-
- Args:
- name: The name of the method to patch
- """
- method = getattr(datetime.datetime, name)
-
- # Have to give the second argument as method has no __module__ option.
- @functools.wraps(method, ("__name__", "__doc__"), ())
- def wrapper(*args, **kw):
- r = method(*args, **kw)
-
- if isinstance(r, datetime.datetime) and not isinstance(r, datetime_tz):
- r = datetime_tz(r)
- return r
-
- setattr(datetime_tz, name, wrapper)
-
- for methodname in ["__add__", "__radd__", "__rsub__", "__sub__", "combine"]:
-
- # Make sure we have not already got an override for this method
- assert methodname not in datetime_tz.__dict__
-
- _wrap_method(methodname)
-
-
- __all__ = ['datetime_tz', 'detect_timezone', 'iterate', 'localtz',
- 'localtz_set', 'timedelta', '_detect_timezone_environ',
- '_detect_timezone_etc_localtime', '_detect_timezone_etc_timezone',
- '_detect_timezone_php']
|