diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b38853 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/README.md b/README.md index 3f17748..c9a5a86 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,11 @@ loop = asyncio.get_event_loop() loop.run_until_complete(run_example()) ``` +Or with Redis as cache backend: + +```python +client = CachingClient(client, cache=AsyncRedisCache()) +``` **Documentation:** diff --git a/gen_sync.py b/gen_sync.py index a33e456..4af277d 100644 --- a/gen_sync.py +++ b/gen_sync.py @@ -20,6 +20,7 @@ additional_replacements={ "AsyncBaseTransport": "BaseTransport", "AsyncHTTPTransport": "HTTPTransport", + "AsyncRedisCache": "SyncRedisCache", "async_client": "client", "AsyncClient": "Client", "make_async_client": "make_client", diff --git a/httpx_caching/__init__.py b/httpx_caching/__init__.py index c459097..ff26612 100644 --- a/httpx_caching/__init__.py +++ b/httpx_caching/__init__.py @@ -1,6 +1,8 @@ from httpx_caching._async._cache import AsyncDictCache +from httpx_caching._async._cache_redis import AsyncRedisCache from httpx_caching._async._transport import AsyncCachingTransport from httpx_caching._sync._cache import SyncDictCache +from httpx_caching._sync._cache_redis import SyncRedisCache from httpx_caching._sync._transport import SyncCachingTransport from httpx_caching._wrapper import CachingClient @@ -15,7 +17,9 @@ "SyncCachingTransport", "CachingClient", "AsyncDictCache", + "AsyncRedisCache", "SyncDictCache", + "SyncRedisCache", "ExpiresAfterHeuristic", "LastModifiedHeuristic", "OneDayCacheHeuristic", diff --git a/httpx_caching/_async/_cache_redis.py b/httpx_caching/_async/_cache_redis.py new file mode 100644 index 0000000..1cf6f0a --- /dev/null +++ b/httpx_caching/_async/_cache_redis.py @@ -0,0 +1,34 @@ +from typing import Optional, Tuple +import redis.asyncio as redis +from httpx_caching._models import Response +from httpx_caching._serializer import Serializer +from httpx_caching._utils import AsyncLock + + +class AsyncRedisCache: + def __init__(self, serializer: Optional[Serializer] = None) -> None: + self.serializer = serializer if serializer else Serializer() + self.redis = redis.Redis() + self.lock = None + + def get_lock(self): + if not self.lock: + self.lock = AsyncLock() + return self.lock + + async def aget(self, key: str) -> Tuple[Optional[Response], Optional[dict]]: + value = await self.redis.get(key) + return self.serializer.loads(value) + + async def aset( + self, key: str, response: Response, vary_header_data: dict, response_body: bytes + ) -> None: + async with self.get_lock(): + await self.redis.set(key, self.serializer.dumps(response, vary_header_data, response_body)) + + async def adelete(self, key: str) -> None: + async with self.get_lock(): + await self.redis.delete(key) + + async def aclose(self): + await self.redis.close() diff --git a/httpx_caching/_sync/_cache_redis.py b/httpx_caching/_sync/_cache_redis.py new file mode 100644 index 0000000..3fe22f4 --- /dev/null +++ b/httpx_caching/_sync/_cache_redis.py @@ -0,0 +1,34 @@ +from typing import Optional, Tuple +import redis as redis +from httpx_caching._models import Response +from httpx_caching._serializer import Serializer +from httpx_caching._utils import SyncLock + + +class SyncRedisCache: + def __init__(self, serializer: Optional[Serializer] = None) -> None: + self.serializer = serializer if serializer else Serializer() + self.redis = redis.Redis() + self.lock = None + + def get_lock(self): + if not self.lock: + self.lock = SyncLock() + return self.lock + + def get(self, key: str) -> Tuple[Optional[Response], Optional[dict]]: + value = self.redis.get(key) + return self.serializer.loads(value) + + def set( + self, key: str, response: Response, vary_header_data: dict, response_body: bytes + ) -> None: + with self.get_lock(): + self.redis.set(key, self.serializer.dumps(response, vary_header_data, response_body)) + + def delete(self, key: str) -> None: + with self.get_lock(): + self.redis.delete(key) + + def close(self): + self.redis.close() diff --git a/setup.py b/setup.py index f28d013..a648a8e 100644 --- a/setup.py +++ b/setup.py @@ -54,10 +54,11 @@ def get_packages(package): include_package_data=True, zip_safe=False, install_requires=[ - "httpx==0.22.*", + "httpx==0.23.*", "msgpack", "anyio", "multimethod", + "redis" ], extras_require={}, classifiers=[ diff --git a/tests/_async/test_vary.py b/tests/_async/test_vary.py index 03e30d4..dc4ead9 100644 --- a/tests/_async/test_vary.py +++ b/tests/_async/test_vary.py @@ -3,10 +3,11 @@ # SPDX-License-Identifier: Apache-2.0 from urllib.parse import urljoin +from httpx import AsyncClient import pytest -from httpx_caching import AsyncDictCache +from httpx_caching import AsyncDictCache, CachingClient from tests.conftest import cache_hit, make_async_client @@ -17,7 +18,8 @@ def cache(self): @pytest.fixture() async def async_client(self, cache): - async_client = make_async_client(cache=cache) + async_client = AsyncClient() + async_client = CachingClient(async_client, cache=cache) yield async_client await async_client.aclose()