Skip to content

Commit 2fb831e

Browse files
authored
✨ Feature: add redis cache support (#162)
1 parent 28b8b65 commit 2fb831e

File tree

7 files changed

+156
-1
lines changed

7 files changed

+156
-1
lines changed

docs/usage/configuration.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ The `timeout` option is used to set the request timeout. You can pass a float, `
7272

7373
The `cache_strategy` option defines how to cache the tokens or http responses. You can provide a githubkit built-in cache strategy or a custom one that implements the `BaseCacheStrategy` interface. By default, githubkit uses the `MemCacheStrategy` to cache the data in memory.
7474

75+
Available built-in cache strategies:
76+
77+
- `MemCacheStrategy`: Cache the data in memory.
78+
- `RedisCacheStrategy`: Cache the data in Redis (Sync only).
79+
- `AsyncRedisCacheStrategy`: Cache the data in Redis (Async only).
80+
7581
### `http_cache`
7682

7783
The `http_cache` option enables the http caching feature powered by [Hishel](https://hishel.com/) for HTTPX. GitHub API limits the number of requests that you can make within a specific amount of time. This feature is useful to reduce the number of requests to GitHub API and avoid hitting the rate limit.

githubkit/cache/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
from .base import BaseCache as BaseCache
22
from .mem_cache import MemCache as MemCache
3+
from .redis import RedisCache as RedisCache
34
from .base import AsyncBaseCache as AsyncBaseCache
5+
from .redis import AsyncRedisCache as AsyncRedisCache
46
from .base import BaseCacheStrategy as BaseCacheStrategy
57
from .mem_cache import MemCacheStrategy as MemCacheStrategy
8+
from .redis import RedisCacheStrategy as RedisCacheStrategy
9+
from .redis import AsyncRedisCacheStrategy as AsyncRedisCacheStrategy
610

711
DEFAULT_CACHE_STRATEGY = MemCacheStrategy()

githubkit/cache/mem_cache.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Optional
22
from dataclasses import dataclass
3+
from typing_extensions import override
34
from datetime import datetime, timezone, timedelta
45

56
from hishel import InMemoryStorage, AsyncInMemoryStorage
@@ -25,17 +26,21 @@ def expire(self):
2526
if item.expire_at is not None and item.expire_at < now:
2627
self._cache.pop(key, None)
2728

29+
@override
2830
def get(self, key: str) -> Optional[str]:
2931
self.expire()
3032
return (item := self._cache.get(key, None)) and item.value
3133

34+
@override
3235
async def aget(self, key: str) -> Optional[str]:
3336
return self.get(key)
3437

38+
@override
3539
def set(self, key: str, value: str, ex: timedelta) -> None:
3640
self.expire()
3741
self._cache[key] = _Item(value, datetime.now(timezone.utc) + ex)
3842

43+
@override
3944
async def aset(self, key: str, value: str, ex: timedelta) -> None:
4045
return self.set(key, value, ex)
4146

@@ -46,19 +51,23 @@ def __init__(self) -> None:
4651
self._hishel_storage: Optional[InMemoryStorage] = None
4752
self._hishel_async_storage: Optional[AsyncInMemoryStorage] = None
4853

54+
@override
4955
def get_cache_storage(self) -> MemCache:
5056
if self._cache is None:
5157
self._cache = MemCache()
5258
return self._cache
5359

60+
@override
5461
def get_async_cache_storage(self) -> MemCache:
5562
return self.get_cache_storage()
5663

64+
@override
5765
def get_hishel_storage(self) -> InMemoryStorage:
5866
if self._hishel_storage is None:
5967
self._hishel_storage = InMemoryStorage()
6068
return self._hishel_storage
6169

70+
@override
6271
def get_async_hishel_storage(self) -> AsyncInMemoryStorage:
6372
if self._hishel_async_storage is None:
6473
self._hishel_async_storage = AsyncInMemoryStorage()

githubkit/cache/redis.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from datetime import timedelta
2+
from typing_extensions import override
3+
from typing import TYPE_CHECKING, Any, NoReturn, Optional
4+
5+
from hishel import RedisStorage, AsyncBaseStorage, AsyncRedisStorage
6+
7+
from githubkit.exception import CacheUnsupportedError
8+
9+
from .base import BaseCache, AsyncBaseCache, BaseCacheStrategy
10+
11+
if TYPE_CHECKING:
12+
from redis import Redis
13+
from redis.asyncio import Redis as AsyncRedis
14+
15+
16+
def _ensure_str_or_none(value: Any) -> Optional[str]:
17+
if isinstance(value, str):
18+
return value
19+
elif isinstance(value, bytes):
20+
return value.decode("utf-8")
21+
elif value is None:
22+
return None
23+
else:
24+
raise RuntimeError(f"Unexpected redis value {value!r} with type {type(value)}")
25+
26+
27+
class RedisCache(BaseCache):
28+
def __init__(self, client: "Redis") -> None:
29+
self.client = client
30+
31+
@override
32+
def get(self, key: str) -> Optional[str]:
33+
data = self.client.get(key)
34+
return _ensure_str_or_none(data)
35+
36+
@override
37+
def set(self, key: str, value: str, ex: timedelta) -> None:
38+
self.client.set(key, value, ex)
39+
40+
41+
class AsyncRedisCache(AsyncBaseCache):
42+
def __init__(self, client: "AsyncRedis") -> None:
43+
self.client = client
44+
45+
@override
46+
async def aget(self, key: str) -> Optional[str]:
47+
data = await self.client.get(key)
48+
return _ensure_str_or_none(data)
49+
50+
@override
51+
async def aset(self, key: str, value: str, ex: timedelta) -> None:
52+
await self.client.set(key, value, ex)
53+
54+
55+
class RedisCacheStrategy(BaseCacheStrategy):
56+
def __init__(self, client: "Redis") -> None:
57+
self.client = client
58+
59+
@override
60+
def get_cache_storage(self) -> RedisCache:
61+
return RedisCache(self.client)
62+
63+
@override
64+
def get_async_cache_storage(self) -> NoReturn:
65+
raise CacheUnsupportedError(
66+
"Sync redis cache strategy does not support async usage"
67+
)
68+
69+
@override
70+
def get_hishel_storage(self) -> RedisStorage:
71+
return RedisStorage(client=self.client)
72+
73+
@override
74+
def get_async_hishel_storage(self) -> NoReturn:
75+
raise CacheUnsupportedError(
76+
"Sync redis cache strategy does not support async usage"
77+
)
78+
79+
80+
class AsyncRedisCacheStrategy(BaseCacheStrategy):
81+
def __init__(self, client: "AsyncRedis") -> None:
82+
self.client = client
83+
84+
@override
85+
def get_cache_storage(self) -> NoReturn:
86+
raise CacheUnsupportedError(
87+
"Async redis cache strategy does not support sync usage"
88+
)
89+
90+
@override
91+
def get_async_cache_storage(self) -> AsyncRedisCache:
92+
return AsyncRedisCache(self.client)
93+
94+
@override
95+
def get_hishel_storage(self) -> NoReturn:
96+
raise CacheUnsupportedError(
97+
"Async redis cache strategy does not support sync usage"
98+
)
99+
100+
@override
101+
def get_async_hishel_storage(self) -> AsyncBaseStorage:
102+
return AsyncRedisStorage(client=self.client)

githubkit/exception.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
class GitHubException(Exception): ...
1515

1616

17+
class CacheUnsupportedError(GitHubException):
18+
"""Unsupported Cache Usage Error"""
19+
20+
1721
class AuthCredentialError(GitHubException):
1822
"""Auth Credential Error"""
1923

poetry.lock

Lines changed: 30 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ PyJWT = { version = "^2.4.0", extras = ["crypto"], optional = true }
2222

2323
[tool.poetry.group.dev.dependencies]
2424
ruff = "^0.7.0"
25+
redis = "^5.2.0"
2526
isort = "^5.13.2"
2627
Jinja2 = "^3.1.2"
2728
nonemoji = "^0.1.2"

0 commit comments

Comments
 (0)