"""Main classes to add caching features to ``requests.Session``"""fromcontextlibimportcontextmanagerfromloggingimportgetLoggerfromthreadingimportRLockfromtypingimportTYPE_CHECKING,Any,Callable,Dict,Iterable,OptionalfromrequestsimportPreparedRequest,ResponsefromrequestsimportSessionasOriginalSessionfromrequests.hooksimportdispatch_hookfromurllib3importfilepostfrom.backendsimportBackendSpecifier,get_valid_kwargs,init_backendfrom.cache_controlimportCacheActions,ExpirationTimefrom.cache_keysimportnormalize_dictfrom.modelsimportAnyResponse,set_response_defaultsALL_METHODS=['GET','HEAD','OPTIONS','POST','PUT','PATCH','DELETE']logger=getLogger(__name__)# MIXIN_BASE: Type = OriginalSession if TYPE_CHECKING else objectifTYPE_CHECKING:MIXIN_BASE=OriginalSessionelse:MIXIN_BASE=object
[docs]classCacheMixin(MIXIN_BASE):"""Mixin class that extends :py:class:`requests.Session` with caching features. See :py:class:`.CachedSession` for usage information. """def__init__(self,cache_name:str='http_cache',backend:BackendSpecifier=None,expire_after:ExpirationTime=-1,urls_expire_after:Dict[str,ExpirationTime]=None,allowable_codes:Iterable[int]=(200,),allowable_methods:Iterable[str]=('GET','HEAD'),filter_fn:Callable=None,old_data_on_error:bool=False,cache_control:bool=False,**kwargs,):self.cache=init_backend(backend,cache_name,**kwargs)self.allowable_codes=allowable_codesself.allowable_methods=allowable_methodsself.expire_after=expire_afterself.urls_expire_after=urls_expire_afterself.filter_fn=filter_fnor(lambdar:True)self.old_data_on_error=old_data_on_errorself.cache_control=cache_controlself.cache.name=cache_name# Set to handle backend=<instance>self._request_expire_after:ExpirationTime=Noneself._disabled=Falseself._lock=RLock()# If the superclass is custom Session, pass along valid kwargs (if any)session_kwargs=get_valid_kwargs(super().__init__,kwargs)super().__init__(**session_kwargs)# type: ignoredefrequest(# type: ignore # Note: Session.request() doesn't have expire_after paramself,method:str,url:str,params:Dict=None,data:Any=None,json:Dict=None,expire_after:ExpirationTime=None,**kwargs,)->AnyResponse:"""This method prepares and sends a request while automatically performing any necessary caching operations. This will be called by any other method-specific ``requests`` functions (get, post, etc.). This does not include prepared requests, which will still be cached via ``send()``. See :py:meth:`requests.Session.request` for parameters. Additional parameters: Args: expire_after: Expiration time to set only for this request; see details below. Overrides ``CachedSession.expire_after``. Accepts all the same values as ``CachedSession.expire_after`` except for ``None``; use ``-1`` to disable expiration on a per-request basis. Returns: Either a new or cached response **Order of operations:** For reference, a request will pass through the following methods: 1. :py:func:`requests.get`/:py:meth:`requests.Session.get` or other method-specific functions (optional) 2. :py:meth:`.CachedSession.request` 3. :py:meth:`requests.Session.request` 4. :py:meth:`.CachedSession.send` 5. :py:meth:`.BaseCache.get_response` 6. :py:meth:`requests.Session.send` (if not previously cached) 7. :py:meth:`.BaseCache.save_response` (if not previously cached) """withself.request_expire_after(expire_after),patch_form_boundary(**kwargs):returnsuper().request(method,url,params=normalize_dict(params),data=normalize_dict(data),json=normalize_dict(json),**kwargs,)defsend(self,request:PreparedRequest,**kwargs)->AnyResponse:"""Send a prepared request, with caching. See :py:meth:`.request` for notes on behavior."""# Determine which actions to take based on request info, headers, and cache settingscache_key=self.cache.create_key(request,**kwargs)actions=CacheActions(cache_key=cache_key,request=request,request_expire_after=self._request_expire_after,session_expire_after=self.expire_after,urls_expire_after=self.urls_expire_after,cache_control=self.cache_control,**kwargs,)# Attempt to fetch a cached responseresponse:Optional[AnyResponse]=Noneifnot(self._disabledoractions.skip_read):response=self.cache.get_response(cache_key)is_expired=getattr(response,'is_expired',False)# If the cache is disabled, doesn't have the response, or it's expired, then fetch a new oneifresponseisNone:response=self._send_and_cache(request,actions,**kwargs)elifis_expiredandself.old_data_on_error:response=self._resend_and_ignore(request,actions,**kwargs)orresponseelifis_expired:response=self._resend(request,actions,**kwargs)# Dispatch any hooks here, because they are removed before picklingresponse=dispatch_hook('response',request.hooks,response,**kwargs)ifTYPE_CHECKING:assertresponseisnotNone# If the request has been filtered out, delete previously cached response if it existsifnotself.filter_fn(response):logger.debug(f'Deleting filtered response for URL: {response.url}')self.cache.delete(cache_key)returnresponse# Cache redirect historyforrinresponse.history:self.cache.save_redirect(r.request,cache_key)returnresponsedef_send_and_cache(self,request:PreparedRequest,actions:CacheActions,**kwargs):"""Send the request and cache the response, unless disabled by settings or headers"""response=super().send(request,**kwargs)actions.update_from_response(response)ifself._is_cacheable(response,actions):logger.debug(f'Skipping cache write for URL: {request.url}')self.cache.save_response(response,actions.cache_key,actions.expires)returnset_response_defaults(response,actions.cache_key)def_resend(self,request:PreparedRequest,actions:CacheActions,**kwargs)->AnyResponse:"""Attempt to resend the request and cache the new response. If the request fails, delete the expired cache item. """logger.debug('Expired response; attempting to re-send request')try:returnself._send_and_cache(request,actions,**kwargs)exceptException:self.cache.delete(actions.cache_key)raisedef_resend_and_ignore(self,request:PreparedRequest,actions:CacheActions,**kwargs)->Optional[AnyResponse]:"""Attempt to send the request and cache the new response. If there are any errors, ignore them and and return ``None``. """# Attempt to send the request and cache the new responselogger.debug('Expired response; attempting to re-send request')try:response=self._send_and_cache(request,actions,**kwargs)response.raise_for_status()returnresponseexceptExceptionase:logger.warning('Request failed; using stale cache data: %s',e)returnNonedef_is_cacheable(self,response:Response,actions:CacheActions)->bool:"""Perform all checks needed to determine if the given response should be saved to the cache"""cache_criteria={'disabled cache':self._disabled,'disabled method':str(response.request.method)notinself.allowable_methods,'disabled status':response.status_codenotinself.allowable_codes,'disabled by filter':notself.filter_fn(response),'disabled by headers or expiration params':actions.skip_write,}logger.debug(f'Pre-cache checks for response from {response.url}: {cache_criteria}')returnnotany(cache_criteria.values())@contextmanagerdefcache_disabled(self):""" Context manager for temporary disabling the cache .. warning:: This method is not thread-safe. Example: >>> s = CachedSession() >>> with s.cache_disabled(): ... s.get('http://httpbin.org/ip') """ifself._disabled:yieldelse:self._disabled=Truetry:yieldfinally:self._disabled=False@contextmanagerdefrequest_expire_after(self,expire_after:ExpirationTime=None):"""Temporarily override ``expire_after`` for an individual request. This is needed to persist the value between requests.Session.request() -> send()."""# TODO: Is there a way to pass this via request kwargs -> PreparedRequest?withself._lock:self._request_expire_after=expire_afteryieldself._request_expire_after=Nonedefremove_expired_responses(self,expire_after:ExpirationTime=None):"""Remove expired responses from the cache, optionally with revalidation Args: expire_after: A new expiration time used to revalidate the cache """self.cache.remove_expired_responses(expire_after)def__repr__(self):repr_attrs=['cache','expire_after','urls_expire_after','allowable_codes','allowable_methods','old_data_on_error','cache_control',]attr_strs=[f'{k}={repr(getattr(self,k))}'forkinrepr_attrs]returnf'<CachedSession({", ".join(attr_strs)})>'
[docs]classCachedSession(CacheMixin,OriginalSession):"""Class that extends :py:class:`requests.Session` with caching features. See individual :py:mod:`backend classes <requests_cache.backends>` for additional backend-specific arguments. Also see :ref:`user-guide` for more details and examples on how the following arguments affect cache behavior. Args: cache_name: Cache prefix or namespace, depending on backend backend: Cache backend name, class, or instance; name may be one of ``['sqlite', 'mongodb', 'gridfs', 'redis', 'dynamodb', 'memory']``. expire_after: Time after which cached items will expire urls_expire_after: Expiration times to apply for different URL patterns allowable_codes: Only cache responses with one of these codes allowable_methods: Cache only responses for one of these HTTP methods include_get_headers: Make request headers part of the cache key ignored_parameters: List of request parameters to be excluded from the cache key filter_fn: function that takes a :py:class:`aiohttp.ClientResponse` object and returns a boolean indicating whether or not that response should be cached. Will be applied to both new and previously cached responses. old_data_on_error: Return stale cache data if a new request raises an exception cache_control: Use Cache-Control request and response headers """
@contextmanagerdefpatch_form_boundary(**request_kwargs):"""This patches the form boundary used to separate multipart uploads. Requests does not provide a way to pass a custom boundary to urllib3, so this just monkey-patches it instead. """ifrequest_kwargs.get('files'):original_boundary=filepost.choose_boundaryfilepost.choose_boundary=lambda:'##requests-cache-form-boundary##'yieldfilepost.choose_boundary=original_boundaryelse:yield