Skip to content

Commit

Permalink
Add documentation, missing options and datetime support to expire com…
Browse files Browse the repository at this point in the history
…mands (#36)

* Add doc and datetime support to expire

* Add datetime support to other expire commands

* Change signatures

* Add tests for the datetime support

* Add the missing options to expire NX,XX,GT,LT

* Add test for xx and gt options
  • Loading branch information
ytkimirti committed Nov 8, 2023
1 parent 50e1faf commit 874b264
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 12 deletions.
20 changes: 20 additions & 0 deletions tests/commands/asyncio/generic/test_expire.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from tests.execute_on_http import execute_on_http
from upstash_redis.asyncio import Redis

import datetime


@mark.asyncio
async def test(async_redis: Redis) -> None:
Expand All @@ -13,3 +15,21 @@ async def test(async_redis: Redis) -> None:
# Check if the expiry was correctly set.
await sleep(1)
assert await execute_on_http("EXISTS", "string_for_expire") == 0

@mark.asyncio
async def test_with_datetime(async_redis: Redis) -> None:
assert await async_redis.expire("string_for_expire_dt", datetime.timedelta(seconds=1)) is True

# Check if the expiry was correctly set.
await sleep(1)
assert await execute_on_http("EXISTS", "string_for_expire_dt") == 0

@mark.asyncio
async def test_xx(async_redis: Redis) -> None:
# Must fail since it does not have an expiry.
assert await async_redis.expire("string_without_expire", 1, xx=True) is False

@mark.asyncio
async def test_gt(async_redis: Redis) -> None:
# Must fail since it 1 is not greater than infinity.
assert await async_redis.expire("string_without_expire", 1, gt=True) is False
9 changes: 9 additions & 0 deletions tests/commands/asyncio/generic/test_expireat.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from asyncio import sleep
import datetime
from time import time

from pytest import mark
Expand All @@ -19,3 +20,11 @@ async def test(async_redis: Redis) -> None:

await sleep(2)
assert await execute_on_http("EXISTS", "string_for_expireat") == 0

@mark.asyncio
async def test_with_datetime(async_redis: Redis) -> None:
assert await async_redis.expireat("string_for_expireat_dt", datetime.datetime.now() + datetime.timedelta(seconds=1)) is True

# Check if the expiry was correctly set.
await sleep(1)
assert await execute_on_http("EXISTS", "string_for_expireat_dt") == 0
9 changes: 9 additions & 0 deletions tests/commands/asyncio/generic/test_pexpire.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from asyncio import sleep
import datetime

from pytest import mark

Expand All @@ -13,3 +14,11 @@ async def test(async_redis: Redis) -> None:
# Check if the expiry was correctly set.
await sleep(1)
assert await execute_on_http("EXISTS", "string_for_pexpire") == 0

@mark.asyncio
async def test_with_datetime(async_redis: Redis) -> None:
assert await async_redis.pexpire("string_for_pexpire_dt", datetime.timedelta(milliseconds=200)) is True

# Check if the expiry was correctly set.
await sleep(0.2)
assert await execute_on_http("EXISTS", "string_for_pexpire_dt") == 0
9 changes: 9 additions & 0 deletions tests/commands/asyncio/generic/test_pexpireat.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from asyncio import sleep
import datetime
from time import time

from pytest import mark
Expand All @@ -19,3 +20,11 @@ async def test(async_redis: Redis) -> None:

await sleep(2)
assert await execute_on_http("EXISTS", "string_for_pexpireat") == 0

@mark.asyncio
async def test_with_datetime(async_redis: Redis) -> None:
assert await async_redis.pexpireat("string_for_pexpireat_dt", datetime.datetime.now() + datetime.timedelta(milliseconds=200)) is True

# Check if the expiry was correctly set.
await sleep(0.2)
assert await execute_on_http("EXISTS", "string_for_pexpireat_dt") == 0
12 changes: 12 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@
"a",
"string_for_pexpireat",
"a",
# Strings for testing datetime with expire
"string_for_expire_dt",
"a",
"string_for_expireat_dt",
"a",
"string_for_pexpire_dt",
"a",
"string_for_pexpireat_dt",
"a",
"string_without_expire",
"a",
#Other expire stuff
"string_for_persist",
"a",
"string_for_ttl",
Expand Down
176 changes: 172 additions & 4 deletions upstash_redis/commands.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
from typing import Any, Awaitable, Dict, List, Literal, Optional, Tuple, Union

from upstash_redis.typing import FloatMinMaxT
Expand Down Expand Up @@ -165,22 +166,108 @@ def exists(self, *keys: str) -> ResponseT:

return self.execute(command)

def expire(self, key: str, seconds: int) -> ResponseT:
def expire(
self,
key: str,
seconds: Union[int, datetime.timedelta],
nx: bool = False,
xx: bool = False,
gt: bool = False,
lt: bool = False,
) -> ResponseT:
"""
Sets a timeout on a key in seconds.
After the timeout has expired, the key will automatically be deleted.
:param 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
# With seconds
redis.set("mykey", "Hello")
redis.expire("mykey", 5)
assert redis.get("mykey") == "Hello"
time.sleep(5)
assert redis.get("mykey") is None
# With a timedelta
redis.set("mykey", "Hello")
redis.expire("mykey", datetime.timedelta(seconds=5))
```
See https://redis.io/commands/expire
"""

if isinstance(seconds, datetime.timedelta):
seconds = int(seconds.total_seconds())

command: List = ["EXPIRE", key, seconds]

if nx:
command.append("NX")
if xx:
command.append("XX")
if gt:
command.append("GT")
if lt:
command.append("LT")

return self.execute(command)

def expireat(self, key: str, unix_time_seconds: int) -> ResponseT:
def expireat(
self,
key: str,
unix_time_seconds: Union[int, datetime.datetime],
nx: bool = False,
xx: bool = False,
gt: bool = False,
lt: bool = False,
) -> ResponseT:
"""
Expires a key at a specific unix timestamp (seconds).
After the timeout has expired, the key will automatically be deleted.
:param seconds: the timeout in unix seconds timestamp as int or a datetime.datetime 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
# With a datetime object
redis.set("mykey", "Hello")
redis.expireat("mykey", datetime.datetime.now() + datetime.timedelta(seconds=5))
# With a unix timestamp
redis.set("mykey", "Hello")
redis.expireat("mykey", int(time.time()) + 5)
```
See https://redis.io/commands/expireat
"""

if isinstance(unix_time_seconds, datetime.datetime):
unix_time_seconds = int(unix_time_seconds.timestamp())

command: List = ["EXPIREAT", key, unix_time_seconds]

if nx:
command.append("NX")
if xx:
command.append("XX")
if gt:
command.append("GT")
if lt:
command.append("LT")

return self.execute(command)

def keys(self, pattern: str) -> ResponseT:
Expand All @@ -201,22 +288,103 @@ def persist(self, key: str) -> ResponseT:

return self.execute(command)

def pexpire(self, key: str, milliseconds: int) -> ResponseT:
def pexpire(
self,
key: str,
milliseconds: Union[int, datetime.timedelta],
nx: bool = False,
xx: bool = False,
gt: bool = False,
lt: bool = False,
) -> ResponseT:
"""
Sets a timeout on a key in milliseconds.
After the timeout has expired, the key will automatically be deleted.
:param milliseconds: the timeout in milliseconds as int or datetime.timedelta.
: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
# With milliseconds
redis.set("mykey", "Hello")
redis.expire("mykey", 500)
# With a timedelta
redis.set("mykey", "Hello")
redis.expire("mykey", datetime.timedelta(milliseconds=500))
```
See https://redis.io/commands/pexpire
"""

if isinstance(milliseconds, datetime.timedelta):
# Total seconds returns float, so this is OK.
milliseconds = int(milliseconds.total_seconds() * 1000)

command: List = ["PEXPIRE", key, milliseconds]

if nx:
command.append("NX")
if xx:
command.append("XX")
if gt:
command.append("GT")
if lt:
command.append("LT")

return self.execute(command)

def pexpireat(self, key: str, unix_time_milliseconds: int) -> ResponseT:
def pexpireat(
self,
key: str,
unix_time_milliseconds: Union[int, datetime.datetime],
nx: bool = False,
xx: bool = False,
gt: bool = False,
lt: bool = False,
) -> ResponseT:
"""
Expires a key at a specific unix timestamp (milliseconds).
After the timeout has expired, the key will automatically be deleted.
:param unix_time_milliseconds: the timeout in unix miliseconds timestamp as int or a datetime.datetime 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
# With a unix timestamp
redis.set("mykey", "Hello")
redis.pexpireat("mykey", int(time.time() * 1000) )
# With a datetime object
redis.set("mykey", "Hello")
redis.pexpireat("mykey", datetime.datetime.now() + datetime.timedelta(seconds=5))
```
See https://redis.io/commands/pexpireat
"""

if isinstance(unix_time_milliseconds, datetime.datetime):
unix_time_milliseconds = int(unix_time_milliseconds.timestamp() * 1000)

command: List = ["PEXPIREAT", key, unix_time_milliseconds]

if nx:
command.append("NX")
if xx:
command.append("XX")
if gt:
command.append("GT")
if lt:
command.append("LT")

return self.execute(command)

def pttl(self, key: str) -> ResponseT:
Expand Down
Loading

0 comments on commit 874b264

Please sign in to comment.