Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix disabling expiration for a single request #527

Merged
merged 3 commits into from Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions .all-contributorsrc
Expand Up @@ -744,6 +744,15 @@
"contributions": [
"bug"
]
},
{
"login": "johnraz",
"name": "Jonathan Liuti",
"avatar_url": "https://avatars.githubusercontent.com/u/304164?v=4",
"profile": "https://github.com/johnraz",
"contributions": [
"bug"
]
}
],
"contributorsPerLine": 7,
Expand Down
13 changes: 7 additions & 6 deletions CONTRIBUTORS.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions HISTORY.md
@@ -1,8 +1,9 @@
# History

## 0.9.2 (Unreleased)
* Support `params`, `data`, and `json` as positional arguments to `CachedSession.request()` (fixes
regression in `0.9.0`)
## 0.9.2 (2022-02-15)
Fix some regression bugs introduced in 0.9.0:
* Add support `params` as a positional argument to `CachedSession.request()`
* Add support for disabling expiration for a single request with `CachedSession.request(..., expire_after=-1)`

## 0.9.1 (2022-01-15)
* Add support for python 3.10.2 and 3.9.10 (regarding resolving `ForwardRef` types during deserialization)
Expand Down
6 changes: 3 additions & 3 deletions docs/user_guide/expiration.md
Expand Up @@ -75,10 +75,10 @@ retrieving a new response. If you would like to use expired response data in the

For example:
```python
>>> # Cache a test response that will expire immediately
>>> # Cache a test response and wait until it's expired
>>> session = CachedSession(stale_if_error=True)
>>> session.get('https://httpbin.org/get', expire_after=0.0001)
>>> time.sleep(0.0001)
>>> session.get('https://httpbin.org/get', expire_after=1)
>>> time.sleep(1)
```

Afterward, let's say the page has moved and you get a 404, or the site is experiencing downtime and
Expand Down
16 changes: 10 additions & 6 deletions requests_cache/cache_control.py
Expand Up @@ -27,8 +27,9 @@

__all__ = ['DO_NOT_CACHE', 'CacheActions']

# May be set by either headers or expire_after param to disable caching
# May be set by either headers or expire_after param to disable caching or disable expiration
DO_NOT_CACHE = 0
NEVER_EXPIRE = -1
# Supported Cache-Control directives
CACHE_DIRECTIVES = ['immutable', 'max-age', 'no-cache', 'no-store']

Expand Down Expand Up @@ -139,7 +140,7 @@ def update_from_response(self, response: Response):

