|
| 1 | +# coding: utf-8 |
| 2 | +from collections import namedtuple |
| 3 | +import sys |
| 4 | +import struct |
| 5 | + |
| 6 | + |
| 7 | +PY2 = sys.version_info[0] == 2 |
| 8 | +if not PY2: |
| 9 | + long = int |
| 10 | + |
| 11 | + |
| 12 | +class ExtType(namedtuple('ExtType', 'code data')): |
| 13 | + """ExtType represents ext type in msgpack.""" |
| 14 | + def __new__(cls, code, data): |
| 15 | + if not isinstance(code, int): |
| 16 | + raise TypeError("code must be int") |
| 17 | + if not isinstance(data, bytes): |
| 18 | + raise TypeError("data must be bytes") |
| 19 | + if code == -1: |
| 20 | + return Timestamp.from_bytes(data) |
| 21 | + if not 0 <= code <= 127: |
| 22 | + raise ValueError("code must be 0~127") |
| 23 | + return super(ExtType, cls).__new__(cls, code, data) |
| 24 | + |
| 25 | + |
| 26 | +class Timestamp(object): |
| 27 | + """Timestamp represents the Timestamp extension type in msgpack. |
| 28 | +
|
| 29 | + When built with Cython, msgpack uses C methods to pack and unpack `Timestamp`. When using pure-Python |
| 30 | + msgpack, :func:`to_bytes` and :func:`from_bytes` are used to pack and unpack `Timestamp`. |
| 31 | + """ |
| 32 | + __slots__ = ["seconds", "nanoseconds"] |
| 33 | + |
| 34 | + def __init__(self, seconds, nanoseconds=0): |
| 35 | + """Initialize a Timestamp object. |
| 36 | +
|
| 37 | + :param seconds: Number of seconds since the UNIX epoch (00:00:00 UTC Jan 1 1970, minus leap seconds). May be |
| 38 | + negative. If :code:`seconds` includes a fractional part, :code:`nanoseconds` must be 0. |
| 39 | + :type seconds: int or float |
| 40 | +
|
| 41 | + :param nanoseconds: Number of nanoseconds to add to `seconds` to get fractional time. Maximum is 999_999_999. |
| 42 | + Default is 0. |
| 43 | + :type nanoseconds: int |
| 44 | +
|
| 45 | + Note: Negative times (before the UNIX epoch) are represented as negative seconds + positive ns. |
| 46 | + """ |
| 47 | + if not isinstance(seconds, (int, long, float)): |
| 48 | + raise TypeError("seconds must be numeric") |
| 49 | + if not isinstance(nanoseconds, (int, long)): |
| 50 | + raise TypeError("nanoseconds must be an integer") |
| 51 | + if nanoseconds: |
| 52 | + if nanoseconds < 0 or nanoseconds % 1 != 0 or nanoseconds > (1e9 - 1): |
| 53 | + raise ValueError("nanoseconds must be a non-negative integer less than 999999999.") |
| 54 | + if not isinstance(seconds, (int, long)): |
| 55 | + raise ValueError("seconds must be an integer if also providing nanoseconds.") |
| 56 | + self.nanoseconds = nanoseconds |
| 57 | + else: |
| 58 | + # round helps with floating point issues |
| 59 | + self.nanoseconds = int(round(seconds % 1 * 1e9, 0)) |
| 60 | + self.seconds = int(seconds // 1) |
| 61 | + |
| 62 | + def __repr__(self): |
| 63 | + """String representation of Timestamp.""" |
| 64 | + return "Timestamp(seconds={0}, nanoseconds={1})".format(self.seconds, self.nanoseconds) |
| 65 | + |
| 66 | + def __eq__(self, other): |
| 67 | + """Check for equality with another Timestamp object""" |
| 68 | + if type(other) is self.__class__: |
| 69 | + return self.seconds == other.seconds and self.nanoseconds == other.nanoseconds |
| 70 | + return False |
| 71 | + |
| 72 | + def __ne__(self, other): |
| 73 | + """not-equals method (see :func:`__eq__()`)""" |
| 74 | + return not self.__eq__(other) |
| 75 | + |
| 76 | + @staticmethod |
| 77 | + def from_bytes(b): |
| 78 | + """Unpack bytes into a `Timestamp` object. |
| 79 | +
|
| 80 | + Used for pure-Python msgpack unpacking. |
| 81 | +
|
| 82 | + :param b: Payload from msgpack ext message with code -1 |
| 83 | + :type b: bytes |
| 84 | +
|
| 85 | + :returns: Timestamp object unpacked from msgpack ext payload |
| 86 | + :rtype: Timestamp |
| 87 | + """ |
| 88 | + if len(b) == 4: |
| 89 | + seconds = struct.unpack("!L", b)[0] |
| 90 | + nanoseconds = 0 |
| 91 | + elif len(b) == 8: |
| 92 | + data64 = struct.unpack("!Q", b)[0] |
| 93 | + seconds = data64 & 0x00000003ffffffff |
| 94 | + nanoseconds = data64 >> 34 |
| 95 | + elif len(b) == 12: |
| 96 | + nanoseconds, seconds = struct.unpack("!Iq", b) |
| 97 | + else: |
| 98 | + raise ValueError("Timestamp type can only be created from 32, 64, or 96-bit byte objects") |
| 99 | + return Timestamp(seconds, nanoseconds) |
| 100 | + |
| 101 | + def to_bytes(self): |
| 102 | + """Pack this Timestamp object into bytes. |
| 103 | +
|
| 104 | + Used for pure-Python msgpack packing. |
| 105 | +
|
| 106 | + :returns data: Payload for EXT message with code -1 (timestamp type) |
| 107 | + :rtype: bytes |
| 108 | + """ |
| 109 | + if (self.seconds >> 34) == 0: # seconds is non-negative and fits in 34 bits |
| 110 | + data64 = self.nanoseconds << 34 | self.seconds |
| 111 | + if data64 & 0xffffffff00000000 == 0: |
| 112 | + # nanoseconds is zero and seconds < 2**32, so timestamp 32 |
| 113 | + data = struct.pack("!L", data64) |
| 114 | + else: |
| 115 | + # timestamp 64 |
| 116 | + data = struct.pack("!Q", data64) |
| 117 | + else: |
| 118 | + # timestamp 96 |
| 119 | + data = struct.pack("!Iq", self.nanoseconds, self.seconds) |
| 120 | + return data |
| 121 | + |
| 122 | + def to_float_s(self): |
| 123 | + """Get the timestamp as a floating-point value. |
| 124 | +
|
| 125 | + :returns: posix timestamp |
| 126 | + :rtype: float |
| 127 | + """ |
| 128 | + return self.seconds + self.nanoseconds/1e9 |
| 129 | + |
| 130 | + def to_unix_ns(self): |
| 131 | + """Get the timestamp as a unixtime in nanoseconds. |
| 132 | +
|
| 133 | + :returns: posix timestamp in nanoseconds |
| 134 | + :rtype: int |
| 135 | + """ |
| 136 | + return int(self.seconds * 1e9 + self.nanoseconds) |
0 commit comments