diff --git a/CHANGELOG.md b/CHANGELOG.md index b021a031f3..484a5e6b1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ * Python: Added BITOP command ([#1596](https://github.com/aws/glide-for-redis/pull/1596)) * Python: Added BITPOS command ([#1604](https://github.com/aws/glide-for-redis/pull/1604)) * Python: Added GETEX command ([#1612](https://github.com/aws/glide-for-redis/pull/1612)) +* Python: Added BITFIELD and BITFIELD_RO commands ([#1615](https://github.com/aws/glide-for-redis/pull/1615)) ### Breaking Changes * Node: Update XREAD to return a Map of Map ([#1494](https://github.com/aws/glide-for-redis/pull/1494)) diff --git a/python/python/glide/__init__.py b/python/python/glide/__init__.py index 393cd966a3..38e09d950c 100644 --- a/python/python/glide/__init__.py +++ b/python/python/glide/__init__.py @@ -1,6 +1,22 @@ # Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 -from glide.async_commands.bitmap import BitmapIndexType, BitwiseOperation, OffsetOptions +from glide.async_commands.bitmap import ( + BitEncoding, + BitFieldGet, + BitFieldIncrBy, + BitFieldOffset, + BitFieldOverflow, + BitFieldSet, + BitFieldSubCommands, + BitmapIndexType, + BitOffset, + BitOffsetMultiplier, + BitOverflowControl, + BitwiseOperation, + OffsetOptions, + SignedEncoding, + UnsignedEncoding, +) from glide.async_commands.command_args import Limit, ListDirection, OrderBy from glide.async_commands.core import ( ConditionalChange, @@ -90,8 +106,21 @@ # Response "OK", # Commands + "BitEncoding", + "BitFieldGet", + "BitFieldIncrBy", + "BitFieldOffset", + "BitFieldOverflow", + "BitFieldSet", + "BitFieldSubCommands", "BitmapIndexType", + "BitOffset", + "BitOffsetMultiplier", + "BitOverflowControl", "BitwiseOperation", + "OffsetOptions", + "SignedEncoding", + "UnsignedEncoding", "Script", "ScoreBoundary", "ConditionalChange", @@ -112,7 +141,6 @@ "LexBoundary", "Limit", "ListDirection", - "OffsetOptions", "RangeByIndex", "RangeByLex", "RangeByScore", diff --git a/python/python/glide/async_commands/bitmap.py b/python/python/glide/async_commands/bitmap.py index 8b073bd2a1..a8ac48d13f 100644 --- a/python/python/glide/async_commands/bitmap.py +++ b/python/python/glide/async_commands/bitmap.py @@ -1,4 +1,5 @@ # Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +from abc import ABC, abstractmethod from enum import Enum from typing import List, Optional @@ -60,3 +61,245 @@ class BitwiseOperation(Enum): OR = "OR" XOR = "XOR" NOT = "NOT" + + +class BitEncoding(ABC): + """ + Abstract Base Class used to specify a signed or unsigned argument encoding for the `BITFIELD` or `BITFIELD_RO` + commands. + """ + + @abstractmethod + def to_arg(self) -> str: + """ + Returns the encoding as a string argument to be used in the `BITFIELD` or `BITFIELD_RO` + commands. + """ + pass + + +class SignedEncoding(BitEncoding): + # Prefix specifying that the encoding is signed. + SIGNED_ENCODING_PREFIX = "i" + + def __init__(self, encoding_length: int): + """ + Represents a signed argument encoding. Must be less than 65 bits long. + + Args: + encoding_length (int): The bit size of the encoding. + """ + self._encoding = f"{self.SIGNED_ENCODING_PREFIX}{str(encoding_length)}" + + def to_arg(self) -> str: + return self._encoding + + +class UnsignedEncoding(BitEncoding): + # Prefix specifying that the encoding is unsigned. + UNSIGNED_ENCODING_PREFIX = "u" + + def __init__(self, encoding_length: int): + """ + Represents an unsigned argument encoding. Must be less than 64 bits long. + + Args: + encoding_length (int): The bit size of the encoding. + """ + self._encoding = f"{self.UNSIGNED_ENCODING_PREFIX}{str(encoding_length)}" + + def to_arg(self) -> str: + return self._encoding + + +class BitFieldOffset(ABC): + """Abstract Base Class representing an offset for an array of bits for the `BITFIELD` or `BITFIELD_RO` commands.""" + + @abstractmethod + def to_arg(self) -> str: + """ + Returns the offset as a string argument to be used in the `BITFIELD` or `BITFIELD_RO` + commands. + """ + pass + + +class BitOffset(BitFieldOffset): + def __init__(self, offset: int): + """ + Represents an offset in an array of bits for the `BITFIELD` or `BITFIELD_RO` commands. Must be greater than or + equal to 0. + + For example, if we have the binary `01101001` with offset of 1 for an unsigned encoding of size 4, then the value + is 13 from `0(1101)001`. + + Args: + offset (int): The bit index offset in the array of bits. + """ + self._offset = str(offset) + + def to_arg(self) -> str: + return self._offset + + +class BitOffsetMultiplier(BitFieldOffset): + # Prefix specifying that the offset uses an encoding multiplier. + OFFSET_MULTIPLIER_PREFIX = "#" + + def __init__(self, offset: int): + """ + Represents an offset in an array of bits for the `BITFIELD` or `BITFIELD_RO` commands. The bit offset index is + calculated as the numerical value of the offset multiplied by the encoding value. Must be greater than or equal + to 0. + + For example, if we have the binary 01101001 with offset multiplier of 1 for an unsigned encoding of size 4, then + the value is 9 from `0110(1001)`. + + Args: + offset (int): The offset in the array of bits, which will be multiplied by the encoding value to get the + final bit index offset. + """ + self._offset = f"{self.OFFSET_MULTIPLIER_PREFIX}{str(offset)}" + + def to_arg(self) -> str: + return self._offset + + +class BitFieldSubCommands(ABC): + """Abstract Base Class representing subcommands for the `BITFIELD` or `BITFIELD_RO` commands.""" + + @abstractmethod + def to_args(self) -> List[str]: + """ + Returns the subcommand as a list of string arguments to be used in the `BITFIELD` or `BITFIELD_RO` commands. + """ + pass + + +class BitFieldGet(BitFieldSubCommands): + # "GET" subcommand string for use in the `BITFIELD` or `BITFIELD_RO` commands. + GET_COMMAND_STRING = "GET" + + def __init__(self, encoding: BitEncoding, offset: BitFieldOffset): + """ + Represents the "GET" subcommand for getting a value in the binary representation of the string stored in `key`. + + Args: + encoding (BitEncoding): The bit encoding for the subcommand. + offset (BitFieldOffset): The offset in the array of bits from which to get the value. + """ + self._encoding = encoding + self._offset = offset + + def to_args(self) -> List[str]: + return [self.GET_COMMAND_STRING, self._encoding.to_arg(), self._offset.to_arg()] + + +class BitFieldSet(BitFieldSubCommands): + # "SET" subcommand string for use in the `BITFIELD` command. + SET_COMMAND_STRING = "SET" + + def __init__(self, encoding: BitEncoding, offset: BitFieldOffset, value: int): + """ + Represents the "SET" subcommand for setting bits in the binary representation of the string stored in `key`. + + Args: + encoding (BitEncoding): The bit encoding for the subcommand. + offset (BitOffset): The offset in the array of bits where the value will be set. + value (int): The value to set the bits in the binary value to. + """ + self._encoding = encoding + self._offset = offset + self._value = value + + def to_args(self) -> List[str]: + return [ + self.SET_COMMAND_STRING, + self._encoding.to_arg(), + self._offset.to_arg(), + str(self._value), + ] + + +class BitFieldIncrBy(BitFieldSubCommands): + # "INCRBY" subcommand string for use in the `BITFIELD` command. + INCRBY_COMMAND_STRING = "INCRBY" + + def __init__(self, encoding: BitEncoding, offset: BitFieldOffset, increment: int): + """ + Represents the "INCRBY" subcommand for increasing or decreasing bits in the binary representation of the + string stored in `key`. + + Args: + encoding (BitEncoding): The bit encoding for the subcommand. + offset (BitOffset): The offset in the array of bits where the value will be incremented. + increment (int): The value to increment the bits in the binary value by. + """ + self._encoding = encoding + self._offset = offset + self._increment = increment + + def to_args(self) -> List[str]: + return [ + self.INCRBY_COMMAND_STRING, + self._encoding.to_arg(), + self._offset.to_arg(), + str(self._increment), + ] + + +class BitOverflowControl(Enum): + """ + Enumeration specifying bit overflow controls for the `BITFIELD` command. + """ + + WRAP = "WRAP" + """ + Performs modulo when overflows occur with unsigned encoding. When overflows occur with signed encoding, the value + restarts at the most negative value. When underflows occur with signed encoding, the value restarts at the most + positive value. + """ + SAT = "SAT" + """ + Underflows remain set to the minimum value, and overflows remain set to the maximum value. + """ + FAIL = "FAIL" + """ + Returns `None` when overflows occur. + """ + + +class BitFieldOverflow(BitFieldSubCommands): + # "OVERFLOW" subcommand string for use in the `BITFIELD` command. + OVERFLOW_COMMAND_STRING = "OVERFLOW" + + def __init__(self, overflow_control: BitOverflowControl): + """ + Represents the "OVERFLOW" subcommand that determines the result of the "SET" or "INCRBY" `BITFIELD` subcommands + when an underflow or overflow occurs. + + Args: + overflow_control (BitOverflowControl): The desired overflow behavior. + """ + self._overflow_control = overflow_control + + def to_args(self) -> List[str]: + return [self.OVERFLOW_COMMAND_STRING, self._overflow_control.value] + + +def _create_bitfield_args(subcommands: List[BitFieldSubCommands]) -> List[str]: + args = [] + for subcommand in subcommands: + args.extend(subcommand.to_args()) + + return args + + +def _create_bitfield_read_only_args( + subcommands: List[BitFieldGet], +) -> List[str]: + args = [] + for subcommand in subcommands: + args.extend(subcommand.to_args()) + + return args diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index aab77feaf0..100c398223 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -16,7 +16,15 @@ get_args, ) -from glide.async_commands.bitmap import BitmapIndexType, BitwiseOperation, OffsetOptions +from glide.async_commands.bitmap import ( + BitFieldGet, + BitFieldSubCommands, + BitmapIndexType, + BitwiseOperation, + OffsetOptions, + _create_bitfield_args, + _create_bitfield_read_only_args, +) from glide.async_commands.command_args import Limit, ListDirection, OrderBy from glide.async_commands.sorted_set import ( AggregationType, @@ -4695,6 +4703,72 @@ async def bitop( ), ) + async def bitfield( + self, key: str, subcommands: List[BitFieldSubCommands] + ) -> List[Optional[int]]: + """ + Reads or modifies the array of bits representing the string that is held at `key` based on the specified + `subcommands`. + + See https://valkey.io/commands/bitfield for more details. + + Args: + key (str): The key of the string. + subcommands (List[BitFieldSubCommands]): The subcommands to be performed on the binary value of the string + at `key`, which could be any of the following: + - `BitFieldGet` + - `BitFieldSet` + - `BitFieldIncrBy` + - `BitFieldOverflow` + + Returns: + List[Optional[int]]: An array of results from the executed subcommands: + - `BitFieldGet` returns the value in `Offset` or `OffsetMultiplier`. + - `BitFieldSet` returns the old value in `Offset` or `OffsetMultiplier`. + - `BitFieldIncrBy` returns the new value in `Offset` or `OffsetMultiplier`. + - `BitFieldOverflow` determines the behavior of the "SET" and "INCRBY" subcommands when an overflow or + underflow occurs. "OVERFLOW" does not return a value and does not contribute a value to the list + response. + + Examples: + >>> await client.set("my_key", "A") # "A" has binary value 01000001 + >>> await client.bitfield("my_key", [BitFieldSet(UnsignedEncoding(2), Offset(1), 3), BitFieldGet(UnsignedEncoding(2), Offset(1))]) + [2, 3] # The old value at offset 1 with an unsigned encoding of 2 was 2. The new value at offset 1 with an unsigned encoding of 2 is 3. + """ + args = [key] + _create_bitfield_args(subcommands) + return cast( + List[Optional[int]], + await self._execute_command(RequestType.BitField, args), + ) + + async def bitfield_read_only( + self, key: str, subcommands: List[BitFieldGet] + ) -> List[int]: + """ + Reads the array of bits representing the string that is held at `key` based on the specified `subcommands`. + + See https://valkey.io/commands/bitfield_ro for more details. + + Args: + key (str): The key of the string. + subcommands (List[BitFieldGet]): The "GET" subcommands to be performed. + + Returns: + List[int]: An array of results from the "GET" subcommands. + + Examples: + >>> await client.set("my_key", "A") # "A" has binary value 01000001 + >>> await client.bitfield_read_only("my_key", [BitFieldGet(UnsignedEncoding(2), Offset(1))]) + [2] # The value at offset 1 with an unsigned encoding of 2 is 3. + + Since: Redis version 6.0.0. + """ + args = [key] + _create_bitfield_read_only_args(subcommands) + return cast( + List[int], + await self._execute_command(RequestType.BitFieldReadOnly, args), + ) + async def object_encoding(self, key: str) -> Optional[str]: """ Returns the internal encoding for the Redis object stored at `key`. diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index b936ff52db..0997a33481 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -3,7 +3,15 @@ import threading from typing import List, Mapping, Optional, Tuple, TypeVar, Union -from glide.async_commands.bitmap import BitmapIndexType, BitwiseOperation, OffsetOptions +from glide.async_commands.bitmap import ( + BitFieldGet, + BitFieldSubCommands, + BitmapIndexType, + BitwiseOperation, + OffsetOptions, + _create_bitfield_args, + _create_bitfield_read_only_args, +) from glide.async_commands.command_args import Limit, ListDirection, OrderBy from glide.async_commands.core import ( ConditionalChange, @@ -3196,6 +3204,56 @@ def bitop( RequestType.BitOp, [operation.value, destination] + keys ) + def bitfield( + self: TTransaction, key: str, subcommands: List[BitFieldSubCommands] + ) -> TTransaction: + """ + Reads or modifies the array of bits representing the string that is held at `key` based on the specified + `subcommands`. + + See https://valkey.io/commands/bitfield for more details. + + Args: + key (str): The key of the string. + subcommands (List[BitFieldSubCommands]): The subcommands to be performed on the binary value of the string + at `key`, which could be any of the following: + - `BitFieldGet` + - `BitFieldSet` + - `BitFieldIncrBy` + - `BitFieldOverflow` + + Command response: + List[Optional[int]]: An array of results from the executed subcommands: + - `BitFieldGet` returns the value in `Offset` or `OffsetMultiplier`. + - `BitFieldSet` returns the old value in `Offset` or `OffsetMultiplier`. + - `BitFieldIncrBy` returns the new value in `Offset` or `OffsetMultiplier`. + - `BitFieldOverflow` determines the behavior of the "SET" and "INCRBY" subcommands when an overflow or + underflow occurs. "OVERFLOW" does not return a value and does not contribute a value to the list + response. + """ + args = [key] + _create_bitfield_args(subcommands) + return self.append_command(RequestType.BitField, args) + + def bitfield_read_only( + self: TTransaction, key: str, subcommands: List[BitFieldGet] + ) -> TTransaction: + """ + Reads the array of bits representing the string that is held at `key` based on the specified `subcommands`. + + See https://valkey.io/commands/bitfield_ro for more details. + + Args: + key (str): The key of the string. + subcommands (List[BitFieldGet]): The "GET" subcommands to be performed. + + Command response: + List[int]: An array of results from the "GET" subcommands. + + Since: Redis version 6.0.0. + """ + args = [key] + _create_bitfield_read_only_args(subcommands) + return self.append_command(RequestType.BitFieldReadOnly, args) + def object_encoding(self: TTransaction, key: str) -> TTransaction: """ Returns the internal encoding for the Redis object stored at `key`. diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index 893c361c43..84f7e7e38f 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -11,7 +11,20 @@ import pytest from glide import ClosingError, RequestError, Script -from glide.async_commands.bitmap import BitmapIndexType, BitwiseOperation, OffsetOptions +from glide.async_commands.bitmap import ( + BitFieldGet, + BitFieldIncrBy, + BitFieldOverflow, + BitFieldSet, + BitmapIndexType, + BitOffset, + BitOffsetMultiplier, + BitOverflowControl, + BitwiseOperation, + OffsetOptions, + SignedEncoding, + UnsignedEncoding, +) from glide.async_commands.command_args import Limit, ListDirection, OrderBy from glide.async_commands.core import ( ConditionalChange, @@ -5051,6 +5064,205 @@ async def test_bitop(self, redis_client: TRedisClient): with pytest.raises(RequestError): await redis_client.bitop(BitwiseOperation.AND, destination, [set_key]) + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_bitfield(self, redis_client: TRedisClient): + key1 = get_random_string(10) + key2 = get_random_string(10) + non_existing_key = get_random_string(10) + set_key = get_random_string(10) + foobar = "foobar" + u2 = UnsignedEncoding(2) + u7 = UnsignedEncoding(7) + i3 = SignedEncoding(3) + i8 = SignedEncoding(8) + offset1 = BitOffset(1) + offset5 = BitOffset(5) + offset_multiplier4 = BitOffsetMultiplier(4) + offset_multiplier8 = BitOffsetMultiplier(8) + overflow_set = BitFieldSet(u2, offset1, -10) + overflow_get = BitFieldGet(u2, offset1) + + # binary value: 01100110 01101111 01101111 01100010 01100001 01110010 + assert await redis_client.set(key1, foobar) == OK + + # SET tests + assert await redis_client.bitfield( + key1, + [ + # binary value becomes: 0(10)00110 01101111 01101111 01100010 01100001 01110010 + BitFieldSet(u2, offset1, 2), + # binary value becomes: 01000(011) 01101111 01101111 01100010 01100001 01110010 + BitFieldSet(i3, offset5, 3), + # binary value becomes: 01000011 01101111 01101111 0110(0010 010)00001 01110010 + BitFieldSet(u7, offset_multiplier4, 18), + # addressing with SET or INCRBY bits outside the current string length will enlarge the string, + # zero-padding it, as needed, for the minimal length needed, according to the most far bit touched. + # + # binary value becomes: + # 01000011 01101111 01101111 01100010 01000001 01110010 00000000 00000000 (00010100) + BitFieldSet(i8, offset_multiplier8, 20), + BitFieldGet(u2, offset1), + BitFieldGet(i3, offset5), + BitFieldGet(u7, offset_multiplier4), + BitFieldGet(i8, offset_multiplier8), + ], + ) == [3, -2, 19, 0, 2, 3, 18, 20] + + # INCRBY tests + assert await redis_client.bitfield( + key1, + [ + # binary value becomes: + # 0(11)00011 01101111 01101111 01100010 01000001 01110010 00000000 00000000 00010100 + BitFieldIncrBy(u2, offset1, 1), + # binary value becomes: + # 01100(101) 01101111 01101111 01100010 01000001 01110010 00000000 00000000 00010100 + BitFieldIncrBy(i3, offset5, 2), + # binary value becomes: + # 01100101 01101111 01101111 0110(0001 111)00001 01110010 00000000 00000000 00010100 + BitFieldIncrBy(u7, offset_multiplier4, -3), + # binary value becomes: + # 01100101 01101111 01101111 01100001 11100001 01110010 00000000 00000000 (00011110) + BitFieldIncrBy(i8, offset_multiplier8, 10), + ], + ) == [3, -3, 15, 30] + + # OVERFLOW WRAP is used by default if no OVERFLOW is specified + assert await redis_client.bitfield( + key2, + [ + overflow_set, + BitFieldOverflow(BitOverflowControl.WRAP), + overflow_set, + overflow_get, + ], + ) == [0, 2, 2] + + # OVERFLOW affects only SET or INCRBY after OVERFLOW subcommand + assert await redis_client.bitfield( + key2, + [ + overflow_set, + BitFieldOverflow(BitOverflowControl.SAT), + overflow_set, + overflow_get, + BitFieldOverflow(BitOverflowControl.FAIL), + overflow_set, + ], + ) == [2, 2, 3, None] + + # if the key doesn't exist, the operation is performed as though the missing value was a string with all bits + # set to 0. + assert await redis_client.bitfield( + non_existing_key, [BitFieldSet(UnsignedEncoding(2), BitOffset(3), 2)] + ) == [0] + + # empty subcommands argument returns an empty list + assert await redis_client.bitfield(key1, []) == [] + + # invalid argument - offset must be >= 0 + with pytest.raises(RequestError): + await redis_client.bitfield( + key1, [BitFieldSet(UnsignedEncoding(5), BitOffset(-1), 1)] + ) + + # invalid argument - encoding size must be > 0 + with pytest.raises(RequestError): + await redis_client.bitfield( + key1, [BitFieldSet(UnsignedEncoding(0), BitOffset(1), 1)] + ) + + # invalid argument - unsigned encoding size must be < 64 + with pytest.raises(RequestError): + await redis_client.bitfield( + key1, [BitFieldSet(UnsignedEncoding(64), BitOffset(1), 1)] + ) + + # invalid argument - signed encoding size must be < 65 + with pytest.raises(RequestError): + await redis_client.bitfield( + key1, [BitFieldSet(SignedEncoding(65), BitOffset(1), 1)] + ) + + # key exists, but it is not a string + assert await redis_client.sadd(set_key, [foobar]) == 1 + with pytest.raises(RequestError): + await redis_client.bitfield( + set_key, [BitFieldSet(SignedEncoding(3), BitOffset(1), 2)] + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_bitfield_read_only(self, redis_client: TRedisClient): + min_version = "6.0.0" + if await check_if_server_version_lt(redis_client, min_version): + return pytest.mark.skip(reason=f"Redis version required >= {min_version}") + + key = get_random_string(10) + non_existing_key = get_random_string(10) + set_key = get_random_string(10) + foobar = "foobar" + unsigned_offset_get = BitFieldGet(UnsignedEncoding(2), BitOffset(1)) + + # binary value: 01100110 01101111 01101111 01100010 01100001 01110010 + assert await redis_client.set(key, foobar) == OK + assert await redis_client.bitfield_read_only( + key, + [ + # Get value in: 0(11)00110 01101111 01101111 01100010 01100001 01110010 00010100 + unsigned_offset_get, + # Get value in: 01100(110) 01101111 01101111 01100010 01100001 01110010 00010100 + BitFieldGet(SignedEncoding(3), BitOffset(5)), + # Get value in: 01100110 01101111 01101(111 0110)0010 01100001 01110010 00010100 + BitFieldGet(UnsignedEncoding(7), BitOffsetMultiplier(3)), + # Get value in: 01100110 01101111 (01101111) 01100010 01100001 01110010 00010100 + BitFieldGet(SignedEncoding(8), BitOffsetMultiplier(2)), + ], + ) == [3, -2, 118, 111] + # offset is greater than current length of string: the operation is performed like the missing part all consists + # of bits set to 0. + assert await redis_client.bitfield_read_only( + key, [BitFieldGet(UnsignedEncoding(3), BitOffset(100))] + ) == [0] + # similarly, if the key doesn't exist, the operation is performed as though the missing value was a string with + # all bits set to 0. + assert await redis_client.bitfield_read_only( + non_existing_key, [unsigned_offset_get] + ) == [0] + + # empty subcommands argument returns an empty list + assert await redis_client.bitfield_read_only(key, []) == [] + + # invalid argument - offset must be >= 0 + with pytest.raises(RequestError): + await redis_client.bitfield_read_only( + key, [BitFieldGet(UnsignedEncoding(5), BitOffset(-1))] + ) + + # invalid argument - encoding size must be > 0 + with pytest.raises(RequestError): + await redis_client.bitfield_read_only( + key, [BitFieldGet(UnsignedEncoding(0), BitOffset(1))] + ) + + # invalid argument - unsigned encoding size must be < 64 + with pytest.raises(RequestError): + await redis_client.bitfield_read_only( + key, [BitFieldGet(UnsignedEncoding(64), BitOffset(1))] + ) + + # invalid argument - signed encoding size must be < 65 + with pytest.raises(RequestError): + await redis_client.bitfield_read_only( + key, [BitFieldGet(SignedEncoding(65), BitOffset(1))] + ) + + # key exists, but it is not a string + assert await redis_client.sadd(set_key, [foobar]) == 1 + with pytest.raises(RequestError): + await redis_client.bitfield_read_only(set_key, [unsigned_offset_get]) + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_object_encoding(self, redis_client: TRedisClient): diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index 692a120845..323598288d 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -6,7 +6,17 @@ import pytest from glide import RequestError -from glide.async_commands.bitmap import BitmapIndexType, BitwiseOperation, OffsetOptions +from glide.async_commands.bitmap import ( + BitFieldGet, + BitFieldSet, + BitmapIndexType, + BitOffset, + BitOffsetMultiplier, + BitwiseOperation, + OffsetOptions, + SignedEncoding, + UnsignedEncoding, +) from glide.async_commands.command_args import Limit, ListDirection, OrderBy from glide.async_commands.core import ( ExpiryGetEx, @@ -378,10 +388,8 @@ async def transaction_test( transaction.setbit(key19, 1, 1) args.append(0) - transaction.setbit(key19, 1, 0) - args.append(1) transaction.getbit(key19, 1) - args.append(0) + args.append(1) transaction.set(key20, "foobar") args.append(OK) @@ -392,14 +400,26 @@ async def transaction_test( transaction.bitpos(key20, 1) args.append(1) + if not await check_if_server_version_lt(redis_client, "6.0.0"): + transaction.bitfield_read_only( + key20, [BitFieldGet(SignedEncoding(5), BitOffset(3))] + ) + args.append([6]) + transaction.set(key19, "abcdef") args.append(OK) transaction.bitop(BitwiseOperation.AND, key19, [key19, key20]) args.append(6) transaction.get(key19) args.append("`bc`ab") + transaction.bitfield( + key20, [BitFieldSet(UnsignedEncoding(10), BitOffsetMultiplier(3), 4)] + ) + args.append([609]) if not await check_if_server_version_lt(redis_client, "7.0.0"): + transaction.set(key20, "foobar") + args.append(OK) transaction.bitcount(key20, OffsetOptions(5, 30, BitmapIndexType.BIT)) args.append(17) transaction.bitpos_interval(key20, 1, 44, 50, BitmapIndexType.BIT)