Skip to content

Commit a441bb1

Browse files
msgpack: support tzoffset in datetime
Support non-zero tzoffset in datetime extended type. If tzoffset and tzindex are not specified, return object with timezone-naive `pandas.Timestamp` internals. If tzoffset is specified, return object with timezone-aware `pandas.Timestamp` with in-built datetime.timezone info. Part of #204
1 parent 3332e3d commit a441bb1

File tree

3 files changed

+71
-6
lines changed

3 files changed

+71
-6
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3131
convert to a `pandas.Timestamp` and then use `to_datetime64()`
3232
or `to_datetime()` converter.
3333

34+
- Offset in datetime type support (#204).
35+
36+
In-built `datetime.timezone(datetime.timedelta(minutes=offset))`
37+
is used to store offset timezones.
38+
3439
### Changed
3540
- Bump msgpack requirement to 1.0.4 (PR #223).
3641
The only reason of this bump is various vulnerability fixes,

tarantool/msgpack_ext/types/datetime.py

+29-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from copy import deepcopy
22

3+
import datetime
34
import pandas
45

56
# https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type
@@ -41,6 +42,8 @@
4142
BYTEORDER = 'little'
4243

4344
NSEC_IN_SEC = 1000000000
45+
SEC_IN_MIN = 60
46+
MIN_IN_DAY = 60 * 24
4447

4548

4649
def get_bytes_as_int(data, cursor, size):
@@ -50,6 +53,18 @@ def get_bytes_as_int(data, cursor, size):
5053
def get_int_as_bytes(data, size):
5154
return data.to_bytes(size, byteorder=BYTEORDER, signed=True)
5255

56+
def compute_offset(timestamp):
57+
if timestamp.tz is None:
58+
return 0
59+
60+
utc_offset = timestamp.tz.utcoffset(timestamp)
61+
62+
if utc_offset is None:
63+
raise MsgpackError('Cannot encode value with timezone and None utcoffset')
64+
65+
# There is no precision loss since offset is in minutes
66+
return utc_offset.days * MIN_IN_DAY + utc_offset.seconds // SEC_IN_MIN
67+
5368
def msgpack_decode(data):
5469
cursor = 0
5570
seconds, cursor = get_bytes_as_int(data, cursor, SECONDS_SIZE_BYTES)
@@ -67,12 +82,20 @@ def msgpack_decode(data):
6782
else:
6883
raise MsgpackError(f'Unexpected datetime payload length {data_len}')
6984

70-
if (tzoffset != 0) or (tzindex != 0):
71-
raise NotImplementedError
72-
7385
total_nsec = seconds * NSEC_IN_SEC + nsec
7486

75-
return pandas.to_datetime(total_nsec, unit='ns')
87+
if (tzindex != 0):
88+
raise NotImplementedError
89+
elif (tzoffset != 0):
90+
tzinfo = datetime.timezone(datetime.timedelta(minutes=tzoffset))
91+
return pandas.to_datetime(total_nsec, unit='ns')\
92+
.replace(tzinfo=datetime.timezone.utc)\
93+
.tz_convert(tzinfo)
94+
else:
95+
# return timezone-naive pandas.Timestamp
96+
return pandas.to_datetime(total_nsec, unit='ns')
97+
98+
return timestamp
7699

77100
class Datetime():
78101
def __init__(self, *args, **kwargs):
@@ -83,7 +106,7 @@ def __init__(self, *args, **kwargs):
83106
return
84107

85108
if isinstance(data, pandas.Timestamp):
86-
self._timestamp = = deepcopy(data)
109+
self._timestamp = deepcopy(data)
87110
return
88111

89112
if isinstance(data, Datetime):
@@ -115,7 +138,7 @@ def msgpack_encode(self):
115138

116139
seconds = ts_value // NSEC_IN_SEC
117140
nsec = ts_value % NSEC_IN_SEC
118-
tzoffset = 0
141+
tzoffset = compute_offset(self._timestamp)
119142
tzindex = 0
120143

121144
buf = get_int_as_bytes(seconds, SECONDS_SIZE_BYTES)

test/suites/test_datetime.py

+37
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import sys
66
import unittest
7+
import datetime
78
import msgpack
89
import warnings
910
import tarantool
@@ -99,6 +100,42 @@ def setUp(self):
99100
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
100101
r"nsec=308543321})",
101102
},
103+
'datetime_with_positive_offset': {
104+
'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, second=54,
105+
microsecond=308543, nanosecond=321,
106+
tzinfo=datetime.timezone(datetime.timedelta(minutes=180))),
107+
'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\x00\x00'),
108+
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
109+
r"nsec=308543321, tzoffset=180})",
110+
},
111+
'datetime_with_negative_offset': {
112+
'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, second=54,
113+
microsecond=308543, nanosecond=321,
114+
tzinfo=datetime.timezone(datetime.timedelta(minutes=-60))),
115+
'msgpack': (b'\x8a\xb1\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xc4\xff\x00\x00'),
116+
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
117+
r"nsec=308543321, tzoffset=-60})",
118+
},
119+
'pandas_timestamp_with_positive_offset': {
120+
'python': tarantool.Datetime(pandas.Timestamp(
121+
year=2022, month=8, day=31, hour=18, minute=7, second=54,
122+
microsecond=308543, nanosecond=321,
123+
tzinfo=datetime.timezone(datetime.timedelta(minutes=180))
124+
)),
125+
'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\x00\x00'),
126+
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
127+
r"nsec=308543321, tzoffset=180})",
128+
},
129+
'pandas_timestamp_with_negative_offset': {
130+
'python': tarantool.Datetime(pandas.Timestamp(
131+
year=2022, month=8, day=31, hour=18, minute=7, second=54,
132+
microsecond=308543, nanosecond=321,
133+
tzinfo=datetime.timezone(datetime.timedelta(minutes=-60))
134+
)),
135+
'msgpack': (b'\x8a\xb1\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xc4\xff\x00\x00'),
136+
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
137+
r"nsec=308543321, tzoffset=-60})",
138+
},
102139
}
103140

104141
def test_msgpack_decode(self):

0 commit comments

Comments
 (0)