Skip to content

Commit

Permalink
Merge df5118e into 247293c
Browse files Browse the repository at this point in the history
  • Loading branch information
JWCook committed Aug 28, 2021
2 parents 247293c + df5118e commit 42a97df
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 40 deletions.
7 changes: 5 additions & 2 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@
* SQLite: Add `SQLiteCache.db_path` property

**Serialization:**
* Use `cattrs` for serialization by default, which enables a more forwards-compatible serialization format
(e.g., less prone to invalidation due to future updates)
* Use `cattrs` by default for optimized serialization

**Other features:**
* Add `BaseCache.update()` method as a shortcut for exporting to a different cache instance
* Allow `BaseCache.has_url()` and `delete_url()` to optionally take arguments for `requests.Request`
instead of just a URL
* Allow `create_key()` to optionally take arguments for `requests.Request` instead of a request
object
* Add support for custom cache key callbacks with `key_fn` parameter
* Slightly reduce size of serialized responses

Expand Down
9 changes: 9 additions & 0 deletions requests_cache/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# flake8: noqa: E402,F401
from logging import getLogger
from typing import Callable, Iterable, Dict

logger = getLogger(__name__)

Expand Down Expand Up @@ -31,9 +32,17 @@ def dumps(self, *args, **kwargs):
return Placeholder


def get_valid_kwargs(func: Callable, kwargs: Dict, extras: Iterable[str] = None) -> Dict:
"""Get the subset of non-None ``kwargs`` that are valid params for ``func``"""
params = list(signature(func).parameters)
params.extend(extras or [])
return {k: v for k, v in kwargs.items() if k in params and v is not None}


try:
from .backends import *
from .cache_control import DO_NOT_CACHE, CacheActions
from .cache_keys import create_key
from .models import *
from .patcher import *
from .serializers import *
Expand Down
9 changes: 1 addition & 8 deletions requests_cache/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from logging import getLogger
from typing import Callable, Dict, Iterable, Type, Union

from .. import get_placeholder_class
from .. import get_placeholder_class, get_valid_kwargs
from .base import BaseCache, BaseStorage

# Backend-specific keyword arguments equivalent to 'cache_name'
Expand All @@ -29,13 +29,6 @@
logger = getLogger(__name__)


def get_valid_kwargs(func: Callable, kwargs: Dict, extras: Iterable[str] = None) -> Dict:
"""Get the subset of non-None ``kwargs`` that are valid params for ``func``"""
params = list(signature(func).parameters)
params.extend(extras or [])
return {k: v for k, v in kwargs.items() if k in params and v is not None}


# Import all backend classes for which dependencies are installed
try:
from .dynamodb import DynamoCache, DynamoDict
Expand Down
29 changes: 16 additions & 13 deletions requests_cache/backends/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from typing import Callable, Iterable, Iterator, Tuple, Union

from ..cache_control import ExpirationTime
from ..cache_keys import create_key, remove_ignored_params, url_to_key
from ..cache_keys import create_key, remove_ignored_params
from ..models import AnyRequest, AnyResponse, CachedResponse
from ..serializers import init_serializer

Expand Down Expand Up @@ -112,10 +112,10 @@ def clear(self):
self.responses.clear()
self.redirects.clear()

