Skip to content

Commit

Permalink
Python: add BITPOS command (#1604)
Browse files Browse the repository at this point in the history
* wip

* Python: add BITPOS command

* Fix example

* Add test for invalid bit argument
  • Loading branch information
aaron-congo authored Jun 19, 2024
1 parent a44c3c4 commit 7423404
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
* Python: Added BITCOUNT command ([#1592](https://github.com/aws/glide-for-redis/pull/1592))
* Python: Added TOUCH command ([#1582](https://github.com/aws/glide-for-redis/pull/1582))
* 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))

### Breaking Changes
* Node: Update XREAD to return a Map of Map ([#1494](https://github.com/aws/glide-for-redis/pull/1494))
Expand Down
84 changes: 83 additions & 1 deletion python/python/glide/async_commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
get_args,
)

from glide.async_commands.bitmap import BitwiseOperation, OffsetOptions
from glide.async_commands.bitmap import BitmapIndexType, BitwiseOperation, OffsetOptions
from glide.async_commands.command_args import Limit, ListDirection, OrderBy
from glide.async_commands.sorted_set import (
AggregationType,
Expand Down Expand Up @@ -4491,6 +4491,88 @@ async def getbit(self, key: str, offset: int) -> int:
await self._execute_command(RequestType.GetBit, [key, str(offset)]),
)

async def bitpos(self, key: str, bit: int, start: Optional[int] = None) -> int:
"""
Returns the position of the first bit matching the given `bit` value. The optional starting offset
`start` is a zero-based index, with `0` being the first byte of the list, `1` being the next byte and so on.
The offset can also be a negative number indicating an offset starting at the end of the list, with `-1` being
the last byte of the list, `-2` being the penultimate, and so on.
See https://valkey.io/commands/bitpos for more details.
Args:
key (str): The key of the string.
bit (int): The bit value to match. Must be `0` or `1`.
start (Optional[int]): The starting offset.
Returns:
int: The position of the first occurrence of `bit` in the binary value of the string held at `key`.
If `start` was provided, the search begins at the offset indicated by `start`.
Examples:
>>> await client.set("key1", "A1") # "A1" has binary value 01000001 00110001
>>> await client.bitpos("key1", 1)
1 # The first occurrence of bit value 1 in the string stored at "key1" is at the second position.
>>> await client.bitpos("key1", 1, -1)
10 # The first occurrence of bit value 1, starting at the last byte in the string stored at "key1", is at the eleventh position.
"""
args = [key, str(bit)] if start is None else [key, str(bit), str(start)]
return cast(
int,
await self._execute_command(RequestType.BitPos, args),
)

async def bitpos_interval(
self,
key: str,
bit: int,
start: int,
end: int,
index_type: Optional[BitmapIndexType] = None,
) -> int:
"""
Returns the position of the first bit matching the given `bit` value. The offsets are zero-based indexes, with
`0` being the first element of the list, `1` being the next, and so on. These offsets can also be negative
numbers indicating offsets starting at the end of the list, with `-1` being the last element of the list, `-2`
being the penultimate, and so on.
If you are using Redis 7.0.0 or above, the optional `index_type` can also be provided to specify whether the
`start` and `end` offsets specify BIT or BYTE offsets. If `index_type` is not provided, BYTE offsets
are assumed. If BIT is specified, `start=0` and `end=2` means to look at the first three bits. If BYTE is
specified, `start=0` and `end=2` means to look at the first three bytes.
See https://valkey.io/commands/bitpos for more details.
Args:
key (str): The key of the string.
bit (int): The bit value to match. Must be `0` or `1`.
start (int): The starting offset.
end (int): The ending offset.
index_type (Optional[BitmapIndexType]): The index offset type. This option can only be specified if you are
using Redis version 7.0.0 or above. Could be either `BitmapIndexType.BYTE` or `BitmapIndexType.BIT`.
If no index type is provided, the indexes will be assumed to be byte indexes.
Returns:
int: The position of the first occurrence from the `start` to the `end` offsets of the `bit` in the binary
value of the string held at `key`.
Examples:
>>> await client.set("key1", "A12") # "A1" has binary value 01000001 00110001 00110010
>>> await client.bitpos_interval("key1", 1, 1, -1)
10 # The first occurrence of bit value 1 in the second byte to the last byte of the string stored at "key1" is at the eleventh position.
>>> await client.bitpos_interval("key1", 1, 2, 9, BitmapIndexType.BIT)
7 # The first occurrence of bit value 1 in the third to tenth bits of the string stored at "key1" is at the eighth position.
"""
if index_type is not None:
args = [key, str(bit), str(start), str(end), index_type.value]
else:
args = [key, str(bit), str(start), str(end)]

return cast(
int,
await self._execute_command(RequestType.BitPos, args),
)

async def bitop(
self, operation: BitwiseOperation, destination: str, keys: List[str]
) -> int:
Expand Down
66 changes: 65 additions & 1 deletion python/python/glide/async_commands/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import threading
from typing import List, Mapping, Optional, Tuple, TypeVar, Union

from glide.async_commands.bitmap import BitwiseOperation, OffsetOptions
from glide.async_commands.bitmap import BitmapIndexType, BitwiseOperation, OffsetOptions
from glide.async_commands.command_args import Limit, ListDirection, OrderBy
from glide.async_commands.core import (
ConditionalChange,
Expand Down Expand Up @@ -3106,6 +3106,70 @@ def getbit(self: TTransaction, key: str, offset: int) -> TTransaction:
"""
return self.append_command(RequestType.GetBit, [key, str(offset)])

def bitpos(
self: TTransaction, key: str, bit: int, start: Optional[int] = None
) -> TTransaction:
"""
Returns the position of the first bit matching the given `bit` value. The optional starting offset
`start` is a zero-based index, with `0` being the first byte of the list, `1` being the next byte and so on.
The offset can also be a negative number indicating an offset starting at the end of the list, with `-1` being
the last byte of the list, `-2` being the penultimate, and so on.
See https://valkey.io/commands/bitpos for more details.
Args:
key (str): The key of the string.
bit (int): The bit value to match. Must be `0` or `1`.
start (Optional[int]): The starting offset.
Command response:
int: The position of the first occurrence of `bit` in the binary value of the string held at `key`.
If `start` was provided, the search begins at the offset indicated by `start`.
"""
args = [key, str(bit)] if start is None else [key, str(bit), str(start)]
return self.append_command(RequestType.BitPos, args)

def bitpos_interval(
self: TTransaction,
key: str,
bit: int,
start: int,
end: int,
index_type: Optional[BitmapIndexType] = None,
) -> TTransaction:
"""
Returns the position of the first bit matching the given `bit` value. The offsets are zero-based indexes, with
`0` being the first element of the list, `1` being the next, and so on. These offsets can also be negative
numbers indicating offsets starting at the end of the list, with `-1` being the last element of the list, `-2`
being the penultimate, and so on.
If you are using Redis 7.0.0 or above, the optional `index_type` can also be provided to specify whether the
`start` and `end` offsets specify BIT or BYTE offsets. If `index_type` is not provided, BYTE offsets
are assumed. If BIT is specified, `start=0` and `end=2` means to look at the first three bits. If BYTE is
specified, `start=0` and `end=2` means to look at the first three bytes.
See https://valkey.io/commands/bitpos for more details.
Args:
key (str): The key of the string.
bit (int): The bit value to match. Must be `0` or `1`.
start (int): The starting offset.
end (int): The ending offset.
index_type (Optional[BitmapIndexType]): The index offset type. This option can only be specified if you are
using Redis version 7.0.0 or above. Could be either `BitmapIndexType.BYTE` or `BitmapIndexType.BIT`.
If no index type is provided, the indexes will be assumed to be byte indexes.
Command response:
int: The position of the first occurrence from the `start` to the `end` offsets of the `bit` in the binary
value of the string held at `key`.
"""
if index_type is not None:
args = [key, str(bit), str(start), str(end), index_type.value]
else:
args = [key, str(bit), str(start), str(end)]

return self.append_command(RequestType.BitPos, args)

def bitop(
self: TTransaction,
operation: BitwiseOperation,
Expand Down
67 changes: 67 additions & 0 deletions python/python/tests/test_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4895,6 +4895,73 @@ async def test_getbit(self, redis_client: TRedisClient):
with pytest.raises(RequestError):
await redis_client.getbit(set_key, 0)

@pytest.mark.parametrize("cluster_mode", [True, False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_bitpos_and_bitpos_interval(self, redis_client: TRedisClient):
key = get_random_string(10)
non_existing_key = get_random_string(10)
set_key = get_random_string(10)
value = (
"?f0obar" # 00111111 01100110 00110000 01101111 01100010 01100001 01110010
)

assert await redis_client.set(key, value) == OK
assert await redis_client.bitpos(key, 0) == 0
assert await redis_client.bitpos(key, 1) == 2
assert await redis_client.bitpos(key, 1, 1) == 9
assert await redis_client.bitpos_interval(key, 0, 3, 5) == 24

# `BITPOS` returns -1 for non-existing strings
assert await redis_client.bitpos(non_existing_key, 1) == -1
assert await redis_client.bitpos_interval(non_existing_key, 1, 3, 5) == -1

# invalid argument - bit value must be 0 or 1
with pytest.raises(RequestError):
await redis_client.bitpos(key, 2)
with pytest.raises(RequestError):
await redis_client.bitpos_interval(key, 2, 3, 5)

# key exists, but it is not a string
assert await redis_client.sadd(set_key, [value]) == 1
with pytest.raises(RequestError):
await redis_client.bitpos(set_key, 1)
with pytest.raises(RequestError):
await redis_client.bitpos_interval(set_key, 1, 1, -1)

if await check_if_server_version_lt(redis_client, "7.0.0"):
# error thrown because BIT and BYTE options were implemented after 7.0.0
with pytest.raises(RequestError):
await redis_client.bitpos_interval(key, 1, 1, -1, BitmapIndexType.BYTE)
with pytest.raises(RequestError):
await redis_client.bitpos_interval(key, 1, 1, -1, BitmapIndexType.BIT)
else:
assert (
await redis_client.bitpos_interval(key, 0, 3, 5, BitmapIndexType.BYTE)
== 24
)
assert (
await redis_client.bitpos_interval(key, 1, 43, -2, BitmapIndexType.BIT)
== 47
)
assert (
await redis_client.bitpos_interval(
non_existing_key, 1, 3, 5, BitmapIndexType.BYTE
)
== -1
)
assert (
await redis_client.bitpos_interval(
non_existing_key, 1, 3, 5, BitmapIndexType.BIT
)
== -1
)

# key exists, but it is not a string
with pytest.raises(RequestError):
await redis_client.bitpos_interval(
set_key, 1, 1, -1, BitmapIndexType.BIT
)

@pytest.mark.parametrize("cluster_mode", [True, False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_bitop(self, redis_client: TRedisClient):
Expand Down
4 changes: 4 additions & 0 deletions python/python/tests/test_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,8 @@ async def transaction_test(
args.append(26)
transaction.bitcount(key20, OffsetOptions(1, 1))
args.append(6)
transaction.bitpos(key20, 1)
args.append(1)

transaction.set(key19, "abcdef")
args.append(OK)
Expand All @@ -392,6 +394,8 @@ async def transaction_test(
if not await check_if_server_version_lt(redis_client, "7.0.0"):
transaction.bitcount(key20, OffsetOptions(5, 30, BitmapIndexType.BIT))
args.append(17)
transaction.bitpos_interval(key20, 1, 44, 50, BitmapIndexType.BIT)
args.append(46)

transaction.geoadd(
key12,
Expand Down

0 comments on commit 7423404

Please sign in to comment.