Skip to content

Commit bf11727

Browse files
feat: add support of valkey
1 parent 2d0aa4e commit bf11727

5 files changed

Lines changed: 99 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ This project uses [*towncrier*](https://towncrier.readthedocs.io/) and the chang
1010

1111
## 0.2
1212

13+
### 0.2.2
14+
- Valkey backend support
15+
- Example for Valkey backend
16+
1317
### 0.2.1
1418
- Fix picklecoder
1519
- Fix connection failure transparency and add logging

examples/valkey/Readme.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Valkey Backend Example
2+
3+
This example demonstrates using FastAPI-Cache with Valkey as the backend.
4+
5+
## Prerequisites
6+
7+
1. Install Valkey:
8+
```bash
9+
# Using Docker
10+
docker run -d -p 6379:6379 valkey/valkey:latest
11+
12+
# Or install locally
13+
# See: https://valkey.io/download/
14+
```
15+
16+
2. Install dependencies:
17+
```bash
18+
poetry install
19+
# or
20+
pip install fastapi-cache2[valkey]
21+
```
22+
23+
## Running the Example
24+
25+
```bash
26+
cd examples/valkey
27+
fastapi dev main.py
28+
```
29+
30+
## Endpoints
31+
32+
- `GET /` - Cached endpoint (10s TTL)
33+
- `GET /clear` - Clear cache
34+
- `GET /date` - Get cached date
35+
- `GET /datetime` - Get cached datetime
36+
- `GET /blocking` - Sync cached endpoint
37+
- `GET /html` - Cached HTML response
38+
- `GET /cache_response_obj` - Cached JSON response
39+
40+
## Configuration
41+
42+
The example uses these Valkey settings:
43+
- Host: localhost
44+
- Port: 6379
45+
- DB: 0
46+
- decode_responses: False (required for pickle coder)
47+
```

examples/valkey/main.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,38 @@
1010
from fastapi.staticfiles import StaticFiles
1111
from fastapi.templating import Jinja2Templates
1212
from fastapi_cache import FastAPICache
13-
from fastapi_cache.backends.redis import RedisBackend
13+
from fastapi_cache.backends.valkey import ValkeyBackend
1414
from fastapi_cache.coder import PickleCoder
1515
from fastapi_cache.decorator import cache
1616
from starlette.requests import Request
1717
from starlette.responses import JSONResponse, Response
1818

19-
import redis.asyncio as redis
20-
from redis.asyncio.connection import ConnectionPool
19+
from valkey.asyncio import Valkey
2120

2221

2322
@asynccontextmanager
2423
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
25-
pool = ConnectionPool.from_url(url="redis://redis")
26-
r = redis.Redis(connection_pool=pool)
27-
FastAPICache.init(RedisBackend(r), prefix="fastapi-cache")
24+
client = Valkey(
25+
host="localhost",
26+
port=6379,
27+
db=0,
28+
decode_responses=False,
29+
)
30+
31+
# Test the connection
32+
try:
33+
await client.ping()
34+
print(f"✓ Connected to Valkey at localhost:6379")
35+
except Exception as e:
36+
print(f"✗ Failed to connect to Valkey: {e}")
37+
raise
38+
39+
FastAPICache.init(ValkeyBackend(client), prefix="fastapi-cache")
40+
2841
yield
42+
43+
print("Closing Valkey connection...")
44+
await client.close()
2945

3046

3147
app = FastAPI(lifespan=lifespan)
@@ -63,10 +79,9 @@ async def get_data(request: Request, response: Response):
6379
return pendulum.today()
6480

6581

66-
# Note: This function MUST be sync to demonstrate fastapi-cache's correct handling,
67-
# i.e. running cached sync functions in threadpool just like FastAPI itself!
82+
# MUST be sync to verify threadpool + cache handling
6883
@app.get("/blocking")
69-
@cache(namespace="test", expire=10) # pyright: ignore[reportArgumentType]
84+
@cache(namespace="test", expire=10) # pyright: ignore[reportArgumentType]
7085
def blocking():
7186
time.sleep(2)
7287
return {"ret": 42}
@@ -82,7 +97,9 @@ async def get_datetime(request: Request, response: Response):
8297
@app.get("/html", response_class=HTMLResponse)
8398
@cache(expire=60, namespace="html", coder=PickleCoder)
8499
async def cache_html(request: Request):
85-
return templates.TemplateResponse("index.html", {"request": request, "ret": await get_ret()})
100+
return templates.TemplateResponse(
101+
"index.html", {"request": request, "ret": await get_ret()}
102+
)
86103

87104

88105
@app.get("/cache_response_obj")
@@ -92,4 +109,4 @@ async def cache_response_obj():
92109

93110

94111
if __name__ == "__main__":
95-
uvicorn.run("main:app", reload=True)
112+
uvicorn.run("main:app", reload=True)

fastapi_cache/backends/valkey.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,28 @@ def __init__(self, valkey: Union["Valkey[bytes]", "ValkeyCluster[bytes]"]):
1313

1414
async def get_with_ttl(self, key: str) -> Tuple[int, Optional[bytes]]:
1515
async with self.valkey.pipeline(transaction=not self.is_cluster) as pipe:
16-
# return await pipe.ttl(key).get(key).execute()
17-
return await pipe.ttl(key).get(key).execute()
16+
return await pipe.ttl(key).get(key).execute() # type: ignore[union-attr,no-any-return]
1817

1918
async def get(self, key: str) -> Optional[bytes]:
20-
return await self.valkey.get(key)
19+
return await self.valkey.get(key) # type: ignore[union-attr]
2120

2221
async def set(self, key: str, value: bytes, expire: Optional[int] = None) -> None:
23-
await self.valkey.set(key, value, ex=expire)
22+
await self.valkey.set(key, value, ex=expire) # type: ignore[union-attr]
2423

2524
async def clear(self, namespace: Optional[str] = None, key: Optional[str] = None) -> int:
2625
if namespace:
27-
lua = f"for i, name in ipairs(valkey.call('KEYS', '{namespace}:*')) do valkey.call('DEL', name); end"
28-
return await self.valkey.eval(lua, numkeys=0)
29-
26+
cursor = 0
27+
deleted = 0
28+
pattern = f"{namespace}:*"
29+
30+
while True:
31+
cursor, keys = await self.valkey.scan(cursor, match=pattern, count=100) # type: ignore[union-attr]
32+
if keys:
33+
deleted += await self.valkey.delete(*keys) # type: ignore[union-attr]
34+
if cursor == 0:
35+
break
36+
37+
return deleted
3038
elif key:
31-
return await self.valkey.delete(key)
32-
33-
return 0
39+
return await self.valkey.delete(key) # type: ignore[union-attr]
40+
return 0

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pendulum = "^3.0.0"
2424
aiomcache = { version = "^0.8.2", optional = true }
2525
aiobotocore = {version = "^2.13.1", optional = true}
2626
redis = {version = "^5.0.8", extras = ["redis"]}
27-
valkey = "^6.1.1"
27+
valkey = { version = "^6.0.0", optional = true }
2828

2929
[tool.poetry.group.linting]
3030
optional = true
@@ -53,8 +53,8 @@ twine = { version = "^4.0.2", python = "^3.10" }
5353
[tool.poetry.extras]
5454
redis = ["redis"]
5555
memcache = ["aiomcache"]
56-
dynamodb = ["aiobotocore"]
57-
all = ["redis", "aiomcache", "aiobotocore"]
56+
valkey = ["valkey"]
57+
all = ["redis", "aiomcache", "valkey"]
5858

5959
[tool.mypy]
6060
files = ["."]

0 commit comments

Comments
 (0)