mirror of
https://github.com/long2ice/fastapi-cache.git
synced 2026-03-25 13:07:53 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d04be274e9 | ||
|
|
80563fd6e7 | ||
|
|
98cf8a78a1 | ||
|
|
01c895dbbb | ||
|
|
e3b08dda2c | ||
|
|
552a7695e8 | ||
|
|
ea1ffcd7b4 | ||
|
|
e8193b5c22 | ||
|
|
ab26fad604 | ||
|
|
7a89f28b54 | ||
|
|
334b829a80 | ||
|
|
62ef8bed37 | ||
|
|
9a39db7a73 | ||
|
|
e23289fcbf | ||
|
|
cb9fe5c065 | ||
|
|
e5250c7f58 | ||
|
|
1795c048d1 |
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
## 0.2
|
## 0.2
|
||||||
|
|
||||||
|
### 0.2.1
|
||||||
|
- Fix picklecoder
|
||||||
|
- Fix connection failure transparency and add logging
|
||||||
|
- Add Cache-Control and ETag on first response
|
||||||
|
- Support Async RedisCluster client from redis-py
|
||||||
|
|
||||||
### 0.2.0
|
### 0.2.0
|
||||||
|
|
||||||
- Make `request` and `response` optional.
|
- Make `request` and `response` optional.
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ async def get_date():
|
|||||||
async def get_datetime(request: Request, response: Response):
|
async def get_datetime(request: Request, response: Response):
|
||||||
return {"now": pendulum.now()}
|
return {"now": pendulum.now()}
|
||||||
|
|
||||||
|
@cache(namespace="test")
|
||||||
|
async def func_kwargs(*unused_args, **kwargs):
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
@app.get("/kwargs")
|
||||||
|
async def get_kwargs(name: str):
|
||||||
|
return await func_kwargs(name, name=name)
|
||||||
|
|
||||||
@app.get("/sync-me")
|
@app.get("/sync-me")
|
||||||
@cache(namespace="test")
|
@cache(namespace="test")
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
from redis.asyncio.client import Redis
|
from redis.asyncio.client import AbstractRedis
|
||||||
|
from redis.asyncio.cluster import AbstractRedisCluster
|
||||||
|
|
||||||
from fastapi_cache.backends import Backend
|
from fastapi_cache.backends import Backend
|
||||||
|
|
||||||
|
|
||||||
class RedisBackend(Backend):
|
class RedisBackend(Backend):
|
||||||
def __init__(self, redis: Redis):
|
def __init__(self, redis: AbstractRedis):
|
||||||
self.redis = redis
|
self.redis = redis
|
||||||
|
self.is_cluster = isinstance(redis, AbstractRedisCluster)
|
||||||
|
|
||||||
async def get_with_ttl(self, key: str) -> Tuple[int, str]:
|
async def get_with_ttl(self, key: str) -> Tuple[int, str]:
|
||||||
async with self.redis.pipeline(transaction=True) as pipe:
|
async with self.redis.pipeline(transaction=not self.is_cluster) as pipe:
|
||||||
return await (pipe.ttl(key).get(key).execute())
|
return await (pipe.ttl(key).get(key).execute())
|
||||||
|
|
||||||
async def get(self, key: str) -> Optional[str]:
|
async def get(self, key: str) -> Optional[str]:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import codecs
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import pickle # nosec:B403
|
import pickle # nosec:B403
|
||||||
@@ -45,7 +46,7 @@ class Coder:
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def decode(cls, value: Any) -> Any:
|
def decode(cls, value: str) -> Any:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ class JsonCoder(Coder):
|
|||||||
return json.dumps(value, cls=JsonEncoder)
|
return json.dumps(value, cls=JsonEncoder)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def decode(cls, value: Any) -> str:
|
def decode(cls, value: str) -> str:
|
||||||
return json.loads(value, object_hook=object_hook)
|
return json.loads(value, object_hook=object_hook)
|
||||||
|
|
||||||
|
|
||||||
@@ -66,8 +67,8 @@ class PickleCoder(Coder):
|
|||||||
def encode(cls, value: Any) -> str:
|
def encode(cls, value: Any) -> str:
|
||||||
if isinstance(value, TemplateResponse):
|
if isinstance(value, TemplateResponse):
|
||||||
value = value.body
|
value = value.body
|
||||||
return str(pickle.dumps(value))
|
return codecs.encode(pickle.dumps(value), "base64").decode()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def decode(cls, value: Any) -> Any:
|
def decode(cls, value: str) -> Any:
|
||||||
return pickle.loads(bytes(value)) # nosec:B403,B301
|
return pickle.loads(codecs.decode(value.encode(), "base64")) # nosec:B403,B301
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import inspect
|
import inspect
|
||||||
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Any, Awaitable, Callable, Optional, Type, TypeVar
|
from typing import Any, Awaitable, Callable, Optional, Type, TypeVar
|
||||||
@@ -15,6 +16,8 @@ from starlette.responses import Response
|
|||||||
from fastapi_cache import FastAPICache
|
from fastapi_cache import FastAPICache
|
||||||
from fastapi_cache.coder import Coder
|
from fastapi_cache.coder import Coder
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.addHandler(logging.NullHandler())
|
||||||
P = ParamSpec("P")
|
P = ParamSpec("P")
|
||||||
R = TypeVar("R")
|
R = TypeVar("R")
|
||||||
|
|
||||||
@@ -45,7 +48,13 @@ def cache(
|
|||||||
(param for param in signature.parameters.values() if param.annotation is Response),
|
(param for param in signature.parameters.values() if param.annotation is Response),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
parameters = [*signature.parameters.values()]
|
parameters = []
|
||||||
|
extra_params = []
|
||||||
|
for p in signature.parameters.values():
|
||||||
|
if p.kind <= inspect.Parameter.KEYWORD_ONLY:
|
||||||
|
parameters.append(p)
|
||||||
|
else:
|
||||||
|
extra_params.append(p)
|
||||||
if not request_param:
|
if not request_param:
|
||||||
parameters.append(
|
parameters.append(
|
||||||
inspect.Parameter(
|
inspect.Parameter(
|
||||||
@@ -62,6 +71,7 @@ def cache(
|
|||||||
kind=inspect.Parameter.KEYWORD_ONLY,
|
kind=inspect.Parameter.KEYWORD_ONLY,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
parameters.extend(extra_params)
|
||||||
if parameters:
|
if parameters:
|
||||||
signature = signature.replace(parameters=parameters)
|
signature = signature.replace(parameters=parameters)
|
||||||
func.__signature__ = signature
|
func.__signature__ = signature
|
||||||
@@ -125,18 +135,17 @@ def cache(
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
ttl, ret = await backend.get_with_ttl(cache_key)
|
ttl, ret = await backend.get_with_ttl(cache_key)
|
||||||
except ConnectionError:
|
except Exception:
|
||||||
|
logger.warning(f"Error retrieving cache key '{cache_key}' from backend:", exc_info=True)
|
||||||
ttl, ret = 0, None
|
ttl, ret = 0, None
|
||||||
if not request:
|
if not request:
|
||||||
if ret is not None:
|
if ret is not None:
|
||||||
return coder.decode(ret)
|
return coder.decode(ret)
|
||||||
ret = await ensure_async_func(*args, **kwargs)
|
ret = await ensure_async_func(*args, **kwargs)
|
||||||
try:
|
try:
|
||||||
await backend.set(
|
await backend.set(cache_key, coder.encode(ret), expire)
|
||||||
cache_key, coder.encode(ret), expire or FastAPICache.get_expire()
|
except Exception:
|
||||||
)
|
logger.warning(f"Error setting cache key '{cache_key}' in backend:", exc_info=True)
|
||||||
except ConnectionError:
|
|
||||||
pass
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
if request.method != "GET":
|
if request.method != "GET":
|
||||||
@@ -154,11 +163,16 @@ def cache(
|
|||||||
return coder.decode(ret)
|
return coder.decode(ret)
|
||||||
|
|
||||||
ret = await ensure_async_func(*args, **kwargs)
|
ret = await ensure_async_func(*args, **kwargs)
|
||||||
|
encoded_ret = coder.encode(ret)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await backend.set(cache_key, coder.encode(ret), expire or FastAPICache.get_expire())
|
await backend.set(cache_key, encoded_ret, expire)
|
||||||
except ConnectionError:
|
except Exception:
|
||||||
pass
|
logger.warning(f"Error setting cache key '{cache_key}' in backend:", exc_info=True)
|
||||||
|
|
||||||
|
response.headers["Cache-Control"] = f"max-age={expire}"
|
||||||
|
etag = f"W/{hash(encoded_ret)}"
|
||||||
|
response.headers["ETag"] = etag
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
return inner
|
return inner
|
||||||
|
|||||||
1965
poetry.lock
generated
1965
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "fastapi-cache2"
|
name = "fastapi-cache2"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
description = "Cache for FastAPI"
|
description = "Cache for FastAPI"
|
||||||
authors = ["long2ice <long2ice@gmail.com>"]
|
authors = ["long2ice <long2ice@gmail.com>"]
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|||||||
22
tests/test_codecs.py
Normal file
22
tests/test_codecs.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from fastapi_cache.coder import PickleCoder
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"value",
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
"some_string",
|
||||||
|
(1, 2),
|
||||||
|
[1, 2, 3],
|
||||||
|
{"some_key": 1, "other_key": 2},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_pickle_coder(value: Any) -> None:
|
||||||
|
encoded_value = PickleCoder.encode(value)
|
||||||
|
assert isinstance(encoded_value, str)
|
||||||
|
decoded_value = PickleCoder.decode(encoded_value)
|
||||||
|
assert decoded_value == value
|
||||||
@@ -67,3 +67,9 @@ def test_cache_response_obj() -> None:
|
|||||||
assert get_cache_response.json() == {"a": 1}
|
assert get_cache_response.json() == {"a": 1}
|
||||||
assert get_cache_response.headers.get("cache-control")
|
assert get_cache_response.headers.get("cache-control")
|
||||||
assert get_cache_response.headers.get("etag")
|
assert get_cache_response.headers.get("etag")
|
||||||
|
|
||||||
|
def test_kwargs() -> None:
|
||||||
|
with TestClient(app) as client:
|
||||||
|
name = "Jon"
|
||||||
|
response = client.get("/kwargs", params = {"name": name})
|
||||||
|
assert response.json() == {"name": name}
|
||||||
|
|||||||
Reference in New Issue
Block a user