summaryrefslogtreecommitdiff
path: root/requests_cache/policy
diff options
context:
space:
mode:
authorJordan Cook <jordan.cook@pioneer.com>2022-04-22 20:10:57 -0500
committerJordan Cook <jordan.cook@pioneer.com>2022-05-04 16:17:22 -0500
commitbbd984375d22dedaf33c8d0cad718cc09d072d25 (patch)
tree37eb6cfad188feefcd18dff05de9e591c7dff1aa /requests_cache/policy
parent3aa84ee6724491858a81ce401487fc85d0ee9c9d (diff)
downloadrequests-cache-bbd984375d22dedaf33c8d0cad718cc09d072d25.tar.gz
Implement Cache-Control: stale-while-revalidate
Diffstat (limited to 'requests_cache/policy')
-rw-r--r--requests_cache/policy/actions.py44
-rw-r--r--requests_cache/policy/directives.py1
-rw-r--r--requests_cache/policy/settings.py1
3 files changed, 34 insertions, 12 deletions
diff --git a/requests_cache/policy/actions.py b/requests_cache/policy/actions.py
index cdacefd..5d02b04 100644
--- a/requests_cache/policy/actions.py
+++ b/requests_cache/policy/actions.py
@@ -42,16 +42,19 @@ class CacheActions(RichMixin):
expire_after: User or header-provided expiration value
send_request: Send a new request
resend_request: Send a new request to refresh a stale cache item
+ resend_async: Return a stale cache item, and send a non-blocking request to refresh it
skip_read: Skip reading from the cache
skip_write: Skip writing to the cache
"""
# Outputs
+ # TODO: Besides skip read/write, will there always be only one action? Should these be an enum instead?
cache_key: str = field(default=None, repr=False)
error_504: bool = field(default=False)
expire_after: ExpirationTime = field(default=None)
- resend_request: bool = field(default=False)
send_request: bool = field(default=False)
+ resend_request: bool = field(default=False)
+ resend_async: bool = field(default=False)
skip_read: bool = field(default=False)
skip_write: bool = field(default=False)
@@ -63,6 +66,7 @@ class CacheActions(RichMixin):
_only_if_cached: bool = field(default=False, repr=False)
_refresh: bool = field(default=False, 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)
@classmethod
@@ -82,6 +86,9 @@ class CacheActions(RichMixin):
only_if_cached = settings.only_if_cached or directives.only_if_cached
refresh = directives.max_age == EXPIRE_IMMEDIATELY or directives.must_revalidate
stale_if_error = settings.stale_if_error or directives.stale_if_error
+ stale_while_revalidate = (
+ settings.stale_while_revalidate or directives.stale_while_revalidate
+ )
# Check expiration values in order of precedence
expire_after = coalesce(
@@ -107,6 +114,7 @@ class CacheActions(RichMixin):
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,
)
@@ -121,18 +129,27 @@ class CacheActions(RichMixin):
def is_usable(self, cached_response: 'CachedResponse', error: bool = False):
"""Determine whether a given cached response is "fresh enough" to satisfy the request,
- based on min-fresh, max-stale, or stale-if-error (if an error has occured).
+ based on:
+
+ * min-fresh
+ * max-stale
+ * stale-if-error (if an error has occured)
+ * stale-while-revalidate
"""
if cached_response is None:
return False
- elif cached_response.expires is None:
- return True
- # Handle additional types supported for stale_if_error
- elif error and self._stale_if_error is True:
+ elif (
+ cached_response.expires is None
+ or (cached_response.is_expired and self._stale_while_revalidate is True)
+ or (error and self._stale_if_error is True)
+ ):
return True
+ # Handle stale_if_error as a time value
elif error and self._stale_if_error:
- offset_seconds = get_expiration_seconds(self._stale_if_error)
- offset = timedelta(seconds=offset_seconds)
+ offset = timedelta(seconds=get_expiration_seconds(self._stale_if_error))
+ # Handle stale_while_revalidate as a time value
+ elif cached_response.is_expired and self._stale_while_revalidate:
+ offset = timedelta(seconds=get_expiration_seconds(self._stale_while_revalidate))
# Handle min-fresh and max-stale
else:
offset = self._directives.get_expire_offset()
@@ -145,18 +162,21 @@ class CacheActions(RichMixin):
Used after fetching a cached response, but before potentially sending a new request.
"""
- valid_response = self.is_usable(cached_response)
- valid_if_error = self.is_usable(cached_response, error=True)
+ usable_response = self.is_usable(cached_response)
+ usable_if_error = self.is_usable(cached_response, error=True)
# Can't satisfy the request
- if not valid_response and self._only_if_cached and not valid_if_error:
+ if not usable_response and self._only_if_cached and not usable_if_error:
self.error_504 = True
# Send the request for the first time
elif cached_response is None:
self.send_request = True
# Resend the request, unless settings permit a stale response
- elif not valid_response and not (self._only_if_cached and valid_if_error):
+ elif not usable_response and not (self._only_if_cached and usable_if_error):
self.resend_request = True
+ # Resend the request in the background; meanwhile return stale response
+ elif cached_response.is_expired and usable_response and self._stale_while_revalidate:
+ self.resend_async = True
if cached_response is not None:
self._update_validation_headers(cached_response)
diff --git a/requests_cache/policy/directives.py b/requests_cache/policy/directives.py
index 3236fe4..d6008a0 100644
--- a/requests_cache/policy/directives.py
+++ b/requests_cache/policy/directives.py
@@ -25,6 +25,7 @@ class CacheDirectives(RichMixin):
no_store: bool = field(default=False)
only_if_cached: bool = field(default=False)
stale_if_error: int = field(default=None, converter=try_int)
+ stale_while_revalidate: int = field(default=None, converter=try_int)
etag: str = field(default=None)
last_modified: str = field(default=None)
diff --git a/requests_cache/policy/settings.py b/requests_cache/policy/settings.py
index 2f9b9fe..0070ee6 100644
--- a/requests_cache/policy/settings.py
+++ b/requests_cache/policy/settings.py
@@ -35,6 +35,7 @@ class CacheSettings(RichMixin):
match_headers: Union[Iterable[str], bool] = field(default=False)
only_if_cached: bool = field(default=False)
stale_if_error: Union[bool, ExpirationTime] = field(default=False)
+ stale_while_revalidate: Union[bool, ExpirationTime] = field(default=False)
urls_expire_after: Dict[str, ExpirationTime] = field(factory=dict)
@classmethod