diff options
author | Jordan Cook <jordan.cook@pioneer.com> | 2022-05-21 12:54:56 -0500 |
---|---|---|
committer | Jordan Cook <jordan.cook@pioneer.com> | 2022-06-16 15:45:42 -0500 |
commit | 20dcc26d7d49d2ec8ee9571161a2722bb09fed25 (patch) | |
tree | e99bc0a4597004cf6eb63ed26e6d980e0b0c1064 /requests_cache | |
parent | ccaf6b0b2d9a7dc612b5129e1c2841a04a2b587c (diff) | |
download | requests-cache-20dcc26d7d49d2ec8ee9571161a2722bb09fed25.tar.gz |
Add support for Vary
Diffstat (limited to 'requests_cache')
-rw-r--r-- | requests_cache/backends/base.py | 6 | ||||
-rw-r--r-- | requests_cache/policy/actions.py | 39 | ||||
-rw-r--r-- | requests_cache/session.py | 11 |
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: |