summaryrefslogtreecommitdiff
path: root/requests_cache
diff options
context:
space:
mode:
authorJordan Cook <jordan.cook@pioneer.com>2022-05-21 12:54:56 -0500
committerJordan Cook <jordan.cook@pioneer.com>2022-06-16 15:45:42 -0500
commit20dcc26d7d49d2ec8ee9571161a2722bb09fed25 (patch)
treee99bc0a4597004cf6eb63ed26e6d980e0b0c1064 /requests_cache
parentccaf6b0b2d9a7dc612b5129e1c2841a04a2b587c (diff)
downloadrequests-cache-20dcc26d7d49d2ec8ee9571161a2722bb09fed25.tar.gz
Add support for Vary
Diffstat (limited to 'requests_cache')
-rw-r--r--requests_cache/backends/base.py6
-rw-r--r--requests_cache/policy/actions.py39
-rw-r--r--requests_cache/session.py11
3 files changed, 49 insertions, 7 deletions
diff --git a/requests_cache/backends/base.py b/requests_cache/backends/base.py
index 5e00db0..2c5b0b6 100644
--- a/requests_cache/backends/base.py
+++ b/requests_cache/backends/base.py
@@ -104,13 +104,15 @@ class BaseCache:
self.responses.close()
self.redirects.close()
- def create_key(self, request: AnyRequest = None, **kwargs) -> str:
+ def create_key(
+ self, request: AnyRequest = None, match_headers: Iterable[str] = None, **kwargs
+ ) -> str:
"""Create a normalized cache key from a request object"""
key_fn = self._settings.key_fn or create_key
return key_fn(
request=request,
ignored_parameters=self._settings.ignored_parameters,
- match_headers=self._settings.match_headers,
+ match_headers=match_headers or self._settings.match_headers,
serializer=self.responses.serializer,
**kwargs,
)
diff --git a/requests_cache/policy/actions.py b/requests_cache/policy/actions.py
index 707b0d7..ad201f9 100644
--- a/requests_cache/policy/actions.py
+++ b/requests_cache/policy/actions.py
@@ -17,7 +17,7 @@ from . import (
get_expiration_seconds,
get_url_expiration,
)
-from .settings import CacheSettings
+from .settings import CacheSettings, KeyCallback
if TYPE_CHECKING:
from ..models import CachedResponse
@@ -64,6 +64,7 @@ class CacheActions(RichMixin):
# Temporary attributes
_only_if_cached: bool = field(default=False, repr=False)
_refresh: bool = field(default=False, repr=False)
+ _request: PreparedRequest = field(default=None, repr=False)
_stale_if_error: Union[bool, ExpirationTime] = field(default=None, repr=False)
_stale_while_revalidate: Union[bool, ExpirationTime] = field(default=None, repr=False)
_validation_headers: Dict[str, str] = field(factory=dict, repr=False)
@@ -76,6 +77,10 @@ class CacheActions(RichMixin):
indicate a user-requested refresh. Typically that's only used in response headers, and
`max-age=0` would be used by a client to request a refresh. However, this would conflict
with the `expire_after` option provided in :py:meth:`.CachedSession.request`.
+
+ Args:
+ request: The outgoing request
+ settings: Session-level cache settings
"""
settings = settings or CacheSettings()
directives = CacheDirectives.from_headers(request.headers)
@@ -107,15 +112,16 @@ class CacheActions(RichMixin):
actions = cls(
cache_key=cache_key,
+ directives=directives,
expire_after=expire_after,
only_if_cached=only_if_cached,
refresh=refresh,
+ request=request,
+ settings=settings,
skip_read=any(read_criteria.values()),
skip_write=directives.no_store,
stale_if_error=stale_if_error,
stale_while_revalidate=stale_while_revalidate,
- directives=directives,
- settings=settings,
)
return actions
@@ -155,11 +161,18 @@ class CacheActions(RichMixin):
return datetime.utcnow() < cached_response.expires + offset
- def update_from_cached_response(self, cached_response: 'CachedResponse'):
+ def update_from_cached_response(
+ self, cached_response: 'CachedResponse', create_key: KeyCallback = None, **key_kwargs
+ ):
"""Determine if we can reuse a cached response, or set headers for a conditional request
if possible.
Used after fetching a cached response, but before potentially sending a new request.
+
+ Args:
+ cached_response: Cached response to examine
+ create_key: Cache key function, used for validating ``Vary`` headers
+ key_kwargs: Additional keyword arguments for ``create_key``.
"""
usable_response = self.is_usable(cached_response)
usable_if_error = self.is_usable(cached_response, error=True)
@@ -170,6 +183,9 @@ class CacheActions(RichMixin):
# Send the request for the first time
elif cached_response is None:
self.send_request = True
+ # If response contains Vary and doesn't match, consider it a cache miss
+ elif create_key and not self._validate_vary(cached_response, create_key, **key_kwargs):
+ self.send_request = True
# Resend the request, unless settings permit a stale response
elif not usable_response and not (self._only_if_cached and usable_if_error):
self.resend_request = True
@@ -262,6 +278,21 @@ class CacheActions(RichMixin):
self.send_request = True
self.resend_request = False
+ def _validate_vary(
+ self, cached_response: 'CachedResponse', create_key: KeyCallback, **key_kwargs
+ ) -> bool:
+ """If the cached response contains Vary, check that the specified request headers match"""
+ vary = cached_response.headers.get('Vary')
+ if not vary:
+ return True
+ elif vary == '*':
+ return False
+
+ # Generate a secondary cache key based on Vary for both the cached request and new request
+ key_kwargs['match_headers'] = [k.strip() for k in vary.split(',')]
+ vary_cache_key = create_key(cached_response.request, **key_kwargs)
+ return create_key(self._request, **key_kwargs) == vary_cache_key
+
def _log_cache_criteria(operation: str, criteria: Dict):
"""Log details on any failed checks for cache read or write"""
diff --git a/requests_cache/session.py b/requests_cache/session.py
index b65da1e..f3dec60 100644
--- a/requests_cache/session.py
+++ b/requests_cache/session.py
@@ -192,7 +192,16 @@ class CacheMixin(MIXIN_BASE):
cached_response: Optional[CachedResponse] = None
if not actions.skip_read:
cached_response = self.cache.get_response(actions.cache_key)
- actions.update_from_cached_response(cached_response)
+ actions.update_from_cached_response(cached_response, self.cache.create_key, **kwargs)
+
+ # TODO: Does this fit better here, or in CacheActions?
+ # If response contains Vary, check that the specified request headers match
+ # if cached_response and cached_response.headers.get('Vary'):
+ # vary = cached_response.headers['Vary']
+ # new_cache_key = self.cache.create_key(request, match_headers=vary)
+ # vary_cache_key = self.cache.create_key(cached_response.request, match_headers=vary)
+ # if new_cache_key != vary_cache_key:
+ # cached_response = None
# Handle missing and expired responses based on settings and headers
if actions.error_504: