Skip to content

Commit e791685

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 pytz.FixedOffset [1] timezone info. pytz module is already a dependency of pandas, but this patch adds it as a requirement just in case something will change in the future. 1. https://pypi.org/project/pytz/ Part of #204
1 parent 7836260 commit e791685

File tree

4 files changed

+64
-7
lines changed

4 files changed

+64
-7
lines changed

Diff for: CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ 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+
3436
### Changed
3537
- Bump msgpack requirement to 1.0.4 (PR #223).
3638
The only reason of this bump is various vulnerability fixes,

Diff for: requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
msgpack>=1.0.4
22
pandas
3+
pytz

Diff for: tarantool/msgpack_ext/types/datetime.py

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

33
import pandas
4+
import pytz
45

56
# https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type
67
#
@@ -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,14 @@ 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+
# There is no precision loss since offset is in minutes
62+
return utc_offset.days * MIN_IN_DAY + utc_offset.seconds // SEC_IN_MIN
63+
5364
def msgpack_decode(data):
5465
cursor = 0
5566
seconds, cursor = get_bytes_as_int(data, cursor, SECONDS_SIZE_BYTES)
@@ -63,12 +74,17 @@ def msgpack_decode(data):
6374
tzoffset = 0
6475
tzindex = 0
6576

66-
if (tzoffset != 0) or (tzindex != 0):
67-
raise NotImplementedError
68-
6977
total_nsec = seconds * NSEC_IN_SEC + nsec
7078

71-
timestamp = pandas.to_datetime(total_nsec, unit='ns')
79+
if (tzindex != 0):
80+
raise NotImplementedError
81+
elif (tzoffset != 0):
82+
tzinfo = pytz.FixedOffset(tzoffset)
83+
timestamp = pandas.to_datetime(total_nsec, unit='ns').replace(tzinfo=pytz.utc).tz_convert(tzinfo)
84+
else:
85+
# return timezone-naive pandas.Timestamp
86+
timestamp = pandas.to_datetime(total_nsec, unit='ns')
87+
7288
return timestamp, tzoffset, tzindex
7389

7490
class Datetime():
@@ -79,12 +95,16 @@ def __init__(self, *args, **kwargs):
7995
timestamp, tzoffset, tzindex = msgpack_decode(data)
8096
elif isinstance(data, pandas.Timestamp):
8197
timestamp = deepcopy(data)
98+
tzoffset = compute_offset(timestamp)
8299
elif isinstance(data, Datetime):
83100
timestamp = deepcopy(data._timestamp)
101+
tzoffset = deepcopy(data._tzoffset)
84102
else:
85103
timestamp = pandas.Timestamp(*args, **kwargs)
104+
tzoffset = compute_offset(timestamp)
86105

87106
self._timestamp = timestamp
107+
self._tzoffset = tzoffset
88108

89109
def __eq__(self, other):
90110
if isinstance(other, Datetime):
@@ -93,11 +113,12 @@ def __eq__(self, other):
93113
return self._timestamp == other
94114
else:
95115
return False
116+
96117
def __str__(self):
97-
return f'tarantool.Datetime(timestamp={self._timestamp})'
118+
return f'tarantool.Datetime(timestamp={self._timestamp}, tzoffset={self._tzoffset})'
98119

99120
def __repr__(self):
100-
return f'tarantool.Datetime(timestamp={self._timestamp})'
121+
return f'tarantool.Datetime(timestamp={self._timestamp}, tzoffset={self._tzoffset})'
101122

102123
def to_pd_timestamp(self):
103124
return deepcopy(self._timestamp)
@@ -107,7 +128,7 @@ def msgpack_encode(self):
107128

108129
seconds = ts_value // NSEC_IN_SEC
109130
nsec = ts_value % NSEC_IN_SEC
110-
tzoffset = 0
131+
tzoffset = self._tzoffset
111132
tzindex = 0
112133

113134
buf = get_int_as_bytes(seconds, SECONDS_SIZE_BYTES)

Diff for: test/suites/test_datetime.py

+33
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import warnings
99
import tarantool
1010
import pandas
11+
import pytz
1112

1213
from tarantool.msgpack_ext.packer import default as packer_default
1314
from tarantool.msgpack_ext.unpacker import ext_hook as unpacker_ext_hook
@@ -97,6 +98,38 @@ def setUp(self):
9798
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
9899
r"nsec=308543321})",
99100
},
101+
'datetime_with_positive_offset': {
102+
'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, second=54,
103+
microsecond=308543, nanosecond=321,
104+
tzinfo=pytz.FixedOffset(180)),
105+
'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\x00\x00'),
106+
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
107+
r"nsec=308543321, tzoffset=180})",
108+
},
109+
'datetime_with_negative_offset': {
110+
'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, second=54,
111+
microsecond=308543, nanosecond=321,
112+
tzinfo=pytz.FixedOffset(-60)),
113+
'msgpack': (b'\x8a\xb1\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xc4\xff\x00\x00'),
114+
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
115+
r"nsec=308543321, tzoffset=-60})",
116+
},
117+
'pandas_timestamp_with_positive_offset': {
118+
'python': pandas.Timestamp(year=2022, month=8, day=31, hour=18, minute=7, second=54,
119+
microsecond=308543, nanosecond=321,
120+
tzinfo=pytz.FixedOffset(180)),
121+
'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\x00\x00'),
122+
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
123+
r"nsec=308543321, tzoffset=180})",
124+
},
125+
'pandas_timestamp_with_negative_offset': {
126+
'python': pandas.Timestamp(year=2022, month=8, day=31, hour=18, minute=7, second=54,
127+
microsecond=308543, nanosecond=321,
128+
tzinfo=pytz.FixedOffset(-60)),
129+
'msgpack': (b'\x8a\xb1\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xc4\xff\x00\x00'),
130+
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
131+
r"nsec=308543321, tzoffset=-60})",
132+
},
100133
}
101134

102135
def test_msgpack_decode(self):

0 commit comments

Comments
 (0)