diff --git a/CHANGELOG.md b/CHANGELOG.md index a136d3b..f52d7f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,16 @@ ## 0.2 -### 0.2.1 - -- Support cache jinja2 template response. - ### 0.2.0 - Make `request` and `response` optional. - Add typing info to the `cache` decorator. +- Support cache jinja2 template response. +- Support cache `JSONResponse` +- Add `py.typed` file and type hints +- Add TestCase +- Fix cache decorate sync function +- Transparently handle backend connection failures. ## 0.1 diff --git a/examples/in_memory/main.py b/examples/in_memory/main.py index e7164ba..68b6e17 100644 --- a/examples/in_memory/main.py +++ b/examples/in_memory/main.py @@ -2,7 +2,7 @@ import pendulum import uvicorn from fastapi import FastAPI from starlette.requests import Request -from starlette.responses import Response +from starlette.responses import JSONResponse, Response from fastapi_cache import FastAPICache from fastapi_cache.backends.inmemory import InMemoryBackend @@ -51,6 +51,12 @@ def sync_me(): return 42 +@app.get("/cache_response_obj") +@cache(namespace="test", expire=5) +async def cache_response_obj(): + return JSONResponse({"a": 1}) + + @app.on_event("startup") async def startup(): FastAPICache.init(InMemoryBackend()) diff --git a/examples/redis/main.py b/examples/redis/main.py index 434acd5..e05eda5 100644 --- a/examples/redis/main.py +++ b/examples/redis/main.py @@ -9,7 +9,7 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from redis.asyncio.connection import ConnectionPool from starlette.requests import Request -from starlette.responses import Response +from starlette.responses import JSONResponse, Response from fastapi_cache import FastAPICache from fastapi_cache.backends.redis import RedisBackend @@ -73,6 +73,12 @@ async def cache_html(request: Request): return templates.TemplateResponse("index.html", {"request": request, "ret": await get_ret()}) +@app.get("/cache_response_obj") +@cache(namespace="test", expire=5) +async def cache_response_obj(): + return JSONResponse({"a": 1}) + + @app.on_event("startup") async def startup(): pool = ConnectionPool.from_url(url="redis://redis") diff --git a/fastapi_cache/coder.py b/fastapi_cache/coder.py index 15ed118..f3df837 100644 --- a/fastapi_cache/coder.py +++ b/fastapi_cache/coder.py @@ -7,6 +7,7 @@ from typing import Any import pendulum from fastapi.encoders import jsonable_encoder +from starlette.responses import JSONResponse from starlette.templating import _TemplateResponse as TemplateResponse CONVERTERS = { @@ -52,6 +53,8 @@ class Coder: class JsonCoder(Coder): @classmethod def encode(cls, value: Any) -> str: + if isinstance(value, JSONResponse): + return value.body return json.dumps(value, cls=JsonEncoder) @classmethod diff --git a/fastapi_cache/decorator.py b/fastapi_cache/decorator.py index 6869885..5018b1e 100644 --- a/fastapi_cache/decorator.py +++ b/fastapi_cache/decorator.py @@ -77,9 +77,9 @@ def cache( # if the wrapped function does NOT have request or response in its function signature, # make sure we don't pass them in as keyword arguments if not request_param: - kwargs.pop("request") + kwargs.pop("request", None) if not response_param: - kwargs.pop("response") + kwargs.pop("response", None) if inspect.iscoroutinefunction(func): # async, return as is. @@ -123,13 +123,20 @@ def cache( args=args, kwargs=copy_kwargs, ) - - ttl, ret = await backend.get_with_ttl(cache_key) + try: + ttl, ret = await backend.get_with_ttl(cache_key) + except ConnectionError: + ttl, ret = 0, None if not request: if ret is not None: return coder.decode(ret) ret = await ensure_async_func(*args, **kwargs) - await backend.set(cache_key, coder.encode(ret), expire or FastAPICache.get_expire()) + try: + await backend.set( + cache_key, coder.encode(ret), expire or FastAPICache.get_expire() + ) + except ConnectionError: + pass return ret if request.method != "GET": @@ -148,7 +155,10 @@ def cache( ret = await ensure_async_func(*args, **kwargs) - await backend.set(cache_key, coder.encode(ret), expire or FastAPICache.get_expire()) + try: + await backend.set(cache_key, coder.encode(ret), expire or FastAPICache.get_expire()) + except ConnectionError: + pass return ret return inner diff --git a/pyproject.toml b/pyproject.toml index 36998f7..292d273 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "fastapi-cache2" -version = "0.2.1" +version = "0.2.0" description = "Cache for FastAPI" authors = ["long2ice "] license = "Apache-2.0" @@ -23,7 +23,7 @@ aiomcache = { version = "*", optional = true } pendulum = "*" aiobotocore = { version = "^1.4.1", optional = true } typing-extensions = { version = ">=4.1.0", markers = "python_version < \"3.10\"" } -aiohttp= { version = ">=3.8.3", markers = "python_version >= \"3.11\""} +aiohttp = { version = ">=3.8.3", markers = "python_version >= \"3.11\"" } [tool.poetry.dev-dependencies] flake8 = "*" diff --git a/tests/test_decorator.py b/tests/test_decorator.py index b118cde..0eb96c1 100644 --- a/tests/test_decorator.py +++ b/tests/test_decorator.py @@ -57,3 +57,13 @@ def test_sync() -> None: with TestClient(app) as client: response = client.get("/sync-me") assert response.json() == 42 + + +def test_cache_response_obj() -> None: + with TestClient(app) as client: + cache_response = client.get("cache_response_obj") + assert cache_response.json() == {"a": 1} + get_cache_response = client.get("cache_response_obj") + assert get_cache_response.json() == {"a": 1} + assert get_cache_response.headers.get("cache-control") + assert get_cache_response.headers.get("etag")