Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ 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).
- Bump readthedocs build os version to ubuntu-24.04 (PR #342).
Ubuntu-20.04 is no longer supported.
Comment thread
bigbes marked this conversation as resolved.
Outdated

## 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
Loading