[docs]@define(repr=False)classCacheActions(RichMixin):"""Translates cache settings and headers into specific actions to take for a given cache item. The resulting actions are then handled in :py:meth:`CachedSession.send`. .. rubric:: Notes * See :ref:`precedence` for behavior if multiple sources provide an expiration * See :ref:`headers` for more details about header behavior * The following arguments/properties are the outputs of this class: Args: cache_key: The cache key created based on the initial request error_504: Indicates the request cannot be fulfilled based on cache settings 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 """# Outputscache_key:str=field(default=None,repr=False)error_504:bool=field(default=False)expire_after:ExpirationTime=field(default=None)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)# Inputs_directives:CacheDirectives=field(default=None,repr=False)_settings:CacheSettings=field(default=None,repr=False)# 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)
[docs]@classmethoddeffrom_request(cls,cache_key:str,request:PreparedRequest,settings:Optional[CacheSettings]=None):"""Initialize from request info and cache settings. Note on refreshing: `must-revalidate` isn't a standard request header, but is used here to 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=settingsorCacheSettings()directives=CacheDirectives.from_headers(request.headers)logger.debug(f'Cache directives from request headers: {directives}')# Merge values that may come from either settings or headersonly_if_cached=settings.only_if_cachedordirectives.only_if_cachedrefresh=directives.max_age==EXPIRE_IMMEDIATELYordirectives.must_revalidatestale_if_error=settings.stale_if_errorordirectives.stale_if_errorstale_while_revalidate=(settings.stale_while_revalidateordirectives.stale_while_revalidate)# Check expiration values in order of precedenceexpire_after=coalesce(directives.max_age,get_url_expiration(request.url,settings.urls_expire_after),settings.expire_after,)# Check and log conditions for reading from the cacheread_criteria={'disabled cache':settings.disabled,'disabled method':str(request.method)notinsettings.allowable_methods,'disabled by headers or refresh':directives.no_cacheordirectives.no_store,'disabled by expiration':expire_after==DO_NOT_CACHE,}_log_cache_criteria('read',read_criteria)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,)returnactions
@propertydefexpires(self)->Optional[datetime]:"""Convert the user/header-provided expiration value to a datetime. Applies to new cached responses, and previously cached responses that are being revalidated. """returnget_expiration_datetime(self.expire_after)
[docs]defis_usable(self,cached_response:Optional['CachedResponse'],error:bool=False):"""Determine whether a given cached response is "fresh enough" to satisfy the request, based on: * min-fresh * max-stale * stale-if-error (if an error has occured) * stale-while-revalidate """ifcached_responseisNone:returnFalseelif(cached_response.expiresisNoneor(cached_response.is_expiredandself._stale_while_revalidateisTrue)or(errorandself._stale_if_errorisTrue)):returnTrue# Handle stale_if_error as a time valueeliferrorandself._stale_if_error:offset=timedelta(seconds=get_expiration_seconds(self._stale_if_error))# Handle stale_while_revalidate as a time valueelifcached_response.is_expiredandself._stale_while_revalidate:offset=timedelta(seconds=get_expiration_seconds(self._stale_while_revalidate))# Handle min-fresh and max-staleelse:offset=self._directives.get_expire_offset()returndatetime.utcnow()<cached_response.expires+offset
[docs]defupdate_from_cached_response(self,cached_response:Optional['CachedResponse'],create_key:Optional[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)# Can't satisfy the requestifnotusable_responseandself._only_if_cachedandnotusable_if_error:self.error_504=True# Send the request for the first timeelifcached_responseisNone:self.send_request=True# If response contains Vary and doesn't match, consider it a cache misselifcreate_keyandnotself._validate_vary(cached_response,create_key,**key_kwargs):self.send_request=True# Resend the request, unless settings permit a stale responseelifnotusable_responseandnot(self._only_if_cachedandusable_if_error):self.resend_request=True# Resend the request in the background; meanwhile return stale responseelifcached_response.is_expiredandusable_responseandself._stale_while_revalidate:self.resend_async=Trueifcached_responseisnotNoneandnotself._only_if_cached:self._update_validation_headers(cached_response)logger.debug(f'Post-read cache actions: {self}')
[docs]defupdate_from_response(self,response:Response):"""Update expiration + actions based on headers and other details from a new response. Used after receiving a new response, but before saving it to the cache. """directives=CacheDirectives.from_headers(response.headers)ifself._settings.cache_control:self._update_from_response_headers(directives)# If "expired" but there's a validator, save it to the cache and revalidate on useskip_stale=self.expire_after==EXPIRE_IMMEDIATELYandnotdirectives.has_validatordo_not_cache=self.expire_after==DO_NOT_CACHE# Apply filter callback, if anycallback=self._settings.filter_fnfiltered_out=callbackisnotNoneandnotcallback(response)# Check and log conditions for writing to the cachewrite_criteria={'disabled cache':self._settings.disabled,'disabled method':str(response.request.method)notinself._settings.allowable_methods,'disabled status':response.status_codenotinself._settings.allowable_codes,'disabled by filter':filtered_out,'disabled by headers':self.skip_write,'disabled by expiration':do_not_cacheorskip_stale,}self.skip_write=any(write_criteria.values())_log_cache_criteria('write',write_criteria)
[docs]defupdate_request(self,request:PreparedRequest)->PreparedRequest:"""Apply validation headers (if any) before sending a request"""request.headers.update(self._validation_headers)returnrequest
[docs]defupdate_revalidated_response(self,response:Response,cached_response:'CachedResponse')->'CachedResponse':"""After revalidation, update the cached response's expiration and headers"""logger.debug(f'Response for URL {response.request.url} has not been modified')cached_response.expires=self.expirescached_response.headers.update(response.headers)cached_response.revalidated=Truereturncached_response
def_update_from_response_headers(self,directives:CacheDirectives):"""Check response headers for expiration and other cache directives"""logger.debug(f'Cache directives from response headers: {directives}')self._stale_if_error=self._stale_if_errorordirectives.stale_if_errorifdirectives.immutable:self.expire_after=NEVER_EXPIREelse:self.expire_after=coalesce(directives.max_age,directives.expires,self.expire_after,)self.skip_write=self.skip_writeordirectives.no_storedef_update_validation_headers(self,cached_response:'CachedResponse'):"""If needed, get validation headers based on a cached response. Revalidation may be triggered by a stale response, request headers, or cached response headers. """directives=CacheDirectives.from_headers(cached_response.headers)# These conditions always applyrevalidate=directives.has_validatorand(cached_response.is_expiredorself._refreshorself._settings.always_revalidate)# These conditions only apply if cache_control=Truecc_revalidate=self._settings.cache_controland(directives.no_cacheordirectives.must_revalidate)# Add the appropriate validation headers, if neededifrevalidateorcc_revalidate:ifdirectives.etag:self._validation_headers['If-None-Match']=directives.etagifdirectives.last_modified:self._validation_headers['If-Modified-Since']=directives.last_modifiedself.send_request=Trueself.resend_request=Falsedef_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')ifnotvary:returnTrueelifvary=='*':returnFalse# Generate a secondary cache key based on Vary for both the cached request and new requestkey_kwargs['match_headers']=[k.strip()forkinvary.split(',')]vary_cache_key=create_key(cached_response.request,**key_kwargs)headers_match=create_key(self._request,**key_kwargs)==vary_cache_keyifnotheaders_match:_log_vary_diff(self._request.headers,cached_response.request.headers,key_kwargs['match_headers'])returnheaders_match
def_log_vary_diff(headers_1:MutableMapping[str,str],headers_2:MutableMapping[str,str],vary:List[str]):"""Log which specific headers specified by Vary did not match"""iflogger.level>DEBUG:returnheaders_1=normalize_headers(headers_1)headers_2=normalize_headers(headers_2)nonmatching=[kforkinvaryifheaders_1.get(k)!=headers_2.get(k)]logger.debug(f'Failed Vary check. Non-matching headers: {", ".join(nonmatching)}')def_log_cache_criteria(operation:str,criteria:Dict):"""Log details on any failed checks for cache read or write"""iflogger.level>DEBUG:returnifany(criteria.values()):status=', '.join([kfork,vincriteria.items()ifv])else:status='Passed'logger.debug(f'Pre-{operation} cache checks: {status}')