From e9a3481d87ac587d773f74f91f3e92f098fcf4c3 Mon Sep 17 00:00:00 2001 From: Grant Ramsay Date: Wed, 3 Apr 2024 16:17:09 +0100 Subject: [PATCH] first working basic `expires` decorator Signed-off-by: Grant Ramsay --- fastapi_redis_cache/__init__.py | 2 ++ fastapi_redis_cache/cache.py | 40 ++++++++++++++++++++++++--------- fastapi_redis_cache/client.py | 4 +++- requirements-dev.txt | 16 ++++++------- requirements.txt | 4 ++-- tests/live_test.py | 11 +++++++++ 6 files changed, 55 insertions(+), 22 deletions(-) diff --git a/fastapi_redis_cache/__init__.py b/fastapi_redis_cache/__init__.py index 3c2ee38..cd0ebd4 100644 --- a/fastapi_redis_cache/__init__.py +++ b/fastapi_redis_cache/__init__.py @@ -8,6 +8,7 @@ cache_one_month, cache_one_week, cache_one_year, + expires, ) from fastapi_redis_cache.client import FastApiRedisCache @@ -19,5 +20,6 @@ "cache_one_month", "cache_one_week", "cache_one_year", + "expires", "FastApiRedisCache", ] diff --git a/fastapi_redis_cache/cache.py b/fastapi_redis_cache/cache.py index 1750b0e..0491bba 100644 --- a/fastapi_redis_cache/cache.py +++ b/fastapi_redis_cache/cache.py @@ -127,13 +127,19 @@ async def inner_wrapper( return outer_wrapper -def expires(tag: str | None = None) -> Callable[..., Any]: +def expires( + tag: str | None = None, + arg: str | None = None, # noqa: ARG001 +) -> Callable[..., Any]: """Invalidate all cached responses with the same tag. Args: - tag (str, optional): A tag to associate with the cached response. This - can later be used to invalidate all cached responses with the same - tag, or for further fine-grained cache expiry. Defaults to None. + tag (str, optional): The tag to search for keys to expire. + Defaults to None. + arg: (str, optional): The function arguement to filter for expiry. This + would generally be the varying arguement suppplied to the route. + Defaults to None. If not specified, the kwargs for the route will + be used to search for the key to expire. """ def outer_wrapper(func: Callable[..., Any]) -> Callable[..., Any]: @@ -144,14 +150,26 @@ async def inner_wrapper( ) -> Any: # noqa: ANN401 """Invalidate all cached responses with the same tag.""" redis_cache = FastApiRedisCache() - if redis_cache.not_connected: - return await get_api_response_async(func, *args, **kwargs) - if tag: - # expire all keys with the same tag. This is a test we will - # later only expire keys that have the search argument in the - # key. + orig_response = await get_api_response_async(func, *args, **kwargs) + + if not redis_cache.redis or not redis_cache.connected or not tag: + # we only want to invalidate the cache if the redis client is + # connected and a tag is provided. + return orig_response + if kwargs: + search = "".join( + [f"({key}={value})" for key, value in kwargs.items()] + ) + tag_keys = redis_cache.get_tagged_keys(tag) + found_keys = [key for key in tag_keys if search.encode() in key] + for key in found_keys: + redis_cache.redis.delete(key) + redis_cache.redis.srem(tag, key) + else: + # will fill this later, what to do if no kwargs are provided pass - return await get_api_response_async(func, *args, **kwargs) + + return orig_response return inner_wrapper diff --git a/fastapi_redis_cache/client.py b/fastapi_redis_cache/client.py index 68ee0fe..2197ded 100644 --- a/fastapi_redis_cache/client.py +++ b/fastapi_redis_cache/client.py @@ -20,6 +20,8 @@ from fastapi_redis_cache.util import serialize_json if TYPE_CHECKING: # pragma: no cover + from collections.abc import ByteString + from fastapi import Request, Response from redis import client @@ -158,7 +160,7 @@ def add_key_to_tag_set(self, tag: str, key: str) -> None: if self.redis: self.redis.sadd(tag, key) - def get_tagged_keys(self, tag: str) -> set[str]: + def get_tagged_keys(self, tag: str) -> set[ByteString]: """Return a set of keys associated with a tag.""" return self.redis.smembers(tag) if self.redis else set() diff --git a/requirements-dev.txt b/requirements-dev.txt index 27cc0c4..59be869 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -16,9 +16,9 @@ distlib==0.3.8 ; python_version >= "3.9" and python_version < "4.0" dnspython==2.6.1 ; python_version >= "3.9" and python_version < "4.0" email-validator==2.1.1 ; python_version >= "3.9" and python_version < "4.0" exceptiongroup==1.2.0 ; python_version >= "3.9" and python_version < "3.11" -faker==24.3.0 ; python_version >= "3.9" and python_version < "4.0" +faker==24.4.0 ; python_version >= "3.9" and python_version < "4.0" fakeredis==2.21.3 ; python_version >= "3.9" and python_version < "4.0" -fastapi[all]==0.110.0 ; python_version >= "3.9" and python_version < "4.0" +fastapi[all]==0.110.1 ; python_version >= "3.9" and python_version < "4.0" filelock==3.13.1 ; python_version >= "3.9" and python_version < "4.0" github-changelog-md==0.9.2 ; python_version >= "3.9" and python_version < "4.0" h11==0.14.0 ; python_version >= "3.9" and python_version < "4.0" @@ -44,7 +44,7 @@ pastel==0.2.1 ; python_version >= "3.9" and python_version < "4.0" platformdirs==4.2.0 ; python_version >= "3.9" and python_version < "4.0" pluggy==1.4.0 ; python_version >= "3.9" and python_version < "4.0" poethepoet==0.25.0 ; python_version >= "3.9" and python_version < "4.0" -pre-commit==3.6.2 ; python_version >= "3.9" and python_version < "4.0" +pre-commit==3.7.0 ; python_version >= "3.9" and python_version < "4.0" pycparser==2.21 ; python_version >= "3.9" and python_version < "4.0" pydantic-core==2.16.3 ; python_version >= "3.9" and python_version < "4.0" pydantic-extra-types==2.6.0 ; python_version >= "3.9" and python_version < "4.0" @@ -57,14 +57,14 @@ pyjwt[crypto]==2.8.0 ; python_version >= "3.9" and python_version < "4.0" pymarkdownlnt==0.9.18 ; python_version >= "3.9" and python_version < "4.0" pynacl==1.5.0 ; python_version >= "3.9" and python_version < "4.0" pytest-asyncio==0.21.1 ; python_version >= "3.9" and python_version < "4.0" -pytest-cov==4.1.0 ; python_version >= "3.9" and python_version < "4.0" +pytest-cov==5.0.0 ; python_version >= "3.9" and python_version < "4.0" pytest-env==1.1.3 ; python_version >= "3.9" and python_version < "4.0" pytest-mock==3.14.0 ; python_version >= "3.9" and python_version < "4.0" -pytest-order==1.2.0 ; python_version >= "3.9" and python_version < "4.0" +pytest-order==1.2.1 ; python_version >= "3.9" and python_version < "4.0" pytest-randomly==3.15.0 ; python_version >= "3.9" and python_version < "4.0" pytest-reverse==1.7.0 ; python_version >= "3.9" and python_version < "4.0" pytest-sugar==1.0.0 ; python_version >= "3.9" and python_version < "4.0" -pytest-watcher==0.4.1 ; python_version >= "3.9" and python_version < "4.0" +pytest-watcher==0.4.2 ; python_version >= "3.9" and python_version < "4.0" pytest==8.1.1 ; python_version >= "3.9" and python_version < "4.0" python-dateutil==2.9.0.post0 ; python_version >= "3.9" and python_version < "4.0" python-dotenv==1.0.1 ; python_version >= "3.9" and python_version < "4.0" @@ -74,13 +74,13 @@ redis==5.0.3 ; python_version >= "3.9" and python_version < "4.0" requests==2.31.0 ; python_version >= "3.9" and python_version < "4.0" rich==13.7.1 ; python_version >= "3.9" and python_version < "4.0" rtoml==0.9.0 ; python_version >= "3.9" and python_version < "4.0" -ruff==0.3.4 ; python_version >= "3.9" and python_version < "4.0" +ruff==0.3.5 ; python_version >= "3.9" and python_version < "4.0" setuptools==69.2.0 ; python_version >= "3.9" and python_version < "4.0" simple-toml-settings==0.6.0 ; python_version >= "3.9" and python_version < "4.0" six==1.16.0 ; python_version >= "3.9" and python_version < "4.0" sniffio==1.3.1 ; python_version >= "3.9" and python_version < "4.0" sortedcontainers==2.4.0 ; python_version >= "3.9" and python_version < "4.0" -starlette==0.36.3 ; python_version >= "3.9" and python_version < "4.0" +starlette==0.37.2 ; python_version >= "3.9" and python_version < "4.0" termcolor==2.4.0 ; python_version >= "3.9" and python_version < "4.0" tomli==2.0.1 ; python_version >= "3.9" and python_version < "4.0" toolz==0.12.1 ; python_version >= "3.9" and python_version < "4.0" diff --git a/requirements.txt b/requirements.txt index e78f8ed..646986a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ colorama==0.4.6 ; python_version >= "3.9" and python_version < "4.0" and (sys_pl dnspython==2.6.1 ; python_version >= "3.9" and python_version < "4.0" email-validator==2.1.1 ; python_version >= "3.9" and python_version < "4.0" exceptiongroup==1.2.0 ; python_version >= "3.9" and python_version < "3.11" -fastapi[all]==0.110.0 ; python_version >= "3.9" and python_version < "4.0" +fastapi[all]==0.110.1 ; python_version >= "3.9" and python_version < "4.0" h11==0.14.0 ; python_version >= "3.9" and python_version < "4.0" httpcore==1.0.4 ; python_version >= "3.9" and python_version < "4.0" httptools==0.6.1 ; python_version >= "3.9" and python_version < "4.0" @@ -26,7 +26,7 @@ python-multipart==0.0.9 ; python_version >= "3.9" and python_version < "4.0" pyyaml==6.0.1 ; python_version >= "3.9" and python_version < "4.0" redis==5.0.3 ; python_version >= "3.9" and python_version < "4.0" sniffio==1.3.1 ; python_version >= "3.9" and python_version < "4.0" -starlette==0.36.3 ; python_version >= "3.9" and python_version < "4.0" +starlette==0.37.2 ; python_version >= "3.9" and python_version < "4.0" typing-extensions==4.10.0 ; python_version >= "3.9" and python_version < "4.0" ujson==5.9.0 ; python_version >= "3.9" and python_version < "4.0" uvicorn[standard]==0.28.0 ; python_version >= "3.9" and python_version < "4.0" diff --git a/tests/live_test.py b/tests/live_test.py index 7006be6..9bf556d 100644 --- a/tests/live_test.py +++ b/tests/live_test.py @@ -19,6 +19,7 @@ cache, cache_one_hour, cache_one_minute, + expires, ) REDIS_SERVER_URL = "redis://127.0.0.1:" @@ -104,3 +105,13 @@ def cache_with_args(user: int) -> dict[str, Union[bool, str]]: "success": True, "message": f"this data is for user {user}", } + + +@app.put("/cache_with_args/{user}") +@expires(tag="user_tag") +def put_cache_with_args(user: int) -> dict[str, Union[bool, str]]: + """Put request to change data for a specific user.""" + return { + "success": True, + "message": f"New data for User {user}", + }