diff --git a/CHANGELOG.md b/CHANGELOG.md index ca47682d35..b156030e04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Python: Added RENAME command ([#1252](https://github.com/aws/glide-for-redis/pull/1252)) * Python: Added APPEND command ([#1152](https://github.com/aws/glide-for-redis/pull/1152)) * Python: Added GEOADD command ([#1259](https://github.com/aws/glide-for-redis/pull/1259)) +* Python: Added GEODIST command ([#1260](https://github.com/aws/glide-for-redis/pull/1260)) * Python: Added GEOHASH command ([#1281](https://github.com/aws/glide-for-redis/pull/1281)) * Python: Added ZLEXCOUNT command ([#1305](https://github.com/aws/glide-for-redis/pull/1305)) * Python: Added ZREMRANGEBYLEX command ([#1306](https://github.com/aws/glide-for-redis/pull/1306)) diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index e9b4ebafc0..e651a7d365 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -300,7 +300,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { | b"SISMEMBER" | b"PERSIST" | b"SMOVE" => Some(ExpectedReturnType::Boolean), b"SMISMEMBER" => Some(ExpectedReturnType::ArrayOfBools), b"SMEMBERS" | b"SINTER" => Some(ExpectedReturnType::Set), - b"ZSCORE" => Some(ExpectedReturnType::DoubleOrNull), + b"ZSCORE" | b"GEODIST" => Some(ExpectedReturnType::DoubleOrNull), b"ZPOPMIN" | b"ZPOPMAX" => Some(ExpectedReturnType::MapOfStringToDouble), b"JSON.TOGGLE" => Some(ExpectedReturnType::JsonToggleReturnType), b"ZADD" => cmd diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index 4f2515ba2d..698dec1b4b 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -166,6 +166,7 @@ enum RequestType { SDiff = 124; ObjectRefcount = 126; LOLWUT = 100500; + GeoDist = 127; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index 47146f2e24..b9b66f4e25 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -134,6 +134,7 @@ pub enum RequestType { SDiff = 124, ObjectRefcount = 126, LOLWUT = 100500, + GeoDist = 127, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -268,6 +269,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::GeoAdd => RequestType::GeoAdd, ProtobufRequestType::GeoHash => RequestType::GeoHash, ProtobufRequestType::ObjectEncoding => RequestType::ObjectEncoding, + ProtobufRequestType::GeoDist => RequestType::GeoDist, ProtobufRequestType::SDiff => RequestType::SDiff, ProtobufRequestType::ObjectRefcount => RequestType::ObjectRefcount, ProtobufRequestType::LOLWUT => RequestType::LOLWUT, @@ -401,6 +403,7 @@ impl RequestType { RequestType::GeoAdd => Some(cmd("GEOADD")), RequestType::GeoHash => Some(cmd("GEOHASH")), RequestType::ObjectEncoding => Some(get_two_word_command("OBJECT", "ENCODING")), + RequestType::GeoDist => Some(cmd("GEODIST")), RequestType::SDiff => Some(cmd("SDIFF")), RequestType::ObjectRefcount => Some(get_two_word_command("OBJECT", "REFCOUNT")), RequestType::LOLWUT => Some(cmd("LOLWUT")), diff --git a/python/python/glide/__init__.py b/python/python/glide/__init__.py index 83828fea88..0812e0537e 100644 --- a/python/python/glide/__init__.py +++ b/python/python/glide/__init__.py @@ -6,6 +6,7 @@ ExpirySet, ExpiryType, GeospatialData, + GeoUnit, InfoSection, UpdateOptions, ) @@ -57,10 +58,11 @@ "RedisClientConfiguration", "ScoreBoundary", "ConditionalChange", - "GeospatialData", "ExpireOptions", "ExpirySet", "ExpiryType", + "GeoUnit", + "GeospatialData", "InfBound", "InfoSection", "json", diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index 4554dfabb9..f304625e2e 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -149,6 +149,29 @@ def __init__(self, longitude: float, latitude: float): self.latitude = latitude +class GeoUnit(Enum): + """ + Enumeration representing distance units options for the `GEODIST` command. + """ + + METERS = "m" + """ + Represents distance in meters. + """ + KILOMETERS = "km" + """ + Represents distance in kilometers. + """ + MILES = "mi" + """ + Represents distance in miles. + """ + FEET = "ft" + """ + Represents distance in feet. + """ + + class ExpirySet: """SET option: Represents the expiry type and value to be executed with "SET" command.""" @@ -1627,6 +1650,47 @@ async def geoadd( await self._execute_command(RequestType.GeoAdd, args), ) + async def geodist( + self, + key: str, + member1: str, + member2: str, + unit: Optional[GeoUnit] = None, + ) -> Optional[float]: + """ + Returns the distance between two members in the geospatial index stored at `key`. + + See https://valkey.io/commands/geodist for more details. + + Args: + key (str): The key of the sorted set. + member1 (str): The name of the first member. + member2 (str): The name of the second member. + unit (Optional[GeoUnit]): The unit of distance measurement. See `GeoUnit`. + If not specified, the default unit is `METERS`. + + Returns: + Optional[float]: The distance between `member1` and `member2`. + If one or both members do not exist, or if the key does not exist, returns None. + + Examples: + >>> await client.geoadd("my_geo_set", {"Palermo": GeospatialData(13.361389, 38.115556), "Catania": GeospatialData(15.087269, 37.502669)}) + >>> await client.geodist("my_geo_set", "Palermo", "Catania") + 166274.1516 # Indicates the distance between "Palermo" and "Catania" in meters. + >>> await client.geodist("my_geo_set", "Palermo", "Palermo", unit=GeoUnit.KILOMETERS) + 166.2742 # Indicates the distance between "Palermo" and "Palermo" in kilometers. + >>> await client.geodist("my_geo_set", "non-existing", "Palermo", unit=GeoUnit.KILOMETERS) + None # Returns None for non-existing member. + """ + args = [key, member1, member2] + if unit: + args.append(unit.value) + + return cast( + Optional[float], + await self._execute_command(RequestType.GeoDist, args), + ) + async def geohash(self, key: str, members: List[str]) -> List[Optional[str]]: """ Returns the GeoHash strings representing the positions of all the specified members in the sorted set stored at diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 577a01e4ab..30ff00a871 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -8,6 +8,7 @@ ExpireOptions, ExpirySet, GeospatialData, + GeoUnit, InfoSection, InsertPosition, UpdateOptions, @@ -1233,6 +1234,35 @@ def geoadd( return self.append_command(RequestType.GeoAdd, args) + def geodist( + self: TTransaction, + key: str, + member1: str, + member2: str, + unit: Optional[GeoUnit] = None, + ) -> TTransaction: + """ + Returns the distance between two members in the geospatial index stored at `key`. + + See https://valkey.io/commands/geodist for more details. + + Args: + key (str): The key of the sorted set. + member1 (str): The name of the first member. + member2 (str): The name of the second member. + unit (Optional[GeoUnit]): The unit of distance measurement. See `GeoUnit`. + If not specified, the default unit is meters. + + Commands response: + Optional[float]: The distance between `member1` and `member2`. + If one or both members do not exist, or if the key does not exist, returns None. + """ + args = [key, member1, member2] + if unit: + args.append(unit.value) + + return self.append_command(RequestType.GeoDist, args) + def geohash(self: TTransaction, key: str, members: List[str]) -> TTransaction: """ Returns the GeoHash strings representing the positions of all the specified members in the sorted set stored at diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index caed049a64..ea61122240 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -17,6 +17,7 @@ ExpirySet, ExpiryType, GeospatialData, + GeoUnit, InfBound, InfoSection, InsertPosition, @@ -1338,6 +1339,33 @@ async def test_geohash(self, redis_client: TRedisClient): with pytest.raises(RequestError): await redis_client.geohash(key, ["Palermo", "Catania"]) + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_geodist(self, redis_client: TRedisClient): + key, key2 = get_random_string(10), get_random_string(10) + members_coordinates = { + "Palermo": GeospatialData(13.361389, 38.115556), + "Catania": GeospatialData(15.087269, 37.502669), + } + assert await redis_client.geoadd(key, members_coordinates) == 2 + + assert await redis_client.geodist(key, "Palermo", "Catania") == 166274.1516 + assert ( + await redis_client.geodist(key, "Palermo", "Catania", GeoUnit.KILOMETERS) + == 166.2742 + ) + assert await redis_client.geodist(key, "Palermo", "Palermo", GeoUnit.MILES) == 0 + assert ( + await redis_client.geodist( + key, "Palermo", "non-existing-member", GeoUnit.FEET + ) + == None + ) + + assert await redis_client.set(key2, "value") == OK + with pytest.raises(RequestError): + await redis_client.geodist(key2, "Palmero", "Catania") + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_zadd_zaddincr(self, redis_client: TRedisClient): diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index 0f89cfe6fe..c2545e2a19 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -220,6 +220,8 @@ async def transaction_test( }, ) args.append(2) + transaction.geodist(key9, "Palermo", "Catania") + args.append(166274.1516) transaction.geohash(key9, ["Palermo", "Catania", "Place"]) args.append(["sqc8b49rny0", "sqdtr74hyu0", None]) return args