Skip to content

Commit 608e3e5

Browse files
api: extract datetime encode/decode from class
Extract tarantool.Datetime encode and decode to external functions. This is a breaking change, but since there is no tagged release with Datetime yet and API was more internal rather than public, it shouldn't be an issue. Follows #204
1 parent aa240cb commit 608e3e5

File tree

4 files changed

+153
-194
lines changed

4 files changed

+153
-194
lines changed

Diff for: CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
189189
- Update API documentation strings (#67).
190190
- Update documentation index, quick start and guide pages (#67).
191191
- Use git version to set package version (#238).
192+
- Extract tarantool.Datetime encode and decode to external
193+
functions (PR #252).
192194

193195
### Fixed
194196
- Package build (#238).

Diff for: tarantool/msgpack_ext/datetime.py

+140-6
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,83 @@
11
"""
22
Tarantool `datetime`_ extension type support module.
33
4-
Refer to :mod:`~tarantool.msgpack_ext.types.datetime`.
4+
The datetime MessagePack representation looks like this:
55
6-
.. _datetime: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type
6+
.. code-block:: text
7+
8+
+---------+----------------+==========+-----------------+
9+
| MP_EXT | MP_DATETIME | seconds | nsec; tzoffset; |
10+
| = d7/d8 | = 4 | | tzindex; |
11+
+---------+----------------+==========+-----------------+
12+
13+
MessagePack data contains:
14+
15+
* Seconds (8 bytes) as an unencoded 64-bit signed integer stored in the
16+
little-endian order.
17+
* The optional fields (8 bytes), if any of them have a non-zero value.
18+
The fields include nsec (4 bytes), tzoffset (2 bytes), and
19+
tzindex (2 bytes) packed in the little-endian order.
20+
21+
``seconds`` is seconds since Epoch, where the epoch is the point where
22+
the time starts, and is platform dependent. For Unix, the epoch is
23+
January 1, 1970, 00:00:00 (UTC). Tarantool uses a ``double`` type, see a
24+
structure definition in src/lib/core/datetime.h and reasons in
25+
`datetime RFC`_.
26+
27+
``nsec`` is nanoseconds, fractional part of seconds. Tarantool uses
28+
``int32_t``, see a definition in src/lib/core/datetime.h.
29+
30+
``tzoffset`` is timezone offset in minutes from UTC. Tarantool uses
31+
``int16_t`` type, see a structure definition in src/lib/core/datetime.h.
32+
33+
``tzindex`` is Olson timezone id. Tarantool uses ``int16_t`` type, see
34+
a structure definition in src/lib/core/datetime.h. If both
35+
``tzoffset`` and ``tzindex`` are specified, ``tzindex`` has the
36+
preference and the ``tzoffset`` value is ignored.
37+
38+
.. _datetime RFC: https://github.com/tarantool/tarantool/wiki/Datetime-internals#intervals-in-c
739
"""
840

9-
from tarantool.msgpack_ext.types.datetime import Datetime
41+
from tarantool.msgpack_ext.types.datetime import (
42+
NSEC_IN_SEC,
43+
SEC_IN_MIN,
44+
Datetime,
45+
)
46+
import tarantool.msgpack_ext.types.timezones as tt_timezones
47+
48+
from tarantool.error import MsgpackError
1049

1150
EXT_ID = 4
1251
"""
1352
`datetime`_ type id.
1453
"""
1554

55+
BYTEORDER = 'little'
56+
57+
SECONDS_SIZE_BYTES = 8
58+
NSEC_SIZE_BYTES = 4
59+
TZOFFSET_SIZE_BYTES = 2
60+
TZINDEX_SIZE_BYTES = 2
61+
62+
63+
def get_int_as_bytes(data, size):
64+
"""
65+
Get binary representation of integer value.
66+
67+
:param data: Integer value.
68+
:type data: :obj:`int`
69+
70+
:param size: Integer size, in bytes.
71+
:type size: :obj:`int`
72+
73+
:return: Encoded integer.
74+
:rtype: :obj:`bytes`
75+
76+
:meta private:
77+
"""
78+
79+
return data.to_bytes(size, byteorder=BYTEORDER, signed=True)
80+
1681
def encode(obj):
1782
"""
1883
Encode a datetime object.
@@ -26,7 +91,48 @@ def encode(obj):
2691
:raise: :exc:`tarantool.Datetime.msgpack_encode` exceptions
2792
"""
2893

29-
return obj.msgpack_encode()
94+
seconds = obj.value // NSEC_IN_SEC
95+
nsec = obj.nsec
96+
tzoffset = obj.tzoffset
97+
98+
tz = obj.tz
99+
if tz != '':
100+
tzindex = tt_timezones.timezoneToIndex[tz]
101+
else:
102+
tzindex = 0
103+
104+
buf = get_int_as_bytes(seconds, SECONDS_SIZE_BYTES)
105+
106+
if (nsec != 0) or (tzoffset != 0) or (tzindex != 0):
107+
buf = buf + get_int_as_bytes(nsec, NSEC_SIZE_BYTES)
108+
buf = buf + get_int_as_bytes(tzoffset, TZOFFSET_SIZE_BYTES)
109+
buf = buf + get_int_as_bytes(tzindex, TZINDEX_SIZE_BYTES)
110+
111+
return buf
112+
113+
114+
def get_bytes_as_int(data, cursor, size):
115+
"""
116+
Get integer value from binary data.
117+
118+
:param data: MessagePack binary data.
119+
:type data: :obj:`bytes`
120+
121+
:param cursor: Index after last parsed byte.
122+
:type cursor: :obj:`int`
123+
124+
:param size: Integer size, in bytes.
125+
:type size: :obj:`int`
126+
127+
:return: First value: parsed integer, second value: new cursor
128+
position.
129+
:rtype: first value: :obj:`int`, second value: :obj:`int`
130+
131+
:meta private:
132+
"""
133+
134+
part = data[cursor:cursor + size]
135+
return int.from_bytes(part, BYTEORDER, signed=True), cursor + size
30136

31137
def decode(data):
32138
"""
@@ -38,7 +144,35 @@ def decode(data):
38144
:return: Decoded datetime.
39145
:rtype: :class:`tarantool.Datetime`
40146
41-
:raise: :exc:`tarantool.Datetime` exceptions
147+
:raise: :exc:`~tarantool.error.MsgpackError`,
148+
:exc:`tarantool.Datetime` exceptions
42149
"""
43150

44-
return Datetime(data)
151+
cursor = 0
152+
seconds, cursor = get_bytes_as_int(data, cursor, SECONDS_SIZE_BYTES)
153+
154+
data_len = len(data)
155+
if data_len == (SECONDS_SIZE_BYTES + NSEC_SIZE_BYTES + \
156+
TZOFFSET_SIZE_BYTES + TZINDEX_SIZE_BYTES):
157+
nsec, cursor = get_bytes_as_int(data, cursor, NSEC_SIZE_BYTES)
158+
tzoffset, cursor = get_bytes_as_int(data, cursor, TZOFFSET_SIZE_BYTES)
159+
tzindex, cursor = get_bytes_as_int(data, cursor, TZINDEX_SIZE_BYTES)
160+
elif data_len == SECONDS_SIZE_BYTES:
161+
nsec = 0
162+
tzoffset = 0
163+
tzindex = 0
164+
else:
165+
raise MsgpackError(f'Unexpected datetime payload length {data_len}')
166+
167+
if tzindex != 0:
168+
if tzindex not in tt_timezones.indexToTimezone:
169+
raise MsgpackError(f'Failed to decode datetime with unknown tzindex "{tzindex}"')
170+
tz = tt_timezones.indexToTimezone[tzindex]
171+
return Datetime(timestamp=seconds, nsec=nsec, tz=tz,
172+
timestamp_since_utc_epoch=True)
173+
elif tzoffset != 0:
174+
return Datetime(timestamp=seconds, nsec=nsec, tzoffset=tzoffset,
175+
timestamp_since_utc_epoch=True)
176+
else:
177+
return Datetime(timestamp=seconds, nsec=nsec,
178+
timestamp_since_utc_epoch=True)

0 commit comments

Comments
 (0)