summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJordan Cook <jordan.cook.git@proton.me>2022-09-30 18:42:34 -0500
committerGitHub <noreply@github.com>2022-09-30 18:42:34 -0500
commitf7e029ee27562634d42b609f6484216dc7960a57 (patch)
tree0070428d0148f9142be8ea3cc797bff5ce2e4da0
parente1de1742fc8f91229fff069b12c3ce0b23a43d16 (diff)
parent55dc02d773048362f0738759f25d07effc9cd951 (diff)
downloadrequests-cache-f7e029ee27562634d42b609f6484216dc7960a57.tar.gz
Merge pull request #699 from requests-cache/deploy-config
Improvements for deployment workflow
-rw-r--r--.github/workflows/build.yml2
-rw-r--r--.github/workflows/deploy.yml88
-rw-r--r--HISTORY.md1
-rw-r--r--requests_cache/backends/base.py17
-rwxr-xr-xrequests_cache/models/response.py6
-rw-r--r--tests/conftest.py10
-rw-r--r--tests/unit/test_base_cache.py63
-rw-r--r--tests/unit/test_patcher.py5
-rw-r--r--tests/unit/test_session.py3
9 files changed, 131 insertions, 64 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e2f5e9e..7f02b51 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -38,7 +38,7 @@ jobs:
# Start integration test databases
- uses: supercharge/mongodb-github-action@1.8.0
with:
- mongodb-version: 4.4
+ mongodb-version: 5.0
- uses: supercharge/redis-github-action@1.4.0
with:
redis-version: 6
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 8c3d14b..2efcd7d 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -16,25 +16,27 @@ on:
description: 'Version number for pre-releases; defaults to build number'
required: false
default: ''
+ skip-stress:
+ description: 'Set to "true" to skip stress tests'
+ required: false
+ default: 'false'
+ skip-publish:
+ description: 'Set to "true" to skip publishing to PyPI'
+ required: false
+ default: 'false'
env:
LATEST_PY_VERSION: '3.10'
PYTEST_VERBOSE: 'true'
- STRESS_TEST_MULTIPLIER: 5
+ STRESS_TEST_MULTIPLIER: 7
jobs:
- # Run tests for all supported requests versions and minimum supported python version
- test:
+
+ # Run additional integration stress tests
+ test-stress:
+ if: ${{ github.event.inputs.skip-stress != 'true' }}
runs-on: ubuntu-latest
- strategy:
- matrix:
- python-version: [3.7]
- requests-version: [2.22, 2.23, 2.24, 2.25, 2.26, 2.27, 2.28]
- # Run tests for most recent python and requests versions
- include:
- - python-version: '3.10'
- requests-version: latest
- fail-fast: false
+
services:
nginx:
image: kennethreitz/httpbin
@@ -46,7 +48,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
- python-version: ${{ matrix.python-version }}
+ python-version: ${{ env.LATEST_PY_VERSION }}
- uses: snok/install-poetry@v1.3
with:
virtualenvs-in-project: true
@@ -54,7 +56,7 @@ jobs:
# Start integration test databases
- uses: supercharge/mongodb-github-action@1.8.0
with:
- mongodb-version: 4.4
+ mongodb-version: 5.0
- uses: supercharge/redis-github-action@1.4.0
with:
redis-version: 6
@@ -66,18 +68,15 @@ jobs:
uses: actions/cache@v3
with:
path: .venv
- key: venv-${{ matrix.python-version }}-${{ matrix.requests-version }}-${{ hashFiles('poetry.lock') }}
+ key: venv-${{ env.LATEST_PY_VERSION }}-latest-${{ hashFiles('poetry.lock') }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
- run: |
- poetry add requests@${{ matrix.requests-version }} --lock
- poetry install -v -E all
+ run: poetry install -v -E all
- # Run unit + integration tests, with additional stress tests
- - name: Run tests
+ # Run tests
+ - name: Run stress tests
run: |
source $VENV
- nox -e test-current
nox -e stress -- ${{ env.STRESS_TEST_MULTIPLIER }}
# Run unit tests without any optional dependencies installed
@@ -92,7 +91,6 @@ jobs:
python-version: ${{ env.LATEST_PY_VERSION }}
- uses: snok/install-poetry@v1.3
with:
- version: 1.2.0b1
virtualenvs-in-project: true
# Cache packages per python version, and reuse until lockfile changes
@@ -106,22 +104,60 @@ jobs:
if: steps.cache.outputs.cache-hit != 'true'
run: poetry install -v
- - name: Run unit tests with no optional dependencies
+ # Run tests
+ - name: Run tests with no optional dependencies
run: |
source $VENV
pytest -n auto tests/unit
+ # Run unit tests for all supported platforms, python versions, and requests versions
+ test:
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ python-version: [3.7, 3.8, 3.9, '3.10']
+ requests-version: [2.22, 2.23, 2.24, 2.25, 2.26, 2.27, latest]
+ fail-fast: false
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ # Set up python + poetry
+ - uses: actions/checkout@v3
+ - uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ - uses: snok/install-poetry@v1.3
+ with:
+ virtualenvs-in-project: true
+
+ # Cache packages per python version, and reuse until lockfile changes
+ - name: Cache python packages
+ id: cache
+ uses: actions/cache@v3
+ with:
+ path: .venv
+ key: venv-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.requests-version }}-${{ hashFiles('poetry.lock') }}
+ - name: Install dependencies
+ if: steps.cache.outputs.cache-hit != 'true'
+ run: |
+ poetry add requests@${{ matrix.requests-version }} --lock
+ poetry install -v -E all
+
+ # Run tests
+ - name: Run tests
+ run: poetry run pytest -n auto tests/unit
# Deploy stable builds on tags only, and pre-release builds from manual trigger ("workflow_dispatch")
release:
- needs: [test, test-minimum-deps]
+ if: ${{ github.event.inputs.skip-publish != 'true' }}
+ needs: [test, test-minimum-deps, test-stress]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ env.LATEST_PY_VERSION }}
- - uses: snok/install-psoetry@v1.3
+ - uses: snok/install-poetry@v1.3
with:
virtualenvs-in-project: true
@@ -134,7 +170,7 @@ jobs:
poetry version $(poetry version -s).${{ env.pre-release-suffix }}${{ env.pre-release-version }}
poetry version
- - name: Build and publish to pypi
+ - name: Build and publish to PyPI
run: |
poetry build
poetry publish -u __token__ -p ${{ secrets.PYPI_TOKEN }}
diff --git a/HISTORY.md b/HISTORY.md
index 2a381d9..ed8c6c5 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -89,6 +89,7 @@
* Fix usage of memory backend with `install_cache()`
* Add `CachedRequest.path_url` property for compatibility with `RequestEncodingMixin`
* Add compatibility with cattrs 22.1+
+* Fix issue on Windows with occasional missing `CachedResponse.created_at` timestamp
**Dependencies:**
* Replace `appdirs` with `platformdirs`
diff --git a/requests_cache/backends/base.py b/requests_cache/backends/base.py
index 9814799..cd1dd44 100644
--- a/requests_cache/backends/base.py
+++ b/requests_cache/backends/base.py
@@ -247,7 +247,8 @@ class BaseCache:
def delete_url(self, url: str, method: str = 'GET', **kwargs):
warn(
- 'BaseCache.delete_url() is deprecated; please use .delete() instead', DeprecationWarning
+ 'BaseCache.delete_url() is deprecated; please use .delete() instead',
+ DeprecationWarning,
)
self.delete(requests=[Request(method, url, **kwargs)])
@@ -260,14 +261,15 @@ class BaseCache:
def has_url(self, url: str, method: str = 'GET', **kwargs) -> bool:
warn(
- 'BaseCache.has_url() is deprecated; please use .contains() instead', DeprecationWarning
+ 'BaseCache.has_url() is deprecated; please use .contains() instead',
+ DeprecationWarning,
)
return self.contains(request=Request(method, url, **kwargs))
def keys(self, check_expiry: bool = False) -> Iterator[str]:
warn(
- 'BaseCache.keys() is deprecated; please use .filter() or '
- 'BaseCache.responses.keys() instead',
+ 'BaseCache.keys() is deprecated; '
+ 'please use .filter() or BaseCache.responses.keys() instead',
DeprecationWarning,
)
yield from self.redirects.keys()
@@ -276,15 +278,16 @@ class BaseCache:
def response_count(self, check_expiry: bool = False) -> int:
warn(
- 'BaseCache.response_count() is deprecated; please use .filter() or '
- 'len(BaseCache.responses) instead',
+ 'BaseCache.response_count() is deprecated; '
+ 'please use .filter() or len(BaseCache.responses) instead',
DeprecationWarning,
)
return len(list(self.filter(expired=not check_expiry)))
def remove_expired_responses(self, expire_after: ExpirationTime = None):
warn(
- 'BaseCache.remove_expired_responses() is deprecated; please use .delete() instead',
+ 'BaseCache.remove_expired_responses() is deprecated; '
+ 'please use .delete(expired=True) instead',
DeprecationWarning,
)
self.delete(expired=True, invalid=True)
diff --git a/requests_cache/models/response.py b/requests_cache/models/response.py
index c1f85e1..e704f03 100755
--- a/requests_cache/models/response.py
+++ b/requests_cache/models/response.py
@@ -66,7 +66,7 @@ class CachedResponse(RichMixin, BaseResponse):
_decoded_content: DecodedContent = field(default=None)
_next: Optional[CachedRequest] = field(default=None)
cookies: RequestsCookieJar = field(factory=RequestsCookieJar)
- created_at: datetime = field(factory=datetime.utcnow)
+ created_at: datetime = field(default=None)
elapsed: timedelta = field(factory=timedelta)
encoding: str = field(default=None)
expires: Optional[datetime] = field(default=None)
@@ -79,7 +79,9 @@ class CachedResponse(RichMixin, BaseResponse):
url: str = field(default=None)
def __attrs_post_init__(self):
- """Re-initialize raw (urllib3) response after deserialization"""
+ # Not using created_at field default due to possible bug on Windows with omit_if_default
+ self.created_at = self.created_at or datetime.utcnow()
+ # Re-initialize raw (urllib3) response after deserialization
self.raw = self.raw or CachedHTTPResponse.from_cached_response(self)
@classmethod
diff --git a/tests/conftest.py b/tests/conftest.py
index e09d7f1..84710ae 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -9,6 +9,8 @@ Note: The protocol ``http(s)+mock://`` helps :py:class:`requests_mock.Adapter` p
https://requests-mock.readthedocs.io/en/latest/adapter.html
"""
import os
+import warnings
+from contextlib import contextmanager
from datetime import datetime, timedelta
from functools import wraps
from importlib import import_module
@@ -278,5 +280,13 @@ def skip_missing_deps(module_name: str) -> pytest.Mark:
)
+@contextmanager
+def ignore_deprecation():
+ """Temporarily ilence deprecation warnings"""
+ with warnings.catch_warnings():
+ warnings.simplefilter('ignore', category=DeprecationWarning)
+ yield
+
+
# Some tests must disable url normalization to retain the custom `http+mock://` protocol
patch_normalize_url = patch('requests_cache.cache_keys.normalize_url', side_effect=lambda x, y: x)
diff --git a/tests/unit/test_base_cache.py b/tests/unit/test_base_cache.py
index 033a673..0a0b265 100644
--- a/tests/unit/test_base_cache.py
+++ b/tests/unit/test_base_cache.py
@@ -17,6 +17,7 @@ from tests.conftest import (
MOCKED_URL_HTTPS,
MOCKED_URL_JSON,
MOCKED_URL_REDIRECT,
+ ignore_deprecation,
patch_normalize_url,
)
@@ -58,7 +59,8 @@ def test_delete__expired(mock_normalize_url, mock_session):
mock_session.settings.expire_after = 1
mock_session.get(MOCKED_URL)
mock_session.get(MOCKED_URL_JSON)
- sleep(1)
+ sleep(1.1)
+ mock_session.settings.expire_after = 2
mock_session.get(unexpired_url)
# At this point we should have 1 unexpired response and 2 expired responses
@@ -71,7 +73,7 @@ def test_delete__expired(mock_normalize_url, mock_session):
assert cached_response.url == unexpired_url
# Now the last response should be expired as well
- sleep(1)
+ sleep(2)
BaseCache.delete(mock_session.cache, expired=True)
assert len(mock_session.cache.responses) == 0
@@ -226,8 +228,8 @@ def test_clear(mock_session):
mock_session.get(MOCKED_URL)
mock_session.get(MOCKED_URL_REDIRECT)
mock_session.cache.clear()
- assert not mock_session.cache.has_url(MOCKED_URL)
- assert not mock_session.cache.has_url(MOCKED_URL_REDIRECT)
+ assert not mock_session.cache.contains(request=Request('GET', MOCKED_URL))
+ assert not mock_session.cache.contains(request=Request('GET', MOCKED_URL_REDIRECT))
def test_save_response__manual(mock_session):
@@ -273,51 +275,59 @@ def test_urls__error(mock_session):
def test_has_url(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)
+ with ignore_deprecation():
+ 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)
+ with ignore_deprecation():
+ 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'})
+ with ignore_deprecation():
+ 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)
+ with ignore_deprecation():
+ mock_session.cache.delete_url(MOCKED_URL)
- mock_session.get(MOCKED_URL)
- mock_session.cache.delete_url(MOCKED_URL)
- assert not mock_session.cache.has_url(MOCKED_URL)
- mock_session.cache.delete_url(MOCKED_URL) # Should fail silently
+ mock_session.get(MOCKED_URL)
+ mock_session.cache.delete_url(MOCKED_URL)
+
+ assert not mock_session.cache.has_url(MOCKED_URL)
+ mock_session.cache.delete_url(MOCKED_URL) # Should fail silently
def test_delete_urls(mock_session):
mock_session.get(MOCKED_URL)
- mock_session.cache.delete_urls([MOCKED_URL])
- assert not mock_session.cache.has_url(MOCKED_URL)
+ with ignore_deprecation():
+ mock_session.cache.delete_urls([MOCKED_URL])
+ assert not mock_session.cache.has_url(MOCKED_URL)
def test_keys(mock_session):
for url in [MOCKED_URL, MOCKED_URL_JSON, MOCKED_URL_REDIRECT]:
mock_session.get(url)
- all_keys = set(mock_session.cache.responses.keys()) | set(mock_session.cache.redirects.keys())
- assert set(mock_session.cache.keys()) == all_keys
+ with ignore_deprecation():
+ response_keys = set(mock_session.cache.responses.keys())
+ redirect_keys = set(mock_session.cache.redirects.keys())
+ assert set(mock_session.cache.keys()) == response_keys | redirect_keys
def test_remove_expired_responses(mock_session):
"""Test for backwards-compatibility"""
- with patch.object(mock_session.cache, 'delete') as mock_delete, patch.object(
- mock_session.cache, 'reset_expiration'
- ) as mock_reset:
+ with ignore_deprecation(), patch.object(
+ mock_session.cache, 'delete'
+ ) as mock_delete, patch.object(mock_session.cache, 'reset_expiration') as mock_reset:
mock_session.cache.remove_expired_responses(expire_after=1)
mock_delete.assert_called_once_with(expired=True, invalid=True)
mock_reset.assert_called_once_with(1)
@@ -335,14 +345,17 @@ def test_response_count(check_expiry, expected_count, mock_session):
mock_session.cache.responses['expired_response'] = CachedResponse(expires=YESTERDAY)
mock_session.cache.responses['invalid_response'] = InvalidResponse()
- assert mock_session.cache.response_count(check_expiry=check_expiry) == expected_count
+ with ignore_deprecation():
+ response_count = mock_session.cache.response_count(check_expiry=check_expiry)
+ assert response_count == expected_count
def test_values(mock_session):
for url in [MOCKED_URL, MOCKED_URL_JSON, MOCKED_URL_HTTPS]:
mock_session.get(url)
- responses = list(mock_session.cache.values())
+ with ignore_deprecation():
+ responses = list(mock_session.cache.values())
assert len(responses) == 3
assert all([isinstance(response, CachedResponse) for response in responses])
@@ -354,7 +367,7 @@ def test_values__with_invalid_responses(check_expiry, expected_count, mock_sessi
responses[1] = AttributeError
responses[2] = CachedResponse(expires=YESTERDAY, url='test')
- with patch.object(SQLiteDict, '__getitem__', side_effect=responses):
+ with ignore_deprecation(), patch.object(SQLiteDict, '__getitem__', side_effect=responses):
values = mock_session.cache.values(check_expiry=check_expiry)
assert len(list(values)) == expected_count
diff --git a/tests/unit/test_patcher.py b/tests/unit/test_patcher.py
index ceb2a4c..8977271 100644
--- a/tests/unit/test_patcher.py
+++ b/tests/unit/test_patcher.py
@@ -6,7 +6,7 @@ from requests.sessions import Session as OriginalSession
import requests_cache
from requests_cache import CachedSession
from requests_cache.backends import BaseCache, SQLiteCache
-from tests.conftest import CACHE_NAME
+from tests.conftest import CACHE_NAME, ignore_deprecation
def test_install_uninstall():
@@ -98,6 +98,7 @@ def test_delete__cache_not_installed(mock_delete):
@patch.object(BaseCache, 'delete')
def test_remove_expired_responses(mock_delete):
requests_cache.install_cache(backend='memory', expire_after=360)
- requests_cache.remove_expired_responses()
+ with ignore_deprecation():
+ requests_cache.remove_expired_responses()
assert mock_delete.called is True
requests_cache.uninstall_cache()
diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py
index dda449a..60f2d42 100644
--- a/tests/unit/test_session.py
+++ b/tests/unit/test_session.py
@@ -29,6 +29,7 @@ from tests.conftest import (
MOCKED_URL_REDIRECT,
MOCKED_URL_REDIRECT_TARGET,
MOCKED_URL_VARY,
+ ignore_deprecation,
patch_normalize_url,
)
@@ -895,6 +896,6 @@ def test_request_force_refresh__prepared_request(mock_session):
def test_remove_expired_responses(mock_session):
- with patch.object(mock_session.cache, 'delete') as mock_delete:
+ with ignore_deprecation(), patch.object(mock_session.cache, 'delete') as mock_delete:
mock_session.remove_expired_responses()
mock_delete.assert_called_once_with(expired=True, invalid=True)