Skip to content

Feature: support custom redis key prefix for cache #193

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

Merged
merged 2 commits into from
Mar 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 6 additions & 2 deletions docs/usage/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,13 @@ Available built-in cache strategies:

github = GitHub(
cache_strategy=RedisCacheStrategy(
client=Redis(host="localhost", port=6379)
client=Redis(host="localhost", port=6379), prefix="githubkit:"
)
)
```

The `prefix` option is used to set the key prefix in Redis. You should add a `:` at the end of the prefix if you want to use namespace like key format. Both `githubkit` and `hishel` will use the prefix to store the cache data.

Note that using this sync only cache strategy will cause the `GitHub` instance to be sync only.

- `AsyncRedisCacheStrategy`: Cache the data in Redis (Async only).
Expand All @@ -115,11 +117,13 @@ Available built-in cache strategies:

github = GitHub(
cache_strategy=AsyncRedisCacheStrategy(
client=Redis(host="localhost", port=6379)
client=Redis(host="localhost", port=6379), prefix="githubkit:"
)
)
```

The `prefix` option is used to set the key prefix in Redis. You should add a `:` at the end of the prefix if you want to use namespace like key format. Both `githubkit` and `hishel` will use the prefix to store the cache data.

Note that using this async only cache strategy will cause the `GitHub` instance to be async only.

### `http_cache`
Expand Down
19 changes: 18 additions & 1 deletion githubkit/cache/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from datetime import timedelta
from typing import Optional

from hishel import AsyncBaseStorage, BaseStorage
from hishel import AsyncBaseStorage, BaseStorage, Controller


class BaseCache(abc.ABC):
Expand All @@ -28,16 +28,33 @@ async def aset(self, key: str, value: str, ex: timedelta) -> None:
class BaseCacheStrategy(abc.ABC):
@abc.abstractmethod
def get_cache_storage(self) -> BaseCache:
"""Get the cache storage instance used in sync context

raise CacheUnsupportedError if the strategy does not support sync usage
"""
raise NotImplementedError

@abc.abstractmethod
def get_async_cache_storage(self) -> AsyncBaseCache:
"""Get the cache storage instance used in async context

raise CacheUnsupportedError if the strategy does not support async usage
"""
raise NotImplementedError

def get_hishel_controller(self) -> Optional[Controller]:
"""Get the hishel controller instance

Return `None` to use the default controller
"""
return None

@abc.abstractmethod
def get_hishel_storage(self) -> BaseStorage:
"""Get the hishel storage instance used in sync context"""
raise NotImplementedError

@abc.abstractmethod
def get_async_hishel_storage(self) -> AsyncBaseStorage:
"""Get the hishel storage instance used in async context"""
raise NotImplementedError
47 changes: 36 additions & 11 deletions githubkit/cache/redis.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from datetime import timedelta
from functools import partial
from typing import TYPE_CHECKING, Any, NoReturn, Optional
from typing_extensions import override

from hishel import AsyncBaseStorage, AsyncRedisStorage, RedisStorage
from hishel import AsyncBaseStorage, AsyncRedisStorage, Controller, RedisStorage

from githubkit.exception import CacheUnsupportedError
from githubkit.utils import hishel_key_generator_with_prefix

from .base import AsyncBaseCache, BaseCache, BaseCacheStrategy

Expand All @@ -25,47 +27,69 @@ def _ensure_str_or_none(value: Any) -> Optional[str]:


class RedisCache(BaseCache):
def __init__(self, client: "Redis") -> None:
def __init__(self, client: "Redis", prefix: Optional[str] = None) -> None:
self.client = client
self.prefix = prefix

def _get_key(self, key: str) -> str:
if self.prefix is not None:
return f"{self.prefix}{key}"
return key

@override
def get(self, key: str) -> Optional[str]:
data = self.client.get(key)
data = self.client.get(self._get_key(key))
return _ensure_str_or_none(data)

