Skip to content

Commit 7343acf

Browse files
msgpack: support tzoffset in datetime
Support non-zero tzoffset in datetime extended type. Use `tzoffset` parameter to set up offset timezone: ``` dt = tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, sec=54, nsec=308543321, tzoffset=180) ``` You may use `tzoffset` property to get timezone offset of a datetime object. Offset timezone is built with pytz.FixedOffset(). 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. This patch doesn't yet introduce the support of named timezones (tzindex). Part of #204
1 parent 26b6a59 commit 7343acf

File tree

4 files changed

+84
-15
lines changed

4 files changed

+84
-15
lines changed

Diff for: CHANGELOG.md

+13
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4040
nanosecond=(dt.nsec % 1000))
4141
```
4242

43+
- Offset in datetime type support (#204).
44+
45+
Use `tzoffset` parameter to set up offset timezone:
46+
47+
```python
48+
dt = tarantool.Datetime(year=2022, month=8, day=31,
49+
hour=18, minute=7, sec=54,
50+
nsec=308543321, tzoffset=180)
51+
```
52+
53+
You may use `tzoffset` property to get timezone offset of a datetime
54+
object.
55+
4356
### Changed
4457
- Bump msgpack requirement to 1.0.4 (PR #223).
4558
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

+42-12
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
#
@@ -42,6 +43,7 @@
4243

4344
NSEC_IN_SEC = 1000000000
4445
NSEC_IN_MKSEC = 1000
46+
SEC_IN_MIN = 60
4547

4648
def get_bytes_as_int(data, cursor, size):
4749
part = data[cursor:cursor + size]
@@ -50,6 +52,17 @@ def get_bytes_as_int(data, cursor, size):
5052
def get_int_as_bytes(data, size):
5153
return data.to_bytes(size, byteorder=BYTEORDER, signed=True)
5254

55+
def compute_offset(timestamp):
56+
utc_offset = timestamp.tzinfo.utcoffset(timestamp)
57+
58+
# `None` offset is a valid utcoffset implementation,
59+
# but it seems that pytz timezones never return `None`:
60+
# https://github.com/pandas-dev/pandas/issues/15986
61+
assert utc_offset is not None
62+
63+
# There is no precision loss since offset is in minutes
64+
return int(utc_offset.total_seconds()) // SEC_IN_MIN
65+
5366
def msgpack_decode(data):
5467
cursor = 0
5568
seconds, cursor = get_bytes_as_int(data, cursor, SECONDS_SIZE_BYTES)
@@ -67,16 +80,21 @@ def msgpack_decode(data):
6780
else:
6881
raise MsgpackError(f'Unexpected datetime payload length {data_len}')
6982

70-
if (tzoffset != 0) or (tzindex != 0):
71-
raise NotImplementedError
72-
7383
total_nsec = seconds * NSEC_IN_SEC + nsec
84+
datetime = pandas.to_datetime(total_nsec, unit='ns')
7485

75-
return pandas.to_datetime(total_nsec, unit='ns')
86+
if tzindex != 0:
87+
raise NotImplementedError
88+
elif tzoffset != 0:
89+
tzinfo = pytz.FixedOffset(tzoffset)
90+
return datetime.replace(tzinfo=pytz.UTC).tz_convert(tzinfo)
91+
else:
92+
return datetime
7693

7794
class Datetime():
7895
def __init__(self, data=None, *, timestamp=None, year=None, month=None,
79-
day=None, hour=None, minute=None, sec=None, nsec=None):
96+
day=None, hour=None, minute=None, sec=None, nsec=None,
97+
tzoffset=0):
8098
if data is not None:
8199
if not isinstance(data, bytes):
82100
raise ValueError('data argument (first positional argument) ' +
@@ -99,9 +117,9 @@ def __init__(self, data=None, *, timestamp=None, year=None, month=None,
99117
raise ValueError('timestamp must be int if nsec provided')
100118

101119
total_nsec = timestamp * NSEC_IN_SEC + nsec
102-
self._datetime = pandas.to_datetime(total_nsec, unit='ns')
120+
datetime = pandas.to_datetime(total_nsec, unit='ns')
103121
else:
104-
self._datetime = pandas.to_datetime(timestamp, unit='s')
122+
datetime = pandas.to_datetime(timestamp, unit='s')
105123
else:
106124
if nsec is not None:
107125
microsecond = nsec // NSEC_IN_MKSEC
@@ -110,10 +128,16 @@ def __init__(self, data=None, *, timestamp=None, year=None, month=None,
110128
microsecond = 0
111129
nanosecond = 0
112130

113-
self._datetime = pandas.Timestamp(year=year, month=month, day=day,
114-
hour=hour, minute=minute, second=sec,
115-
microsecond=microsecond,
116-
nanosecond=nanosecond)
131+
datetime = pandas.Timestamp(year=year, month=month, day=day,
132+
hour=hour, minute=minute, second=sec,
133+
microsecond=microsecond,
134+
nanosecond=nanosecond)
135+
136+
if tzoffset != 0:
137+
tzinfo = pytz.FixedOffset(tzoffset)
138+
datetime = datetime.replace(tzinfo=tzinfo)
139+
140+
self._datetime = datetime
117141

118142
def __eq__(self, other):
119143
if isinstance(other, Datetime):
@@ -176,14 +200,20 @@ def nsec(self):
176200
def timestamp(self):
177201
return self._datetime.timestamp()
178202

203+
@property
204+
def tzoffset(self):
205+
if self._datetime.tzinfo is not None:
206+
return compute_offset(self._datetime)
207+
return 0
208+
179209
@property
180210
def value(self):
181211
return self._datetime.value
182212

183213
def msgpack_encode(self):
184214
seconds = self.value // NSEC_IN_SEC
185215
nsec = self.nsec
186-
tzoffset = 0
216+
tzoffset = self.tzoffset
187217
tzindex = 0
188218

189219
buf = get_int_as_bytes(seconds, SECONDS_SIZE_BYTES)

Diff for: test/suites/test_datetime.py

+28-3
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def setUp(self):
5353

5454
def test_Datetime_class_API(self):
5555
dt = tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, sec=54,
56-
nsec=308543321)
56+
nsec=308543321, tzoffset=180)
5757

5858
self.assertEqual(dt.year, 2022)
5959
self.assertEqual(dt.month, 8)
@@ -63,8 +63,9 @@ def test_Datetime_class_API(self):
6363
self.assertEqual(dt.sec, 54)
6464
self.assertEqual(dt.nsec, 308543321)
6565
# Both Tarantool and pandas prone to precision loss for timestamp() floats
66-
self.assertEqual(dt.timestamp, 1661969274.308543)
67-
self.assertEqual(dt.value, 1661969274308543321)
66+
self.assertEqual(dt.timestamp, 1661958474.308543)
67+
self.assertEqual(dt.tzoffset, 180)
68+
self.assertEqual(dt.value, 1661958474308543321)
6869

6970

7071
datetime_class_invalid_init_cases = {
@@ -158,6 +159,30 @@ def test_Datetime_class_invalid_init(self):
158159
'msgpack': (b'\x7a\xa3\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\x00\x00\x00\x00'),
159160
'tarantool': r"datetime.new({timestamp=1661969274, nsec=308543321})",
160161
},
162+
'datetime_with_positive_offset': {
163+
'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, sec=54,
164+
nsec=308543321, tzoffset=180),
165+
'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\x00\x00'),
166+
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
167+
r"nsec=308543321, tzoffset=180})",
168+
},
169+
'datetime_with_negative_offset': {
170+
'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, sec=54,
171+
nsec=308543321, tzoffset=-60),
172+
'msgpack': (b'\x8a\xb1\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xc4\xff\x00\x00'),
173+
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
174+
r"nsec=308543321, tzoffset=-60})",
175+
},
176+
'timestamp_with_positive_offset': {
177+
'python': tarantool.Datetime(timestamp=1661969274, tzoffset=180),
178+
'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x00\x00\x00'),
179+
'tarantool': r"datetime.new({timestamp=1661969274, tzoffset=180})",
180+
},
181+
'timestamp_with_negative_offset': {
182+
'python': tarantool.Datetime(timestamp=1661969274, tzoffset=-60),
183+
'msgpack': (b'\x8a\xb1\x0f\x63\x00\x00\x00\x00\x00\x00\x00\x00\xc4\xff\x00\x00'),
184+
'tarantool': r"datetime.new({timestamp=1661969274, tzoffset=-60})",
185+
},
161186
}
162187

163188
def test_msgpack_decode(self):

0 commit comments

Comments
 (0)