Skip to content

Commit

Permalink
msgpack: support datetime interval extended type
Browse files Browse the repository at this point in the history
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
  • Loading branch information
DifferentialOrange committed Oct 3, 2022
1 parent 976a990 commit e40470e
Show file tree
Hide file tree
Showing 8 changed files with 424 additions and 2 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

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

- Datetime interval type support and tarantool.Interval type (#229).

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:

```python
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.

### Changed
- Bump msgpack requirement to 1.0.4 (PR #223).
The only reason of this bump is various vulnerability fixes,
Expand Down
7 changes: 6 additions & 1 deletion tarantool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
Datetime,
)

from tarantool.msgpack_ext.types.interval import (
Adjust as IntervalAdjust,
Interval,
)

__version__ = "0.9.0"


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

__all__ = ['connect', 'Connection', 'connectmesh', 'MeshConnection', 'Schema',
'Error', 'DatabaseError', 'NetworkError', 'NetworkWarning',
'SchemaError', 'dbapi', 'Datetime']
'SchemaError', 'dbapi', 'Datetime', 'Interval', 'IntervalAdjust']

# ConnectionPool is supported only for Python 3.7 or newer.
if sys.version_info.major >= 3 and sys.version_info.minor >= 7:
Expand Down
9 changes: 9 additions & 0 deletions tarantool/msgpack_ext/interval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from tarantool.msgpack_ext.types.interval import Interval

EXT_ID = 6

def encode(obj):
return obj.msgpack_encode()

def decode(data):
return Interval(data)
3 changes: 3 additions & 0 deletions tarantool/msgpack_ext/packer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
from msgpack import ExtType

from tarantool.msgpack_ext.types.datetime import Datetime
from tarantool.msgpack_ext.types.interval import Interval

import tarantool.msgpack_ext.decimal as ext_decimal
import tarantool.msgpack_ext.uuid as ext_uuid
import tarantool.msgpack_ext.datetime as ext_datetime
import tarantool.msgpack_ext.interval as ext_interval

encoders = [
{'type': Decimal, 'ext': ext_decimal },
{'type': UUID, 'ext': ext_uuid },
{'type': Datetime, 'ext': ext_datetime},
{'type': Interval, 'ext': ext_interval},
]

def default(obj):
Expand Down
149 changes: 149 additions & 0 deletions tarantool/msgpack_ext/types/interval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import msgpack
from enum import Enum

from tarantool.error import MsgpackError

# https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-interval-type
#
# The interval MessagePack representation looks like this:
# +--------+-------------------------+-------------+----------------+
# | MP_EXT | Size of packed interval | MP_INTERVAL | PackedInterval |
# +--------+-------------------------+-------------+----------------+
# Packed interval consists of:
# - Packed number of non-zero fields.
# - Packed non-null fields.
#
# Each packed field has the following structure:
# +----------+=====================+
# | field ID | field value |
# +----------+=====================+
#
# The number of defined (non-null) fields can be zero. In this case,
# the packed interval will be encoded as integer 0.
#
# List of the field IDs:
# - 0 – year
# - 1 – month
# - 2 – week
# - 3 – day
# - 4 – hour
# - 5 – minute
# - 6 – second
# - 7 – nanosecond
# - 8 – adjust

id_map = {
0: 'year',
1: 'month',
2: 'week',
3: 'day',
4: 'hour',
5: 'minute',
6: 'sec',
7: 'nsec',
8: 'adjust',
}

# https://github.com/tarantool/c-dt/blob/cec6acebb54d9e73ea0b99c63898732abd7683a6/dt_arithmetic.h#L34
class Adjust(Enum):
EXCESS = 0 # DT_EXCESS in c-dt, "excess" in Tarantool
NONE = 1 # DT_LIMIT in c-dt, "none" in Tarantool
LAST = 2 # DT_SNAP in c-dt, "last" in Tarantool

class Interval():
def __init__(self, data=None, *, year=0, month=0, week=0,
day=0, hour=0, minute=0, sec=0,
nsec=0, adjust=Adjust.NONE):
# If msgpack data does not contain a field value, it is zero.
# If built not from msgpack data, set argument values later.
self.year = 0
self.month = 0
self.week = 0
self.day = 0
self.hour = 0
self.minute = 0
self.sec = 0
self.nsec = 0
self.adjust = Adjust(0)

if data is not None:
if len(data) == 0:
return

# To create an unpacker is the only way to parse
# a sequence of values in Python msgpack module.
unpacker = msgpack.Unpacker()
unpacker.feed(data)
field_count = unpacker.unpack()
for _ in range(field_count):
field_id = unpacker.unpack()
value = unpacker.unpack()

if field_id not in id_map:
raise MsgpackError(f'Unknown interval field id {field_id}')

field_name = id_map[field_id]

if field_name == 'adjust':
try:
value = Adjust(value)
except ValueError as e:
raise MsgpackError(e)

setattr(self, id_map[field_id], value)
else:
self.year = year
self.month = month
self.week = week
self.day = day
self.hour = hour
self.minute = minute
self.sec = sec
self.nsec = nsec
self.adjust = adjust

def __eq__(self, other):
if not isinstance(other, Interval):
return False

# Tarantool interval compare is naive too
#
# Tarantool 2.10.1-0-g482d91c66
#
# tarantool> datetime.interval.new{hour=1} == datetime.interval.new{min=60}
# ---
# - false
# ...

for field_id in id_map.keys():
field_name = id_map[field_id]
if getattr(self, field_name) != getattr(other, field_name):
return False

return True

def __repr__(self):
return f'tarantool.Interval(year={self.year}, month={self.month}, day={self.day}, ' + \
f'hour={self.hour}, minute={self.minute}, sec={self.sec}, ' + \
f'nsec={self.nsec}, adjust={self.adjust})'

__str__ = __repr__

def msgpack_encode(self):
buf = bytes()

count = 0
for field_id in id_map.keys():
field_name = id_map[field_id]
value = getattr(self, field_name)

if field_name == 'adjust':
value = value.value

if value != 0:
buf = buf + msgpack.packb(field_id) + msgpack.packb(value)
count = count + 1

buf = msgpack.packb(count) + buf

return buf
2 changes: 2 additions & 0 deletions tarantool/msgpack_ext/unpacker.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import tarantool.msgpack_ext.decimal as ext_decimal
import tarantool.msgpack_ext.uuid as ext_uuid
import tarantool.msgpack_ext.datetime as ext_datetime
import tarantool.msgpack_ext.interval as ext_interval

decoders = {
ext_decimal.EXT_ID : ext_decimal.decode ,
ext_uuid.EXT_ID : ext_uuid.decode ,
ext_datetime.EXT_ID: ext_datetime.decode,
ext_interval.EXT_ID: ext_interval.decode,
}

def ext_hook(code, data):
Expand Down
4 changes: 3 additions & 1 deletion test/suites/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
from .test_decimal import TestSuite_Decimal
from .test_uuid import TestSuite_UUID
from .test_datetime import TestSuite_Datetime
from .test_interval import TestSuite_Interval

test_cases = (TestSuite_Schema_UnicodeConnection,
TestSuite_Schema_BinaryConnection,
TestSuite_Request, TestSuite_Protocol, TestSuite_Reconnect,
TestSuite_Mesh, TestSuite_Execute, TestSuite_DBAPI,
TestSuite_Encoding, TestSuite_Pool, TestSuite_Ssl,
TestSuite_Decimal, TestSuite_UUID, TestSuite_Datetime)
TestSuite_Decimal, TestSuite_UUID, TestSuite_Datetime,
TestSuite_Interval)

def load_tests(loader, tests, pattern):
suite = unittest.TestSuite()
Expand Down
Loading

0 comments on commit e40470e

Please sign in to comment.