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.
525 lines
15 KiB
525 lines
15 KiB
#!/usr/bin/env python |
|
# Copyright 2002 Google Inc. All Rights Reserved. |
|
# |
|
# 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. |
|
|
|
"""Set of classes and functions for dealing with dates and timestamps. |
|
|
|
The BaseTimestamp and Timestamp are timezone-aware wrappers around Python |
|
datetime.datetime class. |
|
""" |
|
|
|
|
|
|
|
import calendar |
|
import copy |
|
import datetime |
|
import re |
|
import sys |
|
import time |
|
import types |
|
import warnings |
|
|
|
import dateutil.parser |
|
import pytz |
|
|
|
|
|
_MICROSECONDS_PER_SECOND = 1000000 |
|
_MICROSECONDS_PER_SECOND_F = float(_MICROSECONDS_PER_SECOND) |
|
|
|
|
|
def SecondsToMicroseconds(seconds): |
|
"""Convert seconds to microseconds. |
|
|
|
Args: |
|
seconds: number |
|
Returns: |
|
microseconds |
|
""" |
|
return seconds * _MICROSECONDS_PER_SECOND |
|
|
|
|
|
def MicrosecondsToSeconds(microseconds): |
|
"""Convert microseconds to seconds. |
|
|
|
Args: |
|
microseconds: A number representing some duration of time measured in |
|
microseconds. |
|
Returns: |
|
A number representing the same duration of time measured in seconds. |
|
""" |
|
return microseconds / _MICROSECONDS_PER_SECOND_F |
|
|
|
|
|
def _GetCurrentTimeMicros(): |
|
"""Get the current time in microseconds, in UTC. |
|
|
|
Returns: |
|
The number of microseconds since the epoch. |
|
""" |
|
return int(SecondsToMicroseconds(time.time())) |
|
|
|
|
|
def GetSecondsSinceEpoch(time_tuple): |
|
"""Convert time_tuple (in UTC) to seconds (also in UTC). |
|
|
|
Args: |
|
time_tuple: tuple with at least 6 items. |
|
Returns: |
|
seconds. |
|
""" |
|
return calendar.timegm(time_tuple[:6] + (0, 0, 0)) |
|
|
|
|
|
def GetTimeMicros(time_tuple): |
|
"""Get a time in microseconds. |
|
|
|
Arguments: |
|
time_tuple: A (year, month, day, hour, minute, second) tuple (the python |
|
time tuple format) in the UTC time zone. |
|
|
|
Returns: |
|
The number of microseconds since the epoch represented by the input tuple. |
|
""" |
|
return int(SecondsToMicroseconds(GetSecondsSinceEpoch(time_tuple))) |
|
|
|
|
|
def DatetimeToUTCMicros(date): |
|
"""Converts a datetime object to microseconds since the epoch in UTC. |
|
|
|
Args: |
|
date: A datetime to convert. |
|
Returns: |
|
The number of microseconds since the epoch, in UTC, represented by the input |
|
datetime. |
|
""" |
|
# Using this guide: http://wiki.python.org/moin/WorkingWithTime |
|
# And this conversion guide: http://docs.python.org/library/time.html |
|
|
|
# Turn the date parameter into a tuple (struct_time) that can then be |
|
# manipulated into a long value of seconds. During the conversion from |
|
# struct_time to long, the source date in UTC, and so it follows that the |
|
# correct transformation is calendar.timegm() |
|
micros = calendar.timegm(date.utctimetuple()) * _MICROSECONDS_PER_SECOND |
|
return micros + date.microsecond |
|
|
|
|
|
def DatetimeToUTCMillis(date): |
|
"""Converts a datetime object to milliseconds since the epoch in UTC. |
|
|
|
Args: |
|
date: A datetime to convert. |
|
Returns: |
|
The number of milliseconds since the epoch, in UTC, represented by the input |
|
datetime. |
|
""" |
|
return DatetimeToUTCMicros(date) / 1000 |
|
|
|
|
|
def UTCMicrosToDatetime(micros, tz=None): |
|
"""Converts a microsecond epoch time to a datetime object. |
|
|
|
Args: |
|
micros: A UTC time, expressed in microseconds since the epoch. |
|
tz: The desired tzinfo for the datetime object. If None, the |
|
datetime will be naive. |
|
Returns: |
|
The datetime represented by the input value. |
|
""" |
|
# The conversion from micros to seconds for input into the |
|
# utcfromtimestamp function needs to be done as a float to make sure |
|
# we dont lose the sub-second resolution of the input time. |
|
dt = datetime.datetime.utcfromtimestamp( |
|
micros / _MICROSECONDS_PER_SECOND_F) |
|
if tz is not None: |
|
dt = tz.fromutc(dt) |
|
return dt |
|
|
|
|
|
def UTCMillisToDatetime(millis, tz=None): |
|
"""Converts a millisecond epoch time to a datetime object. |
|
|
|
Args: |
|
millis: A UTC time, expressed in milliseconds since the epoch. |
|
tz: The desired tzinfo for the datetime object. If None, the |
|
datetime will be naive. |
|
Returns: |
|
The datetime represented by the input value. |
|
""" |
|
return UTCMicrosToDatetime(millis * 1000, tz) |
|
|
|
|
|
UTC = pytz.UTC |
|
US_PACIFIC = pytz.timezone('US/Pacific') |
|
|
|
|
|
class TimestampError(ValueError): |
|
"""Generic timestamp-related error.""" |
|
pass |
|
|
|
|
|
class TimezoneNotSpecifiedError(TimestampError): |
|
"""This error is raised when timezone is not specified.""" |
|
pass |
|
|
|
|
|
class TimeParseError(TimestampError): |
|
"""This error is raised when we can't parse the input.""" |
|
pass |
|
|
|
|
|
# TODO(user): this class needs to handle daylight better |
|
|
|
|
|
class LocalTimezoneClass(datetime.tzinfo): |
|
"""This class defines local timezone.""" |
|
|
|
ZERO = datetime.timedelta(0) |
|
HOUR = datetime.timedelta(hours=1) |
|
|
|
STDOFFSET = datetime.timedelta(seconds=-time.timezone) |
|
if time.daylight: |
|
DSTOFFSET = datetime.timedelta(seconds=-time.altzone) |
|
else: |
|
DSTOFFSET = STDOFFSET |
|
|
|
DSTDIFF = DSTOFFSET - STDOFFSET |
|
|
|
def utcoffset(self, dt): |
|
"""datetime -> minutes east of UTC (negative for west of UTC).""" |
|
if self._isdst(dt): |
|
return self.DSTOFFSET |
|
else: |
|
return self.STDOFFSET |
|
|
|
def dst(self, dt): |
|
"""datetime -> DST offset in minutes east of UTC.""" |
|
if self._isdst(dt): |
|
return self.DSTDIFF |
|
else: |
|
return self.ZERO |
|
|
|
def tzname(self, dt): |
|
"""datetime -> string name of time zone.""" |
|
return time.tzname[self._isdst(dt)] |
|
|
|
def _isdst(self, dt): |
|
"""Return true if given datetime is within local DST.""" |
|
tt = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, |
|
dt.weekday(), 0, -1) |
|
stamp = time.mktime(tt) |
|
tt = time.localtime(stamp) |
|
return tt.tm_isdst > 0 |
|
|
|
def __repr__(self): |
|
"""Return string '<Local>'.""" |
|
return '<Local>' |
|
|
|
def localize(self, dt, unused_is_dst=False): |
|
"""Convert naive time to local time.""" |
|
if dt.tzinfo is not None: |
|
raise ValueError('Not naive datetime (tzinfo is already set)') |
|
return dt.replace(tzinfo=self) |
|
|
|
def normalize(self, dt, unused_is_dst=False): |
|
"""Correct the timezone information on the given datetime.""" |
|
if dt.tzinfo is None: |
|
raise ValueError('Naive time - no tzinfo set') |
|
return dt.replace(tzinfo=self) |
|
|
|
|
|
LocalTimezone = LocalTimezoneClass() |
|
|
|
|
|
class BaseTimestamp(datetime.datetime): |
|
"""Our kind of wrapper over datetime.datetime. |
|
|
|
The objects produced by methods now, today, fromtimestamp, utcnow, |
|
utcfromtimestamp are timezone-aware (with correct timezone). |
|
We also overload __add__ and __sub__ method, to fix the result of arithmetic |
|
operations. |
|
""" |
|
|
|
LocalTimezone = LocalTimezone |
|
|
|
@classmethod |
|
def AddLocalTimezone(cls, obj): |
|
"""If obj is naive, add local timezone to it.""" |
|
if not obj.tzinfo: |
|
return obj.replace(tzinfo=cls.LocalTimezone) |
|
return obj |
|
|
|
@classmethod |
|
def Localize(cls, obj): |
|
"""If obj is naive, localize it to cls.LocalTimezone.""" |
|
if not obj.tzinfo: |
|
return cls.LocalTimezone.localize(obj) |
|
return obj |
|
|
|
def __add__(self, *args, **kwargs): |
|
"""x.__add__(y) <==> x+y.""" |
|
r = super(BaseTimestamp, self).__add__(*args, **kwargs) |
|
return type(self)(r.year, r.month, r.day, r.hour, r.minute, r.second, |
|
r.microsecond, r.tzinfo) |
|
|
|
def __sub__(self, *args, **kwargs): |
|
"""x.__add__(y) <==> x-y.""" |
|
r = super(BaseTimestamp, self).__sub__(*args, **kwargs) |
|
if isinstance(r, datetime.datetime): |
|
return type(self)(r.year, r.month, r.day, r.hour, r.minute, r.second, |
|
r.microsecond, r.tzinfo) |
|
return r |
|
|
|
@classmethod |
|
def now(cls, *args, **kwargs): |
|
"""Get a timestamp corresponding to right now. |
|
|
|
Args: |
|
args: Positional arguments to pass to datetime.datetime.now(). |
|
kwargs: Keyword arguments to pass to datetime.datetime.now(). If tz is not |
|
specified, local timezone is assumed. |
|
|
|
Returns: |
|
A new BaseTimestamp with tz's local day and time. |
|
""" |
|
return cls.AddLocalTimezone( |
|
super(BaseTimestamp, cls).now(*args, **kwargs)) |
|
|
|
@classmethod |
|
def today(cls): |
|
"""Current BaseTimestamp. |
|
|
|
Same as self.__class__.fromtimestamp(time.time()). |
|
Returns: |
|
New self.__class__. |
|
""" |
|
return cls.AddLocalTimezone(super(BaseTimestamp, cls).today()) |
|
|
|
@classmethod |
|
def fromtimestamp(cls, *args, **kwargs): |
|
"""Get a new localized timestamp from a POSIX timestamp. |
|
|
|
Args: |
|
args: Positional arguments to pass to datetime.datetime.fromtimestamp(). |
|
kwargs: Keyword arguments to pass to datetime.datetime.fromtimestamp(). |
|
If tz is not specified, local timezone is assumed. |
|
|
|
Returns: |
|
A new BaseTimestamp with tz's local day and time. |
|
""" |
|
return cls.Localize( |
|
super(BaseTimestamp, cls).fromtimestamp(*args, **kwargs)) |
|
|
|
@classmethod |
|
def utcnow(cls): |
|
"""Return a new BaseTimestamp representing UTC day and time.""" |
|
return super(BaseTimestamp, cls).utcnow().replace(tzinfo=pytz.utc) |
|
|
|
@classmethod |
|
def utcfromtimestamp(cls, *args, **kwargs): |
|
"""timestamp -> UTC datetime from a POSIX timestamp (like time.time()).""" |
|
return super(BaseTimestamp, cls).utcfromtimestamp( |
|
*args, **kwargs).replace(tzinfo=pytz.utc) |
|
|
|
@classmethod |
|
def strptime(cls, date_string, format, tz=None): |
|
"""Parse date_string according to format and construct BaseTimestamp. |
|
|
|
Args: |
|
date_string: string passed to time.strptime. |
|
format: format string passed to time.strptime. |
|
tz: if not specified, local timezone assumed. |
|
Returns: |
|
New BaseTimestamp. |
|
""" |
|
date_time = super(BaseTimestamp, cls).strptime(date_string, format) |
|
return (tz.localize if tz else cls.Localize)(date_time) |
|
|
|
def astimezone(self, *args, **kwargs): |
|
"""tz -> convert to time in new timezone tz.""" |
|
r = super(BaseTimestamp, self).astimezone(*args, **kwargs) |
|
return type(self)(r.year, r.month, r.day, r.hour, r.minute, r.second, |
|
r.microsecond, r.tzinfo) |
|
|
|
@classmethod |
|
def FromMicroTimestamp(cls, ts): |
|
"""Create new Timestamp object from microsecond UTC timestamp value. |
|
|
|
Args: |
|
ts: integer microsecond UTC timestamp |
|
Returns: |
|
New cls() |
|
""" |
|
return cls.utcfromtimestamp(ts/_MICROSECONDS_PER_SECOND_F) |
|
|
|
def AsSecondsSinceEpoch(self): |
|
"""Return number of seconds since epoch (timestamp in seconds).""" |
|
return GetSecondsSinceEpoch(self.utctimetuple()) |
|
|
|
def AsMicroTimestamp(self): |
|
"""Return microsecond timestamp constructed from this object.""" |
|
return (SecondsToMicroseconds(self.AsSecondsSinceEpoch()) + |
|
self.microsecond) |
|
|
|
@classmethod |
|
def combine(cls, datepart, timepart, tz=None): |
|
"""Combine date and time into timestamp, timezone-aware. |
|
|
|
Args: |
|
datepart: datetime.date |
|
timepart: datetime.time |
|
tz: timezone or None |
|
Returns: |
|
timestamp object |
|
""" |
|
result = super(BaseTimestamp, cls).combine(datepart, timepart) |
|
if tz: |
|
result = tz.localize(result) |
|
return result |
|
|
|
|
|
# Conversions from interval suffixes to number of seconds. |
|
# (m => 60s, d => 86400s, etc) |
|
_INTERVAL_CONV_DICT = {'s': 1} |
|
_INTERVAL_CONV_DICT['m'] = 60 * _INTERVAL_CONV_DICT['s'] |
|
_INTERVAL_CONV_DICT['h'] = 60 * _INTERVAL_CONV_DICT['m'] |
|
_INTERVAL_CONV_DICT['d'] = 24 * _INTERVAL_CONV_DICT['h'] |
|
_INTERVAL_CONV_DICT['D'] = _INTERVAL_CONV_DICT['d'] |
|
_INTERVAL_CONV_DICT['w'] = 7 * _INTERVAL_CONV_DICT['d'] |
|
_INTERVAL_CONV_DICT['W'] = _INTERVAL_CONV_DICT['w'] |
|
_INTERVAL_CONV_DICT['M'] = 30 * _INTERVAL_CONV_DICT['d'] |
|
_INTERVAL_CONV_DICT['Y'] = 365 * _INTERVAL_CONV_DICT['d'] |
|
_INTERVAL_REGEXP = re.compile('^([0-9]+)([%s])?' % ''.join(_INTERVAL_CONV_DICT)) |
|
|
|
|
|
def ConvertIntervalToSeconds(interval): |
|
"""Convert a formatted string representing an interval into seconds. |
|
|
|
Args: |
|
interval: String to interpret as an interval. A basic interval looks like |
|
"<number><suffix>". Complex intervals consisting of a chain of basic |
|
intervals are also allowed. |
|
|
|
Returns: |
|
An integer representing the number of seconds represented by the interval |
|
string, or None if the interval string could not be decoded. |
|
""" |
|
total = 0 |
|
while interval: |
|
match = _INTERVAL_REGEXP.match(interval) |
|
if not match: |
|
return None |
|
|
|
try: |
|
num = int(match.group(1)) |
|
except ValueError: |
|
return None |
|
|
|
suffix = match.group(2) |
|
if suffix: |
|
multiplier = _INTERVAL_CONV_DICT.get(suffix) |
|
if not multiplier: |
|
return None |
|
num *= multiplier |
|
|
|
total += num |
|
interval = interval[match.end(0):] |
|
return total |
|
|
|
|
|
class Timestamp(BaseTimestamp): |
|
"""This subclass contains methods to parse W3C and interval date spec. |
|
|
|
The interval date specification is in the form "1D", where "D" can be |
|
"s"econds "m"inutes "h"ours "D"ays "W"eeks "M"onths "Y"ears. |
|
""" |
|
INTERVAL_CONV_DICT = _INTERVAL_CONV_DICT |
|
INTERVAL_REGEXP = _INTERVAL_REGEXP |
|
|
|
@classmethod |
|
def _StringToTime(cls, timestring, tz=None): |
|
"""Use dateutil.parser to convert string into timestamp. |
|
|
|
dateutil.parser understands ISO8601 which is really handy. |
|
|
|
Args: |
|
timestring: string with datetime |
|
tz: optional timezone, if timezone is omitted from timestring. |
|
|
|
Returns: |
|
New Timestamp or None if unable to parse the timestring. |
|
""" |
|
try: |
|
r = dateutil.parser.parse(timestring) |
|
# dateutil will raise ValueError if it's an unknown format -- or |
|
# TypeError in some cases, due to bugs. |
|
except (TypeError, ValueError): |
|
return None |
|
if not r.tzinfo: |
|
r = (tz or cls.LocalTimezone).localize(r) |
|
result = cls(r.year, r.month, r.day, r.hour, r.minute, r.second, |
|
r.microsecond, r.tzinfo) |
|
|
|
return result |
|
|
|
@classmethod |
|
def _IntStringToInterval(cls, timestring): |
|
"""Parse interval date specification and create a timedelta object. |
|
|
|
Args: |
|
timestring: string interval. |
|
|
|
Returns: |
|
A datetime.timedelta representing the specified interval or None if |
|
unable to parse the timestring. |
|
""" |
|
seconds = ConvertIntervalToSeconds(timestring) |
|
return datetime.timedelta(seconds=seconds) if seconds else None |
|
|
|
@classmethod |
|
def FromString(cls, value, tz=None): |
|
"""Create a Timestamp from a string. |
|
|
|
Args: |
|
value: String interval or datetime. |
|
e.g. "2013-01-05 13:00:00" or "1d" |
|
tz: optional timezone, if timezone is omitted from timestring. |
|
|
|
Returns: |
|
A new Timestamp. |
|
|
|
Raises: |
|
TimeParseError if unable to parse value. |
|
""" |
|
result = cls._StringToTime(value, tz=tz) |
|
if result: |
|
return result |
|
|
|
result = cls._IntStringToInterval(value) |
|
if result: |
|
return cls.utcnow() - result |
|
|
|
raise TimeParseError(value) |
|
|
|
|
|
# What's written below is a clear python bug. I mean, okay, I can apply |
|
# negative timezone to it and end result will be inconversible. |
|
|
|
MAXIMUM_PYTHON_TIMESTAMP = Timestamp( |
|
9999, 12, 31, 23, 59, 59, 999999, UTC) |
|
|
|
# This is also a bug. It is called 32bit time_t. I hate it. |
|
# This is fixed in 2.5, btw. |
|
|
|
MAXIMUM_MICROSECOND_TIMESTAMP = 0x80000000 * _MICROSECONDS_PER_SECOND - 1 |
|
MAXIMUM_MICROSECOND_TIMESTAMP_AS_TS = Timestamp(2038, 1, 19, 3, 14, 7, 999999)
|
|
|