Skip to content

Commit 780c116

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 05358d2 commit 780c116

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)
@@ -65,12 +80,20 @@ def msgpack_decode(data):
6580
else:
6681
raise MsgpackError('Unexpected datetime payload length')
6782

68-
if (tzoffset != 0) or (tzindex != 0):
69-
raise NotImplementedError
70-
7183
total_nsec = seconds * NSEC_IN_SEC + nsec
7284

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

7598
class Datetime():
7699
def __init__(self, *args, **kwargs):
@@ -81,7 +104,7 @@ def __init__(self, *args, **kwargs):
81104
return
82105

83106
if isinstance(data, pandas.Timestamp):
84-
self._timestamp = = deepcopy(data)
107+
self._timestamp = deepcopy(data)
85108
return
86109

87110
if isinstance(data, Datetime):
@@ -113,7 +136,7 @@ def msgpack_encode(self):
113136

114137
seconds = ts_value // NSEC_IN_SEC
115138
nsec = ts_value % NSEC_IN_SEC
116-
tzoffset = 0
139+
tzoffset = compute_offset(self._timestamp)
117140
tzindex = 0
118141

119142
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)