Skip to content

Commit edf4273

Browse files
msgpack: support datetime interval extended type
Tarantool supports datetime interval type since version 2.10.0 [1]. This patch introduced the support of Tarantool interval type in msgpack decoders and encoders. Tarantool datetime interval objects are decoded to `tarantool.Interval` type. `tarantool.Interval` may be encoded to Tarantool interval objects. You can create `tarantool.Interval` objects either from msgpack data or by using the same API as in Tarantool: ``` di = tarantool.Interval(year=-1, month=2, day=3, hour=4, minute=-5, sec=6, nsec=308543321, adjust=tarantool.IntervalAdjust.NONE) ``` Its attributes (same as in init API) are exposed, so you can use them if needed. datetime, numpy and pandas tools doesn't seem to be sufficient to cover all adjust cases supported by Tarantool. This patch does not yet introduce the support of datetime interval arithmetic. 1. tarantool/tarantool#5941 Part of #229
1 parent 8c92b86 commit edf4273

File tree

8 files changed

+397
-2
lines changed

8 files changed

+397
-2
lines changed

Diff for: CHANGELOG.md

+19
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6767

6868
You may use `tz` property to get timezone name of a datetime object.
6969

70+
- Datetime interval type support and tarantool.Interval type (#229).
71+
72+
Tarantool datetime interval objects are decoded to `tarantool.Interval`
73+
type. `tarantool.Interval` may be encoded to Tarantool interval
74+
objects.
75+
76+
You can create `tarantool.Interval` objects either from msgpack
77+
data or by using the same API as in Tarantool:
78+
79+
```python
80+
di = tarantool.Interval(year=-1, month=2, day=3,
81+
hour=4, minute=-5, sec=6,
82+
nsec=308543321,
83+
adjust=tarantool.IntervalAdjust.NONE)
84+
```
85+
86+
Its attributes (same as in init API) are exposed, so you can
87+
use them if needed.
88+
7089
### Changed
7190
- Bump msgpack requirement to 1.0.4 (PR #223).
7291
The only reason of this bump is various vulnerability fixes,

Diff for: tarantool/__init__.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636
Datetime,
3737
)
3838

39+
from tarantool.msgpack_ext.types.interval import (
40+
Adjust as IntervalAdjust,
41+
Interval,
42+
)
43+
3944
__version__ = "0.9.0"
4045

4146

@@ -95,7 +100,7 @@ def connectmesh(addrs=({'host': 'localhost', 'port': 3301},), user=None,
95100

96101
__all__ = ['connect', 'Connection', 'connectmesh', 'MeshConnection', 'Schema',
97102
'Error', 'DatabaseError', 'NetworkError', 'NetworkWarning',
98-
'SchemaError', 'dbapi', 'Datetime']
103+
'SchemaError', 'dbapi', 'Datetime', 'Interval', 'IntervalAdjust']
99104

100105
# ConnectionPool is supported only for Python 3.7 or newer.
101106
if sys.version_info.major >= 3 and sys.version_info.minor >= 7:

Diff for: tarantool/msgpack_ext/interval.py

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from tarantool.msgpack_ext.types.interval import Interval
2+
3+
EXT_ID = 6
4+
5+
def encode(obj):
6+
return obj.msgpack_encode()
7+
8+
def decode(data):
9+
return Interval(data)

Diff for: tarantool/msgpack_ext/packer.py

+3
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
from msgpack import ExtType
44

55
from tarantool.msgpack_ext.types.datetime import Datetime
6+
from tarantool.msgpack_ext.types.interval import Interval
67

78
import tarantool.msgpack_ext.decimal as ext_decimal
89
import tarantool.msgpack_ext.uuid as ext_uuid
910
import tarantool.msgpack_ext.datetime as ext_datetime
11+
import tarantool.msgpack_ext.interval as ext_interval
1012

1113
encoders = [
1214
{'type': Decimal, 'ext': ext_decimal },
1315
{'type': UUID, 'ext': ext_uuid },
1416
{'type': Datetime, 'ext': ext_datetime},
17+
{'type': Interval, 'ext': ext_interval},
1518
]
1619

1720
def default(obj):

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

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import msgpack
2+
from enum import Enum
3+
4+
from tarantool.error import MsgpackError
5+
6+
# https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-interval-type
7+
#
8+
# The interval MessagePack representation looks like this:
9+
# +--------+-------------------------+-------------+----------------+
10+
# | MP_EXT | Size of packed interval | MP_INTERVAL | PackedInterval |
11+
# +--------+-------------------------+-------------+----------------+
12+
# Packed interval consists of:
13+
# - Packed number of non-zero fields.
14+
# - Packed non-null fields.
15+
#
16+
# Each packed field has the following structure:
17+
# +----------+=====================+
18+
# | field ID | field value |
19+
# +----------+=====================+
20+
#
21+
# The number of defined (non-null) fields can be zero. In this case,
22+
# the packed interval will be encoded as integer 0.
23+
#
24+
# List of the field IDs:
25+
# - 0 – year
26+
# - 1 – month
27+
# - 2 – week
28+
# - 3 – day
29+
# - 4 – hour
30+
# - 5 – minute
31+
# - 6 – second
32+
# - 7 – nanosecond
33+
# - 8 – adjust
34+
35+
id_map = {
36+
0: 'year',
37+
1: 'month',
38+
2: 'week',
39+
3: 'day',
40+
4: 'hour',
41+
5: 'minute',
42+
6: 'sec',
43+
7: 'nsec',
44+
8: 'adjust',
45+
}
46+
47+
# https://github.com/tarantool/c-dt/blob/cec6acebb54d9e73ea0b99c63898732abd7683a6/dt_arithmetic.h#L34
48+
class Adjust(Enum):
49+
EXCESS = 0 # DT_EXCESS in c-dt, "excess" in Tarantool
50+
NONE = 1 # DT_LIMIT in c-dt, "none" in Tarantool
51+
LAST = 2 # DT_SNAP in c-dt, "last" in Tarantool
52+
53+
class Interval():
54+
def __init__(self, data=None, *, year=0, month=0, week=0,
55+
day=0, hour=0, minute=0, sec=0,
56+
nsec=0, adjust=Adjust.NONE):
57+
# If msgpack data does not contain a field value, it is zero.
58+
# If built not from msgpack data, set argument values later.
59+
self.year = 0
60+
self.month = 0
61+
self.week = 0
62+
self.day = 0
63+
self.hour = 0
64+
self.minute = 0
65+
self.sec = 0
66+
self.nsec = 0
67+
self.adjust = Adjust(0)
68+
69+
if data is not None:
70+
if len(data) == 0:
71+
return
72+
73+
# To create an unpacker is the only way to parse
74+
# a sequence of values in Python msgpack module.
75+
unpacker = msgpack.Unpacker()
76+
unpacker.feed(data)
77+
field_count = unpacker.unpack()
78+
for _ in range(field_count):
79+
field_id = unpacker.unpack()
80+
value = unpacker.unpack()
81+
82+
if field_id not in id_map:
83+
raise MsgpackError(f'Unknown interval field id {field_id}')
84+
85+
field_name = id_map[field_id]
86+
87+
if field_name == 'adjust':
88+
try:
89+
value = Adjust(value)
90+
except ValueError as e:
91+
raise MsgpackError(e)
92+
93+
setattr(self, id_map[field_id], value)
94+
else:
95+
self.year = year
96+
self.month = month
97+
self.week = week
98+
self.day = day
99+
self.hour = hour
100+
self.minute = minute
101+
self.sec = sec
102+
self.nsec = nsec
103+
self.adjust = adjust
104+
105+
def __eq__(self, other):
106+
if not isinstance(other, Interval):
107+
return False
108+
109+
# Tarantool interval compare is naive too
110+
#
111+
# Tarantool 2.10.1-0-g482d91c66
112+
#
113+
# tarantool> datetime.interval.new{hour=1} == datetime.interval.new{min=60}
114+
# ---
115+
# - false
116+
# ...
117+
118+
for field_id in id_map.keys():
119+
field_name = id_map[field_id]
120+
if getattr(self, field_name) != getattr(other, field_name):
121+
return False
122+
123+
return True
124+
125+
def __repr__(self):
126+
return f'tarantool.Interval(year={self.year}, month={self.month}, day={self.day}, ' + \
127+
f'hour={self.hour}, minute={self.minute}, sec={self.sec}, ' + \
128+
f'nsec={self.nsec}, adjust={self.adjust})'
129+
130+
__str__ = __repr__
131+
132+
def msgpack_encode(self):
133+
buf = bytes()
134+
135+
count = 0
136+
for field_id in id_map.keys():
137+
field_name = id_map[field_id]
138+
value = getattr(self, field_name)
139+
140+
if field_name == 'adjust':
141+
value = value.value
142+
143+
if value != 0:
144+
buf = buf + msgpack.packb(field_id) + msgpack.packb(value)
145+
count = count + 1
146+
147+
buf = msgpack.packb(count) + buf
148+
149+
return buf

Diff for: tarantool/msgpack_ext/unpacker.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import tarantool.msgpack_ext.decimal as ext_decimal
22
import tarantool.msgpack_ext.uuid as ext_uuid
33
import tarantool.msgpack_ext.datetime as ext_datetime
4+
import tarantool.msgpack_ext.interval as ext_interval
45

56
decoders = {
67
ext_decimal.EXT_ID : ext_decimal.decode ,
78
ext_uuid.EXT_ID : ext_uuid.decode ,
89
ext_datetime.EXT_ID: ext_datetime.decode,
10+
ext_interval.EXT_ID: ext_interval.decode,
911
}
1012

1113
def ext_hook(code, data):

Diff for: test/suites/__init__.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818
from .test_decimal import TestSuite_Decimal
1919
from .test_uuid import TestSuite_UUID
2020
from .test_datetime import TestSuite_Datetime
21+
from .test_interval import TestSuite_Interval
2122

2223
test_cases = (TestSuite_Schema_UnicodeConnection,
2324
TestSuite_Schema_BinaryConnection,
2425
TestSuite_Request, TestSuite_Protocol, TestSuite_Reconnect,
2526
TestSuite_Mesh, TestSuite_Execute, TestSuite_DBAPI,
2627
TestSuite_Encoding, TestSuite_Pool, TestSuite_Ssl,
27-
TestSuite_Decimal, TestSuite_UUID, TestSuite_Datetime)
28+
TestSuite_Decimal, TestSuite_UUID, TestSuite_Datetime,
29+
TestSuite_Interval)
2830

2931
def load_tests(loader, tests, pattern):
3032
suite = unittest.TestSuite()

0 commit comments

Comments
 (0)