Refine Timestamp APIs (#395)

This commit is contained in:
Inada Naoki 2019-12-12 19:43:59 +09:00 committed by GitHub
parent aab29ff277
commit 887d3a7d22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 69 additions and 50 deletions

View file

@ -7,8 +7,11 @@ import struct
PY2 = sys.version_info[0] == 2 PY2 = sys.version_info[0] == 2
if not PY2: if PY2:
long = int int_types = (int, long)
_utc = None
else:
int_types = int
try: try:
_utc = datetime.timezone.utc _utc = datetime.timezone.utc
except AttributeError: except AttributeError:
@ -23,8 +26,6 @@ class ExtType(namedtuple("ExtType", "code data")):
raise TypeError("code must be int") raise TypeError("code must be int")
if not isinstance(data, bytes): if not isinstance(data, bytes):
raise TypeError("data must be bytes") raise TypeError("data must be bytes")
if code == -1:
return Timestamp.from_bytes(data)
if not 0 <= code <= 127: if not 0 <= code <= 127:
raise ValueError("code must be 0~127") raise ValueError("code must be 0~127")
return super(ExtType, cls).__new__(cls, code, data) return super(ExtType, cls).__new__(cls, code, data)
@ -42,34 +43,26 @@ class Timestamp(object):
def __init__(self, seconds, nanoseconds=0): def __init__(self, seconds, nanoseconds=0):
"""Initialize a Timestamp object. """Initialize a Timestamp object.
:param seconds: Number of seconds since the UNIX epoch (00:00:00 UTC Jan 1 1970, minus leap seconds). May be :param int seconds:
negative. If :code:`seconds` includes a fractional part, :code:`nanoseconds` must be 0. Number of seconds since the UNIX epoch (00:00:00 UTC Jan 1 1970, minus leap seconds).
:type seconds: int or float May be negative.
:param nanoseconds: Number of nanoseconds to add to `seconds` to get fractional time. Maximum is 999_999_999. :param int nanoseconds:
Default is 0. Number of nanoseconds to add to `seconds` to get fractional time.
:type nanoseconds: int Maximum is 999_999_999. Default is 0.
Note: Negative times (before the UNIX epoch) are represented as negative seconds + positive ns. Note: Negative times (before the UNIX epoch) are represented as negative seconds + positive ns.
""" """
if not isinstance(seconds, (int, long, float)): if not isinstance(seconds, int_types):
raise TypeError("seconds must be numeric") raise TypeError("seconds must be an interger")
if not isinstance(nanoseconds, (int, long)): if not isinstance(nanoseconds, int_types):
raise TypeError("nanoseconds must be an integer") raise TypeError("nanoseconds must be an integer")
if nanoseconds: if not (0 <= nanoseconds < 10 ** 9):
if nanoseconds < 0 or nanoseconds % 1 != 0 or nanoseconds > (1e9 - 1):
raise ValueError( raise ValueError(
"nanoseconds must be a non-negative integer less than 999999999." "nanoseconds must be a non-negative integer less than 999999999."
) )
if not isinstance(seconds, (int, long)): self.seconds = seconds
raise ValueError(
"seconds must be an integer if also providing nanoseconds."
)
self.nanoseconds = nanoseconds self.nanoseconds = nanoseconds
else:
# round helps with floating point issues
self.nanoseconds = int(round(seconds % 1 * 1e9, 0))
self.seconds = int(seconds // 1)
def __repr__(self): def __repr__(self):
"""String representation of Timestamp.""" """String representation of Timestamp."""
@ -137,7 +130,18 @@ class Timestamp(object):
data = struct.pack("!Iq", self.nanoseconds, self.seconds) data = struct.pack("!Iq", self.nanoseconds, self.seconds)
return data return data
def to_float(self): @staticmethod
def from_unix(unix_sec):
"""Create a Timestamp from posix timestamp in seconds.
:param unix_float: Posix timestamp in seconds.
:type unix_float: int or float.
"""
seconds = int(unix_sec // 1)
nanoseconds = int((unix_sec % 1) * 10 ** 9)
return Timestamp(seconds, nanoseconds)
def to_unix(self):
"""Get the timestamp as a floating-point value. """Get the timestamp as a floating-point value.
:returns: posix timestamp :returns: posix timestamp
@ -146,28 +150,37 @@ class Timestamp(object):
return self.seconds + self.nanoseconds / 1e9 return self.seconds + self.nanoseconds / 1e9
@staticmethod @staticmethod
def from_float(unix_float): def from_unix_nano(unix_ns):
seconds = int(unix_float) """Create a Timestamp from posix timestamp in nanoseconds.
nanoseconds = int((unix_float % 1) * 1000000000)
return Timestamp(seconds, nanoseconds)
def to_unix_ns(self): :param int unix_ns: Posix timestamp in nanoseconds.
:rtype: Timestamp
"""
return Timestamp(*divmod(unix_ns, 10 ** 9))
def to_unix_nano(self):
"""Get the timestamp as a unixtime in nanoseconds. """Get the timestamp as a unixtime in nanoseconds.
:returns: posix timestamp in nanoseconds :returns: posix timestamp in nanoseconds
:rtype: int :rtype: int
""" """
return int(self.seconds * 1e9 + self.nanoseconds) return self.seconds * 10 ** 9 + self.nanoseconds
if not PY2:
def to_datetime(self): def to_datetime(self):
"""Get the timestamp as a UTC datetime. """Get the timestamp as a UTC datetime.
Python 2 is not supported.
:rtype: datetime. :rtype: datetime.
""" """
return datetime.datetime.fromtimestamp(self.to_float(), _utc) return datetime.datetime.fromtimestamp(self.to_unix(), _utc)
@staticmethod @staticmethod
def from_datetime(dt): def from_datetime(dt):
return Timestamp.from_float(dt.timestamp()) """Create a Timestamp from datetime with tzinfo.
Python 2 is not supported.
:rtype: Timestamp
"""
return Timestamp.from_unix(dt.timestamp())

View file

@ -691,9 +691,9 @@ class Unpacker(object):
if n == -1: # timestamp if n == -1: # timestamp
ts = Timestamp.from_bytes(bytes(obj)) ts = Timestamp.from_bytes(bytes(obj))
if self._timestamp == 1: if self._timestamp == 1:
return ts.to_float() return ts.to_unix()
elif self._timestamp == 2: elif self._timestamp == 2:
return ts.to_unix_ns() return ts.to_unix_nano()
elif self._timestamp == 3: elif self._timestamp == 3:
return ts.to_datetime() return ts.to_datetime()
else: else:

View file

@ -37,19 +37,25 @@ def test_timestamp():
assert ts.seconds == 2 ** 63 - 1 and ts.nanoseconds == 999999999 assert ts.seconds == 2 ** 63 - 1 and ts.nanoseconds == 999999999
# negative fractional # negative fractional
ts = Timestamp(-2.3) # s: -3, ns: 700000000 ts = Timestamp.from_unix(-2.3) # s: -3, ns: 700000000
assert ts.seconds == -3 and ts.nanoseconds == 700000000
assert ts.to_bytes() == b"\x29\xb9\x27\x00\xff\xff\xff\xff\xff\xff\xff\xfd" assert ts.to_bytes() == b"\x29\xb9\x27\x00\xff\xff\xff\xff\xff\xff\xff\xfd"
packed = msgpack.packb(ts) packed = msgpack.packb(ts)
assert packed == b"\xc7\x0c\xff" + ts.to_bytes() assert packed == b"\xc7\x0c\xff" + ts.to_bytes()
unpacked = msgpack.unpackb(packed) unpacked = msgpack.unpackb(packed)
assert ts == unpacked assert ts == unpacked
assert ts.seconds == -3 and ts.nanoseconds == 700000000
def test_timestamp_from():
t = Timestamp(42, 14000)
assert Timestamp.from_unix(42.000014) == t
assert Timestamp.from_unix_nano(42000014000) == t
def test_timestamp_to(): def test_timestamp_to():
t = Timestamp(42, 14) t = Timestamp(42, 14000)
assert t.to_float() == 42.000000014 assert t.to_unix() == 42.000014
assert t.to_unix_ns() == 42000000014 assert t.to_unix_nano() == 42000014000
@pytest.mark.skipif(sys.version_info[0] == 2, reason="datetime support is PY3+ only") @pytest.mark.skipif(sys.version_info[0] == 2, reason="datetime support is PY3+ only")