diff --git a/tests/commands/hash/test_hexpireat.py b/tests/commands/hash/test_hexpireat.py new file mode 100644 index 0000000..59b4c2a --- /dev/null +++ b/tests/commands/hash/test_hexpireat.py @@ -0,0 +1,22 @@ +import pytest +import time +from upstash_redis import Redis + + +@pytest.fixture(autouse=True) +def flush_hash(redis: Redis): + hash_name = "myhash" + redis.delete(hash_name) + + +def test_hexpireat_sets_expiry(redis: Redis): + hash_name = "myhash" + field = "field1" + value = "value1" + + redis.hset(hash_name, field, value) + future_timestamp = int(time.time()) + 2 + assert redis.hexpireat(hash_name, field, future_timestamp)[0] == 1 + + time.sleep(3) + assert redis.hexists(hash_name, field) == 0 diff --git a/tests/commands/hash/test_hexpiretime.py b/tests/commands/hash/test_hexpiretime.py new file mode 100644 index 0000000..2f0d64b --- /dev/null +++ b/tests/commands/hash/test_hexpiretime.py @@ -0,0 +1,38 @@ +import pytest +import time +from upstash_redis import Redis + + +@pytest.fixture(autouse=True) +def flush_hash(redis: Redis): + hash_name = "myhash" + redis.delete(hash_name) + + +def test_hexpiretime_returns_correct_expiry(redis: Redis): + hash_name = "myhash" + field = "field1" + value = "value1" + + redis.hset(hash_name, field, value) + redis.hexpire(hash_name, field, 5) + expiry_time = redis.hexpiretime(hash_name, [field])[0] + + assert expiry_time > int(time.time()) + assert expiry_time <= int(time.time()) + 5 + + +def test_hexpiretime_returns_minus1_if_no_expiry(redis: Redis): + hash_name = "myhash" + field = "field1" + value = "value1" + + redis.hset(hash_name, field, value) + assert redis.hexpiretime(hash_name, [field])[0] == -1 + + +def test_hexpiretime_returns_minus2_if_field_does_not_exist(redis: Redis): + hash_name = "myhash" + field = "field1" + + assert redis.hexpiretime(hash_name, [field])[0] == -2 diff --git a/tests/commands/hash/test_hpersist.py b/tests/commands/hash/test_hpersist.py new file mode 100644 index 0000000..f4e20a4 --- /dev/null +++ b/tests/commands/hash/test_hpersist.py @@ -0,0 +1,32 @@ +import pytest +from upstash_redis import Redis + + +@pytest.fixture(autouse=True) +def flush_hash(redis: Redis): + hash_name = "myhash" + redis.delete(hash_name) + + +def test_hpersist_removes_expiry(redis: Redis): + hash_name = "myhash" + field = "field1" + value = "value1" + + redis.hset(hash_name, field, value) + redis.hexpire(hash_name, field, 500) + ttl_before = redis.httl(hash_name, field) + assert ttl_before[0] > 0 + + redis.hpersist(hash_name, field) + ttl_after = redis.httl(hash_name, field) + assert ttl_after == [-1] + + +def test_hpersist_does_nothing_if_no_expiry(redis: Redis): + hash_name = "myhash" + field = "field1" + value = "value1500" + + redis.hset(hash_name, field, value) + assert redis.hpersist(hash_name, field) == [-1] diff --git a/tests/commands/hash/test_hpexpire.py b/tests/commands/hash/test_hpexpire.py new file mode 100644 index 0000000..af44dda --- /dev/null +++ b/tests/commands/hash/test_hpexpire.py @@ -0,0 +1,41 @@ +import pytest +import time +from upstash_redis import Redis + + +@pytest.fixture(autouse=True) +def flush_hash(redis: Redis): + hash_name = "myhash" + redis.delete(hash_name) + + +def test_hpexpire_sets_expiry_in_milliseconds(redis: Redis): + hash_name = "myhash" + field = "field1" + value = "value1" + + redis.hset(hash_name, field, value) + assert redis.hpexpire(hash_name, field, 500) == [1] + + time.sleep(1) + assert redis.hget(hash_name, field) is None + + +def test_hpexpire_does_not_set_expiry_if_field_does_not_exist(redis: Redis): + hash_name = "myhash" + field = "field1" + + assert redis.hpexpire(hash_name, field, 500) == [-2] + + +def test_hpexpire_overwrites_existing_expiry(redis: Redis): + hash_name = "myhash" + field = "field1" + value = "value1" + + redis.hset(hash_name, field, value) + redis.hpexpire(hash_name, field, 1000) + assert redis.hpexpire(hash_name, field, 2000) == [1] + + time.sleep(2.5) + assert redis.hget(hash_name, field) is None diff --git a/tests/commands/hash/test_hpexpireat.py b/tests/commands/hash/test_hpexpireat.py new file mode 100644 index 0000000..ad603f3 --- /dev/null +++ b/tests/commands/hash/test_hpexpireat.py @@ -0,0 +1,22 @@ +import pytest +import time +from upstash_redis import Redis + + +@pytest.fixture(autouse=True) +def flush_hash(redis: Redis): + hash_name = "myhash" + redis.delete(hash_name) + + +def test_hpexpireat_sets_expiry_in_milliseconds(redis: Redis): + hash_name = "myhash" + field = "field1" + value = "value1" + + redis.hset(hash_name, field, value) + future_timestamp_ms = int(time.time() * 1000) + 500 + assert redis.hpexpireat(hash_name, field, future_timestamp_ms) == [1] + + time.sleep(1) + assert redis.hget(hash_name, field) is None diff --git a/tests/commands/hash/test_hpexpiretime.py b/tests/commands/hash/test_hpexpiretime.py new file mode 100644 index 0000000..9e92aac --- /dev/null +++ b/tests/commands/hash/test_hpexpiretime.py @@ -0,0 +1,38 @@ +import pytest +import time +from upstash_redis import Redis + + +@pytest.fixture(autouse=True) +def flush_hash(redis: Redis): + hash_name = "myhash" + redis.delete(hash_name) + + +def test_hpexpiretime_returns_correct_expiry(redis: Redis): + hash_name = "myhash" + field = "field1" + value = "value1" + + redis.hset(hash_name, field, value) + redis.hpexpire(hash_name, field, 500) + expiry_time_ms = redis.hpexpiretime(hash_name, [field])[0] + + assert expiry_time_ms > 0 + assert expiry_time_ms <= int(time.time() * 1000) + 1000 + + +def test_hpexpiretime_returns_minus1_if_no_expiry(redis: Redis): + hash_name = "myhash" + field = "field1" + value = "value1" + + redis.hset(hash_name, field, value) + assert redis.hpexpiretime(hash_name, [field])[0] == -1 + + +def test_hpexpiretime_returns_minus2_if_field_does_not_exist(redis: Redis): + hash_name = "myhash" + field = "field1" + + assert redis.hpexpiretime(hash_name, [field])[0] == -2 diff --git a/tests/commands/hash/test_hpttl.py b/tests/commands/hash/test_hpttl.py new file mode 100644 index 0000000..f7577eb --- /dev/null +++ b/tests/commands/hash/test_hpttl.py @@ -0,0 +1,36 @@ +import pytest +from upstash_redis import Redis + + +@pytest.fixture(autouse=True) +def flush_hash(redis: Redis): + hash_name = "myhash" + redis.delete(hash_name) + + +def test_hpttl_returns_correct_ttl(redis: Redis): + hash_name = "myhash" + field = "field1" + value = "value1" + + redis.hset(hash_name, field, value) + redis.hpexpire(hash_name, field, 1500) + ttl = redis.hpttl(hash_name, [field])[0] + + assert ttl > 0 + + +def test_hpttl_returns_minus1_if_no_expiry(redis: Redis): + hash_name = "myhash" + field = "field1" + value = "value1" + + redis.hset(hash_name, field, value) + assert redis.hpttl(hash_name, [field])[0] == -1 + + +def test_hpttl_returns_minus2_if_field_does_not_exist(redis: Redis): + hash_name = "myhash" + field = "field1" + + assert redis.hpttl(hash_name, [field])[0] == -2 diff --git a/tests/commands/hash/test_httl.py b/tests/commands/hash/test_httl.py new file mode 100644 index 0000000..1a79e0a --- /dev/null +++ b/tests/commands/hash/test_httl.py @@ -0,0 +1,37 @@ +import pytest +from upstash_redis import Redis + + +@pytest.fixture(autouse=True) +def flush_hash(redis: Redis): + hash_name = "myhash" + redis.delete(hash_name) + + +def test_httl_returns_correct_ttl(redis: Redis): + hash_name = "myhash" + field = "field1" + value = "value1" + + redis.hset(hash_name, field, value) + redis.hexpire(hash_name, field, 5) + ttl = redis.httl(hash_name, [field])[0] + + assert ttl > 0 + assert ttl <= 5 + + +def test_httl_returns_minus1_if_no_expiry(redis: Redis): + hash_name = "myhash" + field = "field1" + value = "value1" + + redis.hset(hash_name, field, value) + assert redis.httl(hash_name, [field])[0] == -1 + + +def test_httl_returns_minus2_if_field_does_not_exist(redis: Redis): + hash_name = "myhash" + field = "field1" + + assert redis.httl(hash_name, [field])[0] == -2 diff --git a/upstash_redis/commands.py b/upstash_redis/commands.py index 7946221..3bb5fb6 100644 --- a/upstash_redis/commands.py +++ b/upstash_redis/commands.py @@ -427,6 +427,268 @@ def hexpire( return self.execute(command) + def hpexpire( + self, + key: str, + fields: Union[str, List[str]], + milliseconds: Union[int, datetime.timedelta], + nx: bool = False, + xx: bool = False, + gt: bool = False, + lt: bool = False, + ) -> ResponseT: + """ + Sets a timeout on a hash field in milliseconds. + After the timeout has expired, the hash field will automatically be deleted. + + :param key: The key of the hash. + :param fields: The field(s) within the hash to set the expiry for. + :param milliseconds: The timeout in milliseconds as an int or a datetime.timedelta object. + :param nx: Set expiry only when the field has no expiry. + :param xx: Set expiry only when the field has an existing expiry. + :param gt: Set expiry only when the new expiry is greater than the current one. + :param lt: Set expiry only when the new expiry is less than the current one. + + See https://redis.io/commands/hexpire + """ + if isinstance(milliseconds, datetime.timedelta): + milliseconds = int(milliseconds.total_seconds() * 1000) + + command: List = ["HPEXPIRE", key, milliseconds] + + if nx: + command.append("NX") + if xx: + command.append("XX") + if gt: + command.append("GT") + if lt: + command.append("LT") + + if isinstance(fields, str): + fields = [fields] + + command.extend(["FIELDS", len(fields), *fields]) + + return self.execute(command) + + def hexpireat( + self, + key: str, + fields: Union[str, List[str]], + unix_time_seconds: Union[int, datetime.datetime], + nx: bool = False, + xx: bool = False, + gt: bool = False, + lt: bool = False, + ) -> ResponseT: + """ + Set an expiration time for specific fields in a hash at a given Unix timestamp. + + :param key: The key of the hash. + :param fields: The field or list of fields in the hash to set the expiration for. + :param unix_time_seconds: the timeout in seconds as int or datetime.timedelta object + :param nx: Set expiry only when the key has no expiry + :param xx: Set expiry only when the key has an existing expiry + :param gt: Set expiry only when the new expiry is greater than current one + :param lt: Set expiry only when the new expiry is less than current one + + Example: + ```python + redis.hset("myhash", "field1", "value1") + redis.hexpireat("my_hash", ["field1", "field2"], 1672531200) + ``` + + See https://redis.io/commands/hexpireat + """ + if isinstance(unix_time_seconds, datetime.datetime): + unix_time_seconds = int(unix_time_seconds.timestamp()) + + command: List = ["HEXPIREAT", key, unix_time_seconds] + + if nx: + command.append("NX") + if xx: + command.append("XX") + if gt: + command.append("GT") + if lt: + command.append("LT") + + if isinstance(fields, str): + fields = [fields] + + command.extend(["FIELDS", len(fields), *fields]) + + return self.execute(command) + + def hpexpireat( + self, + key: str, + fields: Union[str, List[str]], + unix_time_milliseconds: Union[int, datetime.datetime], + nx: bool = False, + xx: bool = False, + gt: bool = False, + lt: bool = False, + ) -> ResponseT: + """ + Set an expiration time for specific fields in a hash at a given Unix timestamp in milliseconds. + + :param key: The key of the hash. + :param fields: The field or list of fields in the hash to set the expiration for. + :param unix_time_milliseconds: the timeout milliseconds as int or datetime.timedelta object + :param nx: Set expiry only when the key has no expiry + :param xx: Set expiry only when the key has an existing expiry + :param gt: Set expiry only when the new expiry is greater than current one + :param lt: Set expiry only when the new expiry is less than current one + + Example: + ```python + redis.hset("myhash", "field1", "value1") + redis.hpexpireat("my_hash", ["field1", "field2"], 1672531200000) + ``` + + See https://redis.io/commands/hpexpireat + """ + if isinstance(unix_time_milliseconds, datetime.datetime): + unix_time_milliseconds = int(unix_time_milliseconds.timestamp() * 1000) + + command: List = ["HPEXPIREAT", key, unix_time_milliseconds] + + if nx: + command.append("NX") + if xx: + command.append("XX") + if gt: + command.append("GT") + if lt: + command.append("LT") + + if isinstance(fields, str): + fields = [fields] + + command.extend(["FIELDS", len(fields), *fields]) + + return self.execute(command) + + def httl(self, key: str, fields: Union[str, List[str]]) -> ResponseT: + """ + Retrieve the time-to-live (TTL) of one or more fields within a hash in seconds. + + :param key: The key of the hash. + :param fields: One or more field names within the hash to check the TTL for. + + Example: + ```python + redis.hset("myhash", "field1", "value1") + redis.hpexpireat("my_hash", ["field1", "field2"], 1672531200000) + redis.httl("myhash", "field1") + ``` + + See https://redis.io/commands/httl + """ + + if isinstance(fields, str): + fields = [fields] + + command: List = ["HTTL", key, "FIELDS", len(fields), *fields] + return self.execute(command) + + def hpttl(self, key: str, fields: Union[str, List[str]]) -> ResponseT: + """ + Retrieve the time-to-live (TTL) of one or more fields within a hash in milliseconds. + + :param key: The key of the hash. + :param fields: One or more field names within the hash to check the TTL for. + + Example: + ```python + redis.hset("myhash", "field1", "value1") + redis.hpexpireat("my_hash", ["field1", "field2"], 1672531200000) + redis.hpttl("myhash", "field1") + ``` + + See https://redis.io/commands/hpttl + """ + + if isinstance(fields, str): + fields = [fields] + + command: List = ["HPTTL", key, "FIELDS", len(fields), *fields] + return self.execute(command) + + def hexpiretime(self, key: str, fields: Union[str, List[str]]) -> ResponseT: + """ + Retrieve the expiration time (as unix timestamp seconds) of one or more + hash fields in a Redis hash. + + :param key: The key of the Redis hash. + :param fields: One or more field names within the hash. + + Example: + ```python + redis.hset("myhash", "field1", "value1") + redis.hpexpireat("my_hash", ["field1", "field2"], 1672531200000) + redis.hexpiretime("myhash", "field1") + ``` + + See https://redis.io/commands/hexpiretime + """ + + if isinstance(fields, str): + fields = [fields] + + command: List = ["HEXPIRETIME", key, "FIELDS", len(fields), *fields] + return self.execute(command) + + def hpexpiretime(self, key: str, fields: Union[str, List[str]]) -> ResponseT: + """ + Retrieve the expiration time (as unix timestamp in milliseconds) of one or + more hash fields in a Redis hash. + + :param key: The key of the Redis hash. + :param fields: One or more field names within the hash. + + Example: + ```python + redis.hset("myhash", "field1", "value1") + redis.hpexpireat("my_hash", ["field1", "field2"], 1672531200000) + redis.hpexpiretime("myhash", "field1") + ``` + + See https://redis.io/commands/hpexpiretime + """ + + if isinstance(fields, str): + fields = [fields] + + command: List = ["HPEXPIRETIME", key, "FIELDS", len(fields), *fields] + return self.execute(command) + + def hpersist(self, key: str, fields: Union[str, List[str]]) -> ResponseT: + """ + Removes the expiration from one or more fields in a hash. + + :param key: The key of the Redis hash. + :param fields: One or more field names within the hash to remove expiration from. + + Example: + ```python + redis.hset("myhash", "field1", "value1") + redis.hpexpireat("my_hash", ["field1", "field2"], 1672531200000) + redis.hpersist("myhash", "field1") + ``` + + See https://redis.io/commands/hpersist + """ + + if isinstance(fields, str): + fields = [fields] + + command: List = ["HPERSIST", key, "FIELDS", len(fields), *fields] + return self.execute(command) + def expireat( self, key: str, diff --git a/upstash_redis/commands.pyi b/upstash_redis/commands.pyi index 73ab40d..6b461dd 100644 --- a/upstash_redis/commands.pyi +++ b/upstash_redis/commands.pyi @@ -202,6 +202,41 @@ class Commands: gt: bool = False, lt: bool = False, ) -> List[int]: ... + def hpexpire( + self, + key: str, + fields: Union[str, List[str]], + milliseconds: Union[int, datetime.timedelta], + nx: bool = False, + xx: bool = False, + gt: bool = False, + lt: bool = False, + ) -> List[int]: ... + def hexpireat( + self, + key: str, + fields: Union[str, List[str]], + unix_time_seconds: Union[int, datetime.datetime], + nx: bool = False, + xx: bool = False, + gt: bool = False, + lt: bool = False, + ) -> List[int]: ... + def hpexpireat( + self, + key: str, + fields: Union[str, List[str]], + unix_time_milliseconds: Union[int, datetime.datetime], + nx: bool = False, + xx: bool = False, + gt: bool = False, + lt: bool = False, + ) -> List[int]: ... + def httl(self, key: str, fields: Union[str, List[str]]) -> List[int]: ... + def hpttl(self, key: str, fields: Union[str, List[str]]) -> List[int]: ... + def hexpiretime(self, key: str, fields: Union[str, List[str]]) -> List[int]: ... + def hpexpiretime(self, key: str, fields: Union[str, List[str]]) -> List[int]: ... + def hpersist(self, key: str, fields: Union[str, List[str]]) -> int: ... def hget(self, key: str, field: str) -> Optional[str]: ... def hgetall(self, key: str) -> Dict[str, str]: ... def hincrby(self, key: str, field: str, increment: int) -> int: ... @@ -710,7 +745,46 @@ class AsyncCommands: xx: bool = False, gt: bool = False, lt: bool = False, - ) -> int: ... + ) -> List[int]: ... + async def hpexpire( + self, + key: str, + fields: Union[str, List[str]], + milliseconds: Union[int, datetime.timedelta], + nx: bool = False, + xx: bool = False, + gt: bool = False, + lt: bool = False, + ) -> List[int]: ... + async def hexpireat( + self, + key: str, + fields: Union[str, List[str]], + unix_time_seconds: Union[int, datetime.datetime], + nx: bool = False, + xx: bool = False, + gt: bool = False, + lt: bool = False, + ) -> List[int]: ... + async def hpexpireat( + self, + key: str, + fields: Union[str, List[str]], + unix_time_milliseconds: Union[int, datetime.datetime], + nx: bool = False, + xx: bool = False, + gt: bool = False, + lt: bool = False, + ) -> List[int]: ... + async def httl(self, key: str, fields: Union[str, List[str]]) -> List[int]: ... + async def hpttl(self, key: str, fields: Union[str, List[str]]) -> List[int]: ... + async def hexpiretime( + self, key: str, fields: Union[str, List[str]] + ) -> List[int]: ... + async def hpexpiretime( + self, key: str, fields: Union[str, List[str]] + ) -> List[int]: ... + async def hpersist(self, key: str, fields: Union[str, List[str]]) -> List[int]: ... async def hget(self, key: str, field: str) -> Optional[str]: ... async def hgetall(self, key: str) -> Dict[str, str]: ... async def hincrby(self, key: str, field: str, increment: int) -> int: ... @@ -1263,6 +1337,45 @@ class PipelineCommands: gt: bool = False, lt: bool = False, ) -> PipelineCommands: ... + def hpexpire( + self, + key: str, + fields: Union[str, List[str]], + milliseconds: Union[int, datetime.timedelta], + nx: bool = False, + xx: bool = False, + gt: bool = False, + lt: bool = False, + ) -> PipelineCommands: ... + def hexpireat( + self, + key: str, + fields: Union[str, List[str]], + unix_time_seconds: Union[int, datetime.datetime], + nx: bool = False, + xx: bool = False, + gt: bool = False, + lt: bool = False, + ) -> PipelineCommands: ... + def hpexpireat( + self, + key: str, + fields: Union[str, List[str]], + unix_time_milliseconds: Union[int, datetime.datetime], + nx: bool = False, + xx: bool = False, + gt: bool = False, + lt: bool = False, + ) -> PipelineCommands: ... + def httl(self, key: str, fields: Union[str, List[str]]) -> PipelineCommands: ... + def hpttl(self, key: str, fields: Union[str, List[str]]) -> PipelineCommands: ... + def hexpiretime( + self, key: str, fields: Union[str, List[str]] + ) -> PipelineCommands: ... + def hpexpiretime( + self, key: str, fields: Union[str, List[str]] + ) -> PipelineCommands: ... + def hpersist(self, key: str, fields: Union[str, List[str]]) -> PipelineCommands: ... def hget(self, key: str, field: str) -> PipelineCommands: ... def hgetall(self, key: str) -> PipelineCommands: ... def hincrby(self, key: str, field: str, increment: int) -> PipelineCommands: ...