Skip to content

Commit ad15365

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. pandas >= 1.0.0 restriction was added to ensure that Timestamp.tz() setter is disabled. 1. https://pypi.org/project/pytz/ Part of #204
1 parent 22442a0 commit ad15365

File tree

4 files changed

+119
-8
lines changed

4 files changed

+119
-8
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- Decimal type support (#203).
1111
- UUID type support (#202).
1212
- Datetime type support and tarantool.Datetime type (#204).
13+
- Offset in datetime type support (#204).
1314

1415
### Changed
1516
- Bump msgpack requirement to 1.0.4 (PR #223).

requirements.txt

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

tarantool/msgpack_ext/types/datetime.py

+51-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pandas
2+
import pytz
23

34
# https://www.tarantool.io/ru/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type
45
#
@@ -39,7 +40,16 @@
3940
BYTEORDER = 'little'
4041

4142
NSEC_IN_SEC = 1000000000
43+
SEC_IN_MIN = 60
44+
MIN_IN_DAY = 60 * 24
4245

46+
def compute_offset(dt):
47+
if dt.tz is None:
48+
return 0
49+
50+
utc_offset = dt.tz.utcoffset(dt)
51+
# There is no precision loss since pytz.FixedOffset is in minutes
52+
return utc_offset.days * MIN_IN_DAY + utc_offset.seconds // SEC_IN_MIN
4353

4454
def get_bytes_as_int(data, cursor, size):
4555
part = data[cursor:cursor + size]
@@ -61,22 +71,35 @@ def msgpack_decode(data):
6171
tzoffset = 0
6272
tzindex = 0
6373

64-
if (tzoffset != 0) or (tzindex != 0):
65-
raise NotImplementedError
66-
6774
total_nsec = seconds * NSEC_IN_SEC + nsec
6875

69-
dt = pandas.to_datetime(total_nsec, unit='ns')
76+
if (tzindex != 0):
77+
raise NotImplementedError
78+
elif (tzoffset != 0):
79+
tzinfo = pytz.FixedOffset(tzoffset)
80+
dt = pandas.to_datetime(total_nsec, unit='ns').replace(tzinfo=pytz.utc).tz_convert(tzinfo)
81+
else:
82+
# return timezone-naive pandas.Timestamp
83+
dt = pandas.to_datetime(total_nsec, unit='ns')
84+
7085
return dt, tzoffset, tzindex
7186

7287
class Datetime(pandas.Timestamp):
7388
def __new__(cls, *args, **kwargs):
74-
if len(args) > 0 and isinstance(args[0], bytes):
75-
dt, tzoffset, tzindex = msgpack_decode(args[0])
76-
else:
89+
dt = None
90+
if len(args) > 0:
91+
if isinstance(args[0], bytes):
92+
dt, tzoffset, tzindex = msgpack_decode(args[0])
93+
elif isinstance(args[0], Datetime):
94+
dt = pandas.Timestamp.__new__(cls, *args, **kwargs)
95+
tzoffset = args[0].tarantool_tzoffset
96+
97+
if dt is None:
7798
dt = super().__new__(cls, *args, **kwargs)
99+
tzoffset = compute_offset(dt)
78100

79101
dt.__class__ = cls
102+
dt.tarantool_tzoffset = tzoffset
80103
return dt
81104

82105
def msgpack_encode(self):
@@ -85,6 +108,11 @@ def msgpack_encode(self):
85108
tzoffset = 0
86109
tzindex = 0
87110

111+
if isinstance(self, Datetime):
112+
tzoffset = self.tarantool_tzoffset
113+
else:
114+
tzoffset = compute_offset(self)
115+
88116
buf = get_int_as_bytes(seconds, SECONDS_SIZE_BYTES)
89117

90118
if (nsec != 0) or (tzoffset != 0) or (tzindex != 0):
@@ -93,3 +121,19 @@ def msgpack_encode(self):
93121
buf = buf + get_int_as_bytes(tzindex, TZINDEX_SIZE_BYTES)
94122

95123
return buf
124+
125+
def replace(self, *args, **kwargs):
126+
dt = super().replace(*args, **kwargs)
127+
return Datetime(dt)
128+
129+
def astimezone(self, *args, **kwargs):
130+
dt = super().astimezone(*args, **kwargs)
131+
return Datetime(dt)
132+
133+
def tz_convert(self, *args, **kwargs):
134+
dt = super().tz_convert(*args, **kwargs)
135+
return Datetime(dt)
136+
137+
def tz_localize(self, *args, **kwargs):
138+
dt = super().tz_localize(*args, **kwargs)
139+
return Datetime(dt)

test/suites/test_datetime.py

+65
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,70 @@ 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+
},
133+
'datetime_offset_replace': {
134+
'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, second=54,
135+
microsecond=308543, nanosecond=321,
136+
).replace(tzinfo=pytz.FixedOffset(180)),
137+
'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\x00\x00'),
138+
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
139+
r"nsec=308543321, tzoffset=180})",
140+
},
141+
'datetime_offset_convert': {
142+
'python': tarantool.Datetime(year=2022, month=8, day=31, hour=16, minute=7, second=54,
143+
microsecond=308543, nanosecond=321,
144+
tzinfo=pytz.FixedOffset(60)).tz_convert(pytz.FixedOffset(180)),
145+
'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\x00\x00'),
146+
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
147+
r"nsec=308543321, tzoffset=180})",
148+
},
149+
'datetime_offset_localize': {
150+
'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, second=54,
151+
microsecond=308543, nanosecond=321,
152+
).tz_localize(pytz.FixedOffset(180)),
153+
'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\x00\x00'),
154+
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
155+
r"nsec=308543321, tzoffset=180})",
156+
},
157+
'datetime_offset_astimezone': {
158+
'python': tarantool.Datetime(year=2022, month=8, day=31, hour=16, minute=7, second=54,
159+
microsecond=308543, nanosecond=321,
160+
tzinfo=pytz.FixedOffset(60)).astimezone(pytz.FixedOffset(180)),
161+
'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\x00\x00'),
162+
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
163+
r"nsec=308543321, tzoffset=180})",
164+
},
100165
}
101166

102167
def test_msgpack_decode(self):

0 commit comments

Comments
 (0)