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
48 changes: 34 additions & 14 deletions tarantool/msgpack_ext/decimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,18 @@
`decimal`_ type id.
"""

TARANTOOL_DECIMAL_MAX_DIGITS = 38

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 +125,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, max_digits):
"""
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 +181,9 @@ def check_valid_tarantool_decimal(str_repr, scale, first_digit_ind):
representation.
:type first_digit_ind: :obj:`int`

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

:return: ``True``, if decimal can be encoded to Tarantool decimal
without precision loss. ``False`` otherwise.
:rtype: :obj:`bool`
Expand All @@ -184,26 +198,26 @@ def check_valid_tarantool_decimal(str_repr, scale, 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, max_digits):
"""
Strip decimal digits after the decimal point if decimal cannot be
represented as Tarantool decimal without precision loss.
Expand All @@ -218,26 +232,32 @@ def strip_decimal_str(str_repr, scale, first_digit_ind):
representation.
:type first_digit_ind: :obj:`int`

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

:meta private:
"""

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, max_digits):
"""
Encode a decimal object.

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

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

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

Expand All @@ -262,8 +282,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, max_digits):
str_repr = strip_decimal_str(str_repr, scale, first_digit_ind, max_digits)

bytes_reverted.append(get_mp_sign(sign))

Expand Down
76 changes: 76 additions & 0 deletions tarantool/msgpack_ext/extensions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""
This module provides Tarantool `extension`_ types handlers.

.. _extension: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/
"""
# encoding: utf-8
import functools
from collections import namedtuple

from decimal import Decimal
from uuid import UUID
from tarantool.types import BoxError
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.error as ext_error
import tarantool.msgpack_ext.datetime as ext_datetime
import tarantool.msgpack_ext.interval as ext_interval
from tarantool.utils import version_id


class Extension(namedtuple("Extension", "decode encode type")):
"""
MessagePack extension type handlers.

:ivar decode: Extension type decoder.
:vartype decode: :obj:`callable`

:ivar encode: Extension type encoder.
:vartype encode: :obj:`callable`

:ivar type: Python type represented by the extension.
:vartype type: :obj:`type`
"""


TARANTOOL_DECIMAL_MAX_DIGITS = 38
TARANTOOL_DECIMAL_MAX_DIGITS_V35 = 76


def init_msgpack_extensions(tarantool_version=None):
"""
Initialize MessagePack extension type handlers.

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

:return: Mapping from Tarantool extension type id to its handlers.
:rtype: :obj:`dict`
"""

max_digits = TARANTOOL_DECIMAL_MAX_DIGITS
if tarantool_version is not None and tarantool_version >= version_id(3, 5, 0):
max_digits = TARANTOOL_DECIMAL_MAX_DIGITS_V35

return {
ext_decimal.EXT_ID: Extension(
ext_decimal.decode,
functools.partial(ext_decimal.encode, max_digits=max_digits),
Decimal,
),
ext_uuid.EXT_ID: Extension(
ext_uuid.decode, ext_uuid.encode, UUID,
),
ext_error.EXT_ID: Extension(
ext_error.decode, ext_error.encode, BoxError,
),
ext_datetime.EXT_ID: Extension(
ext_datetime.decode, ext_datetime.encode, Datetime,
),
ext_interval.EXT_ID: Extension(
ext_interval.decode, ext_interval.encode, Interval,
),
}
32 changes: 8 additions & 24 deletions tarantool/msgpack_ext/packer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,13 @@

.. _extension: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/
"""
# pylint: disable=duplicate-code

from decimal import Decimal
from uuid import UUID
from msgpack import ExtType

from tarantool.types import BoxError
from tarantool.msgpack_ext.types.datetime import Datetime
from tarantool.msgpack_ext.types.interval import Interval
from tarantool.msgpack_ext.extensions import init_msgpack_extensions

import tarantool.msgpack_ext.decimal as ext_decimal
import tarantool.msgpack_ext.uuid as ext_uuid
import tarantool.msgpack_ext.error as ext_error
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': BoxError, 'ext': ext_error},
{'type': Datetime, 'ext': ext_datetime},
{'type': Interval, 'ext': ext_interval},
]


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

Expand All @@ -41,13 +22,16 @@ 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`

:raise: :exc:`~TypeError`
"""

for encoder in encoders:
if isinstance(obj, encoder['type']):
return ExtType(encoder['ext'].EXT_ID, encoder['ext'].encode(obj, packer))
for ext_id, ext in init_msgpack_extensions(tarantool_version).items():
if isinstance(obj, ext.type):
return ExtType(ext_id, ext.encode(obj, packer))
raise TypeError(f"Unknown type: {repr(obj)}")
26 changes: 8 additions & 18 deletions tarantool/msgpack_ext/unpacker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,10 @@

.. _extension: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/
"""
# pylint: disable=duplicate-code
from tarantool.msgpack_ext.extensions import init_msgpack_extensions

import tarantool.msgpack_ext.decimal as ext_decimal
import tarantool.msgpack_ext.uuid as ext_uuid
import tarantool.msgpack_ext.error as ext_error
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_error.EXT_ID: ext_error.decode,
ext_datetime.EXT_ID: ext_datetime.decode,
ext_interval.EXT_ID: ext_interval.decode,
}


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

Expand All @@ -34,14 +20,18 @@ 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`
or :class:`tarantool.Interval`

:raise: :exc:`NotImplementedError`
"""
ext = init_msgpack_extensions(tarantool_version)

if code in decoders:
return decoders[code](data, unpacker)
if code in ext:
return ext[code].decode(data, unpacker)
raise NotImplementedError(f"Unknown msgpack extension type code {code}")
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