Skip to content

Commit

Permalink
Extend caching options in controller (#204)
Browse files Browse the repository at this point in the history
* Extend caching options in controller

Allow setting force_cache in controller
Allow custom hooks to determine cachable requests/responses

* Fix formatting

* Fix tests

* Fix set not being subscriptable

* Extend force_cache test

* Remove is_cacheable_hooks

* Revert lint changes

* Add check in construct_response_from_cache method

* add docs

* Add test for construct_response_from_cache method

* Add changelog

---------

Co-authored-by: karpetrosyan <kar.petrosyanpy@gmail.com>
  • Loading branch information
ratzrattillo and karpetrosyan authored Mar 22, 2024
1 parent 4d36949 commit 319635a
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Add `force_cache` property to the controller, allowing RFC9111 rules to be completely disabled. (#204)
- Add `.gitignore` to cache directory created by `FIleStorage`. (#197)
- Remove `stale_*` headers from the `CacheControl` class. (#199)

Expand Down
16 changes: 16 additions & 0 deletions docs/advanced/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ icon: material/brain

You can choose which parts of [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html) to ignore. For example, this is useful when you want to ensure that your client does **not use stale responses** even if they are **acceptable from the server.**

### Force caching

If you only need to cache responses without validating the headers and following RFC9111 rules, simply set the `force_cache` property to true.

Example:

```python
import hishel

controller = hishel.Controller(force_cache=True)
client = hishel.CacheClient(controller=controller)
```

!!! note
[force_cache](extensions.md#force_cache) extension will always overwrite the controller's force_cache property.

### Cachable HTTP methods

You can specify which HTTP methods `Hishel` should cache.
Expand Down
3 changes: 3 additions & 0 deletions docs/advanced/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ For example, if the response has a `Cache-Control` header that contains a `no-st
>>> response = client.get("https://www.example.com/uncachable-endpoint", extensions={"force_cache": True})
```

!!! note
You can [configure this extension globally for the controller](controllers.md#force-caching), rather than setting force_cache to True for each request.

### cache_disabled

This extension temporarily disables the cache by passing appropriate RFC9111 headers to
Expand Down
8 changes: 6 additions & 2 deletions hishel/_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ def __init__(
clock: tp.Optional[BaseClock] = None,
allow_stale: bool = False,
always_revalidate: bool = False,
force_cache: bool = False,
key_generator: tp.Optional[tp.Callable[[Request, tp.Optional[bytes]], str]] = None,
):
self._cacheable_methods = []
Expand All @@ -131,6 +132,7 @@ def __init__(
self._allow_heuristics = allow_heuristics
self._allow_stale = allow_stale
self._always_revalidate = always_revalidate
self._force_cache = force_cache
self._key_generator = key_generator or generate_key

def is_cachable(self, request: Request, response: Response) -> bool:
Expand All @@ -143,8 +145,9 @@ def is_cachable(self, request: Request, response: Response) -> bool:
lists the steps that this method simply follows.
"""
method = request.method.decode("ascii")
force_cache = request.extensions.get("force_cache", None)

if request.extensions.get("force_cache", False):
if force_cache if force_cache is not None else self._force_cache:
return True

if response.status not in self._cacheable_status_codes:
Expand Down Expand Up @@ -279,7 +282,8 @@ def construct_response_from_cache(
return None # pragma: no cover

# !!! this should be after the "vary" header validation.
if request.extensions.get("force_cache", False):
force_cache = request.extensions.get("force_cache", None)
if force_cache if force_cache is not None else self._force_cache:
return response

# the stored response does not contain the
Expand Down
57 changes: 55 additions & 2 deletions tests/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,55 @@ def test_is_cachable_for_cachables():
assert controller.is_cachable(request=request, response=response)


def test_force_cache_property_for_is_cachable():
controller = Controller(force_cache=True)
request = Request("GET", "https://example.com", extensions={"force_cache": False})
uncachable_response = Response(status=400)

assert controller.is_cachable(request=request, response=uncachable_response) is False

request = Request("GET", "https://example.com")

assert controller.is_cachable(request=request, response=uncachable_response) is True


def test_force_cache_property_for_construct_response_from_cache():
class MockedClock(BaseClock):
def now(self) -> int:
return 1440504001 # Mon, 25 Aug 2015 12:00:01 GMT

controller = Controller(clock=MockedClock(), force_cache=True)
original_request = Request("GET", "https://example.com")
request = Request("GET", "https://example.com", extensions={"force_cache": False})
cachable_response = Response(
200,
headers=[
(b"Cache-Control", b"max-age=0"),
(b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), # 1 second before the clock
],
)

assert isinstance(
controller.construct_response_from_cache(
request=request,
response=cachable_response,
original_request=original_request,
),
Request,
)

request = Request("Get", "https://example.com")

assert isinstance(
controller.construct_response_from_cache(
request=request,
response=cachable_response,
original_request=original_request,
),
Response,
)


def test_is_cachable_for_non_cachables():
controller = Controller()

Expand Down Expand Up @@ -812,7 +861,9 @@ def now(self) -> int:

assert isinstance(
controller.construct_response_from_cache(
request=request, response=cachable_response, original_request=original_request
request=request,
response=cachable_response,
original_request=original_request,
),
Request,
)
Expand All @@ -821,7 +872,9 @@ def now(self) -> int:

assert isinstance(
controller.construct_response_from_cache(
request=request, response=cachable_response, original_request=original_request
request=request,
response=cachable_response,
original_request=original_request,
),
Response,
)

0 comments on commit 319635a

Please sign in to comment.