Skip to content

Commit

Permalink
Add default attrs to ClientResponse objects returned by CachedSession
Browse files Browse the repository at this point in the history
  • Loading branch information
JWCook committed Apr 10, 2021
1 parent 595ea98 commit d53a7b0
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 13 deletions.
4 changes: 3 additions & 1 deletion HISTORY.md
Expand Up @@ -10,11 +10,13 @@
* Add case-insensitive response headers for compatibility with aiohttp.ClientResponse.headers
* Add optional integration with `itsdangerous` for safer serialization
* Add `CacheBackend.get_urls()` to get all urls currently in the cache
* Allow passing all connection kwargs (for SQLite, Redis, and MongoDB connection methods) via CacheBackend
* Add some default attributes (`from_cache, is_expired`, etc.) to returned ClientResponse objects
* Allow passing all backend-specific connection kwargs via CacheBackend
* Add support for `json` request body
* Convert all `keys()` and `values()` methods into async generators
* Fix serialization of Content-Disposition
* Fix filtering ignored parameters for request body (`data` and `json`)
* Add user guide, more examples, and other project docs

## 0.2.0 (2021-02-28)
[See all issues & PRs for v0.2](https://github.com/JWCook/aiohttp-client-cache/milestone/1?closed=1)
Expand Down
5 changes: 0 additions & 5 deletions aiohttp_client_cache/backends/base.py
Expand Up @@ -244,11 +244,6 @@ def deserialize(self, item: ResponseOrKey) -> Union[CachedResponse, str, None]:
return item
return self._serializer.loads(item) if item else None

# TODO: Remove once all backends have been updated to use serialize/deserialize
@staticmethod
def unpickle(result):
return pickle.loads(bytes(result)) if result else None

@staticmethod
def _get_serializer(secret_key, salt):
"""Get the appropriate serializer to use; either ``itsdangerous``, if a secret key is
Expand Down
29 changes: 26 additions & 3 deletions aiohttp_client_cache/response.py
Expand Up @@ -23,6 +23,15 @@
'real_url',
'request_info',
}

# Default attriutes to add to ClientResponse objects
RESPONSE_DEFAULTS = {
'created_at': None,
'expires': None,
'from_cache': False,
'is_expired': False,
}

JsonResponse = Optional[Dict[str, Any]]
DictItems = List[Tuple[str, str]]
LinkItems = List[Tuple[str, DictItems]]
Expand Down Expand Up @@ -94,6 +103,10 @@ def content_disposition(self) -> Optional[ContentDisposition]:
filename = multipart.content_disposition_filename(params)
return ContentDisposition(disposition_type, params, filename)

@property
def from_cache(self):
return True

@property
def headers(self) -> CIMultiDictProxy[str]:
"""Get headers as an immutable, case-insensitive multidict from raw headers"""
Expand Down Expand Up @@ -171,12 +184,22 @@ async def text(self, encoding: Optional[str] = None, errors: str = "strict") ->
return self._body.decode(encoding or self.encoding, errors=errors)


AnyResponse = Union[ClientResponse, CachedResponse]


def set_response_defaults(response: AnyResponse) -> AnyResponse:
"""Set some default CachedResponse values on a ClientResponse object, so they can be
expected to always be present
"""
if not isinstance(response, CachedResponse):
for k, v in RESPONSE_DEFAULTS.items():
setattr(response, k, v)
return response


def _to_str_tuples(data: Mapping) -> DictItems:
return [(k, str(v)) for k, v in data.items()]


def _to_url_multidict(data: DictItems) -> MultiDict:
return MultiDict([(k, URL(url)) for k, url in data])


AnyResponse = Union[ClientResponse, CachedResponse]
4 changes: 2 additions & 2 deletions aiohttp_client_cache/session.py
Expand Up @@ -9,7 +9,7 @@
from aiohttp_client_cache.backends import CacheBackend
from aiohttp_client_cache.docs import copy_signature, extend_signature
from aiohttp_client_cache.expiration import ExpirationTime
from aiohttp_client_cache.response import AnyResponse
from aiohttp_client_cache.response import AnyResponse, set_response_defaults

logger = getLogger(__name__)

Expand Down Expand Up @@ -38,7 +38,7 @@ async def _request(
new_response = await super()._request(method, str_or_url, **kwargs) # type: ignore
await new_response.read()
await self.cache.save_response(cache_key, new_response, expire_after=expire_after)
return new_response
return set_response_defaults(new_response)

@asynccontextmanager
async def disabled(self):
Expand Down
15 changes: 14 additions & 1 deletion docs/backends.rst
Expand Up @@ -33,7 +33,20 @@ The ``cache_name`` parameter will be used as follows depending on the backend:
* DynamoDb: Table name
* MongoDb: Database name
* Redis: Namespace, meaning all keys will be prefixed with ``'<cache_name>:'``
* SQLite: Database path, e.g ``~/.cache/my_cache.sqlite``
* SQLite: Database path; user paths are allowed, e.g ``~/.cache/my_cache.sqlite``

Backend-Specific Arguments
~~~~~~~~~~~~~~~~~~~~~~~~~~
When initializing a :py:class:`.CacheBackend`, you can provide any valid keyword arguments for the
backend's internal connection class or function.

For example, with :py:class:`.SQLiteBackend`, you can pass arguments accepted by
:py:func:`sqlite3.connect`:

>>> cache = SQLiteBackend(
... timeout=2.5,
... uri='file://home/user/.cache/aiohttp-cache.db?mode=ro&cache=private',
... )

Custom Backends
~~~~~~~~~~~~~~~
Expand Down
2 changes: 1 addition & 1 deletion docs/user_guide.rst
Expand Up @@ -176,7 +176,7 @@ Here are some ways to get additional information out of the cache session, backe

Response Attributes
~~~~~~~~~~~~~~~~~~~
The following attributes are available on responses:
The following attributes are available on both cached and new responses returned from :py:class:`.CachedSession`:
* ``from_cache``: indicates if the response came from the cache
* ``created_at``: :py:class:`~datetime.datetime` of when the cached response was created or last updated
* ``expires``: :py:class:`~datetime.datetime` after which the cached response will expire
Expand Down
10 changes: 10 additions & 0 deletions test/unit/test_session.py
Expand Up @@ -41,3 +41,13 @@ async def test_session__cache_miss(mock_request):

await session.get('http://test.url')
assert mock_request.called is True


@patch.object(ClientSession, '_request')
async def test_session__default_attrs(mock_request):
cache = MagicMock(spec=CacheBackend)
cache.get_response.return_value = None
session = CachedSession(cache=cache)

response = await session.get('http://test.url')
assert response.from_cache is False and response.is_expired is False

0 comments on commit d53a7b0

Please sign in to comment.