Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python: add BITPOS command #1604

Merged
merged 4 commits into from
Jun 19, 2024
Merged
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
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
>>> await client.set("key1", "A12") # "A1" has binary value 01000001 00110001 00110010
>>> await client.set("key1", "A12") # "A12" 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):
Comment on lines +4959 to +4960
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reduce indentation for this check to move it outside of else block

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
Loading