summaryrefslogtreecommitdiff
path: root/requests_cache
diff options
context:
space:
mode:
authorJordan Cook <jordan.cook@pioneer.com>2021-08-21 18:39:52 -0500
committerJordan Cook <jordan.cook@pioneer.com>2021-12-01 12:45:43 -0600
commit7e0690254ed45fa66f8b38571a9efd8fddda73cf (patch)
tree7aab1adaa68fce189f81974130bfa35a1d91b450 /requests_cache
parent85dce906c7b8d7343098c0b76571a3293c860d8d (diff)
downloadrequests-cache-7e0690254ed45fa66f8b38571a9efd8fddda73cf.tar.gz
Add a new RedisDict class that stores responses in separate hashes instead of in a single hash
Diffstat (limited to 'requests_cache')
-rw-r--r--requests_cache/backends/__init__.py4
-rw-r--r--requests_cache/backends/redis.py104
-rwxr-xr-xrequests_cache/models/response.py8
-rw-r--r--requests_cache/serializers/preconf.py2
4 files changed, 102 insertions, 16 deletions
diff --git a/requests_cache/backends/__init__.py b/requests_cache/backends/__init__.py
index 27841b7..385e6a8 100644
--- a/requests_cache/backends/__init__.py
+++ b/requests_cache/backends/__init__.py
@@ -42,9 +42,9 @@ try:
except ImportError as e:
MongoCache = MongoDict = MongoPickleDict = get_placeholder_class(e) # type: ignore
try:
- from .redis import RedisCache, RedisHashDict
+ from .redis import RedisCache, RedisDict, RedisHashDict
except ImportError as e:
- RedisCache = RedisHashDict = get_placeholder_class(e) # type: ignore
+ RedisCache = RedisDict = RedisHashDict = get_placeholder_class(e) # type: ignore
try:
# Note: Heroku doesn't support SQLite due to ephemeral storage
from .sqlite import SQLiteCache, SQLiteDict, SQLitePickleDict
diff --git a/requests_cache/backends/redis.py b/requests_cache/backends/redis.py
index c460b12..f40a735 100644
--- a/requests_cache/backends/redis.py
+++ b/requests_cache/backends/redis.py
@@ -9,7 +9,7 @@ applications.
Persistence
^^^^^^^^^^^
Redis operates on data in memory, and by default also persists data to snapshots on disk. This is
-optimized for performance with a minor risk of data loss, which is usually the best configuration
+optimized for performance, with a minor risk of data loss, and is usually the best configuration
for a cache. If you need different behavior, the frequency and type of persistence can be customized
or disabled entirely. See `Redis Persistence <https://redis.io/topics/persistence>`_ for details.
@@ -32,12 +32,13 @@ API Reference
:nosignatures:
"""
from logging import getLogger
-from typing import Any, Iterable, Iterator, List, Tuple
+from typing import Iterable
from redis import Redis, StrictRedis
from .._utils import get_valid_kwargs
from ..cache_keys import decode, encode
+from ..serializers import dict_serializer
from . import BaseCache, BaseStorage
logger = getLogger(__name__)
@@ -54,17 +55,94 @@ class RedisCache(BaseCache):
def __init__(self, namespace='http_cache', connection: Redis = None, **kwargs):
super().__init__(**kwargs)
- self.responses = RedisHashDict(namespace, 'responses', connection=connection, **kwargs)
+ self.responses = RedisDict(namespace, connection=connection, **kwargs)
self.redirects = RedisHashDict(
namespace, 'redirects', connection=self.responses.connection, **kwargs
)
+class RedisDict(BaseStorage):
+ """A dictionary-like interface for Redis operations.
+
+ **Notes:**
+ * Each item is stored as a separate hash
+ * All keys will be encoded as bytes
+ * Does not support custom serializers
+ * Supports TTL
+ """
+
+ def __init__(self, namespace: str, connection=None, serializer=None, **kwargs):
+ if serializer:
+ logger.warning('RedisDict does not support custom serializers')
+ super().__init__(serializer=dict_serializer, **kwargs)
+
+ connection_kwargs = get_valid_kwargs(Redis, kwargs)
+ self.connection = connection or StrictRedis(**connection_kwargs)
+ self.namespace = namespace
+
+ def _bkey(self, key: str) -> bytes:
+ """Get a full hash key as bytes"""
+ return encode(f'{self.namespace}:{key}')
+
+ def _bkeys(self, keys: Iterable[str]):
+ return [self._bkey(key) for key in keys]
+
+ def __contains__(self, key) -> bool:
+ return bool(self.connection.exists(self._bkey(key)))
+
+ def __getitem__(self, key):
+ # result = self.connection.get(self._bkey(key))
+ result = self.connection.hgetall(self._bkey(key))
+ if not result:
+ raise KeyError
+ return self.serializer.loads(result)
+
+ def __setitem__(self, key, item):
+ """Save an item to the cache, optionally with TTL"""
+ # if getattr(item, 'ttl', None):
+ # self.connection.setex(self._bkey(key), item.ttl, self.serializer.dumps(item))
+ # else:
+ # self.connection.set(self._bkey(key), self.serializer.dumps(item))
+ self.connection.hmset(self._bkey(key), self.serializer.dumps(item))
+ if getattr(item, 'ttl', None):
+ self.connection.expire(self._bkey(key), item.ttl)
+
+ def __delitem__(self, key):
+ if not self.connection.delete(self._bkey(key)):
+ raise KeyError
+
+ def __iter__(self):
+ yield from self.keys()
+
+ def __len__(self):
+ return len(list(self.keys()))
+
+ def bulk_delete(self, keys: Iterable[str]):
+ """Delete multiple keys from the cache, without raising errors for missing keys"""
+ if keys:
+ self.connection.delete(*self._bkeys(keys))
+
+ def clear(self):
+ self.bulk_delete(self.keys())
+
+ def keys(self):
+ return [
+ decode(key).replace(f'{self.namespace}:', '')
+ for key in self.connection.keys(f'{self.namespace}:*')
+ ]
+
+ def items(self):
+ return [(k, self[k]) for k in self.keys()]
+
+ def values(self):
+ return [self.serializer.loads(v) for v in self.connection.mget(*self._bkeys(self.keys()))]
+
+
class RedisHashDict(BaseStorage):
"""A dictionary-like interface for operations on a single Redis hash
**Notes:**
- * All keys will be encoded and all values will be serialized
+ * All keys will be encoded as bytes, and all values will be serialized
* Items will be stored in a hash named ``namespace:collection_name``
"""
@@ -76,26 +154,26 @@ class RedisHashDict(BaseStorage):
self.connection = connection or StrictRedis(**connection_kwargs)
self._hash_key = f'{namespace}:{collection_name}'
- def __contains__(self, key: str) -> bool:
+ def __contains__(self, key):
return self.connection.hexists(self._hash_key, encode(key))
- def __getitem__(self, key: str):
+ def __getitem__(self, key):
result = self.connection.hget(self._hash_key, encode(key))
if result is None:
raise KeyError
return self.serializer.loads(result)
- def __setitem__(self, key: str, item):
+ def __setitem__(self, key, item):
self.connection.hset(self._hash_key, encode(key), self.serializer.dumps(item))
- def __delitem__(self, key: str):
+ def __delitem__(self, key):
if not self.connection.hdel(self._hash_key, encode(key)):
raise KeyError
- def __iter__(self) -> Iterator[str]:
+ def __iter__(self):
yield from self.keys()
- def __len__(self) -> int:
+ def __len__(self):
return self.connection.hlen(self._hash_key)
def bulk_delete(self, keys: Iterable[str]):
@@ -106,16 +184,16 @@ class RedisHashDict(BaseStorage):
def clear(self):
self.connection.delete(self._hash_key)
- def keys(self) -> List[str]:
+ def keys(self):
return [decode(key) for key in self.connection.hkeys(self._hash_key)]
- def items(self) -> List[Tuple[str, Any]]:
+ def items(self):
"""Get all ``(key, value)`` pairs in the hash"""
return [
(decode(k), self.serializer.loads(v))
for k, v in self.connection.hgetall(self._hash_key).items()
]
- def values(self) -> List:
+ def values(self):
"""Get all values in the hash"""
return [self.serializer.loads(v) for v in self.connection.hvals(self._hash_key)]
diff --git a/requests_cache/models/response.py b/requests_cache/models/response.py
index ec65034..561d6c8 100755
--- a/requests_cache/models/response.py
+++ b/requests_cache/models/response.py
@@ -96,6 +96,14 @@ class CachedResponse(Response):
return self.expires is not None and datetime.utcnow() >= self.expires
@property
+ def ttl(self) -> Optional[int]:
+ """Get time to expiration in seconds"""
+ if self.expires is None or self.is_expired:
+ return None
+ delta = self.expires - datetime.utcnow()
+ return int(delta.total_seconds())
+
+ @property
def next(self) -> Optional[PreparedRequest]:
"""Returns a PreparedRequest for the next request in a redirect chain, if there is one."""
return self._next.prepare() if self._next else None
diff --git a/requests_cache/serializers/preconf.py b/requests_cache/serializers/preconf.py
index 4c695a5..dce7e60 100644
--- a/requests_cache/serializers/preconf.py
+++ b/requests_cache/serializers/preconf.py
@@ -24,7 +24,7 @@ from .cattrs import CattrStage
from .pipeline import SerializerPipeline, Stage
base_stage = CattrStage() #: Base stage for all serializer pipelines (or standalone dict serializer)
-dict_serializer = base_stage #: Partial serializer that converts responses to dicts
+dict_serializer = base_stage #: Partial serializer that unstructures responses into dicts
bson_preconf_stage = CattrStage(bson_preconf.make_converter) #: Pre-serialization steps for BSON
json_preconf_stage = CattrStage(json_preconf.make_converter) #: Pre-serialization steps for JSON
msgpack_preconf_stage = CattrStage(msgpack.make_converter) #: Pre-serialization steps for msgpack