Skip to content

Implement auto cache removal #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 29 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d7f79a4
add a test route with varying arguments
seapagan Mar 30, 2024
bfef247
add a skeleton 'expires' decorator
seapagan Mar 31, 2024
652a497
first working basic `expires` decorator
seapagan Apr 3, 2024
d3841a2
update TODO
seapagan Apr 6, 2024
b704c00
wip
seapagan Apr 6, 2024
4303df6
log key deletion and return data as a Response()
seapagan Apr 28, 2024
06f3902
refactor new code slightly to suit mypy
seapagan Apr 29, 2024
235433c
update TODO
seapagan Apr 29, 2024
3fb7838
Bump urllib3 from 2.2.1 to 2.2.2 (#99)
dependabot[bot] Jun 19, 2024
5dc64b1
Bump redis from 5.0.4 to 5.0.6 (#97)
dependabot[bot] Jun 19, 2024
ebf33a1
Bump pytest-asyncio from 0.21.1 to 0.23.7 (#90)
dependabot[bot] Jun 19, 2024
eaf5ed7
Bump ruff from 0.4.7 to 0.4.9 (#96)
dependabot[bot] Jun 19, 2024
36c5b9c
Bump mkdocs-material from 9.5.25 to 9.5.27 (#98)
dependabot[bot] Jun 19, 2024
20f6b86
Bump pydantic from 2.7.1 to 2.7.4 (#95)
dependabot[bot] Jun 19, 2024
02dc05f
Bump faker from 25.4.0 to 25.8.0 (#92)
dependabot[bot] Jun 19, 2024
dde3b60
Bump pytest from 8.2.1 to 8.2.2 (#89)
dependabot[bot] Jun 19, 2024
c0379a4
Bump pygments from 2.17.2 to 2.18.0 (#88)
dependabot[bot] Jun 19, 2024
25aa110
[pre-commit.ci] pre-commit autoupdate (#85)
pre-commit-ci[bot] Jun 19, 2024
3fe36cd
Modify the way the local TZ is detected (#100)
seapagan Jun 21, 2024
e354fd5
prep for 0.3.1 release
seapagan Jun 21, 2024
bf95ab4
add a test route with varying arguments
seapagan Mar 30, 2024
fa6abfb
add a skeleton 'expires' decorator
seapagan Mar 31, 2024
f6cd432
first working basic `expires` decorator
seapagan Apr 3, 2024
80c874e
update TODO
seapagan Apr 6, 2024
010fcaa
wip
seapagan Apr 6, 2024
6ed59ff
log key deletion and return data as a Response()
seapagan Apr 28, 2024
4f984be
refactor new code slightly to suit mypy
seapagan Apr 29, 2024
dccf417
update TODO
seapagan Apr 29, 2024
7615899
Merge branch 'implement-auto-cache-removal' of github.com:seapagan/fa…
seapagan Jun 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ repos:
- id: end-of-file-fixer

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.7
rev: v0.4.9
hooks:
- id: ruff
name: "lint with ruff"
Expand All @@ -36,6 +36,7 @@ repos:
additional_dependencies:
- "types-python-dateutil"
- "types-redis"
- "types-tzlocal"

- repo: https://github.com/python-poetry/poetry
rev: "1.8.0"
Expand Down
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,34 @@ project since the first release, with the latest changes at the top.
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [0.3.1](https://github.com/seapagan/fastapi-redis-cache-reborn/releases/tag/0.3.1) (June 21, 2024)

**Closed Issues**

- Bug when trying to create FastApiRedisCache() object ([#94](https://github.com/seapagan/fastapi-redis-cache-reborn/issues/94)) by [seapagan](https://github.com/seapagan)

**Bug Fixes**

- Modify the way the local TZ is detected ([#100](https://github.com/seapagan/fastapi-redis-cache-reborn/pull/100)) by [seapagan](https://github.com/seapagan)

**Documentation**

- Add a basic documentation site ([#84](https://github.com/seapagan/fastapi-redis-cache-reborn/pull/84)) by [seapagan](https://github.com/seapagan)

**Dependency Updates**

- Bump urllib3 from 2.2.1 to 2.2.2 ([#99](https://github.com/seapagan/fastapi-redis-cache-reborn/pull/99)) by [dependabot[bot]](https://github.com/apps/dependabot)
- Bump mkdocs-material from 9.5.25 to 9.5.27 ([#98](https://github.com/seapagan/fastapi-redis-cache-reborn/pull/98)) by [dependabot[bot]](https://github.com/apps/dependabot)
- Bump redis from 5.0.4 to 5.0.6 ([#97](https://github.com/seapagan/fastapi-redis-cache-reborn/pull/97)) by [dependabot[bot]](https://github.com/apps/dependabot)
- Bump ruff from 0.4.7 to 0.4.9 ([#96](https://github.com/seapagan/fastapi-redis-cache-reborn/pull/96)) by [dependabot[bot]](https://github.com/apps/dependabot)
- Bump pydantic from 2.7.1 to 2.7.4 ([#95](https://github.com/seapagan/fastapi-redis-cache-reborn/pull/95)) by [dependabot[bot]](https://github.com/apps/dependabot)
- Bump faker from 25.4.0 to 25.8.0 ([#92](https://github.com/seapagan/fastapi-redis-cache-reborn/pull/92)) by [dependabot[bot]](https://github.com/apps/dependabot)
- Bump pytest-asyncio from 0.21.1 to 0.23.7 ([#90](https://github.com/seapagan/fastapi-redis-cache-reborn/pull/90)) by [dependabot[bot]](https://github.com/apps/dependabot)
- Bump pytest from 8.2.1 to 8.2.2 ([#89](https://github.com/seapagan/fastapi-redis-cache-reborn/pull/89)) by [dependabot[bot]](https://github.com/apps/dependabot)
- Bump pygments from 2.17.2 to 2.18.0 ([#88](https://github.com/seapagan/fastapi-redis-cache-reborn/pull/88)) by [dependabot[bot]](https://github.com/apps/dependabot)

[`Full Changelog`](https://github.com/seapagan/fastapi-redis-cache-reborn/compare/0.3.0...0.3.1) | [`Diff`](https://github.com/seapagan/fastapi-redis-cache-reborn/compare/0.3.0...0.3.1.diff) | [`Patch`](https://github.com/seapagan/fastapi-redis-cache-reborn/compare/0.3.0...0.3.1.patch)

## [0.3.0](https://github.com/seapagan/fastapi-redis-cache-reborn/releases/tag/0.3.0) (June 04, 2024)

**_'It's Alive!!!!'_**
Expand Down
10 changes: 10 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ These below are from Issues or PRs in the original repository.
(<https://github.com/a-luna/fastapi-redis-cache/issues/63>)
- Add support for caching non-FastAPI functions
(<https://github.com/a-luna/fastapi-redis-cache/pull/66>)
- Add functionality for specifying a web cache expiration
(<https://github.com/a-luna/fastapi-redis-cache/pull/60>)
- Take a look at other issues in the original repository to see if any need to
be added here.

Expand All @@ -22,3 +24,11 @@ These below are from Issues or PRs in the original repository.
the test logic not production code.
- catch invalid cache type exceptions and raise a more informative error
message.
- configure the log time display format in the `Cache` class. Make it
totally configurable through an optional parameter and/or detect and use the
preferred local time format.
- catch invalid cache type exceptions and just return a normal response
instead of raising Internal Server (500) error
- use Async connections now that redis-py is async.
- add the `hiredis` module to the requirements, this is a C extension that
should speed up the connection to Redis (ensure no issues with current code).
2 changes: 2 additions & 0 deletions fastapi_redis_cache/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
cache_one_month,
cache_one_week,
cache_one_year,
expires,
)
from fastapi_redis_cache.client import FastApiRedisCache

Expand All @@ -19,5 +20,6 @@
"cache_one_month",
"cache_one_week",
"cache_one_year",
"expires",
"FastApiRedisCache",
]
78 changes: 78 additions & 0 deletions fastapi_redis_cache/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from fastapi import Response

from fastapi_redis_cache.client import FastApiRedisCache
from fastapi_redis_cache.enums import RedisEvent
from fastapi_redis_cache.util import (
ONE_DAY_IN_SECONDS,
ONE_HOUR_IN_SECONDS,
Expand Down Expand Up @@ -54,6 +55,7 @@ async def inner_wrapper(
request = func_kwargs.pop("request", None)
response = func_kwargs.pop("response", None)
create_response_directly = not response

if create_response_directly:
response = Response()
# below fix by @jaepetto on the original repo.
Expand All @@ -68,6 +70,7 @@ async def inner_wrapper(
# if the redis client is not connected or request is not
# cacheable, no caching behavior is performed.
return await get_api_response_async(func, *args, **kwargs)

key = redis_cache.get_cache_key(tag, func, *args, **kwargs)
ttl, in_cache = redis_cache.check_cache(key)
if in_cache:
Expand Down Expand Up @@ -127,6 +130,81 @@ async def inner_wrapper(
return outer_wrapper


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): 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]:
@wraps(func)
async def inner_wrapper(
*args: Any, # noqa: ANN401
**kwargs: Any, # noqa: ANN401
) -> Any: # noqa: ANN401
"""Invalidate all cached responses with the same tag."""
response = kwargs.get("response", None)
create_response_directly = not response

if create_response_directly:
response = Response()
if "content-length" in response.headers:
del response.headers["content-length"]

redis_cache = FastApiRedisCache()
orig_response = await get_api_response_async(func, *args, **kwargs)

ignore_args = redis_cache.ignore_arg_types

if redis_cache.redis and redis_cache.connected and tag and kwargs:
# remove any args that should not be used to generate the cache
# key.
filtered_kwargs = kwargs.copy()
for key in list(filtered_kwargs.keys()):
if type(filtered_kwargs[key]) in ignore_args:
del filtered_kwargs[key]
# create the search string to find the keys to expire.
search = "".join(
[
f"({key}={value})"
for key, value in filtered_kwargs.items()
]
)
tag_keys = redis_cache.get_tagged_keys(tag)
found_keys = [key for key in tag_keys if search.encode() in key]
for this_key in found_keys:
key_str = (
this_key.decode()
if isinstance(this_key, bytes)
else this_key
)

redis_cache.log(
RedisEvent.KEY_DELETED_FROM_CACHE, key=str(key_str)
)
redis_cache.redis.delete(key_str)
redis_cache.redis.srem(tag, key_str)

return Response(
content=serialize_json(orig_response),
media_type=JSON_MEDIA_TYPE,
headers=response.headers,
)

return inner_wrapper

return outer_wrapper


async def get_api_response_async(
func: Callable[..., Any],
*args: Any, # noqa: ANN401
Expand Down
11 changes: 8 additions & 3 deletions fastapi_redis_cache/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@
Optional,
Union,
)
from zoneinfo import ZoneInfo

import tzlocal

from fastapi_redis_cache.enums import RedisEvent, RedisStatus
from fastapi_redis_cache.key_gen import get_cache_key
from fastapi_redis_cache.redis import redis_connect
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

Expand Down Expand Up @@ -158,7 +161,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()

Expand Down Expand Up @@ -203,7 +206,9 @@ def add_to_cache(self, key: str, value: Any, expire: int) -> bool:
message = f"Object of type {type(value)} is not JSON-serializable"
self.log(RedisEvent.FAILED_TO_CACHE_KEY, msg=message, key=key)
return False

cached = self.redis.set(name=key, value=response_data, ex=expire)

if not cached:
self.log(RedisEvent.FAILED_TO_CACHE_KEY, key=key, value=value)
return False
Expand Down Expand Up @@ -261,5 +266,5 @@ def get_etag(cached_data: Union[str, bytes, dict[str, Any]]) -> str:
@staticmethod
def get_log_time() -> str:
"""Get a timestamp to include with a log message."""
local_tz = ZoneInfo("localtime")
local_tz = tzlocal.get_localzone()
return datetime.now(local_tz).strftime(LOG_TIMESTAMP)
1 change: 1 addition & 0 deletions fastapi_redis_cache/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ class RedisEvent(IntEnum):
KEY_ADDED_TO_CACHE = 4
KEY_FOUND_IN_CACHE = 5
FAILED_TO_CACHE_KEY = 6
KEY_DELETED_FROM_CACHE = 7
Loading
Loading