# Check headers for expiration, validators, and other cache directives
if directives.get('immutable'):
self.expire_after = -1
self.expire_after = NEVER_EXPIRE
else:
self.expire_after = coalesce(
directives.get('max-age'), directives.get('expires'), self.expire_after
Expand All @@ -156,7 +157,7 @@ def update_from_response(self, response: Response):
def get_expiration_datetime(expire_after: ExpirationTime) -> Optional[datetime]:
"""Convert an expiration value in any supported format to an absolute datetime"""
# Never expire
if expire_after is None or expire_after == -1:
if expire_after is None or expire_after == NEVER_EXPIRE:
return None
# Expire immediately
elif try_int(expire_after) == DO_NOT_CACHE:
Expand All @@ -173,10 +174,10 @@ def get_expiration_datetime(expire_after: ExpirationTime) -> Optional[datetime]:
return datetime.utcnow() + expire_after


def get_expiration_seconds(expire_after: ExpirationTime) -> Optional[int]:
def get_expiration_seconds(expire_after: ExpirationTime) -> int:
"""Convert an expiration value in any supported format to an expiration time in seconds"""
expires = get_expiration_datetime(expire_after)
return ceil((expires - datetime.utcnow()).total_seconds()) if expires else None
return ceil((expires - datetime.utcnow()).total_seconds()) if expires else NEVER_EXPIRE


def get_cache_directives(headers: Mapping) -> Dict:
Expand Down Expand Up @@ -242,7 +243,10 @@ def to_utc(dt: datetime):

def try_int(value: Any) -> Optional[int]:
"""Convert a value to an int, if possible, otherwise ``None``"""
return int(str(value)) if str(value).isnumeric() else None
try:
return int(value)
except (TypeError, ValueError):
return None


def url_match(url: str, pattern: str) -> bool:
Expand Down
33 changes: 21 additions & 12 deletions tests/unit/test_session.py
Expand Up @@ -10,7 +10,7 @@

import pytest
import requests
from requests import Request
from requests import Request, RequestException
from requests.structures import CaseInsensitiveDict

from requests_cache import ALL_METHODS, CachedResponse, CachedSession
Expand Down Expand Up @@ -154,12 +154,12 @@ def test_response_history(mock_session):

def test_repr(mock_session):
"""Test session and cache string representations"""
mock_session.expire_after = 10.5
mock_session.expire_after = 11
mock_session.cache.responses['key'] = 'value'
mock_session.cache.redirects['key'] = 'value'
mock_session.cache.redirects['key_2'] = 'value'

assert mock_session.cache.cache_name in repr(mock_session) and '10.5' in repr(mock_session)
assert mock_session.cache.cache_name in repr(mock_session) and '11' in repr(mock_session)
assert '2 redirects' in str(mock_session.cache) and '1 responses' in str(mock_session.cache)


Expand Down Expand Up @@ -515,9 +515,9 @@ def test_expired_request_error(mock_session):
"""Without stale_if_error (default), if there is an error while re-fetching an expired
response, the request should be re-raised and the expired item deleted"""
mock_session.stale_if_error = False
mock_session.expire_after = 0.01
mock_session.expire_after = 1
mock_session.get(MOCKED_URL)
time.sleep(0.01)
time.sleep(1)

with patch.object(mock_session.cache, 'save_response', side_effect=ValueError):
with pytest.raises(ValueError):
Expand All @@ -528,25 +528,25 @@ def test_expired_request_error(mock_session):
def test_stale_if_error__exception(mock_session):
"""With stale_if_error, expect to get old cache data if there is an exception during a request"""
mock_session.stale_if_error = True
mock_session.expire_after = 0.2
mock_session.expire_after = 1

assert mock_session.get(MOCKED_URL).from_cache is False
assert mock_session.get(MOCKED_URL).from_cache is True
time.sleep(0.2)
with patch.object(mock_session.cache, 'save_response', side_effect=ValueError):
time.sleep(1)
with patch.object(mock_session.cache, 'save_response', side_effect=RequestException):
response = mock_session.get(MOCKED_URL)
assert response.from_cache is True and response.is_expired is True


def test_stale_if_error__error_code(mock_session):
"""With stale_if_error, expect to get old cache data if a response has an error status code"""
mock_session.stale_if_error = True
mock_session.expire_after = 0.2
mock_session.expire_after = 1
mock_session.allowable_codes = (200, 404)

assert mock_session.get(MOCKED_URL_404).from_cache is False

time.sleep(0.2)
time.sleep(1)
response = mock_session.get(MOCKED_URL_404)
assert response.from_cache is True and response.is_expired is True

Expand Down Expand Up @@ -718,8 +718,8 @@ def test_remove_expired_responses__per_request(mock_session):
assert len(mock_session.cache.responses) == 1


def test_per_request__expiration(mock_session):
"""No per-session expiration is set, but then overridden with per-request expiration"""
def test_per_request__enable_expiration(mock_session):
"""No per-session expiration is set, but then overridden for a single request"""
mock_session.expire_after = None
response = mock_session.get(MOCKED_URL, expire_after=1)
assert response.from_cache is False
Expand All @@ -730,6 +730,15 @@ def test_per_request__expiration(mock_session):
assert response.from_cache is False


def test_per_request__disable_expiration(mock_session):
"""A per-session expiration is set, but then disabled for a single request"""
mock_session.expire_after = 60
response = mock_session.get(MOCKED_URL, expire_after=-1)
response = mock_session.get(MOCKED_URL, expire_after=-1)
assert response.from_cache is True
assert response.expires is None


def test_per_request__prepared_request(mock_session):
"""The same should work for PreparedRequests with CachedSession.send()"""
mock_session.expire_after = None
Expand Down