def create_key(self, request: AnyRequest, **kwargs) -> str:
def create_key(self, request: AnyRequest = None, **kwargs) -> str:
"""Create a normalized cache key from a request object"""
return self.key_fn(
request,
request=request,
ignored_parameters=self.ignored_parameters,
include_get_headers=self.include_get_headers,
**kwargs,
Expand All @@ -136,21 +136,24 @@ def delete(self, key: str):
except KeyError:
pass

def delete_url(self, url: str):
"""Delete a cached response + redirects for ``GET <url>``"""
self.delete(url_to_key(url, self.ignored_parameters))
def delete_url(self, url: str, method: str = 'GET', **kwargs):
"""Delete a cached response for the specified request"""
key = self.create_key(method=method, url=url, **kwargs)
self.delete(key)

def delete_urls(self, urls: Iterable[str]):
"""Delete cached responses + redirects for multiple request URLs (``GET`` requests only)"""
self.bulk_delete([url_to_key(url, self.ignored_parameters) for url in urls])
def delete_urls(self, urls: Iterable[str], method: str = 'GET', **kwargs):
"""Delete all cached responses for the specified requests"""
keys = [self.create_key(method=method, url=url, **kwargs) for url in urls]
self.bulk_delete(keys)

def has_key(self, key: str) -> bool:
"""Returns `True` if cache has `key`, `False` otherwise"""
"""Returns ``True`` if ``key`` is in the cache"""
return key in self.responses or key in self.redirects

def has_url(self, url: str) -> bool:
"""Returns `True` if cache has `url`, `False` otherwise. Works only for GET request urls"""
return self.has_key(url_to_key(url, self.ignored_parameters)) # noqa: W601
def has_url(self, url: str, method: str = 'GET', **kwargs) -> bool:
"""Returns ``True`` if the specified request is cached"""
key = self.create_key(method=method, url=url, **kwargs)
return self.has_key(key) # noqa: W601

def keys(self, check_expiry=False) -> Iterator[str]:
"""Get all cache keys for redirects and valid responses combined"""
Expand Down
24 changes: 12 additions & 12 deletions requests_cache/cache_keys.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""Internal utilities for generating cache keys based on request details + :py:class:`.BaseCache`
settings
"""Internal utilities for generating the cache keys that are used to match requests
.. automodsumm:: requests_cache.cache_keys
:functions-only:
Expand All @@ -18,6 +17,8 @@
from requests.utils import default_headers
from url_normalize import url_normalize

from . import get_valid_kwargs

if TYPE_CHECKING:
from .models import AnyRequest

Expand All @@ -27,14 +28,19 @@


def create_key(
request: AnyRequest,
request: AnyRequest = None,
ignored_parameters: Iterable[str] = None,
include_get_headers: bool = False,
**kwargs,
) -> str:
"""Create a normalized cache key from a request object"""
key = sha256()
key.update(encode((request.method or '').upper()))
"""Create a normalized cache key from a request object or :py:class:`~requests.Request`
arguments
"""
if not request:
request_kwargs = get_valid_kwargs(Request.__init__, kwargs)
request = Session().prepare_request(Request(**request_kwargs))

key = sha256(encode((request.method or '').upper()))
url = remove_ignored_url_params(request, ignored_parameters)
url = url_normalize(url)
key.update(encode(url))
Expand Down Expand Up @@ -147,12 +153,6 @@ def sort_dict(d):
return items


def url_to_key(url: str, *args, **kwargs) -> str:
"""Create a cache key from a request URL"""
request = Session().prepare_request(Request('GET', url))
return create_key(request, *args, **kwargs)


def encode(value, encoding='utf-8') -> bytes:
"""Encode a value to bytes, if it hasn't already been"""
return value if isinstance(value, bytes) else str(value).encode(encoding)
Expand Down
3 changes: 2 additions & 1 deletion requests_cache/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
from requests.hooks import dispatch_hook
from urllib3 import filepost

from .backends import BackendSpecifier, get_valid_kwargs, init_backend
from . import get_valid_kwargs
from .backends import BackendSpecifier, init_backend
from .cache_control import CacheActions, ExpirationTime
from .cache_keys import normalize_dict
from .models import AnyResponse, CachedResponse, set_response_defaults
Expand Down
41 changes: 39 additions & 2 deletions tests/unit/test_cache_keys.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,53 @@
"""The cache_keys module is mostly covered indirectly via other tests.
This just contains a couple extra edge cases not covered elsewhere.
This just contains tests for some extra edge cases not covered elsewhere.
"""
import pytest
from requests import PreparedRequest

from requests_cache.cache_keys import normalize_dict, remove_ignored_body_params, remove_ignored_headers
from requests_cache.cache_keys import (
create_key,
normalize_dict,
remove_ignored_body_params,
remove_ignored_headers,
)


def test_normalize_dict__skip_body():
assert normalize_dict(b'some bytes', normalize_data=False) == b'some bytes'


CACHE_KEY = 'ece61ff38c7c76fd951bcd4b7bf36bae24bd9ce7f7eebc43720880596093a10b'


# All of the following variations should produce the same cache key
@pytest.mark.parametrize(
'url, params',
[
('https://example.com?foo=bar&param=1', None),
('https://example.com?foo=bar&param=1', {}),
('https://example.com/?foo=bar&param=1', {}),
('https://example.com?foo=bar&param=1&', {}),
('https://example.com?param=1&foo=bar', {}),
('https://example.com?param=1', {'foo': 'bar'}),
('https://example.com?foo=bar', {'param': '1'}),
('https://example.com', {'foo': 'bar', 'param': '1'}),
('https://example.com', {'foo': 'bar', 'param': 1}),
('https://example.com?', {'foo': 'bar', 'param': '1'}),
],
)
def test_normalize_url_params(url, params):
request = PreparedRequest()
request.prepare(
method='GET',
url=url,
params=params,
)
assert create_key(request) == CACHE_KEY


def test_remove_ignored_body_params__binary():
request = PreparedRequest()
request.method = 'GET'
request.url = 'https://img.site.com/base/img.jpg'
request.body = b'some bytes'
request.headers = {'Content-Type': 'application/octet-stream'}
Expand All @@ -20,6 +56,7 @@ def test_remove_ignored_body_params__binary():

def test_remove_ignored_headers__empty():
request = PreparedRequest()
request.method = 'GET'
request.url = 'https://img.site.com/base/img.jpg'
request.headers = {'foo': 'bar'}
assert remove_ignored_headers(request, ignored_parameters=None) == request.headers
22 changes: 20 additions & 2 deletions tests/unit/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
get_placeholder_class,
)
from requests_cache.backends.base import DESERIALIZE_ERRORS
from requests_cache.cache_keys import url_to_key
from requests_cache.cache_keys import create_key
from tests.conftest import (
MOCKED_URL,
MOCKED_URL_404,
Expand Down Expand Up @@ -320,12 +320,30 @@ def test_clear(mock_session):
assert not mock_session.cache.has_url(MOCKED_URL_REDIRECT)


def test_has_url(mock_session):
mock_session.get(MOCKED_URL)
assert mock_session.cache.has_url(MOCKED_URL)
assert not mock_session.cache.has_url(MOCKED_URL_REDIRECT)


def test_has_url__request_args(mock_session):
mock_session.get(MOCKED_URL, params={'foo': 'bar'})
assert mock_session.cache.has_url(MOCKED_URL, params={'foo': 'bar'})
assert not mock_session.cache.has_url(MOCKED_URL)


def test_delete_url(mock_session):
mock_session.get(MOCKED_URL)
mock_session.cache.delete_url(MOCKED_URL)
assert not mock_session.cache.has_url(MOCKED_URL)


def test_delete_url__request_args(mock_session):
mock_session.get(MOCKED_URL, params={'foo': 'bar'})
mock_session.cache.delete_url(MOCKED_URL, params={'foo': 'bar'})
assert not mock_session.cache.has_url(MOCKED_URL, params={'foo': 'bar'})


def test_delete_url__nonexistent_response(mock_session):
"""Deleting a response that was either already deleted (or never added) should fail silently"""
mock_session.cache.delete_url(MOCKED_URL)
Expand Down Expand Up @@ -545,7 +563,7 @@ def test_remove_expired_responses__error(mock_session):
mock_session.get(MOCKED_URL_JSON)

def error_on_key(key):
if key == url_to_key(MOCKED_URL_JSON):
if key == create_key(method='GET', url=MOCKED_URL_JSON):
raise PickleError
return mock_session.get(MOCKED_URL_JSON)

Expand Down

0 comments on commit 42a97df

Please sign in to comment.