Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: 2

build:
os: "ubuntu-20.04"
os: "ubuntu-24.04"
tools:
python: "3.10"
jobs:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed
- Drop Python 3.6 support (PR #327).
- Support the maximum decimal precision for tarantool 3.5 and above (PR #342).

### Fixed
- Set upper bound for version of setuptools (PR #342).

## 1.2.0 - 2024-03-27

Expand Down
2 changes: 1 addition & 1 deletion requirements-test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ flake8 == 6.1.0 ; python_version >= '3.8'
flake8 == 5.0.4 ; python_version < '3.8'
codespell == 2.3.0 ; python_version >= '3.8'
codespell == 2.2.5 ; python_version < '3.8'
setuptools >= 75.3.2
setuptools >= 75.3.2, < 82.0.0
4 changes: 2 additions & 2 deletions tarantool/msgpack_ext/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def get_int_as_bytes(data, size):
return data.to_bytes(size, byteorder=BYTEORDER, signed=True)


def encode(obj, _):
def encode(obj, _packer, _tarantool_version):
"""
Encode a datetime object.

Expand Down Expand Up @@ -135,7 +135,7 @@ def get_bytes_as_int(data, cursor, size):
return int.from_bytes(part, BYTEORDER, signed=True), cursor + size


def decode(data, _):
def decode(data, _unpacker, _tarantool_version):
"""
Decode a datetime object.

Expand Down
74 changes: 60 additions & 14 deletions tarantool/msgpack_ext/decimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,46 @@
import msgpack

from tarantool.error import MsgpackError, MsgpackWarning, warn
from tarantool.utils import version_id

EXT_ID = 1
"""
`decimal`_ type id.
"""

TARANTOOL_DECIMAL_MAX_DIGITS = 38
TARANTOOL_DECIMAL_MAX_DIGITS_V35 = 76
TARANTOOL_DECIMAL_76_DIGITS_VERSION = version_id(3, 5, 0)


def get_tarantool_decimal_max_digits(tarantool_version=None):
"""
Get max decimal precision supported by Tarantool version.

:param tarantool_version: Tarantool version identifier.
:type tarantool_version: :obj:`int`, optional

:rtype: :obj:`int`
"""

if tarantool_version is not None \
and tarantool_version >= TARANTOOL_DECIMAL_76_DIGITS_VERSION:
return TARANTOOL_DECIMAL_MAX_DIGITS_V35

return TARANTOOL_DECIMAL_MAX_DIGITS


def decimal_max_digits_errmsg(max_digits):
"""
Build an error / warning message for max decimal precision.

:param max_digits: Max supported precision.
:type max_digits: :obj:`int`

:rtype: :obj:`str`
"""

return f'Tarantool decimal supports a maximum of {max_digits} digits.'


def get_mp_sign(sign):
Expand Down Expand Up @@ -114,7 +147,7 @@ def add_mp_digit(digit, bytes_reverted, digit_count):
bytes_reverted.append(digit)


def check_valid_tarantool_decimal(str_repr, scale, first_digit_ind):
def check_valid_tarantool_decimal(str_repr, scale, first_digit_ind, tarantool_version=None):
"""
Decimal numbers have 38 digits of precision, that is, the total
number of digits before and after the decimal point can be 38. If
Expand Down Expand Up @@ -170,6 +203,9 @@ def check_valid_tarantool_decimal(str_repr, scale, first_digit_ind):
representation.
:type first_digit_ind: :obj:`int`

:param tarantool_version: Tarantool version identifier.
:type tarantool_version: :obj:`int`, optional

:return: ``True``, if decimal can be encoded to Tarantool decimal
without precision loss. ``False`` otherwise.
:rtype: :obj:`bool`
Expand All @@ -179,31 +215,33 @@ def check_valid_tarantool_decimal(str_repr, scale, first_digit_ind):
:meta private:
"""

max_digits = get_tarantool_decimal_max_digits(tarantool_version)

if scale > 0:
digit_count = len(str_repr) - 1 - first_digit_ind
else:
digit_count = len(str_repr) - first_digit_ind

if digit_count <= TARANTOOL_DECIMAL_MAX_DIGITS:
if digit_count <= max_digits:
return True

if (digit_count - scale) > TARANTOOL_DECIMAL_MAX_DIGITS:
raise MsgpackError('Decimal cannot be encoded: Tarantool decimal '
'supports a maximum of 38 digits.')
if (digit_count - scale) > max_digits:
raise MsgpackError('Decimal cannot be encoded: '
+ decimal_max_digits_errmsg(max_digits))

starts_with_zero = str_repr[first_digit_ind] == '0'

if (digit_count > TARANTOOL_DECIMAL_MAX_DIGITS + 1) or \
(digit_count == TARANTOOL_DECIMAL_MAX_DIGITS + 1 and not starts_with_zero):
if (digit_count > max_digits + 1) or \
(digit_count == max_digits + 1 and not starts_with_zero):
warn('Decimal encoded with loss of precision: '
'Tarantool decimal supports a maximum of 38 digits.',
+ decimal_max_digits_errmsg(max_digits),
MsgpackWarning)
return False

return True


def strip_decimal_str(str_repr, scale, first_digit_ind):
def strip_decimal_str(str_repr, scale, first_digit_ind, tarantool_version=None):
"""
Strip decimal digits after the decimal point if decimal cannot be
represented as Tarantool decimal without precision loss.
Expand All @@ -218,26 +256,34 @@ def strip_decimal_str(str_repr, scale, first_digit_ind):
representation.
:type first_digit_ind: :obj:`int`

:param tarantool_version: Tarantool version identifier.
:type tarantool_version: :obj:`int`, optional

:meta private:
"""

max_digits = get_tarantool_decimal_max_digits(tarantool_version)

assert scale > 0
# Strip extra bytes
str_repr = str_repr[:TARANTOOL_DECIMAL_MAX_DIGITS + 1 + first_digit_ind]
str_repr = str_repr[:max_digits + 1 + first_digit_ind]

str_repr = str_repr.rstrip('0')
str_repr = str_repr.rstrip('.')
# Do not strips zeroes before the decimal point
return str_repr


def encode(obj, _):
def encode(obj, _packer, tarantool_version=None):
"""
Encode a decimal object.

:param obj: Decimal to encode.
:type obj: :obj:`decimal.Decimal`

:param tarantool_version: Tarantool version identifier.
:type tarantool_version: :obj:`int`, optional

:return: Encoded decimal.
:rtype: :obj:`bytes`

Expand All @@ -262,8 +308,8 @@ def encode(obj, _):
sign = '+'
first_digit_ind = 0

if not check_valid_tarantool_decimal(str_repr, scale, first_digit_ind):
str_repr = strip_decimal_str(str_repr, scale, first_digit_ind)
if not check_valid_tarantool_decimal(str_repr, scale, first_digit_ind, tarantool_version):
str_repr = strip_decimal_str(str_repr, scale, first_digit_ind, tarantool_version)

bytes_reverted.append(get_mp_sign(sign))

Expand Down Expand Up @@ -342,7 +388,7 @@ def add_str_digit(digit, digits_reverted, scale):
digits_reverted.append(str(digit))


def decode(data, _):
def decode(data, _unpacker, _tarantool_version):
"""
Decode a decimal object.

Expand Down
4 changes: 2 additions & 2 deletions tarantool/msgpack_ext/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"""


def encode(obj, packer):
def encode(obj, packer, _tarantool_version):
"""
Encode an error object.

Expand All @@ -35,7 +35,7 @@ def encode(obj, packer):
return packer.pack(err_map)


def decode(data, unpacker):
def decode(data, unpacker, _tarantool_version):
"""
Decode an error object.

Expand Down
4 changes: 2 additions & 2 deletions tarantool/msgpack_ext/interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"""


def encode(obj, _):
def encode(obj, _packer, _tarantool_version):
"""
Encode an interval object.

Expand Down Expand Up @@ -81,7 +81,7 @@ def encode(obj, _):
return buf


def decode(data, unpacker):
def decode(data, unpacker, _tarantool_version):
"""
Decode an interval object.

Expand Down
9 changes: 7 additions & 2 deletions tarantool/msgpack_ext/packer.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
]


def default(obj, packer=None):
def default(obj, packer=None, tarantool_version=None):
"""
:class:`msgpack.Packer` encoder.

Expand All @@ -41,6 +41,9 @@ def default(obj, packer=None):
(like dictionary in extended error payload)
:type packer: :class:`msgpack.Packer`, optional

:param tarantool_version: Tarantool version identifier.
:type tarantool_version: :obj:`int`, optional

:return: Encoded value.
:rtype: :class:`msgpack.ExtType`

Expand All @@ -49,5 +52,7 @@ def default(obj, packer=None):

for encoder in encoders:
if isinstance(obj, encoder['type']):
return ExtType(encoder['ext'].EXT_ID, encoder['ext'].encode(obj, packer))
return ExtType(
encoder['ext'].EXT_ID, encoder['ext'].encode(obj, packer, tarantool_version),
)
raise TypeError(f"Unknown type: {repr(obj)}")
7 changes: 5 additions & 2 deletions tarantool/msgpack_ext/unpacker.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
}


def ext_hook(code, data, unpacker=None):
def ext_hook(code, data, unpacker=None, tarantool_version=None):
"""
:class:`msgpack.Unpacker` decoder.

Expand All @@ -34,6 +34,9 @@ def ext_hook(code, data, unpacker=None):
(like dictionary in extended error payload)
:type unpacker: :class:`msgpack.Unpacker`, optional

:param tarantool_version: Tarantool version identifier.
:type tarantool_version: :obj:`int`, optional

:return: Decoded value.
:rtype: :class:`decimal.Decimal` or :class:`uuid.UUID` or
or :class:`tarantool.BoxError` or :class:`tarantool.Datetime`
Expand All @@ -43,5 +46,5 @@ def ext_hook(code, data, unpacker=None):
"""

if code in decoders:
return decoders[code](data, unpacker)
return decoders[code](data, unpacker, tarantool_version)
raise NotImplementedError(f"Unknown msgpack extension type code {code}")
4 changes: 2 additions & 2 deletions tarantool/msgpack_ext/uuid.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"""


def encode(obj, _):
def encode(obj, _packer, _tarantool_version):
"""
Encode an UUID object.

Expand All @@ -35,7 +35,7 @@ def encode(obj, _):
return obj.bytes


def decode(data, _):
def decode(data, _unpacker, _tarantool_version):
"""
Decode an UUID object.

Expand Down
2 changes: 1 addition & 1 deletion tarantool/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def packer_factory(conn):
# inside extension type packers.
def default(obj):
packer_no_ext = msgpack.Packer(**packer_kwargs)
return packer_default(obj, packer_no_ext)
return packer_default(obj, packer_no_ext, conn.version_id)
packer_kwargs['default'] = default

return msgpack.Packer(**packer_kwargs)
Expand Down
Loading