@override
def set(self, key: str, value: str, ex: timedelta) -> None:
self.client.set(key, value, ex)
self.client.set(self._get_key(key), value, ex)


class AsyncRedisCache(AsyncBaseCache):
def __init__(self, client: "AsyncRedis") -> None:
def __init__(self, client: "AsyncRedis", prefix: Optional[str] = None) -> None:
self.client = client
self.prefix = prefix

def _get_key(self, key: str) -> str:
if self.prefix is not None:
return f"{self.prefix}{key}"
return key

@override
async def aget(self, key: str) -> Optional[str]:
data = await self.client.get(key)
data = await self.client.get(self._get_key(key))
return _ensure_str_or_none(data)

@override
async def aset(self, key: str, value: str, ex: timedelta) -> None:
await self.client.set(key, value, ex)
await self.client.set(self._get_key(key), value, ex)


class RedisCacheStrategy(BaseCacheStrategy):
def __init__(self, client: "Redis") -> None:
def __init__(self, client: "Redis", prefix: Optional[str] = None) -> None:
self.client = client
self.prefix = prefix

@override
def get_cache_storage(self) -> RedisCache:
return RedisCache(self.client)
return RedisCache(self.client, self.prefix)

@override
def get_async_cache_storage(self) -> NoReturn:
raise CacheUnsupportedError(
"Sync redis cache strategy does not support async usage"
)

@override
def get_hishel_controller(self) -> Optional[Controller]:
if self.prefix is not None:
return Controller(
key_generator=partial(
hishel_key_generator_with_prefix, prefix=self.prefix
)
)

@override
def get_hishel_storage(self) -> RedisStorage:
return RedisStorage(client=self.client)
Expand All @@ -78,8 +102,9 @@ def get_async_hishel_storage(self) -> NoReturn:


class AsyncRedisCacheStrategy(BaseCacheStrategy):
def __init__(self, client: "AsyncRedis") -> None:
def __init__(self, client: "AsyncRedis", prefix: Optional[str] = None) -> None:
self.client = client
self.prefix = prefix

@override
def get_cache_storage(self) -> NoReturn:
Expand All @@ -89,7 +114,7 @@ def get_cache_storage(self) -> NoReturn:

@override
def get_async_cache_storage(self) -> AsyncRedisCache:
return AsyncRedisCache(self.client)
return AsyncRedisCache(self.client, self.prefix)

@override
def get_hishel_storage(self) -> NoReturn:
Expand Down
2 changes: 2 additions & 0 deletions githubkit/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ def _create_sync_client(self) -> httpx.Client:
transport = hishel.CacheTransport(
httpx.HTTPTransport(),
storage=self.config.cache_strategy.get_hishel_storage(),
controller=self.config.cache_strategy.get_hishel_controller(),
)
else:
transport = httpx.HTTPTransport()
Expand All @@ -244,6 +245,7 @@ def _create_async_client(self) -> httpx.AsyncClient:
transport = hishel.AsyncCacheTransport(
httpx.AsyncHTTPTransport(),
storage=self.config.cache_strategy.get_async_hishel_storage(),
controller=self.config.cache_strategy.get_hishel_controller(),
)
else:
transport = httpx.AsyncHTTPTransport()
Expand Down
10 changes: 9 additions & 1 deletion githubkit/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from enum import Enum
import inspect
from typing import Any, Generic, Literal, TypeVar, final
from typing import Any, Generic, Literal, Optional, TypeVar, final

from hishel._utils import generate_key
import httpcore
from pydantic import BaseModel

from .compat import PYDANTIC_V2, custom_validation, type_validate_python
Expand Down Expand Up @@ -97,3 +99,9 @@ def __get_pydantic_core_schema__(
),
),
)


def hishel_key_generator_with_prefix(
request: httpcore.Request, body: Optional[bytes], prefix: str = ""
) -> str:
return prefix + generate_key(request, b"" if body is None else body)