blob: 8f5bb73c557b3b9f6620b8e80350ace482e756cf [file] [edit]
"""Utilities for patching ``requests``. See :ref:`patching` for general usage info.
.. warning:: These functions are not thread-safe. Use :py:class:`.CachedSession` if you want to use
caching in a multi-threaded environment.
.. automodsumm:: requests_cache.patcher
:functions-only:
:nosignatures:
"""
import inspect
from contextlib import contextmanager
from logging import getLogger
from typing import TYPE_CHECKING, List, Optional, Type
from warnings import warn
import requests
from .backends import BackendSpecifier, BaseCache, init_backend
from .session import CachedSession, OriginalSession
# TODO: This is going to require thinking through many more edge cases.
# Lots of ways this could potentially go wrong.
# TODO: What if we want to use module_only but patch an external library? Options:
# 1. Check if enabled module is anywhere in the call stack (e.g., it called some other module that
# made the request)
# 2. Provide target module to patch (e.g., 'github.Requester')
logger = getLogger(__name__)
if TYPE_CHECKING:
MIXIN_BASE = CachedSession
else:
MIXIN_BASE = object
def install_cache(
cache_name: str = 'http_cache',
backend: Optional[BackendSpecifier] = None,
module_only: bool = False,
session: CachedSession = None,
session_factory: Type[OriginalSession] = CachedSession,
**kwargs,
):
"""
Install the cache for all ``requests`` functions by monkey-patching :py:class:`requests.Session`
Example:
>>> requests_cache.install_cache('demo_cache')
Accepts all parameters for :py:class:`.CachedSession`. Additional parameters:
Args:
module_only: Only install the cache for the current module
session: An existing session object to use
session_factory: Session class to use. It must inherit from either
:py:class:`.CachedSession` or :py:class:`.CacheMixin`
"""
# Patch with an existing session object
if session:
cls = _get_session_wrapper(session)
_patch_session_factory(cls)
# Patch only for the current module
elif module_only:
if not is_installed:
session = ModuleCachedSession(
cache_name=cache_name,
backend=init_backend(cache_name, backend, **kwargs),
**kwargs,
)
cls = _get_session_wrapper(session)
_patch_session_factory(cls)
requests.Session._session.enable_module()
# modules = get_installed_modules() + [_calling_module()]
# _enable_module(
# cache_name,
# init_backend(cache_name, backend, **kwargs),
# modules,
# **kwargs,
# )
# cls = _get_configured_session(
# ModuleCachedSession,
# cache_name=cache_name,
# backend=backend,
# include_modules=modules,
# **kwargs,
# )
# _patch_session_factory(cls)
# Patch with CachedSession or session_factory
else:
cls = _get_configured_session(
session_factory,
cache_name=cache_name,
backend=init_backend(cache_name, backend, **kwargs),
**kwargs,
)
_patch_session_factory(cls)
def uninstall_cache(module_only: bool = False):
"""Disable the cache by restoring the original :py:class:`requests.Session`
Args:
module_only: Only uninstall the cache for the current module
"""
if not is_installed():
return
elif module_only:
requests.Session._session.disable_module()
else:
_patch_session_factory(OriginalSession)
@contextmanager
def disabled():
"""
Context manager for temporarily disabling caching for all ``requests`` functions
Example:
>>> with requests_cache.disabled():
... requests.get('https://httpbin.org/get')
"""
previous = requests.Session
uninstall_cache()
try:
yield
finally:
_patch_session_factory(previous)
@contextmanager
def enabled(*args, **kwargs):
"""
Context manager for temporarily enabling caching for all ``requests`` functions
Example:
>>> with requests_cache.enabled('cache.db'):
... requests.get('https://httpbin.org/get')
Accepts the same arguments as :py:class:`.CachedSession` and :py:func:`.install_cache`.
"""
install_cache(*args, **kwargs)
try:
yield
finally:
uninstall_cache()
def get_cache() -> Optional[BaseCache]:
"""Get the internal cache object from the currently installed ``CachedSession`` (if any)"""
return getattr(requests.Session(), 'cache', None)
def get_installed_modules() -> List[str]:
"""Get all modules that have caching installed"""
session = requests.Session()
if isinstance(session, ModuleCachedSession):
return list(session.include_modules)
else:
return []
def is_installed() -> bool:
"""Indicate whether or not requests-cache is currently installed"""
session = requests.Session()
if isinstance(session, ModuleCachedSession):
return session.is_module_enabled()
else:
return isinstance(session, CachedSession)
def clear():
"""Clear the currently installed cache (if any)"""
if get_cache():
get_cache().clear()
def delete(*args, **kwargs):
"""Remove responses from the cache according one or more conditions.
See :py:meth:`.BaseCache.delete for usage details.
"""
session = requests.Session()
if isinstance(session, CachedSession):
session.cache.delete(*args, **kwargs)
def remove_expired_responses():
"""Remove expired responses from the cache"""
warn(
'remove_expired_responses() is deprecated; please use delete() instead',
DeprecationWarning,
)
delete(expired=True)
class ModuleCachedSession(CachedSession):
"""Session mixin that only caches requests sent from specific modules. May be used in one of two
modes:
* Opt-in: caching is disabled by default, and enabled for modules in ``include_modules``
* Opt-out: caching is enabled by default, and disabled for modules in ``exclude_modules``
Args:
include_modules: List of modules to enable caching for
exclude_modules: List of modules to disable caching for
opt_in: Whether to use opt-in mode (``True``) or opt-out mode (``False``)
"""
def __init__(
self,
*args,
include_modules: List[str] = None,
exclude_modules: List[str] = None,
opt_in: bool = True,
**kwargs,
):
super().__init__(*args, **kwargs)
self.include_modules = set(include_modules or [])
self.exclude_modules = set(exclude_modules or [])
self.opt_in = opt_in
def close(self):
"""Only close adapter(s) and skip closing backend connections in CachedSession.close(), as
the object may be reused after closing. :py:func:`requests.request` will create a session
for the request and close it after use. We will keep this behavior to avoid memory leaks.
Adapter(s) will create new connections if re-used after closing.
"""
for v in self.adapters.values():
v.close()
def disable_module(self):
module = _calling_module()
if self.opt_in:
self.include_modules -= {module}
else:
self.exclude_modules |= {module}
logger.info(f'Caching disabled for {module}')
def enable_module(self):
module = _calling_module()
if self.opt_in:
self.include_modules |= {module}
else:
self.exclude_modules -= {module}
logger.info(f'Caching enabled for {module}')
def is_module_enabled(self, back: int = 2) -> bool:
module = _calling_module(back=back)
if self.opt_in:
return module in self.include_modules
else:
return module not in self.exclude_modules
def request(self, *args, **kwargs):
if self.is_module_enabled(back=3):
return super().request(*args, **kwargs)
else:
return OriginalSession.request(self, *args, **kwargs)
def _calling_module(back: int = 2) -> str:
"""Get the name of the module ``back`` frames up in the call stack"""
frame = inspect.stack()[back].frame
module = inspect.getmodule(frame)
return getattr(module, '__name__', '')
# testing
def _calling_module_2() -> str:
"""Get the name of the calling module (first module outside of requests-cache)"""
for module in _stack_modules():
if not module.startswith('requests_cache'):
return module
raise RuntimeError('Could not determine calling module')
def _stack_modules() -> Iterator[str]:
for frame_info in inspect.stack():
module = inspect.getmodule(frame_info.frame)
yield getattr(module, '__name__', '')
# def _enable_module(
# cache_name: str,
# backend: BackendSpecifier,
# modules: List[str],
# **kwargs,
# ):
# """Install the cache for specific modules"""
# if not is_installed():
# return
# requests.Session._session.enable_module()
# class _ConfiguredCachedSession(ModuleCachedSession): # type: ignore # See mypy issue #5865
# def __init__(self):
# super().__init__(
# cache_name=cache_name, backend=backend, include_modules=modules, **kwargs
# )
# _patch_session_factory(_ConfiguredCachedSession)
# def _disable_module():
# """Uninstall the cache for the current module"""
# session = requests.Session()
# if not isinstance(session, ModuleCachedSession):
# return
# modules = get_installed_modules()
# modules.remove(_calling_module())
# # No enabled modules remaining; restore the original Session
# if not modules:
# uninstall_cache()
# # Reinstall cache with updated modules
# else:
# _enable_module(
# session.cache.cache_name,
# session.cache,
# CachedSession,
# modules,
# **session.settings.to_dict(),
# )
def _get_configured_session(
session_factory: Type[OriginalSession], *args, **kwargs
) -> Type[OriginalSession]:
class _ConfiguredCachedSession(session_factory): # type: ignore # See mypy issue #5865
def __init__(self):
super().__init__(*args, **kwargs)
return _ConfiguredCachedSession
def _get_session_wrapper(session: CachedSession):
"""Create a wrapper Session class that returns an existing session object when created"""
class _SessionWrapper(OriginalSession):
"""Wrapper for an existing session object, as a mutable class attribute"""
_session = session
def __new__(cls, *args, **kwargs):
return cls._session
return _SessionWrapper
def _patch_session_factory(session_factory: Type[OriginalSession]):
logger.debug(f'Patching requests.Session with class: {session_factory.__name__}')
requests.Session = requests.sessions.Session = session_factory # type: ignore